From ba32a25afa29d96cf2010d75c4438cf121408fe5 Mon Sep 17 00:00:00 2001 From: Thijs Daniels Date: Fri, 29 Sep 2023 12:34:43 +0200 Subject: [PATCH 1/5] fix(react-parallax): apply transform directly to ref --- .../react-parallax/useParallax.stories.tsx | 12 +- package-lock.json | 120 ++++++++++++++++++ packages/react-parallax/hooks/useParallax.tsx | 45 ++++--- 3 files changed, 149 insertions(+), 28 deletions(-) diff --git a/apps/storybook/stories/react-parallax/useParallax.stories.tsx b/apps/storybook/stories/react-parallax/useParallax.stories.tsx index 44e8e589..658b674f 100644 --- a/apps/storybook/stories/react-parallax/useParallax.stories.tsx +++ b/apps/storybook/stories/react-parallax/useParallax.stories.tsx @@ -7,12 +7,11 @@ import { SizedBox, Stack, Text, - Transform, } from "@codedazur/react-components"; import { ParallaxFactor, useParallax } from "@codedazur/react-parallax"; import { faker } from "@faker-js/faker"; import { Meta, StoryObj } from "@storybook/react"; -import { ReactNode } from "react"; +import { ReactNode, useRef } from "react"; import docs from "./useParallax.docs.mdx"; import layerOne from "./diorama/layer-one.png"; import layerTwo from "./diorama/layer-two.png"; @@ -59,13 +58,10 @@ const Parallax = ({ factor: ParallaxFactor; children?: ReactNode; }) => { - const { x, y } = useParallax({ factor }); + const ref = useRef(null); + useParallax({ targetRef: ref, factor }); - return ( - - {children} - - ); + return
{children}
; }; export const Hero: StoryObj = { diff --git a/package-lock.json b/package-lock.json index 2faad16c..1366221e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24106,6 +24106,126 @@ "name": "@codedazur/tsconfig", "version": "0.0.5", "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.3.tgz", + "integrity": "sha512-6hiYNJxJmyYvvKGrVThzo4nTcqvqUTA/JvKim7Auaj33NexDqSNwN5YrrQu+QhZJCIpv2tULSHt+lf+rUflLSw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.3.tgz", + "integrity": "sha512-5AzM7Yx1Ky+oLY6pHs7tjONTF22JirDPd5Jw/3/NazJ73uGB05NqhGhB4SbeCchg7SlVYVBeRMrMSZwJwq/xoA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.3.tgz", + "integrity": "sha512-A/C1shbyUhj7wRtokmn73eBksjTM7fFQoY2v/0rTM5wehpkjQRLOXI8WJsag2uLhnZ4ii5OzR1rFPwoD9cvOgA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.3.tgz", + "integrity": "sha512-FubPuw/Boz8tKkk+5eOuDHOpk36F80rbgxlx4+xty/U71e3wZZxVYHfZXmf0IRToBn1Crb8WvLM9OYj/Ur815g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.3.tgz", + "integrity": "sha512-DPw8nFuM1uEpbX47tM3wiXIR0Qa+atSzs9Q3peY1urkhofx44o7E1svnq+a5Q0r8lAcssLrwiM+OyJJgV/oj7g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.3.tgz", + "integrity": "sha512-zBPSP8cHL51Gub/YV8UUePW7AVGukp2D8JU93IHbVDu2qmhFAn9LWXiOOLKplZQKxnIPUkJTQAJDCWBWU4UWUA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.3.tgz", + "integrity": "sha512-ONcL/lYyGUj4W37D4I2I450SZtSenmFAvapkJQNIJhrPMhzDU/AdfLkW98NvH1D2+7FXwe7yclf3+B7v28uzBQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.3.tgz", + "integrity": "sha512-2Vz2tYWaLqJvLcWbbTlJ5k9AN6JD7a5CN2pAeIzpbecK8ZF/yobA39cXtv6e+Z8c5UJuVOmaTldEAIxvsIux/Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/packages/react-parallax/hooks/useParallax.tsx b/packages/react-parallax/hooks/useParallax.tsx index 3652a0fe..9531a3ea 100644 --- a/packages/react-parallax/hooks/useParallax.tsx +++ b/packages/react-parallax/hooks/useParallax.tsx @@ -1,23 +1,30 @@ import { Vector2 } from "@codedazur/essentials"; -import { MaybeRef, ScrollState, useScroll } from "@codedazur/react-essentials"; +import { + MaybeRef, + ScrollState, + resolveMaybeRef, + useScroll, +} from "@codedazur/react-essentials"; import { useCallback, useState } from "react"; interface UseParallaxProps { scrollRef?: MaybeRef; - factor: ParallaxFactor | ParallaxFactor[]; + targetRef: MaybeRef; + factor: ParallaxFactor; } export type ParallaxFactor = number | ((position: Vector2) => Vector2); export function useParallax(parameters: { scrollRef?: MaybeRef; + targetRef: MaybeRef; factor: ParallaxFactor; -}): Vector2; +}): void; -export function useParallax(parameters: { - scrollRef?: MaybeRef; - factor: ParallaxFactor[]; -}): Vector2[]; +// export function useParallax(parameters: { +// scrollRef?: MaybeRef; +// factor: ParallaxFactor[]; +// }); /** * @todo Investigate if we can implement a pure CSS approach to resolve the @@ -36,29 +43,27 @@ export function useParallax(parameters: { */ export function useParallax({ scrollRef, + targetRef, factor, -}: UseParallaxProps): Vector2 | Vector2[] { - const [translation, setTranslation] = useState( - Array.isArray(factor) ? factor.map(() => Vector2.zero) : Vector2.zero - ); - +}: UseParallaxProps) { const handleScroll = useCallback( ({ position }: ScrollState) => { - setTranslation( - Array.isArray(factor) - ? factor.map((factor) => translate(position, factor)) - : translate(position, factor) - ); + const target = resolveMaybeRef(targetRef); + + if (!target) { + return; + } + + const translated = translate(position, factor); + target.style.transform = `translateX(${translated.x}px) translateY(${translated.y}px)`; }, - [factor] + [targetRef, factor], ); useScroll({ ref: scrollRef, onScroll: handleScroll, }); - - return translation; } function translate(position: Vector2, factor: ParallaxFactor): Vector2 { From afcfd210330b4d2728d14bc2292a50fb33d33e0a Mon Sep 17 00:00:00 2001 From: Thijs Daniels Date: Fri, 29 Sep 2023 12:50:51 +0200 Subject: [PATCH 2/5] fix(react-parallax): request animation frame --- packages/react-parallax/hooks/useParallax.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react-parallax/hooks/useParallax.tsx b/packages/react-parallax/hooks/useParallax.tsx index 9531a3ea..fc9ff165 100644 --- a/packages/react-parallax/hooks/useParallax.tsx +++ b/packages/react-parallax/hooks/useParallax.tsx @@ -55,7 +55,10 @@ export function useParallax({ } const translated = translate(position, factor); - target.style.transform = `translateX(${translated.x}px) translateY(${translated.y}px)`; + + window.requestAnimationFrame(() => { + target.style.transform = `translateX(${translated.x}px) translateY(${translated.y}px)`; + }); }, [targetRef, factor], ); From 5351db15765faf590958fe1453039d342d0c2317 Mon Sep 17 00:00:00 2001 From: Thijs Daniels Date: Fri, 29 Sep 2023 16:12:37 +0200 Subject: [PATCH 3/5] feat(react-parallax): return ref object --- .../react-parallax/useParallax.stories.tsx | 174 +++++++++--------- package.json | 3 +- packages/essentials/package.json | 4 +- packages/react-date-picker/package.json | 4 +- packages/react-dictionary/package.json | 2 +- packages/react-essentials/package.json | 4 +- packages/react-forms/package.json | 2 +- packages/react-media/package.json | 4 +- packages/react-notifications/package.json | 4 +- packages/react-pagination/package.json | 4 +- packages/react-parallax/hooks/useParallax.tsx | 23 +-- packages/react-parallax/package.json | 4 +- packages/react-preferences/package.json | 4 +- 13 files changed, 114 insertions(+), 122 deletions(-) diff --git a/apps/storybook/stories/react-parallax/useParallax.stories.tsx b/apps/storybook/stories/react-parallax/useParallax.stories.tsx index 658b674f..e8d55557 100644 --- a/apps/storybook/stories/react-parallax/useParallax.stories.tsx +++ b/apps/storybook/stories/react-parallax/useParallax.stories.tsx @@ -8,17 +8,16 @@ import { Stack, Text, } from "@codedazur/react-components"; -import { ParallaxFactor, useParallax } from "@codedazur/react-parallax"; +import { useParallax } from "@codedazur/react-parallax"; import { faker } from "@faker-js/faker"; import { Meta, StoryObj } from "@storybook/react"; -import { ReactNode, useRef } from "react"; -import docs from "./useParallax.docs.mdx"; -import layerOne from "./diorama/layer-one.png"; -import layerTwo from "./diorama/layer-two.png"; -import layerThree from "./diorama/layer-three.png"; -import layerFour from "./diorama/layer-four.png"; import layerFive from "./diorama/layer-five.png"; +import layerFour from "./diorama/layer-four.png"; +import layerOne from "./diorama/layer-one.png"; import layerSix from "./diorama/layer-six.png"; +import layerThree from "./diorama/layer-three.png"; +import layerTwo from "./diorama/layer-two.png"; +import docs from "./useParallax.docs.mdx"; const meta: Meta = { title: "react-parallax/useParallax", @@ -38,11 +37,14 @@ export const Default: StoryObj = { {[-0.5, 0, 0.5, 1, 1.5].map((factor, index) => ( - - - {factor} - - + + {factor} + ))} @@ -51,98 +53,86 @@ export const Default: StoryObj = { ), }; -const Parallax = ({ - factor, - children, -}: { - factor: ParallaxFactor; - children?: ReactNode; -}) => { - const ref = useRef(null); - useParallax({ targetRef: ref, factor }); +export const Hero: StoryObj = { + render: function Default() { + const ref = useParallax({ + factor: 0.5, + }); - return
{children}
; + return ( + <> + + +
+ {faker.commerce.productName()} +
+
+ + + ); + }, }; -export const Hero: StoryObj = { - render: () => ( - <> - - - - +export const Diorama: StoryObj = { + render: function Diorama() { + const layers = [ + { src: layerOne, ref: useParallax({ factor: 0.7 }) }, + { src: layerTwo, ref: useParallax({ factor: 0.75 }) }, + { src: layerThree, ref: useParallax({ factor: 0.85 }) }, + { src: layerFour, ref: useParallax({ factor: 0.95 }) }, + { src: layerFive, ref: useParallax({ factor: 1 }) }, + { src: layerSix, ref: useParallax({ factor: 1 }) }, + ]; + + return ( + <>
- {faker.commerce.productName()} + + {layers.map((props, index) => ( + + ))} +
-
- - - ), -}; - -export const Diorama: StoryObj = { - render: () => ( - <> -
- - - - - - - - - - - - - - - - - - - - -
- - - ), + + + ); + }, }; export const NonLinear: StoryObj = { - render: () => ( - <> - - - new Vector2(5 * Math.sqrt(x), 0.005 * Math.pow(y, 2)) - } - > - + render: function NonLinear() { + const ref = useParallax({ + factor: ({ x, y }) => + new Vector2(5 * Math.sqrt(x), 0.005 * Math.pow(y, 2)), + }); + + return ( + <> + + 5⋅√x 0.005⋅y² - - - - - ), + + + + ); + }, }; export const Dynamic: StoryObj = { - render: () => ( - <> - - - new Vector2(0, y < 200 ? y : y > 400 ? y - 200 : 200) - } - > - - - - - - ), + render: function Dynamic() { + const ref = useParallax({ + factor: ({ y }) => new Vector2(0, y < 200 ? y : y > 400 ? y - 200 : 200), + }); + + return ( + <> + + + + + + ); + }, }; diff --git a/package.json b/package.json index b865be38..bc54c34c 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ ], "scripts": { "dev": "turbo run dev --parallel --concurrency=20", + "packages": "turbo run dev --filter @codedazur/* --parallel --concurrency=20", "storybook": "turbo run dev --filter @apps/storybook", "website": "turbo run dev --filter @apps/website", "audit": "npm audit --omit-dev", @@ -41,4 +42,4 @@ "vitest": "^0.34.1", "@vitest/coverage-v8": "^0.34.3" } -} \ No newline at end of file +} diff --git a/packages/essentials/package.json b/packages/essentials/package.json index 2eabd0ca..ff4bd576 100644 --- a/packages/essentials/package.json +++ b/packages/essentials/package.json @@ -17,7 +17,7 @@ "scripts": { "lint": "TIMING=1 eslint \"**/*.ts*\"", "build": "tsup index.ts --format esm,cjs --dts", - "dev": "tsup index.ts --format esm,cjs --watch", + "dev": "tsup index.ts --format esm,cjs --dts --watch", "test": "vitest run --coverage" }, "devDependencies": { @@ -26,4 +26,4 @@ "@codedazur/tsconfig": "*", "typescript": "^5.1.6" } -} \ No newline at end of file +} diff --git a/packages/react-date-picker/package.json b/packages/react-date-picker/package.json index 390cd592..0afa7ee7 100644 --- a/packages/react-date-picker/package.json +++ b/packages/react-date-picker/package.json @@ -18,7 +18,7 @@ "lint": "TIMING=1 eslint \"**/*.ts*\"", "test": "echo \"No tests configured.\"", "build": "tsup index.ts --format esm,cjs --dts", - "dev": "tsup index.ts --format esm,cjs --watch --external react" + "dev": "tsup index.ts --format esm,cjs --dts --watch --external react" }, "dependencies": { "@codedazur/essentials": "*", @@ -36,4 +36,4 @@ "@codedazur/tsconfig": "*", "typescript": "^5.1.6" } -} \ No newline at end of file +} diff --git a/packages/react-dictionary/package.json b/packages/react-dictionary/package.json index 839ede81..e573cf13 100644 --- a/packages/react-dictionary/package.json +++ b/packages/react-dictionary/package.json @@ -18,7 +18,7 @@ "test": "vitest run --coverage", "coverage": "vitest run --coverage", "build": "tsup index.ts --format esm,cjs --dts", - "dev": "tsup index.ts --format esm,cjs --watch --external react" + "dev": "tsup index.ts --format esm,cjs --dts --watch --external react" }, "peerDependencies": { "react": ">=16.8.0", diff --git a/packages/react-essentials/package.json b/packages/react-essentials/package.json index 5481d468..3ebbfea9 100644 --- a/packages/react-essentials/package.json +++ b/packages/react-essentials/package.json @@ -18,7 +18,7 @@ "test": "vitest run --coverage", "coverage": "vitest run --coverage", "build": "tsup index.ts --format esm,cjs --dts", - "dev": "tsup index.ts --format esm,cjs --watch --external react" + "dev": "tsup index.ts --format esm,cjs --dts --watch --external react" }, "dependencies": { "@codedazur/essentials": "*" @@ -38,4 +38,4 @@ "react": "^18.2.0", "typescript": "^5.1.6" } -} \ No newline at end of file +} diff --git a/packages/react-forms/package.json b/packages/react-forms/package.json index 3770ffeb..3dc4ddb0 100644 --- a/packages/react-forms/package.json +++ b/packages/react-forms/package.json @@ -15,7 +15,7 @@ "lint": "TIMING=1 eslint \"**/*.ts*\"", "test": "echo \"No tests configured.\"", "build": "tsup index.ts --format esm,cjs --dts", - "dev": "tsup index.ts --format esm,cjs --watch --external react" + "dev": "tsup index.ts --format esm,cjs --dts --watch --external react" }, "dependencies": { "@codedazur/essentials": "*", diff --git a/packages/react-media/package.json b/packages/react-media/package.json index 446715b6..a97d6df7 100644 --- a/packages/react-media/package.json +++ b/packages/react-media/package.json @@ -18,7 +18,7 @@ "lint": "TIMING=1 eslint \"**/*.ts*\"", "test": "vitest run --coverage", "build": "tsup index.ts --format esm,cjs --dts", - "dev": "tsup index.ts --format esm,cjs --watch --external react" + "dev": "tsup index.ts --format esm,cjs --dts --watch --external react" }, "dependencies": { "@codedazur/essentials": "*", @@ -36,4 +36,4 @@ "@codedazur/tsconfig": "*", "typescript": "^5.1.6" } -} \ No newline at end of file +} diff --git a/packages/react-notifications/package.json b/packages/react-notifications/package.json index 4ad19308..d4645c16 100644 --- a/packages/react-notifications/package.json +++ b/packages/react-notifications/package.json @@ -18,7 +18,7 @@ "lint": "TIMING=1 eslint \"**/*.ts*\"", "test": "echo \"No tests configured.\"", "build": "tsup index.ts --format esm,cjs --dts", - "dev": "tsup index.ts --format esm,cjs --watch --external react" + "dev": "tsup index.ts --format esm,cjs --dts --watch --external react" }, "dependencies": { "@codedazur/essentials": "*", @@ -36,4 +36,4 @@ "@codedazur/tsconfig": "*", "typescript": "^5.1.6" } -} \ No newline at end of file +} diff --git a/packages/react-pagination/package.json b/packages/react-pagination/package.json index 05be8bec..9d65ed46 100644 --- a/packages/react-pagination/package.json +++ b/packages/react-pagination/package.json @@ -18,7 +18,7 @@ "lint": "TIMING=1 eslint \"**/*.ts*\"", "test": "vitest run --coverage", "build": "tsup index.ts --format esm,cjs --dts", - "dev": "tsup index.ts --format esm,cjs --watch --external react" + "dev": "tsup index.ts --format esm,cjs --dts --watch --external react" }, "dependencies": { "@codedazur/essentials": "*" @@ -35,4 +35,4 @@ "react": "^18.2.0", "typescript": "^5.1.6" } -} \ No newline at end of file +} diff --git a/packages/react-parallax/hooks/useParallax.tsx b/packages/react-parallax/hooks/useParallax.tsx index fc9ff165..eb447fc5 100644 --- a/packages/react-parallax/hooks/useParallax.tsx +++ b/packages/react-parallax/hooks/useParallax.tsx @@ -5,21 +5,19 @@ import { resolveMaybeRef, useScroll, } from "@codedazur/react-essentials"; -import { useCallback, useState } from "react"; +import { RefObject, useCallback, useRef, useState } from "react"; interface UseParallaxProps { scrollRef?: MaybeRef; - targetRef: MaybeRef; factor: ParallaxFactor; } export type ParallaxFactor = number | ((position: Vector2) => Vector2); -export function useParallax(parameters: { +export function useParallax(parameters: { scrollRef?: MaybeRef; - targetRef: MaybeRef; factor: ParallaxFactor; -}): void; +}): RefObject; // export function useParallax(parameters: { // scrollRef?: MaybeRef; @@ -41,14 +39,15 @@ export function useParallax(parameters: { * This is only relevant if we cannot implement a way to avoid re-rendering on * every scroll event. */ -export function useParallax({ - scrollRef, - targetRef, +export function useParallax({ factor, -}: UseParallaxProps) { + scrollRef, +}: UseParallaxProps): RefObject { + const ref = useRef(null); + const handleScroll = useCallback( ({ position }: ScrollState) => { - const target = resolveMaybeRef(targetRef); + const target = resolveMaybeRef(ref); if (!target) { return; @@ -60,13 +59,15 @@ export function useParallax({ target.style.transform = `translateX(${translated.x}px) translateY(${translated.y}px)`; }); }, - [targetRef, factor], + [ref, factor], ); useScroll({ ref: scrollRef, onScroll: handleScroll, }); + + return ref; } function translate(position: Vector2, factor: ParallaxFactor): Vector2 { diff --git a/packages/react-parallax/package.json b/packages/react-parallax/package.json index cd756835..732cd630 100644 --- a/packages/react-parallax/package.json +++ b/packages/react-parallax/package.json @@ -18,7 +18,7 @@ "lint": "TIMING=1 eslint \"**/*.ts*\"", "test": "echo \"No tests configured.\"", "build": "tsup index.ts --format esm,cjs --dts", - "dev": "tsup index.ts --format esm,cjs --watch --external react" + "dev": "tsup index.ts --format esm,cjs --dts --watch --external react" }, "dependencies": { "@codedazur/essentials": "*", @@ -36,4 +36,4 @@ "@codedazur/tsconfig": "*", "typescript": "^5.1.6" } -} \ No newline at end of file +} diff --git a/packages/react-preferences/package.json b/packages/react-preferences/package.json index ed1ecc84..b10562bf 100644 --- a/packages/react-preferences/package.json +++ b/packages/react-preferences/package.json @@ -18,7 +18,7 @@ "lint": "TIMING=1 eslint \"**/*.ts*\"", "test": "echo \"No tests configured.\"", "build": "tsup index.ts --format esm,cjs --dts", - "dev": "tsup index.ts --format esm,cjs --watch --external react" + "dev": "tsup index.ts --format esm,cjs --dts --watch --external react" }, "peerDependencies": { "react": ">=16.8.0" @@ -32,4 +32,4 @@ "@codedazur/tsconfig": "*", "typescript": "^5.1.6" } -} \ No newline at end of file +} From 1444e54aaf9b891c3080977927168883168a22de Mon Sep 17 00:00:00 2001 From: Thijs Daniels Date: Sat, 30 Sep 2023 01:08:42 +0200 Subject: [PATCH 4/5] docs(react-parallax): simplify stories --- .../react-parallax/useParallax.stories.tsx | 18 ++++------ packages/react-parallax/hooks/useParallax.tsx | 35 ++++++------------- 2 files changed, 17 insertions(+), 36 deletions(-) diff --git a/apps/storybook/stories/react-parallax/useParallax.stories.tsx b/apps/storybook/stories/react-parallax/useParallax.stories.tsx index e8d55557..fe6c4014 100644 --- a/apps/storybook/stories/react-parallax/useParallax.stories.tsx +++ b/apps/storybook/stories/react-parallax/useParallax.stories.tsx @@ -75,22 +75,16 @@ export const Hero: StoryObj = { export const Diorama: StoryObj = { render: function Diorama() { - const layers = [ - { src: layerOne, ref: useParallax({ factor: 0.7 }) }, - { src: layerTwo, ref: useParallax({ factor: 0.75 }) }, - { src: layerThree, ref: useParallax({ factor: 0.85 }) }, - { src: layerFour, ref: useParallax({ factor: 0.95 }) }, - { src: layerFive, ref: useParallax({ factor: 1 }) }, - { src: layerSix, ref: useParallax({ factor: 1 }) }, - ]; - return ( <>
- {layers.map((props, index) => ( - - ))} + + + + + +
diff --git a/packages/react-parallax/hooks/useParallax.tsx b/packages/react-parallax/hooks/useParallax.tsx index eb447fc5..eda456ee 100644 --- a/packages/react-parallax/hooks/useParallax.tsx +++ b/packages/react-parallax/hooks/useParallax.tsx @@ -5,39 +5,26 @@ import { resolveMaybeRef, useScroll, } from "@codedazur/react-essentials"; -import { RefObject, useCallback, useRef, useState } from "react"; +import { RefObject, useCallback, useRef } from "react"; -interface UseParallaxProps { +export interface UseParallaxProps { scrollRef?: MaybeRef; factor: ParallaxFactor; } export type ParallaxFactor = number | ((position: Vector2) => Vector2); -export function useParallax(parameters: { - scrollRef?: MaybeRef; - factor: ParallaxFactor; -}): RefObject; - -// export function useParallax(parameters: { -// scrollRef?: MaybeRef; -// factor: ParallaxFactor[]; -// }); - /** - * @todo Investigate if we can implement a pure CSS approach to resolve the - * performance issues. We should do this before the other todo's, because the - * outcome might affect the other tasks. - * - * @todo Improve the performance of this hook by preventing the need to re- - * render on every scroll event. We could use Framer Motion's `MotionValue` for - * this, but in order to avoid a dependency, we might be able to implement - * something similar ourselves using refs. + * Creates a ref that can be used to apply a parallax effect to an element. + * @param factor The factor of the parallax effect. A number between -1 and 1. + * @param scrollRef The scrollable element. Defaults to the window. + * @returns A ref to the element. + * @example + * const ref = useParallax({ + * factor: 0.5, + * }); * - * @todo Option: Support just a single layer instead of an entire array, so that - * it is easier to move the hook down in the render tree to improve performance. - * This is only relevant if we cannot implement a way to avoid re-rendering on - * every scroll event. + * return
*/ export function useParallax({ factor, From 43e902bce11ffe819b2719c2f66323567baa6720 Mon Sep 17 00:00:00 2001 From: Thijs Daniels Date: Fri, 13 Oct 2023 18:27:18 +0200 Subject: [PATCH 5/5] feat(react-parallax): support scaling and offset --- .changeset/polite-rings-live.md | 5 + .../react-parallax/useParallax.stories.tsx | 57 +++-- packages/react-parallax/hooks/useParallax.tsx | 225 ++++++++++++++++-- packages/react-parallax/package.json | 2 +- 4 files changed, 259 insertions(+), 30 deletions(-) create mode 100644 .changeset/polite-rings-live.md diff --git a/.changeset/polite-rings-live.md b/.changeset/polite-rings-live.md new file mode 100644 index 00000000..9a876359 --- /dev/null +++ b/.changeset/polite-rings-live.md @@ -0,0 +1,5 @@ +--- +"@codedazur/react-parallax": minor +--- + +support scaling and offset diff --git a/apps/storybook/stories/react-parallax/useParallax.stories.tsx b/apps/storybook/stories/react-parallax/useParallax.stories.tsx index fe6c4014..f84e9b3f 100644 --- a/apps/storybook/stories/react-parallax/useParallax.stories.tsx +++ b/apps/storybook/stories/react-parallax/useParallax.stories.tsx @@ -1,5 +1,8 @@ import { Vector2 } from "@codedazur/essentials"; import { + AspectRatio, + Background, + Box, Center, Image, Placeholder, @@ -7,10 +10,12 @@ import { SizedBox, Stack, Text, + black, } from "@codedazur/react-components"; import { useParallax } from "@codedazur/react-parallax"; import { faker } from "@faker-js/faker"; import { Meta, StoryObj } from "@storybook/react"; +import styled from "styled-components"; import layerFive from "./diorama/layer-five.png"; import layerFour from "./diorama/layer-four.png"; import layerOne from "./diorama/layer-one.png"; @@ -53,26 +58,48 @@ export const Default: StoryObj = { ), }; -export const Hero: StoryObj = { - render: function Default() { - const ref = useParallax({ - factor: 0.5, - }); +export const Heroes: StoryObj = { + render: () => ( + + + + + + + + ), +}; - return ( - <> - - +const Hero = () => { + const ref = useParallax({ + factor: 0.5, + cover: true, + }); + + return ( + + + + + +
- {faker.commerce.productName()} + {faker.commerce.productName()}
-
- - - ); - }, + +
+ + ); }; +const Title = styled(Text)` + font-size: 2.5vw; +`; + export const Diorama: StoryObj = { render: function Diorama() { return ( diff --git a/packages/react-parallax/hooks/useParallax.tsx b/packages/react-parallax/hooks/useParallax.tsx index eda456ee..5472b1ce 100644 --- a/packages/react-parallax/hooks/useParallax.tsx +++ b/packages/react-parallax/hooks/useParallax.tsx @@ -5,48 +5,105 @@ import { resolveMaybeRef, useScroll, } from "@codedazur/react-essentials"; -import { RefObject, useCallback, useRef } from "react"; +import { RefObject, useCallback, useEffect, useRef } from "react"; -export interface UseParallaxProps { +interface BaseUseParallaxProps { scrollRef?: MaybeRef; factor: ParallaxFactor; + cover?: boolean; } -export type ParallaxFactor = number | ((position: Vector2) => Vector2); +interface CoverUseParallaxProps extends BaseUseParallaxProps { + factor: PrimitiveParallaxFactor; + cover?: boolean; +} + +interface CustomUseParallaxProps extends BaseUseParallaxProps { + factor: ParallaxFactorFunction; + cover?: false; +} + +export type UseParallaxProps = CoverUseParallaxProps | CustomUseParallaxProps; + +type PrimitiveParallaxFactor = number | Vector2; + +type ParallaxFactorFunction = (position: Vector2) => Vector2; + +type ParallaxFactor = PrimitiveParallaxFactor | ParallaxFactorFunction; /** * Creates a ref that can be used to apply a parallax effect to an element. + * * @param factor The factor of the parallax effect. A number between -1 and 1. * @param scrollRef The scrollable element. Defaults to the window. * @returns A ref to the element. + * * @example * const ref = useParallax({ * factor: 0.5, * }); * * return
+ * + * @todo A bug currently causes the element to jump on the first scroll event + * when `cover` is set to true, because the initial position is not calculated. + * This can be fixed by calling the `onScroll` callback of the `useScroll` hook + * on the initial render to set the initial position. */ export function useParallax({ - factor, scrollRef, + factor, + cover = false, }: UseParallaxProps): RefObject { const ref = useRef(null); + const position = useRef(Vector2.zero); + const offset = useRef(Vector2.zero); + const windowSize = useRef(Vector2.zero); + const elementSize = useRef(Vector2.zero); + + const translation = useRef(Vector2.zero); + const scale = useRef(1); + + const applyTransform = useCallback(() => { + const target = resolveMaybeRef(ref); + + if (!target) { + return; + } + + window.requestAnimationFrame(() => { + target.style.transform = `translate3d(${translation.current.x}px, ${translation.current.y}px, 0) scale(${scale.current})`; + }); + }, []); + const handleScroll = useCallback( - ({ position }: ScrollState) => { - const target = resolveMaybeRef(ref); + (state: ScrollState) => { + position.current = state.position; + + if (cover === true && ref.current) { + const scrollOffset = getOffset(ref.current).subtract( + translation.current, + ); - if (!target) { - return; + offset.current = scrollOffset + .subtract(windowSize.current.divide(2)) + .add(elementSize.current.divide(2)) + .multiply(Vector2.one.subtract(factor as PrimitiveParallaxFactor)) + /** + * @todo Make this configurable in case the scroll direction is + * different, or determine it based on the state.overflow. + */ + .multiply(Direction.up); } - const translated = translate(position, factor); + translation.current = translate(position.current, factor).add( + offset.current, + ); - window.requestAnimationFrame(() => { - target.style.transform = `translateX(${translated.x}px) translateY(${translated.y}px)`; - }); + applyTransform(); }, - [ref, factor], + [applyTransform, cover, factor], ); useScroll({ @@ -54,11 +111,151 @@ export function useParallax({ onScroll: handleScroll, }); + const setScale = useCallback(() => { + if (!cover) { + return; + } + + scale.current = Math.max( + windowSize.current.x / elementSize.current.x, + windowSize.current.y / elementSize.current.y, + ); + + applyTransform(); + }, [applyTransform, cover]); + + useSize({ + ref, + onResize: (size) => { + elementSize.current = size; + setScale(); + }, + }); + + useWindowSize({ + onResize: (size) => { + windowSize.current = size; + setScale(); + }, + }); + return ref; } +/** + * This function returns the offset of an element relative to the document. + * @todo Check if there is a simpler way to do this. + */ +function getOffset(element: HTMLElement) { + const rectangle = element.getBoundingClientRect(); + + const scrollX = document.documentElement.scrollLeft; + const scrollY = document.documentElement.scrollTop; + + const clientX = document.documentElement.clientLeft || 0; + const clientY = document.documentElement.clientTop || 0; + + const x = rectangle.x + scrollX - clientX; + const y = rectangle.y + scrollY - clientY; + + return new Vector2(Math.round(x), Math.round(y)); +} + function translate(position: Vector2, factor: ParallaxFactor): Vector2 { return factor instanceof Function ? factor(position) - : position.multiply(1 - factor); + : position.multiply(Vector2.one.subtract(factor)); } + +/** + * A hook that uses a resize observer to call a callback whenever the element + * resizes. if no particular ref is provided, it will use the window. + * @todo Release this as part of the @codedazur/react-essentials package. + */ +function useSize({ + ref, + onResize, +}: { + ref?: MaybeRef; + onResize: (size: Vector2) => void; +}): void { + const observer = useRef(null); + + const handleResize = useCallback( + (entries: ResizeObserverEntry[]) => { + if (entries.length > 0) { + const { width, height } = entries[0].contentRect; + onResize(new Vector2(width, height)); + } + }, + [onResize], + ); + + useEffect(() => { + const target = resolveMaybeRef(ref ?? document.body); + + if (!target) { + return; + } + + observer.current = new ResizeObserver(handleResize); + observer.current.observe(target); + + return () => { + if (observer.current) { + observer.current.disconnect(); + } + }; + }, [handleResize, ref]); + + /** + * Since the ResizeObserver only fires when the size changes, we need to + * manually call the callback once to initialize the size. + */ + useEffect(() => { + const target = resolveMaybeRef(ref ?? document.body); + + if (!target) { + return; + } + + const { width, height } = target.getBoundingClientRect(); + + onResize(new Vector2(width, height)); + }, [onResize, ref]); +} + +/** + * This function uses window.addEventListener('resize', ...) to call a callback + * whenever the window resizes. + * @todo Release this as part of the @codedazur/react-essentials package. + */ +function useWindowSize({ + onResize, +}: { + onResize: (size: Vector2) => void; +}): void { + const handleResize = useCallback(() => { + onResize(new Vector2(window.innerWidth, window.innerHeight)); + }, [onResize]); + + useEffect(() => { + handleResize(); + + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [handleResize]); +} + +/** + * @todo Release this as part of the @codedazur/essentials package. + */ +const Direction = { + up: new Vector2(0, -1), + down: new Vector2(0, 1), + left: new Vector2(-1, 0), + right: new Vector2(1, 0), +}; diff --git a/packages/react-parallax/package.json b/packages/react-parallax/package.json index 732cd630..ac36e587 100644 --- a/packages/react-parallax/package.json +++ b/packages/react-parallax/package.json @@ -11,7 +11,7 @@ } }, "publishConfig": { - "access": "restricted" + "access": "public" }, "license": "MIT", "scripts": {