glyphdust 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -32,6 +32,38 @@ function fillScatterCluster(out, count, offsetX, offsetY, random) {
32
32
  out[i * 3 + 2] = (random() - 0.5) * 0.2;
33
33
  }
34
34
  }
35
+ function segmentsToRunLines(segments, defaultFont) {
36
+ const lines = [[]];
37
+ for (const seg of segments) {
38
+ const font = seg.font ?? defaultFont;
39
+ const parts = seg.text.split("\n");
40
+ parts.forEach((part, i) => {
41
+ if (i > 0) lines.push([]);
42
+ if (part.length > 0) lines[lines.length - 1].push({ text: part, font });
43
+ });
44
+ }
45
+ return lines;
46
+ }
47
+ function drawSegmentedLines(ctx, runLines, cw, ch, lineHeight, align, leftPad) {
48
+ ctx.fillStyle = "#000";
49
+ ctx.textBaseline = "middle";
50
+ ctx.textAlign = "left";
51
+ const blockH = lineHeight * (runLines.length - 1);
52
+ runLines.forEach((runs, i) => {
53
+ const y = ch / 2 - blockH / 2 + i * lineHeight;
54
+ let total = 0;
55
+ for (const r of runs) {
56
+ ctx.font = r.font;
57
+ total += ctx.measureText(r.text).width;
58
+ }
59
+ let x = align === "left" ? leftPad : cw / 2 - total / 2;
60
+ for (const r of runs) {
61
+ ctx.font = r.font;
62
+ ctx.fillText(r.text, x, y);
63
+ x += ctx.measureText(r.text).width;
64
+ }
65
+ });
66
+ }
35
67
  function buildTextTargets(count, lines, opts) {
36
68
  const out = new Float32Array(count * 3);
37
69
  const random = opts.random ?? Math.random;
@@ -40,17 +72,22 @@ function buildTextTargets(count, lines, opts) {
40
72
  const ctx = createSamplingContext(cw, ch);
41
73
  if (!ctx) return out;
42
74
  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
75
  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
- });
76
+ ctx.clearRect(0, 0, cw, ch);
77
+ if (opts.segments && opts.segments.length > 0) {
78
+ const runLines = segmentsToRunLines(opts.segments, opts.font);
79
+ drawSegmentedLines(ctx, runLines, cw, ch, lh, align, cw * 0.04);
80
+ } else {
81
+ ctx.fillStyle = "#000";
82
+ ctx.textAlign = align === "left" ? "left" : "center";
83
+ ctx.textBaseline = "middle";
84
+ ctx.font = opts.font;
85
+ const drawX = align === "left" ? cw * 0.04 : cw / 2;
86
+ const blockH = lh * (lines.length - 1);
87
+ lines.forEach((line, i) => {
88
+ ctx.fillText(line, drawX, ch / 2 - blockH / 2 + i * lh);
89
+ });
90
+ }
54
91
  const step = opts.step ?? 2;
55
92
  const pts = collectFilledPixels(ctx, cw, ch, step);
56
93
  const filled = pts.length / 2;
@@ -84,15 +121,20 @@ function buildDenseTextTargets(count, lines, opts) {
84
121
  const ctx = createSamplingContext(cw, ch);
85
122
  if (!ctx) return out;
86
123
  ctx.clearRect(0, 0, cw, ch);
87
- ctx.fillStyle = "#000";
88
- ctx.textAlign = "center";
89
- ctx.textBaseline = "middle";
90
- ctx.font = opts.font;
91
124
  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
- });
125
+ if (opts.segments && opts.segments.length > 0) {
126
+ const runLines = segmentsToRunLines(opts.segments, opts.font);
127
+ drawSegmentedLines(ctx, runLines, cw, ch, lh, "center", cw * 0.04);
128
+ } else {
129
+ ctx.fillStyle = "#000";
130
+ ctx.textAlign = "center";
131
+ ctx.textBaseline = "middle";
132
+ ctx.font = opts.font;
133
+ const blockH = lh * (lines.length - 1);
134
+ lines.forEach((line, i) => {
135
+ ctx.fillText(line, cw / 2, ch / 2 - blockH / 2 + i * lh);
136
+ });
137
+ }
96
138
  const step = opts.step ?? 1;
