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.
Files changed (115) hide show
  1. package/FEATURES.md +1 -0
  2. package/README.md +29 -0
  3. package/dist/animation/animator.js +25 -14
  4. package/dist/animation/animator.test.js +54 -21
  5. package/dist/animation/cursor-overlay.js +0 -2
  6. package/dist/capture/emoji.js +29 -18
  7. package/dist/capture/index.js +5 -4
  8. package/dist/capture/script/color-norm.d.ts +1 -0
  9. package/dist/capture/script/color-norm.js +43 -1
  10. package/dist/capture/script/emoji-detect.js +14 -0
  11. package/dist/capture/script/index.js +593 -65
  12. package/dist/capture/script/walker/borders-backgrounds.d.ts +24 -17
  13. package/dist/capture/script/walker/borders-backgrounds.js +123 -7
  14. package/dist/capture/script/walker/counter-style-resolver.d.ts +7 -0
  15. package/dist/capture/script/walker/counter-style-resolver.js +218 -0
  16. package/dist/capture/script/walker/input-value.js +14 -1
  17. package/dist/capture/script/walker/lists-counters.d.ts +3 -1
  18. package/dist/capture/script/walker/lists-counters.js +22 -2
  19. package/dist/capture/script/walker/masks-clips.d.ts +2 -0
  20. package/dist/capture/script/walker/masks-clips.js +41 -1
  21. package/dist/capture/script/walker/pseudo-content.d.ts +14 -1
  22. package/dist/capture/script/walker/pseudo-content.js +301 -61
  23. package/dist/capture/script/walker/pseudo-inject.js +20 -0
  24. package/dist/capture/script/walker/text-segments.js +98 -4
  25. package/dist/capture/script/walker/transforms.d.ts +1 -0
  26. package/dist/capture/script/walker/transforms.js +16 -0
  27. package/dist/capture/script.generated.js +1 -1
  28. package/dist/capture/types.d.ts +213 -2
  29. package/dist/cli/animate.js +151 -15
  30. package/dist/mask.test.js +12 -7
  31. package/dist/render/borders.d.ts +9 -13
  32. package/dist/render/borders.js +379 -14
  33. package/dist/render/element-tree-to-svg.d.ts +11 -12
  34. package/dist/render/element-tree-to-svg.js +2046 -241
  35. package/dist/render/embedded-font-builder.d.ts +49 -0
  36. package/dist/render/embedded-font-builder.js +149 -0
  37. package/dist/render/form-controls.js +45 -24
  38. package/dist/render/gradients.d.ts +15 -0
  39. package/dist/render/gradients.js +103 -2
  40. package/dist/render/gradients.test.js +34 -0
  41. package/dist/render/text-to-path.d.ts +38 -1
  42. package/dist/render/text-to-path.js +654 -29
  43. package/dist/render/text-to-path.test.js +230 -9
  44. package/dist/render/text.d.ts +14 -0
  45. package/dist/render/text.js +344 -40
  46. package/dist/scroll/composer.d.ts +26 -0
  47. package/dist/scroll/composer.js +199 -11
  48. package/dist/scroll/composer.test.js +293 -16
  49. package/dist/scroll/executor.d.ts +3 -1
  50. package/dist/scroll/executor.js +15 -6
  51. package/dist/scroll/executor.test.js +25 -0
  52. package/dist/scroll/hoist-fixed.d.ts +48 -0
  53. package/dist/scroll/hoist-fixed.js +85 -0
  54. package/dist/scroll/hoist-fixed.test.d.ts +1 -0
  55. package/dist/scroll/hoist-fixed.test.js +103 -0
  56. package/dist/scroll/hoist-sticky.d.ts +45 -0
  57. package/dist/scroll/hoist-sticky.js +157 -0
  58. package/dist/scroll/hoist-sticky.test.d.ts +1 -0
  59. package/dist/scroll/hoist-sticky.test.js +154 -0
  60. package/dist/scroll/pattern.d.ts +22 -5
  61. package/dist/scroll/pattern.js +55 -7
  62. package/dist/scroll/pattern.test.js +48 -1
  63. package/dist/tree-ops/frame-merge.d.ts +10 -0
  64. package/dist/tree-ops/frame-merge.js +23 -5
  65. package/dist/tree-ops/frame-merge.test.js +45 -0
  66. package/dist/tree-ops/tree-diff.js +1 -1
  67. package/dist/tree-ops/viewbox-culling.js +32 -18
  68. package/dist/tree-ops/viewbox-culling.test.js +40 -6
  69. package/package.json +8 -2
  70. package/src/animation/animator.test.ts +56 -21
  71. package/src/animation/animator.ts +25 -14
  72. package/src/animation/cursor-overlay.ts +0 -2
  73. package/src/capture/emoji.ts +28 -18
  74. package/src/capture/index.ts +15 -14
  75. package/src/capture/script/color-norm.ts +38 -1
  76. package/src/capture/script/emoji-detect.ts +14 -0
  77. package/src/capture/script/index.ts +555 -48
  78. package/src/capture/script/walker/borders-backgrounds.ts +114 -7
  79. package/src/capture/script/walker/counter-style-resolver.ts +184 -0
  80. package/src/capture/script/walker/input-value.ts +14 -1
  81. package/src/capture/script/walker/lists-counters.ts +24 -2
  82. package/src/capture/script/walker/masks-clips.ts +40 -1
  83. package/src/capture/script/walker/pseudo-content.ts +297 -55
  84. package/src/capture/script/walker/pseudo-inject.ts +20 -0
  85. package/src/capture/script/walker/text-segments.ts +93 -4
  86. package/src/capture/script/walker/transforms.ts +14 -0
  87. package/src/capture/script.generated.ts +1 -1
  88. package/src/capture/types.ts +202 -2
  89. package/src/cli/animate.ts +135 -15
  90. package/src/mask.test.ts +12 -7
  91. package/src/render/borders.ts +383 -17
  92. package/src/render/element-tree-to-svg.ts +2051 -238
  93. package/src/render/embedded-font-builder.ts +221 -0
  94. package/src/render/form-controls.ts +45 -24
  95. package/src/render/gradients.test.ts +46 -0
  96. package/src/render/gradients.ts +94 -2
  97. package/src/render/opentype.js.d.ts +7 -0
  98. package/src/render/text-to-path.test.ts +246 -9
  99. package/src/render/text-to-path.ts +702 -31
  100. package/src/render/text.ts +344 -40
  101. package/src/scroll/composer.test.ts +322 -16
  102. package/src/scroll/composer.ts +246 -13
  103. package/src/scroll/executor.test.ts +27 -0
  104. package/src/scroll/executor.ts +19 -10
  105. package/src/scroll/hoist-fixed.test.ts +117 -0
  106. package/src/scroll/hoist-fixed.ts +95 -0
  107. package/src/scroll/hoist-sticky.test.ts +173 -0
  108. package/src/scroll/hoist-sticky.ts +193 -0
  109. package/src/scroll/pattern.test.ts +58 -1
  110. package/src/scroll/pattern.ts +71 -8
  111. package/src/tree-ops/frame-merge.test.ts +51 -0
  112. package/src/tree-ops/frame-merge.ts +24 -6
  113. package/src/tree-ops/tree-diff.ts +3 -1
  114. package/src/tree-ops/viewbox-culling.test.ts +42 -6
  115. package/src/tree-ops/viewbox-culling.ts +32 -18
