buninu-browser 0.7.0 → 0.9.0

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 (142) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +90 -1
  3. package/buninu-browser.js +1 -1
  4. package/frontends/wayland/README.md +35 -2
  5. package/frontends/wayland/client.js +8 -6
  6. package/frontends/wayland/clipboard.js +1 -1
  7. package/frontends/wayland/index.js +1 -1
  8. package/frontends/wayland/input.js +1 -1
  9. package/frontends/wayland/resource-io.js +6 -0
  10. package/frontends/wayland/transport-posix.js +120 -0
  11. package/frontends/wayland/transport-win32.js +9 -0
  12. package/frontends/wayland/transport.js +3 -120
  13. package/frontends/wayland/win32-resources.js +80 -0
  14. package/frontends/wayland/win32-wire.js +62 -0
  15. package/lib/audio/biquad.js +25 -0
  16. package/lib/audio/piano.js +6 -3
  17. package/lib/audio/playback-buffer.js +5 -5
  18. package/lib/audio/pulse-client.js +24 -5
  19. package/lib/audio/web-audio.js +570 -0
  20. package/lib/browser-dialog.js +116 -0
  21. package/lib/cdp-server.js +43 -11
  22. package/lib/file-picker.js +48 -0
  23. package/lib/headless-shell.js +34 -12
  24. package/lib/layout/text-layout.js +112 -19
  25. package/lib/media/mp4-frame-times.js +93 -0
  26. package/lib/paint/direct-canvas.ts +39 -0
  27. package/lib/paint/display-list.js +37 -12
  28. package/lib/paint/raster-readback.js +16 -7
  29. package/lib/renderer/animation-frames.js +10 -5
  30. package/lib/renderer/bindings.js +69 -16
  31. package/lib/renderer/canvas-2d.js +265 -15
  32. package/lib/renderer/canvas-round-rect.js +36 -0
  33. package/lib/renderer/css-animations.js +173 -0
  34. package/lib/renderer/dialog-client.js +30 -0
  35. package/lib/renderer/dialog-worker.js +35 -0
  36. package/lib/renderer/eval-environment.js +28 -0
  37. package/lib/renderer/file-input.js +49 -0
  38. package/lib/renderer/host.js +33 -4
  39. package/lib/renderer/html-drag.js +77 -0
  40. package/lib/renderer/image-loading.js +45 -2
  41. package/lib/renderer/main.js +4 -0
  42. package/lib/renderer/module-transform.js +5 -3
  43. package/lib/renderer/page-builtins.js +59 -0
  44. package/lib/renderer/page-realm.js +177 -40
  45. package/lib/renderer/page-renderer.js +397 -49
  46. package/lib/renderer/piano-input.js +3 -1
  47. package/lib/renderer/raster-surface.js +18 -0
  48. package/lib/renderer/script-rewrite.js +165 -9
  49. package/lib/renderer/video-presentation.js +5 -4
  50. package/lib/renderer/webgl/clip.js +24 -0
  51. package/lib/renderer/webgl/context.js +158 -0
  52. package/lib/renderer/webgl/cpu-fragment.js +211 -0
  53. package/lib/renderer/webgl/draw-cache.js +22 -0
  54. package/lib/renderer/webgl/pipeline.js +70 -0
  55. package/lib/renderer/webgl/shaders.js +58 -0
  56. package/lib/renderer/webgl/textures.js +43 -0
  57. package/lib/renderer/window-named-properties.js +46 -0
  58. package/lib/screenshot-cli.js +4 -7
  59. package/lib/style/animation-values.js +29 -0
  60. package/lib/style/computed-style.js +109 -13
  61. package/lib/style/transform.js +44 -0
  62. package/lib/window-size.js +10 -0
  63. package/package.json +1 -1
  64. package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
  65. package/patches/happy-dom/README.md +16 -0
  66. package/test/animation-frames.test.js +15 -0
  67. package/test/canvas-2d.test.js +252 -4
  68. package/test/canvas-frame-pacing.test.js +7 -0
  69. package/test/cdp-viewers.test.js +36 -0
  70. package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
  71. package/test/classic-games-patches/README.md +156 -0
  72. package/test/classic-games-scenarios.js +133 -0
  73. package/test/clipboard.test.js +16 -0
  74. package/test/computed-style.test.js +18 -0
  75. package/test/css-animation-lifecycle.test.js +28 -0
  76. package/test/css-animations.test.js +83 -0
  77. package/test/direct-canvas.test.js +39 -0
  78. package/test/dynamic-inline-handlers.test.js +25 -0
  79. package/test/eval-declarations.test.js +22 -0
  80. package/test/file-picker.test.js +26 -0
  81. package/test/fixtures/canvas-damage.js +23 -0
  82. package/test/fixtures/canvas-frame-pacing.js +35 -0
  83. package/test/fixtures/direct-canvas.js +96 -0
  84. package/test/fixtures/javascript-dialogs.html +8 -0
  85. package/test/fixtures/media-audio-lifetime.js +65 -0
  86. package/test/fixtures/renderer-memory.js +7 -5
  87. package/test/fixtures/video-black-replay.js +8 -6
  88. package/test/fixtures/video-slow-end.js +39 -0
  89. package/test/fixtures/web-audio-renderer.js +45 -0
  90. package/test/flex.test.js +26 -0
  91. package/test/grid.test.js +13 -0
  92. package/test/headless-shell.test.js +9 -0
  93. package/test/headless-window-size.test.js +28 -0
  94. package/test/html-rcdata.test.js +35 -0
  95. package/test/image-decode.test.js +18 -0
  96. package/test/javascript-dialog.test.js +83 -0
  97. package/test/manual-breakout-paddle.js +42 -0
  98. package/test/manual-breakout-static.js +35 -0
  99. package/test/manual-classic-games.js +41 -0
  100. package/test/manual-clumsy-bird-three-pipes.js +81 -0
  101. package/test/manual-clumsy-bird.js +52 -0
  102. package/test/manual-hextris.js +30 -0
  103. package/test/manual-numbered-music.js +24 -0
  104. package/test/manual-t-rex-runner.js +49 -0
  105. package/test/manual-threejs-scene.js +64 -0
  106. package/test/manual-webgl-benchmark.js +27 -0
  107. package/test/manual-webgl-cpu.html +26 -0
  108. package/test/media-audio-lifetime.test.js +8 -0
  109. package/test/mp4-frame-times.test.js +33 -0
  110. package/test/page-scripts.test.js +96 -1
  111. package/test/piano-input.test.js +15 -2
  112. package/test/piano.test.js +11 -2
  113. package/test/playback-buffer.test.js +11 -0
  114. package/test/pointer-drag.test.js +33 -0
  115. package/test/positioning.test.js +82 -0
  116. package/test/pulse-client.test.js +51 -2
  117. package/test/raster-readback.test.js +14 -0
  118. package/test/script-rewrite.test.js +61 -3
  119. package/test/video-slow-end.test.js +6 -0
  120. package/test/viewport-keyboard-overflow.test.js +25 -0
  121. package/test/wayland-win32-resources.test.js +47 -0
  122. package/test/wayland-win32-wire.test.js +59 -0
  123. package/test/web-audio-renderer.test.js +8 -0
  124. package/test/web-audio.test.js +264 -0
  125. package/test/webgl-clip.test.js +9 -0
  126. package/test/webgl-cpu-fragment.test.js +43 -0
  127. package/test/window-named-properties.test.js +41 -0
  128. package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
  129. package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
  130. package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
  131. package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
  132. package/vendor/land-renderer/LICENSE +21 -0
  133. package/vendor/land-renderer/README.md +14 -0
  134. package/vendor/land-renderer/direct-compositor.ts +443 -0
  135. package/vendor/land-renderer/frame.js +88 -0
  136. package/vendor/land-renderer/png-fast.js +85 -0
  137. package/vendor/land-renderer/raw-frames.js +66 -0
  138. package/vendor/land-renderer/render-types.ts +7 -0
  139. package/vendor/land-renderer/renderer.ts +134 -0
  140. package/vendor/land-renderer/snapshot.js +37 -0
  141. package/vendor/land-renderer/viewport.js +18 -0
  142. package/vendor/ses/dist/ses.mjs +1 -1
