q5 2.5.4 → 2.6.1

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.6
4
4
  * @author quinton-ashley, Tezumie, and LingDong-
5
5
  * @license LGPL-3.0
6
6
  * @class Q5
@@ -259,7 +259,7 @@ Q5._nodejs = typeof process == 'object';
259
259
 
260
260
  Q5._instanceCount = 0;
261
261
  Q5._friendlyError = (msg, func) => {
262
- throw Error(func + ': ' + msg);
262
+ console.error(func + ': ' + msg);
263
263
  };
264
264
  Q5._validateParameters = () => true;
265
265
 
@@ -635,8 +635,7 @@ Q5.renderers.q2d.canvas = ($, q) => {
635
635
  };
636
636
 
637
637
  $.fill = function (c) {
638
- $._doFill = true;
639
- $._fillSet = true;
638
+ $._doFill = $._fillSet = true;
640
639
  if (Q5.Color) {
641
640
  if (!c._q5Color) {
642
641
  if (typeof c != 'string') c = $.color(...arguments);
@@ -648,8 +647,7 @@ Q5.renderers.q2d.canvas = ($, q) => {
648
647
  };
649
648
  $.noFill = () => ($._doFill = false);
650
649
  $.stroke = function (c) {
651
- $._doStroke = true;
652
- $._strokeSet = true;
650
+ $._doStroke = $._strokeSet = true;
653
651
  if (Q5.Color) {
654
652
  if (!c._q5Color) {
655
653
  if (typeof c != 'string') c = $.color(...arguments);
@@ -2759,7 +2757,7 @@ Q5.modules.util = ($, q) => {
2759
2757
  return ret;
2760
2758
  };
2761
2759
 
2762
- $.loadStrings = (path, cb) => $._loadFile(path, cb, 'text');
2760
+ $.loadText = (path, cb) => $._loadFile(path, cb, 'text');
2763
2761
  $.loadJSON = (path, cb) => $._loadFile(path, cb, 'json');
2764
2762
  $.loadCSV = (path, cb) => $._loadFile(path, cb, 'csv');
2765
2763
 
@@ -3075,18 +3073,30 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3075
3073
  c.width = $.width = 500;
3076
3074
  c.height = $.height = 500;
3077
3075
 
3078
- if ($.colorMode) $.colorMode('rgb', 'float');
3076
+ if ($.colorMode) $.colorMode('rgb', 1);
3079
3077
 
3080
- let pass;
3078
+ let pass,
3079
+ mainView,
3080
+ colorsLayout,
3081
+ colorIndex = 1,
3082
+ colorStackIndex = 8;
3081
3083
 
3082
- $.pipelines = [];
3084
+ $._pipelineConfigs = [];
3085
+ $._pipelines = [];
3083
3086
 
3084
3087
  // local variables used for slightly better performance
3085
3088
  // stores pipeline shifts and vertex counts/image indices
3086
3089
  let drawStack = ($.drawStack = []);
3087
3090
 
3088
3091
  // colors used for each draw call
3089
- let colorsStack = ($.colorsStack = [1, 1, 1, 1]);
3092
+
3093
+ let colorStack = ($.colorStack = new Float32Array(1e6));
3094
+
3095
+ // prettier-ignore
3096
+ colorStack.set([
3097
+ 0, 0, 0, 1, // black
3098
+ 1, 1, 1, 1 // white
3099
+ ]);
3090
3100
 
3091
3101
  $._transformLayout = Q5.device.createBindGroupLayout({
3092
3102
  label: 'transformLayout',
@@ -3110,32 +3120,70 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3110
3120
  ]
3111
3121
  });
3112
3122
 
3113
- $.bindGroupLayouts = [$._transformLayout];
3123
+ colorsLayout = Q5.device.createBindGroupLayout({
3124
+ label: 'colorsLayout',
3125
+ entries: [
3126
+ {
3127
+ binding: 0,
3128
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
3129
+ buffer: {
3130
+ type: 'read-only-storage',
3131
+ hasDynamicOffset: false
3132
+ }
3133
+ }
3134
+ ]
3135
+ });
3136
+
3137
+ $.bindGroupLayouts = [$._transformLayout, colorsLayout];
3114
3138
 
3115
3139
  let uniformBuffer = Q5.device.createBuffer({
3116
3140
  size: 8, // Size of two floats
3117
3141
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
3118
3142
  });
3119
3143
 
3144
+ let createMainView = () => {
3145
+ mainView = Q5.device
3146
+ .createTexture({
3147
+ size: [$.canvas.width, $.canvas.height],
3148
+ sampleCount: 4,
3149
+ format: 'bgra8unorm',
3150
+ usage: GPUTextureUsage.RENDER_ATTACHMENT
3151
+ })
3152
+ .createView();
3153
+ };
3154
+
3120
3155
  $._createCanvas = (w, h, opt) => {
3121
3156
  q.ctx = q.drawingContext = c.getContext('webgpu');
3122
3157
 
3123
3158
  opt.format ??= navigator.gpu.getPreferredCanvasFormat();
3124
3159
  opt.device ??= Q5.device;
3125
3160
 
3161
+ // needed for other blend modes but couldn't get it working
3162
+ // opt.alphaMode = 'premultiplied';
3163
+
3126
3164
  $.ctx.configure(opt);
3127
3165
 
3128
3166
  Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([$.canvas.hw, $.canvas.hh]));
3129
3167
 
3168
+ createMainView();
3169
+
3130
3170
  return c;
3131
3171
  };
3132
3172
 
3133
3173
  $._resizeCanvas = (w, h) => {
3134
3174
  $._setCanvasSize(w, h);
3175
+ createMainView();
3176
+ };
3177
+
3178
+ $.pixelDensity = (v) => {
3179
+ if (!v || v == $._pixelDensity) return $._pixelDensity;
3180
+ $._pixelDensity = v;
3181
+ $._setCanvasSize(c.w, c.h);
3182
+ createMainView();
3183
+ return v;
3135
3184
  };
3136
3185
 
3137
3186
  // current color index, used to associate a vertex with a color
3138
- let colorIndex = 0;
3139
3187
  let addColor = (r, g, b, a = 1) => {
3140
3188
  if (typeof r == 'string') r = $.color(r);
3141
3189
  else if (b == undefined) {
@@ -3143,21 +3191,35 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3143
3191
  a = g ?? 1;
3144
3192
  g = b = r;
3145
3193
  }
3146
- if (r._q5Color) colorsStack.push(r.r, r.g, r.b, r.a);
3147
- else colorsStack.push(r, g, b, a);
3194
+ if (r._q5Color) {
3195
+ a = r.a;
3196
+ b = r.b;
3197
+ g = r.g;
3198
+ r = r.r;
3199
+ }
3200
+
3201
+ let cs = colorStack,
3202
+ i = colorStackIndex;
3203
+ cs[i++] = r;
3204
+ cs[i++] = g;
3205
+ cs[i++] = b;
3206
+ cs[i++] = a;
3207
+ colorStackIndex = i;
3208
+
3148
3209
  colorIndex++;
3149
3210
  };
3150
3211
 
3151
- $._fillIndex = $._strokeIndex = -1;
3212
+ $._fillIndex = $._strokeIndex = 0;
3213
+ $._doFill = $._doStroke = true;
3152
3214
 
3153
3215
  $.fill = (r, g, b, a) => {
3154
3216
  addColor(r, g, b, a);
3155
- $._doFill = true;
3217
+ $._doFill = $._fillSet = true;
3156
3218
  $._fillIndex = colorIndex;
3157
3219
  };
3158
3220
  $.stroke = (r, g, b, a) => {
3159
3221
  addColor(r, g, b, a);
3160
- $._doStroke = true;
3222
+ $._doStroke = $._strokeSet = true;
3161
3223
  $._strokeIndex = colorIndex;
3162
3224
  };
3163
3225
 
@@ -3185,7 +3247,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3185
3247
  $._matrixDirty = false;
3186
3248
 
3187
3249
  // array to store transformation matrices for the render pass
3188
- $.transformStates = [$._matrix.slice()];
3250
+ let transformStates = [$._matrix.slice()];
3189
3251
 
3190
3252
  // stack to keep track of transformation matrix indexes
3191
3253
  $._transformIndexStack = [];
@@ -3302,8 +3364,8 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3302
3364
 
3303
3365
  // Function to save the current matrix state if dirty
3304
3366
  $._saveMatrix = () => {
3305
- $.transformStates.push($._matrix.slice());
3306
- $._transformIndex = $.transformStates.length - 1;
3367
+ transformStates.push($._matrix.slice());
3368
+ $._transformIndex = transformStates.length - 1;
3307
3369
  $._matrixDirty = false;
3308
3370
  };
3309
3371
 
@@ -3318,7 +3380,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3318
3380
  }
3319
3381
  // Pop the last matrix index and set it as the current matrix index
3320
3382
  let idx = $._transformIndexStack.pop();
3321
- $._matrix = $.transformStates[idx].slice();
3383
+ $._matrix = transformStates[idx].slice();
3322
3384
  $._transformIndex = idx;
3323
3385
  $._matrixDirty = false;
3324
3386
  };
@@ -3359,6 +3421,68 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3359
3421
  return [l, r, t, b];
3360
3422
  };
3361
3423
 
3424
+ // prettier-ignore
3425
+ let blendFactors = [
3426
+ 'zero', // 0
3427
+ 'one', // 1
3428
+ 'src-alpha', // 2
3429
+ 'one-minus-src-alpha', // 3
3430
+ 'dst', // 4
3431
+ 'dst-alpha', // 5
3432
+ 'one-minus-dst-alpha', // 6
3433
+ 'one-minus-src' // 7
3434
+ ];
3435
+ let blendOps = [
3436
+ 'add', // 0
3437
+ 'subtract', // 1
3438
+ 'reverse-subtract', // 2
3439
+ 'min', // 3
3440
+ 'max' // 4
3441
+ ];
3442
+
3443
+ const blendModes = {
3444
+ normal: [2, 3, 0, 2, 3, 0],
3445
+ // destination_over: [6, 1, 0, 6, 1, 0],
3446
+ additive: [1, 1, 0, 1, 1, 0]
3447
+ // source_in: [5, 0, 0, 5, 0, 0],
3448
+ // destination_in: [0, 2, 0, 0, 2, 0],
3449
+ // source_out: [6, 0, 0, 6, 0, 0],
3450
+ // destination_out: [0, 3, 0, 0, 3, 0],
3451
+ // source_atop: [5, 3, 0, 5, 3, 0],
3452
+ // destination_atop: [6, 2, 0, 6, 2, 0]
3453
+ };
3454
+
3455
+ $.blendConfigs = {};
3456
+
3457
+ for (const [name, mode] of Object.entries(blendModes)) {
3458
+ $.blendConfigs[name] = {
3459
+ color: {
3460
+ srcFactor: blendFactors[mode[0]],
3461
+ dstFactor: blendFactors[mode[1]],
3462
+ operation: blendOps[mode[2]]
3463
+ },
3464
+ alpha: {
3465
+ srcFactor: blendFactors[mode[3]],
3466
+ dstFactor: blendFactors[mode[4]],
3467
+ operation: blendOps[mode[5]]
3468
+ }
3469
+ };
3470
+ }
3471
+
3472
+ $._blendMode = 'normal';
3473
+ $.blendMode = (mode) => {
3474
+ if (mode == $._blendMode) return;
3475
+ if (mode == 'source-over') mode = 'normal';
3476
+ if (mode == 'lighter') mode = 'additive';
3477
+ mode = mode.toLowerCase().replace(/[ -]/g, '_');
3478
+ $._blendMode = mode;
3479
+
3480
+ for (let i = 0; i < $._pipelines.length; i++) {
3481
+ $._pipelineConfigs[i].fragment.targets[0].blend = $.blendConfigs[mode];
3482
+ $._pipelines[i] = Q5.device.createRenderPipeline($._pipelineConfigs[i]);
3483
+ }
3484
+ };
3485
+
3362
3486
  $.clear = () => {};
3363
3487
 
3364
3488
  $._beginRender = () => {
@@ -3368,7 +3492,8 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3368
3492
  label: 'q5-webgpu',
3369
3493
  colorAttachments: [
3370
3494
  {
3371
- view: ctx.getCurrentTexture().createView(),
3495
+ view: mainView,
3496
+ resolveTarget: $.ctx.getCurrentTexture().createView(),
3372
3497
  loadOp: 'clear',
3373
3498
  storeOp: 'store'
3374
3499
  }
@@ -3379,58 +3504,62 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3379
3504
  $._render = () => {
3380
3505
  if (transformStates.length > 1 || !$._transformBindGroup) {
3381
3506
  let transformBuffer = Q5.device.createBuffer({
3382
- size: transformStates.length * 64, // Size of 16 floats
3383
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
3507
+ size: transformStates.length * 64, // 64 is the size of 16 floats
3508
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
3509
+ mappedAtCreation: true
3384
3510
  });
3385
3511
 
3386
- Q5.device.queue.writeBuffer(transformBuffer, 0, new Float32Array(transformStates.flat()));
3512
+ new Float32Array(transformBuffer.getMappedRange()).set(transformStates.flat());
3513
+ transformBuffer.unmap();
3387
3514
 
3388
3515
  $._transformBindGroup = Q5.device.createBindGroup({
3389
3516
  layout: $._transformLayout,
3390
3517
  entries: [
3391
- {
3392
- binding: 0,
3393
- resource: {
3394
- buffer: uniformBuffer
3395
- }
3396
- },
3397
- {
3398
- binding: 1,
3399
- resource: {
3400
- buffer: transformBuffer
3401
- }
3402
- }
3518
+ { binding: 0, resource: { buffer: uniformBuffer } },
3519
+ { binding: 1, resource: { buffer: transformBuffer } }
3403
3520
  ]
3404
3521
  });
3405
3522
  }
3406
3523
 
3407
3524
  pass.setBindGroup(0, $._transformBindGroup);
3408
3525
 
3526
+ let colorsBuffer = Q5.device.createBuffer({
3527
+ size: colorStackIndex * 4,
3528
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
3529
+ mappedAtCreation: true
3530
+ });
3531
+
3532
+ new Float32Array(colorsBuffer.getMappedRange()).set(colorStack.slice(0, colorStackIndex));
3533
+ colorsBuffer.unmap();
3534
+
3535
+ $._colorsBindGroup = Q5.device.createBindGroup({
3536
+ layout: colorsLayout,
3537
+ entries: [{ binding: 0, resource: { buffer: colorsBuffer } }]
3538
+ });
3539
+
3540
+ $.pass.setBindGroup(1, $._colorsBindGroup);
3541
+
3409
3542
  for (let m of $._hooks.preRender) m();
3410
3543
 
3411
- let drawVertOffset = 0;
3412
- let imageVertOffset = 0;
3413
- let textCharOffset = 0;
3414
- let curPipelineIndex = -1;
3415
- let curTextureIndex = -1;
3544
+ let drawVertOffset = 0,
3545
+ imageVertOffset = 0,
3546
+ textCharOffset = 0,
3547
+ curPipelineIndex = -1,
3548
+ curTextureIndex = -1;
3416
3549
 
3417
- for (let i = 0; i < drawStack.length; i += 2) {
3550
+ for (let i = 0; i < drawStack.length; i += 3) {
3418
3551
  let v = drawStack[i + 1];
3419
-
3420
- if (drawStack[i] == -1) {
3421
- v();
3422
- continue;
3423
- }
3552
+ let o = drawStack[i + 2];
3424
3553
 
3425
3554
  if (curPipelineIndex != drawStack[i]) {
3426
3555
  curPipelineIndex = drawStack[i];
3427
- pass.setPipeline($.pipelines[curPipelineIndex]);
3556
+ pass.setPipeline($._pipelines[curPipelineIndex]);
3428
3557
  }
3429
3558
 
3430
3559
  if (curPipelineIndex == 0) {
3431
3560
  // v is the number of vertices
3432
- pass.draw(v, 1, drawVertOffset);
3433
- drawVertOffset += v;
3561
+ pass.drawIndexed(v, 1, 0, drawVertOffset);
3562
+ drawVertOffset += o;
3434
3563
  } else if (curPipelineIndex == 1) {
3435
3564
  if (curTextureIndex != v) {
3436
3565
  // v is the texture index
@@ -3439,7 +3568,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3439
3568
  pass.draw(6, 1, imageVertOffset);
3440
3569
  imageVertOffset += 6;
3441
3570
  } else if (curPipelineIndex == 2) {
3442
- pass.setBindGroup(2, $._font.bindGroup);
3571
+ pass.setBindGroup(2, $._fonts[o].bindGroup);
3443
3572
  pass.setBindGroup(3, $._textBindGroup);
3444
3573
 
3445
3574
  // v is the number of characters in the text
@@ -3455,69 +3584,84 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3455
3584
  pass.end();
3456
3585
  let commandBuffer = $.encoder.finish();
3457
3586
  Q5.device.queue.submit([commandBuffer]);
3587
+
3458
3588
  q.pass = $.encoder = null;
3459
3589
 
3460
3590
  // clear the stacks for the next frame
3461
3591
  $.drawStack.length = 0;
3462
- $.colorsStack.length = 4;
3463
- colorIndex = 0;
3592
+ colorIndex = 1;
3593
+ colorStackIndex = 8;
3464
3594
  rotation = 0;
3465
- $.transformStates.length = 1;
3595
+ transformStates.length = 1;
3466
3596
  $._transformIndexStack.length = 0;
3467
3597
  };
3468
3598
  };
3469
3599
 
3470
- Q5.webgpu = async function (scope, parent) {
3471
- if (!scope || scope == 'global') Q5._hasGlobal = true;
3600
+ Q5.initWebGPU = async () => {
3472
3601
  if (!navigator.gpu) {
3473
3602
  console.warn('q5 WebGPU not supported on this browser!');
3603
+ return false;
3604
+ }
3605
+ if (!Q5.device) {
3606
+ let adapter = await navigator.gpu.requestAdapter();
3607
+ if (!adapter) throw new Error('No appropriate GPUAdapter found.');
3608
+ Q5.device = await adapter.requestDevice();
3609
+ }
3610
+ return true;
3611
+ };
3612
+
3613
+ Q5.webgpu = async function (scope, parent) {
3614
+ if (!scope || scope == 'global') Q5._hasGlobal = true;
3615
+ if (!(await Q5.initWebGPU())) {
3474
3616
  let q = new Q5(scope, parent);
3475
3617
  q.colorMode('rgb', 1);
3476
3618
  q._beginRender = () => q.translate(q.canvas.hw, q.canvas.hh);
3477
3619
  return q;
3478
3620
  }
3479
- let adapter = await navigator.gpu.requestAdapter();
3480
- if (!adapter) throw new Error('No appropriate GPUAdapter found.');
3481
- Q5.device = await adapter.requestDevice();
3482
-
3483
3621
  return new Q5(scope, parent, 'webgpu');
3484
3622
  };
3485
3623
  Q5.renderers.webgpu.drawing = ($, q) => {
3486
- let c = $.canvas;
3487
-
3488
- let drawStack = $.drawStack;
3489
- let colorsStack = $.colorsStack;
3490
-
3491
- let verticesStack = [];
3492
-
3493
- let colorIndex, colorsLayout;
3624
+ let c = $.canvas,
3625
+ drawStack = $.drawStack,
3626
+ vertexStack = new Float32Array(1e7),
3627
+ indexStack = new Uint32Array(1e6),
3628
+ vertIndex = 0,
3629
+ vertCount = 0,
3630
+ idxBufferIndex = 0,
3631
+ colorIndex;
3494
3632
 
3495
3633
  let vertexShader = Q5.device.createShaderModule({
3496
3634
  label: 'drawingVertexShader',
3497
3635
  code: `
3636
+ struct VertexInput {
3637
+ @location(0) pos: vec2f,
3638
+ @location(1) colorIndex: f32,
3639
+ @location(2) transformIndex: f32
3640
+ }
3498
3641
  struct VertexOutput {
3499
3642
  @builtin(position) position: vec4f,
3500
- @location(0) colorIndex: f32
3501
- };
3502
-
3643
+ @location(0) color: vec4f
3644
+ }
3503
3645
  struct Uniforms {
3504
3646
  halfWidth: f32,
3505
3647
  halfHeight: f32
3506
- };
3648
+ }
3507
3649
 
3508
3650
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3509
- @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
3651
+ @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
3652
+
3653
+ @group(1) @binding(0) var<storage> colors : array<vec4f>;
3510
3654
 
3511
3655
  @vertex
3512
- fn vertexMain(@location(0) pos: vec2f, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput {
3513
- var vert = vec4f(pos, 0.0, 1.0);
3514
- vert = transforms[i32(transformIndex)] * vert;
3656
+ fn vertexMain(input: VertexInput) -> VertexOutput {
3657
+ var vert = vec4f(input.pos, 0.0, 1.0);
3658
+ vert = transforms[i32(input.transformIndex)] * vert;
3515
3659
  vert.x /= uniforms.halfWidth;
3516
3660
  vert.y /= uniforms.halfHeight;
3517
3661
 
3518
3662
  var output: VertexOutput;
3519
3663
  output.position = vert;
3520
- output.colorIndex = colorIndex;
3664
+ output.color = colors[i32(input.colorIndex)];
3521
3665
  return output;
3522
3666
  }
3523
3667
  `
@@ -3526,32 +3670,13 @@ fn vertexMain(@location(0) pos: vec2f, @location(1) colorIndex: f32, @location(2
3526
3670
  let fragmentShader = Q5.device.createShaderModule({
3527
3671
  label: 'drawingFragmentShader',
3528
3672
  code: `
3529
- @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
3530
-
3531
3673
  @fragment
3532
- fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3533
- let index = i32(colorIndex);
3534
- return mix(colors[index], colors[index + 1], fract(colorIndex));
3674
+ fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
3675
+ return color;
3535
3676
  }
3536
3677
  `
3537
3678
  });
3538
3679
 
3539
- colorsLayout = Q5.device.createBindGroupLayout({
3540
- label: 'colorsLayout',
3541
- entries: [
3542
- {
3543
- binding: 0,
3544
- visibility: GPUShaderStage.FRAGMENT,
3545
- buffer: {
3546
- type: 'read-only-storage',
3547
- hasDynamicOffset: false
3548
- }
3549
- }
3550
- ]
3551
- });
3552
-
3553
- $.bindGroupLayouts.push(colorsLayout);
3554
-
3555
3680
  let vertexBufferLayout = {
3556
3681
  arrayStride: 16, // 2 coordinates + 1 color index + 1 transform index * 4 bytes each
3557
3682
  attributes: [
@@ -3561,193 +3686,212 @@ fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3561
3686
  ]
3562
3687
  };
3563
3688
 
3564
- // prettier-ignore
3565
- let blendFactors = [
3566
- 'zero', // 0
3567
- 'one', // 1
3568
- 'src-alpha', // 2
3569
- 'one-minus-src-alpha', // 3
3570
- 'dst', // 4
3571
- 'dst-alpha', // 5
3572
- 'one-minus-dst-alpha', // 6
3573
- 'one-minus-src' // 7
3574
- ];
3575
- let blendOps = [
3576
- 'add', // 0
3577
- 'subtract', // 1
3578
- 'reverse-subtract', // 2
3579
- 'min', // 3
3580
- 'max' // 4
3581
- ];
3582
-
3583
- const blendModes = {
3584
- normal: [2, 3, 0, 2, 3, 0],
3585
- lighter: [2, 1, 0, 2, 1, 0],
3586
- subtract: [2, 1, 2, 2, 1, 2],
3587
- multiply: [4, 0, 0, 5, 0, 0],
3588
- screen: [1, 3, 0, 1, 3, 0],
3589
- darken: [1, 3, 3, 1, 3, 3],
3590
- lighten: [1, 3, 4, 1, 3, 4],
3591
- overlay: [2, 3, 0, 2, 3, 0],
3592
- hard_light: [2, 3, 0, 2, 3, 0],
3593
- soft_light: [2, 3, 0, 2, 3, 0],
3594
- difference: [2, 3, 2, 2, 3, 2],
3595
- exclusion: [2, 3, 0, 2, 3, 0],
3596
- color_dodge: [1, 7, 0, 1, 7, 0],
3597
- color_burn: [6, 1, 0, 6, 1, 0],
3598
- linear_dodge: [2, 1, 0, 2, 1, 0],
3599
- linear_burn: [2, 7, 1, 2, 7, 1],
3600
- vivid_light: [2, 7, 0, 2, 7, 0],
3601
- pin_light: [2, 7, 0, 2, 7, 0],
3602
- hard_mix: [2, 7, 0, 2, 7, 0]
3603
- };
3604
-
3605
- $.blendConfigs = {};
3606
-
3607
- for (const [name, mode] of Object.entries(blendModes)) {
3608
- $.blendConfigs[name] = {
3609
- color: {
3610
- srcFactor: blendFactors[mode[0]],
3611
- dstFactor: blendFactors[mode[1]],
3612
- operation: blendOps[mode[2]]
3613
- },
3614
- alpha: {
3615
- srcFactor: blendFactors[mode[3]],
3616
- dstFactor: blendFactors[mode[4]],
3617
- operation: blendOps[mode[5]]
3618
- }
3619
- };
3620
- }
3621
-
3622
- $._blendMode = 'normal';
3623
- $.blendMode = (mode) => {
3624
- if (mode == $._blendMode) return;
3625
- if (mode == 'source-over') mode = 'normal';
3626
- mode = mode.toLowerCase().replace(/[ -]/g, '_');
3627
- $._blendMode = mode;
3628
- $.pipelines[0] = $._createPipeline($.blendConfigs[mode]);
3629
- };
3630
-
3631
3689
  let pipelineLayout = Q5.device.createPipelineLayout({
3632
3690
  label: 'drawingPipelineLayout',
3633
3691
  bindGroupLayouts: $.bindGroupLayouts
3634
3692
  });
3635
3693
 
3636
- $._createPipeline = (blendConfig) => {
3637
- return Q5.device.createRenderPipeline({
3638
- label: 'drawingPipeline',
3639
- layout: pipelineLayout,
3640
- vertex: {
3641
- module: vertexShader,
3642
- entryPoint: 'vertexMain',
3643
- buffers: [vertexBufferLayout]
3644
- },
3645
- fragment: {
3646
- module: fragmentShader,
3647
- entryPoint: 'fragmentMain',
3648
- targets: [{ format: 'bgra8unorm', blend: blendConfig }]
3649
- },
3650
- primitive: { topology: 'triangle-list' }
3651
- });
3694
+ $._pipelineConfigs[0] = {
3695
+ label: 'drawingPipeline',
3696
+ layout: pipelineLayout,
3697
+ vertex: {
3698
+ module: vertexShader,
3699
+ entryPoint: 'vertexMain',
3700
+ buffers: [vertexBufferLayout]
3701
+ },
3702
+ fragment: {
3703
+ module: fragmentShader,
3704
+ entryPoint: 'fragmentMain',
3705
+ targets: [{ format: 'bgra8unorm', blend: $.blendConfigs.normal }]
3706
+ },
3707
+ primitive: { topology: 'triangle-list' },
3708
+ multisample: {
3709
+ count: 4
3710
+ }
3711
+ };
3712
+
3713
+ $._pipelines[0] = Q5.device.createRenderPipeline($._pipelineConfigs[0]);
3714
+
3715
+ const addVert = (x, y, ci, ti) => {
3716
+ let v = vertexStack,
3717
+ i = vertIndex;
3718
+ v[i++] = x;
3719
+ v[i++] = y;
3720
+ v[i++] = ci;
3721
+ v[i++] = ti;
3722
+ vertIndex = i;
3723
+ vertCount++;
3724
+ };
3725
+
3726
+ const addIndex = (i1, i2, i3) => {
3727
+ let is = indexStack,
3728
+ ii = idxBufferIndex;
3729
+ is[ii++] = i1;
3730
+ is[ii++] = i2;
3731
+ is[ii++] = i3;
3732
+ idxBufferIndex = ii;
3733
+ };
3734
+
3735
+ const addQuad = (x1, y1, x2, y2, x3, y3, x4, y4, ci, ti) => {
3736
+ let v = vertexStack,
3737
+ i = vertIndex;
3738
+
3739
+ let i1 = vertCount++;
3740
+ v[i++] = x1;
3741
+ v[i++] = y1;
3742
+ v[i++] = ci;
3743
+ v[i++] = ti;
3744
+
3745
+ let i2 = vertCount++;
3746
+ v[i++] = x2;
3747
+ v[i++] = y2;
3748
+ v[i++] = ci;
3749
+ v[i++] = ti;
3750
+
3751
+ let i3 = vertCount++;
3752
+ v[i++] = x3;
3753
+ v[i++] = y3;
3754
+ v[i++] = ci;
3755
+ v[i++] = ti;
3756
+
3757
+ let i4 = vertCount++;
3758
+ v[i++] = x4;
3759
+ v[i++] = y4;
3760
+ v[i++] = ci;
3761
+ v[i++] = ti;
3762
+
3763
+ vertIndex = i;
3764
+
3765
+ let is = indexStack,
3766
+ ii = idxBufferIndex;
3767
+ is[ii++] = i1;
3768
+ is[ii++] = i2;
3769
+ is[ii++] = i3;
3770
+ is[ii++] = i1;
3771
+ is[ii++] = i3;
3772
+ is[ii++] = i4;
3773
+ idxBufferIndex = ii;
3774
+
3775
+ drawStack.push(0, 6, 4);
3776
+ };
3777
+
3778
+ const addEllipse = (x, y, a, b, n, ci, ti) => {
3779
+ let t = 0,
3780
+ angleIncrement = $.TAU / n,
3781
+ indicesStart = vertIndex / 4;
3782
+ addVert(x, y, ci, ti); // Center vertex
3783
+ for (let i = 0; i <= n; i++) {
3784
+ let vx = x + a * Math.cos(t),
3785
+ vy = y + b * Math.sin(t);
3786
+ addVert(vx, vy, ci, ti);
3787
+ if (i > 0) {
3788
+ addIndex(indicesStart, indicesStart + i, indicesStart + i + 1);
3789
+ }
3790
+ t += angleIncrement;
3791
+ }
3792
+ drawStack.push(0, n * 3, n + 2);
3652
3793
  };
3653
3794
 
3654
- $.pipelines[0] = $._createPipeline($.blendConfigs.normal);
3795
+ $.rectMode = (x) => ($._rectMode = x);
3655
3796
 
3656
- let shapeVertices;
3797
+ $.rect = (x, y, w, h) => {
3798
+ let [l, r, t, b] = $._calcBox(x, y, w, h, $._rectMode);
3799
+ let ci, ti;
3800
+ if ($._matrixDirty) $._saveMatrix();
3801
+ ti = $._transformIndex;
3657
3802
 
3658
- $.beginShape = () => {
3659
- shapeVertices = [];
3660
- };
3803
+ if ($._doStroke) {
3804
+ ci = $._strokeIndex;
3661
3805
 
3662
- $.vertex = (x, y) => {
3663
- if ($._matrixDirty) $._saveMatrix();
3664
- shapeVertices.push(x, -y, $._fillIndex, $._transformIndex);
3665
- };
3806
+ // outer rectangle coordinates
3807
+ let sw = $._strokeWeight / 2;
3808
+ let to = t + sw,
3809
+ bo = b - sw,
3810
+ lo = l - sw,
3811
+ ro = r + sw;
3666
3812
 
3667
- $.endShape = (close) => {
3668
- if (!$._doFill) {
3669
- shapeVertices = [];
3670
- return;
3671
- }
3672
- let v = shapeVertices;
3673
- if (v.length < 12) {
3674
- throw new Error('A shape must have at least 3 vertices.');
3675
- }
3676
- if (close) {
3677
- // Close the shape by adding the first vertex at the end
3678
- v.push(v[0], v[1], v[2], v[3]);
3679
- }
3680
- // Convert the shape to triangles
3681
- let triangles = [];
3682
- for (let i = 4; i < v.length; i += 4) {
3683
- triangles.push(
3684
- v[0], // First vertex
3685
- v[1],
3686
- v[2],
3687
- v[3],
3688
- v[i - 4], // Previous vertex
3689
- v[i - 3],
3690
- v[i - 2],
3691
- v[i - 1],
3692
- v[i], // Current vertex
3693
- v[i + 1],
3694
- v[i + 2],
3695
- v[i + 3]
3696
- );
3813
+ // stroke is simply a bigger rectangle drawn first
3814
+ addQuad(lo, to, ro, to, ro, bo, lo, bo, ci, ti);
3815
+
3816
+ // inner rectangle coordinates
3817
+ t -= sw;
3818
+ b += sw;
3819
+ l += sw;
3820
+ r -= sw;
3697
3821
  }
3698
- shapeVertices = [];
3699
3822
 
3700
- verticesStack.push(...triangles);
3701
- drawStack.push(0, triangles.length / 4);
3702
- };
3823
+ if ($._doFill) {
3824
+ ci = colorIndex ?? $._fillIndex;
3703
3825
 
3704
- $.triangle = (x1, y1, x2, y2, x3, y3) => {
3705
- $.beginShape();
3706
- $.vertex(x1, y1);
3707
- $.vertex(x2, y2);
3708
- $.vertex(x3, y3);
3709
- $.endShape(1);
3826
+ // two triangles make a rectangle
3827
+ addQuad(l, t, r, t, r, b, l, b, ci, ti);
3828
+ }
3710
3829
  };
3711
3830
 
3712
- $.quad = (x1, y1, x2, y2, x3, y3, x4, y4) => {
3713
- $.beginShape();
3714
- $.vertex(x1, y1);
3715
- $.vertex(x2, y2);
3716
- $.vertex(x3, y3);
3717
- $.vertex(x4, y4);
3718
- $.endShape(1);
3719
- };
3831
+ $.square = (x, y, s) => $.rect(x, y, s, s);
3720
3832
 
3721
- $.rectMode = (x) => ($._rectMode = x);
3833
+ // prettier-ignore
3834
+ const getArcSegments = (d) =>
3835
+ d < 4 ? 6 :
3836
+ d < 6 ? 8 :
3837
+ d < 10 ? 10 :
3838
+ d < 16 ? 12 :
3839
+ d < 20 ? 14 :
3840
+ d < 22 ? 16 :
3841
+ d < 24 ? 18 :
3842
+ d < 28 ? 20 :
3843
+ d < 34 ? 22 :
3844
+ d < 42 ? 24 :
3845
+ d < 48 ? 26 :
3846
+ d < 56 ? 28 :
3847
+ d < 64 ? 30 :
3848
+ d < 72 ? 32 :
3849
+ d < 84 ? 34 :
3850
+ d < 96 ? 36 :
3851
+ d < 98 ? 38 :
3852
+ d < 113 ? 40 :
3853
+ d < 149 ? 44 :
3854
+ d < 199 ? 48 :
3855
+ d < 261 ? 52 :
3856
+ d < 353 ? 56 :
3857
+ d < 461 ? 60 :
3858
+ d < 585 ? 64 :
3859
+ d < 1200 ? 70 :
3860
+ d < 1800 ? 80 :
3861
+ d < 2400 ? 90 :
3862
+ 100;
3722
3863
 
3723
- $.rect = (x, y, w, h) => {
3724
- let [l, r, t, b] = $._calcBox(x, y, w, h, $._rectMode);
3864
+ $.ellipseMode = (x) => ($._ellipseMode = x);
3725
3865
 
3726
- let ci = colorIndex ?? $._fillIndex;
3866
+ $.ellipse = (x, y, w, h) => {
3867
+ let n = getArcSegments(w == h ? w : Math.max(w, h));
3868
+ let a = Math.max(w, 1) / 2;
3869
+ let b = w == h ? a : Math.max(h, 1) / 2;
3870
+ let ci;
3727
3871
  if ($._matrixDirty) $._saveMatrix();
3728
3872
  let ti = $._transformIndex;
3729
- // two triangles make a rectangle
3730
- // prettier-ignore
3731
- verticesStack.push(
3732
- l, t, ci, ti,
3733
- r, t, ci, ti,
3734
- l, b, ci, ti,
3735
- r, t, ci, ti,
3736
- l, b, ci, ti,
3737
- r, b, ci, ti
3738
- );
3739
- drawStack.push(0, 6);
3873
+ if ($._doStroke) {
3874
+ let sw = $._strokeWeight / 2;
3875
+ addEllipse(x, y, a + sw, b + sw, n, $._strokeIndex, ti);
3876
+ a -= sw;
3877
+ b -= sw;
3878
+ }
3879
+ if ($._doFill) {
3880
+ addEllipse(x, y, a, b, n, colorIndex ?? $._fillIndex, ti);
3881
+ }
3740
3882
  };
3741
3883
 
3742
- $.square = (x, y, s) => $.rect(x, y, s, s);
3884
+ $.circle = (x, y, d) => $.ellipse(x, y, d, d);
3743
3885
 
3744
3886
  $.point = (x, y) => {
3745
3887
  colorIndex = $._strokeIndex;
3888
+ $._doStroke = false;
3746
3889
  let sw = $._strokeWeight;
3747
3890
  if (sw < 2) {
3748
3891
  sw = Math.round(sw);
3749
3892
  $.rect(x, y, sw, sw);
3750
3893
  } else $.ellipse(x, y, sw, sw);
3894
+ $._doStroke = true;
3751
3895
  colorIndex = null;
3752
3896
  };
3753
3897
 
@@ -3755,19 +3899,97 @@ fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3755
3899
  colorIndex = $._strokeIndex;
3756
3900
 
3757
3901
  $.push();
3758
- $.translate(x1, y1);
3902
+ $._doStroke = false;
3903
+ $.translate(x1, -y1);
3759
3904
  $.rotate($.atan2(y2 - y1, x2 - x1));
3760
3905
  let length = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
3761
- let sw = $._strokeWeight;
3762
- $.rect(0, -sw / 2, length, sw);
3906
+ let sw = $._strokeWeight,
3907
+ hsw = sw / 2;
3908
+ $._rectMode = 'corner';
3909
+ if (sw < 4) {
3910
+ $.rect(-hsw, -hsw, length + hsw, sw);
3911
+ } else {
3912
+ $._ellipseMode = 'center';
3913
+ $.ellipse(0, 0, sw, sw);
3914
+ $.ellipse(length, 0, sw, sw);
3915
+ $.rect(0, -hsw, length, sw);
3916
+ }
3917
+
3763
3918
  $.pop();
3764
3919
 
3765
3920
  colorIndex = null;
3766
3921
  };
3767
3922
 
3923
+ let shapeVertCount;
3924
+
3925
+ $.beginShape = () => {
3926
+ shapeVertCount = 0;
3927
+ };
3928
+
3929
+ $.vertex = (x, y) => {
3930
+ if ($._matrixDirty) $._saveMatrix();
3931
+ addVert(x, -y, $._fillIndex, $._transformIndex);
3932
+ shapeVertCount++;
3933
+ };
3934
+
3935
+ $.endShape = (close) => {
3936
+ if (shapeVertCount < 3) {
3937
+ throw new Error('A shape must have at least 3 vertices.');
3938
+ }
3939
+
3940
+ let firstVert = vertCount - shapeVertCount;
3941
+
3942
+ if ($._doFill) {
3943
+ // make a simple triangle fan, starting from the first vertex
3944
+ for (let i = firstVert + 1; i < vertCount - 1; i++) {
3945
+ addIndex(firstVert, i, i + 1);
3946
+ }
3947
+ drawStack.push(0, (shapeVertCount - 2) * 3, shapeVertCount);
3948
+ }
3949
+
3950
+ if ($._doStroke) {
3951
+ let first = firstVert * 4,
3952
+ last = vertIndex - 4;
3953
+ for (let i = first; i < last; i += 4) {
3954
+ let x1 = vertexStack[i],
3955
+ y1 = vertexStack[i + 1],
3956
+ x2 = vertexStack[i + 4],
3957
+ y2 = vertexStack[i + 5];
3958
+ $.line(x1, y1, x2, y2);
3959
+ }
3960
+ if (close) {
3961
+ let x1 = vertexStack[last],
3962
+ y1 = vertexStack[last + 1],
3963
+ x2 = vertexStack[first],
3964
+ y2 = vertexStack[first + 1];
3965
+ $.line(x1, y1, x2, y2);
3966
+ }
3967
+ }
3968
+
3969
+ shapeVertCount = 0;
3970
+ };
3971
+
3972
+ $.triangle = (x1, y1, x2, y2, x3, y3) => {
3973
+ $.beginShape();
3974
+ $.vertex(x1, y1);
3975
+ $.vertex(x2, y2);
3976
+ $.vertex(x3, y3);
3977
+ $.endShape();
3978
+ };
3979
+
3980
+ $.quad = (x1, y1, x2, y2, x3, y3, x4, y4) => {
3981
+ $.beginShape();
3982
+ $.vertex(x1, y1);
3983
+ $.vertex(x2, y2);
3984
+ $.vertex(x3, y3);
3985
+ $.vertex(x4, y4);
3986
+ $.endShape();
3987
+ };
3988
+
3768
3989
  $.background = (r, g, b, a) => {
3769
3990
  $.push();
3770
3991
  $.resetMatrix();
3992
+ $._doStroke = false;
3771
3993
  if (r.src) {
3772
3994
  let og = $._imageMode;
3773
3995
  $._imageMode = 'corner';
@@ -3781,121 +4003,44 @@ fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3781
4003
  $._rectMode = og;
3782
4004
  }
3783
4005
  $.pop();
4006
+ if (!$._fillSet) $._fillIndex = 1;
3784
4007
  };
3785
4008
 
3786
- /**
3787
- * Derived from: ceil(Math.log(d) * 7) * 2 - ceil(28)
3788
- * This lookup table is used for better performance.
3789
- * @param {Number} d diameter of the circle
3790
- * @returns n number of segments
3791
- */
3792
- // prettier-ignore
3793
- const getArcSegments = (d) =>
3794
- d < 4 ? 6 :
3795
- d < 6 ? 8 :
3796
- d < 10 ? 10 :
3797
- d < 16 ? 12 :
3798
- d < 20 ? 14 :
3799
- d < 22 ? 16 :
3800
- d < 24 ? 18 :
3801
- d < 28 ? 20 :
3802
- d < 34 ? 22 :
3803
- d < 42 ? 24 :
3804
- d < 48 ? 26 :
3805
- d < 56 ? 28 :
3806
- d < 64 ? 30 :
3807
- d < 72 ? 32 :
3808
- d < 84 ? 34 :
3809
- d < 96 ? 36 :
3810
- d < 98 ? 38 :
3811
- d < 113 ? 40 :
3812
- d < 149 ? 44 :
3813
- d < 199 ? 48 :
3814
- d < 261 ? 52 :
3815
- d < 353 ? 56 :
3816
- d < 461 ? 60 :
3817
- d < 585 ? 64 :
3818
- d < 1200 ? 70 :
3819
- d < 1800 ? 80 :
3820
- d < 2400 ? 90 :
3821
- 100;
3822
-
3823
- $.ellipseMode = (x) => ($._ellipseMode = x);
3824
-
3825
- $.ellipse = (x, y, w, h) => {
3826
- const n = getArcSegments(w == h ? w : Math.max(w, h));
3827
-
3828
- let a = Math.max(w, 1) / 2;
3829
- let b = w == h ? a : Math.max(h, 1) / 2;
3830
-
3831
- let t = 0; // theta
3832
- const angleIncrement = $.TAU / n;
3833
- const ci = colorIndex ?? $._fillIndex;
3834
- if ($._matrixDirty) $._saveMatrix();
3835
- const ti = $._transformIndex;
3836
- let vx1, vy1, vx2, vy2;
3837
- for (let i = 0; i <= n; i++) {
3838
- vx1 = vx2;
3839
- vy1 = vy2;
3840
- vx2 = x + a * Math.cos(t);
3841
- vy2 = y + b * Math.sin(t);
3842
- t += angleIncrement;
3843
-
3844
- if (i == 0) continue;
3845
-
3846
- verticesStack.push(x, y, ci, ti, vx1, vy1, ci, ti, vx2, vy2, ci, ti);
3847
- }
3848
-
3849
- drawStack.push(0, n * 3);
3850
- };
3851
-
3852
- $.circle = (x, y, d) => $.ellipse(x, y, d, d);
3853
-
3854
4009
  $._hooks.preRender.push(() => {
3855
- $.pass.setPipeline($.pipelines[0]);
4010
+ $.pass.setPipeline($._pipelines[0]);
3856
4011
 
3857
- const vertices = new Float32Array(verticesStack);
3858
-
3859
- const vertexBuffer = Q5.device.createBuffer({
3860
- size: vertices.byteLength,
3861
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
4012
+ let vertexBuffer = Q5.device.createBuffer({
4013
+ size: vertIndex * 4,
4014
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
4015
+ mappedAtCreation: true
3862
4016
  });
3863
4017
 
3864
- Q5.device.queue.writeBuffer(vertexBuffer, 0, vertices);
4018
+ new Float32Array(vertexBuffer.getMappedRange()).set(vertexStack.slice(0, vertIndex));
4019
+ vertexBuffer.unmap();
4020
+
3865
4021
  $.pass.setVertexBuffer(0, vertexBuffer);
3866
4022
 
3867
- const colorsBuffer = Q5.device.createBuffer({
3868
- size: colorsStack.length * 4,
3869
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
4023
+ let indexBuffer = Q5.device.createBuffer({
4024
+ size: idxBufferIndex * 4,
4025
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
4026
+ mappedAtCreation: true
3870
4027
  });
3871
4028
 
3872
- Q5.device.queue.writeBuffer(colorsBuffer, 0, new Float32Array(colorsStack));
3873
-
3874
- $._colorsBindGroup = Q5.device.createBindGroup({
3875
- layout: colorsLayout,
3876
- entries: [
3877
- {
3878
- binding: 0,
3879
- resource: {
3880
- buffer: colorsBuffer,
3881
- offset: 0,
3882
- size: colorsStack.length * 4
3883
- }
3884
- }
3885
- ]
3886
- });
4029
+ new Uint32Array(indexBuffer.getMappedRange()).set(indexStack.slice(0, idxBufferIndex));
4030
+ indexBuffer.unmap();
3887
4031
 
3888
- // set the bind group once before rendering
3889
- $.pass.setBindGroup(1, $._colorsBindGroup);
4032
+ $.pass.setIndexBuffer(indexBuffer, 'uint32');
3890
4033
  });
3891
4034
 
3892
4035
  $._hooks.postRender.push(() => {
3893
- verticesStack.length = 0;
4036
+ vertIndex = 0;
4037
+ vertCount = 0;
4038
+ idxBufferIndex = 0;
3894
4039
  });
3895
4040
  };
3896
4041
  Q5.renderers.webgpu.image = ($, q) => {
3897
4042
  $._textureBindGroups = [];
3898
- let verticesStack = [];
4043
+ let vertexStack = [];
3899
4044
 
3900
4045
  let vertexShader = Q5.device.createShaderModule({
3901
4046
  label: 'imageVertexShader',
@@ -3903,15 +4048,14 @@ Q5.renderers.webgpu.image = ($, q) => {
3903
4048
  struct VertexOutput {
3904
4049
  @builtin(position) position: vec4f,
3905
4050
  @location(0) texCoord: vec2f
3906
- };
3907
-
4051
+ }
3908
4052
  struct Uniforms {
3909
4053
  halfWidth: f32,
3910
4054
  halfHeight: f32
3911
- };
4055
+ }
3912
4056
 
3913
4057
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3914
- @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
4058
+ @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
3915
4059
 
3916
4060
  @vertex
3917
4061
  fn vertexMain(@location(0) pos: vec2f, @location(1) texCoord: vec2f, @location(2) transformIndex: f32) -> VertexOutput {
@@ -3972,7 +4116,7 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
3972
4116
  bindGroupLayouts: [...$.bindGroupLayouts, textureLayout]
3973
4117
  });
3974
4118
 
3975
- $.pipelines[1] = Q5.device.createRenderPipeline({
4119
+ $._pipelineConfigs[1] = {
3976
4120
  label: 'imagePipeline',
3977
4121
  layout: pipelineLayout,
3978
4122
  vertex: {
@@ -3983,28 +4127,12 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
3983
4127
  fragment: {
3984
4128
  module: fragmentShader,
3985
4129
  entryPoint: 'fragmentMain',
3986
- targets: [
3987
- {
3988
- format: 'bgra8unorm',
3989
- blend: $.blendConfigs?.normal || {
3990
- color: {
3991
- srcFactor: 'src-alpha',
3992
- dstFactor: 'one-minus-src-alpha',
3993
- operation: 'add'
3994
- },
3995
- alpha: {
3996
- srcFactor: 'src-alpha',
3997
- dstFactor: 'one-minus-src-alpha',
3998
- operation: 'add'
3999
- }
4000
- }
4001
- }
4002
- ]
4130
+ targets: [{ format: 'bgra8unorm', blend: $.blendConfigs.normal }]
4003
4131
  },
4004
- primitive: {
4005
- topology: 'triangle-list'
4006
- }
4007
- });
4132
+ primitive: { topology: 'triangle-list' }
4133
+ };
4134
+
4135
+ $._pipelines[1] = Q5.device.createRenderPipeline($._pipelineConfigs[1]);
4008
4136
 
4009
4137
  let sampler = Q5.device.createSampler({
4010
4138
  magFilter: 'linear',
@@ -4029,7 +4157,11 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4029
4157
 
4030
4158
  Q5.device.queue.copyExternalImageToTexture(
4031
4159
  { source: img },
4032
- { texture, colorSpace: $.canvas.colorSpace },
4160
+ {
4161
+ texture,
4162
+ colorSpace: $.canvas.colorSpace
4163
+ // premultipliedAlpha: true
4164
+ },
4033
4165
  textureSize
4034
4166
  );
4035
4167
 
@@ -4082,7 +4214,7 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4082
4214
  let [l, r, t, b] = $._calcBox(x, y, w, h, $._imageMode);
4083
4215
 
4084
4216
  // prettier-ignore
4085
- verticesStack.push(
4217
+ vertexStack.push(
4086
4218
  l, t, 0, 0, ti,
4087
4219
  r, t, 1, 0, ti,
4088
4220
  l, b, 0, 1, ti,
@@ -4091,29 +4223,29 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4091
4223
  r, b, 1, 1, ti
4092
4224
  );
4093
4225
 
4094
- $.drawStack.push(1, img.textureIndex);
4226
+ $.drawStack.push(1, img.textureIndex, 0);
4095
4227
  };
4096
4228
 
4097
4229
  $._hooks.preRender.push(() => {
4098
4230
  if (!$._textureBindGroups.length) return;
4099
4231
 
4100
4232
  // Switch to image pipeline
4101
- $.pass.setPipeline($.pipelines[1]);
4102
-
4103
- // Create a vertex buffer for the image quads
4104
- const vertices = new Float32Array(verticesStack);
4233
+ $.pass.setPipeline($._pipelines[1]);
4105
4234
 
4106
4235
  const vertexBuffer = Q5.device.createBuffer({
4107
- size: vertices.byteLength,
4108
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
4236
+ size: vertexStack.length * 4,
4237
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
4238
+ mappedAtCreation: true
4109
4239
  });
4110
4240
 
4111
- Q5.device.queue.writeBuffer(vertexBuffer, 0, vertices);
4241
+ new Float32Array(vertexBuffer.getMappedRange()).set(vertices);
4242
+ vertexBuffer.unmap();
4243
+
4112
4244
  $.pass.setVertexBuffer(1, vertexBuffer);
4113
4245
  });
4114
4246
 
4115
4247
  $._hooks.postRender.push(() => {
4116
- verticesStack.length = 0;
4248
+ vertexStack.length = 0;
4117
4249
  });
4118
4250
  };
4119
4251
 
@@ -4134,35 +4266,35 @@ const pos = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0));
4134
4266
 
4135
4267
  struct VertexInput {
4136
4268
  @builtin(vertex_index) vertex : u32,
4137
- @builtin(instance_index) instance : u32,
4138
- };
4269
+ @builtin(instance_index) instance : u32
4270
+ }
4139
4271
  struct VertexOutput {
4140
4272
  @builtin(position) position : vec4f,
4141
- @location(0) texcoord : vec2f,
4142
- @location(1) colorIndex : f32
4143
- };
4273
+ @location(0) texCoord : vec2f,
4274
+ @location(1) fillColor : vec4f
4275
+ }
4144
4276
  struct Char {
4145
4277
  texOffset: vec2f,
4146
4278
  texExtent: vec2f,
4147
4279
  size: vec2f,
4148
4280
  offset: vec2f,
4149
- };
4281
+ }
4150
4282
  struct Text {
4151
4283
  pos: vec2f,
4152
4284
  scale: f32,
4153
4285
  transformIndex: f32,
4154
4286
  fillIndex: f32,
4155
4287
  strokeIndex: f32
4156
- };
4288
+ }
4157
4289
  struct Uniforms {
4158
4290
  halfWidth: f32,
4159
4291
  halfHeight: f32
4160
- };
4292
+ }
4161
4293
 
4162
4294
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
4163
- @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
4295
+ @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
4164
4296
 
4165
- @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
4297
+ @group(1) @binding(0) var<storage> colors : array<vec4f>;
4166
4298
 
4167
4299
  @group(2) @binding(0) var fontTexture: texture_2d<f32>;
4168
4300
  @group(2) @binding(1) var fontSampler: sampler;
@@ -4188,13 +4320,13 @@ fn vertexMain(input : VertexInput) -> VertexOutput {
4188
4320
 
4189
4321
  var output : VertexOutput;
4190
4322
  output.position = vert;
4191
- output.texcoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;
4192
- output.colorIndex = text.fillIndex;
4323
+ output.texCoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;
4324
+ output.fillColor = colors[i32(text.fillIndex)];
4193
4325
  return output;
4194
4326
  }
4195
4327
 
4196
- fn sampleMsdf(texcoord: vec2f) -> f32 {
4197
- let c = textureSample(fontTexture, fontSampler, texcoord);
4328
+ fn sampleMsdf(texCoord: vec2f) -> f32 {
4329
+ let c = textureSample(fontTexture, fontSampler, texCoord);
4198
4330
  return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
4199
4331
  }
4200
4332
 
@@ -4204,25 +4336,83 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4204
4336
  // uses the default which is 4.
4205
4337
  let pxRange = 4.0;
4206
4338
  let sz = vec2f(textureDimensions(fontTexture, 0));
4207
- let dx = sz.x*length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));
4208
- let dy = sz.y*length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));
4339
+ let dx = sz.x*length(vec2f(dpdxFine(input.texCoord.x), dpdyFine(input.texCoord.x)));
4340
+ let dy = sz.y*length(vec2f(dpdxFine(input.texCoord.y), dpdyFine(input.texCoord.y)));
4209
4341
  let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
4210
- let sigDist = sampleMsdf(input.texcoord) - 0.5;
4342
+ let sigDist = sampleMsdf(input.texCoord) - 0.5;
4211
4343
  let pxDist = sigDist * toPixels;
4212
4344
  let edgeWidth = 0.5;
4213
4345
  let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
4214
4346
  if (alpha < 0.001) {
4215
4347
  discard;
4216
4348
  }
4217
- let fillColor = colors[i32(input.colorIndex)];
4218
- return vec4f(fillColor.rgb, fillColor.a * alpha);
4349
+ return vec4f(input.fillColor.rgb, input.fillColor.a * alpha);
4219
4350
  }
4220
4351
  `
4221
4352
  });
4222
4353
 
4354
+ let textBindGroupLayout = Q5.device.createBindGroupLayout({
4355
+ label: 'MSDF text group layout',
4356
+ entries: [
4357
+ {
4358
+ binding: 0,
4359
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4360
+ buffer: { type: 'read-only-storage' }
4361
+ },
4362
+ {
4363
+ binding: 1,
4364
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4365
+ buffer: { type: 'read-only-storage' }
4366
+ }
4367
+ ]
4368
+ });
4369
+
4370
+ let fontSampler = Q5.device.createSampler({
4371
+ minFilter: 'linear',
4372
+ magFilter: 'linear',
4373
+ mipmapFilter: 'linear',
4374
+ maxAnisotropy: 16
4375
+ });
4376
+ let fontBindGroupLayout = Q5.device.createBindGroupLayout({
4377
+ label: 'MSDF font group layout',
4378
+ entries: [
4379
+ {
4380
+ binding: 0,
4381
+ visibility: GPUShaderStage.FRAGMENT,
4382
+ texture: {}
4383
+ },
4384
+ {
4385
+ binding: 1,
4386
+ visibility: GPUShaderStage.FRAGMENT,
4387
+ sampler: {}
4388
+ },
4389
+ {
4390
+ binding: 2,
4391
+ visibility: GPUShaderStage.VERTEX,
4392
+ buffer: { type: 'read-only-storage' }
4393
+ }
4394
+ ]
4395
+ });
4396
+
4397
+ let fontPipelineLayout = Q5.device.createPipelineLayout({
4398
+ bindGroupLayouts: [...$.bindGroupLayouts, fontBindGroupLayout, textBindGroupLayout]
4399
+ });
4400
+
4401
+ $._pipelineConfigs[2] = {
4402
+ label: 'msdf font pipeline',
4403
+ layout: fontPipelineLayout,
4404
+ vertex: { module: textShader, entryPoint: 'vertexMain' },
4405
+ fragment: {
4406
+ module: textShader,
4407
+ entryPoint: 'fragmentMain',
4408
+ targets: [{ format: 'bgra8unorm', blend: $.blendConfigs.normal }]
4409
+ },
4410
+ primitive: { topology: 'triangle-strip', stripIndexFormat: 'uint32' }
4411
+ };
4412
+ $._pipelines[2] = Q5.device.createRenderPipeline($._pipelineConfigs[2]);
4413
+
4223
4414
  class MsdfFont {
4224
- constructor(pipeline, bindGroup, lineHeight, chars, kernings) {
4225
- this.pipeline = pipeline;
4415
+ constructor(bindGroup, lineHeight, chars, kernings) {
4226
4416
  this.bindGroup = bindGroup;
4227
4417
  this.lineHeight = lineHeight;
4228
4418
  this.chars = chars;
@@ -4248,22 +4438,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4248
4438
  }
4249
4439
  }
4250
4440
 
4251
- let textBindGroupLayout = Q5.device.createBindGroupLayout({
4252
- label: 'MSDF text group layout',
4253
- entries: [
4254
- {
4255
- binding: 0,
4256
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4257
- buffer: { type: 'read-only-storage' }
4258
- },
4259
- {
4260
- binding: 1,
4261
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4262
- buffer: { type: 'read-only-storage' }
4263
- }
4264
- ]
4265
- });
4266
-
4441
+ $._fonts = [];
4267
4442
  let fonts = {};
4268
4443
 
4269
4444
  let createFont = async (fontJsonUrl, fontName, cb) => {
@@ -4326,74 +4501,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4326
4501
  }
4327
4502
  charsBuffer.unmap();
4328
4503
 
4329
- let fontSampler = Q5.device.createSampler({
4330
- minFilter: 'linear',
4331
- magFilter: 'linear',
4332
- mipmapFilter: 'linear',
4333
- maxAnisotropy: 16
4334
- });
4335
- let fontBindGroupLayout = Q5.device.createBindGroupLayout({
4336
- label: 'MSDF font group layout',
4337
- entries: [
4338
- {
4339
- binding: 0,
4340
- visibility: GPUShaderStage.FRAGMENT,
4341
- texture: {}
4342
- },
4343
- {
4344
- binding: 1,
4345
- visibility: GPUShaderStage.FRAGMENT,
4346
- sampler: {}
4347
- },
4348
- {
4349
- binding: 2,
4350
- visibility: GPUShaderStage.VERTEX,
4351
- buffer: { type: 'read-only-storage' }
4352
- }
4353
- ]
4354
- });
4355
- let fontPipeline = Q5.device.createRenderPipeline({
4356
- label: 'msdf font pipeline',
4357
- layout: Q5.device.createPipelineLayout({
4358
- bindGroupLayouts: [...$.bindGroupLayouts, fontBindGroupLayout, textBindGroupLayout]
4359
- }),
4360
- vertex: {
4361
- module: textShader,
4362
- entryPoint: 'vertexMain'
4363
- },
4364
- fragment: {
4365
- module: textShader,
4366
- entryPoint: 'fragmentMain',
4367
- targets: [
4368
- {
4369
- format: 'bgra8unorm',
4370
- blend: {
4371
- color: {
4372
- srcFactor: 'src-alpha',
4373
- dstFactor: 'one-minus-src-alpha'
4374
- },
4375
- alpha: {
4376
- srcFactor: 'one',
4377
- dstFactor: 'one'
4378
- }
4379
- }
4380
- }
4381
- ]
4382
- },
4383
- primitive: {
4384
- topology: 'triangle-strip',
4385
- stripIndexFormat: 'uint32'
4386
- }
4387
- });
4388
-
4389
4504
  let fontBindGroup = Q5.device.createBindGroup({
4390
4505
  label: 'msdf font bind group',
4391
4506
  layout: fontBindGroupLayout,
4392
4507
  entries: [
4393
- {
4394
- binding: 0,
4395
- resource: texture.createView()
4396
- },
4508
+ { binding: 0, resource: texture.createView() },
4397
4509
  { binding: 1, resource: fontSampler },
4398
4510
  { binding: 2, resource: { buffer: charsBuffer } }
4399
4511
  ]
@@ -4411,10 +4523,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4411
4523
  }
4412
4524
  }
4413
4525
 
4414
- $._font = new MsdfFont(fontPipeline, fontBindGroup, atlas.common.lineHeight, chars, kernings);
4526
+ $._font = new MsdfFont(fontBindGroup, atlas.common.lineHeight, chars, kernings);
4415
4527
 
4528
+ $._font.index = $._fonts.length;
4529
+ $._fonts.push($._font);
4416
4530
  fonts[fontName] = $._font;
4417
- $.pipelines[2] = $._font.pipeline;
4418
4531
 
4419
4532
  q._preloadCount--;
4420
4533
 
@@ -4443,12 +4556,6 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4443
4556
 
4444
4557
  $.textFont = (fontName) => {
4445
4558
  $._font = fonts[fontName];
4446
-
4447
- // replay the change of font in the draw stack
4448
- $.drawStack.push(-1, () => {
4449
- $._font = fonts[fontName];
4450
- $.pipelines[2] = $._font.pipeline;
4451
- });
4452
4559
  };
4453
4560
  $.textSize = (size) => {
4454
4561
  $._textSize = size;
@@ -4561,7 +4668,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4561
4668
  }
4562
4669
  }
4563
4670
 
4564
- let charsData = new Float32Array((str.length - spaces) * 4);
4671
+ let charsData = [];
4565
4672
 
4566
4673
  let ta = $._textAlign,
4567
4674
  tb = $._textBaseline,
@@ -4607,7 +4714,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4607
4714
  }
4608
4715
  $._charStack.push(charsData);
4609
4716
 
4610
- let text = new Float32Array(6);
4717
+ let text = [];
4611
4718
 
4612
4719
  if ($._matrixDirty) $._saveMatrix();
4613
4720
 
@@ -4615,11 +4722,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4615
4722
  text[1] = -y;
4616
4723
  text[2] = $._textSize / 44;
4617
4724
  text[3] = $._transformIndex;
4618
- text[4] = $._fillIndex;
4725
+ text[4] = $._fillSet ? $._fillIndex : 0;
4619
4726
  text[5] = $._strokeIndex;
4620
4727
 
4621
4728
  $._textStack.push(text);
4622
- $.drawStack.push(2, measurements.printedCharCount);
4729
+ $.drawStack.push(2, measurements.printedCharCount, $._font.index);
4623
4730
  };
4624
4731
 
4625
4732
  $.textWidth = (str) => {
@@ -4632,11 +4739,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4632
4739
 
4633
4740
  if ($._doFill) {
4634
4741
  let fi = $._fillIndex * 4;
4635
- g.fill(colorsStack.slice(fi, fi + 4));
4742
+ g.fill(colorStack.slice(fi, fi + 4));
4636
4743
  }
4637
4744
  if ($._doStroke) {
4638
4745
  let si = $._strokeIndex * 4;
4639
- g.stroke(colorsStack.slice(si, si + 4));
4746
+ g.stroke(colorStack.slice(si, si + 4));
4640
4747
  }
4641
4748
 
4642
4749
  let img = g.createTextImage(str, w, h);
@@ -4687,21 +4794,15 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4687
4794
  totalTextSize += charsData.length * 4;
4688
4795
  }
4689
4796
 
4690
- // Create a single buffer for all text data
4797
+ // Create a single buffer for all char data
4691
4798
  let charBuffer = Q5.device.createBuffer({
4692
- label: 'charBuffer',
4693
4799
  size: totalTextSize,
4694
4800
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4695
4801
  mappedAtCreation: true
4696
4802
  });
4697
4803
 
4698
4804
  // Copy all text data into the buffer
4699
- let textArray = new Float32Array(charBuffer.getMappedRange());
4700
- let o = 0;
4701
- for (let array of $._charStack) {
4702
- textArray.set(array, o);
4703
- o += array.length;
4704
- }
4805
+ new Float32Array(charBuffer.getMappedRange()).set($._charStack.flat());
4705
4806
  charBuffer.unmap();
4706
4807
 
4707
4808
  // Calculate total buffer size for metadata
@@ -4716,12 +4817,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4716
4817
  });
4717
4818
 
4718
4819
  // Copy all metadata into the buffer
4719
- let metadataArray = new Float32Array(textBuffer.getMappedRange());
4720
- o = 0;
4721
- for (let array of $._textStack) {
4722
- metadataArray.set(array, o);
4723
- o += array.length;
4724
- }
4820
+ new Float32Array(textBuffer.getMappedRange()).set($._textStack.flat());
4725
4821
  textBuffer.unmap();
4726
4822
 
4727
4823
  // Create a single bind group for the text buffer and metadata buffer
@@ -4729,14 +4825,8 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4729
4825
  label: 'msdf text bind group',
4730
4826
  layout: textBindGroupLayout,
4731
4827
  entries: [
4732
- {
4733
- binding: 0,
4734
- resource: { buffer: charBuffer }
4735
- },
4736
- {
4737
- binding: 1,
4738
- resource: { buffer: textBuffer }
4739
- }
4828
+ { binding: 0, resource: { buffer: charBuffer } },
4829
+ { binding: 1, resource: { buffer: textBuffer } }
4740
4830
  ]
4741
4831
  });
4742
4832
  });