@@ -138,6 +138,57 @@ export const createTextSegmentsHandler = ({ vp, measureFontMetrics, needsRaster
138
138
  const flFsRaw = parseFloat(flStyle.fontSize) || 0;
139
139
  const firstLetterStyled = flFsRaw > 0 && Math.abs(flFsRaw - elFsRaw) > 0.5;
140
140
  let firstCharSeen = false;
141
+ // DM-747 / DM-791: MathML `<mi>` with a single token character is
142
+ // automatically painted with the Mathematical Italic alphabet (the
143
+ // mathvariant=italic mapping from MathML 4 / Core). Chrome paints
144
+ // `<mi>a</mi>` using U+1D44E (𝑎), `<mi>α</mi>` using U+1D6FC (𝛼), etc.
145
+ // The computed `font-style` stays `normal` and `font-family` stays
146
+ // `math`, so we can't detect this through CSS — we apply the mapping
147
+ // ourselves at capture time so the downstream text-shaping pipeline
148
+ // picks up the right glyphs from whatever math font the system has.
149
+ const elTag = el.tagName != null ? el.tagName.toLowerCase() : '';
150
+ const _miText = (el.textContent || '').trim();
151
+ const mathItalicizeMi = elTag === 'mi'
152
+ && [..._miText].length === 1
153
+ && /^[a-zA-ZΑ-ΩΆΈΉΊΌΎΏα-ωϐϑϕϖϗϰϱϵ∂∇]$/u.test(_miText);
154
+ const mathItalicChar = (ch) => {
155
+ const code = ch.codePointAt(0);
156
+ if (code == null)
157
+ return ch;
158
+ // Latin Mathematical Italic Capital A..Z = U+1D434..U+1D44D.
159
+ if (code >= 0x41 && code <= 0x5A)
160
+ return String.fromCodePoint(0x1D434 + (code - 0x41));
161
+ // Latin Mathematical Italic Small a..z = U+1D44E..U+1D467, except
162
+ // U+1D455 ("h") is reserved — Chrome paints U+210E (ℎ, PLANCK CONSTANT).
163
+ if (code >= 0x61 && code <= 0x7A) {
164
+ if (code === 0x68)
165
+ return 'ℎ';
166
+ return String.fromCodePoint(0x1D44E + (code - 0x61));
167
+ }
168
+ // Greek Mathematical Italic Capital Α..Ω = U+1D6E2..U+1D6FA.
169
+ // The block is dense: 25 codepoints for Α (U+0391) through Ω (U+03A9),
170
+ // skipping nothing in the source range.
171
+ if (code >= 0x0391 && code <= 0x03A9)
172
+ return String.fromCodePoint(0x1D6E2 + (code - 0x0391));
173
+ // Greek Mathematical Italic Small α..ω = U+1D6FC..U+1D71B.
174
+ // U+03C2 (final sigma ς) maps to the regular U+1D70D position; the
175
+ // block is contiguous.
176
+ if (code >= 0x03B1 && code <= 0x03C9)
177
+ return String.fromCodePoint(0x1D6FC + (code - 0x03B1));
178
+ // Greek symbol variants — these alternate forms get their own italic
179
+ // codepoints at the tail of the lowercase Greek block.
180
+ switch (code) {
181
+ case 0x2202: return String.fromCodePoint(0x1D715); // ∂ → italic partial differential
182
+ case 0x03F5: return String.fromCodePoint(0x1D716); // ϵ (lunate) → italic epsilon symbol
183
+ case 0x03D1: return String.fromCodePoint(0x1D717); // ϑ (theta sym) → italic theta symbol
184
+ case 0x03F0: return String.fromCodePoint(0x1D718); // ϰ (kappa sym) → italic kappa symbol
185
+ case 0x03D5: return String.fromCodePoint(0x1D719); // ϕ (phi sym) → italic phi symbol
186
+ case 0x03F1: return String.fromCodePoint(0x1D71A); // ϱ (rho sym) → italic rho symbol
187
+ case 0x03D6: return String.fromCodePoint(0x1D71B); // ϖ (pi sym) → italic pi symbol
188
+ case 0x2207: return String.fromCodePoint(0x1D6FB); // ∇ (nabla) → italic nabla (upper-block tail)
189
+ default: return ch;
190
+ }
191
+ };
141
192
  for (const node of el.childNodes) {
142
193
  if (node.nodeType !== Node.TEXT_NODE)
143
194
  continue;
@@ -152,7 +203,13 @@ export const createTextSegmentsHandler = ({ vp, measureFontMetrics, needsRaster
152
203
  raw = raw.replace(/\b\p{L}/gu, (ch) => ch.toUpperCase());
153
204
  if (!raw.trim())
154
205
  continue;
155
- text += raw.trim() + ' ';
206
+ // DM-747: when `<mi>` math-italic substitution applies, the element's
207
+ // aggregate `text` field should carry the substituted codepoint too —
208
+ // it's used for aria-label / accessibility and matches the painted
209
+ // glyph. The per-character ranges still measure against the original
210
+ // textContent (see below).
211
+ const rawForText = mathItalicizeMi ? mathItalicChar(raw.trim()) : raw.trim();
212
+ text += rawForText + ' ';
156
213
  // Group characters by their laid-out line (matching rect.top).
157
214
  const lines = [];
158
215
  let cur = null;
@@ -169,7 +226,15 @@ export const createTextSegmentsHandler = ({ vp, measureFontMetrics, needsRaster
169
226
  i += step - 1;
170
227
  continue;
171
228
  }
172
- const ch = raw.slice(i, i + step);
229
+ let ch = raw.slice(i, i + step);
230
+ // DM-747: see `mathItalicizeMi` block above — apply the mathvariant=
231
+ // italic substitution AFTER the Range-based measurement so the Range
232
+ // offsets stay valid against the original textContent (`a`, 1 code
233
+ // unit) while the captured glyph string carries the surrogate-pair
234
+ // math-italic codepoint (`𝑎`, 2 code units) that the downstream
235
+ // shaping pipeline picks up.
236
+ if (mathItalicizeMi)
237
+ ch = mathItalicChar(ch);
173
238
  const charRec = { ch, left: cr.left, top: cr.top, right: cr.right, bottom: cr.bottom };
174
239
  if (cur == null || Math.abs(cr.top - cur.top) > 1) {
175
240
  if (cur != null)
@@ -250,13 +315,42 @@ export const createTextSegmentsHandler = ({ vp, measureFontMetrics, needsRaster
250
315
  if (isFirstLetter)
251
316
  firstCharSeen = true;
252
317
  if ((cp != null && needsRaster(cp, nextCp)) || isFirstLetter) {
318
+ // DM-823: for floated ::first-letter drop caps (initial-letter:
319
+ // N M), `Range.getBoundingClientRect()` returns the line-box of
320
+ // the first character in the NORMAL flow — but Chrome paints the
321
+ // float at a much larger box (~N × parent-line-height tall). The
322
+ // bitmap captured at the Range rect gets vertically truncated,
323
+ // visible as a clipped W / B / T drop cap in the rendered SVG.
324
+ // Expand the rasterRect downward by the difference between
325
+ // (N × parent-line-height) and the natural-flow Range height so
326
+ // the screenshot includes the full painted glyph. Width stays
327
+ // unchanged — Chrome's drop cap matches the natural-flow
328
+ // first-char width.
329
+ let rasterTop = cRec.top - vp.y;
330
+ let rasterHeight = cRec.bottom - cRec.top;
331
+ if (isFirstLetter) {
332
+ const ilRaw = flStyle.initialLetter || flStyle.webkitInitialLetter || '';
333
+ const ilN = parseFloat(ilRaw);
334
+ const parentLineHeight = parseFloat(cs.lineHeight);
335
+ if (Number.isFinite(ilN) && ilN > 1 && Number.isFinite(parentLineHeight) && parentLineHeight > 0) {
336
+ const expectedHeight = ilN * parentLineHeight;
337
+ if (expectedHeight > rasterHeight) {
338
+ // Extend downward only — Chrome aligns the cap-top at the
339
+ // first line's cap-height position; extra space the
340
+ // expansion captures past the actual ink is empty (will
341
+ // raster as transparent, with `omitBackground: true`) and
342
+ // doesn't paint anything visible.
343
+ rasterHeight = expectedHeight;
344
+ }
345
+ }
346
+ }
253
347
  rasterGlyphs.push({
254
348
  charIndex: utf16Idx,
255
349
  rect: {
256
350
  x: cRec.left - vp.x,
257
- y: cRec.top - vp.y,
351
+ y: rasterTop,
258
352
  width: cRec.right - cRec.left,
259
- height: cRec.bottom - cRec.top,
353
+ height: rasterHeight,
260
354
  },
261
355
  // ::first-letter drop caps: suppress the path glyph so
262
356
  // only the rasterized big letter paints (DM-439).
@@ -3,6 +3,7 @@ export declare const createTransformsHandler: () => {
3
3
  threadFrozenTransform: (cs: any, frozenTransform: any, _frozenTransformOrigin: any) => {
4
4
  transform: any;
5
5
  transformOrigin: any;
6
+ translateZ: number | undefined;
6
7
  transformCreatesSc: boolean;
7
8
  };
8
9
  };
@@ -130,6 +130,22 @@ export const createTransformsHandler = () => {
130
130
  ? frozenTransform
131
131
  : 'none',
132
132
  transformOrigin: cs.transformOrigin,
133
+ // DM-751: extract `matrix3d` translateZ so the paint-order sort can
134
+ // honor 3D Z position when the parent element has
135
+ // `transform-style: preserve-3d` (which sorts children by Z in 3D
136
+ // space, not by z-index per CSS Transforms 2 §6). We can't represent
137
+ // perspective / actual 3D rendering in SVG; this is paint-order only.
138
+ translateZ: (function () {
139
+ const tt = cs.transform;
140
+ if (tt == null || tt === 'none' || tt === '')
141
+ return undefined;
142
+ const m3 = /^matrix3d\(([^)]+)\)/.exec(tt);
143
+ if (m3 == null)
144
+ return undefined;
145
+ const parts = m3[1].split(',').map((s) => parseFloat(s.trim()));
146
+ const tz = parts[14];
147
+ return Number.isFinite(tz) && tz !== 0 ? tz : undefined;
148
+ })(),
133
149
  // DM-587: separately flag elements that ORIGINALLY had a non-none
134
150
  // transform — even though we discard the value to suppress the wrap,
135
151
  // CSS Transforms 2 §4 says any non-none transform creates a stacking