domotion-svg 0.29.0 → 0.30.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.
Files changed (350) hide show
  1. package/FEATURES.md +12 -0
  2. package/README.md +29 -3
  3. package/assets/fonts/fixture/README.md +4 -4
  4. package/dist/animation/caret-metrics.js +8 -4
  5. package/dist/animation/caret-track.js +16 -8
  6. package/dist/animation/composite.js +23 -12
  7. package/dist/animation/compressed-run.d.ts +11 -0
  8. package/dist/animation/compressed-run.js +134 -33
  9. package/dist/animation/cursor-glyphs.js +24 -5
  10. package/dist/animation/cursor-overlay.js +6 -6
  11. package/dist/animation/embed-namespace.js +6 -1
  12. package/dist/animation/embed-timeline.js +54 -10
  13. package/dist/animation/index.d.ts +3 -3
  14. package/dist/animation/index.js +3 -3
  15. package/dist/animation/magic-move.js +22 -16
  16. package/dist/animation/motion-presets.js +35 -9
  17. package/dist/animation/overlay-schema.js +9 -4
  18. package/dist/animation/resolve-overlays.js +14 -5
  19. package/dist/animation/svg-generator.d.ts +1 -1
  20. package/dist/animation/svg-generator.js +283 -133
  21. package/dist/animation/text-address.js +13 -4
  22. package/dist/animation/transition-schema.js +264 -60
  23. package/dist/capture/animated-image-static-frame.js +86 -35
  24. package/dist/capture/animation-frame.js +26 -11
  25. package/dist/capture/authenticated-animated-image-bytes.js +106 -44
  26. package/dist/capture/backdrop-composite-raster.js +46 -25
  27. package/dist/capture/backdrop-effect-neutralization.js +17 -11
  28. package/dist/capture/backdrop-effect-space.js +18 -19
  29. package/dist/capture/backdrop-isolation.js +7 -8
  30. package/dist/capture/background-image-sizing.js +84 -43
  31. package/dist/capture/broken-image-fallback.js +43 -34
  32. package/dist/capture/broken-image-icon-raster.js +8 -4
  33. package/dist/capture/collapsed-border-fragment-cdp.js +94 -75
  34. package/dist/capture/collapsed-border-fragment-record.js +111 -91
  35. package/dist/capture/effective-appearance-cdp.js +5 -5
  36. package/dist/capture/effective-appearance.js +70 -25
  37. package/dist/capture/embed.d.ts +4 -2
  38. package/dist/capture/embed.js +27 -14
  39. package/dist/capture/emoji.js +91 -40
  40. package/dist/capture/frame-scroll-state.js +114 -80
  41. package/dist/capture/generic-font-probe.js +23 -13
  42. package/dist/capture/index.d.ts +6 -4
  43. package/dist/capture/index.js +220 -115
  44. package/dist/capture/input-value-geometry.js +4 -4
  45. package/dist/capture/line-clamp.d.ts +2 -2
  46. package/dist/capture/line-clamp.js +63 -58
  47. package/dist/capture/native-control-decoration-raster.js +124 -77
  48. package/dist/capture/native-control-decoration.d.ts +1 -1
  49. package/dist/capture/native-control-decoration.js +21 -14
  50. package/dist/capture/native-control-raster.js +54 -25
  51. package/dist/capture/native-scrollbar-raster.js +24 -23
  52. package/dist/capture/projective-owner.d.ts +1 -10
  53. package/dist/capture/pseudo-backdrop-isolation.js +5 -4
  54. package/dist/capture/pseudo-fragment-cdp.js +111 -68
  55. package/dist/capture/pseudo-fragment-protocol.js +45 -24
  56. package/dist/capture/pseudo-style-cdp.d.ts +1 -1
  57. package/dist/capture/pseudo-style-cdp.js +59 -46
  58. package/dist/capture/raf-clock.js +28 -18
  59. package/dist/capture/replaced-element-raster.js +42 -31
  60. package/dist/capture/replaced-media-frame.js +51 -23
  61. package/dist/capture/replaced-snapshot-geometry.js +19 -13
  62. package/dist/capture/script/color-norm.js +15 -13
  63. package/dist/capture/script/dotted-circle-detect.js +9 -9
  64. package/dist/capture/script/emoji-detect.js +18 -14
  65. package/dist/capture/script/font-family-default.js +70 -39
  66. package/dist/capture/script/font-feature-values.js +13 -9
  67. package/dist/capture/script/font-metrics.js +26 -21
  68. package/dist/capture/script/font-palette.js +17 -9
  69. package/dist/capture/script/index.js +459 -420
  70. package/dist/capture/script/line-clamp.d.ts +4 -4
  71. package/dist/capture/script/line-clamp.js +91 -95
  72. package/dist/capture/script/placeholder-shown.js +11 -7
  73. package/dist/capture/script/pseudo-rules.js +16 -18
  74. package/dist/capture/script/utils.js +24 -25
  75. package/dist/capture/script/walker/background-attachment.d.ts +1 -1
  76. package/dist/capture/script/walker/background-attachment.js +59 -48
  77. package/dist/capture/script/walker/borders-backgrounds.d.ts +1 -1
  78. package/dist/capture/script/walker/borders-backgrounds.js +204 -120
  79. package/dist/capture/script/walker/capture-phases.js +58 -53
  80. package/dist/capture/script/walker/collapsed-border.js +45 -10
  81. package/dist/capture/script/walker/counter-prewalk.js +33 -19
  82. package/dist/capture/script/walker/counter-style-resolver.js +58 -46
  83. package/dist/capture/script/walker/fieldset-legend.js +3 -3
  84. package/dist/capture/script/walker/form-controls.d.ts +1 -1
  85. package/dist/capture/script/walker/form-controls.js +176 -147
  86. package/dist/capture/script/walker/fragmentation.js +53 -37
  87. package/dist/capture/script/walker/inline-svg-decisions.d.ts +8 -0
  88. package/dist/capture/script/walker/inline-svg-decisions.js +37 -0
  89. package/dist/capture/script/walker/inline-svg.js +212 -187
  90. package/dist/capture/script/walker/input-value.d.ts +1 -1
  91. package/dist/capture/script/walker/input-value.js +70 -62
  92. package/dist/capture/script/walker/lists-counters.d.ts +1 -1
  93. package/dist/capture/script/walker/lists-counters.js +17 -15
  94. package/dist/capture/script/walker/masks-clips-decisions.d.ts +15 -0
  95. package/dist/capture/script/walker/masks-clips-decisions.js +105 -0
  96. package/dist/capture/script/walker/masks-clips.js +172 -224
  97. package/dist/capture/script/walker/pseudo-content.d.ts +59 -3
  98. package/dist/capture/script/walker/pseudo-content.js +493 -470
  99. package/dist/capture/script/walker/pseudo-inject.js +36 -24
  100. package/dist/capture/script/walker/replaced-elements.js +18 -13
  101. package/dist/capture/script/walker/resize-handle.js +21 -21
  102. package/dist/capture/script/walker/text-segments.d.ts +139 -0
  103. package/dist/capture/script/walker/text-segments.js +607 -540
  104. package/dist/capture/script/walker/transforms.js +23 -22
  105. package/dist/capture/script/warnings.js +7 -7
  106. package/dist/capture/script.generated.js +1 -1
  107. package/dist/capture/scrollbar-capture.js +119 -87
  108. package/dist/capture/summary-marker-cdp.js +32 -16
  109. package/dist/capture/svg-affine-freeze.js +12 -8
  110. package/dist/capture/text-fragment-geometry.js +9 -9
  111. package/dist/capture/text-fragment-spans.js +87 -40
  112. package/dist/capture/text-line-origin.js +4 -6
  113. package/dist/capture/text-paint-geometry-cdp.js +70 -45
  114. package/dist/capture/tree-envelope.js +15 -18
  115. package/dist/capture/types.d.ts +6 -24
  116. package/dist/capture/warnings.d.ts +8 -7
  117. package/dist/capture/warnings.js +11 -8
  118. package/dist/cli/animate-artifact.js +1 -3
  119. package/dist/cli/animate-capture-session.js +1 -3
  120. package/dist/cli/animate-captured-frame-stage.d.ts +42 -0
  121. package/dist/cli/animate-captured-frame-stage.js +29 -0
  122. package/dist/cli/animate-command.js +23 -0
  123. package/dist/cli/animate-compression.d.ts +42 -0
  124. package/dist/cli/animate-compression.js +307 -0
  125. package/dist/cli/animate-debug.js +8 -8
  126. package/dist/cli/animate-frame-capture.d.ts +5 -0
  127. package/dist/cli/animate-frame-capture.js +1 -1
  128. package/dist/cli/animate-orchestrator.d.ts +4 -44
  129. package/dist/cli/animate-orchestrator.js +442 -523
  130. package/dist/cli/animate-states-run-stage.d.ts +7 -0
  131. package/dist/cli/animate-states-run-stage.js +6 -0
  132. package/dist/cli/capture.js +36 -103
  133. package/dist/cli/common.js +16 -10
  134. package/dist/cli/composite.js +36 -13
  135. package/dist/cli/hover-detect.js +18 -7
  136. package/dist/cli/index.js +14 -22
  137. package/dist/cli/mutation-detect.d.ts +1 -0
  138. package/dist/cli/mutation-detect.js +31 -10
  139. package/dist/cli/review.d.ts +1 -1
  140. package/dist/cli/review.js +20 -8
  141. package/dist/cli/scrubber.js +6 -2
  142. package/dist/cli/storyboard.js +8 -2
  143. package/dist/cli/studio.js +6 -2
  144. package/dist/cli/svg-to-image.js +1 -1
  145. package/dist/cli/svg-to-video-core.js +15 -10
  146. package/dist/cli/template.js +11 -11
  147. package/dist/cli/term.js +3 -5
  148. package/dist/cli/type-resample.js +36 -10
  149. package/dist/font-family-stack.js +8 -8
  150. package/dist/font-feature-values-cascade.js +1 -2
  151. package/dist/index.d.ts +5 -17
  152. package/dist/index.js +4 -11
  153. package/dist/post-processing/hoist-image-payloads.js +11 -11
  154. package/dist/post-processing/optimize.js +5 -2
  155. package/dist/render/advanced-gradient-raster.js +4 -7
  156. package/dist/render/background-attachment.js +7 -5
  157. package/dist/render/background-inline-paint.d.ts +97 -0
  158. package/dist/render/background-inline-paint.js +616 -0
  159. package/dist/render/border-paint.d.ts +44 -0
  160. package/dist/render/border-paint.js +894 -0
  161. package/dist/render/borders.d.ts +4 -4
  162. package/dist/render/borders.js +230 -86
  163. package/dist/render/broken-image-fallback.js +1 -2
  164. package/dist/render/clip-path.js +47 -13
  165. package/dist/render/cluster-fallback.js +96 -37
  166. package/dist/render/colors.js +9 -3
  167. package/dist/render/conic-raster.js +1 -1
  168. package/dist/render/culling-geometry.js +101 -75
  169. package/dist/render/custom-scrollbar.js +1 -3
  170. package/dist/render/decoration-fragment-ownership.js +19 -11
  171. package/dist/render/device-chrome.js +19 -15
  172. package/dist/render/element-tree-to-svg.d.ts +100 -12
  173. package/dist/render/element-tree-to-svg.js +575 -2452
  174. package/dist/render/embedded-font-builder.d.ts +1 -1
  175. package/dist/render/embedded-font-builder.js +50 -35
  176. package/dist/render/embolden-outline.js +10 -12
  177. package/dist/render/emoji-presentation-priority.js +6 -2
  178. package/dist/render/font-resolution.d.ts +9 -4
  179. package/dist/render/font-resolution.js +826 -526
  180. package/dist/render/form-controls.js +93 -57
  181. package/dist/render/generic-script-families.js +446 -125
  182. package/dist/render/glyph-helper-font.js +70 -28
  183. package/dist/render/glyph-helper-outline.js +1 -1
  184. package/dist/render/glyph-helper-transport.js +42 -18
  185. package/dist/render/glyph-helper.d.ts +3 -3
  186. package/dist/render/glyph-helper.js +133 -88
  187. package/dist/render/gradient-defs.js +51 -26
  188. package/dist/render/gradients.js +47 -18
  189. package/dist/render/harfbuzz-shaper.d.ts +1 -1
  190. package/dist/render/harfbuzz-shaper.js +91 -27
  191. package/dist/render/hb-subset.js +53 -21
  192. package/dist/render/helper-acquire.js +1 -2
  193. package/dist/render/helper-availability-contract.js +1 -2
  194. package/dist/render/host-platform.js +1 -1
  195. package/dist/render/icu-helper-acquire.js +24 -14
  196. package/dist/render/icu-helper.js +7 -5
  197. package/dist/render/image-pattern.js +21 -25
  198. package/dist/render/index.d.ts +4 -4
  199. package/dist/render/index.js +3 -3
  200. package/dist/render/linux-target-strike.js +16 -9
  201. package/dist/render/list-marker-geometry.js +43 -13
  202. package/dist/render/mask-origin-clip.js +2 -4
  203. package/dist/render/mask-position.js +12 -14
  204. package/dist/render/mask.js +84 -66
  205. package/dist/render/outline-paint.d.ts +20 -0
  206. package/dist/render/outline-paint.js +255 -0
  207. package/dist/render/overflow-clip.js +28 -17
  208. package/dist/render/paint-order.js +2 -5
  209. package/dist/render/pseudo-filter.js +1 -4
  210. package/dist/render/pseudo-fragments.js +69 -36
  211. package/dist/render/real-text-layer.js +14 -8
  212. package/dist/render/render-state-collectors.d.ts +11 -0
  213. package/dist/render/render-state-collectors.js +87 -0
  214. package/dist/render/resize-handle.js +9 -16
  215. package/dist/render/script-segmentation.d.ts +1 -1
  216. package/dist/render/script-segmentation.js +17 -11
  217. package/dist/render/stacking.js +66 -36
  218. package/dist/render/svg-inline.d.ts +17 -0
  219. package/dist/render/svg-inline.js +178 -5
  220. package/dist/render/svg-viewport-matrix.d.ts +72 -0
  221. package/dist/render/svg-viewport-matrix.js +139 -0
  222. package/dist/render/synchronous-scope.js +3 -3
  223. package/dist/render/synth-test-fonts.js +20 -4
  224. package/dist/render/synthesis-decision.js +4 -4
  225. package/dist/render/text-affine.js +31 -27
  226. package/dist/render/text-paint.d.ts +42 -0
  227. package/dist/render/text-paint.js +478 -0
  228. package/dist/render/text-run-provenance.js +42 -15
  229. package/dist/render/text-to-path.d.ts +3 -3
  230. package/dist/render/text-to-path.js +375 -271
  231. package/dist/render/text.js +318 -144
  232. package/dist/render/unicode-classification.d.ts +2 -2
  233. package/dist/render/unicode-classification.js +327 -130
  234. package/dist/render/unicode-font-routing.darwin.generated.js +1 -1
  235. package/dist/render/vertical-text.d.ts +11 -0
  236. package/dist/render/vertical-text.js +143 -50
  237. package/dist/render/win-font-fallback.js +163 -84
  238. package/dist/review/client.bundle.generated.js +1 -1
  239. package/dist/review/client.js +3 -1
  240. package/dist/review/compare-pngs.js +1 -2
  241. package/dist/review/glyph-compare.js +69 -48
  242. package/dist/review/linux-unicode-evidence.d.ts +1 -1
  243. package/dist/review/linux-unicode-evidence.js +180 -27
  244. package/dist/review/region-overlay.js +10 -2
  245. package/dist/review/roll-differential.js +48 -12
  246. package/dist/review/server.d.ts +1 -1
  247. package/dist/review/server.js +5 -3
  248. package/dist/review/side-digest.js +6 -1
  249. package/dist/review/source-drift-gate.js +25 -8
  250. package/dist/review/stage-evidence.js +10 -9
  251. package/dist/scroll/composer.d.ts +10 -0
  252. package/dist/scroll/composer.js +72 -46
  253. package/dist/scroll/executor.js +30 -17
  254. package/dist/scroll/hoist-fixed.d.ts +1 -1
  255. package/dist/scroll/hoist-fixed.js +1 -1
  256. package/dist/scroll/hoist-sticky.js +1 -3
  257. package/dist/scroll/pattern.js +18 -7
  258. package/dist/scrubber/client.bundle.generated.js +1 -1
  259. package/dist/scrubber/client.js +170 -45
  260. package/dist/scrubber/crop.js +2 -2
  261. package/dist/scrubber/embed.js +21 -23
  262. package/dist/scrubber/server.js +48 -16
  263. package/dist/scrubber/trim.d.ts +1 -1
  264. package/dist/scrubber/trim.js +6 -3
  265. package/dist/studio/agent-tools.js +77 -23
  266. package/dist/studio/annotations.js +41 -26
  267. package/dist/studio/app-projects.js +10 -6
  268. package/dist/studio/authoring.js +137 -96
  269. package/dist/studio/client.bundle.generated.js +1 -1
  270. package/dist/studio/client.js +343 -167
  271. package/dist/studio/compile.js +5 -3
  272. package/dist/studio/cursor-choreography.js +22 -8
  273. package/dist/studio/healing.js +59 -12
  274. package/dist/studio/index.d.ts +1 -1
  275. package/dist/studio/index.js +1 -1
  276. package/dist/studio/interaction-observer.js +157 -33
  277. package/dist/studio/interactions.js +88 -57
  278. package/dist/studio/interactive-compile.js +42 -15
  279. package/dist/studio/project-json-schema.js +1 -1
  280. package/dist/studio/project-schema.js +102 -31
  281. package/dist/studio/project.js +4 -2
  282. package/dist/studio/recording.js +141 -38
  283. package/dist/studio/server.js +17 -9
  284. package/dist/studio/timeline.d.ts +1 -1
  285. package/dist/studio/timeline.js +68 -40
  286. package/dist/studio/treatment-schema.js +12 -3
  287. package/dist/studio/treatments.js +56 -17
  288. package/dist/studio/video-review.d.ts +1 -1
  289. package/dist/studio/video-review.js +18 -6
  290. package/dist/templates/builtin/background-loop.d.ts +6 -6
  291. package/dist/templates/builtin/background-loop.js +70 -35
  292. package/dist/templates/builtin/caption.js +40 -5
  293. package/dist/templates/builtin/chart.js +247 -47
  294. package/dist/templates/builtin/chat.js +56 -12
  295. package/dist/templates/builtin/compare.js +34 -17
  296. package/dist/templates/builtin/counter.js +11 -7
  297. package/dist/templates/builtin/cta.js +14 -4
  298. package/dist/templates/builtin/device-mockup.js +30 -6
  299. package/dist/templates/builtin/kinetic-text.js +69 -11
  300. package/dist/templates/builtin/lower-third.js +11 -7
  301. package/dist/templates/builtin/odometer.js +12 -6
  302. package/dist/templates/builtin/quote.js +5 -2
  303. package/dist/templates/builtin/stat.js +26 -11
  304. package/dist/templates/builtin/subscribe.js +79 -11
  305. package/dist/templates/builtin/title-card.js +5 -2
  306. package/dist/templates/formats.js +5 -1
  307. package/dist/templates/index.d.ts +7 -7
  308. package/dist/templates/index.js +7 -7
  309. package/dist/templates/registry.js +22 -13
  310. package/dist/templates/render.js +4 -1
  311. package/dist/terminal/emulator.js +9 -2
  312. package/dist/terminal/incremental.js +24 -6
  313. package/dist/terminal/index.d.ts +4 -0
  314. package/dist/terminal/index.js +9 -3
  315. package/dist/terminal/pty.js +7 -5
  316. package/dist/terminal/render.js +19 -4
  317. package/dist/terminal/theme.js +48 -6
  318. package/dist/tree-ops/decoration-propagation.js +9 -12
  319. package/dist/tree-ops/resize-embedded-images.js +11 -5
  320. package/dist/tree-ops/swept-transform-bounds.js +16 -11
  321. package/dist/tree-ops/tree-diff.js +1 -3
  322. package/dist/tree-ops/viewbox-culling.js +9 -7
  323. package/dist/utils/escapeHtml.js +1 -5
  324. package/dist/utils/local-server.js +7 -2
  325. package/dist/utils/region-feedback.js +1 -4
  326. package/dist/utils/transparent-background.js +4 -1
  327. package/dist/vertical-orientation.js +2 -5
  328. package/llms.txt +22 -6
  329. package/package.json +19 -20
  330. package/schemas/animate-config.schema.json +3 -0
  331. package/dist/capture/paged-capture-bundle-json-schema.d.ts +0 -4
  332. package/dist/capture/paged-capture-bundle-json-schema.js +0 -21
  333. package/dist/capture/paged-capture-bundle.d.ts +0 -295
  334. package/dist/capture/paged-capture-bundle.js +0 -283
  335. package/dist/capture/paged-capture-helper-json-schema.d.ts +0 -4
  336. package/dist/capture/paged-capture-helper-json-schema.js +0 -21
  337. package/dist/capture/paged-capture-helper.d.ts +0 -254
  338. package/dist/capture/paged-capture-helper.js +0 -907
  339. package/dist/capture/paged-capture.d.ts +0 -40
  340. package/dist/capture/paged-capture.js +0 -299
  341. package/dist/capture/paged-collapsed-table-cdp.d.ts +0 -67
  342. package/dist/capture/paged-collapsed-table-cdp.js +0 -88
  343. package/dist/capture/paged-collapsed-table-record.d.ts +0 -197
  344. package/dist/capture/paged-collapsed-table-record.js +0 -616
  345. package/dist/capture/paged-page-record.d.ts +0 -183
  346. package/dist/capture/paged-page-record.js +0 -857
  347. package/dist/render/paged-page-svg.d.ts +0 -48
  348. package/dist/render/paged-page-svg.js +0 -187
  349. package/schemas/paged-capture-bundle.schema.json +0 -628
  350. package/schemas/paged-capture-helper-bundle.schema.json +0 -384
