@motion-script/web 0.1.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.
- package/README.md +47 -0
- package/dist/audio/player.d.ts +43 -0
- package/dist/audio/player.d.ts.map +1 -0
- package/dist/audio/player.js +165 -0
- package/dist/audio/player.js.map +1 -0
- package/dist/effects/bloom.d.ts +19 -0
- package/dist/effects/bloom.d.ts.map +1 -0
- package/dist/effects/bloom.js +64 -0
- package/dist/effects/bloom.js.map +1 -0
- package/dist/effects/blur.d.ts +8 -0
- package/dist/effects/blur.d.ts.map +1 -0
- package/dist/effects/blur.js +12 -0
- package/dist/effects/blur.js.map +1 -0
- package/dist/effects/bulge-pinch.d.ts +20 -0
- package/dist/effects/bulge-pinch.d.ts.map +1 -0
- package/dist/effects/bulge-pinch.js +86 -0
- package/dist/effects/bulge-pinch.js.map +1 -0
- package/dist/effects/chromatic-aberration.d.ts +19 -0
- package/dist/effects/chromatic-aberration.d.ts.map +1 -0
- package/dist/effects/chromatic-aberration.js +59 -0
- package/dist/effects/chromatic-aberration.js.map +1 -0
- package/dist/effects/effect.d.ts +32 -0
- package/dist/effects/effect.d.ts.map +1 -0
- package/dist/effects/effect.js +22 -0
- package/dist/effects/effect.js.map +1 -0
- package/dist/effects/grayscale.d.ts +12 -0
- package/dist/effects/grayscale.d.ts.map +1 -0
- package/dist/effects/grayscale.js +31 -0
- package/dist/effects/grayscale.js.map +1 -0
- package/dist/effects/index.d.ts +13 -0
- package/dist/effects/index.d.ts.map +1 -0
- package/dist/effects/index.js +13 -0
- package/dist/effects/index.js.map +1 -0
- package/dist/effects/pixelate.d.ts +23 -0
- package/dist/effects/pixelate.d.ts.map +1 -0
- package/dist/effects/pixelate.js +37 -0
- package/dist/effects/pixelate.js.map +1 -0
- package/dist/effects/registry.d.ts +17 -0
- package/dist/effects/registry.d.ts.map +1 -0
- package/dist/effects/registry.js +56 -0
- package/dist/effects/registry.js.map +1 -0
- package/dist/effects/sksl-cache.d.ts +6 -0
- package/dist/effects/sksl-cache.d.ts.map +1 -0
- package/dist/effects/sksl-cache.js +21 -0
- package/dist/effects/sksl-cache.js.map +1 -0
- package/dist/effects/sksl-layer.d.ts +30 -0
- package/dist/effects/sksl-layer.d.ts.map +1 -0
- package/dist/effects/sksl-layer.js +82 -0
- package/dist/effects/sksl-layer.js.map +1 -0
- package/dist/effects/texture.d.ts +31 -0
- package/dist/effects/texture.d.ts.map +1 -0
- package/dist/effects/texture.js +66 -0
- package/dist/effects/texture.js.map +1 -0
- package/dist/effects/vintage.d.ts +20 -0
- package/dist/effects/vintage.d.ts.map +1 -0
- package/dist/effects/vintage.js +47 -0
- package/dist/effects/vintage.js.map +1 -0
- package/dist/effects/zoom.d.ts +20 -0
- package/dist/effects/zoom.d.ts.map +1 -0
- package/dist/effects/zoom.js +65 -0
- package/dist/effects/zoom.js.map +1 -0
- package/dist/exporter.d.ts +24 -0
- package/dist/exporter.d.ts.map +1 -0
- package/dist/exporter.js +177 -0
- package/dist/exporter.js.map +1 -0
- package/dist/fills/conic-gradient.d.ts +12 -0
- package/dist/fills/conic-gradient.d.ts.map +1 -0
- package/dist/fills/conic-gradient.js +44 -0
- package/dist/fills/conic-gradient.js.map +1 -0
- package/dist/fills/filters/alpha.d.ts +9 -0
- package/dist/fills/filters/alpha.d.ts.map +1 -0
- package/dist/fills/filters/alpha.js +21 -0
- package/dist/fills/filters/alpha.js.map +1 -0
- package/dist/fills/filters/blur.d.ts +9 -0
- package/dist/fills/filters/blur.d.ts.map +1 -0
- package/dist/fills/filters/blur.js +12 -0
- package/dist/fills/filters/blur.js.map +1 -0
- package/dist/fills/filters/color-adjustment.d.ts +14 -0
- package/dist/fills/filters/color-adjustment.d.ts.map +1 -0
- package/dist/fills/filters/color-adjustment.js +147 -0
- package/dist/fills/filters/color-adjustment.js.map +1 -0
- package/dist/fills/filters/color-matrix.d.ts +9 -0
- package/dist/fills/filters/color-matrix.d.ts.map +1 -0
- package/dist/fills/filters/color-matrix.js +14 -0
- package/dist/fills/filters/color-matrix.js.map +1 -0
- package/dist/fills/filters/curves.d.ts +9 -0
- package/dist/fills/filters/curves.d.ts.map +1 -0
- package/dist/fills/filters/curves.js +89 -0
- package/dist/fills/filters/curves.js.map +1 -0
- package/dist/fills/filters/exposure.d.ts +9 -0
- package/dist/fills/filters/exposure.d.ts.map +1 -0
- package/dist/fills/filters/exposure.js +22 -0
- package/dist/fills/filters/exposure.js.map +1 -0
- package/dist/fills/filters/filter.d.ts +17 -0
- package/dist/fills/filters/filter.d.ts.map +1 -0
- package/dist/fills/filters/filter.js +16 -0
- package/dist/fills/filters/filter.js.map +1 -0
- package/dist/fills/filters/grayscale.d.ts +9 -0
- package/dist/fills/filters/grayscale.d.ts.map +1 -0
- package/dist/fills/filters/grayscale.js +25 -0
- package/dist/fills/filters/grayscale.js.map +1 -0
- package/dist/fills/filters/registry.d.ts +16 -0
- package/dist/fills/filters/registry.d.ts.map +1 -0
- package/dist/fills/filters/registry.js +50 -0
- package/dist/fills/filters/registry.js.map +1 -0
- package/dist/fills/gradient-cache.d.ts +29 -0
- package/dist/fills/gradient-cache.d.ts.map +1 -0
- package/dist/fills/gradient-cache.js +57 -0
- package/dist/fills/gradient-cache.js.map +1 -0
- package/dist/fills/handler.d.ts +49 -0
- package/dist/fills/handler.d.ts.map +1 -0
- package/dist/fills/handler.js +172 -0
- package/dist/fills/handler.js.map +1 -0
- package/dist/fills/image.d.ts +34 -0
- package/dist/fills/image.d.ts.map +1 -0
- package/dist/fills/image.js +91 -0
- package/dist/fills/image.js.map +1 -0
- package/dist/fills/linear-gradient.d.ts +12 -0
- package/dist/fills/linear-gradient.d.ts.map +1 -0
- package/dist/fills/linear-gradient.js +48 -0
- package/dist/fills/linear-gradient.js.map +1 -0
- package/dist/fills/noise.d.ts +11 -0
- package/dist/fills/noise.d.ts.map +1 -0
- package/dist/fills/noise.js +82 -0
- package/dist/fills/noise.js.map +1 -0
- package/dist/fills/radial-gradient.d.ts +9 -0
- package/dist/fills/radial-gradient.d.ts.map +1 -0
- package/dist/fills/radial-gradient.js +43 -0
- package/dist/fills/radial-gradient.js.map +1 -0
- package/dist/fills/registry.d.ts +10 -0
- package/dist/fills/registry.d.ts.map +1 -0
- package/dist/fills/registry.js +34 -0
- package/dist/fills/registry.js.map +1 -0
- package/dist/fills/renderer.d.ts +24 -0
- package/dist/fills/renderer.d.ts.map +1 -0
- package/dist/fills/renderer.js +5 -0
- package/dist/fills/renderer.js.map +1 -0
- package/dist/fills/solid.d.ts +7 -0
- package/dist/fills/solid.d.ts.map +1 -0
- package/dist/fills/solid.js +13 -0
- package/dist/fills/solid.js.map +1 -0
- package/dist/fills/stripe.d.ts +7 -0
- package/dist/fills/stripe.d.ts.map +1 -0
- package/dist/fills/stripe.js +85 -0
- package/dist/fills/stripe.js.map +1 -0
- package/dist/fills/video.d.ts +6 -0
- package/dist/fills/video.d.ts.map +1 -0
- package/dist/fills/video.js +14 -0
- package/dist/fills/video.js.map +1 -0
- package/dist/font-style.d.ts +23 -0
- package/dist/font-style.d.ts.map +1 -0
- package/dist/font-style.js +34 -0
- package/dist/font-style.js.map +1 -0
- package/dist/getter.d.ts +9 -0
- package/dist/getter.d.ts.map +1 -0
- package/dist/getter.js +26 -0
- package/dist/getter.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/master-clock.d.ts +42 -0
- package/dist/master-clock.d.ts.map +1 -0
- package/dist/master-clock.js +134 -0
- package/dist/master-clock.js.map +1 -0
- package/dist/measure-scope.d.ts +14 -0
- package/dist/measure-scope.d.ts.map +1 -0
- package/dist/measure-scope.js +29 -0
- package/dist/measure-scope.js.map +1 -0
- package/dist/render-context.d.ts +107 -0
- package/dist/render-context.d.ts.map +1 -0
- package/dist/render-context.js +940 -0
- package/dist/render-context.js.map +1 -0
- package/dist/shapes/alpha-contour.d.ts +27 -0
- package/dist/shapes/alpha-contour.d.ts.map +1 -0
- package/dist/shapes/alpha-contour.js +330 -0
- package/dist/shapes/alpha-contour.js.map +1 -0
- package/dist/shapes/base.d.ts +46 -0
- package/dist/shapes/base.d.ts.map +1 -0
- package/dist/shapes/base.js +95 -0
- package/dist/shapes/base.js.map +1 -0
- package/dist/shapes/boolean.d.ts +28 -0
- package/dist/shapes/boolean.d.ts.map +1 -0
- package/dist/shapes/boolean.js +90 -0
- package/dist/shapes/boolean.js.map +1 -0
- package/dist/shapes/ellipse.d.ts +32 -0
- package/dist/shapes/ellipse.d.ts.map +1 -0
- package/dist/shapes/ellipse.js +50 -0
- package/dist/shapes/ellipse.js.map +1 -0
- package/dist/shapes/image.d.ts +66 -0
- package/dist/shapes/image.d.ts.map +1 -0
- package/dist/shapes/image.js +214 -0
- package/dist/shapes/image.js.map +1 -0
- package/dist/shapes/index.d.ts +67 -0
- package/dist/shapes/index.d.ts.map +1 -0
- package/dist/shapes/index.js +297 -0
- package/dist/shapes/index.js.map +1 -0
- package/dist/shapes/line.d.ts +25 -0
- package/dist/shapes/line.d.ts.map +1 -0
- package/dist/shapes/line.js +87 -0
- package/dist/shapes/line.js.map +1 -0
- package/dist/shapes/mask.d.ts +28 -0
- package/dist/shapes/mask.d.ts.map +1 -0
- package/dist/shapes/mask.js +106 -0
- package/dist/shapes/mask.js.map +1 -0
- package/dist/shapes/paragraph-layout.d.ts +64 -0
- package/dist/shapes/paragraph-layout.d.ts.map +1 -0
- package/dist/shapes/paragraph-layout.js +156 -0
- package/dist/shapes/paragraph-layout.js.map +1 -0
- package/dist/shapes/path.d.ts +29 -0
- package/dist/shapes/path.d.ts.map +1 -0
- package/dist/shapes/path.js +71 -0
- package/dist/shapes/path.js.map +1 -0
- package/dist/shapes/polygon.d.ts +33 -0
- package/dist/shapes/polygon.d.ts.map +1 -0
- package/dist/shapes/polygon.js +86 -0
- package/dist/shapes/polygon.js.map +1 -0
- package/dist/shapes/polygram.d.ts +34 -0
- package/dist/shapes/polygram.d.ts.map +1 -0
- package/dist/shapes/polygram.js +90 -0
- package/dist/shapes/polygram.js.map +1 -0
- package/dist/shapes/rect.d.ts +41 -0
- package/dist/shapes/rect.d.ts.map +1 -0
- package/dist/shapes/rect.js +111 -0
- package/dist/shapes/rect.js.map +1 -0
- package/dist/shapes/richtext.d.ts +28 -0
- package/dist/shapes/richtext.d.ts.map +1 -0
- package/dist/shapes/richtext.js +32 -0
- package/dist/shapes/richtext.js.map +1 -0
- package/dist/shapes/shape-handler.d.ts +79 -0
- package/dist/shapes/shape-handler.d.ts.map +1 -0
- package/dist/shapes/shape-handler.js +304 -0
- package/dist/shapes/shape-handler.js.map +1 -0
- package/dist/shapes/text.d.ts +13 -0
- package/dist/shapes/text.d.ts.map +1 -0
- package/dist/shapes/text.js +67 -0
- package/dist/shapes/text.js.map +1 -0
- package/dist/shapes/trim.d.ts +10 -0
- package/dist/shapes/trim.d.ts.map +1 -0
- package/dist/shapes/trim.js +49 -0
- package/dist/shapes/trim.js.map +1 -0
- package/dist/storage-adapter.d.ts +56 -0
- package/dist/storage-adapter.d.ts.map +1 -0
- package/dist/storage-adapter.js +188 -0
- package/dist/storage-adapter.js.map +1 -0
- package/dist/stroke/index.d.ts +34 -0
- package/dist/stroke/index.d.ts.map +1 -0
- package/dist/stroke/index.js +360 -0
- package/dist/stroke/index.js.map +1 -0
- package/dist/stroke/stroke-handler.d.ts +45 -0
- package/dist/stroke/stroke-handler.d.ts.map +1 -0
- package/dist/stroke/stroke-handler.js +371 -0
- package/dist/stroke/stroke-handler.js.map +1 -0
- package/dist/video/extract.d.ts +54 -0
- package/dist/video/extract.d.ts.map +1 -0
- package/dist/video/extract.js +192 -0
- package/dist/video/extract.js.map +1 -0
- package/dist/video/extract.worker.d.ts +50 -0
- package/dist/video/extract.worker.d.ts.map +1 -0
- package/dist/video/extract.worker.js +224 -0
- package/dist/video/extract.worker.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
import { PathBuilder, RenderContext, withImageDescriptor, withRichTextDescriptor, resolveFillArray, resolveStrokeArray, resolveShadowArray, } from "@motion-script/core";
|
|
2
|
+
import { layoutRichText } from "./shapes/richtext";
|
|
3
|
+
import { drawShapedRun, layoutParagraph } from "./shapes/paragraph-layout";
|
|
4
|
+
import { ImageNodeRenderer } from "./shapes/image";
|
|
5
|
+
import { RectShape } from "./shapes/rect";
|
|
6
|
+
import { EllipseShape } from "./shapes/ellipse";
|
|
7
|
+
import { PolygonShape } from "./shapes/polygon";
|
|
8
|
+
import { PolygramShape } from "./shapes/polygram";
|
|
9
|
+
import { PathShape } from "./shapes/path";
|
|
10
|
+
import { LineShape } from "./shapes/line";
|
|
11
|
+
import { CanvasKitEffectRegistry } from "./effects/registry";
|
|
12
|
+
import { makeBulgePinchShader, disposeBulgePinch } from "./effects/bulge-pinch";
|
|
13
|
+
import { makeZoomShader, disposeZoom } from "./effects/zoom";
|
|
14
|
+
import { getOrCompileSkSL, disposeSkSLCache } from "./effects/sksl-cache";
|
|
15
|
+
import { StrokeHandler } from "./stroke/stroke-handler";
|
|
16
|
+
import { ShapeHandler } from "./shapes/shape-handler";
|
|
17
|
+
import { FillHandler } from "./fills/handler";
|
|
18
|
+
/**
|
|
19
|
+
* CanvasKit/Skia implementation of {@link RenderContext} — the main render
|
|
20
|
+
* loop driving a mounted `<canvas>` (or an offscreen one during export).
|
|
21
|
+
* Owns the WebGL surface and the per-frame draw stack (transforms, clips,
|
|
22
|
+
* masks, camera, backdrop effects); delegates shape/fill/stroke painting to
|
|
23
|
+
* the handlers built in {@link buildHandlers}. All async asset work happens
|
|
24
|
+
* up front in {@link WebStorageAdapter} so render() stays synchronous per frame.
|
|
25
|
+
*/
|
|
26
|
+
export class WebRenderContext extends RenderContext {
|
|
27
|
+
currentCanvas;
|
|
28
|
+
canvasKit;
|
|
29
|
+
canvasElement;
|
|
30
|
+
surface;
|
|
31
|
+
paint;
|
|
32
|
+
layerPaint;
|
|
33
|
+
mounted = false;
|
|
34
|
+
isRendering = false;
|
|
35
|
+
// Tracks extra saveLayer() calls pushed by transform() for each begin()/end() pair.
|
|
36
|
+
effectLayerStack = [];
|
|
37
|
+
clipRestoreStack = [];
|
|
38
|
+
fillHandler;
|
|
39
|
+
strokeHandler;
|
|
40
|
+
shapeHandler;
|
|
41
|
+
imageRenderer;
|
|
42
|
+
// Per-mask-scope deferred paint calls (filled by stroke/fill when apply filtering is active).
|
|
43
|
+
// Each entry on the stack corresponds to one active mask scope.
|
|
44
|
+
deferredPaintsStack = [];
|
|
45
|
+
// Pending image state collected by `image()` until paint methods are called.
|
|
46
|
+
pendingImage = null;
|
|
47
|
+
pendingImageShadows = [];
|
|
48
|
+
pendingImageFills = [];
|
|
49
|
+
pendingImageStrokes = [];
|
|
50
|
+
// Initialized in init() once CanvasKit is loaded.
|
|
51
|
+
storageAdapter;
|
|
52
|
+
constructor(canvasKit, storageAdapter) {
|
|
53
|
+
super();
|
|
54
|
+
this.canvasKit = canvasKit;
|
|
55
|
+
this.storageAdapter = storageAdapter;
|
|
56
|
+
this.paint = new this.canvasKit.Paint();
|
|
57
|
+
this.paint.setAntiAlias(true);
|
|
58
|
+
this.layerPaint = new this.canvasKit.Paint();
|
|
59
|
+
this.buildHandlers();
|
|
60
|
+
}
|
|
61
|
+
measureText(text, fontSize, fontFamily, fontWeight = 400, letterSpacing = 0, fontStyle = 'normal') {
|
|
62
|
+
if (text.length === 0)
|
|
63
|
+
return 0;
|
|
64
|
+
const fontMgr = this.storageAdapter.getFontMgr();
|
|
65
|
+
const layout = layoutParagraph(this.canvasKit, fontMgr, [{ text, fontFamily, fontSize, fontWeight, letterSpacing, fontStyle }], { align: 'center', lineHeight: 1, maxWidth: Infinity, originX: 0, originY: 0 });
|
|
66
|
+
const w = layout.width;
|
|
67
|
+
for (const f of layout.fonts)
|
|
68
|
+
f.delete();
|
|
69
|
+
return w;
|
|
70
|
+
}
|
|
71
|
+
buildHandlers() {
|
|
72
|
+
const getCanvas = () => this.currentCanvas;
|
|
73
|
+
const getPaint = () => this.paint;
|
|
74
|
+
this.shapeHandler = new ShapeHandler(this.canvasKit, getCanvas, getPaint, this.storageAdapter.getFontMgr());
|
|
75
|
+
this.fillHandler = new FillHandler(this.canvasKit, getPaint, getCanvas, () => this.shapeHandler.getShapeBounds(), (space) => this.spaceRect(space), this.storageAdapter);
|
|
76
|
+
this.strokeHandler = new StrokeHandler(this.canvasKit, getCanvas, getPaint, this.fillHandler);
|
|
77
|
+
this.imageRenderer = new ImageNodeRenderer(this.canvasKit, getCanvas, getPaint, this.storageAdapter, this.fillHandler, this.shapeHandler, this.strokeHandler);
|
|
78
|
+
}
|
|
79
|
+
flushPendingImage() {
|
|
80
|
+
if (!this.pendingImage)
|
|
81
|
+
return;
|
|
82
|
+
const state = withImageDescriptor(this.pendingImage);
|
|
83
|
+
this.imageRenderer.draw(state, this.pendingImageShadows, this.pendingImageFills, this.pendingImageStrokes);
|
|
84
|
+
this.pendingImage = null;
|
|
85
|
+
this.pendingImageShadows = [];
|
|
86
|
+
this.pendingImageFills = [];
|
|
87
|
+
this.pendingImageStrokes = [];
|
|
88
|
+
}
|
|
89
|
+
pixelRatio = 1;
|
|
90
|
+
renderPass(callback) {
|
|
91
|
+
this.currentCanvas = this.surface.getCanvas();
|
|
92
|
+
this.currentCanvas.clear(this.canvasKit.BLACK);
|
|
93
|
+
this.currentCanvas.save();
|
|
94
|
+
const logicalW = this.surface.width() / this.pixelRatio;
|
|
95
|
+
const logicalH = this.surface.height() / this.pixelRatio;
|
|
96
|
+
this.currentCanvas.scale(this.pixelRatio, this.pixelRatio);
|
|
97
|
+
this.currentCanvas.translate(logicalW / 2, logicalH / 2);
|
|
98
|
+
this.isRendering = true;
|
|
99
|
+
callback();
|
|
100
|
+
this.isRendering = false;
|
|
101
|
+
this.currentCanvas.restore();
|
|
102
|
+
this.surface.flush();
|
|
103
|
+
}
|
|
104
|
+
begin(id, rects) {
|
|
105
|
+
super.begin(id, rects);
|
|
106
|
+
this.shapeHandler.beginNode(id);
|
|
107
|
+
this.shapeHandler.reset();
|
|
108
|
+
if (!this.currentCanvas) {
|
|
109
|
+
throw new Error("begin() must be called within the draw() method.");
|
|
110
|
+
}
|
|
111
|
+
this.effectLayerStack.push(0);
|
|
112
|
+
this.currentCanvas.save();
|
|
113
|
+
}
|
|
114
|
+
end() {
|
|
115
|
+
if (!this.currentCanvas) {
|
|
116
|
+
throw new Error("end() must be called within the draw() method.");
|
|
117
|
+
}
|
|
118
|
+
this.flushPendingImage();
|
|
119
|
+
const extraLayers = this.effectLayerStack.pop() ?? 0;
|
|
120
|
+
for (let i = 0; i < extraLayers; i++) {
|
|
121
|
+
this.currentCanvas.restore();
|
|
122
|
+
}
|
|
123
|
+
this.currentCanvas.restore();
|
|
124
|
+
super.end();
|
|
125
|
+
}
|
|
126
|
+
dispose() {
|
|
127
|
+
this.fillHandler?.dispose();
|
|
128
|
+
this.shapeHandler?.dispose();
|
|
129
|
+
this.storageAdapter.dispose();
|
|
130
|
+
if (this.surface) {
|
|
131
|
+
this.surface.dispose();
|
|
132
|
+
}
|
|
133
|
+
// Intentionally do NOT call loseContext() on the canvas — the canvas
|
|
134
|
+
// element survives this component (HMR/StrictMode remount it), and a
|
|
135
|
+
// fresh CanvasKit surface needs a live WebGL context to attach to.
|
|
136
|
+
this.mounted = false;
|
|
137
|
+
this.canvasKit = undefined;
|
|
138
|
+
this.paint?.delete();
|
|
139
|
+
this.paint = undefined;
|
|
140
|
+
this.layerPaint?.delete();
|
|
141
|
+
this.layerPaint = undefined;
|
|
142
|
+
this.currentCanvas = undefined;
|
|
143
|
+
this.effectLayerStack.length = 0;
|
|
144
|
+
this.clipRestoreStack.length = 0;
|
|
145
|
+
this.deferredPaintsStack.length = 0;
|
|
146
|
+
this.backgroundBlurStack.length = 0;
|
|
147
|
+
this.backgroundDistortionStack.length = 0;
|
|
148
|
+
this.backdropSkSLStack.length = 0;
|
|
149
|
+
disposeBulgePinch();
|
|
150
|
+
disposeZoom();
|
|
151
|
+
disposeSkSLCache();
|
|
152
|
+
super.dispose();
|
|
153
|
+
}
|
|
154
|
+
/** Runs one synchronous draw pass (`callback`) against the mounted surface. */
|
|
155
|
+
async render(callback) {
|
|
156
|
+
await this.renderPass(callback);
|
|
157
|
+
}
|
|
158
|
+
/** Attaches a WebGL CanvasKit surface to `canvas`, remounting if already mounted (HMR/StrictMode). */
|
|
159
|
+
mount(canvas) {
|
|
160
|
+
if (this.mounted)
|
|
161
|
+
this.unmount();
|
|
162
|
+
this.canvasElement = canvas;
|
|
163
|
+
const surface = this.canvasKit.MakeWebGLCanvasSurface(canvas);
|
|
164
|
+
if (!surface)
|
|
165
|
+
throw new Error("Failed to create CanvasKit surface");
|
|
166
|
+
this.surface = surface;
|
|
167
|
+
this.mounted = true;
|
|
168
|
+
}
|
|
169
|
+
unmount() {
|
|
170
|
+
if (this.mounted) {
|
|
171
|
+
this.surface.dispose();
|
|
172
|
+
this.mounted = false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Snapshots the current surface and encodes it as a PNG data URL via the browser's canvas (CanvasKit ships no wasm encoders). */
|
|
176
|
+
screenshot() {
|
|
177
|
+
if (!this.mounted) {
|
|
178
|
+
console.warn("screenshot() must be called after mount().");
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
this.surface.flush();
|
|
182
|
+
const image = this.surface.makeImageSnapshot();
|
|
183
|
+
if (!image)
|
|
184
|
+
return undefined;
|
|
185
|
+
const ck = this.canvasKit;
|
|
186
|
+
const w = image.width();
|
|
187
|
+
const h = image.height();
|
|
188
|
+
// This CanvasKit build ships no wasm image encoders (encoding is handled
|
|
189
|
+
// by mediabunny / the browser). Read straight to unpremultiplied RGBA so
|
|
190
|
+
// it maps 1:1 onto ImageData, then let the browser do the PNG encode.
|
|
191
|
+
const pixels = image.readPixels(0, 0, {
|
|
192
|
+
width: w,
|
|
193
|
+
height: h,
|
|
194
|
+
colorType: ck.ColorType.RGBA_8888,
|
|
195
|
+
alphaType: ck.AlphaType.Unpremul,
|
|
196
|
+
colorSpace: ck.ColorSpace.SRGB,
|
|
197
|
+
});
|
|
198
|
+
image.delete();
|
|
199
|
+
if (!pixels)
|
|
200
|
+
return undefined;
|
|
201
|
+
const canvas = document.createElement("canvas");
|
|
202
|
+
canvas.width = w;
|
|
203
|
+
canvas.height = h;
|
|
204
|
+
const ctx = canvas.getContext("2d");
|
|
205
|
+
if (!ctx)
|
|
206
|
+
return undefined;
|
|
207
|
+
// Copy out of the wasm heap before handing the buffer to ImageData.
|
|
208
|
+
const imageData = new ImageData(new Uint8ClampedArray(pixels), w, h);
|
|
209
|
+
ctx.putImageData(imageData, 0, 0);
|
|
210
|
+
// Returns a data: URL (was a blob: URL before); both work as an <img> src
|
|
211
|
+
// and a data URL needs no revoke.
|
|
212
|
+
return canvas.toDataURL("image/png");
|
|
213
|
+
}
|
|
214
|
+
/** Wraps the just-flushed canvas as a `VideoFrame` for the export pipeline (mediabunny's `CanvasSource`). */
|
|
215
|
+
captureVideoFrame(timestampUs, durationUs) {
|
|
216
|
+
if (!this.mounted) {
|
|
217
|
+
console.warn("captureVideoFrame() must be called after mount().");
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
this.surface.flush();
|
|
221
|
+
return new VideoFrame(this.canvasElement, {
|
|
222
|
+
timestamp: timestampUs,
|
|
223
|
+
duration: durationUs,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
// ─── Draw commands ───────────────────────────────────────────────────────
|
|
227
|
+
/** Applies the node's transform to the canvas and, when opacity < 1 or effects are present, pushes a `saveLayer` tracked in {@link effectLayerStack} for `end()` to unwind. */
|
|
228
|
+
transform(state) {
|
|
229
|
+
if (!this.isRendering) {
|
|
230
|
+
console.warn("transform() must be called within the draw() method.");
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
233
|
+
const x = state.x ?? 0;
|
|
234
|
+
const y = state.y ?? 0;
|
|
235
|
+
const width = state.width ?? 0;
|
|
236
|
+
const height = state.height ?? 0;
|
|
237
|
+
const opacity = state.opacity ?? 1;
|
|
238
|
+
const rotate = state.rotation ?? 0;
|
|
239
|
+
const scale = state.scale ?? 1;
|
|
240
|
+
const effects = state.effects ?? [];
|
|
241
|
+
const pivot = state.pivot ?? { x: 0, y: 0 };
|
|
242
|
+
const pivotX = pivot.x * (width / 2);
|
|
243
|
+
const pivotY = -pivot.y * (height / 2);
|
|
244
|
+
this.currentCanvas.translate(x + pivotX, y + pivotY);
|
|
245
|
+
this.currentCanvas.rotate(rotate, 0, 0);
|
|
246
|
+
this.currentCanvas.scale(scale, scale);
|
|
247
|
+
this.currentCanvas.translate(-pivotX, -pivotY);
|
|
248
|
+
let effectFilter = null;
|
|
249
|
+
if (effects.length > 0) {
|
|
250
|
+
const w = this.surface.width();
|
|
251
|
+
const h = this.surface.height();
|
|
252
|
+
effectFilter = CanvasKitEffectRegistry.composeFilters(effects, this.canvasKit, w, h);
|
|
253
|
+
}
|
|
254
|
+
const needsLayer = opacity < 1 || effectFilter != null;
|
|
255
|
+
if (needsLayer) {
|
|
256
|
+
this.layerPaint.setAlphaf(opacity < 1 ? opacity : 1);
|
|
257
|
+
this.layerPaint.setImageFilter(effectFilter ?? null);
|
|
258
|
+
this.currentCanvas.saveLayer(this.layerPaint);
|
|
259
|
+
this.layerPaint.setAlphaf(1);
|
|
260
|
+
this.layerPaint.setImageFilter(null);
|
|
261
|
+
this.effectLayerStack[this.effectLayerStack.length - 1]++;
|
|
262
|
+
}
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
// Resolve the reference rect for a fill `space`, in the current node's local
|
|
266
|
+
// space. `parent` is supplied by the node via begin(); `view` is the render
|
|
267
|
+
// viewport mapped from device space through the inverse current matrix.
|
|
268
|
+
spaceRect(space) {
|
|
269
|
+
if (space === "parent") {
|
|
270
|
+
return this.currentSpaceRects().parent ?? null;
|
|
271
|
+
}
|
|
272
|
+
if (space === "view") {
|
|
273
|
+
const m = this.currentCanvas?.getTotalMatrix();
|
|
274
|
+
if (!m)
|
|
275
|
+
return null;
|
|
276
|
+
const inv = this.canvasKit.Matrix.invert(m);
|
|
277
|
+
if (!inv)
|
|
278
|
+
return null;
|
|
279
|
+
// Surface corners in device px → local space.
|
|
280
|
+
const w = this.surface.width();
|
|
281
|
+
const h = this.surface.height();
|
|
282
|
+
const tl = this.canvasKit.Matrix.mapPoints(inv, [0, 0]);
|
|
283
|
+
const br = this.canvasKit.Matrix.mapPoints(inv, [w, h]);
|
|
284
|
+
return {
|
|
285
|
+
left: Math.min(tl[0], br[0]),
|
|
286
|
+
top: Math.min(tl[1], br[1]),
|
|
287
|
+
right: Math.max(tl[0], br[0]),
|
|
288
|
+
bottom: Math.max(tl[1], br[1]),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
rect(state) {
|
|
294
|
+
if (!this.isRendering) {
|
|
295
|
+
console.warn("fillRect must be called within the draw() method.");
|
|
296
|
+
return this;
|
|
297
|
+
}
|
|
298
|
+
this.flushPendingImage();
|
|
299
|
+
if (this.shapeHandler.paintApplied)
|
|
300
|
+
this.shapeHandler.reset();
|
|
301
|
+
this.shapeHandler.rect(state);
|
|
302
|
+
if (this.shapeHandler.paintApplied)
|
|
303
|
+
this.shapeHandler.reset();
|
|
304
|
+
return this;
|
|
305
|
+
}
|
|
306
|
+
ellipse(state) {
|
|
307
|
+
if (!this.isRendering) {
|
|
308
|
+
console.warn("ellipse must be called within the draw() method.");
|
|
309
|
+
return this;
|
|
310
|
+
}
|
|
311
|
+
this.flushPendingImage();
|
|
312
|
+
if (this.shapeHandler.paintApplied)
|
|
313
|
+
this.shapeHandler.reset();
|
|
314
|
+
this.shapeHandler.ellipse(state);
|
|
315
|
+
return this;
|
|
316
|
+
}
|
|
317
|
+
path(state) {
|
|
318
|
+
if (!this.currentCanvas) {
|
|
319
|
+
console.warn("path() must be called within the draw() method.");
|
|
320
|
+
return this;
|
|
321
|
+
}
|
|
322
|
+
this.flushPendingImage();
|
|
323
|
+
if (this.shapeHandler.paintApplied)
|
|
324
|
+
this.shapeHandler.reset();
|
|
325
|
+
this.shapeHandler.path(state instanceof PathBuilder ? state.toPathState() : state);
|
|
326
|
+
return this;
|
|
327
|
+
}
|
|
328
|
+
line(state) {
|
|
329
|
+
if (!this.currentCanvas) {
|
|
330
|
+
console.warn("line() must be called within the draw() method.");
|
|
331
|
+
return this;
|
|
332
|
+
}
|
|
333
|
+
this.flushPendingImage();
|
|
334
|
+
if (this.shapeHandler.paintApplied)
|
|
335
|
+
this.shapeHandler.reset();
|
|
336
|
+
this.shapeHandler.line(state);
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
polygon(state) {
|
|
340
|
+
if (!this.isRendering) {
|
|
341
|
+
console.warn("polygon() must be called within the draw() method.");
|
|
342
|
+
return this;
|
|
343
|
+
}
|
|
344
|
+
this.flushPendingImage();
|
|
345
|
+
if (this.shapeHandler.paintApplied)
|
|
346
|
+
this.shapeHandler.reset();
|
|
347
|
+
this.shapeHandler.polygon(state);
|
|
348
|
+
return this;
|
|
349
|
+
}
|
|
350
|
+
polygram(state) {
|
|
351
|
+
if (!this.isRendering) {
|
|
352
|
+
console.warn("polygram() must be called within the draw() method.");
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
this.flushPendingImage();
|
|
356
|
+
if (this.shapeHandler.paintApplied)
|
|
357
|
+
this.shapeHandler.reset();
|
|
358
|
+
this.shapeHandler.polygram(state);
|
|
359
|
+
return this;
|
|
360
|
+
}
|
|
361
|
+
text(state) {
|
|
362
|
+
if (!this.isRendering) {
|
|
363
|
+
console.warn("text() must be called within the draw() method.");
|
|
364
|
+
return this;
|
|
365
|
+
}
|
|
366
|
+
this.flushPendingImage();
|
|
367
|
+
if (this.shapeHandler.paintApplied)
|
|
368
|
+
this.shapeHandler.reset();
|
|
369
|
+
this.shapeHandler.text(state);
|
|
370
|
+
return this;
|
|
371
|
+
}
|
|
372
|
+
/** Lays out spans/runs and paints each run's fill/stroke immediately (rich text carries per-span paint, bypassing the usual `.fill()/.stroke()` chain). */
|
|
373
|
+
richText(state) {
|
|
374
|
+
if (!this.isRendering) {
|
|
375
|
+
console.warn("richText() must be called within the draw() method.");
|
|
376
|
+
return this;
|
|
377
|
+
}
|
|
378
|
+
this.flushPendingImage();
|
|
379
|
+
if (this.shapeHandler.paintApplied)
|
|
380
|
+
this.shapeHandler.reset();
|
|
381
|
+
const fullState = withRichTextDescriptor(state);
|
|
382
|
+
const layout = layoutRichText(this.canvasKit, this.storageAdapter.getFontMgr(), fullState);
|
|
383
|
+
// Spans carry their own resolved fills/strokes, so we draw eagerly
|
|
384
|
+
// here rather than going through the .fill()/.stroke() chain. Push
|
|
385
|
+
// the overall bounds so any per-run gradient resolves against the
|
|
386
|
+
// whole rich-text box, not just the run.
|
|
387
|
+
this.shapeHandler.pushBounds(layout.bounds);
|
|
388
|
+
try {
|
|
389
|
+
for (const run of layout.runs) {
|
|
390
|
+
if (run.glyphs.length === 0)
|
|
391
|
+
continue;
|
|
392
|
+
const shape = {
|
|
393
|
+
isText: true,
|
|
394
|
+
draw: (paint) => {
|
|
395
|
+
drawShapedRun(this.currentCanvas, run, paint);
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
if (run.span.fill.length > 0) {
|
|
399
|
+
this.fillHandler.applyFills(run.span.fill, [shape]);
|
|
400
|
+
}
|
|
401
|
+
if (run.span.stroke.length > 0) {
|
|
402
|
+
this.strokeHandler.applyStrokes(run.span.stroke, [shape]);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
this.shapeHandler.popBounds();
|
|
408
|
+
for (const font of layout.fonts)
|
|
409
|
+
font.delete();
|
|
410
|
+
}
|
|
411
|
+
return this;
|
|
412
|
+
}
|
|
413
|
+
/** Defers drawing until a chained `.fill()/.stroke()/.shadow()` or the next shape call flushes via {@link flushPendingImage} — lets images join the same paint-context chaining as other shapes. */
|
|
414
|
+
image(state) {
|
|
415
|
+
if (!this.isRendering) {
|
|
416
|
+
console.warn("image() must be called within the draw() method.");
|
|
417
|
+
return this.imagePaintCtx;
|
|
418
|
+
}
|
|
419
|
+
this.flushPendingImage();
|
|
420
|
+
if (this.shapeHandler.paintApplied)
|
|
421
|
+
this.shapeHandler.reset();
|
|
422
|
+
this.pendingImage = state;
|
|
423
|
+
this.pendingImageShadows = [];
|
|
424
|
+
this.pendingImageFills = [];
|
|
425
|
+
this.pendingImageStrokes = [];
|
|
426
|
+
return this.imagePaintCtx;
|
|
427
|
+
}
|
|
428
|
+
/** @internal */ _appendImageShadows(s) {
|
|
429
|
+
if (this.pendingImage && s.length > 0)
|
|
430
|
+
this.pendingImageShadows.push(...s);
|
|
431
|
+
}
|
|
432
|
+
/** @internal */ _appendImageFills(f) {
|
|
433
|
+
if (this.pendingImage && f.length > 0)
|
|
434
|
+
this.pendingImageFills.push(...f);
|
|
435
|
+
}
|
|
436
|
+
/** @internal */ _appendImageStrokes(s) {
|
|
437
|
+
if (this.pendingImage && s.length > 0)
|
|
438
|
+
this.pendingImageStrokes.push(...s);
|
|
439
|
+
}
|
|
440
|
+
imagePaintCtx = new WebImagePaintContext(this);
|
|
441
|
+
fill(fills) {
|
|
442
|
+
const resolved = resolveFillArray(fills);
|
|
443
|
+
if (resolved.length === 0)
|
|
444
|
+
return this;
|
|
445
|
+
if (!this.isRendering) {
|
|
446
|
+
console.warn("fill() must be called within the draw() method.");
|
|
447
|
+
return this;
|
|
448
|
+
}
|
|
449
|
+
if (this.shapeHandler.isCollectingPaths()) {
|
|
450
|
+
this.shapeHandler.paintApplied = true;
|
|
451
|
+
return this;
|
|
452
|
+
}
|
|
453
|
+
const maskApply = this.shapeHandler.getMaskApply();
|
|
454
|
+
if (maskApply !== null && !maskApply.has('fill')) {
|
|
455
|
+
const top = this.deferredPaintsStack[this.deferredPaintsStack.length - 1];
|
|
456
|
+
if (top) {
|
|
457
|
+
top.push({ kind: 'fill', shapes: [...this.shapeHandler.shapes], fills: resolved, shadows: this.shapeHandler.takePendingShadows() });
|
|
458
|
+
this.shapeHandler.paintApplied = true;
|
|
459
|
+
return this;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const pendingShadows = this.shapeHandler.takePendingShadows();
|
|
463
|
+
if (pendingShadows) {
|
|
464
|
+
const space = pendingShadows[0].fill.space ?? "global";
|
|
465
|
+
const { shapes, dispose } = this.strokeShapesForSpace(space);
|
|
466
|
+
this.strokeHandler.applyShadows(pendingShadows, shapes, resolved, [], this.applyFillSpaceBounds);
|
|
467
|
+
dispose();
|
|
468
|
+
}
|
|
469
|
+
this.fillHandler.applyFills(resolved, this.shapeHandler.shapes);
|
|
470
|
+
this.shapeHandler.paintApplied = true;
|
|
471
|
+
return this;
|
|
472
|
+
}
|
|
473
|
+
// Set the fill handler's bounds for a fill, honouring its `space`. Used as
|
|
474
|
+
// the per-shape resolveBounds hook for stroke/shadow shaders.
|
|
475
|
+
applyFillSpaceBounds = (fill, shape) => {
|
|
476
|
+
this.fillHandler.setCurrentBounds(this.fillHandler.boundsForSpace(fill.space ?? "global", shape));
|
|
477
|
+
};
|
|
478
|
+
// Pick the shapes a stroke/shadow should be drawn over for the given space.
|
|
479
|
+
// `local` strokes each shape individually; every other space strokes the
|
|
480
|
+
// union outline so overlapping shapes show no internal seams. Returns the
|
|
481
|
+
// shape list plus a disposer for any transient union path.
|
|
482
|
+
strokeShapesForSpace(space) {
|
|
483
|
+
if (space === "local") {
|
|
484
|
+
return { shapes: this.shapeHandler.shapes, dispose: () => { } };
|
|
485
|
+
}
|
|
486
|
+
const union = this.shapeHandler.unionStrokeShape();
|
|
487
|
+
if (union) {
|
|
488
|
+
return { shapes: [union], dispose: () => union.ckPath?.delete() };
|
|
489
|
+
}
|
|
490
|
+
return { shapes: this.shapeHandler.shapes, dispose: () => { } };
|
|
491
|
+
}
|
|
492
|
+
stroke(strokes) {
|
|
493
|
+
const resolved = resolveStrokeArray(strokes);
|
|
494
|
+
if (resolved.length === 0)
|
|
495
|
+
return this;
|
|
496
|
+
if (!this.currentCanvas) {
|
|
497
|
+
console.warn("stroke() must be called within the draw() method.");
|
|
498
|
+
return this;
|
|
499
|
+
}
|
|
500
|
+
if (this.shapeHandler.isCollectingPaths()) {
|
|
501
|
+
this.shapeHandler.paintApplied = true;
|
|
502
|
+
return this;
|
|
503
|
+
}
|
|
504
|
+
const maskApply = this.shapeHandler.getMaskApply();
|
|
505
|
+
if (maskApply !== null && !maskApply.has('stroke')) {
|
|
506
|
+
const top = this.deferredPaintsStack[this.deferredPaintsStack.length - 1];
|
|
507
|
+
if (top) {
|
|
508
|
+
top.push({ kind: 'stroke', shapes: [...this.shapeHandler.shapes], strokes: resolved, shadows: this.shapeHandler.takePendingShadows() });
|
|
509
|
+
this.shapeHandler.paintApplied = true;
|
|
510
|
+
return this;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const pendingShadows = this.shapeHandler.takePendingShadows();
|
|
514
|
+
if (pendingShadows) {
|
|
515
|
+
const shadowSpace = pendingShadows[0].fill.space ?? "global";
|
|
516
|
+
const { shapes: shadowShapes, dispose: shadowDispose } = this.strokeShapesForSpace(shadowSpace);
|
|
517
|
+
this.strokeHandler.applyShadows(pendingShadows, shadowShapes, [], resolved, this.applyFillSpaceBounds);
|
|
518
|
+
shadowDispose();
|
|
519
|
+
}
|
|
520
|
+
// The first stroke's space decides geometry grouping (local = per shape,
|
|
521
|
+
// else union outline). Bounds for each stroke's shader are resolved per
|
|
522
|
+
// shape from that stroke's own fill space.
|
|
523
|
+
const space = resolved[0].fill.space ?? "global";
|
|
524
|
+
const { shapes, dispose } = this.strokeShapesForSpace(space);
|
|
525
|
+
this.strokeHandler.applyStrokes(resolved, shapes, this.applyFillSpaceBounds);
|
|
526
|
+
dispose();
|
|
527
|
+
this.shapeHandler.paintApplied = true;
|
|
528
|
+
return this;
|
|
529
|
+
}
|
|
530
|
+
shadow(shadows) {
|
|
531
|
+
const resolved = resolveShadowArray(shadows);
|
|
532
|
+
if (resolved.length === 0)
|
|
533
|
+
return this;
|
|
534
|
+
if (!this.isRendering) {
|
|
535
|
+
console.warn("shadow() must be called within the draw() method.");
|
|
536
|
+
return this;
|
|
537
|
+
}
|
|
538
|
+
if (this.shapeHandler.isCollectingPaths())
|
|
539
|
+
return this;
|
|
540
|
+
this.shapeHandler.storePendingShadows(resolved);
|
|
541
|
+
return this;
|
|
542
|
+
}
|
|
543
|
+
cut() {
|
|
544
|
+
if (!this.isRendering) {
|
|
545
|
+
console.warn("cut() must be called within the draw() method.");
|
|
546
|
+
return this;
|
|
547
|
+
}
|
|
548
|
+
this.shapeHandler.cut();
|
|
549
|
+
return this;
|
|
550
|
+
}
|
|
551
|
+
// ─── Camera viewport ─────────────────────────────────────────────────────
|
|
552
|
+
cameraRestoreStack = [];
|
|
553
|
+
beginCamera(viewport, centerOn, zoom, heading) {
|
|
554
|
+
if (!this.isRendering) {
|
|
555
|
+
console.warn("beginCamera() must be called within the draw() method.");
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const canvas = this.currentCanvas;
|
|
559
|
+
const ck = this.canvasKit;
|
|
560
|
+
canvas.save();
|
|
561
|
+
const left = viewport.x - viewport.width / 2;
|
|
562
|
+
const top = viewport.y - viewport.height / 2;
|
|
563
|
+
const right = viewport.x + viewport.width / 2;
|
|
564
|
+
const bottom = viewport.y + viewport.height / 2;
|
|
565
|
+
canvas.clipRect(ck.LTRBRect(left, top, right, bottom), ck.ClipOp.Intersect, true);
|
|
566
|
+
canvas.save();
|
|
567
|
+
canvas.translate(viewport.x, viewport.y);
|
|
568
|
+
canvas.rotate(-heading, 0, 0);
|
|
569
|
+
canvas.scale(zoom, zoom);
|
|
570
|
+
canvas.translate(-centerOn.x, centerOn.y);
|
|
571
|
+
this.cameraRestoreStack.push(2);
|
|
572
|
+
}
|
|
573
|
+
endCamera() {
|
|
574
|
+
if (!this.isRendering) {
|
|
575
|
+
console.warn("endCamera() must be called within the draw() method.");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const restores = this.cameraRestoreStack.pop() ?? 0;
|
|
579
|
+
for (let i = 0; i < restores; i++) {
|
|
580
|
+
this.currentCanvas.restore();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// ─── Clip scope ──────────────────────────────────────────────────────────
|
|
584
|
+
beginClipRect(state) {
|
|
585
|
+
if (!this.isRendering) {
|
|
586
|
+
console.warn("beginClipRect() must be called within the draw() method.");
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const canvas = this.currentCanvas;
|
|
590
|
+
canvas.save();
|
|
591
|
+
const shape = new RectShape(this.canvasKit, canvas, state);
|
|
592
|
+
shape.clip(/* isolated= */ true);
|
|
593
|
+
if (shape.ckPath)
|
|
594
|
+
shape.ckPath.delete();
|
|
595
|
+
this.clipRestoreStack.push(1);
|
|
596
|
+
}
|
|
597
|
+
beginClipEllipse(state) {
|
|
598
|
+
if (!this.isRendering) {
|
|
599
|
+
console.warn("beginClipEllipse() must be called within the draw() method.");
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const canvas = this.currentCanvas;
|
|
603
|
+
canvas.save();
|
|
604
|
+
const shape = new EllipseShape(this.canvasKit, canvas, state);
|
|
605
|
+
shape.clip(/* isolated= */ true);
|
|
606
|
+
if (shape.ckPath)
|
|
607
|
+
shape.ckPath.delete();
|
|
608
|
+
this.clipRestoreStack.push(1);
|
|
609
|
+
}
|
|
610
|
+
endClip() {
|
|
611
|
+
if (!this.isRendering) {
|
|
612
|
+
console.warn("endClip() must be called within the draw() method.");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const restores = this.clipRestoreStack.pop() ?? 0;
|
|
616
|
+
for (let i = 0; i < restores; i++) {
|
|
617
|
+
this.currentCanvas.restore();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
buildClipShape(shape) {
|
|
621
|
+
const ck = this.canvasKit;
|
|
622
|
+
const canvas = this.currentCanvas;
|
|
623
|
+
// Call clip(true) to apply the clip (uses native clipRect/clipRRect when possible),
|
|
624
|
+
// then return a stub CurrentShape only if a ckPath was built so beginClipShape
|
|
625
|
+
// can delete it. Returns null when the clip was applied without a path.
|
|
626
|
+
let s;
|
|
627
|
+
switch (shape.kind) {
|
|
628
|
+
case "rect":
|
|
629
|
+
s = new RectShape(ck, canvas, shape.state);
|
|
630
|
+
break;
|
|
631
|
+
case "ellipse":
|
|
632
|
+
s = new EllipseShape(ck, canvas, shape.state);
|
|
633
|
+
break;
|
|
634
|
+
case "polygon":
|
|
635
|
+
s = new PolygonShape(ck, canvas, shape.state);
|
|
636
|
+
break;
|
|
637
|
+
case "polygram":
|
|
638
|
+
s = new PolygramShape(ck, canvas, shape.state);
|
|
639
|
+
break;
|
|
640
|
+
case "path":
|
|
641
|
+
s = new PathShape(ck, canvas, shape.state);
|
|
642
|
+
break;
|
|
643
|
+
case "line":
|
|
644
|
+
s = new LineShape(ck, canvas, shape.state);
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
s.clip(true);
|
|
648
|
+
return s.ckPath ? { draw: () => { }, ckPath: s.ckPath } : null;
|
|
649
|
+
}
|
|
650
|
+
beginClipShape(shape) {
|
|
651
|
+
if (!this.isRendering) {
|
|
652
|
+
console.warn("beginClipShape() must be called within the draw() method.");
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const canvas = this.currentCanvas;
|
|
656
|
+
canvas.save();
|
|
657
|
+
const built = this.buildClipShape(shape);
|
|
658
|
+
if (built?.ckPath) {
|
|
659
|
+
canvas.clipPath(built.ckPath, this.canvasKit.ClipOp.Intersect, true);
|
|
660
|
+
built.ckPath.delete();
|
|
661
|
+
}
|
|
662
|
+
this.clipRestoreStack.push(1);
|
|
663
|
+
}
|
|
664
|
+
// ─── Background blur ───────────────────────────────────────────────────────
|
|
665
|
+
// Tracks the layer pushed by beginBackgroundBlur so endBackgroundBlur can
|
|
666
|
+
// pop exactly that many — kept as a stack to allow nesting.
|
|
667
|
+
backgroundBlurStack = [];
|
|
668
|
+
beginBackgroundBlur(radius) {
|
|
669
|
+
if (!this.isRendering) {
|
|
670
|
+
console.warn("beginBackgroundBlur() must be called within the draw() method.");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (radius <= 0) {
|
|
674
|
+
this.backgroundBlurStack.push(0);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const ck = this.canvasKit;
|
|
678
|
+
// Blur radius is given in logical px; the surface is scaled by pixelRatio,
|
|
679
|
+
// and sigma ≈ radius/2 matches the node `blur` effect's mapping.
|
|
680
|
+
const sigma = (radius * this.pixelRatio) / 2;
|
|
681
|
+
const backdrop = ck.ImageFilter.MakeBlur(sigma, sigma, ck.TileMode.Clamp, null);
|
|
682
|
+
// saveLayer with a backdrop filter seeds the new layer with the current
|
|
683
|
+
// canvas content run through `backdrop`. Clamp tiling samples beyond the
|
|
684
|
+
// active clip so the blur doesn't darken toward the silhouette edge. The
|
|
685
|
+
// layer is bounded by the active clip, so only the silhouette composites
|
|
686
|
+
// back on restore.
|
|
687
|
+
this.currentCanvas.saveLayer(undefined, null, backdrop, undefined, ck.TileMode.Clamp);
|
|
688
|
+
backdrop.delete();
|
|
689
|
+
this.backgroundBlurStack.push(1);
|
|
690
|
+
}
|
|
691
|
+
endBackgroundBlur() {
|
|
692
|
+
if (!this.isRendering) {
|
|
693
|
+
console.warn("endBackgroundBlur() must be called within the draw() method.");
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const layers = this.backgroundBlurStack.pop() ?? 0;
|
|
697
|
+
for (let i = 0; i < layers; i++) {
|
|
698
|
+
this.currentCanvas.restore();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// ─── Background distortion (bulge/pinch) ─────────────────────────────────────
|
|
702
|
+
backgroundDistortionStack = [];
|
|
703
|
+
beginBackgroundDistortion(effect, width, height) {
|
|
704
|
+
if (!this.isRendering) {
|
|
705
|
+
console.warn("beginBackgroundDistortion() must be called within the draw() method.");
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const ck = this.canvasKit;
|
|
709
|
+
// The node was already translated to its centre by applyTransform, so the
|
|
710
|
+
// CTM maps the node's local origin (0,0) to its device centre, and the CTM
|
|
711
|
+
// scale converts the node's logical size into device px for the lens.
|
|
712
|
+
const m = this.currentCanvas.getTotalMatrix(); // row-major 3x3
|
|
713
|
+
const centerX = m[2];
|
|
714
|
+
const centerY = m[5];
|
|
715
|
+
const sx = Math.hypot(m[0], m[3]);
|
|
716
|
+
const sy = Math.hypot(m[1], m[4]);
|
|
717
|
+
// Snapshot the content painted so far (the backdrop) and wrap it as a child
|
|
718
|
+
// shader. The lens shader resamples this snapshot at bulge/pinch-remapped
|
|
719
|
+
// device coordinates — a real magnifier, not a nudge.
|
|
720
|
+
const snapshot = this.surface.makeImageSnapshot();
|
|
721
|
+
const backdropShader = snapshot.makeShaderOptions(ck.TileMode.Clamp, ck.TileMode.Clamp, ck.FilterMode.Linear, ck.MipmapMode.None);
|
|
722
|
+
const lens = effect.type === "zoom"
|
|
723
|
+
? makeZoomShader(effect, ck, backdropShader, centerX, centerY, width * sx, height * sy)
|
|
724
|
+
: makeBulgePinchShader(effect, ck, backdropShader, centerX, centerY, width * sx, height * sy);
|
|
725
|
+
if (lens == null) {
|
|
726
|
+
backdropShader.delete();
|
|
727
|
+
snapshot.delete();
|
|
728
|
+
this.backgroundDistortionStack.push(0);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
// Draw the warped backdrop in device space (identity CTM, so fragCoord ==
|
|
732
|
+
// snapshot px), bounded by the active silhouette clip — which is stored in
|
|
733
|
+
// device space, so it survives the matrix reset. Only the node's shape
|
|
734
|
+
// region is repainted with the distorted backdrop. CanvasKit has no
|
|
735
|
+
// resetMatrix, so concat the inverse of the current CTM to reach identity.
|
|
736
|
+
this.currentCanvas.save();
|
|
737
|
+
const inverse = ck.Matrix.invert(m);
|
|
738
|
+
if (inverse)
|
|
739
|
+
this.currentCanvas.concat(inverse);
|
|
740
|
+
const paint = new ck.Paint();
|
|
741
|
+
paint.setShader(lens);
|
|
742
|
+
paint.setAntiAlias(true);
|
|
743
|
+
this.currentCanvas.drawRect(ck.LTRBRect(0, 0, this.surface.width(), this.surface.height()), paint);
|
|
744
|
+
paint.delete();
|
|
745
|
+
lens.delete();
|
|
746
|
+
backdropShader.delete();
|
|
747
|
+
snapshot.delete();
|
|
748
|
+
this.currentCanvas.restore();
|
|
749
|
+
this.backgroundDistortionStack.push(0);
|
|
750
|
+
}
|
|
751
|
+
endBackgroundDistortion() {
|
|
752
|
+
if (!this.isRendering) {
|
|
753
|
+
console.warn("endBackgroundDistortion() must be called within the draw() method.");
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
// The distortion is painted entirely within begin(); the stack entry is kept
|
|
757
|
+
// only for API symmetry with the other backdrop scopes.
|
|
758
|
+
this.backgroundDistortionStack.pop();
|
|
759
|
+
}
|
|
760
|
+
// ─── Custom SkSL backdrop ─────────────────────────────────────────────────
|
|
761
|
+
backdropSkSLStack = [];
|
|
762
|
+
beginBackdropSkSL(effect, width, height) {
|
|
763
|
+
if (!this.isRendering) {
|
|
764
|
+
console.warn("beginBackdropSkSL() must be called within the draw() method.");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const ck = this.canvasKit;
|
|
768
|
+
if (width <= 0 || height <= 0) {
|
|
769
|
+
this.backdropSkSLStack.push(0);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const rte = getOrCompileSkSL(effect.shader, ck);
|
|
773
|
+
if (!rte) {
|
|
774
|
+
this.backdropSkSLStack.push(0);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
// Read the current CTM so we can reset to device-space for the draw.
|
|
778
|
+
const m = this.currentCanvas.getTotalMatrix();
|
|
779
|
+
// Snapshot what's beneath the node (before any of the node's own draws).
|
|
780
|
+
const snapshot = this.surface.makeImageSnapshot();
|
|
781
|
+
const backdropShader = snapshot.makeShaderOptions(ck.TileMode.Clamp, ck.TileMode.Clamp, ck.FilterMode.Linear, ck.MipmapMode.None);
|
|
782
|
+
// Flatten user uniforms in declaration order.
|
|
783
|
+
const flat = effect.uniforms.flatMap((u) => typeof u.value === "number" ? [u.value] : u.value);
|
|
784
|
+
// The first child shader is always u_backdrop.
|
|
785
|
+
const shader = rte.makeShaderWithChildren(flat, [backdropShader]);
|
|
786
|
+
// Draw the warped backdrop in device space (reset to identity via inverse CTM)
|
|
787
|
+
// so fragCoord matches snapshot pixel coordinates. The active silhouette clip
|
|
788
|
+
// (stored in device space) confines the repaint to the node's shape.
|
|
789
|
+
this.currentCanvas.save();
|
|
790
|
+
const inverse = ck.Matrix.invert(m);
|
|
791
|
+
if (inverse)
|
|
792
|
+
this.currentCanvas.concat(inverse);
|
|
793
|
+
const paint = new ck.Paint();
|
|
794
|
+
paint.setShader(shader);
|
|
795
|
+
paint.setAntiAlias(true);
|
|
796
|
+
this.currentCanvas.drawRect(ck.LTRBRect(0, 0, this.surface.width(), this.surface.height()), paint);
|
|
797
|
+
paint.delete();
|
|
798
|
+
shader.delete();
|
|
799
|
+
backdropShader.delete();
|
|
800
|
+
snapshot.delete();
|
|
801
|
+
this.currentCanvas.restore();
|
|
802
|
+
this.backdropSkSLStack.push(0);
|
|
803
|
+
}
|
|
804
|
+
endBackdropSkSL() {
|
|
805
|
+
if (!this.isRendering) {
|
|
806
|
+
console.warn("endBackdropSkSL() must be called within the draw() method.");
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
this.backdropSkSLStack.pop();
|
|
810
|
+
}
|
|
811
|
+
drawWebGLCanvas(canvas, x, y, w, h) {
|
|
812
|
+
if (!this.isRendering || !this.surface)
|
|
813
|
+
return false;
|
|
814
|
+
const img = this.surface.makeImageFromTextureSource(canvas, {
|
|
815
|
+
width: canvas.width,
|
|
816
|
+
height: canvas.height,
|
|
817
|
+
alphaType: this.canvasKit.AlphaType.Premul,
|
|
818
|
+
colorType: this.canvasKit.ColorType.RGBA_8888,
|
|
819
|
+
colorSpace: this.canvasKit.ColorSpace.SRGB,
|
|
820
|
+
});
|
|
821
|
+
if (!img)
|
|
822
|
+
return false;
|
|
823
|
+
const ck = this.canvasKit;
|
|
824
|
+
const paint = this.paint;
|
|
825
|
+
paint.setStyle(ck.PaintStyle.Fill);
|
|
826
|
+
paint.setAlphaf(1);
|
|
827
|
+
paint.setBlendMode(ck.BlendMode.SrcOver);
|
|
828
|
+
const half_w = w / 2;
|
|
829
|
+
const half_h = h / 2;
|
|
830
|
+
const dst = ck.LTRBRect(x - half_w, y - half_h, x + half_w, y + half_h);
|
|
831
|
+
const src = ck.LTRBRect(0, 0, canvas.width, canvas.height);
|
|
832
|
+
this.currentCanvas.drawImageRect(img, src, dst, paint);
|
|
833
|
+
paint.setBlendMode(ck.BlendMode.SrcOver);
|
|
834
|
+
paint.setAlphaf(1);
|
|
835
|
+
img.delete();
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
// ─── Boolean group ───────────────────────────────────────────────────────
|
|
839
|
+
beginBoolean(op) {
|
|
840
|
+
if (!this.isRendering) {
|
|
841
|
+
console.warn("beginBoolean() must be called within the draw() method.");
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
this.shapeHandler.beginBoolean(op);
|
|
845
|
+
}
|
|
846
|
+
endBoolean() {
|
|
847
|
+
if (!this.isRendering) {
|
|
848
|
+
console.warn("endBoolean() must be called within the draw() method.");
|
|
849
|
+
return this;
|
|
850
|
+
}
|
|
851
|
+
this.shapeHandler.endBoolean();
|
|
852
|
+
return this;
|
|
853
|
+
}
|
|
854
|
+
// ─── Mask group ──────────────────────────────────────────────────────────
|
|
855
|
+
mask(options) {
|
|
856
|
+
if (!this.isRendering) {
|
|
857
|
+
console.warn("mask() must be called within the draw() method.");
|
|
858
|
+
return this;
|
|
859
|
+
}
|
|
860
|
+
this.shapeHandler.beginMask(options);
|
|
861
|
+
this.deferredPaintsStack.push([]);
|
|
862
|
+
return this;
|
|
863
|
+
}
|
|
864
|
+
beginMask(options) {
|
|
865
|
+
if (!this.isRendering) {
|
|
866
|
+
console.warn("beginMask() must be called within the draw() method.");
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
this.shapeHandler.beginMask(options);
|
|
870
|
+
this.deferredPaintsStack.push([]);
|
|
871
|
+
}
|
|
872
|
+
applyMask() {
|
|
873
|
+
if (!this.isRendering) {
|
|
874
|
+
console.warn("applyMask() must be called within the draw() method.");
|
|
875
|
+
return this;
|
|
876
|
+
}
|
|
877
|
+
this.shapeHandler.applyMask();
|
|
878
|
+
return this;
|
|
879
|
+
}
|
|
880
|
+
endMask() {
|
|
881
|
+
if (!this.isRendering) {
|
|
882
|
+
console.warn("endMask() must be called within the draw() method.");
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
this.shapeHandler.endMask();
|
|
886
|
+
const deferred = this.deferredPaintsStack.pop() ?? [];
|
|
887
|
+
this.flushDeferredPaints(deferred);
|
|
888
|
+
}
|
|
889
|
+
/** Replays fill/stroke calls that were postponed by an active mask scope (see `mask`/`fill`/`stroke`), in original order, once the mask resolves at `endMask`. */
|
|
890
|
+
flushDeferredPaints(deferred) {
|
|
891
|
+
for (const d of deferred) {
|
|
892
|
+
if (d.kind === 'stroke') {
|
|
893
|
+
if (d.shadows) {
|
|
894
|
+
this.strokeHandler.applyShadows(d.shadows, d.shapes, [], d.strokes, this.applyFillSpaceBounds);
|
|
895
|
+
}
|
|
896
|
+
this.strokeHandler.applyStrokes(d.strokes, d.shapes, this.applyFillSpaceBounds);
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
if (d.shadows) {
|
|
900
|
+
this.strokeHandler.applyShadows(d.shadows, d.shapes, d.fills, [], this.applyFillSpaceBounds);
|
|
901
|
+
}
|
|
902
|
+
this.fillHandler.applyFills(d.fills, d.shapes);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/** Paint-context returned by `image()`; routes `.fill/.stroke/.shadow` into the pending image's buffers and forwards shape calls back to the render context (which flushes the pending image first). */
|
|
908
|
+
class WebImagePaintContext {
|
|
909
|
+
ctx;
|
|
910
|
+
constructor(ctx) {
|
|
911
|
+
this.ctx = ctx;
|
|
912
|
+
}
|
|
913
|
+
fill(fills) {
|
|
914
|
+
this.ctx._appendImageFills(resolveFillArray(fills));
|
|
915
|
+
return this;
|
|
916
|
+
}
|
|
917
|
+
stroke(strokes) {
|
|
918
|
+
this.ctx._appendImageStrokes(resolveStrokeArray(strokes));
|
|
919
|
+
return this;
|
|
920
|
+
}
|
|
921
|
+
shadow(shadows) {
|
|
922
|
+
this.ctx._appendImageShadows(resolveShadowArray(shadows));
|
|
923
|
+
return this;
|
|
924
|
+
}
|
|
925
|
+
// Chaining a new shape after an image flushes the pending image (the shape
|
|
926
|
+
// methods on the context do this) and starts the next shape on the context.
|
|
927
|
+
rect(state) { return this.ctx.rect(state); }
|
|
928
|
+
ellipse(state) { return this.ctx.ellipse(state); }
|
|
929
|
+
text(state) { return this.ctx.text(state); }
|
|
930
|
+
richText(state) { return this.ctx.richText(state); }
|
|
931
|
+
path(state) { return this.ctx.path(state); }
|
|
932
|
+
line(state) { return this.ctx.line(state); }
|
|
933
|
+
image(state) { return this.ctx.image(state); }
|
|
934
|
+
polygon(state) { return this.ctx.polygon(state); }
|
|
935
|
+
polygram(state) { return this.ctx.polygram(state); }
|
|
936
|
+
cut() { return this.ctx.cut(); }
|
|
937
|
+
applyMask() { return this.ctx.applyMask(); }
|
|
938
|
+
endMask() { this.ctx.endMask(); }
|
|
939
|
+
}
|
|
940
|
+
//# sourceMappingURL=render-context.js.map
|