@oh-just-another/renderer-canvas 0.58.1 → 0.60.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/CHANGELOG.md +78 -0
- package/README.md +63 -15
- package/dist/.tsbuildinfo +1 -1
- package/dist/canvas-target.d.ts +6 -1
- package/dist/canvas-target.d.ts.map +1 -1
- package/dist/canvas-target.js +18 -5
- package/dist/canvas-target.js.map +1 -1
- package/dist/constants.d.ts +81 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +81 -0
- package/dist/constants.js.map +1 -1
- package/dist/hi-dpi.d.ts +21 -13
- package/dist/hi-dpi.d.ts.map +1 -1
- package/dist/hi-dpi.js +28 -33
- package/dist/hi-dpi.js.map +1 -1
- package/dist/image-source.d.ts +11 -0
- package/dist/image-source.d.ts.map +1 -1
- package/dist/image-source.js +20 -0
- package/dist/image-source.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -12
- package/dist/index.js.map +1 -1
- package/dist/layered-canvas.d.ts +7 -1
- package/dist/layered-canvas.d.ts.map +1 -1
- package/dist/layered-canvas.js +6 -3
- package/dist/layered-canvas.js.map +1 -1
- package/dist/layered-surface.d.ts +16 -0
- package/dist/layered-surface.d.ts.map +1 -1
- package/dist/layered-surface.js +53 -15
- package/dist/layered-surface.js.map +1 -1
- package/dist/recording-target.d.ts +85 -25
- package/dist/recording-target.d.ts.map +1 -1
- package/dist/recording-target.js +250 -50
- package/dist/recording-target.js.map +1 -1
- package/dist/render-worker.js +16 -3
- package/dist/render-worker.js.map +1 -1
- package/dist/tile-compositor.d.ts +43 -1
- package/dist/tile-compositor.d.ts.map +1 -1
- package/dist/tile-compositor.js +48 -5
- package/dist/tile-compositor.js.map +1 -1
- package/dist/webgl-helpers.d.ts +7 -0
- package/dist/webgl-helpers.d.ts.map +1 -0
- package/dist/webgl-helpers.js +32 -0
- package/dist/webgl-helpers.js.map +1 -0
- package/dist/webgl2-curve.d.ts +11 -0
- package/dist/webgl2-curve.d.ts.map +1 -1
- package/dist/webgl2-curve.js +151 -68
- package/dist/webgl2-curve.js.map +1 -1
- package/dist/webgl2-ellipse.d.ts +0 -27
- package/dist/webgl2-ellipse.d.ts.map +1 -1
- package/dist/webgl2-ellipse.js +21 -47
- package/dist/webgl2-ellipse.js.map +1 -1
- package/dist/webgl2-msdf-text.d.ts +14 -0
- package/dist/webgl2-msdf-text.d.ts.map +1 -1
- package/dist/webgl2-msdf-text.js +84 -36
- package/dist/webgl2-msdf-text.js.map +1 -1
- package/dist/webgl2-rect-batch.d.ts +81 -0
- package/dist/webgl2-rect-batch.d.ts.map +1 -0
- package/dist/webgl2-rect-batch.js +201 -0
- package/dist/webgl2-rect-batch.js.map +1 -0
- package/dist/webgl2-stroke.d.ts +6 -30
- package/dist/webgl2-stroke.d.ts.map +1 -1
- package/dist/webgl2-stroke.js +17 -30
- package/dist/webgl2-stroke.js.map +1 -1
- package/dist/webgl2-target.d.ts +74 -8
- package/dist/webgl2-target.d.ts.map +1 -1
- package/dist/webgl2-target.js +356 -257
- package/dist/webgl2-target.js.map +1 -1
- package/package.json +10 -8
package/dist/webgl2-target.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import { req } from "@oh-just-another/types";
|
|
1
2
|
import { getActiveRasterizer, getActiveTextShaper } from "@oh-just-another/renderer-core";
|
|
2
3
|
import { GlyphAtlas } from "@oh-just-another/glyph-atlas";
|
|
4
|
+
import { resolveBundledFamily } from "@oh-just-another/fonts";
|
|
3
5
|
import earcut from "earcut";
|
|
4
6
|
import { parseWebGL2Color } from "./webgl2-color.js";
|
|
5
|
-
import { WEBGL2_IMAGE_TEXTURE_CACHE_CAP, WEBGL2_TEXT_BITMAP_CACHE_CAP } from "./constants.js";
|
|
6
|
-
import { MsdfTextPipeline } from "./webgl2-msdf-text.js";
|
|
7
|
+
import { ELLIPSE_MAX_SEGMENTS, ELLIPSE_MIN_SEGMENTS, WEBGL2_IMAGE_TEXTURE_CACHE_CAP, WEBGL2_TEXT_BITMAP_CACHE_CAP, } from "./constants.js";
|
|
8
|
+
import { MsdfTextPipeline, measureGlyphRunEm } from "./webgl2-msdf-text.js";
|
|
7
9
|
import { drawPolylineStroke as drawPolylineStrokeImpl } from "./webgl2-stroke.js";
|
|
8
10
|
import { LoopBlinnCurvePipeline } from "./webgl2-curve.js";
|
|
9
11
|
import { EllipsePipeline } from "./webgl2-ellipse.js";
|
|
10
12
|
import { isDrawableImageSource, warnSkippedImage } from "./image-source.js";
|
|
13
|
+
import { compileShader, glReq, linkProgram } from "./webgl-helpers.js";
|
|
14
|
+
import { RectBatch, RectInstancePipeline } from "./webgl2-rect-batch.js";
|
|
11
15
|
/**
|
|
12
16
|
* WebGL2 RenderTarget. Implements clear, transform/state stack, path
|
|
13
17
|
* primitives (rect / polyline / ellipse / Bezier), fill, stroke, text
|
|
@@ -62,10 +66,27 @@ export class WebGL2Target {
|
|
|
62
66
|
textAlign = "left";
|
|
63
67
|
textBaseline = "top";
|
|
64
68
|
/**
|
|
65
|
-
* Polyline path being assembled by moveTo / lineTo
|
|
66
|
-
* `
|
|
69
|
+
* Polyline path being assembled by moveTo / lineTo, as a flat
|
|
70
|
+
* growable `[x0, y0, x1, y1, ...]` buffer with a point-count cursor —
|
|
71
|
+
* no per-vertex `{x, y}` object churn on the hot path-building path.
|
|
72
|
+
* Cleared on `beginPath()`; consumed by `fill()` / `stroke()`.
|
|
73
|
+
* Per-instance (not module-level) so interleaved path building on two
|
|
74
|
+
* targets can't stomp each other. Capacity ratchets up, never shrinks.
|
|
67
75
|
*/
|
|
68
|
-
|
|
76
|
+
pathXY = new Float64Array(INITIAL_PATH_CAPACITY * 2);
|
|
77
|
+
/** Number of (x, y) points currently in `pathXY`. */
|
|
78
|
+
pathPts = 0;
|
|
79
|
+
/** Append one point to the flat path buffer, growing it if needed. */
|
|
80
|
+
pushPathPoint(x, y) {
|
|
81
|
+
if ((this.pathPts + 1) * 2 > this.pathXY.length) {
|
|
82
|
+
const next = new Float64Array(this.pathXY.length * 2);
|
|
83
|
+
next.set(this.pathXY);
|
|
84
|
+
this.pathXY = next;
|
|
85
|
+
}
|
|
86
|
+
this.pathXY[this.pathPts * 2] = x;
|
|
87
|
+
this.pathXY[this.pathPts * 2 + 1] = y;
|
|
88
|
+
this.pathPts++;
|
|
89
|
+
}
|
|
69
90
|
/**
|
|
70
91
|
* Curve segments collected since the last `beginPath()`. Quadratic
|
|
71
92
|
* and cubic Bezier `*CurveTo` calls push here in addition to pushing
|
|
@@ -93,24 +114,47 @@ export class WebGL2Target {
|
|
|
93
114
|
* snapshot the path either.
|
|
94
115
|
*/
|
|
95
116
|
stack = [];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Sharp-rect fill batcher — coalesces consecutive axis-aligned
|
|
119
|
+
* `rect()` + `fill()` calls into one `drawArraysInstanced` (B19). Any
|
|
120
|
+
* non-batchable draw (`stroke`, ellipse / polygon / curve fill, image,
|
|
121
|
+
* text) and every surface op (`clear`, `resize`, frame `present`)
|
|
122
|
+
* drain it first via {@link flushRectBatch}, so submission order —
|
|
123
|
+
* hence z-order — is preserved. The GL pipeline is created lazily on
|
|
124
|
+
* the first flush; a scene with no sharp rects never allocates it.
|
|
125
|
+
*/
|
|
126
|
+
rectBatch = new RectBatch();
|
|
127
|
+
rectPipeline = null;
|
|
128
|
+
constructor(canvas, width, height, options = {}) {
|
|
129
|
+
// `preserveDrawingBuffer` keeps the drawing buffer across composites.
|
|
130
|
+
// It defaults to `true` because the interactive editor renders
|
|
131
|
+
// incrementally: `renderScene` clears + redraws only the dirty rect
|
|
132
|
+
// each frame (see the dirty-rect logic in renderer-core /
|
|
133
|
+
// render-orchestrator) and relies on the rest of the previous frame
|
|
134
|
+
// surviving. When this flag is `false` the spec permits the browser
|
|
135
|
+
// to wipe the buffer after each composite, so everything outside the
|
|
136
|
+
// dirty rect disappears in the steady state.
|
|
137
|
+
//
|
|
138
|
+
// This is NOT for readback: PNG export / screenshots render through a
|
|
139
|
+
// separate offscreen Canvas2D target (`createOffscreenCanvas2DTarget`
|
|
140
|
+
// in `png-export` / `exporter` / `tile-compositor`), never this live
|
|
141
|
+
// context, so they don't depend on the flag.
|
|
102
142
|
//
|
|
143
|
+
// On Safari / iOS `true` forces a full re-composite per swap. A host
|
|
144
|
+
// that redraws the whole frame every time (dirty-rect culling
|
|
145
|
+
// disabled) can pass `preserveDrawingBuffer: false` for that win.
|
|
146
|
+
const preserveDrawingBuffer = options.preserveDrawingBuffer ?? true;
|
|
103
147
|
// Try with antialiasing first; some integrated GPUs deny the
|
|
104
148
|
// context when MSAA isn't available. Retry plain on failure so
|
|
105
149
|
// WebGL2 isn't lost entirely for a stylistic preference.
|
|
106
150
|
let gl = canvas.getContext("webgl2", {
|
|
107
151
|
antialias: true,
|
|
108
152
|
premultipliedAlpha: true,
|
|
109
|
-
preserveDrawingBuffer
|
|
153
|
+
preserveDrawingBuffer,
|
|
110
154
|
});
|
|
111
155
|
gl ??= canvas.getContext("webgl2", {
|
|
112
156
|
premultipliedAlpha: true,
|
|
113
|
-
preserveDrawingBuffer
|
|
157
|
+
preserveDrawingBuffer,
|
|
114
158
|
});
|
|
115
159
|
if (!gl) {
|
|
116
160
|
throw new Error("WebGL2 unavailable in this environment (probably hit the per-page GL context cap; " +
|
|
@@ -118,9 +162,9 @@ export class WebGL2Target {
|
|
|
118
162
|
}
|
|
119
163
|
this.gl = gl;
|
|
120
164
|
this._size = { width, height };
|
|
121
|
-
const vert =
|
|
122
|
-
const frag =
|
|
123
|
-
this.program =
|
|
165
|
+
const vert = compileShader(this.gl, this.gl.VERTEX_SHADER, VERTEX_SHADER, "WebGL2");
|
|
166
|
+
const frag = compileShader(this.gl, this.gl.FRAGMENT_SHADER, FRAGMENT_SHADER, "WebGL2");
|
|
167
|
+
this.program = linkProgram(this.gl, vert, frag, "WebGL2");
|
|
124
168
|
this.gl.useProgram(this.program);
|
|
125
169
|
// Single quad shared across every solid-fill rect — the vertex
|
|
126
170
|
// shader applies the per-call transform to scale + translate it
|
|
@@ -147,7 +191,7 @@ export class WebGL2Target {
|
|
|
147
191
|
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
|
|
148
192
|
// Initial viewport — must match the canvas drawing buffer size. The
|
|
149
193
|
// WebGL spec defaults to the canvas's initial size, but if the
|
|
150
|
-
// canvas was resized via
|
|
194
|
+
// canvas was resized via setupHiDpi (no-context) after creation the
|
|
151
195
|
// viewport stays at the first size, so set it explicitly.
|
|
152
196
|
this.gl.viewport(0, 0, canvas.width, canvas.height);
|
|
153
197
|
}
|
|
@@ -161,6 +205,9 @@ export class WebGL2Target {
|
|
|
161
205
|
* in sync so downstream renderers see the new dimensions.
|
|
162
206
|
*/
|
|
163
207
|
resize(width, height) {
|
|
208
|
+
// Queued instances carry a clip-space matrix projected against the
|
|
209
|
+
// old size; drain them before the size / viewport change.
|
|
210
|
+
this.flushRectBatch();
|
|
164
211
|
this._size.width = width;
|
|
165
212
|
this._size.height = height;
|
|
166
213
|
this.gl.viewport(0, 0, width, height);
|
|
@@ -172,6 +219,13 @@ export class WebGL2Target {
|
|
|
172
219
|
* and runtime backend switches quickly hit the cap.
|
|
173
220
|
*/
|
|
174
221
|
dispose() {
|
|
222
|
+
// Drop any undrawn queued rects and release the instance pipeline's
|
|
223
|
+
// GL resources (VAO / buffers / program).
|
|
224
|
+
this.rectBatch.reset();
|
|
225
|
+
if (this.rectPipeline) {
|
|
226
|
+
this.rectPipeline.dispose();
|
|
227
|
+
this.rectPipeline = null;
|
|
228
|
+
}
|
|
175
229
|
if (this.msdfPipeline) {
|
|
176
230
|
this.msdfPipeline.dispose();
|
|
177
231
|
this.msdfPipeline = null;
|
|
@@ -298,7 +352,7 @@ export class WebGL2Target {
|
|
|
298
352
|
// --- Path primitives ---
|
|
299
353
|
beginPath() {
|
|
300
354
|
this.currentPath = null;
|
|
301
|
-
this.
|
|
355
|
+
this.pathPts = 0;
|
|
302
356
|
this.currentCurves = [];
|
|
303
357
|
this.currentEllipse = null;
|
|
304
358
|
}
|
|
@@ -314,15 +368,15 @@ export class WebGL2Target {
|
|
|
314
368
|
}
|
|
315
369
|
_pathRect = { x: 0, y: 0, width: 0, height: 0 };
|
|
316
370
|
moveTo(x, y) {
|
|
317
|
-
this.
|
|
371
|
+
this.pathPts = 0;
|
|
372
|
+
this.pushPathPoint(x, y);
|
|
318
373
|
}
|
|
319
374
|
lineTo(x, y) {
|
|
320
|
-
this.
|
|
375
|
+
this.pushPathPoint(x, y);
|
|
321
376
|
}
|
|
322
377
|
closePath() {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
this.currentPolyline.push({ ...start });
|
|
378
|
+
if (this.pathPts > 1) {
|
|
379
|
+
this.pushPathPoint(req(this.pathXY[0]), req(this.pathXY[1]));
|
|
326
380
|
}
|
|
327
381
|
}
|
|
328
382
|
/**
|
|
@@ -334,7 +388,7 @@ export class WebGL2Target {
|
|
|
334
388
|
*/
|
|
335
389
|
ellipse(cx, cy, rx, ry) {
|
|
336
390
|
this.currentEllipse = { cx, cy, rx, ry };
|
|
337
|
-
this.
|
|
391
|
+
this.pathPts = 0;
|
|
338
392
|
this.currentPath = null;
|
|
339
393
|
}
|
|
340
394
|
/**
|
|
@@ -346,13 +400,10 @@ export class WebGL2Target {
|
|
|
346
400
|
const scale = Math.hypot(this.transform.a, this.transform.b);
|
|
347
401
|
const screenRadius = Math.max(e.rx, e.ry) * (Number.isFinite(scale) && scale > 0 ? scale : 1);
|
|
348
402
|
const segments = Math.max(ELLIPSE_MIN_SEGMENTS, Math.min(ELLIPSE_MAX_SEGMENTS, Math.ceil(Math.PI * screenRadius * 0.7)));
|
|
349
|
-
this.
|
|
403
|
+
this.pathPts = 0;
|
|
350
404
|
for (let i = 0; i <= segments; i++) {
|
|
351
405
|
const t = (i / segments) * Math.PI * 2;
|
|
352
|
-
this.
|
|
353
|
-
x: e.cx + e.rx * Math.cos(t),
|
|
354
|
-
y: e.cy + e.ry * Math.sin(t),
|
|
355
|
-
});
|
|
406
|
+
this.pushPathPoint(e.cx + e.rx * Math.cos(t), e.cy + e.ry * Math.sin(t));
|
|
356
407
|
}
|
|
357
408
|
}
|
|
358
409
|
/**
|
|
@@ -368,7 +419,7 @@ export class WebGL2Target {
|
|
|
368
419
|
* (sub-pixel zoom-aware).
|
|
369
420
|
*/
|
|
370
421
|
quadraticCurveTo(cx, cy, x, y) {
|
|
371
|
-
const start = this.
|
|
422
|
+
const start = this.lastPathPoint() ?? { x: cx, y: cy };
|
|
372
423
|
this.currentCurves.push({
|
|
373
424
|
kind: "q",
|
|
374
425
|
points: [start, { x: cx, y: cy }, { x, y }],
|
|
@@ -382,18 +433,24 @@ export class WebGL2Target {
|
|
|
382
433
|
{ kind: "M", to: start },
|
|
383
434
|
{ kind: "Q", control: { x: cx, y: cy }, to: { x, y } },
|
|
384
435
|
], tolerance);
|
|
385
|
-
for (let i = 1; i < pts.length; i++)
|
|
386
|
-
|
|
436
|
+
for (let i = 1; i < pts.length; i++) {
|
|
437
|
+
const p = req(pts[i]);
|
|
438
|
+
this.pushPathPoint(p.x, p.y);
|
|
439
|
+
}
|
|
387
440
|
return;
|
|
388
441
|
}
|
|
389
|
-
const count = Math.max(8, Math.min(128, Math.ceil(curveLengthEstimate(start,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
442
|
+
const count = Math.max(8, Math.min(128, Math.ceil(curveLengthEstimate(start.x, start.y, x, y) / tolerance)));
|
|
443
|
+
// Sample t in (0, 1] directly into the flat path buffer — the
|
|
444
|
+
// start point (t = 0) is already the path's last vertex.
|
|
445
|
+
for (let i = 1; i <= count; i++) {
|
|
446
|
+
const t = i / count;
|
|
447
|
+
const u = 1 - t;
|
|
448
|
+
this.pushPathPoint(u * u * start.x + 2 * u * t * cx + t * t * x, u * u * start.y + 2 * u * t * cy + t * t * y);
|
|
449
|
+
}
|
|
393
450
|
}
|
|
394
451
|
/** Cubic Bezier — same dual-track approach as quadratic. */
|
|
395
452
|
bezierCurveTo(c1x, c1y, c2x, c2y, x, y) {
|
|
396
|
-
const start = this.
|
|
453
|
+
const start = this.lastPathPoint() ?? { x, y };
|
|
397
454
|
this.currentCurves.push({
|
|
398
455
|
kind: "c",
|
|
399
456
|
points: [start, { x: c1x, y: c1y }, { x: c2x, y: c2y }, { x, y }],
|
|
@@ -410,14 +467,32 @@ export class WebGL2Target {
|
|
|
410
467
|
to: { x, y },
|
|
411
468
|
},
|
|
412
469
|
], tolerance);
|
|
413
|
-
for (let i = 1; i < pts.length; i++)
|
|
414
|
-
|
|
470
|
+
for (let i = 1; i < pts.length; i++) {
|
|
471
|
+
const p = req(pts[i]);
|
|
472
|
+
this.pushPathPoint(p.x, p.y);
|
|
473
|
+
}
|
|
415
474
|
return;
|
|
416
475
|
}
|
|
417
|
-
const count = Math.max(12, Math.min(192, Math.ceil(curveLengthEstimate(start,
|
|
418
|
-
|
|
419
|
-
for (let i = 1; i
|
|
420
|
-
|
|
476
|
+
const count = Math.max(12, Math.min(192, Math.ceil(curveLengthEstimate(start.x, start.y, x, y) / tolerance)));
|
|
477
|
+
// Sample t in (0, 1] directly into the flat path buffer.
|
|
478
|
+
for (let i = 1; i <= count; i++) {
|
|
479
|
+
const t = i / count;
|
|
480
|
+
const u = 1 - t;
|
|
481
|
+
const u2 = u * u;
|
|
482
|
+
const u3 = u2 * u;
|
|
483
|
+
const t2 = t * t;
|
|
484
|
+
const t3 = t2 * t;
|
|
485
|
+
this.pushPathPoint(u3 * start.x + 3 * u2 * t * c1x + 3 * u * t2 * c2x + t3 * x, u3 * start.y + 3 * u2 * t * c1y + 3 * u * t2 * c2y + t3 * y);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/** Last point of the flat path buffer as a fresh `{x, y}`, or `undefined`. */
|
|
489
|
+
lastPathPoint() {
|
|
490
|
+
if (this.pathPts === 0)
|
|
491
|
+
return undefined;
|
|
492
|
+
return {
|
|
493
|
+
x: req(this.pathXY[(this.pathPts - 1) * 2]),
|
|
494
|
+
y: req(this.pathXY[(this.pathPts - 1) * 2 + 1]),
|
|
495
|
+
};
|
|
421
496
|
}
|
|
422
497
|
/**
|
|
423
498
|
* World-unit tolerance that maps to ~`SCREEN_TOLERANCE_PX` on screen
|
|
@@ -440,7 +515,8 @@ export class WebGL2Target {
|
|
|
440
515
|
* as a textured quad via a dedicated program created lazily on the
|
|
441
516
|
* first image call.
|
|
442
517
|
*/
|
|
443
|
-
drawImage(image, dx, dy, dw, dh, dynamic) {
|
|
518
|
+
drawImage(image, dx, dy, dw, dh, dynamic, crop) {
|
|
519
|
+
this.flushRectBatch(); // preserve z-order: emit queued rect fills first
|
|
444
520
|
const tex = this.textureFor(image, dynamic ?? false);
|
|
445
521
|
if (!tex)
|
|
446
522
|
return;
|
|
@@ -461,7 +537,7 @@ export class WebGL2Target {
|
|
|
461
537
|
this.gl.enableVertexAttribArray(ip.aUV);
|
|
462
538
|
this.gl.vertexAttribPointer(ip.aUV, 2, this.gl.FLOAT, false, 16, 8);
|
|
463
539
|
// Project the drawn region through transform + viewport.
|
|
464
|
-
const projected =
|
|
540
|
+
const projected = applyMat({
|
|
465
541
|
a: this.transform.a * dw,
|
|
466
542
|
b: this.transform.b * dw,
|
|
467
543
|
c: this.transform.c * dh,
|
|
@@ -471,6 +547,17 @@ export class WebGL2Target {
|
|
|
471
547
|
}, this._size.width, this._size.height);
|
|
472
548
|
this.gl.uniformMatrix3fv(ip.uTransform, false, projected);
|
|
473
549
|
this.gl.uniform1f(ip.uOpacity, this.opacity);
|
|
550
|
+
// Crop as a UV sub-rect. Identity (no crop) is offset (0,0), scale (1,1);
|
|
551
|
+
// crop fractions are already in texture-UV [0,1] space (unlike Canvas2D,
|
|
552
|
+
// which multiplies by intrinsic pixels), so they map straight to uniforms.
|
|
553
|
+
if (crop && (crop.x !== 0 || crop.y !== 0 || crop.width !== 1 || crop.height !== 1)) {
|
|
554
|
+
this.gl.uniform2f(ip.uUvOffset, crop.x, crop.y);
|
|
555
|
+
this.gl.uniform2f(ip.uUvScale, crop.width, crop.height);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
this.gl.uniform2f(ip.uUvOffset, 0, 0);
|
|
559
|
+
this.gl.uniform2f(ip.uUvScale, 1, 1);
|
|
560
|
+
}
|
|
474
561
|
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
475
562
|
this.gl.bindTexture(this.gl.TEXTURE_2D, tex);
|
|
476
563
|
this.gl.uniform1i(ip.uTex, 0);
|
|
@@ -575,6 +662,7 @@ export class WebGL2Target {
|
|
|
575
662
|
// Ellipse path — single fragment-SDF quad regardless of radius.
|
|
576
663
|
// Vector-perfect at any zoom; 4 vertices instead of 24-512.
|
|
577
664
|
if (this.currentEllipse) {
|
|
665
|
+
this.flushRectBatch(); // preserve z-order: emit queued rects first
|
|
578
666
|
this.ellipsePipeline ??= new EllipsePipeline(this.gl);
|
|
579
667
|
const e = this.currentEllipse;
|
|
580
668
|
this.ellipsePipeline.draw(e.cx, e.cy, e.rx, e.ry, this.fillColor, effectiveAlpha, this.transform, this._size);
|
|
@@ -586,27 +674,30 @@ export class WebGL2Target {
|
|
|
586
674
|
// most shape backgrounds (rectangles) hit it.
|
|
587
675
|
if (this.currentPath) {
|
|
588
676
|
const r = this.currentPath;
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
677
|
+
// Project the rect's unit-quad → clip affine, then queue it as one
|
|
678
|
+
// instance instead of issuing a draw. Same math as `applyMat`
|
|
679
|
+
// (unit-quad → NDC) inlined into scalars — the batcher stores the
|
|
680
|
+
// two variable columns + translation; the constant [0,0,1] third
|
|
681
|
+
// column is reconstructed in the instance vertex shader.
|
|
682
|
+
const t = this.transform;
|
|
683
|
+
const sx = 2 / this._size.width;
|
|
684
|
+
const sy = -2 / this._size.height;
|
|
685
|
+
const pa = t.a * r.width;
|
|
686
|
+
const pb = t.b * r.width;
|
|
687
|
+
const pc = t.c * r.height;
|
|
688
|
+
const pd = t.d * r.height;
|
|
689
|
+
const pe = t.e + t.a * r.x + t.c * r.y;
|
|
690
|
+
const pf = t.f + t.b * r.x + t.d * r.y;
|
|
691
|
+
this.rectBatch.add(pa * sx, pb * sy, pc * sx, pd * sy, pe * sx - 1, pf * sy + 1, this.fillColor[0], this.fillColor[1], this.fillColor[2], effectiveAlpha);
|
|
602
692
|
return;
|
|
603
693
|
}
|
|
604
694
|
// Polygon path — assembled via moveTo / lineTo / bezierCurveTo.
|
|
605
695
|
// Triangulated through earcut so concave shapes (arrows, stars,
|
|
606
696
|
// lightning bolts) fill correctly. Earcut is dependency-free and
|
|
607
697
|
// handles holes too if ever needed.
|
|
608
|
-
if (this.
|
|
609
|
-
this.
|
|
698
|
+
if (this.pathPts >= 3) {
|
|
699
|
+
this.flushRectBatch(); // preserve z-order: emit queued rects first
|
|
700
|
+
this.fillPolygonEarcut(this.pathXY, this.pathPts, effectiveAlpha);
|
|
610
701
|
}
|
|
611
702
|
// Loop-Blinn curve overlay. Adds fragment-tested quadratic / cubic
|
|
612
703
|
// triangles on top of the polygon fill so curve regions
|
|
@@ -623,6 +714,7 @@ export class WebGL2Target {
|
|
|
623
714
|
// doesn't have. The artefact is invisible at 1× zoom and tiny even
|
|
624
715
|
// at 20×.
|
|
625
716
|
if (this.currentCurves.length > 0) {
|
|
717
|
+
this.flushRectBatch(); // preserve z-order: emit queued rects first
|
|
626
718
|
this.curvePipeline ??= new LoopBlinnCurvePipeline(this.gl);
|
|
627
719
|
this.curvePipeline.draw(this.currentCurves, this.fillColor, effectiveAlpha, this.transform, this._size);
|
|
628
720
|
this.restoreSolidProgram();
|
|
@@ -637,47 +729,36 @@ export class WebGL2Target {
|
|
|
637
729
|
* earcut returns an empty index list (degenerate self-intersecting
|
|
638
730
|
* polygon).
|
|
639
731
|
*/
|
|
640
|
-
fillPolygonEarcut(
|
|
732
|
+
fillPolygonEarcut(xy, pointCount, effectiveAlpha) {
|
|
641
733
|
// Skip the implicitly-closed duplicate last vertex if the caller
|
|
642
734
|
// already issued `closePath` — earcut would treat it as a degenerate
|
|
643
735
|
// sliver.
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
polyFirst.y === polyLast.y
|
|
650
|
-
? polyline.length - 1
|
|
651
|
-
: polyline.length;
|
|
736
|
+
const n = pointCount >= 4 &&
|
|
737
|
+
xy[0] === xy[(pointCount - 1) * 2] &&
|
|
738
|
+
xy[1] === xy[(pointCount - 1) * 2 + 1]
|
|
739
|
+
? pointCount - 1
|
|
740
|
+
: pointCount;
|
|
652
741
|
if (n < 3)
|
|
653
742
|
return;
|
|
654
|
-
// earcut wants a flat [x0, y0, x1, y1, ...] in world coords
|
|
655
|
-
//
|
|
656
|
-
// with [i] + length, so a
|
|
657
|
-
|
|
658
|
-
const flat = scratchEarcutFlat;
|
|
659
|
-
for (let i = 0; i < n; i++) {
|
|
660
|
-
const p = req(polyline[i]);
|
|
661
|
-
flat[i * 2] = p.x;
|
|
662
|
-
flat[i * 2 + 1] = p.y;
|
|
663
|
-
}
|
|
664
|
-
// Pass only the populated prefix — `subarray` is a view, no copy.
|
|
665
|
-
const indices = earcut(flat.subarray(0, n * 2));
|
|
743
|
+
// earcut wants a flat [x0, y0, x1, y1, ...] in world coords — the
|
|
744
|
+
// path buffer already is one, and earcut accepts any array-like
|
|
745
|
+
// with [i] + length, so pass a no-copy `subarray` view directly.
|
|
746
|
+
const indices = earcut(xy.subarray(0, n * 2));
|
|
666
747
|
if (indices.length === 0) {
|
|
667
748
|
// Pathological polygon — fall back to a fan so something renders.
|
|
668
|
-
this.drawTriangleFan(
|
|
749
|
+
this.drawTriangleFan(xy, n, effectiveAlpha);
|
|
669
750
|
return;
|
|
670
751
|
}
|
|
671
|
-
// Project once into clip space, then index-draw.
|
|
672
|
-
|
|
673
|
-
// grew `scratchEarcutVerts` if needed.
|
|
752
|
+
// Project once into clip space, then index-draw.
|
|
753
|
+
ensureEarcutVertexCapacity(n);
|
|
674
754
|
const sx = 2 / this._size.width;
|
|
675
755
|
const sy = -2 / this._size.height;
|
|
676
756
|
const verts = scratchEarcutVerts;
|
|
677
757
|
for (let i = 0; i < n; i++) {
|
|
678
|
-
const
|
|
679
|
-
const
|
|
680
|
-
const
|
|
758
|
+
const px = req(xy[i * 2]);
|
|
759
|
+
const py = req(xy[i * 2 + 1]);
|
|
760
|
+
const wx = this.transform.a * px + this.transform.c * py + this.transform.e;
|
|
761
|
+
const wy = this.transform.b * px + this.transform.d * py + this.transform.f;
|
|
681
762
|
verts[i * 2] = wx * sx - 1;
|
|
682
763
|
verts[i * 2 + 1] = wy * sy + 1;
|
|
683
764
|
}
|
|
@@ -715,16 +796,17 @@ export class WebGL2Target {
|
|
|
715
796
|
* convex polygons correctly; concave ones get a wrong silhouette (the
|
|
716
797
|
* earcut path handles those instead).
|
|
717
798
|
*/
|
|
718
|
-
drawTriangleFan(
|
|
799
|
+
drawTriangleFan(xy, n, effectiveAlpha) {
|
|
719
800
|
const sx = 2 / this._size.width;
|
|
720
801
|
const sy = -2 / this._size.height;
|
|
721
802
|
// Share the module-level scratch verts with `fillPolygonEarcut`.
|
|
722
803
|
ensureEarcutVertexCapacity(n);
|
|
723
804
|
const verts = scratchEarcutVerts;
|
|
724
805
|
for (let i = 0; i < n; i++) {
|
|
725
|
-
const
|
|
726
|
-
const
|
|
727
|
-
const
|
|
806
|
+
const px = req(xy[i * 2]);
|
|
807
|
+
const py = req(xy[i * 2 + 1]);
|
|
808
|
+
const wx = this.transform.a * px + this.transform.c * py + this.transform.e;
|
|
809
|
+
const wy = this.transform.b * px + this.transform.d * py + this.transform.f;
|
|
728
810
|
verts[i * 2] = wx * sx - 1;
|
|
729
811
|
verts[i * 2 + 1] = wy * sy + 1;
|
|
730
812
|
}
|
|
@@ -745,9 +827,9 @@ export class WebGL2Target {
|
|
|
745
827
|
* `bounds` wipes only the rectangle the editor's dirty-rect pass
|
|
746
828
|
* identified. Honouring `bounds` is mandatory — when the scene
|
|
747
829
|
* reference doesn't change, the editor sends a zero-area dirty rect
|
|
748
|
-
* and expects the previous frame to survive untouched.
|
|
830
|
+
* and expects the previous frame to survive untouched. The default
|
|
749
831
|
* `preserveDrawingBuffer: true` carries the persistent frame across
|
|
750
|
-
* composites.
|
|
832
|
+
* composites (see the constructor for the opt-out).
|
|
751
833
|
*
|
|
752
834
|
* For bounded clears the implementation flips on a scissor box so the
|
|
753
835
|
* clear is confined to the dirty rect, mirroring Canvas2D's
|
|
@@ -756,6 +838,10 @@ export class WebGL2Target {
|
|
|
756
838
|
* caller's top-left CSS-pixel rect.
|
|
757
839
|
*/
|
|
758
840
|
clear(bounds) {
|
|
841
|
+
// Drain queued rect fills before wiping pixels — a clear that lands
|
|
842
|
+
// mid-stream must not erase rects queued after it, nor let them
|
|
843
|
+
// survive a wipe meant to cover them.
|
|
844
|
+
this.flushRectBatch();
|
|
759
845
|
const bitmapW = this.gl.canvas.width;
|
|
760
846
|
const bitmapH = this.gl.canvas.height;
|
|
761
847
|
if (bounds) {
|
|
@@ -791,51 +877,69 @@ export class WebGL2Target {
|
|
|
791
877
|
this.strokeWidth = Math.max(0, width);
|
|
792
878
|
}
|
|
793
879
|
stroke() {
|
|
880
|
+
this.flushRectBatch(); // preserve z-order: emit queued rect fills first
|
|
794
881
|
if (this.currentPath) {
|
|
795
882
|
// Rect outline → 4 corners as a closed polyline.
|
|
796
883
|
const r = this.currentPath;
|
|
797
|
-
this.
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
];
|
|
884
|
+
this.pathPts = 0;
|
|
885
|
+
this.pushPathPoint(r.x, r.y);
|
|
886
|
+
this.pushPathPoint(r.x + r.width, r.y);
|
|
887
|
+
this.pushPathPoint(r.x + r.width, r.y + r.height);
|
|
888
|
+
this.pushPathPoint(r.x, r.y + r.height);
|
|
889
|
+
this.pushPathPoint(r.x, r.y);
|
|
804
890
|
}
|
|
805
891
|
// Ellipse outline — lazily generate the polyline approximation here
|
|
806
892
|
// so callers that only fill don't pay for the 24-512 vertex
|
|
807
893
|
// allocation. EllipsePipeline owns the fill path; stroke still goes
|
|
808
894
|
// through the polygon stroke pipeline.
|
|
809
|
-
if (this.currentEllipse && this.
|
|
895
|
+
if (this.currentEllipse && this.pathPts < 2) {
|
|
810
896
|
this.buildEllipseStrokePolyline(this.currentEllipse);
|
|
811
897
|
}
|
|
812
|
-
if (this.
|
|
898
|
+
if (this.pathPts < 2)
|
|
813
899
|
return;
|
|
814
900
|
const effectiveAlpha = this.opacity * this.strokeAlpha;
|
|
815
901
|
if (effectiveAlpha <= 0)
|
|
816
902
|
return; // transparent stroke — nothing to draw
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
903
|
+
const style = {
|
|
904
|
+
width: this.strokeWidth,
|
|
905
|
+
color: this.strokeColor,
|
|
906
|
+
opacity: effectiveAlpha,
|
|
907
|
+
join: this.lineJoin,
|
|
908
|
+
cap: this.lineCap,
|
|
909
|
+
};
|
|
910
|
+
if (this.dashArray) {
|
|
911
|
+
// Dashed: split the polyline into "on" sub-polylines in world
|
|
912
|
+
// units (Canvas2D dashes in the world-space ctx transform, so
|
|
913
|
+
// this matches it), then stroke each run. `dashPolyline` keeps
|
|
914
|
+
// its Vec2 contract, so materialise the flat path once — dashing
|
|
915
|
+
// is opt-in styling, the solid hot path below stays object-free.
|
|
916
|
+
const pts = new Array(this.pathPts);
|
|
917
|
+
for (let i = 0; i < this.pathPts; i++) {
|
|
918
|
+
pts[i] = { x: req(this.pathXY[i * 2]), y: req(this.pathXY[i * 2 + 1]) };
|
|
919
|
+
}
|
|
920
|
+
for (const run of dashPolyline(pts, this.dashArray)) {
|
|
921
|
+
if (run.length < 2)
|
|
922
|
+
continue;
|
|
923
|
+
ensureDashRunCapacity(run.length * 2);
|
|
924
|
+
for (let i = 0; i < run.length; i++) {
|
|
925
|
+
const p = req(run[i]);
|
|
926
|
+
scratchDashRunXY[i * 2] = p.x;
|
|
927
|
+
scratchDashRunXY[i * 2 + 1] = p.y;
|
|
928
|
+
}
|
|
929
|
+
this.strokePolylineFlat(scratchDashRunXY, run.length, style);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
this.strokePolylineFlat(this.pathXY, this.pathPts, style);
|
|
834
934
|
}
|
|
835
935
|
// Stroke wrote into the dynamic VBO; rebind the static unit-quad VBO
|
|
836
936
|
// so the next solid rect fill picks up the right vertex stream.
|
|
837
937
|
this.restoreSolidProgram();
|
|
838
938
|
}
|
|
939
|
+
/** Route one flat polyline through the shared stroke pipeline. */
|
|
940
|
+
strokePolylineFlat(xy, pointCount, style) {
|
|
941
|
+
drawPolylineStrokeImpl(this.gl, xy, pointCount, style, this.transform, this._size, this.program, this.uTransformLoc, this.uColorLoc, this.uOpacityLoc, this.dynamicVbo, this.aPosLoc, IDENTITY_MAT3);
|
|
942
|
+
}
|
|
839
943
|
// --- Stroke style state (consumed by stroke()) ---
|
|
840
944
|
setLineCap(cap) {
|
|
841
945
|
this.lineCap = cap;
|
|
@@ -873,6 +977,7 @@ export class WebGL2Target {
|
|
|
873
977
|
fillText(text, x, y, maxWidth) {
|
|
874
978
|
if (text.length === 0)
|
|
875
979
|
return;
|
|
980
|
+
this.flushRectBatch(); // preserve z-order: emit queued rect fills first
|
|
876
981
|
void maxWidth;
|
|
877
982
|
const atlas = this.ensureGlyphAtlas();
|
|
878
983
|
if (atlas) {
|
|
@@ -936,16 +1041,11 @@ export class WebGL2Target {
|
|
|
936
1041
|
// it measures the run and shifts via the transform), so no separate
|
|
937
1042
|
// width-measuring pass here.
|
|
938
1043
|
const alignFactor = this.textAlign === "center" ? 0.5 : this.textAlign === "right" ? 1 : 0;
|
|
939
|
-
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
|
|
944
|
-
if (this.textBaseline === "top")
|
|
945
|
-
py += this.fontSize;
|
|
946
|
-
else if (this.textBaseline === "middle")
|
|
947
|
-
py += this.fontSize / 2;
|
|
948
|
-
// baseline=bottom uses py as-is (cursor sits on the baseline).
|
|
1044
|
+
// The MSDF quad math places glyphs relative to the font baseline, so
|
|
1045
|
+
// convert the requested `textBaseline` into a baseline `y` using the same
|
|
1046
|
+
// browser metrics Canvas2D honours (measured on a hidden 2D context), so
|
|
1047
|
+
// the two backends place text at exactly the same height.
|
|
1048
|
+
const py = y + this.baselineOffsetForBaseline();
|
|
949
1049
|
this.msdfPipeline.drawText(text, x, py, this.fontSize, atlas, {
|
|
950
1050
|
opacity: this.opacity,
|
|
951
1051
|
color: this.fillColor,
|
|
@@ -969,6 +1069,33 @@ export class WebGL2Target {
|
|
|
969
1069
|
this.gl.enableVertexAttribArray(this.aPosLoc);
|
|
970
1070
|
this.gl.vertexAttribPointer(this.aPosLoc, 2, this.gl.FLOAT, false, 0, 0);
|
|
971
1071
|
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Draw any queued sharp-rect fill instances as one instanced call,
|
|
1074
|
+
* then restore the solid-program plumbing. Called before every
|
|
1075
|
+
* non-batchable draw and surface op to keep z-order intact, and from
|
|
1076
|
+
* {@link flushBatch} at frame end. No-op when the queue is empty.
|
|
1077
|
+
*/
|
|
1078
|
+
flushRectBatch() {
|
|
1079
|
+
if (this.rectBatch.pending === 0)
|
|
1080
|
+
return;
|
|
1081
|
+
this.rectPipeline ??= new RectInstancePipeline(this.gl);
|
|
1082
|
+
const pipeline = this.rectPipeline;
|
|
1083
|
+
this.rectBatch.flush((data, count) => {
|
|
1084
|
+
pipeline.draw(data, count);
|
|
1085
|
+
});
|
|
1086
|
+
this.restoreSolidProgram();
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Flush the deferred sharp-rect batch to the GPU. The host's
|
|
1090
|
+
* `LayeredSurface.present()` calls this once per frame after the
|
|
1091
|
+
* Editor finishes drawing, so trailing rect fills reach the
|
|
1092
|
+
* framebuffer within the frame that queued them. Backends without a
|
|
1093
|
+
* batcher (Canvas2D) have no equivalent — this is WebGL2-specific and
|
|
1094
|
+
* not part of the `RenderTarget` interface.
|
|
1095
|
+
*/
|
|
1096
|
+
flushBatch() {
|
|
1097
|
+
this.flushRectBatch();
|
|
1098
|
+
}
|
|
972
1099
|
measureText(text) {
|
|
973
1100
|
return this.textMetrics(text);
|
|
974
1101
|
}
|
|
@@ -1001,7 +1128,45 @@ export class WebGL2Target {
|
|
|
1001
1128
|
return this.textCtx;
|
|
1002
1129
|
}
|
|
1003
1130
|
textFontSpec() {
|
|
1004
|
-
|
|
1131
|
+
// CSS font shorthand order: `<style> <weight> <size> <family>` — must
|
|
1132
|
+
// carry weight/style so the no-MSDF fallback (OffscreenCanvas bitmaps)
|
|
1133
|
+
// draws bold/italic like the MSDF path and the Canvas2D backend do.
|
|
1134
|
+
// It also keys the bitmap cache, so a bold word can't collide with the
|
|
1135
|
+
// regular one (which would render regular while colour still applied).
|
|
1136
|
+
// Bundled face first so the fallback matches the MSDF/Canvas2D metrics.
|
|
1137
|
+
const style = this.fontStyle === "italic" ? "italic " : "";
|
|
1138
|
+
const weight = this.fontWeight === "bold" ? "bold " : "";
|
|
1139
|
+
return `${style}${weight}${this.fontSize}px "${resolveBundledFamily(this.fontFamily)}", ${this.fontFamily}`;
|
|
1140
|
+
}
|
|
1141
|
+
baselineOffsetCache = new Map();
|
|
1142
|
+
/**
|
|
1143
|
+
* Distance (px) from the line of the active `textBaseline` down to the
|
|
1144
|
+
* alphabetic baseline, for the current font. Measured on a hidden 2D
|
|
1145
|
+
* context so it uses the exact same browser font metrics Canvas2D applies
|
|
1146
|
+
* when it honours `textBaseline` — keeping the MSDF text at the same
|
|
1147
|
+
* vertical position as the Canvas2D backend. Falls back to a proportional
|
|
1148
|
+
* estimate where measurement isn't available.
|
|
1149
|
+
*/
|
|
1150
|
+
baselineOffsetForBaseline() {
|
|
1151
|
+
const ctx = this.ensureTextCtx();
|
|
1152
|
+
if (!ctx)
|
|
1153
|
+
return this.fontSize * 0.8;
|
|
1154
|
+
const spec = this.textFontSpec();
|
|
1155
|
+
const key = `${spec}|${this.textBaseline}`;
|
|
1156
|
+
const cached = this.baselineOffsetCache.get(key);
|
|
1157
|
+
if (cached !== undefined)
|
|
1158
|
+
return cached;
|
|
1159
|
+
ctx.font = spec;
|
|
1160
|
+
ctx.textBaseline = "alphabetic";
|
|
1161
|
+
const alphaDescent = ctx.measureText("Mg").fontBoundingBoxDescent;
|
|
1162
|
+
ctx.textBaseline = this.textBaseline;
|
|
1163
|
+
const thisDescent = ctx.measureText("Mg").fontBoundingBoxDescent;
|
|
1164
|
+
// `fontBoundingBox*` is absent on a few old engines — fall back there.
|
|
1165
|
+
const offset = Number.isFinite(alphaDescent) && Number.isFinite(thisDescent)
|
|
1166
|
+
? thisDescent - alphaDescent
|
|
1167
|
+
: this.fontSize * 0.8;
|
|
1168
|
+
this.baselineOffsetCache.set(key, offset);
|
|
1169
|
+
return offset;
|
|
1005
1170
|
}
|
|
1006
1171
|
textMetrics(text) {
|
|
1007
1172
|
// When the MSDF path is active, measure with the same atlas glyph
|
|
@@ -1012,17 +1177,11 @@ export class WebGL2Target {
|
|
|
1012
1177
|
const atlas = this.ensureGlyphAtlas();
|
|
1013
1178
|
if (atlas) {
|
|
1014
1179
|
const fontId = atlas.resolveFontId(this.fontFamily, this.fontWeight === "bold", this.fontStyle === "italic");
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
const glyph = atlas.getOrRasterize(cp, fontId);
|
|
1021
|
-
if (!glyph)
|
|
1022
|
-
continue;
|
|
1023
|
-
w += (glyph.advance * this.fontSize) / glyph.unitsPerEm;
|
|
1024
|
-
}
|
|
1025
|
-
return { width: w };
|
|
1180
|
+
// Shared single-pass, memoized walk — same advances `fillTextMSDF`
|
|
1181
|
+
// lays out (`advance * fontSize / unitsPerEm`), so measured width
|
|
1182
|
+
// and drawn width stay 1:1. em-width is fontSize-independent; scale
|
|
1183
|
+
// here. A measure after the same run was drawn hits the memo.
|
|
1184
|
+
return { width: measureGlyphRunEm(text, atlas, fontId) * this.fontSize };
|
|
1026
1185
|
}
|
|
1027
1186
|
// Fallback (no MSDF shaper): Canvas2D system-font measurement, which
|
|
1028
1187
|
// matches the Canvas2D bitmap text path used in that case.
|
|
@@ -1164,19 +1323,37 @@ export const dashPolyline = (pts, pattern) => {
|
|
|
1164
1323
|
* and never shrinks; safe for single-threaded WebGL (fill calls are
|
|
1165
1324
|
* serialised through the editor's render path).
|
|
1166
1325
|
*/
|
|
1167
|
-
let scratchEarcutFlat = new Float64Array(128);
|
|
1168
1326
|
let scratchEarcutVerts = new Float32Array(128);
|
|
1169
1327
|
let scratchEarcutIndices = new Uint16Array(256);
|
|
1170
1328
|
const ensureEarcutVertexCapacity = (vertexCount) => {
|
|
1171
1329
|
const needed = vertexCount * 2;
|
|
1172
|
-
if (
|
|
1330
|
+
if (scratchEarcutVerts.length >= needed)
|
|
1173
1331
|
return;
|
|
1174
|
-
let cap =
|
|
1332
|
+
let cap = scratchEarcutVerts.length;
|
|
1175
1333
|
while (cap < needed)
|
|
1176
1334
|
cap *= 2;
|
|
1177
|
-
scratchEarcutFlat = new Float64Array(cap);
|
|
1178
1335
|
scratchEarcutVerts = new Float32Array(cap);
|
|
1179
1336
|
};
|
|
1337
|
+
/**
|
|
1338
|
+
* Module-level scratch for converting one dashed-stroke run back to the
|
|
1339
|
+
* flat layout `drawPolylineStroke` consumes. Only the dashed path uses
|
|
1340
|
+
* it, and runs are consumed synchronously one at a time.
|
|
1341
|
+
*/
|
|
1342
|
+
let scratchDashRunXY = new Float64Array(128);
|
|
1343
|
+
const ensureDashRunCapacity = (n) => {
|
|
1344
|
+
if (scratchDashRunXY.length >= n)
|
|
1345
|
+
return;
|
|
1346
|
+
let cap = scratchDashRunXY.length;
|
|
1347
|
+
while (cap < n)
|
|
1348
|
+
cap *= 2;
|
|
1349
|
+
scratchDashRunXY = new Float64Array(cap);
|
|
1350
|
+
};
|
|
1351
|
+
/**
|
|
1352
|
+
* Initial per-target flat path buffer capacity, in points. 128 covers a
|
|
1353
|
+
* rounded rect (≈ 60-100 flattened vertices at 1×) without a grow;
|
|
1354
|
+
* capacity doubles on demand and never shrinks.
|
|
1355
|
+
*/
|
|
1356
|
+
const INITIAL_PATH_CAPACITY = 128;
|
|
1180
1357
|
const ensureEarcutIndexCapacity = (n) => {
|
|
1181
1358
|
if (scratchEarcutIndices.length >= n)
|
|
1182
1359
|
return;
|
|
@@ -1185,14 +1362,6 @@ const ensureEarcutIndexCapacity = (n) => {
|
|
|
1185
1362
|
cap *= 2;
|
|
1186
1363
|
scratchEarcutIndices = new Uint16Array(cap);
|
|
1187
1364
|
};
|
|
1188
|
-
/**
|
|
1189
|
-
* Lower / upper bounds on the polygon approximation of an ellipse. The
|
|
1190
|
-
* minimum keeps small ellipses from collapsing to a hexagon at far zoom;
|
|
1191
|
-
* the maximum caps GPU work for huge ellipses where the marginal
|
|
1192
|
-
* pixel-error improvement is invisible.
|
|
1193
|
-
*/
|
|
1194
|
-
const ELLIPSE_MIN_SEGMENTS = 24;
|
|
1195
|
-
const ELLIPSE_MAX_SEGMENTS = 512;
|
|
1196
1365
|
const IDENTITY_MAT3 = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
1197
1366
|
/**
|
|
1198
1367
|
* Flatten tolerance, computed per-call: 0.5 / zoom in screen pixels.
|
|
@@ -1219,65 +1388,23 @@ const isMsdfShaper = (shaper) => {
|
|
|
1219
1388
|
* proportional to it, used to pick a JS-fallback sample count
|
|
1220
1389
|
* commensurate with the tolerance.
|
|
1221
1390
|
*/
|
|
1222
|
-
const curveLengthEstimate = (
|
|
1223
|
-
/** Sample a quadratic Bezier curve at `count` evenly-spaced t values. */
|
|
1224
|
-
const sampleQuadratic = (p0, p1, p2, count) => {
|
|
1225
|
-
const out = [];
|
|
1226
|
-
for (let i = 0; i <= count; i++) {
|
|
1227
|
-
const t = i / count;
|
|
1228
|
-
const u = 1 - t;
|
|
1229
|
-
out.push({
|
|
1230
|
-
x: u * u * p0.x + 2 * u * t * p1.x + t * t * p2.x,
|
|
1231
|
-
y: u * u * p0.y + 2 * u * t * p1.y + t * t * p2.y,
|
|
1232
|
-
});
|
|
1233
|
-
}
|
|
1234
|
-
return out;
|
|
1235
|
-
};
|
|
1236
|
-
/** Sample a cubic Bezier curve at `count` evenly-spaced t values. */
|
|
1237
|
-
const sampleCubic = (p0, p1, p2, p3, count) => {
|
|
1238
|
-
const out = [];
|
|
1239
|
-
for (let i = 0; i <= count; i++) {
|
|
1240
|
-
const t = i / count;
|
|
1241
|
-
const u = 1 - t;
|
|
1242
|
-
const u2 = u * u;
|
|
1243
|
-
const u3 = u2 * u;
|
|
1244
|
-
const t2 = t * t;
|
|
1245
|
-
const t3 = t2 * t;
|
|
1246
|
-
out.push({
|
|
1247
|
-
x: u3 * p0.x + 3 * u2 * t * p1.x + 3 * u * t2 * p2.x + t3 * p3.x,
|
|
1248
|
-
y: u3 * p0.y + 3 * u2 * t * p1.y + 3 * u * t2 * p2.y + t3 * p3.y,
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
return out;
|
|
1252
|
-
};
|
|
1253
|
-
/** Same projection as `applyMat` but emitted from drawImage. */
|
|
1254
|
-
const applyImageMat = (t, w, h) => {
|
|
1255
|
-
const sx = 2 / w;
|
|
1256
|
-
const sy = -2 / h;
|
|
1257
|
-
return new Float32Array([
|
|
1258
|
-
t.a * sx,
|
|
1259
|
-
t.b * sy,
|
|
1260
|
-
0,
|
|
1261
|
-
t.c * sx,
|
|
1262
|
-
t.d * sy,
|
|
1263
|
-
0,
|
|
1264
|
-
t.e * sx - 1,
|
|
1265
|
-
t.f * sy + 1,
|
|
1266
|
-
1,
|
|
1267
|
-
]);
|
|
1268
|
-
};
|
|
1391
|
+
const curveLengthEstimate = (ax, ay, bx, by) => Math.hypot(ax - bx, ay - by);
|
|
1269
1392
|
const createImageProgram = (gl) => {
|
|
1270
|
-
const vert =
|
|
1393
|
+
const vert = compileShader(gl, gl.VERTEX_SHADER, `#version 300 es
|
|
1271
1394
|
in vec2 aPos;
|
|
1272
1395
|
in vec2 aUV;
|
|
1273
1396
|
uniform mat3 uTransform;
|
|
1397
|
+
uniform vec2 uUvOffset;
|
|
1398
|
+
uniform vec2 uUvScale;
|
|
1274
1399
|
out vec2 vUV;
|
|
1275
1400
|
void main() {
|
|
1276
1401
|
vec3 p = uTransform * vec3(aPos, 1.0);
|
|
1277
1402
|
gl_Position = vec4(p.xy, 0.0, 1.0);
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1403
|
+
// Crop: map the unit-quad UV into the source sub-rect. Identity is
|
|
1404
|
+
// offset (0,0) + scale (1,1); a crop narrows it to the kept region.
|
|
1405
|
+
vUV = aUV * uUvScale + uUvOffset;
|
|
1406
|
+
}`, "WebGL2");
|
|
1407
|
+
const frag = compileShader(gl, gl.FRAGMENT_SHADER, `#version 300 es
|
|
1281
1408
|
precision mediump float;
|
|
1282
1409
|
in vec2 vUV;
|
|
1283
1410
|
uniform sampler2D uTex;
|
|
@@ -1290,8 +1417,8 @@ void main() {
|
|
|
1290
1417
|
// channels by the per-call opacity.
|
|
1291
1418
|
vec4 t = texture(uTex, vUV);
|
|
1292
1419
|
fragColor = vec4(t.rgb * uOpacity, t.a * uOpacity);
|
|
1293
|
-
}
|
|
1294
|
-
const program =
|
|
1420
|
+
}`, "WebGL2");
|
|
1421
|
+
const program = linkProgram(gl, vert, frag, "WebGL2");
|
|
1295
1422
|
return {
|
|
1296
1423
|
program,
|
|
1297
1424
|
aPos: gl.getAttribLocation(program, "aPos"),
|
|
@@ -1299,28 +1426,38 @@ void main() {
|
|
|
1299
1426
|
uTransform: gl.getUniformLocation(program, "uTransform"),
|
|
1300
1427
|
uTex: gl.getUniformLocation(program, "uTex"),
|
|
1301
1428
|
uOpacity: gl.getUniformLocation(program, "uOpacity"),
|
|
1429
|
+
uUvOffset: gl.getUniformLocation(program, "uUvOffset"),
|
|
1430
|
+
uUvScale: gl.getUniformLocation(program, "uUvScale"),
|
|
1302
1431
|
};
|
|
1303
1432
|
};
|
|
1433
|
+
/**
|
|
1434
|
+
* Module-level scratch for `applyMat` — the same reuse pattern as the
|
|
1435
|
+
* earcut / stroke scratch buffers above. The projected mat3 is consumed
|
|
1436
|
+
* synchronously by `uniformMatrix3fv` (which copies the values into GL
|
|
1437
|
+
* state) before the next `applyMat` call, so one shared buffer avoids a
|
|
1438
|
+
* Float32Array allocation per rect-fill / drawImage.
|
|
1439
|
+
*/
|
|
1440
|
+
const scratchMat3 = new Float32Array(9);
|
|
1304
1441
|
/**
|
|
1305
1442
|
* Build a 3×3 column-major matrix that maps a unit quad [0,0]–[1,1]
|
|
1306
1443
|
* through the supplied 2D affine + a screen-to-clip conversion
|
|
1307
|
-
* (pixels → NDC).
|
|
1444
|
+
* (pixels → NDC). Returns the module-level scratch — consume it before
|
|
1445
|
+
* the next call.
|
|
1308
1446
|
*/
|
|
1309
1447
|
const applyMat = (t, w, h) => {
|
|
1310
1448
|
// Pixel-space → clip-space: x' = (x / w) * 2 - 1; y' = 1 - (y / h) * 2.
|
|
1311
1449
|
const sx = 2 / w;
|
|
1312
1450
|
const sy = -2 / h;
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
]);
|
|
1451
|
+
scratchMat3[0] = t.a * sx;
|
|
1452
|
+
scratchMat3[1] = t.b * sy;
|
|
1453
|
+
scratchMat3[2] = 0;
|
|
1454
|
+
scratchMat3[3] = t.c * sx;
|
|
1455
|
+
scratchMat3[4] = t.d * sy;
|
|
1456
|
+
scratchMat3[5] = 0;
|
|
1457
|
+
scratchMat3[6] = t.e * sx - 1;
|
|
1458
|
+
scratchMat3[7] = t.f * sy + 1;
|
|
1459
|
+
scratchMat3[8] = 1;
|
|
1460
|
+
return scratchMat3;
|
|
1324
1461
|
};
|
|
1325
1462
|
const VERTEX_SHADER = `#version 300 es
|
|
1326
1463
|
in vec2 aPos;
|
|
@@ -1339,42 +1476,4 @@ void main() {
|
|
|
1339
1476
|
// premultipliedAlpha:true contract + blendFunc(ONE, 1-SRC_ALPHA).
|
|
1340
1477
|
fragColor = vec4(uColor * uOpacity, uOpacity);
|
|
1341
1478
|
}`;
|
|
1342
|
-
/**
|
|
1343
|
-
* Asserts a WebGL resource handle is non-null. Creation APIs are typed
|
|
1344
|
-
* non-null but return `null` on context loss; surface that as a throw.
|
|
1345
|
-
*/
|
|
1346
|
-
const glReq = (v) => {
|
|
1347
|
-
if (v === null)
|
|
1348
|
-
throw new Error("packages/renderer-canvas: WebGL resource creation failed");
|
|
1349
|
-
return v;
|
|
1350
|
-
};
|
|
1351
|
-
/** Asserts an array index is in range (loop bounds guarantee it). */
|
|
1352
|
-
const req = (v) => {
|
|
1353
|
-
if (v === undefined)
|
|
1354
|
-
throw new Error("packages/renderer-canvas: index out of range");
|
|
1355
|
-
return v;
|
|
1356
|
-
};
|
|
1357
|
-
const compile = (gl, type, src) => {
|
|
1358
|
-
const sh = glReq(gl.createShader(type));
|
|
1359
|
-
gl.shaderSource(sh, src);
|
|
1360
|
-
gl.compileShader(sh);
|
|
1361
|
-
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
|
1362
|
-
const log = gl.getShaderInfoLog(sh);
|
|
1363
|
-
gl.deleteShader(sh);
|
|
1364
|
-
throw new Error(`Shader compile failed: ${log}`);
|
|
1365
|
-
}
|
|
1366
|
-
return sh;
|
|
1367
|
-
};
|
|
1368
|
-
const link = (gl, vert, frag) => {
|
|
1369
|
-
const program = gl.createProgram();
|
|
1370
|
-
gl.attachShader(program, vert);
|
|
1371
|
-
gl.attachShader(program, frag);
|
|
1372
|
-
gl.linkProgram(program);
|
|
1373
|
-
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
1374
|
-
const log = gl.getProgramInfoLog(program);
|
|
1375
|
-
gl.deleteProgram(program);
|
|
1376
|
-
throw new Error(`Program link failed: ${log}`);
|
|
1377
|
-
}
|
|
1378
|
-
return program;
|
|
1379
|
-
};
|
|
1380
1479
|
//# sourceMappingURL=webgl2-target.js.map
|