q5 2.5.5 → 2.7.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/q5.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * q5.js
3
- * @version 2.5
3
+ * @version 2.7
4
4
  * @author quinton-ashley, Tezumie, and LingDong-
5
5
  * @license LGPL-3.0
6
6
  * @class Q5
@@ -89,7 +89,13 @@ function Q5(scope, parent, renderer) {
89
89
  $.resetMatrix();
90
90
  if ($._beginRender) $._beginRender();
91
91
  for (let m of Q5.methods.pre) m.call($);
92
- $.draw();
92
+ try {
93
+ $.draw();
94
+ } catch (e) {
95
+ if (!Q5.disableFriendlyErrors && $._askAI) $._askAI(e);
96
+ if (!Q5.errorTolerant) $.noLoop();
97
+ throw e;
98
+ }
93
99
  for (let m of Q5.methods.post) m.call($);
94
100
  if ($._render) $._render();
95
101
  if ($._finishRender) $._finishRender();
@@ -128,7 +134,7 @@ function Q5(scope, parent, renderer) {
128
134
  }
129
135
  return $._frameRate;
130
136
  };
131
- $.getTargetFrameRate = () => $._targetFrameRate;
137
+ $.getTargetFrameRate = () => $._targetFrameRate || 60;
132
138
  $.getFPS = () => $._fps;
133
139
 
