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 44e8e589..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,19 +10,19 @@ import { SizedBox, Stack, Text, - Transform, + black, } 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 } 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 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"; 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", @@ -39,11 +42,14 @@ export const Default: StoryObj = { {[-0.5, 0, 0.5, 1, 1.5].map((factor, index) => ( - - - {factor} - - + + {factor} + ))} @@ -52,101 +58,102 @@ export const Default: StoryObj = { ), }; -const Parallax = ({ - factor, - children, -}: { - factor: ParallaxFactor; - children?: ReactNode; -}) => { - const { x, y } = useParallax({ factor }); - - return ( - - {children} - - ); +export const Heroes: StoryObj = { + render: () => ( + + + + + + + + ), }; -export const Hero: StoryObj = { - render: () => ( - <> +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: () => ( - <> -
- - - - - - - - - - - - - - - - - - - - -
- - - ), + render: function Diorama() { + return ( + <> +
+ + + + + + + + +
+ + + ); + }, }; 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-lock.json b/package-lock.json index 6e5ed6aa..d896a3ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -821,10 +821,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "license": "MIT", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -877,10 +879,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.9", - "license": "MIT", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -1004,18 +1007,20 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "license": "MIT", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "license": "MIT", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -1158,8 +1163,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "license": "MIT", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -1197,11 +1203,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "license": "MIT", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -1209,8 +1216,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.7", - "license": "MIT", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -2741,29 +2749,31 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "license": "MIT", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.8", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/types": "^7.22.5", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2779,11 +2789,12 @@ } }, "node_modules/@babel/types": { - "version": "7.22.5", - "license": "MIT", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -4290,11 +4301,12 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.5.4", + "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" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -4303,6 +4315,111 @@ "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" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", diff --git a/package.json b/package.json index 58aa1855..1ad20fb2 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 ca5a0aae..cd7074b3 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 807e3a61..5472b1ce 100644 --- a/packages/react-parallax/hooks/useParallax.tsx +++ b/packages/react-parallax/hooks/useParallax.tsx @@ -1,56 +1,109 @@ import { Vector2 } from "@codedazur/essentials"; -import { MaybeRef, ScrollState, useScroll } from "@codedazur/react-essentials"; -import { useCallback, useState } from "react"; +import { + MaybeRef, + ScrollState, + resolveMaybeRef, + useScroll, +} from "@codedazur/react-essentials"; +import { RefObject, useCallback, useEffect, useRef } from "react"; -interface UseParallaxProps { +interface BaseUseParallaxProps { scrollRef?: MaybeRef; - factor: ParallaxFactor | ParallaxFactor[]; + factor: ParallaxFactor; + cover?: boolean; } -export type ParallaxFactor = number | ((position: Vector2) => Vector2); +interface CoverUseParallaxProps extends BaseUseParallaxProps { + factor: PrimitiveParallaxFactor; + cover?: boolean; +} -export function useParallax(parameters: { - scrollRef?: MaybeRef; - factor: ParallaxFactor; -}): Vector2; +interface CustomUseParallaxProps extends BaseUseParallaxProps { + factor: ParallaxFactorFunction; + cover?: false; +} -export function useParallax(parameters: { - scrollRef?: MaybeRef; - factor: ParallaxFactor[]; -}): Vector2[]; +export type UseParallaxProps = CoverUseParallaxProps | CustomUseParallaxProps; + +type PrimitiveParallaxFactor = number | Vector2; + +type ParallaxFactorFunction = (position: Vector2) => Vector2; + +type ParallaxFactor = PrimitiveParallaxFactor | ParallaxFactorFunction; /** - * @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. + * 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. * - * @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. + * @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
+ * + * @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({ +export function useParallax({ scrollRef, factor, -}: UseParallaxProps): Vector2 | Vector2[] { - const [translation, setTranslation] = useState( - Array.isArray(factor) ? factor.map(() => Vector2.zero) : Vector2.zero, - ); + 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) => { - setTranslation( - Array.isArray(factor) - ? factor.map((factor) => translate(position, factor)) - : translate(position, factor), + (state: ScrollState) => { + position.current = state.position; + + if (cover === true && ref.current) { + const scrollOffset = getOffset(ref.current).subtract( + translation.current, + ); + + 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); + } + + translation.current = translate(position.current, factor).add( + offset.current, ); + + applyTransform(); }, - [factor], + [applyTransform, cover, factor], ); useScroll({ @@ -58,11 +111,151 @@ export function useParallax({ onScroll: handleScroll, }); - return translation; + 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 cd756835..ac36e587 100644 --- a/packages/react-parallax/package.json +++ b/packages/react-parallax/package.json @@ -11,14 +11,14 @@ } }, "publishConfig": { - "access": "restricted" + "access": "public" }, "license": "MIT", "scripts": { "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 +}