@okinoxis/hero-scene 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -0
- package/dist/index.cjs +424 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +106 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +385 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# @okinoxis/hero-scene
|
|
2
|
+
|
|
3
|
+
Compound component for cinematic hero backgrounds in Next.js — image rotation, parallax, vignettes, blur, and patterns.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @okinoxis/hero-scene
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { HeroScene } from '@okinoxis/hero-scene'
|
|
15
|
+
|
|
16
|
+
const images = [
|
|
17
|
+
{ src: '/karate.jpg', color: '70, 150, 220' },
|
|
18
|
+
{ src: '/surf.jpg', color: '220, 160, 60' },
|
|
19
|
+
{ src: '/tennis.jpg', color: '200, 110, 50' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
<HeroScene images={images} interval={30000} className="min-h-svh">
|
|
23
|
+
<HeroScene.Parallax />
|
|
24
|
+
<HeroScene.Vignette />
|
|
25
|
+
<HeroScene.Blur />
|
|
26
|
+
<HeroScene.Pattern />
|
|
27
|
+
<HeroScene.DarkOverlay />
|
|
28
|
+
<HeroScene.Content>
|
|
29
|
+
<h1>Your headline</h1>
|
|
30
|
+
</HeroScene.Content>
|
|
31
|
+
</HeroScene>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Only include the sub-components you need. All are optional.
|
|
35
|
+
|
|
36
|
+
## Layers
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Background z:0 — images (crossfade) + fallback color, moved by Parallax
|
|
40
|
+
DarkOverlay z:10 — dark mode only overlay
|
|
41
|
+
Blur z:15 — backdrop blur with radial mask (clear center)
|
|
42
|
+
Vignette z:20 — color-matched radial gradient per image
|
|
43
|
+
Pattern z:20 — repeating dot grid
|
|
44
|
+
Content z:30 — your content
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### `<HeroScene>`
|
|
50
|
+
|
|
51
|
+
| Prop | Type | Default |
|
|
52
|
+
|------|------|---------|
|
|
53
|
+
| `images` * | `{ src: string, color: string }[]` | — |
|
|
54
|
+
| `initialIndex` | `number` | `0` |
|
|
55
|
+
| `interval` | `number` | `30000` |
|
|
56
|
+
| `transitionDuration` | `number` | `700` |
|
|
57
|
+
| `className` | `string` | — |
|
|
58
|
+
| `onIndexChange` | `(index: number) => void` | — |
|
|
59
|
+
|
|
60
|
+
`color` is `"r, g, b"` (e.g. `"210, 100, 60"`) for CSS `color-mix()`.
|
|
61
|
+
|
|
62
|
+
### `<HeroScene.Parallax>`
|
|
63
|
+
|
|
64
|
+
| Prop | Type | Default |
|
|
65
|
+
|------|------|---------|
|
|
66
|
+
| `speed` | `number` | `0.4` |
|
|
67
|
+
| `mouseShiftX` | `number` | `25` |
|
|
68
|
+
| `mouseShiftY` | `number` | `15` |
|
|
69
|
+
| `mouseLerp` | `number` | `0.04` |
|
|
70
|
+
|
|
71
|
+
### `<HeroScene.Vignette>`
|
|
72
|
+
|
|
73
|
+
| Prop | Type | Default |
|
|
74
|
+
|------|------|---------|
|
|
75
|
+
| `centerX` | `number` | `50` |
|
|
76
|
+
| `centerY` | `number` | `100` |
|
|
77
|
+
| `shape` | `'circle' \| 'ellipse'` | `'circle'` |
|
|
78
|
+
| `stops` | `[number, number][]` | `[[0,0]...[100,0.6]]` |
|
|
79
|
+
| `transitionDuration` | `number` | `1000` |
|
|
80
|
+
|
|
81
|
+
### `<HeroScene.Blur>`
|
|
82
|
+
|
|
83
|
+
| Prop | Type | Default |
|
|
84
|
+
|------|------|---------|
|
|
85
|
+
| `amount` | `number` | `24` |
|
|
86
|
+
| `centerX` | `number` | `50` |
|
|
87
|
+
| `centerY` | `number` | `65` |
|
|
88
|
+
| `innerRadius` | `number` | `15` |
|
|
89
|
+
| `outerRadius` | `number` | `55` |
|
|
90
|
+
|
|
91
|
+
### `<HeroScene.Pattern>`
|
|
92
|
+
|
|
93
|
+
| Prop | Type | Default |
|
|
94
|
+
|------|------|---------|
|
|
95
|
+
| `dotSize` | `number` | `1` |
|
|
96
|
+
| `spacing` | `number` | `20` |
|
|
97
|
+
| `lightColor` | `string` | `'rgba(0 0 0 / 0.15)'` |
|
|
98
|
+
| `darkColor` | `string` | `'rgba(255 255 255 / 0.1)'` |
|
|
99
|
+
|
|
100
|
+
### `<HeroScene.DarkOverlay>`
|
|
101
|
+
|
|
102
|
+
| Prop | Type | Default |
|
|
103
|
+
|------|------|---------|
|
|
104
|
+
| `opacity` | `number` | `0.4` |
|
|
105
|
+
|
|
106
|
+
### `<HeroScene.Content>`
|
|
107
|
+
|
|
108
|
+
| Prop | Type | Default |
|
|
109
|
+
|------|------|---------|
|
|
110
|
+
| `className` | `string` | — |
|
|
111
|
+
|
|
112
|
+
## Accessibility
|
|
113
|
+
|
|
114
|
+
- Respects `prefers-reduced-motion` — disables all animations
|
|
115
|
+
- Pauses off-screen via Intersection Observer
|
|
116
|
+
- All decorative layers are `aria-hidden`
|
|
117
|
+
|
|
118
|
+
## Requirements
|
|
119
|
+
|
|
120
|
+
React 18+, Next.js 14+, Tailwind CSS (for `dark:` variant)
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/index.ts
|
|
32
|
+
var index_exports = {};
|
|
33
|
+
__export(index_exports, {
|
|
34
|
+
HeroScene: () => HeroScene,
|
|
35
|
+
buildBlurMask: () => buildBlurMask,
|
|
36
|
+
buildVignetteGradient: () => buildVignetteGradient,
|
|
37
|
+
useReducedMotion: () => useReducedMotion
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
|
|
41
|
+
// src/hero-scene.tsx
|
|
42
|
+
var import_image = __toESM(require("next/image"), 1);
|
|
43
|
+
var import_react4 = require("react");
|
|
44
|
+
|
|
45
|
+
// src/hero-scene-context.ts
|
|
46
|
+
var import_react = require("react");
|
|
47
|
+
var HeroSceneContext = (0, import_react.createContext)(null);
|
|
48
|
+
function useHeroScene() {
|
|
49
|
+
const ctx = (0, import_react.useContext)(HeroSceneContext);
|
|
50
|
+
if (!ctx) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"HeroScene compound components (Parallax, Vignette, Blur, Pattern, DarkOverlay, Content) must be rendered inside a <HeroScene> parent."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return ctx;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/utils.ts
|
|
59
|
+
var DEFAULT_STOPS = [
|
|
60
|
+
[0, 0],
|
|
61
|
+
[15, 0.08],
|
|
62
|
+
[30, 0.15],
|
|
63
|
+
[45, 0.25],
|
|
64
|
+
[60, 0.35],
|
|
65
|
+
[75, 0.45],
|
|
66
|
+
[88, 0.55],
|
|
67
|
+
[100, 0.6]
|
|
68
|
+
];
|
|
69
|
+
function buildVignetteGradient(color, config) {
|
|
70
|
+
const shape = config.shape ?? "circle";
|
|
71
|
+
const cx = config.centerX ?? 50;
|
|
72
|
+
const cy = config.centerY ?? 100;
|
|
73
|
+
const stops = config.stops ?? DEFAULT_STOPS;
|
|
74
|
+
const gradientStops = stops.map(
|
|
75
|
+
([pos, opacity]) => opacity === 0 ? `transparent ${pos}%` : `color-mix(in srgb, rgb(${color}) ${Math.round(opacity * 100)}%, transparent) ${pos}%`
|
|
76
|
+
).join(", ");
|
|
77
|
+
return `radial-gradient(${shape} at ${cx}% ${cy}%, ${gradientStops})`;
|
|
78
|
+
}
|
|
79
|
+
function buildBlurMask(config) {
|
|
80
|
+
const cx = config.centerX ?? 50;
|
|
81
|
+
const cy = config.centerY ?? 65;
|
|
82
|
+
const inner = config.innerRadius ?? 15;
|
|
83
|
+
const outer = config.outerRadius ?? 55;
|
|
84
|
+
return `radial-gradient(circle at ${cx}% ${cy}%, transparent ${inner}%, black ${outer}%)`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/use-in-viewport.ts
|
|
88
|
+
var import_react2 = require("react");
|
|
89
|
+
function useInViewport(ref) {
|
|
90
|
+
const [inViewport, setInViewport] = (0, import_react2.useState)(true);
|
|
91
|
+
(0, import_react2.useEffect)(() => {
|
|
92
|
+
const el = ref.current;
|
|
93
|
+
if (!el) return;
|
|
94
|
+
const observer = new IntersectionObserver(
|
|
95
|
+
([entry]) => {
|
|
96
|
+
setInViewport(entry.isIntersecting);
|
|
97
|
+
},
|
|
98
|
+
{ threshold: 0 }
|
|
99
|
+
);
|
|
100
|
+
observer.observe(el);
|
|
101
|
+
return () => observer.disconnect();
|
|
102
|
+
}, [ref]);
|
|
103
|
+
return inViewport;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/use-reduced-motion.ts
|
|
107
|
+
var import_react3 = require("react");
|
|
108
|
+
function useReducedMotion() {
|
|
109
|
+
const [reduced, setReduced] = (0, import_react3.useState)(false);
|
|
110
|
+
(0, import_react3.useEffect)(() => {
|
|
111
|
+
const mql = globalThis.matchMedia("(prefers-reduced-motion: reduce)");
|
|
112
|
+
setReduced(mql.matches);
|
|
113
|
+
function onChange(e) {
|
|
114
|
+
setReduced(e.matches);
|
|
115
|
+
}
|
|
116
|
+
mql.addEventListener("change", onChange);
|
|
117
|
+
return () => mql.removeEventListener("change", onChange);
|
|
118
|
+
}, []);
|
|
119
|
+
return reduced;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/hero-scene.tsx
|
|
123
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
124
|
+
function HeroSceneRoot({
|
|
125
|
+
images,
|
|
126
|
+
initialIndex = 0,
|
|
127
|
+
interval = 3e4,
|
|
128
|
+
transitionDuration = 700,
|
|
129
|
+
className,
|
|
130
|
+
onIndexChange,
|
|
131
|
+
children
|
|
132
|
+
}) {
|
|
133
|
+
const [index, setIndex] = (0, import_react4.useState)(initialIndex);
|
|
134
|
+
const rootRef = (0, import_react4.useRef)(null);
|
|
135
|
+
const reducedMotion = useReducedMotion();
|
|
136
|
+
const isInViewport = useInViewport(rootRef);
|
|
137
|
+
(0, import_react4.useEffect)(() => {
|
|
138
|
+
if (interval <= 0 || images.length <= 1) return;
|
|
139
|
+
let current = initialIndex;
|
|
140
|
+
const id = setInterval(() => {
|
|
141
|
+
const next = (current + 1) % images.length;
|
|
142
|
+
current = next;
|
|
143
|
+
setIndex(next);
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
onIndexChange?.(next);
|
|
146
|
+
}, 0);
|
|
147
|
+
}, interval);
|
|
148
|
+
return () => clearInterval(id);
|
|
149
|
+
}, [initialIndex, interval, images.length, onIndexChange]);
|
|
150
|
+
const activeColor = images[index]?.color ?? "128, 128, 128";
|
|
151
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
152
|
+
HeroSceneContext.Provider,
|
|
153
|
+
{
|
|
154
|
+
value: {
|
|
155
|
+
images,
|
|
156
|
+
index,
|
|
157
|
+
transitionDuration,
|
|
158
|
+
reducedMotion,
|
|
159
|
+
isInViewport,
|
|
160
|
+
containerRef: rootRef
|
|
161
|
+
},
|
|
162
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
163
|
+
"div",
|
|
164
|
+
{
|
|
165
|
+
ref: rootRef,
|
|
166
|
+
className,
|
|
167
|
+
style: { position: "relative", overflow: "hidden" },
|
|
168
|
+
children: [
|
|
169
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
170
|
+
"div",
|
|
171
|
+
{
|
|
172
|
+
"data-hero-images": "",
|
|
173
|
+
"aria-hidden": "true",
|
|
174
|
+
style: {
|
|
175
|
+
position: "absolute",
|
|
176
|
+
inset: 0,
|
|
177
|
+
backgroundColor: `rgb(${activeColor})`,
|
|
178
|
+
transition: reducedMotion ? "none" : "background-color 1s ease"
|
|
179
|
+
},
|
|
180
|
+
children: images.map((img, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
181
|
+
import_image.default,
|
|
182
|
+
{
|
|
183
|
+
src: img.src,
|
|
184
|
+
alt: "",
|
|
185
|
+
width: 1920,
|
|
186
|
+
height: 1080,
|
|
187
|
+
priority: i === initialIndex,
|
|
188
|
+
sizes: "100vw",
|
|
189
|
+
style: {
|
|
190
|
+
position: "absolute",
|
|
191
|
+
inset: 0,
|
|
192
|
+
width: "100%",
|
|
193
|
+
height: "100%",
|
|
194
|
+
objectFit: "cover",
|
|
195
|
+
opacity: i === index ? 1 : 0,
|
|
196
|
+
transition: reducedMotion ? "none" : `opacity ${transitionDuration}ms ease`
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
img.src
|
|
200
|
+
))
|
|
201
|
+
}
|
|
202
|
+
),
|
|
203
|
+
children
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
function Parallax({
|
|
211
|
+
speed = 0.4,
|
|
212
|
+
mouseShiftX = 25,
|
|
213
|
+
mouseShiftY = 15,
|
|
214
|
+
mouseLerp = 0.04
|
|
215
|
+
}) {
|
|
216
|
+
const { reducedMotion, isInViewport, containerRef } = useHeroScene();
|
|
217
|
+
const scrollY = (0, import_react4.useRef)(0);
|
|
218
|
+
const targetMouseX = (0, import_react4.useRef)(0);
|
|
219
|
+
const targetMouseY = (0, import_react4.useRef)(0);
|
|
220
|
+
const currentMouseX = (0, import_react4.useRef)(0);
|
|
221
|
+
const currentMouseY = (0, import_react4.useRef)(0);
|
|
222
|
+
(0, import_react4.useEffect)(() => {
|
|
223
|
+
if (reducedMotion) return;
|
|
224
|
+
const root = containerRef.current;
|
|
225
|
+
if (!root) return;
|
|
226
|
+
const imagesEl = root.querySelector("[data-hero-images]");
|
|
227
|
+
if (!imagesEl) return;
|
|
228
|
+
imagesEl.style.inset = "-15%";
|
|
229
|
+
imagesEl.style.willChange = "transform";
|
|
230
|
+
return () => {
|
|
231
|
+
imagesEl.style.inset = "0";
|
|
232
|
+
imagesEl.style.willChange = "";
|
|
233
|
+
imagesEl.style.transform = "";
|
|
234
|
+
};
|
|
235
|
+
}, [reducedMotion, containerRef]);
|
|
236
|
+
(0, import_react4.useEffect)(() => {
|
|
237
|
+
if (reducedMotion) return;
|
|
238
|
+
const root = containerRef.current;
|
|
239
|
+
if (!root) return;
|
|
240
|
+
const imagesEl = root.querySelector("[data-hero-images]");
|
|
241
|
+
if (!imagesEl) return;
|
|
242
|
+
let rafId;
|
|
243
|
+
let running = true;
|
|
244
|
+
function loop() {
|
|
245
|
+
if (!running) return;
|
|
246
|
+
currentMouseX.current += (targetMouseX.current - currentMouseX.current) * mouseLerp;
|
|
247
|
+
currentMouseY.current += (targetMouseY.current - currentMouseY.current) * mouseLerp;
|
|
248
|
+
const y = scrollY.current * speed;
|
|
249
|
+
const mx = currentMouseX.current * mouseShiftX;
|
|
250
|
+
const my = currentMouseY.current * mouseShiftY;
|
|
251
|
+
imagesEl.style.transform = `translate3d(${mx}px, ${y + my}px, 0)`;
|
|
252
|
+
rafId = requestAnimationFrame(loop);
|
|
253
|
+
}
|
|
254
|
+
function onScroll() {
|
|
255
|
+
if (!isInViewport) return;
|
|
256
|
+
scrollY.current = globalThis.scrollY;
|
|
257
|
+
}
|
|
258
|
+
function onMouseMove(e) {
|
|
259
|
+
if (!isInViewport) return;
|
|
260
|
+
targetMouseX.current = (e.clientX / globalThis.innerWidth - 0.5) * 2;
|
|
261
|
+
targetMouseY.current = (e.clientY / globalThis.innerHeight - 0.5) * 2;
|
|
262
|
+
}
|
|
263
|
+
globalThis.addEventListener("scroll", onScroll, { passive: true });
|
|
264
|
+
globalThis.addEventListener("mousemove", onMouseMove, { passive: true });
|
|
265
|
+
rafId = requestAnimationFrame(loop);
|
|
266
|
+
return () => {
|
|
267
|
+
running = false;
|
|
268
|
+
globalThis.removeEventListener("scroll", onScroll);
|
|
269
|
+
globalThis.removeEventListener("mousemove", onMouseMove);
|
|
270
|
+
cancelAnimationFrame(rafId);
|
|
271
|
+
};
|
|
272
|
+
}, [
|
|
273
|
+
reducedMotion,
|
|
274
|
+
isInViewport,
|
|
275
|
+
containerRef,
|
|
276
|
+
speed,
|
|
277
|
+
mouseShiftX,
|
|
278
|
+
mouseShiftY,
|
|
279
|
+
mouseLerp
|
|
280
|
+
]);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
function Vignette({
|
|
284
|
+
centerX = 50,
|
|
285
|
+
centerY = 100,
|
|
286
|
+
shape = "circle",
|
|
287
|
+
stops,
|
|
288
|
+
transitionDuration = 1e3
|
|
289
|
+
}) {
|
|
290
|
+
const { images, index, reducedMotion } = useHeroScene();
|
|
291
|
+
const transitionMs = reducedMotion ? "0ms" : `${transitionDuration}ms`;
|
|
292
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: images.map((img, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
293
|
+
"div",
|
|
294
|
+
{
|
|
295
|
+
"aria-hidden": "true",
|
|
296
|
+
style: {
|
|
297
|
+
position: "absolute",
|
|
298
|
+
inset: 0,
|
|
299
|
+
zIndex: 20,
|
|
300
|
+
pointerEvents: "none",
|
|
301
|
+
opacity: i === index ? 1 : 0,
|
|
302
|
+
transition: `opacity ${transitionMs} ease`,
|
|
303
|
+
background: buildVignetteGradient(img.color, {
|
|
304
|
+
centerX,
|
|
305
|
+
centerY,
|
|
306
|
+
shape,
|
|
307
|
+
stops
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
`vignette-${img.color}`
|
|
312
|
+
)) });
|
|
313
|
+
}
|
|
314
|
+
function Blur({
|
|
315
|
+
amount = 24,
|
|
316
|
+
centerX = 50,
|
|
317
|
+
centerY = 65,
|
|
318
|
+
innerRadius = 15,
|
|
319
|
+
outerRadius = 55
|
|
320
|
+
}) {
|
|
321
|
+
useHeroScene();
|
|
322
|
+
const mask = buildBlurMask({ centerX, centerY, innerRadius, outerRadius });
|
|
323
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
324
|
+
"div",
|
|
325
|
+
{
|
|
326
|
+
"aria-hidden": "true",
|
|
327
|
+
style: {
|
|
328
|
+
position: "absolute",
|
|
329
|
+
inset: 0,
|
|
330
|
+
zIndex: 15,
|
|
331
|
+
backdropFilter: `blur(${amount}px)`,
|
|
332
|
+
WebkitBackdropFilter: `blur(${amount}px)`,
|
|
333
|
+
maskImage: mask,
|
|
334
|
+
WebkitMaskImage: mask,
|
|
335
|
+
pointerEvents: "none"
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
function Pattern({
|
|
341
|
+
dotSize = 1,
|
|
342
|
+
spacing = 20,
|
|
343
|
+
lightColor = "rgba(0 0 0 / 0.15)",
|
|
344
|
+
darkColor = "rgba(255 255 255 / 0.1)"
|
|
345
|
+
}) {
|
|
346
|
+
useHeroScene();
|
|
347
|
+
const bgImage = (color) => `radial-gradient(circle, ${color} ${dotSize}px, transparent ${dotSize}px)`;
|
|
348
|
+
const bgSize = `${spacing}px ${spacing}px`;
|
|
349
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
350
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
351
|
+
"div",
|
|
352
|
+
{
|
|
353
|
+
"aria-hidden": "true",
|
|
354
|
+
className: "dark:hidden",
|
|
355
|
+
style: {
|
|
356
|
+
position: "absolute",
|
|
357
|
+
inset: 0,
|
|
358
|
+
zIndex: 20,
|
|
359
|
+
pointerEvents: "none",
|
|
360
|
+
backgroundImage: bgImage(lightColor),
|
|
361
|
+
backgroundSize: bgSize
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
),
|
|
365
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
366
|
+
"div",
|
|
367
|
+
{
|
|
368
|
+
"aria-hidden": "true",
|
|
369
|
+
className: "hidden dark:block",
|
|
370
|
+
style: {
|
|
371
|
+
position: "absolute",
|
|
372
|
+
inset: 0,
|
|
373
|
+
zIndex: 20,
|
|
374
|
+
pointerEvents: "none",
|
|
375
|
+
backgroundImage: bgImage(darkColor),
|
|
376
|
+
backgroundSize: bgSize
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
] });
|
|
381
|
+
}
|
|
382
|
+
function DarkOverlay({ opacity = 0.4 }) {
|
|
383
|
+
useHeroScene();
|
|
384
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
385
|
+
"div",
|
|
386
|
+
{
|
|
387
|
+
"aria-hidden": "true",
|
|
388
|
+
className: "pointer-events-none hidden dark:block",
|
|
389
|
+
style: {
|
|
390
|
+
position: "absolute",
|
|
391
|
+
inset: 0,
|
|
392
|
+
zIndex: 10,
|
|
393
|
+
backgroundColor: `rgba(0 0 0 / ${opacity})`
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
function Content({ className, children }) {
|
|
399
|
+
useHeroScene();
|
|
400
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
401
|
+
"div",
|
|
402
|
+
{
|
|
403
|
+
className,
|
|
404
|
+
style: { position: "relative", zIndex: 30 },
|
|
405
|
+
children
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
var HeroScene = Object.assign(HeroSceneRoot, {
|
|
410
|
+
Parallax,
|
|
411
|
+
Vignette,
|
|
412
|
+
Blur,
|
|
413
|
+
Pattern,
|
|
414
|
+
DarkOverlay,
|
|
415
|
+
Content
|
|
416
|
+
});
|
|
417
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
418
|
+
0 && (module.exports = {
|
|
419
|
+
HeroScene,
|
|
420
|
+
buildBlurMask,
|
|
421
|
+
buildVignetteGradient,
|
|
422
|
+
useReducedMotion
|
|
423
|
+
});
|
|
424
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/hero-scene.tsx","../src/hero-scene-context.ts","../src/utils.ts","../src/use-in-viewport.ts","../src/use-reduced-motion.ts"],"sourcesContent":["export { HeroScene } from './hero-scene'\nexport { buildVignetteGradient, buildBlurMask } from './utils'\nexport { useReducedMotion } from './use-reduced-motion'\n\nexport type {\n HeroSceneProps,\n HeroImage,\n ParallaxProps,\n VignetteProps,\n BlurProps,\n PatternProps,\n DarkOverlayProps,\n ContentProps,\n} from './types'\n","'use client'\n\nimport Image from 'next/image'\nimport { useEffect, useRef, useState } from 'react'\n\nimport { HeroSceneContext, useHeroScene } from './hero-scene-context'\nimport type {\n BlurProps,\n ContentProps,\n DarkOverlayProps,\n HeroSceneProps,\n ParallaxProps,\n PatternProps,\n VignetteProps,\n} from './types'\nimport { buildBlurMask, buildVignetteGradient } from './utils'\nimport { useInViewport } from './use-in-viewport'\nimport { useReducedMotion } from './use-reduced-motion'\n\n// ─── Root Component ──────────────────────────────────────────\n\nfunction HeroSceneRoot({\n images,\n initialIndex = 0,\n interval = 30_000,\n transitionDuration = 700,\n className,\n onIndexChange,\n children,\n}: HeroSceneProps) {\n const [index, setIndex] = useState(initialIndex)\n const rootRef = useRef<HTMLDivElement>(null)\n const reducedMotion = useReducedMotion()\n const isInViewport = useInViewport(rootRef)\n\n // ── Image rotation ──\n useEffect(() => {\n if (interval <= 0 || images.length <= 1) return\n\n let current = initialIndex\n const id = setInterval(() => {\n const next = (current + 1) % images.length\n current = next\n setIndex(next)\n setTimeout(() => {\n onIndexChange?.(next)\n }, 0)\n }, interval)\n return () => clearInterval(id)\n }, [initialIndex, interval, images.length, onIndexChange])\n\n const activeColor = images[index]?.color ?? '128, 128, 128'\n\n return (\n <HeroSceneContext.Provider\n value={{\n images,\n index,\n transitionDuration,\n reducedMotion,\n isInViewport,\n containerRef: rootRef,\n }}\n >\n <div\n ref={rootRef}\n className={className}\n style={{ position: 'relative', overflow: 'hidden' }}\n >\n {/* ── Background images (no parallax — Parallax child wraps these) ── */}\n <div\n data-hero-images=\"\"\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: 0,\n backgroundColor: `rgb(${activeColor})`,\n transition: reducedMotion ? 'none' : 'background-color 1s ease',\n }}\n >\n {images.map((img, i) => (\n <Image\n key={img.src}\n src={img.src}\n alt=\"\"\n width={1920}\n height={1080}\n priority={i === initialIndex}\n sizes=\"100vw\"\n style={{\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n objectFit: 'cover',\n opacity: i === index ? 1 : 0,\n transition: reducedMotion\n ? 'none'\n : `opacity ${transitionDuration}ms ease`,\n }}\n />\n ))}\n </div>\n\n {children}\n </div>\n </HeroSceneContext.Provider>\n )\n}\n\n// ─── Parallax ────────────────────────────────────────────────\n\nfunction Parallax({\n speed = 0.4,\n mouseShiftX = 25,\n mouseShiftY = 15,\n mouseLerp = 0.04,\n}: ParallaxProps) {\n const { reducedMotion, isInViewport, containerRef } = useHeroScene()\n\n const scrollY = useRef(0)\n const targetMouseX = useRef(0)\n const targetMouseY = useRef(0)\n const currentMouseX = useRef(0)\n const currentMouseY = useRef(0)\n\n useEffect(() => {\n if (reducedMotion) return\n\n const root = containerRef.current\n if (!root) return\n\n const imagesEl = root.querySelector<HTMLDivElement>('[data-hero-images]')\n if (!imagesEl) return\n\n // Expand the images container to allow parallax overflow\n imagesEl.style.inset = '-15%'\n imagesEl.style.willChange = 'transform'\n\n return () => {\n imagesEl.style.inset = '0'\n imagesEl.style.willChange = ''\n imagesEl.style.transform = ''\n }\n }, [reducedMotion, containerRef])\n\n useEffect(() => {\n if (reducedMotion) return\n\n const root = containerRef.current\n if (!root) return\n\n const imagesEl = root.querySelector<HTMLDivElement>('[data-hero-images]')\n if (!imagesEl) return\n\n let rafId: number\n let running = true\n\n function loop() {\n if (!running) return\n\n currentMouseX.current +=\n (targetMouseX.current - currentMouseX.current) * mouseLerp\n currentMouseY.current +=\n (targetMouseY.current - currentMouseY.current) * mouseLerp\n\n const y = scrollY.current * speed\n const mx = currentMouseX.current * mouseShiftX\n const my = currentMouseY.current * mouseShiftY\n imagesEl!.style.transform = `translate3d(${mx}px, ${y + my}px, 0)`\n\n rafId = requestAnimationFrame(loop)\n }\n\n function onScroll() {\n if (!isInViewport) return\n scrollY.current = globalThis.scrollY\n }\n\n function onMouseMove(e: MouseEvent) {\n if (!isInViewport) return\n targetMouseX.current = (e.clientX / globalThis.innerWidth - 0.5) * 2\n targetMouseY.current = (e.clientY / globalThis.innerHeight - 0.5) * 2\n }\n\n globalThis.addEventListener('scroll', onScroll, { passive: true })\n globalThis.addEventListener('mousemove', onMouseMove, { passive: true })\n rafId = requestAnimationFrame(loop)\n\n return () => {\n running = false\n globalThis.removeEventListener('scroll', onScroll)\n globalThis.removeEventListener('mousemove', onMouseMove)\n cancelAnimationFrame(rafId)\n }\n }, [\n reducedMotion,\n isInViewport,\n containerRef,\n speed,\n mouseShiftX,\n mouseShiftY,\n mouseLerp,\n ])\n\n // Parallax is a behavior-only component — renders nothing\n return null\n}\n\n// ─── Vignette ────────────────────────────────────────────────\n\nfunction Vignette({\n centerX = 50,\n centerY = 100,\n shape = 'circle',\n stops,\n transitionDuration = 1000,\n}: VignetteProps) {\n const { images, index, reducedMotion } = useHeroScene()\n const transitionMs = reducedMotion ? '0ms' : `${transitionDuration}ms`\n\n return (\n <>\n {images.map((img, i) => (\n <div\n key={`vignette-${img.color}`}\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 20,\n pointerEvents: 'none',\n opacity: i === index ? 1 : 0,\n transition: `opacity ${transitionMs} ease`,\n background: buildVignetteGradient(img.color, {\n centerX,\n centerY,\n shape,\n stops,\n }),\n }}\n />\n ))}\n </>\n )\n}\n\n// ─── Blur ────────────────────────────────────────────────────\n\nfunction Blur({\n amount = 24,\n centerX = 50,\n centerY = 65,\n innerRadius = 15,\n outerRadius = 55,\n}: BlurProps) {\n useHeroScene() // validate context\n\n const mask = buildBlurMask({ centerX, centerY, innerRadius, outerRadius })\n\n return (\n <div\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 15,\n backdropFilter: `blur(${amount}px)`,\n WebkitBackdropFilter: `blur(${amount}px)`,\n maskImage: mask,\n WebkitMaskImage: mask,\n pointerEvents: 'none',\n }}\n />\n )\n}\n\n// ─── Pattern ─────────────────────────────────────────────────\n\nfunction Pattern({\n dotSize = 1,\n spacing = 20,\n lightColor = 'rgba(0 0 0 / 0.15)',\n darkColor = 'rgba(255 255 255 / 0.1)',\n}: PatternProps) {\n useHeroScene() // validate context\n\n const bgImage = (color: string) =>\n `radial-gradient(circle, ${color} ${dotSize}px, transparent ${dotSize}px)`\n const bgSize = `${spacing}px ${spacing}px`\n\n return (\n <>\n <div\n aria-hidden=\"true\"\n className=\"dark:hidden\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 20,\n pointerEvents: 'none',\n backgroundImage: bgImage(lightColor),\n backgroundSize: bgSize,\n }}\n />\n <div\n aria-hidden=\"true\"\n className=\"hidden dark:block\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 20,\n pointerEvents: 'none',\n backgroundImage: bgImage(darkColor),\n backgroundSize: bgSize,\n }}\n />\n </>\n )\n}\n\n// ─── DarkOverlay ─────────────────────────────────────────────\n\nfunction DarkOverlay({ opacity = 0.4 }: DarkOverlayProps) {\n useHeroScene() // validate context\n\n return (\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none hidden dark:block\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 10,\n backgroundColor: `rgba(0 0 0 / ${opacity})`,\n }}\n />\n )\n}\n\n// ─── Content ─────────────────────────────────────────────────\n\nfunction Content({ className, children }: ContentProps) {\n useHeroScene() // validate context\n\n return (\n <div\n className={className}\n style={{ position: 'relative', zIndex: 30 }}\n >\n {children}\n </div>\n )\n}\n\n// ─── Compound export ─────────────────────────────────────────\n\nexport const HeroScene = Object.assign(HeroSceneRoot, {\n Parallax,\n Vignette,\n Blur,\n Pattern,\n DarkOverlay,\n Content,\n})\n","'use client'\n\nimport { createContext, useContext } from 'react'\nimport type { HeroSceneContextValue } from './types'\n\nexport const HeroSceneContext = createContext<HeroSceneContextValue | null>(null)\n\nexport function useHeroScene(): HeroSceneContextValue {\n const ctx = useContext(HeroSceneContext)\n if (!ctx) {\n throw new Error(\n 'HeroScene compound components (Parallax, Vignette, Blur, Pattern, DarkOverlay, Content) ' +\n 'must be rendered inside a <HeroScene> parent.',\n )\n }\n return ctx\n}\n","import type { VignetteProps, BlurProps } from './types'\n\nconst DEFAULT_STOPS: [number, number][] = [\n [0, 0],\n [15, 0.08],\n [30, 0.15],\n [45, 0.25],\n [60, 0.35],\n [75, 0.45],\n [88, 0.55],\n [100, 0.6],\n]\n\nexport function buildVignetteGradient(\n color: string,\n config: Pick<VignetteProps, 'shape' | 'centerX' | 'centerY' | 'stops'>,\n): string {\n const shape = config.shape ?? 'circle'\n const cx = config.centerX ?? 50\n const cy = config.centerY ?? 100\n const stops = config.stops ?? DEFAULT_STOPS\n\n const gradientStops = stops\n .map(([pos, opacity]) =>\n opacity === 0\n ? `transparent ${pos}%`\n : `color-mix(in srgb, rgb(${color}) ${Math.round(opacity * 100)}%, transparent) ${pos}%`,\n )\n .join(', ')\n\n return `radial-gradient(${shape} at ${cx}% ${cy}%, ${gradientStops})`\n}\n\nexport function buildBlurMask(\n config: Pick<BlurProps, 'centerX' | 'centerY' | 'innerRadius' | 'outerRadius'>,\n): string {\n const cx = config.centerX ?? 50\n const cy = config.centerY ?? 65\n const inner = config.innerRadius ?? 15\n const outer = config.outerRadius ?? 55\n\n return `radial-gradient(circle at ${cx}% ${cy}%, transparent ${inner}%, black ${outer}%)`\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport type { RefObject } from 'react'\n\n/**\n * Returns true when the referenced element is at least partially visible\n * in the viewport, using IntersectionObserver. SSR-safe — defaults to true\n * so effects run immediately on first paint before the observer fires.\n */\nexport function useInViewport(ref: RefObject<HTMLElement | null>): boolean {\n const [inViewport, setInViewport] = useState(true)\n\n useEffect(() => {\n const el = ref.current\n if (!el) return\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n setInViewport(entry.isIntersecting)\n },\n { threshold: 0 },\n )\n\n observer.observe(el)\n return () => observer.disconnect()\n }, [ref])\n\n return inViewport\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\n\n/**\n * Returns true when the user has enabled \"prefers-reduced-motion: reduce\"\n * in their OS or browser settings. SSR-safe — defaults to false.\n */\nexport function useReducedMotion(): boolean {\n const [reduced, setReduced] = useState(false)\n\n useEffect(() => {\n const mql = globalThis.matchMedia('(prefers-reduced-motion: reduce)')\n setReduced(mql.matches)\n\n function onChange(e: MediaQueryListEvent) {\n setReduced(e.matches)\n }\n\n mql.addEventListener('change', onChange)\n return () => mql.removeEventListener('change', onChange)\n }, [])\n\n return reduced\n}\n"],"mappings":"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","names":["import_react","import_react","import_react","Image"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
type HeroImage = {
|
|
4
|
+
/** Image URL */
|
|
5
|
+
src: string;
|
|
6
|
+
/** Dominant color as "r, g, b" string — used for vignette and fallback bg */
|
|
7
|
+
color: string;
|
|
8
|
+
};
|
|
9
|
+
type HeroSceneProps = {
|
|
10
|
+
/** Array of images to rotate through */
|
|
11
|
+
images: HeroImage[];
|
|
12
|
+
/** Index of the initial image to show. Default: 0 */
|
|
13
|
+
initialIndex?: number;
|
|
14
|
+
/** Rotation interval in ms. 0 disables rotation. Default: 30000 */
|
|
15
|
+
interval?: number;
|
|
16
|
+
/** Image transition duration in ms. Default: 700 */
|
|
17
|
+
transitionDuration?: number;
|
|
18
|
+
/** Extra className for the container */
|
|
19
|
+
className?: string;
|
|
20
|
+
/** Callback when the active image index changes */
|
|
21
|
+
onIndexChange?: (index: number) => void;
|
|
22
|
+
/** Compound children — HeroScene.Parallax, HeroScene.Vignette, etc. */
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
};
|
|
25
|
+
type ParallaxProps = {
|
|
26
|
+
/** Vertical parallax speed (0 = fixed, 1 = normal scroll). Default: 0.4 */
|
|
27
|
+
speed?: number;
|
|
28
|
+
/** Max horizontal shift from mouse in px. Default: 25 */
|
|
29
|
+
mouseShiftX?: number;
|
|
30
|
+
/** Max vertical shift from mouse in px. Default: 15 */
|
|
31
|
+
mouseShiftY?: number;
|
|
32
|
+
/** Mouse follow lerp factor (0-1, lower = smoother). Default: 0.04 */
|
|
33
|
+
mouseLerp?: number;
|
|
34
|
+
};
|
|
35
|
+
type VignetteProps = {
|
|
36
|
+
/** Vignette shape center X position (%). Default: 50 */
|
|
37
|
+
centerX?: number;
|
|
38
|
+
/** Vignette shape center Y position (%). Default: 100 */
|
|
39
|
+
centerY?: number;
|
|
40
|
+
/** Vignette shape type. Default: 'circle' */
|
|
41
|
+
shape?: 'circle' | 'ellipse';
|
|
42
|
+
/** Opacity stops — array of [percentage, opacity] pairs from center to edge.
|
|
43
|
+
* Default: [[0, 0], [15, 0.08], [30, 0.15], [45, 0.25], [60, 0.35], [75, 0.45], [88, 0.55], [100, 0.6]] */
|
|
44
|
+
stops?: [number, number][];
|
|
45
|
+
/** Transition duration in ms when color changes. Default: 1000 */
|
|
46
|
+
transitionDuration?: number;
|
|
47
|
+
};
|
|
48
|
+
type BlurProps = {
|
|
49
|
+
/** Blur amount in px. Default: 24 (backdrop-blur-xl) */
|
|
50
|
+
amount?: number;
|
|
51
|
+
/** Blur mask center X (%). Default: 50 */
|
|
52
|
+
centerX?: number;
|
|
53
|
+
/** Blur mask center Y (%). Default: 65 */
|
|
54
|
+
centerY?: number;
|
|
55
|
+
/** Inner radius where blur starts (%). Default: 15 */
|
|
56
|
+
innerRadius?: number;
|
|
57
|
+
/** Outer radius where blur is full (%). Default: 55 */
|
|
58
|
+
outerRadius?: number;
|
|
59
|
+
};
|
|
60
|
+
type PatternProps = {
|
|
61
|
+
/** Dot size in px. Default: 1 */
|
|
62
|
+
dotSize?: number;
|
|
63
|
+
/** Grid spacing in px. Default: 20 */
|
|
64
|
+
spacing?: number;
|
|
65
|
+
/** Dot color for light mode. Default: 'rgba(0 0 0 / 0.15)' */
|
|
66
|
+
lightColor?: string;
|
|
67
|
+
/** Dot color for dark mode. Default: 'rgba(255 255 255 / 0.1)' */
|
|
68
|
+
darkColor?: string;
|
|
69
|
+
};
|
|
70
|
+
type DarkOverlayProps = {
|
|
71
|
+
/** Overlay opacity. Default: 0.4 */
|
|
72
|
+
opacity?: number;
|
|
73
|
+
};
|
|
74
|
+
type ContentProps = {
|
|
75
|
+
/** Extra className for the content wrapper */
|
|
76
|
+
className?: string;
|
|
77
|
+
/** Content to render above all layers */
|
|
78
|
+
children?: React.ReactNode;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
declare function HeroSceneRoot({ images, initialIndex, interval, transitionDuration, className, onIndexChange, children, }: HeroSceneProps): react_jsx_runtime.JSX.Element;
|
|
82
|
+
declare function Parallax({ speed, mouseShiftX, mouseShiftY, mouseLerp, }: ParallaxProps): null;
|
|
83
|
+
declare function Vignette({ centerX, centerY, shape, stops, transitionDuration, }: VignetteProps): react_jsx_runtime.JSX.Element;
|
|
84
|
+
declare function Blur({ amount, centerX, centerY, innerRadius, outerRadius, }: BlurProps): react_jsx_runtime.JSX.Element;
|
|
85
|
+
declare function Pattern({ dotSize, spacing, lightColor, darkColor, }: PatternProps): react_jsx_runtime.JSX.Element;
|
|
86
|
+
declare function DarkOverlay({ opacity }: DarkOverlayProps): react_jsx_runtime.JSX.Element;
|
|
87
|
+
declare function Content({ className, children }: ContentProps): react_jsx_runtime.JSX.Element;
|
|
88
|
+
declare const HeroScene: typeof HeroSceneRoot & {
|
|
89
|
+
Parallax: typeof Parallax;
|
|
90
|
+
Vignette: typeof Vignette;
|
|
91
|
+
Blur: typeof Blur;
|
|
92
|
+
Pattern: typeof Pattern;
|
|
93
|
+
DarkOverlay: typeof DarkOverlay;
|
|
94
|
+
Content: typeof Content;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
declare function buildVignetteGradient(color: string, config: Pick<VignetteProps, 'shape' | 'centerX' | 'centerY' | 'stops'>): string;
|
|
98
|
+
declare function buildBlurMask(config: Pick<BlurProps, 'centerX' | 'centerY' | 'innerRadius' | 'outerRadius'>): string;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns true when the user has enabled "prefers-reduced-motion: reduce"
|
|
102
|
+
* in their OS or browser settings. SSR-safe — defaults to false.
|
|
103
|
+
*/
|
|
104
|
+
declare function useReducedMotion(): boolean;
|
|
105
|
+
|
|
106
|
+
export { type BlurProps, type ContentProps, type DarkOverlayProps, type HeroImage, HeroScene, type HeroSceneProps, type ParallaxProps, type PatternProps, type VignetteProps, buildBlurMask, buildVignetteGradient, useReducedMotion };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
type HeroImage = {
|
|
4
|
+
/** Image URL */
|
|
5
|
+
src: string;
|
|
6
|
+
/** Dominant color as "r, g, b" string — used for vignette and fallback bg */
|
|
7
|
+
color: string;
|
|
8
|
+
};
|
|
9
|
+
type HeroSceneProps = {
|
|
10
|
+
/** Array of images to rotate through */
|
|
11
|
+
images: HeroImage[];
|
|
12
|
+
/** Index of the initial image to show. Default: 0 */
|
|
13
|
+
initialIndex?: number;
|
|
14
|
+
/** Rotation interval in ms. 0 disables rotation. Default: 30000 */
|
|
15
|
+
interval?: number;
|
|
16
|
+
/** Image transition duration in ms. Default: 700 */
|
|
17
|
+
transitionDuration?: number;
|
|
18
|
+
/** Extra className for the container */
|
|
19
|
+
className?: string;
|
|
20
|
+
/** Callback when the active image index changes */
|
|
21
|
+
onIndexChange?: (index: number) => void;
|
|
22
|
+
/** Compound children — HeroScene.Parallax, HeroScene.Vignette, etc. */
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
};
|
|
25
|
+
type ParallaxProps = {
|
|
26
|
+
/** Vertical parallax speed (0 = fixed, 1 = normal scroll). Default: 0.4 */
|
|
27
|
+
speed?: number;
|
|
28
|
+
/** Max horizontal shift from mouse in px. Default: 25 */
|
|
29
|
+
mouseShiftX?: number;
|
|
30
|
+
/** Max vertical shift from mouse in px. Default: 15 */
|
|
31
|
+
mouseShiftY?: number;
|
|
32
|
+
/** Mouse follow lerp factor (0-1, lower = smoother). Default: 0.04 */
|
|
33
|
+
mouseLerp?: number;
|
|
34
|
+
};
|
|
35
|
+
type VignetteProps = {
|
|
36
|
+
/** Vignette shape center X position (%). Default: 50 */
|
|
37
|
+
centerX?: number;
|
|
38
|
+
/** Vignette shape center Y position (%). Default: 100 */
|
|
39
|
+
centerY?: number;
|
|
40
|
+
/** Vignette shape type. Default: 'circle' */
|
|
41
|
+
shape?: 'circle' | 'ellipse';
|
|
42
|
+
/** Opacity stops — array of [percentage, opacity] pairs from center to edge.
|
|
43
|
+
* Default: [[0, 0], [15, 0.08], [30, 0.15], [45, 0.25], [60, 0.35], [75, 0.45], [88, 0.55], [100, 0.6]] */
|
|
44
|
+
stops?: [number, number][];
|
|
45
|
+
/** Transition duration in ms when color changes. Default: 1000 */
|
|
46
|
+
transitionDuration?: number;
|
|
47
|
+
};
|
|
48
|
+
type BlurProps = {
|
|
49
|
+
/** Blur amount in px. Default: 24 (backdrop-blur-xl) */
|
|
50
|
+
amount?: number;
|
|
51
|
+
/** Blur mask center X (%). Default: 50 */
|
|
52
|
+
centerX?: number;
|
|
53
|
+
/** Blur mask center Y (%). Default: 65 */
|
|
54
|
+
centerY?: number;
|
|
55
|
+
/** Inner radius where blur starts (%). Default: 15 */
|
|
56
|
+
innerRadius?: number;
|
|
57
|
+
/** Outer radius where blur is full (%). Default: 55 */
|
|
58
|
+
outerRadius?: number;
|
|
59
|
+
};
|
|
60
|
+
type PatternProps = {
|
|
61
|
+
/** Dot size in px. Default: 1 */
|
|
62
|
+
dotSize?: number;
|
|
63
|
+
/** Grid spacing in px. Default: 20 */
|
|
64
|
+
spacing?: number;
|
|
65
|
+
/** Dot color for light mode. Default: 'rgba(0 0 0 / 0.15)' */
|
|
66
|
+
lightColor?: string;
|
|
67
|
+
/** Dot color for dark mode. Default: 'rgba(255 255 255 / 0.1)' */
|
|
68
|
+
darkColor?: string;
|
|
69
|
+
};
|
|
70
|
+
type DarkOverlayProps = {
|
|
71
|
+
/** Overlay opacity. Default: 0.4 */
|
|
72
|
+
opacity?: number;
|
|
73
|
+
};
|
|
74
|
+
type ContentProps = {
|
|
75
|
+
/** Extra className for the content wrapper */
|
|
76
|
+
className?: string;
|
|
77
|
+
/** Content to render above all layers */
|
|
78
|
+
children?: React.ReactNode;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
declare function HeroSceneRoot({ images, initialIndex, interval, transitionDuration, className, onIndexChange, children, }: HeroSceneProps): react_jsx_runtime.JSX.Element;
|
|
82
|
+
declare function Parallax({ speed, mouseShiftX, mouseShiftY, mouseLerp, }: ParallaxProps): null;
|
|
83
|
+
declare function Vignette({ centerX, centerY, shape, stops, transitionDuration, }: VignetteProps): react_jsx_runtime.JSX.Element;
|
|
84
|
+
declare function Blur({ amount, centerX, centerY, innerRadius, outerRadius, }: BlurProps): react_jsx_runtime.JSX.Element;
|
|
85
|
+
declare function Pattern({ dotSize, spacing, lightColor, darkColor, }: PatternProps): react_jsx_runtime.JSX.Element;
|
|
86
|
+
declare function DarkOverlay({ opacity }: DarkOverlayProps): react_jsx_runtime.JSX.Element;
|
|
87
|
+
declare function Content({ className, children }: ContentProps): react_jsx_runtime.JSX.Element;
|
|
88
|
+
declare const HeroScene: typeof HeroSceneRoot & {
|
|
89
|
+
Parallax: typeof Parallax;
|
|
90
|
+
Vignette: typeof Vignette;
|
|
91
|
+
Blur: typeof Blur;
|
|
92
|
+
Pattern: typeof Pattern;
|
|
93
|
+
DarkOverlay: typeof DarkOverlay;
|
|
94
|
+
Content: typeof Content;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
declare function buildVignetteGradient(color: string, config: Pick<VignetteProps, 'shape' | 'centerX' | 'centerY' | 'stops'>): string;
|
|
98
|
+
declare function buildBlurMask(config: Pick<BlurProps, 'centerX' | 'centerY' | 'innerRadius' | 'outerRadius'>): string;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns true when the user has enabled "prefers-reduced-motion: reduce"
|
|
102
|
+
* in their OS or browser settings. SSR-safe — defaults to false.
|
|
103
|
+
*/
|
|
104
|
+
declare function useReducedMotion(): boolean;
|
|
105
|
+
|
|
106
|
+
export { type BlurProps, type ContentProps, type DarkOverlayProps, type HeroImage, HeroScene, type HeroSceneProps, type ParallaxProps, type PatternProps, type VignetteProps, buildBlurMask, buildVignetteGradient, useReducedMotion };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/hero-scene.tsx
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { useEffect as useEffect3, useRef, useState as useState3 } from "react";
|
|
6
|
+
|
|
7
|
+
// src/hero-scene-context.ts
|
|
8
|
+
import { createContext, useContext } from "react";
|
|
9
|
+
var HeroSceneContext = createContext(null);
|
|
10
|
+
function useHeroScene() {
|
|
11
|
+
const ctx = useContext(HeroSceneContext);
|
|
12
|
+
if (!ctx) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"HeroScene compound components (Parallax, Vignette, Blur, Pattern, DarkOverlay, Content) must be rendered inside a <HeroScene> parent."
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return ctx;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/utils.ts
|
|
21
|
+
var DEFAULT_STOPS = [
|
|
22
|
+
[0, 0],
|
|
23
|
+
[15, 0.08],
|
|
24
|
+
[30, 0.15],
|
|
25
|
+
[45, 0.25],
|
|
26
|
+
[60, 0.35],
|
|
27
|
+
[75, 0.45],
|
|
28
|
+
[88, 0.55],
|
|
29
|
+
[100, 0.6]
|
|
30
|
+
];
|
|
31
|
+
function buildVignetteGradient(color, config) {
|
|
32
|
+
const shape = config.shape ?? "circle";
|
|
33
|
+
const cx = config.centerX ?? 50;
|
|
34
|
+
const cy = config.centerY ?? 100;
|
|
35
|
+
const stops = config.stops ?? DEFAULT_STOPS;
|
|
36
|
+
const gradientStops = stops.map(
|
|
37
|
+
([pos, opacity]) => opacity === 0 ? `transparent ${pos}%` : `color-mix(in srgb, rgb(${color}) ${Math.round(opacity * 100)}%, transparent) ${pos}%`
|
|
38
|
+
).join(", ");
|
|
39
|
+
return `radial-gradient(${shape} at ${cx}% ${cy}%, ${gradientStops})`;
|
|
40
|
+
}
|
|
41
|
+
function buildBlurMask(config) {
|
|
42
|
+
const cx = config.centerX ?? 50;
|
|
43
|
+
const cy = config.centerY ?? 65;
|
|
44
|
+
const inner = config.innerRadius ?? 15;
|
|
45
|
+
const outer = config.outerRadius ?? 55;
|
|
46
|
+
return `radial-gradient(circle at ${cx}% ${cy}%, transparent ${inner}%, black ${outer}%)`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/use-in-viewport.ts
|
|
50
|
+
import { useEffect, useState } from "react";
|
|
51
|
+
function useInViewport(ref) {
|
|
52
|
+
const [inViewport, setInViewport] = useState(true);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const el = ref.current;
|
|
55
|
+
if (!el) return;
|
|
56
|
+
const observer = new IntersectionObserver(
|
|
57
|
+
([entry]) => {
|
|
58
|
+
setInViewport(entry.isIntersecting);
|
|
59
|
+
},
|
|
60
|
+
{ threshold: 0 }
|
|
61
|
+
);
|
|
62
|
+
observer.observe(el);
|
|
63
|
+
return () => observer.disconnect();
|
|
64
|
+
}, [ref]);
|
|
65
|
+
return inViewport;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/use-reduced-motion.ts
|
|
69
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
70
|
+
function useReducedMotion() {
|
|
71
|
+
const [reduced, setReduced] = useState2(false);
|
|
72
|
+
useEffect2(() => {
|
|
73
|
+
const mql = globalThis.matchMedia("(prefers-reduced-motion: reduce)");
|
|
74
|
+
setReduced(mql.matches);
|
|
75
|
+
function onChange(e) {
|
|
76
|
+
setReduced(e.matches);
|
|
77
|
+
}
|
|
78
|
+
mql.addEventListener("change", onChange);
|
|
79
|
+
return () => mql.removeEventListener("change", onChange);
|
|
80
|
+
}, []);
|
|
81
|
+
return reduced;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/hero-scene.tsx
|
|
85
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
86
|
+
function HeroSceneRoot({
|
|
87
|
+
images,
|
|
88
|
+
initialIndex = 0,
|
|
89
|
+
interval = 3e4,
|
|
90
|
+
transitionDuration = 700,
|
|
91
|
+
className,
|
|
92
|
+
onIndexChange,
|
|
93
|
+
children
|
|
94
|
+
}) {
|
|
95
|
+
const [index, setIndex] = useState3(initialIndex);
|
|
96
|
+
const rootRef = useRef(null);
|
|
97
|
+
const reducedMotion = useReducedMotion();
|
|
98
|
+
const isInViewport = useInViewport(rootRef);
|
|
99
|
+
useEffect3(() => {
|
|
100
|
+
if (interval <= 0 || images.length <= 1) return;
|
|
101
|
+
let current = initialIndex;
|
|
102
|
+
const id = setInterval(() => {
|
|
103
|
+
const next = (current + 1) % images.length;
|
|
104
|
+
current = next;
|
|
105
|
+
setIndex(next);
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
onIndexChange?.(next);
|
|
108
|
+
}, 0);
|
|
109
|
+
}, interval);
|
|
110
|
+
return () => clearInterval(id);
|
|
111
|
+
}, [initialIndex, interval, images.length, onIndexChange]);
|
|
112
|
+
const activeColor = images[index]?.color ?? "128, 128, 128";
|
|
113
|
+
return /* @__PURE__ */ jsx(
|
|
114
|
+
HeroSceneContext.Provider,
|
|
115
|
+
{
|
|
116
|
+
value: {
|
|
117
|
+
images,
|
|
118
|
+
index,
|
|
119
|
+
transitionDuration,
|
|
120
|
+
reducedMotion,
|
|
121
|
+
isInViewport,
|
|
122
|
+
containerRef: rootRef
|
|
123
|
+
},
|
|
124
|
+
children: /* @__PURE__ */ jsxs(
|
|
125
|
+
"div",
|
|
126
|
+
{
|
|
127
|
+
ref: rootRef,
|
|
128
|
+
className,
|
|
129
|
+
style: { position: "relative", overflow: "hidden" },
|
|
130
|
+
children: [
|
|
131
|
+
/* @__PURE__ */ jsx(
|
|
132
|
+
"div",
|
|
133
|
+
{
|
|
134
|
+
"data-hero-images": "",
|
|
135
|
+
"aria-hidden": "true",
|
|
136
|
+
style: {
|
|
137
|
+
position: "absolute",
|
|
138
|
+
inset: 0,
|
|
139
|
+
backgroundColor: `rgb(${activeColor})`,
|
|
140
|
+
transition: reducedMotion ? "none" : "background-color 1s ease"
|
|
141
|
+
},
|
|
142
|
+
children: images.map((img, i) => /* @__PURE__ */ jsx(
|
|
143
|
+
Image,
|
|
144
|
+
{
|
|
145
|
+
src: img.src,
|
|
146
|
+
alt: "",
|
|
147
|
+
width: 1920,
|
|
148
|
+
height: 1080,
|
|
149
|
+
priority: i === initialIndex,
|
|
150
|
+
sizes: "100vw",
|
|
151
|
+
style: {
|
|
152
|
+
position: "absolute",
|
|
153
|
+
inset: 0,
|
|
154
|
+
width: "100%",
|
|
155
|
+
height: "100%",
|
|
156
|
+
objectFit: "cover",
|
|
157
|
+
opacity: i === index ? 1 : 0,
|
|
158
|
+
transition: reducedMotion ? "none" : `opacity ${transitionDuration}ms ease`
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
img.src
|
|
162
|
+
))
|
|
163
|
+
}
|
|
164
|
+
),
|
|
165
|
+
children
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
function Parallax({
|
|
173
|
+
speed = 0.4,
|
|
174
|
+
mouseShiftX = 25,
|
|
175
|
+
mouseShiftY = 15,
|
|
176
|
+
mouseLerp = 0.04
|
|
177
|
+
}) {
|
|
178
|
+
const { reducedMotion, isInViewport, containerRef } = useHeroScene();
|
|
179
|
+
const scrollY = useRef(0);
|
|
180
|
+
const targetMouseX = useRef(0);
|
|
181
|
+
const targetMouseY = useRef(0);
|
|
182
|
+
const currentMouseX = useRef(0);
|
|
183
|
+
const currentMouseY = useRef(0);
|
|
184
|
+
useEffect3(() => {
|
|
185
|
+
if (reducedMotion) return;
|
|
186
|
+
const root = containerRef.current;
|
|
187
|
+
if (!root) return;
|
|
188
|
+
const imagesEl = root.querySelector("[data-hero-images]");
|
|
189
|
+
if (!imagesEl) return;
|
|
190
|
+
imagesEl.style.inset = "-15%";
|
|
191
|
+
imagesEl.style.willChange = "transform";
|
|
192
|
+
return () => {
|
|
193
|
+
imagesEl.style.inset = "0";
|
|
194
|
+
imagesEl.style.willChange = "";
|
|
195
|
+
imagesEl.style.transform = "";
|
|
196
|
+
};
|
|
197
|
+
}, [reducedMotion, containerRef]);
|
|
198
|
+
useEffect3(() => {
|
|
199
|
+
if (reducedMotion) return;
|
|
200
|
+
const root = containerRef.current;
|
|
201
|
+
if (!root) return;
|
|
202
|
+
const imagesEl = root.querySelector("[data-hero-images]");
|
|
203
|
+
if (!imagesEl) return;
|
|
204
|
+
let rafId;
|
|
205
|
+
let running = true;
|
|
206
|
+
function loop() {
|
|
207
|
+
if (!running) return;
|
|
208
|
+
currentMouseX.current += (targetMouseX.current - currentMouseX.current) * mouseLerp;
|
|
209
|
+
currentMouseY.current += (targetMouseY.current - currentMouseY.current) * mouseLerp;
|
|
210
|
+
const y = scrollY.current * speed;
|
|
211
|
+
const mx = currentMouseX.current * mouseShiftX;
|
|
212
|
+
const my = currentMouseY.current * mouseShiftY;
|
|
213
|
+
imagesEl.style.transform = `translate3d(${mx}px, ${y + my}px, 0)`;
|
|
214
|
+
rafId = requestAnimationFrame(loop);
|
|
215
|
+
}
|
|
216
|
+
function onScroll() {
|
|
217
|
+
if (!isInViewport) return;
|
|
218
|
+
scrollY.current = globalThis.scrollY;
|
|
219
|
+
}
|
|
220
|
+
function onMouseMove(e) {
|
|
221
|
+
if (!isInViewport) return;
|
|
222
|
+
targetMouseX.current = (e.clientX / globalThis.innerWidth - 0.5) * 2;
|
|
223
|
+
targetMouseY.current = (e.clientY / globalThis.innerHeight - 0.5) * 2;
|
|
224
|
+
}
|
|
225
|
+
globalThis.addEventListener("scroll", onScroll, { passive: true });
|
|
226
|
+
globalThis.addEventListener("mousemove", onMouseMove, { passive: true });
|
|
227
|
+
rafId = requestAnimationFrame(loop);
|
|
228
|
+
return () => {
|
|
229
|
+
running = false;
|
|
230
|
+
globalThis.removeEventListener("scroll", onScroll);
|
|
231
|
+
globalThis.removeEventListener("mousemove", onMouseMove);
|
|
232
|
+
cancelAnimationFrame(rafId);
|
|
233
|
+
};
|
|
234
|
+
}, [
|
|
235
|
+
reducedMotion,
|
|
236
|
+
isInViewport,
|
|
237
|
+
containerRef,
|
|
238
|
+
speed,
|
|
239
|
+
mouseShiftX,
|
|
240
|
+
mouseShiftY,
|
|
241
|
+
mouseLerp
|
|
242
|
+
]);
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
function Vignette({
|
|
246
|
+
centerX = 50,
|
|
247
|
+
centerY = 100,
|
|
248
|
+
shape = "circle",
|
|
249
|
+
stops,
|
|
250
|
+
transitionDuration = 1e3
|
|
251
|
+
}) {
|
|
252
|
+
const { images, index, reducedMotion } = useHeroScene();
|
|
253
|
+
const transitionMs = reducedMotion ? "0ms" : `${transitionDuration}ms`;
|
|
254
|
+
return /* @__PURE__ */ jsx(Fragment, { children: images.map((img, i) => /* @__PURE__ */ jsx(
|
|
255
|
+
"div",
|
|
256
|
+
{
|
|
257
|
+
"aria-hidden": "true",
|
|
258
|
+
style: {
|
|
259
|
+
position: "absolute",
|
|
260
|
+
inset: 0,
|
|
261
|
+
zIndex: 20,
|
|
262
|
+
pointerEvents: "none",
|
|
263
|
+
opacity: i === index ? 1 : 0,
|
|
264
|
+
transition: `opacity ${transitionMs} ease`,
|
|
265
|
+
background: buildVignetteGradient(img.color, {
|
|
266
|
+
centerX,
|
|
267
|
+
centerY,
|
|
268
|
+
shape,
|
|
269
|
+
stops
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
`vignette-${img.color}`
|
|
274
|
+
)) });
|
|
275
|
+
}
|
|
276
|
+
function Blur({
|
|
277
|
+
amount = 24,
|
|
278
|
+
centerX = 50,
|
|
279
|
+
centerY = 65,
|
|
280
|
+
innerRadius = 15,
|
|
281
|
+
outerRadius = 55
|
|
282
|
+
}) {
|
|
283
|
+
useHeroScene();
|
|
284
|
+
const mask = buildBlurMask({ centerX, centerY, innerRadius, outerRadius });
|
|
285
|
+
return /* @__PURE__ */ jsx(
|
|
286
|
+
"div",
|
|
287
|
+
{
|
|
288
|
+
"aria-hidden": "true",
|
|
289
|
+
style: {
|
|
290
|
+
position: "absolute",
|
|
291
|
+
inset: 0,
|
|
292
|
+
zIndex: 15,
|
|
293
|
+
backdropFilter: `blur(${amount}px)`,
|
|
294
|
+
WebkitBackdropFilter: `blur(${amount}px)`,
|
|
295
|
+
maskImage: mask,
|
|
296
|
+
WebkitMaskImage: mask,
|
|
297
|
+
pointerEvents: "none"
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
function Pattern({
|
|
303
|
+
dotSize = 1,
|
|
304
|
+
spacing = 20,
|
|
305
|
+
lightColor = "rgba(0 0 0 / 0.15)",
|
|
306
|
+
darkColor = "rgba(255 255 255 / 0.1)"
|
|
307
|
+
}) {
|
|
308
|
+
useHeroScene();
|
|
309
|
+
const bgImage = (color) => `radial-gradient(circle, ${color} ${dotSize}px, transparent ${dotSize}px)`;
|
|
310
|
+
const bgSize = `${spacing}px ${spacing}px`;
|
|
311
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
312
|
+
/* @__PURE__ */ jsx(
|
|
313
|
+
"div",
|
|
314
|
+
{
|
|
315
|
+
"aria-hidden": "true",
|
|
316
|
+
className: "dark:hidden",
|
|
317
|
+
style: {
|
|
318
|
+
position: "absolute",
|
|
319
|
+
inset: 0,
|
|
320
|
+
zIndex: 20,
|
|
321
|
+
pointerEvents: "none",
|
|
322
|
+
backgroundImage: bgImage(lightColor),
|
|
323
|
+
backgroundSize: bgSize
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
),
|
|
327
|
+
/* @__PURE__ */ jsx(
|
|
328
|
+
"div",
|
|
329
|
+
{
|
|
330
|
+
"aria-hidden": "true",
|
|
331
|
+
className: "hidden dark:block",
|
|
332
|
+
style: {
|
|
333
|
+
position: "absolute",
|
|
334
|
+
inset: 0,
|
|
335
|
+
zIndex: 20,
|
|
336
|
+
pointerEvents: "none",
|
|
337
|
+
backgroundImage: bgImage(darkColor),
|
|
338
|
+
backgroundSize: bgSize
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
] });
|
|
343
|
+
}
|
|
344
|
+
function DarkOverlay({ opacity = 0.4 }) {
|
|
345
|
+
useHeroScene();
|
|
346
|
+
return /* @__PURE__ */ jsx(
|
|
347
|
+
"div",
|
|
348
|
+
{
|
|
349
|
+
"aria-hidden": "true",
|
|
350
|
+
className: "pointer-events-none hidden dark:block",
|
|
351
|
+
style: {
|
|
352
|
+
position: "absolute",
|
|
353
|
+
inset: 0,
|
|
354
|
+
zIndex: 10,
|
|
355
|
+
backgroundColor: `rgba(0 0 0 / ${opacity})`
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
function Content({ className, children }) {
|
|
361
|
+
useHeroScene();
|
|
362
|
+
return /* @__PURE__ */ jsx(
|
|
363
|
+
"div",
|
|
364
|
+
{
|
|
365
|
+
className,
|
|
366
|
+
style: { position: "relative", zIndex: 30 },
|
|
367
|
+
children
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
var HeroScene = Object.assign(HeroSceneRoot, {
|
|
372
|
+
Parallax,
|
|
373
|
+
Vignette,
|
|
374
|
+
Blur,
|
|
375
|
+
Pattern,
|
|
376
|
+
DarkOverlay,
|
|
377
|
+
Content
|
|
378
|
+
});
|
|
379
|
+
export {
|
|
380
|
+
HeroScene,
|
|
381
|
+
buildBlurMask,
|
|
382
|
+
buildVignetteGradient,
|
|
383
|
+
useReducedMotion
|
|
384
|
+
};
|
|
385
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hero-scene.tsx","../src/hero-scene-context.ts","../src/utils.ts","../src/use-in-viewport.ts","../src/use-reduced-motion.ts"],"sourcesContent":["'use client'\n\nimport Image from 'next/image'\nimport { useEffect, useRef, useState } from 'react'\n\nimport { HeroSceneContext, useHeroScene } from './hero-scene-context'\nimport type {\n BlurProps,\n ContentProps,\n DarkOverlayProps,\n HeroSceneProps,\n ParallaxProps,\n PatternProps,\n VignetteProps,\n} from './types'\nimport { buildBlurMask, buildVignetteGradient } from './utils'\nimport { useInViewport } from './use-in-viewport'\nimport { useReducedMotion } from './use-reduced-motion'\n\n// ─── Root Component ──────────────────────────────────────────\n\nfunction HeroSceneRoot({\n images,\n initialIndex = 0,\n interval = 30_000,\n transitionDuration = 700,\n className,\n onIndexChange,\n children,\n}: HeroSceneProps) {\n const [index, setIndex] = useState(initialIndex)\n const rootRef = useRef<HTMLDivElement>(null)\n const reducedMotion = useReducedMotion()\n const isInViewport = useInViewport(rootRef)\n\n // ── Image rotation ──\n useEffect(() => {\n if (interval <= 0 || images.length <= 1) return\n\n let current = initialIndex\n const id = setInterval(() => {\n const next = (current + 1) % images.length\n current = next\n setIndex(next)\n setTimeout(() => {\n onIndexChange?.(next)\n }, 0)\n }, interval)\n return () => clearInterval(id)\n }, [initialIndex, interval, images.length, onIndexChange])\n\n const activeColor = images[index]?.color ?? '128, 128, 128'\n\n return (\n <HeroSceneContext.Provider\n value={{\n images,\n index,\n transitionDuration,\n reducedMotion,\n isInViewport,\n containerRef: rootRef,\n }}\n >\n <div\n ref={rootRef}\n className={className}\n style={{ position: 'relative', overflow: 'hidden' }}\n >\n {/* ── Background images (no parallax — Parallax child wraps these) ── */}\n <div\n data-hero-images=\"\"\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: 0,\n backgroundColor: `rgb(${activeColor})`,\n transition: reducedMotion ? 'none' : 'background-color 1s ease',\n }}\n >\n {images.map((img, i) => (\n <Image\n key={img.src}\n src={img.src}\n alt=\"\"\n width={1920}\n height={1080}\n priority={i === initialIndex}\n sizes=\"100vw\"\n style={{\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n objectFit: 'cover',\n opacity: i === index ? 1 : 0,\n transition: reducedMotion\n ? 'none'\n : `opacity ${transitionDuration}ms ease`,\n }}\n />\n ))}\n </div>\n\n {children}\n </div>\n </HeroSceneContext.Provider>\n )\n}\n\n// ─── Parallax ────────────────────────────────────────────────\n\nfunction Parallax({\n speed = 0.4,\n mouseShiftX = 25,\n mouseShiftY = 15,\n mouseLerp = 0.04,\n}: ParallaxProps) {\n const { reducedMotion, isInViewport, containerRef } = useHeroScene()\n\n const scrollY = useRef(0)\n const targetMouseX = useRef(0)\n const targetMouseY = useRef(0)\n const currentMouseX = useRef(0)\n const currentMouseY = useRef(0)\n\n useEffect(() => {\n if (reducedMotion) return\n\n const root = containerRef.current\n if (!root) return\n\n const imagesEl = root.querySelector<HTMLDivElement>('[data-hero-images]')\n if (!imagesEl) return\n\n // Expand the images container to allow parallax overflow\n imagesEl.style.inset = '-15%'\n imagesEl.style.willChange = 'transform'\n\n return () => {\n imagesEl.style.inset = '0'\n imagesEl.style.willChange = ''\n imagesEl.style.transform = ''\n }\n }, [reducedMotion, containerRef])\n\n useEffect(() => {\n if (reducedMotion) return\n\n const root = containerRef.current\n if (!root) return\n\n const imagesEl = root.querySelector<HTMLDivElement>('[data-hero-images]')\n if (!imagesEl) return\n\n let rafId: number\n let running = true\n\n function loop() {\n if (!running) return\n\n currentMouseX.current +=\n (targetMouseX.current - currentMouseX.current) * mouseLerp\n currentMouseY.current +=\n (targetMouseY.current - currentMouseY.current) * mouseLerp\n\n const y = scrollY.current * speed\n const mx = currentMouseX.current * mouseShiftX\n const my = currentMouseY.current * mouseShiftY\n imagesEl!.style.transform = `translate3d(${mx}px, ${y + my}px, 0)`\n\n rafId = requestAnimationFrame(loop)\n }\n\n function onScroll() {\n if (!isInViewport) return\n scrollY.current = globalThis.scrollY\n }\n\n function onMouseMove(e: MouseEvent) {\n if (!isInViewport) return\n targetMouseX.current = (e.clientX / globalThis.innerWidth - 0.5) * 2\n targetMouseY.current = (e.clientY / globalThis.innerHeight - 0.5) * 2\n }\n\n globalThis.addEventListener('scroll', onScroll, { passive: true })\n globalThis.addEventListener('mousemove', onMouseMove, { passive: true })\n rafId = requestAnimationFrame(loop)\n\n return () => {\n running = false\n globalThis.removeEventListener('scroll', onScroll)\n globalThis.removeEventListener('mousemove', onMouseMove)\n cancelAnimationFrame(rafId)\n }\n }, [\n reducedMotion,\n isInViewport,\n containerRef,\n speed,\n mouseShiftX,\n mouseShiftY,\n mouseLerp,\n ])\n\n // Parallax is a behavior-only component — renders nothing\n return null\n}\n\n// ─── Vignette ────────────────────────────────────────────────\n\nfunction Vignette({\n centerX = 50,\n centerY = 100,\n shape = 'circle',\n stops,\n transitionDuration = 1000,\n}: VignetteProps) {\n const { images, index, reducedMotion } = useHeroScene()\n const transitionMs = reducedMotion ? '0ms' : `${transitionDuration}ms`\n\n return (\n <>\n {images.map((img, i) => (\n <div\n key={`vignette-${img.color}`}\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 20,\n pointerEvents: 'none',\n opacity: i === index ? 1 : 0,\n transition: `opacity ${transitionMs} ease`,\n background: buildVignetteGradient(img.color, {\n centerX,\n centerY,\n shape,\n stops,\n }),\n }}\n />\n ))}\n </>\n )\n}\n\n// ─── Blur ────────────────────────────────────────────────────\n\nfunction Blur({\n amount = 24,\n centerX = 50,\n centerY = 65,\n innerRadius = 15,\n outerRadius = 55,\n}: BlurProps) {\n useHeroScene() // validate context\n\n const mask = buildBlurMask({ centerX, centerY, innerRadius, outerRadius })\n\n return (\n <div\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 15,\n backdropFilter: `blur(${amount}px)`,\n WebkitBackdropFilter: `blur(${amount}px)`,\n maskImage: mask,\n WebkitMaskImage: mask,\n pointerEvents: 'none',\n }}\n />\n )\n}\n\n// ─── Pattern ─────────────────────────────────────────────────\n\nfunction Pattern({\n dotSize = 1,\n spacing = 20,\n lightColor = 'rgba(0 0 0 / 0.15)',\n darkColor = 'rgba(255 255 255 / 0.1)',\n}: PatternProps) {\n useHeroScene() // validate context\n\n const bgImage = (color: string) =>\n `radial-gradient(circle, ${color} ${dotSize}px, transparent ${dotSize}px)`\n const bgSize = `${spacing}px ${spacing}px`\n\n return (\n <>\n <div\n aria-hidden=\"true\"\n className=\"dark:hidden\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 20,\n pointerEvents: 'none',\n backgroundImage: bgImage(lightColor),\n backgroundSize: bgSize,\n }}\n />\n <div\n aria-hidden=\"true\"\n className=\"hidden dark:block\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 20,\n pointerEvents: 'none',\n backgroundImage: bgImage(darkColor),\n backgroundSize: bgSize,\n }}\n />\n </>\n )\n}\n\n// ─── DarkOverlay ─────────────────────────────────────────────\n\nfunction DarkOverlay({ opacity = 0.4 }: DarkOverlayProps) {\n useHeroScene() // validate context\n\n return (\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none hidden dark:block\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 10,\n backgroundColor: `rgba(0 0 0 / ${opacity})`,\n }}\n />\n )\n}\n\n// ─── Content ─────────────────────────────────────────────────\n\nfunction Content({ className, children }: ContentProps) {\n useHeroScene() // validate context\n\n return (\n <div\n className={className}\n style={{ position: 'relative', zIndex: 30 }}\n >\n {children}\n </div>\n )\n}\n\n// ─── Compound export ─────────────────────────────────────────\n\nexport const HeroScene = Object.assign(HeroSceneRoot, {\n Parallax,\n Vignette,\n Blur,\n Pattern,\n DarkOverlay,\n Content,\n})\n","'use client'\n\nimport { createContext, useContext } from 'react'\nimport type { HeroSceneContextValue } from './types'\n\nexport const HeroSceneContext = createContext<HeroSceneContextValue | null>(null)\n\nexport function useHeroScene(): HeroSceneContextValue {\n const ctx = useContext(HeroSceneContext)\n if (!ctx) {\n throw new Error(\n 'HeroScene compound components (Parallax, Vignette, Blur, Pattern, DarkOverlay, Content) ' +\n 'must be rendered inside a <HeroScene> parent.',\n )\n }\n return ctx\n}\n","import type { VignetteProps, BlurProps } from './types'\n\nconst DEFAULT_STOPS: [number, number][] = [\n [0, 0],\n [15, 0.08],\n [30, 0.15],\n [45, 0.25],\n [60, 0.35],\n [75, 0.45],\n [88, 0.55],\n [100, 0.6],\n]\n\nexport function buildVignetteGradient(\n color: string,\n config: Pick<VignetteProps, 'shape' | 'centerX' | 'centerY' | 'stops'>,\n): string {\n const shape = config.shape ?? 'circle'\n const cx = config.centerX ?? 50\n const cy = config.centerY ?? 100\n const stops = config.stops ?? DEFAULT_STOPS\n\n const gradientStops = stops\n .map(([pos, opacity]) =>\n opacity === 0\n ? `transparent ${pos}%`\n : `color-mix(in srgb, rgb(${color}) ${Math.round(opacity * 100)}%, transparent) ${pos}%`,\n )\n .join(', ')\n\n return `radial-gradient(${shape} at ${cx}% ${cy}%, ${gradientStops})`\n}\n\nexport function buildBlurMask(\n config: Pick<BlurProps, 'centerX' | 'centerY' | 'innerRadius' | 'outerRadius'>,\n): string {\n const cx = config.centerX ?? 50\n const cy = config.centerY ?? 65\n const inner = config.innerRadius ?? 15\n const outer = config.outerRadius ?? 55\n\n return `radial-gradient(circle at ${cx}% ${cy}%, transparent ${inner}%, black ${outer}%)`\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport type { RefObject } from 'react'\n\n/**\n * Returns true when the referenced element is at least partially visible\n * in the viewport, using IntersectionObserver. SSR-safe — defaults to true\n * so effects run immediately on first paint before the observer fires.\n */\nexport function useInViewport(ref: RefObject<HTMLElement | null>): boolean {\n const [inViewport, setInViewport] = useState(true)\n\n useEffect(() => {\n const el = ref.current\n if (!el) return\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n setInViewport(entry.isIntersecting)\n },\n { threshold: 0 },\n )\n\n observer.observe(el)\n return () => observer.disconnect()\n }, [ref])\n\n return inViewport\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\n\n/**\n * Returns true when the user has enabled \"prefers-reduced-motion: reduce\"\n * in their OS or browser settings. SSR-safe — defaults to false.\n */\nexport function useReducedMotion(): boolean {\n const [reduced, setReduced] = useState(false)\n\n useEffect(() => {\n const mql = globalThis.matchMedia('(prefers-reduced-motion: reduce)')\n setReduced(mql.matches)\n\n function onChange(e: MediaQueryListEvent) {\n setReduced(e.matches)\n }\n\n mql.addEventListener('change', onChange)\n return () => mql.removeEventListener('change', onChange)\n }, [])\n\n return reduced\n}\n"],"mappings":"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","names":["useEffect","useState","useEffect","useState","useState","useEffect"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@okinoxis/hero-scene",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Cinematic hero sections for Next.js — compound component API with image rotation, parallax, vignette overlays, blur masks, and dot patterns.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist"],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"dev": "tsup --watch",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"publint": "publint",
|
|
27
|
+
"size": "size-limit",
|
|
28
|
+
"prepublishOnly": "npm run build && npm run publint && npm run size"
|
|
29
|
+
},
|
|
30
|
+
"size-limit": [
|
|
31
|
+
{
|
|
32
|
+
"path": "./dist/index.js",
|
|
33
|
+
"limit": "10 KB"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
38
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
39
|
+
"next": ">=14.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@size-limit/preset-small-lib": "^11.2.0",
|
|
43
|
+
"@types/react": "^19.2.14",
|
|
44
|
+
"@types/react-dom": "^19.2.3",
|
|
45
|
+
"next": "^16.1.6",
|
|
46
|
+
"publint": "^0.3.12",
|
|
47
|
+
"react": "^19.2.4",
|
|
48
|
+
"react-dom": "^19.2.4",
|
|
49
|
+
"size-limit": "^11.2.0",
|
|
50
|
+
"tsup": "^8.5.1",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"hero",
|
|
55
|
+
"parallax",
|
|
56
|
+
"vignette",
|
|
57
|
+
"nextjs",
|
|
58
|
+
"react",
|
|
59
|
+
"cinematic",
|
|
60
|
+
"compound-component"
|
|
61
|
+
],
|
|
62
|
+
"license": "MIT"
|
|
63
|
+
}
|