domotion-svg 0.2.2 → 0.3.2
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/FEATURES.md +1 -0
- package/README.md +29 -0
- package/dist/animation/animator.js +25 -14
- package/dist/animation/animator.test.js +54 -21
- package/dist/animation/cursor-overlay.js +0 -2
- package/dist/capture/emoji.js +29 -18
- package/dist/capture/index.js +5 -4
- package/dist/capture/script/color-norm.d.ts +1 -0
- package/dist/capture/script/color-norm.js +43 -1
- package/dist/capture/script/emoji-detect.js +14 -0
- package/dist/capture/script/index.js +593 -65
- package/dist/capture/script/walker/borders-backgrounds.d.ts +24 -17
- package/dist/capture/script/walker/borders-backgrounds.js +123 -7
- package/dist/capture/script/walker/counter-style-resolver.d.ts +7 -0
- package/dist/capture/script/walker/counter-style-resolver.js +218 -0
- package/dist/capture/script/walker/input-value.js +14 -1
- package/dist/capture/script/walker/lists-counters.d.ts +3 -1
- package/dist/capture/script/walker/lists-counters.js +22 -2
- package/dist/capture/script/walker/masks-clips.d.ts +2 -0
- package/dist/capture/script/walker/masks-clips.js +41 -1
- package/dist/capture/script/walker/pseudo-content.d.ts +14 -1
- package/dist/capture/script/walker/pseudo-content.js +301 -61
- package/dist/capture/script/walker/pseudo-inject.js +20 -0
- package/dist/capture/script/walker/text-segments.js +98 -4
- package/dist/capture/script/walker/transforms.d.ts +1 -0
- package/dist/capture/script/walker/transforms.js +16 -0
- package/dist/capture/script.generated.js +1 -1
- package/dist/capture/types.d.ts +213 -2
- package/dist/cli/animate.js +151 -15
- package/dist/mask.test.js +12 -7
- package/dist/render/borders.d.ts +9 -13
- package/dist/render/borders.js +379 -14
- package/dist/render/element-tree-to-svg.d.ts +11 -12
- package/dist/render/element-tree-to-svg.js +2046 -241
- package/dist/render/embedded-font-builder.d.ts +49 -0
- package/dist/render/embedded-font-builder.js +149 -0
- package/dist/render/form-controls.js +45 -24
- package/dist/render/gradients.d.ts +15 -0
- package/dist/render/gradients.js +103 -2
- package/dist/render/gradients.test.js +34 -0
- package/dist/render/text-to-path.d.ts +38 -1
- package/dist/render/text-to-path.js +654 -29
- package/dist/render/text-to-path.test.js +230 -9
- package/dist/render/text.d.ts +14 -0
- package/dist/render/text.js +344 -40
- package/dist/scroll/composer.d.ts +26 -0
- package/dist/scroll/composer.js +199 -11
- package/dist/scroll/composer.test.js +293 -16
- package/dist/scroll/executor.d.ts +3 -1
- package/dist/scroll/executor.js +15 -6
- package/dist/scroll/executor.test.js +25 -0
- package/dist/scroll/hoist-fixed.d.ts +48 -0
- package/dist/scroll/hoist-fixed.js +85 -0
- package/dist/scroll/hoist-fixed.test.d.ts +1 -0
- package/dist/scroll/hoist-fixed.test.js +103 -0
- package/dist/scroll/hoist-sticky.d.ts +45 -0
- package/dist/scroll/hoist-sticky.js +157 -0
- package/dist/scroll/hoist-sticky.test.d.ts +1 -0
- package/dist/scroll/hoist-sticky.test.js +154 -0
- package/dist/scroll/pattern.d.ts +22 -5
- package/dist/scroll/pattern.js +55 -7
- package/dist/scroll/pattern.test.js +48 -1
- package/dist/tree-ops/frame-merge.d.ts +10 -0
- package/dist/tree-ops/frame-merge.js +23 -5
- package/dist/tree-ops/frame-merge.test.js +45 -0
- package/dist/tree-ops/tree-diff.js +1 -1
- package/dist/tree-ops/viewbox-culling.js +32 -18
- package/dist/tree-ops/viewbox-culling.test.js +40 -6
- package/package.json +8 -2
- package/src/animation/animator.test.ts +56 -21
- package/src/animation/animator.ts +25 -14
- package/src/animation/cursor-overlay.ts +0 -2
- package/src/capture/emoji.ts +28 -18
- package/src/capture/index.ts +15 -14
- package/src/capture/script/color-norm.ts +38 -1
- package/src/capture/script/emoji-detect.ts +14 -0
- package/src/capture/script/index.ts +555 -48
- package/src/capture/script/walker/borders-backgrounds.ts +114 -7
- package/src/capture/script/walker/counter-style-resolver.ts +184 -0
- package/src/capture/script/walker/input-value.ts +14 -1
- package/src/capture/script/walker/lists-counters.ts +24 -2
- package/src/capture/script/walker/masks-clips.ts +40 -1
- package/src/capture/script/walker/pseudo-content.ts +297 -55
- package/src/capture/script/walker/pseudo-inject.ts +20 -0
- package/src/capture/script/walker/text-segments.ts +93 -4
- package/src/capture/script/walker/transforms.ts +14 -0
- package/src/capture/script.generated.ts +1 -1
- package/src/capture/types.ts +202 -2
- package/src/cli/animate.ts +135 -15
- package/src/mask.test.ts +12 -7
- package/src/render/borders.ts +383 -17
- package/src/render/element-tree-to-svg.ts +2051 -238
- package/src/render/embedded-font-builder.ts +221 -0
- package/src/render/form-controls.ts +45 -24
- package/src/render/gradients.test.ts +46 -0
- package/src/render/gradients.ts +94 -2
- package/src/render/opentype.js.d.ts +7 -0
- package/src/render/text-to-path.test.ts +246 -9
- package/src/render/text-to-path.ts +702 -31
- package/src/render/text.ts +344 -40
- package/src/scroll/composer.test.ts +322 -16
- package/src/scroll/composer.ts +246 -13
- package/src/scroll/executor.test.ts +27 -0
- package/src/scroll/executor.ts +19 -10
- package/src/scroll/hoist-fixed.test.ts +117 -0
- package/src/scroll/hoist-fixed.ts +95 -0
- package/src/scroll/hoist-sticky.test.ts +173 -0
- package/src/scroll/hoist-sticky.ts +193 -0
- package/src/scroll/pattern.test.ts +58 -1
- package/src/scroll/pattern.ts +71 -8
- package/src/tree-ops/frame-merge.test.ts +51 -0
- package/src/tree-ops/frame-merge.ts +24 -6
- package/src/tree-ops/tree-diff.ts +3 -1
- package/src/tree-ops/viewbox-culling.test.ts +42 -6
- package/src/tree-ops/viewbox-culling.ts +32 -18
|
@@ -152,11 +152,24 @@ function parseAttrs(s: string): Record<string, string> {
|
|
|
152
152
|
* including `id`, those two defs would collapse into one and `<use href="#g198"/>`
|
|
153
153
|
* references would break — see frame-merge.test.ts "preserves distinct ids
|
|
154
154
|
* for paths with identical d".
|
|
155
|
+
*
|
|
156
|
+
* Resource references (`clip-path`, `mask`, `filter`) are included because
|
|
157
|
+
* they point at frame-scoped defs whose IDs are unique per frame. Two
|
|
158
|
+
* wrappers with different clip-path ids are NOT the same logical element
|
|
159
|
+
* even if every other attribute matches — merging them would (a) drop one
|
|
160
|
+
* frame's clip-path entirely, and (b) push that frame's unique content
|
|
161
|
+
* into the merge bucket of a different frame, which then sorts as one
|
|
162
|
+
* earlier-firstFrame group and lets later-frame siblings (e.g. solid
|
|
163
|
+
* background rects) emit *after* the content they should sit underneath.
|
|
164
|
+
* See frame-merge.test.ts "keeps per-frame body bg ordered before content".
|
|
155
165
|
*/
|
|
156
166
|
export function structuralFingerprint(n: ParsedNode): string {
|
|
157
167
|
if (n.kind === "text") return `T:${n.text}`;
|
|
158
168
|
const a = n.attrs ?? {};
|
|
159
|
-
const keyAttrs = [
|
|
169
|
+
const keyAttrs = [
|
|
170
|
+
"id", "transform", "href", "x", "y", "d", "width", "height",
|
|
171
|
+
"fill", "role", "class", "clip-path", "mask", "filter",
|
|
172
|
+
];
|
|
160
173
|
const pairs = keyAttrs.filter((k) => a[k] != null).map((k) => `${k}=${a[k]}`);
|
|
161
174
|
return `${n.tag}|${pairs.join(",")}`;
|
|
162
175
|
}
|
|
@@ -312,7 +325,7 @@ function mergeNode(
|
|
|
312
325
|
const parts: string[] = [];
|
|
313
326
|
for (const g of groups) {
|
|
314
327
|
const visibleFrames = g.occurrences.map((o, i) => (o != null ? i : -1)).filter((i) => i >= 0);
|
|
315
|
-
const contents = g.occurrences.filter((o) => o != null)
|
|
328
|
+
const contents = g.occurrences.filter((o): o is ParsedNode => o != null);
|
|
316
329
|
|
|
317
330
|
// Fast path: this element is byte-identical across all its occurrences.
|
|
318
331
|
const raws = new Set(contents.map((c) => c.raw));
|
|
@@ -428,9 +441,14 @@ function buildTimelineKeyframes(name: string, visibleFrames: number[], timing: F
|
|
|
428
441
|
if (rangeStart != null && prev != null) ranges.push([rangeStart, prev]);
|
|
429
442
|
|
|
430
443
|
// Build keyframe stops. Use step-end so opacity switches instantly.
|
|
431
|
-
// DM-599: emit
|
|
432
|
-
//
|
|
433
|
-
// window.
|
|
444
|
+
// DM-599: emit a paint-skip toggle alongside opacity so the browser can
|
|
445
|
+
// skip painting elements that aren't currently in their visible-frames
|
|
446
|
+
// window. DM-641: this was `display: none/inline`, which broke for any
|
|
447
|
+
// element whose 0% keyframe is `display: none` — Chromium parks the
|
|
448
|
+
// animation when the element drops out of the render tree and never
|
|
449
|
+
// ticks the keyframe that would bring it back. Switching to
|
|
450
|
+
// `visibility` keeps the element rendered (still no paint) so the
|
|
451
|
+
// animation continues across cycles.
|
|
434
452
|
const stops: Array<[number, number]> = []; // (pct, opacity)
|
|
435
453
|
stops.push([0, 0]);
|
|
436
454
|
for (const [lo, hi] of ranges) {
|
|
@@ -446,7 +464,7 @@ function buildTimelineKeyframes(name: string, visibleFrames: number[], timing: F
|
|
|
446
464
|
stops.push([100, 0]);
|
|
447
465
|
|
|
448
466
|
const lines = stops.map(([p, o]) =>
|
|
449
|
-
` ${p.toFixed(3)}% { opacity: ${o};
|
|
467
|
+
` ${p.toFixed(3)}% { opacity: ${o}; visibility: ${o === 1 ? "visible" : "hidden"}; }`,
|
|
450
468
|
);
|
|
451
469
|
return ` @keyframes ${name} {\n${lines.join("\n")}\n }\n .${name} { animation: ${name} var(--scene-dur) infinite; animation-timing-function: step-end; }`;
|
|
452
470
|
}
|
|
@@ -215,7 +215,9 @@ export function entriesOfKind(diff: TreeDiff, ...kinds: DiffEntryKind[]): DiffEn
|
|
|
215
215
|
* per-element keyframes.
|
|
216
216
|
*/
|
|
217
217
|
export function dominantTranslate(diff: TreeDiff): { dx: number; dy: number; fraction: number } | null {
|
|
218
|
-
const movers = diff.entries.filter((e)
|
|
218
|
+
const movers = diff.entries.filter((e): e is DiffEntry & { dx: number; dy: number } =>
|
|
219
|
+
e.kind === "translated" && typeof e.dx === "number" && typeof e.dy === "number",
|
|
220
|
+
);
|
|
219
221
|
if (movers.length === 0) return null;
|
|
220
222
|
// Bucket by rounded (dx, dy) and pick the largest bucket.
|
|
221
223
|
const buckets = new Map<string, { dx: number; dy: number; n: number }>();
|
|
@@ -187,8 +187,9 @@ describe("cullElementsOutsideViewBox — tree walk", () => {
|
|
|
187
187
|
expect(tree.children![1].cullClass).toBe("cull-0");
|
|
188
188
|
// One coalesced keyframes block, not two.
|
|
189
189
|
expect((css.match(/@keyframes cull-\d+/g) ?? []).length).toBe(1);
|
|
190
|
-
|
|
191
|
-
expect(css).toContain("
|
|
190
|
+
// DM-641: keyframes now toggle visibility instead of display.
|
|
191
|
+
expect(css).toContain("visibility: visible");
|
|
192
|
+
expect(css).toContain("visibility: hidden");
|
|
192
193
|
});
|
|
193
194
|
|
|
194
195
|
it("element fully outside viewBox under an animation that never reaches it: alwaysHidden", () => {
|
|
@@ -210,6 +211,42 @@ describe("cullElementsOutsideViewBox — tree walk", () => {
|
|
|
210
211
|
expect(css).toBe("");
|
|
211
212
|
});
|
|
212
213
|
|
|
214
|
+
it("DM-650: parent bbox outside viewBox, child bbox inside — parent kept, child kept, all visible", () => {
|
|
215
|
+
// Reproduces NYT-mobile-scroll: at scrollY=844 the body element has
|
|
216
|
+
// height: 100vh (height=844) and rect.top=-844, so its bbox sits
|
|
217
|
+
// exactly above the viewport (b.y + b.h = 0). Children are at their
|
|
218
|
+
// own viewport-relative coordinates and ARE in-viewport. Before the
|
|
219
|
+
// fix, the bottom-up walk culled the body and skipped recursion,
|
|
220
|
+
// hiding the whole subtree → seg renders white.
|
|
221
|
+
const tree: CapturedElement = el({
|
|
222
|
+
x: 0, y: -844, width: 390, height: 844, tag: "body",
|
|
223
|
+
children: [
|
|
224
|
+
el({ x: 0, y: 100, width: 390, height: 200, tag: "div", text: "headline" }),
|
|
225
|
+
el({ x: 0, y: 400, width: 390, height: 100, tag: "p", text: "body copy" }),
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
cullElementsOutsideViewBox(tree, 390, 844, undefined, 0, 1000);
|
|
229
|
+
expect(tree.displayNone).toBeFalsy();
|
|
230
|
+
expect(tree.children![0].displayNone).toBeFalsy();
|
|
231
|
+
expect(tree.children![1].displayNone).toBeFalsy();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("DM-650: parent bbox outside, every child also outside — parent AND children all hidden", () => {
|
|
235
|
+
// Same shape as above but with children also outside viewBox. Now it
|
|
236
|
+
// IS safe to mark the parent displayNone (and every descendant too).
|
|
237
|
+
const tree: CapturedElement = el({
|
|
238
|
+
x: 0, y: -844, width: 390, height: 844, tag: "body",
|
|
239
|
+
children: [
|
|
240
|
+
el({ x: 0, y: -800, width: 390, height: 100, tag: "div" }), // above viewBox
|
|
241
|
+
el({ x: 0, y: -700, width: 390, height: 200, tag: "p" }), // above viewBox
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
cullElementsOutsideViewBox(tree, 390, 844, undefined, 0, 1000);
|
|
245
|
+
expect(tree.displayNone).toBe(true);
|
|
246
|
+
expect(tree.children![0].displayNone).toBe(true);
|
|
247
|
+
expect(tree.children![1].displayNone).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
213
250
|
it("respects child's own animId over inherited animation", () => {
|
|
214
251
|
// Parent has a scroll animation; child has its OWN slide-in animation.
|
|
215
252
|
// Child's culling should use its OWN animation, not the parent's.
|
|
@@ -248,9 +285,8 @@ describe("cullElementsOutsideViewBox — keyframes structure", () => {
|
|
|
248
285
|
const { css } = cullElementsOutsideViewBox(tree, VW, VH, [anim], 0, 1000);
|
|
249
286
|
expect(css).toContain("animation-timing-function: step-end");
|
|
250
287
|
expect(css).toContain("var(--scene-dur)");
|
|
251
|
-
// 0%
|
|
252
|
-
expect(css).toMatch(/0% \{
|
|
253
|
-
|
|
254
|
-
expect(css).toMatch(/100% \{ display: none/);
|
|
288
|
+
// 0% / 100% bookends with visibility:hidden (DM-641 — was display:none).
|
|
289
|
+
expect(css).toMatch(/0% \{ visibility: hidden/);
|
|
290
|
+
expect(css).toMatch(/100% \{ visibility: hidden/);
|
|
255
291
|
});
|
|
256
292
|
});
|
|
@@ -262,12 +262,25 @@ export function cullElementsOutsideViewBox(
|
|
|
262
262
|
const windowToClass = new Map<string, string>();
|
|
263
263
|
const cssBlocks: string[] = [];
|
|
264
264
|
|
|
265
|
-
|
|
265
|
+
// Walk bottom-up: recurse FIRST, then decide the element's own cull. A
|
|
266
|
+
// parent can only safely inherit `displayNone` if every descendant is
|
|
267
|
+
// also fully hidden — children of an `overflow: visible` parent paint
|
|
268
|
+
// outside the parent's bbox and stay in-viewport even when the parent's
|
|
269
|
+
// bbox is entirely above/below the viewBox (DM-650: NYT body is
|
|
270
|
+
// height: 100vh, so at scrollY > 0 the body bbox sits exactly above
|
|
271
|
+
// the viewport but its descendants are in-viewport). Returns true if
|
|
272
|
+
// any element in the subtree (including `el` itself) is visible.
|
|
273
|
+
const walk = (el: CapturedElement, inheritedCtx: AnimationFrameContext | null): boolean => {
|
|
266
274
|
const { ctx, decision } = decideForElement(el, viewportW, viewportH, inheritedCtx, animsById);
|
|
267
|
-
|
|
275
|
+
let anyDescendantVisible = false;
|
|
276
|
+
if (el.children != null) {
|
|
277
|
+
for (const child of el.children) {
|
|
278
|
+
if (walk(child, ctx)) anyDescendantVisible = true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (decision.alwaysHidden && !anyDescendantVisible) {
|
|
268
282
|
el.displayNone = true;
|
|
269
|
-
|
|
270
|
-
return;
|
|
283
|
+
return false;
|
|
271
284
|
}
|
|
272
285
|
if (decision.visStartPct != null && decision.visEndPct != null) {
|
|
273
286
|
const key = `${r3(decision.visStartPct)},${r3(decision.visEndPct)}`;
|
|
@@ -277,14 +290,9 @@ export function cullElementsOutsideViewBox(
|
|
|
277
290
|
windowToClass.set(key, className);
|
|
278
291
|
cssBlocks.push(buildCullKeyframes(className, decision.visStartPct, decision.visEndPct));
|
|
279
292
|
}
|
|
280
|
-
// Merge with any existing cullClass (shouldn't happen — each pass
|
|
281
|
-
// assigns at most one — but be defensive).
|
|
282
293
|
el.cullClass = el.cullClass == null || el.cullClass === "" ? className : `${el.cullClass} ${className}`;
|
|
283
294
|
}
|
|
284
|
-
|
|
285
|
-
if (el.children != null) {
|
|
286
|
-
for (const child of el.children) walk(child, ctx);
|
|
287
|
-
}
|
|
295
|
+
return true;
|
|
288
296
|
};
|
|
289
297
|
for (const root of roots) walk(root, null);
|
|
290
298
|
|
|
@@ -292,22 +300,28 @@ export function cullElementsOutsideViewBox(
|
|
|
292
300
|
}
|
|
293
301
|
|
|
294
302
|
/**
|
|
295
|
-
* Step-end `@keyframes` block + class rule that toggles `
|
|
296
|
-
* during [visStart, visEnd] and `
|
|
303
|
+
* Step-end `@keyframes` block + class rule that toggles `visibility: visible`
|
|
304
|
+
* during [visStart, visEnd] and `visibility: hidden` outside. The 0.001 % gap
|
|
297
305
|
* pattern keeps the discrete snap point inside a sliver-thin keyframe pair
|
|
298
306
|
* regardless of how the animation timing function is configured on the
|
|
299
307
|
* element.
|
|
308
|
+
*
|
|
309
|
+
* DM-641: toggling `display` here breaks the same way `fv-${i}` did — when
|
|
310
|
+
* a culled element starts the cycle at `display: none` the animation engine
|
|
311
|
+
* never starts ticking and the element stays hidden forever. Using
|
|
312
|
+
* `visibility` keeps the element in the render tree (still skips painting)
|
|
313
|
+
* so the animation runs every cycle.
|
|
300
314
|
*/
|
|
301
315
|
function buildCullKeyframes(name: string, visStartPct: number, visEndPct: number): string {
|
|
302
316
|
const startMinus = Math.max(0, visStartPct - 0.001).toFixed(3);
|
|
303
317
|
const endPlus = Math.min(100, visEndPct + 0.001).toFixed(3);
|
|
304
318
|
return ` @keyframes ${name} {
|
|
305
|
-
0% {
|
|
306
|
-
${startMinus}% {
|
|
307
|
-
${visStartPct.toFixed(3)}% {
|
|
308
|
-
${visEndPct.toFixed(3)}% {
|
|
309
|
-
${endPlus}% {
|
|
310
|
-
100% {
|
|
319
|
+
0% { visibility: hidden; }
|
|
320
|
+
${startMinus}% { visibility: hidden; }
|
|
321
|
+
${visStartPct.toFixed(3)}% { visibility: visible; }
|
|
322
|
+
${visEndPct.toFixed(3)}% { visibility: visible; }
|
|
323
|
+
${endPlus}% { visibility: hidden; }
|
|
324
|
+
100% { visibility: hidden; }
|
|
311
325
|
}
|
|
312
326
|
.${name} { animation: ${name} var(--scene-dur) infinite; animation-timing-function: step-end; }`;
|
|
313
327
|
}
|