domotion-svg 0.28.1 → 0.28.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.
@@ -21,6 +21,8 @@
21
21
  * shrinking output size on mostly-static-content pages.
22
22
  */
23
23
  import type { ScrollSegmentCapture } from "./executor.js";
24
+ import type { CapturedElement } from "../capture/types.js";
25
+ import { type ChildOverflowClipGeometry } from "../render/element-tree-to-svg.js";
24
26
  import { type RenderTextMode } from "../render/text-to-path.js";
25
27
  export interface ScrollComposerOptions {
26
28
  /** Visible viewport width (output SVG width). */
@@ -75,6 +77,20 @@ export interface ScrollComposerOptions {
75
77
  */
76
78
  renderText?: RenderTextMode;
77
79
  }
80
+ export interface ElementScrollStaticLayers {
81
+ underlay: CapturedElement[];
82
+ foregroundLayers: Array<{
83
+ elements: CapturedElement[];
84
+ clips: ChildOverflowClipGeometry[];
85
+ }>;
86
+ }
87
+ /**
88
+ * Split the static context around an element scroll owner's paint slot at each
89
+ * ancestor level. Siblings which paint after the owner branch must remain
90
+ * stationary but render above its moving captures; leaving them in one
91
+ * wholesale underlay reverses CSS z-order wherever the two overlap.
92
+ */
93
+ export declare function splitElementScrollStaticLayers(tree: CapturedElement[], ownerId: string): ElementScrollStaticLayers | null;
78
94
  /**
79
95
  * Fail closed before composition when frame/allowlist/offset authority is
80
96
  * omitted, stale, or assigned to a sibling Chromium frame.
@@ -31,6 +31,7 @@ import { hoistDuplicateImagePayloads } from "../post-processing/hoist-image-payl
31
31
  import { extractFixedSubtrees, dedupeFixedAcrossSegments } from "./hoist-fixed.js";
32
32
  import { extractStickyWindows } from "./hoist-sticky.js";
33
33
  import { mapTreePruning } from "../tree-ops/prune-tree.js";
34
+ import { sortChildrenByPaintOrder } from "../render/stacking.js";
34
35
  /**
35
36
  * Map a parsed scroll-action easing (DM-1076) to a CSS `<timing-function>`, or
36
37
  * `null` when it's absent or `linear` — the composite's animation-level default
@@ -94,6 +95,41 @@ function findCapturedScrollOwnerPath(tree, ownerId, ancestors = []) {
94
95
  }
95
96
  return undefined;
96
97
  }
98
+ /**
99
+ * Split the static context around an element scroll owner's paint slot at each
100
+ * ancestor level. Siblings which paint after the owner branch must remain
101
+ * stationary but render above its moving captures; leaving them in one
102
+ * wholesale underlay reverses CSS z-order wherever the two overlap.
103
+ */
104
+ export function splitElementScrollStaticLayers(tree, ownerId) {
105
+ const ownerPath = findCapturedScrollOwnerPath(tree, ownerId);
106
+ const owner = ownerPath?.at(-1);
107
+ if (ownerPath == null || owner == null || ownerPath.length < 2)
108
+ return null;
109
+ const foregroundLayers = [];
110
+ const foregroundSet = new Set();
111
+ for (let branchIndex = ownerPath.length - 1; branchIndex >= 1; branchIndex--) {
112
+ const parent = ownerPath[branchIndex - 1];
113
+ const branch = ownerPath[branchIndex];
114
+ const paintOrderedSiblings = sortChildrenByPaintOrder(parent.children, parent.styles.display, parent.styles.flexDirection);
115
+ const branchPaintIndex = paintOrderedSiblings.indexOf(branch);
116
+ if (branchPaintIndex < 0)
117
+ return null;
118
+ const elements = paintOrderedSiblings.slice(branchPaintIndex + 1);
119
+ if (elements.length === 0)
120
+ continue;
121
+ for (const element of elements)
122
+ foregroundSet.add(element);
123
+ foregroundLayers.push({
124
+ elements,
125
+ // A sibling shares the branch's clip ancestors but is not a descendant
126
+ // of the branch's own overflow clip.
127
+ clips: ownerPath.slice(0, branchIndex).map((element) => childOverflowClipGeometry(element)).filter((clip) => clip != null),
128
+ });
129
+ }
130
+ const underlay = mapTreePruning(tree, (element) => element === owner || foregroundSet.has(element));
131
+ return { underlay, foregroundLayers };
132
+ }
97
133
  /**
98
134
  * An element scroll owner moves its own clipped contents, not the page around
99
135
  * it. Keep the first capture's surrounding page as a static underlay and feed
@@ -114,7 +150,9 @@ function isolateElementScrollOwner(segments) {
114
150
  // the established list-strip contract; there is no surrounding page to pin.
115
151
  if (firstOwnerPath == null || firstOwner == null || first.tree.includes(firstOwner))
116
152
  return null;
117
- const staticUnderlay = mapTreePruning(first.tree, (element) => element.scrollbars?.owner?.ownerId === first.scrollOwnerId);
153
+ const staticLayers = splitElementScrollStaticLayers(first.tree, first.scrollOwnerId);
154
+ if (staticLayers == null)
155
+ return null;
118
156
  const isolated = segments.map((segment, index) => {
119
157
  if (segment.frameScrollState == null || segment.scrollOwnerId == null) {
120
158
  throw new Error(`composeScrollSvg: inner-scroll segment ${index} omitted its owner authority`);
@@ -136,7 +174,8 @@ function isolateElementScrollOwner(segments) {
136
174
  });
137
175
  return {
138
176
  segments: isolated,
139
- staticUnderlay,
177
+ staticUnderlay: staticLayers.underlay,
178
+ staticForegroundLayers: staticLayers.foregroundLayers,
140
179
  ownerClips: firstOwnerPath.map((element) => childOverflowClipGeometry(element)).filter((clip) => clip != null),
141
180
  };
142
181
  }
@@ -329,6 +368,7 @@ export function composeScrollSvg(segments, opts) {
329
368
  return withRenderTextMode(renderTextMode, () => composeScrollSvgBody(composedSegments, opts, {
330
369
  axis, W, VH, bg, paintBg, hiDPIFactor, chunkSize,
331
370
  staticUnderlay: elementScroll?.staticUnderlay,
371
+ staticForegroundLayers: elementScroll?.staticForegroundLayers,
332
372
  elementOwnerClips: elementScroll?.ownerClips,
333
373
  }));
334
374
  }
@@ -385,7 +425,7 @@ function buildScrollVisibility(segments, segOffsets, totalMs) {
385
425
  * reads (axis / viewport / background / hiDPI / chunk size).
386
426
  */
387
427
  function composeScrollSvgBody(segments, opts, ctx) {
388
- const { axis, W, VH, bg, paintBg, hiDPIFactor, chunkSize, staticUnderlay, elementOwnerClips, } = ctx;
428
+ const { axis, W, VH, bg, paintBg, hiDPIFactor, chunkSize, staticUnderlay, staticForegroundLayers, elementOwnerClips, } = ctx;
389
429
  // ── Total scene duration ──
390
430
  // The last segment's endMs is the cycle length. For a single-segment input,
391
431
  // the scene is effectively static — emit a 1 s loop so the SVG renders
@@ -568,6 +608,14 @@ function composeScrollSvgBody(segments, opts, ctx) {
568
608
  : `\n <g data-scroll-static-context="true"><svg x="0" y="0" width="${W}" height="${VH}" viewBox="0 0 ${W} ${VH}">` +
569
609
  elementTreeToSvgInner(staticUnderlay, W, VH, "static-", false, hiDPIFactor, false) +
570
610
  `</svg></g>`;
611
+ const staticForegroundMarkup = (staticForegroundLayers ?? []).map((layer, layerIndex) => {
612
+ const clipOpen = layer.clips.map((_clip, clipIndex) => ` <g clip-path="url(#${animClass}-foreground-${layerIndex}-clip-${clipIndex})">`).join("\n");
613
+ const clipClose = layer.clips.map(() => " </g>").join("\n");
614
+ const markup = `<g data-scroll-static-foreground="true"><svg x="0" y="0" width="${W}" height="${VH}" viewBox="0 0 ${W} ${VH}">` +
615
+ elementTreeToSvgInner(layer.elements, W, VH, `static-foreground-${layerIndex}-`, false, hiDPIFactor, false) +
616
+ `</svg></g>`;
617
+ return `${clipOpen === "" ? "" : clipOpen + "\n"} ${markup}\n${clipClose}`;
618
+ }).join("\n");
571
619
  // DM-652: collect every `@font-face` rule the embedded-font path
572
620
  // registered during segment + overlay rendering above, into a single
573
621
  // top-level <style> block. Each font appears once (registry is keyed
@@ -585,6 +633,7 @@ function composeScrollSvgBody(segments, opts, ctx) {
585
633
  const ownerClipDefs = (elementOwnerClips ?? []).map((clip, index) => ` <clipPath id="${animClass}-owner-clip-${index}">${roundedRectSvg(clip.x, clip.y, clip.width, clip.height, clip.corners, "")}</clipPath>`).join("\n");
586
634
  const ownerClipOpen = (elementOwnerClips ?? []).map((_clip, index) => ` <g clip-path="url(#${animClass}-owner-clip-${index})">`).join("\n");
587
635
  const ownerClipClose = (elementOwnerClips ?? []).map(() => " </g>").join("\n");
636
+ const foregroundClipDefs = (staticForegroundLayers ?? []).flatMap((layer, layerIndex) => layer.clips.map((clip, clipIndex) => ` <clipPath id="${animClass}-foreground-${layerIndex}-clip-${clipIndex}">${roundedRectSvg(clip.x, clip.y, clip.width, clip.height, clip.corners, "")}</clipPath>`)).join("\n");
588
637
  // ── Compose final SVG ──
589
638
  // Share each raster payload across the segments that show it (the `<image>`
590
639
  // emit is per-element, and a scroll composite repeats a sticky header /
@@ -594,7 +643,7 @@ function composeScrollSvgBody(segments, opts, ctx) {
594
643
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${VH}" width="${W}" height="${VH}"${a11y.roleAttr}>${a11y.markup}
595
644
  <defs>
596
645
  <clipPath id="${animClass}-clip"><rect width="${W}" height="${VH}"/></clipPath>
597
- ${ownerClipDefs === "" ? "" : ownerClipDefs + "\n"}${glyphDefs !== "" ? ` ${glyphDefs}\n` : ""} <style>
646
+ ${ownerClipDefs === "" ? "" : ownerClipDefs + "\n"}${foregroundClipDefs === "" ? "" : foregroundClipDefs + "\n"}${glyphDefs !== "" ? ` ${glyphDefs}\n` : ""} <style>
598
647
  ${fontFaceCss !== "" ? fontFaceCss + "\n" : ""} .${animClass} { animation: ${animClass} ${totalSec.toFixed(3)}s linear infinite; will-change: transform; }
599
648
  @keyframes ${animClass} {
600
649
  ${keyframes}
@@ -615,6 +664,7 @@ ${paintBg ? ` <rect width="${compositeW}" height="${compositeH}" fill="${
615
664
  </svg>
616
665
  </g>
617
666
  ${ownerClipClose === "" ? "" : ownerClipClose + "\n"}
667
+ ${staticForegroundMarkup === "" ? "" : staticForegroundMarkup + "\n"}
618
668
  </g>${overlayMarkup}
619
669
  </svg>`);
620
670
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domotion-svg",
3
- "version": "0.28.1",
3
+ "version": "0.28.2",
4
4
  "description": "DOM-to-animated-SVG renderer. Captures HTML/CSS via Playwright Chromium and converts it to self-contained SVG with CSS animations — pixel-faithful demos that scale crisply and load lazily.",
5
5
  "license": "MIT",
6
6
  "author": "Brian Westphal",