@glissade/scene 0.19.1 → 0.20.0-pre.3
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/collapseReplacer.d.ts +40 -0
- package/dist/constructionProps.js +81 -0
- package/dist/describe.d.ts +26 -1
- package/dist/describe.js +104 -23
- package/dist/diagnostics.d.ts +159 -0
- package/dist/diagnostics.js +384 -0
- package/dist/displayList.d.ts +27 -0
- package/dist/grid.d.ts +56 -0
- package/dist/grid.js +83 -0
- package/dist/identity.d.ts +33 -0
- package/dist/identity.js +67 -0
- package/dist/index.d.ts +14 -302
- package/dist/index.js +19 -523
- package/dist/layout.d.ts +3 -95
- package/dist/layout.js +11 -196
- package/dist/layoutCtors.d.ts +99 -0
- package/dist/layoutCtors.js +210 -0
- package/dist/motion.d.ts +60 -0
- package/dist/motion.js +171 -0
- package/dist/nodes.d.ts +32 -8
- package/dist/nodes.js +62 -187
- package/dist/scene.d.ts +71 -0
- package/dist/scene.js +120 -0
- package/package.json +23 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as hashStr, B as validateFilters, C as segmentWords, D as arcLength, E as SketchValidationError, F as validateSketch, G as invert, H as IDENTITY, I as FilterValidationError, K as matEquals, L as createDisplayListBuilder, M as roughen, N as sketchStrokes, O as flatten, P as validateHachure, R as filtersToCanvasFilter, S as segmentGraphemes, U as applyToPoint, V as collapseReplacer, W as fromTRS, _ as breakLines, a as Path, b as isEstimatingMeasurer, c as Video, d as revealSchedule, f as roundedRectSegs, g as __resetEstimateWarnings, h as MEASURE_QUANTUM_PX, i as ImageNode, j as resolveSketch, k as hachureLines, l as coercePathData, m as resolveAnchor, n as Custom, o as Rect, p as Node, q as multiply, r as Group, s as Text, t as Circle, u as pathFromSegs, v as estimatingMeasurer, w as setDefaultMeasurer, x as quantize, z as glow } from "./nodes.js";
|
|
2
|
+
import { a as evaluate, i as createScene, n as ReservedNodeIdError, r as bindScene, t as DuplicateNodeIdError } from "./scene.js";
|
|
2
3
|
import { i as setLayoutEngine, n as getLayoutEngine, r as requireLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
|
|
3
|
-
import {
|
|
4
|
+
import { buildFontRegistry, emitDevWarning, key, lerpColor, oklabToRgba, parseCmap, parseColor, random, rgbaToOklab, signal, stagger, track, validateFonts } from "@glissade/core";
|
|
4
5
|
//#region src/taxonomy.ts
|
|
5
6
|
/**
|
|
6
7
|
* The CLOSED node taxonomy (DESIGN.md §3.1): exactly nine built-in node TYPES.
|
|
@@ -45,8 +46,8 @@ var Highlight = class extends Node {
|
|
|
45
46
|
constructor(props) {
|
|
46
47
|
super(props);
|
|
47
48
|
this.target = props.text;
|
|
48
|
-
this.color = init$
|
|
49
|
-
this.progress = init$
|
|
49
|
+
this.color = init$1(signal("#ffe066"), props.color);
|
|
50
|
+
this.progress = init$1(signal(1), props.progress);
|
|
50
51
|
this.padding = props.padding ?? [4, 2];
|
|
51
52
|
this.cornerRadius = props.cornerRadius ?? 4;
|
|
52
53
|
this.registerTarget("progress", this.progress, "number");
|
|
@@ -99,7 +100,7 @@ function highlight(text, props = {}) {
|
|
|
99
100
|
text
|
|
100
101
|
});
|
|
101
102
|
}
|
|
102
|
-
function init$
|
|
103
|
+
function init$1(sig, v) {
|
|
103
104
|
if (typeof v === "function") sig.bindSource(v);
|
|
104
105
|
else if (v !== void 0) sig.set(v);
|
|
105
106
|
return sig;
|
|
@@ -132,7 +133,7 @@ var TextCursor = class extends Node {
|
|
|
132
133
|
this.blinkPhase = props.blinkPhase ?? 0;
|
|
133
134
|
this.solidWhileTyping = props.solidWhileTyping ?? true;
|
|
134
135
|
this.caretWidth = props.width ?? 2;
|
|
135
|
-
this.fill = init
|
|
136
|
+
this.fill = init(signal(""), props.fill);
|
|
136
137
|
this.registerTarget("fill", this.fill, "color");
|
|
137
138
|
}
|
|
138
139
|
draw(out, ctx) {
|
|
@@ -173,7 +174,7 @@ function textCursor(text, props = {}) {
|
|
|
173
174
|
text
|
|
174
175
|
});
|
|
175
176
|
}
|
|
176
|
-
function init
|
|
177
|
+
function init(sig, v) {
|
|
177
178
|
if (typeof v === "function") sig.bindSource(v);
|
|
178
179
|
else if (v !== void 0) sig.set(v);
|
|
179
180
|
return sig;
|
|
@@ -528,164 +529,6 @@ function withDeterminismGuards(mode, fn) {
|
|
|
528
529
|
}
|
|
529
530
|
}
|
|
530
531
|
//#endregion
|
|
531
|
-
//#region src/scene.ts
|
|
532
|
-
/**
|
|
533
|
-
* Scene assembly + the canonical evaluate(scene, timeline, t) (DESIGN.md §2.5,
|
|
534
|
-
* §3.1): pure, total, deterministic — driver writes the playhead (sanctioned
|
|
535
|
-
* entry write), then a pull-only read phase collects the DisplayList.
|
|
536
|
-
*/
|
|
537
|
-
var DuplicateNodeIdError = class extends Error {
|
|
538
|
-
constructor(id) {
|
|
539
|
-
super(`duplicate node id '${id}' — explicit ids must be unique (§3.1/§6.5)`);
|
|
540
|
-
this.name = "DuplicateNodeIdError";
|
|
541
|
-
}
|
|
542
|
-
};
|
|
543
|
-
var ReservedNodeIdError = class extends Error {
|
|
544
|
-
constructor(id) {
|
|
545
|
-
super(`node id '${id}' uses the reserved '~' prefix — that namespace is for structural fallback ids (§6.5), which are inspection-only and never track targets; choose another id`);
|
|
546
|
-
this.name = "ReservedNodeIdError";
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
function indexNodes(root, into, measurerSource) {
|
|
550
|
-
root.measurerSource = measurerSource;
|
|
551
|
-
if (root.id !== void 0) {
|
|
552
|
-
if (root.id.startsWith("~")) throw new ReservedNodeIdError(root.id);
|
|
553
|
-
if (into.has(root.id)) throw new DuplicateNodeIdError(root.id);
|
|
554
|
-
into.set(root.id, root);
|
|
555
|
-
}
|
|
556
|
-
if (root instanceof Group) for (const child of root.children) indexNodes(child, into, measurerSource);
|
|
557
|
-
}
|
|
558
|
-
function createScene(init) {
|
|
559
|
-
const root = new Group({
|
|
560
|
-
id: "__root",
|
|
561
|
-
children: init.children
|
|
562
|
-
});
|
|
563
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
564
|
-
const playhead = createPlayhead();
|
|
565
|
-
const measurer = signal(null);
|
|
566
|
-
indexNodes(root, nodes, () => measurer() ?? fallbackMeasurer());
|
|
567
|
-
return {
|
|
568
|
-
root,
|
|
569
|
-
nodes,
|
|
570
|
-
size: init.size,
|
|
571
|
-
playhead,
|
|
572
|
-
resolveTarget: (target) => {
|
|
573
|
-
for (let slash = target.lastIndexOf("/"); slash > 0; slash = target.lastIndexOf("/", slash - 1)) {
|
|
574
|
-
const node = nodes.get(target.slice(0, slash));
|
|
575
|
-
if (node) return node.resolveTarget(target.slice(slash + 1));
|
|
576
|
-
}
|
|
577
|
-
},
|
|
578
|
-
setTextMeasurer: (m) => {
|
|
579
|
-
measurer.set(m);
|
|
580
|
-
},
|
|
581
|
-
get textMeasurer() {
|
|
582
|
-
return measurer.peek() ?? fallbackMeasurer();
|
|
583
|
-
}
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
const bindings = /* @__PURE__ */ new WeakMap();
|
|
587
|
-
function bindScene(scene, doc) {
|
|
588
|
-
let perScene = bindings.get(scene);
|
|
589
|
-
if (!perScene) {
|
|
590
|
-
perScene = /* @__PURE__ */ new WeakMap();
|
|
591
|
-
bindings.set(scene, perScene);
|
|
592
|
-
}
|
|
593
|
-
let entry = perScene.get(doc);
|
|
594
|
-
if (!entry) {
|
|
595
|
-
const compiled = compileTimeline(doc);
|
|
596
|
-
entry = {
|
|
597
|
-
compiled,
|
|
598
|
-
bound: bindTimeline(compiled, scene.resolveTarget, scene.playhead)
|
|
599
|
-
};
|
|
600
|
-
perScene.set(doc, entry);
|
|
601
|
-
}
|
|
602
|
-
return entry;
|
|
603
|
-
}
|
|
604
|
-
/**
|
|
605
|
-
* Empty timeline — zero tracks, so binding installs ZERO computed sources and
|
|
606
|
-
* any imperative `node.set(...)` value survives evaluate untouched. Shared and
|
|
607
|
-
* frozen so the WeakMap binding cache keys on a single stable document across
|
|
608
|
-
* the whole controlled-drive loop (one bind, reused every frame).
|
|
609
|
-
*/
|
|
610
|
-
const EMPTY_TIMELINE = Object.freeze({
|
|
611
|
-
version: 1,
|
|
612
|
-
tracks: []
|
|
613
|
-
});
|
|
614
|
-
function evaluate(scene, doc, t = 0) {
|
|
615
|
-
if (doc === void 0) return evaluate(scene, EMPTY_TIMELINE, scene.playhead.peek());
|
|
616
|
-
bindScene(scene, doc);
|
|
617
|
-
const fps = doc.fps;
|
|
618
|
-
const ctx = {
|
|
619
|
-
time: t,
|
|
620
|
-
frame: fps !== void 0 ? Math.round(t * fps) : -1,
|
|
621
|
-
measurer: scene.textMeasurer
|
|
622
|
-
};
|
|
623
|
-
return evaluateAt(scene.playhead, t, () => {
|
|
624
|
-
const out = createDisplayListBuilder(scene.size);
|
|
625
|
-
scene.root.emit(out, ctx);
|
|
626
|
-
return out.finish();
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
//#endregion
|
|
630
|
-
//#region src/cacheColdAudit.ts
|
|
631
|
-
/** Stable string of a DisplayList for comparison (opaque resources collapse to a marker). */
|
|
632
|
-
function hashDisplayList(dl) {
|
|
633
|
-
return JSON.stringify(dl, collapseReplacer);
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Evaluate two fresh scenes from `createScene` at `t` and confirm the
|
|
637
|
-
* DisplayLists are byte-identical. Returns `{ ok: true }` for a pure scene, or
|
|
638
|
-
* `{ ok: false, node }` naming the first divergent node. DEV-only — never on
|
|
639
|
-
* the render hot path.
|
|
640
|
-
*/
|
|
641
|
-
function auditCacheCold(createScene, doc, t) {
|
|
642
|
-
const warm = createScene();
|
|
643
|
-
const cold = createScene();
|
|
644
|
-
const a = evaluate(warm, doc, t);
|
|
645
|
-
const b = evaluate(cold, doc, t);
|
|
646
|
-
if (hashDisplayList(a) === hashDisplayList(b)) return { ok: true };
|
|
647
|
-
const frame = doc.fps !== void 0 ? Math.round(t * doc.fps) : -1;
|
|
648
|
-
const ctxA = {
|
|
649
|
-
time: t,
|
|
650
|
-
frame,
|
|
651
|
-
measurer: warm.textMeasurer
|
|
652
|
-
};
|
|
653
|
-
const ctxB = {
|
|
654
|
-
time: t,
|
|
655
|
-
frame,
|
|
656
|
-
measurer: cold.textMeasurer
|
|
657
|
-
};
|
|
658
|
-
let groupFallback;
|
|
659
|
-
for (const [id, nodeA] of warm.nodes) {
|
|
660
|
-
const nodeB = cold.nodes.get(id);
|
|
661
|
-
if (!nodeB) return {
|
|
662
|
-
ok: false,
|
|
663
|
-
node: id
|
|
664
|
-
};
|
|
665
|
-
const ea = createDisplayListBuilder(warm.size);
|
|
666
|
-
nodeA.emit(ea, ctxA);
|
|
667
|
-
const eb = createDisplayListBuilder(cold.size);
|
|
668
|
-
nodeB.emit(eb, ctxB);
|
|
669
|
-
const dlA = ea.finish();
|
|
670
|
-
const dlB = eb.finish();
|
|
671
|
-
if (hashDisplayList(dlA) === hashDisplayList(dlB)) continue;
|
|
672
|
-
if (nodeA instanceof Group) {
|
|
673
|
-
groupFallback ??= id;
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
|
-
const first = diffDisplayLists(dlA, dlB).deltas[0];
|
|
677
|
-
return {
|
|
678
|
-
ok: false,
|
|
679
|
-
node: id,
|
|
680
|
-
...first !== void 0 ? { delta: first } : {}
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
return {
|
|
684
|
-
ok: false,
|
|
685
|
-
...groupFallback !== void 0 ? { node: groupFallback } : {}
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
//#endregion
|
|
689
532
|
//#region src/renderBackend.ts
|
|
690
533
|
/** Every filter kind the shared Raster2D interpreter rasterizes (§3.4). */
|
|
691
534
|
const ALL_FILTER_KINDS = new Set([
|
|
@@ -696,363 +539,6 @@ const ALL_FILTER_KINDS = new Set([
|
|
|
696
539
|
"saturate"
|
|
697
540
|
]);
|
|
698
541
|
//#endregion
|
|
699
|
-
//#region src/motionPath.ts
|
|
700
|
-
/**
|
|
701
|
-
* Motion along a path: sample a point (and tangent) at an arc-length position on
|
|
702
|
-
* a PathValue, and drive a node along it over time. The Path node draws/morphs
|
|
703
|
-
* geometry; this is the companion that makes another node — a cursor, a dot, an
|
|
704
|
-
* arrow — *travel* that geometry.
|
|
705
|
-
*
|
|
706
|
-
* Sampling is arc-length parameterized (constant speed), so progress 0→1 moves
|
|
707
|
-
* evenly instead of bunching at the control points. Pure and deterministic: the
|
|
708
|
-
* table is built once from a static PathValue and `atProgress` is a pure
|
|
709
|
-
* function of progress, so evaluate() stays pure and goldens are byte-stable.
|
|
710
|
-
*/
|
|
711
|
-
const clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
712
|
-
function cubicPoint(p0, p1, p2, p3, t) {
|
|
713
|
-
const mt = 1 - t;
|
|
714
|
-
const a = mt * mt * mt;
|
|
715
|
-
const b = 3 * mt * mt * t;
|
|
716
|
-
const c = 3 * mt * t * t;
|
|
717
|
-
const d = t * t * t;
|
|
718
|
-
return [a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1]];
|
|
719
|
-
}
|
|
720
|
-
/** PathValue contours → cubic segments, same v/in/out math as Path.pathSegs. */
|
|
721
|
-
function toCubics(path) {
|
|
722
|
-
const out = [];
|
|
723
|
-
for (const ct of path) {
|
|
724
|
-
const n = ct.v.length;
|
|
725
|
-
for (let i = 0; i < n - 1; i++) out.push([
|
|
726
|
-
ct.v[i],
|
|
727
|
-
[ct.v[i][0] + ct.out[i][0], ct.v[i][1] + ct.out[i][1]],
|
|
728
|
-
[ct.v[i + 1][0] + ct.in[i + 1][0], ct.v[i + 1][1] + ct.in[i + 1][1]],
|
|
729
|
-
ct.v[i + 1]
|
|
730
|
-
]);
|
|
731
|
-
if (ct.closed && n > 1) out.push([
|
|
732
|
-
ct.v[n - 1],
|
|
733
|
-
[ct.v[n - 1][0] + ct.out[n - 1][0], ct.v[n - 1][1] + ct.out[n - 1][1]],
|
|
734
|
-
[ct.v[0][0] + ct.in[0][0], ct.v[0][1] + ct.in[0][1]],
|
|
735
|
-
ct.v[0]
|
|
736
|
-
]);
|
|
737
|
-
}
|
|
738
|
-
return out;
|
|
739
|
-
}
|
|
740
|
-
/**
|
|
741
|
-
* Build a reusable arc-length sampler. Densely samples each cubic into a
|
|
742
|
-
* cumulative-length polyline (samplesPerSegment, default 32) so `at`/`tangent`
|
|
743
|
-
* are simple span lerps — smooth enough for motion, no per-call bezier solve.
|
|
744
|
-
*/
|
|
745
|
-
function motionPath(path, opts = {}) {
|
|
746
|
-
const steps = Math.max(1, Math.floor(opts.samplesPerSegment ?? 32));
|
|
747
|
-
const cubics = toCubics(path);
|
|
748
|
-
const pts = [];
|
|
749
|
-
const cum = [];
|
|
750
|
-
if (cubics.length > 0) {
|
|
751
|
-
let prev = cubicPoint(...cubics[0], 0);
|
|
752
|
-
pts.push(prev);
|
|
753
|
-
cum.push(0);
|
|
754
|
-
let acc = 0;
|
|
755
|
-
for (const cub of cubics) for (let k = 1; k <= steps; k++) {
|
|
756
|
-
const p = cubicPoint(...cub, k / steps);
|
|
757
|
-
acc += Math.hypot(p[0] - prev[0], p[1] - prev[1]);
|
|
758
|
-
pts.push(p);
|
|
759
|
-
cum.push(acc);
|
|
760
|
-
prev = p;
|
|
761
|
-
}
|
|
762
|
-
} else {
|
|
763
|
-
const first = path[0]?.v[0];
|
|
764
|
-
pts.push(first ? [first[0], first[1]] : [0, 0]);
|
|
765
|
-
cum.push(0);
|
|
766
|
-
}
|
|
767
|
-
const total = cum[cum.length - 1];
|
|
768
|
-
const locate = (s) => {
|
|
769
|
-
if (total <= 0 || s <= 0) return {
|
|
770
|
-
i: 0,
|
|
771
|
-
f: 0
|
|
772
|
-
};
|
|
773
|
-
if (s >= total) return {
|
|
774
|
-
i: pts.length - 2,
|
|
775
|
-
f: 1
|
|
776
|
-
};
|
|
777
|
-
let i = 0;
|
|
778
|
-
while (i < cum.length - 1 && cum[i + 1] < s) i++;
|
|
779
|
-
const span = cum[i + 1] - cum[i];
|
|
780
|
-
return {
|
|
781
|
-
i,
|
|
782
|
-
f: span > 0 ? (s - cum[i]) / span : 0
|
|
783
|
-
};
|
|
784
|
-
};
|
|
785
|
-
const at = (s) => {
|
|
786
|
-
if (pts.length === 1) return [pts[0][0], pts[0][1]];
|
|
787
|
-
const { i, f } = locate(s);
|
|
788
|
-
const a = pts[i];
|
|
789
|
-
const b = pts[i + 1];
|
|
790
|
-
return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f];
|
|
791
|
-
};
|
|
792
|
-
const tangentAt = (s) => {
|
|
793
|
-
if (pts.length === 1) return [1, 0];
|
|
794
|
-
const { i } = locate(s);
|
|
795
|
-
const a = pts[i];
|
|
796
|
-
const b = pts[i + 1];
|
|
797
|
-
const dx = b[0] - a[0];
|
|
798
|
-
const dy = b[1] - a[1];
|
|
799
|
-
const len = Math.hypot(dx, dy);
|
|
800
|
-
return len > 0 ? [dx / len, dy / len] : [1, 0];
|
|
801
|
-
};
|
|
802
|
-
return {
|
|
803
|
-
length: total,
|
|
804
|
-
at,
|
|
805
|
-
tangentAt,
|
|
806
|
-
atProgress: (u) => at(clamp01(u) * total),
|
|
807
|
-
tangentAtProgress: (u) => tangentAt(clamp01(u) * total)
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
/** Total arc length of a path. */
|
|
811
|
-
function pathLength(path) {
|
|
812
|
-
return motionPath(path).length;
|
|
813
|
-
}
|
|
814
|
-
/** Point at arc-length s along a path (clamped to [0, length]). */
|
|
815
|
-
function pointAtLength(path, s) {
|
|
816
|
-
return motionPath(path).at(s);
|
|
817
|
-
}
|
|
818
|
-
/**
|
|
819
|
-
* A companion node that drives `target` along `path` as `progress` animates.
|
|
820
|
-
* Owns the target's `position` (and `rotation` when `orient`) via pull-based
|
|
821
|
-
* binding, so there's no eval-order side effect. Add it to the scene (its
|
|
822
|
-
* `progress` is the animatable target); it draws nothing itself.
|
|
823
|
-
*/
|
|
824
|
-
var FollowPath = class extends Node {
|
|
825
|
-
target;
|
|
826
|
-
progress;
|
|
827
|
-
constructor(props) {
|
|
828
|
-
super(props);
|
|
829
|
-
this.target = props.target;
|
|
830
|
-
this.progress = signal(1);
|
|
831
|
-
if (typeof props.progress === "function") this.progress.bindSource(props.progress);
|
|
832
|
-
else if (props.progress !== void 0) this.progress.set(props.progress);
|
|
833
|
-
this.registerTarget("progress", this.progress, "number");
|
|
834
|
-
const sOpts = props.samplesPerSegment !== void 0 ? { samplesPerSegment: props.samplesPerSegment } : {};
|
|
835
|
-
const getPath = props.path instanceof Path ? () => props.path.data() : () => props.path;
|
|
836
|
-
let cachedPath = getPath();
|
|
837
|
-
let cachedSampler = motionPath(cachedPath, sOpts);
|
|
838
|
-
const sampler = () => {
|
|
839
|
-
const pv = getPath();
|
|
840
|
-
if (pv !== cachedPath) {
|
|
841
|
-
cachedPath = pv;
|
|
842
|
-
cachedSampler = motionPath(pv, sOpts);
|
|
843
|
-
}
|
|
844
|
-
return cachedSampler;
|
|
845
|
-
};
|
|
846
|
-
props.target.position.bindSource(() => sampler().atProgress(this.progress()));
|
|
847
|
-
if (props.orient) {
|
|
848
|
-
const offset = props.orientOffset ?? 0;
|
|
849
|
-
props.target.rotation.bindSource(() => {
|
|
850
|
-
const t = sampler().tangentAtProgress(this.progress());
|
|
851
|
-
return Math.atan2(t[1], t[0]) * 180 / Math.PI + offset;
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
draw() {}
|
|
856
|
-
};
|
|
857
|
-
/** `children: [route, cursor, followPath(cursor, route, { orient: true })]` — cursor traces the route.
|
|
858
|
-
* Pass the Path *node* to follow it as it morphs; pass a PathValue for a fixed route. */
|
|
859
|
-
function followPath(target, path, props = {}) {
|
|
860
|
-
return new FollowPath({
|
|
861
|
-
...props,
|
|
862
|
-
target,
|
|
863
|
-
path
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
//#endregion
|
|
867
|
-
//#region src/tokenHighlight.ts
|
|
868
|
-
/**
|
|
869
|
-
* Multi-range token highlight: sub-line ranges over a Text node's wordBoxes,
|
|
870
|
-
* each with its OWN animatable fill/opacity/progress/scale — four-color
|
|
871
|
-
* category passes, per-token flips, karaoke-with-color. Design answers from
|
|
872
|
-
* downstream production (the NNDL verification ritual):
|
|
873
|
-
* - ranges VALIDATE at construction and THROW on copy drift at draw — the
|
|
874
|
-
* throw is load-bearing (it catches edited copy that no longer matches);
|
|
875
|
-
* `rematch: true` opts animated text into per-frame re-resolution.
|
|
876
|
-
* - a range spanning a wrap produces one rect per line segment.
|
|
877
|
-
* - string matches are whitespace-stripped consecutive box runs and must end
|
|
878
|
-
* boundary-exact (mid-segment end = error listing the actual segments);
|
|
879
|
-
* `[wordIndex, wordIndex]` ranges sidestep matching entirely.
|
|
880
|
-
*/
|
|
881
|
-
var TokenMatchError = class extends Error {
|
|
882
|
-
constructor(message) {
|
|
883
|
-
super(message);
|
|
884
|
-
this.name = "TokenMatchError";
|
|
885
|
-
}
|
|
886
|
-
};
|
|
887
|
-
const stripWs = (s) => s.replace(/\s+/g, "");
|
|
888
|
-
/**
|
|
889
|
-
* Find the Nth whitespace-stripped consecutive run of boxes equal to token.
|
|
890
|
-
* Boundary-exact: a run that diverges mid-segment is not a match; ending
|
|
891
|
-
* mid-segment throws (with the real segment list) rather than half-boxing.
|
|
892
|
-
*/
|
|
893
|
-
function matchTokenRun(boxes, token, occurrence = 1) {
|
|
894
|
-
const want = stripWs(token);
|
|
895
|
-
if (want.length === 0) throw new TokenMatchError("empty token");
|
|
896
|
-
let seen = 0;
|
|
897
|
-
for (let i = 0; i < boxes.length; i++) {
|
|
898
|
-
let acc = "";
|
|
899
|
-
for (let j = i; j < boxes.length; j++) {
|
|
900
|
-
acc += stripWs(boxes[j].text);
|
|
901
|
-
if (acc === want) {
|
|
902
|
-
seen++;
|
|
903
|
-
if (seen === occurrence) return [i, j];
|
|
904
|
-
break;
|
|
905
|
-
}
|
|
906
|
-
if (!want.startsWith(acc)) break;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
throw new TokenMatchError(`no match for token '${token}'${occurrence > 1 ? ` (occurrence ${occurrence})` : ""} — segments are [${boxes.map((b) => `'${b.text}'`).join(", ")}]`);
|
|
910
|
-
}
|
|
911
|
-
var TokenHighlight = class extends Node {
|
|
912
|
-
target;
|
|
913
|
-
padding;
|
|
914
|
-
cornerRadius;
|
|
915
|
-
rematch;
|
|
916
|
-
ranges;
|
|
917
|
-
constructor(props) {
|
|
918
|
-
super(props);
|
|
919
|
-
this.target = props.text;
|
|
920
|
-
this.padding = props.padding ?? [4, 2];
|
|
921
|
-
this.cornerRadius = props.cornerRadius ?? 4;
|
|
922
|
-
this.rematch = props.rematch ?? false;
|
|
923
|
-
const boxes = this.target.wordBoxes();
|
|
924
|
-
this.ranges = props.ranges.map((spec, index) => {
|
|
925
|
-
const run = this.resolveRun(boxes, spec);
|
|
926
|
-
const id = spec.id ?? `r${index}`;
|
|
927
|
-
const r = {
|
|
928
|
-
spec,
|
|
929
|
-
fill: init(signal("#ffe066"), spec.fill),
|
|
930
|
-
opacity: init(signal(1), spec.opacity),
|
|
931
|
-
progress: init(signal(1), spec.progress),
|
|
932
|
-
scale: init(signal(1), spec.scale),
|
|
933
|
-
offset: initVec(vec2Signal([0, 0]), spec.offset),
|
|
934
|
-
run,
|
|
935
|
-
bound: runText(boxes, run)
|
|
936
|
-
};
|
|
937
|
-
this.registerTarget(`${id}/fill`, r.fill, "color");
|
|
938
|
-
this.registerTarget(`${id}/opacity`, r.opacity, "number");
|
|
939
|
-
this.registerTarget(`${id}/progress`, r.progress, "number");
|
|
940
|
-
this.registerTarget(`${id}/scale`, r.scale, "number");
|
|
941
|
-
this.registerTarget(`${id}/offset`, r.offset, "vec2");
|
|
942
|
-
this.registerTarget(`${id}/offset.x`, r.offset.x, "number");
|
|
943
|
-
this.registerTarget(`${id}/offset.y`, r.offset.y, "number");
|
|
944
|
-
return r;
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
resolveRun(boxes, spec) {
|
|
948
|
-
if (typeof spec.match !== "string") {
|
|
949
|
-
const [from, to] = spec.match;
|
|
950
|
-
if (from < 0 || to >= boxes.length || from > to) throw new TokenMatchError(`word index range [${from}, ${to}] out of bounds (${boxes.length} boxes)`);
|
|
951
|
-
return [from, to];
|
|
952
|
-
}
|
|
953
|
-
return matchTokenRun(boxes, spec.match, spec.occurrence ?? 1);
|
|
954
|
-
}
|
|
955
|
-
draw(out, ctx) {
|
|
956
|
-
const boxes = this.target.wordBoxes(ctx.measurer);
|
|
957
|
-
if (boxes.length === 0) return;
|
|
958
|
-
const tm = this.target.localMatrix();
|
|
959
|
-
const [px, py] = this.padding;
|
|
960
|
-
let pushedTransform = false;
|
|
961
|
-
for (const r of this.ranges) {
|
|
962
|
-
const opacity = r.opacity();
|
|
963
|
-
if (opacity <= 0) continue;
|
|
964
|
-
const progress = Math.min(1, Math.max(0, r.progress()));
|
|
965
|
-
if (progress <= 0) continue;
|
|
966
|
-
let run = r.run;
|
|
967
|
-
if (this.rematch && typeof r.spec.match === "string") run = matchTokenRun(boxes, r.spec.match, r.spec.occurrence ?? 1);
|
|
968
|
-
else if (runText(boxes, run) !== r.bound) throw new TokenMatchError(`bound token '${r.bound}' no longer matches boxes [${run[0]}, ${run[1]}] — the text changed (segments now [${boxes.slice(run[0], run[1] + 1).map((b) => `'${b.text}'`).join(", ")}]); pass rematch: true for animated text`);
|
|
969
|
-
const lineRects = [];
|
|
970
|
-
for (let i = run[0]; i <= run[1]; i++) {
|
|
971
|
-
const b = boxes[i];
|
|
972
|
-
const last = lineRects[lineRects.length - 1];
|
|
973
|
-
const cur = i > run[0] && boxes[i - 1].line === b.line ? last : void 0;
|
|
974
|
-
if (cur) {
|
|
975
|
-
cur.w = b.x + b.w + px - cur.x;
|
|
976
|
-
cur.y = Math.min(cur.y, b.y - py);
|
|
977
|
-
cur.h = Math.max(cur.h, b.y + b.h + py - cur.y);
|
|
978
|
-
} else lineRects.push({
|
|
979
|
-
x: b.x - px,
|
|
980
|
-
y: b.y - py,
|
|
981
|
-
w: b.w + 2 * px,
|
|
982
|
-
h: b.h + 2 * py
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
if (!pushedTransform && !matEquals(tm, IDENTITY)) {
|
|
986
|
-
out.push({
|
|
987
|
-
op: "transform",
|
|
988
|
-
m: tm
|
|
989
|
-
});
|
|
990
|
-
pushedTransform = true;
|
|
991
|
-
}
|
|
992
|
-
const grouped = opacity < 1;
|
|
993
|
-
if (grouped) out.push({
|
|
994
|
-
op: "pushGroup",
|
|
995
|
-
opacity,
|
|
996
|
-
blend: "source-over",
|
|
997
|
-
filters: []
|
|
998
|
-
});
|
|
999
|
-
const fill = r.fill();
|
|
1000
|
-
const scale = r.scale();
|
|
1001
|
-
const [ox, oy] = r.offset();
|
|
1002
|
-
let remaining = progress * lineRects.reduce((sum, q) => sum + q.w, 0);
|
|
1003
|
-
for (const q of lineRects) {
|
|
1004
|
-
const fillW = Math.min(q.w, remaining);
|
|
1005
|
-
remaining -= fillW;
|
|
1006
|
-
if (fillW <= 0) break;
|
|
1007
|
-
const cx = q.x + q.w / 2 + ox;
|
|
1008
|
-
const cy = q.y + q.h / 2 + oy;
|
|
1009
|
-
const w = fillW * scale;
|
|
1010
|
-
const h = q.h * scale;
|
|
1011
|
-
const x = cx - q.w / 2 * scale;
|
|
1012
|
-
const y = cy - h / 2;
|
|
1013
|
-
const rad = Math.min(this.cornerRadius, w / 2, h / 2);
|
|
1014
|
-
const path = out.resource({
|
|
1015
|
-
kind: "path",
|
|
1016
|
-
segs: roundedRectSegs(x, y, w, h, rad)
|
|
1017
|
-
});
|
|
1018
|
-
out.push({
|
|
1019
|
-
op: "fillPath",
|
|
1020
|
-
path,
|
|
1021
|
-
paint: {
|
|
1022
|
-
kind: "color",
|
|
1023
|
-
color: fill
|
|
1024
|
-
}
|
|
1025
|
-
});
|
|
1026
|
-
if (remaining <= 0) break;
|
|
1027
|
-
}
|
|
1028
|
-
if (grouped) out.push({ op: "popGroup" });
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
};
|
|
1032
|
-
function runText(boxes, run) {
|
|
1033
|
-
return boxes.slice(run[0], run[1] + 1).map((b) => stripWs(b.text)).join("");
|
|
1034
|
-
}
|
|
1035
|
-
/**
|
|
1036
|
-
* `children: [tokenHighlight(para, { ranges: [{ match: '$48,200', fill: cat.money }] }), para]`
|
|
1037
|
-
* — each range animates independently via '<id>/<rangeId>/fill|opacity|progress|scale'.
|
|
1038
|
-
*/
|
|
1039
|
-
function tokenHighlight(text, props) {
|
|
1040
|
-
return new TokenHighlight({
|
|
1041
|
-
...props,
|
|
1042
|
-
text
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
function init(sig, v) {
|
|
1046
|
-
if (typeof v === "function") sig.bindSource(v);
|
|
1047
|
-
else if (v !== void 0) sig.set(v);
|
|
1048
|
-
return sig;
|
|
1049
|
-
}
|
|
1050
|
-
function initVec(sig, v) {
|
|
1051
|
-
if (typeof v === "function") sig.bindSource(v);
|
|
1052
|
-
else if (v !== void 0) sig.set(v);
|
|
1053
|
-
return sig;
|
|
1054
|
-
}
|
|
1055
|
-
//#endregion
|
|
1056
542
|
//#region src/fontUsage.ts
|
|
1057
543
|
/**
|
|
1058
544
|
* Scene → font-validation bridge (DESIGN.md §3.6). Core owns the AssetRef,
|
|
@@ -1488,6 +974,7 @@ var Raster2D = class {
|
|
|
1488
974
|
images = /* @__PURE__ */ new Map();
|
|
1489
975
|
videos = /* @__PURE__ */ new Map();
|
|
1490
976
|
warnedShaders = false;
|
|
977
|
+
warnedFontVariation = false;
|
|
1491
978
|
/**
|
|
1492
979
|
* §3.5 bitmap LRU: device-transform-qualified cacheKey → rasterized layer.
|
|
1493
980
|
* A Map preserves insertion order, so the oldest key is `keys().next()` —
|
|
@@ -1756,10 +1243,19 @@ var Raster2D = class {
|
|
|
1756
1243
|
case "fillText": {
|
|
1757
1244
|
const ctx = ctxOf();
|
|
1758
1245
|
ctx.font = fontString(cmd.font);
|
|
1246
|
+
const axes = cmd.font.fontVariationSettings;
|
|
1247
|
+
if (axes !== void 0) {
|
|
1248
|
+
if ("fontVariationSettings" in ctx) ctx.fontVariationSettings = axes;
|
|
1249
|
+
else if (!this.warnedFontVariation) {
|
|
1250
|
+
this.warnedFontVariation = true;
|
|
1251
|
+
emitDevWarning("fontVariationSettings ('" + axes + "') is not applied here: this 2D context has no fontVariationSettings property (the browser DOM canvas) — variable-font axes render on the Skia/export path; use the discrete fontWeight named instances for a browser weight (§3.6)");
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1759
1254
|
ctx.fillStyle = resolveFill(ctx, cmd.paint, null);
|
|
1760
1255
|
ctx.textBaseline = "alphabetic";
|
|
1761
1256
|
ctx.textAlign = cmd.align ?? "left";
|
|
1762
1257
|
ctx.fillText(cmd.text, cmd.x, cmd.y);
|
|
1258
|
+
if (axes !== void 0 && "fontVariationSettings" in ctx) ctx.fontVariationSettings = "normal";
|
|
1763
1259
|
try {
|
|
1764
1260
|
const width = ctx.measureText(cmd.text).width;
|
|
1765
1261
|
const align = cmd.align ?? "left";
|
|
@@ -1857,4 +1353,4 @@ var Raster2D = class {
|
|
|
1857
1353
|
}
|
|
1858
1354
|
};
|
|
1859
1355
|
//#endregion
|
|
1860
|
-
export { ALL_FILTER_KINDS, Circle, ColdAssetError, Custom,
|
|
1356
|
+
export { ALL_FILTER_KINDS, Circle, ColdAssetError, Custom, DeterminismViolationError, DuplicateNodeIdError, EachError, FilterValidationError, Group, Highlight, IDENTITY, ImageNode as Image, ImageNode, LayoutEngineMissingError, MEASURE_QUANTUM_PX, MESH_DOWNSCALE, MESH_SHEPARD_POWER, MESH_SIGMA, NODE_TAXONOMY, Node, Path, Raster2D, Rect, ReservedNodeIdError, ShaderEffect, SketchValidationError, Text, TextCursor, Video, __resetEstimateWarnings, applyToPoint, arcLength, bindScene, breakLines, coercePathData, collapseReplacer, collectLocalizedTextUsages, collectTextUsages, createDisplayListBuilder, createScene, drawOn, drawOnEach, each, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, isEstimatingMeasurer, matEquals, meshRasterSize, multiply, pathFromSegs, quantize, rasterizeMesh, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
|