@vectojs/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/dist/Entity-D-rfAFCf.d.mts +572 -0
- package/dist/Entity-D-rfAFCf.d.ts +572 -0
- package/dist/chunk-2Y45S4JK.mjs +1380 -0
- package/dist/chunk-53DAQC3U.js +907 -0
- package/dist/chunk-72WVPMSJ.js +615 -0
- package/dist/chunk-LIX7DJTI.js +1380 -0
- package/dist/chunk-M2IZPGOL.mjs +907 -0
- package/dist/chunk-RW6NC4RB.js +376 -0
- package/dist/chunk-T456DL4P.mjs +615 -0
- package/dist/chunk-YA2J5ZH7.mjs +376 -0
- package/dist/index-ByBDSmMK.d.mts +365 -0
- package/dist/index-C3Fd_XmG.d.ts +365 -0
- package/dist/index.d.mts +577 -0
- package/dist/index.d.ts +577 -0
- package/dist/index.js +2238 -0
- package/dist/index.mjs +2238 -0
- package/dist/layout.d.mts +319 -0
- package/dist/layout.d.ts +319 -0
- package/dist/layout.js +16 -0
- package/dist/layout.mjs +16 -0
- package/dist/renderer.d.mts +2 -0
- package/dist/renderer.d.ts +2 -0
- package/dist/renderer.js +14 -0
- package/dist/renderer.mjs +14 -0
- package/dist/text.d.mts +201 -0
- package/dist/text.d.ts +201 -0
- package/dist/text.js +16 -0
- package/dist/text.mjs +16 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1380 @@
|
|
|
1
|
+
// src/renderer/CanvasRenderer.ts
|
|
2
|
+
var TWO_PI = Math.PI * 2;
|
|
3
|
+
function getDevicePixelRatio() {
|
|
4
|
+
return typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
5
|
+
}
|
|
6
|
+
var CanvasRenderer = class _CanvasRenderer {
|
|
7
|
+
ctx;
|
|
8
|
+
width;
|
|
9
|
+
height;
|
|
10
|
+
/**
|
|
11
|
+
* Max circles per batched `fill()`. A single Canvas 2D `fill()` over a path is
|
|
12
|
+
* superlinear in sub-path count, so an unbounded batch is *slower* than many
|
|
13
|
+
* small fills at high entity counts. Capping bounds each fill's path
|
|
14
|
+
* complexity while still amortizing per-draw overhead. Tuned via the benchmark.
|
|
15
|
+
*/
|
|
16
|
+
static MAX_BATCH = 64;
|
|
17
|
+
// Order-preserving batch state for fillCircle(): a run of same-style circles
|
|
18
|
+
// accumulates into one path and is committed by a single fill() on flush().
|
|
19
|
+
batchActive = false;
|
|
20
|
+
batchColor = "";
|
|
21
|
+
batchAlpha = 1;
|
|
22
|
+
batchCount = 0;
|
|
23
|
+
constructor(canvas) {
|
|
24
|
+
const dpr = getDevicePixelRatio();
|
|
25
|
+
this.width = typeof window !== "undefined" ? window.innerWidth : canvas.width || 0;
|
|
26
|
+
this.height = typeof window !== "undefined" ? window.innerHeight : canvas.height || 0;
|
|
27
|
+
canvas.width = this.width * dpr;
|
|
28
|
+
canvas.height = this.height * dpr;
|
|
29
|
+
const ctx = canvas.getContext("2d");
|
|
30
|
+
this.ctx = ctx;
|
|
31
|
+
if (ctx) ctx.scale(dpr, dpr);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Expose the underlying `CanvasRenderingContext2D` for operations not
|
|
35
|
+
* covered by the {@link IRenderer} interface.
|
|
36
|
+
*
|
|
37
|
+
* @returns The raw 2D rendering context.
|
|
38
|
+
*/
|
|
39
|
+
getContext() {
|
|
40
|
+
return this.ctx;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resize the backing canvas buffer and re-apply DPR scaling.
|
|
44
|
+
*
|
|
45
|
+
* Called automatically by {@link Scene} on `window.resize` events.
|
|
46
|
+
*
|
|
47
|
+
* @param width - New logical width in CSS pixels.
|
|
48
|
+
* @param height - New logical height in CSS pixels.
|
|
49
|
+
*/
|
|
50
|
+
resize(width, height) {
|
|
51
|
+
const dpr = getDevicePixelRatio();
|
|
52
|
+
this.width = width;
|
|
53
|
+
this.height = height;
|
|
54
|
+
this.ctx.canvas.width = width * dpr;
|
|
55
|
+
this.ctx.canvas.height = height * dpr;
|
|
56
|
+
this.ctx.canvas.style.width = `${width}px`;
|
|
57
|
+
this.ctx.canvas.style.height = `${height}px`;
|
|
58
|
+
this.ctx.scale(dpr, dpr);
|
|
59
|
+
}
|
|
60
|
+
/** @inheritdoc */
|
|
61
|
+
clear() {
|
|
62
|
+
this.flush();
|
|
63
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
64
|
+
}
|
|
65
|
+
/** @inheritdoc */
|
|
66
|
+
save() {
|
|
67
|
+
this.flush();
|
|
68
|
+
this.ctx.save();
|
|
69
|
+
}
|
|
70
|
+
/** @inheritdoc */
|
|
71
|
+
restore() {
|
|
72
|
+
this.flush();
|
|
73
|
+
this.ctx.restore();
|
|
74
|
+
}
|
|
75
|
+
/** @inheritdoc */
|
|
76
|
+
translate(x, y) {
|
|
77
|
+
this.ctx.translate(x, y);
|
|
78
|
+
}
|
|
79
|
+
/** @inheritdoc */
|
|
80
|
+
scale(x, y) {
|
|
81
|
+
this.ctx.scale(x, y);
|
|
82
|
+
}
|
|
83
|
+
/** @inheritdoc */
|
|
84
|
+
rotate(angle) {
|
|
85
|
+
this.ctx.rotate(angle);
|
|
86
|
+
}
|
|
87
|
+
/** @inheritdoc */
|
|
88
|
+
setGlobalAlpha(alpha) {
|
|
89
|
+
this.ctx.globalAlpha = alpha;
|
|
90
|
+
}
|
|
91
|
+
/** @inheritdoc */
|
|
92
|
+
clip(x, y, width, height) {
|
|
93
|
+
this.flush();
|
|
94
|
+
this.ctx.beginPath();
|
|
95
|
+
this.ctx.rect(x, y, width, height);
|
|
96
|
+
this.ctx.clip();
|
|
97
|
+
}
|
|
98
|
+
/** @inheritdoc */
|
|
99
|
+
beginPath() {
|
|
100
|
+
this.flush();
|
|
101
|
+
this.ctx.beginPath();
|
|
102
|
+
}
|
|
103
|
+
/** @inheritdoc */
|
|
104
|
+
moveTo(x, y) {
|
|
105
|
+
this.ctx.moveTo(x, y);
|
|
106
|
+
}
|
|
107
|
+
/** @inheritdoc */
|
|
108
|
+
lineTo(x, y) {
|
|
109
|
+
this.ctx.lineTo(x, y);
|
|
110
|
+
}
|
|
111
|
+
/** @inheritdoc */
|
|
112
|
+
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
|
|
113
|
+
this.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
|
|
114
|
+
}
|
|
115
|
+
/** @inheritdoc */
|
|
116
|
+
closePath() {
|
|
117
|
+
this.ctx.closePath();
|
|
118
|
+
}
|
|
119
|
+
/** @inheritdoc */
|
|
120
|
+
arc(x, y, radius, startAngle, endAngle, counterclockwise) {
|
|
121
|
+
this.ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise);
|
|
122
|
+
}
|
|
123
|
+
/** @inheritdoc */
|
|
124
|
+
roundRect(x, y, width, height, radii) {
|
|
125
|
+
this.ctx.roundRect(x, y, width, height, radii);
|
|
126
|
+
}
|
|
127
|
+
/** @inheritdoc */
|
|
128
|
+
drawImage(source, dx, dy, dw, dh) {
|
|
129
|
+
this.flush();
|
|
130
|
+
this.ctx.drawImage(source, dx, dy, dw, dh);
|
|
131
|
+
}
|
|
132
|
+
/** @inheritdoc */
|
|
133
|
+
fillCircle(cx, cy, radius, color, alpha = 1) {
|
|
134
|
+
if (this.batchActive && (color !== this.batchColor || alpha !== this.batchAlpha)) {
|
|
135
|
+
this.flush();
|
|
136
|
+
}
|
|
137
|
+
if (!this.batchActive) {
|
|
138
|
+
this.ctx.beginPath();
|
|
139
|
+
this.batchActive = true;
|
|
140
|
+
this.batchColor = color;
|
|
141
|
+
this.batchAlpha = alpha;
|
|
142
|
+
}
|
|
143
|
+
this.ctx.moveTo(cx + radius, cy);
|
|
144
|
+
this.ctx.arc(cx, cy, radius, 0, TWO_PI);
|
|
145
|
+
this.batchCount++;
|
|
146
|
+
if (this.batchCount >= _CanvasRenderer.MAX_BATCH) this.flush();
|
|
147
|
+
}
|
|
148
|
+
/** @inheritdoc */
|
|
149
|
+
flush() {
|
|
150
|
+
if (!this.batchActive) return;
|
|
151
|
+
this.ctx.globalAlpha = this.batchAlpha;
|
|
152
|
+
this.ctx.fillStyle = this.batchColor;
|
|
153
|
+
this.ctx.fill();
|
|
154
|
+
this.ctx.globalAlpha = 1;
|
|
155
|
+
this.batchActive = false;
|
|
156
|
+
this.batchCount = 0;
|
|
157
|
+
}
|
|
158
|
+
/** @inheritdoc */
|
|
159
|
+
fill(color) {
|
|
160
|
+
this.flush();
|
|
161
|
+
this.ctx.fillStyle = color;
|
|
162
|
+
this.ctx.fill();
|
|
163
|
+
}
|
|
164
|
+
/** @inheritdoc */
|
|
165
|
+
stroke(color, lineWidth = 1) {
|
|
166
|
+
this.flush();
|
|
167
|
+
this.ctx.strokeStyle = color;
|
|
168
|
+
this.ctx.lineWidth = lineWidth;
|
|
169
|
+
this.ctx.lineCap = "round";
|
|
170
|
+
this.ctx.lineJoin = "round";
|
|
171
|
+
this.ctx.stroke();
|
|
172
|
+
}
|
|
173
|
+
/** @inheritdoc */
|
|
174
|
+
fillText(text, x, y, font, color) {
|
|
175
|
+
this.flush();
|
|
176
|
+
this.ctx.font = font;
|
|
177
|
+
this.ctx.fillStyle = color;
|
|
178
|
+
this.ctx.fillText(text, x, y);
|
|
179
|
+
}
|
|
180
|
+
/** @inheritdoc */
|
|
181
|
+
createLinearGradient(x0, y0, x1, y1, colorStops) {
|
|
182
|
+
const grad = this.ctx.createLinearGradient(x0, y0, x1, y1);
|
|
183
|
+
for (const cs of colorStops) {
|
|
184
|
+
grad.addColorStop(cs.stop, cs.color);
|
|
185
|
+
}
|
|
186
|
+
return grad;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/renderer/SVGRenderer.ts
|
|
191
|
+
var SVGRenderer = class {
|
|
192
|
+
width;
|
|
193
|
+
height;
|
|
194
|
+
buffer = [];
|
|
195
|
+
defsBuffer = [];
|
|
196
|
+
currentPath = [];
|
|
197
|
+
// Inline matrix states (3x3 representation)
|
|
198
|
+
// [a, b, c, d, e, f] corresponding to:
|
|
199
|
+
// | a c e |
|
|
200
|
+
// | b d f |
|
|
201
|
+
// | 0 0 1 |
|
|
202
|
+
mStack = [];
|
|
203
|
+
ma = 1;
|
|
204
|
+
mb = 0;
|
|
205
|
+
mc = 0;
|
|
206
|
+
md = 1;
|
|
207
|
+
me = 0;
|
|
208
|
+
mf = 0;
|
|
209
|
+
// Alpha stack
|
|
210
|
+
alphaStack = [];
|
|
211
|
+
globalAlpha = 1;
|
|
212
|
+
// Clipping stack
|
|
213
|
+
clipDepthStack = [];
|
|
214
|
+
clipDepth = 0;
|
|
215
|
+
clipCounter = 0;
|
|
216
|
+
// uniquely identifies clipPaths
|
|
217
|
+
// Batch circles states (local coordinates)
|
|
218
|
+
batchCircles = [];
|
|
219
|
+
batchMatrix = [1, 0, 0, 1, 0, 0];
|
|
220
|
+
batchColor = "";
|
|
221
|
+
batchAlpha = 1;
|
|
222
|
+
batchActive = false;
|
|
223
|
+
// Cache for generated gradient defs
|
|
224
|
+
gradientCounter = 0;
|
|
225
|
+
gradientCache = /* @__PURE__ */ new Map();
|
|
226
|
+
constructor(width, height) {
|
|
227
|
+
this.width = width;
|
|
228
|
+
this.height = height;
|
|
229
|
+
}
|
|
230
|
+
clear() {
|
|
231
|
+
this.buffer = [];
|
|
232
|
+
this.defsBuffer = [];
|
|
233
|
+
this.currentPath = [];
|
|
234
|
+
this.gradientCounter = 0;
|
|
235
|
+
this.clipCounter = 0;
|
|
236
|
+
this.gradientCache.clear();
|
|
237
|
+
this.mStack = [];
|
|
238
|
+
this.alphaStack = [];
|
|
239
|
+
this.clipDepthStack = [];
|
|
240
|
+
this.ma = 1;
|
|
241
|
+
this.mb = 0;
|
|
242
|
+
this.mc = 0;
|
|
243
|
+
this.md = 1;
|
|
244
|
+
this.me = 0;
|
|
245
|
+
this.mf = 0;
|
|
246
|
+
this.globalAlpha = 1;
|
|
247
|
+
this.clipDepth = 0;
|
|
248
|
+
this.batchCircles = [];
|
|
249
|
+
this.batchActive = false;
|
|
250
|
+
}
|
|
251
|
+
save() {
|
|
252
|
+
this.flush();
|
|
253
|
+
this.mStack.push([this.ma, this.mb, this.mc, this.md, this.me, this.mf]);
|
|
254
|
+
this.alphaStack.push(this.globalAlpha);
|
|
255
|
+
this.clipDepthStack.push(this.clipDepth);
|
|
256
|
+
}
|
|
257
|
+
restore() {
|
|
258
|
+
this.flush();
|
|
259
|
+
if (this.mStack.length > 0) {
|
|
260
|
+
const m = this.mStack.pop();
|
|
261
|
+
this.ma = m[0];
|
|
262
|
+
this.mb = m[1];
|
|
263
|
+
this.mc = m[2];
|
|
264
|
+
this.md = m[3];
|
|
265
|
+
this.me = m[4];
|
|
266
|
+
this.mf = m[5];
|
|
267
|
+
}
|
|
268
|
+
if (this.alphaStack.length > 0) {
|
|
269
|
+
this.globalAlpha = this.alphaStack.pop();
|
|
270
|
+
}
|
|
271
|
+
if (this.clipDepthStack.length > 0) {
|
|
272
|
+
const poppedClipDepth = this.clipDepthStack.pop();
|
|
273
|
+
if (poppedClipDepth < this.clipDepth) {
|
|
274
|
+
for (let i = 0; i < this.clipDepth - poppedClipDepth; i++) {
|
|
275
|
+
this.buffer.push("</g>");
|
|
276
|
+
}
|
|
277
|
+
this.clipDepth = poppedClipDepth;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
translate(dx, dy) {
|
|
282
|
+
this.me = this.ma * dx + this.mc * dy + this.me;
|
|
283
|
+
this.mf = this.mb * dx + this.md * dy + this.mf;
|
|
284
|
+
}
|
|
285
|
+
scale(sx, sy) {
|
|
286
|
+
this.ma *= sx;
|
|
287
|
+
this.mb *= sx;
|
|
288
|
+
this.mc *= sy;
|
|
289
|
+
this.md *= sy;
|
|
290
|
+
}
|
|
291
|
+
rotate(angle) {
|
|
292
|
+
const cos = Math.cos(angle);
|
|
293
|
+
const sin = Math.sin(angle);
|
|
294
|
+
const a0 = this.ma, b0 = this.mb, c0 = this.mc, d0 = this.md;
|
|
295
|
+
this.ma = a0 * cos + c0 * sin;
|
|
296
|
+
this.mb = b0 * cos + d0 * sin;
|
|
297
|
+
this.mc = -a0 * sin + c0 * cos;
|
|
298
|
+
this.md = -b0 * sin + d0 * cos;
|
|
299
|
+
}
|
|
300
|
+
setGlobalAlpha(alpha) {
|
|
301
|
+
this.globalAlpha = alpha;
|
|
302
|
+
}
|
|
303
|
+
beginPath() {
|
|
304
|
+
this.currentPath = [];
|
|
305
|
+
}
|
|
306
|
+
moveTo(x, y) {
|
|
307
|
+
this.currentPath.push(`M ${x} ${y}`);
|
|
308
|
+
}
|
|
309
|
+
lineTo(x, y) {
|
|
310
|
+
this.currentPath.push(`L ${x} ${y}`);
|
|
311
|
+
}
|
|
312
|
+
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
|
|
313
|
+
this.currentPath.push(`C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${x} ${y}`);
|
|
314
|
+
}
|
|
315
|
+
closePath() {
|
|
316
|
+
this.currentPath.push("Z");
|
|
317
|
+
}
|
|
318
|
+
arc(x, y, r, startAngle, endAngle, ccw) {
|
|
319
|
+
const xs = x + r * Math.cos(startAngle);
|
|
320
|
+
const ys = y + r * Math.sin(startAngle);
|
|
321
|
+
if (this.currentPath.length === 0) {
|
|
322
|
+
this.currentPath.push(`M ${xs} ${ys}`);
|
|
323
|
+
} else {
|
|
324
|
+
this.currentPath.push(`L ${xs} ${ys}`);
|
|
325
|
+
}
|
|
326
|
+
const deltaAngle = Math.abs(endAngle - startAngle);
|
|
327
|
+
if (deltaAngle >= Math.PI * 2 - 1e-4) {
|
|
328
|
+
const xm = x - r * Math.cos(startAngle);
|
|
329
|
+
const ym = y - r * Math.sin(startAngle);
|
|
330
|
+
const sweep = ccw ? 0 : 1;
|
|
331
|
+
this.currentPath.push(`A ${r} ${r} 0 0 ${sweep} ${xm} ${ym}`);
|
|
332
|
+
this.currentPath.push(`A ${r} ${r} 0 0 ${sweep} ${xs} ${ys}`);
|
|
333
|
+
} else {
|
|
334
|
+
const xe = x + r * Math.cos(endAngle);
|
|
335
|
+
const ye = y + r * Math.sin(endAngle);
|
|
336
|
+
const largeArc = deltaAngle > Math.PI ? 1 : 0;
|
|
337
|
+
const sweep = ccw ? 0 : 1;
|
|
338
|
+
this.currentPath.push(`A ${r} ${r} 0 ${largeArc} ${sweep} ${xe} ${ye}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
roundRect(x, y, w, h, radii) {
|
|
342
|
+
if (w < 0) {
|
|
343
|
+
x += w;
|
|
344
|
+
w = -w;
|
|
345
|
+
}
|
|
346
|
+
if (h < 0) {
|
|
347
|
+
y += h;
|
|
348
|
+
h = -h;
|
|
349
|
+
}
|
|
350
|
+
let r_tl = 0, r_tr = 0, r_br = 0, r_bl = 0;
|
|
351
|
+
if (typeof radii === "number") {
|
|
352
|
+
r_tl = r_tr = r_br = r_bl = radii;
|
|
353
|
+
} else if (Array.isArray(radii)) {
|
|
354
|
+
if (radii.length === 1) {
|
|
355
|
+
r_tl = r_tr = r_br = r_bl = radii[0];
|
|
356
|
+
} else if (radii.length === 2) {
|
|
357
|
+
r_tl = r_br = radii[0];
|
|
358
|
+
r_tr = r_bl = radii[1];
|
|
359
|
+
} else if (radii.length === 3) {
|
|
360
|
+
r_tl = radii[0];
|
|
361
|
+
r_tr = r_bl = radii[1];
|
|
362
|
+
r_br = radii[2];
|
|
363
|
+
} else if (radii.length >= 4) {
|
|
364
|
+
r_tl = radii[0];
|
|
365
|
+
r_tr = radii[1];
|
|
366
|
+
r_br = radii[2];
|
|
367
|
+
r_bl = radii[3];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const tl_tr = r_tl + r_tr;
|
|
371
|
+
const bl_br = r_bl + r_br;
|
|
372
|
+
const tl_bl = r_tl + r_bl;
|
|
373
|
+
const tr_br = r_tr + r_br;
|
|
374
|
+
let factor = 1;
|
|
375
|
+
if (tl_tr > w) factor = Math.min(factor, w / tl_tr);
|
|
376
|
+
if (bl_br > w) factor = Math.min(factor, w / bl_br);
|
|
377
|
+
if (tl_bl > h) factor = Math.min(factor, h / tl_bl);
|
|
378
|
+
if (tr_br > h) factor = Math.min(factor, h / tr_br);
|
|
379
|
+
if (factor < 1) {
|
|
380
|
+
r_tl *= factor;
|
|
381
|
+
r_tr *= factor;
|
|
382
|
+
r_br *= factor;
|
|
383
|
+
r_bl *= factor;
|
|
384
|
+
}
|
|
385
|
+
this.currentPath.push(`M ${x + r_tl} ${y}`);
|
|
386
|
+
this.currentPath.push(`L ${x + w - r_tr} ${y}`);
|
|
387
|
+
this.currentPath.push(`A ${r_tr} ${r_tr} 0 0 1 ${x + w} ${y + r_tr}`);
|
|
388
|
+
this.currentPath.push(`L ${x + w} ${y + h - r_br}`);
|
|
389
|
+
this.currentPath.push(`A ${r_br} ${r_br} 0 0 1 ${x + w - r_br} ${y + h}`);
|
|
390
|
+
this.currentPath.push(`L ${x + r_bl} ${y + h}`);
|
|
391
|
+
this.currentPath.push(`A ${r_bl} ${r_bl} 0 0 1 ${x} ${y + h - r_bl}`);
|
|
392
|
+
this.currentPath.push(`L ${x} ${y + r_tl}`);
|
|
393
|
+
this.currentPath.push(`A ${r_tl} ${r_tl} 0 0 1 ${x + r_tl} ${y}`);
|
|
394
|
+
this.currentPath.push("Z");
|
|
395
|
+
}
|
|
396
|
+
fill(colorOrGradient) {
|
|
397
|
+
this.flush();
|
|
398
|
+
const fillVal = this.resolveGradient(colorOrGradient);
|
|
399
|
+
const dStr = this.currentPath.join(" ");
|
|
400
|
+
const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
|
|
401
|
+
this.buffer.push(
|
|
402
|
+
`<path d="${dStr}" transform="${transformStr}" fill="${fillVal}" opacity="${this.globalAlpha}" />`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
stroke(colorOrGradient, lineWidth = 1) {
|
|
406
|
+
this.flush();
|
|
407
|
+
const strokeVal = this.resolveGradient(colorOrGradient);
|
|
408
|
+
const dStr = this.currentPath.join(" ");
|
|
409
|
+
const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
|
|
410
|
+
this.buffer.push(
|
|
411
|
+
`<path d="${dStr}" transform="${transformStr}" fill="none" stroke="${strokeVal}" stroke-width="${lineWidth}" stroke-opacity="${this.globalAlpha}" />`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
fillText(text, x, y, font, color) {
|
|
415
|
+
this.flush();
|
|
416
|
+
const sizeMatch = font.match(/(\d+(?:\.\d+)?)(px|em|rem)/);
|
|
417
|
+
let fontSize = sizeMatch ? parseFloat(sizeMatch[1]) : 16;
|
|
418
|
+
if (sizeMatch && sizeMatch[2] !== "px") {
|
|
419
|
+
fontSize = fontSize * 16;
|
|
420
|
+
}
|
|
421
|
+
const styleMatch = font.match(/(italic|oblique)/i);
|
|
422
|
+
const fontWeightMatch = font.match(/(bold|[1-9]00)/i);
|
|
423
|
+
const fontStyle = styleMatch ? styleMatch[1].toLowerCase() : "normal";
|
|
424
|
+
const fontWeight = fontWeightMatch ? fontWeightMatch[1].toLowerCase() : "normal";
|
|
425
|
+
const cleanFont = font.replace(/\d+(?:\.\d+)?(px|em|rem)(?:\/\d+(?:\.\d+)?(?:px|em|rem|%)?)?/, "").trim();
|
|
426
|
+
const fontFamily = cleanFont.replace(/(bold|italic|normal|600|500|400|300|100)\s+/gi, "").trim() || "sans-serif";
|
|
427
|
+
const fillVal = this.resolveGradient(color);
|
|
428
|
+
const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
|
|
429
|
+
this.buffer.push(
|
|
430
|
+
`<g transform="${transformStr}"><text x="${x}" y="${y}" font-size="${fontSize}" font-weight="${fontWeight}" font-style="${fontStyle}" font-family="${this.escapeXML(fontFamily)}" fill="${fillVal}" opacity="${this.globalAlpha}">${this.escapeXML(text)}</text></g>`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
fillCircle(cx, cy, radius, color, alpha) {
|
|
434
|
+
const targetAlpha = alpha ?? 1;
|
|
435
|
+
const matrixEq = this.batchMatrix[0] === this.ma && this.batchMatrix[1] === this.mb && this.batchMatrix[2] === this.mc && this.batchMatrix[3] === this.md && this.batchMatrix[4] === this.me && this.batchMatrix[5] === this.mf;
|
|
436
|
+
if (this.batchActive && (!matrixEq || this.batchColor !== color || this.batchAlpha !== targetAlpha)) {
|
|
437
|
+
this.flush();
|
|
438
|
+
}
|
|
439
|
+
if (!this.batchActive) {
|
|
440
|
+
this.batchMatrix = [this.ma, this.mb, this.mc, this.md, this.me, this.mf];
|
|
441
|
+
this.batchColor = color;
|
|
442
|
+
this.batchAlpha = targetAlpha;
|
|
443
|
+
this.batchActive = true;
|
|
444
|
+
}
|
|
445
|
+
this.batchCircles.push({ cx, cy, r: radius });
|
|
446
|
+
}
|
|
447
|
+
drawImage(source, dx, dy, dw, dh) {
|
|
448
|
+
this.flush();
|
|
449
|
+
const href = typeof source?.toDataURL === "function" ? source.toDataURL() : source?.src || "";
|
|
450
|
+
const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
|
|
451
|
+
if (href) {
|
|
452
|
+
this.buffer.push(
|
|
453
|
+
`<image href="${this.escapeXML(href)}" x="${dx}" y="${dy}" width="${dw}" height="${dh}" transform="${transformStr}" />`
|
|
454
|
+
);
|
|
455
|
+
} else {
|
|
456
|
+
this.buffer.push(
|
|
457
|
+
`<rect x="${dx}" y="${dy}" width="${dw}" height="${dh}" transform="${transformStr}" fill="rgba(0,0,0,0.5)" />`
|
|
458
|
+
);
|
|
459
|
+
console.warn("drawImage source fallback triggered");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
flush() {
|
|
463
|
+
if (!this.batchActive || this.batchCircles.length === 0) return;
|
|
464
|
+
let d = "";
|
|
465
|
+
for (const c of this.batchCircles) {
|
|
466
|
+
const cx_minus_r = c.cx - c.r;
|
|
467
|
+
const cx_plus_r = c.cx + c.r;
|
|
468
|
+
d += `M ${cx_minus_r} ${c.cy} A ${c.r} ${c.r} 0 1 0 ${cx_plus_r} ${c.cy} A ${c.r} ${c.r} 0 1 0 ${cx_minus_r} ${c.cy} `;
|
|
469
|
+
}
|
|
470
|
+
const transformStr = `matrix(${this.batchMatrix.join(",")})`;
|
|
471
|
+
const pathNode = `<path d="${d.trim()}" transform="${transformStr}" fill="${this.escapeXML(this.batchColor)}" opacity="${this.batchAlpha}" />`;
|
|
472
|
+
this.buffer.push(pathNode);
|
|
473
|
+
this.batchCircles = [];
|
|
474
|
+
this.batchActive = false;
|
|
475
|
+
}
|
|
476
|
+
createLinearGradient(x0, y0, x1, y1, colorStops) {
|
|
477
|
+
return {
|
|
478
|
+
type: "linear",
|
|
479
|
+
x0,
|
|
480
|
+
y0,
|
|
481
|
+
x1,
|
|
482
|
+
y1,
|
|
483
|
+
colorStops,
|
|
484
|
+
createMatrix: [this.ma, this.mb, this.mc, this.md, this.me, this.mf]
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
clip(x, y, width, height) {
|
|
488
|
+
this.flush();
|
|
489
|
+
const id = `clip-${this.clipCounter++}`;
|
|
490
|
+
const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
|
|
491
|
+
const clipXML = `<clipPath id="${id}"><rect x="${x}" y="${y}" width="${width}" height="${height}" transform="${transformStr}" /></clipPath>`;
|
|
492
|
+
this.defsBuffer.push(clipXML);
|
|
493
|
+
this.buffer.push(`<g clip-path="url(#${id})">`);
|
|
494
|
+
this.clipDepth++;
|
|
495
|
+
}
|
|
496
|
+
toXMLString() {
|
|
497
|
+
this.flush();
|
|
498
|
+
let xml = `<svg width="${this.width}" height="${this.height}" viewBox="0 0 ${this.width} ${this.height}" xmlns="http://www.w3.org/2000/svg">`;
|
|
499
|
+
if (this.defsBuffer.length > 0) {
|
|
500
|
+
xml += `<defs>${this.defsBuffer.join("\n")}</defs>`;
|
|
501
|
+
}
|
|
502
|
+
xml += this.buffer.join("\n");
|
|
503
|
+
for (let i = 0; i < this.clipDepth; i++) {
|
|
504
|
+
xml += "</g>";
|
|
505
|
+
}
|
|
506
|
+
xml += "</svg>";
|
|
507
|
+
return xml;
|
|
508
|
+
}
|
|
509
|
+
resolveGradient(colorOrGradient) {
|
|
510
|
+
if (typeof colorOrGradient === "string") {
|
|
511
|
+
return colorOrGradient;
|
|
512
|
+
}
|
|
513
|
+
const gradient = colorOrGradient;
|
|
514
|
+
const [c_a, c_b, c_c, c_d, c_e, c_f] = gradient.createMatrix;
|
|
515
|
+
const Det = this.ma * this.md - this.mb * this.mc;
|
|
516
|
+
let inv_a = 1, inv_b = 0, inv_c = 0, inv_d = 1, inv_e = 0, inv_f = 0;
|
|
517
|
+
if (Math.abs(Det) > 1e-6) {
|
|
518
|
+
inv_a = this.md / Det;
|
|
519
|
+
inv_b = -this.mb / Det;
|
|
520
|
+
inv_c = -this.mc / Det;
|
|
521
|
+
inv_d = this.ma / Det;
|
|
522
|
+
inv_e = (this.mc * this.mf - this.md * this.me) / Det;
|
|
523
|
+
inv_f = (this.mb * this.me - this.ma * this.mf) / Det;
|
|
524
|
+
}
|
|
525
|
+
const g_a = inv_a * c_a + inv_c * c_b;
|
|
526
|
+
const g_b = inv_b * c_a + inv_d * c_b;
|
|
527
|
+
const g_c = inv_a * c_c + inv_c * c_d;
|
|
528
|
+
const g_d = inv_b * c_c + inv_d * c_d;
|
|
529
|
+
const g_e = inv_a * c_e + inv_c * c_f + inv_e;
|
|
530
|
+
const g_f = inv_b * c_e + inv_d * c_f + inv_f;
|
|
531
|
+
const stopsStr = JSON.stringify(gradient.colorStops);
|
|
532
|
+
const x0_prime = c_a * gradient.x0 + c_c * gradient.y0 + c_e;
|
|
533
|
+
const y0_prime = c_b * gradient.x0 + c_d * gradient.y0 + c_f;
|
|
534
|
+
const x1_prime = c_a * gradient.x1 + c_c * gradient.y1 + c_e;
|
|
535
|
+
const y1_prime = c_b * gradient.x1 + c_d * gradient.y1 + c_f;
|
|
536
|
+
const key = `${x0_prime}_${y0_prime}_${x1_prime}_${y1_prime}_${stopsStr}_${this.ma}_${this.mb}_${this.mc}_${this.md}_${this.me}_${this.mf}`;
|
|
537
|
+
let id = this.gradientCache.get(key);
|
|
538
|
+
if (!id) {
|
|
539
|
+
id = `vecto-linear-grad-${this.gradientCounter++}`;
|
|
540
|
+
let stopsXML = "";
|
|
541
|
+
for (const stop of gradient.colorStops) {
|
|
542
|
+
stopsXML += `<stop offset="${stop.stop}" stop-color="${this.escapeXML(stop.color)}" />`;
|
|
543
|
+
}
|
|
544
|
+
const gradientTransform = `matrix(${g_a},${g_b},${g_c},${g_d},${g_e},${g_f})`;
|
|
545
|
+
const gradXML = `<linearGradient id="${id}" x1="${gradient.x0}" y1="${gradient.y0}" x2="${gradient.x1}" y2="${gradient.y1}" gradientUnits="userSpaceOnUse" gradientTransform="${gradientTransform}">${stopsXML}</linearGradient>`;
|
|
546
|
+
this.defsBuffer.push(gradXML);
|
|
547
|
+
this.gradientCache.set(key, id);
|
|
548
|
+
}
|
|
549
|
+
return `url(#${id})`;
|
|
550
|
+
}
|
|
551
|
+
escapeXML(str) {
|
|
552
|
+
return str.replace(/[<>&'"]/g, (c) => {
|
|
553
|
+
switch (c) {
|
|
554
|
+
case "<":
|
|
555
|
+
return "<";
|
|
556
|
+
case ">":
|
|
557
|
+
return ">";
|
|
558
|
+
case "&":
|
|
559
|
+
return "&";
|
|
560
|
+
case "'":
|
|
561
|
+
return "'";
|
|
562
|
+
case '"':
|
|
563
|
+
return """;
|
|
564
|
+
default:
|
|
565
|
+
return c;
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
// src/renderer/colorParse.ts
|
|
572
|
+
var cache = /* @__PURE__ */ new Map();
|
|
573
|
+
var fallbackCtx;
|
|
574
|
+
function fromHex(hex) {
|
|
575
|
+
const h = hex.slice(1);
|
|
576
|
+
const n = h.length;
|
|
577
|
+
if (n !== 3 && n !== 4 && n !== 6 && n !== 8) return null;
|
|
578
|
+
if (!/^[0-9a-f]+$/i.test(h)) return null;
|
|
579
|
+
const short = n === 3 || n === 4;
|
|
580
|
+
const hx = (i) => {
|
|
581
|
+
const s = short ? h[i] + h[i] : h.slice(i * 2, i * 2 + 2);
|
|
582
|
+
return parseInt(s, 16) / 255;
|
|
583
|
+
};
|
|
584
|
+
const hasA = n === 4 || n === 8;
|
|
585
|
+
return [hx(0), hx(1), hx(2), hasA ? hx(3) : 1];
|
|
586
|
+
}
|
|
587
|
+
var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
588
|
+
function fromRgbFunc(css) {
|
|
589
|
+
const m = /^rgba?\(([^)]+)\)$/i.exec(css.trim());
|
|
590
|
+
if (!m) return null;
|
|
591
|
+
const [rgbPart, alphaPart] = m[1].split("/");
|
|
592
|
+
const parts = rgbPart.trim().split(/[\s,]+/).filter(Boolean);
|
|
593
|
+
if (parts.length < 3) return null;
|
|
594
|
+
const chan = (p) => p.endsWith("%") ? parseFloat(p) / 100 * 255 : parseFloat(p);
|
|
595
|
+
const r = chan(parts[0]) / 255;
|
|
596
|
+
const g = chan(parts[1]) / 255;
|
|
597
|
+
const b = chan(parts[2]) / 255;
|
|
598
|
+
const alphaToken = alphaPart !== void 0 ? alphaPart.trim() : parts[3];
|
|
599
|
+
const a = alphaToken === void 0 ? 1 : alphaToken.endsWith("%") ? parseFloat(alphaToken) / 100 : parseFloat(alphaToken);
|
|
600
|
+
if ([r, g, b, a].some((v) => Number.isNaN(v))) return null;
|
|
601
|
+
return [clamp01(r), clamp01(g), clamp01(b), clamp01(a)];
|
|
602
|
+
}
|
|
603
|
+
function fromCanvas(css) {
|
|
604
|
+
if (typeof document === "undefined") return null;
|
|
605
|
+
if (!fallbackCtx) fallbackCtx = document.createElement("canvas").getContext("2d");
|
|
606
|
+
if (!fallbackCtx) return null;
|
|
607
|
+
fallbackCtx.fillStyle = css;
|
|
608
|
+
fallbackCtx.fillRect(0, 0, 1, 1);
|
|
609
|
+
const d = fallbackCtx.getImageData(0, 0, 1, 1).data;
|
|
610
|
+
return [d[0] / 255, d[1] / 255, d[2] / 255, d[3] / 255];
|
|
611
|
+
}
|
|
612
|
+
function parseColorToRGBA(css) {
|
|
613
|
+
const hit = cache.get(css);
|
|
614
|
+
if (hit) return hit;
|
|
615
|
+
const trimmed = css.trim();
|
|
616
|
+
const rgba = (trimmed[0] === "#" ? fromHex(trimmed) : null) ?? fromRgbFunc(trimmed) ?? fromCanvas(trimmed) ?? [0, 0, 0, 1];
|
|
617
|
+
cache.set(css, rgba);
|
|
618
|
+
return rgba;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/renderer/WebGLPointRenderer.ts
|
|
622
|
+
var FLOATS_PER_POINT = 7;
|
|
623
|
+
var POINT_STRIDE = FLOATS_PER_POINT * 4;
|
|
624
|
+
var FLOATS_PER_RECT_VERT = 6;
|
|
625
|
+
var RECT_VERT_STRIDE = FLOATS_PER_RECT_VERT * 4;
|
|
626
|
+
var VERTS_PER_RECT = 6;
|
|
627
|
+
var FLOATS_PER_SPRITE_VERT = 8;
|
|
628
|
+
var SPRITE_VERT_STRIDE = FLOATS_PER_SPRITE_VERT * 4;
|
|
629
|
+
var VERTS_PER_SPRITE = 6;
|
|
630
|
+
var POINT_VERT = `#version 300 es
|
|
631
|
+
in vec2 a_pos;
|
|
632
|
+
in float a_radius;
|
|
633
|
+
in vec4 a_color;
|
|
634
|
+
uniform vec2 u_resolution;
|
|
635
|
+
uniform float u_dpr;
|
|
636
|
+
out vec4 v_color;
|
|
637
|
+
void main() {
|
|
638
|
+
vec2 clip = (a_pos / u_resolution) * 2.0 - 1.0;
|
|
639
|
+
gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
|
|
640
|
+
gl_PointSize = a_radius * 2.0 * u_dpr;
|
|
641
|
+
v_color = a_color;
|
|
642
|
+
}`;
|
|
643
|
+
var POINT_FRAG = `#version 300 es
|
|
644
|
+
precision mediump float;
|
|
645
|
+
in vec4 v_color;
|
|
646
|
+
out vec4 outColor;
|
|
647
|
+
void main() {
|
|
648
|
+
vec2 c = gl_PointCoord - 0.5;
|
|
649
|
+
float d = length(c);
|
|
650
|
+
float aa = fwidth(d);
|
|
651
|
+
float alpha = 1.0 - smoothstep(0.5 - aa, 0.5, d);
|
|
652
|
+
if (alpha <= 0.0) discard;
|
|
653
|
+
outColor = vec4(v_color.rgb, v_color.a * alpha);
|
|
654
|
+
}`;
|
|
655
|
+
var RECT_VERT = `#version 300 es
|
|
656
|
+
in vec2 a_pos;
|
|
657
|
+
in vec4 a_rcolor;
|
|
658
|
+
uniform vec2 u_resolution;
|
|
659
|
+
out vec4 v_color;
|
|
660
|
+
void main() {
|
|
661
|
+
vec2 clip = (a_pos / u_resolution) * 2.0 - 1.0;
|
|
662
|
+
gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
|
|
663
|
+
v_color = a_rcolor;
|
|
664
|
+
}`;
|
|
665
|
+
var RECT_FRAG = `#version 300 es
|
|
666
|
+
precision mediump float;
|
|
667
|
+
in vec4 v_color;
|
|
668
|
+
out vec4 outColor;
|
|
669
|
+
void main() {
|
|
670
|
+
outColor = vec4(v_color.rgb, v_color.a);
|
|
671
|
+
}`;
|
|
672
|
+
var SPRITE_VERT = `#version 300 es
|
|
673
|
+
in vec2 a_pos;
|
|
674
|
+
in vec2 a_uv;
|
|
675
|
+
in vec4 a_tint;
|
|
676
|
+
uniform vec2 u_resolution;
|
|
677
|
+
out vec2 v_uv;
|
|
678
|
+
out vec4 v_tint;
|
|
679
|
+
void main() {
|
|
680
|
+
vec2 clip = (a_pos / u_resolution) * 2.0 - 1.0;
|
|
681
|
+
gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
|
|
682
|
+
v_uv = a_uv;
|
|
683
|
+
v_tint = a_tint;
|
|
684
|
+
}`;
|
|
685
|
+
var SPRITE_FRAG = `#version 300 es
|
|
686
|
+
precision mediump float;
|
|
687
|
+
uniform sampler2D u_tex;
|
|
688
|
+
in vec2 v_uv;
|
|
689
|
+
in vec4 v_tint;
|
|
690
|
+
out vec4 outColor;
|
|
691
|
+
void main() {
|
|
692
|
+
vec4 t = texture(u_tex, v_uv);
|
|
693
|
+
outColor = vec4(t.rgb * v_tint.rgb, t.a * v_tint.a);
|
|
694
|
+
}`;
|
|
695
|
+
var MSDF_FRAG = `#version 300 es
|
|
696
|
+
precision mediump float;
|
|
697
|
+
uniform sampler2D u_tex;
|
|
698
|
+
uniform float u_distanceRange;
|
|
699
|
+
in vec2 v_uv;
|
|
700
|
+
in vec4 v_tint;
|
|
701
|
+
out vec4 outColor;
|
|
702
|
+
float median(float r, float g, float b) {
|
|
703
|
+
return max(min(r, g), min(max(r, g), b));
|
|
704
|
+
}
|
|
705
|
+
void main() {
|
|
706
|
+
vec3 msd = texture(u_tex, v_uv).rgb;
|
|
707
|
+
float sd = median(msd.r, msd.g, msd.b);
|
|
708
|
+
vec2 unitRange = vec2(u_distanceRange) / vec2(textureSize(u_tex, 0));
|
|
709
|
+
vec2 screenTexSize = vec2(1.0) / fwidth(v_uv);
|
|
710
|
+
float screenPxRange = max(0.5 * dot(unitRange, screenTexSize), 1.0);
|
|
711
|
+
float screenPxDistance = screenPxRange * (sd - 0.5);
|
|
712
|
+
float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
|
|
713
|
+
if (opacity <= 0.0) discard;
|
|
714
|
+
outColor = vec4(v_tint.rgb, v_tint.a * opacity);
|
|
715
|
+
}`;
|
|
716
|
+
function compile(gl, type, src) {
|
|
717
|
+
const sh = gl.createShader(type);
|
|
718
|
+
if (!sh) return null;
|
|
719
|
+
gl.shaderSource(sh, src);
|
|
720
|
+
gl.compileShader(sh);
|
|
721
|
+
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
|
722
|
+
gl.deleteShader(sh);
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
return sh;
|
|
726
|
+
}
|
|
727
|
+
function link(gl, vsSrc, fsSrc) {
|
|
728
|
+
const vs = compile(gl, gl.VERTEX_SHADER, vsSrc);
|
|
729
|
+
const fs = compile(gl, gl.FRAGMENT_SHADER, fsSrc);
|
|
730
|
+
if (!vs || !fs) return null;
|
|
731
|
+
const program = gl.createProgram();
|
|
732
|
+
if (!program) return null;
|
|
733
|
+
gl.attachShader(program, vs);
|
|
734
|
+
gl.attachShader(program, fs);
|
|
735
|
+
gl.linkProgram(program);
|
|
736
|
+
gl.deleteShader(vs);
|
|
737
|
+
gl.deleteShader(fs);
|
|
738
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
739
|
+
gl.deleteProgram(program);
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
return program;
|
|
743
|
+
}
|
|
744
|
+
function grow(data, needed) {
|
|
745
|
+
if (needed <= data.length) return data;
|
|
746
|
+
let cap = data.length;
|
|
747
|
+
while (cap < needed) cap *= 2;
|
|
748
|
+
const grown = new Float32Array(cap);
|
|
749
|
+
grown.set(data);
|
|
750
|
+
return grown;
|
|
751
|
+
}
|
|
752
|
+
function createWebGLPointRenderer(canvas) {
|
|
753
|
+
const gl = canvas.getContext("webgl2");
|
|
754
|
+
if (!gl) return null;
|
|
755
|
+
const pointProgram = link(gl, POINT_VERT, POINT_FRAG);
|
|
756
|
+
const rectProgram = link(gl, RECT_VERT, RECT_FRAG);
|
|
757
|
+
const spriteProgram = link(gl, SPRITE_VERT, SPRITE_FRAG);
|
|
758
|
+
const msdfProgram = link(gl, SPRITE_VERT, MSDF_FRAG);
|
|
759
|
+
if (!pointProgram || !rectProgram || !spriteProgram || !msdfProgram) return null;
|
|
760
|
+
gl.enable(gl.BLEND);
|
|
761
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
762
|
+
const pAPos = gl.getAttribLocation(pointProgram, "a_pos");
|
|
763
|
+
const pARadius = gl.getAttribLocation(pointProgram, "a_radius");
|
|
764
|
+
const pAColor = gl.getAttribLocation(pointProgram, "a_color");
|
|
765
|
+
const pURes = gl.getUniformLocation(pointProgram, "u_resolution");
|
|
766
|
+
const pUDpr = gl.getUniformLocation(pointProgram, "u_dpr");
|
|
767
|
+
const pointBuffer = gl.createBuffer();
|
|
768
|
+
const pointVAO = gl.createVertexArray();
|
|
769
|
+
gl.bindVertexArray(pointVAO);
|
|
770
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, pointBuffer);
|
|
771
|
+
gl.enableVertexAttribArray(pAPos);
|
|
772
|
+
gl.vertexAttribPointer(pAPos, 2, gl.FLOAT, false, POINT_STRIDE, 0);
|
|
773
|
+
gl.enableVertexAttribArray(pARadius);
|
|
774
|
+
gl.vertexAttribPointer(pARadius, 1, gl.FLOAT, false, POINT_STRIDE, 8);
|
|
775
|
+
gl.enableVertexAttribArray(pAColor);
|
|
776
|
+
gl.vertexAttribPointer(pAColor, 4, gl.FLOAT, false, POINT_STRIDE, 12);
|
|
777
|
+
const rAPos = gl.getAttribLocation(rectProgram, "a_pos");
|
|
778
|
+
const rAColor = gl.getAttribLocation(rectProgram, "a_rcolor");
|
|
779
|
+
const rURes = gl.getUniformLocation(rectProgram, "u_resolution");
|
|
780
|
+
const rectBuffer = gl.createBuffer();
|
|
781
|
+
const rectVAO = gl.createVertexArray();
|
|
782
|
+
gl.bindVertexArray(rectVAO);
|
|
783
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, rectBuffer);
|
|
784
|
+
gl.enableVertexAttribArray(rAPos);
|
|
785
|
+
gl.vertexAttribPointer(rAPos, 2, gl.FLOAT, false, RECT_VERT_STRIDE, 0);
|
|
786
|
+
gl.enableVertexAttribArray(rAColor);
|
|
787
|
+
gl.vertexAttribPointer(rAColor, 4, gl.FLOAT, false, RECT_VERT_STRIDE, 8);
|
|
788
|
+
const sAPos = gl.getAttribLocation(spriteProgram, "a_pos");
|
|
789
|
+
const sAUv = gl.getAttribLocation(spriteProgram, "a_uv");
|
|
790
|
+
const sATint = gl.getAttribLocation(spriteProgram, "a_tint");
|
|
791
|
+
const sURes = gl.getUniformLocation(spriteProgram, "u_resolution");
|
|
792
|
+
const sUTex = gl.getUniformLocation(spriteProgram, "u_tex");
|
|
793
|
+
const spriteBuffer = gl.createBuffer();
|
|
794
|
+
const spriteVAO = gl.createVertexArray();
|
|
795
|
+
gl.bindVertexArray(spriteVAO);
|
|
796
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, spriteBuffer);
|
|
797
|
+
gl.enableVertexAttribArray(sAPos);
|
|
798
|
+
gl.vertexAttribPointer(sAPos, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 0);
|
|
799
|
+
gl.enableVertexAttribArray(sAUv);
|
|
800
|
+
gl.vertexAttribPointer(sAUv, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 8);
|
|
801
|
+
gl.enableVertexAttribArray(sATint);
|
|
802
|
+
gl.vertexAttribPointer(sATint, 4, gl.FLOAT, false, SPRITE_VERT_STRIDE, 16);
|
|
803
|
+
const gAPos = gl.getAttribLocation(msdfProgram, "a_pos");
|
|
804
|
+
const gAUv = gl.getAttribLocation(msdfProgram, "a_uv");
|
|
805
|
+
const gATint = gl.getAttribLocation(msdfProgram, "a_tint");
|
|
806
|
+
const gURes = gl.getUniformLocation(msdfProgram, "u_resolution");
|
|
807
|
+
const gUTex = gl.getUniformLocation(msdfProgram, "u_tex");
|
|
808
|
+
const gURange = gl.getUniformLocation(msdfProgram, "u_distanceRange");
|
|
809
|
+
const glyphBuffer = gl.createBuffer();
|
|
810
|
+
const glyphVAO = gl.createVertexArray();
|
|
811
|
+
gl.bindVertexArray(glyphVAO);
|
|
812
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, glyphBuffer);
|
|
813
|
+
gl.enableVertexAttribArray(gAPos);
|
|
814
|
+
gl.vertexAttribPointer(gAPos, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 0);
|
|
815
|
+
gl.enableVertexAttribArray(gAUv);
|
|
816
|
+
gl.vertexAttribPointer(gAUv, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 8);
|
|
817
|
+
gl.enableVertexAttribArray(gATint);
|
|
818
|
+
gl.vertexAttribPointer(gATint, 4, gl.FLOAT, false, SPRITE_VERT_STRIDE, 16);
|
|
819
|
+
gl.bindVertexArray(null);
|
|
820
|
+
let texture = null;
|
|
821
|
+
let msdfTexture = null;
|
|
822
|
+
let distanceRange = 4;
|
|
823
|
+
let pointData = new Float32Array(FLOATS_PER_POINT * 1024);
|
|
824
|
+
let pointCount = 0;
|
|
825
|
+
let rectData = new Float32Array(FLOATS_PER_RECT_VERT * VERTS_PER_RECT * 256);
|
|
826
|
+
let rectCount = 0;
|
|
827
|
+
let spriteData = new Float32Array(FLOATS_PER_SPRITE_VERT * VERTS_PER_SPRITE * 256);
|
|
828
|
+
let spriteCount = 0;
|
|
829
|
+
let glyphData = new Float32Array(FLOATS_PER_SPRITE_VERT * VERTS_PER_SPRITE * 1024);
|
|
830
|
+
let glyphCount = 0;
|
|
831
|
+
let logicalW = 0;
|
|
832
|
+
let logicalH = 0;
|
|
833
|
+
let dpr = 1;
|
|
834
|
+
return {
|
|
835
|
+
resize(width, height) {
|
|
836
|
+
logicalW = width;
|
|
837
|
+
logicalH = height;
|
|
838
|
+
dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio || 1 : 1;
|
|
839
|
+
canvas.width = Math.round(width * dpr);
|
|
840
|
+
canvas.height = Math.round(height * dpr);
|
|
841
|
+
canvas.style.width = `${width}px`;
|
|
842
|
+
canvas.style.height = `${height}px`;
|
|
843
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
844
|
+
},
|
|
845
|
+
begin() {
|
|
846
|
+
pointCount = 0;
|
|
847
|
+
rectCount = 0;
|
|
848
|
+
spriteCount = 0;
|
|
849
|
+
glyphCount = 0;
|
|
850
|
+
},
|
|
851
|
+
setTexture(source) {
|
|
852
|
+
if (!texture) {
|
|
853
|
+
texture = gl.createTexture();
|
|
854
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
855
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
856
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
857
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
858
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
859
|
+
} else {
|
|
860
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
861
|
+
}
|
|
862
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
863
|
+
},
|
|
864
|
+
addSprite(x, y, width, height, u0, v0, u1, v1, color = "#ffffff", alpha = 1, rotation = 0) {
|
|
865
|
+
if (!texture) return;
|
|
866
|
+
const stride = FLOATS_PER_SPRITE_VERT * VERTS_PER_SPRITE;
|
|
867
|
+
spriteData = grow(spriteData, (spriteCount + 1) * stride);
|
|
868
|
+
const [r, g, b, a] = parseColorToRGBA(color);
|
|
869
|
+
const al = a * alpha;
|
|
870
|
+
const s = Math.sin(rotation);
|
|
871
|
+
const c = Math.cos(rotation);
|
|
872
|
+
const corner = (lx, ly) => [
|
|
873
|
+
x + lx * c - ly * s,
|
|
874
|
+
y + lx * s + ly * c
|
|
875
|
+
];
|
|
876
|
+
const quad = [
|
|
877
|
+
[corner(0, 0), [u0, v0]],
|
|
878
|
+
[corner(width, 0), [u1, v0]],
|
|
879
|
+
[corner(width, height), [u1, v1]],
|
|
880
|
+
[corner(0, height), [u0, v1]]
|
|
881
|
+
];
|
|
882
|
+
const order = [0, 1, 2, 0, 2, 3];
|
|
883
|
+
let o = spriteCount * stride;
|
|
884
|
+
for (const i of order) {
|
|
885
|
+
const [[vx, vy], [vu, vv]] = quad[i];
|
|
886
|
+
spriteData[o] = vx;
|
|
887
|
+
spriteData[o + 1] = vy;
|
|
888
|
+
spriteData[o + 2] = vu;
|
|
889
|
+
spriteData[o + 3] = vv;
|
|
890
|
+
spriteData[o + 4] = r;
|
|
891
|
+
spriteData[o + 5] = g;
|
|
892
|
+
spriteData[o + 6] = b;
|
|
893
|
+
spriteData[o + 7] = al;
|
|
894
|
+
o += FLOATS_PER_SPRITE_VERT;
|
|
895
|
+
}
|
|
896
|
+
spriteCount++;
|
|
897
|
+
},
|
|
898
|
+
setMSDFTexture(source, range) {
|
|
899
|
+
distanceRange = range;
|
|
900
|
+
if (!msdfTexture) {
|
|
901
|
+
msdfTexture = gl.createTexture();
|
|
902
|
+
gl.bindTexture(gl.TEXTURE_2D, msdfTexture);
|
|
903
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
904
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
905
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
906
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
907
|
+
} else {
|
|
908
|
+
gl.bindTexture(gl.TEXTURE_2D, msdfTexture);
|
|
909
|
+
}
|
|
910
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
911
|
+
},
|
|
912
|
+
addGlyph(x, y, width, height, u0, v0, u1, v1, color = "#ffffff", alpha = 1, rotation = 0) {
|
|
913
|
+
if (!msdfTexture) return;
|
|
914
|
+
const stride = FLOATS_PER_SPRITE_VERT * VERTS_PER_SPRITE;
|
|
915
|
+
glyphData = grow(glyphData, (glyphCount + 1) * stride);
|
|
916
|
+
const [r, g, b, a] = parseColorToRGBA(color);
|
|
917
|
+
const al = a * alpha;
|
|
918
|
+
const s = Math.sin(rotation);
|
|
919
|
+
const c = Math.cos(rotation);
|
|
920
|
+
const corner = (lx, ly) => [
|
|
921
|
+
x + lx * c - ly * s,
|
|
922
|
+
y + lx * s + ly * c
|
|
923
|
+
];
|
|
924
|
+
const quad = [
|
|
925
|
+
[corner(0, 0), [u0, v0]],
|
|
926
|
+
[corner(width, 0), [u1, v0]],
|
|
927
|
+
[corner(width, height), [u1, v1]],
|
|
928
|
+
[corner(0, height), [u0, v1]]
|
|
929
|
+
];
|
|
930
|
+
const order = [0, 1, 2, 0, 2, 3];
|
|
931
|
+
let o = glyphCount * stride;
|
|
932
|
+
for (const i of order) {
|
|
933
|
+
const [[vx, vy], [vu, vv]] = quad[i];
|
|
934
|
+
glyphData[o] = vx;
|
|
935
|
+
glyphData[o + 1] = vy;
|
|
936
|
+
glyphData[o + 2] = vu;
|
|
937
|
+
glyphData[o + 3] = vv;
|
|
938
|
+
glyphData[o + 4] = r;
|
|
939
|
+
glyphData[o + 5] = g;
|
|
940
|
+
glyphData[o + 6] = b;
|
|
941
|
+
glyphData[o + 7] = al;
|
|
942
|
+
o += FLOATS_PER_SPRITE_VERT;
|
|
943
|
+
}
|
|
944
|
+
glyphCount++;
|
|
945
|
+
},
|
|
946
|
+
addCircle(x, y, radius, color, alpha = 1) {
|
|
947
|
+
pointData = grow(pointData, (pointCount + 1) * FLOATS_PER_POINT);
|
|
948
|
+
const [r, g, b, a] = parseColorToRGBA(color);
|
|
949
|
+
const o = pointCount * FLOATS_PER_POINT;
|
|
950
|
+
pointData[o] = x;
|
|
951
|
+
pointData[o + 1] = y;
|
|
952
|
+
pointData[o + 2] = radius;
|
|
953
|
+
pointData[o + 3] = r;
|
|
954
|
+
pointData[o + 4] = g;
|
|
955
|
+
pointData[o + 5] = b;
|
|
956
|
+
pointData[o + 6] = a * alpha;
|
|
957
|
+
pointCount++;
|
|
958
|
+
},
|
|
959
|
+
addRect(x, y, width, height, color, alpha = 1, rotation = 0) {
|
|
960
|
+
const stride = FLOATS_PER_RECT_VERT * VERTS_PER_RECT;
|
|
961
|
+
rectData = grow(rectData, (rectCount + 1) * stride);
|
|
962
|
+
const [r, g, b, a] = parseColorToRGBA(color);
|
|
963
|
+
const al = a * alpha;
|
|
964
|
+
const s = Math.sin(rotation);
|
|
965
|
+
const c = Math.cos(rotation);
|
|
966
|
+
const corner = (lx, ly) => [
|
|
967
|
+
x + lx * c - ly * s,
|
|
968
|
+
y + lx * s + ly * c
|
|
969
|
+
];
|
|
970
|
+
const p0 = corner(0, 0);
|
|
971
|
+
const p1 = corner(width, 0);
|
|
972
|
+
const p2 = corner(width, height);
|
|
973
|
+
const p3 = corner(0, height);
|
|
974
|
+
const verts = [p0, p1, p2, p0, p2, p3];
|
|
975
|
+
let o = rectCount * stride;
|
|
976
|
+
for (const [vx, vy] of verts) {
|
|
977
|
+
rectData[o] = vx;
|
|
978
|
+
rectData[o + 1] = vy;
|
|
979
|
+
rectData[o + 2] = r;
|
|
980
|
+
rectData[o + 3] = g;
|
|
981
|
+
rectData[o + 4] = b;
|
|
982
|
+
rectData[o + 5] = al;
|
|
983
|
+
o += FLOATS_PER_RECT_VERT;
|
|
984
|
+
}
|
|
985
|
+
rectCount++;
|
|
986
|
+
},
|
|
987
|
+
flush() {
|
|
988
|
+
gl.clearColor(0, 0, 0, 0);
|
|
989
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
990
|
+
if (rectCount > 0) {
|
|
991
|
+
const floats = rectCount * VERTS_PER_RECT * FLOATS_PER_RECT_VERT;
|
|
992
|
+
gl.useProgram(rectProgram);
|
|
993
|
+
gl.bindVertexArray(rectVAO);
|
|
994
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, rectBuffer);
|
|
995
|
+
gl.bufferData(gl.ARRAY_BUFFER, rectData.subarray(0, floats), gl.DYNAMIC_DRAW);
|
|
996
|
+
gl.uniform2f(rURes, logicalW, logicalH);
|
|
997
|
+
gl.drawArrays(gl.TRIANGLES, 0, rectCount * VERTS_PER_RECT);
|
|
998
|
+
}
|
|
999
|
+
if (pointCount > 0) {
|
|
1000
|
+
gl.useProgram(pointProgram);
|
|
1001
|
+
gl.bindVertexArray(pointVAO);
|
|
1002
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, pointBuffer);
|
|
1003
|
+
gl.bufferData(
|
|
1004
|
+
gl.ARRAY_BUFFER,
|
|
1005
|
+
pointData.subarray(0, pointCount * FLOATS_PER_POINT),
|
|
1006
|
+
gl.DYNAMIC_DRAW
|
|
1007
|
+
);
|
|
1008
|
+
gl.uniform2f(pURes, logicalW, logicalH);
|
|
1009
|
+
gl.uniform1f(pUDpr, dpr);
|
|
1010
|
+
gl.drawArrays(gl.POINTS, 0, pointCount);
|
|
1011
|
+
}
|
|
1012
|
+
if (spriteCount > 0 && texture) {
|
|
1013
|
+
const floats = spriteCount * VERTS_PER_SPRITE * FLOATS_PER_SPRITE_VERT;
|
|
1014
|
+
gl.useProgram(spriteProgram);
|
|
1015
|
+
gl.bindVertexArray(spriteVAO);
|
|
1016
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, spriteBuffer);
|
|
1017
|
+
gl.bufferData(gl.ARRAY_BUFFER, spriteData.subarray(0, floats), gl.DYNAMIC_DRAW);
|
|
1018
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1019
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
1020
|
+
gl.uniform1i(sUTex, 0);
|
|
1021
|
+
gl.uniform2f(sURes, logicalW, logicalH);
|
|
1022
|
+
gl.drawArrays(gl.TRIANGLES, 0, spriteCount * VERTS_PER_SPRITE);
|
|
1023
|
+
}
|
|
1024
|
+
if (glyphCount > 0 && msdfTexture) {
|
|
1025
|
+
const floats = glyphCount * VERTS_PER_SPRITE * FLOATS_PER_SPRITE_VERT;
|
|
1026
|
+
gl.useProgram(msdfProgram);
|
|
1027
|
+
gl.bindVertexArray(glyphVAO);
|
|
1028
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, glyphBuffer);
|
|
1029
|
+
gl.bufferData(gl.ARRAY_BUFFER, glyphData.subarray(0, floats), gl.DYNAMIC_DRAW);
|
|
1030
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1031
|
+
gl.bindTexture(gl.TEXTURE_2D, msdfTexture);
|
|
1032
|
+
gl.uniform1i(gUTex, 0);
|
|
1033
|
+
gl.uniform2f(gURes, logicalW, logicalH);
|
|
1034
|
+
gl.uniform1f(gURange, distanceRange);
|
|
1035
|
+
gl.drawArrays(gl.TRIANGLES, 0, glyphCount * VERTS_PER_SPRITE);
|
|
1036
|
+
}
|
|
1037
|
+
gl.bindVertexArray(null);
|
|
1038
|
+
},
|
|
1039
|
+
destroy() {
|
|
1040
|
+
gl.deleteBuffer(pointBuffer);
|
|
1041
|
+
gl.deleteBuffer(rectBuffer);
|
|
1042
|
+
gl.deleteBuffer(spriteBuffer);
|
|
1043
|
+
gl.deleteBuffer(glyphBuffer);
|
|
1044
|
+
gl.deleteVertexArray(pointVAO);
|
|
1045
|
+
gl.deleteVertexArray(rectVAO);
|
|
1046
|
+
gl.deleteVertexArray(spriteVAO);
|
|
1047
|
+
gl.deleteVertexArray(glyphVAO);
|
|
1048
|
+
gl.deleteProgram(pointProgram);
|
|
1049
|
+
gl.deleteProgram(rectProgram);
|
|
1050
|
+
gl.deleteProgram(spriteProgram);
|
|
1051
|
+
gl.deleteProgram(msdfProgram);
|
|
1052
|
+
if (texture) gl.deleteTexture(texture);
|
|
1053
|
+
if (msdfTexture) gl.deleteTexture(msdfTexture);
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/renderer/WebGPUParticleSystemManager.ts
|
|
1059
|
+
var COMPUTE_SHADER = `
|
|
1060
|
+
struct Particle {
|
|
1061
|
+
position: vec2<f32>,
|
|
1062
|
+
velocity: vec2<f32>,
|
|
1063
|
+
origin: vec2<f32>,
|
|
1064
|
+
size: f32,
|
|
1065
|
+
life: f32,
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
struct Params {
|
|
1069
|
+
base_color: vec4<f32>,
|
|
1070
|
+
mouse_pos: vec2<f32>,
|
|
1071
|
+
screen_size: vec2<f32>,
|
|
1072
|
+
explosion_pos: vec2<f32>,
|
|
1073
|
+
dt: f32,
|
|
1074
|
+
spring_k: f32,
|
|
1075
|
+
damping: f32,
|
|
1076
|
+
explosion_force: f32,
|
|
1077
|
+
bounce_damping: f32,
|
|
1078
|
+
max_particles: u32,
|
|
1079
|
+
max_velocity: f32,
|
|
1080
|
+
pad0: f32,
|
|
1081
|
+
pad1: f32,
|
|
1082
|
+
pad2: f32,
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
1086
|
+
@group(0) @binding(1) var<storage, read_write> particles: array<Particle>;
|
|
1087
|
+
|
|
1088
|
+
@compute @workgroup_size(256)
|
|
1089
|
+
fn cs_main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
1090
|
+
let idx = global_id.x;
|
|
1091
|
+
if (idx >= params.max_particles) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
var p = particles[idx];
|
|
1096
|
+
let dt = clamp(params.dt, 0.0, 0.1);
|
|
1097
|
+
let safe_screen_size = max(params.screen_size, vec2<f32>(1.0, 1.0));
|
|
1098
|
+
|
|
1099
|
+
let spring_k = clamp(params.spring_k, 0.0, 10.0);
|
|
1100
|
+
let damping = clamp(params.damping, 0.0, 1.0);
|
|
1101
|
+
let bounce_damping = clamp(params.bounce_damping, 0.0, 1.0);
|
|
1102
|
+
let max_velocity = max(params.max_velocity, 1.0);
|
|
1103
|
+
|
|
1104
|
+
let to_origin = p.origin - p.position;
|
|
1105
|
+
let spring_force = to_origin * spring_k;
|
|
1106
|
+
|
|
1107
|
+
var mouse_force = vec2<f32>(0.0, 0.0);
|
|
1108
|
+
let to_mouse = params.mouse_pos - p.position;
|
|
1109
|
+
let dist = length(to_mouse);
|
|
1110
|
+
if (dist < 120.0 && dist > 0.1) {
|
|
1111
|
+
let force_magnitude = (120.0 - dist) * 2.0;
|
|
1112
|
+
mouse_force = -normalize(to_mouse) * force_magnitude;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
var expl_force = vec2<f32>(0.0, 0.0);
|
|
1116
|
+
if (params.explosion_force > 0.0) {
|
|
1117
|
+
let to_expl = params.explosion_pos - p.position;
|
|
1118
|
+
let expl_dist = length(to_expl);
|
|
1119
|
+
if (expl_dist < 150.0 && expl_dist > 0.1) {
|
|
1120
|
+
let f = (150.0 - expl_dist) * params.explosion_force;
|
|
1121
|
+
expl_force = -normalize(to_expl) * f;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
let accel = spring_force + mouse_force + expl_force;
|
|
1126
|
+
p.velocity = (p.velocity + accel * dt) * damping;
|
|
1127
|
+
|
|
1128
|
+
let speed = length(p.velocity);
|
|
1129
|
+
if (speed > max_velocity) {
|
|
1130
|
+
p.velocity = normalize(p.velocity) * max_velocity;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
p.position = p.position + p.velocity * dt;
|
|
1134
|
+
|
|
1135
|
+
if (p.position.x <= 0.0 && p.velocity.x < 0.0) {
|
|
1136
|
+
p.velocity.x = -p.velocity.x * bounce_damping;
|
|
1137
|
+
} else if (p.position.x >= safe_screen_size.x && p.velocity.x > 0.0) {
|
|
1138
|
+
p.velocity.x = -p.velocity.x * bounce_damping;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (p.position.y <= 0.0 && p.velocity.y < 0.0) {
|
|
1142
|
+
p.velocity.y = -p.velocity.y * bounce_damping;
|
|
1143
|
+
} else if (p.position.y >= safe_screen_size.y && p.velocity.y > 0.0) {
|
|
1144
|
+
p.velocity.y = -p.velocity.y * bounce_damping;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
p.position = clamp(p.position, vec2<f32>(0.0, 0.0), safe_screen_size);
|
|
1148
|
+
|
|
1149
|
+
if (p.life >= 0.0) {
|
|
1150
|
+
p.life = max(0.0, p.life - dt * 0.5);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
particles[idx] = p;
|
|
1154
|
+
}
|
|
1155
|
+
`;
|
|
1156
|
+
var RENDER_SHADER = `
|
|
1157
|
+
struct Particle {
|
|
1158
|
+
position: vec2<f32>,
|
|
1159
|
+
velocity: vec2<f32>,
|
|
1160
|
+
origin: vec2<f32>,
|
|
1161
|
+
size: f32,
|
|
1162
|
+
life: f32,
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
struct Params {
|
|
1166
|
+
base_color: vec4<f32>,
|
|
1167
|
+
mouse_pos: vec2<f32>,
|
|
1168
|
+
screen_size: vec2<f32>,
|
|
1169
|
+
explosion_pos: vec2<f32>,
|
|
1170
|
+
dt: f32,
|
|
1171
|
+
spring_k: f32,
|
|
1172
|
+
damping: f32,
|
|
1173
|
+
explosion_force: f32,
|
|
1174
|
+
bounce_damping: f32,
|
|
1175
|
+
max_particles: u32,
|
|
1176
|
+
max_velocity: f32,
|
|
1177
|
+
pad0: f32,
|
|
1178
|
+
pad1: f32,
|
|
1179
|
+
pad2: f32,
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
struct VertexOutput {
|
|
1183
|
+
@builtin(position) pos: vec4<f32>,
|
|
1184
|
+
@location(0) uv: vec2<f32>,
|
|
1185
|
+
@location(1) color: vec4<f32>,
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
1189
|
+
@group(0) @binding(1) var<storage, read> particles: array<Particle>;
|
|
1190
|
+
|
|
1191
|
+
@vertex
|
|
1192
|
+
fn vs_main(
|
|
1193
|
+
@builtin(vertex_index) vertex_idx: u32,
|
|
1194
|
+
@builtin(instance_index) instance_idx: u32
|
|
1195
|
+
) -> VertexOutput {
|
|
1196
|
+
let p = particles[instance_idx];
|
|
1197
|
+
let uvs = array<vec2<f32>, 6>(
|
|
1198
|
+
vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(-1.0, 1.0),
|
|
1199
|
+
vec2<f32>(-1.0, 1.0), vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0)
|
|
1200
|
+
);
|
|
1201
|
+
|
|
1202
|
+
let safe_screen_size = max(params.screen_size, vec2<f32>(1.0, 1.0));
|
|
1203
|
+
var life_scale = 1.0;
|
|
1204
|
+
if (p.life >= 0.0) {
|
|
1205
|
+
life_scale = clamp(p.life, 0.0, 1.0);
|
|
1206
|
+
}
|
|
1207
|
+
let visual_size = p.size * life_scale;
|
|
1208
|
+
let offset = uvs[vertex_idx] * visual_size;
|
|
1209
|
+
let world_pos = p.position + offset;
|
|
1210
|
+
|
|
1211
|
+
let ndc_x = (world_pos.x / safe_screen_size.x) * 2.0 - 1.0;
|
|
1212
|
+
let ndc_y = 1.0 - (world_pos.y / safe_screen_size.y) * 2.0;
|
|
1213
|
+
|
|
1214
|
+
var out: VertexOutput;
|
|
1215
|
+
out.pos = vec4<f32>(ndc_x, ndc_y, 0.0, 1.0);
|
|
1216
|
+
out.uv = uvs[vertex_idx];
|
|
1217
|
+
out.color = vec4<f32>(params.base_color.rgb, params.base_color.a * life_scale);
|
|
1218
|
+
return out;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
@fragment
|
|
1222
|
+
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
|
1223
|
+
let dist = length(in.uv);
|
|
1224
|
+
if (dist > 1.0) {
|
|
1225
|
+
discard;
|
|
1226
|
+
}
|
|
1227
|
+
let alpha = 1.0 - smoothstep(0.85, 1.0, dist);
|
|
1228
|
+
return vec4<f32>(in.color.rgb, in.color.a * alpha);
|
|
1229
|
+
}
|
|
1230
|
+
`;
|
|
1231
|
+
var WebGPUParticleSystemManager = class {
|
|
1232
|
+
device;
|
|
1233
|
+
computePipeline = null;
|
|
1234
|
+
renderPipeline = null;
|
|
1235
|
+
computeBindGroupLayout = null;
|
|
1236
|
+
renderBindGroupLayout = null;
|
|
1237
|
+
constructor(device) {
|
|
1238
|
+
this.device = device;
|
|
1239
|
+
}
|
|
1240
|
+
initPipelines(format) {
|
|
1241
|
+
const computeModule = this.device.createShaderModule({ code: COMPUTE_SHADER });
|
|
1242
|
+
const renderModule = this.device.createShaderModule({ code: RENDER_SHADER });
|
|
1243
|
+
this.computeBindGroupLayout = this.device.createBindGroupLayout({
|
|
1244
|
+
entries: [
|
|
1245
|
+
{
|
|
1246
|
+
binding: 0,
|
|
1247
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1248
|
+
buffer: { type: "uniform" }
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
binding: 1,
|
|
1252
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1253
|
+
buffer: { type: "storage" }
|
|
1254
|
+
}
|
|
1255
|
+
]
|
|
1256
|
+
});
|
|
1257
|
+
this.renderBindGroupLayout = this.device.createBindGroupLayout({
|
|
1258
|
+
entries: [
|
|
1259
|
+
{
|
|
1260
|
+
binding: 0,
|
|
1261
|
+
visibility: GPUShaderStage.VERTEX,
|
|
1262
|
+
buffer: { type: "uniform" }
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
binding: 1,
|
|
1266
|
+
visibility: GPUShaderStage.VERTEX,
|
|
1267
|
+
buffer: { type: "read-only-storage" }
|
|
1268
|
+
}
|
|
1269
|
+
]
|
|
1270
|
+
});
|
|
1271
|
+
const computePipelineLayout = this.device.createPipelineLayout({
|
|
1272
|
+
bindGroupLayouts: [this.computeBindGroupLayout]
|
|
1273
|
+
});
|
|
1274
|
+
const renderPipelineLayout = this.device.createPipelineLayout({
|
|
1275
|
+
bindGroupLayouts: [this.renderBindGroupLayout]
|
|
1276
|
+
});
|
|
1277
|
+
this.computePipeline = this.device.createComputePipeline({
|
|
1278
|
+
layout: computePipelineLayout,
|
|
1279
|
+
compute: { module: computeModule, entryPoint: "cs_main" }
|
|
1280
|
+
});
|
|
1281
|
+
this.renderPipeline = this.device.createRenderPipeline({
|
|
1282
|
+
layout: renderPipelineLayout,
|
|
1283
|
+
vertex: { module: renderModule, entryPoint: "vs_main" },
|
|
1284
|
+
fragment: {
|
|
1285
|
+
module: renderModule,
|
|
1286
|
+
entryPoint: "fs_main",
|
|
1287
|
+
targets: [
|
|
1288
|
+
{
|
|
1289
|
+
format,
|
|
1290
|
+
blend: {
|
|
1291
|
+
color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
1292
|
+
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
]
|
|
1296
|
+
},
|
|
1297
|
+
primitive: { topology: "triangle-list" }
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
setupEntityResources(entity) {
|
|
1301
|
+
const storageSize = entity.maxParticles * 32;
|
|
1302
|
+
entity.gpuStorageBuffer = this.device.createBuffer({
|
|
1303
|
+
size: storageSize,
|
|
1304
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
1305
|
+
});
|
|
1306
|
+
entity.gpuUniformBuffer = this.device.createBuffer({
|
|
1307
|
+
size: 80,
|
|
1308
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
1309
|
+
});
|
|
1310
|
+
entity.computeBindGroup = this.device.createBindGroup({
|
|
1311
|
+
layout: this.computeBindGroupLayout,
|
|
1312
|
+
entries: [
|
|
1313
|
+
{ binding: 0, resource: { buffer: entity.gpuUniformBuffer } },
|
|
1314
|
+
{ binding: 1, resource: { buffer: entity.gpuStorageBuffer } }
|
|
1315
|
+
]
|
|
1316
|
+
});
|
|
1317
|
+
entity.renderBindGroup = this.device.createBindGroup({
|
|
1318
|
+
layout: this.renderBindGroupLayout,
|
|
1319
|
+
entries: [
|
|
1320
|
+
{ binding: 0, resource: { buffer: entity.gpuUniformBuffer } },
|
|
1321
|
+
{ binding: 1, resource: { buffer: entity.gpuStorageBuffer } }
|
|
1322
|
+
]
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
recordComputePass(pass, entity, dt, mouseX, mouseY, width, height) {
|
|
1326
|
+
if (!this.computePipeline || !entity.computeBindGroup) return;
|
|
1327
|
+
const uniformArray = new Float32Array(20);
|
|
1328
|
+
const color = parseColorToRGBA(entity.baseColor);
|
|
1329
|
+
uniformArray[0] = color[0];
|
|
1330
|
+
uniformArray[1] = color[1];
|
|
1331
|
+
uniformArray[2] = color[2];
|
|
1332
|
+
uniformArray[3] = color[3];
|
|
1333
|
+
const mActive = !isNaN(mouseX) && !isNaN(mouseY) && mouseX > -9e3 && mouseY > -9e3;
|
|
1334
|
+
uniformArray[4] = mActive ? mouseX : -9999;
|
|
1335
|
+
uniformArray[5] = mActive ? mouseY : -9999;
|
|
1336
|
+
uniformArray[6] = Math.max(1, width);
|
|
1337
|
+
uniformArray[7] = Math.max(1, height);
|
|
1338
|
+
if (entity.pendingExplosion) {
|
|
1339
|
+
uniformArray[8] = entity.pendingExplosion.x;
|
|
1340
|
+
uniformArray[9] = entity.pendingExplosion.y;
|
|
1341
|
+
uniformArray[13] = entity.pendingExplosion.force;
|
|
1342
|
+
entity.pendingExplosion = null;
|
|
1343
|
+
} else {
|
|
1344
|
+
uniformArray[8] = 0;
|
|
1345
|
+
uniformArray[9] = 0;
|
|
1346
|
+
uniformArray[13] = 0;
|
|
1347
|
+
}
|
|
1348
|
+
uniformArray[10] = isNaN(dt) ? 0.016 : dt;
|
|
1349
|
+
uniformArray[11] = Math.max(0, Math.min(10, entity.springK));
|
|
1350
|
+
uniformArray[12] = Math.max(0, Math.min(1, entity.damping));
|
|
1351
|
+
uniformArray[14] = Math.max(0, Math.min(1, entity.bounceDamping));
|
|
1352
|
+
uniformArray[16] = Math.max(1, entity.maxVelocity);
|
|
1353
|
+
new Uint32Array(uniformArray.buffer)[15] = entity.maxParticles;
|
|
1354
|
+
this.device.queue.writeBuffer(entity.gpuUniformBuffer, 0, uniformArray);
|
|
1355
|
+
pass.setPipeline(this.computePipeline);
|
|
1356
|
+
pass.setBindGroup(0, entity.computeBindGroup);
|
|
1357
|
+
const workgroups = Math.ceil(entity.maxParticles / 256);
|
|
1358
|
+
pass.dispatchWorkgroups(workgroups);
|
|
1359
|
+
}
|
|
1360
|
+
recordRenderPass(pass, entity) {
|
|
1361
|
+
if (!this.renderPipeline || !entity.renderBindGroup) return;
|
|
1362
|
+
pass.setPipeline(this.renderPipeline);
|
|
1363
|
+
pass.setBindGroup(0, entity.renderBindGroup);
|
|
1364
|
+
pass.draw(6, entity.maxParticles);
|
|
1365
|
+
}
|
|
1366
|
+
destroy() {
|
|
1367
|
+
this.computePipeline = null;
|
|
1368
|
+
this.renderPipeline = null;
|
|
1369
|
+
this.computeBindGroupLayout = null;
|
|
1370
|
+
this.renderBindGroupLayout = null;
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
export {
|
|
1375
|
+
CanvasRenderer,
|
|
1376
|
+
SVGRenderer,
|
|
1377
|
+
parseColorToRGBA,
|
|
1378
|
+
createWebGLPointRenderer,
|
|
1379
|
+
WebGPUParticleSystemManager
|
|
1380
|
+
};
|