apex-ui-kit 2.9.0 → 2.10.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 CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/apex-ui-kit?color=a3e635)](https://www.npmjs.com/package/apex-ui-kit)
5
5
  [![license](https://img.shields.io/npm/l/apex-ui-kit?color=a3e635)](./Licence.md)
6
6
 
7
- **116 free, animated UI components for React.** Pick one, add it with a single command, and the source lands in your project so you can read it and change anything.
7
+ **118 free, animated UI components for React.** Pick one, add it with a single command, and the source lands in your project so you can read it and change anything.
8
8
 
9
9
  Built with React, Tailwind CSS and Framer Motion, plus GSAP, Canvas and WebGL where a component needs them.
10
10
 
@@ -15,7 +15,7 @@ Built with React, Tailwind CSS and Framer Motion, plus GSAP, Canvas and WebGL wh
15
15
 
16
16
  ## Why ApexUI
17
17
 
18
- - **116 components** across backgrounds, cards, text effects, navigation, buttons, interactive UI, AI interfaces, loaders and maps.
18
+ - **118 components** across backgrounds, cards, text effects, navigation, buttons, interactive UI, AI interfaces, loaders and maps.
19
19
  - **You own the code.** The CLI copies plain `.jsx` files into your project, no runtime package to keep in sync.
20
20
  - **Every docs page has** a live preview, a playground to tweak props, a Desktop / Tablet / Mobile preview, the full props table and a ready prompt you can paste into ChatGPT or Claude.
21
21
  - **Smooth on real devices.** Animations stick to transform and opacity, respect reduced motion, and work with mouse, touch and keyboard.
@@ -196,7 +196,7 @@ Every component's props, variants and copy-ready code are on its docs page.
196
196
  | [Magnetic Button](https://apex-ui.in/components/magnetic-button) | `magnetic-button` |
197
197
  | [Star Border](https://apex-ui.in/components/star-border) | `star-border` |
198
198
 
199
- ### Interactive (31)
199
+ ### Interactive (33)
200
200
 
201
201
  | Component | CLI name |
202
202
  | --- | --- |
@@ -215,6 +215,7 @@ Every component's props, variants and copy-ready code are on its docs page.
215
215
  | [Elastic Slider](https://apex-ui.in/components/elastic-slider) | `elastic-slider` |
216
216
  | [FAQ Accordion](https://apex-ui.in/components/faq-accordion) | `faq-accordion` |
217
217
  | [Flip Book](https://apex-ui.in/components/flip-book) | `flip-book` |
218
+ | [Fluid Cursor](https://apex-ui.in/components/fluid-cursor) | `fluid-cursor` |
218
219
  | [Glass Lens](https://apex-ui.in/components/glass-lens) | `glass-lens` |
219
220
  | [Image Compare](https://apex-ui.in/components/image-compare) | `image-compare` |
220
221
  | [Infinite Canvas](https://apex-ui.in/components/infinite-canvas) | `infinite-canvas` |
@@ -227,6 +228,7 @@ Every component's props, variants and copy-ready code are on its docs page.
227
228
  | [Particle Morph](https://apex-ui.in/components/particle-morph) | `particle-morph` |
228
229
  | [Pointer Follower](https://apex-ui.in/components/pointer-follower) | `pointer-follower` |
229
230
  | [Receipt Printer](https://apex-ui.in/components/receipt-printer) | `receipt-printer` |
231
+ | [Sketch Reveal](https://apex-ui.in/components/sketch-reveal) | `sketch-reveal` |
230
232
  | [Stepper](https://apex-ui.in/components/stepper) | `stepper` |
231
233
  | [Toast Stack](https://apex-ui.in/components/toast-stack) | `toast-stack` |
232
234
  | [ToolTip](https://apex-ui.in/components/tool-tip) | `tooltip` |
@@ -260,8 +262,9 @@ Every component's props, variants and copy-ready code are on its docs page.
260
262
  | [Dot Globe](https://apex-ui.in/components/dot-globe) | `dot-globe` |
261
263
  | [World Map](https://apex-ui.in/components/world-map) | `world-map` |
262
264
 
263
- ## What's new in 2.9
265
+ ## What's new in 2.10
264
266
 
267
+ - New in 2.10: Fluid Cursor (a real-time WebGL fluid sim that trails the cursor) and Sketch Reveal (a photo turns into a glowing sketch and burns away into the next one as you scroll).
265
268
  - New in 2.9: Liquid Glass (iOS style WebGL glass), 10 live page templates on the site, Arc Gallery auto scroll, Scroll Stack on the page scroll, Sunrise trees and birds.
266
269
  - New in 2.8: Glass Lens (WebGL liquid glass), Dither Waves (WebGL 1-bit background), Infinite Canvas, Image Accordion, Orbit Icons, Command Palette, Crossed Marquee (tech-stack bands with simple-icons) and Theme Switcher (8 themes with a circle reveal).
267
270
  - Toast Stack and Radial Menu no longer blink in production builds; Radial Menu, 3D Text Ring, Animated Tabs and Gooey Nav now fit 320px phones.
@@ -0,0 +1,418 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { Color, HalfFloatType, LinearFilter, Mesh, OrthographicCamera, PlaneGeometry, RGBAFormat, Scene, ShaderMaterial, Vector2, WebGLRenderer, WebGLRenderTarget } from "three";
3
+
4
+ /*
5
+ * Fluid Cursor: a real-time fluid simulation that trails the pointer, drawn
6
+ * in WebGL. Moving the cursor pushes coloured ink through the fluid; it
7
+ * swirls (vorticity), spreads (pressure) and slowly fades. A click blows a
8
+ * burst. It sits over its parent as a transparent layer and never blocks
9
+ * clicks, so content underneath stays usable.
10
+ *
11
+ * Props: colors (cycled; a rainbow when empty), simResolution, dyeResolution,
12
+ * dissipation (how fast the ink fades), velocityDissipation, curl (swirl),
13
+ * splatRadius, splatForce, idle (soft splashes when nobody moves),
14
+ * className, children.
15
+ */
16
+
17
+ const BASE_VERT = `
18
+ uniform vec2 texelSize;
19
+ varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB;
20
+ void main(){
21
+ vUv = uv;
22
+ vL = vUv - vec2(texelSize.x, 0.0);
23
+ vR = vUv + vec2(texelSize.x, 0.0);
24
+ vT = vUv + vec2(0.0, texelSize.y);
25
+ vB = vUv - vec2(0.0, texelSize.y);
26
+ gl_Position = vec4(position.xy, 0.0, 1.0);
27
+ }`;
28
+
29
+ const HEAD = `precision highp float;
30
+ varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB;
31
+ `;
32
+
33
+ const SPLAT = `${HEAD}
34
+ uniform sampler2D uTarget; uniform float aspect; uniform vec3 color; uniform vec2 point; uniform float radius;
35
+ void main(){
36
+ vec2 p = vUv - point;
37
+ p.x *= aspect;
38
+ vec3 splat = exp(-dot(p, p) / radius) * color;
39
+ gl_FragColor = vec4(texture2D(uTarget, vUv).xyz + splat, 1.0);
40
+ }`;
41
+
42
+ const ADVECT = `${HEAD}
43
+ uniform sampler2D uVelocity; uniform sampler2D uSource; uniform vec2 velTexel; uniform float dt; uniform float dissipation;
44
+ void main(){
45
+ vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * velTexel;
46
+ gl_FragColor = texture2D(uSource, coord) / (1.0 + dissipation * dt);
47
+ }`;
48
+
49
+ const DIVERGENCE = `${HEAD}
50
+ uniform sampler2D uVelocity;
51
+ void main(){
52
+ float L = texture2D(uVelocity, vL).x;
53
+ float R = texture2D(uVelocity, vR).x;
54
+ float T = texture2D(uVelocity, vT).y;
55
+ float B = texture2D(uVelocity, vB).y;
56
+ vec2 C = texture2D(uVelocity, vUv).xy;
57
+ // Walls: reflect velocity at the edges.
58
+ if (vL.x < 0.0) L = -C.x;
59
+ if (vR.x > 1.0) R = -C.x;
60
+ if (vT.y > 1.0) T = -C.y;
61
+ if (vB.y < 0.0) B = -C.y;
62
+ gl_FragColor = vec4(0.5 * (R - L + T - B), 0.0, 0.0, 1.0);
63
+ }`;
64
+
65
+ const CURL = `${HEAD}
66
+ uniform sampler2D uVelocity;
67
+ void main(){
68
+ float L = texture2D(uVelocity, vL).y;
69
+ float R = texture2D(uVelocity, vR).y;
70
+ float T = texture2D(uVelocity, vT).x;
71
+ float B = texture2D(uVelocity, vB).x;
72
+ gl_FragColor = vec4(0.5 * (R - L - T + B), 0.0, 0.0, 1.0);
73
+ }`;
74
+
75
+ const VORTICITY = `${HEAD}
76
+ uniform sampler2D uVelocity; uniform sampler2D uCurl; uniform float curl; uniform float dt;
77
+ void main(){
78
+ float L = texture2D(uCurl, vL).x;
79
+ float R = texture2D(uCurl, vR).x;
80
+ float T = texture2D(uCurl, vT).x;
81
+ float B = texture2D(uCurl, vB).x;
82
+ float C = texture2D(uCurl, vUv).x;
83
+ vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));
84
+ force /= length(force) + 0.0001;
85
+ force *= curl * C;
86
+ force.y *= -1.0;
87
+ vec2 vel = texture2D(uVelocity, vUv).xy + force * dt;
88
+ gl_FragColor = vec4(clamp(vel, -1000.0, 1000.0), 0.0, 1.0);
89
+ }`;
90
+
91
+ const SCALE = `${HEAD}
92
+ uniform sampler2D uTexture; uniform float value;
93
+ void main(){ gl_FragColor = value * texture2D(uTexture, vUv); }`;
94
+
95
+ const PRESSURE = `${HEAD}
96
+ uniform sampler2D uPressure; uniform sampler2D uDivergence;
97
+ void main(){
98
+ float L = texture2D(uPressure, vL).x;
99
+ float R = texture2D(uPressure, vR).x;
100
+ float T = texture2D(uPressure, vT).x;
101
+ float B = texture2D(uPressure, vB).x;
102
+ float div = texture2D(uDivergence, vUv).x;
103
+ gl_FragColor = vec4((L + R + B + T - div) * 0.25, 0.0, 0.0, 1.0);
104
+ }`;
105
+
106
+ const GRADIENT = `${HEAD}
107
+ uniform sampler2D uPressure; uniform sampler2D uVelocity;
108
+ void main(){
109
+ float L = texture2D(uPressure, vL).x;
110
+ float R = texture2D(uPressure, vR).x;
111
+ float T = texture2D(uPressure, vT).x;
112
+ float B = texture2D(uPressure, vB).x;
113
+ vec2 vel = texture2D(uVelocity, vUv).xy - vec2(R - L, T - B);
114
+ gl_FragColor = vec4(vel, 0.0, 1.0);
115
+ }`;
116
+
117
+ // Shaded ink: a little light on the slopes of the dye gives it body.
118
+ const DISPLAY = `${HEAD}
119
+ uniform sampler2D uTexture; uniform vec2 dyeTexel;
120
+ void main(){
121
+ vec3 c = texture2D(uTexture, vUv).rgb;
122
+ vec3 lc = texture2D(uTexture, vUv - vec2(dyeTexel.x, 0.0)).rgb;
123
+ vec3 rc = texture2D(uTexture, vUv + vec2(dyeTexel.x, 0.0)).rgb;
124
+ vec3 tc = texture2D(uTexture, vUv + vec2(0.0, dyeTexel.y)).rgb;
125
+ vec3 bc = texture2D(uTexture, vUv - vec2(0.0, dyeTexel.y)).rgb;
126
+ float dx = length(rc) - length(lc);
127
+ float dy = length(tc) - length(bc);
128
+ vec3 n = normalize(vec3(dx, dy, length(dyeTexel)));
129
+ float diffuse = clamp(dot(n, vec3(0.0, 0.0, 1.0)) + 0.7, 0.7, 1.0);
130
+ c *= diffuse;
131
+ float a = clamp(max(c.r, max(c.g, c.b)), 0.0, 1.0);
132
+ gl_FragColor = vec4(c, a);
133
+ }`;
134
+
135
+ const hsvToRgb = (h, s, v) => {
136
+ const i = Math.floor(h * 6);
137
+ const f = h * 6 - i;
138
+ const p = v * (1 - s);
139
+ const q = v * (1 - f * s);
140
+ const t = v * (1 - (1 - f) * s);
141
+ return [
142
+ [v, t, p],
143
+ [q, v, p],
144
+ [p, v, t],
145
+ [p, q, v],
146
+ [t, p, v],
147
+ [v, p, q],
148
+ ][i % 6];
149
+ };
150
+
151
+ const FluidCursor = ({ colors = [], simResolution = 128, dyeResolution = 1024, dissipation = 1.6, velocityDissipation = 1.2, curl = 18, splatRadius = 0.22, splatForce = 6000, idle = true, className = "", children }) => {
152
+ const wrap = useRef(null);
153
+ const live = useRef({});
154
+ live.current = { colors, dissipation, velocityDissipation, curl, splatRadius, splatForce, idle };
155
+
156
+ useEffect(() => {
157
+ const el = wrap.current;
158
+ const renderer = new WebGLRenderer({ alpha: true, premultipliedAlpha: true, antialias: false, powerPreference: "high-performance" });
159
+ if (!renderer.capabilities.isWebGL2 && !renderer.extensions.get("OES_texture_half_float")) {
160
+ renderer.dispose();
161
+ return undefined;
162
+ }
163
+ renderer.setPixelRatio(1);
164
+ renderer.autoClear = false;
165
+ Object.assign(renderer.domElement.style, { position: "absolute", inset: "0", width: "100%", height: "100%", display: "block", pointerEvents: "none" });
166
+ el.prepend(renderer.domElement);
167
+
168
+ const scene = new Scene();
169
+ const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
170
+ const quad = new Mesh(new PlaneGeometry(2, 2));
171
+ scene.add(quad);
172
+ const texel = { value: new Vector2() };
173
+ const mat = (frag, uniforms) => new ShaderMaterial({ vertexShader: BASE_VERT, fragmentShader: frag, uniforms: { texelSize: texel, ...uniforms }, depthTest: false, depthWrite: false });
174
+ const m = {
175
+ splat: mat(SPLAT, { uTarget: { value: null }, aspect: { value: 1 }, color: { value: new Color() }, point: { value: new Vector2() }, radius: { value: 0.01 } }),
176
+ advect: mat(ADVECT, { uVelocity: { value: null }, uSource: { value: null }, velTexel: { value: new Vector2() }, dt: { value: 0.016 }, dissipation: { value: 1 } }),
177
+ divergence: mat(DIVERGENCE, { uVelocity: { value: null } }),
178
+ curl: mat(CURL, { uVelocity: { value: null } }),
179
+ vorticity: mat(VORTICITY, { uVelocity: { value: null }, uCurl: { value: null }, curl: { value: 18 }, dt: { value: 0.016 } }),
180
+ scale: mat(SCALE, { uTexture: { value: null }, value: { value: 0.8 } }),
181
+ pressure: mat(PRESSURE, { uPressure: { value: null }, uDivergence: { value: null } }),
182
+ gradient: mat(GRADIENT, { uPressure: { value: null }, uVelocity: { value: null } }),
183
+ display: mat(DISPLAY, { uTexture: { value: null }, dyeTexel: { value: new Vector2() } }),
184
+ };
185
+ m.display.transparent = true;
186
+
187
+ const target = (w, h) => new WebGLRenderTarget(w, h, { type: HalfFloatType, format: RGBAFormat, minFilter: LinearFilter, magFilter: LinearFilter, depthBuffer: false, stencilBuffer: false });
188
+ const double = (w, h) => {
189
+ const d = { read: target(w, h), write: target(w, h), w, h };
190
+ d.swap = () => ([d.read, d.write] = [d.write, d.read]);
191
+ d.dispose = () => {
192
+ d.read.dispose();
193
+ d.write.dispose();
194
+ };
195
+ return d;
196
+ };
197
+ let fbo = null;
198
+ const sizeFor = (res, w, h) => {
199
+ const a = w / h;
200
+ const min = Math.round(res);
201
+ const max = Math.round(res * Math.max(a, 1 / a));
202
+ return a > 1 ? [max, min] : [min, max];
203
+ };
204
+ const build = () => {
205
+ const { width, height } = el.getBoundingClientRect();
206
+ if (!width || !height) return false;
207
+ renderer.setSize(width, height, false);
208
+ fbo?.all.forEach((t) => t.dispose());
209
+ const [sw, sh] = sizeFor(simResolution, width, height);
210
+ const [dw, dh] = sizeFor(Math.min(dyeResolution, Math.max(width, height)), width, height);
211
+ const velocity = double(sw, sh);
212
+ const dye = double(dw, dh);
213
+ const pressure = double(sw, sh);
214
+ const divergence = target(sw, sh);
215
+ const curlT = target(sw, sh);
216
+ fbo = { velocity, dye, pressure, divergence, curl: curlT, all: [velocity, dye, pressure, divergence, curlT], aspect: width / height };
217
+ return true;
218
+ };
219
+
220
+ const pass = (material, out, w, h) => {
221
+ texel.value.set(1 / w, 1 / h);
222
+ quad.material = material;
223
+ renderer.setRenderTarget(out);
224
+ renderer.render(scene, camera);
225
+ };
226
+
227
+ const splat = (x, y, dx, dy, color) => {
228
+ const o = live.current;
229
+ const f = fbo;
230
+ m.splat.uniforms.aspect.value = f.aspect;
231
+ m.splat.uniforms.point.value.set(x, y);
232
+ m.splat.uniforms.radius.value = (o.splatRadius / 100) * (f.aspect > 1 ? f.aspect : 1);
233
+ m.splat.uniforms.uTarget.value = f.velocity.read.texture;
234
+ m.splat.uniforms.color.value.setRGB(dx, dy, 0);
235
+ pass(m.splat, f.velocity.write, f.velocity.w, f.velocity.h);
236
+ f.velocity.swap();
237
+ m.splat.uniforms.uTarget.value = f.dye.read.texture;
238
+ m.splat.uniforms.color.value.setRGB(color[0], color[1], color[2]);
239
+ pass(m.splat, f.dye.write, f.dye.w, f.dye.h);
240
+ f.dye.swap();
241
+ };
242
+
243
+ let hue = Math.random();
244
+ let pick = 0;
245
+ const nextColor = (strength = 0.3) => {
246
+ const list = live.current.colors;
247
+ if (list?.length) {
248
+ const c = new Color(list[pick++ % list.length]);
249
+ return [c.r * strength, c.g * strength, c.b * strength];
250
+ }
251
+ hue = (hue + 0.07) % 1;
252
+ return hsvToRgb(hue, 1, 1).map((v) => v * strength);
253
+ };
254
+
255
+ const step = (dt) => {
256
+ const o = live.current;
257
+ const f = fbo;
258
+ const { velocity, dye, pressure } = f;
259
+ // Swirl.
260
+ m.curl.uniforms.uVelocity.value = velocity.read.texture;
261
+ pass(m.curl, f.curl, velocity.w, velocity.h);
262
+ m.vorticity.uniforms.uVelocity.value = velocity.read.texture;
263
+ m.vorticity.uniforms.uCurl.value = f.curl.texture;
264
+ m.vorticity.uniforms.curl.value = o.curl;
265
+ m.vorticity.uniforms.dt.value = dt;
266
+ pass(m.vorticity, velocity.write, velocity.w, velocity.h);
267
+ velocity.swap();
268
+ // Make it incompressible.
269
+ m.divergence.uniforms.uVelocity.value = velocity.read.texture;
270
+ pass(m.divergence, f.divergence, velocity.w, velocity.h);
271
+ m.scale.uniforms.uTexture.value = pressure.read.texture;
272
+ m.scale.uniforms.value.value = 0.8;
273
+ pass(m.scale, pressure.write, pressure.w, pressure.h);
274
+ pressure.swap();
275
+ m.pressure.uniforms.uDivergence.value = f.divergence.texture;
276
+ for (let i = 0; i < 20; i++) {
277
+ m.pressure.uniforms.uPressure.value = pressure.read.texture;
278
+ pass(m.pressure, pressure.write, pressure.w, pressure.h);
279
+ pressure.swap();
280
+ }
281
+ m.gradient.uniforms.uPressure.value = pressure.read.texture;
282
+ m.gradient.uniforms.uVelocity.value = velocity.read.texture;
283
+ pass(m.gradient, velocity.write, velocity.w, velocity.h);
284
+ velocity.swap();
285
+ // Carry velocity and ink along.
286
+ m.advect.uniforms.velTexel.value.set(1 / velocity.w, 1 / velocity.h);
287
+ m.advect.uniforms.dt.value = dt;
288
+ m.advect.uniforms.uVelocity.value = velocity.read.texture;
289
+ m.advect.uniforms.uSource.value = velocity.read.texture;
290
+ m.advect.uniforms.dissipation.value = o.velocityDissipation;
291
+ pass(m.advect, velocity.write, velocity.w, velocity.h);
292
+ velocity.swap();
293
+ m.advect.uniforms.uVelocity.value = velocity.read.texture;
294
+ m.advect.uniforms.uSource.value = dye.read.texture;
295
+ m.advect.uniforms.dissipation.value = o.dissipation;
296
+ pass(m.advect, dye.write, dye.w, dye.h);
297
+ dye.swap();
298
+ };
299
+
300
+ const draw = () => {
301
+ m.display.uniforms.uTexture.value = fbo.dye.read.texture;
302
+ m.display.uniforms.dyeTexel.value.set(1 / fbo.dye.w, 1 / fbo.dye.h);
303
+ texel.value.set(1 / fbo.dye.w, 1 / fbo.dye.h);
304
+ quad.material = m.display;
305
+ renderer.setRenderTarget(null);
306
+ renderer.clear();
307
+ renderer.render(scene, camera);
308
+ };
309
+
310
+ const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
311
+ const pointer = { x: 0, y: 0, dx: 0, dy: 0, moved: false, inside: false, color: nextColor(), lastMove: 0 };
312
+ let raf = 0;
313
+ let last = 0;
314
+ let visible = true;
315
+ let nextIdle = 0;
316
+
317
+ const frame = (now) => {
318
+ raf = 0;
319
+ const o = live.current;
320
+ const dt = last ? Math.min((now - last) / 1000, 0.016) : 0.016;
321
+ last = now;
322
+ if (pointer.moved) {
323
+ pointer.moved = false;
324
+ splat(pointer.x, pointer.y, pointer.dx * o.splatForce, pointer.dy * o.splatForce, pointer.color);
325
+ }
326
+ // Idle: soft splashes from the edges so the space never looks empty.
327
+ if (o.idle && now - pointer.lastMove > 2500 && now > nextIdle) {
328
+ nextIdle = now + 1400 + Math.random() * 1200;
329
+ const x = Math.random();
330
+ const y = Math.random() * 0.6 + 0.2;
331
+ const ang = Math.random() * Math.PI * 2;
332
+ splat(x, y, Math.cos(ang) * 600, Math.sin(ang) * 600, nextColor(0.35));
333
+ }
334
+ step(dt);
335
+ draw();
336
+ if (visible) raf = requestAnimationFrame(frame);
337
+ };
338
+ const start = () => {
339
+ if (!raf && visible && !reduced) {
340
+ last = 0;
341
+ raf = requestAnimationFrame(frame);
342
+ }
343
+ };
344
+
345
+ // Listen on the window so the layer can sit over clickable content.
346
+ const onMove = (e) => {
347
+ const r = el.getBoundingClientRect();
348
+ const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
349
+ if (!inside) {
350
+ pointer.inside = false;
351
+ return;
352
+ }
353
+ const x = (e.clientX - r.left) / r.width;
354
+ const y = 1 - (e.clientY - r.top) / r.height;
355
+ if (!pointer.inside) {
356
+ pointer.inside = true;
357
+ pointer.x = x;
358
+ pointer.y = y;
359
+ return;
360
+ }
361
+ pointer.dx = (x - pointer.x) * (fbo.aspect < 1 ? fbo.aspect : 1);
362
+ pointer.dy = (y - pointer.y) / (fbo.aspect > 1 ? fbo.aspect : 1);
363
+ pointer.x = x;
364
+ pointer.y = y;
365
+ pointer.moved = Math.abs(pointer.dx) + Math.abs(pointer.dy) > 0;
366
+ pointer.lastMove = performance.now();
367
+ if (Math.random() < 0.08) pointer.color = nextColor();
368
+ };
369
+ const onDown = (e) => {
370
+ const r = el.getBoundingClientRect();
371
+ if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) return;
372
+ const x = (e.clientX - r.left) / r.width;
373
+ const y = 1 - (e.clientY - r.top) / r.height;
374
+ const c = nextColor(0.6);
375
+ for (let i = 0; i < 6; i++) {
376
+ const ang = (i / 6) * Math.PI * 2;
377
+ splat(x, y, Math.cos(ang) * 1400, Math.sin(ang) * 1400, c);
378
+ }
379
+ pointer.lastMove = performance.now();
380
+ };
381
+ window.addEventListener("pointermove", onMove, { passive: true });
382
+ window.addEventListener("pointerdown", onDown, { passive: true });
383
+
384
+ const ro = new ResizeObserver(() => {
385
+ if (build()) start();
386
+ });
387
+ ro.observe(el);
388
+ const io = new IntersectionObserver(([entry]) => {
389
+ visible = entry.isIntersecting;
390
+ if (visible) start();
391
+ });
392
+ io.observe(el);
393
+ if (build()) start();
394
+
395
+ return () => {
396
+ cancelAnimationFrame(raf);
397
+ ro.disconnect();
398
+ io.disconnect();
399
+ window.removeEventListener("pointermove", onMove);
400
+ window.removeEventListener("pointerdown", onDown);
401
+ fbo?.all.forEach((t) => t.dispose());
402
+ Object.values(m).forEach((x) => x.dispose());
403
+ quad.geometry.dispose();
404
+ renderer.dispose();
405
+ renderer.forceContextLoss();
406
+ renderer.domElement.remove();
407
+ };
408
+ // Resolutions rebuild the simulation; the rest is read live.
409
+ }, [simResolution, dyeResolution]);
410
+
411
+ return (
412
+ <div ref={wrap} className={`pointer-events-none absolute inset-0 overflow-hidden ${className}`}>
413
+ {children}
414
+ </div>
415
+ );
416
+ };
417
+
418
+ export default FluidCursor;
@@ -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;
package/dist/cli/index.js CHANGED
@@ -140,6 +140,8 @@ const componentMap = {
140
140
  "crossed-marquee": "CrossedMarquee",
141
141
  "theme-switcher": "ThemeSwitcher",
142
142
  "liquid-glass": "LiquidGlass",
143
+ "fluid-cursor": "FluidCursor",
144
+ "sketch-reveal": "SketchReveal",
143
145
  "water-drop-reveal": "MaskView",
144
146
  };
145
147
  // Packages only some components need, installed on top of the shared ones.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "apex-ui-kit",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "license": "MIT",
5
- "description": "116 free, animated UI components for React, styled with Tailwind CSS. Add any of them with one CLI command.",
5
+ "description": "118 free, animated UI components for React, styled with Tailwind CSS. Add any of them with one CLI command.",
6
6
  "homepage": "https://apex-ui.in",
7
7
  "repository": {
8
8
  "type": "git",