glyphdust 0.1.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/LICENSE +21 -0
- package/README.md +180 -0
- package/dist/index.cjs +966 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +382 -0
- package/dist/index.d.ts +382 -0
- package/dist/index.js +929 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
import { useCallback, useState, useEffect, useRef, useMemo } from 'react';
|
|
2
|
+
import { Canvas, useThree, useFrame } from '@react-three/fiber';
|
|
3
|
+
import * as THREE from 'three';
|
|
4
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/sampling.ts
|
|
7
|
+
var ALPHA_THRESHOLD = 128;
|
|
8
|
+
var HASH_MULTIPLIER = 2654435761;
|
|
9
|
+
function createSamplingContext(cw, ch) {
|
|
10
|
+
if (typeof document === "undefined") return null;
|
|
11
|
+
const canvas = document.createElement("canvas");
|
|
12
|
+
canvas.width = cw;
|
|
13
|
+
canvas.height = ch;
|
|
14
|
+
return canvas.getContext("2d", { willReadFrequently: true });
|
|
15
|
+
}
|
|
16
|
+
function collectFilledPixels(ctx, cw, ch, step) {
|
|
17
|
+
const { data } = ctx.getImageData(0, 0, cw, ch);
|
|
18
|
+
const pts = [];
|
|
19
|
+
for (let y = 0; y < ch; y += step) {
|
|
20
|
+
for (let x = 0; x < cw; x += step) {
|
|
21
|
+
if (data[(y * cw + x) * 4 + 3] > ALPHA_THRESHOLD) pts.push(x, y);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return pts;
|
|
25
|
+
}
|
|
26
|
+
function fillScatterCluster(out, count, offsetX, offsetY, random) {
|
|
27
|
+
for (let i = 0; i < count; i++) {
|
|
28
|
+
const r = Math.cbrt(random()) * 1.4;
|
|
29
|
+
const th = random() * Math.PI * 2;
|
|
30
|
+
out[i * 3] = Math.cos(th) * r + offsetX;
|
|
31
|
+
out[i * 3 + 1] = Math.sin(th) * r * 0.4 + offsetY;
|
|
32
|
+
out[i * 3 + 2] = (random() - 0.5) * 0.2;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function buildTextTargets(count, lines, opts) {
|
|
36
|
+
const out = new Float32Array(count * 3);
|
|
37
|
+
const random = opts.random ?? Math.random;
|
|
38
|
+
const cw = opts.cw ?? 1280;
|
|
39
|
+
const ch = opts.ch ?? 480;
|
|
40
|
+
const ctx = createSamplingContext(cw, ch);
|
|
41
|
+
if (!ctx) return out;
|
|
42
|
+
const align = opts.align ?? "center";
|
|
43
|
+
ctx.clearRect(0, 0, cw, ch);
|
|
44
|
+
ctx.fillStyle = "#000";
|
|
45
|
+
ctx.textAlign = align === "left" ? "left" : "center";
|
|
46
|
+
ctx.textBaseline = "middle";
|
|
47
|
+
ctx.font = opts.font;
|
|
48
|
+
const drawX = align === "left" ? cw * 0.04 : cw / 2;
|
|
49
|
+
const lh = opts.lineHeight;
|
|
50
|
+
const blockH = lh * (lines.length - 1);
|
|
51
|
+
lines.forEach((line, i) => {
|
|
52
|
+
ctx.fillText(line, drawX, ch / 2 - blockH / 2 + i * lh);
|
|
53
|
+
});
|
|
54
|
+
const step = opts.step ?? 2;
|
|
55
|
+
const pts = collectFilledPixels(ctx, cw, ch, step);
|
|
56
|
+
const filled = pts.length / 2;
|
|
57
|
+
const offsetX = opts.offsetX ?? 0;
|
|
58
|
+
const offsetY = opts.offsetY ?? 0;
|
|
59
|
+
if (filled === 0) {
|
|
60
|
+
fillScatterCluster(out, count, offsetX, offsetY, random);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
const scale = opts.worldW / cw;
|
|
64
|
+
const thickness = opts.thickness ?? 0.18;
|
|
65
|
+
for (let i = 0; i < count; i++) {
|
|
66
|
+
const idx = (Math.floor(i / count * filled) + i * HASH_MULTIPLIER % filled) % filled;
|
|
67
|
+
const px = pts[idx * 2];
|
|
68
|
+
const py = pts[idx * 2 + 1];
|
|
69
|
+
const wx = (px - cw / 2) * scale + offsetX;
|
|
70
|
+
const wy = -(py - ch / 2) * scale + offsetY;
|
|
71
|
+
const jx = (random() - 0.5) * scale * step;
|
|
72
|
+
const jy = (random() - 0.5) * scale * step;
|
|
73
|
+
out[i * 3] = wx + jx;
|
|
74
|
+
out[i * 3 + 1] = wy + jy;
|
|
75
|
+
out[i * 3 + 2] = (random() - 0.5) * thickness;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function buildDenseTextTargets(count, lines, opts) {
|
|
80
|
+
const out = new Float32Array(count * 3);
|
|
81
|
+
const random = opts.random ?? Math.random;
|
|
82
|
+
const cw = opts.cw ?? 1280;
|
|
83
|
+
const ch = opts.ch ?? 400;
|
|
84
|
+
const ctx = createSamplingContext(cw, ch);
|
|
85
|
+
if (!ctx) return out;
|
|
86
|
+
ctx.clearRect(0, 0, cw, ch);
|
|
87
|
+
ctx.fillStyle = "#000";
|
|
88
|
+
ctx.textAlign = "center";
|
|
89
|
+
ctx.textBaseline = "middle";
|
|
90
|
+
ctx.font = opts.font;
|
|
91
|
+
const lh = ch * (opts.lineHeightRatio ?? 0.46);
|
|
92
|
+
const blockH = lh * (lines.length - 1);
|
|
93
|
+
lines.forEach((line, i) => {
|
|
94
|
+
ctx.fillText(line, cw / 2, ch / 2 - blockH / 2 + i * lh);
|
|
95
|
+
});
|
|
96
|
+
const step = opts.step ?? 1;
|
|
97
|
+
const pts = collectFilledPixels(ctx, cw, ch, step);
|
|
98
|
+
const filled = pts.length / 2;
|
|
99
|
+
const offsetX = opts.offsetX ?? 0;
|
|
100
|
+
const offsetY = opts.offsetY ?? 0;
|
|
101
|
+
if (filled === 0) {
|
|
102
|
+
fillScatterCluster(out, count, offsetX, offsetY, random);
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
const scale = opts.worldW / cw;
|
|
106
|
+
const thickness = opts.thickness ?? 0.08;
|
|
107
|
+
const order = new Uint32Array(filled);
|
|
108
|
+
for (let i = 0; i < filled; i++) order[i] = i;
|
|
109
|
+
for (let i = filled - 1; i > 0; i--) {
|
|
110
|
+
const j = random() * (i + 1) | 0;
|
|
111
|
+
const t = order[i];
|
|
112
|
+
order[i] = order[j];
|
|
113
|
+
order[j] = t;
|
|
114
|
+
}
|
|
115
|
+
const jitter = scale * step * 0.5;
|
|
116
|
+
for (let i = 0; i < count; i++) {
|
|
117
|
+
const idx = order[i % filled];
|
|
118
|
+
const px = pts[idx * 2];
|
|
119
|
+
const py = pts[idx * 2 + 1];
|
|
120
|
+
const wx = (px - cw / 2) * scale + offsetX;
|
|
121
|
+
const wy = -(py - ch / 2) * scale + offsetY;
|
|
122
|
+
out[i * 3] = wx + (random() - 0.5) * jitter;
|
|
123
|
+
out[i * 3 + 1] = wy + (random() - 0.5) * jitter;
|
|
124
|
+
out[i * 3 + 2] = (random() - 0.5) * thickness;
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/shaders.ts
|
|
130
|
+
var GLYPH_POSITION_ATTRIBUTE_PREFIX = "aPos";
|
|
131
|
+
function glyphPositionAttribute(index) {
|
|
132
|
+
return `${GLYPH_POSITION_ATTRIBUTE_PREFIX}${index}`;
|
|
133
|
+
}
|
|
134
|
+
function buildVertexShader(keyframeCount) {
|
|
135
|
+
if (!Number.isInteger(keyframeCount) || keyframeCount < 1) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`buildVertexShader: keyframeCount must be an integer >= 1 (got ${keyframeCount})`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const attributeDecls = Array.from(
|
|
141
|
+
{ length: keyframeCount },
|
|
142
|
+
(_, i) => ` attribute vec3 ${glyphPositionAttribute(i)};`
|
|
143
|
+
).join("\n");
|
|
144
|
+
const mixChain = Array.from(
|
|
145
|
+
{ length: keyframeCount - 1 },
|
|
146
|
+
(_, k) => ` pos = mix(pos, ${glyphPositionAttribute(k + 1)}, smoothRange(uTimes[${k}], uTimes[${k + 1}], uStage));`
|
|
147
|
+
).join("\n");
|
|
148
|
+
return (
|
|
149
|
+
/* glsl */
|
|
150
|
+
`
|
|
151
|
+
uniform float uTime;
|
|
152
|
+
uniform float uStage;
|
|
153
|
+
uniform float uTimes[${keyframeCount}];
|
|
154
|
+
uniform float uForm;
|
|
155
|
+
uniform float uSettle;
|
|
156
|
+
uniform float uBurst;
|
|
157
|
+
uniform float uSwap;
|
|
158
|
+
uniform float uResolve;
|
|
159
|
+
uniform float uReduced;
|
|
160
|
+
uniform vec3 uPointer;
|
|
161
|
+
uniform float uPointerActive;
|
|
162
|
+
uniform float uSize;
|
|
163
|
+
uniform float uPixelRatio;
|
|
164
|
+
|
|
165
|
+
${attributeDecls}
|
|
166
|
+
attribute float aSeed;
|
|
167
|
+
attribute float aAccent;
|
|
168
|
+
|
|
169
|
+
varying float vSeed;
|
|
170
|
+
varying float vAccent;
|
|
171
|
+
varying float vDepth;
|
|
172
|
+
varying float vForm;
|
|
173
|
+
varying float vAlpha;
|
|
174
|
+
varying float vSettle;
|
|
175
|
+
|
|
176
|
+
float smoothRange(float a, float b, float x) {
|
|
177
|
+
float t = clamp((x - a) / (b - a), 0.0, 1.0);
|
|
178
|
+
return t * t * (3.0 - 2.0 * t);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
void main() {
|
|
182
|
+
vSeed = aSeed;
|
|
183
|
+
vAccent = aAccent;
|
|
184
|
+
vForm = uForm;
|
|
185
|
+
|
|
186
|
+
// --- \u30AD\u30FC\u30D5\u30EC\u30FC\u30E0\u9593\u306E\u4F4D\u7F6E\u88DC\u9593\uFF08\u96A3\u63A5\u30DA\u30A2\u306E mix \u9023\u9396\uFF09 ---
|
|
187
|
+
vec3 pos = ${glyphPositionAttribute(0)};
|
|
188
|
+
${mixChain}
|
|
189
|
+
|
|
190
|
+
// \u9077\u79FB\u4E2D\uFF08\u98DB\u6563\u533A\u9593\uFF09\u306B\u5916\u5411\u304D\u30C9\u30EA\u30D5\u30C8\u3092\u8DB3\u3057\u3066\u30C0\u30A4\u30CA\u30DF\u30C3\u30AF\u306B\u3002
|
|
191
|
+
// \u65B9\u5411\u306F\u539F\u70B9\u304B\u3089\u306E\u5916\u5411\u304D\uFF08\u7279\u5B9A\u30AD\u30FC\u30D5\u30EC\u30FC\u30E0\u306B\u4F9D\u5B58\u3057\u306A\u3044\u4E00\u822C\u5F62\uFF09\u3002
|
|
192
|
+
float ph = aSeed * 6.2831;
|
|
193
|
+
vec3 dir = normalize(pos + 0.0001);
|
|
194
|
+
pos += dir * uBurst * (0.4 + aSeed * 0.6);
|
|
195
|
+
|
|
196
|
+
// \u30A2\u30A4\u30C9\u30EB\u306E\u6F02\u3044\uFF08\u6574\u5217\u6642 settle / \u5B57\u5F62\u6642 form \u3067\u5F31\u3081\u308B\uFF09\u3002
|
|
197
|
+
vSettle = uSettle;
|
|
198
|
+
float drift = (1.0 - uReduced) * (1.0 - uSettle * 0.9) * (1.0 - uForm);
|
|
199
|
+
pos.x += sin(uTime * 0.35 + ph) * 0.06 * drift;
|
|
200
|
+
pos.y += cos(uTime * 0.30 + ph * 1.7) * 0.06 * drift;
|
|
201
|
+
pos.z += sin(uTime * 0.27 + ph * 2.3) * 0.06 * drift;
|
|
202
|
+
|
|
203
|
+
// \u30EF\u30FC\u30EB\u30C9\u7A7A\u9593\u3067\u30DD\u30A4\u30F3\u30BF\u53CD\u767A\uFF08\u56DE\u8EE2\u5F8C\u306E\u898B\u305F\u76EE\u306B\u5408\u308F\u305B modelMatrix \u7D4C\u7531\uFF09\u3002
|
|
204
|
+
vec4 world = modelMatrix * vec4(pos, 1.0);
|
|
205
|
+
if (uPointerActive > 0.5) {
|
|
206
|
+
vec3 diff = world.xyz - uPointer;
|
|
207
|
+
float dist = length(diff);
|
|
208
|
+
float radius = 1.3;
|
|
209
|
+
float force = (1.0 - smoothstep(0.0, radius, dist)) * 0.55;
|
|
210
|
+
world.xyz += normalize(diff + 0.0001) * force;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
vec4 mvPosition = viewMatrix * world;
|
|
214
|
+
vDepth = -mvPosition.z;
|
|
215
|
+
gl_Position = projectionMatrix * mvPosition;
|
|
216
|
+
|
|
217
|
+
// --- \u900F\u660E\u5EA6: \u30B9\u30EF\u30C3\u30D7\u70B9\u307E\u3067\u4E0D\u53EF\u8996\u3001\u30B9\u30EF\u30C3\u30D7\u70B9\u3067\u5373\u30FB\u4E0D\u900F\u660E\uFF08\u30D5\u30A7\u30FC\u30C9\u7121\u3057\uFF09\u3002 ---
|
|
218
|
+
// DOM \u898B\u51FA\u3057\u3068\u540C\u4F4D\u7F6E\u30FB\u540C\u30B5\u30A4\u30BA\u3067\u4E00\u81F4\u3057\u3066\u3044\u308B\u305F\u3081\u3001\u77AC\u6642\u306E\u5207\u66FF\u304C\u300C\u6587\u5B57\u2192\u7C92\u5B50\u300D\u306B\u898B\u3048\u308B\u3002
|
|
219
|
+
// \u30D5\u30A3\u30CA\u30FC\u30EC(uResolve)\u3067\u7C92\u5B50\u3092\u7D20\u65E9\u304F\u6D88\u3057\u3001\u5B9F DOM \u6587\u5B57\u3078\u53D7\u3051\u6E21\u3059\u3002
|
|
220
|
+
vAlpha = uSwap * (1.0 - uResolve);
|
|
221
|
+
|
|
222
|
+
// \u70B9\u30B5\u30A4\u30BA\uFF08\u9060\u8FD1 + \u500B\u4F53\u5DEE\uFF09\u3002\u6574\u5217\u6642\u306F\u3084\u3084\u5747\u4E00\u30FB\u5C0F\u3055\u3081\u306B\u3057\u3066\u53EF\u8AAD\u6027\u3092\u4E0A\u3052\u308B\u3002
|
|
223
|
+
float sizeVar = mix(0.55 + aSeed * 0.9, 0.72 + aSeed * 0.35, uSettle);
|
|
224
|
+
// \u5B57\u5F62\u53CE\u675F\u6642\u306F\u96A3\u63A5\u7C92\u5B50\u3067\u9699\u9593\u3092\u57CB\u3081\u308B\u305F\u3081\u308F\u305A\u304B\u306B\u5927\u304D\u3081\uFF06\u5747\u4E00\u306B\u3002
|
|
225
|
+
sizeVar = mix(sizeVar, 0.95 + aSeed * 0.18, uForm);
|
|
226
|
+
float s = uSize * sizeVar;
|
|
227
|
+
gl_PointSize = s * uPixelRatio * (1.0 / -mvPosition.z);
|
|
228
|
+
gl_PointSize = clamp(gl_PointSize, 1.0, mix(7.0, 9.0, uForm) * uPixelRatio);
|
|
229
|
+
gl_PointSize = 10.0; // DEBUG4
|
|
230
|
+
}
|
|
231
|
+
`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
var FRAGMENT_SHADER = (
|
|
235
|
+
/* glsl */
|
|
236
|
+
`
|
|
237
|
+
uniform vec3 uColorInk;
|
|
238
|
+
uniform vec3 uColorAccent;
|
|
239
|
+
|
|
240
|
+
varying float vSeed;
|
|
241
|
+
varying float vAccent;
|
|
242
|
+
varying float vDepth;
|
|
243
|
+
varying float vForm;
|
|
244
|
+
varying float vAlpha;
|
|
245
|
+
varying float vSettle;
|
|
246
|
+
|
|
247
|
+
void main() {
|
|
248
|
+
// \u5186\u5F62\u306E\u30BD\u30D5\u30C8\u306A\u70B9\u3002\u4E2D\u5FC3\u3067 1\u3001\u7E01\u3067 0\uFF08smoothstep \u306F edge0<edge1 \u5FC5\u9808\u306A\u306E\u3067\u53CD\u8EE2\u3057\u3066\u4F7F\u3046\uFF09\u3002
|
|
249
|
+
vec2 uv = gl_PointCoord - 0.5;
|
|
250
|
+
float r = length(uv);
|
|
251
|
+
float alpha = 1.0 - smoothstep(0.12, 0.5, r);
|
|
252
|
+
if (alpha < 0.02) discard;
|
|
253
|
+
|
|
254
|
+
// \u4E3B\u4F53\u306F\u30A4\u30F3\u30AF\u3001\u4E00\u90E8\u306E\u7C92\u3060\u3051\u30A2\u30AF\u30BB\u30F3\u30C8\u8272\u3002\u5B57\u5F62\u53CE\u675F\u6642\u306F\u3084\u3084\u63A7\u3048\u3081\u306B\u3002
|
|
255
|
+
float accentAmt = vAccent * mix(0.85, 0.55, vForm);
|
|
256
|
+
vec3 col = mix(uColorInk, uColorAccent, accentAmt);
|
|
257
|
+
|
|
258
|
+
// \u4E00\u90E8\u306E\u7C92\u306B\u660E\u308B\u3044\u304D\u3089\u3081\u304D\uFF08\u98DB\u6563\u6642\u306B\u6620\u3048\u308B\uFF09\u3002\u6574\u5217\u6642\u306F\u63A7\u3048\u3081\u3002
|
|
259
|
+
float spark = step(0.94, vSeed);
|
|
260
|
+
col = mix(col, uColorAccent, spark * mix(0.45, 0.15, vSettle));
|
|
261
|
+
|
|
262
|
+
// \u5965\u884C\u304D\u3067\u6FC3\u6DE1\uFF08\u660E\u80CC\u666F\u3067\u306E\u8996\u8A8D\u6027\u78BA\u4FDD\u306E\u305F\u3081\u4E0B\u9650\u3092\u6301\u305F\u305B\u308B\uFF09\u3002
|
|
263
|
+
float floorFade = mix(0.45, 0.78, vSettle);
|
|
264
|
+
float depthFade = clamp(1.0 - (vDepth - 3.0) * 0.10, floorFade, 1.0);
|
|
265
|
+
|
|
266
|
+
// \u6574\u5217\u6642\u306F\u4E0D\u900F\u660E\u5BC4\u308A\u306B\u3057\u3066\u30A8\u30C3\u30B8\u3092\u7DE0\u3081\u308B\u3002
|
|
267
|
+
float a = alpha * depthFade * vAlpha;
|
|
268
|
+
a = mix(a, clamp(a * 1.3, 0.0, 1.0), vSettle);
|
|
269
|
+
|
|
270
|
+
gl_FragColor = vec4(col, a);
|
|
271
|
+
}
|
|
272
|
+
`
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// src/dom-overlay.ts
|
|
276
|
+
var ALPHA_THRESHOLD2 = 128;
|
|
277
|
+
function viewSizeAtZ0(viewportW, viewportH, fovDeg, cameraZ) {
|
|
278
|
+
const worldH = 2 * Math.tan(fovDeg * Math.PI / 360) * cameraZ;
|
|
279
|
+
const worldW = worldH * (viewportW / viewportH);
|
|
280
|
+
return { worldH, worldW };
|
|
281
|
+
}
|
|
282
|
+
function buildGlyphFromDOM(count, lines, opts) {
|
|
283
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const el = document.querySelector(opts.selector);
|
|
287
|
+
if (!el) return null;
|
|
288
|
+
const rect = el.getBoundingClientRect();
|
|
289
|
+
if (rect.width < 2 || rect.height < 2) return null;
|
|
290
|
+
const random = opts.random ?? Math.random;
|
|
291
|
+
const cs = window.getComputedStyle(el);
|
|
292
|
+
const fontSize = parseFloat(cs.fontSize) || 64;
|
|
293
|
+
let lineHeight = parseFloat(cs.lineHeight);
|
|
294
|
+
if (!isFinite(lineHeight) || lineHeight <= 0) lineHeight = fontSize * 1.1;
|
|
295
|
+
const letterSpacing = parseFloat(cs.letterSpacing) || 0;
|
|
296
|
+
const fontWeight = cs.fontWeight || "600";
|
|
297
|
+
const fontFamily = cs.fontFamily || "sans-serif";
|
|
298
|
+
const padL = parseFloat(cs.paddingLeft) || 0;
|
|
299
|
+
const padT = parseFloat(cs.paddingTop) || 0;
|
|
300
|
+
const contentLeft = rect.left + padL;
|
|
301
|
+
const contentTop = rect.top + padT;
|
|
302
|
+
const res = opts.resolution ?? 2;
|
|
303
|
+
const cw = Math.max(2, Math.ceil(rect.width * res));
|
|
304
|
+
const ch = Math.max(2, Math.ceil(rect.height * res));
|
|
305
|
+
const canvas = document.createElement("canvas");
|
|
306
|
+
canvas.width = cw;
|
|
307
|
+
canvas.height = ch;
|
|
308
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
309
|
+
if (!ctx) return null;
|
|
310
|
+
ctx.clearRect(0, 0, cw, ch);
|
|
311
|
+
ctx.scale(res, res);
|
|
312
|
+
ctx.fillStyle = "#000";
|
|
313
|
+
ctx.textAlign = "left";
|
|
314
|
+
ctx.textBaseline = "alphabetic";
|
|
315
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
316
|
+
if ("letterSpacing" in ctx) {
|
|
317
|
+
try {
|
|
318
|
+
ctx.letterSpacing = `${letterSpacing}px`;
|
|
319
|
+
} catch {
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const ascent = fontSize * (opts.ascentRatio ?? 0.82);
|
|
323
|
+
lines.forEach((line, i) => {
|
|
324
|
+
const lineTop = i * lineHeight;
|
|
325
|
+
ctx.fillText(line, 0, lineTop + (lineHeight - fontSize) / 2 + ascent);
|
|
326
|
+
});
|
|
327
|
+
const { data } = ctx.getImageData(0, 0, cw, ch);
|
|
328
|
+
const pts = [];
|
|
329
|
+
const step = opts.step ?? 2;
|
|
330
|
+
for (let y = 0; y < ch; y += step) {
|
|
331
|
+
for (let x = 0; x < cw; x += step) {
|
|
332
|
+
if (data[(y * cw + x) * 4 + 3] > ALPHA_THRESHOLD2) pts.push(x, y);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const filled = pts.length / 2;
|
|
336
|
+
if (filled === 0) return null;
|
|
337
|
+
const vpW = window.innerWidth;
|
|
338
|
+
const vpH = window.innerHeight;
|
|
339
|
+
const { worldW, worldH } = viewSizeAtZ0(vpW, vpH, opts.fovDeg, opts.cameraZ);
|
|
340
|
+
const pxToWorld = worldW / vpW;
|
|
341
|
+
const thickness = opts.thickness ?? 0.14;
|
|
342
|
+
const out = new Float32Array(count * 3);
|
|
343
|
+
for (let i = 0; i < count; i++) {
|
|
344
|
+
const idx = (Math.floor(i / count * filled) + i * 2654435761 % filled) % filled;
|
|
345
|
+
const cx = pts[idx * 2] / res;
|
|
346
|
+
const cy = pts[idx * 2 + 1] / res;
|
|
347
|
+
const sx = contentLeft + cx;
|
|
348
|
+
const sy = contentTop + cy;
|
|
349
|
+
const wx = (sx / vpW - 0.5) * worldW;
|
|
350
|
+
const wy = -(sy / vpH - 0.5) * worldH;
|
|
351
|
+
out[i * 3] = wx + (random() - 0.5) * pxToWorld * step;
|
|
352
|
+
out[i * 3 + 1] = wy + (random() - 0.5) * pxToWorld * step;
|
|
353
|
+
out[i * 3 + 2] = (random() - 0.5) * thickness;
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
function computeScreenRect(targets, viewportW, viewportH, visibleWorldW) {
|
|
358
|
+
let minX = Infinity;
|
|
359
|
+
let maxX = -Infinity;
|
|
360
|
+
let minY = Infinity;
|
|
361
|
+
let maxY = -Infinity;
|
|
362
|
+
for (let i = 0; i < targets.length; i += 3) {
|
|
363
|
+
const x = targets[i];
|
|
364
|
+
const y = targets[i + 1];
|
|
365
|
+
if (x < minX) minX = x;
|
|
366
|
+
if (x > maxX) maxX = x;
|
|
367
|
+
if (y < minY) minY = y;
|
|
368
|
+
if (y > maxY) maxY = y;
|
|
369
|
+
}
|
|
370
|
+
if (!isFinite(minX)) return null;
|
|
371
|
+
const worldW = visibleWorldW;
|
|
372
|
+
const worldH = visibleWorldW * (viewportH / viewportW);
|
|
373
|
+
const toScreenX = (wx) => (wx / worldW + 0.5) * viewportW;
|
|
374
|
+
const toScreenY = (wy) => (0.5 - wy / worldH) * viewportH;
|
|
375
|
+
const left = toScreenX(minX);
|
|
376
|
+
const right = toScreenX(maxX);
|
|
377
|
+
const top = toScreenY(maxY);
|
|
378
|
+
const bottom = toScreenY(minY);
|
|
379
|
+
return {
|
|
380
|
+
left,
|
|
381
|
+
top,
|
|
382
|
+
width: right - left,
|
|
383
|
+
height: bottom - top,
|
|
384
|
+
cx: (left + right) / 2,
|
|
385
|
+
cy: (top + bottom) / 2
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
var DEFAULT_TRIGGER_HEIGHT = 2;
|
|
389
|
+
function clamp01(x) {
|
|
390
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
391
|
+
}
|
|
392
|
+
function createScrollProgress(element) {
|
|
393
|
+
return () => {
|
|
394
|
+
if (element === null || typeof window === "undefined") return 0;
|
|
395
|
+
const rect = element.getBoundingClientRect();
|
|
396
|
+
const total = rect.height - window.innerHeight;
|
|
397
|
+
if (total <= 0) return 0;
|
|
398
|
+
return clamp01(-rect.top / total);
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function useScrollProgress(ref) {
|
|
402
|
+
return useCallback(() => {
|
|
403
|
+
const el = ref.current;
|
|
404
|
+
if (el === null || typeof window === "undefined") return 0;
|
|
405
|
+
const rect = el.getBoundingClientRect();
|
|
406
|
+
const total = rect.height - window.innerHeight;
|
|
407
|
+
if (total <= 0) return 0;
|
|
408
|
+
return clamp01(-rect.top / total);
|
|
409
|
+
}, [ref]);
|
|
410
|
+
}
|
|
411
|
+
var QUERY = "(prefers-reduced-motion: reduce)";
|
|
412
|
+
function prefersReducedMotion() {
|
|
413
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
return window.matchMedia(QUERY).matches;
|
|
417
|
+
}
|
|
418
|
+
function useReducedMotion() {
|
|
419
|
+
const [reduced, setReduced] = useState(false);
|
|
420
|
+
useEffect(() => {
|
|
421
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const mq = window.matchMedia(QUERY);
|
|
425
|
+
setReduced(mq.matches);
|
|
426
|
+
const onChange = () => setReduced(mq.matches);
|
|
427
|
+
mq.addEventListener("change", onChange);
|
|
428
|
+
return () => mq.removeEventListener("change", onChange);
|
|
429
|
+
}, []);
|
|
430
|
+
return reduced;
|
|
431
|
+
}
|
|
432
|
+
var DEFAULT_TEXT_FONT = "700 140px system-ui, 'Hiragino Sans', 'Noto Sans JP', sans-serif";
|
|
433
|
+
var DEFAULT_DENSE_FONT = "900 260px 'Helvetica Neue', Helvetica, Arial, sans-serif";
|
|
434
|
+
function isMobile() {
|
|
435
|
+
return typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches;
|
|
436
|
+
}
|
|
437
|
+
function smooth(a, b, x) {
|
|
438
|
+
const t = THREE.MathUtils.clamp((x - a) / (b - a), 0, 1);
|
|
439
|
+
return t * t * (3 - 2 * t);
|
|
440
|
+
}
|
|
441
|
+
function bump(x, c, prev, next) {
|
|
442
|
+
const rise = c <= 0 ? 1 : smooth(prev, c, x);
|
|
443
|
+
const fall = c >= 1 ? 1 : 1 - smooth(c, next, x);
|
|
444
|
+
return rise * fall;
|
|
445
|
+
}
|
|
446
|
+
function buildScatter(count, spread, random) {
|
|
447
|
+
const out = new Float32Array(count * 3);
|
|
448
|
+
for (let i = 0; i < count; i++) {
|
|
449
|
+
const r = (3 + Math.cbrt(random()) * 2.6) * spread;
|
|
450
|
+
const theta = random() * Math.PI * 2;
|
|
451
|
+
const phi = Math.acos(2 * random() - 1);
|
|
452
|
+
out[i * 3] = r * Math.sin(phi) * Math.cos(theta);
|
|
453
|
+
out[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta) * 0.8;
|
|
454
|
+
out[i * 3 + 2] = r * Math.cos(phi) * 0.9;
|
|
455
|
+
}
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
function buildKeyframeTargets(kf, count, ctx) {
|
|
459
|
+
if (kf.type === "scatter") {
|
|
460
|
+
return buildScatter(count, kf.spread ?? 1, Math.random);
|
|
461
|
+
}
|
|
462
|
+
const lines = kf.text.split("\n");
|
|
463
|
+
if (kf.domSelector) {
|
|
464
|
+
const dom = buildGlyphFromDOM(count, lines, {
|
|
465
|
+
selector: kf.domSelector,
|
|
466
|
+
fovDeg: ctx.cameraFov,
|
|
467
|
+
cameraZ: ctx.cameraZ
|
|
468
|
+
});
|
|
469
|
+
if (dom) return dom;
|
|
470
|
+
}
|
|
471
|
+
if (kf.dense) {
|
|
472
|
+
return buildDenseTextTargets(count, lines, {
|
|
473
|
+
font: kf.font ?? DEFAULT_DENSE_FONT,
|
|
474
|
+
worldW: kf.worldW ?? ctx.visW * (ctx.mobile ? 0.86 : 0.62),
|
|
475
|
+
offsetX: kf.offsetX ?? 0,
|
|
476
|
+
offsetY: kf.offsetY ?? 0,
|
|
477
|
+
thickness: 0.06,
|
|
478
|
+
cw: 1400,
|
|
479
|
+
ch: 440,
|
|
480
|
+
step: 1
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return buildTextTargets(count, lines, {
|
|
484
|
+
font: kf.font ?? DEFAULT_TEXT_FONT,
|
|
485
|
+
worldW: kf.worldW ?? ctx.visW * 0.7,
|
|
486
|
+
lineHeight: 178,
|
|
487
|
+
offsetX: kf.offsetX ?? 0,
|
|
488
|
+
offsetY: kf.offsetY ?? 0,
|
|
489
|
+
thickness: 0.16,
|
|
490
|
+
cw: 1280,
|
|
491
|
+
ch: 560,
|
|
492
|
+
step: 2
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
function GlyphPoints(props) {
|
|
496
|
+
const {
|
|
497
|
+
keyframes,
|
|
498
|
+
count,
|
|
499
|
+
colors,
|
|
500
|
+
cameraZ,
|
|
501
|
+
cameraFov,
|
|
502
|
+
pointer: pointerEnabled,
|
|
503
|
+
drag: dragEnabled,
|
|
504
|
+
getProgress,
|
|
505
|
+
timing,
|
|
506
|
+
resolveRef
|
|
507
|
+
} = props;
|
|
508
|
+
const pointsRef = useRef(null);
|
|
509
|
+
const matRef = useRef(null);
|
|
510
|
+
const { size, gl } = useThree();
|
|
511
|
+
const pointer = useRef({ x: 0, y: 0, active: 0 });
|
|
512
|
+
const rot = useRef({ x: 0, y: 0, vx: 0, vy: 0 });
|
|
513
|
+
const dragState = useRef({ down: false, lx: 0, ly: 0 });
|
|
514
|
+
const stage = useRef(0);
|
|
515
|
+
const guardRef = useRef(0);
|
|
516
|
+
const n = keyframes.length;
|
|
517
|
+
const times = useMemo(() => {
|
|
518
|
+
if (timing && timing.length === n) return timing.slice();
|
|
519
|
+
if (n <= 1) return [0];
|
|
520
|
+
const lastIsText = keyframes[n - 1]?.type === "text";
|
|
521
|
+
const end = lastIsText ? 0.85 : 1;
|
|
522
|
+
return Array.from({ length: n }, (_, i) => i / (n - 1) * end);
|
|
523
|
+
}, [timing, n, keyframes]);
|
|
524
|
+
const timeline = useMemo(() => {
|
|
525
|
+
const isText = keyframes.map((k) => k.type === "text");
|
|
526
|
+
const isScatter = keyframes.map((k) => k.type === "scatter");
|
|
527
|
+
const last = keyframes[n - 1];
|
|
528
|
+
const hasResolve = n >= 1 && last?.type === "text" && last.resolveToDom === true;
|
|
529
|
+
const resolveText = last?.type === "text" ? last.text.replace(/\n/g, " ") : "";
|
|
530
|
+
const swapAt = times[1] !== void 0 ? times[1] * 0.15 : 0;
|
|
531
|
+
return { isText, isScatter, hasResolve, resolveText, swapAt };
|
|
532
|
+
}, [keyframes, n, times]);
|
|
533
|
+
const built = useMemo(() => {
|
|
534
|
+
const geo = new THREE.BufferGeometry();
|
|
535
|
+
const seed = new Float32Array(count);
|
|
536
|
+
const accent = new Float32Array(count);
|
|
537
|
+
for (let i = 0; i < count; i++) {
|
|
538
|
+
seed[i] = Math.random();
|
|
539
|
+
accent[i] = Math.random() < colors.accentRatio ? 1 : 0;
|
|
540
|
+
}
|
|
541
|
+
const mobile = isMobile();
|
|
542
|
+
const vpW = typeof window !== "undefined" ? window.innerWidth : 1440;
|
|
543
|
+
const vpH = typeof window !== "undefined" ? window.innerHeight : 900;
|
|
544
|
+
const { worldW: visW } = viewSizeAtZ0(vpW, vpH, cameraFov, cameraZ);
|
|
545
|
+
const buffers = keyframes.map(
|
|
546
|
+
(kf) => buildKeyframeTargets(kf, count, {
|
|
547
|
+
visW,
|
|
548
|
+
mobile,
|
|
549
|
+
cameraFov,
|
|
550
|
+
cameraZ
|
|
551
|
+
})
|
|
552
|
+
);
|
|
553
|
+
buffers.forEach((buf, i) => {
|
|
554
|
+
geo.setAttribute(
|
|
555
|
+
glyphPositionAttribute(i),
|
|
556
|
+
new THREE.BufferAttribute(buf, 3)
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
geo.setAttribute("aSeed", new THREE.BufferAttribute(seed, 1));
|
|
560
|
+
geo.setAttribute("aAccent", new THREE.BufferAttribute(accent, 1));
|
|
561
|
+
const first = buffers[0] ?? new Float32Array(count * 3);
|
|
562
|
+
geo.setAttribute("position", new THREE.BufferAttribute(first.slice(), 3));
|
|
563
|
+
geo.computeBoundingSphere();
|
|
564
|
+
return { geo, buffers, visW, vpW, vpH };
|
|
565
|
+
}, [keyframes, count, colors.accentRatio, cameraFov, cameraZ]);
|
|
566
|
+
const vertexShader = useMemo(() => buildVertexShader(Math.max(n, 1)), [n]);
|
|
567
|
+
const uniforms = useMemo(
|
|
568
|
+
() => ({
|
|
569
|
+
uTime: { value: 0 },
|
|
570
|
+
uStage: { value: 0 },
|
|
571
|
+
uTimes: { value: times.slice() },
|
|
572
|
+
uForm: { value: 0 },
|
|
573
|
+
uSettle: { value: 0 },
|
|
574
|
+
uBurst: { value: 0 },
|
|
575
|
+
uSwap: { value: 0 },
|
|
576
|
+
uResolve: { value: 0 },
|
|
577
|
+
uReduced: { value: 0 },
|
|
578
|
+
uPointer: { value: new THREE.Vector3(0, 0, 0) },
|
|
579
|
+
uPointerActive: { value: 0 },
|
|
580
|
+
uSize: { value: 1 },
|
|
581
|
+
uPixelRatio: { value: 1 },
|
|
582
|
+
uColorInk: { value: colors.ink.clone() },
|
|
583
|
+
uColorAccent: { value: colors.accent.clone() }
|
|
584
|
+
}),
|
|
585
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
586
|
+
[vertexShader]
|
|
587
|
+
);
|
|
588
|
+
const positionOverlay = () => {
|
|
589
|
+
const el = resolveRef?.current;
|
|
590
|
+
if (!el || !timeline.hasResolve) return;
|
|
591
|
+
const finalBuf = built.buffers[n - 1];
|
|
592
|
+
if (!finalBuf) return;
|
|
593
|
+
const vpW = window.innerWidth;
|
|
594
|
+
const vpH = window.innerHeight;
|
|
595
|
+
const { worldW: visW } = viewSizeAtZ0(vpW, vpH, cameraFov, cameraZ);
|
|
596
|
+
const rect = computeScreenRect(finalBuf, vpW, vpH, visW);
|
|
597
|
+
if (!rect) return;
|
|
598
|
+
el.style.left = `${rect.left}px`;
|
|
599
|
+
el.style.top = `${rect.top}px`;
|
|
600
|
+
el.style.width = `${rect.width}px`;
|
|
601
|
+
el.style.height = `${rect.height}px`;
|
|
602
|
+
el.style.fontSize = `${rect.height * 0.92}px`;
|
|
603
|
+
};
|
|
604
|
+
const rebuildDomGlyphs = () => {
|
|
605
|
+
keyframes.forEach((kf, i) => {
|
|
606
|
+
if (kf.type !== "text" || !kf.domSelector) return;
|
|
607
|
+
const next = buildGlyphFromDOM(count, kf.text.split("\n"), {
|
|
608
|
+
selector: kf.domSelector,
|
|
609
|
+
fovDeg: cameraFov,
|
|
610
|
+
cameraZ
|
|
611
|
+
});
|
|
612
|
+
if (!next) return;
|
|
613
|
+
const attr = built.geo.getAttribute(glyphPositionAttribute(i));
|
|
614
|
+
if (!attr) return;
|
|
615
|
+
attr.array.set(next);
|
|
616
|
+
attr.needsUpdate = true;
|
|
617
|
+
});
|
|
618
|
+
positionOverlay();
|
|
619
|
+
};
|
|
620
|
+
useEffect(() => {
|
|
621
|
+
const raf = requestAnimationFrame(rebuildDomGlyphs);
|
|
622
|
+
const t1 = window.setTimeout(rebuildDomGlyphs, 120);
|
|
623
|
+
const t2 = window.setTimeout(rebuildDomGlyphs, 500);
|
|
624
|
+
const fonts = document.fonts;
|
|
625
|
+
if (fonts && typeof fonts.ready?.then === "function") {
|
|
626
|
+
fonts.ready.then(() => rebuildDomGlyphs()).catch(() => {
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const onResize = () => rebuildDomGlyphs();
|
|
630
|
+
window.addEventListener("resize", onResize, { passive: true });
|
|
631
|
+
return () => {
|
|
632
|
+
cancelAnimationFrame(raf);
|
|
633
|
+
window.clearTimeout(t1);
|
|
634
|
+
window.clearTimeout(t2);
|
|
635
|
+
window.removeEventListener("resize", onResize);
|
|
636
|
+
};
|
|
637
|
+
}, [built]);
|
|
638
|
+
useEffect(() => {
|
|
639
|
+
if (!pointerEnabled && !dragEnabled) return;
|
|
640
|
+
const el = gl.domElement;
|
|
641
|
+
const toNDC = (clientX, clientY) => {
|
|
642
|
+
const r = el.getBoundingClientRect();
|
|
643
|
+
return {
|
|
644
|
+
x: (clientX - r.left) / r.width * 2 - 1,
|
|
645
|
+
y: -((clientY - r.top) / r.height * 2 - 1)
|
|
646
|
+
};
|
|
647
|
+
};
|
|
648
|
+
const onMove = (e) => {
|
|
649
|
+
if (pointerEnabled) {
|
|
650
|
+
const ndc = toNDC(e.clientX, e.clientY);
|
|
651
|
+
pointer.current.x = ndc.x;
|
|
652
|
+
pointer.current.y = ndc.y;
|
|
653
|
+
pointer.current.active = 1;
|
|
654
|
+
}
|
|
655
|
+
if (dragEnabled && dragState.current.down) {
|
|
656
|
+
const dx = e.clientX - dragState.current.lx;
|
|
657
|
+
const dy = e.clientY - dragState.current.ly;
|
|
658
|
+
const grip = 1 - guardRef.current * 0.85;
|
|
659
|
+
rot.current.vy += dx * 35e-5 * grip;
|
|
660
|
+
rot.current.vx += dy * 25e-5 * grip;
|
|
661
|
+
dragState.current.lx = e.clientX;
|
|
662
|
+
dragState.current.ly = e.clientY;
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
const onDown = (e) => {
|
|
666
|
+
dragState.current.down = true;
|
|
667
|
+
dragState.current.lx = e.clientX;
|
|
668
|
+
dragState.current.ly = e.clientY;
|
|
669
|
+
};
|
|
670
|
+
const onUp = () => {
|
|
671
|
+
dragState.current.down = false;
|
|
672
|
+
};
|
|
673
|
+
const onLeave = () => {
|
|
674
|
+
pointer.current.active = 0;
|
|
675
|
+
dragState.current.down = false;
|
|
676
|
+
};
|
|
677
|
+
el.addEventListener("pointermove", onMove, { passive: true });
|
|
678
|
+
if (dragEnabled) el.addEventListener("pointerdown", onDown, { passive: true });
|
|
679
|
+
window.addEventListener("pointerup", onUp, { passive: true });
|
|
680
|
+
el.addEventListener("pointerleave", onLeave, { passive: true });
|
|
681
|
+
return () => {
|
|
682
|
+
el.removeEventListener("pointermove", onMove);
|
|
683
|
+
el.removeEventListener("pointerdown", onDown);
|
|
684
|
+
window.removeEventListener("pointerup", onUp);
|
|
685
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
686
|
+
};
|
|
687
|
+
}, [gl, pointerEnabled, dragEnabled]);
|
|
688
|
+
useEffect(() => {
|
|
689
|
+
const mat = matRef.current;
|
|
690
|
+
if (!mat) return;
|
|
691
|
+
const u = mat.uniforms;
|
|
692
|
+
u.uPixelRatio.value = Math.min(window.devicePixelRatio || 1, 2);
|
|
693
|
+
u.uSize.value = Math.min(size.height / 18, 26);
|
|
694
|
+
}, [size]);
|
|
695
|
+
useFrame((state, delta) => {
|
|
696
|
+
const p = pointsRef.current;
|
|
697
|
+
const mat = matRef.current;
|
|
698
|
+
if (!p || !mat) return;
|
|
699
|
+
const u = mat.uniforms;
|
|
700
|
+
const d = Math.min(delta, 0.05);
|
|
701
|
+
const raw = THREE.MathUtils.clamp(getProgress(), 0, 1);
|
|
702
|
+
stage.current = THREE.MathUtils.lerp(stage.current, raw, 0.1);
|
|
703
|
+
const s = stage.current;
|
|
704
|
+
let settle = 0;
|
|
705
|
+
let burst = 0;
|
|
706
|
+
for (let i = 0; i < n; i++) {
|
|
707
|
+
const c = times[i] ?? 0;
|
|
708
|
+
const prev = times[i - 1] ?? 0;
|
|
709
|
+
const next = times[i + 1] ?? 1;
|
|
710
|
+
const b = bump(s, c, prev, next);
|
|
711
|
+
if (timeline.isText[i]) settle = Math.max(settle, b);
|
|
712
|
+
if (timeline.isScatter[i]) burst = Math.max(burst, b);
|
|
713
|
+
}
|
|
714
|
+
let form = 0;
|
|
715
|
+
const lastIsText = timeline.isText[n - 1] === true;
|
|
716
|
+
if (lastIsText && n >= 2) {
|
|
717
|
+
form = smooth(times[n - 2] ?? 0, times[n - 1] ?? 1, s);
|
|
718
|
+
}
|
|
719
|
+
const guard = THREE.MathUtils.clamp(Math.max(settle, form), 0, 1);
|
|
720
|
+
guardRef.current = guard;
|
|
721
|
+
const swapped = raw >= timeline.swapAt ? 1 : 0;
|
|
722
|
+
const resolve = timeline.hasResolve ? smooth(0.9, 0.98, raw) : 0;
|
|
723
|
+
u.uTime.value = state.clock.elapsedTime;
|
|
724
|
+
u.uStage.value = s;
|
|
725
|
+
u.uForm.value = form;
|
|
726
|
+
u.uSettle.value = settle;
|
|
727
|
+
u.uBurst.value = burst * (1 - form);
|
|
728
|
+
u.uSwap.value = swapped;
|
|
729
|
+
u.uResolve.value = resolve;
|
|
730
|
+
u.uPointer.value.set(pointer.current.x * 3.2, pointer.current.y * 2, 0);
|
|
731
|
+
u.uPointerActive.value = pointer.current.active * (1 - guard);
|
|
732
|
+
rot.current.x += rot.current.vx;
|
|
733
|
+
rot.current.y += rot.current.vy;
|
|
734
|
+
rot.current.vx *= 0.92;
|
|
735
|
+
rot.current.vy *= 0.92;
|
|
736
|
+
rot.current.y += d * 0.05 * (1 - guard);
|
|
737
|
+
const recenter = 0.02 + guard * 0.2;
|
|
738
|
+
const wrappedY = Math.atan2(
|
|
739
|
+
Math.sin(rot.current.y),
|
|
740
|
+
Math.cos(rot.current.y)
|
|
741
|
+
);
|
|
742
|
+
rot.current.y = THREE.MathUtils.lerp(
|
|
743
|
+
rot.current.y,
|
|
744
|
+
rot.current.y - wrappedY,
|
|
745
|
+
recenter
|
|
746
|
+
);
|
|
747
|
+
rot.current.x = THREE.MathUtils.lerp(rot.current.x, 0, 0.04 + guard * 0.14);
|
|
748
|
+
p.rotation.x = rot.current.x;
|
|
749
|
+
p.rotation.y = rot.current.y;
|
|
750
|
+
const overlay = resolveRef?.current;
|
|
751
|
+
if (overlay && timeline.hasResolve) overlay.style.opacity = String(resolve);
|
|
752
|
+
});
|
|
753
|
+
return /* @__PURE__ */ jsx("points", { ref: pointsRef, geometry: built.geo, frustumCulled: false, children: /* @__PURE__ */ jsx(
|
|
754
|
+
"shaderMaterial",
|
|
755
|
+
{
|
|
756
|
+
ref: matRef,
|
|
757
|
+
uniforms,
|
|
758
|
+
transparent: true,
|
|
759
|
+
depthWrite: false,
|
|
760
|
+
blending: THREE.NormalBlending,
|
|
761
|
+
vertexShader,
|
|
762
|
+
fragmentShader: FRAGMENT_SHADER
|
|
763
|
+
}
|
|
764
|
+
) });
|
|
765
|
+
}
|
|
766
|
+
var DEFAULT_INK = "#1b2330";
|
|
767
|
+
var DEFAULT_ACCENT = "#0055ff";
|
|
768
|
+
var DEFAULT_ACCENT_RATIO = 0.18;
|
|
769
|
+
var DEFAULT_COUNT_DESKTOP = 11e3;
|
|
770
|
+
var DEFAULT_COUNT_MOBILE = 5200;
|
|
771
|
+
var DEFAULT_CAMERA_Z = 7;
|
|
772
|
+
var DEFAULT_CAMERA_FOV = 42;
|
|
773
|
+
var DEFAULT_DPR = [1, 1.75];
|
|
774
|
+
function clamp012(x) {
|
|
775
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
776
|
+
}
|
|
777
|
+
function isWebGLAvailable() {
|
|
778
|
+
if (typeof window === "undefined") return false;
|
|
779
|
+
try {
|
|
780
|
+
const canvas = document.createElement("canvas");
|
|
781
|
+
return Boolean(
|
|
782
|
+
window.WebGLRenderingContext && (canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
|
|
783
|
+
);
|
|
784
|
+
} catch {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function GlyphDust(props) {
|
|
789
|
+
const {
|
|
790
|
+
keyframes,
|
|
791
|
+
driver = { type: "scroll" },
|
|
792
|
+
colors,
|
|
793
|
+
count,
|
|
794
|
+
dpr = DEFAULT_DPR,
|
|
795
|
+
interaction,
|
|
796
|
+
camera,
|
|
797
|
+
timing,
|
|
798
|
+
fallback = null,
|
|
799
|
+
className
|
|
800
|
+
} = props;
|
|
801
|
+
const reduced = useReducedMotion();
|
|
802
|
+
const [webgl, setWebgl] = useState(true);
|
|
803
|
+
useEffect(() => {
|
|
804
|
+
setWebgl(isWebGLAvailable());
|
|
805
|
+
}, []);
|
|
806
|
+
const [mobile, setMobile] = useState(false);
|
|
807
|
+
useEffect(() => {
|
|
808
|
+
if (typeof window === "undefined") return;
|
|
809
|
+
setMobile(window.matchMedia("(max-width: 768px)").matches);
|
|
810
|
+
}, []);
|
|
811
|
+
const wrapperRef = useRef(null);
|
|
812
|
+
const resolveRef = useRef(null);
|
|
813
|
+
const manualRef = useRef(0);
|
|
814
|
+
if (driver.type === "manual") manualRef.current = clamp012(driver.progress);
|
|
815
|
+
const getProgress = useCallback(() => {
|
|
816
|
+
if (driver.type === "manual") return manualRef.current;
|
|
817
|
+
const el = wrapperRef.current;
|
|
818
|
+
if (el === null || typeof window === "undefined") return 0;
|
|
819
|
+
const rect = el.getBoundingClientRect();
|
|
820
|
+
const total = rect.height - window.innerHeight;
|
|
821
|
+
if (total <= 0) return 0;
|
|
822
|
+
return clamp012(-rect.top / total);
|
|
823
|
+
}, [driver.type]);
|
|
824
|
+
const resolvedColors = useMemo(
|
|
825
|
+
() => ({
|
|
826
|
+
ink: new THREE.Color(colors?.ink ?? DEFAULT_INK),
|
|
827
|
+
accent: new THREE.Color(colors?.accent ?? DEFAULT_ACCENT),
|
|
828
|
+
accentRatio: colors?.accentRatio ?? DEFAULT_ACCENT_RATIO
|
|
829
|
+
}),
|
|
830
|
+
[colors?.ink, colors?.accent, colors?.accentRatio]
|
|
831
|
+
);
|
|
832
|
+
const particleCount = mobile ? count?.mobile ?? DEFAULT_COUNT_MOBILE : count?.desktop ?? DEFAULT_COUNT_DESKTOP;
|
|
833
|
+
const cameraZ = camera?.z ?? DEFAULT_CAMERA_Z;
|
|
834
|
+
const cameraFov = camera?.fov ?? DEFAULT_CAMERA_FOV;
|
|
835
|
+
const pointerEnabled = interaction?.pointer ?? true;
|
|
836
|
+
const dragEnabled = interaction?.drag ?? true;
|
|
837
|
+
const finalKf = keyframes[keyframes.length - 1];
|
|
838
|
+
const hasResolve = finalKf?.type === "text" && finalKf.resolveToDom === true;
|
|
839
|
+
const resolveText = finalKf?.type === "text" ? finalKf.text.replace(/\n/g, " ") : "";
|
|
840
|
+
if (reduced || !webgl) {
|
|
841
|
+
return /* @__PURE__ */ jsx(Fragment, { children: fallback });
|
|
842
|
+
}
|
|
843
|
+
const scene = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
844
|
+
/* @__PURE__ */ jsx(
|
|
845
|
+
Canvas,
|
|
846
|
+
{
|
|
847
|
+
dpr,
|
|
848
|
+
camera: { position: [0, 0, cameraZ], fov: cameraFov },
|
|
849
|
+
gl: { antialias: true, alpha: true, powerPreference: "high-performance" },
|
|
850
|
+
frameloop: "always",
|
|
851
|
+
style: { width: "100%", height: "100%" },
|
|
852
|
+
children: /* @__PURE__ */ jsx(
|
|
853
|
+
GlyphPoints,
|
|
854
|
+
{
|
|
855
|
+
keyframes,
|
|
856
|
+
count: particleCount,
|
|
857
|
+
colors: resolvedColors,
|
|
858
|
+
cameraZ,
|
|
859
|
+
cameraFov,
|
|
860
|
+
pointer: pointerEnabled,
|
|
861
|
+
drag: dragEnabled,
|
|
862
|
+
getProgress,
|
|
863
|
+
timing,
|
|
864
|
+
resolveRef: hasResolve ? resolveRef : void 0
|
|
865
|
+
}
|
|
866
|
+
)
|
|
867
|
+
}
|
|
868
|
+
),
|
|
869
|
+
hasResolve ? /* @__PURE__ */ jsx(
|
|
870
|
+
"div",
|
|
871
|
+
{
|
|
872
|
+
ref: resolveRef,
|
|
873
|
+
"aria-hidden": "true",
|
|
874
|
+
style: {
|
|
875
|
+
position: "absolute",
|
|
876
|
+
opacity: 0,
|
|
877
|
+
display: "flex",
|
|
878
|
+
alignItems: "center",
|
|
879
|
+
justifyContent: "center",
|
|
880
|
+
lineHeight: 1,
|
|
881
|
+
whiteSpace: "nowrap",
|
|
882
|
+
fontWeight: 900,
|
|
883
|
+
color: colors?.ink ?? DEFAULT_INK,
|
|
884
|
+
pointerEvents: "none"
|
|
885
|
+
},
|
|
886
|
+
children: resolveText
|
|
887
|
+
}
|
|
888
|
+
) : null
|
|
889
|
+
] });
|
|
890
|
+
if (driver.type === "manual") {
|
|
891
|
+
return /* @__PURE__ */ jsx(
|
|
892
|
+
"div",
|
|
893
|
+
{
|
|
894
|
+
className,
|
|
895
|
+
style: { position: "relative", width: "100%", height: "100%" },
|
|
896
|
+
children: scene
|
|
897
|
+
}
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
const triggerHeight = driver.triggerHeight ?? DEFAULT_TRIGGER_HEIGHT;
|
|
901
|
+
return /* @__PURE__ */ jsx(
|
|
902
|
+
"div",
|
|
903
|
+
{
|
|
904
|
+
ref: wrapperRef,
|
|
905
|
+
className,
|
|
906
|
+
style: { position: "relative", height: `${triggerHeight * 100}vh` },
|
|
907
|
+
children: /* @__PURE__ */ jsx(
|
|
908
|
+
"div",
|
|
909
|
+
{
|
|
910
|
+
style: {
|
|
911
|
+
position: "sticky",
|
|
912
|
+
top: 0,
|
|
913
|
+
height: "100vh",
|
|
914
|
+
width: "100%",
|
|
915
|
+
overflow: "hidden"
|
|
916
|
+
},
|
|
917
|
+
children: scene
|
|
918
|
+
}
|
|
919
|
+
)
|
|
920
|
+
}
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/index.ts
|
|
925
|
+
var VERSION = "0.1.0";
|
|
926
|
+
|
|
927
|
+
export { DEFAULT_TRIGGER_HEIGHT, FRAGMENT_SHADER, GLYPH_POSITION_ATTRIBUTE_PREFIX, GlyphDust, VERSION, buildDenseTextTargets, buildGlyphFromDOM, buildTextTargets, buildVertexShader, computeScreenRect, createScrollProgress, glyphPositionAttribute, prefersReducedMotion, useReducedMotion, useScrollProgress, viewSizeAtZ0 };
|
|
928
|
+
//# sourceMappingURL=index.js.map
|
|
929
|
+
//# sourceMappingURL=index.js.map
|