apex-ui-kit 2.8.0 → 2.9.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 +6 -4
- package/dist/ApexUI-Kit/templates/ArcGallery/ArcGallery.jsx +29 -11
- package/dist/ApexUI-Kit/templates/CircularCarousel/CircularCarousel.jsx +0 -1
- package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +3 -2
- package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +25 -18
- package/dist/ApexUI-Kit/templates/GlassLens/GlassLens.jsx +0 -1
- package/dist/ApexUI-Kit/templates/LiquidGlass/LiquidGlass.jsx +444 -0
- package/dist/ApexUI-Kit/templates/ScrollStack/ScrollStack.jsx +3 -3
- package/dist/ApexUI-Kit/templates/SunriseBackground/SunriseBackground.jsx +68 -2
- package/dist/cli/index.js +1 -0
- package/package.json +2 -2
package/ReadMe.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/apex-ui-kit)
|
|
5
5
|
[](./Licence.md)
|
|
6
6
|
|
|
7
|
-
**
|
|
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.
|
|
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
|
-
- **
|
|
18
|
+
- **116 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 (
|
|
199
|
+
### Interactive (31)
|
|
200
200
|
|
|
201
201
|
| Component | CLI name |
|
|
202
202
|
| --- | --- |
|
|
@@ -219,6 +219,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
219
219
|
| [Image Compare](https://apex-ui.in/components/image-compare) | `image-compare` |
|
|
220
220
|
| [Infinite Canvas](https://apex-ui.in/components/infinite-canvas) | `infinite-canvas` |
|
|
221
221
|
| [Liquid Blob 3D](https://apex-ui.in/components/liquid-blob-3d) | `liquid-blob-3d` |
|
|
222
|
+
| [Liquid Glass](https://apex-ui.in/components/liquid-glass) | `liquid-glass` |
|
|
222
223
|
| [Magnet Lines](https://apex-ui.in/components/magnet-lines) | `magnet-lines` |
|
|
223
224
|
| [Magnetic Cursor](https://apex-ui.in/components/magnetic-cursor) | `magnetic-cursor` |
|
|
224
225
|
| [Marquee 3D](https://apex-ui.in/components/marquee-3d) | `marquee-3d` |
|
|
@@ -259,8 +260,9 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
259
260
|
| [Dot Globe](https://apex-ui.in/components/dot-globe) | `dot-globe` |
|
|
260
261
|
| [World Map](https://apex-ui.in/components/world-map) | `world-map` |
|
|
261
262
|
|
|
262
|
-
## What's new in 2.
|
|
263
|
+
## What's new in 2.9
|
|
263
264
|
|
|
265
|
+
- 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.
|
|
264
266
|
- 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).
|
|
265
267
|
- 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.
|
|
266
268
|
- 38 new components since 2.6, including WebGL and three.js (Sunrise Background, Horizon Glow, Circular Carousel, Arc Gallery, Galaxy 3D, Warp Tunnel, Wireframe Knot 3D, Marquee 3D, Distortion Image, Liquid Blob 3D, Particle Morph, Retro Grid 3D), Scroll Stack, Particle Text, Edge Blur, Magnetic Button, Radial Menu, the AI voice family (Wave, Bars, Ring, Blob), Gooey Nav (WebGL metaball pill), Coverflow 3D, Card Swap 3D, 3D Text Ring, Elastic Slider, Stepper and Pixel Transition.
|
|
@@ -3,9 +3,10 @@ import { Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, SRGBCol
|
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
* Arc Gallery: an endless row of image cards laid along a gentle arc and
|
|
6
|
-
* drawn in WebGL.
|
|
7
|
-
*
|
|
8
|
-
* the edges fade out, and a title sits under
|
|
6
|
+
* drawn in WebGL. It drifts on its own (pausing on hover); drag, flick or
|
|
7
|
+
* swipe sideways to move it and it eases to a stop on a card. While it
|
|
8
|
+
* moves the cards ripple and bow, the edges fade out, and a title sits under
|
|
9
|
+
* each card. Arrow keys step. Vertical wheel scrolling is left to the page.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
|
|
@@ -54,13 +55,13 @@ void main(){
|
|
|
54
55
|
gl_FragColor = vec4(col, edge * f * uReady);
|
|
55
56
|
}`;
|
|
56
57
|
|
|
57
|
-
const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspectRatio = 0.78, gap = 0.22, cornerRadius = 0.05, wave = 1, pull = 1, fadeEdges = 0.14, ease = 0.08, snap = true, showTitles = true, height = "26rem", onChange, className = "" }) => {
|
|
58
|
+
const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspectRatio = 0.78, gap = 0.22, cornerRadius = 0.05, wave = 1, pull = 1, fadeEdges = 0.14, ease = 0.08, snap = true, autoScroll = 40, pauseOnHover = true, showTitles = true, height = "26rem", onChange, className = "" }) => {
|
|
58
59
|
const wrap = useRef(null);
|
|
59
60
|
const labels = useRef([]);
|
|
60
61
|
const api = useRef(null);
|
|
61
62
|
const [active, setActive] = useState(0);
|
|
62
63
|
const live = useRef({});
|
|
63
|
-
live.current = { bend, wave, pull, fadeEdges, ease, snap, onChange };
|
|
64
|
+
live.current = { bend, wave, pull, fadeEdges, ease, snap, autoScroll, pauseOnHover, onChange };
|
|
64
65
|
const list = items.length ? items : DEFAULT_ITEMS;
|
|
65
66
|
const key = list.map((i) => i.image).join("|");
|
|
66
67
|
|
|
@@ -84,7 +85,6 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
84
85
|
const textures = list.map((item) => {
|
|
85
86
|
const entry = { tex: null, aspect: 1, at: 0 };
|
|
86
87
|
loader.load(item.image, (t) => {
|
|
87
|
-
t.colorSpace = SRGBColorSpace;
|
|
88
88
|
entry.tex = t;
|
|
89
89
|
entry.aspect = t.image.width / t.image.height;
|
|
90
90
|
entry.at = performance.now();
|
|
@@ -116,7 +116,8 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
116
116
|
return { mesh, m };
|
|
117
117
|
};
|
|
118
118
|
|
|
119
|
-
const s = { scroll: 0, target: 0, vel: 0, pull: 0, drag: null, time: 0 };
|
|
119
|
+
const s = { scroll: 0, target: 0, vel: 0, pull: 0, drag: null, time: 0, hover: false, hold: 0 };
|
|
120
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
120
121
|
let cardW = 1;
|
|
121
122
|
let cardH = 1;
|
|
122
123
|
let spacing = 1;
|
|
@@ -172,6 +173,10 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
172
173
|
const o = live.current;
|
|
173
174
|
const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
|
|
174
175
|
last = now;
|
|
176
|
+
// Auto scroll drifts the row along, pausing while hovered, dragged or
|
|
177
|
+
// just after someone moved it by hand.
|
|
178
|
+
const auto = o.autoScroll && !reduced && !s.drag && !(o.pauseOnHover && s.hover) && now > s.hold;
|
|
179
|
+
if (auto) s.target += o.autoScroll * dt;
|
|
175
180
|
const before = s.scroll;
|
|
176
181
|
const follow = 1 - Math.pow(1 - Math.min(1, Math.max(0.01, o.ease)), dt * 60);
|
|
177
182
|
s.scroll += (s.target - s.scroll) * follow;
|
|
@@ -185,7 +190,7 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
185
190
|
const pullGoal = Math.max(-1, Math.min(1, s.vel / (spacing * 7))) * 0.32 * o.pull;
|
|
186
191
|
s.pull += (pullGoal - s.pull) * (1 - Math.exp(-dt / 0.12));
|
|
187
192
|
if (Math.abs(s.pull) < 0.0005 && !pullGoal) s.pull = 0;
|
|
188
|
-
let busy = s.pull !== 0 || s.scroll !== s.target || s.vel !== 0 || !!s.drag;
|
|
193
|
+
let busy = auto || s.pull !== 0 || s.scroll !== s.target || s.vel !== 0 || !!s.drag;
|
|
189
194
|
|
|
190
195
|
const first = Math.floor((s.scroll - view.w / 2) / spacing) - 1;
|
|
191
196
|
slots.forEach((slot, k) => {
|
|
@@ -251,6 +256,7 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
251
256
|
const onDown = (e) => {
|
|
252
257
|
if (e.button !== 0) return;
|
|
253
258
|
s.drag = { x: e.clientX, start: s.target, moved: false, samples: [] };
|
|
259
|
+
wake();
|
|
254
260
|
};
|
|
255
261
|
const onMove = (e) => {
|
|
256
262
|
const g = s.drag;
|
|
@@ -269,6 +275,7 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
269
275
|
const onUp = (e) => {
|
|
270
276
|
const g = s.drag;
|
|
271
277
|
s.drag = null;
|
|
278
|
+
s.hold = performance.now() + 1800;
|
|
272
279
|
if (!g) return;
|
|
273
280
|
if (g.moved && g.samples.length > 1) {
|
|
274
281
|
const a = g.samples[0];
|
|
@@ -286,10 +293,12 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
286
293
|
settle();
|
|
287
294
|
};
|
|
288
295
|
const onWheel = (e) => {
|
|
289
|
-
|
|
290
|
-
|
|
296
|
+
// Only sideways wheel / trackpad swipes move the row; vertical
|
|
297
|
+
// scrolling belongs to the page.
|
|
298
|
+
if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
|
|
291
299
|
e.preventDefault();
|
|
292
|
-
s.target +=
|
|
300
|
+
s.target += e.deltaX * 1.2;
|
|
301
|
+
s.hold = performance.now() + 1800;
|
|
293
302
|
clearTimeout(snapTimer);
|
|
294
303
|
snapTimer = setTimeout(settle, 140);
|
|
295
304
|
wake();
|
|
@@ -299,6 +308,13 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
299
308
|
canvas.addEventListener("pointerup", onUp);
|
|
300
309
|
canvas.addEventListener("pointercancel", onUp);
|
|
301
310
|
canvas.addEventListener("wheel", onWheel, { passive: false });
|
|
311
|
+
const onEnter = () => (s.hover = true);
|
|
312
|
+
const onLeave = () => {
|
|
313
|
+
s.hover = false;
|
|
314
|
+
wake();
|
|
315
|
+
};
|
|
316
|
+
canvas.addEventListener("pointerenter", onEnter);
|
|
317
|
+
canvas.addEventListener("pointerleave", onLeave);
|
|
302
318
|
const io = new IntersectionObserver(([entry]) => {
|
|
303
319
|
visible = entry.isIntersecting;
|
|
304
320
|
wake();
|
|
@@ -316,6 +332,8 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
316
332
|
canvas.removeEventListener("pointerup", onUp);
|
|
317
333
|
canvas.removeEventListener("pointercancel", onUp);
|
|
318
334
|
canvas.removeEventListener("wheel", onWheel);
|
|
335
|
+
canvas.removeEventListener("pointerenter", onEnter);
|
|
336
|
+
canvas.removeEventListener("pointerleave", onLeave);
|
|
319
337
|
textures.forEach((t) => t.tex?.dispose());
|
|
320
338
|
slots.forEach((slot) => slot.m.dispose());
|
|
321
339
|
geometry.dispose();
|
|
@@ -124,7 +124,6 @@ const CircularCarousel = ({
|
|
|
124
124
|
},
|
|
125
125
|
});
|
|
126
126
|
loader.load(item.src, (t) => {
|
|
127
|
-
t.colorSpace = SRGBColorSpace;
|
|
128
127
|
m.uniforms.uTex.value = t;
|
|
129
128
|
const ia = t.image.width / t.image.height;
|
|
130
129
|
// Cover crop: show the middle of the image at the card's shape.
|
|
@@ -92,8 +92,9 @@ const FlipClock = ({ mode = "seconds", target, seconds = 3 * 86400 + 4 * 3600 +
|
|
|
92
92
|
|
|
93
93
|
return (
|
|
94
94
|
// The clock is sized in em; on narrow screens the font shrinks with the
|
|
95
|
-
// container (cqw) so every unit fits instead of being cut off.
|
|
96
|
-
|
|
95
|
+
// container (cqw) so every unit fits instead of being cut off. The min
|
|
96
|
+
// width keeps it from collapsing inside a shrink-to-fit parent.
|
|
97
|
+
<div className="flex w-full justify-center" style={{ containerType: "inline-size", minWidth: `min(${(size * parts.length * 2.55).toFixed(2)}rem, 92vw)` }}>
|
|
97
98
|
<div role="timer" aria-live="off" aria-label={parts.map(([l, v]) => `${v} ${l}`).join(", ")} className={`inline-flex items-start gap-[0.35em] font-mono font-bold ${className}`} style={{ fontSize: `min(${size}rem, ${(100 / (parts.length * 2.55)).toFixed(2)}cqw)` }}>
|
|
98
99
|
{parts.map(([label, value], i) => (
|
|
99
100
|
<React.Fragment key={label}>
|
|
@@ -6,24 +6,31 @@ function cn(...classes) {
|
|
|
6
6
|
import React, { useRef } from "react";
|
|
7
7
|
import { motion, useMotionValue, useSpring, useMotionTemplate } from "framer-motion";
|
|
8
8
|
const bgHexa = 'https://ik.imagekit.io/ny6yinyut/apexui/assets/bg-hexa.jpg?tr=f-auto,q-80';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
9
|
+
// Shown when no children are passed (the look from the docs preview).
|
|
10
|
+
const DEMO_CLASS = "flex flex-col items-center justify-center bg-gradient-to-br from-gray-900 via-slate-800 to-[#181825] p-8 border border-slate-800 shadow-2xl relative overflow-hidden group group-hover:scale-100 transition-transform duration-300 ease-in-out";
|
|
11
|
+
const DemoContent = () => (
|
|
12
|
+
<>
|
|
13
|
+
<img src={bgHexa} alt="bg" className="absolute inset-0 w-full h-full object-cover opacity-30 pointer-events-none select-none z-0" draggable="false" />
|
|
14
|
+
<div className="flex items-center justify-center w-20 h-20 rounded-md bg-gradient-to-br from-[#23233a] to-[#181825] border-2 border-[#a855f7]/60 shadow-xl mb-4 z-10 group">
|
|
15
|
+
<span className="text-5xl font-extrabold text-[#a855f7] drop-shadow-[0_2px_12px_#a855f7aa]">A</span>
|
|
16
|
+
</div>
|
|
17
|
+
<p className="text-slate-200 font-extrabold text-2xl mt-2 tracking-wide z-10">ApexUI</p>
|
|
18
|
+
</>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Default export: renders your children inside the card, or the demo when there are none.
|
|
22
|
+
export default function GlareCardDefault({ children, className = "", ...rest }) {
|
|
23
|
+
return children ? (
|
|
24
|
+
<GlareCard className={className} {...rest}>
|
|
25
|
+
{children}
|
|
26
|
+
</GlareCard>
|
|
27
|
+
) : (
|
|
28
|
+
<GlareCard className={`${DEMO_CLASS} ${className}`} {...rest}>
|
|
29
|
+
<DemoContent />
|
|
30
|
+
</GlareCard>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
export function GlareCard({
|
|
28
35
|
children,
|
|
29
36
|
className = "",
|
|
@@ -126,7 +126,6 @@ const GlassLens = ({ image = DEFAULT_IMAGE, size = 9, refraction = 0.7, dispersi
|
|
|
126
126
|
u.uCover.value.set(ia > a ? a / ia : 1, ia > a ? 1 : ia / a);
|
|
127
127
|
};
|
|
128
128
|
new TextureLoader().setCrossOrigin("anonymous").load(image, (t) => {
|
|
129
|
-
t.colorSpace = SRGBColorSpace;
|
|
130
129
|
u.uTex.value = t;
|
|
131
130
|
view.imgAspect = t.image.width / t.image.height;
|
|
132
131
|
cover();
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { Color, Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, TextureLoader, Vector2, Vector4, WebGLRenderer } from "three";
|
|
3
|
+
import { CloudSun, Compass, Home, Music2, Plus, Search, User } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Liquid Glass: iOS style glass panels over a wallpaper, drawn in one WebGL
|
|
7
|
+
* shader. Each panel bends the picture at its rounded edges, frosts it a
|
|
8
|
+
* little inside, splits colours along the rim and catches a specular edge
|
|
9
|
+
* light. Panels are draggable, wobble like jelly while they move, and melt
|
|
10
|
+
* into each other when they get close. Content inside each panel is plain
|
|
11
|
+
* HTML that rides along with it.
|
|
12
|
+
*
|
|
13
|
+
* Props: items ({ id, x, y, offset?, width, maxWidth?, height, radius, content }[],
|
|
14
|
+
* up to 4; x / y place the centre as 0..1 of the box, offset nudges x in px,
|
|
15
|
+
* a width of 1 or less is a share of the box width), children (shown under
|
|
16
|
+
* the glass, e.g. hero text), image (wallpaper; a soft animated gradient when empty),
|
|
17
|
+
* refraction, frost, chroma, vibrancy, tint, tintAmount, merge (px of liquid
|
|
18
|
+
* blending),
|
|
19
|
+
* draggable, height, className.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const VERT = `void main(){ gl_Position = vec4(position.xy, 0.0, 1.0); }`;
|
|
23
|
+
|
|
24
|
+
const FRAG = `
|
|
25
|
+
precision highp float;
|
|
26
|
+
uniform vec2 uRes; uniform float uTime; uniform float uDpr;
|
|
27
|
+
uniform sampler2D uTex; uniform float uHasTex; uniform vec2 uCover;
|
|
28
|
+
uniform vec4 uBox[4]; uniform float uRad[4]; uniform float uCount;
|
|
29
|
+
uniform float uRefract; uniform float uFrost; uniform float uChroma; uniform vec3 uTint; uniform float uTintAmt; uniform float uMerge;
|
|
30
|
+
uniform vec2 uLight; uniform float uVibrancy;
|
|
31
|
+
|
|
32
|
+
float sdBox(vec2 p, vec2 b, float r){ vec2 q = abs(p) - b + r; return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r; }
|
|
33
|
+
float smin(float a, float b, float k){ float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k * h * (1.0 - h); }
|
|
34
|
+
|
|
35
|
+
float scene(vec2 p){
|
|
36
|
+
float d = 1e5;
|
|
37
|
+
for (int i = 0; i < 4; i++) {
|
|
38
|
+
if (float(i) >= uCount) break;
|
|
39
|
+
float s = sdBox(p - uBox[i].xy, uBox[i].zw, uRad[i]);
|
|
40
|
+
d = smin(d, s, max(uMerge, 0.001));
|
|
41
|
+
}
|
|
42
|
+
return d;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Half-size (shorter side) of the panel closest to p, for a bevel that suits it.
|
|
46
|
+
float nearestSize(vec2 p){
|
|
47
|
+
float best = 1e5; float size = 30.0;
|
|
48
|
+
for (int i = 0; i < 4; i++) {
|
|
49
|
+
if (float(i) >= uCount) break;
|
|
50
|
+
float s = sdBox(p - uBox[i].xy, uBox[i].zw, uRad[i]);
|
|
51
|
+
if (s < best) { best = s; size = min(uBox[i].z, uBox[i].w); }
|
|
52
|
+
}
|
|
53
|
+
return size;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Default wallpaper: drifting colour blobs with fine stripes so the bending shows.
|
|
57
|
+
vec3 wallpaper(vec2 uv){
|
|
58
|
+
float t = uTime * 0.06;
|
|
59
|
+
vec2 a = vec2(0.25 + 0.15 * sin(t * 2.1), 0.3 + 0.2 * cos(t * 1.7));
|
|
60
|
+
vec2 b = vec2(0.75 + 0.12 * cos(t * 1.3), 0.65 + 0.15 * sin(t * 2.3));
|
|
61
|
+
vec2 c = vec2(0.5 + 0.25 * sin(t * 0.9 + 2.0), 0.15 + 0.1 * cos(t * 1.1));
|
|
62
|
+
vec2 asp = vec2(uRes.x / uRes.y, 1.0);
|
|
63
|
+
vec3 col = vec3(0.06, 0.07, 0.12);
|
|
64
|
+
col += vec3(1.0, 0.36, 0.42) * exp(-length((uv - a) * asp) * 3.2) * 0.95;
|
|
65
|
+
col += vec3(0.25, 0.45, 1.0) * exp(-length((uv - b) * asp) * 2.8) * 0.95;
|
|
66
|
+
col += vec3(1.0, 0.75, 0.25) * exp(-length((uv - c) * asp) * 3.6) * 0.8;
|
|
67
|
+
float stripes = smoothstep(0.35, 0.5, abs(fract((uv.x * asp.x + uv.y) * 14.0) - 0.5));
|
|
68
|
+
col *= 0.88 + 0.12 * stripes;
|
|
69
|
+
return col;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
vec3 bg(vec2 px){
|
|
73
|
+
vec2 uv = px / uRes;
|
|
74
|
+
if (uHasTex > 0.5) return texture2D(uTex, (uv - 0.5) * uCover + 0.5).rgb;
|
|
75
|
+
return wallpaper(uv);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
void main(){
|
|
79
|
+
vec2 p = gl_FragCoord.xy;
|
|
80
|
+
float d = scene(p);
|
|
81
|
+
vec3 base = bg(p);
|
|
82
|
+
|
|
83
|
+
// Two soft shadows under the glass: a tight contact one and a wide ambient one.
|
|
84
|
+
float sd = scene(p + vec2(0.0, 6.0 * uDpr));
|
|
85
|
+
float sd2 = scene(p + vec2(0.0, 18.0 * uDpr));
|
|
86
|
+
float outside = step(0.0, d);
|
|
87
|
+
vec3 col = base * (1.0 - outside * (0.22 * exp(-max(sd, 0.0) / (8.0 * uDpr)) + 0.18 * exp(-max(sd2, 0.0) / (34.0 * uDpr))));
|
|
88
|
+
|
|
89
|
+
if (d < 1.5 * uDpr) {
|
|
90
|
+
vec2 e = vec2(1.0, 0.0);
|
|
91
|
+
vec2 n = normalize(vec2(scene(p + e.xy) - scene(p - e.xy), scene(p + e.yx) - scene(p - e.yx)) + 1e-6);
|
|
92
|
+
// The rim curves over about half of the short side, like a real slab of glass.
|
|
93
|
+
float bevel = clamp(nearestSize(p) * 0.55, 10.0 * uDpr, 34.0 * uDpr);
|
|
94
|
+
float depth = clamp(-d / bevel, 0.0, 1.0); // 0 at the rim, 1 on the flat top
|
|
95
|
+
// A rounded edge profile: the steeper the surface, the more it bends light.
|
|
96
|
+
float x = 1.0 - depth;
|
|
97
|
+
float h = sqrt(max(1.0 - x * x, 0.0));
|
|
98
|
+
float slope = x / max(h, 0.08);
|
|
99
|
+
vec2 off = n * min(slope, 6.0) * uRefract * bevel * 0.32;
|
|
100
|
+
vec3 g = vec3(0.0);
|
|
101
|
+
float f = uFrost * 5.0 * uDpr;
|
|
102
|
+
for (int i = 0; i < 9; i++) {
|
|
103
|
+
float a = float(i) * 2.399;
|
|
104
|
+
vec2 o = f > 0.0 ? vec2(cos(a), sin(a)) * f * sqrt(float(i) / 9.0) : vec2(0.0);
|
|
105
|
+
vec2 q = p + off + o;
|
|
106
|
+
vec2 ch = n * x * x * uChroma * 2.5 * uDpr;
|
|
107
|
+
g += vec3(bg(q + ch).r, bg(q).g, bg(q - ch).b);
|
|
108
|
+
}
|
|
109
|
+
g /= 9.0;
|
|
110
|
+
// Vibrancy: what shows through gets a little richer and brighter.
|
|
111
|
+
float luma = dot(g, vec3(0.299, 0.587, 0.114));
|
|
112
|
+
g = mix(vec3(luma), g, 1.0 + 0.35 * uVibrancy) * (1.0 + 0.07 * uVibrancy) + 0.02 * uVibrancy;
|
|
113
|
+
g = mix(g, uTint, uTintAmt);
|
|
114
|
+
|
|
115
|
+
// Light follows the pointer, like tilting a phone.
|
|
116
|
+
vec2 L = normalize(uLight);
|
|
117
|
+
float facing = max(dot(n, L), 0.0);
|
|
118
|
+
float away = max(dot(n, -L), 0.0);
|
|
119
|
+
// Fresnel: the curved edge reflects more than the flat top.
|
|
120
|
+
g = mix(g, vec3(1.0), pow(x, 3.0) * 0.18);
|
|
121
|
+
// A crisp specular line on the rim, strongest facing the light, with a
|
|
122
|
+
// fainter bounce on the opposite edge.
|
|
123
|
+
float line = smoothstep(1.6 * uDpr, 0.0, abs(d + 0.9 * uDpr));
|
|
124
|
+
g += line * (0.75 * pow(facing, 1.5) + 0.32 * pow(away, 2.0) + 0.06);
|
|
125
|
+
// A broad glint just inside the lit edge.
|
|
126
|
+
g += smoothstep(0.55, 1.0, x) * pow(facing, 3.0) * 0.22;
|
|
127
|
+
// Inner shadow along the far edge gives the slab some thickness.
|
|
128
|
+
g *= 1.0 - smoothstep(0.6, 1.0, x) * pow(away, 1.5) * 0.18;
|
|
129
|
+
float mask = smoothstep(0.75 * uDpr, -0.75 * uDpr, d);
|
|
130
|
+
col = mix(col, g, mask);
|
|
131
|
+
}
|
|
132
|
+
gl_FragColor = vec4(col, 1.0);
|
|
133
|
+
}`;
|
|
134
|
+
|
|
135
|
+
const Tab = ({ icon: Icon, label, active }) => (
|
|
136
|
+
<span className={`flex flex-1 flex-col items-center gap-0.5 rounded-full py-1.5 text-[0.62rem] font-semibold ${active ? "bg-white/25 text-white" : "text-white/85"}`}>
|
|
137
|
+
<Icon className="h-[18px] w-[18px]" strokeWidth={2.2} />
|
|
138
|
+
{label}
|
|
139
|
+
</span>
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const DEFAULT_ITEMS = [
|
|
143
|
+
{
|
|
144
|
+
id: "bar",
|
|
145
|
+
x: 0.5,
|
|
146
|
+
y: 0.84,
|
|
147
|
+
width: 300,
|
|
148
|
+
height: 62,
|
|
149
|
+
radius: 31,
|
|
150
|
+
offset: -36,
|
|
151
|
+
content: (
|
|
152
|
+
<div className="flex h-full items-center gap-1 px-2">
|
|
153
|
+
<Tab icon={Home} label="Home" active />
|
|
154
|
+
<Tab icon={Compass} label="Explore" />
|
|
155
|
+
<Tab icon={Music2} label="Music" />
|
|
156
|
+
<Tab icon={User} label="Profile" />
|
|
157
|
+
</div>
|
|
158
|
+
),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "search",
|
|
162
|
+
x: 0.5,
|
|
163
|
+
y: 0.84,
|
|
164
|
+
width: 62,
|
|
165
|
+
height: 62,
|
|
166
|
+
radius: 31,
|
|
167
|
+
offset: 155,
|
|
168
|
+
content: (
|
|
169
|
+
<div className="grid h-full place-items-center text-white">
|
|
170
|
+
<Search className="h-5 w-5" strokeWidth={2.4} />
|
|
171
|
+
</div>
|
|
172
|
+
),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "widget",
|
|
176
|
+
x: 0.3,
|
|
177
|
+
y: 0.3,
|
|
178
|
+
width: 170,
|
|
179
|
+
height: 150,
|
|
180
|
+
radius: 34,
|
|
181
|
+
content: (
|
|
182
|
+
<div className="flex h-full flex-col justify-between p-4 text-white">
|
|
183
|
+
<div className="flex items-center justify-between text-xs font-semibold text-white/85">
|
|
184
|
+
Cupertino <CloudSun className="h-5 w-5" />
|
|
185
|
+
</div>
|
|
186
|
+
<div>
|
|
187
|
+
<p className="text-4xl font-light leading-none">24°</p>
|
|
188
|
+
<p className="mt-1 text-[0.7rem] text-white/80">Partly cloudy · H 27° L 18°</p>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
),
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "add",
|
|
195
|
+
x: 0.74,
|
|
196
|
+
y: 0.3,
|
|
197
|
+
width: 64,
|
|
198
|
+
height: 64,
|
|
199
|
+
radius: 22,
|
|
200
|
+
content: (
|
|
201
|
+
<div className="grid h-full place-items-center text-white">
|
|
202
|
+
<Plus className="h-6 w-6" strokeWidth={2.4} />
|
|
203
|
+
</div>
|
|
204
|
+
),
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const LiquidGlass = ({ items = DEFAULT_ITEMS, children, image, refraction = 1, frost = 0.2, chroma = 1, vibrancy = 1, tint = "#ffffff", tintAmount = 0.06, merge = 18, draggable = true, height = "32rem", className = "" }) => {
|
|
209
|
+
const wrap = useRef(null);
|
|
210
|
+
const panels = useRef([]);
|
|
211
|
+
const live = useRef({});
|
|
212
|
+
live.current = { refraction, frost, chroma, vibrancy, tint, tintAmount, merge, draggable };
|
|
213
|
+
const list = items.slice(0, 4);
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
const el = wrap.current;
|
|
217
|
+
const renderer = new WebGLRenderer({ antialias: false, alpha: false, powerPreference: "high-performance" });
|
|
218
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
219
|
+
renderer.setPixelRatio(dpr);
|
|
220
|
+
Object.assign(renderer.domElement.style, { position: "absolute", inset: "0", width: "100%", height: "100%", display: "block" });
|
|
221
|
+
el.prepend(renderer.domElement);
|
|
222
|
+
const scene = new Scene();
|
|
223
|
+
const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
224
|
+
const u = {
|
|
225
|
+
uRes: { value: new Vector2(1, 1) },
|
|
226
|
+
uTime: { value: 0 },
|
|
227
|
+
uDpr: { value: dpr },
|
|
228
|
+
uTex: { value: null },
|
|
229
|
+
uHasTex: { value: 0 },
|
|
230
|
+
uCover: { value: new Vector2(1, 1) },
|
|
231
|
+
uBox: { value: Array.from({ length: 4 }, () => new Vector4()) },
|
|
232
|
+
uRad: { value: [0, 0, 0, 0] },
|
|
233
|
+
uCount: { value: 0 },
|
|
234
|
+
uRefract: { value: 1 },
|
|
235
|
+
uFrost: { value: 0.2 },
|
|
236
|
+
uLight: { value: new Vector2(-0.6, 0.8) },
|
|
237
|
+
uVibrancy: { value: 1 },
|
|
238
|
+
uChroma: { value: 1 },
|
|
239
|
+
uTint: { value: new Color(1, 1, 1) },
|
|
240
|
+
uTintAmt: { value: 0.08 },
|
|
241
|
+
uMerge: { value: 18 },
|
|
242
|
+
};
|
|
243
|
+
const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms: u, depthTest: false, depthWrite: false });
|
|
244
|
+
const geometry = new PlaneGeometry(2, 2);
|
|
245
|
+
scene.add(new Mesh(geometry, material));
|
|
246
|
+
|
|
247
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
248
|
+
const view = { w: 1, h: 1, imgAspect: 1 };
|
|
249
|
+
// Panel state in CSS px: target (tx, ty), spring position, velocity, press.
|
|
250
|
+
const st = list.map((it) => ({ it, tx: 0, ty: 0, x: 0, y: 0, vx: 0, vy: 0, press: 0, moved: false, placed: false }));
|
|
251
|
+
let raf = 0;
|
|
252
|
+
let last = 0;
|
|
253
|
+
let time = 0;
|
|
254
|
+
let visible = true;
|
|
255
|
+
let drag = null;
|
|
256
|
+
const light = { x: -0.6, y: 0.8, on: false };
|
|
257
|
+
|
|
258
|
+
const scaleFor = () => Math.min(1, (view.w - 24) / 360);
|
|
259
|
+
const place = () => {
|
|
260
|
+
const s = scaleFor();
|
|
261
|
+
st.forEach((p) => {
|
|
262
|
+
if (p.moved && p.placed) return;
|
|
263
|
+
p.tx = view.w * p.it.x + (p.it.offset ?? 0) * s;
|
|
264
|
+
p.ty = view.h * p.it.y;
|
|
265
|
+
if (!p.placed) {
|
|
266
|
+
p.x = p.tx;
|
|
267
|
+
p.y = p.ty;
|
|
268
|
+
p.placed = true;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
const cover = () => {
|
|
273
|
+
const a = view.w / view.h;
|
|
274
|
+
const ia = view.imgAspect;
|
|
275
|
+
u.uCover.value.set(ia > a ? a / ia : 1, ia > a ? 1 : ia / a);
|
|
276
|
+
};
|
|
277
|
+
if (image) {
|
|
278
|
+
new TextureLoader().setCrossOrigin("anonymous").load(image, (t) => {
|
|
279
|
+
u.uTex.value = t;
|
|
280
|
+
u.uHasTex.value = 1;
|
|
281
|
+
view.imgAspect = t.image.width / t.image.height;
|
|
282
|
+
cover();
|
|
283
|
+
start();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const fit = () => {
|
|
288
|
+
const { width, height: hh } = el.getBoundingClientRect();
|
|
289
|
+
if (!width || !hh) return;
|
|
290
|
+
view.w = width;
|
|
291
|
+
view.h = hh;
|
|
292
|
+
renderer.setSize(width, hh, false);
|
|
293
|
+
u.uRes.value.set(width * dpr, hh * dpr);
|
|
294
|
+
cover();
|
|
295
|
+
place();
|
|
296
|
+
start();
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const frame = (now) => {
|
|
300
|
+
raf = 0;
|
|
301
|
+
const o = live.current;
|
|
302
|
+
const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
|
|
303
|
+
last = now;
|
|
304
|
+
time += dt;
|
|
305
|
+
const s = scaleFor();
|
|
306
|
+
let busy = !u.uHasTex.value && !reduced; // the default wallpaper drifts
|
|
307
|
+
st.forEach((p, i) => {
|
|
308
|
+
// Jelly spring: a little underdamped so panels wobble into place.
|
|
309
|
+
const k = 260;
|
|
310
|
+
const c = 2 * Math.sqrt(k) * 0.62;
|
|
311
|
+
p.vx += (k * (p.tx - p.x) - c * p.vx) * dt;
|
|
312
|
+
p.vy += (k * (p.ty - p.y) - c * p.vy) * dt;
|
|
313
|
+
p.x += p.vx * dt;
|
|
314
|
+
p.y += p.vy * dt;
|
|
315
|
+
p.press += ((drag?.i === i ? 1 : 0) - p.press) * (1 - Math.exp(-dt / 0.08));
|
|
316
|
+
const speed = Math.hypot(p.vx, p.vy);
|
|
317
|
+
if (speed > 0.5 || Math.abs(p.tx - p.x) > 0.2 || Math.abs(p.ty - p.y) > 0.2 || Math.abs(p.press - (drag?.i === i ? 1 : 0)) > 0.01) busy = true;
|
|
318
|
+
// Stretch along the motion, squash across it; grow a touch while held.
|
|
319
|
+
const sx = 1 + Math.min(0.18, Math.abs(p.vx) / 4000) - Math.min(0.1, Math.abs(p.vy) / 6000);
|
|
320
|
+
const sy = 1 + Math.min(0.18, Math.abs(p.vy) / 4000) - Math.min(0.1, Math.abs(p.vx) / 6000);
|
|
321
|
+
const grow = 1 + p.press * 0.06;
|
|
322
|
+
// A width of 1 or less is a share of the box (handy for nav bars).
|
|
323
|
+
const baseW = Math.min(p.it.width <= 1 ? p.it.width * view.w : p.it.width * s, p.it.maxWidth ?? Infinity);
|
|
324
|
+
const hw = (baseW * sx * grow) / 2;
|
|
325
|
+
const hh = (p.it.height * s * sy * grow) / 2;
|
|
326
|
+
u.uBox.value[i].set(p.x * dpr, (view.h - p.y) * dpr, hw * dpr, hh * dpr);
|
|
327
|
+
u.uRad.value[i] = Math.min(p.it.radius * s * grow, hw, hh) * dpr;
|
|
328
|
+
const node = panels.current[i];
|
|
329
|
+
if (node) {
|
|
330
|
+
node.style.width = `${hw * 2}px`;
|
|
331
|
+
node.style.height = `${hh * 2}px`;
|
|
332
|
+
node.style.transform = `translate3d(${(p.x - hw).toFixed(2)}px, ${(p.y - hh).toFixed(2)}px, 0)`;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
u.uCount.value = st.length;
|
|
336
|
+
// Ease the light toward the pointer direction from the centre.
|
|
337
|
+
const lv = u.uLight.value;
|
|
338
|
+
const lx = light.on ? light.x : -0.6;
|
|
339
|
+
const ly = light.on ? light.y : 0.8;
|
|
340
|
+
lv.set(lv.x + (lx - lv.x) * (1 - Math.exp(-dt / 0.25)), lv.y + (ly - lv.y) * (1 - Math.exp(-dt / 0.25)));
|
|
341
|
+
if (Math.abs(lx - lv.x) + Math.abs(ly - lv.y) > 0.002) busy = true;
|
|
342
|
+
u.uVibrancy.value = o.vibrancy;
|
|
343
|
+
u.uTime.value = time;
|
|
344
|
+
u.uRefract.value = o.refraction;
|
|
345
|
+
u.uFrost.value = o.frost;
|
|
346
|
+
u.uChroma.value = o.chroma;
|
|
347
|
+
u.uTint.value.set(o.tint).convertLinearToSRGB();
|
|
348
|
+
u.uTintAmt.value = o.tintAmount;
|
|
349
|
+
u.uMerge.value = o.merge * dpr;
|
|
350
|
+
renderer.render(scene, camera);
|
|
351
|
+
if (visible && busy) raf = requestAnimationFrame(frame);
|
|
352
|
+
else last = 0;
|
|
353
|
+
};
|
|
354
|
+
function start() {
|
|
355
|
+
if (!raf && visible) raf = requestAnimationFrame(frame);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const local = (e) => {
|
|
359
|
+
const r = el.getBoundingClientRect();
|
|
360
|
+
return [e.clientX - r.left, e.clientY - r.top];
|
|
361
|
+
};
|
|
362
|
+
const onDown = (e) => {
|
|
363
|
+
if (!live.current.draggable) return;
|
|
364
|
+
const node = e.target.closest("[data-glass]");
|
|
365
|
+
if (!node) return;
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
const i = Number(node.dataset.glass);
|
|
368
|
+
const [x, y] = local(e);
|
|
369
|
+
drag = { i, dx: st[i].tx - x, dy: st[i].ty - y };
|
|
370
|
+
el.setPointerCapture(e.pointerId);
|
|
371
|
+
start();
|
|
372
|
+
};
|
|
373
|
+
const onMove = (e) => {
|
|
374
|
+
const [x, y] = local(e);
|
|
375
|
+
// The light comes from where the pointer is, relative to the centre.
|
|
376
|
+
const lx = x - view.w / 2;
|
|
377
|
+
const ly = view.h / 2 - y;
|
|
378
|
+
const len = Math.hypot(lx, ly) || 1;
|
|
379
|
+
light.x = lx / len;
|
|
380
|
+
light.y = ly / len;
|
|
381
|
+
light.on = true;
|
|
382
|
+
start();
|
|
383
|
+
if (!drag) return;
|
|
384
|
+
const p = st[drag.i];
|
|
385
|
+
p.tx = Math.max(0, Math.min(view.w, x + drag.dx));
|
|
386
|
+
p.ty = Math.max(0, Math.min(view.h, y + drag.dy));
|
|
387
|
+
p.moved = true;
|
|
388
|
+
start();
|
|
389
|
+
};
|
|
390
|
+
const onUp = () => {
|
|
391
|
+
drag = null;
|
|
392
|
+
start();
|
|
393
|
+
};
|
|
394
|
+
el.addEventListener("pointerdown", onDown);
|
|
395
|
+
el.addEventListener("pointermove", onMove);
|
|
396
|
+
el.addEventListener("pointerup", onUp);
|
|
397
|
+
el.addEventListener("pointercancel", onUp);
|
|
398
|
+
const ro = new ResizeObserver(fit);
|
|
399
|
+
ro.observe(el);
|
|
400
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
401
|
+
visible = entry.isIntersecting;
|
|
402
|
+
if (visible) start();
|
|
403
|
+
});
|
|
404
|
+
io.observe(el);
|
|
405
|
+
fit();
|
|
406
|
+
|
|
407
|
+
return () => {
|
|
408
|
+
cancelAnimationFrame(raf);
|
|
409
|
+
ro.disconnect();
|
|
410
|
+
io.disconnect();
|
|
411
|
+
el.removeEventListener("pointerdown", onDown);
|
|
412
|
+
el.removeEventListener("pointermove", onMove);
|
|
413
|
+
el.removeEventListener("pointerup", onUp);
|
|
414
|
+
el.removeEventListener("pointercancel", onUp);
|
|
415
|
+
u.uTex.value?.dispose();
|
|
416
|
+
geometry.dispose();
|
|
417
|
+
material.dispose();
|
|
418
|
+
renderer.dispose();
|
|
419
|
+
renderer.forceContextLoss();
|
|
420
|
+
renderer.domElement.remove();
|
|
421
|
+
};
|
|
422
|
+
// Rebuild when the panels or the wallpaper change; the look is live.
|
|
423
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
424
|
+
}, [image, list.length]);
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
<div ref={wrap} className={`relative w-full select-none overflow-hidden bg-[#0f1220] ${draggable ? "touch-none" : ""} ${className}`} style={{ height }}>
|
|
428
|
+
{children && <div className="pointer-events-none absolute inset-0 z-[5] [&_a]:pointer-events-auto [&_button]:pointer-events-auto">{children}</div>}
|
|
429
|
+
{list.map((it, i) => (
|
|
430
|
+
<div
|
|
431
|
+
key={it.id ?? i}
|
|
432
|
+
ref={(e) => (panels.current[i] = e)}
|
|
433
|
+
data-glass={i}
|
|
434
|
+
className={`absolute left-0 top-0 z-10 overflow-hidden [text-shadow:0_1px_2px_rgba(0,0,0,0.25)] ${draggable ? "cursor-grab active:cursor-grabbing" : ""}`}
|
|
435
|
+
style={{ borderRadius: it.radius }}
|
|
436
|
+
>
|
|
437
|
+
{it.content}
|
|
438
|
+
</div>
|
|
439
|
+
))}
|
|
440
|
+
</div>
|
|
441
|
+
);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
export default LiquidGlass;
|
|
@@ -5,8 +5,8 @@ import React, { Children, useLayoutEffect, useRef } from "react";
|
|
|
5
5
|
* card that gets covered shrinks a little, darkens and (optionally) blurs or
|
|
6
6
|
* leans back, so the pile reads as depth. It rides on native scrolling
|
|
7
7
|
* (position: sticky + one rAF update per scroll), so it stays smooth with
|
|
8
|
-
* mouse, trackpad, touch and keyboard.
|
|
9
|
-
*
|
|
8
|
+
* mouse, trackpad, touch and keyboard. By default it stacks with the page
|
|
9
|
+
* scroll; set useWindowScroll={false} to stack inside its own scroll box.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export const ScrollStackItem = ({ children, className = "", style }) => (
|
|
@@ -16,7 +16,7 @@ export const ScrollStackItem = ({ children, className = "", style }) => (
|
|
|
16
16
|
</div>
|
|
17
17
|
);
|
|
18
18
|
|
|
19
|
-
const ScrollStack = ({ children, gap = 90, offset = 22, top = 64, scaleStep = 0.05, minScale = 0.82, dim = 0.22, blur = 0, tilt = 0, endSpace = 180, useWindowScroll =
|
|
19
|
+
const ScrollStack = ({ children, gap = 90, offset = 22, top = 64, scaleStep = 0.05, minScale = 0.82, dim = 0.22, blur = 0, tilt = 0, endSpace = 180, useWindowScroll = true, height = "100%", className = "" }) => {
|
|
20
20
|
const root = useRef(null);
|
|
21
21
|
const slots = useRef([]);
|
|
22
22
|
const items = Children.toArray(children);
|
|
@@ -23,6 +23,7 @@ precision highp float;
|
|
|
23
23
|
uniform vec2 uRes; uniform float uTime; uniform float uRise; uniform vec2 uMouse;
|
|
24
24
|
uniform vec3 uSun; uniform vec3 uSkyTop; uniform vec3 uSkyLow; uniform vec3 uMount;
|
|
25
25
|
uniform float uRays; uniform float uStars; uniform float uHaze; uniform float uSunSize; uniform float uSunHeight;
|
|
26
|
+
uniform float uTrees; uniform float uBirds;
|
|
26
27
|
|
|
27
28
|
float h1(float x){ return fract(sin(x * 127.1) * 43758.5453); }
|
|
28
29
|
float h2(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
@@ -47,6 +48,23 @@ float range(float x, float c, float w, float peak, float base, float amp, float
|
|
|
47
48
|
return base + s * peak + (crag(x * freq + seed) - 0.45) * amp * (0.35 + s);
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
// A pine silhouette: three stacked tiers, each wider at its base, on a short trunk.
|
|
52
|
+
// dx / dy are measured from the foot of the tree, h is its height (all in screen units).
|
|
53
|
+
float pine(float dx, float dy, float h){
|
|
54
|
+
if (h <= 0.0) return 0.0;
|
|
55
|
+
float t = dy / h;
|
|
56
|
+
if (t < 0.0 || t > 1.0) return 0.0;
|
|
57
|
+
float aa = 0.0015;
|
|
58
|
+
float trunk = smoothstep(h * 0.035 + aa, h * 0.035 - aa, abs(dx)) * step(t, 0.14);
|
|
59
|
+
float tiers = 3.0;
|
|
60
|
+
float k = floor(min((t - 0.1) / 0.9, 0.999) * tiers);
|
|
61
|
+
float local = fract(min((t - 0.1) / 0.9, 0.999) * tiers);
|
|
62
|
+
float base = 0.36 * h * (1.0 - k / tiers * 0.62);
|
|
63
|
+
float w = base * (1.0 - local * 0.78);
|
|
64
|
+
float crown = smoothstep(w + aa, w - aa, abs(dx)) * step(0.1, t);
|
|
65
|
+
return max(trunk, crown);
|
|
66
|
+
}
|
|
67
|
+
|
|
50
68
|
void main(){
|
|
51
69
|
vec2 uv = gl_FragCoord.xy / uRes;
|
|
52
70
|
float asp = uRes.x / uRes.y;
|
|
@@ -114,6 +132,46 @@ void main(){
|
|
|
114
132
|
cn += uSun * exp(-max(hn - y, 0.0) * 120.0) * toSun * 0.9;
|
|
115
133
|
col = mix(col, cn, mn);
|
|
116
134
|
|
|
135
|
+
// Pines along the near ridge, thinning out toward the valley so the sun stays clear.
|
|
136
|
+
// Sizes are in screen-height units so a tree keeps its shape on any screen.
|
|
137
|
+
float unit = 0.5 * clamp(asp, 0.7, 2.2); // one xn unit in height units
|
|
138
|
+
float tx = (xn + mx * 0.05) * 26.0;
|
|
139
|
+
float tcell = floor(tx);
|
|
140
|
+
float cellW = unit / 26.0;
|
|
141
|
+
float jitter = (h1(tcell + 1.9) - 0.5) * 0.3;
|
|
142
|
+
float dx = (fract(tx) - 0.5 - jitter) * cellW;
|
|
143
|
+
float pres = step(0.3, h1(tcell + 3.1)) * smoothstep(0.1, 0.42, abs(xn));
|
|
144
|
+
float ht = min(0.022 + 0.04 * h1(tcell + 7.7), cellW * 1.25) * pres * uTrees;
|
|
145
|
+
float trees = pine(dx, y - hn + 0.004, ht);
|
|
146
|
+
col = mix(col, uMount * 0.6 + uSun * toSun * 0.05, trees * (1.0 - mn));
|
|
147
|
+
|
|
148
|
+
// A few big pines framing the bottom corners.
|
|
149
|
+
float tx2 = (xn + mx * 0.08) * 6.0;
|
|
150
|
+
float cell2 = floor(tx2);
|
|
151
|
+
float cellW2 = unit / 6.0;
|
|
152
|
+
float dx2 = (fract(tx2) - 0.5) * cellW2;
|
|
153
|
+
float ht2 = min(0.16 + 0.1 * h1(cell2 + 1.3), cellW2 * 1.3) * step(0.35, h1(cell2 + 4.4)) * smoothstep(0.62, 0.78, abs(xn)) * uTrees;
|
|
154
|
+
col = mix(col, uMount * 0.38, pine(dx2, y + 0.015, ht2));
|
|
155
|
+
|
|
156
|
+
// A small flock crossing the sky, wings flapping out of step.
|
|
157
|
+
if (uBirds > 0.0) {
|
|
158
|
+
float lap = fract(uTime * 0.03 + 0.38);
|
|
159
|
+
vec2 flock = vec2(mix(-asp * 0.5 - 0.25, asp * 0.5 + 0.25, lap), 0.66 + 0.035 * sin(uTime * 0.31));
|
|
160
|
+
for (int i = 0; i < 7; i++) {
|
|
161
|
+
float fi = float(i);
|
|
162
|
+
if (fi >= uBirds * 7.0) break;
|
|
163
|
+
float side = mod(fi, 2.0) * 2.0 - 1.0;
|
|
164
|
+
float rank = floor((fi + 1.0) / 2.0);
|
|
165
|
+
vec2 bpos = flock + vec2(-rank * 0.045, -rank * 0.022 * side) + vec2(0.006 * sin(uTime * 1.3 + fi), 0.008 * sin(uTime * 0.9 + fi * 1.7));
|
|
166
|
+
float size = 0.016 * (1.0 - rank * 0.08);
|
|
167
|
+
vec2 r = q - bpos;
|
|
168
|
+
float flap = 0.15 + 0.75 * (0.5 + 0.5 * sin(uTime * 9.0 + fi * 1.9));
|
|
169
|
+
float dist = abs(r.y - abs(r.x) * flap + size * 0.15) - 0.0006;
|
|
170
|
+
float bird = smoothstep(0.0024, 0.0008, dist) * step(abs(r.x), size);
|
|
171
|
+
col = mix(col, uMount * 0.45, bird * smoothstep(0.2, 0.5, rise));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
117
175
|
// Low fog over everything and light bleeding over the ridges.
|
|
118
176
|
col += haze * exp(-y * 7.0) * 0.12 * uHaze * rise;
|
|
119
177
|
col += uSun * exp(-d * 3.0) * 0.18 * rise;
|
|
@@ -132,7 +190,9 @@ const SunriseBackground = ({
|
|
|
132
190
|
mountainColor,
|
|
133
191
|
duration = 6,
|
|
134
192
|
autoRise = true,
|
|
135
|
-
sunHeight = 0.
|
|
193
|
+
sunHeight = 0.28,
|
|
194
|
+
trees = true,
|
|
195
|
+
birds = 1,
|
|
136
196
|
sunSize = 0.075,
|
|
137
197
|
rays = 1,
|
|
138
198
|
stars = 1,
|
|
@@ -158,6 +218,8 @@ const SunriseBackground = ({
|
|
|
158
218
|
rays,
|
|
159
219
|
stars,
|
|
160
220
|
haze,
|
|
221
|
+
trees,
|
|
222
|
+
birds,
|
|
161
223
|
speed,
|
|
162
224
|
interactive,
|
|
163
225
|
};
|
|
@@ -189,7 +251,9 @@ const SunriseBackground = ({
|
|
|
189
251
|
uStars: { value: 1 },
|
|
190
252
|
uHaze: { value: 1 },
|
|
191
253
|
uSunSize: { value: 0.075 },
|
|
192
|
-
uSunHeight: { value: 0.
|
|
254
|
+
uSunHeight: { value: 0.28 },
|
|
255
|
+
uTrees: { value: 1 },
|
|
256
|
+
uBirds: { value: 1 },
|
|
193
257
|
};
|
|
194
258
|
const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms: u, depthTest: false, depthWrite: false });
|
|
195
259
|
const geometry = new PlaneGeometry(2, 2);
|
|
@@ -229,6 +293,8 @@ const SunriseBackground = ({
|
|
|
229
293
|
u.uRays.value = o.rays;
|
|
230
294
|
u.uStars.value = o.stars;
|
|
231
295
|
u.uHaze.value = o.haze;
|
|
296
|
+
u.uTrees.value = o.trees ? 1 : 0;
|
|
297
|
+
u.uBirds.value = reduced ? 0 : Math.max(0, Math.min(1, o.birds));
|
|
232
298
|
u.uSunSize.value = o.sunSize;
|
|
233
299
|
u.uSunHeight.value = o.sunHeight;
|
|
234
300
|
renderer.render(scene, camera);
|
package/dist/cli/index.js
CHANGED
|
@@ -139,6 +139,7 @@ const componentMap = {
|
|
|
139
139
|
"command-palette": "CommandPalette",
|
|
140
140
|
"crossed-marquee": "CrossedMarquee",
|
|
141
141
|
"theme-switcher": "ThemeSwitcher",
|
|
142
|
+
"liquid-glass": "LiquidGlass",
|
|
142
143
|
"water-drop-reveal": "MaskView",
|
|
143
144
|
};
|
|
144
145
|
// 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.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "116 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",
|