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
+}