97
139
  const pts = collectFilledPixels(ctx, cw, ch, step);
98
140
  const filled = pts.length / 2;
@@ -160,6 +202,8 @@ function buildVertexShader(keyframeCount) {
160
202
  uniform vec3 uPointer;
161
203
  uniform float uPointerActive;
162
204
  uniform float uSize;
205
+ uniform float uSizeScale;
206
+ uniform float uDrift;
163
207
  uniform float uPixelRatio;
164
208
 
165
209
  ${attributeDecls}
@@ -195,7 +239,7 @@ ${mixChain}
195
239
 
196
240
  // \u30A2\u30A4\u30C9\u30EB\u306E\u6F02\u3044\uFF08\u6574\u5217\u6642 settle / \u5B57\u5F62\u6642 form \u3067\u5F31\u3081\u308B\uFF09\u3002
197
241
  vSettle = uSettle;
198
- float drift = (1.0 - uReduced) * (1.0 - uSettle * 0.9) * (1.0 - uForm);
242
+ float drift = (1.0 - uReduced) * (1.0 - uSettle * 0.9) * (1.0 - uForm) * uDrift;
199
243
  pos.x += sin(uTime * 0.35 + ph) * 0.06 * drift;
200
244
  pos.y += cos(uTime * 0.30 + ph * 1.7) * 0.06 * drift;
201
245
  pos.z += sin(uTime * 0.27 + ph * 2.3) * 0.06 * drift;
@@ -223,9 +267,11 @@ ${mixChain}
223
267
  float sizeVar = mix(0.55 + aSeed * 0.9, 0.72 + aSeed * 0.35, uSettle);
224
268
  // \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
269
  sizeVar = mix(sizeVar, 0.95 + aSeed * 0.18, uForm);
226
- float s = uSize * sizeVar;
270
+ // \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
271
+ // \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
272
+ float s = uSize * sizeVar * 0.62 * uSizeScale;
227
273
  gl_PointSize = s * uPixelRatio * (1.0 / -mvPosition.z);
228
- gl_PointSize = clamp(gl_PointSize, 1.0, mix(7.0, 9.0, uForm) * uPixelRatio);
274
+ gl_PointSize = clamp(gl_PointSize, 1.0, mix(4.0, 5.0, uForm) * uPixelRatio);
229
275
  }
230
276
  `
231
277
  );
@@ -235,6 +281,7 @@ var FRAGMENT_SHADER = (
235
281
  `
236
282
  uniform vec3 uColorInk;
237
283
  uniform vec3 uColorAccent;
284
+ uniform float uSparkle;
238
285
 
239
286
  varying float vSeed;
240
287
  varying float vAccent;
@@ -256,7 +303,7 @@ var FRAGMENT_SHADER = (
256
303
 
257
304
  // \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
258
305
  float spark = step(0.94, vSeed);
259
- col = mix(col, uColorAccent, spark * mix(0.45, 0.15, vSettle));
306
+ col = mix(col, uColorAccent, spark * mix(0.45, 0.15, vSettle) * uSparkle);
260
307
 
261
308
  // \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
262
309
  float floorFade = mix(0.45, 0.78, vSettle);
@@ -390,6 +437,21 @@ function computeScreenRect(targets, viewportW, viewportH, visibleWorldW) {
390
437
  };
391
438
  }
392
439
  var DEFAULT_TRIGGER_HEIGHT = 2;
