apex-ui-kit 2.9.0 → 2.11.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.
@@ -0,0 +1,257 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { Color, Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, TextureLoader, Vector2, WebGLRenderer } from "three";
3
+
4
+ /*
5
+ * Sketch Reveal: one picture turns into a glowing line sketch, then a ragged,
6
+ * pixelated circle burns outward from the centre and reveals a second
7
+ * picture, with sparks along the burning edge. Drawn in one WebGL shader
8
+ * (edge detection + noise). Drive it with the page scroll (the section pins
9
+ * while it plays), with hover, on a loop, or with your own progress value.
10
+ *
11
+ * Props: from, to (image urls), mode ("scroll" | "hover" | "auto" |
12
+ * "manual"), progress (0..1 for manual), scrollLength (extra scroll in vh
13
+ * for scroll mode), stickyTop (px, e.g. your header height), edgeColor,
14
+ * sparkle, center ([x, y] 0..1), duration (seconds per auto loop),
15
+ * height, className, children (shown on top).
16
+ */
17
+
18
+ const photo = (id) => `https://images.unsplash.com/${id}?w=1800&q=80&auto=format&fit=crop`;
19
+
20
+ const VERT = `varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }`;
21
+
22
+ const FRAG = `
23
+ precision highp float;
24
+ varying vec2 vUv;
25
+ uniform sampler2D uFrom; uniform sampler2D uTo; uniform vec2 uCoverFrom; uniform vec2 uCoverTo;
26
+ uniform vec2 uRes; uniform float uProgress; uniform float uTime; uniform vec2 uCenter;
27
+ uniform vec3 uEdge; uniform float uSparkle; uniform float uReady;
28
+
29
+ float h2(vec2 p){ return fract(sin(dot(p, vec2(41.31, 289.97))) * 23758.5453); }
30
+ float n2(vec2 p){
31
+ vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f);
32
+ return mix(mix(h2(i), h2(i + vec2(1, 0)), f.x), mix(h2(i + vec2(0, 1)), h2(i + vec2(1, 1)), f.x), f.y);
33
+ }
34
+ float fbm(vec2 p){ float v = 0.0; float a = 0.55; for (int i = 0; i < 5; i++) { v += a * n2(p); p = p * 2.07 + 3.1; a *= 0.5; } return v; }
35
+ float luma(vec3 c){ return dot(c, vec3(0.299, 0.587, 0.114)); }
36
+
37
+ vec2 coverUv(vec2 uv, vec2 k){ return (uv - 0.5) * k + 0.5; }
38
+
39
+ // Edge strength from a 3x3 Sobel pass over the picture's brightness.
40
+ float edges(vec2 uv){
41
+ vec2 t = 1.0 / uRes;
42
+ float tl = luma(texture2D(uFrom, uv + t * vec2(-1.0, 1.0)).rgb);
43
+ float tc = luma(texture2D(uFrom, uv + t * vec2( 0.0, 1.0)).rgb);
44
+ float tr = luma(texture2D(uFrom, uv + t * vec2( 1.0, 1.0)).rgb);
45
+ float ml = luma(texture2D(uFrom, uv + t * vec2(-1.0, 0.0)).rgb);
46
+ float mr = luma(texture2D(uFrom, uv + t * vec2( 1.0, 0.0)).rgb);
47
+ float bl = luma(texture2D(uFrom, uv + t * vec2(-1.0, -1.0)).rgb);
48
+ float bc = luma(texture2D(uFrom, uv + t * vec2( 0.0, -1.0)).rgb);
49
+ float br = luma(texture2D(uFrom, uv + t * vec2( 1.0, -1.0)).rgb);
50
+ float gx = -tl - 2.0 * ml - bl + tr + 2.0 * mr + br;
51
+ float gy = -bl - 2.0 * bc - br + tl + 2.0 * tc + tr;
52
+ // Keep the strong lines only, so busy textures (leaves, water) stay calm.
53
+ return smoothstep(0.1, 0.62, length(vec2(gx, gy)));
54
+ }
55
+
56
+ void main(){
57
+ vec2 uvA = coverUv(vUv, uCoverFrom);
58
+ vec2 uvB = coverUv(vUv, uCoverTo);
59
+ vec3 a = texture2D(uFrom, uvA).rgb;
60
+ vec3 b = texture2D(uTo, uvB).rgb;
61
+ float p = uProgress;
62
+
63
+ // Stage 1: the first picture drains to a dark line sketch.
64
+ float sketchK = smoothstep(0.0, 0.42, p);
65
+ float e = edges(uvA);
66
+ vec3 sketch = vec3(0.025) + uEdge * e * 0.92 + vec3(luma(a)) * 0.07;
67
+ vec3 col = mix(a, sketch, sketchK);
68
+
69
+ // Stage 2: a ragged, pixelated circle burns outward and shows the second picture.
70
+ float asp = uRes.x / uRes.y;
71
+ vec2 d = vUv - uCenter; d.x *= asp;
72
+ float r = length(d);
73
+ float maxR = length(vec2(max(uCenter.x, 1.0 - uCenter.x) * asp, max(uCenter.y, 1.0 - uCenter.y)));
74
+ vec2 cell = floor(gl_FragCoord.xy / 7.0);
75
+ float ragged = (fbm(cell * 0.09) - 0.5) * 0.22 + (fbm(vec2(atan(d.y, d.x) * 3.0, 1.7)) - 0.5) * 0.18;
76
+ float front = mix(-0.15, 1.22, smoothstep(0.3, 1.0, p)) * maxR;
77
+ float dist = r + ragged * maxR;
78
+ float revealed = smoothstep(front + 0.006, front - 0.006, dist);
79
+ col = mix(col, b, revealed);
80
+
81
+ // Burning edge: a glowing band with flickering sparks just outside the front.
82
+ float band = smoothstep(0.075 * maxR, 0.0, abs(dist - front)) * step(0.3, p) * (1.0 - smoothstep(0.96, 1.0, p));
83
+ float flick = step(0.62, h2(cell + floor(uTime * 14.0)));
84
+ col += uEdge * band * (0.55 + flick * 1.2 * uSparkle);
85
+ col += uEdge * smoothstep(0.22 * maxR, 0.0, abs(dist - front)) * 0.18 * step(0.3, p) * (1.0 - smoothstep(0.96, 1.0, p));
86
+
87
+ col = mix(vec3(0.03), col, uReady);
88
+ gl_FragColor = vec4(col, 1.0);
89
+ }`;
90
+
91
+ const SketchReveal = ({
92
+ from = photo("photo-1464822759023-fed622ff2c3b"),
93
+ to = photo("photo-1506905925346-21bda4d32df4"),
94
+ mode = "scroll",
95
+ progress = 0,
96
+ scrollLength = 150,
97
+ stickyTop = 0,
98
+ edgeColor = "#fff4d6",
99
+ sparkle = 1,
100
+ center = [0.5, 0.5],
101
+ duration = 6,
102
+ height = "100%",
103
+ className = "",
104
+ children,
105
+ }) => {
106
+ const outer = useRef(null);
107
+ const stage = useRef(null);
108
+ const live = useRef({});
109
+ live.current = { mode, progress, edgeColor, sparkle, center, duration, stickyTop };
110
+
111
+ useEffect(() => {
112
+ const el = stage.current;
113
+ const renderer = new WebGLRenderer({ antialias: false, alpha: false, powerPreference: "high-performance" });
114
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
115
+ renderer.setPixelRatio(dpr);
116
+ Object.assign(renderer.domElement.style, { position: "absolute", inset: "0", width: "100%", height: "100%", display: "block" });
117
+ el.prepend(renderer.domElement);
118
+ const scene = new Scene();
119
+ const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
120
+ const u = {
121
+ uFrom: { value: null },
122
+ uTo: { value: null },
123
+ uCoverFrom: { value: new Vector2(1, 1) },
124
+ uCoverTo: { value: new Vector2(1, 1) },
125
+ uRes: { value: new Vector2(1, 1) },
126
+ uProgress: { value: 0 },
127
+ uTime: { value: 0 },
128
+ uCenter: { value: new Vector2(0.5, 0.5) },
129
+ uEdge: { value: new Color() },
130
+ uSparkle: { value: 1 },
131
+ uReady: { value: 0 },
132
+ };
133
+ const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms: u, depthTest: false, depthWrite: false });
134
+ const geometry = new PlaneGeometry(2, 2);
135
+ scene.add(new Mesh(geometry, material));
136
+
137
+ const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
138
+ const view = { w: 1, h: 1, aspects: [1, 1] };
139
+ const s = { p: 0, hover: false, loaded: 0, readyAt: 0 };
140
+ let raf = 0;
141
+ let last = 0;
142
+ let time = 0;
143
+ let visible = true;
144
+
145
+ const cover = (ia) => {
146
+ const a = view.w / view.h;
147
+ return ia > a ? [a / ia, 1] : [1, ia / a];
148
+ };
149
+ const recover = () => {
150
+ u.uCoverFrom.value.set(...cover(view.aspects[0]));
151
+ u.uCoverTo.value.set(...cover(view.aspects[1]));
152
+ };
153
+ const loader = new TextureLoader().setCrossOrigin("anonymous");
154
+ [from, to].forEach((src, i) =>
155
+ loader.load(src, (t) => {
156
+ (i ? u.uTo : u.uFrom).value = t;
157
+ view.aspects[i] = t.image.width / t.image.height;
158
+ recover();
159
+ if (++s.loaded === 2) s.readyAt = performance.now();
160
+ start();
161
+ }),
162
+ );
163
+
164
+ const fit = () => {
165
+ const { width, height: hh } = el.getBoundingClientRect();
166
+ if (!width || !hh) return;
167
+ view.w = width;
168
+ view.h = hh;
169
+ renderer.setSize(width, hh, false);
170
+ u.uRes.value.set(width * dpr, hh * dpr);
171
+ recover();
172
+ start();
173
+ };
174
+
175
+ // Where the section is in its pinned scroll run, 0 to 1.
176
+ const scrollProgress = () => {
177
+ const box = outer.current.getBoundingClientRect();
178
+ const run = box.height - (window.innerHeight - live.current.stickyTop);
179
+ return run > 0 ? Math.min(1, Math.max(0, (live.current.stickyTop - box.top) / run)) : 0;
180
+ };
181
+
182
+ const frame = (now) => {
183
+ raf = 0;
184
+ const o = live.current;
185
+ const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
186
+ last = now;
187
+ time += dt;
188
+ let target;
189
+ if (o.mode === "scroll") target = scrollProgress();
190
+ else if (o.mode === "hover") target = s.hover ? 1 : 0;
191
+ else if (o.mode === "auto") target = reduced ? 1 : 0.5 - 0.5 * Math.cos((time / Math.max(1, o.duration)) * Math.PI * 2);
192
+ else target = Math.min(1, Math.max(0, o.progress));
193
+ // Ease toward the target so scroll and hover feel smooth.
194
+ const k = o.mode === "auto" ? 1 : 1 - Math.exp(-dt / (o.mode === "hover" ? 0.45 : 0.12));
195
+ s.p += (target - s.p) * k;
196
+ u.uProgress.value = s.p;
197
+ u.uTime.value = time;
198
+ u.uCenter.value.set(o.center[0], 1 - o.center[1]);
199
+ u.uEdge.value.set(o.edgeColor).convertLinearToSRGB();
200
+ u.uSparkle.value = o.sparkle;
201
+ u.uReady.value = s.readyAt ? Math.min(1, (now - s.readyAt) / 500) : 0;
202
+ renderer.render(scene, camera);
203
+ if (visible) raf = requestAnimationFrame(frame);
204
+ else last = 0;
205
+ };
206
+ function start() {
207
+ if (!raf && visible) raf = requestAnimationFrame(frame);
208
+ }
209
+
210
+ const onEnter = () => (s.hover = true);
211
+ const onLeave = () => (s.hover = false);
212
+ el.addEventListener("pointerenter", onEnter);
213
+ el.addEventListener("pointerleave", onLeave);
214
+ const ro = new ResizeObserver(fit);
215
+ ro.observe(el);
216
+ const io = new IntersectionObserver(([entry]) => {
217
+ visible = entry.isIntersecting;
218
+ if (visible) start();
219
+ });
220
+ io.observe(el);
221
+ fit();
222
+
223
+ return () => {
224
+ cancelAnimationFrame(raf);
225
+ ro.disconnect();
226
+ io.disconnect();
227
+ el.removeEventListener("pointerenter", onEnter);
228
+ el.removeEventListener("pointerleave", onLeave);
229
+ u.uFrom.value?.dispose();
230
+ u.uTo.value?.dispose();
231
+ geometry.dispose();
232
+ material.dispose();
233
+ renderer.dispose();
234
+ renderer.forceContextLoss();
235
+ renderer.domElement.remove();
236
+ };
237
+ }, [from, to]);
238
+
239
+ const inner = (
240
+ <div ref={stage} className={`relative w-full overflow-hidden bg-[#08080a] ${mode === "scroll" ? "" : className}`} style={mode === "scroll" ? { position: "sticky", top: stickyTop, height: `calc(100vh - ${stickyTop}px)` } : { height }}>
241
+ {children && <div className="pointer-events-none relative z-10 h-full w-full [&_a]:pointer-events-auto [&_button]:pointer-events-auto">{children}</div>}
242
+ </div>
243
+ );
244
+
245
+ // Scroll mode: a tall section whose inner stage pins while the reveal plays.
246
+ return mode === "scroll" ? (
247
+ <div ref={outer} className={`relative w-full ${className}`} style={{ height: `calc(100vh - ${stickyTop}px + ${scrollLength}vh)` }}>
248
+ {inner}
249
+ </div>
250
+ ) : (
251
+ <div ref={outer} className="h-full w-full">
252
+ {inner}
253
+ </div>
254
+ );
255
+ };
256
+
257
+ export default SketchReveal;