buninu-browser 0.7.0 → 0.9.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.
- package/AGENTS.md +5 -0
- package/README.md +90 -1
- package/buninu-browser.js +1 -1
- package/frontends/wayland/README.md +35 -2
- package/frontends/wayland/client.js +8 -6
- package/frontends/wayland/clipboard.js +1 -1
- package/frontends/wayland/index.js +1 -1
- package/frontends/wayland/input.js +1 -1
- package/frontends/wayland/resource-io.js +6 -0
- package/frontends/wayland/transport-posix.js +120 -0
- package/frontends/wayland/transport-win32.js +9 -0
- package/frontends/wayland/transport.js +3 -120
- package/frontends/wayland/win32-resources.js +80 -0
- package/frontends/wayland/win32-wire.js +62 -0
- package/lib/audio/biquad.js +25 -0
- package/lib/audio/piano.js +6 -3
- package/lib/audio/playback-buffer.js +5 -5
- package/lib/audio/pulse-client.js +24 -5
- package/lib/audio/web-audio.js +570 -0
- package/lib/browser-dialog.js +126 -0
- package/lib/cdp-server.js +43 -11
- package/lib/file-picker.js +48 -0
- package/lib/headless-shell.js +34 -12
- package/lib/layout/text-layout.js +112 -19
- package/lib/media/mp4-frame-times.js +93 -0
- package/lib/paint/direct-canvas.ts +39 -0
- package/lib/paint/display-list.js +37 -12
- package/lib/paint/raster-readback.js +16 -7
- package/lib/renderer/animation-frames.js +10 -5
- package/lib/renderer/bindings.js +69 -16
- package/lib/renderer/canvas-2d.js +265 -15
- package/lib/renderer/canvas-round-rect.js +36 -0
- package/lib/renderer/css-animations.js +173 -0
- package/lib/renderer/dialog-client.js +30 -0
- package/lib/renderer/dialog-worker.js +35 -0
- package/lib/renderer/eval-environment.js +28 -0
- package/lib/renderer/file-input.js +49 -0
- package/lib/renderer/host.js +33 -4
- package/lib/renderer/html-drag.js +77 -0
- package/lib/renderer/image-loading.js +45 -2
- package/lib/renderer/main.js +4 -0
- package/lib/renderer/module-transform.js +5 -3
- package/lib/renderer/page-builtins.js +59 -0
- package/lib/renderer/page-realm.js +177 -40
- package/lib/renderer/page-renderer.js +397 -49
- package/lib/renderer/piano-input.js +3 -1
- package/lib/renderer/raster-surface.js +18 -0
- package/lib/renderer/script-rewrite.js +165 -9
- package/lib/renderer/video-presentation.js +5 -4
- package/lib/renderer/webgl/clip.js +24 -0
- package/lib/renderer/webgl/context.js +158 -0
- package/lib/renderer/webgl/cpu-fragment.js +211 -0
- package/lib/renderer/webgl/draw-cache.js +22 -0
- package/lib/renderer/webgl/pipeline.js +70 -0
- package/lib/renderer/webgl/shaders.js +58 -0
- package/lib/renderer/webgl/textures.js +43 -0
- package/lib/renderer/window-named-properties.js +46 -0
- package/lib/screenshot-cli.js +4 -7
- package/lib/style/animation-values.js +29 -0
- package/lib/style/computed-style.js +109 -13
- package/lib/style/transform.js +44 -0
- package/lib/window-size.js +10 -0
- package/package.json +1 -1
- package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
- package/patches/happy-dom/README.md +16 -0
- package/test/animation-frames.test.js +15 -0
- package/test/canvas-2d.test.js +252 -4
- package/test/canvas-frame-pacing.test.js +7 -0
- package/test/cdp-viewers.test.js +36 -0
- package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
- package/test/classic-games-patches/README.md +156 -0
- package/test/classic-games-scenarios.js +133 -0
- package/test/clipboard.test.js +16 -0
- package/test/computed-style.test.js +18 -0
- package/test/css-animation-lifecycle.test.js +28 -0
- package/test/css-animations.test.js +83 -0
- package/test/direct-canvas.test.js +39 -0
- package/test/dynamic-inline-handlers.test.js +25 -0
- package/test/eval-declarations.test.js +22 -0
- package/test/file-picker.test.js +34 -0
- package/test/fixtures/canvas-damage.js +23 -0
- package/test/fixtures/canvas-frame-pacing.js +35 -0
- package/test/fixtures/direct-canvas.js +96 -0
- package/test/fixtures/javascript-dialogs.html +8 -0
- package/test/fixtures/media-audio-lifetime.js +65 -0
- package/test/fixtures/renderer-memory.js +7 -5
- package/test/fixtures/video-black-replay.js +8 -6
- package/test/fixtures/video-slow-end.js +39 -0
- package/test/fixtures/web-audio-renderer.js +45 -0
- package/test/flex.test.js +26 -0
- package/test/grid.test.js +13 -0
- package/test/headless-shell.test.js +9 -0
- package/test/headless-window-size.test.js +28 -0
- package/test/html-rcdata.test.js +35 -0
- package/test/image-decode.test.js +18 -0
- package/test/javascript-dialog.test.js +83 -0
- package/test/manual-breakout-paddle.js +42 -0
- package/test/manual-breakout-static.js +35 -0
- package/test/manual-classic-games.js +41 -0
- package/test/manual-clumsy-bird-three-pipes.js +81 -0
- package/test/manual-clumsy-bird.js +52 -0
- package/test/manual-hextris.js +30 -0
- package/test/manual-numbered-music.js +24 -0
- package/test/manual-t-rex-runner.js +49 -0
- package/test/manual-threejs-scene.js +64 -0
- package/test/manual-webgl-benchmark.js +27 -0
- package/test/manual-webgl-cpu.html +26 -0
- package/test/media-audio-lifetime.test.js +8 -0
- package/test/mp4-frame-times.test.js +33 -0
- package/test/page-scripts.test.js +96 -1
- package/test/piano-input.test.js +15 -2
- package/test/piano.test.js +11 -2
- package/test/playback-buffer.test.js +11 -0
- package/test/pointer-drag.test.js +33 -0
- package/test/positioning.test.js +82 -0
- package/test/pulse-client.test.js +51 -2
- package/test/raster-readback.test.js +14 -0
- package/test/script-rewrite.test.js +61 -3
- package/test/video-slow-end.test.js +6 -0
- package/test/viewport-keyboard-overflow.test.js +25 -0
- package/test/wayland-win32-resources.test.js +47 -0
- package/test/wayland-win32-wire.test.js +59 -0
- package/test/web-audio-renderer.test.js +8 -0
- package/test/web-audio.test.js +264 -0
- package/test/webgl-clip.test.js +9 -0
- package/test/webgl-cpu-fragment.test.js +43 -0
- package/test/window-named-properties.test.js +41 -0
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
- package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
- package/vendor/land-renderer/LICENSE +21 -0
- package/vendor/land-renderer/README.md +14 -0
- package/vendor/land-renderer/direct-compositor.ts +443 -0
- package/vendor/land-renderer/frame.js +88 -0
- package/vendor/land-renderer/png-fast.js +85 -0
- package/vendor/land-renderer/raw-frames.js +66 -0
- package/vendor/land-renderer/render-types.ts +7 -0
- package/vendor/land-renderer/renderer.ts +134 -0
- package/vendor/land-renderer/snapshot.js +37 -0
- package/vendor/land-renderer/viewport.js +18 -0
- 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)
|
|
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 (
|
|
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
|
|
121
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|