domotion-svg 0.28.0 → 0.28.1

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.
@@ -117,6 +117,10 @@ export interface PageStateSnapshot {
117
117
  maxScrollY: number;
118
118
  /** Maximum scrollable x. */
119
119
  maxScrollX: number;
120
+ /** Width of the live scroll owner's content viewport, when available. */
121
+ clientWidth?: number;
122
+ /** Height of the live scroll owner's content viewport, when available. */
123
+ clientHeight?: number;
120
124
  /** Current scroll position. */
121
125
  scrollX: number;
122
126
  scrollY: number;
@@ -133,6 +137,15 @@ export interface PageQuery {
133
137
  /** Bbox of an element matching the CSS selector, in document coordinates. */
134
138
  selectorBbox(css: string): Promise<SelectorBbox | null>;
135
139
  }
140
+ /**
141
+ * Size of one contiguous captured slice along the active scroll axis.
142
+ *
143
+ * Window-owned captures fill the configured output viewport. Element-owned
144
+ * captures only fill the element's client box, even when a larger ancestor or
145
+ * the whole body is captured around them. Stepping an element by the outer
146
+ * frame size would therefore leave uncaptured bands in the moving stack.
147
+ */
148
+ export declare function scrollCaptureChunkSize(axis: ScrollAxis, viewport: Pick<ScrollExecutorOptions, "viewportW" | "viewportH">, snapshot: PageStateSnapshot, elementOwned: boolean): number;
136
149
  /**
137
150
  * Resolve an `AbsoluteTarget` AST node to an absolute scroll position along
138
151
  * the given axis. Returns the position in scroll-coordinate space (i.e. the
@@ -76,6 +76,23 @@ function directionSign(action) {
76
76
  return 1;
77
77
  return -1;
78
78
  }
79
+ /**
80
+ * Size of one contiguous captured slice along the active scroll axis.
81
+ *
82
+ * Window-owned captures fill the configured output viewport. Element-owned
83
+ * captures only fill the element's client box, even when a larger ancestor or
84
+ * the whole body is captured around them. Stepping an element by the outer
85
+ * frame size would therefore leave uncaptured bands in the moving stack.
86
+ */
87
+ export function scrollCaptureChunkSize(axis, viewport, snapshot, elementOwned) {
88
+ const outputSize = axis === "x" ? viewport.viewportW : viewport.viewportH;
89
+ if (!elementOwned)
90
+ return outputSize;
91
+ const ownerSize = axis === "x" ? snapshot.clientWidth : snapshot.clientHeight;
92
+ return typeof ownerSize === "number" && Number.isFinite(ownerSize) && ownerSize > 0
93
+ ? ownerSize
94
+ : outputSize;
95
+ }
79
96
  /**
80
97
  * Resolve an `AbsoluteTarget` AST node to an absolute scroll position along
81
98
  * the given axis. Returns the position in scroll-coordinate space (i.e. the
@@ -286,27 +303,26 @@ export async function executeScrollPattern(page, pattern, opts) {
286
303
  }
287
304
  // op.kind === "scroll"
288
305
  // DM-604 §4(a): for smooth-mode scrolls (single long action covering
289
- // multiple viewport-heights), subdivide into viewport-height chunks so
290
- // the composer has enough anchor points to stack contiguously. Without
291
- // this, a `down:bottom/30s` action on a 10000-tall page produces only
292
- // two captures (initial + post-scroll), and the composite ends up with
293
- // 9400 px of empty space between them. ScrollPattern-mode scrolls with
294
- // explicit per-token magnitudes ≤ viewport height naturally produce
295
- // one chunk per token — no over-subdivision there.
306
+ // multiple visible slices), subdivide by the selected owner's captured
307
+ // scrollport so the composer has enough anchor points to stack
308
+ // contiguously. For the page owner that slice is the configured output
309
+ // viewport; for an element owner it is the element's client box, which can
310
+ // be much smaller than a surrounding body capture. Without this, a long
311
+ // action produces only its endpoints and the composite has an uncaptured
312
+ // band between them. ScrollPattern-mode scrolls with explicit per-token
313
+ // magnitudes ≤ one slice naturally produce one chunk per token.
296
314
  //
297
- // DM-633: chunks MUST land at exact viewport-height multiples (not
298
- // evenly-distributed fractions of totalDelta). Each segment's captured
299
- // tree fills the entire viewport at its scrollY, so the composer stacks
300
- // each VH-tall slice at composite y = scrollY. If consecutive scrollYs
301
- // are closer than VH (e.g. 784 px increments when VH = 844), segments
302
- // overlap by `VH - delta` and the upper segment's `position: fixed`
303
- // header bleeds into the lower segment's tail — visible as a duplicate
304
- // nav bar at the bottom of the viewport at t=0.
315
+ // DM-633: chunks MUST land at exact captured-slice multiples (not
316
+ // evenly-distributed fractions of totalDelta). Each segment contributes
317
+ // one visible owner slice at its scroll offset, so the composer stacks it
318
+ // at that coordinate. For window owners, closer-than-viewport anchors
319
+ // overlap and can duplicate fixed paint. For element owners, farther-than-
320
+ // client-box anchors leave a blank band between captured slices.
305
321
  const snap0 = await pageQuery.snapshot();
306
322
  const dx = op.destX - snap0.scrollX;
307
323
  const dy = op.destY - snap0.scrollY;
308
324
  const totalDelta = op.axis === "x" ? dx : dy;
309
- const viewportSize = op.axis === "x" ? opts.viewportW : opts.viewportH;
325
+ const viewportSize = scrollCaptureChunkSize(op.axis, opts, snap0, selector != null);
310
326
  const numChunks = Math.max(1, Math.ceil(Math.abs(totalDelta) / viewportSize));
311
327
  const dir = totalDelta >= 0 ? 1 : -1;
312
328
  for (let ci = 1; ci <= numChunks; ci++) {
@@ -314,7 +330,7 @@ export async function executeScrollPattern(page, pattern, opts) {
314
330
  // tile contiguously (no overlap, no gap) in the composer. The final
315
331
  // chunk clamps to op.destX/op.destY so the scroll completes at the
316
332
  // intended target — that single clamped step may overlap the prior
317
- // chunk by < VH, but it only affects the last animation frame and
333
+ // chunk by less than one slice, but it only affects the last frame and
318
334
  // never the much-more-common mid-scroll frames.
319
335
  const isLast = ci === numChunks;
320
336
  const chunkDestX = op.axis === "x"
@@ -517,17 +533,22 @@ function realPageQuery(page, selector) {
517
533
  return page.evaluate(() => ({
518
534
  maxScrollX: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth),
519
535
  maxScrollY: Math.max(0, document.documentElement.scrollHeight - document.documentElement.clientHeight),
536
+ clientWidth: document.documentElement.clientWidth,
537
+ clientHeight: document.documentElement.clientHeight,
520
538
  scrollX: window.scrollX,
521
539
  scrollY: window.scrollY,
522
540
  }));
523
541
  }
524
542
  return page.evaluate((sel) => {
525
543
  const el = document.querySelector(sel);
526
- if (!(el instanceof HTMLElement))
527
- return { maxScrollX: 0, maxScrollY: 0, scrollX: 0, scrollY: 0 };
544
+ if (!(el instanceof HTMLElement)) {
545
+ return { maxScrollX: 0, maxScrollY: 0, clientWidth: 0, clientHeight: 0, scrollX: 0, scrollY: 0 };
546
+ }
528
547
  return {
529
548
  maxScrollX: Math.max(0, el.scrollWidth - el.clientWidth),
530
549
  maxScrollY: Math.max(0, el.scrollHeight - el.clientHeight),
550
+ clientWidth: el.clientWidth,
551
+ clientHeight: el.clientHeight,
531
552
  scrollX: el.scrollLeft,
532
553
  scrollY: el.scrollTop,
533
554
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domotion-svg",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
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",