440
+ function triangle(x) {
441
+ const t = x % 2;
442
+ return t <= 1 ? t : 2 - t;
443
+ }
444
+ function computeAutoplayProgress(elapsedSec, cfg) {
445
+ const duration = cfg.duration && cfg.duration > 0 ? cfg.duration : 4;
446
+ const delay = cfg.delay && cfg.delay > 0 ? cfg.delay : 0;
447
+ const t = elapsedSec - delay;
448
+ if (t <= 0) return 0;
449
+ const raw = t / duration;
450
+ if (cfg.loop) {
451
+ return cfg.pingpong ? triangle(raw) : raw % 1;
452
+ }
453
+ return clamp01(raw);
454
+ }
393
455
  function clamp01(x) {
394
456
  return x < 0 ? 0 : x > 1 ? 1 : x;
395
457
  }
@@ -475,6 +537,7 @@ function buildKeyframeTargets(kf, count, ctx) {
475
537
  if (kf.dense) {
476
538
  return buildDenseTextTargets(count, lines, {
477
539
  font: kf.font ?? DEFAULT_DENSE_FONT,
540
+ segments: kf.segments,
478
541
  worldW: kf.worldW ?? ctx.visW * (ctx.mobile ? 0.86 : 0.62),
479
542
  offsetX: kf.offsetX ?? 0,
480
543
  offsetY: kf.offsetY ?? 0,
@@ -486,6 +549,7 @@ function buildKeyframeTargets(kf, count, ctx) {
486
549
  }
487
550
  return buildTextTargets(count, lines, {
488
551
  font: kf.font ?? DEFAULT_TEXT_FONT,
552
+ segments: kf.segments,
489
553
  worldW: kf.worldW ?? ctx.visW * 0.7,
490
554
  lineHeight: 178,
491
555
  offsetX: kf.offsetX ?? 0,
@@ -501,6 +565,7 @@ function GlyphPoints(props) {
501
565
  keyframes,
502
566
  count,
503
567
  colors,
568
+ style,
504
569
  cameraZ,
505
570
  cameraFov,
506
571
  pointer: pointerEnabled,
@@ -584,6 +649,9 @@ function GlyphPoints(props) {
584
649
  uPointer: { value: new THREE.Vector3(0, 0, 0) },
585
650
  uPointerActive: { value: 0 },
586
651
  uSize: { value: 1 },
652
+ uSizeScale: { value: style.size },
653
+ uDrift: { value: style.drift },
654
+ uSparkle: { value: style.sparkle },
587
655
  uPixelRatio: { value: 1 },
588
656
  uColorInk: { value: colors.ink.clone() },
589
657
  uColorAccent: { value: colors.accent.clone() }
@@ -781,9 +849,19 @@ function GlyphPoints(props) {
781
849
  const mat = matRef.current;
782
850
  if (!mat) return;
783
851
  const u = mat.uniforms;
784
- u.uPixelRatio.value = Math.min(window.devicePixelRatio || 1, 2);
852
+ u.uPixelRatio.value = Math.min(window.devicePixelRatio || 1, 3);
785
853
  u.uSize.value = Math.min(size.height / 18, 26);
786
854
  }, [size]);
855
+ useEffect(() => {
856
+ const mat = matRef.current;
857
+ if (!mat) return;
858
+ const u = mat.uniforms;
859
+ u.uSizeScale.value = style.size;
860
+ u.uDrift.value = style.drift;
861
+ u.uSparkle.value = style.sparkle;
862
+ mat.blending = style.blend === "additive" ? THREE.AdditiveBlending : THREE.NormalBlending;
863
+ mat.needsUpdate = true;
864
+ }, [style.size, style.drift, style.sparkle, style.blend]);
787
865
  useFrame((state, delta) => {
788
866
  const p = pointsRef.current;
789
867
  const mat = matRef.current;
@@ -791,7 +869,7 @@ function GlyphPoints(props) {
791
869
  const u = mat.uniforms;
792
870
  const d = Math.min(delta, 0.05);
793
871
  const raw = THREE.MathUtils.clamp(getProgress(), 0, 1);
794
- stage.current = THREE.MathUtils.lerp(stage.current, raw, 0.1);
872
+ stage.current = raw;
795
873
  const s = stage.current;
796
874
  let settle = 0;
797
875
  let burst = 0;
@@ -808,6 +886,11 @@ function GlyphPoints(props) {
808
886
  if (lastIsText && n >= 2) {
809
887
  form = smooth(times[n - 2] ?? 0, times[n - 1] ?? 1, s);
810
888
  }
889
+ const firstIsText = timeline.isText[0] === true;
890
+ if (firstIsText && n >= 2) {
891
+ const formStart = 1 - smooth(times[0] ?? 0, times[1] ?? 1, s);
892
+ form = Math.max(form, formStart);
893
+ }
811
894
  const guard = THREE.MathUtils.clamp(Math.max(settle, form), 0, 1);
812
895
  guardRef.current = guard;
813
896
  const swapped = raw >= timeline.swapAt ? 1 : 0;
@@ -858,7 +941,7 @@ function GlyphPoints(props) {
858
941
  uniforms,
859
942
  transparent: true,
860
943
  depthWrite: false,
861
- blending: THREE.NormalBlending,
944
+ blending: style.blend === "additive" ? THREE.AdditiveBlending : THREE.NormalBlending,
862
945
  vertexShader,
863
946
  fragmentShader: FRAGMENT_SHADER
864
947
  }
@@ -872,6 +955,12 @@ var DEFAULT_COUNT_MOBILE = 5200;
872
955
  var DEFAULT_CAMERA_Z = 7;
873
956
  var DEFAULT_CAMERA_FOV = 42;
874
957
  var DEFAULT_DPR = [1, 1.75];
958
+ var PRESETS = {
959
+ default: { size: 1, blend: "normal", drift: 1, sparkle: 1 },
960
+ minimal: { size: 0.92, blend: "normal", drift: 0.35, sparkle: 0 },
961
+ lively: { size: 1.05, blend: "normal", drift: 1.4, sparkle: 1.4 },
962
+ glow: { size: 1.1, blend: "additive", drift: 1.1, sparkle: 1.5 }
963
+ };
875
964
  function clamp012(x) {
876
965
  return x < 0 ? 0 : x > 1 ? 1 : x;
877
966
  }
@@ -890,6 +979,8 @@ function GlyphDust(props) {
890
979
  const {
891
980
  keyframes,
892
981
  driver = { type: "scroll" },
982
+ preset = "default",
983
+ style,
893
984
  colors,
894
985
  count,
895
986
  dpr = DEFAULT_DPR,
@@ -913,15 +1004,62 @@ function GlyphDust(props) {
913
1004
  const resolveRef = useRef(null);
914
1005
  const manualRef = useRef(0);
915
1006
  if (driver.type === "manual") manualRef.current = clamp012(driver.progress);
1007
+ const autoplay = driver.type === "autoplay" ? driver : null;
1008
+ const playingRef = useRef(false);
1009
+ const startMsRef = useRef(null);
1010
+ const lastAutoRef = useRef(0);
1011
+ useEffect(() => {
1012
+ if (!autoplay) return;
1013
+ if (autoplay.playOnView === false) {
1014
+ playingRef.current = true;
1015
+ return;
1016
+ }
1017
+ const el = wrapperRef.current;
1018
+ if (el === null || typeof IntersectionObserver === "undefined") {
1019
+ playingRef.current = true;
1020
+ return;
1021
+ }
1022
+ const io = new IntersectionObserver(
1023
+ (entries) => {
1024
+ for (const e of entries) {
1025
+ if (e.isIntersecting && !playingRef.current) {
1026
+ playingRef.current = true;
1027
+ startMsRef.current = null;
1028
+ }
1029
+ }
1030
+ },
1031
+ { threshold: 0.25 }
1032
+ );
1033
+ io.observe(el);
1034
+ return () => io.disconnect();
1035
+ }, [autoplay?.playOnView]);
916
1036
  const getProgress = useCallback(() => {
917
1037
  if (driver.type === "manual") return manualRef.current;
1038
+ if (driver.type === "autoplay") {
1039
+ if (!playingRef.current || typeof performance === "undefined") {
1040
+ return lastAutoRef.current;
1041
+ }
1042
+ if (startMsRef.current === null) startMsRef.current = performance.now();
1043
+ const elapsed = (performance.now() - startMsRef.current) / 1e3;
1044
+ lastAutoRef.current = computeAutoplayProgress(elapsed, driver);
1045
+ return lastAutoRef.current;
1046
+ }
918
1047
  const el = wrapperRef.current;
919
1048
  if (el === null || typeof window === "undefined") return 0;
920
1049
  const rect = el.getBoundingClientRect();
921
1050
  const total = rect.height - window.innerHeight;
922
1051
  if (total <= 0) return 0;
923
1052
  return clamp012(-rect.top / total);
924
- }, [driver.type]);
1053
+ }, [driver]);
1054
+ const resolvedStyle = useMemo(() => {
1055
+ const base = PRESETS[preset] ?? PRESETS.default;
1056
+ return {
1057
+ size: style?.size ?? base.size,
1058
+ blend: style?.blend ?? base.blend,
1059
+ drift: style?.drift ?? base.drift,
1060
+ sparkle: style?.sparkle ?? base.sparkle
1061
+ };
1062
+ }, [preset, style?.size, style?.blend, style?.drift, style?.sparkle]);
925
1063
  const resolvedColors = useMemo(
926
1064
  () => ({
927
1065
  ink: new THREE.Color(colors?.ink ?? DEFAULT_INK),
@@ -958,6 +1096,7 @@ function GlyphDust(props) {
958
1096
  keyframes,
959
1097
  count: particleCount,
960
1098
  colors: resolvedColors,
1099
+ style: resolvedStyle,
961
1100
  cameraZ,
962
1101
  cameraFov,
963
1102
  pointer: pointerEnabled,
@@ -991,10 +1130,11 @@ function GlyphDust(props) {
991
1130
  }
992
1131
  ) : null
993
1132
  ] });
994
- if (driver.type === "manual") {
1133
+ if (driver.type === "manual" || driver.type === "autoplay") {
995
1134
  return /* @__PURE__ */ jsx(
996
1135
  "div",
997
1136
  {
1137
+ ref: wrapperRef,
998
1138
  className,
999
1139
  style: { position: "relative", width: "100%", height: "100%" },
1000
1140
  children: scene
@@ -1026,8 +1166,8 @@ function GlyphDust(props) {
1026
1166
  }
1027
1167
 
1028
1168
  // src/index.ts
1029
- var VERSION = "0.1.0";
1169
+ var VERSION = "0.3.0";
1030
1170
 
1031
- export { DEFAULT_TRIGGER_HEIGHT, FRAGMENT_SHADER, GLYPH_POSITION_ATTRIBUTE_PREFIX, GlyphDust, VERSION, buildDenseTextTargets, buildGlyphFromDOM, buildTextTargets, buildVertexShader, computeScreenRect, createScrollProgress, glyphPositionAttribute, prefersReducedMotion, useReducedMotion, useScrollProgress, viewSizeAtZ0 };
1171
+ export { DEFAULT_TRIGGER_HEIGHT, FRAGMENT_SHADER, GLYPH_POSITION_ATTRIBUTE_PREFIX, GlyphDust, VERSION, buildDenseTextTargets, buildGlyphFromDOM, buildTextTargets, buildVertexShader, computeAutoplayProgress, computeScreenRect, createScrollProgress, glyphPositionAttribute, prefersReducedMotion, useReducedMotion, useScrollProgress, viewSizeAtZ0 };
1032
1172
  //# sourceMappingURL=index.js.map
1033
1173
  //# sourceMappingURL=index.js.map