package/FEATURES.md CHANGED
@@ -5,6 +5,7 @@ Each feature has a visual regression test that compares HTML-to-PNG with SVG-to-
5
5
  ## Rendering Features
6
6
 
7
7
  ### Text
8
+
8
9
  - [x] **text-basic**: Plain text rendering (font-size, font-family, color)
9
10
  - [x] **text-bold**: Bold/weight text
10
11
  - [x] **text-center**: Centered text (text-align: center)
@@ -18,6 +19,7 @@ Each feature has a visual regression test that compares HTML-to-PNG with SVG-to-
18
19
  - [x] **text-font-stretch-underline**: CSS `font-stretch` end to end — condensed/expanded cut selection for declared families (Blink's trait-based style matcher), the `wdth` variation axis on the macOS `system-ui` face and variable webfonts, and decoration metrics resolved on the same width-matched face as the glyphs
19
20
 
20
21
  ### Backgrounds & Colors
22
+
21
23
  - [x] **bg-solid**: Solid background colors
22
24
  - [x] **bg-transparent**: Semi-transparent backgrounds (rgba)
23
25
  - [x] **bg-nested**: Nested backgrounds (child on top of parent)
@@ -26,12 +28,14 @@ Each feature has a visual regression test that compares HTML-to-PNG with SVG-to-
26
28
  - [ ] **bg-dark-mode** (DM-455, doc 29): Dark-mode capture support — caller-chosen `colorScheme`, dark form-control palette, scheme-aware transparent-root fallback
27
29
 
28
30
  ### Borders
31
+
29
32
  - [x] **border-solid**: Solid borders with color
30
33
  - [x] **border-radius**: Rounded corners
31
34
  - [x] **border-radius-pill**: Fully rounded (pill shape)
32
35
  - [x] **inline-box-decoration-break**: wrapped inline backgrounds / borders paint per line-fragment (`slice` + `clone`); first/last fragment own the start/end edges in slice mode, every fragment paints a full box in clone mode
33
36
 
34
37
  ### Layout
38
+
35
39
  - [x] **layout-flex-row**: Horizontal flex layout with gap
36
40
  - [x] **layout-flex-col**: Vertical flex layout
37
41
  - [x] **layout-flex-center**: Centered content (justify-content/align-items)
@@ -43,6 +47,7 @@ Each feature has a visual regression test that compares HTML-to-PNG with SVG-to-
43
47
  - [x] **animated-projective-frame-state** (DM-2359/DM-2356/DM-2492/DM-2493, docs 186/189): `animationTimeMs` exactly pauses document timelines before all capture prepasses and exposes the CDP quad, composed 3D state, residual, and selected atomic raster owner without a fitted 2D approximation. Same-frame used-preserve facts select Blink's direct-parent rendering-context root; a strict 25-family/four-profile native three-OS DPR-1/2 gate rejects larger crops, warnings, restoration drift, incomplete evidence, and nine ownership mutations.
44
48
 
45
49
  ### Components
50
+
46
51
  - [x] **comp-button**: Button with background, border, padding, text
47
52
  - [x] **comp-badge**: Small badge with colored background and text
48
53
  - [x] **comp-card**: Card with background, border, radius, content
@@ -54,17 +59,21 @@ Each feature has a visual regression test that compares HTML-to-PNG with SVG-to-
54
59
  - [ ] **comp-nav**: Navigation bar with logo and links
55
60
 
56
61
  ### SVG
62
+
57
63
  - [x] **svg-inline**: Inline SVG elements are cloned as self-contained vectors; CSS-only clip/mask effects preserve native fill-box, stroke-box, and view-box resolution (DM-2328)
58
64
  - [x] **clip-path-url-reference-ownership** (DM-2362, docs 39/130): bare same-document/external URL clips preserve HTML border/object-bounding-box and SVG forced-fill ownership; Blink-invalid URL-plus-box declarations stay inactive, including stale captured trees and nested frames.
59
65
 
60
66
  ### Masks
67
+
61
68
  - [x] **mask-contain-cover-arbitrary-position** (DM-2379, doc 20): URL masks capture Chromium-decoded intrinsic ratios and emit concrete Blink-fitted rectangles for percentage, length, and calc positions; zoom/DPR, physical writing axes, multilayer, and slice/clone fragment controls reject the retired SVG Min/Mid/Max buckets.
62
69
 
63
70
  ### Text effects
71
+
64
72
  - [x] **text-bg-clip-gradient** (DM-462): `background-clip: text` + `-webkit-text-fill-color: transparent` — gradient fills the glyph shapes via SVG `<mask>` over the bg-color rect
65
73
  - [x] **pseudo-filter-functions** (DM-2367, doc 38): generated empty/text/image paint supports blur, drop-shadow, brightness, contrast, grayscale, hue-rotate, invert, opacity, saturate, sepia, and ordered lists through Chromium's native SVG CSS-filter pipeline; identity/none, transform, clip, stack slot, zoom, DPR, and filter-stripped mutation controls are browser-gated.
66
74
 
67
75
  ### Replaced elements (rasterized as static snapshot — DM-457)
76
+
68
77
  - [x] **replaced-canvas-shape**: `<canvas>` with drawn shapes — bitmap survives via `page.screenshot`
69
78
  - [x] **replaced-video-poster**: `<video poster=…>` paused — poster image captured
70
79
  - [x] **replaced-canvas-overlay**: `<canvas>` under a positioned `<div z-index:10>` overlay — overlay does NOT bleed into the canvas snapshot
@@ -79,11 +88,13 @@ Each feature has a visual regression test that compares HTML-to-PNG with SVG-to-
79
88
  - [x] **snapshot-isolation-pseudo-overlay** (DM-458, `tests/snapshot-isolation.tsx`): canvas covered by a sibling's `::after` pseudo overlay. Inspection-style — decodes the captured snapshot's PNG data URI and asserts no overlay-color pixels leaked through. Catches regressions in the hide-everything-else stylesheet that a comparison-style fixture wouldn't.
80
89
 
81
90
  ### Showcase Integration Tests
91
+
82
92
  - [x] **showcase-typography**: Full-page layout with headings, badges, code blocks, inline code, status text
83
93
  - [x] **showcase-cards**: Card list with badges, buttons, metadata
84
94
  - [x] **showcase-forms**: Publish form with labels, inputs, pre block, button
85
95
 
86
96
  ### Composed Parity Integration Tests
97
+
87
98
  - [x] **composed-multilingual-flex-grid**: Platform fallback and bidi isolation inside flex/grid messaging UI, with neutral-wrapper and node-split relations
88
99
  - [x] **composed-responsive-fragmented-controls**: Native controls inside responsive multicol layout with equivalent grid syntax
89
100
  - [x] **composed-gradient-mask-clip-stacking**: Gradients, masks, and clips under explicit stacking ownership and reversed DOM order
@@ -97,6 +108,7 @@ Each feature has a visual regression test that compares HTML-to-PNG with SVG-to-
97
108
  Tests are defined in `tests/features.ts` (26 feature tests) and `tests/showcase.ts` (3 integration tests). Both use the shared runner in `tests/runner.ts`.
98
109
 
99
110
  For each test case:
111
+
100
112
  1. Define an HTML snippet (feature tests) or full-page HTML (showcase tests) exercising the feature
101
113
  2. Render the HTML in Playwright (Chromium) and capture as PNG ("expected")
102
114
  3. Capture the DOM via `captureElementTree` on the Playwright page
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  **Domotion turns real HTML/CSS into one self-contained, animated SVG** — an accurate reproduction of the rendered page, with optional animation and simulated interaction built in. It embeds the exact captured glyph data and positioning, so playback never falls back to a viewer's system fonts; the output scales crisply at any size and embeds anywhere with a plain `<img>`, no external assets.
16
16
 
17
- Beyond raw capture it ships a **template library** that turns a few flags into a polished animated SVG, **terminal-session capture** (a recording → an animated terminal), **scroll capture** (a long page replayed as one self-contained scrolling SVG), multi-frame **animation** with parameterized and custom transitions, overlays, and simulated interaction, **scene storyboarding** (sequence distinct scenes end-to-end), **brand kits** and **social-format presets** (reel / square / portrait / landscape), **device-chrome** framing, **nested compositing** (animated layers inside animated layers), one-command **SVG → MP4/WebM**, and a fidelity **review** tool.
17
+ Beyond raw capture it ships **Domotion Studio**, a local visual workspace for authoring and reviewing versioned projects; a **template library** that turns a few flags into a polished animated SVG; **terminal-session capture** (a recording → an animated terminal); **scroll capture** (a long page replayed as one self-contained scrolling SVG); multi-frame **animation** with parameterized and custom transitions, overlays, and simulated interaction; **scene storyboarding** (sequence distinct scenes end-to-end); **brand kits** and **social-format presets** (reel / square / portrait / landscape); **device-chrome** framing; **nested compositing** (animated layers inside animated layers); one-command **SVG → MP4/WebM**; and fidelity **review** tools.
18
18
 
19
19
  <p align="center">
20
20
  <img src="examples/output/hero-product-demo.svg" alt="An analytics dashboard assembling itself inside a browser window — KPI cards rise in, a bar chart grows with its peak highlighted, a search query types itself, and a nav item is clicked — all in one self-contained animated SVG" width="760">
@@ -65,6 +65,8 @@ predictable. For a zero-install trial, run
65
65
  [animation guide](https://brianwestphal.github.io/domotion/usage/animate/).
66
66
  - **Generate a polished asset from flags:** use `domotion template`; browse the
67
67
  [template guide](https://brianwestphal.github.io/domotion/usage/templates/).
68
+ - **Author and review a versioned project visually:** use `domotion-studio`; see
69
+ the [Studio guide](https://brianwestphal.github.io/domotion/usage/studio/).
68
70
 
69
71
  The [Quick start](https://brianwestphal.github.io/domotion/start/quickstart/)
70
72
  walks through the first capture and animation end to end.
@@ -97,6 +99,12 @@ never become idle.
97
99
 
98
100
  Same-origin `<iframe>` content is recursed into the capture as native SVG rather than flattened to a screenshot; opt into cross-origin frames you trust with `--cross-origin-frames "<hosts>"`.
99
101
 
102
+ Text defaults to self-contained, pixel-faithful embedded fonts. For inline SVG,
103
+ `--real-text` adds a paintless authored-text layer for search, selection, copy,
104
+ and assistive technology. `--text-mode system-font` instead paints authored
105
+ `<text>` using the viewer's installed fonts; it can be smaller, but it is not
106
+ pixel-faithful and should be used only when those fonts are guaranteed.
107
+
100
108
  For a multi-frame animated SVG, write a JSON config and run:
101
109
 
102
110
  ```bash
@@ -126,6 +134,24 @@ domotion template kinetic-text --text "Ship it" --variant pop --by char -o title
126
134
 
127
135
  Built-ins (14): **lower-third** (broadcast banner) · **kinetic-text** (animated typography) · **chart** (column / bar / line / pie / donut) · **chat** (message thread) · **subscribe** (follow pop-up) · **background-loop** (seamless looping background) · **device-mockup** (wrap a page in a phone / browser / window bezel) · and a **creative-template pack** of full-bleed text/number cards: **title-card**, **quote**, **caption**, **cta**, **counter**, **stat**, **compare**. Every template adapts to a `--format` social preset (reel / square / portrait / landscape) and a `--brand` kit (palette / type / logo). Third-party templates are npm packages named `domotion-template-<name>`.
128
136
 
137
+ ### Domotion Studio — visual project authoring
138
+
139
+ Domotion Studio is a local, browser-based workspace for building and reviewing
140
+ versioned Domotion projects. It provides story and scene authoring, a detailed
141
+ multitrack timeline, embedded playback and review annotations, real-interaction
142
+ import, and generation/review hooks while keeping project files as portable,
143
+ validated JSON inside an explicitly scoped workspace.
144
+
145
+ ```bash
146
+ # Open a workspace, or pass an existing Studio project JSON file.
147
+ domotion-studio --workspace ./demo-project
148
+ domotion-studio ./demo-project/product-tour.json
149
+ ```
150
+
151
+ Studio opens locally by default. In automation, pass `--no-open` and use the
152
+ printed loopback URL. Run `domotion-studio --help` or read the
153
+ [Studio guide](https://brianwestphal.github.io/domotion/usage/studio/).
154
+
129
155
  ### Terminal sessions
130
156
 
131
157
  Turn a recorded terminal session into a self-contained animated SVG — real text, real color, native SVG (no raster frames). Record with [asciinema](https://asciinema.org), then convert:
@@ -186,7 +212,7 @@ writes the final `actual.svg`, one shared HAR, and an `expected.png` plus
186
212
  `captured-tree.json` for each composed frame. Use `--debug-dir <path>` to choose
187
213
  the bundle location.
188
214
 
189
- For an *animated* SVG, the package also ships `svg-scrubber` — a local video-style bench to play / pause / scrub / mark an in-out range, export the current frame as PNG, export the range as MP4, or trim it to a new self-contained animated SVG. Add `--review` to file a focused issue against a moment in the timeline: it writes an importable `.ticket` (frame time, range, and drawn regions) the same way `svg-review` builds a report for a still.
215
+ For an _animated_ SVG, the package also ships `svg-scrubber` — a local video-style bench to play / pause / scrub / mark an in-out range, export the current frame as PNG, export the range as MP4, or trim it to a new self-contained animated SVG. Add `--review` to file a focused issue against a moment in the timeline: it writes an importable `.ticket` (frame time, range, and drawn regions) the same way `svg-review` builds a report for a still.
190
216
 
191
217
  ### Scripting API
192
218
 
@@ -238,7 +264,7 @@ npm run demos:examples # run the bundled example demo scripts
238
264
  - `FEATURES.md` — per-feature support checklist with links to test fixtures.
239
265
  - `docs/` — requirements docs covering rendering fidelity, supported CSS features, and known caveats.
240
266
  - [`llms.txt`](llms.txt) — a concise, self-contained guide for **AI agents using Domotion as a tool** (Claude, Cursor, etc.): the CLIs, config schema, template library, API, gotchas, and a required rendered-pixel/video review gate before an SVG is handed back. Point your agent at it.
241
- - `CLAUDE.md` — guidance for AI assistants working *on this repo's* source (different audience from `llms.txt`).
267
+ - `CLAUDE.md` — guidance for AI assistants working _on this repo's_ source (different audience from `llms.txt`).
242
268
 
243
269
  ## License
244
270
 
@@ -24,10 +24,10 @@ Chrome's layout, `registerWebfont` for Domotion's outlines).
24
24
 
25
25
  ## Provenance
26
26
 
27
- | File | Upstream | License |
28
- |---|---|---|
29
- | `DomotionFixtureMono-Regular.ttf` | [JetBrains Mono](https://github.com/JetBrains/JetBrainsMono) Regular | SIL OFL 1.1 — `LICENSE-jetbrains-mono.txt` |
30
- | `DomotionFixtureSerif-Regular.ttf` | [IBM Plex Serif](https://github.com/IBM/plex) Regular | SIL OFL 1.1 — `LICENSE-ibm-plex-serif.txt` |
27
+ | File | Upstream | License |
28
+ | ---------------------------------- | -------------------------------------------------------------------- | ------------------------------------------ |
29
+ | `DomotionFixtureMono-Regular.ttf` | [JetBrains Mono](https://github.com/JetBrains/JetBrainsMono) Regular | SIL OFL 1.1 — `LICENSE-jetbrains-mono.txt` |
30
+ | `DomotionFixtureSerif-Regular.ttf` | [IBM Plex Serif](https://github.com/IBM/plex) Regular | SIL OFL 1.1 — `LICENSE-ibm-plex-serif.txt` |
31
31
 
32
32
  Both are unmodified apart from being subsetted to the characters the fixtures
33
33
  paint. The OFL permits redistribution of modified copies under a reserved-name
@@ -90,7 +90,13 @@ export function caretShapeRect(inp) {
90
90
  case "block":
91
91
  return { x: leftOf(cellW), y: boxTop, width: cellW, height: boxHeight, opacity: BLOCK_CARET_ALPHA };
92
92
  case "underscore":
93
- return { x: leftOf(cellW), y: inp.baselineY, width: cellW, height: underscoreCaretThicknessPx(inp.fontSize), opacity: 1 };
93
+ return {
94
+ x: leftOf(cellW),
95
+ y: inp.baselineY,
96
+ width: cellW,
97
+ height: underscoreCaretThicknessPx(inp.fontSize),
98
+ opacity: 1,
99
+ };
94
100
  case "bar":
95
101
  default: {
96
102
  const barW = inp.barWidthPx ?? DEFAULT_CARET_WIDTH_PX;
@@ -148,9 +154,7 @@ export function firstLineBaseline(m) {
148
154
  const descentPx = fontBox > 0 ? Math.round(m.fontDescentPx) : m.fontSize * 0.25;
149
155
  const textBoxHeight = ascentPx + descentPx;
150
156
  const lineH = m.lineHeightPx || fontBox || m.fontSize * 1.2;
151
- const lineTop = m.centerInContentBox
152
- ? m.contentTop + Math.max(0, (m.contentHeight - lineH) / 2)
153
- : m.contentTop;
157
+ const lineTop = m.centerInContentBox ? m.contentTop + Math.max(0, (m.contentHeight - lineH) / 2) : m.contentTop;
154
158
  const boxTop = lineTop + Math.floor((lineH - textBoxHeight) / 2);
155
159
  return { baselineY: boxTop + ascentPx, ascentPx, descentPx };
156
160
  }
@@ -39,7 +39,7 @@
39
39
  * (docs/100, Primitive 1), which owns the glyphs and can interleave layers.
40
40
  */
41
41
  import { caretShapeRect, BLOCK_CARET_ALPHA, DEFAULT_CARET_WIDTH_PX } from "./caret-metrics.js";
42
- import { resolveCaretPoint, resolveRangeRects, findAddressedElement } from "./text-address.js";
42
+ import { resolveCaretPoint, resolveRangeRects, findAddressedElement, } from "./text-address.js";
43
43
  import { renderTextAsPath } from "../render/text-to-path.js";
44
44
  import { withRenderTextMode } from "../render/font-resolution.js";
45
45
  /** The standard caret blink period (ms) — the terminal cursor's 1.06 s cycle. */
@@ -169,7 +169,13 @@ function resolveCoveredGlyph(roots, target, charOffset) {
169
169
  for (const run of runs) {
170
170
  for (const ch of run.text) {
171
171
  if (remaining === 0) {
172
- return { char: ch, fontFamily: run.fontFamily, fontWeight: run.fontWeight, fontStyle: run.fontStyle, fontSize: run.fontSize };
172
+ return {
173
+ char: ch,
174
+ fontFamily: run.fontFamily,
175
+ fontWeight: run.fontWeight,
176
+ fontStyle: run.fontStyle,
177
+ fontSize: run.fontSize,
178
+ };
173
179
  }
174
180
  remaining--;
175
181
  }
@@ -330,8 +336,12 @@ function invertedCaretMarkup(track, uid, kf, totalDurationMs, totalSec) {
330
336
  // The glyph is repainted at the CELL's left edge, which is the caret x
331
337
  // for an LTR insertion point and `x − cellWidth` for an RTL one.
332
338
  renderTextAsPath(cg.char, g.x, g.y, {
333
- fontSize: cg.fontSize, fontFamily: cg.fontFamily, fontWeight: cg.fontWeight,
334
- fill: glyphInk, fontStyle: cg.fontStyle, ascentOverride: wp.point.ascentPx,
339
+ fontSize: cg.fontSize,
340
+ fontFamily: cg.fontFamily,
341
+ fontWeight: cg.fontWeight,
342
+ fill: glyphInk,
343
+ fontStyle: cg.fontStyle,
344
+ ascentOverride: wp.point.ascentPx,
335
345
  }));
336
346
  glyphMarkup = gm;
337
347
  }
@@ -410,7 +420,7 @@ function selectionMarkup(sel, si, uid, kf, totalDurationMs, totalSec) {
410
420
  // Sweep anchor: the rect's leading edge in reading order. `edges` are the
411
421
  // successive trailing edges, so the swept extent is their distance from it.
412
422
  const rtl = rect.rtl === true;
413
- const anchor = vertical ? rect.y : (rtl ? rect.x + rect.width : rect.x);
423
+ const anchor = vertical ? rect.y : rtl ? rect.x + rect.width : rect.x;
414
424
  const sweptTo = (edge) => (rtl && !vertical ? anchor - edge : edge - anchor);
415
425
  const stops = [`0%{${prop}:0.01px}`];
416
426
  if (startMs > 0)
@@ -448,9 +458,7 @@ function selectionMarkup(sel, si, uid, kf, totalDurationMs, totalSec) {
448
458
  const place = rtl && !vertical
449
459
  ? `x="0" y="${num(rect.y)}" transform="translate(${num(anchor)},0) scale(-1,1)"`
450
460
  : `x="${num(rect.x)}" y="${num(rect.y)}"`;
451
- const size = vertical
452
- ? `width="${num(rect.width)}" height="0.01"`
453
- : `width="0.01" height="${num(rect.height)}"`;
461
+ const size = vertical ? `width="${num(rect.width)}" height="0.01"` : `width="0.01" height="${num(rect.height)}"`;
454
462
  parts.push(` <rect class="tt-sel" ${place} ${size} fill="${sel.color}" style="animation:${name} ${totalSec.toFixed(2)}s ${timing} infinite"/>`);
455
463
  sweptBefore += rect.edges.length;
456
464
  }
@@ -55,10 +55,13 @@ function innerOf(svg) {
55
55
  function resolveClipOriginPx(origin, base, size) {
56
56
  switch (origin) {
57
57
  case "left":
58
- case "top": return base;
58
+ case "top":
59
+ return base;
59
60
  case "right":
60
- case "bottom": return base + size;
61
- case "center": return base + size / 2;
61
+ case "bottom":
62
+ return base + size;
63
+ case "center":
64
+ return base + size / 2;
62
65
  }
63
66
  const pct = /^([-\d.]+)%$/.exec(origin);
64
67
  if (pct != null)
@@ -71,11 +74,16 @@ function resolveClipOriginPx(origin, base, size) {
71
74
  /** Render a non-clip layer-animation endpoint to its CSS value (transform string / opacity). */
72
75
  function renderAnimValue(property, v) {
73
76
  switch (property) {
74
- case "scale": return `scale(${v})`;
75
- case "translateX": return `translateX(${typeof v === "number" ? `${v}px` : v})`;
76
- case "translateY": return `translateY(${typeof v === "number" ? `${v}px` : v})`;
77
- case "opacity": return `${v}`;
78
- case "transform": return `${v}`;
77
+ case "scale":
78
+ return `scale(${v})`;
79
+ case "translateX":
80
+ return `translateX(${typeof v === "number" ? `${v}px` : v})`;
81
+ case "translateY":
82
+ return `translateY(${typeof v === "number" ? `${v}px` : v})`;
83
+ case "opacity":
84
+ return `${v}`;
85
+ case "transform":
86
+ return `${v}`;
79
87
  }
80
88
  }
81
89
  /**
@@ -100,9 +108,9 @@ export function composeAnimatedLayers(layers, opts) {
100
108
  });
101
109
  // Resolve each layer's geometry + intrinsic size first so we can size the master.
102
110
  const resolved = layers.map((layer, i) => {
103
- const intrinsic = (layer.contentWidth != null && layer.contentHeight != null)
111
+ const intrinsic = layer.contentWidth != null && layer.contentHeight != null
104
112
  ? { w: layer.contentWidth, h: layer.contentHeight }
105
- : parseSvgIntrinsicSize(layer.svg) ?? { w: 0, h: 0 };
113
+ : (parseSvgIntrinsicSize(layer.svg) ?? { w: 0, h: 0 });
106
114
  const w = layer.width ?? intrinsic.w;
107
115
  const h = layer.height ?? intrinsic.h;
108
116
  const x = layer.x ?? 0;
@@ -140,8 +148,11 @@ export function composeAnimatedLayers(layers, opts) {
140
148
  // 2. …then re-anchor its internal timeline to its own start within the master.
141
149
  if (r.period > 0) {
142
150
  content = offsetEmbeddedAnimatedSvgTimeline(content, {
143
- periodMs: r.period, startMs: r.start, masterMs: master,
144
- mode: r.mode, windowMs: r.windowMs,
151
+ periodMs: r.period,
152
+ startMs: r.start,
153
+ masterMs: master,
154
+ mode: r.mode,
155
+ windowMs: r.windowMs,
145
156
  });
146
157
  }
147
158
  // 3. Strip the outer wrapper and re-nest at the layer's box (a nested <svg>
@@ -117,6 +117,17 @@ export interface CompressedRunOptions {
117
117
  /** Paint a root background rect first (e.g. the captured
118
118
  * `styles.rootBgComputed`). Omit for a transparent run. */
119
119
  background?: string;
120
+ /**
121
+ * DM-6SQXGF: append the paintless real-text layer (doc 260) to the run. A
122
+ * compressed run flattens N states into ONE emission (shared content once,
123
+ * step-end birth/shift/recolor tracks) — there are no per-frame visibility
124
+ * groups to gate per-state text, and the glyph births use `opacity` (still
125
+ * find-in-page-visible). So the run emits ONE layer from the FINAL state's
126
+ * tree (the completed content — a stable deduplicated story flow), and the
127
+ * visible chrome+glyph runs are `aria-hidden` while it owns semantics.
128
+ * Intermediate typing prefixes are deliberately not exposed. Default off.
129
+ */
130
+ realText?: boolean;
120
131
  /** Emit the auto-caret track derived from the per-state edit points
121
132
  * (docs/101 machinery). Default false — the config surface decides
122
133
  * defaults later. */
@@ -105,9 +105,10 @@
105
105
  import { elementTreeToSvgInner } from "../render/element-tree-to-svg.js";
106
106
  import { paintOrderHitSequence } from "../render/paint-order.js";
107
107
  import { clearEmbeddedFonts, clearGlyphDefs, getEmbeddedFontFaceCss } from "../render/index.js";
108
+ import { renderRealTextLayer, withRealTextLayerVisualSemantics } from "../render/real-text-layer.js";
108
109
  import { IDENTITY_TEXT_AFFINE, prepareAffineTextPaint, textAffineEquals } from "../render/text-affine.js";
109
110
  import { alignLineGlyphs } from "./glyph-align.js";
110
- import { textTrackMarkup, CARET_BLINK_MS, DEFAULT_SELECTION_COLOR } from "./caret-track.js";
111
+ import { textTrackMarkup, CARET_BLINK_MS, DEFAULT_SELECTION_COLOR, } from "./caret-track.js";
111
112
  import { DEFAULT_CARET_WIDTH_PX } from "./caret-metrics.js";
112
113
  import { resolveRangeRects } from "./text-address.js";
113
114
  // ── Small helpers ───────────────────────────────────────────────────────────
@@ -156,8 +157,10 @@ function paintsBox(el) {
156
157
  if (s.backgroundImage != null && s.backgroundImage !== "none" && s.backgroundImage !== "")
157
158
  return true;
158
159
  const side = (w, st) => num(w) > 0 && st !== "none" && st != null;
159
- if (side(s.borderTopWidth, s.borderTopStyle) || side(s.borderRightWidth, s.borderRightStyle)
160
- || side(s.borderBottomWidth, s.borderBottomStyle) || side(s.borderLeftWidth, s.borderLeftStyle))
160
+ if (side(s.borderTopWidth, s.borderTopStyle) ||
161
+ side(s.borderRightWidth, s.borderRightStyle) ||
162
+ side(s.borderBottomWidth, s.borderBottomStyle) ||
163
+ side(s.borderLeftWidth, s.borderLeftStyle))
161
164
  return true;
162
165
  if (s.boxShadow != null && s.boxShadow !== "none" && s.boxShadow !== "")
163
166
  return true;
@@ -165,7 +168,23 @@ function paintsBox(el) {
165
168
  return true;
166
169
  if (el.elementRaster != null)
167
170
  return true;
168
- const REPLACED = new Set(["img", "canvas", "video", "svg", "iframe", "embed", "object", "picture", "input", "textarea", "select", "button", "progress", "meter", "hr"]);
171
+ const REPLACED = new Set([
172
+ "img",
173
+ "canvas",
174
+ "video",
175
+ "svg",
176
+ "iframe",
177
+ "embed",
178
+ "object",
179
+ "picture",
180
+ "input",
181
+ "textarea",
182
+ "select",
183
+ "button",
184
+ "progress",
185
+ "meter",
186
+ "hr",
187
+ ]);
169
188
  return REPLACED.has(el.tag);
170
189
  }
171
190
  function maxBorderRadius(el) {
@@ -212,8 +231,7 @@ function segmentEligible(seg) {
212
231
  // they proved inert: no PNG was materialized and the ordinary glyph remains
213
232
  // the sole paint. Only a real overlay or the zero-area ::first-letter
214
233
  // suppression marker changes rendering and must stay in the chrome layer.
215
- if (seg.rasterGlyphs?.some((glyph) => glyph.dataUri != null
216
- || (glyph.suppressGlyph === true && glyph.rect.width === 0 && glyph.rect.height === 0)))
234
+ if (seg.rasterGlyphs?.some((glyph) => glyph.dataUri != null || (glyph.suppressGlyph === true && glyph.rect.width === 0 && glyph.rect.height === 0)))
217
235
  return false;
218
236
  if (seg.dottedCircleMarks != null && seg.dottedCircleMarks.length > 0)
219
237
  return false;
@@ -237,8 +255,7 @@ function textPlaneForCompression(el) {
237
255
  // The compressor emits absolute glyph coordinates and its own translation
238
256
  // tracks. A non-identity residual would need matrix-aware track composition;
239
257
  // keep that text in the byte-safe chrome flipbook until such support exists.
240
- if (prepared.residualMatrix != null
241
- && !textAffineEquals(prepared.residualMatrix, IDENTITY_TEXT_AFFINE))
258
+ if (prepared.residualMatrix != null && !textAffineEquals(prepared.residualMatrix, IDENTITY_TEXT_AFFINE))
242
259
  return null;
243
260
  return prepared.element;
244
261
  }
@@ -269,9 +286,10 @@ function elementTextEligible(el, textEl, ctx) {
269
286
  return false;
270
287
  for (const clip of ctx.clips) {
271
288
  const inset = clip.inset;
272
- if (seg.x < clip.x + inset - 0.5 || seg.y < clip.y + inset - 0.5
273
- || seg.x + seg.width > clip.x + clip.w - inset + 0.5
274
- || seg.y + seg.height > clip.y + clip.h - inset + 0.5)
289
+ if (seg.x < clip.x + inset - 0.5 ||
290
+ seg.y < clip.y + inset - 0.5 ||
291
+ seg.x + seg.width > clip.x + clip.w - inset + 0.5 ||
292
+ seg.y + seg.height > clip.y + clip.h - inset + 0.5)
275
293
  return false;
276
294
  }
277
295
  }
@@ -499,16 +517,26 @@ function extractState(tree, regionRootIds, demoted) {
499
517
  for (const ch of seg.text) {
500
518
  const x = xs[u];
501
519
  const nextU = u + ch.length;
502
- let advance = nextU < seg.text.length ? xs[nextU] - x : (seg.width > 0 ? seg.x + seg.width - x : 0);
520
+ let advance = nextU < seg.text.length ? xs[nextU] - x : seg.width > 0 ? seg.x + seg.width - x : 0;
503
521
  if (!(advance > 0))
504
522
  advance = fontSize * 0.6;
505
523
  const isWs = WS_RE.test(ch);
506
524
  glyphs.push({
507
- ch, x, advance,
508
- lineKey: lineKeyOf(ctx.region, seg.y), region: ctx.region, segY: seg.y, segHeight: seg.height,
509
- fill: isWs ? "" : fillResolved, isWs, styleKey,
510
- fontSize, ascent, descent,
511
- srcEl: textEl, srcSeg: seg,
525
+ ch,
526
+ x,
527
+ advance,
528
+ lineKey: lineKeyOf(ctx.region, seg.y),
529
+ region: ctx.region,
530
+ segY: seg.y,
531
+ segHeight: seg.height,
532
+ fill: isWs ? "" : fillResolved,
533
+ isWs,
534
+ styleKey,
535
+ fontSize,
536
+ ascent,
537
+ descent,
538
+ srcEl: textEl,
539
+ srcSeg: seg,
512
540
  });
513
541
  u = nextU;
514
542
  }
@@ -590,10 +618,20 @@ function lineSignature(items) {
590
618
  function pairBuckets(live, next) {
591
619
  const result = [];
592
620
  const liveArr = [...live.entries()]
593
- .map(([k, glyphs]) => ({ k, glyphs, y: liveBucketY(glyphs), sig: lineSignature(glyphs.map((t) => ({ ch: t.rec.ch, styleKey: t.rec.styleKey, x: t.xs[t.xs.length - 1] }))) }))
621
+ .map(([k, glyphs]) => ({
622
+ k,
623
+ glyphs,
624
+ y: liveBucketY(glyphs),
625
+ sig: lineSignature(glyphs.map((t) => ({ ch: t.rec.ch, styleKey: t.rec.styleKey, x: t.xs[t.xs.length - 1] }))),
626
+ }))
594
627
  .sort((a, b) => a.y - b.y);
595
628
  const nextArr = [...next.entries()]
596
- .map(([k, recs]) => ({ k, recs, y: recs[0].segY, sig: lineSignature(recs.map((g) => ({ ch: g.ch, styleKey: g.styleKey, x: g.x }))) }))
629
+ .map(([k, recs]) => ({
630
+ k,
631
+ recs,
632
+ y: recs[0].segY,
633
+ sig: lineSignature(recs.map((g) => ({ ch: g.ch, styleKey: g.styleKey, x: g.x }))),
634
+ }))
597
635
  .sort((a, b) => a.y - b.y);
598
636
  const usedLive = new Set();
599
637
  const usedNext = new Set();
@@ -734,7 +772,12 @@ function threadGlyphs(perState, stateCount) {
734
772
  const prevList = pairing.prev.slice().sort((a, b) => a.xs[a.xs.length - 1] - b.xs[b.xs.length - 1]);
735
773
  const nextList = pairing.next;
736
774
  totalNext += nextList.length;
737
- const prevSeq = prevList.map((t) => ({ ch: t.rec.ch, x: t.xs[t.xs.length - 1], fill: t.fills[t.fills.length - 1], styleKey: t.rec.styleKey }));
775
+ const prevSeq = prevList.map((t) => ({
776
+ ch: t.rec.ch,
777
+ x: t.xs[t.xs.length - 1],
778
+ fill: t.fills[t.fills.length - 1],
779
+ styleKey: t.rec.styleKey,
780
+ }));
738
781
  const nextSeq = nextList.map((g) => ({ ch: g.ch, x: g.x, fill: g.fill, styleKey: g.styleKey }));
739
782
  const align = alignLineGlyphs(prevSeq, nextSeq);
740
783
  const survivors = [];
@@ -803,7 +846,15 @@ function threadGlyphs(perState, stateCount) {
803
846
  for (const g of placeBirths)
804
847
  if (g.x > last.x)
805
848
  last = g;
806
- edits.push({ state: s, x: last.x + last.advance, lineTop: last.segY, ascent: last.ascent, descent: last.descent, fontSize: last.fontSize, cellWidth: last.advance });
849
+ edits.push({
850
+ state: s,
851
+ x: last.x + last.advance,
852
+ lineTop: last.segY,
853
+ ascent: last.ascent,
854
+ descent: last.descent,
855
+ fontSize: last.fontSize,
856
+ cellWidth: last.advance,
857
+ });
807
858
  }
808
859
  else {
809
860
  let first = placeDeaths[0];
@@ -811,7 +862,15 @@ function threadGlyphs(perState, stateCount) {
811
862
  if (t.xs[t.xs.length - 1] < first.xs[first.xs.length - 1])
812
863
  first = t;
813
864
  const r = first.rec;
814
- edits.push({ state: s, x: first.xs[first.xs.length - 1], lineTop: r.segY, ascent: r.ascent, descent: r.descent, fontSize: r.fontSize, cellWidth: r.advance });
865
+ edits.push({
866
+ state: s,
867
+ x: first.xs[first.xs.length - 1],
868
+ lineTop: r.segY,
869
+ ascent: r.ascent,
870
+ descent: r.descent,
871
+ fontSize: r.fontSize,
872
+ cellWidth: r.advance,
873
+ });
815
874
  }
816
875
  }
817
876
  }
@@ -863,7 +922,18 @@ function buildGlyphGroups(threaded, uid) {
863
922
  const dxTl = t.xs.map((x) => round2(x - t.xs[0])).join(",");
864
923
  const dyTl = t.ys.map((y) => round2(y - t.ys[0])).join(",");
865
924
  const fillTl = t.fills.join("~");
866
- const key = [t.birth, t.death, t.rec.lineKey, t.rec.styleKey, round2(t.rec.segY), round2(t.rec.segHeight), round2(t.rec.ascent), dxTl, dyTl, fillTl].join("§");
925
+ const key = [
926
+ t.birth,
927
+ t.death,
928
+ t.rec.lineKey,
929
+ t.rec.styleKey,
930
+ round2(t.rec.segY),
931
+ round2(t.rec.segHeight),
932
+ round2(t.rec.ascent),
933
+ dxTl,
934
+ dyTl,
935
+ fillTl,
936
+ ].join("§");
867
937
  const arr = byKey.get(key);
868
938
  if (arr != null)
869
939
  arr.push(t);
@@ -947,7 +1017,7 @@ function groupElement(group) {
947
1017
  // output is byte-identical.
948
1018
  if (el.textTop != null) {
949
1019
  const firstSegY = first.rec.srcEl.textSegments?.[0]?.y ?? first.ys[0];
950
- el.textTop += (first.ys[0] - firstSegY) + dyTotal;
1020
+ el.textTop += first.ys[0] - firstSegY + dyTotal;
951
1021
  }
952
1022
  return el;
953
1023
  }
@@ -955,7 +1025,15 @@ const shallowKeyCache = new WeakMap();
955
1025
  function shallowKeyOf(el) {
956
1026
  let k = shallowKeyCache.get(el);
957
1027
  if (k == null) {
958
- k = fnv(JSON.stringify({ ...el, children: undefined, animId: undefined, animatedProperties: undefined, magicKey: undefined, cullClass: undefined, displayNone: undefined }));
1028
+ k = fnv(JSON.stringify({
1029
+ ...el,
1030
+ children: undefined,
1031
+ animId: undefined,
1032
+ animatedProperties: undefined,
1033
+ magicKey: undefined,
1034
+ cullClass: undefined,
1035
+ displayNone: undefined,
1036
+ }));
959
1037
  shallowKeyCache.set(el, k);
960
1038
  }
961
1039
  return k;
@@ -1348,8 +1426,22 @@ export function composeCompressedRun(states, opts) {
1348
1426
  // Render the two layers through the production pipeline. Chrome first
1349
1427
  // (below), glyphs above — the occlusion guard demoted anything a later
1350
1428
  // box-painting element overlaps, so this flattening is paint-safe.
1351
- const chromeInner = elementTreeToSvgInner(chromeTree, width, height, `${uid}c-`, true, 2, false);
1352
- const glyphInner = glyphEls.length > 0 ? elementTreeToSvgInner(glyphEls, width, height, `${uid}g-`, true, 2, false) : "";
1429
+ // DM-6SQXGF: when the run owns a real-text layer, render the two VISIBLE
1430
+ // layers inside `withRealTextLayerVisualSemantics` so their glyph runs emit
1431
+ // `aria-hidden="true"` (the paintless layer owns the readable flow). The two
1432
+ // renders pass `includeRealTextLayer=false` — the run emits ONE layer from the
1433
+ // final state below, not one per layer.
1434
+ let chromeInner = "";
1435
+ let glyphInner = "";
1436
+ const renderVisibleLayers = () => {
1437
+ chromeInner = elementTreeToSvgInner(chromeTree, width, height, `${uid}c-`, true, 2, false);
1438
+ glyphInner = glyphEls.length > 0 ? elementTreeToSvgInner(glyphEls, width, height, `${uid}g-`, true, 2, false) : "";
1439
+ };
1440
+ if (opts.realText === true)
1441
+ withRealTextLayerVisualSemantics(renderVisibleLayers);
1442
+ else
1443
+ renderVisibleLayers();
1444
+ const realTextLayer = opts.realText === true ? renderRealTextLayer(structuredClone(states[stateCount - 1].tree)) : "";
1353
1445
  // Behind-glyph selection: docs/101 selection rects resolved against each
1354
1446
  // selection's appear-state captured tree, emitted into the chrome↔glyph gap
1355
1447
  // (below `glyphInner`), so the highlight paints BEHIND the glyph ink — true
@@ -1377,8 +1469,13 @@ export function composeCompressedRun(states, opts) {
1377
1469
  : {}),
1378
1470
  };
1379
1471
  const track = {
1380
- shape: "bar", color: "#111111", barWidthPx: DEFAULT_CARET_WIDTH_PX,
1381
- blinkMs: CARET_BLINK_MS, waypoints: [], hides: [], selections: [sel],
1472
+ shape: "bar",
1473
+ color: "#111111",
1474
+ barWidthPx: DEFAULT_CARET_WIDTH_PX,
1475
+ blinkMs: CARET_BLINK_MS,
1476
+ waypoints: [],
1477
+ hides: [],
1478
+ selections: [sel],
1382
1479
  };
1383
1480
  parts.push(textTrackMarkup(track, totalMs, 100 + i));
1384
1481
  }
@@ -1398,8 +1495,12 @@ export function composeCompressedRun(states, opts) {
1398
1495
  selections: [],
1399
1496
  };
1400
1497
  const pointOf = (e) => ({
1401
- x: e.x, baselineY: e.lineTop + e.ascent, ascentPx: e.ascent, descentPx: e.descent,
1402
- fontSize: e.fontSize, cellWidthPx: e.cellWidth,
1498
+ x: e.x,
1499
+ baselineY: e.lineTop + e.ascent,
1500
+ ascentPx: e.ascent,
1501
+ descentPx: e.descent,
1502
+ fontSize: e.fontSize,
1503
+ cellWidthPx: e.cellWidth,
1403
1504
  });
1404
1505
  // Parked at the first upcoming edit point from t=0 (reads as "about to
1405
1506
  // type here"), then stepping to each edit's after-position at its boundary.
@@ -1413,8 +1514,8 @@ export function composeCompressedRun(states, opts) {
1413
1514
  const trackCss = css.css();
1414
1515
  const styleCss = `${fontFaceCss !== "" ? fontFaceCss + "\n" : ""}${trackCss}`;
1415
1516
  const bgRect = opts.background != null ? `<rect width="${width}" height="${height}" fill="${esc(opts.background)}"/>` : "";
1416
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`
1417
- + `<style>${styleCss}</style>${bgRect}${chromeInner}${selectionMarkup}${glyphInner}${caretMarkup}</svg>`;
1517
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">` +
1518
+ `<style>${styleCss}</style>${bgRect}${chromeInner}${selectionMarkup}${glyphInner}${caretMarkup}${realTextLayer}</svg>`;
1418
1519
  const pairingStats = {
1419
1520
  states: stateCount,
1420
1521
  glyphs: plan.thread.totalNext,