react-x11 2.13.0 → 2.15.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/package.json +11 -8
- package/src/Reconciler.js +34 -21
- package/src/components/Select.js +8 -2
- package/src/events.js +8 -2
- package/src/glnodes.js +15 -3
- package/src/index.d.ts +5 -0
- package/src/nodes/boxpaint.js +9 -0
- package/src/nodes/preedit.js +64 -14
- package/src/ntk.js +10 -1
- package/src/scale.js +52 -22
- package/src/screencolor.js +104 -17
- package/src/types/system.d.ts +14 -1
- package/src/wayland/app.js +574 -0
- package/src/wayland/backendwindow.js +1199 -0
- package/src/wayland/clipboard.js +326 -0
- package/src/wayland/connection.js +485 -0
- package/src/wayland/context2d.js +2290 -0
- package/src/wayland/decorations.js +476 -0
- package/src/wayland/device.js +55 -0
- package/src/wayland/dmabuf.js +89 -0
- package/src/wayland/dnd.js +581 -0
- package/src/wayland/fdutil.js +108 -0
- package/src/wayland/framestyle.js +257 -0
- package/src/wayland/framewatch.js +190 -0
- package/src/wayland/glarea.js +372 -0
- package/src/wayland/glcontext.js +415 -0
- package/src/wayland/glyphatlas.js +237 -0
- package/src/wayland/input.js +417 -0
- package/src/wayland/keysymnames.js +35 -0
- package/src/wayland/layershell.js +363 -0
- package/src/wayland/outputs.js +601 -0
- package/src/wayland/protocols/cursor-shape-v1.json +1 -0
- package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
- package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
- package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
- package/src/wayland/protocols/fractional-scale-v1.json +1 -0
- package/src/wayland/protocols/index.json +131 -0
- package/src/wayland/protocols/kde-server-decoration.json +1 -0
- package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
- package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
- package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
- package/src/wayland/protocols/presentation-time.json +1 -0
- package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
- package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
- package/src/wayland/protocols/tablet-v2.json +1 -0
- package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
- package/src/wayland/protocols/viewporter.json +1 -0
- package/src/wayland/protocols/wayland.json +1 -0
- package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
- package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-activation-v1.json +1 -0
- package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-shell.json +1 -0
- package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
- package/src/wayland/readback.js +99 -0
- package/src/wayland/screencopy.js +584 -0
- package/src/wayland/seat.js +584 -0
- package/src/wayland/shm.js +226 -0
- package/src/wayland/ssd.js +234 -0
- package/src/wayland/surface.js +141 -0
- package/src/wayland/swapchain.js +411 -0
- package/src/wayland/tablet.js +522 -0
- package/src/wayland/target.js +269 -0
- package/src/wayland/text.js +113 -0
- package/src/wayland/textinput.js +671 -0
- package/src/wayland/touch.js +284 -0
- package/src/wayland/window.js +854 -0
- package/src/wayland/xkb.js +425 -0
- package/src/windowstate.js +41 -1
|
@@ -0,0 +1,2290 @@
|
|
|
1
|
+
// Tier D's 2d context: canvas-shaped drawing that ends up as GLES draw calls.
|
|
2
|
+
//
|
|
3
|
+
// This is the piece the RFC calls the largest workstream, and the shape it
|
|
4
|
+
// takes here is worth stating because it is not the obvious one.
|
|
5
|
+
//
|
|
6
|
+
// **Analytic shapes first, stencil second.** A rounded rectangle — which is
|
|
7
|
+
// most of what a UI draws — is a signed distance field evaluated in the
|
|
8
|
+
// fragment shader: antialiased without multisampling, and batched, so every
|
|
9
|
+
// box in a window can share one draw call. Arbitrary paths (SVG, custom
|
|
10
|
+
// canvas drawing) go the conventional way, stencil-then-cover: the flattened
|
|
11
|
+
// path's winding is counted into the stencil attachment of the render target
|
|
12
|
+
// (target.js), then a quad over its bounds is drawn wherever the count is
|
|
13
|
+
// non-zero. That path is aliased for now; the stencil bits it needs came with
|
|
14
|
+
// x11-dri 0.8.0 and the target's own renderbuffer.
|
|
15
|
+
//
|
|
16
|
+
// **One shader, batched by mode.** Vertices carry position, texture
|
|
17
|
+
// coordinates, a premultiplied colour and four shape parameters; per-batch
|
|
18
|
+
// uniforms pick how the fragment shader reads them. A batch flushes when the
|
|
19
|
+
// mode, the texture, the texture's byte layout or the scissor changes, which
|
|
20
|
+
// for a widget UI means a handful of draw calls for a whole window.
|
|
21
|
+
//
|
|
22
|
+
// **Transforms on the CPU.** The matrix is applied as vertices are emitted
|
|
23
|
+
// rather than sent as a uniform, so a translate does not break a batch. A UI
|
|
24
|
+
// emits thousands of vertices per frame, not millions.
|
|
25
|
+
//
|
|
26
|
+
// **Clipping is the scissor.** Rectangular clips — a scroll pane, a list row,
|
|
27
|
+
// a text field — are exact. A non-rectangular clip falls back to its bounding
|
|
28
|
+
// box, which over-paints rather than under-paints; `clipApproximations`
|
|
29
|
+
// counts them.
|
|
30
|
+
//
|
|
31
|
+
// **Three pixel layouts, one texture path.** ntk's `Image` is premultiplied
|
|
32
|
+
// BGRA (it was made for XRender), canvas `ImageData` is straight RGBA (the
|
|
33
|
+
// spec), and a render target is premultiplied RGBA (ours). Converting on the
|
|
34
|
+
// CPU would cost a pass per upload; the fragment shader swizzles instead,
|
|
35
|
+
// selected per batch.
|
|
36
|
+
//
|
|
37
|
+
// **Text is ntk's up to the last inch.** `drawGlyphs` is the contract
|
|
38
|
+
// `TextLayout.draw` calls, and everything before it — matching, shaping,
|
|
39
|
+
// bidi, line breaking — is the same code the X11 backend runs. Only the
|
|
40
|
+
// composite changes: an A8 atlas texture instead of a server glyph set.
|
|
41
|
+
|
|
42
|
+
import { createRequire } from 'node:module';
|
|
43
|
+
import path from 'node:path';
|
|
44
|
+
import { pathToFileURL } from 'node:url';
|
|
45
|
+
import { Path2D, Image as NtkImage } from 'ntk';
|
|
46
|
+
import { glDevice } from './device.js';
|
|
47
|
+
import { GlyphAtlas } from './glyphatlas.js';
|
|
48
|
+
import { GLTarget } from './target.js';
|
|
49
|
+
|
|
50
|
+
// ntk's exports map hides lib/path.js, and `flattenPath` is what turns a
|
|
51
|
+
// Path2D into device-space polygons — reach it by file.
|
|
52
|
+
const require = createRequire(import.meta.url);
|
|
53
|
+
const ntkLib = path.dirname(require.resolve('ntk'));
|
|
54
|
+
const { flattenPath } = await import(
|
|
55
|
+
pathToFileURL(path.join(ntkLib, 'path.js'))
|
|
56
|
+
);
|
|
57
|
+
const { rasterizePolys } = await import(
|
|
58
|
+
pathToFileURL(path.join(ntkLib, 'rasterize.js'))
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const MODE_SOLID = 0;
|
|
62
|
+
const MODE_TEXTURE = 1;
|
|
63
|
+
const MODE_GLYPH = 2;
|
|
64
|
+
const MODE_RRECT = 3;
|
|
65
|
+
const MODE_GRADIENT = 4;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The stencil buffer, shared between two uses: bit 7 is the clip (set where
|
|
69
|
+
* drawing may land while a non-rectangular clip is in force) and the low
|
|
70
|
+
* seven bits are the winding count stencil-then-cover works in. A path's
|
|
71
|
+
* passes write with `WINDING_MASK` and never disturb the clip; the clip's
|
|
72
|
+
* passes write with `CLIP_BIT` and leave the counts to their cleanup.
|
|
73
|
+
*/
|
|
74
|
+
const CLIP_BIT = 0x80;
|
|
75
|
+
const WINDING_MASK = 0x7f;
|
|
76
|
+
/**
|
|
77
|
+
* The largest fill the CPU rasteriser takes (see `_fillCoverage`): a
|
|
78
|
+
* 512×512 raster is well under a millisecond; a screen-sized one is not, and
|
|
79
|
+
* it goes through the stencil instead.
|
|
80
|
+
*/
|
|
81
|
+
const COVERAGE_MAX_PIXELS = 512 * 512;
|
|
82
|
+
|
|
83
|
+
/** `uSwizzle`: how the bound texture's bytes are to be read. */
|
|
84
|
+
const SWIZZLE_RGBA = 0; // premultiplied RGBA (render targets)
|
|
85
|
+
const SWIZZLE_BGRA = 1; // premultiplied BGRA (ntk Image)
|
|
86
|
+
const SWIZZLE_STRAIGHT = 2; // straight RGBA (ImageData)
|
|
87
|
+
|
|
88
|
+
/** floats per vertex: pos(2) uv(2) color(4) params(4) */
|
|
89
|
+
const STRIDE = 12;
|
|
90
|
+
const MAX_VERTS = 6 * 4096;
|
|
91
|
+
const TRI = [0, 1, 2, 0, 2, 3];
|
|
92
|
+
|
|
93
|
+
const VERT = `
|
|
94
|
+
attribute vec2 aPos;
|
|
95
|
+
attribute vec2 aUV;
|
|
96
|
+
attribute vec4 aColor;
|
|
97
|
+
attribute vec4 aParams;
|
|
98
|
+
uniform vec2 uViewport;
|
|
99
|
+
varying vec2 vUV;
|
|
100
|
+
varying vec4 vColor;
|
|
101
|
+
varying vec4 vParams;
|
|
102
|
+
void main() {
|
|
103
|
+
vUV = aUV;
|
|
104
|
+
vColor = aColor;
|
|
105
|
+
vParams = aParams;
|
|
106
|
+
// Device pixels (y down, origin top-left) to clip space (y up).
|
|
107
|
+
gl_Position = vec4(aPos.x / uViewport.x * 2.0 - 1.0,
|
|
108
|
+
1.0 - aPos.y / uViewport.y * 2.0, 0.0, 1.0);
|
|
109
|
+
}`;
|
|
110
|
+
|
|
111
|
+
const FRAG = `
|
|
112
|
+
precision mediump float;
|
|
113
|
+
varying vec2 vUV;
|
|
114
|
+
varying vec4 vColor;
|
|
115
|
+
varying vec4 vParams;
|
|
116
|
+
uniform int uMode;
|
|
117
|
+
uniform int uSwizzle;
|
|
118
|
+
uniform sampler2D uTex;
|
|
119
|
+
// highp to match the vertex stage's default: GLSL ES links a uniform used in
|
|
120
|
+
// both stages only when their precisions agree, and device coordinates past
|
|
121
|
+
// 2048 need the range anyway.
|
|
122
|
+
uniform highp vec2 uViewport;
|
|
123
|
+
uniform int uGradKind;
|
|
124
|
+
uniform vec4 uGradA;
|
|
125
|
+
uniform vec4 uGradB;
|
|
126
|
+
|
|
127
|
+
float sdRoundBox(vec2 p, vec2 b, float r) {
|
|
128
|
+
vec2 q = abs(p) - b + r;
|
|
129
|
+
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
vec4 sampleTex(vec2 uv) {
|
|
133
|
+
vec4 c = texture2D(uTex, uv);
|
|
134
|
+
if (uSwizzle == 1) c = c.bgra;
|
|
135
|
+
else if (uSwizzle == 2) c = vec4(c.rgb * c.a, c.a);
|
|
136
|
+
return c;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
void main() {
|
|
140
|
+
if (uMode == 0) {
|
|
141
|
+
gl_FragColor = vColor;
|
|
142
|
+
} else if (uMode == 1) {
|
|
143
|
+
gl_FragColor = vColor * sampleTex(vUV);
|
|
144
|
+
} else if (uMode == 2) {
|
|
145
|
+
gl_FragColor = vColor * texture2D(uTex, vUV).r; // an R8 atlas
|
|
146
|
+
} else if (uMode == 3) {
|
|
147
|
+
// vUV is the offset from the rect centre, in pixels; vParams.w selects
|
|
148
|
+
// fill (0), stroke of that width (> 0) or a shadow feathered by that
|
|
149
|
+
// much (< 0).
|
|
150
|
+
float d = sdRoundBox(vUV, vParams.xy, vParams.z);
|
|
151
|
+
float w = vParams.w;
|
|
152
|
+
float a;
|
|
153
|
+
if (w > 0.0) a = 1.0 - smoothstep(w * 0.5 - 0.5, w * 0.5 + 0.5, abs(d));
|
|
154
|
+
else if (w < 0.0) a = 1.0 - smoothstep(w, -w, d);
|
|
155
|
+
else a = 1.0 - smoothstep(-0.5, 0.5, d);
|
|
156
|
+
gl_FragColor = vColor * a;
|
|
157
|
+
} else {
|
|
158
|
+
// Gradients are evaluated per fragment from its device position, so any
|
|
159
|
+
// shape — a cover quad over a path included — can be filled with one.
|
|
160
|
+
vec2 p = vec2(gl_FragCoord.x, uViewport.y - gl_FragCoord.y);
|
|
161
|
+
float t;
|
|
162
|
+
if (uGradKind == 0) {
|
|
163
|
+
vec2 d = uGradB.xy - uGradA.xy;
|
|
164
|
+
float l = dot(d, d);
|
|
165
|
+
t = l > 0.0 ? dot(p - uGradA.xy, d) / l : 0.0;
|
|
166
|
+
} else {
|
|
167
|
+
float r0 = uGradA.z;
|
|
168
|
+
float r1 = uGradB.x;
|
|
169
|
+
float dist = length(p - uGradA.xy);
|
|
170
|
+
t = r1 > r0 ? (dist - r0) / (r1 - r0) : 0.0;
|
|
171
|
+
}
|
|
172
|
+
gl_FragColor = vColor * texture2D(uTex, vec2(clamp(t, 0.0, 1.0), 0.5));
|
|
173
|
+
}
|
|
174
|
+
}`;
|
|
175
|
+
|
|
176
|
+
// ---- small helpers ----------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/** A 2x3 affine matrix in canvas order [a b c d e f]. */
|
|
179
|
+
const IDENTITY = Object.freeze([1, 0, 0, 1, 0, 0]);
|
|
180
|
+
|
|
181
|
+
function mul(m, n) {
|
|
182
|
+
// m × n, where n is applied first (canvas semantics for ctx.transform)
|
|
183
|
+
return [
|
|
184
|
+
m[0] * n[0] + m[2] * n[1],
|
|
185
|
+
m[1] * n[0] + m[3] * n[1],
|
|
186
|
+
m[0] * n[2] + m[2] * n[3],
|
|
187
|
+
m[1] * n[2] + m[3] * n[3],
|
|
188
|
+
m[0] * n[4] + m[2] * n[5] + m[4],
|
|
189
|
+
m[1] * n[4] + m[3] * n[5] + m[5],
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function applyX(m, x, y) {
|
|
194
|
+
return m[0] * x + m[2] * y + m[4];
|
|
195
|
+
}
|
|
196
|
+
function applyY(m, x, y) {
|
|
197
|
+
return m[1] * x + m[3] * y + m[5];
|
|
198
|
+
}
|
|
199
|
+
function isAxisAligned(m) {
|
|
200
|
+
return m[1] === 0 && m[2] === 0;
|
|
201
|
+
}
|
|
202
|
+
/** the uniform scale a matrix applies, for stroke widths */
|
|
203
|
+
function scaleOf(m) {
|
|
204
|
+
return Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2])) || 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The Render ops a `drawGlyphs` call can name, numbered as XRender numbers
|
|
209
|
+
* them — the same table the Cocoa backend answers (`src/cocoa/context2d.js`),
|
|
210
|
+
* so `ctx.Render.PictOp.Over` reads the same on every backend. The op is
|
|
211
|
+
* ignored here: text composites Over, and for the opaque inks text uses Src
|
|
212
|
+
* and Over agree.
|
|
213
|
+
*/
|
|
214
|
+
const RENDER = Object.freeze({ PictOp: Object.freeze({ Src: 1, Over: 3 }) });
|
|
215
|
+
|
|
216
|
+
const clamp01 = (v) => Math.min(1, Math.max(0, Number(v) || 0));
|
|
217
|
+
|
|
218
|
+
const colorCache = new Map();
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Parse a CSS-ish colour into premultiplied 0..1 RGBA. Memoised: the
|
|
222
|
+
* renderer above resolves palettes long before a paint call, so a frame sees
|
|
223
|
+
* a few dozen distinct strings thousands of times.
|
|
224
|
+
*/
|
|
225
|
+
export function parseColor(value) {
|
|
226
|
+
if (Array.isArray(value)) return value;
|
|
227
|
+
if (typeof value !== 'string') return BLACK;
|
|
228
|
+
const hit = colorCache.get(value);
|
|
229
|
+
if (hit) return hit;
|
|
230
|
+
const c = parseColorUncached(value);
|
|
231
|
+
if (colorCache.size > 4096) colorCache.clear();
|
|
232
|
+
colorCache.set(value, c);
|
|
233
|
+
return c;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const BLACK = Object.freeze([0, 0, 0, 1]);
|
|
237
|
+
const CLEAR = Object.freeze([0, 0, 0, 0]);
|
|
238
|
+
const OPAQUE = Object.freeze([1, 1, 1, 1]);
|
|
239
|
+
|
|
240
|
+
function parseColorUncached(value) {
|
|
241
|
+
const s = value.trim();
|
|
242
|
+
if (s[0] === '#') {
|
|
243
|
+
const h = s.slice(1);
|
|
244
|
+
const n = h.length;
|
|
245
|
+
const q = (i, len) =>
|
|
246
|
+
parseInt(len === 1 ? h[i] + h[i] : h.slice(i * 2, i * 2 + 2), 16) / 255;
|
|
247
|
+
if (n === 3 || n === 4)
|
|
248
|
+
return premul(q(0, 1), q(1, 1), q(2, 1), n === 4 ? q(3, 1) : 1);
|
|
249
|
+
if (n === 6 || n === 8)
|
|
250
|
+
return premul(q(0, 2), q(1, 2), q(2, 2), n === 8 ? q(3, 2) : 1);
|
|
251
|
+
}
|
|
252
|
+
const m = s.match(/^rgba?\(([^)]+)\)$/i);
|
|
253
|
+
if (m) {
|
|
254
|
+
const p = m[1].split(/[,\s/]+/).filter(Boolean);
|
|
255
|
+
const chan = (v) =>
|
|
256
|
+
(v.endsWith('%') ? parseFloat(v) * 2.55 : parseFloat(v)) / 255;
|
|
257
|
+
const alpha =
|
|
258
|
+
p.length > 3
|
|
259
|
+
? p[3].endsWith('%')
|
|
260
|
+
? parseFloat(p[3]) / 100
|
|
261
|
+
: parseFloat(p[3])
|
|
262
|
+
: 1;
|
|
263
|
+
return premul(
|
|
264
|
+
chan(p[0] || '0'),
|
|
265
|
+
chan(p[1] || '0'),
|
|
266
|
+
chan(p[2] || '0'),
|
|
267
|
+
Number.isFinite(alpha) ? alpha : 1,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (s === 'transparent' || s === 'none') return CLEAR;
|
|
271
|
+
const named = NAMED[s.toLowerCase()];
|
|
272
|
+
if (named) return named;
|
|
273
|
+
return BLACK;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function premul(r, g, b, a) {
|
|
277
|
+
return Object.freeze([r * a, g * a, b * a, a]);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const NAMED = {
|
|
281
|
+
black: BLACK,
|
|
282
|
+
white: premul(1, 1, 1, 1),
|
|
283
|
+
red: premul(1, 0, 0, 1),
|
|
284
|
+
green: premul(0, 128 / 255, 0, 1),
|
|
285
|
+
blue: premul(0, 0, 1, 1),
|
|
286
|
+
gray: premul(128 / 255, 128 / 255, 128 / 255, 1),
|
|
287
|
+
grey: premul(128 / 255, 128 / 255, 128 / 255, 1),
|
|
288
|
+
yellow: premul(1, 1, 0, 1),
|
|
289
|
+
orange: premul(1, 165 / 255, 0, 1),
|
|
290
|
+
purple: premul(128 / 255, 0, 128 / 255, 1),
|
|
291
|
+
cyan: premul(0, 1, 1, 1),
|
|
292
|
+
magenta: premul(1, 0, 1, 1),
|
|
293
|
+
silver: premul(192 / 255, 192 / 255, 192 / 255, 1),
|
|
294
|
+
currentcolor: BLACK,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* A gradient: stops, and a 256-texel ramp texture baked from them on first
|
|
299
|
+
* use. Interpolated premultiplied, which is what keeps a fade to transparent
|
|
300
|
+
* from passing through grey.
|
|
301
|
+
*/
|
|
302
|
+
class Gradient {
|
|
303
|
+
constructor(kind, a) {
|
|
304
|
+
this.kind = kind; // 'linear' | 'radial'
|
|
305
|
+
this.a = a; // linear: [x0,y0,x1,y1]; radial: [cx0,cy0,r0,cx1,cy1,r1]
|
|
306
|
+
this.stops = [];
|
|
307
|
+
this._ramp = null;
|
|
308
|
+
this._baked = 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
addColorStop(offset, color) {
|
|
312
|
+
const o = Math.min(1, Math.max(0, Number(offset) || 0));
|
|
313
|
+
this.stops.push({ offset: o, color: parseColor(color) });
|
|
314
|
+
this.stops.sort((p, q) => p.offset - q.offset);
|
|
315
|
+
return this;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** the ramp as premultiplied RGBA8, 256 texels */
|
|
319
|
+
ramp() {
|
|
320
|
+
if (this._ramp && this._baked === this.stops.length) return this._ramp;
|
|
321
|
+
const out = new Uint8Array(256 * 4);
|
|
322
|
+
const stops = this.stops.length
|
|
323
|
+
? this.stops
|
|
324
|
+
: [{ offset: 0, color: CLEAR }];
|
|
325
|
+
for (let i = 0; i < 256; i++) {
|
|
326
|
+
const t = i / 255;
|
|
327
|
+
let lo = stops[0];
|
|
328
|
+
let hi = stops[stops.length - 1];
|
|
329
|
+
for (let s = 0; s < stops.length - 1; s++) {
|
|
330
|
+
if (t >= stops[s].offset && t <= stops[s + 1].offset) {
|
|
331
|
+
lo = stops[s];
|
|
332
|
+
hi = stops[s + 1];
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const span = hi.offset - lo.offset;
|
|
337
|
+
const f = span > 0 ? Math.min(1, Math.max(0, (t - lo.offset) / span)) : 0;
|
|
338
|
+
for (let c = 0; c < 4; c++) {
|
|
339
|
+
out[i * 4 + c] = Math.round(
|
|
340
|
+
(lo.color[c] + (hi.color[c] - lo.color[c]) * f) * 255,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
this._ramp = out;
|
|
345
|
+
this._baked = this.stops.length;
|
|
346
|
+
this._dirty = true;
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The CSS font shorthand, as much of it as a UI writes:
|
|
353
|
+
* `[style] [weight] size[px|pt] family[, family…]`.
|
|
354
|
+
*/
|
|
355
|
+
export function parseFont(font) {
|
|
356
|
+
const m = String(font ?? '')
|
|
357
|
+
.trim()
|
|
358
|
+
.match(
|
|
359
|
+
/^(?:(italic|oblique|normal)\s+)?(?:(normal|bold|bolder|lighter|[1-9]00)\s+)?(\d+(?:\.\d+)?)(px|pt)?(?:\s*\/\s*[^\s]+)?\s+(.+)$/i,
|
|
360
|
+
);
|
|
361
|
+
if (!m) return { italic: false, weight: 400, size: 16, family: 'sans-serif' };
|
|
362
|
+
const size = parseFloat(m[3]) * (m[4]?.toLowerCase() === 'pt' ? 4 / 3 : 1);
|
|
363
|
+
const w = (m[2] ?? 'normal').toLowerCase();
|
|
364
|
+
const weight =
|
|
365
|
+
w === 'bold' || w === 'bolder'
|
|
366
|
+
? 700
|
|
367
|
+
: w === 'lighter'
|
|
368
|
+
? 300
|
|
369
|
+
: w === 'normal'
|
|
370
|
+
? 400
|
|
371
|
+
: parseInt(w, 10);
|
|
372
|
+
const family = m[5]
|
|
373
|
+
.split(',')[0]
|
|
374
|
+
.trim()
|
|
375
|
+
.replace(/^["']|["']$/g, '');
|
|
376
|
+
return { italic: /italic|oblique/i.test(m[1] ?? ''), weight, size, family };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---- the context --------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* A font's identity for the glyph cache. ntk's `font.key` is its file and
|
|
383
|
+
* face, plus the axis values of a variable-font instance, so Adwaita Sans at
|
|
384
|
+
* 400 and at 700 are two keys. The PostScript name used before is shared by
|
|
385
|
+
* every instance of a variable font and missing from some faces — and two
|
|
386
|
+
* faces with one key share bitmaps, which is text drawn in another face's
|
|
387
|
+
* glyphs. A key ntk derived from the name alone (`mem:#…`) gets the object's
|
|
388
|
+
* identity added.
|
|
389
|
+
*/
|
|
390
|
+
const fontIds = new WeakMap();
|
|
391
|
+
let nextFontId = 1;
|
|
392
|
+
function fontKey(font) {
|
|
393
|
+
let id = fontIds.get(font);
|
|
394
|
+
if (!id) fontIds.set(font, (id = `f${nextFontId++}`));
|
|
395
|
+
const key = typeof font.key === 'string' ? font.key : '';
|
|
396
|
+
return key && !key.startsWith('mem:') ? key : `${key}@${id}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export class WaylandContext2D {
|
|
400
|
+
/**
|
|
401
|
+
* @param {object} gl the GLES entry points
|
|
402
|
+
* @param {object} [opts]
|
|
403
|
+
* @param {object} [opts.fontManager] an ntk FontManager, for `fillText`
|
|
404
|
+
* @param {import('./target.js').GLTarget} [opts.target] where to draw
|
|
405
|
+
* @param {() => void} [opts.makeCurrent] called before this context takes
|
|
406
|
+
* the device back, for an owner that has to make a GL surface current
|
|
407
|
+
* first — an offscreen surface, whose context may be drawn through at
|
|
408
|
+
* any time, not only inside a window's frame
|
|
409
|
+
*/
|
|
410
|
+
constructor(gl, { fontManager = null, target = null, makeCurrent } = {}) {
|
|
411
|
+
this.gl = gl;
|
|
412
|
+
this.fontManager = fontManager;
|
|
413
|
+
/** @see ./device.js — the state this context shares with its siblings */
|
|
414
|
+
this._device = glDevice(gl);
|
|
415
|
+
this._makeCurrent = makeCurrent ?? null;
|
|
416
|
+
// `TextLayout.draw` opens with `ctx.window.app.display.Render` and reads
|
|
417
|
+
// one constant from it. Satisfy the shape rather than fork the layout.
|
|
418
|
+
this.window = { app: { display: { Render: RENDER } } };
|
|
419
|
+
this.atlas = new GlyphAtlas(gl);
|
|
420
|
+
// Quads already buffered against the atlas must be drawn before the
|
|
421
|
+
// atlas re-lays itself out under them.
|
|
422
|
+
this.atlas.onBeforeGrow = () => this._flush();
|
|
423
|
+
// Path fills that go through the CPU rasteriser land here as A8 masks,
|
|
424
|
+
// drawn like glyphs; emptied every frame, since a mask is one draw.
|
|
425
|
+
this.masks = new GlyphAtlas(gl, { size: 256 });
|
|
426
|
+
this.masks.onBeforeGrow = () => this._flush();
|
|
427
|
+
this._maskSeq = 0;
|
|
428
|
+
/** the non-rectangular clips in force, device space, outermost first */
|
|
429
|
+
this._clipPath = null;
|
|
430
|
+
/** inside a stencil pass: `_flush` leaves the stencil state alone */
|
|
431
|
+
this._stencilling = false;
|
|
432
|
+
this._target = target;
|
|
433
|
+
|
|
434
|
+
this.fillStyle = '#000000';
|
|
435
|
+
this.strokeStyle = '#000000';
|
|
436
|
+
this.lineWidth = 1;
|
|
437
|
+
this.lineCap = 'butt';
|
|
438
|
+
this.lineJoin = 'miter';
|
|
439
|
+
this.miterLimit = 10;
|
|
440
|
+
this.globalAlpha = 1;
|
|
441
|
+
this.textAlign = 'start';
|
|
442
|
+
this.textBaseline = 'alphabetic';
|
|
443
|
+
this.timestamp = 0;
|
|
444
|
+
this._font = '16px sans-serif';
|
|
445
|
+
this._fontSpec = parseFont(this._font);
|
|
446
|
+
this._fontFace = null;
|
|
447
|
+
this._fontFaceFor = null;
|
|
448
|
+
|
|
449
|
+
/** how many non-rectangular clips were approximated by their bbox */
|
|
450
|
+
this.clipApproximations = 0;
|
|
451
|
+
this.shapeStats = { quads: 0, batches: 0, glyphs: 0, paths: 0 };
|
|
452
|
+
|
|
453
|
+
this._m = IDENTITY;
|
|
454
|
+
this._stack = [];
|
|
455
|
+
this._clip = null;
|
|
456
|
+
this._path = new Path2D();
|
|
457
|
+
this._rects = [];
|
|
458
|
+
this._rectOnly = true;
|
|
459
|
+
|
|
460
|
+
this._width = 0;
|
|
461
|
+
this._height = 0;
|
|
462
|
+
|
|
463
|
+
this._verts = new Float32Array(MAX_VERTS * STRIDE);
|
|
464
|
+
this._n = 0;
|
|
465
|
+
this._mode = MODE_SOLID;
|
|
466
|
+
this._texture = null;
|
|
467
|
+
this._swizzle = SWIZZLE_RGBA;
|
|
468
|
+
this._drew = false;
|
|
469
|
+
|
|
470
|
+
this._program = null;
|
|
471
|
+
this._buffer = null;
|
|
472
|
+
this._loc = {};
|
|
473
|
+
/** image object -> { tex, w, h } */
|
|
474
|
+
this._textures = new WeakMap();
|
|
475
|
+
this._gradTex = new WeakMap();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ---- lifecycle --------------------------------------------------------
|
|
479
|
+
|
|
480
|
+
/** Compile the program and allocate the vertex buffer. Context must be current. */
|
|
481
|
+
init() {
|
|
482
|
+
const gl = this.gl;
|
|
483
|
+
if (this._program) return;
|
|
484
|
+
const vs = compile(gl, gl.VERTEX_SHADER, VERT, 'vertex');
|
|
485
|
+
const fs = compile(gl, gl.FRAGMENT_SHADER, FRAG, 'fragment');
|
|
486
|
+
const p = gl.createProgram();
|
|
487
|
+
gl.attachShader(p, vs);
|
|
488
|
+
gl.attachShader(p, fs);
|
|
489
|
+
gl.linkProgram(p);
|
|
490
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
|
|
491
|
+
throw new Error(`2d program failed to link: ${gl.getProgramInfoLog(p)}`);
|
|
492
|
+
}
|
|
493
|
+
this._program = p;
|
|
494
|
+
const u = (n) => gl.getUniformLocation(p, n);
|
|
495
|
+
this._loc = {
|
|
496
|
+
aPos: gl.getAttribLocation(p, 'aPos'),
|
|
497
|
+
aUV: gl.getAttribLocation(p, 'aUV'),
|
|
498
|
+
aColor: gl.getAttribLocation(p, 'aColor'),
|
|
499
|
+
aParams: gl.getAttribLocation(p, 'aParams'),
|
|
500
|
+
uViewport: u('uViewport'),
|
|
501
|
+
uMode: u('uMode'),
|
|
502
|
+
uSwizzle: u('uSwizzle'),
|
|
503
|
+
uTex: u('uTex'),
|
|
504
|
+
uGradKind: u('uGradKind'),
|
|
505
|
+
uGradA: u('uGradA'),
|
|
506
|
+
uGradB: u('uGradB'),
|
|
507
|
+
};
|
|
508
|
+
this._buffer = gl.createBuffer();
|
|
509
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this._buffer);
|
|
510
|
+
// The addon's bufferData takes a TypedArray, not the byte-length
|
|
511
|
+
// overload WebGL also accepts. Allocated once; frames use bufferSubData.
|
|
512
|
+
gl.bufferData(gl.ARRAY_BUFFER, this._verts, gl.DYNAMIC_DRAW);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Where this context draws. A window's backing, or a surface's target. */
|
|
516
|
+
attach(target) {
|
|
517
|
+
if (this._target === target) return;
|
|
518
|
+
this._flush();
|
|
519
|
+
this._target = target;
|
|
520
|
+
// The projection and the bound framebuffer both belonged to the old
|
|
521
|
+
// target; the next draw re-establishes them against this one.
|
|
522
|
+
this._release();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
get target() {
|
|
526
|
+
return this._target;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Start a frame. Resets state as a fresh canvas would, binds the target,
|
|
531
|
+
* and sizes the projection to it.
|
|
532
|
+
*/
|
|
533
|
+
begin(width, height, timestamp = 0) {
|
|
534
|
+
this.init();
|
|
535
|
+
this._device.owner = this;
|
|
536
|
+
const t = this._target;
|
|
537
|
+
if (t) {
|
|
538
|
+
t.bind();
|
|
539
|
+
width = t.width;
|
|
540
|
+
height = t.height;
|
|
541
|
+
}
|
|
542
|
+
this._width = width;
|
|
543
|
+
this._height = height;
|
|
544
|
+
this._m = IDENTITY;
|
|
545
|
+
this._stack.length = 0;
|
|
546
|
+
this._clip = null;
|
|
547
|
+
this._path = new Path2D();
|
|
548
|
+
this._rects = [];
|
|
549
|
+
this._rectOnly = true;
|
|
550
|
+
this._n = 0;
|
|
551
|
+
this._drew = false;
|
|
552
|
+
this.timestamp = timestamp;
|
|
553
|
+
this.shapeStats = { quads: 0, batches: 0, glyphs: 0, paths: 0 };
|
|
554
|
+
this.clipApproximations = 0;
|
|
555
|
+
this._clipPath = null;
|
|
556
|
+
this._stencilling = false;
|
|
557
|
+
this._maskSeq = 0;
|
|
558
|
+
this.masks.reset();
|
|
559
|
+
this._applyState();
|
|
560
|
+
// scratch for paths and clips: a frame starts with it clean
|
|
561
|
+
const gl = this.gl;
|
|
562
|
+
gl.stencilMask(0xff);
|
|
563
|
+
gl.clear(gl.STENCIL_BUFFER_BIT);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* The GL state every draw here assumes: the target bound, the projection
|
|
568
|
+
* sized to it, and the rest of the device saying what the shader was
|
|
569
|
+
* written against. `begin()` sets it; `_claim()` sets it again whenever
|
|
570
|
+
* something else has been drawing — another context, or a `<glarea>`'s
|
|
571
|
+
* foreign GL — since this one last did.
|
|
572
|
+
*/
|
|
573
|
+
_applyState() {
|
|
574
|
+
const gl = this.gl;
|
|
575
|
+
this._target?.bind();
|
|
576
|
+
gl.useProgram(this._program);
|
|
577
|
+
gl.uniform2f(this._loc.uViewport, this._width, this._height);
|
|
578
|
+
gl.disable(gl.DEPTH_TEST);
|
|
579
|
+
gl.disable(gl.CULL_FACE);
|
|
580
|
+
gl.disable(gl.STENCIL_TEST);
|
|
581
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
582
|
+
gl.enable(gl.BLEND);
|
|
583
|
+
gl.colorMask(true, true, true, true);
|
|
584
|
+
// Premultiplied alpha throughout — colours are premultiplied on the way
|
|
585
|
+
// in, and the glyph and texture paths both produce premultiplied output.
|
|
586
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
587
|
+
// …and what a frame of this context's own never has to say, because
|
|
588
|
+
// nothing here changes it. Foreign GL does.
|
|
589
|
+
if (typeof gl.bindVertexArray === 'function') gl.bindVertexArray(null);
|
|
590
|
+
gl.blendEquation(gl.FUNC_ADD);
|
|
591
|
+
gl.stencilMask(0xff);
|
|
592
|
+
gl.frontFace(gl.CCW);
|
|
593
|
+
gl.depthMask(true);
|
|
594
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Take the device before touching GL (device.js).
|
|
599
|
+
*
|
|
600
|
+
* Contexts on this backend share one GLES context, so the framebuffer and
|
|
601
|
+
* the state a draw needs are whatever the last one to draw left behind.
|
|
602
|
+
* This is the one place that notices and puts them back — which is what
|
|
603
|
+
* makes a held offscreen-surface context work the way ntk documents it,
|
|
604
|
+
* rather than only inside `Surface#render` (#566), and what spares a node
|
|
605
|
+
* that painted into a surface mid-frame from having to restore the
|
|
606
|
+
* window's context by hand afterwards.
|
|
607
|
+
*
|
|
608
|
+
* Nothing happens in the common case: a property compare against a
|
|
609
|
+
* context that is already the owner.
|
|
610
|
+
*/
|
|
611
|
+
_claim() {
|
|
612
|
+
// Mid stencil-then-cover the state is set up for the pass, not for a
|
|
613
|
+
// plain batch, and the device cannot have changed hands under it.
|
|
614
|
+
if (this._device.owner === this || this._stencilling) return;
|
|
615
|
+
this._device.owner = this;
|
|
616
|
+
this._makeCurrent?.();
|
|
617
|
+
this.init();
|
|
618
|
+
// As `begin()` has it: a target's size *is* the projection.
|
|
619
|
+
const t = this._target;
|
|
620
|
+
if (t) {
|
|
621
|
+
this._width = t.width;
|
|
622
|
+
this._height = t.height;
|
|
623
|
+
}
|
|
624
|
+
this._applyState();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Give the device up, so the next draw through this context re-takes it. */
|
|
628
|
+
_release() {
|
|
629
|
+
if (this._device.owner === this) this._device.owner = null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** After foreign GL drew — a `<glarea>`'s frame — before drawing again. */
|
|
633
|
+
restoreGLState() {
|
|
634
|
+
// Foreign GL leaves no trace in the bookkeeping, so say so and re-take
|
|
635
|
+
// the device. State first, then the flush: anything still buffered is
|
|
636
|
+
// drawn under this context's state rather than under what was left.
|
|
637
|
+
this._release();
|
|
638
|
+
this._claim();
|
|
639
|
+
this._flush();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Flush what is buffered without ending the frame. */
|
|
643
|
+
flush() {
|
|
644
|
+
this._flush();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Flush anything still buffered. Call before presenting. */
|
|
648
|
+
end() {
|
|
649
|
+
this._flush();
|
|
650
|
+
this.gl.disable(this.gl.SCISSOR_TEST);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** Whether anything was drawn since `begin()`. */
|
|
654
|
+
get drew() {
|
|
655
|
+
return this._drew || this._n > 0;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ---- state ------------------------------------------------------------
|
|
659
|
+
|
|
660
|
+
save() {
|
|
661
|
+
this._stack.push({
|
|
662
|
+
m: this._m,
|
|
663
|
+
clip: this._clip,
|
|
664
|
+
clipPath: this._clipPath,
|
|
665
|
+
fill: this.fillStyle,
|
|
666
|
+
stroke: this.strokeStyle,
|
|
667
|
+
lw: this.lineWidth,
|
|
668
|
+
cap: this.lineCap,
|
|
669
|
+
join: this.lineJoin,
|
|
670
|
+
miter: this.miterLimit,
|
|
671
|
+
alpha: this.globalAlpha,
|
|
672
|
+
font: this._font,
|
|
673
|
+
align: this.textAlign,
|
|
674
|
+
baseline: this.textBaseline,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
restore() {
|
|
679
|
+
const s = this._stack.pop();
|
|
680
|
+
if (!s) return;
|
|
681
|
+
if (s.clipPath !== this._clipPath)
|
|
682
|
+
this._rebuildClipStencil(s.clipPath, s.clip);
|
|
683
|
+
if (s.clip !== this._clip) this._flush();
|
|
684
|
+
this._m = s.m;
|
|
685
|
+
this._clip = s.clip;
|
|
686
|
+
this.fillStyle = s.fill;
|
|
687
|
+
this.strokeStyle = s.stroke;
|
|
688
|
+
this.lineWidth = s.lw;
|
|
689
|
+
this.lineCap = s.cap;
|
|
690
|
+
this.lineJoin = s.join;
|
|
691
|
+
this.miterLimit = s.miter;
|
|
692
|
+
this.globalAlpha = s.alpha;
|
|
693
|
+
if (s.font !== this._font) this.font = s.font;
|
|
694
|
+
this.textAlign = s.align;
|
|
695
|
+
this.textBaseline = s.baseline;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
translate(x, y) {
|
|
699
|
+
this._m = mul(this._m, [1, 0, 0, 1, x, y]);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
scale(x, y = x) {
|
|
703
|
+
this._m = mul(this._m, [x, 0, 0, y, 0, 0]);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
rotate(angle) {
|
|
707
|
+
const c = Math.cos(angle);
|
|
708
|
+
const s = Math.sin(angle);
|
|
709
|
+
this._m = mul(this._m, [c, s, -s, c, 0, 0]);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
transform(a, b, c, d, e, f) {
|
|
713
|
+
this._m = mul(this._m, [a, b, c, d, e, f]);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
setTransform(a, b, c, d, e, f) {
|
|
717
|
+
if (typeof a === 'object' && a) this._m = [a.a, a.b, a.c, a.d, a.e, a.f];
|
|
718
|
+
else this._m = [a, b, c, d, e, f];
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
resetTransform() {
|
|
722
|
+
this._m = IDENTITY;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
getTransform() {
|
|
726
|
+
const [a, b, c, d, e, f] = this._m;
|
|
727
|
+
return { a, b, c, d, e, f };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
setLineDash() {
|
|
731
|
+
// Dashes are a stroke-geometry feature not implemented here; accepted
|
|
732
|
+
// and ignored so a caller that dashes a focus ring still gets the ring.
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
getLineDash() {
|
|
736
|
+
return [];
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
get font() {
|
|
740
|
+
return this._font;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
set font(value) {
|
|
744
|
+
if (value === this._font) return;
|
|
745
|
+
this._font = String(value);
|
|
746
|
+
this._fontSpec = parseFont(this._font);
|
|
747
|
+
this._fontFace = null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
createLinearGradient(x0, y0, x1, y1) {
|
|
751
|
+
return new Gradient('linear', [x0, y0, x1, y1]);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
createRadialGradient(x0, y0, r0, x1, y1, r1) {
|
|
755
|
+
return new Gradient('radial', [x0, y0, r0, x1, y1, r1]);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
createPattern() {
|
|
759
|
+
// Patterns need a repeating texture mode; not yet.
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ---- paths ------------------------------------------------------------
|
|
764
|
+
|
|
765
|
+
beginPath() {
|
|
766
|
+
this._path = new Path2D();
|
|
767
|
+
this._rects = [];
|
|
768
|
+
this._rectOnly = true;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
rect(x, y, w, h) {
|
|
772
|
+
this._rects.push({ x, y, w, h, r: 0 });
|
|
773
|
+
this._path.rect(x, y, w, h);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
roundRect(x, y, w, h, radii) {
|
|
777
|
+
// One radius for all four corners is the analytic rounded rectangle;
|
|
778
|
+
// anything else — `[0, 0, r, r]`, a box with one corner rounded — is a
|
|
779
|
+
// path. Taking the first radius for all four drew the wrong shape, and
|
|
780
|
+
// as a clip made `[0, 0, r, r]` a plain rectangle.
|
|
781
|
+
const list = Array.isArray(radii) ? radii : [radii];
|
|
782
|
+
const first = Number(list[0]) || 0;
|
|
783
|
+
if (list.every((v) => (Number(v) || 0) === first)) {
|
|
784
|
+
this._rects.push({ x, y, w, h, r: first });
|
|
785
|
+
} else {
|
|
786
|
+
this._rectOnly = false;
|
|
787
|
+
}
|
|
788
|
+
this._path.roundRect(x, y, w, h, radii);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
moveTo(x, y) {
|
|
792
|
+
this._rectOnly = false;
|
|
793
|
+
this._path.moveTo(x, y);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
lineTo(x, y) {
|
|
797
|
+
this._rectOnly = false;
|
|
798
|
+
this._path.lineTo(x, y);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
closePath() {
|
|
802
|
+
this._path.closePath();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
bezierCurveTo(x1, y1, x2, y2, x, y) {
|
|
806
|
+
this._rectOnly = false;
|
|
807
|
+
this._path.bezierCurveTo(x1, y1, x2, y2, x, y);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
quadraticCurveTo(x1, y1, x, y) {
|
|
811
|
+
this._rectOnly = false;
|
|
812
|
+
this._path.quadraticCurveTo(x1, y1, x, y);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
arc(x, y, r, a0, a1, ccw) {
|
|
816
|
+
this._rectOnly = false;
|
|
817
|
+
this._path.arc(x, y, r, a0, a1, ccw);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
arcTo(x1, y1, x2, y2, r) {
|
|
821
|
+
this._rectOnly = false;
|
|
822
|
+
this._path.arcTo(x1, y1, x2, y2, r);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
ellipse(x, y, rx, ry, rot, a0, a1, ccw) {
|
|
826
|
+
this._rectOnly = false;
|
|
827
|
+
this._path.ellipse(x, y, rx, ry, rot, a0, a1, ccw);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* `fill()`, `fill(rule)`, `fill(path)`, `fill(path, rule)`.
|
|
832
|
+
*
|
|
833
|
+
* Rectangles and rounded rectangles built with `rect`/`roundRect` take the
|
|
834
|
+
* analytic path; anything else is flattened and stencilled.
|
|
835
|
+
*/
|
|
836
|
+
fill(a, b) {
|
|
837
|
+
const external = a instanceof Path2D ? a : null;
|
|
838
|
+
const rule = (external ? b : a) === 'evenodd' ? 'evenodd' : 'nonzero';
|
|
839
|
+
if (!external && this._rectOnly) {
|
|
840
|
+
for (const r of this._rects)
|
|
841
|
+
this._rect(r.x, r.y, r.w, r.h, r.r, this.fillStyle, 0);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const cmds = external ? external._cmds : this._path._cmds;
|
|
845
|
+
const polys = flattenPath(cmds, this._m);
|
|
846
|
+
this._fillPolys(polys, rule, this.fillStyle);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
stroke(external) {
|
|
850
|
+
const p = external instanceof Path2D ? external : null;
|
|
851
|
+
if (!p && this._rectOnly) {
|
|
852
|
+
for (const r of this._rects)
|
|
853
|
+
this._rect(
|
|
854
|
+
r.x,
|
|
855
|
+
r.y,
|
|
856
|
+
r.w,
|
|
857
|
+
r.h,
|
|
858
|
+
r.r,
|
|
859
|
+
this.strokeStyle,
|
|
860
|
+
Math.max(0.5, this.lineWidth),
|
|
861
|
+
);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const cmds = p ? p._cmds : this._path._cmds;
|
|
865
|
+
const polylines = flattenPath(cmds, this._m);
|
|
866
|
+
const width = Math.max(0.5, this.lineWidth * scaleOf(this._m));
|
|
867
|
+
const polys = strokePolys(
|
|
868
|
+
polylines,
|
|
869
|
+
width,
|
|
870
|
+
this.lineJoin,
|
|
871
|
+
this.lineCap,
|
|
872
|
+
this.miterLimit,
|
|
873
|
+
);
|
|
874
|
+
this._fillPolys(polys, 'nonzero', this.strokeStyle);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Intersect the clip with the current path.
|
|
879
|
+
*
|
|
880
|
+
* One axis-aligned rectangle is the scissor, as before, and costs nothing
|
|
881
|
+
* per draw. Anything else — a rounded rectangle (`overflow: hidden` with
|
|
882
|
+
* a radius), a path, several rectangles — is exact through the stencil's
|
|
883
|
+
* clip bit (`_applyClipPath`), and every batch drawn while it is in force
|
|
884
|
+
* tests that bit. The scissor still narrows to the shape's bounds, so the
|
|
885
|
+
* test is only ever paid inside them.
|
|
886
|
+
*/
|
|
887
|
+
clip(a, b) {
|
|
888
|
+
const external = a instanceof Path2D ? a : null;
|
|
889
|
+
const rule = (external ? b : a) === 'evenodd' ? 'evenodd' : 'nonzero';
|
|
890
|
+
if (
|
|
891
|
+
!external &&
|
|
892
|
+
this._rectOnly &&
|
|
893
|
+
this._rects.length === 1 &&
|
|
894
|
+
!(this._rects[0].r > 0) &&
|
|
895
|
+
isAxisAligned(this._m)
|
|
896
|
+
) {
|
|
897
|
+
const r = this._rects[0];
|
|
898
|
+
const bounds = this._deviceRect(r.x, r.y, r.w, r.h);
|
|
899
|
+
this._flush();
|
|
900
|
+
this._clip = this._clip ? intersectRect(this._clip, bounds) : bounds;
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const polys = flattenPath(
|
|
904
|
+
external ? external._cmds : this._path._cmds,
|
|
905
|
+
this._m,
|
|
906
|
+
);
|
|
907
|
+
const bounds = polysBounds(polys);
|
|
908
|
+
if (!bounds || !(bounds.w > 0) || !(bounds.h > 0)) {
|
|
909
|
+
// an empty path clips everything
|
|
910
|
+
this._flush();
|
|
911
|
+
this._clip = { x: 0, y: 0, w: 0, h: 0 };
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
this._applyClipPath(polys, rule, bounds);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
isPointInPath(x, y) {
|
|
918
|
+
const polys = flattenPath(this._path._cmds, this._m);
|
|
919
|
+
const dx = applyX(this._m, x, y);
|
|
920
|
+
const dy = applyY(this._m, x, y);
|
|
921
|
+
return polysContain(polys, dx, dy);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// ---- fills ------------------------------------------------------------
|
|
925
|
+
|
|
926
|
+
fillRect(x, y, w, h) {
|
|
927
|
+
this._rect(x, y, w, h, 0, this.fillStyle, 0);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
strokeRect(x, y, w, h) {
|
|
931
|
+
this._rect(x, y, w, h, 0, this.strokeStyle, Math.max(0.5, this.lineWidth));
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* The batched form react-x11 uses for backgrounds and selection bands.
|
|
936
|
+
*
|
|
937
|
+
* Three shapes, because all three are in use: `[[x,y,w,h],…]`,
|
|
938
|
+
* `[{x,y,width,height},…]`, and the flat `[x,y,w,h,x,y,w,h,…]` that
|
|
939
|
+
* `Render.FillRectangles` takes on the wire. ntk and the Cocoa context
|
|
940
|
+
* both accept the flat list, so it is what a caller written against
|
|
941
|
+
* either of them hands us; reading it as one rectangle per element makes
|
|
942
|
+
* every field `undefined` and draws nothing at all.
|
|
943
|
+
*/
|
|
944
|
+
fillRects(rects) {
|
|
945
|
+
if (!rects?.length) return;
|
|
946
|
+
if (typeof rects[0] === 'number') {
|
|
947
|
+
// a trailing partial rectangle is not one, so stop four short
|
|
948
|
+
for (let i = 0; i + 3 < rects.length; i += 4) {
|
|
949
|
+
const x = rects[i];
|
|
950
|
+
const y = rects[i + 1];
|
|
951
|
+
const w = rects[i + 2];
|
|
952
|
+
const h = rects[i + 3];
|
|
953
|
+
this._rect(x, y, w, h, 0, this.fillStyle, 0);
|
|
954
|
+
}
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
for (const r of rects) {
|
|
958
|
+
const x = r.x ?? r[0];
|
|
959
|
+
const y = r.y ?? r[1];
|
|
960
|
+
const w = r.width ?? r.w ?? r[2];
|
|
961
|
+
const h = r.height ?? r.h ?? r[3];
|
|
962
|
+
this._rect(x, y, w, h, 0, this.fillStyle, 0);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* A blurred rounded-rectangle shadow, drawn analytically: the distance
|
|
968
|
+
* field's edge is feathered by the blur radius instead of stepping at
|
|
969
|
+
* zero. `boxpaint.js` calls this when it exists, in place of rendering
|
|
970
|
+
* coverage to a surface and gaussian-blurring it on the CPU — the same
|
|
971
|
+
* look for the shapes a box shadow can have, at the cost of one quad.
|
|
972
|
+
*/
|
|
973
|
+
fillShadow(rect, radius, blur, color) {
|
|
974
|
+
const feather = Math.max(0.5, blur);
|
|
975
|
+
this._rect(
|
|
976
|
+
rect.x,
|
|
977
|
+
rect.y,
|
|
978
|
+
rect.width,
|
|
979
|
+
rect.height,
|
|
980
|
+
radius,
|
|
981
|
+
color,
|
|
982
|
+
-feather,
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/** Clear to transparent — writes zeroes rather than blending. */
|
|
987
|
+
clearRect(x, y, w, h) {
|
|
988
|
+
if (!(w > 0 && h > 0)) return;
|
|
989
|
+
this._flush();
|
|
990
|
+
const gl = this.gl;
|
|
991
|
+
gl.blendFunc(gl.ONE, gl.ZERO);
|
|
992
|
+
this._emitRect(x, y, w, h, CLEAR);
|
|
993
|
+
this._flush();
|
|
994
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
_rect(x, y, w, h, radius, style, strokeWidth) {
|
|
998
|
+
if (!(w > 0 && h > 0)) return;
|
|
999
|
+
if (style instanceof Gradient) {
|
|
1000
|
+
if (radius <= 0 && strokeWidth === 0)
|
|
1001
|
+
return this._gradientQuad(x, y, w, h, style);
|
|
1002
|
+
// rounded/stroked gradient: stencil the shape, cover with the gradient
|
|
1003
|
+
const p = new Path2D();
|
|
1004
|
+
p.roundRect(x, y, w, h, radius);
|
|
1005
|
+
const polys = flattenPath(p._cmds, this._m);
|
|
1006
|
+
return this._fillPolys(polys, 'nonzero', style);
|
|
1007
|
+
}
|
|
1008
|
+
const color = this._color(style);
|
|
1009
|
+
if (color[3] === 0) return;
|
|
1010
|
+
if (radius <= 0 && strokeWidth === 0 && isAxisAligned(this._m)) {
|
|
1011
|
+
return this._emitRect(x, y, w, h, color);
|
|
1012
|
+
}
|
|
1013
|
+
// Rounded, stroked, shadowed or rotated: hand the shader the rect in its
|
|
1014
|
+
// own space, padded so the antialiased edge (or feather) has room.
|
|
1015
|
+
const pad =
|
|
1016
|
+
strokeWidth > 0
|
|
1017
|
+
? strokeWidth * 0.5 + 1
|
|
1018
|
+
: strokeWidth < 0
|
|
1019
|
+
? -strokeWidth + 1
|
|
1020
|
+
: 1;
|
|
1021
|
+
const hw = w / 2;
|
|
1022
|
+
const hh = h / 2;
|
|
1023
|
+
const cx = x + hw;
|
|
1024
|
+
const cy = y + hh;
|
|
1025
|
+
const r = Math.min(Math.max(0, radius), hw, hh);
|
|
1026
|
+
this._setMode(MODE_RRECT, null, SWIZZLE_RGBA);
|
|
1027
|
+
const m = this._m;
|
|
1028
|
+
const s = scaleOf(m);
|
|
1029
|
+
const px = pad / s;
|
|
1030
|
+
const c = [
|
|
1031
|
+
[-hw - px, -hh - px],
|
|
1032
|
+
[hw + px, -hh - px],
|
|
1033
|
+
[hw + px, hh + px],
|
|
1034
|
+
[-hw - px, hh + px],
|
|
1035
|
+
];
|
|
1036
|
+
const pos = new Array(8);
|
|
1037
|
+
const uv = new Array(8);
|
|
1038
|
+
for (let i = 0; i < 4; i++) {
|
|
1039
|
+
const [dx, dy] = c[i];
|
|
1040
|
+
pos[i * 2] = applyX(m, cx + dx, cy + dy);
|
|
1041
|
+
pos[i * 2 + 1] = applyY(m, cx + dx, cy + dy);
|
|
1042
|
+
uv[i * 2] = dx * s;
|
|
1043
|
+
uv[i * 2 + 1] = dy * s;
|
|
1044
|
+
}
|
|
1045
|
+
this._quad(pos, uv, color, [hw * s, hh * s, r * s, strokeWidth]);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
_emitRect(x, y, w, h, color) {
|
|
1049
|
+
this._setMode(MODE_SOLID, null, SWIZZLE_RGBA);
|
|
1050
|
+
const m = this._m;
|
|
1051
|
+
this._quad(
|
|
1052
|
+
[
|
|
1053
|
+
applyX(m, x, y),
|
|
1054
|
+
applyY(m, x, y),
|
|
1055
|
+
applyX(m, x + w, y),
|
|
1056
|
+
applyY(m, x + w, y),
|
|
1057
|
+
applyX(m, x + w, y + h),
|
|
1058
|
+
applyY(m, x + w, y + h),
|
|
1059
|
+
applyX(m, x, y + h),
|
|
1060
|
+
applyY(m, x, y + h),
|
|
1061
|
+
],
|
|
1062
|
+
UV_UNIT,
|
|
1063
|
+
color,
|
|
1064
|
+
ZERO4,
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** Bind a gradient's ramp and endpoints as the current batch's uniforms. */
|
|
1069
|
+
_useGradient(grad) {
|
|
1070
|
+
const gl = this.gl;
|
|
1071
|
+
this._flush();
|
|
1072
|
+
let tex = this._gradTex.get(grad);
|
|
1073
|
+
const ramp = grad.ramp();
|
|
1074
|
+
if (!tex) {
|
|
1075
|
+
tex = gl.createTexture();
|
|
1076
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1077
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1078
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1079
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1080
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1081
|
+
this._gradTex.set(grad, tex);
|
|
1082
|
+
grad._dirty = true;
|
|
1083
|
+
}
|
|
1084
|
+
if (grad._dirty) {
|
|
1085
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1086
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
1087
|
+
gl.texImage2D(
|
|
1088
|
+
gl.TEXTURE_2D,
|
|
1089
|
+
0,
|
|
1090
|
+
gl.RGBA,
|
|
1091
|
+
256,
|
|
1092
|
+
1,
|
|
1093
|
+
0,
|
|
1094
|
+
gl.RGBA,
|
|
1095
|
+
gl.UNSIGNED_BYTE,
|
|
1096
|
+
ramp,
|
|
1097
|
+
);
|
|
1098
|
+
grad._dirty = false;
|
|
1099
|
+
}
|
|
1100
|
+
this._mode = MODE_GRADIENT;
|
|
1101
|
+
this._texture = tex;
|
|
1102
|
+
this._swizzle = SWIZZLE_RGBA;
|
|
1103
|
+
const m = this._m;
|
|
1104
|
+
const a = grad.a;
|
|
1105
|
+
gl.useProgram(this._program);
|
|
1106
|
+
if (grad.kind === 'linear') {
|
|
1107
|
+
gl.uniform1i(this._loc.uGradKind, 0);
|
|
1108
|
+
gl.uniform4f(
|
|
1109
|
+
this._loc.uGradA,
|
|
1110
|
+
applyX(m, a[0], a[1]),
|
|
1111
|
+
applyY(m, a[0], a[1]),
|
|
1112
|
+
0,
|
|
1113
|
+
0,
|
|
1114
|
+
);
|
|
1115
|
+
gl.uniform4f(
|
|
1116
|
+
this._loc.uGradB,
|
|
1117
|
+
applyX(m, a[2], a[3]),
|
|
1118
|
+
applyY(m, a[2], a[3]),
|
|
1119
|
+
0,
|
|
1120
|
+
0,
|
|
1121
|
+
);
|
|
1122
|
+
} else {
|
|
1123
|
+
const s = scaleOf(m);
|
|
1124
|
+
gl.uniform1i(this._loc.uGradKind, 1);
|
|
1125
|
+
gl.uniform4f(
|
|
1126
|
+
this._loc.uGradA,
|
|
1127
|
+
applyX(m, a[3], a[4]),
|
|
1128
|
+
applyY(m, a[3], a[4]),
|
|
1129
|
+
a[2] * s,
|
|
1130
|
+
0,
|
|
1131
|
+
);
|
|
1132
|
+
gl.uniform4f(this._loc.uGradB, a[5] * s, 0, 0, 0);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
_gradientQuad(x, y, w, h, grad) {
|
|
1137
|
+
this._useGradient(grad);
|
|
1138
|
+
const m = this._m;
|
|
1139
|
+
const alpha = this.globalAlpha;
|
|
1140
|
+
this._quad(
|
|
1141
|
+
[
|
|
1142
|
+
applyX(m, x, y),
|
|
1143
|
+
applyY(m, x, y),
|
|
1144
|
+
applyX(m, x + w, y),
|
|
1145
|
+
applyY(m, x + w, y),
|
|
1146
|
+
applyX(m, x + w, y + h),
|
|
1147
|
+
applyY(m, x + w, y + h),
|
|
1148
|
+
applyX(m, x, y + h),
|
|
1149
|
+
applyY(m, x, y + h),
|
|
1150
|
+
],
|
|
1151
|
+
UV_UNIT,
|
|
1152
|
+
[alpha, alpha, alpha, alpha],
|
|
1153
|
+
ZERO4,
|
|
1154
|
+
);
|
|
1155
|
+
this._flush();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// ---- path fills -------------------------------------------------------
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Fill device-space polygons with a paint.
|
|
1162
|
+
*
|
|
1163
|
+
* Two routes. A solid fill of a shape that fits the CPU budget is
|
|
1164
|
+
* **coverage**: ntk's own rasteriser — the one the X11 backend's local
|
|
1165
|
+
* raster policy and every glyph use — turns the polygons into an A8 mask,
|
|
1166
|
+
* which is drawn like a glyph. Antialiased, deterministic, the same
|
|
1167
|
+
* pixels as X11, at ~25µs for an icon. Everything else — a gradient, or a
|
|
1168
|
+
* shape bigger than `COVERAGE_MAX_PIXELS` — is **stencil-then-cover**:
|
|
1169
|
+
* pass 1 counts winding into the stencil buffer with colour writes off
|
|
1170
|
+
* (one triangle fan per subpath, incrementing for one facing and
|
|
1171
|
+
* decrementing for the other, or inverting for even-odd), pass 2 covers
|
|
1172
|
+
* the bounds with the paint wherever the count is non-zero, zeroing the
|
|
1173
|
+
* count as it goes. Aliased at the edge; MSAA is the fix, when a target
|
|
1174
|
+
* that can be validated has it.
|
|
1175
|
+
*
|
|
1176
|
+
* Under a path clip (`_clipPath`) the cover also requires the clip bit,
|
|
1177
|
+
* and a third pass clears the counts the cover did not reach.
|
|
1178
|
+
*/
|
|
1179
|
+
_fillPolys(polys, rule, style) {
|
|
1180
|
+
const bounds = polysBounds(polys);
|
|
1181
|
+
if (!bounds || bounds.w <= 0 || bounds.h <= 0) return;
|
|
1182
|
+
// Before `_fillCoverage`, which cuts its raster to `_width`/`_height`:
|
|
1183
|
+
// on a context that has never opened a frame those are the target's
|
|
1184
|
+
// only once the device has been claimed.
|
|
1185
|
+
this._claim();
|
|
1186
|
+
let color = null;
|
|
1187
|
+
if (!(style instanceof Gradient)) {
|
|
1188
|
+
color = this._color(style);
|
|
1189
|
+
if (color[3] === 0) return;
|
|
1190
|
+
if (this._fillCoverage(polys, rule, bounds, color)) return;
|
|
1191
|
+
}
|
|
1192
|
+
const gl = this.gl;
|
|
1193
|
+
this._flush();
|
|
1194
|
+
this.shapeStats.paths++;
|
|
1195
|
+
this._stencilling = true;
|
|
1196
|
+
this._windingPass(polys, rule);
|
|
1197
|
+
|
|
1198
|
+
// cover: where the count is non-zero — and, under a clip, the bit is set
|
|
1199
|
+
gl.colorMask(true, true, true, true);
|
|
1200
|
+
if (this._clipPath) gl.stencilFunc(gl.LESS, CLIP_BIT, 0xff);
|
|
1201
|
+
else gl.stencilFunc(gl.NOTEQUAL, 0, WINDING_MASK);
|
|
1202
|
+
// the mask is still WINDING_MASK: the count goes, the clip bit stays
|
|
1203
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.ZERO);
|
|
1204
|
+
const { x, y, w, h } = bounds;
|
|
1205
|
+
const saved = this._m;
|
|
1206
|
+
this._m = IDENTITY;
|
|
1207
|
+
if (style instanceof Gradient) {
|
|
1208
|
+
this._m = saved; // gradient endpoints are in user space
|
|
1209
|
+
this._useGradient(style);
|
|
1210
|
+
this._m = IDENTITY;
|
|
1211
|
+
const a = this.globalAlpha;
|
|
1212
|
+
this._quad(
|
|
1213
|
+
[x, y, x + w, y, x + w, y + h, x, y + h],
|
|
1214
|
+
UV_UNIT,
|
|
1215
|
+
[a, a, a, a],
|
|
1216
|
+
ZERO4,
|
|
1217
|
+
);
|
|
1218
|
+
} else {
|
|
1219
|
+
this._emitRect(x, y, w, h, color);
|
|
1220
|
+
}
|
|
1221
|
+
this._flush();
|
|
1222
|
+
this._m = saved;
|
|
1223
|
+
if (this._clipPath) {
|
|
1224
|
+
// counts outside the clip, which the cover did not reach
|
|
1225
|
+
gl.colorMask(false, false, false, false);
|
|
1226
|
+
gl.stencilFunc(gl.NOTEQUAL, 0, WINDING_MASK);
|
|
1227
|
+
this._stencilQuad(x, y, w, h);
|
|
1228
|
+
gl.colorMask(true, true, true, true);
|
|
1229
|
+
}
|
|
1230
|
+
gl.disable(gl.STENCIL_TEST);
|
|
1231
|
+
this._stencilling = false;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* The coverage route. The raster is cut to the clip and the target first:
|
|
1236
|
+
* pixels outside are never seen, and a large path that is mostly clipped
|
|
1237
|
+
* away — a chart line scrolled half out of view — is a small raster.
|
|
1238
|
+
*
|
|
1239
|
+
* @returns {boolean} false when the stencil route should take the fill
|
|
1240
|
+
*/
|
|
1241
|
+
_fillCoverage(polys, rule, bounds, color) {
|
|
1242
|
+
const c = this._clip;
|
|
1243
|
+
const x0 = Math.max(0, Math.floor(bounds.x) - 1, c ? Math.floor(c.x) : 0);
|
|
1244
|
+
const y0 = Math.max(0, Math.floor(bounds.y) - 1, c ? Math.floor(c.y) : 0);
|
|
1245
|
+
const x1 = Math.min(
|
|
1246
|
+
this._width,
|
|
1247
|
+
Math.ceil(bounds.x + bounds.w) + 1,
|
|
1248
|
+
c ? Math.ceil(c.x + c.w) : this._width,
|
|
1249
|
+
);
|
|
1250
|
+
const y1 = Math.min(
|
|
1251
|
+
this._height,
|
|
1252
|
+
Math.ceil(bounds.y + bounds.h) + 1,
|
|
1253
|
+
c ? Math.ceil(c.y + c.h) : this._height,
|
|
1254
|
+
);
|
|
1255
|
+
const w = x1 - x0;
|
|
1256
|
+
const h = y1 - y0;
|
|
1257
|
+
if (w <= 0 || h <= 0) return true; // nothing of it is visible
|
|
1258
|
+
if (w * h > COVERAGE_MAX_PIXELS) return false;
|
|
1259
|
+
const data = rasterizePolys(
|
|
1260
|
+
polys.map((poly) => poly.pts),
|
|
1261
|
+
w,
|
|
1262
|
+
h,
|
|
1263
|
+
rule,
|
|
1264
|
+
{ dx: -x0, dy: -y0 },
|
|
1265
|
+
);
|
|
1266
|
+
const entry = this.masks.get(`m${this._maskSeq++}`, () => ({
|
|
1267
|
+
width: w,
|
|
1268
|
+
height: h,
|
|
1269
|
+
left: 0,
|
|
1270
|
+
top: 0,
|
|
1271
|
+
data,
|
|
1272
|
+
}));
|
|
1273
|
+
// no room even after growing: the stencil takes this one
|
|
1274
|
+
if (entry.empty) return false;
|
|
1275
|
+
this.shapeStats.paths++;
|
|
1276
|
+
this._setMode(MODE_GLYPH, this.masks.bind(), SWIZZLE_RGBA);
|
|
1277
|
+
this._quad(
|
|
1278
|
+
[x0, y0, x1, y0, x1, y1, x0, y1],
|
|
1279
|
+
[
|
|
1280
|
+
entry.u0,
|
|
1281
|
+
entry.v0,
|
|
1282
|
+
entry.u1,
|
|
1283
|
+
entry.v0,
|
|
1284
|
+
entry.u1,
|
|
1285
|
+
entry.v1,
|
|
1286
|
+
entry.u0,
|
|
1287
|
+
entry.v1,
|
|
1288
|
+
],
|
|
1289
|
+
color,
|
|
1290
|
+
ZERO4,
|
|
1291
|
+
);
|
|
1292
|
+
return true;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** Pass 1 of stencil-then-cover: winding counts into the low seven bits,
|
|
1296
|
+
* colour writes off. Leaves the stencil test on and colour writes off. */
|
|
1297
|
+
_windingPass(polys, rule) {
|
|
1298
|
+
const gl = this.gl;
|
|
1299
|
+
gl.enable(gl.STENCIL_TEST);
|
|
1300
|
+
gl.stencilMask(WINDING_MASK);
|
|
1301
|
+
gl.colorMask(false, false, false, false);
|
|
1302
|
+
gl.stencilFunc(gl.ALWAYS, 0, 0xff);
|
|
1303
|
+
this._mode = MODE_SOLID;
|
|
1304
|
+
this._texture = null;
|
|
1305
|
+
this._swizzle = SWIZZLE_RGBA;
|
|
1306
|
+
|
|
1307
|
+
const fans = () => {
|
|
1308
|
+
for (const poly of polys) {
|
|
1309
|
+
const p = poly.pts;
|
|
1310
|
+
if (p.length < 6) continue;
|
|
1311
|
+
for (let i = 2; i + 1 < p.length; i += 2) {
|
|
1312
|
+
this._tri(p[0], p[1], p[i - 2], p[i - 1], p[i], p[i + 1]);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
this._flush();
|
|
1316
|
+
};
|
|
1317
|
+
if (rule === 'evenodd') {
|
|
1318
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.INVERT);
|
|
1319
|
+
fans();
|
|
1320
|
+
} else {
|
|
1321
|
+
// two draws with culling, since the binding has no stencilOpSeparate
|
|
1322
|
+
gl.enable(gl.CULL_FACE);
|
|
1323
|
+
gl.cullFace(gl.BACK);
|
|
1324
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.INCR_WRAP);
|
|
1325
|
+
fans();
|
|
1326
|
+
gl.cullFace(gl.FRONT);
|
|
1327
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.DECR_WRAP);
|
|
1328
|
+
fans();
|
|
1329
|
+
gl.disable(gl.CULL_FACE);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/** A device-space rectangle drawn for its stencil effect alone. */
|
|
1334
|
+
_stencilQuad(x, y, w, h) {
|
|
1335
|
+
const saved = this._m;
|
|
1336
|
+
this._m = IDENTITY;
|
|
1337
|
+
this._emitRect(x, y, w, h, OPAQUE);
|
|
1338
|
+
this._flush();
|
|
1339
|
+
this._m = saved;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Put a shape into the clip.
|
|
1344
|
+
*
|
|
1345
|
+
* The first clip sets the bit where the shape's count is non-zero. A
|
|
1346
|
+
* nested one intersects instead: across the clip so far, the bit is
|
|
1347
|
+
* *cleared* where the new count is zero — which covers the region outside
|
|
1348
|
+
* the new shape's bounds too, so the pass runs over the old clip rect,
|
|
1349
|
+
* before the scissor narrows. Either way the counts are then cleared, and
|
|
1350
|
+
* the scissor narrows to the bounds.
|
|
1351
|
+
*/
|
|
1352
|
+
_applyClipPath(polys, rule, bounds) {
|
|
1353
|
+
const gl = this.gl;
|
|
1354
|
+
this._flush();
|
|
1355
|
+
this._stencilling = true;
|
|
1356
|
+
this._windingPass(polys, rule);
|
|
1357
|
+
if (!this._clipPath) {
|
|
1358
|
+
gl.stencilMask(CLIP_BIT);
|
|
1359
|
+
gl.stencilFunc(gl.NOTEQUAL, CLIP_BIT, WINDING_MASK);
|
|
1360
|
+
// `stencilOp(sfail, dpfail, dppass)`: only the selected pixels change
|
|
1361
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
|
|
1362
|
+
this._stencilQuad(bounds.x, bounds.y, bounds.w, bounds.h);
|
|
1363
|
+
} else {
|
|
1364
|
+
const old = this._clip ?? {
|
|
1365
|
+
x: 0,
|
|
1366
|
+
y: 0,
|
|
1367
|
+
w: this._width,
|
|
1368
|
+
h: this._height,
|
|
1369
|
+
};
|
|
1370
|
+
gl.stencilMask(CLIP_BIT);
|
|
1371
|
+
gl.stencilFunc(gl.EQUAL, CLIP_BIT, 0xff);
|
|
1372
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.ZERO);
|
|
1373
|
+
this._stencilQuad(old.x, old.y, old.w, old.h);
|
|
1374
|
+
}
|
|
1375
|
+
gl.stencilMask(WINDING_MASK);
|
|
1376
|
+
gl.stencilFunc(gl.NOTEQUAL, 0, WINDING_MASK);
|
|
1377
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.ZERO);
|
|
1378
|
+
this._stencilQuad(bounds.x, bounds.y, bounds.w, bounds.h);
|
|
1379
|
+
gl.colorMask(true, true, true, true);
|
|
1380
|
+
gl.disable(gl.STENCIL_TEST);
|
|
1381
|
+
this._stencilling = false;
|
|
1382
|
+
this._clipPath = [...(this._clipPath ?? []), { polys, rule, bounds }];
|
|
1383
|
+
this._clip = this._clip ? intersectRect(this._clip, bounds) : bounds;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* `restore()` to a state with a different clip path: the bit is dropped
|
|
1388
|
+
* everywhere the current clip could have set it — its rect — and the
|
|
1389
|
+
* restored list is applied again from the restored rect. Clips nest
|
|
1390
|
+
* rarely and shallowly; re-rendering beats keeping a bit per level.
|
|
1391
|
+
*/
|
|
1392
|
+
_rebuildClipStencil(list, rect) {
|
|
1393
|
+
const gl = this.gl;
|
|
1394
|
+
this._flush();
|
|
1395
|
+
if (this._clipPath) {
|
|
1396
|
+
const old = this._clip ?? {
|
|
1397
|
+
x: 0,
|
|
1398
|
+
y: 0,
|
|
1399
|
+
w: this._width,
|
|
1400
|
+
h: this._height,
|
|
1401
|
+
};
|
|
1402
|
+
this._stencilling = true;
|
|
1403
|
+
gl.enable(gl.STENCIL_TEST);
|
|
1404
|
+
gl.colorMask(false, false, false, false);
|
|
1405
|
+
gl.stencilMask(CLIP_BIT);
|
|
1406
|
+
gl.stencilFunc(gl.ALWAYS, 0, 0xff);
|
|
1407
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.ZERO);
|
|
1408
|
+
this._stencilQuad(old.x, old.y, old.w, old.h);
|
|
1409
|
+
gl.colorMask(true, true, true, true);
|
|
1410
|
+
gl.disable(gl.STENCIL_TEST);
|
|
1411
|
+
this._stencilling = false;
|
|
1412
|
+
}
|
|
1413
|
+
this._clipPath = null;
|
|
1414
|
+
this._clip = rect;
|
|
1415
|
+
for (const c of list ?? []) this._applyClipPath(c.polys, c.rule, c.bounds);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// ---- images -----------------------------------------------------------
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Draw an image, a canvas `ImageData`, or an offscreen surface.
|
|
1422
|
+
*
|
|
1423
|
+
* `(img, dx, dy)`, `(img, dx, dy, dw, dh)` and
|
|
1424
|
+
* `(img, sx, sy, sw, sh, dx, dy, dw, dh)`, as canvas.
|
|
1425
|
+
*/
|
|
1426
|
+
drawImage(img, ...args) {
|
|
1427
|
+
this._claim();
|
|
1428
|
+
const src = this._textureFor(img);
|
|
1429
|
+
if (!src) return;
|
|
1430
|
+
let sx = 0;
|
|
1431
|
+
let sy = 0;
|
|
1432
|
+
let sw = src.w;
|
|
1433
|
+
let sh = src.h;
|
|
1434
|
+
let dx;
|
|
1435
|
+
let dy;
|
|
1436
|
+
let dw;
|
|
1437
|
+
let dh;
|
|
1438
|
+
if (args.length >= 8) [sx, sy, sw, sh, dx, dy, dw, dh] = args;
|
|
1439
|
+
else if (args.length >= 4) [dx, dy, dw, dh] = args;
|
|
1440
|
+
else {
|
|
1441
|
+
[dx, dy] = args;
|
|
1442
|
+
dw = sw;
|
|
1443
|
+
dh = sh;
|
|
1444
|
+
}
|
|
1445
|
+
if (!(dw > 0 && dh > 0 && sw > 0 && sh > 0)) return;
|
|
1446
|
+
this._setMode(MODE_TEXTURE, src.tex, src.swizzle);
|
|
1447
|
+
let u0 = sx / src.w;
|
|
1448
|
+
let v0 = sy / src.h;
|
|
1449
|
+
let u1 = (sx + sw) / src.w;
|
|
1450
|
+
let v1 = (sy + sh) / src.h;
|
|
1451
|
+
if (src.flipY) {
|
|
1452
|
+
// a render target's row 0 is the bottom of what was drawn into it
|
|
1453
|
+
v0 = 1 - v0;
|
|
1454
|
+
v1 = 1 - v1;
|
|
1455
|
+
}
|
|
1456
|
+
const m = this._m;
|
|
1457
|
+
const a = this.globalAlpha;
|
|
1458
|
+
this._quad(
|
|
1459
|
+
[
|
|
1460
|
+
applyX(m, dx, dy),
|
|
1461
|
+
applyY(m, dx, dy),
|
|
1462
|
+
applyX(m, dx + dw, dy),
|
|
1463
|
+
applyY(m, dx + dw, dy),
|
|
1464
|
+
applyX(m, dx + dw, dy + dh),
|
|
1465
|
+
applyY(m, dx + dw, dy + dh),
|
|
1466
|
+
applyX(m, dx, dy + dh),
|
|
1467
|
+
applyY(m, dx, dy + dh),
|
|
1468
|
+
],
|
|
1469
|
+
[u0, v0, u1, v0, u1, v1, u0, v1],
|
|
1470
|
+
[a, a, a, a],
|
|
1471
|
+
ZERO4,
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* Write pixels, ignoring the transform and the clip (the canvas rule).
|
|
1477
|
+
* `dirtyX/Y/Width/Height` select part of the data, as the spec has it.
|
|
1478
|
+
*/
|
|
1479
|
+
putImageData(data, x, y, dirtyX = 0, dirtyY = 0, dirtyWidth, dirtyHeight) {
|
|
1480
|
+
if (!data?.width || !data?.height) return;
|
|
1481
|
+
const dw = dirtyWidth ?? data.width;
|
|
1482
|
+
const dh = dirtyHeight ?? data.height;
|
|
1483
|
+
this._flush();
|
|
1484
|
+
const savedM = this._m;
|
|
1485
|
+
const savedClip = this._clip;
|
|
1486
|
+
const savedAlpha = this.globalAlpha;
|
|
1487
|
+
this._m = IDENTITY;
|
|
1488
|
+
this._clip = null;
|
|
1489
|
+
this.globalAlpha = 1;
|
|
1490
|
+
// Replace rather than blend, as the spec requires.
|
|
1491
|
+
const gl = this.gl;
|
|
1492
|
+
gl.blendFunc(gl.ONE, gl.ZERO);
|
|
1493
|
+
this.drawImage(
|
|
1494
|
+
data,
|
|
1495
|
+
dirtyX,
|
|
1496
|
+
dirtyY,
|
|
1497
|
+
dw,
|
|
1498
|
+
dh,
|
|
1499
|
+
x + dirtyX,
|
|
1500
|
+
y + dirtyY,
|
|
1501
|
+
dw,
|
|
1502
|
+
dh,
|
|
1503
|
+
);
|
|
1504
|
+
this._flush();
|
|
1505
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1506
|
+
this._m = savedM;
|
|
1507
|
+
this._clip = savedClip;
|
|
1508
|
+
this.globalAlpha = savedAlpha;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/** Read pixels back as straight RGBA, top row first. */
|
|
1512
|
+
getImageData(x, y, w, h) {
|
|
1513
|
+
this._flush();
|
|
1514
|
+
const gl = this.gl;
|
|
1515
|
+
// `readPixels` reads the bound framebuffer, and a blit or another
|
|
1516
|
+
// context may have left it pointing elsewhere since the last draw.
|
|
1517
|
+
this._target?.bind();
|
|
1518
|
+
const W = Math.max(0, w | 0);
|
|
1519
|
+
const H = Math.max(0, h | 0);
|
|
1520
|
+
const raw = new Uint8Array(W * H * 4);
|
|
1521
|
+
if (W && H) {
|
|
1522
|
+
gl.readPixels(
|
|
1523
|
+
x | 0,
|
|
1524
|
+
this._height - (y | 0) - H,
|
|
1525
|
+
W,
|
|
1526
|
+
H,
|
|
1527
|
+
gl.RGBA,
|
|
1528
|
+
gl.UNSIGNED_BYTE,
|
|
1529
|
+
raw,
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
const data = new Uint8ClampedArray(W * H * 4);
|
|
1533
|
+
const row = W * 4;
|
|
1534
|
+
for (let r = 0; r < H; r++)
|
|
1535
|
+
data.set(raw.subarray((H - 1 - r) * row, (H - r) * row), r * row);
|
|
1536
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
1537
|
+
const a = data[i + 3];
|
|
1538
|
+
if (a === 0 || a === 255) continue;
|
|
1539
|
+
data[i] = Math.min(255, (data[i] * 255) / a);
|
|
1540
|
+
data[i + 1] = Math.min(255, (data[i + 1] * 255) / a);
|
|
1541
|
+
data[i + 2] = Math.min(255, (data[i + 2] * 255) / a);
|
|
1542
|
+
}
|
|
1543
|
+
return { width: W, height: H, data };
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
createImageData(w, h) {
|
|
1547
|
+
return {
|
|
1548
|
+
width: w | 0,
|
|
1549
|
+
height: h | 0,
|
|
1550
|
+
data: new Uint8ClampedArray((w | 0) * (h | 0) * 4),
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* A texture for an image-like thing, with its byte layout.
|
|
1556
|
+
*
|
|
1557
|
+
* ntk `Image`s are immutable and uploaded once. `ImageData` is written by
|
|
1558
|
+
* the caller between draws, so its bytes go up every time — into the same
|
|
1559
|
+
* texture when the size has not changed. A render target *is* a texture.
|
|
1560
|
+
*/
|
|
1561
|
+
_textureFor(img) {
|
|
1562
|
+
if (!img) return null;
|
|
1563
|
+
const gl = this.gl;
|
|
1564
|
+
const target =
|
|
1565
|
+
img instanceof GLTarget
|
|
1566
|
+
? img
|
|
1567
|
+
: img.target instanceof GLTarget
|
|
1568
|
+
? img.target
|
|
1569
|
+
: null;
|
|
1570
|
+
if (target)
|
|
1571
|
+
return {
|
|
1572
|
+
tex: target.texture,
|
|
1573
|
+
w: target.width,
|
|
1574
|
+
h: target.height,
|
|
1575
|
+
swizzle: SWIZZLE_RGBA,
|
|
1576
|
+
flipY: true,
|
|
1577
|
+
};
|
|
1578
|
+
|
|
1579
|
+
const w = img.width | 0;
|
|
1580
|
+
const h = img.height | 0;
|
|
1581
|
+
const data = img.data ?? img.pixels;
|
|
1582
|
+
if (!w || !h || !data) return null;
|
|
1583
|
+
const isNtkImage = img instanceof NtkImage;
|
|
1584
|
+
const swizzle = isNtkImage
|
|
1585
|
+
? SWIZZLE_BGRA
|
|
1586
|
+
: data instanceof Uint8ClampedArray
|
|
1587
|
+
? SWIZZLE_STRAIGHT
|
|
1588
|
+
: img.premultiplied === false
|
|
1589
|
+
? SWIZZLE_STRAIGHT
|
|
1590
|
+
: SWIZZLE_RGBA;
|
|
1591
|
+
|
|
1592
|
+
let entry = this._textures.get(img);
|
|
1593
|
+
const bytes =
|
|
1594
|
+
data instanceof Uint8Array
|
|
1595
|
+
? data
|
|
1596
|
+
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1597
|
+
if (!entry || entry.w !== w || entry.h !== h) {
|
|
1598
|
+
if (entry) gl.deleteTexture(entry.tex);
|
|
1599
|
+
const tex = gl.createTexture();
|
|
1600
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1601
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
1602
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1603
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1604
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1605
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1606
|
+
gl.texImage2D(
|
|
1607
|
+
gl.TEXTURE_2D,
|
|
1608
|
+
0,
|
|
1609
|
+
gl.RGBA,
|
|
1610
|
+
w,
|
|
1611
|
+
h,
|
|
1612
|
+
0,
|
|
1613
|
+
gl.RGBA,
|
|
1614
|
+
gl.UNSIGNED_BYTE,
|
|
1615
|
+
bytes,
|
|
1616
|
+
);
|
|
1617
|
+
entry = { tex, w, h, swizzle, flipY: false, immutable: isNtkImage };
|
|
1618
|
+
this._textures.set(img, entry);
|
|
1619
|
+
} else if (!entry.immutable) {
|
|
1620
|
+
this._flush();
|
|
1621
|
+
gl.bindTexture(gl.TEXTURE_2D, entry.tex);
|
|
1622
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
1623
|
+
gl.texSubImage2D(
|
|
1624
|
+
gl.TEXTURE_2D,
|
|
1625
|
+
0,
|
|
1626
|
+
0,
|
|
1627
|
+
0,
|
|
1628
|
+
w,
|
|
1629
|
+
h,
|
|
1630
|
+
gl.RGBA,
|
|
1631
|
+
gl.UNSIGNED_BYTE,
|
|
1632
|
+
bytes,
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
return entry;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// ---- text -------------------------------------------------------------
|
|
1639
|
+
|
|
1640
|
+
/** ntk's Render extension object, as much of it as glyph runs need. */
|
|
1641
|
+
get Render() {
|
|
1642
|
+
return RENDER;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* A solid ink for `drawGlyphs`. ntk answers an XRender `Picture` here and
|
|
1647
|
+
* the Cocoa backend the colour itself; this answers the same stand-in
|
|
1648
|
+
* `_stylePicture` builds, so the way ntk documents the call
|
|
1649
|
+
*
|
|
1650
|
+
* ctx.drawGlyphs(ctx.Render.PictOp.Over,
|
|
1651
|
+
* ctx.createSolidPicture(r, g, b, a), runs)
|
|
1652
|
+
*
|
|
1653
|
+
* draws on every backend unchanged. Half the pair — `drawGlyphs` without
|
|
1654
|
+
* this — left every caller that positions its own glyphs (a terminal
|
|
1655
|
+
* grid, a tabular column) feature-detecting, and a feature test that came
|
|
1656
|
+
* back false drew a frame with no text in it rather than throwing (issue
|
|
1657
|
+
* #565).
|
|
1658
|
+
*
|
|
1659
|
+
* The channels are **premultiplied** 0..1, as XRender solids are, which
|
|
1660
|
+
* is the form this context carries colours in — so they pass through
|
|
1661
|
+
* `parseColor` untouched. A channel brighter than the alpha is not a
|
|
1662
|
+
* premultiplied colour; it is clamped down to it, which is the same
|
|
1663
|
+
* colour the Cocoa backend lands on for that input (it un-premultiplies
|
|
1664
|
+
* and clamps to 1).
|
|
1665
|
+
*/
|
|
1666
|
+
createSolidPicture(r, g, b, a) {
|
|
1667
|
+
const alpha = clamp01(a);
|
|
1668
|
+
return {
|
|
1669
|
+
color: Object.freeze([
|
|
1670
|
+
Math.min(clamp01(r), alpha),
|
|
1671
|
+
Math.min(clamp01(g), alpha),
|
|
1672
|
+
Math.min(clamp01(b), alpha),
|
|
1673
|
+
alpha,
|
|
1674
|
+
]),
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* ntk's glyph-drawing contract, which is how `TextLayout.draw` reaches a
|
|
1680
|
+
* context — and therefore how every `<text>`, `<textinput>` and
|
|
1681
|
+
* `<textarea>` in react-x11 gets on screen.
|
|
1682
|
+
*
|
|
1683
|
+
* The signature is XRender's: a `PictOp`, a source `Picture`, and glyph
|
|
1684
|
+
* runs already placed at absolute baselines. The op is always `Over` for
|
|
1685
|
+
* text and the source is always a solid colour, so what this does is take
|
|
1686
|
+
* the colour back out of the ink handed over (`_inkOf`) and draw the runs
|
|
1687
|
+
* through the atlas.
|
|
1688
|
+
*
|
|
1689
|
+
* @param {number} op ignored — text is always drawn `Over`
|
|
1690
|
+
* @param {object|string|number[]|null} src a `createSolidPicture` ink, a
|
|
1691
|
+
* `_stylePicture` stand-in, a colour, or null for the fill style
|
|
1692
|
+
* @param {Array<{run:object, x:number, y:number}>} positioned
|
|
1693
|
+
*/
|
|
1694
|
+
drawGlyphs(op, src, positioned) {
|
|
1695
|
+
if (!Array.isArray(positioned)) return;
|
|
1696
|
+
const color = this._inkOf(src);
|
|
1697
|
+
const runs = [];
|
|
1698
|
+
for (const p of positioned) {
|
|
1699
|
+
const run = p.run;
|
|
1700
|
+
if (!run?.glyphs?.length) continue;
|
|
1701
|
+
// ntk's shaped glyphs carry advances, not positions: the server walked
|
|
1702
|
+
// the run and accumulated them. There is no server, so walk it here.
|
|
1703
|
+
let pen = p.x;
|
|
1704
|
+
const glyphs = [];
|
|
1705
|
+
for (const g of run.glyphs) {
|
|
1706
|
+
glyphs.push({ id: g.id, x: pen + (g.dx ?? 0), y: p.y + (g.dy ?? 0) });
|
|
1707
|
+
pen += g.ax ?? 0;
|
|
1708
|
+
}
|
|
1709
|
+
runs.push({ font: run.font, size: run.size, glyphs, color });
|
|
1710
|
+
}
|
|
1711
|
+
this.drawTextRuns(runs);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
/** A solid-colour stand-in for the XRender `Picture` a source would be. */
|
|
1715
|
+
_stylePicture(color) {
|
|
1716
|
+
return { color };
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* The colour a `drawGlyphs` source paints with: a `createSolidPicture`
|
|
1721
|
+
* ink or a `_stylePicture` stand-in (both carry `.color`), a CSS colour
|
|
1722
|
+
* string, a premultiplied `[r, g, b, a]` — or, for anything this context
|
|
1723
|
+
* cannot ink glyphs with (a gradient), the fill style in force. The same
|
|
1724
|
+
* set the Cocoa backend takes, so a caller that skips the picture and
|
|
1725
|
+
* names a colour draws here too.
|
|
1726
|
+
*/
|
|
1727
|
+
_inkOf(src) {
|
|
1728
|
+
if (typeof src === 'string' || Array.isArray(src)) return src;
|
|
1729
|
+
const color = src?.color;
|
|
1730
|
+
return typeof color === 'string' || Array.isArray(color)
|
|
1731
|
+
? color
|
|
1732
|
+
: this.fillStyle;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
get _backgroundPicture() {
|
|
1736
|
+
return { color: this.fillStyle };
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/** The X11 context tracks dirty regions for its damage; this one does not. */
|
|
1740
|
+
_markDirty() {}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Draw positioned glyph runs through the atlas.
|
|
1744
|
+
*
|
|
1745
|
+
* Glyph origins are rounded to whole device pixels — the same rule ntk's
|
|
1746
|
+
* layout applies — because a glyph bitmap sampled at a fractional offset
|
|
1747
|
+
* under linear filtering is a blurred glyph.
|
|
1748
|
+
*
|
|
1749
|
+
* @param {Array<{font, size, glyphs:Array<{id,x,y}>, color}>} runs
|
|
1750
|
+
*/
|
|
1751
|
+
drawTextRuns(runs) {
|
|
1752
|
+
const m = this._m;
|
|
1753
|
+
for (const run of runs) {
|
|
1754
|
+
const font = run.font;
|
|
1755
|
+
if (!font) continue;
|
|
1756
|
+
const size = run.size ?? font.size ?? 16;
|
|
1757
|
+
const color = this._color(run.color ?? this.fillStyle);
|
|
1758
|
+
if (color[3] === 0) continue;
|
|
1759
|
+
const key = fontKey(font);
|
|
1760
|
+
const tex = this.atlas.bind();
|
|
1761
|
+
this._setMode(MODE_GLYPH, tex, SWIZZLE_RGBA);
|
|
1762
|
+
for (const g of run.glyphs) {
|
|
1763
|
+
const entry = this.atlas.get(`${key}|${size}|${g.id}`, () =>
|
|
1764
|
+
font.rasterize(g.id, size),
|
|
1765
|
+
);
|
|
1766
|
+
if (entry.empty) continue;
|
|
1767
|
+
// `left`/`top` are the bitmap's offset from the glyph origin in the
|
|
1768
|
+
// rasteriser's **y-down** pixel space: a cap-height glyph has a
|
|
1769
|
+
// negative `top` (H at 32px: -24).
|
|
1770
|
+
const ox = Math.round(applyX(m, g.x, g.y)) + entry.left;
|
|
1771
|
+
const oy = Math.round(applyY(m, g.x, g.y)) + entry.top;
|
|
1772
|
+
// the atlas may have grown mid-run; rebind so the batch names the
|
|
1773
|
+
// new texture (the old quads were flushed by onBeforeGrow)
|
|
1774
|
+
if (this.atlas.texture !== this._texture)
|
|
1775
|
+
this._setMode(MODE_GLYPH, this.atlas.bind(), SWIZZLE_RGBA);
|
|
1776
|
+
this._quad(
|
|
1777
|
+
[
|
|
1778
|
+
ox,
|
|
1779
|
+
oy,
|
|
1780
|
+
ox + entry.w,
|
|
1781
|
+
oy,
|
|
1782
|
+
ox + entry.w,
|
|
1783
|
+
oy + entry.h,
|
|
1784
|
+
ox,
|
|
1785
|
+
oy + entry.h,
|
|
1786
|
+
],
|
|
1787
|
+
[
|
|
1788
|
+
entry.u0,
|
|
1789
|
+
entry.v0,
|
|
1790
|
+
entry.u1,
|
|
1791
|
+
entry.v0,
|
|
1792
|
+
entry.u1,
|
|
1793
|
+
entry.v1,
|
|
1794
|
+
entry.u0,
|
|
1795
|
+
entry.v1,
|
|
1796
|
+
],
|
|
1797
|
+
color,
|
|
1798
|
+
ZERO4,
|
|
1799
|
+
);
|
|
1800
|
+
this.shapeStats.glyphs++;
|
|
1801
|
+
}
|
|
1802
|
+
// anything rasterised in this run goes up before the batch is drawn
|
|
1803
|
+
if (this.atlas._dirty || this.atlas._fresh) this.atlas.bind();
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/** The Font for the current `font` string, or null while it is loading. */
|
|
1808
|
+
_resolveFont() {
|
|
1809
|
+
if (!this.fontManager) return null;
|
|
1810
|
+
if (this._fontFace && this._fontFaceFor === this._font)
|
|
1811
|
+
return this._fontFace;
|
|
1812
|
+
const spec = this._fontSpec;
|
|
1813
|
+
let face = null;
|
|
1814
|
+
try {
|
|
1815
|
+
face = this.fontManager.match({
|
|
1816
|
+
family: spec.family,
|
|
1817
|
+
weight: spec.weight,
|
|
1818
|
+
italic: spec.italic,
|
|
1819
|
+
size: spec.size,
|
|
1820
|
+
});
|
|
1821
|
+
} catch {
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
if (face && typeof face.then === 'function') {
|
|
1825
|
+
// a face still opening: use it next time
|
|
1826
|
+
face.then(
|
|
1827
|
+
(f) => {
|
|
1828
|
+
if (this._font === spec && f) {
|
|
1829
|
+
this._fontFace = f;
|
|
1830
|
+
this._fontFaceFor = this._font;
|
|
1831
|
+
}
|
|
1832
|
+
},
|
|
1833
|
+
() => {},
|
|
1834
|
+
);
|
|
1835
|
+
return null;
|
|
1836
|
+
}
|
|
1837
|
+
this._fontFace = face;
|
|
1838
|
+
this._fontFaceFor = this._font;
|
|
1839
|
+
return face;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/** Shaped text for `fillText`/`measureText`, through ntk's shaping memo. */
|
|
1843
|
+
_shape(text) {
|
|
1844
|
+
const face = this._resolveFont();
|
|
1845
|
+
if (!face) return null;
|
|
1846
|
+
const spec = this._fontSpec;
|
|
1847
|
+
const style = {
|
|
1848
|
+
font: face,
|
|
1849
|
+
family: spec.family,
|
|
1850
|
+
size: spec.size,
|
|
1851
|
+
weight: spec.weight,
|
|
1852
|
+
italic: spec.italic,
|
|
1853
|
+
};
|
|
1854
|
+
const shaped = this.fontManager._shapeCachedWhole(String(text), style);
|
|
1855
|
+
return { shaped, style };
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
fillText(text, x, y) {
|
|
1859
|
+
text = String(text ?? '');
|
|
1860
|
+
if (!text) return;
|
|
1861
|
+
const r = this._shape(text);
|
|
1862
|
+
if (!r) return;
|
|
1863
|
+
const { shaped, style } = r;
|
|
1864
|
+
const metrics = style.font.metrics(style.size);
|
|
1865
|
+
let ax = 0;
|
|
1866
|
+
let align = this.textAlign;
|
|
1867
|
+
if (align === 'start') align = shaped.baseLevel & 1 ? 'right' : 'left';
|
|
1868
|
+
if (align === 'end') align = shaped.baseLevel & 1 ? 'left' : 'right';
|
|
1869
|
+
if (align === 'center') ax = -shaped.width / 2;
|
|
1870
|
+
else if (align === 'right') ax = -shaped.width;
|
|
1871
|
+
let ay = 0;
|
|
1872
|
+
switch (this.textBaseline) {
|
|
1873
|
+
case 'top':
|
|
1874
|
+
ay = metrics.ascent;
|
|
1875
|
+
break;
|
|
1876
|
+
case 'hanging':
|
|
1877
|
+
ay = metrics.ascent * 0.8;
|
|
1878
|
+
break;
|
|
1879
|
+
case 'middle':
|
|
1880
|
+
ay = (metrics.ascent - metrics.descent) / 2;
|
|
1881
|
+
break;
|
|
1882
|
+
case 'bottom':
|
|
1883
|
+
case 'ideographic':
|
|
1884
|
+
ay = -metrics.descent;
|
|
1885
|
+
break;
|
|
1886
|
+
default:
|
|
1887
|
+
ay = 0;
|
|
1888
|
+
}
|
|
1889
|
+
const positioned = [];
|
|
1890
|
+
let cursor = x + ax;
|
|
1891
|
+
for (const run of shaped.runs) {
|
|
1892
|
+
positioned.push({ run, x: cursor, y: y + ay });
|
|
1893
|
+
cursor += run.width;
|
|
1894
|
+
}
|
|
1895
|
+
this.drawGlyphs(3, { color: this.fillStyle }, positioned);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
strokeText(text, x, y) {
|
|
1899
|
+
// No outline stroking of glyphs; the fill is the honest approximation.
|
|
1900
|
+
const saved = this.fillStyle;
|
|
1901
|
+
this.fillStyle = this.strokeStyle;
|
|
1902
|
+
this.fillText(text, x, y);
|
|
1903
|
+
this.fillStyle = saved;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
measureText(text) {
|
|
1907
|
+
const r = this._shape(String(text ?? ''));
|
|
1908
|
+
if (!r)
|
|
1909
|
+
return {
|
|
1910
|
+
width: 0,
|
|
1911
|
+
actualBoundingBoxLeft: 0,
|
|
1912
|
+
actualBoundingBoxRight: 0,
|
|
1913
|
+
actualBoundingBoxAscent: 0,
|
|
1914
|
+
actualBoundingBoxDescent: 0,
|
|
1915
|
+
};
|
|
1916
|
+
const { shaped, style } = r;
|
|
1917
|
+
const m = style.font.metrics(style.size);
|
|
1918
|
+
return {
|
|
1919
|
+
width: shaped.width,
|
|
1920
|
+
actualBoundingBoxLeft: 0,
|
|
1921
|
+
actualBoundingBoxRight: shaped.width,
|
|
1922
|
+
actualBoundingBoxAscent: m.ascent,
|
|
1923
|
+
actualBoundingBoxDescent: m.descent,
|
|
1924
|
+
fontBoundingBoxAscent: m.ascent,
|
|
1925
|
+
fontBoundingBoxDescent: m.descent,
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// ---- batching ---------------------------------------------------------
|
|
1930
|
+
|
|
1931
|
+
_color(style) {
|
|
1932
|
+
const c = parseColor(style);
|
|
1933
|
+
if (this.globalAlpha >= 1) return c;
|
|
1934
|
+
const a = this.globalAlpha;
|
|
1935
|
+
return [c[0] * a, c[1] * a, c[2] * a, c[3] * a];
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
_setMode(mode, texture, swizzle) {
|
|
1939
|
+
if (
|
|
1940
|
+
mode !== this._mode ||
|
|
1941
|
+
texture !== this._texture ||
|
|
1942
|
+
swizzle !== this._swizzle
|
|
1943
|
+
) {
|
|
1944
|
+
this._flush();
|
|
1945
|
+
this._mode = mode;
|
|
1946
|
+
this._texture = texture;
|
|
1947
|
+
this._swizzle = swizzle;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/** Two triangles from four corners, in order TL, TR, BR, BL. */
|
|
1952
|
+
_quad(pos, uv, color, params) {
|
|
1953
|
+
if (this._n + 6 > MAX_VERTS) this._flush();
|
|
1954
|
+
const v = this._verts;
|
|
1955
|
+
let o = this._n * STRIDE;
|
|
1956
|
+
for (let k = 0; k < 6; k++) {
|
|
1957
|
+
const i = TRI[k];
|
|
1958
|
+
v[o++] = pos[i * 2];
|
|
1959
|
+
v[o++] = pos[i * 2 + 1];
|
|
1960
|
+
v[o++] = uv[i * 2];
|
|
1961
|
+
v[o++] = uv[i * 2 + 1];
|
|
1962
|
+
v[o++] = color[0];
|
|
1963
|
+
v[o++] = color[1];
|
|
1964
|
+
v[o++] = color[2];
|
|
1965
|
+
v[o++] = color[3];
|
|
1966
|
+
v[o++] = params[0];
|
|
1967
|
+
v[o++] = params[1];
|
|
1968
|
+
v[o++] = params[2];
|
|
1969
|
+
v[o++] = params[3];
|
|
1970
|
+
}
|
|
1971
|
+
this._n += 6;
|
|
1972
|
+
this.shapeStats.quads++;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
/** One triangle, for the stencil pass. Colour and params are irrelevant. */
|
|
1976
|
+
_tri(x0, y0, x1, y1, x2, y2) {
|
|
1977
|
+
if (this._n + 3 > MAX_VERTS) this._flush();
|
|
1978
|
+
const v = this._verts;
|
|
1979
|
+
let o = this._n * STRIDE;
|
|
1980
|
+
const pts = [x0, y0, x1, y1, x2, y2];
|
|
1981
|
+
for (let k = 0; k < 3; k++) {
|
|
1982
|
+
v[o++] = pts[k * 2];
|
|
1983
|
+
v[o++] = pts[k * 2 + 1];
|
|
1984
|
+
o += 10;
|
|
1985
|
+
}
|
|
1986
|
+
this._n += 3;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
_flush() {
|
|
1990
|
+
// Before the early return, not after: `clearRect` and `putImageData`
|
|
1991
|
+
// flush, change the blend function and flush again, and the second
|
|
1992
|
+
// flush must not be the one that re-takes the device and puts the
|
|
1993
|
+
// blending back.
|
|
1994
|
+
this._claim();
|
|
1995
|
+
if (this._n === 0) return;
|
|
1996
|
+
const gl = this.gl;
|
|
1997
|
+
const { aPos, aUV, aColor, aParams } = this._loc;
|
|
1998
|
+
|
|
1999
|
+
// Two contexts interleave inside one frame — a window's and the pane a
|
|
2000
|
+
// `<glarea>`'s children are painted on — so the target is bound here,
|
|
2001
|
+
// per batch, rather than trusted to still be bound from `begin()`.
|
|
2002
|
+
this._target?.bind();
|
|
2003
|
+
gl.useProgram(this._program);
|
|
2004
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this._buffer);
|
|
2005
|
+
gl.bufferSubData(
|
|
2006
|
+
gl.ARRAY_BUFFER,
|
|
2007
|
+
0,
|
|
2008
|
+
this._verts.subarray(0, this._n * STRIDE),
|
|
2009
|
+
);
|
|
2010
|
+
|
|
2011
|
+
const bytes = STRIDE * 4;
|
|
2012
|
+
gl.enableVertexAttribArray(aPos);
|
|
2013
|
+
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, bytes, 0);
|
|
2014
|
+
gl.enableVertexAttribArray(aUV);
|
|
2015
|
+
gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, bytes, 8);
|
|
2016
|
+
gl.enableVertexAttribArray(aColor);
|
|
2017
|
+
gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, bytes, 16);
|
|
2018
|
+
gl.enableVertexAttribArray(aParams);
|
|
2019
|
+
gl.vertexAttribPointer(aParams, 4, gl.FLOAT, false, bytes, 32);
|
|
2020
|
+
|
|
2021
|
+
gl.uniform1i(this._loc.uMode, this._mode);
|
|
2022
|
+
gl.uniform1i(this._loc.uSwizzle, this._swizzle);
|
|
2023
|
+
if (this._texture) {
|
|
2024
|
+
// Glyphs and masks placed since their atlas was last bound live only
|
|
2025
|
+
// in its CPU copy. A batch can be drawn before the run that placed
|
|
2026
|
+
// them ends — it fills up, or the atlas grows and flushes first — and
|
|
2027
|
+
// then it samples texels those glyphs never reached: garbled text,
|
|
2028
|
+
// kept until that region is next repainted.
|
|
2029
|
+
if (this._texture === this.atlas.texture) this.atlas.bind();
|
|
2030
|
+
else if (this._texture === this.masks.texture) this.masks.bind();
|
|
2031
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
2032
|
+
gl.bindTexture(gl.TEXTURE_2D, this._texture);
|
|
2033
|
+
gl.uniform1i(this._loc.uTex, 0);
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
if (this._clip) {
|
|
2037
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
2038
|
+
// Scissor is y-up from the bottom-left; our clips are y-down. Snap
|
|
2039
|
+
// outward to whole pixels, as XRender's integer clip would.
|
|
2040
|
+
const x0 = Math.floor(this._clip.x);
|
|
2041
|
+
const y0 = Math.floor(this._clip.y);
|
|
2042
|
+
const x1 = Math.ceil(this._clip.x + this._clip.w);
|
|
2043
|
+
const y1 = Math.ceil(this._clip.y + this._clip.h);
|
|
2044
|
+
gl.scissor(
|
|
2045
|
+
x0,
|
|
2046
|
+
Math.max(0, this._height - y1),
|
|
2047
|
+
Math.max(0, x1 - x0),
|
|
2048
|
+
Math.max(0, y1 - y0),
|
|
2049
|
+
);
|
|
2050
|
+
} else {
|
|
2051
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
if (!this._stencilling) {
|
|
2055
|
+
if (this._clipPath) {
|
|
2056
|
+
gl.enable(gl.STENCIL_TEST);
|
|
2057
|
+
gl.stencilFunc(gl.EQUAL, CLIP_BIT, CLIP_BIT);
|
|
2058
|
+
gl.stencilMask(0);
|
|
2059
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
|
|
2060
|
+
} else {
|
|
2061
|
+
gl.disable(gl.STENCIL_TEST);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
gl.drawArrays(gl.TRIANGLES, 0, this._n);
|
|
2066
|
+
this._n = 0;
|
|
2067
|
+
this._drew = true;
|
|
2068
|
+
this.shapeStats.batches++;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
_deviceRect(x, y, w, h) {
|
|
2072
|
+
const m = this._m;
|
|
2073
|
+
const x0 = applyX(m, x, y);
|
|
2074
|
+
const y0 = applyY(m, x, y);
|
|
2075
|
+
const x1 = applyX(m, x + w, y + h);
|
|
2076
|
+
const y1 = applyY(m, x + w, y + h);
|
|
2077
|
+
return {
|
|
2078
|
+
x: Math.min(x0, x1),
|
|
2079
|
+
y: Math.min(y0, y1),
|
|
2080
|
+
w: Math.abs(x1 - x0),
|
|
2081
|
+
h: Math.abs(y1 - y0),
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
destroy() {
|
|
2086
|
+
// Nothing of this context's is installed any more, and the device
|
|
2087
|
+
// record should not be the one thing still holding on to it.
|
|
2088
|
+
this._release();
|
|
2089
|
+
this.atlas.destroy();
|
|
2090
|
+
this.masks.destroy();
|
|
2091
|
+
if (this._buffer) this.gl.deleteBuffer(this._buffer);
|
|
2092
|
+
if (this._program) this.gl.deleteProgram(this._program);
|
|
2093
|
+
this._buffer = null;
|
|
2094
|
+
this._program = null;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
const UV_UNIT = Object.freeze([0, 0, 1, 0, 1, 1, 0, 1]);
|
|
2099
|
+
const ZERO4 = Object.freeze([0, 0, 0, 0]);
|
|
2100
|
+
|
|
2101
|
+
// ---- geometry helpers ------------------------------------------------------------
|
|
2102
|
+
|
|
2103
|
+
function intersectRect(a, b) {
|
|
2104
|
+
const x = Math.max(a.x, b.x);
|
|
2105
|
+
const y = Math.max(a.y, b.y);
|
|
2106
|
+
const r = Math.min(a.x + a.w, b.x + b.w);
|
|
2107
|
+
const bt = Math.min(a.y + a.h, b.y + b.h);
|
|
2108
|
+
return { x, y, w: Math.max(0, r - x), h: Math.max(0, bt - y) };
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
function polysBounds(polys) {
|
|
2112
|
+
let x0 = Infinity;
|
|
2113
|
+
let y0 = Infinity;
|
|
2114
|
+
let x1 = -Infinity;
|
|
2115
|
+
let y1 = -Infinity;
|
|
2116
|
+
for (const poly of polys) {
|
|
2117
|
+
const p = poly.pts;
|
|
2118
|
+
for (let i = 0; i < p.length; i += 2) {
|
|
2119
|
+
if (p[i] < x0) x0 = p[i];
|
|
2120
|
+
if (p[i] > x1) x1 = p[i];
|
|
2121
|
+
if (p[i + 1] < y0) y0 = p[i + 1];
|
|
2122
|
+
if (p[i + 1] > y1) y1 = p[i + 1];
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
if (!(x1 >= x0 && y1 >= y0)) return null;
|
|
2126
|
+
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
function polysContain(polys, x, y) {
|
|
2130
|
+
let wn = 0;
|
|
2131
|
+
for (const poly of polys) {
|
|
2132
|
+
const p = poly.pts;
|
|
2133
|
+
const n = p.length / 2;
|
|
2134
|
+
for (let i = 0; i < n; i++) {
|
|
2135
|
+
const ax = p[i * 2];
|
|
2136
|
+
const ay = p[i * 2 + 1];
|
|
2137
|
+
const bx = p[((i + 1) % n) * 2];
|
|
2138
|
+
const by = p[((i + 1) % n) * 2 + 1];
|
|
2139
|
+
if (ay <= y) {
|
|
2140
|
+
if (by > y && (bx - ax) * (y - ay) - (x - ax) * (by - ay) > 0) wn++;
|
|
2141
|
+
} else if (by <= y && (bx - ax) * (y - ay) - (x - ax) * (by - ay) < 0)
|
|
2142
|
+
wn--;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return wn !== 0;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
/**
|
|
2149
|
+
* Polygons whose non-zero union is the stroke of the given polylines: a
|
|
2150
|
+
* quad per segment, a join polygon per interior vertex (a small circle for
|
|
2151
|
+
* round joins, a bevel triangle otherwise, plus the miter tip when it is
|
|
2152
|
+
* within the limit), and round caps when asked. Every polygon is emitted
|
|
2153
|
+
* with the same orientation so that overlapping pieces add rather than
|
|
2154
|
+
* cancel under the non-zero rule.
|
|
2155
|
+
*/
|
|
2156
|
+
function strokePolys(polylines, width, join, cap, miterLimit) {
|
|
2157
|
+
const hw = width / 2;
|
|
2158
|
+
const out = [];
|
|
2159
|
+
const push = (pts) => {
|
|
2160
|
+
// consistent orientation: positive signed area
|
|
2161
|
+
let area = 0;
|
|
2162
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
2163
|
+
const j = (i + 2) % pts.length;
|
|
2164
|
+
area += pts[i] * pts[j + 1] - pts[j] * pts[i + 1];
|
|
2165
|
+
}
|
|
2166
|
+
if (area < 0) {
|
|
2167
|
+
const rev = [];
|
|
2168
|
+
for (let i = pts.length - 2; i >= 0; i -= 2) rev.push(pts[i], pts[i + 1]);
|
|
2169
|
+
pts = rev;
|
|
2170
|
+
}
|
|
2171
|
+
out.push({ pts, closed: true });
|
|
2172
|
+
};
|
|
2173
|
+
const circle = (cx, cy) => {
|
|
2174
|
+
const n = hw < 2 ? 6 : hw < 6 ? 10 : 16;
|
|
2175
|
+
const pts = [];
|
|
2176
|
+
for (let i = 0; i < n; i++) {
|
|
2177
|
+
const a = (i / n) * Math.PI * 2;
|
|
2178
|
+
pts.push(cx + Math.cos(a) * hw, cy + Math.sin(a) * hw);
|
|
2179
|
+
}
|
|
2180
|
+
push(pts);
|
|
2181
|
+
};
|
|
2182
|
+
for (const line of polylines) {
|
|
2183
|
+
const p = line.pts;
|
|
2184
|
+
let n = p.length / 2;
|
|
2185
|
+
if (n < 2) {
|
|
2186
|
+
if (n === 1 && cap === 'round') circle(p[0], p[1]);
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
const closed = line.closed;
|
|
2190
|
+
const segs = closed ? n : n - 1;
|
|
2191
|
+
for (let i = 0; i < segs; i++) {
|
|
2192
|
+
const ax = p[i * 2];
|
|
2193
|
+
const ay = p[i * 2 + 1];
|
|
2194
|
+
const j = (i + 1) % n;
|
|
2195
|
+
const bx = p[j * 2];
|
|
2196
|
+
const by = p[j * 2 + 1];
|
|
2197
|
+
let dx = bx - ax;
|
|
2198
|
+
let dy = by - ay;
|
|
2199
|
+
const len = Math.hypot(dx, dy);
|
|
2200
|
+
if (len === 0) continue;
|
|
2201
|
+
dx /= len;
|
|
2202
|
+
dy /= len;
|
|
2203
|
+
const nx = -dy * hw;
|
|
2204
|
+
const ny = dx * hw;
|
|
2205
|
+
let ex = 0;
|
|
2206
|
+
let ey = 0;
|
|
2207
|
+
if (!closed && cap === 'square') {
|
|
2208
|
+
ex = dx * hw;
|
|
2209
|
+
ey = dy * hw;
|
|
2210
|
+
}
|
|
2211
|
+
const sx = i === 0 ? ex : 0;
|
|
2212
|
+
const sy = i === 0 ? ey : 0;
|
|
2213
|
+
const tx = i === segs - 1 ? ex : 0;
|
|
2214
|
+
const ty = i === segs - 1 ? ey : 0;
|
|
2215
|
+
push([
|
|
2216
|
+
ax - sx + nx,
|
|
2217
|
+
ay - sy + ny,
|
|
2218
|
+
bx + tx + nx,
|
|
2219
|
+
by + ty + ny,
|
|
2220
|
+
bx + tx - nx,
|
|
2221
|
+
by + ty - ny,
|
|
2222
|
+
ax - sx - nx,
|
|
2223
|
+
ay - sy - ny,
|
|
2224
|
+
]);
|
|
2225
|
+
}
|
|
2226
|
+
// joins
|
|
2227
|
+
const first = closed ? 0 : 1;
|
|
2228
|
+
const last = closed ? n - 1 : n - 2;
|
|
2229
|
+
for (let i = first; i <= last; i++) {
|
|
2230
|
+
const cx = p[i * 2];
|
|
2231
|
+
const cy = p[i * 2 + 1];
|
|
2232
|
+
if (join === 'round' || hw <= 1) {
|
|
2233
|
+
circle(cx, cy);
|
|
2234
|
+
continue;
|
|
2235
|
+
}
|
|
2236
|
+
const prev = (i - 1 + n) % n;
|
|
2237
|
+
const next = (i + 1) % n;
|
|
2238
|
+
const ux = cx - p[prev * 2];
|
|
2239
|
+
const uy = cy - p[prev * 2 + 1];
|
|
2240
|
+
const vx = p[next * 2] - cx;
|
|
2241
|
+
const vy = p[next * 2 + 1] - cy;
|
|
2242
|
+
const ul = Math.hypot(ux, uy);
|
|
2243
|
+
const vl = Math.hypot(vx, vy);
|
|
2244
|
+
if (!ul || !vl) continue;
|
|
2245
|
+
const n1x = (-uy / ul) * hw;
|
|
2246
|
+
const n1y = (ux / ul) * hw;
|
|
2247
|
+
const n2x = (-vy / vl) * hw;
|
|
2248
|
+
const n2y = (vx / vl) * hw;
|
|
2249
|
+
const cross = ux * vy - uy * vx;
|
|
2250
|
+
const s = cross > 0 ? -1 : 1; // outer side
|
|
2251
|
+
const o1x = cx + n1x * s;
|
|
2252
|
+
const o1y = cy + n1y * s;
|
|
2253
|
+
const o2x = cx + n2x * s;
|
|
2254
|
+
const o2y = cy + n2y * s;
|
|
2255
|
+
// bevel
|
|
2256
|
+
push([cx, cy, o1x, o1y, o2x, o2y]);
|
|
2257
|
+
if (join === 'miter') {
|
|
2258
|
+
const cosHalf = Math.sqrt((1 + (ux * vx + uy * vy) / (ul * vl)) / 2);
|
|
2259
|
+
if (cosHalf > 1e-3 && 1 / cosHalf <= miterLimit) {
|
|
2260
|
+
const mx = (o1x + o2x) / 2 - cx;
|
|
2261
|
+
const my = (o1y + o2y) / 2 - cy;
|
|
2262
|
+
const ml = Math.hypot(mx, my);
|
|
2263
|
+
if (ml > 1e-6) {
|
|
2264
|
+
const tipLen = hw / cosHalf;
|
|
2265
|
+
const tx = cx + (mx / ml) * tipLen;
|
|
2266
|
+
const ty = cy + (my / ml) * tipLen;
|
|
2267
|
+
push([o1x, o1y, tx, ty, o2x, o2y]);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
if (!closed && cap === 'round') {
|
|
2273
|
+
circle(p[0], p[1]);
|
|
2274
|
+
circle(p[(n - 1) * 2], p[(n - 1) * 2 + 1]);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
return out;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function compile(gl, type, src, what) {
|
|
2281
|
+
const sh = gl.createShader(type);
|
|
2282
|
+
gl.shaderSource(sh, src);
|
|
2283
|
+
gl.compileShader(sh);
|
|
2284
|
+
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
|
2285
|
+
throw new Error(
|
|
2286
|
+
`${what} shader failed to compile: ${gl.getShaderInfoLog(sh)}`,
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
return sh;
|
|
2290
|
+
}
|