glyphdust 0.1.0 → 0.2.1
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/CHANGELOG.md +47 -0
- package/README.md +59 -5
- package/dist/index.cjs +227 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -6
- package/dist/index.d.ts +65 -6
- package/dist/index.js +227 -27
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **glyphdust** are documented here.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/), and the
|
|
5
|
+
project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.2.1] — 2026-06-26
|
|
8
|
+
|
|
9
|
+
Flexibility & polish release. Glyphdust is no longer scroll-and-hero only — it now
|
|
10
|
+
drops into any box, plays without scroll, and ships tasteful presets you can override.
|
|
11
|
+
**Defaults reproduce 0.2.0 exactly**, so upgrading is non-breaking.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`autoplay` driver** — time-based progress with no scroll choreography. Fits its
|
|
16
|
+
parent box and starts when scrolled into view (`playOnView`, default on). Options:
|
|
17
|
+
`duration`, `delay`, `loop`, `pingpong`, `playOnView`. Exposed
|
|
18
|
+
`computeAutoplayProgress()` for custom rigs.
|
|
19
|
+
- **`preset` prop** — `"default" | "minimal" | "lively" | "glow"`: a tasteful bundle
|
|
20
|
+
of look + motion.
|
|
21
|
+
- **`style` prop** — per-field overrides on top of the preset:
|
|
22
|
+
`size`, `blend` (`"normal" | "additive"`), `drift`, `sparkle`. Backed by new shader
|
|
23
|
+
uniforms (`uSizeScale`, `uDrift`, `uSparkle`); `additive` enables glow blending for
|
|
24
|
+
dark backgrounds.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Particles render finer and crisper on high-DPI screens: point-size base ×0.62,
|
|
29
|
+
clamp lowered to 4–5 px, and `devicePixelRatio` cap raised 2 → 3. (Validated on the
|
|
30
|
+
LINNO corporate site.)
|
|
31
|
+
- Scroll follow no longer lags: stage progress is applied directly instead of an
|
|
32
|
+
internal lerp. Add inertia in your driver (e.g. Lenis) if you want it.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- No more blank gap when the **first keyframe is text** — particles now start in the
|
|
37
|
+
formed glyph and dissolve outward, instead of appearing only after the real text
|
|
38
|
+
fades.
|
|
39
|
+
- `VERSION` export corrected (was a stale `"0.1.0"`).
|
|
40
|
+
|
|
41
|
+
## [0.2.0] — 2026-06-23
|
|
42
|
+
|
|
43
|
+
- Resolve to real DOM elements with pixel alignment; scrollbar & baseline fixes.
|
|
44
|
+
|
|
45
|
+
## [0.1.0]
|
|
46
|
+
|
|
47
|
+
- Initial public release: text → particles → glyph → real-text resolve, scroll-driven.
|
package/README.md
CHANGED
|
@@ -85,7 +85,34 @@ export function Hero() {
|
|
|
85
85
|
- `domSelector` makes particles land exactly on an existing element's box and font — no jump on cross-fade.
|
|
86
86
|
- `resolveToDom` on the final keyframe hands off from particles to crisp real text.
|
|
87
87
|
|
|
88
|
-
###
|
|
88
|
+
### Just drop in text — no scroll choreography
|
|
89
|
+
|
|
90
|
+
For anything that isn't a full-screen scroll hero, use the **`autoplay`** driver. It
|
|
91
|
+
fits its parent box and plays once when it scrolls into view — drop it anywhere and it
|
|
92
|
+
just animates:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<div style={{ width: 480, height: 220 }}>
|
|
96
|
+
<GlyphDust
|
|
97
|
+
driver={{ type: "autoplay", duration: 3.5 }} // loop / pingpong / delay too
|
|
98
|
+
preset="minimal" // tasteful out of the box
|
|
99
|
+
keyframes={[
|
|
100
|
+
{ type: "scatter" },
|
|
101
|
+
{ type: "text", text: "glyphdust", dense: true },
|
|
102
|
+
]}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Pick a look with presets (then tweak)
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
<GlyphDust preset="glow" style={{ size: 1.2 }} keyframes={[/* … */]} />
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`preset` is a tasteful starting point; `style` overrides just the fields you name.
|
|
114
|
+
|
|
115
|
+
### Drive it yourself (manual)
|
|
89
116
|
|
|
90
117
|
```tsx
|
|
91
118
|
const [p, setP] = useState(0); // 0 → 1 from time, GSAP, a slider, anything
|
|
@@ -105,7 +132,9 @@ const [p, setP] = useState(0); // 0 → 1 from time, GSAP, a slider, anything
|
|
|
105
132
|
| prop | type | default | description |
|
|
106
133
|
|---|---|---|---|
|
|
107
134
|
| `keyframes` | `Keyframe[]` | — (required) | The animation timeline. Minimum 1; typically `text → scatter → text`. |
|
|
108
|
-
| `driver` | `DriverConfig` | `{ type: "scroll" }` | Progress source: `scroll` or `manual`. |
|
|
135
|
+
| `driver` | `DriverConfig` | `{ type: "scroll" }` | Progress source: `scroll`, `autoplay`, or `manual`. |
|
|
136
|
+
| `preset` | `GlyphPreset` | `"default"` | Look/motion preset: `default`, `minimal`, `lively`, `glow`. |
|
|
137
|
+
| `style` | `GlyphStyle` | — | Per-field overrides on top of `preset` (see below). |
|
|
109
138
|
| `colors` | `GlyphColors` | see below | Particle ink / accent colors. |
|
|
110
139
|
| `count` | `GlyphCount` | `{ desktop: 11000, mobile: 5200 }` | Particle count per device class. |
|
|
111
140
|
| `timing` | `number[]` | even spacing | Normalized time `0..1` per keyframe (interpolation boundaries). Length must match `keyframes`. |
|
|
@@ -142,10 +171,35 @@ type Keyframe = TextKeyframe | ScatterKeyframe;
|
|
|
142
171
|
### Drivers
|
|
143
172
|
|
|
144
173
|
```ts
|
|
145
|
-
{ type: "scroll", triggerHeight?: number } // default triggerHeight: 2 (×100vh)
|
|
174
|
+
{ type: "scroll", triggerHeight?: number } // full-screen sticky hero. default triggerHeight: 2 (×100vh)
|
|
146
175
|
{ type: "manual", progress: number } // you supply 0..1
|
|
176
|
+
{ type: "autoplay", // time-based; fits its parent box
|
|
177
|
+
duration?: number, // seconds for 0→1 (default 4)
|
|
178
|
+
delay?: number, // start delay (default 0)
|
|
179
|
+
loop?: boolean, // repeat (default false)
|
|
180
|
+
pingpong?: boolean, // 0→1→0 when looping (default false)
|
|
181
|
+
playOnView?: boolean, // start when scrolled into view (default true)
|
|
182
|
+
}
|
|
147
183
|
```
|
|
148
184
|
|
|
185
|
+
`scroll` builds a tall sticky wrapper for a full-screen hero. `manual` and `autoplay`
|
|
186
|
+
simply **fill their parent**, so you can place them in any sized container.
|
|
187
|
+
|
|
188
|
+
### Presets & style
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
preset: "default" | "minimal" | "lively" | "glow"
|
|
192
|
+
|
|
193
|
+
style: {
|
|
194
|
+
size?: number, // point-size multiplier (default 1)
|
|
195
|
+
blend?: "normal" | "additive", // "additive" = glow, for dark backgrounds
|
|
196
|
+
drift?: number, // idle/scatter wander 0..1 (default 1; 0 = still)
|
|
197
|
+
sparkle?: number, // sparkle strength 0..1 (default 1; 0 = off)
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`style` always wins over `preset`. Defaults reproduce the original look exactly.
|
|
202
|
+
|
|
149
203
|
### Colors
|
|
150
204
|
|
|
151
205
|
```ts
|
|
@@ -156,7 +210,7 @@ type Keyframe = TextKeyframe | ScatterKeyframe;
|
|
|
156
210
|
|
|
157
211
|
### Low-level helpers
|
|
158
212
|
|
|
159
|
-
For custom rigs, the building blocks are exported too: `buildTextTargets`, `buildDenseTextTargets`, `buildVertexShader`, `FRAGMENT_SHADER`, `createScrollProgress`, `useScrollProgress`, `useReducedMotion`, `prefersReducedMotion`, `viewSizeAtZ0`, `buildGlyphFromDOM`, `computeScreenRect`.
|
|
213
|
+
For custom rigs, the building blocks are exported too: `buildTextTargets`, `buildDenseTextTargets`, `buildVertexShader`, `FRAGMENT_SHADER`, `createScrollProgress`, `useScrollProgress`, `computeAutoplayProgress`, `useReducedMotion`, `prefersReducedMotion`, `viewSizeAtZ0`, `buildGlyphFromDOM`, `computeScreenRect`.
|
|
160
214
|
|
|
161
215
|
---
|
|
162
216
|
|
|
@@ -171,7 +225,7 @@ For custom rigs, the building blocks are exported too: `buildTextTargets`, `buil
|
|
|
171
225
|
|
|
172
226
|
## Status
|
|
173
227
|
|
|
174
|
-
`0.1
|
|
228
|
+
`0.2.1` — the component and API above are implemented and demoed (see [`examples/`](./examples)) and [`CHANGELOG.md`](./CHANGELOG.md). Published from [LINNO](https://linno.co.jp). Semantic-versioned; expect minor API polish before `1.0`.
|
|
175
229
|
|
|
176
230
|
## License
|
|
177
231
|
|
package/dist/index.cjs
CHANGED
|
@@ -182,6 +182,8 @@ function buildVertexShader(keyframeCount) {
|
|
|
182
182
|
uniform vec3 uPointer;
|
|
183
183
|
uniform float uPointerActive;
|
|
184
184
|
uniform float uSize;
|
|
185
|
+
uniform float uSizeScale;
|
|
186
|
+
uniform float uDrift;
|
|
185
187
|
uniform float uPixelRatio;
|
|
186
188
|
|
|
187
189
|
${attributeDecls}
|
|
@@ -217,7 +219,7 @@ ${mixChain}
|
|
|
217
219
|
|
|
218
220
|
// \u30A2\u30A4\u30C9\u30EB\u306E\u6F02\u3044\uFF08\u6574\u5217\u6642 settle / \u5B57\u5F62\u6642 form \u3067\u5F31\u3081\u308B\uFF09\u3002
|
|
219
221
|
vSettle = uSettle;
|
|
220
|
-
float drift = (1.0 - uReduced) * (1.0 - uSettle * 0.9) * (1.0 - uForm);
|
|
222
|
+
float drift = (1.0 - uReduced) * (1.0 - uSettle * 0.9) * (1.0 - uForm) * uDrift;
|
|
221
223
|
pos.x += sin(uTime * 0.35 + ph) * 0.06 * drift;
|
|
222
224
|
pos.y += cos(uTime * 0.30 + ph * 1.7) * 0.06 * drift;
|
|
223
225
|
pos.z += sin(uTime * 0.27 + ph * 2.3) * 0.06 * drift;
|
|
@@ -245,10 +247,11 @@ ${mixChain}
|
|
|
245
247
|
float sizeVar = mix(0.55 + aSeed * 0.9, 0.72 + aSeed * 0.35, uSettle);
|
|
246
248
|
// \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
|
|
247
249
|
sizeVar = mix(sizeVar, 0.95 + aSeed * 0.18, uForm);
|
|
248
|
-
|
|
250
|
+
// \u9AD8 dpr \u74B0\u5883\u3067\u306F\u5C0F\u7C92\u30FB\u4E0A\u9650\u4F4E\u3081\u306E\u65B9\u304C\u30A8\u30C3\u30B8\u304C\u7DE0\u307E\u308A\u9AD8\u7CBE\u7D30\u306B\u898B\u3048\u308B
|
|
251
|
+
// \uFF08\u30B3\u30FC\u30DD\u30EC\u30FC\u30C8\u30B5\u30A4\u30C8\u5B9F\u88C5\u3067\u5B9F\u8A3C\u30020.62 \u3068 clamp 4\u301C5 \u304C\u6700\u3082\u300C\u971E\u307E\u306A\u3044\u300D\uFF09\u3002
|
|
252
|
+
float s = uSize * sizeVar * 0.62 * uSizeScale;
|
|
249
253
|
gl_PointSize = s * uPixelRatio * (1.0 / -mvPosition.z);
|
|
250
|
-
gl_PointSize = clamp(gl_PointSize, 1.0, mix(
|
|
251
|
-
gl_PointSize = 10.0; // DEBUG4
|
|
254
|
+
gl_PointSize = clamp(gl_PointSize, 1.0, mix(4.0, 5.0, uForm) * uPixelRatio);
|
|
252
255
|
}
|
|
253
256
|
`
|
|
254
257
|
);
|
|
@@ -258,6 +261,7 @@ var FRAGMENT_SHADER = (
|
|
|
258
261
|
`
|
|
259
262
|
uniform vec3 uColorInk;
|
|
260
263
|
uniform vec3 uColorAccent;
|
|
264
|
+
uniform float uSparkle;
|
|
261
265
|
|
|
262
266
|
varying float vSeed;
|
|
263
267
|
varying float vAccent;
|
|
@@ -279,7 +283,7 @@ var FRAGMENT_SHADER = (
|
|
|
279
283
|
|
|
280
284
|
// \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
|
|
281
285
|
float spark = step(0.94, vSeed);
|
|
282
|
-
col = mix(col, uColorAccent, spark * mix(0.45, 0.15, vSettle));
|
|
286
|
+
col = mix(col, uColorAccent, spark * mix(0.45, 0.15, vSettle) * uSparkle);
|
|
283
287
|
|
|
284
288
|
// \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
|
|
285
289
|
float floorFade = mix(0.45, 0.78, vSettle);
|
|
@@ -341,10 +345,15 @@ function buildGlyphFromDOM(count, lines, opts) {
|
|
|
341
345
|
} catch {
|
|
342
346
|
}
|
|
343
347
|
}
|
|
344
|
-
const
|
|
348
|
+
const fm = ctx.measureText(lines[0] ?? "M");
|
|
349
|
+
const fbAsc = fm.fontBoundingBoxAscent;
|
|
350
|
+
const fbDesc = fm.fontBoundingBoxDescent;
|
|
351
|
+
const useMetrics = Number.isFinite(fbAsc) && Number.isFinite(fbDesc);
|
|
352
|
+
const fallbackAscent = fontSize * (opts.ascentRatio ?? 0.82);
|
|
345
353
|
lines.forEach((line, i) => {
|
|
346
354
|
const lineTop = i * lineHeight;
|
|
347
|
-
|
|
355
|
+
const baseline = useMetrics ? lineTop + (lineHeight - (fbAsc + fbDesc)) / 2 + fbAsc : lineTop + (lineHeight - fontSize) / 2 + fallbackAscent;
|
|
356
|
+
ctx.fillText(line, 0, baseline);
|
|
348
357
|
});
|
|
349
358
|
const { data } = ctx.getImageData(0, 0, cw, ch);
|
|
350
359
|
const pts = [];
|
|
@@ -356,8 +365,8 @@ function buildGlyphFromDOM(count, lines, opts) {
|
|
|
356
365
|
}
|
|
357
366
|
const filled = pts.length / 2;
|
|
358
367
|
if (filled === 0) return null;
|
|
359
|
-
const vpW = window.innerWidth;
|
|
360
|
-
const vpH = window.innerHeight;
|
|
368
|
+
const vpW = opts.viewportW ?? window.innerWidth;
|
|
369
|
+
const vpH = opts.viewportH ?? window.innerHeight;
|
|
361
370
|
const { worldW, worldH } = viewSizeAtZ0(vpW, vpH, opts.fovDeg, opts.cameraZ);
|
|
362
371
|
const pxToWorld = worldW / vpW;
|
|
363
372
|
const thickness = opts.thickness ?? 0.14;
|
|
@@ -408,6 +417,21 @@ function computeScreenRect(targets, viewportW, viewportH, visibleWorldW) {
|
|
|
408
417
|
};
|
|
409
418
|
}
|
|
410
419
|
var DEFAULT_TRIGGER_HEIGHT = 2;
|
|
420
|
+
function triangle(x) {
|
|
421
|
+
const t = x % 2;
|
|
422
|
+
return t <= 1 ? t : 2 - t;
|
|
423
|
+
}
|
|
424
|
+
function computeAutoplayProgress(elapsedSec, cfg) {
|
|
425
|
+
const duration = cfg.duration && cfg.duration > 0 ? cfg.duration : 4;
|
|
426
|
+
const delay = cfg.delay && cfg.delay > 0 ? cfg.delay : 0;
|
|
427
|
+
const t = elapsedSec - delay;
|
|
428
|
+
if (t <= 0) return 0;
|
|
429
|
+
const raw = t / duration;
|
|
430
|
+
if (cfg.loop) {
|
|
431
|
+
return cfg.pingpong ? triangle(raw) : raw % 1;
|
|
432
|
+
}
|
|
433
|
+
return clamp01(raw);
|
|
434
|
+
}
|
|
411
435
|
function clamp01(x) {
|
|
412
436
|
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
413
437
|
}
|
|
@@ -519,15 +543,18 @@ function GlyphPoints(props) {
|
|
|
519
543
|
keyframes,
|
|
520
544
|
count,
|
|
521
545
|
colors,
|
|
546
|
+
style,
|
|
522
547
|
cameraZ,
|
|
523
548
|
cameraFov,
|
|
524
549
|
pointer: pointerEnabled,
|
|
525
550
|
drag: dragEnabled,
|
|
526
551
|
getProgress,
|
|
527
552
|
timing,
|
|
528
|
-
resolveRef
|
|
553
|
+
resolveRef,
|
|
554
|
+
resolveDomSelector
|
|
529
555
|
} = props;
|
|
530
556
|
const pointsRef = react.useRef(null);
|
|
557
|
+
const resolveDomElRef = react.useRef(null);
|
|
531
558
|
const matRef = react.useRef(null);
|
|
532
559
|
const { size, gl } = fiber.useThree();
|
|
533
560
|
const pointer = react.useRef({ x: 0, y: 0, active: 0 });
|
|
@@ -600,6 +627,9 @@ function GlyphPoints(props) {
|
|
|
600
627
|
uPointer: { value: new THREE__namespace.Vector3(0, 0, 0) },
|
|
601
628
|
uPointerActive: { value: 0 },
|
|
602
629
|
uSize: { value: 1 },
|
|
630
|
+
uSizeScale: { value: style.size },
|
|
631
|
+
uDrift: { value: style.drift },
|
|
632
|
+
uSparkle: { value: style.sparkle },
|
|
603
633
|
uPixelRatio: { value: 1 },
|
|
604
634
|
uColorInk: { value: colors.ink.clone() },
|
|
605
635
|
uColorAccent: { value: colors.accent.clone() }
|
|
@@ -617,11 +647,93 @@ function GlyphPoints(props) {
|
|
|
617
647
|
const { worldW: visW } = viewSizeAtZ0(vpW, vpH, cameraFov, cameraZ);
|
|
618
648
|
const rect = computeScreenRect(finalBuf, vpW, vpH, visW);
|
|
619
649
|
if (!rect) return;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
650
|
+
const finalKf = keyframes[n - 1];
|
|
651
|
+
const fontStr = finalKf?.type === "text" && finalKf.font ? finalKf.font : DEFAULT_DENSE_FONT;
|
|
652
|
+
const fontMatch = fontStr.match(/^\s*(\d+)\s+[\d.]+px\s+(.+)$/);
|
|
653
|
+
const fontWeight = fontMatch?.[1] ?? "900";
|
|
654
|
+
const fontFamily = fontMatch?.[2] ?? "sans-serif";
|
|
655
|
+
const text = timeline.resolveText;
|
|
656
|
+
const ctx = document.createElement("canvas").getContext("2d", {
|
|
657
|
+
willReadFrequently: true
|
|
658
|
+
});
|
|
659
|
+
let positioned = false;
|
|
660
|
+
if (ctx && text) {
|
|
661
|
+
const baseSize = 200;
|
|
662
|
+
ctx.font = `${fontWeight} ${baseSize}px ${fontFamily}`;
|
|
663
|
+
const advBase = ctx.measureText(text).width;
|
|
664
|
+
const pad = Math.ceil(baseSize * 0.6);
|
|
665
|
+
const cw = Math.ceil(advBase + pad * 2);
|
|
666
|
+
const ch = Math.ceil(baseSize * 1.8);
|
|
667
|
+
const oc = document.createElement("canvas");
|
|
668
|
+
oc.width = cw;
|
|
669
|
+
oc.height = ch;
|
|
670
|
+
const octx = oc.getContext("2d", { willReadFrequently: true });
|
|
671
|
+
if (octx) {
|
|
672
|
+
const drawX = pad;
|
|
673
|
+
const drawY = Math.round(ch * 0.72);
|
|
674
|
+
octx.font = `${fontWeight} ${baseSize}px ${fontFamily}`;
|
|
675
|
+
octx.textAlign = "left";
|
|
676
|
+
octx.textBaseline = "alphabetic";
|
|
677
|
+
octx.fillStyle = "#000";
|
|
678
|
+
octx.fillText(text, drawX, drawY);
|
|
679
|
+
const data = octx.getImageData(0, 0, cw, ch).data;
|
|
680
|
+
let minX = cw;
|
|
681
|
+
let maxX = 0;
|
|
682
|
+
let minY = ch;
|
|
683
|
+
let maxY = 0;
|
|
684
|
+
let found = 0;
|
|
685
|
+
for (let y = 0; y < ch; y++) {
|
|
686
|
+
for (let x = 0; x < cw; x++) {
|
|
687
|
+
if (data[(y * cw + x) * 4 + 3] > 20) {
|
|
688
|
+
if (x < minX) minX = x;
|
|
689
|
+
if (x > maxX) maxX = x;
|
|
690
|
+
if (y < minY) minY = y;
|
|
691
|
+
if (y > maxY) maxY = y;
|
|
692
|
+
found++;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (found > 0) {
|
|
697
|
+
const fontSize = baseSize * (rect.width / (maxX - minX));
|
|
698
|
+
const scale = fontSize / baseSize;
|
|
699
|
+
const inkCenterXFromStart = ((minX + maxX) / 2 - drawX) * scale;
|
|
700
|
+
const inkCenterYFromBaseline = ((minY + maxY) / 2 - drawY) * scale;
|
|
701
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
702
|
+
const fm = ctx.measureText(text);
|
|
703
|
+
const leading = fontSize - (fm.fontBoundingBoxAscent + fm.fontBoundingBoxDescent);
|
|
704
|
+
const baselineFromTop = leading / 2 + fm.fontBoundingBoxAscent;
|
|
705
|
+
const targetCx = rect.left + rect.width / 2;
|
|
706
|
+
const targetCy = rect.top + rect.height / 2;
|
|
707
|
+
el.style.display = "block";
|
|
708
|
+
el.style.textAlign = "left";
|
|
709
|
+
el.style.whiteSpace = "nowrap";
|
|
710
|
+
el.style.width = "auto";
|
|
711
|
+
el.style.height = "auto";
|
|
712
|
+
el.style.fontFamily = fontFamily;
|
|
713
|
+
el.style.fontWeight = fontWeight;
|
|
714
|
+
el.style.fontSize = `${fontSize}px`;
|
|
715
|
+
el.style.left = `${targetCx - inkCenterXFromStart}px`;
|
|
716
|
+
el.style.top = `${targetCy - inkCenterYFromBaseline - baselineFromTop}px`;
|
|
717
|
+
positioned = true;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (!positioned) {
|
|
722
|
+
const measureSize = 100;
|
|
723
|
+
let fontSize = rect.height * 0.92;
|
|
724
|
+
if (ctx && text) {
|
|
725
|
+
ctx.font = `${fontWeight} ${measureSize}px ${fontFamily}`;
|
|
726
|
+
const w = ctx.measureText(text).width;
|
|
727
|
+
if (w > 0) fontSize = measureSize * (rect.width / w);
|
|
728
|
+
}
|
|
729
|
+
el.style.left = `${rect.left}px`;
|
|
730
|
+
el.style.top = `${rect.top}px`;
|
|
731
|
+
el.style.width = `${rect.width}px`;
|
|
732
|
+
el.style.height = `${rect.height}px`;
|
|
733
|
+
el.style.fontFamily = fontFamily;
|
|
734
|
+
el.style.fontWeight = fontWeight;
|
|
735
|
+
el.style.fontSize = `${fontSize}px`;
|
|
736
|
+
}
|
|
625
737
|
};
|
|
626
738
|
const rebuildDomGlyphs = () => {
|
|
627
739
|
keyframes.forEach((kf, i) => {
|
|
@@ -629,7 +741,11 @@ function GlyphPoints(props) {
|
|
|
629
741
|
const next = buildGlyphFromDOM(count, kf.text.split("\n"), {
|
|
630
742
|
selector: kf.domSelector,
|
|
631
743
|
fovDeg: cameraFov,
|
|
632
|
-
cameraZ
|
|
744
|
+
cameraZ,
|
|
745
|
+
// 粒子がレンダリングされる canvas の実寸(CSS px)。
|
|
746
|
+
// window.innerWidth だとスクロールバー分ずれるため size を使う。
|
|
747
|
+
viewportW: size.width,
|
|
748
|
+
viewportH: size.height
|
|
633
749
|
});
|
|
634
750
|
if (!next) return;
|
|
635
751
|
const attr = built.geo.getAttribute(glyphPositionAttribute(i));
|
|
@@ -711,9 +827,19 @@ function GlyphPoints(props) {
|
|
|
711
827
|
const mat = matRef.current;
|
|
712
828
|
if (!mat) return;
|
|
713
829
|
const u = mat.uniforms;
|
|
714
|
-
u.uPixelRatio.value = Math.min(window.devicePixelRatio || 1,
|
|
830
|
+
u.uPixelRatio.value = Math.min(window.devicePixelRatio || 1, 3);
|
|
715
831
|
u.uSize.value = Math.min(size.height / 18, 26);
|
|
716
832
|
}, [size]);
|
|
833
|
+
react.useEffect(() => {
|
|
834
|
+
const mat = matRef.current;
|
|
835
|
+
if (!mat) return;
|
|
836
|
+
const u = mat.uniforms;
|
|
837
|
+
u.uSizeScale.value = style.size;
|
|
838
|
+
u.uDrift.value = style.drift;
|
|
839
|
+
u.uSparkle.value = style.sparkle;
|
|
840
|
+
mat.blending = style.blend === "additive" ? THREE__namespace.AdditiveBlending : THREE__namespace.NormalBlending;
|
|
841
|
+
mat.needsUpdate = true;
|
|
842
|
+
}, [style.size, style.drift, style.sparkle, style.blend]);
|
|
717
843
|
fiber.useFrame((state, delta) => {
|
|
718
844
|
const p = pointsRef.current;
|
|
719
845
|
const mat = matRef.current;
|
|
@@ -721,7 +847,7 @@ function GlyphPoints(props) {
|
|
|
721
847
|
const u = mat.uniforms;
|
|
722
848
|
const d = Math.min(delta, 0.05);
|
|
723
849
|
const raw = THREE__namespace.MathUtils.clamp(getProgress(), 0, 1);
|
|
724
|
-
stage.current =
|
|
850
|
+
stage.current = raw;
|
|
725
851
|
const s = stage.current;
|
|
726
852
|
let settle = 0;
|
|
727
853
|
let burst = 0;
|
|
@@ -738,10 +864,16 @@ function GlyphPoints(props) {
|
|
|
738
864
|
if (lastIsText && n >= 2) {
|
|
739
865
|
form = smooth(times[n - 2] ?? 0, times[n - 1] ?? 1, s);
|
|
740
866
|
}
|
|
867
|
+
const firstIsText = timeline.isText[0] === true;
|
|
868
|
+
if (firstIsText && n >= 2) {
|
|
869
|
+
const formStart = 1 - smooth(times[0] ?? 0, times[1] ?? 1, s);
|
|
870
|
+
form = Math.max(form, formStart);
|
|
871
|
+
}
|
|
741
872
|
const guard = THREE__namespace.MathUtils.clamp(Math.max(settle, form), 0, 1);
|
|
742
873
|
guardRef.current = guard;
|
|
743
874
|
const swapped = raw >= timeline.swapAt ? 1 : 0;
|
|
744
875
|
const resolve = timeline.hasResolve ? smooth(0.9, 0.98, raw) : 0;
|
|
876
|
+
const textReveal = timeline.hasResolve ? smooth(0.92, 1, raw) : 0;
|
|
745
877
|
u.uTime.value = state.clock.elapsedTime;
|
|
746
878
|
u.uStage.value = s;
|
|
747
879
|
u.uForm.value = form;
|
|
@@ -769,8 +901,16 @@ function GlyphPoints(props) {
|
|
|
769
901
|
rot.current.x = THREE__namespace.MathUtils.lerp(rot.current.x, 0, 0.04 + guard * 0.14);
|
|
770
902
|
p.rotation.x = rot.current.x;
|
|
771
903
|
p.rotation.y = rot.current.y;
|
|
772
|
-
|
|
773
|
-
|
|
904
|
+
if (timeline.hasResolve) {
|
|
905
|
+
let target = resolveRef?.current ?? null;
|
|
906
|
+
if (!target && resolveDomSelector) {
|
|
907
|
+
if (!resolveDomElRef.current) {
|
|
908
|
+
resolveDomElRef.current = document.querySelector(resolveDomSelector);
|
|
909
|
+
}
|
|
910
|
+
target = resolveDomElRef.current;
|
|
911
|
+
}
|
|
912
|
+
if (target) target.style.opacity = String(textReveal);
|
|
913
|
+
}
|
|
774
914
|
});
|
|
775
915
|
return /* @__PURE__ */ jsxRuntime.jsx("points", { ref: pointsRef, geometry: built.geo, frustumCulled: false, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
776
916
|
"shaderMaterial",
|
|
@@ -779,7 +919,7 @@ function GlyphPoints(props) {
|
|
|
779
919
|
uniforms,
|
|
780
920
|
transparent: true,
|
|
781
921
|
depthWrite: false,
|
|
782
|
-
blending: THREE__namespace.NormalBlending,
|
|
922
|
+
blending: style.blend === "additive" ? THREE__namespace.AdditiveBlending : THREE__namespace.NormalBlending,
|
|
783
923
|
vertexShader,
|
|
784
924
|
fragmentShader: FRAGMENT_SHADER
|
|
785
925
|
}
|
|
@@ -793,6 +933,12 @@ var DEFAULT_COUNT_MOBILE = 5200;
|
|
|
793
933
|
var DEFAULT_CAMERA_Z = 7;
|
|
794
934
|
var DEFAULT_CAMERA_FOV = 42;
|
|
795
935
|
var DEFAULT_DPR = [1, 1.75];
|
|
936
|
+
var PRESETS = {
|
|
937
|
+
default: { size: 1, blend: "normal", drift: 1, sparkle: 1 },
|
|
938
|
+
minimal: { size: 0.92, blend: "normal", drift: 0.35, sparkle: 0 },
|
|
939
|
+
lively: { size: 1.05, blend: "normal", drift: 1.4, sparkle: 1.4 },
|
|
940
|
+
glow: { size: 1.1, blend: "additive", drift: 1.1, sparkle: 1.5 }
|
|
941
|
+
};
|
|
796
942
|
function clamp012(x) {
|
|
797
943
|
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
798
944
|
}
|
|
@@ -811,6 +957,8 @@ function GlyphDust(props) {
|
|
|
811
957
|
const {
|
|
812
958
|
keyframes,
|
|
813
959
|
driver = { type: "scroll" },
|
|
960
|
+
preset = "default",
|
|
961
|
+
style,
|
|
814
962
|
colors,
|
|
815
963
|
count,
|
|
816
964
|
dpr = DEFAULT_DPR,
|
|
@@ -834,15 +982,62 @@ function GlyphDust(props) {
|
|
|
834
982
|
const resolveRef = react.useRef(null);
|
|
835
983
|
const manualRef = react.useRef(0);
|
|
836
984
|
if (driver.type === "manual") manualRef.current = clamp012(driver.progress);
|
|
985
|
+
const autoplay = driver.type === "autoplay" ? driver : null;
|
|
986
|
+
const playingRef = react.useRef(false);
|
|
987
|
+
const startMsRef = react.useRef(null);
|
|
988
|
+
const lastAutoRef = react.useRef(0);
|
|
989
|
+
react.useEffect(() => {
|
|
990
|
+
if (!autoplay) return;
|
|
991
|
+
if (autoplay.playOnView === false) {
|
|
992
|
+
playingRef.current = true;
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const el = wrapperRef.current;
|
|
996
|
+
if (el === null || typeof IntersectionObserver === "undefined") {
|
|
997
|
+
playingRef.current = true;
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const io = new IntersectionObserver(
|
|
1001
|
+
(entries) => {
|
|
1002
|
+
for (const e of entries) {
|
|
1003
|
+
if (e.isIntersecting && !playingRef.current) {
|
|
1004
|
+
playingRef.current = true;
|
|
1005
|
+
startMsRef.current = null;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
{ threshold: 0.25 }
|
|
1010
|
+
);
|
|
1011
|
+
io.observe(el);
|
|
1012
|
+
return () => io.disconnect();
|
|
1013
|
+
}, [autoplay?.playOnView]);
|
|
837
1014
|
const getProgress = react.useCallback(() => {
|
|
838
1015
|
if (driver.type === "manual") return manualRef.current;
|
|
1016
|
+
if (driver.type === "autoplay") {
|
|
1017
|
+
if (!playingRef.current || typeof performance === "undefined") {
|
|
1018
|
+
return lastAutoRef.current;
|
|
1019
|
+
}
|
|
1020
|
+
if (startMsRef.current === null) startMsRef.current = performance.now();
|
|
1021
|
+
const elapsed = (performance.now() - startMsRef.current) / 1e3;
|
|
1022
|
+
lastAutoRef.current = computeAutoplayProgress(elapsed, driver);
|
|
1023
|
+
return lastAutoRef.current;
|
|
1024
|
+
}
|
|
839
1025
|
const el = wrapperRef.current;
|
|
840
1026
|
if (el === null || typeof window === "undefined") return 0;
|
|
841
1027
|
const rect = el.getBoundingClientRect();
|
|
842
1028
|
const total = rect.height - window.innerHeight;
|
|
843
1029
|
if (total <= 0) return 0;
|
|
844
1030
|
return clamp012(-rect.top / total);
|
|
845
|
-
}, [driver
|
|
1031
|
+
}, [driver]);
|
|
1032
|
+
const resolvedStyle = react.useMemo(() => {
|
|
1033
|
+
const base = PRESETS[preset] ?? PRESETS.default;
|
|
1034
|
+
return {
|
|
1035
|
+
size: style?.size ?? base.size,
|
|
1036
|
+
blend: style?.blend ?? base.blend,
|
|
1037
|
+
drift: style?.drift ?? base.drift,
|
|
1038
|
+
sparkle: style?.sparkle ?? base.sparkle
|
|
1039
|
+
};
|
|
1040
|
+
}, [preset, style?.size, style?.blend, style?.drift, style?.sparkle]);
|
|
846
1041
|
const resolvedColors = react.useMemo(
|
|
847
1042
|
() => ({
|
|
848
1043
|
ink: new THREE__namespace.Color(colors?.ink ?? DEFAULT_INK),
|
|
@@ -858,6 +1053,8 @@ function GlyphDust(props) {
|
|
|
858
1053
|
const dragEnabled = interaction?.drag ?? true;
|
|
859
1054
|
const finalKf = keyframes[keyframes.length - 1];
|
|
860
1055
|
const hasResolve = finalKf?.type === "text" && finalKf.resolveToDom === true;
|
|
1056
|
+
const resolveDomSelector = finalKf?.type === "text" && finalKf.resolveToDom === true && finalKf.domSelector ? finalKf.domSelector : void 0;
|
|
1057
|
+
const useOwnOverlay = hasResolve && !resolveDomSelector;
|
|
861
1058
|
const resolveText = finalKf?.type === "text" ? finalKf.text.replace(/\n/g, " ") : "";
|
|
862
1059
|
if (reduced || !webgl) {
|
|
863
1060
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
|
|
@@ -877,18 +1074,20 @@ function GlyphDust(props) {
|
|
|
877
1074
|
keyframes,
|
|
878
1075
|
count: particleCount,
|
|
879
1076
|
colors: resolvedColors,
|
|
1077
|
+
style: resolvedStyle,
|
|
880
1078
|
cameraZ,
|
|
881
1079
|
cameraFov,
|
|
882
1080
|
pointer: pointerEnabled,
|
|
883
1081
|
drag: dragEnabled,
|
|
884
1082
|
getProgress,
|
|
885
1083
|
timing,
|
|
886
|
-
resolveRef:
|
|
1084
|
+
resolveRef: useOwnOverlay ? resolveRef : void 0,
|
|
1085
|
+
resolveDomSelector
|
|
887
1086
|
}
|
|
888
1087
|
)
|
|
889
1088
|
}
|
|
890
1089
|
),
|
|
891
|
-
|
|
1090
|
+
useOwnOverlay ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
892
1091
|
"div",
|
|
893
1092
|
{
|
|
894
1093
|
ref: resolveRef,
|
|
@@ -909,10 +1108,11 @@ function GlyphDust(props) {
|
|
|
909
1108
|
}
|
|
910
1109
|
) : null
|
|
911
1110
|
] });
|
|
912
|
-
if (driver.type === "manual") {
|
|
1111
|
+
if (driver.type === "manual" || driver.type === "autoplay") {
|
|
913
1112
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
914
1113
|
"div",
|
|
915
1114
|
{
|
|
1115
|
+
ref: wrapperRef,
|
|
916
1116
|
className,
|
|
917
1117
|
style: { position: "relative", width: "100%", height: "100%" },
|
|
918
1118
|
children: scene
|
|
@@ -944,7 +1144,7 @@ function GlyphDust(props) {
|
|
|
944
1144
|
}
|
|
945
1145
|
|
|
946
1146
|
// src/index.ts
|
|
947
|
-
var VERSION = "0.1
|
|
1147
|
+
var VERSION = "0.2.1";
|
|
948
1148
|
|
|
949
1149
|
exports.DEFAULT_TRIGGER_HEIGHT = DEFAULT_TRIGGER_HEIGHT;
|
|
950
1150
|
exports.FRAGMENT_SHADER = FRAGMENT_SHADER;
|
|
@@ -955,6 +1155,7 @@ exports.buildDenseTextTargets = buildDenseTextTargets;
|
|
|
955
1155
|
exports.buildGlyphFromDOM = buildGlyphFromDOM;
|
|
956
1156
|
exports.buildTextTargets = buildTextTargets;
|
|
957
1157
|
exports.buildVertexShader = buildVertexShader;
|
|
1158
|
+
exports.computeAutoplayProgress = computeAutoplayProgress;
|
|
958
1159
|
exports.computeScreenRect = computeScreenRect;
|
|
959
1160
|
exports.createScrollProgress = createScrollProgress;
|
|
960
1161
|
exports.glyphPositionAttribute = glyphPositionAttribute;
|