134
140
  $.Element = function (a) {
@@ -196,10 +202,10 @@ function Q5(scope, parent, renderer) {
196
202
  let t = globalScope || $;
197
203
  $._isTouchAware = t.touchStarted || t.touchMoved || t.mouseReleased;
198
204
  let preloadDefined = t.preload;
205
+ $.preload ??= () => {};
206
+ $.setup ??= () => {};
207
+ $.draw ??= () => {};
199
208
  let userFns = [
200
- 'setup',
201
- 'draw',
202
- 'preload',
203
209
  'mouseMoved',
204
210
  'mousePressed',
205
211
  'mouseReleased',
@@ -220,15 +226,13 @@ function Q5(scope, parent, renderer) {
220
226
  try {
221
227
  return t[k]();
222
228
  } catch (e) {
223
- if ($._aiErrorAssistance) $._aiErrorAssistance(e);
229
+ if ($._askAI) $._askAI(e);
224
230
  throw e;
225
231
  }
226
232
  };
227
233
  }
228
234
  }
229
235
 
230
- if (!($.setup || $.draw)) return;
231
-
232
236
  async function _start() {
233
237
  $._startDone = true;
234
238
  if ($._preloadCount > 0) return raf(_start);
@@ -236,19 +240,25 @@ function Q5(scope, parent, renderer) {
236
240
  await $.setup();
237
241
  $._setupDone = true;
238
242
  if ($.frameCount) return;
239
- if ($.ctx === null) $.createCanvas(100, 100);
243
+ if ($.ctx === null) $.createCanvas(200, 200);
240
244
  if ($.ctx) $.resetMatrix();
241
245
  raf($._draw);
242
246
  }
243
247
 
244
- if ((arguments.length && scope != 'namespace' && renderer != 'webgpu') || preloadDefined) {
245
- $.preload();
246
- _start();
247
- } else {
248
- t.preload = $.preload = () => {
248
+ function _preStart() {
249
+ try {
250
+ $.preload();
249
251
  if (!$._startDone) _start();
250
- };
251
- setTimeout($.preload, 32);
252
+ } catch (e) {
253
+ if ($._askAI) $._askAI(e);
254
+ throw e;
255
+ }
256
+ }
257
+
258
+ if (preloadDefined || (arguments.length && scope != 'instance' && renderer != 'webgpu')) {
259
+ _preStart();
260
+ } else {
261
+ setTimeout(_preStart, 32);
252
262
  }
253
263
  }
254
264
 
@@ -259,7 +269,7 @@ Q5._nodejs = typeof process == 'object';
259
269
 
260
270
  Q5._instanceCount = 0;
261
271
  Q5._friendlyError = (msg, func) => {
262
- throw Error(func + ': ' + msg);
272
+ if (!Q5.disableFriendlyErrors) console.error(func + ': ' + msg);
263
273
  };
264
274
  Q5._validateParameters = () => true;
265
275
 
@@ -386,8 +396,15 @@ Q5.modules.canvas = ($, q) => {
386
396
  if ($._scope != 'image') {
387
397
  if ($._scope == 'graphics') $._pixelDensity = this._pixelDensity;
388
398
  else if (window.IntersectionObserver) {
399
+ $._wasLooping = $._loop;
389
400
  new IntersectionObserver((e) => {
390
401
  c.visible = e[0].isIntersecting;
402
+ if (c.visible) {
403
+ if ($._wasLooping && !$._loop) $.loop();
404
+ } else {
405
+ $._wasLooping = $._loop;
406
+ $.noLoop();
407
+ }
391
408
  }).observe(c);
392
409
  }
393
410
  }
@@ -635,8 +652,7 @@ Q5.renderers.q2d.canvas = ($, q) => {
635
652
  };
636
653
 
637
654
  $.fill = function (c) {
638
- $._doFill = true;
639
- $._fillSet = true;
655
+ $._doFill = $._fillSet = true;
640
656
  if (Q5.Color) {
641
657
  if (!c._q5Color) {
642
658
  if (typeof c != 'string') c = $.color(...arguments);
@@ -648,8 +664,7 @@ Q5.renderers.q2d.canvas = ($, q) => {
648
664
  };
649
665
  $.noFill = () => ($._doFill = false);
650
666
  $.stroke = function (c) {
651
- $._doStroke = true;
652
- $._strokeSet = true;
667
+ $._doStroke = $._strokeSet = true;
653
668
  if (Q5.Color) {
654
669
  if (!c._q5Color) {
655
670
  if (typeof c != 'string') c = $.color(...arguments);
@@ -753,7 +768,7 @@ Q5.renderers.q2d.drawing = ($) => {
753
768
  $.background = function (c) {
754
769
  $.ctx.save();
755
770
  $.ctx.resetTransform();
756
- if (c.canvas) $.image(c, 0, 0, $.width, $.height);
771
+ if (c.canvas) $.image(c, 0, 0, $.canvas.width, $.canvas.height);
757
772
  else {
758
773
  if (Q5.Color && !c._q5Color) {
759
774
  if (typeof c != 'string') c = $.color(...arguments);
@@ -1226,7 +1241,8 @@ Q5.renderers.q2d.image = ($, q) => {
1226
1241
 
1227
1242
  $.imageMode = (mode) => ($._imageMode = mode);
1228
1243
  $.image = (img, dx, dy, dw, dh, sx = 0, sy = 0, sw, sh) => {
1229
- let drawable = img.canvas || img;
1244
+ if (!img) return;
1245
+ let drawable = img?.canvas || img;
1230
1246
  if (Q5._createNodeJSCanvas) {
1231
1247
  drawable = drawable.context.canvas;
1232
1248
  }
@@ -1680,13 +1696,12 @@ Q5.renderers.q2d.text = ($, q) => {
1680
1696
  };
1681
1697
  Q5.modules.ai = ($) => {
1682
1698
  $.askAI = (question = '') => {
1699
+ Q5.disableFriendlyErrors = false;
1683
1700
  throw Error('Ask AI ✨ ' + question);
1684
1701
  };
1685
1702
 
1686
- $._aiErrorAssistance = async (e) => {
1703
+ $._askAI = async (e) => {
1687
1704
  let askAI = e.message?.includes('Ask AI ✨');
1688
- if (Q5.disableFriendlyErrors && !askAI) return;
1689
- if (askAI || !Q5.errorTolerant) $.noLoop();
1690
1705
  let stackLines = e.stack?.split('\n');
1691
1706
  if (!e.stack || stackLines.length <= 1) return;
1692
1707
 
@@ -2108,11 +2123,11 @@ Q5.modules.input = ($, q) => {
2108
2123
  $.pmouseX = 0;
2109
2124
  $.pmouseY = 0;
2110
2125
  $.touches = [];
2111
- $.mouseButton = null;
2126
+ $.mouseButton = '';
2112
2127
  $.keyIsPressed = false;
2113
2128
  $.mouseIsPressed = false;
2114
- $.key = null;
2115
- $.keyCode = null;
2129
+ $.key = '';
2130
+ $.keyCode = 0;
2116
2131
 
2117
2132
  $.UP_ARROW = 38;
2118
2133
  $.DOWN_ARROW = 40;
@@ -3075,18 +3090,30 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3075
3090
  c.width = $.width = 500;
3076
3091
  c.height = $.height = 500;
3077
3092
 
3078
- if ($.colorMode) $.colorMode('rgb', 'float');
3093
+ if ($.colorMode) $.colorMode('rgb', 1);
3079
3094
 
3080
- let pass;
3095
+ let pass,
3096
+ mainView,
3097
+ colorsLayout,
3098
+ colorIndex = 1,
3099
+ colorStackIndex = 8;
3081
3100
 
3082
- $.pipelines = [];
3101
+ $._pipelineConfigs = [];
3102
+ $._pipelines = [];
3083
3103
 
3084
3104
  // local variables used for slightly better performance
3085
3105
  // stores pipeline shifts and vertex counts/image indices
3086
3106
  let drawStack = ($.drawStack = []);
3087
3107
 
3088
3108
  // colors used for each draw call
3089
- let colorsStack = ($.colorsStack = [1, 1, 1, 1]);
3109
+
3110
+ let colorStack = ($.colorStack = new Float32Array(1e6));
3111
+
3112
+ // prettier-ignore
3113
+ colorStack.set([
3114
+ 0, 0, 0, 1, // black
3115
+ 1, 1, 1, 1 // white
3116
+ ]);
3090
3117
 
3091
3118
  $._transformLayout = Q5.device.createBindGroupLayout({
3092
3119
  label: 'transformLayout',
@@ -3110,32 +3137,70 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3110
3137
  ]
3111
3138
  });
3112
3139
 
3113
- $.bindGroupLayouts = [$._transformLayout];
3140
+ colorsLayout = Q5.device.createBindGroupLayout({
3141
+ label: 'colorsLayout',
3142
+ entries: [
3143
+ {
3144
+ binding: 0,
3145
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
3146
+ buffer: {
3147
+ type: 'read-only-storage',
3148
+ hasDynamicOffset: false
3149
+ }
3150
+ }
3151
+ ]
3152
+ });
3153
+
3154
+ $.bindGroupLayouts = [$._transformLayout, colorsLayout];
3114
3155
 
3115
3156
  let uniformBuffer = Q5.device.createBuffer({
3116
3157
  size: 8, // Size of two floats
3117
3158
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
3118
3159
  });
3119
3160
 
3161
+ let createMainView = () => {
3162
+ mainView = Q5.device
3163
+ .createTexture({
3164
+ size: [$.canvas.width, $.canvas.height],
3165
+ sampleCount: 4,
3166
+ format: 'bgra8unorm',
3167
+ usage: GPUTextureUsage.RENDER_ATTACHMENT
3168
+ })
3169
+ .createView();
3170
+ };
3171
+
3120
3172
  $._createCanvas = (w, h, opt) => {
3121
3173
  q.ctx = q.drawingContext = c.getContext('webgpu');
3122
3174
 
3123
3175
  opt.format ??= navigator.gpu.getPreferredCanvasFormat();
3124
3176
  opt.device ??= Q5.device;
3125
3177
 
3178
+ // needed for other blend modes but couldn't get it working
3179
+ // opt.alphaMode = 'premultiplied';
3180
+
3126
3181
  $.ctx.configure(opt);
3127
3182
 
3128
3183
  Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([$.canvas.hw, $.canvas.hh]));
3129
3184
 
3185
+ createMainView();
3186
+
3130
3187
  return c;
3131
3188
  };
3132
3189
 
3133
3190
  $._resizeCanvas = (w, h) => {
3134
3191
  $._setCanvasSize(w, h);
3192
+ createMainView();
3193
+ };
3194
+
3195
+ $.pixelDensity = (v) => {
3196
+ if (!v || v == $._pixelDensity) return $._pixelDensity;
3197
+ $._pixelDensity = v;
3198
+ $._setCanvasSize(c.w, c.h);
3199
+ createMainView();
3200
+ return v;
3135
3201
  };
3136
3202
 
3137
3203
  // current color index, used to associate a vertex with a color
3138
- let colorIndex = 0;
3139
3204
  let addColor = (r, g, b, a = 1) => {
3140
3205
  if (typeof r == 'string') r = $.color(r);
3141
3206
  else if (b == undefined) {
@@ -3143,21 +3208,35 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3143
3208
  a = g ?? 1;
3144
3209
  g = b = r;
3145
3210
  }
3146
- if (r._q5Color) colorsStack.push(r.r, r.g, r.b, r.a);
3147
- else colorsStack.push(r, g, b, a);
3211
+ if (r._q5Color) {
3212
+ a = r.a;
3213
+ b = r.b;
3214
+ g = r.g;
3215
+ r = r.r;
3216
+ }
3217
+
3218
+ let cs = colorStack,
3219
+ i = colorStackIndex;
3220
+ cs[i++] = r;
3221
+ cs[i++] = g;
3222
+ cs[i++] = b;
3223
+ cs[i++] = a;
3224
+ colorStackIndex = i;
3225
+
3148
3226
  colorIndex++;
3149
3227
  };
3150
3228
 
3151
- $._fillIndex = $._strokeIndex = -1;
3229
+ $._fillIndex = $._strokeIndex = 0;
3230
+ $._doFill = $._doStroke = true;
3152
3231
 
3153
3232
  $.fill = (r, g, b, a) => {
3154
3233
  addColor(r, g, b, a);
3155
- $._doFill = true;
3234
+ $._doFill = $._fillSet = true;
3156
3235
  $._fillIndex = colorIndex;
3157
3236
  };
3158
3237
  $.stroke = (r, g, b, a) => {
3159
3238
  addColor(r, g, b, a);
3160
- $._doStroke = true;
3239
+ $._doStroke = $._strokeSet = true;
3161
3240
  $._strokeIndex = colorIndex;
3162
3241
  };
3163
3242
 
@@ -3359,6 +3438,68 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3359
3438
  return [l, r, t, b];
3360
3439
  };
3361
3440
 
3441
+ // prettier-ignore
3442
+ let blendFactors = [
3443
+ 'zero', // 0
3444
+ 'one', // 1
3445
+ 'src-alpha', // 2
3446
+ 'one-minus-src-alpha', // 3
3447
+ 'dst', // 4
3448
+ 'dst-alpha', // 5
3449
+ 'one-minus-dst-alpha', // 6
3450
+ 'one-minus-src' // 7
3451
+ ];
3452
+ let blendOps = [
3453
+ 'add', // 0
3454
+ 'subtract', // 1
3455
+ 'reverse-subtract', // 2
3456
+ 'min', // 3
3457
+ 'max' // 4
3458
+ ];
3459
+
3460
+ const blendModes = {
3461
+ normal: [2, 3, 0, 2, 3, 0],
3462
+ // destination_over: [6, 1, 0, 6, 1, 0],
3463
+ additive: [1, 1, 0, 1, 1, 0]
3464
+ // source_in: [5, 0, 0, 5, 0, 0],
3465
+ // destination_in: [0, 2, 0, 0, 2, 0],
3466
+ // source_out: [6, 0, 0, 6, 0, 0],
3467
+ // destination_out: [0, 3, 0, 0, 3, 0],
3468
+ // source_atop: [5, 3, 0, 5, 3, 0],
3469
+ // destination_atop: [6, 2, 0, 6, 2, 0]
3470
+ };
3471
+
3472
+ $.blendConfigs = {};
3473
+
3474
+ for (const [name, mode] of Object.entries(blendModes)) {
3475
+ $.blendConfigs[name] = {
3476
+ color: {
3477
+ srcFactor: blendFactors[mode[0]],
3478
+ dstFactor: blendFactors[mode[1]],
3479
+ operation: blendOps[mode[2]]
3480
+ },
3481
+ alpha: {
3482
+ srcFactor: blendFactors[mode[3]],
3483
+ dstFactor: blendFactors[mode[4]],
3484
+ operation: blendOps[mode[5]]
3485
+ }
3486
+ };
3487
+ }
3488
+
3489
+ $._blendMode = 'normal';
3490
+ $.blendMode = (mode) => {
3491
+ if (mode == $._blendMode) return;
3492
+ if (mode == 'source-over') mode = 'normal';
3493
+ if (mode == 'lighter') mode = 'additive';
3494
+ mode = mode.toLowerCase().replace(/[ -]/g, '_');
3495
+ $._blendMode = mode;
3496
+
3497
+ for (let i = 0; i < $._pipelines.length; i++) {
3498
+ $._pipelineConfigs[i].fragment.targets[0].blend = $.blendConfigs[mode];
3499
+ $._pipelines[i] = Q5.device.createRenderPipeline($._pipelineConfigs[i]);
3500
+ }
3501
+ };
3502
+
3362
3503
  $.clear = () => {};
3363
3504
 
3364
3505
  $._beginRender = () => {
@@ -3368,7 +3509,8 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3368
3509
  label: 'q5-webgpu',
3369
3510
  colorAttachments: [
3370
3511
  {
3371
- view: $.ctx.getCurrentTexture().createView(),
3512
+ view: mainView,
3513
+ resolveTarget: $.ctx.getCurrentTexture().createView(),
3372
3514
  loadOp: 'clear',
3373
3515
  storeOp: 'store'
3374
3516
  }
@@ -3379,58 +3521,62 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3379
3521
  $._render = () => {
3380
3522
  if (transformStates.length > 1 || !$._transformBindGroup) {
3381
3523
  let transformBuffer = Q5.device.createBuffer({
3382
- size: transformStates.length * 64, // Size of 16 floats
3383
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
3524
+ size: transformStates.length * 64, // 64 is the size of 16 floats
3525
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
3526
+ mappedAtCreation: true
3384
3527
  });
3385
3528
 
3386
- Q5.device.queue.writeBuffer(transformBuffer, 0, new Float32Array(transformStates.flat()));
3529
+ new Float32Array(transformBuffer.getMappedRange()).set(transformStates.flat());
3530
+ transformBuffer.unmap();
3387
3531
 
3388
3532
  $._transformBindGroup = Q5.device.createBindGroup({
3389
3533
  layout: $._transformLayout,
3390
3534
  entries: [
3391
- {
3392
- binding: 0,
3393
- resource: {
3394
- buffer: uniformBuffer
3395
- }
3396
- },
3397
- {
3398
- binding: 1,
3399
- resource: {
3400
- buffer: transformBuffer
3401
- }
3402
- }
3535
+ { binding: 0, resource: { buffer: uniformBuffer } },
3536
+ { binding: 1, resource: { buffer: transformBuffer } }
3403
3537
  ]
3404
3538
  });
3405
3539
  }
3406
3540
 
3407
3541
  pass.setBindGroup(0, $._transformBindGroup);
3408
3542
 
3543
+ let colorsBuffer = Q5.device.createBuffer({
3544
+ size: colorStackIndex * 4,
3545
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
3546
+ mappedAtCreation: true
3547
+ });
3548
+
3549
+ new Float32Array(colorsBuffer.getMappedRange()).set(colorStack.slice(0, colorStackIndex));
3550
+ colorsBuffer.unmap();
3551
+
3552
+ $._colorsBindGroup = Q5.device.createBindGroup({
3553
+ layout: colorsLayout,
3554
+ entries: [{ binding: 0, resource: { buffer: colorsBuffer } }]
3555
+ });
3556
+
3557
+ $.pass.setBindGroup(1, $._colorsBindGroup);
3558
+
3409
3559
  for (let m of $._hooks.preRender) m();
3410
3560
 
3411
- let drawVertOffset = 0;
3412
- let imageVertOffset = 0;
3413
- let textCharOffset = 0;
3414
- let curPipelineIndex = -1;
3415
- let curTextureIndex = -1;
3561
+ let drawVertOffset = 0,
3562
+ imageVertOffset = 0,
3563
+ textCharOffset = 0,
3564
+ curPipelineIndex = -1,
3565
+ curTextureIndex = -1;
3416
3566
 
3417
- for (let i = 0; i < drawStack.length; i += 2) {
3567
+ for (let i = 0; i < drawStack.length; i += 3) {
3418
3568
  let v = drawStack[i + 1];
3419
-
3420
- if (drawStack[i] == -1) {
3421
- v();
3422
- continue;
3423
- }
3569
+ let o = drawStack[i + 2];
3424
3570
 
3425
3571
  if (curPipelineIndex != drawStack[i]) {
3426
3572
  curPipelineIndex = drawStack[i];
3427
- pass.setPipeline($.pipelines[curPipelineIndex]);
3573
+ pass.setPipeline($._pipelines[curPipelineIndex]);
3428
3574
  }
3429
3575
 
3430
3576
  if (curPipelineIndex == 0) {
3431
3577
  // v is the number of vertices
3432
- pass.draw(v, 1, drawVertOffset);
3433
- drawVertOffset += v;
3578
+ pass.drawIndexed(v, 1, 0, drawVertOffset);
3579
+ drawVertOffset += o;
3434
3580
  } else if (curPipelineIndex == 1) {
3435
3581
  if (curTextureIndex != v) {
3436
3582
  // v is the texture index
@@ -3439,7 +3585,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3439
3585
  pass.draw(6, 1, imageVertOffset);
3440
3586
  imageVertOffset += 6;
3441
3587
  } else if (curPipelineIndex == 2) {
3442
- pass.setBindGroup(2, $._font.bindGroup);
3588
+ pass.setBindGroup(2, $._fonts[o].bindGroup);
3443
3589
  pass.setBindGroup(3, $._textBindGroup);
3444
3590
 
3445
3591
  // v is the number of characters in the text
@@ -3455,12 +3601,13 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3455
3601
  pass.end();
3456
3602
  let commandBuffer = $.encoder.finish();
3457
3603
  Q5.device.queue.submit([commandBuffer]);
3604
+
3458
3605
  q.pass = $.encoder = null;
3459
3606
 
3460
3607
  // clear the stacks for the next frame
3461
3608
  $.drawStack.length = 0;
3462
- $.colorsStack.length = 4;
3463
- colorIndex = 0;
3609
+ colorIndex = 1;
3610
+ colorStackIndex = 8;
3464
3611
  rotation = 0;
3465
3612
  transformStates.length = 1;
3466
3613
  $._transformIndexStack.length = 0;
@@ -3491,41 +3638,47 @@ Q5.webgpu = async function (scope, parent) {
3491
3638
  return new Q5(scope, parent, 'webgpu');
3492
3639
  };
3493
3640
  Q5.renderers.webgpu.drawing = ($, q) => {
3494
- let c = $.canvas;
3495
-
3496
- let drawStack = $.drawStack;
3497
- let colorsStack = $.colorsStack;
3498
-
3499
- let verticesStack = [];
3500
-
3501
- let colorIndex, colorsLayout;
3641
+ let c = $.canvas,
3642
+ drawStack = $.drawStack,
3643
+ vertexStack = new Float32Array(1e7),
3644
+ indexStack = new Uint32Array(1e6),
3645
+ vertIndex = 0,
3646
+ vertCount = 0,
3647
+ idxBufferIndex = 0,
3648
+ colorIndex;
3502
3649
 
3503
3650
  let vertexShader = Q5.device.createShaderModule({
3504
3651
  label: 'drawingVertexShader',
3505
3652
  code: `
3653
+ struct VertexInput {
3654
+ @location(0) pos: vec2f,
3655
+ @location(1) colorIndex: f32,
3656
+ @location(2) transformIndex: f32
3657
+ }
3506
3658
  struct VertexOutput {
3507
3659
  @builtin(position) position: vec4f,
3508
- @location(0) colorIndex: f32
3509
- };
3510
-
3660
+ @location(0) color: vec4f
3661
+ }
3511
3662
  struct Uniforms {
3512
3663
  halfWidth: f32,
3513
3664
  halfHeight: f32
3514
- };
3665
+ }
3515
3666
 
3516
3667
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3517
- @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
3668
+ @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
3669
+
3670
+ @group(1) @binding(0) var<storage> colors : array<vec4f>;
3518
3671
 
3519
3672
  @vertex
3520
- fn vertexMain(@location(0) pos: vec2f, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput {
3521
- var vert = vec4f(pos, 0.0, 1.0);
3522
- vert = transforms[i32(transformIndex)] * vert;
3673
+ fn vertexMain(input: VertexInput) -> VertexOutput {
3674
+ var vert = vec4f(input.pos, 0.0, 1.0);
3675
+ vert = transforms[i32(input.transformIndex)] * vert;
3523
3676
  vert.x /= uniforms.halfWidth;
3524
3677
  vert.y /= uniforms.halfHeight;
3525
3678
 
3526
3679
  var output: VertexOutput;
3527
3680
  output.position = vert;
3528
- output.colorIndex = colorIndex;
3681
+ output.color = colors[i32(input.colorIndex)];
3529
3682
  return output;
3530
3683
  }
3531
3684
  `
@@ -3534,32 +3687,13 @@ fn vertexMain(@location(0) pos: vec2f, @location(1) colorIndex: f32, @location(2
3534
3687
  let fragmentShader = Q5.device.createShaderModule({
3535
3688
  label: 'drawingFragmentShader',
3536
3689
  code: `
3537
- @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
3538
-
3539
3690
  @fragment
3540
- fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3541
- let index = i32(colorIndex);
3542
- return mix(colors[index], colors[index + 1], fract(colorIndex));
3691
+ fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
3692
+ return color;
3543
3693
  }
3544
3694
  `
3545
3695
  });
3546
3696
 
3547
- colorsLayout = Q5.device.createBindGroupLayout({
3548
- label: 'colorsLayout',
3549
- entries: [
3550
- {
3551
- binding: 0,
3552
- visibility: GPUShaderStage.FRAGMENT,
3553
- buffer: {
3554
- type: 'read-only-storage',
3555
- hasDynamicOffset: false
3556
- }
3557
- }
3558
- ]
3559
- });
3560
-
3561
- $.bindGroupLayouts.push(colorsLayout);
3562
-
3563
3697
  let vertexBufferLayout = {
3564
3698
  arrayStride: 16, // 2 coordinates + 1 color index + 1 transform index * 4 bytes each
3565
3699
  attributes: [
@@ -3569,193 +3703,212 @@ fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3569
3703
  ]
3570
3704
  };
3571
3705
 
3572
- // prettier-ignore
3573
- let blendFactors = [
3574
- 'zero', // 0
3575
- 'one', // 1
3576
- 'src-alpha', // 2
3577
- 'one-minus-src-alpha', // 3
3578
- 'dst', // 4
3579
- 'dst-alpha', // 5
3580
- 'one-minus-dst-alpha', // 6
3581
- 'one-minus-src' // 7
3582
- ];
3583
- let blendOps = [
3584
- 'add', // 0
3585
- 'subtract', // 1
3586
- 'reverse-subtract', // 2
3587
- 'min', // 3
3588
- 'max' // 4
3589
- ];
3590
-
3591
- const blendModes = {
3592
- normal: [2, 3, 0, 2, 3, 0],
3593
- lighter: [2, 1, 0, 2, 1, 0],
3594
- subtract: [2, 1, 2, 2, 1, 2],
3595
- multiply: [4, 0, 0, 5, 0, 0],
3596
- screen: [1, 3, 0, 1, 3, 0],
3597
- darken: [1, 3, 3, 1, 3, 3],
3598
- lighten: [1, 3, 4, 1, 3, 4],
3599
- overlay: [2, 3, 0, 2, 3, 0],
3600
- hard_light: [2, 3, 0, 2, 3, 0],
3601
- soft_light: [2, 3, 0, 2, 3, 0],
3602
- difference: [2, 3, 2, 2, 3, 2],
3603
- exclusion: [2, 3, 0, 2, 3, 0],
3604
- color_dodge: [1, 7, 0, 1, 7, 0],
3605
- color_burn: [6, 1, 0, 6, 1, 0],
3606
- linear_dodge: [2, 1, 0, 2, 1, 0],
3607
- linear_burn: [2, 7, 1, 2, 7, 1],
3608
- vivid_light: [2, 7, 0, 2, 7, 0],
3609
- pin_light: [2, 7, 0, 2, 7, 0],
3610
- hard_mix: [2, 7, 0, 2, 7, 0]
3611
- };
3612
-
3613
- $.blendConfigs = {};
3614
-
3615
- for (const [name, mode] of Object.entries(blendModes)) {
3616
- $.blendConfigs[name] = {
3617
- color: {
3618
- srcFactor: blendFactors[mode[0]],
3619
- dstFactor: blendFactors[mode[1]],
3620
- operation: blendOps[mode[2]]
3621
- },
3622
- alpha: {
3623
- srcFactor: blendFactors[mode[3]],
3624
- dstFactor: blendFactors[mode[4]],
3625
- operation: blendOps[mode[5]]
3626
- }
3627
- };
3628
- }
3629
-
3630
- $._blendMode = 'normal';
3631
- $.blendMode = (mode) => {
3632
- if (mode == $._blendMode) return;
3633
- if (mode == 'source-over') mode = 'normal';
3634
- mode = mode.toLowerCase().replace(/[ -]/g, '_');
3635
- $._blendMode = mode;
3636
- $.pipelines[0] = $._createPipeline($.blendConfigs[mode]);
3637
- };
3638
-
3639
3706
  let pipelineLayout = Q5.device.createPipelineLayout({
3640
3707
  label: 'drawingPipelineLayout',
3641
3708
  bindGroupLayouts: $.bindGroupLayouts
3642
3709
  });
3643
3710
 
3644
- $._createPipeline = (blendConfig) => {
3645
- return Q5.device.createRenderPipeline({
3646
- label: 'drawingPipeline',
3647
- layout: pipelineLayout,
3648
- vertex: {
3649
- module: vertexShader,
3650
- entryPoint: 'vertexMain',
3651
- buffers: [vertexBufferLayout]
3652
- },
3653
- fragment: {
3654
- module: fragmentShader,
3655
- entryPoint: 'fragmentMain',
3656
- targets: [{ format: 'bgra8unorm', blend: blendConfig }]
3657
- },
3658
- primitive: { topology: 'triangle-list' }
3659
- });
3711
+ $._pipelineConfigs[0] = {
3712
+ label: 'drawingPipeline',
3713
+ layout: pipelineLayout,
3714
+ vertex: {
3715
+ module: vertexShader,
3716
+ entryPoint: 'vertexMain',
3717
+ buffers: [vertexBufferLayout]
3718
+ },
3719
+ fragment: {
3720
+ module: fragmentShader,
3721
+ entryPoint: 'fragmentMain',
3722
+ targets: [{ format: 'bgra8unorm', blend: $.blendConfigs.normal }]
3723
+ },
3724
+ primitive: { topology: 'triangle-list' },
3725
+ multisample: {
3726
+ count: 4
3727
+ }
3728
+ };
3729
+
3730
+ $._pipelines[0] = Q5.device.createRenderPipeline($._pipelineConfigs[0]);
3731
+
3732
+ const addVert = (x, y, ci, ti) => {
3733
+ let v = vertexStack,
3734
+ i = vertIndex;
3735
+ v[i++] = x;
3736
+ v[i++] = y;
3737
+ v[i++] = ci;
3738
+ v[i++] = ti;
3739
+ vertIndex = i;
3740
+ vertCount++;
3741
+ };
3742
+
3743
+ const addIndex = (i1, i2, i3) => {
3744
+ let is = indexStack,
3745
+ ii = idxBufferIndex;
3746
+ is[ii++] = i1;
3747
+ is[ii++] = i2;
3748
+ is[ii++] = i3;
3749
+ idxBufferIndex = ii;
3750
+ };
3751
+
3752
+ const addQuad = (x1, y1, x2, y2, x3, y3, x4, y4, ci, ti) => {
3753
+ let v = vertexStack,
3754
+ i = vertIndex;
3755
+
3756
+ let i1 = vertCount++;
3757
+ v[i++] = x1;
3758
+ v[i++] = y1;
3759
+ v[i++] = ci;
3760
+ v[i++] = ti;
3761
+
3762
+ let i2 = vertCount++;
3763
+ v[i++] = x2;
3764
+ v[i++] = y2;
3765
+ v[i++] = ci;
3766
+ v[i++] = ti;
3767
+
3768
+ let i3 = vertCount++;
3769
+ v[i++] = x3;
3770
+ v[i++] = y3;
3771
+ v[i++] = ci;
3772
+ v[i++] = ti;
3773
+
3774
+ let i4 = vertCount++;
3775
+ v[i++] = x4;
3776
+ v[i++] = y4;
3777
+ v[i++] = ci;
3778
+ v[i++] = ti;
3779
+
3780
+ vertIndex = i;
3781
+
3782
+ let is = indexStack,
3783
+ ii = idxBufferIndex;
3784
+ is[ii++] = i1;
3785
+ is[ii++] = i2;
3786
+ is[ii++] = i3;
3787
+ is[ii++] = i1;
3788
+ is[ii++] = i3;
3789
+ is[ii++] = i4;
3790
+ idxBufferIndex = ii;
3791
+
3792
+ drawStack.push(0, 6, 4);
3793
+ };
3794
+
3795
+ const addEllipse = (x, y, a, b, n, ci, ti) => {
3796
+ let t = 0,
3797
+ angleIncrement = $.TAU / n,
3798
+ indicesStart = vertIndex / 4;
3799
+ addVert(x, y, ci, ti); // Center vertex
3800
+ for (let i = 0; i <= n; i++) {
3801
+ let vx = x + a * Math.cos(t),
3802
+ vy = y + b * Math.sin(t);
3803
+ addVert(vx, vy, ci, ti);
3804
+ if (i > 0) {
3805
+ addIndex(indicesStart, indicesStart + i, indicesStart + i + 1);
3806
+ }
3807
+ t += angleIncrement;
3808
+ }
3809
+ drawStack.push(0, n * 3, n + 2);
3660
3810
  };
3661
3811
 
3662
- $.pipelines[0] = $._createPipeline($.blendConfigs.normal);
3812
+ $.rectMode = (x) => ($._rectMode = x);
3663
3813
 
3664
- let shapeVertices;
3814
+ $.rect = (x, y, w, h) => {
3815
+ let [l, r, t, b] = $._calcBox(x, y, w, h, $._rectMode);
3816
+ let ci, ti;
3817
+ if ($._matrixDirty) $._saveMatrix();
3818
+ ti = $._transformIndex;
3665
3819
 
3666
- $.beginShape = () => {
3667
- shapeVertices = [];
3668
- };
3820
+ if ($._doStroke) {
3821
+ ci = $._strokeIndex;
3669
3822
 
3670
- $.vertex = (x, y) => {
3671
- if ($._matrixDirty) $._saveMatrix();
3672
- shapeVertices.push(x, -y, $._fillIndex, $._transformIndex);
3673
- };
3823
+ // outer rectangle coordinates
3824
+ let sw = $._strokeWeight / 2;
3825
+ let to = t + sw,
3826
+ bo = b - sw,
3827
+ lo = l - sw,
3828
+ ro = r + sw;
3674
3829
 
3675
- $.endShape = (close) => {
3676
- if (!$._doFill) {
3677
- shapeVertices = [];
3678
- return;
3679
- }
3680
- let v = shapeVertices;
3681
- if (v.length < 12) {
3682
- throw new Error('A shape must have at least 3 vertices.');
3683
- }
3684
- if (close) {
3685
- // Close the shape by adding the first vertex at the end
3686
- v.push(v[0], v[1], v[2], v[3]);
3687
- }
3688
- // Convert the shape to triangles
3689
- let triangles = [];
3690
- for (let i = 4; i < v.length; i += 4) {
3691
- triangles.push(
3692
- v[0], // First vertex
3693
- v[1],
3694
- v[2],
3695
- v[3],
3696
- v[i - 4], // Previous vertex
3697
- v[i - 3],
3698
- v[i - 2],
3699
- v[i - 1],
3700
- v[i], // Current vertex
3701
- v[i + 1],
3702
- v[i + 2],
3703
- v[i + 3]
3704
- );
3830
+ // stroke is simply a bigger rectangle drawn first
3831
+ addQuad(lo, to, ro, to, ro, bo, lo, bo, ci, ti);
3832
+
3833
+ // inner rectangle coordinates
3834
+ t -= sw;
3835
+ b += sw;
3836
+ l += sw;
3837
+ r -= sw;
3705
3838
  }
3706
- shapeVertices = [];
3707
3839
 
3708
- verticesStack.push(...triangles);
3709
- drawStack.push(0, triangles.length / 4);
3710
- };
3840
+ if ($._doFill) {
3841
+ ci = colorIndex ?? $._fillIndex;
3711
3842
 
3712
- $.triangle = (x1, y1, x2, y2, x3, y3) => {
3713
- $.beginShape();
3714
- $.vertex(x1, y1);
3715
- $.vertex(x2, y2);
3716
- $.vertex(x3, y3);
3717
- $.endShape(1);
3843
+ // two triangles make a rectangle
3844
+ addQuad(l, t, r, t, r, b, l, b, ci, ti);
3845
+ }
3718
3846
  };
3719
3847
 
3720
- $.quad = (x1, y1, x2, y2, x3, y3, x4, y4) => {
3721
- $.beginShape();
3722
- $.vertex(x1, y1);
3723
- $.vertex(x2, y2);
3724
- $.vertex(x3, y3);
3725
- $.vertex(x4, y4);
3726
- $.endShape(1);
3727
- };
3848
+ $.square = (x, y, s) => $.rect(x, y, s, s);
3728
3849
 
3729
- $.rectMode = (x) => ($._rectMode = x);
3850
+ // prettier-ignore
3851
+ const getArcSegments = (d) =>
3852
+ d < 4 ? 6 :
3853
+ d < 6 ? 8 :
3854
+ d < 10 ? 10 :
3855
+ d < 16 ? 12 :
3856
+ d < 20 ? 14 :
3857
+ d < 22 ? 16 :
3858
+ d < 24 ? 18 :
3859
+ d < 28 ? 20 :
3860
+ d < 34 ? 22 :
3861
+ d < 42 ? 24 :
3862
+ d < 48 ? 26 :
3863
+ d < 56 ? 28 :
3864
+ d < 64 ? 30 :
3865
+ d < 72 ? 32 :
3866
+ d < 84 ? 34 :
3867
+ d < 96 ? 36 :
3868
+ d < 98 ? 38 :
3869
+ d < 113 ? 40 :
3870
+ d < 149 ? 44 :
3871
+ d < 199 ? 48 :
3872
+ d < 261 ? 52 :
3873
+ d < 353 ? 56 :
3874
+ d < 461 ? 60 :
3875
+ d < 585 ? 64 :
3876
+ d < 1200 ? 70 :
3877
+ d < 1800 ? 80 :
3878
+ d < 2400 ? 90 :
3879
+ 100;
3730
3880
 
3731
- $.rect = (x, y, w, h) => {
3732
- let [l, r, t, b] = $._calcBox(x, y, w, h, $._rectMode);
3881
+ $.ellipseMode = (x) => ($._ellipseMode = x);
3733
3882
 
3734
- let ci = colorIndex ?? $._fillIndex;
3883
+ $.ellipse = (x, y, w, h) => {
3884
+ let n = getArcSegments(w == h ? w : Math.max(w, h));
3885
+ let a = Math.max(w, 1) / 2;
3886
+ let b = w == h ? a : Math.max(h, 1) / 2;
3887
+ let ci;
3735
3888
  if ($._matrixDirty) $._saveMatrix();
3736
3889
  let ti = $._transformIndex;
3737
- // two triangles make a rectangle
3738
- // prettier-ignore
3739
- verticesStack.push(
3740
- l, t, ci, ti,
3741
- r, t, ci, ti,
3742
- l, b, ci, ti,
3743
- r, t, ci, ti,
3744
- l, b, ci, ti,
3745
- r, b, ci, ti
3746
- );
3747
- drawStack.push(0, 6);
3890
+ if ($._doStroke) {
3891
+ let sw = $._strokeWeight / 2;
3892
+ addEllipse(x, y, a + sw, b + sw, n, $._strokeIndex, ti);
3893
+ a -= sw;
3894
+ b -= sw;
3895
+ }
3896
+ if ($._doFill) {
3897
+ addEllipse(x, y, a, b, n, colorIndex ?? $._fillIndex, ti);
3898
+ }
3748
3899
  };
3749
3900
 
3750
- $.square = (x, y, s) => $.rect(x, y, s, s);
3901
+ $.circle = (x, y, d) => $.ellipse(x, y, d, d);
3751
3902
 
3752
3903
  $.point = (x, y) => {
3753
3904
  colorIndex = $._strokeIndex;
3905
+ $._doStroke = false;
3754
3906
  let sw = $._strokeWeight;
3755
3907
  if (sw < 2) {
3756
3908
  sw = Math.round(sw);
3757
3909
  $.rect(x, y, sw, sw);
3758
3910
  } else $.ellipse(x, y, sw, sw);
3911
+ $._doStroke = true;
3759
3912
  colorIndex = null;
3760
3913
  };
3761
3914
 
@@ -3763,19 +3916,97 @@ fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3763
3916
  colorIndex = $._strokeIndex;
3764
3917
 
3765
3918
  $.push();
3766
- $.translate(x1, y1);
3919
+ $._doStroke = false;
3920
+ $.translate(x1, -y1);
3767
3921
  $.rotate($.atan2(y2 - y1, x2 - x1));
3768
3922
  let length = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
3769
- let sw = $._strokeWeight;
3770
- $.rect(0, -sw / 2, length, sw);
3923
+ let sw = $._strokeWeight,
3924
+ hsw = sw / 2;
3925
+ $._rectMode = 'corner';
3926
+ if (sw < 4) {
3927
+ $.rect(-hsw, -hsw, length + hsw, sw);
3928
+ } else {
3929
+ $._ellipseMode = 'center';
3930
+ $.ellipse(0, 0, sw, sw);
3931
+ $.ellipse(length, 0, sw, sw);
3932
+ $.rect(0, -hsw, length, sw);
3933
+ }
3934
+
3771
3935
  $.pop();
3772
3936
 
3773
3937
  colorIndex = null;
3774
3938
  };
3775
3939
 
3940
+ let shapeVertCount;
3941
+
3942
+ $.beginShape = () => {
3943
+ shapeVertCount = 0;
3944
+ };
3945
+
3946
+ $.vertex = (x, y) => {
3947
+ if ($._matrixDirty) $._saveMatrix();
3948
+ addVert(x, -y, $._fillIndex, $._transformIndex);
3949
+ shapeVertCount++;
3950
+ };
3951
+
3952
+ $.endShape = (close) => {
3953
+ if (shapeVertCount < 3) {
3954
+ throw new Error('A shape must have at least 3 vertices.');
3955
+ }
3956
+
3957
+ let firstVert = vertCount - shapeVertCount;
3958
+
3959
+ if ($._doFill) {
3960
+ // make a simple triangle fan, starting from the first vertex
3961
+ for (let i = firstVert + 1; i < vertCount - 1; i++) {
3962
+ addIndex(firstVert, i, i + 1);
3963
+ }
3964
+ drawStack.push(0, (shapeVertCount - 2) * 3, shapeVertCount);
3965
+ }
3966
+
3967
+ if ($._doStroke) {
3968
+ let first = firstVert * 4,
3969
+ last = vertIndex - 4;
3970
+ for (let i = first; i < last; i += 4) {
3971
+ let x1 = vertexStack[i],
3972
+ y1 = vertexStack[i + 1],
3973
+ x2 = vertexStack[i + 4],
3974
+ y2 = vertexStack[i + 5];
3975
+ $.line(x1, y1, x2, y2);
3976
+ }
3977
+ if (close) {
3978
+ let x1 = vertexStack[last],
3979
+ y1 = vertexStack[last + 1],
3980
+ x2 = vertexStack[first],
3981
+ y2 = vertexStack[first + 1];
3982
+ $.line(x1, y1, x2, y2);
3983
+ }
3984
+ }
3985
+
3986
+ shapeVertCount = 0;
3987
+ };
3988
+
3989
+ $.triangle = (x1, y1, x2, y2, x3, y3) => {
3990
+ $.beginShape();
3991
+ $.vertex(x1, y1);
3992
+ $.vertex(x2, y2);
3993
+ $.vertex(x3, y3);
3994
+ $.endShape();
3995
+ };
3996
+
3997
+ $.quad = (x1, y1, x2, y2, x3, y3, x4, y4) => {
3998
+ $.beginShape();
3999
+ $.vertex(x1, y1);
4000
+ $.vertex(x2, y2);
4001
+ $.vertex(x3, y3);
4002
+ $.vertex(x4, y4);
4003
+ $.endShape();
4004
+ };
4005
+
3776
4006
  $.background = (r, g, b, a) => {
3777
4007
  $.push();
3778
4008
  $.resetMatrix();
4009
+ $._doStroke = false;
3779
4010
  if (r.src) {
3780
4011
  let og = $._imageMode;
3781
4012
  $._imageMode = 'corner';
@@ -3789,121 +4020,44 @@ fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3789
4020
  $._rectMode = og;
3790
4021
  }
3791
4022
  $.pop();
4023
+ if (!$._fillSet) $._fillIndex = 1;
3792
4024
  };
3793
4025
 
3794
- /**
3795
- * Derived from: ceil(Math.log(d) * 7) * 2 - ceil(28)
3796
- * This lookup table is used for better performance.
3797
- * @param {Number} d diameter of the circle
3798
- * @returns n number of segments
3799
- */
3800
- // prettier-ignore
3801
- const getArcSegments = (d) =>
3802
- d < 4 ? 6 :
3803
- d < 6 ? 8 :
3804
- d < 10 ? 10 :
3805
- d < 16 ? 12 :
3806
- d < 20 ? 14 :
3807
- d < 22 ? 16 :
3808
- d < 24 ? 18 :
3809
- d < 28 ? 20 :
3810
- d < 34 ? 22 :
3811
- d < 42 ? 24 :
3812
- d < 48 ? 26 :
3813
- d < 56 ? 28 :
3814
- d < 64 ? 30 :
3815
- d < 72 ? 32 :
3816
- d < 84 ? 34 :
3817
- d < 96 ? 36 :
3818
- d < 98 ? 38 :
3819
- d < 113 ? 40 :
3820
- d < 149 ? 44 :
3821
- d < 199 ? 48 :
3822
- d < 261 ? 52 :
3823
- d < 353 ? 56 :
3824
- d < 461 ? 60 :
3825
- d < 585 ? 64 :
3826
- d < 1200 ? 70 :
3827
- d < 1800 ? 80 :
3828
- d < 2400 ? 90 :
3829
- 100;
3830
-
3831
- $.ellipseMode = (x) => ($._ellipseMode = x);
3832
-
3833
- $.ellipse = (x, y, w, h) => {
3834
- const n = getArcSegments(w == h ? w : Math.max(w, h));
3835
-
3836
- let a = Math.max(w, 1) / 2;
3837
- let b = w == h ? a : Math.max(h, 1) / 2;
3838
-
3839
- let t = 0; // theta
3840
- const angleIncrement = $.TAU / n;
3841
- const ci = colorIndex ?? $._fillIndex;
3842
- if ($._matrixDirty) $._saveMatrix();
3843
- const ti = $._transformIndex;
3844
- let vx1, vy1, vx2, vy2;
3845
- for (let i = 0; i <= n; i++) {
3846
- vx1 = vx2;
3847
- vy1 = vy2;
3848
- vx2 = x + a * Math.cos(t);
3849
- vy2 = y + b * Math.sin(t);
3850
- t += angleIncrement;
3851
-
3852
- if (i == 0) continue;
3853
-
3854
- verticesStack.push(x, y, ci, ti, vx1, vy1, ci, ti, vx2, vy2, ci, ti);
3855
- }
3856
-
3857
- drawStack.push(0, n * 3);
3858
- };
3859
-
3860
- $.circle = (x, y, d) => $.ellipse(x, y, d, d);
3861
-
3862
4026
  $._hooks.preRender.push(() => {
3863
- $.pass.setPipeline($.pipelines[0]);
3864
-
3865
- const vertices = new Float32Array(verticesStack);
4027
+ $.pass.setPipeline($._pipelines[0]);
3866
4028
 
3867
- const vertexBuffer = Q5.device.createBuffer({
3868
- size: vertices.byteLength,
3869
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
4029
+ let vertexBuffer = Q5.device.createBuffer({
4030
+ size: vertIndex * 4,
4031
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
4032
+ mappedAtCreation: true
3870
4033
  });
3871
4034
 
3872
- Q5.device.queue.writeBuffer(vertexBuffer, 0, vertices);
4035
+ new Float32Array(vertexBuffer.getMappedRange()).set(vertexStack.slice(0, vertIndex));
4036
+ vertexBuffer.unmap();
4037
+
3873
4038
  $.pass.setVertexBuffer(0, vertexBuffer);
3874
4039
 
3875
- const colorsBuffer = Q5.device.createBuffer({
3876
- size: colorsStack.length * 4,
3877
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
4040
+ let indexBuffer = Q5.device.createBuffer({
4041
+ size: idxBufferIndex * 4,
4042
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
4043
+ mappedAtCreation: true
3878
4044
  });
3879
4045
 
3880
- Q5.device.queue.writeBuffer(colorsBuffer, 0, new Float32Array(colorsStack));
3881
-
3882
- $._colorsBindGroup = Q5.device.createBindGroup({
3883
- layout: colorsLayout,
3884
- entries: [
3885
- {
3886
- binding: 0,
3887
- resource: {
3888
- buffer: colorsBuffer,
3889
- offset: 0,
3890
- size: colorsStack.length * 4
3891
- }
3892
- }
3893
- ]
3894
- });
4046
+ new Uint32Array(indexBuffer.getMappedRange()).set(indexStack.slice(0, idxBufferIndex));
4047
+ indexBuffer.unmap();
3895
4048
 
3896
- // set the bind group once before rendering
3897
- $.pass.setBindGroup(1, $._colorsBindGroup);
4049
+ $.pass.setIndexBuffer(indexBuffer, 'uint32');
3898
4050
  });
3899
4051
 
3900
4052
  $._hooks.postRender.push(() => {
3901
- verticesStack.length = 0;
4053
+ vertIndex = 0;
4054
+ vertCount = 0;
4055
+ idxBufferIndex = 0;
3902
4056
  });
3903
4057
  };
3904
4058
  Q5.renderers.webgpu.image = ($, q) => {
3905
4059
  $._textureBindGroups = [];
3906
- let verticesStack = [];
4060
+ let vertexStack = [];
3907
4061
 
3908
4062
  let vertexShader = Q5.device.createShaderModule({
3909
4063
  label: 'imageVertexShader',
@@ -3911,15 +4065,14 @@ Q5.renderers.webgpu.image = ($, q) => {
3911
4065
  struct VertexOutput {
3912
4066
  @builtin(position) position: vec4f,
3913
4067
  @location(0) texCoord: vec2f
3914
- };
3915
-
4068
+ }
3916
4069
  struct Uniforms {
3917
4070
  halfWidth: f32,
3918
4071
  halfHeight: f32
3919
- };
4072
+ }
3920
4073
 
3921
4074
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3922
- @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
4075
+ @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
3923
4076
 
3924
4077
  @vertex
3925
4078
  fn vertexMain(@location(0) pos: vec2f, @location(1) texCoord: vec2f, @location(2) transformIndex: f32) -> VertexOutput {
@@ -3980,7 +4133,7 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
3980
4133
  bindGroupLayouts: [...$.bindGroupLayouts, textureLayout]
3981
4134
  });
3982
4135
 
3983
- $.pipelines[1] = Q5.device.createRenderPipeline({
4136
+ $._pipelineConfigs[1] = {
3984
4137
  label: 'imagePipeline',
3985
4138
  layout: pipelineLayout,
3986
4139
  vertex: {
@@ -3991,28 +4144,12 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
3991
4144
  fragment: {
3992
4145
  module: fragmentShader,
3993
4146
  entryPoint: 'fragmentMain',
3994
- targets: [
3995
- {
3996
- format: 'bgra8unorm',
3997
- blend: $.blendConfigs?.normal || {
3998
- color: {
3999
- srcFactor: 'src-alpha',
4000
- dstFactor: 'one-minus-src-alpha',
4001
- operation: 'add'
4002
- },
4003
- alpha: {
4004
- srcFactor: 'src-alpha',
4005
- dstFactor: 'one-minus-src-alpha',
4006
- operation: 'add'
4007
- }
4008
- }
4009
- }
4010
- ]
4147
+ targets: [{ format: 'bgra8unorm', blend: $.blendConfigs.normal }]
4011
4148
  },
4012
- primitive: {
4013
- topology: 'triangle-list'
4014
- }
4015
- });
4149
+ primitive: { topology: 'triangle-list' }
4150
+ };
4151
+
4152
+ $._pipelines[1] = Q5.device.createRenderPipeline($._pipelineConfigs[1]);
4016
4153
 
4017
4154
  let sampler = Q5.device.createSampler({
4018
4155
  magFilter: 'linear',
@@ -4037,7 +4174,11 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4037
4174
 
4038
4175
  Q5.device.queue.copyExternalImageToTexture(
4039
4176
  { source: img },
4040
- { texture, colorSpace: $.canvas.colorSpace },
4177
+ {
4178
+ texture,
4179
+ colorSpace: $.canvas.colorSpace
4180
+ // premultipliedAlpha: true
4181
+ },
4041
4182
  textureSize
4042
4183
  );
4043
4184
 
@@ -4090,7 +4231,7 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4090
4231
  let [l, r, t, b] = $._calcBox(x, y, w, h, $._imageMode);
4091
4232
 
4092
4233
  // prettier-ignore
4093
- verticesStack.push(
4234
+ vertexStack.push(
4094
4235
  l, t, 0, 0, ti,
4095
4236
  r, t, 1, 0, ti,
4096
4237
  l, b, 0, 1, ti,
@@ -4099,29 +4240,29 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4099
4240
  r, b, 1, 1, ti
4100
4241
  );
4101
4242
 
4102
- $.drawStack.push(1, img.textureIndex);
4243
+ $.drawStack.push(1, img.textureIndex, 0);
4103
4244
  };
4104
4245
 
4105
4246
  $._hooks.preRender.push(() => {
4106
4247
  if (!$._textureBindGroups.length) return;
4107
4248
 
4108
4249
  // Switch to image pipeline
4109
- $.pass.setPipeline($.pipelines[1]);
4110
-
4111
- // Create a vertex buffer for the image quads
4112
- const vertices = new Float32Array(verticesStack);
4250
+ $.pass.setPipeline($._pipelines[1]);
4113
4251
 
4114
4252
  const vertexBuffer = Q5.device.createBuffer({
4115
- size: vertices.byteLength,
4116
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
4253
+ size: vertexStack.length * 4,
4254
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
4255
+ mappedAtCreation: true
4117
4256
  });
4118
4257
 
4119
- Q5.device.queue.writeBuffer(vertexBuffer, 0, vertices);
4258
+ new Float32Array(vertexBuffer.getMappedRange()).set(vertices);
4259
+ vertexBuffer.unmap();
4260
+
4120
4261
  $.pass.setVertexBuffer(1, vertexBuffer);
4121
4262
  });
4122
4263
 
4123
4264
  $._hooks.postRender.push(() => {
4124
- verticesStack.length = 0;
4265
+ vertexStack.length = 0;
4125
4266
  });
4126
4267
  };
4127
4268
 
@@ -4142,35 +4283,35 @@ const pos = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0));
4142
4283
 
4143
4284
  struct VertexInput {
4144
4285
  @builtin(vertex_index) vertex : u32,
4145
- @builtin(instance_index) instance : u32,
4146
- };
4286
+ @builtin(instance_index) instance : u32
4287
+ }
4147
4288
  struct VertexOutput {
4148
4289
  @builtin(position) position : vec4f,
4149
- @location(0) texcoord : vec2f,
4150
- @location(1) colorIndex : f32
4151
- };
4290
+ @location(0) texCoord : vec2f,
4291
+ @location(1) fillColor : vec4f
4292
+ }
4152
4293
  struct Char {
4153
4294
  texOffset: vec2f,
4154
4295
  texExtent: vec2f,
4155
4296
  size: vec2f,
4156
4297
  offset: vec2f,
4157
- };
4298
+ }
4158
4299
  struct Text {
4159
4300
  pos: vec2f,
4160
4301
  scale: f32,
4161
4302
  transformIndex: f32,
4162
4303
  fillIndex: f32,
4163
4304
  strokeIndex: f32
4164
- };
4305
+ }
4165
4306
  struct Uniforms {
4166
4307
  halfWidth: f32,
4167
4308
  halfHeight: f32
4168
- };
4309
+ }
4169
4310
 
4170
4311
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
4171
- @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
4312
+ @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
4172
4313
 
4173
- @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
4314
+ @group(1) @binding(0) var<storage> colors : array<vec4f>;
4174
4315
 
4175
4316
  @group(2) @binding(0) var fontTexture: texture_2d<f32>;
4176
4317
  @group(2) @binding(1) var fontSampler: sampler;
@@ -4196,13 +4337,13 @@ fn vertexMain(input : VertexInput) -> VertexOutput {
4196
4337
 
4197
4338
  var output : VertexOutput;
4198
4339
  output.position = vert;
4199
- output.texcoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;
4200
- output.colorIndex = text.fillIndex;
4340
+ output.texCoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;
4341
+ output.fillColor = colors[i32(text.fillIndex)];
4201
4342
  return output;
4202
4343
  }
4203
4344
 
4204
- fn sampleMsdf(texcoord: vec2f) -> f32 {
4205
- let c = textureSample(fontTexture, fontSampler, texcoord);
4345
+ fn sampleMsdf(texCoord: vec2f) -> f32 {
4346
+ let c = textureSample(fontTexture, fontSampler, texCoord);
4206
4347
  return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
4207
4348
  }
4208
4349
 
@@ -4212,25 +4353,83 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4212
4353
  // uses the default which is 4.
4213
4354
  let pxRange = 4.0;
4214
4355
  let sz = vec2f(textureDimensions(fontTexture, 0));
4215
- let dx = sz.x*length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));
4216
- let dy = sz.y*length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));
4356
+ let dx = sz.x*length(vec2f(dpdxFine(input.texCoord.x), dpdyFine(input.texCoord.x)));
4357
+ let dy = sz.y*length(vec2f(dpdxFine(input.texCoord.y), dpdyFine(input.texCoord.y)));
4217
4358
  let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
4218
- let sigDist = sampleMsdf(input.texcoord) - 0.5;
4359
+ let sigDist = sampleMsdf(input.texCoord) - 0.5;
4219
4360
  let pxDist = sigDist * toPixels;
4220
4361
  let edgeWidth = 0.5;
4221
4362
  let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
4222
4363
  if (alpha < 0.001) {
4223
4364
  discard;
4224
4365
  }
4225
- let fillColor = colors[i32(input.colorIndex)];
4226
- return vec4f(fillColor.rgb, fillColor.a * alpha);
4366
+ return vec4f(input.fillColor.rgb, input.fillColor.a * alpha);
4227
4367
  }
4228
4368
  `
4229
4369
  });
4230
4370
 
4371
+ let textBindGroupLayout = Q5.device.createBindGroupLayout({
4372
+ label: 'MSDF text group layout',
4373
+ entries: [
4374
+ {
4375
+ binding: 0,
4376
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4377
+ buffer: { type: 'read-only-storage' }
4378
+ },
4379
+ {
4380
+ binding: 1,
4381
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4382
+ buffer: { type: 'read-only-storage' }
4383
+ }
4384
+ ]
4385
+ });
4386
+
4387
+ let fontSampler = Q5.device.createSampler({
4388
+ minFilter: 'linear',
4389
+ magFilter: 'linear',
4390
+ mipmapFilter: 'linear',
4391
+ maxAnisotropy: 16
4392
+ });
4393
+ let fontBindGroupLayout = Q5.device.createBindGroupLayout({
4394
+ label: 'MSDF font group layout',
4395
+ entries: [
4396
+ {
4397
+ binding: 0,
4398
+ visibility: GPUShaderStage.FRAGMENT,
4399
+ texture: {}
4400
+ },
4401
+ {
4402
+ binding: 1,
4403
+ visibility: GPUShaderStage.FRAGMENT,
4404
+ sampler: {}
4405
+ },
4406
+ {
4407
+ binding: 2,
4408
+ visibility: GPUShaderStage.VERTEX,
4409
+ buffer: { type: 'read-only-storage' }
4410
+ }
4411
+ ]
4412
+ });
4413
+
4414
+ let fontPipelineLayout = Q5.device.createPipelineLayout({
4415
+ bindGroupLayouts: [...$.bindGroupLayouts, fontBindGroupLayout, textBindGroupLayout]
4416
+ });
4417
+
4418
+ $._pipelineConfigs[2] = {
4419
+ label: 'msdf font pipeline',
4420
+ layout: fontPipelineLayout,
4421
+ vertex: { module: textShader, entryPoint: 'vertexMain' },
4422
+ fragment: {
4423
+ module: textShader,
4424
+ entryPoint: 'fragmentMain',
4425
+ targets: [{ format: 'bgra8unorm', blend: $.blendConfigs.normal }]
4426
+ },
4427
+ primitive: { topology: 'triangle-strip', stripIndexFormat: 'uint32' }
4428
+ };
4429
+ $._pipelines[2] = Q5.device.createRenderPipeline($._pipelineConfigs[2]);
4430
+
4231
4431
  class MsdfFont {
4232
- constructor(pipeline, bindGroup, lineHeight, chars, kernings) {
4233
- this.pipeline = pipeline;
4432
+ constructor(bindGroup, lineHeight, chars, kernings) {
4234
4433
  this.bindGroup = bindGroup;
4235
4434
  this.lineHeight = lineHeight;
4236
4435
  this.chars = chars;
@@ -4256,22 +4455,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4256
4455
  }
4257
4456
  }
4258
4457
 
4259
- let textBindGroupLayout = Q5.device.createBindGroupLayout({
4260
- label: 'MSDF text group layout',
4261
- entries: [
4262
- {
4263
- binding: 0,
4264
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4265
- buffer: { type: 'read-only-storage' }
4266
- },
4267
- {
4268
- binding: 1,
4269
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4270
- buffer: { type: 'read-only-storage' }
4271
- }
4272
- ]
4273
- });
4274
-
4458
+ $._fonts = [];
4275
4459
  let fonts = {};
4276
4460
 
4277
4461
  let createFont = async (fontJsonUrl, fontName, cb) => {
@@ -4334,74 +4518,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4334
4518
  }
4335
4519
  charsBuffer.unmap();
4336
4520
 
4337
- let fontSampler = Q5.device.createSampler({
4338
- minFilter: 'linear',
4339
- magFilter: 'linear',
4340
- mipmapFilter: 'linear',
4341
- maxAnisotropy: 16
4342
- });
4343
- let fontBindGroupLayout = Q5.device.createBindGroupLayout({
4344
- label: 'MSDF font group layout',
4345
- entries: [
4346
- {
4347
- binding: 0,
4348
- visibility: GPUShaderStage.FRAGMENT,
4349
- texture: {}
4350
- },
4351
- {
4352
- binding: 1,
4353
- visibility: GPUShaderStage.FRAGMENT,
4354
- sampler: {}
4355
- },
4356
- {
4357
- binding: 2,
4358
- visibility: GPUShaderStage.VERTEX,
4359
- buffer: { type: 'read-only-storage' }
4360
- }
4361
- ]
4362
- });
4363
- let fontPipeline = Q5.device.createRenderPipeline({
4364
- label: 'msdf font pipeline',
4365
- layout: Q5.device.createPipelineLayout({
4366
- bindGroupLayouts: [...$.bindGroupLayouts, fontBindGroupLayout, textBindGroupLayout]
4367
- }),
4368
- vertex: {
4369
- module: textShader,
4370
- entryPoint: 'vertexMain'
4371
- },
4372
- fragment: {
4373
- module: textShader,
4374
- entryPoint: 'fragmentMain',
4375
- targets: [
4376
- {
4377
- format: 'bgra8unorm',
4378
- blend: {
4379
- color: {
4380
- srcFactor: 'src-alpha',
4381
- dstFactor: 'one-minus-src-alpha'
4382
- },
4383
- alpha: {
4384
- srcFactor: 'one',
4385
- dstFactor: 'one'
4386
- }
4387
- }
4388
- }
4389
- ]
4390
- },
4391
- primitive: {
4392
- topology: 'triangle-strip',
4393
- stripIndexFormat: 'uint32'
4394
- }
4395
- });
4396
-
4397
4521
  let fontBindGroup = Q5.device.createBindGroup({
4398
4522
  label: 'msdf font bind group',
4399
4523
  layout: fontBindGroupLayout,
4400
4524
  entries: [
4401
- {
4402
- binding: 0,
4403
- resource: texture.createView()
4404
- },
4525
+ { binding: 0, resource: texture.createView() },
4405
4526
  { binding: 1, resource: fontSampler },
4406
4527
  { binding: 2, resource: { buffer: charsBuffer } }
4407
4528
  ]
@@ -4419,10 +4540,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4419
4540
  }
4420
4541
  }
4421
4542
 
4422
- $._font = new MsdfFont(fontPipeline, fontBindGroup, atlas.common.lineHeight, chars, kernings);
4543
+ $._font = new MsdfFont(fontBindGroup, atlas.common.lineHeight, chars, kernings);
4423
4544
 
4545
+ $._font.index = $._fonts.length;
4546
+ $._fonts.push($._font);
4424
4547
  fonts[fontName] = $._font;
4425
- $.pipelines[2] = $._font.pipeline;
4426
4548
 
4427
4549
  q._preloadCount--;
4428
4550
 
@@ -4451,12 +4573,6 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4451
4573
 
4452
4574
  $.textFont = (fontName) => {
4453
4575
  $._font = fonts[fontName];
4454
-
4455
- // replay the change of font in the draw stack
4456
- $.drawStack.push(-1, () => {
4457
- $._font = fonts[fontName];
4458
- $.pipelines[2] = $._font.pipeline;
4459
- });
4460
4576
  };
4461
4577
  $.textSize = (size) => {
4462
4578
  $._textSize = size;
@@ -4569,7 +4685,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4569
4685
  }
4570
4686
  }
4571
4687
 
4572
- let charsData = new Float32Array((str.length - spaces) * 4);
4688
+ let charsData = [];
4573
4689
 
4574
4690
  let ta = $._textAlign,
4575
4691
  tb = $._textBaseline,
@@ -4615,7 +4731,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4615
4731
  }
4616
4732
  $._charStack.push(charsData);
4617
4733
 
4618
- let text = new Float32Array(6);
4734
+ let text = [];
4619
4735
 
4620
4736
  if ($._matrixDirty) $._saveMatrix();
4621
4737
 
@@ -4623,11 +4739,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4623
4739
  text[1] = -y;
4624
4740
  text[2] = $._textSize / 44;
4625
4741
  text[3] = $._transformIndex;
4626
- text[4] = $._fillIndex;
4742
+ text[4] = $._fillSet ? $._fillIndex : 0;
4627
4743
  text[5] = $._strokeIndex;
4628
4744
 
4629
4745
  $._textStack.push(text);
4630
- $.drawStack.push(2, measurements.printedCharCount);
4746
+ $.drawStack.push(2, measurements.printedCharCount, $._font.index);
4631
4747
  };
4632
4748
 
4633
4749
  $.textWidth = (str) => {
@@ -4640,11 +4756,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4640
4756
 
4641
4757
  if ($._doFill) {
4642
4758
  let fi = $._fillIndex * 4;
4643
- g.fill(colorsStack.slice(fi, fi + 4));
4759
+ g.fill(colorStack.slice(fi, fi + 4));
4644
4760
  }
4645
4761
  if ($._doStroke) {
4646
4762
  let si = $._strokeIndex * 4;
4647
- g.stroke(colorsStack.slice(si, si + 4));
4763
+ g.stroke(colorStack.slice(si, si + 4));
4648
4764
  }
4649
4765
 
4650
4766
  let img = g.createTextImage(str, w, h);
@@ -4695,21 +4811,15 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4695
4811
  totalTextSize += charsData.length * 4;
4696
4812
  }
4697
4813
 
4698
- // Create a single buffer for all text data
4814
+ // Create a single buffer for all char data
4699
4815
  let charBuffer = Q5.device.createBuffer({
4700
- label: 'charBuffer',
4701
4816
  size: totalTextSize,
4702
4817
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4703
4818
  mappedAtCreation: true
4704
4819
  });
4705
4820
 
4706
4821
  // Copy all text data into the buffer
4707
- let textArray = new Float32Array(charBuffer.getMappedRange());
4708
- let o = 0;
4709
- for (let array of $._charStack) {
4710
- textArray.set(array, o);
4711
- o += array.length;
4712
- }
4822
+ new Float32Array(charBuffer.getMappedRange()).set($._charStack.flat());
4713
4823
  charBuffer.unmap();
4714
4824
 
4715
4825
  // Calculate total buffer size for metadata
@@ -4724,12 +4834,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4724
4834
  });
4725
4835
 
4726
4836
  // Copy all metadata into the buffer
4727
- let metadataArray = new Float32Array(textBuffer.getMappedRange());
4728
- o = 0;
4729
- for (let array of $._textStack) {
4730
- metadataArray.set(array, o);
4731
- o += array.length;
4732
- }
4837
+ new Float32Array(textBuffer.getMappedRange()).set($._textStack.flat());
4733
4838
  textBuffer.unmap();
4734
4839
 
4735
4840
  // Create a single bind group for the text buffer and metadata buffer
@@ -4737,14 +4842,8 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4737
4842
  label: 'msdf text bind group',
4738
4843
  layout: textBindGroupLayout,
4739
4844
  entries: [
4740
- {
4741
- binding: 0,
4742
- resource: { buffer: charBuffer }
4743
- },
4744
- {
4745
- binding: 1,
4746
- resource: { buffer: textBuffer }
4747
- }
4845
+ { binding: 0, resource: { buffer: charBuffer } },
4846
+ { binding: 1, resource: { buffer: textBuffer } }
4748
4847
  ]
4749
4848
  });
4750
4849
  });