@@ -1,3 +1,4 @@
1
+ import {roundRect} from './canvas-round-rect.js';
1
2
  /**
2
3
  * Page `<canvas>` 2d contexts, drawn by this process's CanvasKit.
3
4
  *
@@ -18,6 +19,10 @@
18
19
  import { drawQmlDomImage } from "../qml/canvas-dom.js";
19
20
  // qml handle end
20
21
 
22
+ import { makeRasterSurface } from "./raster-surface.js";
23
+
24
+ import { createSoftwareGL } from "./webgl/context.js";
25
+
21
26
  let CanvasKit = null;
22
27
 
23
28
  /** The CanvasKit `loadGraphics` instantiated. Page canvases share it. */
@@ -35,13 +40,31 @@ const collectedContexts = new FinalizationRegistry(releaseSurface);
35
40
  const DRAW_METHODS = new Set(["clearRect", "drawImage", "fill", "fillRect", "fillText", "stroke", "strokeRect", "strokeText", "putImageData"]);
36
41
  const CONTEXT_METHODS = new Set(("arc arcTo beginPath bezierCurveTo clearRect clip closePath createImageData createLinearGradient createPattern createRadialGradient drawFocusIfNeeded drawImage ellipse fill fillRect fillText getImageData getLineDash getTransform isPointInPath isPointInStroke lineTo measureText moveTo putImageData quadraticCurveTo rect reset resetTransform restore rotate roundRect save scale setLineDash setTransform stroke strokeRect strokeText transform translate").split(" "));
37
42
  const CONTEXT_PROPERTIES = new Set(("canvas direction fillStyle filter font fontKerning fontStretch fontVariantCaps globalAlpha globalCompositeOperation imageSmoothingEnabled imageSmoothingQuality letterSpacing lineCap lineDashOffset lineJoin lineWidth miterLimit shadowBlur shadowColor shadowOffsetX shadowOffsetY strokeStyle textAlign textBaseline textRendering wordSpacing").split(" "));
43
+ // Native pattern shaders are requested synchronously by the drawing context.
44
+ // A pattern may be shared across contexts: sampling belongs to the consumer.
45
+ let patternSampling = { imageSmoothingEnabled: true, imageSmoothingQuality: "low" };
38
46
  const nativeStyles = new WeakMap();
39
47
  const publicStyles = new WeakMap();
48
+ const patternImages = new WeakMap();
49
+ const gradientDescriptions = new WeakMap();
50
+ const imagePaints = new WeakMap();
51
+ function imagePaint(context) {
52
+ let key=imagePaints.get(context);
53
+ if(key===undefined){
54
+ // Cache the field name, never the Paint: restore() replaces and deletes
55
+ // the old base Paint. Copying it avoids building a fill-style shader.
56
+ key=Object.keys(context).find(name=>context[name] instanceof CanvasKit.Paint);
57
+ if(key!==undefined)imagePaints.set(context,key);
58
+ }
59
+ const paint=key!==undefined?context[key].copy():context.Ke();
60
+ paint.setShader(null);paint.setAlphaf(context.globalAlpha);
61
+ return paint;
62
+ }
40
63
  const collectedStyles = new FinalizationRegistry(native => native.ie());
41
64
 
42
65
  // CanvasKit's gradient/pattern shims expose shader handles and disposal
43
66
  // methods. Only the standard author-facing methods may cross into a page.
