apex-ui-kit 2.8.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 +9 -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/FluidCursor/FluidCursor.jsx +418 -0
- 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/SketchReveal/SketchReveal.jsx +257 -0
- package/dist/ApexUI-Kit/templates/SunriseBackground/SunriseBackground.jsx +68 -2
- package/dist/cli/index.js +3 -0
- package/package.json +2 -2
|
@@ -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);
|