glyphdust 0.1.0 → 0.2.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.cjs CHANGED
@@ -248,7 +248,6 @@ ${mixChain}
248
248
  float s = uSize * sizeVar;
249
249
  gl_PointSize = s * uPixelRatio * (1.0 / -mvPosition.z);
250
250
  gl_PointSize = clamp(gl_PointSize, 1.0, mix(7.0, 9.0, uForm) * uPixelRatio);
251
- gl_PointSize = 10.0; // DEBUG4
252
251
  }
253
252
  `
254
253
  );
@@ -341,10 +340,15 @@ function buildGlyphFromDOM(count, lines, opts) {
341
340
  } catch {
342
341
  }
343
342
  }
344
- const ascent = fontSize * (opts.ascentRatio ?? 0.82);
343
+ const fm = ctx.measureText(lines[0] ?? "M");
344
+ const fbAsc = fm.fontBoundingBoxAscent;
345
+ const fbDesc = fm.fontBoundingBoxDescent;
346
+ const useMetrics = Number.isFinite(fbAsc) && Number.isFinite(fbDesc);
347
+ const fallbackAscent = fontSize * (opts.ascentRatio ?? 0.82);
345
348
  lines.forEach((line, i) => {
346
349
  const lineTop = i * lineHeight;
347
- ctx.fillText(line, 0, lineTop + (lineHeight - fontSize) / 2 + ascent);
350
+ const baseline = useMetrics ? lineTop + (lineHeight - (fbAsc + fbDesc)) / 2 + fbAsc : lineTop + (lineHeight - fontSize) / 2 + fallbackAscent;
351
+ ctx.fillText(line, 0, baseline);
348
352
  });
349
353
  const { data } = ctx.getImageData(0, 0, cw, ch);
350
354
  const pts = [];
@@ -356,8 +360,8 @@ function buildGlyphFromDOM(count, lines, opts) {
356
360
  }
357
361
  const filled = pts.length / 2;
358
362
  if (filled === 0) return null;
359
- const vpW = window.innerWidth;
360
- const vpH = window.innerHeight;
363
+ const vpW = opts.viewportW ?? window.innerWidth;
364
+ const vpH = opts.viewportH ?? window.innerHeight;
361
365
  const { worldW, worldH } = viewSizeAtZ0(vpW, vpH, opts.fovDeg, opts.cameraZ);
362
366
  const pxToWorld = worldW / vpW;
363
367
  const thickness = opts.thickness ?? 0.14;
@@ -525,9 +529,11 @@ function GlyphPoints(props) {
525
529
  drag: dragEnabled,
526
530
  getProgress,
527
531
  timing,
528
- resolveRef
532
+ resolveRef,
533
+ resolveDomSelector
529
534
  } = props;
530
535
  const pointsRef = react.useRef(null);
536
+ const resolveDomElRef = react.useRef(null);
531
537
  const matRef = react.useRef(null);
532
538
  const { size, gl } = fiber.useThree();
533
539
  const pointer = react.useRef({ x: 0, y: 0, active: 0 });
@@ -617,11 +623,93 @@ function GlyphPoints(props) {
617
623
  const { worldW: visW } = viewSizeAtZ0(vpW, vpH, cameraFov, cameraZ);
618
624
  const rect = computeScreenRect(finalBuf, vpW, vpH, visW);
619
625
  if (!rect) return;
620
- el.style.left = `${rect.left}px`;
621
- el.style.top = `${rect.top}px`;
622
- el.style.width = `${rect.width}px`;
623
- el.style.height = `${rect.height}px`;
624
- el.style.fontSize = `${rect.height * 0.92}px`;
626
+ const finalKf = keyframes[n - 1];
627
+ const fontStr = finalKf?.type === "text" && finalKf.font ? finalKf.font : DEFAULT_DENSE_FONT;
628
+ const fontMatch = fontStr.match(/^\s*(\d+)\s+[\d.]+px\s+(.+)$/);
629
+ const fontWeight = fontMatch?.[1] ?? "900";
630
+ const fontFamily = fontMatch?.[2] ?? "sans-serif";
631
+ const text = timeline.resolveText;
632
+ const ctx = document.createElement("canvas").getContext("2d", {
633
+ willReadFrequently: true
634
+ });
635
+ let positioned = false;
636
+ if (ctx && text) {
637
+ const baseSize = 200;
638
+ ctx.font = `${fontWeight} ${baseSize}px ${fontFamily}`;
639
+ const advBase = ctx.measureText(text).width;
640
+ const pad = Math.ceil(baseSize * 0.6);
641
+ const cw = Math.ceil(advBase + pad * 2);
642
+ const ch = Math.ceil(baseSize * 1.8);
643
+ const oc = document.createElement("canvas");
644
+ oc.width = cw;
645
+ oc.height = ch;
646
+ const octx = oc.getContext("2d", { willReadFrequently: true });
647
+ if (octx) {
648
+ const drawX = pad;
649
+ const drawY = Math.round(ch * 0.72);
650
+ octx.font = `${fontWeight} ${baseSize}px ${fontFamily}`;
651
+ octx.textAlign = "left";
652
+ octx.textBaseline = "alphabetic";
653
+ octx.fillStyle = "#000";
654
+ octx.fillText(text, drawX, drawY);
655
+ const data = octx.getImageData(0, 0, cw, ch).data;
656
+ let minX = cw;
657
+ let maxX = 0;
658
+ let minY = ch;
659
+ let maxY = 0;
660
+ let found = 0;
661
+ for (let y = 0; y < ch; y++) {
662
+ for (let x = 0; x < cw; x++) {
663
+ if (data[(y * cw + x) * 4 + 3] > 20) {
664
+ if (x < minX) minX = x;
665
+ if (x > maxX) maxX = x;
666
+ if (y < minY) minY = y;
667
+ if (y > maxY) maxY = y;
668
+ found++;
669
+ }
670
+ }
671
+ }
672
+ if (found > 0) {
673
+ const fontSize = baseSize * (rect.width / (maxX - minX));
674
+ const scale = fontSize / baseSize;
675
+ const inkCenterXFromStart = ((minX + maxX) / 2 - drawX) * scale;
676
+ const inkCenterYFromBaseline = ((minY + maxY) / 2 - drawY) * scale;
677
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
678
+ const fm = ctx.measureText(text);
679
+ const leading = fontSize - (fm.fontBoundingBoxAscent + fm.fontBoundingBoxDescent);
680
+ const baselineFromTop = leading / 2 + fm.fontBoundingBoxAscent;
681
+ const targetCx = rect.left + rect.width / 2;
682
+ const targetCy = rect.top + rect.height / 2;
683
+ el.style.display = "block";
684
+ el.style.textAlign = "left";
685
+ el.style.whiteSpace = "nowrap";
686
+ el.style.width = "auto";
687
+ el.style.height = "auto";
688
+ el.style.fontFamily = fontFamily;
689
+ el.style.fontWeight = fontWeight;
690
+ el.style.fontSize = `${fontSize}px`;
691
+ el.style.left = `${targetCx - inkCenterXFromStart}px`;
692
+ el.style.top = `${targetCy - inkCenterYFromBaseline - baselineFromTop}px`;
693
+ positioned = true;
694
+ }
695
+ }
696
+ }
697
+ if (!positioned) {
698
+ const measureSize = 100;
699
+ let fontSize = rect.height * 0.92;
700
+ if (ctx && text) {
701
+ ctx.font = `${fontWeight} ${measureSize}px ${fontFamily}`;
702
+ const w = ctx.measureText(text).width;
703
+ if (w > 0) fontSize = measureSize * (rect.width / w);
704
+ }
705
+ el.style.left = `${rect.left}px`;
706
+ el.style.top = `${rect.top}px`;
707
+ el.style.width = `${rect.width}px`;
708
+ el.style.height = `${rect.height}px`;
709
+ el.style.fontFamily = fontFamily;
710
+ el.style.fontWeight = fontWeight;
711
+ el.style.fontSize = `${fontSize}px`;
712
+ }
625
713
  };
626
714
  const rebuildDomGlyphs = () => {
627
715
  keyframes.forEach((kf, i) => {
@@ -629,7 +717,11 @@ function GlyphPoints(props) {
629
717
  const next = buildGlyphFromDOM(count, kf.text.split("\n"), {
630
718
  selector: kf.domSelector,
631
719
  fovDeg: cameraFov,
632
- cameraZ
720
+ cameraZ,
721
+ // 粒子がレンダリングされる canvas の実寸(CSS px)。
722
+ // window.innerWidth だとスクロールバー分ずれるため size を使う。
723
+ viewportW: size.width,
724
+ viewportH: size.height
633
725
  });
634
726
  if (!next) return;
635
727
  const attr = built.geo.getAttribute(glyphPositionAttribute(i));
@@ -742,6 +834,7 @@ function GlyphPoints(props) {
742
834
  guardRef.current = guard;
743
835
  const swapped = raw >= timeline.swapAt ? 1 : 0;
744
836
  const resolve = timeline.hasResolve ? smooth(0.9, 0.98, raw) : 0;
837
+ const textReveal = timeline.hasResolve ? smooth(0.92, 1, raw) : 0;
745
838
  u.uTime.value = state.clock.elapsedTime;
746
839
  u.uStage.value = s;
747
840
  u.uForm.value = form;
@@ -769,8 +862,16 @@ function GlyphPoints(props) {
769
862
  rot.current.x = THREE__namespace.MathUtils.lerp(rot.current.x, 0, 0.04 + guard * 0.14);
770
863
  p.rotation.x = rot.current.x;
771
864
  p.rotation.y = rot.current.y;
772
- const overlay = resolveRef?.current;
773
- if (overlay && timeline.hasResolve) overlay.style.opacity = String(resolve);
865
+ if (timeline.hasResolve) {
866
+ let target = resolveRef?.current ?? null;
867
+ if (!target && resolveDomSelector) {
868
+ if (!resolveDomElRef.current) {
869
+ resolveDomElRef.current = document.querySelector(resolveDomSelector);
870
+ }
871
+ target = resolveDomElRef.current;
872
+ }
873
+ if (target) target.style.opacity = String(textReveal);
874
+ }
774
875
  });
775
876
  return /* @__PURE__ */ jsxRuntime.jsx("points", { ref: pointsRef, geometry: built.geo, frustumCulled: false, children: /* @__PURE__ */ jsxRuntime.jsx(
776
877
  "shaderMaterial",
@@ -858,6 +959,8 @@ function GlyphDust(props) {
858
959
  const dragEnabled = interaction?.drag ?? true;
859
960
  const finalKf = keyframes[keyframes.length - 1];
860
961
  const hasResolve = finalKf?.type === "text" && finalKf.resolveToDom === true;
962
+ const resolveDomSelector = finalKf?.type === "text" && finalKf.resolveToDom === true && finalKf.domSelector ? finalKf.domSelector : void 0;
963
+ const useOwnOverlay = hasResolve && !resolveDomSelector;
861
964
  const resolveText = finalKf?.type === "text" ? finalKf.text.replace(/\n/g, " ") : "";
862
965
  if (reduced || !webgl) {
863
966
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
@@ -883,12 +986,13 @@ function GlyphDust(props) {
883
986
  drag: dragEnabled,
884
987
  getProgress,
885
988
  timing,
886
- resolveRef: hasResolve ? resolveRef : void 0
989
+ resolveRef: useOwnOverlay ? resolveRef : void 0,
990
+ resolveDomSelector
887
991
  }
888
992
  )
889
993
  }
890
994
  ),
891
- hasResolve ? /* @__PURE__ */ jsxRuntime.jsx(
995
+ useOwnOverlay ? /* @__PURE__ */ jsxRuntime.jsx(
892
996
  "div",
893
997
  {
894
998
  ref: resolveRef,