44
- function publicStyle(native) {
67
+ function publicStyle(native, patternResource = null) {
45
68
  if (native === null || typeof native !== "object") return native;
46
69
  let wrapper = publicStyles.get(native)?.deref();
47
70
  if (wrapper) return wrapper;
@@ -53,7 +76,11 @@ function publicStyle(native) {
53
76
  // WebIDL converts these before entering the native implementation.
54
77
  // The shim otherwise stores the original offset object in its stops,
55
78
  // which could retain the facade/DOM through finalizer holdings.
56
- if (gradient) native.addColorStop(Number(args[0]), String(args[1]));
79
+ if (gradient) {
80
+ const offset=Number(args[0]),color=String(args[1]);
81
+ native.addColorStop(offset,color);
82
+ gradientDescriptions.get(native)?.stops.push([offset,color]);
83
+ }
57
84
  else native.setTransform(...args);
58
85
  }, writable: true, configurable: true, enumerable: true },
59
86
  [Symbol.toStringTag]: { value: gradient ? "CanvasGradient" : "CanvasPattern", configurable: true },
@@ -67,6 +94,7 @@ function publicStyle(native) {
67
94
  // The current pattern shim may retain its page-supplied image object;
68
95
  // do not root that DOM graph in finalizer holdings. Gradients retain only
69
96
  // numeric coordinates/colors and their private native shader.
97
+ if (patternResource) collectedStyles.register(wrapper, patternResource, patternResource);
70
98
  if (gradient) {
71
99
  collectedStyles.unregister(native);
72
100
  collectedStyles.register(wrapper, native, native);
@@ -74,6 +102,25 @@ function publicStyle(native) {
74
102
  return wrapper;
75
103
  }
76
104
 
105
+ const contextMethods = new WeakMap();
106
+ /** Per-document interface: libraries may feature-detect or extend its prototype. */
107
+ export function canvasRenderingContextInterface() {
108
+ function CanvasRenderingContext2D(){throw new TypeError('Illegal constructor');}
109
+ for(const name of CONTEXT_METHODS) {
110
+ if(['drawFocusIfNeeded','reset'].includes(name))continue;
111
+ Object.defineProperty(CanvasRenderingContext2D.prototype,name,{configurable:true,writable:true,enumerable:true,
112
+ value:function(...args){
113
+ const read=contextMethods.get(this);
114
+ if(!read)throw new TypeError('Illegal invocation');
115
+ const method=read(name);
116
+ if(typeof method!=='function')throw new TypeError('Unsupported Canvas operation');
117
+ return method(...args);
118
+ }});
119
+ }
120
+ Object.defineProperty(CanvasRenderingContext2D.prototype,Symbol.toStringTag,{value:'CanvasRenderingContext2D',configurable:true});
121
+ return CanvasRenderingContext2D;
122
+ }
123
+
77
124
  /** Drawing changes paint, without requiring a DOM mutation or full layout. */
78
125
  export function observeDocumentCanvases(document, changed) {
79
126
  documentChanges.set(document, changed);
@@ -87,8 +134,17 @@ function changed(state) {
87
134
  if (node) documentChanges.get(node.ownerDocument)?.(node);
88
135
  }
89
136
 
137
+ function clearGradientCache(state) {
138
+ for(const entry of state.gradientCache?.values()??[])entry.image.delete();
139
+ state.gradientCache?.clear();state.gradientCandidates?.clear();state.gradientCacheBytes=0;
140
+ }
141
+
90
142
  function releaseSurface(state) {
143
+ clearGradientCache(state);
91
144
  if (!state.surface) return;
145
+ for(const pattern of state.patterns??[])pattern.ie();
146
+ state.patterns?.clear();
147
+ state.gl?.dispose();
92
148
  state.image?.delete();
93
149
  state.image = state.resource = null;
94
150
  state.surface.dispose();
@@ -110,26 +166,37 @@ export function disposeDocumentCanvases(document) {
110
166
 
111
167
  /** The element's 2d context, or null for any other context id or before CanvasKit loads. */
112
168
  export function canvasContext(node, type, options = null) {
113
- if (type !== "2d" || !CanvasKit) return null;
169
+ if (!["2d", "webgl", "experimental-webgl"].includes(type) || !CanvasKit) return null;
170
+ const kind=type==='2d'?'2d':'webgl';
114
171
  let entry = surfaces.get(node);
115
172
  if (!entry) {
116
173
  entry = open(node);
117
174
  if (!entry) return null;
175
+ entry.kind=kind;
176
+ if(kind==='webgl'){
177
+ const gl=createSoftwareGL(CanvasKit,entry.state,changed,options);
178
+ entry.gl=gl;
179
+ entry.state.gl={dispose:gl.dispose,reset:gl.reset,flush:gl.flush};
180
+ collectedContexts.unregister(entry.state);
181
+ collectedContexts.register(gl.proxy,entry.state,entry.state);
182
+ }
118
183
  surfaces.set(node, entry);
119
184
  }
120
- if (options) entry.configure(options);
121
- return entry.proxy;
185
+ if(entry.kind!==kind)return null;
186
+ if (options) {if(entry.gl)entry.gl.configure(options);else entry.configure(options);}
187
+ return entry.gl?.proxy ?? entry.proxy;
122
188
  }
123
189
 
124
190
  /** Setting width or height clears the bitmap and the drawing state. */
125
191
  export function resetCanvas(node) {
126
- surfaces.get(node)?.reopen();
192
+ const entry=surfaces.get(node);entry?.reopen();entry?.state.gl?.reset();
127
193
  }
128
194
 
129
195
  /** A refcounted native snapshot: no PNG/base64/readPixels round trip. */
130
196
  export function canvasResource(node) {
131
197
  const state = surfaces.get(node)?.state;
132
198
  if (!state?.surface || !state.raster) return null;
199
+ state.gl?.flush();
133
200
  if (!state.resource) {
134
201
  state.raster.flush();
135
202
  state.image = state.raster.makeImageSnapshot();
@@ -182,7 +249,7 @@ function open(node) {
182
249
  owner?.add(state);
183
250
  const methods = new Map();
184
251
  let publicCanvas = null, resolveImage = null, parseFont = null, text = null;
185
- const textDefaults = () => ({ font: "10px sans-serif", parsedFont: { fontSize: 10, fontFamily: ["sans-serif"], fontWeight: 400, fontStyle: "normal", fontStretch: 100 },
252
+ const textDefaults = () => ({ imageSmoothingEnabled: true, imageSmoothingQuality: "low", font: "10px sans-serif", parsedFont: { fontSize: 10, fontFamily: ["sans-serif"], fontWeight: 400, fontStyle: "normal", fontStretch: 100 },
186
253
  textAlign: "start", textBaseline: "alphabetic", direction: "inherit" });
187
254
  let settings = textDefaults();
188
255
  const settingsStack = [];
@@ -192,18 +259,20 @@ function open(node) {
192
259
  if (!state.context) throw new DOMException("The canvas document has been discarded.", "InvalidStateError");
193
260
  return state.context;
194
261
  };
195
- const proxy = new Proxy(Object.create(null), {
262
+ let proxy;
263
+ const target=Object.create(null);
264
+ const handler = {
196
265
  get(_target, property) {
197
266
  if (property === "canvas") return publicCanvas;
198
267
  if (!CONTEXT_METHODS.has(property) && !CONTEXT_PROPERTIES.has(property)) return undefined;
199
268
  const context = currentContext();
200
- if (text && Object.hasOwn(settings, property) && property !== "parsedFont") return settings[property];
269
+ if (Object.hasOwn(settings, property) && property !== "parsedFont") return settings[property];
201
270
  // The shim's strokeStyle getter assumes every value is a color, even
202
271
  // after accepting a gradient. Keep the page's branded style identity.
203
272
  if ((property === "fillStyle" || property === "strokeStyle") && styleValues[property]) return styleValues[property];
204
273
  const value = context[property];
205
274
  if (property === "fillStyle" || property === "strokeStyle") return publicStyle(value);
206
- const custom = property === "getTransform";
275
+ const custom = property === "getTransform" || property === "roundRect";
207
276
  if (typeof value !== "function" && !custom) return value;
208
277
  let method = methods.get(property);
209
278
  if (!method) {
@@ -211,14 +280,18 @@ function open(node) {
211
280
  // keeps its resource alive while a caller retains only the method.
212
281
  method = function (...args) {
213
282
  const context = currentContext();
283
+ if (property === "roundRect") return roundRect(context,...args);
214
284
  if (property === "getTransform") {
215
285
  const matrix = state.raster.getCanvas().getTotalMatrix();
216
286
  return { a: matrix[0], b: matrix[3], c: matrix[1], d: matrix[4], e: matrix[2], f: matrix[5], is2D: true };
217
287
  }
218
288
  if (text && ["fillText", "strokeText", "measureText"].includes(property)) {
219
- const result = text({ operation: property, args, context, canvas: state.raster.getCanvas(), settings,
289
+ const previousSampling=patternSampling;
290
+ let result;
291
+ patternSampling=settings;
292
+ try { result = text({ operation: property, args, context, canvas: state.raster.getCanvas(), settings,
220
293
  paintStyle: nativeStyles.get(styleValues[property === "strokeText" ? "strokeStyle" : "fillStyle"]),
221
- });
294
+ }); } finally { patternSampling=previousSampling; }
222
295
  if (property !== "measureText") changed(state);
223
296
  return result;
224
297
  }
@@ -226,6 +299,41 @@ function open(node) {
226
299
  throw new DOMException("The canvas contains cross-origin image data.", "SecurityError");
227
300
  }
228
301
  let source;
302
+ if(property === 'createPattern') {
303
+ const repetition=args[1]===undefined||args[1]===null||args[1]===''?'repeat':String(args[1]);
304
+ if(!['repeat','repeat-x','repeat-y','no-repeat'].includes(repetition))throw new DOMException('Invalid repetition','SyntaxError');
305
+ const resource=resolveImage?resolveImage(args[0]):canvasResource(args[0]);
306
+ if(!resource?.image){
307
+ if(!['IMG','CANVAS','VIDEO','IMAGE'].includes(args[0]?.tagName))throw new TypeError('CanvasImageSource required');
308
+ return null;
309
+ }
310
+ // Retain a native snapshot handle, not a page object or a borrowed
311
+ // image that a source-canvas draw/reset can delete underneath us.
312
+ const image=resource.image.clone();let native;
313
+ try{native=context.createPattern(image,repetition);}catch(error){image.delete();throw error;}
314
+ // The upstream shim always uses bicubic filtering, even when an
315
+ // author disables smoothing. Apart from the wrong pixel-art output,
316
+ // that is very expensive for a repeated full-canvas background.
317
+ // Canvas save retains the same mutable pattern, as the DOM does.
318
+ // The upstream copy constructor also drops its image/transform.
319
+ // Avoid appending the same handle to the shim cleanup list on every save.
320
+ native.oe=undefined;
321
+ native.pe=function(){
322
+ this.ie();
323
+ const sampling=patternSampling;
324
+ return this.Td=sampling.imageSmoothingEnabled && sampling.imageSmoothingQuality==='high'
325
+ ? image.makeShaderCubic(this.ve,this.we,1/3,1/3,this._transform)
326
+ : image.makeShaderOptions(this.ve,this.we,
327
+ sampling.imageSmoothingEnabled?CanvasKit.FilterMode.Linear:CanvasKit.FilterMode.Nearest,
328
+ CanvasKit.MipmapMode.None,this._transform);
329
+ };
330
+ patternImages.set(native,{image,repetition});
331
+ const owned=state.patterns??=new Set();
332
+ const pattern={ie(){if(!owned.delete(pattern))return;native.ie();image.delete();}};
333
+ owned.add(pattern);
334
+ if(resource.originClean===false)state.originClean=false;
335
+ return publicStyle(native,pattern);
336
+ }
229
337
  if (property === "drawImage" && resolveImage) {
230
338
  source = resolveImage(args[0]);
231
339
  if (!source?.image) return;
@@ -240,7 +348,30 @@ function open(node) {
240
348
 
241
349
  args[0] = source.image;
242
350
  }
243
- const result = context[property](...args);
351
+ const previousSampling=patternSampling;
352
+ let result;
353
+ patternSampling=settings;
354
+ try {
355
+ if (property === "fillRect" && fillCachedGradient(state,context,nativeStyles.get(styleValues.fillStyle),args)) {
356
+ // Cache radial rasters and reduce vertical gradients to one column.
357
+ } else if (property === "fillRect" && fillPatternTiles(state,context,settings,nativeStyles.get(styleValues.fillStyle),args)) {
358
+ // Integer, unsmoothed tiles can use Skia's image blit pipeline.
359
+ } else if (property === "drawImage" && [3,5,9].includes(args.length)) {
360
+ // Images use globalAlpha, never the current fill color/pattern.
361
+ const image=args[0],paint=imagePaint(context);
362
+ const src=args.length===9?CanvasKit.XYWHRect(...args.slice(1,5)):CanvasKit.XYWHRect(0,0,image.width(),image.height());
363
+ const dst=args.length===9?CanvasKit.XYWHRect(...args.slice(5,9))
364
+ :CanvasKit.XYWHRect(args[1],args[2],args[3]??image.width(),args[4]??image.height());
365
+ try {
366
+ const canvas=state.raster.getCanvas();
367
+ if(settings.imageSmoothingEnabled && settings.imageSmoothingQuality==='high')
368
+ canvas.drawImageRectCubic(image,src,dst,1/3,1/3,paint);
369
+ else canvas.drawImageRectOptions(image,src,dst,
370
+ settings.imageSmoothingEnabled?CanvasKit.FilterMode.Linear:CanvasKit.FilterMode.Nearest,
371
+ CanvasKit.MipmapMode.None,paint);
372
+ } finally { paint.delete(); }
373
+ } else result = context[property](...args);
374
+ } finally { patternSampling=previousSampling; }
244
375
  if (property === "save") {
245
376
  settingsStack.push({ ...settings });
246
377
  styleStack.push({ ...styleValues });
@@ -253,6 +384,7 @@ function open(node) {
253
384
  }
254
385
  if (source?.originClean === false) state.originClean = false;
255
386
  if (DRAW_METHODS.has(property)) changed(state);
387
+ if (property === "createRadialGradient" || property === "createLinearGradient") gradientDescriptions.set(result,{kind:property,coordinates:args.map(Number),stops:[]});
256
388
  if (property === "createLinearGradient" || property === "createRadialGradient" || property === "createPattern") return publicStyle(result);
257
389
  return property === "getImageData" ? copyImageData(result) : result;
258
390
  }.bind(proxy);
@@ -277,6 +409,11 @@ function open(node) {
277
409
  if (parsed) { settings.font = String(value); settings.parsedFont = parsed; }
278
410
  return true;
279
411
  }
412
+ if (property === "imageSmoothingEnabled") { settings[property]=Boolean(value); return true; }
413
+ if (property === "imageSmoothingQuality") {
414
+ if (["low","medium","high"].includes(String(value))) settings[property]=String(value);
415
+ return true;
416
+ }
280
417
  if (text && Object.hasOwn(settings, property)) {
281
418
  const values = { textAlign: ["start", "end", "left", "right", "center"], textBaseline: ["top", "hanging", "middle", "alphabetic", "ideographic", "bottom"], direction: ["inherit", "ltr", "rtl"] };
282
419
  if (values[property]?.includes(String(value))) settings[property] = String(value);
@@ -286,15 +423,21 @@ function open(node) {
286
423
  return true;
287
424
  },
288
425
  has(_target, property) {
289
- return property === "canvas" || property === "getTransform" || (CONTEXT_METHODS.has(property) || CONTEXT_PROPERTIES.has(property)) && (property in currentContext() || text && Object.hasOwn(settings, property));
426
+ return property === "canvas" || property === "getTransform" || property === "roundRect" || (CONTEXT_METHODS.has(property) || CONTEXT_PROPERTIES.has(property)) && (property in currentContext() || text && Object.hasOwn(settings, property));
290
427
  },
428
+ };
429
+ proxy=new Proxy(target,{...handler,
430
+ get(target,property,receiver){return property in target?Reflect.get(target,property,receiver):handler.get(target,property);},
431
+ has(target,property){return property in target||handler.has(target,property);},
291
432
  });
433
+ contextMethods.set(proxy,property=>handler.get(target,property));
292
434
  collectedContexts.register(proxy, state, state);
293
435
  return {
294
436
  state,
295
437
  proxy,
296
438
  configure(options) {
297
439
  publicCanvas = options.canvas;
440
+ if(options.prototype)Object.setPrototypeOf(target,options.prototype);
298
441
  resolveImage = options.resolveImage;
299
442
  parseFont = options.parseFont;
300
443
  text = options.text;
@@ -304,6 +447,7 @@ function open(node) {
304
447
  const next = makeSurface(node);
305
448
  if (!next) return;
306
449
  const previous = state.surface;
450
+ clearGradientCache(state);
307
451
  state.surface = next;
308
452
  state.raster = nativeSurface(next);
309
453
  state.context = next.getContext("2d");
@@ -319,9 +463,115 @@ function open(node) {
319
463
  };
320
464
  }
321
465
 
466
+ // Cache shadow-free source-over gradient rectangles. Vertical gradients
467
+ // shade one device-pixel column and repeat it without interpolation. The raster includes
468
+ // the device transform but excludes alpha and clipping, which apply at blit.
469
+ // Keys include mutable stops; no DOM or author object is retained by entries.
470
+ function fillCachedGradient(state,context,style,args) {
471
+ const gradient=gradientDescriptions.get(style);
472
+ if(!gradient||args.length!==4||!args.every(Number.isFinite)
473
+ ||context.globalCompositeOperation!=='source-over'
474
+ ||CanvasKit.parseColorString(context.shadowColor)[3]!==0)return false;
475
+ const canvas=state.raster.getCanvas(),matrix=canvas.getTotalMatrix();
476
+ if(matrix[1]!==0||matrix[3]!==0||matrix[6]!==0||matrix[7]!==0||matrix[8]!==1||matrix[0]<=0||matrix[4]<=0)return false;
477
+ const [x,y,w,h]=args;if(w<=0||h<=0)return false;
478
+ const left=Math.max(0,Math.floor(x*matrix[0]+matrix[2])),top=Math.max(0,Math.floor(y*matrix[4]+matrix[5]));
479
+ const right=Math.min(state.width,Math.ceil((x+w)*matrix[0]+matrix[2])),bottom=Math.min(state.height,Math.ceil((y+h)*matrix[4]+matrix[5]));
480
+ const width=right-left,height=bottom-top,bytes=width*height*4;
481
+ if(width<=0||height<=0)return true;
482
+ if(bytes>8*1024*1024)return false;
483
+ const vertical=gradient.kind==='createLinearGradient'&&gradient.coordinates[0]===gradient.coordinates[2];
484
+ if(gradient.kind==='createLinearGradient'&&!vertical)return false;
485
+ if(vertical&&[x*matrix[0]+matrix[2],(x+w)*matrix[0]+matrix[2]].some(v=>Math.abs(v-Math.round(v))>1e-4))return false;
486
+ const rasterWidth=vertical?1:width;
487
+ const key=JSON.stringify([gradient.kind,gradient.coordinates,gradient.stops,args,Array.from(matrix),left,top,width,height]);
488
+ // Admit a full radial raster only after reuse, so animated unique
489
+ // gradients retain their ordinary shader path without snapshot churn.
490
+ if(!vertical){
491
+ const candidates=state.gradientCandidates??=new Set();
492
+ if(!candidates.has(key)){
493
+ if(candidates.size>=16)candidates.delete(candidates.values().next().value);
494
+ candidates.add(key);return false;
495
+ }
496
+ }
497
+ const cache=state.gradientCache??=new Map();let entry=cache.get(key);
498
+ if(!entry){
499
+ const surface=makeRasterSurface(CanvasKit,rasterWidth,height);if(!surface)return false;
500
+ let paint;
501
+ try {
502
+ paint=context.Ke();paint.setAlphaf(1);
503
+ const target=surface.getCanvas();target.translate(-left,-top);target.concat(matrix);
504
+ target.drawRect(CanvasKit.XYWHRect(x,y,w,h),paint);
505
+ entry={image:surface.makeImageSnapshot(),bytes};
506
+ } finally {paint?.dispose();surface.dispose();}
507
+ while(cache.size>=8||(state.gradientCacheBytes??0)+bytes>8*1024*1024){
508
+ const oldest=cache.keys().next().value,victim=cache.get(oldest);
509
+ cache.delete(oldest);state.gradientCacheBytes-=victim.bytes;victim.image.delete();
510
+ }
511
+ state.gradientCacheBytes=(state.gradientCacheBytes??0)+bytes;
512
+ } else cache.delete(key);
513
+ cache.set(key,entry);
514
+ const paint=imagePaint(context),inverse=CanvasKit.Matrix.invert(matrix);
515
+ canvas.save();
516
+ try{canvas.concat(inverse);
517
+ if(vertical)canvas.drawImageRectOptions(entry.image,CanvasKit.XYWHRect(0,0,1,height),CanvasKit.XYWHRect(left,top,width,height),CanvasKit.FilterMode.Nearest,CanvasKit.MipmapMode.None,paint);
518
+ else canvas.drawImage(entry.image,left,top,paint);}
519
+ finally{canvas.restore();paint.delete();}
520
+ return true;
521
+ }
522
+
523
+ // Avoid the generic repeated-image shader for a few pixel-aligned tiles.
524
+ // Restrict this to source-over and nearest sampling: rotated, filtered, shadowed
525
+ // and densely repeated fills keep the complete native shader implementation.
526
+ function fillPatternTiles(state,context,settings,style,args) {
527
+ const resource=patternImages.get(style);
528
+ if(!resource || settings.imageSmoothingEnabled || context.globalCompositeOperation!=='source-over'
529
+ || CanvasKit.parseColorString(context.shadowColor)[3]!==0 || args.length!==4
530
+ || !args.every(Number.isSafeInteger))return false;
531
+ const matrix=state.raster.getCanvas().getTotalMatrix(),m=style._transform;
532
+ if(matrix[0]!==1||matrix[4]!==1||matrix[1]!==0||matrix[3]!==0
533
+ ||matrix[6]!==0||matrix[7]!==0||matrix[8]!==1
534
+ ||!Number.isSafeInteger(matrix[2])||!Number.isSafeInteger(matrix[5])
535
+ ||m[0]!==1||m[4]!==1||m[1]!==0||m[3]!==0
536
+ ||!Number.isSafeInteger(m[2])||!Number.isSafeInteger(m[5]))return false;
537
+ let [x,y,w,h]=args;if(w<0){x+=w;w=-w;}if(h<0){y+=h;h=-h;}
538
+ if(!w||!h)return true;
539
+ // The canvas itself already clips this fill. Bound iteration by its visible
540
+ // rectangle, even when authors fill an enormous offscreen area.
541
+ const left=Math.max(x,-matrix[2]),top=Math.max(y,-matrix[5]);
542
+ const right=Math.min(x+w,state.width-matrix[2]),bottom=Math.min(y+h,state.height-matrix[5]);
543
+ if(right<=left||bottom<=top)return true;
544
+ const {image,repetition}=resource,iw=image.width(),ih=image.height();
545
+ let x0=Math.floor((left-m[2])/iw),x1=Math.ceil((right-m[2])/iw);
546
+ let y0=Math.floor((top-m[5])/ih),y1=Math.ceil((bottom-m[5])/ih);
547
+ if(repetition==='repeat-y'||repetition==='no-repeat'){x0=Math.max(0,x0);x1=Math.min(1,x1);}
548
+ if(repetition==='repeat-x'||repetition==='no-repeat'){y0=Math.max(0,y0);y1=Math.min(1,y1);}
549
+ if((x1-x0)*(y1-y0)>16)return false;
550
+ const canvas=state.raster.getCanvas(),paint=context.Ke();
551
+ paint.setShader(null);canvas.save();
552
+ try {
553
+ canvas.clipRect(CanvasKit.XYWHRect(left,top,right-left,bottom-top),CanvasKit.ClipOp.Intersect,false);
554
+ const source=CanvasKit.XYWHRect(0,0,iw,ih);
555
+ for(let ty=y0;ty<y1;ty++)for(let tx=x0;tx<x1;tx++)
556
+ canvas.drawImageRectOptions(image,source,CanvasKit.XYWHRect(m[2]+tx*iw,m[5]+ty*ih,iw,ih),
557
+ CanvasKit.FilterMode.Nearest,CanvasKit.MipmapMode.None,paint);
558
+ } finally {canvas.restore();paint.dispose();}
559
+ return true;
560
+ }
561
+
322
562
  function makeSurface(node) {
563
+ const kit=CanvasKit;
323
564
  const [width, height] = bitmapSize(node);
324
- return CanvasKit.MakeCanvas(width, height);
565
+ // Skia's fast raster pipelines use premultiplied pixels. The CanvasKit
566
+ // HTML shim allocates an unpremultiplied surface, forcing conversion while
567
+ // blending every image/pattern. Only exports need unpremultiplied RGBA.
568
+ // MakeCanvas constructs its private context synchronously through MakeSurface.
569
+ const factory=kit.MakeSurface;
570
+ let allocated=null;
571
+ kit.MakeSurface=(w,h)=>(allocated=makeRasterSurface(kit,w,h));
572
+ try { return kit.MakeCanvas(width,height); }
573
+ catch(error){allocated?.dispose();throw error;}
574
+ finally { kit.MakeSurface=factory; }
325
575
  }
326
576
 
327
577
  function nativeSurface(canvas) {
@@ -0,0 +1,36 @@
1
+ // HTML Canvas roundRect: CSS radius expansion, edge-overlap reduction and
2
+ // mirrored corners for negative dimensions. Adds to the existing path only.
3
+ export function roundRect(context,x,y,width,height,radii=0) {
4
+ x=Number(x);y=Number(y);width=Number(width);height=Number(height);
5
+ const list=radii===null||typeof radii!=='object'||radii&&!radii[Symbol.iterator]?[radii]:Array.from(radii);
6
+ if(list.length<1||list.length>4)throw new RangeError('Expected one to four radii');
7
+ let corners=list.map(value=>{
8
+ const point=typeof value==='object'&&value!==null?{x:Number(value.x??0),y:Number(value.y??0)}:{x:Number(value),y:Number(value)};
9
+ if(point.x<0||point.y<0)throw new RangeError('Radius must not be negative');return point;
10
+ });
11
+ if(![x,y,width,height,...corners.flatMap(p=>[p.x,p.y])].every(Number.isFinite))return;
12
+ const [a,b=a,c=a,d=b]=corners;corners=[a,b,c,d];
13
+ const flipped=(width<0)!==(height<0);
14
+ if(width<0){x+=width;width=-width;corners=[corners[1],corners[0],corners[3],corners[2]];}
15
+ if(height<0){y+=height;height=-height;corners=[corners[3],corners[2],corners[1],corners[0]];}
16
+ let [tl,tr,br,bl]=corners;
17
+ const fit=(extent,sum)=>sum>0?extent/sum:1;
18
+ const ratio=Math.min(1,fit(width,tl.x+tr.x),fit(width,bl.x+br.x),fit(height,tl.y+bl.y),fit(height,tr.y+br.y));
19
+ [tl,tr,br,bl]=corners.map(p=>({x:p.x*ratio,y:p.y*ratio}));
20
+ const ellipse=(cx,cy,r,start,end)=>{
21
+ if(!r.x||!r.y){context.lineTo(cx+Math.cos(end)*r.x,cy+Math.sin(end)*r.y);return;}
22
+ context.ellipse(cx,cy,r.x,r.y,0,start,end,flipped);
23
+ };
24
+ if(!flipped){
25
+ context.moveTo(x+tl.x,y);context.lineTo(x+width-tr.x,y);ellipse(x+width-tr.x,y+tr.y,tr,-Math.PI/2,0);
26
+ context.lineTo(x+width,y+height-br.y);ellipse(x+width-br.x,y+height-br.y,br,0,Math.PI/2);
27
+ context.lineTo(x+bl.x,y+height);ellipse(x+bl.x,y+height-bl.y,bl,Math.PI/2,Math.PI);
28
+ context.lineTo(x,y+tl.y);ellipse(x+tl.x,y+tl.y,tl,Math.PI,Math.PI*1.5);
29
+ }else{
30
+ context.moveTo(x+tl.x,y);ellipse(x+tl.x,y+tl.y,tl,-Math.PI/2,-Math.PI);
31
+ context.lineTo(x,y+height-bl.y);ellipse(x+bl.x,y+height-bl.y,bl,Math.PI,Math.PI/2);
32
+ context.lineTo(x+width-br.x,y+height);ellipse(x+width-br.x,y+height-br.y,br,Math.PI/2,0);
33
+ context.lineTo(x+width,y+tr.y);ellipse(x+width-tr.x,y+tr.y,tr,0,-Math.PI/2);
34
+ }
35
+ context.closePath();context.moveTo(x+tl.x,y);
36
+ }
@@ -0,0 +1,173 @@
1
+ import {animationShorthand} from '../style/animation-values.js';
2
+ import {extractAnimationKeyframes,parseTransform} from '../style/computed-style.js';
3
+
4
+ const milliseconds = value => parseFloat(value) * (String(value).endsWith('ms') ? 1 : 1000);
5
+ const properties = {width:'width',height:'height',opacity:'opacity'};
6
+
7
+ // CSS animation timeline with numeric and compatible transform interpolation. Unsupported
8
+ // visual value types keep their underlying style; lifecycle events still run.
9
+ // No DOM style attributes are changed by sampling an animation.
10
+ export function createCSSAnimations({invalidate,dispatch,now=()=>performance.now(),setTimer=setTimeout,clearTimer=clearTimeout}) {
11
+ let states=new Map(),definitions=new Map(),sources=null,environment='',timer=null;
12
+ const queued=new Set();
13
+ function emit(element,type,state,elapsed) {
14
+ const detail={animationName:state.config.name,elapsedTime:Math.max(0,elapsed)/1000,pseudoElement:''};
15
+ const task=setTimer(()=>{
16
+ queued.delete(task);
17
+ dispatch(element,type,detail);
18
+ },0);
19
+ queued.add(task);
20
+ }
21
+ function arm() {
22
+ if(timer!==null)return;
23
+ if(![...states.values()].some(list=>list.some(s=>!s.ended&&s.config.playState!=='paused')))return;
24
+ timer=setTimer(()=>{
25
+ timer=null;
26
+ for(const [element,list] of states) {
27
+ if(!element.isConnected){cancel(element,list);states.delete(element);continue;}
28
+ if(list.some(s=>!s.ended&&s.config.playState!=='paused'))invalidate(element);
29
+ }
30
+ },16);
31
+ }
32
+ function cancel(element,list) {
33
+ for(const state of list)if(state.started&&!state.ended)emit(element,'animationcancel',state,now()-state.start-state.delay);
34
+ }
35
+ return {
36
+ setSheets(sheets,viewport) {
37
+ for(const [element,list] of states)if(!element.isConnected){cancel(element,list);states.delete(element);}
38
+ const key=`${viewport.width}:${viewport.height}:${viewport.mobile}`;
39
+ if(sources===sheets&&environment===key)return;
40
+ sources=sheets;environment=key;definitions=new Map();
41
+ for(const sheet of sheets)for(const entry of extractAnimationKeyframes(sheet,viewport))definitions.set(...entry);
42
+ },
43
+ sample(element,style) {
44
+ if(style.display==='none')for(const [child,list] of states)if(child===element||element.contains?.(child)) {
45
+ cancel(child,list);states.delete(child);
46
+ }
47
+ if(!style.cssAnimations&&!style.cssAnimationLonghands&&!states.has(element))return style;
48
+ const longhands=style.cssAnimationLonghands??{};
49
+ const base=style.cssAnimations??animationShorthand('none');
50
+ const names=longhands.name??base.map(item=>item.name);
51
+ const configs=names.map((name,index)=>{
52
+ const config={...base[index%base.length],name};
53
+ for(const [key,values] of Object.entries(longhands))config[key]=values[index%values.length];
54
+ return config;
55
+ }).filter(config=>config.name!=='none'&&definitions.has(config.name)&&style.display!=='none');
56
+ const previous=states.get(element)??[],next=[];
57
+ let result=style;
58
+ const time=now();
59
+ for(let index=0;index<configs.length;index++) {
60
+ const config=configs[index],frames=definitions.get(config.name);
61
+ let state=previous[index];
62
+ // Timeline does not restart for duration/delay changes. Pause freezes
63
+ // local time; resume moves the origin instead of jumping ahead.
64
+ if(!state||state.config.name!==config.name) {
65
+ if(state)cancel(element,[state]);
66
+ state={config,start:time,started:false,ended:false,iteration:0};
67
+ }
68
+ if(state.config.playState!=='paused'&&config.playState==='paused')state.pausedAt=time;
69
+ else if(state.config.playState==='paused'&&config.playState!=='paused') {
70
+ state.start+=time-(state.pausedAt??time);state.pausedAt=null;
71
+ }
72
+ if(config.playState==='paused'&&state.pausedAt==null)state.pausedAt=time;
73
+ state.config=config;state.delay=milliseconds(config.delay)||0;
74
+ const duration=Math.max(0,milliseconds(config.duration)||0);
75
+ const count=config.iterationCount==='infinite'?Infinity:Math.max(0,Number(config.iterationCount)||0);
76
+ const total=duration===0?0:duration*count;
77
+ const elapsed=(state.pausedAt??time)-state.start-state.delay;
78
+ if(elapsed>=0&&!state.started){state.started=true;emit(element,'animationstart',state,Math.min(total,Math.max(0,-state.delay)));}
79
+ const finished=elapsed>=total;
80
+ const iteration=duration>0?Math.floor(Math.max(0,elapsed)/duration):0;
81
+ if(state.started&&!state.ended&&!finished&&iteration>state.iteration)emit(element,'animationiteration',state,iteration*duration);
82
+ state.iteration=iteration;
83
+ if(finished&&!state.ended&&state.started){state.ended=true;emit(element,'animationend',state,total);}
84
+ next.push(state);
85
+ if(elapsed<0&&!['both','backwards'].includes(config.fillMode))continue;
86
+ if(finished&&!['both','forwards'].includes(config.fillMode))continue;
87
+ let progress=elapsed<0?0:finished?(count%1||1):duration?elapsed/duration%1:1;
88
+ const cycle=finished?Math.max(0,Math.ceil(count)-1):iteration;
89
+ if(config.direction==='reverse'||config.direction==='alternate'&&cycle%2===1||config.direction==='alternate-reverse'&&cycle%2===0)progress=1-progress;
90
+
91
+ for(const [property,field] of Object.entries(properties)) {
92
+ const stops=frames.flatMap(frame=>{
93
+ const value=frame.declarations.get(property)?.value;
94
+ if(value==null)return [];
95
+ if(!/^-?(?:\d*\.)?\d+(?:px)?$/.test(value))return [];
96
+ if(property!=='opacity'&&!value.endsWith('px')&&Number(value)!==0)return [];
97
+ return [{offset:frame.offset,value:Number.parseFloat(value),timing:frame.declarations.get("animation-timing-function")?.value}];
98
+ });
99
+ if(!stops.length)continue;
100
+ if(stops[0].offset!==0&&typeof style[field]==='number')stops.unshift({offset:0,value:style[field]});
101
+ if(stops.at(-1).offset!==1&&typeof style[field]==='number')stops.push({offset:1,value:style[field]});
102
+ const left=stops.findLast(stop=>stop.offset<=progress)??stops[0];
103
+ const right=stops.find(stop=>stop.offset>=progress)??stops.at(-1);
104
+ const portion=ease(right.offset===left.offset?0:(progress-left.offset)/(right.offset-left.offset),left.timing??config.timingFunction);
105
+ if(result===style)result={...style};
106
+ result[field]=left.value+(right.value-left.value)*portion;
107
+ }
108
+ const transforms=frames.flatMap(frame=>{
109
+ const raw=frame.declarations.get('transform')?.value;
110
+ if(raw==null)return [];
111
+ const value=raw==='none'?null:parseTransform(raw,style);
112
+ return value===undefined?[]:[{offset:frame.offset,value,timing:frame.declarations.get('animation-timing-function')?.value}];
113
+ });
114
+ if(transforms.length){
115
+ if(transforms[0].offset!==0)transforms.unshift({offset:0,value:style.transform??null});
116
+ if(transforms.at(-1).offset!==1)transforms.push({offset:1,value:style.transform??null});
117
+ const left=transforms.findLast(stop=>stop.offset<=progress)??transforms[0];
118
+ const right=transforms.find(stop=>stop.offset>=progress)??transforms.at(-1);
119
+ const portion=ease(right.offset===left.offset?0:(progress-left.offset)/(right.offset-left.offset),left.timing??config.timingFunction);
120
+ const transform=interpolateTransform(left.value,right.value,portion);
121
+ if(transform!==undefined){if(result===style)result={...style};result.transform=transform;}
122
+ }
123
+ }
124
+ cancel(element,previous.slice(configs.length));
125
+ if(next.length)states.set(element,next);else states.delete(element);
126
+ arm();return result;
127
+ },
128
+ dispose() {
129
+ clearTimer(timer);timer=null;
130
+ for(const task of queued)clearTimer(task);
131
+ queued.clear();states.clear();definitions.clear();sources=null;
132
+ },
133
+ };
134
+ }
135
+
136
+ function ease(progress,name) {
137
+ if(progress<=0)return 0;
138
+ if(progress>=1)return 1;
139
+ if(name==='linear')return progress;
140
+ if(name==='step-start')return 1;
141
+ if(name==='step-end')return progress<1?0:1;
142
+ const presets={ease:[.25,.1,.25,1],'ease-in':[.42,0,1,1],'ease-out':[0,0,.58,1],'ease-in-out':[.42,0,.58,1]};
143
+ const curve=presets[name]??(/^cubic-bezier\(([^)]+)\)$/.exec(name)?.[1].split(',').map(Number));
144
+ if(!curve||curve.length!==4||!curve.every(Number.isFinite))return progress;
145
+ const [x1,y1,x2,y2]=curve;
146
+ const at=(t,a,b)=>3*(1-t)**2*t*a+3*(1-t)*t*t*b+t**3;
147
+ let low=0,high=1;
148
+ for(let n=0;n<16;n++){const middle=(low+high)/2;if(at(middle,x1,x2)<progress)low=middle;else high=middle;}
149
+ return at((low+high)/2,y1,y2);
150
+ }
151
+
152
+ // Keep percentages as symbolic lengths until layout knows the reference box.
153
+ function mixLength(a,b,t){
154
+ if(typeof a==='number'&&typeof b==='number')return a+(b-a)*t;
155
+ return {unit:'math',kind:'sum',values:[{unit:'math',kind:'scale',value:a,factor:1-t},{unit:'math',kind:'scale',value:b,factor:t}]};
156
+ }
157
+ function interpolateTransform(a,b,t){
158
+ if(t===0)return a;if(t===1)return b;
159
+ if(!a&&!b)return null;
160
+ if(!a?.affine&&!b?.affine){
161
+ return {x:mixLength(a?.x??0,b?.x??0,t),y:mixLength(a?.y??0,b?.y??0,t),
162
+ scaleX:(a?.scaleX??1)+((b?.scaleX??1)-(a?.scaleX??1))*t,
163
+ scaleY:(a?.scaleY??1)+((b?.scaleY??1)-(a?.scaleY??1))*t};
164
+ }
165
+ const identity=op=>op.kind==='translate'?{kind:op.kind,x:0,y:0}:op.kind==='scale'?{kind:op.kind,x:1,y:1}:{kind:op.kind,angle:0};
166
+ const x=a?.affine?.operations??(!a?b.affine.operations.map(identity):null);
167
+ const y=b?.affine?.operations??(!b?a.affine.operations.map(identity):null);
168
+ if(!x||!y||x.length!==y.length||x.some((op,i)=>op.kind!==y[i].kind))return undefined;
169
+ const operations=x.map((op,i)=>op.kind==='translate'||op.kind==='scale'
170
+ ?{kind:op.kind,x:mixLength(op.x,y[i].x,t),y:mixLength(op.y,y[i].y,t)}
171
+ :{kind:op.kind,angle:op.angle+(y[i].angle-op.angle)*t});
172
+ return {x:0,y:0,affine:{operations}};
173
+ }