From bcfe1ebd9959a14a63431f467d966d853b5a58bb Mon Sep 17 00:00:00 2001 From: debabin Date: Mon, 2 Sep 2024 14:00:34 +0700 Subject: [PATCH] =?UTF-8?q?main=20=F0=9F=A7=8A=20add=20use=20parallax,=20a?= =?UTF-8?q?dd=20use=20device=20orientation,=20add=20use=20screen=20orienta?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/index.ts | 2 + .../useDeviceMotion/useDeviceMotion.demo.tsx | 4 +- src/hooks/useDeviceMotion/useDeviceMotion.ts | 15 +- .../useDeviceOrientation.demo.tsx | 14 ++ .../useDeviceOrientation.ts | 64 ++++++ src/hooks/useMouse/useMouse.ts | 11 +- src/hooks/useParallax/useParallax.demo.tsx | 125 ++++++++++++ src/hooks/useParallax/useParallax.ts | 182 ++++++++++++++++++ .../useScreenOrientation.demo.tsx | 19 ++ .../useScreenOrientation.ts | 84 ++++++++ 10 files changed, 508 insertions(+), 12 deletions(-) create mode 100644 src/hooks/useDeviceOrientation/useDeviceOrientation.demo.tsx create mode 100644 src/hooks/useDeviceOrientation/useDeviceOrientation.ts create mode 100644 src/hooks/useParallax/useParallax.demo.tsx create mode 100644 src/hooks/useParallax/useParallax.ts create mode 100644 src/hooks/useScreenOrientation/useScreenOrientation.demo.tsx create mode 100644 src/hooks/useScreenOrientation/useScreenOrientation.ts diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 605e594a..6c22dee5 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -11,6 +11,7 @@ export * from './useDebounceCallback/useDebounceCallback'; export * from './useDebounceValue/useDebounceValue'; export * from './useDefault/useDefault'; export * from './useDeviceMotion/useDeviceMotion'; +export * from './useDeviceOrientation/useDeviceOrientation'; export * from './useDidUpdate/useDidUpdate'; export * from './useDisclosure/useDisclosure'; export * from './useDocumentEvent/useDocumentEvent'; @@ -76,6 +77,7 @@ export * from './useRenderCount/useRenderCount'; export * from './useRenderInfo/useRenderInfo'; export * from './useRerender/useRerender'; export * from './useResizeObserver/useResizeObserver'; +export * from './useScreenOrientation/useScreenOrientation'; export * from './useScript/useScript'; export * from './useSessionStorage/useSessionStorage'; export * from './useSet/useSet'; diff --git a/src/hooks/useDeviceMotion/useDeviceMotion.demo.tsx b/src/hooks/useDeviceMotion/useDeviceMotion.demo.tsx index 77d1303b..2d559c06 100644 --- a/src/hooks/useDeviceMotion/useDeviceMotion.demo.tsx +++ b/src/hooks/useDeviceMotion/useDeviceMotion.demo.tsx @@ -1,12 +1,12 @@ import { useDeviceMotion } from './useDeviceMotion'; const Demo = () => { - const deviceMotionData = useDeviceMotion(); + const deviceMotion = useDeviceMotion(); return (
       Device motion data:
-      

{JSON.stringify(deviceMotionData, null, 2)}

+

{JSON.stringify(deviceMotion, null, 2)}

); }; diff --git a/src/hooks/useDeviceMotion/useDeviceMotion.ts b/src/hooks/useDeviceMotion/useDeviceMotion.ts index 3e154775..82109c79 100644 --- a/src/hooks/useDeviceMotion/useDeviceMotion.ts +++ b/src/hooks/useDeviceMotion/useDeviceMotion.ts @@ -10,8 +10,11 @@ export interface UseDeviceMotionReturn { } export interface UseDeviceMotionParams { + /** The delay in milliseconds */ delay?: number; + /** The callback function to be invoked */ callback?: (event: DeviceMotionEvent) => void; + /** Whether to enable the hook */ enabled?: boolean; } @@ -31,7 +34,7 @@ export interface UseDeviceMotionParams { export const useDeviceMotion = (params?: UseDeviceMotionParams) => { const enabled = params?.enabled ?? true; const delay = params?.delay ?? 1000; - const [deviceMotionData, setDeviceMotionData] = useState({ + const [value, setValue] = useState({ interval: 0, rotationRate: { alpha: null, beta: null, gamma: null }, acceleration: { x: null, y: null, z: null }, @@ -45,18 +48,18 @@ export const useDeviceMotion = (params?: UseDeviceMotionParams) => { const onDeviceMotion = throttle<[DeviceMotionEvent]>((event) => { internalCallbackRef.current?.(event); - setDeviceMotionData({ + setValue({ interval: event.interval, rotationRate: { - ...deviceMotionData.rotationRate, + ...value.rotationRate, ...event.rotationRate }, acceleration: { - ...deviceMotionData.acceleration, + ...value.acceleration, ...event.acceleration }, accelerationIncludingGravity: { - ...deviceMotionData.accelerationIncludingGravity, + ...value.accelerationIncludingGravity, ...event.accelerationIncludingGravity } }); @@ -69,5 +72,5 @@ export const useDeviceMotion = (params?: UseDeviceMotionParams) => { }; }, [delay, enabled]); - return deviceMotionData; + return value; }; diff --git a/src/hooks/useDeviceOrientation/useDeviceOrientation.demo.tsx b/src/hooks/useDeviceOrientation/useDeviceOrientation.demo.tsx new file mode 100644 index 00000000..05ddfdf1 --- /dev/null +++ b/src/hooks/useDeviceOrientation/useDeviceOrientation.demo.tsx @@ -0,0 +1,14 @@ +import { useDeviceOrientation } from './useDeviceOrientation'; + +const Demo = () => { + const deviceOrientation = useDeviceOrientation(); + + return ( +
+      Device orientation data:
+      

{JSON.stringify(deviceOrientation.value, null, 2)}

+
+ ); +}; + +export default Demo; diff --git a/src/hooks/useDeviceOrientation/useDeviceOrientation.ts b/src/hooks/useDeviceOrientation/useDeviceOrientation.ts new file mode 100644 index 00000000..35b06810 --- /dev/null +++ b/src/hooks/useDeviceOrientation/useDeviceOrientation.ts @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react'; + +/* The use device orientation value type */ +export interface UseDeviceOrientationValue { + /** A number representing the motion of the device around the z axis, express in degrees with values ranging from 0 to 360 */ + alpha: number | null + /** A number representing the motion of the device around the x axis, express in degrees with values ranging from -180 to 180 */ + beta: number | null + /** A number representing the motion of the device around the y axis, express in degrees with values ranging from -90 to 90 */ + gamma: number | null + /** The current absolute value */ + absolute: boolean +} + +/* The use device orientation return type */ +export interface UseDeviceOrientationReturn { + /** Whether the device orientation is supported */ + supported: boolean + /** The current device orientation value */ + value: UseDeviceOrientationValue +} + +/** + * @name useDeviceOrientation + * @description - Hook that provides the current device orientation + * @category Sensors + * + * @returns {UseDeviceOrientationReturn} The current device orientation + * + * @example + * const { supported, value } = useDeviceOrientation(); + */ +export const useDeviceOrientation = (): UseDeviceOrientationReturn => { + const supported = window && 'DeviceOrientationEvent' in window; + + const [value, setValue] = useState({ + alpha: null, + beta: null, + gamma: null, + absolute: false + }); + + useEffect(() => { + if (!supported) return; + + const onDeviceOrientation = (event: DeviceOrientationEvent) => + setValue({ + alpha: event.alpha, + beta: event.beta, + gamma: event.gamma, + absolute: event.absolute + }); + + window.addEventListener('deviceorientation', onDeviceOrientation); + return () => { + window.removeEventListener('deviceorientation', onDeviceOrientation); + }; + }, []); + + return { + supported, + value + }; +}; diff --git a/src/hooks/useMouse/useMouse.ts b/src/hooks/useMouse/useMouse.ts index 3159f7b5..54506cbb 100644 --- a/src/hooks/useMouse/useMouse.ts +++ b/src/hooks/useMouse/useMouse.ts @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import { getElement } from '@/utils/helpers'; /** The use mouse target element type */ -type UseMouseTarget = RefObject | (() => Element) | Element; +export type UseMouseTarget = RefObject | (() => Element) | Element; /** The use mouse return type */ export interface UseMouseReturn { @@ -12,6 +12,8 @@ export interface UseMouseReturn { x: number; /** The current mouse y position */ y: number; + /** The current element */ + element: Element; /** The current element x position */ elementX: number; /** The current element y position */ @@ -63,6 +65,7 @@ export const useMouse = ((...params: any[]) => { const [internalRef, setInternalRef] = useState(); useEffect(() => { + console.log('@@@@@', target); if (!target && !internalRef) return; const onMouseMove = (event: MouseEvent) => { const element = (target ? getElement(target) : internalRef) as Element; @@ -91,15 +94,15 @@ export const useMouse = ((...params: any[]) => { }; document.addEventListener('mousemove', onMouseMove); - return () => { document.removeEventListener('mousemove', onMouseMove); }; }, [internalRef, target]); - if (target) return value; + if (target) return { ...value, element: target ?? internalRef }; return { ref: setInternalRef, - ...value + ...value, + element: target ?? internalRef }; }) as UseMouse; diff --git a/src/hooks/useParallax/useParallax.demo.tsx b/src/hooks/useParallax/useParallax.demo.tsx new file mode 100644 index 00000000..48f33841 --- /dev/null +++ b/src/hooks/useParallax/useParallax.demo.tsx @@ -0,0 +1,125 @@ +import type { CSSProperties } from 'react'; + +import { useParallax } from './useParallax'; + +const Demo = () => { + const parallax = useParallax(); + + const layerBase: CSSProperties = { + position: 'absolute', + height: '100%', + width: '100%', + transition: '.3s ease-out all' + }; + + const layer0 = { + ...layerBase, + transform: `translateX(${parallax.value.tilt * 10}px) translateY(${ + parallax.value.roll * 10 + }px)` + }; + + const layer1 = { + ...layerBase, + transform: `translateX(${parallax.value.tilt * 20}px) translateY(${ + parallax.value.roll * 20 + }px)` + }; + + const layer2 = { + ...layerBase, + transform: `translateX(${parallax.value.tilt * 30}px) translateY(${ + parallax.value.roll * 30 + }px)` + }; + + const layer3 = { + ...layerBase, + transform: `translateX(${parallax.value.tilt * 40}px) translateY(${ + parallax.value.roll * 40 + }px)` + }; + + const cardStyle = { + background: '#fff', + height: '18rem', + width: '14rem', + borderRadius: '5px', + border: '1px solid #cdcdcd', + overflow: 'hidden', + transition: '.3s ease-out all', + boxShadow: '0 0 20px 0 rgba(255, 255, 255, 0.25)', + transform: `rotateX(${parallax.value.roll * 20}deg) rotateY(${ + parallax.value.tilt * 20 + }deg)` + }; + + const containerStyle: CSSProperties = { + margin: '3em auto', + perspective: '200px' + }; + + const cardContentStyle: CSSProperties = { + overflow: 'hidden', + fontSize: '6rem', + position: 'absolute', + top: 'calc(50% - 1em)', + left: 'calc(50% - 1em)', + height: '2em', + width: '2em', + margin: 'auto' + }; + + const targetStyle: CSSProperties = { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + minHeight: '500px', + transition: '.3s ease-out all' + }; + + return ( +
+
+        Parallax data:
+        

{JSON.stringify(parallax.value, null, 2)}

+
+
+
+
+ layer0 + layer1 + layer2 + layer3 +
+
+
+
+ Credit of images to{' '} + Jarom Vogel + +
+
+ ); +}; + +export default Demo; diff --git a/src/hooks/useParallax/useParallax.ts b/src/hooks/useParallax/useParallax.ts new file mode 100644 index 00000000..a35a9e65 --- /dev/null +++ b/src/hooks/useParallax/useParallax.ts @@ -0,0 +1,182 @@ +import { useEffect, useState } from 'react'; + +import { useDeviceOrientation } from '../useDeviceOrientation/useDeviceOrientation'; +import type { UseMouseTarget } from '../useMouse/useMouse'; +import { useMouse } from '../useMouse/useMouse'; +import { useScreenOrientation } from '../useScreenOrientation/useScreenOrientation'; + +/** The use parallax value type */ +export interface UseParallaxValue { + /** Roll value. Scaled to `-0.5 ~ 0.5` */ + roll: number + /** Tilt value. Scaled to `-0.5 ~ 0.5` */ + tilt: number + /** Sensor source, can be `mouse` or `deviceOrientation` */ + source: 'deviceOrientation' | 'mouse' +} + +/** The use parallax options type */ +export interface UseParallaxOptions { + /** Device orientation tilt adjust function */ + deviceOrientationTiltAdjust?: (value: number) => number + /** Device orientation roll adjust function */ + deviceOrientationRollAdjust?: (value: number) => number + /** Mouse tilt adjust function */ + mouseTiltAdjust?: (value: number) => number + /** Mouse roll adjust function */ + mouseRollAdjust?: (value: number) => number +} + +interface UseParallaxReturn { + value: UseParallaxValue +} + +export interface UseParallax { + ( + target: Target, + options?: UseParallaxOptions + ): UseParallaxReturn + + (params?: UseParallaxOptions): UseParallaxReturn & { + ref: (node: Target) => void + } +} + +/** + * @name useParallax + * @description - Hook to help create parallax effect + * @category Sensors + * + * @overload + * @template Target The target element for the parallax effect + * @param {Target} target The target element for the parallax effect + * @param {UseParallaxOptions} options The options for the parallax effect + * @returns {UseParallaxReturn} An object with the current mouse position + * + * @example + * const { roll, tilt, source } = useParallax(target); + * + * @overload + * @template Target The target element for the parallax effect + * @param {UseParallaxOptions} options The options for the parallax effect + * + * @example + * const { ref, roll, tilt, source } = useParallax(); + */ +export const useParallax = ((...params: any[]) => { + const target = params[0] instanceof Function || (params[0] && 'current' in params[0]) || params[0] instanceof Element ? + params[0] : undefined; + const { + deviceOrientationRollAdjust = (value) => value, + deviceOrientationTiltAdjust = (value) => value, + mouseRollAdjust = (value) => value, + mouseTiltAdjust = (value) => value + } = (target ? params[1] : {}) as UseParallaxOptions; + + const [internalRef, setInternalRef] = useState(); + + const mouse = useMouse(target ?? internalRef); + const screenOrientation = useScreenOrientation(); + const deviceOrientation = useDeviceOrientation(); + + const getSource = () => { + const isDeviceOrientation = deviceOrientation.supported + && (deviceOrientation.value.alpha || deviceOrientation.value.gamma); + + if (isDeviceOrientation) return 'deviceOrientation'; + return 'mouse'; + }; + + const getRoll = () => { + const source = getSource(); + if (source === 'deviceOrientation') { + let value: number; + switch (screenOrientation.value.orientationType) { + case 'landscape-primary': + value = deviceOrientation.value.gamma! / 90; + break; + case 'landscape-secondary': + value = -deviceOrientation.value.gamma! / 90; + break; + case 'portrait-primary': + value = -deviceOrientation.value.beta! / 90; + break; + case 'portrait-secondary': + value = deviceOrientation.value.beta! / 90; + break; + default: + value = -deviceOrientation.value.beta! / 90; + } + return deviceOrientationRollAdjust(value); + } + else { + const y = mouse.y - mouse.elementPositionY; + const height = mouse.element.getBoundingClientRect().height; + const value = -(y - height / 2) / height; + return mouseRollAdjust(value); + } + }; + + const getTilt = () => { + const source = getSource(); + if (source === 'deviceOrientation') { + let value: number; + switch (screenOrientation.value.orientationType) { + case 'landscape-primary': + value = deviceOrientation.value.beta! / 90; + break; + case 'landscape-secondary': + value = -deviceOrientation.value.beta! / 90; + break; + case 'portrait-primary': + value = deviceOrientation.value.gamma! / 90; + break; + case 'portrait-secondary': + value = -deviceOrientation.value.gamma! / 90; + break; + default: + value = deviceOrientation.value.gamma! / 90; + } + return deviceOrientationTiltAdjust(value); + } + else { + const x = mouse.x - mouse.elementPositionX; + const width = mouse.element.getBoundingClientRect().width; + const value = (x - width / 2) / width; + return mouseTiltAdjust(value); + } + }; + + const [value, setValue] = useState({ + roll: 0, + source: 'mouse', + tilt: 0 + }); + + useEffect(() => { + if (!mouse.element) return; + + const source = getSource(); + const roll = getRoll(); + const tilt = getTilt(); + + setValue({ + roll, + source, + tilt + }); + }, [ + screenOrientation.value.angle, + screenOrientation.value.orientationType, + deviceOrientation.value.gamma, + deviceOrientation.value.beta, + deviceOrientation.value.alpha, + deviceOrientation.value.absolute, + mouse.x, + mouse.y, + mouse.element + ]); + + if (target) return { value }; + return { ref: setInternalRef, value }; +}) as UseParallax; diff --git a/src/hooks/useScreenOrientation/useScreenOrientation.demo.tsx b/src/hooks/useScreenOrientation/useScreenOrientation.demo.tsx new file mode 100644 index 00000000..e0381ef9 --- /dev/null +++ b/src/hooks/useScreenOrientation/useScreenOrientation.demo.tsx @@ -0,0 +1,19 @@ +import { useScreenOrientation } from './useScreenOrientation'; + +const Demo = () => { + const screenOrientation = useScreenOrientation(); + + return ( + <> +

+ For best results, please use a mobile or tablet device + or use your browser's native inspector to simulate an orientation change +

+ +
Orientation Type: {screenOrientation.value.orientationType}
+
Orientation Angle: {screenOrientation.value.angle}
+ + ); +}; + +export default Demo; diff --git a/src/hooks/useScreenOrientation/useScreenOrientation.ts b/src/hooks/useScreenOrientation/useScreenOrientation.ts new file mode 100644 index 00000000..dd72c64b --- /dev/null +++ b/src/hooks/useScreenOrientation/useScreenOrientation.ts @@ -0,0 +1,84 @@ +import { useEffect, useState } from 'react'; + +declare global { + interface ScreenOrientation { + lock: (orientation: OrientationLockType) => Promise + } +} + +/* The use device orientation value type */ +export interface UseScreenOrientationValue { + /** The current angle */ + angle: number + /** The current orientation type */ + orientationType: OrientationType +} + +/* The screen lock orientation type */ +export type OrientationLockType = 'any' | 'natural' | 'landscape' | 'portrait' | 'portrait-primary' | 'portrait-secondary' | 'landscape-primary' | 'landscape-secondary'; + +/* The use device orientation return type */ +export interface useScreenOrientationReturn { + /** Whether the screen orientation is supported */ + supported: boolean + /** The current screen orientation value */ + value: UseScreenOrientationValue + /** Lock the screen orientation */ + lock: (orientation: OrientationLockType) => void + /** Unlock the screen orientation */ + unlock: () => void +} + +/** + * @name useScreenOrientation + * @description - Hook that provides the current screen orientation + * @category Sensors + * + * @returns {useScreenOrientationReturn} The current screen orientation + * + * @example + * const { supported, value, lock, unlock } = useScreenOrientation(); + */ +export const useScreenOrientation = (): useScreenOrientationReturn => { + const supported = window && 'screen' in window && 'orientation' in window.screen; + const screenOrientation = (supported ? window.screen.orientation : {}) as ScreenOrientation; + + const [value, setValue] = useState(() => { + return { + angle: screenOrientation?.angle ?? 0, + orientationType: screenOrientation?.type + }; + }); + + useEffect(() => { + if (!supported) return; + + const onOrientationChange = () => + setValue({ + angle: screenOrientation.angle, + orientationType: screenOrientation.type + }); + + window.addEventListener('orientationchange', onOrientationChange); + return () => { + window.removeEventListener('orientationchange', onOrientationChange); + }; + }); + + const lock = (type: OrientationLockType) => { + if (supported && typeof screenOrientation.lock === 'function') + return screenOrientation.lock(type); + }; + + const unlock = () => { + if (supported && typeof screenOrientation.unlock === 'function') + screenOrientation.unlock(); + }; + + return { + supported, + value, + lock, + unlock + }; +};