q5 2.5.5 → 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);
@@ -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
 
@@ -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,12 +3584,13 @@ 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
3595
  transformStates.length = 1;
3466
3596
  $._transformIndexStack.length = 0;
@@ -3491,41 +3621,47 @@ Q5.webgpu = async function (scope, parent) {
3491
3621
  return new Q5(scope, parent, 'webgpu');
3492
3622
  };
3493
3623
  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;
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;
3502
3632
 
3503
3633
  let vertexShader = Q5.device.createShaderModule({
3504
3634
  label: 'drawingVertexShader',
3505
3635
  code: `
3636
+ struct VertexInput {
3637
+ @location(0) pos: vec2f,
3638
+ @location(1) colorIndex: f32,
3639
+ @location(2) transformIndex: f32
3640
+ }
3506
3641
  struct VertexOutput {
3507
3642
  @builtin(position) position: vec4f,
3508
- @location(0) colorIndex: f32
3509
- };
3510
-
3643
+ @location(0) color: vec4f
3644
+ }
3511
3645
  struct Uniforms {
3512
3646
  halfWidth: f32,
3513
3647
  halfHeight: f32
3514
- };
3648
+ }
3515
3649
 
3516
3650
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3517
- @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>;
3518
3654
 
3519
3655
  @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;
3656
+ fn vertexMain(input: VertexInput) -> VertexOutput {
3657
+ var vert = vec4f(input.pos, 0.0, 1.0);
3658
+ vert = transforms[i32(input.transformIndex)] * vert;
3523
3659
  vert.x /= uniforms.halfWidth;
3524
3660
  vert.y /= uniforms.halfHeight;
3525
3661
 
3526
3662
  var output: VertexOutput;
3527
3663
  output.position = vert;
3528
- output.colorIndex = colorIndex;
3664
+ output.color = colors[i32(input.colorIndex)];
3529
3665
  return output;
3530
3666
  }
3531
3667
  `
@@ -3534,32 +3670,13 @@ fn vertexMain(@location(0) pos: vec2f, @location(1) colorIndex: f32, @location(2
3534
3670
  let fragmentShader = Q5.device.createShaderModule({
3535
3671
  label: 'drawingFragmentShader',
3536
3672
  code: `
3537
- @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
3538
-
3539
3673
  @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));
3674
+ fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
3675
+ return color;
3543
3676
  }
3544
3677
  `
3545
3678
  });
3546
3679
 
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
3680
  let vertexBufferLayout = {
3564
3681
  arrayStride: 16, // 2 coordinates + 1 color index + 1 transform index * 4 bytes each
3565
3682
  attributes: [
@@ -3569,193 +3686,212 @@ fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3569
3686
  ]
3570
3687
  };
3571
3688
 
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
3689
  let pipelineLayout = Q5.device.createPipelineLayout({
3640
3690
  label: 'drawingPipelineLayout',
3641
3691
  bindGroupLayouts: $.bindGroupLayouts
3642
3692
  });
3643
3693
 
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
- });
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);
3660
3793
  };
3661
3794
 
3662
- $.pipelines[0] = $._createPipeline($.blendConfigs.normal);
3795
+ $.rectMode = (x) => ($._rectMode = x);
3663
3796
 
3664
- 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;
3665
3802
 
3666
- $.beginShape = () => {
3667
- shapeVertices = [];
3668
- };
3803
+ if ($._doStroke) {
3804
+ ci = $._strokeIndex;
3669
3805
 
3670
- $.vertex = (x, y) => {
3671
- if ($._matrixDirty) $._saveMatrix();
3672
- shapeVertices.push(x, -y, $._fillIndex, $._transformIndex);
3673
- };
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;
3674
3812
 
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
- );
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;
3705
3821
  }
3706
- shapeVertices = [];
3707
3822
 
3708
- verticesStack.push(...triangles);
3709
- drawStack.push(0, triangles.length / 4);
3710
- };
3823
+ if ($._doFill) {
3824
+ ci = colorIndex ?? $._fillIndex;
3711
3825
 
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);
3826
+ // two triangles make a rectangle
3827
+ addQuad(l, t, r, t, r, b, l, b, ci, ti);
3828
+ }
3718
3829
  };
3719
3830
 
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
- };
3831
+ $.square = (x, y, s) => $.rect(x, y, s, s);
3728
3832
 
3729
- $.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;
3730
3863
 
3731
- $.rect = (x, y, w, h) => {
3732
- let [l, r, t, b] = $._calcBox(x, y, w, h, $._rectMode);
3864
+ $.ellipseMode = (x) => ($._ellipseMode = x);
3733
3865
 
3734
- 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;
3735
3871
  if ($._matrixDirty) $._saveMatrix();
3736
3872
  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);
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
+ }
3748
3882
  };
3749
3883
 
3750
- $.square = (x, y, s) => $.rect(x, y, s, s);
3884
+ $.circle = (x, y, d) => $.ellipse(x, y, d, d);
3751
3885
 
3752
3886
  $.point = (x, y) => {
3753
3887
  colorIndex = $._strokeIndex;
3888
+ $._doStroke = false;
3754
3889
  let sw = $._strokeWeight;
3755
3890
  if (sw < 2) {
3756
3891
  sw = Math.round(sw);
3757
3892
  $.rect(x, y, sw, sw);
3758
3893
  } else $.ellipse(x, y, sw, sw);
3894
+ $._doStroke = true;
3759
3895
  colorIndex = null;
3760
3896
  };
3761
3897
 
@@ -3763,19 +3899,97 @@ fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3763
3899
  colorIndex = $._strokeIndex;
3764
3900
 
3765
3901
  $.push();
3766
- $.translate(x1, y1);
3902
+ $._doStroke = false;
3903
+ $.translate(x1, -y1);
3767
3904
  $.rotate($.atan2(y2 - y1, x2 - x1));
3768
3905
  let length = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
3769
- let sw = $._strokeWeight;
3770
- $.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
+
3771
3918
  $.pop();
3772
3919
 
3773
3920
  colorIndex = null;
3774
3921
  };
3775
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
+
3776
3989
  $.background = (r, g, b, a) => {
3777
3990
  $.push();
3778
3991
  $.resetMatrix();
3992
+ $._doStroke = false;
3779
3993
  if (r.src) {
3780
3994
  let og = $._imageMode;
3781
3995
  $._imageMode = 'corner';
@@ -3789,121 +4003,44 @@ fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3789
4003
  $._rectMode = og;
3790
4004
  }
3791
4005
  $.pop();
4006
+ if (!$._fillSet) $._fillIndex = 1;
3792
4007
  };
3793
4008
 
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
4009
  $._hooks.preRender.push(() => {
3863
- $.pass.setPipeline($.pipelines[0]);
3864
-
3865
- const vertices = new Float32Array(verticesStack);
4010
+ $.pass.setPipeline($._pipelines[0]);
3866
4011
 
3867
- const vertexBuffer = Q5.device.createBuffer({
3868
- size: vertices.byteLength,
3869
- 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
3870
4016
  });
3871
4017
 
3872
- Q5.device.queue.writeBuffer(vertexBuffer, 0, vertices);
4018
+ new Float32Array(vertexBuffer.getMappedRange()).set(vertexStack.slice(0, vertIndex));
4019
+ vertexBuffer.unmap();
4020
+
3873
4021
  $.pass.setVertexBuffer(0, vertexBuffer);
3874
4022
 
3875
- const colorsBuffer = Q5.device.createBuffer({
3876
- size: colorsStack.length * 4,
3877
- 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
3878
4027
  });
3879
4028
 
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
- });
4029
+ new Uint32Array(indexBuffer.getMappedRange()).set(indexStack.slice(0, idxBufferIndex));
4030
+ indexBuffer.unmap();
3895
4031
 
3896
- // set the bind group once before rendering
3897
- $.pass.setBindGroup(1, $._colorsBindGroup);
4032
+ $.pass.setIndexBuffer(indexBuffer, 'uint32');
3898
4033
  });
3899
4034
 
3900
4035
  $._hooks.postRender.push(() => {
3901
- verticesStack.length = 0;
4036
+ vertIndex = 0;
4037
+ vertCount = 0;
4038
+ idxBufferIndex = 0;
3902
4039
  });
3903
4040
  };
3904
4041
  Q5.renderers.webgpu.image = ($, q) => {
3905
4042
  $._textureBindGroups = [];
3906
- let verticesStack = [];
4043
+ let vertexStack = [];
3907
4044
 
3908
4045
  let vertexShader = Q5.device.createShaderModule({
3909
4046
  label: 'imageVertexShader',
@@ -3911,15 +4048,14 @@ Q5.renderers.webgpu.image = ($, q) => {
3911
4048
  struct VertexOutput {
3912
4049
  @builtin(position) position: vec4f,
3913
4050
  @location(0) texCoord: vec2f
3914
- };
3915
-
4051
+ }
3916
4052
  struct Uniforms {
3917
4053
  halfWidth: f32,
3918
4054
  halfHeight: f32
3919
- };
4055
+ }
3920
4056
 
3921
4057
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3922
- @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
4058
+ @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
3923
4059
 
3924
4060
  @vertex
3925
4061
  fn vertexMain(@location(0) pos: vec2f, @location(1) texCoord: vec2f, @location(2) transformIndex: f32) -> VertexOutput {
@@ -3980,7 +4116,7 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
3980
4116
  bindGroupLayouts: [...$.bindGroupLayouts, textureLayout]
3981
4117
  });
3982
4118
 
3983
- $.pipelines[1] = Q5.device.createRenderPipeline({
4119
+ $._pipelineConfigs[1] = {
3984
4120
  label: 'imagePipeline',
3985
4121
  layout: pipelineLayout,
3986
4122
  vertex: {
@@ -3991,28 +4127,12 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
3991
4127
  fragment: {
3992
4128
  module: fragmentShader,
3993
4129
  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
- ]
4130
+ targets: [{ format: 'bgra8unorm', blend: $.blendConfigs.normal }]
4011
4131
  },
4012
- primitive: {
4013
- topology: 'triangle-list'
4014
- }
4015
- });
4132
+ primitive: { topology: 'triangle-list' }
4133
+ };
4134
+
4135
+ $._pipelines[1] = Q5.device.createRenderPipeline($._pipelineConfigs[1]);
4016
4136
 
4017
4137
  let sampler = Q5.device.createSampler({
4018
4138
  magFilter: 'linear',
@@ -4037,7 +4157,11 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4037
4157
 
4038
4158
  Q5.device.queue.copyExternalImageToTexture(
4039
4159
  { source: img },
4040
- { texture, colorSpace: $.canvas.colorSpace },
4160
+ {
4161
+ texture,
4162
+ colorSpace: $.canvas.colorSpace
4163
+ // premultipliedAlpha: true
4164
+ },
4041
4165
  textureSize
4042
4166
  );
4043
4167
 
@@ -4090,7 +4214,7 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4090
4214
  let [l, r, t, b] = $._calcBox(x, y, w, h, $._imageMode);
4091
4215
 
4092
4216
  // prettier-ignore
4093
- verticesStack.push(
4217
+ vertexStack.push(
4094
4218
  l, t, 0, 0, ti,
4095
4219
  r, t, 1, 0, ti,
4096
4220
  l, b, 0, 1, ti,
@@ -4099,29 +4223,29 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4099
4223
  r, b, 1, 1, ti
4100
4224
  );
4101
4225
 
4102
- $.drawStack.push(1, img.textureIndex);
4226
+ $.drawStack.push(1, img.textureIndex, 0);
4103
4227
  };
4104
4228
 
4105
4229
  $._hooks.preRender.push(() => {
4106
4230
  if (!$._textureBindGroups.length) return;
4107
4231
 
4108
4232
  // 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);
4233
+ $.pass.setPipeline($._pipelines[1]);
4113
4234
 
4114
4235
  const vertexBuffer = Q5.device.createBuffer({
4115
- size: vertices.byteLength,
4116
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
4236
+ size: vertexStack.length * 4,
4237
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
4238
+ mappedAtCreation: true
4117
4239
  });
4118
4240
 
4119
- Q5.device.queue.writeBuffer(vertexBuffer, 0, vertices);
4241
+ new Float32Array(vertexBuffer.getMappedRange()).set(vertices);
4242
+ vertexBuffer.unmap();
4243
+
4120
4244
  $.pass.setVertexBuffer(1, vertexBuffer);
4121
4245
  });
4122
4246
 
4123
4247
  $._hooks.postRender.push(() => {
4124
- verticesStack.length = 0;
4248
+ vertexStack.length = 0;
4125
4249
  });
4126
4250
  };
4127
4251
 
@@ -4142,35 +4266,35 @@ const pos = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0));
4142
4266
 
4143
4267
  struct VertexInput {
4144
4268
  @builtin(vertex_index) vertex : u32,
4145
- @builtin(instance_index) instance : u32,
4146
- };
4269
+ @builtin(instance_index) instance : u32
4270
+ }
4147
4271
  struct VertexOutput {
4148
4272
  @builtin(position) position : vec4f,
4149
- @location(0) texcoord : vec2f,
4150
- @location(1) colorIndex : f32
4151
- };
4273
+ @location(0) texCoord : vec2f,
4274
+ @location(1) fillColor : vec4f
4275
+ }
4152
4276
  struct Char {
4153
4277
  texOffset: vec2f,
4154
4278
  texExtent: vec2f,
4155
4279
  size: vec2f,
4156
4280
  offset: vec2f,
4157
- };
4281
+ }
4158
4282
  struct Text {
4159
4283
  pos: vec2f,
4160
4284
  scale: f32,
4161
4285
  transformIndex: f32,
4162
4286
  fillIndex: f32,
4163
4287
  strokeIndex: f32
4164
- };
4288
+ }
4165
4289
  struct Uniforms {
4166
4290
  halfWidth: f32,
4167
4291
  halfHeight: f32
4168
- };
4292
+ }
4169
4293
 
4170
4294
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
4171
- @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
4295
+ @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
4172
4296
 
4173
- @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
4297
+ @group(1) @binding(0) var<storage> colors : array<vec4f>;
4174
4298
 
4175
4299
  @group(2) @binding(0) var fontTexture: texture_2d<f32>;
4176
4300
  @group(2) @binding(1) var fontSampler: sampler;
@@ -4196,13 +4320,13 @@ fn vertexMain(input : VertexInput) -> VertexOutput {
4196
4320
 
4197
4321
  var output : VertexOutput;
4198
4322
  output.position = vert;
4199
- output.texcoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;
4200
- output.colorIndex = text.fillIndex;
4323
+ output.texCoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;
4324
+ output.fillColor = colors[i32(text.fillIndex)];
4201
4325
  return output;
4202
4326
  }
4203
4327
 
4204
- fn sampleMsdf(texcoord: vec2f) -> f32 {
4205
- let c = textureSample(fontTexture, fontSampler, texcoord);
4328
+ fn sampleMsdf(texCoord: vec2f) -> f32 {
4329
+ let c = textureSample(fontTexture, fontSampler, texCoord);
4206
4330
  return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
4207
4331
  }
4208
4332
 
@@ -4212,25 +4336,83 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4212
4336
  // uses the default which is 4.
4213
4337
  let pxRange = 4.0;
4214
4338
  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)));
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)));
4217
4341
  let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
4218
- let sigDist = sampleMsdf(input.texcoord) - 0.5;
4342
+ let sigDist = sampleMsdf(input.texCoord) - 0.5;
4219
4343
  let pxDist = sigDist * toPixels;
4220
4344
  let edgeWidth = 0.5;
4221
4345
  let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
4222
4346
  if (alpha < 0.001) {
4223
4347
  discard;
4224
4348
  }
4225
- let fillColor = colors[i32(input.colorIndex)];
4226
- return vec4f(fillColor.rgb, fillColor.a * alpha);
4349
+ return vec4f(input.fillColor.rgb, input.fillColor.a * alpha);
4227
4350
  }
4228
4351
  `
4229
4352
  });
4230
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
+
4231
4414
  class MsdfFont {
4232
- constructor(pipeline, bindGroup, lineHeight, chars, kernings) {
4233
- this.pipeline = pipeline;
4415
+ constructor(bindGroup, lineHeight, chars, kernings) {
4234
4416
  this.bindGroup = bindGroup;
4235
4417
  this.lineHeight = lineHeight;
4236
4418
  this.chars = chars;
@@ -4256,22 +4438,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4256
4438
  }
4257
4439
  }
4258
4440
 
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
-
4441
+ $._fonts = [];
4275
4442
  let fonts = {};
4276
4443
 
4277
4444
  let createFont = async (fontJsonUrl, fontName, cb) => {
@@ -4334,74 +4501,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4334
4501
  }
4335
4502
  charsBuffer.unmap();
4336
4503
 
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
4504
  let fontBindGroup = Q5.device.createBindGroup({
4398
4505
  label: 'msdf font bind group',
4399
4506
  layout: fontBindGroupLayout,
4400
4507
  entries: [
4401
- {
4402
- binding: 0,
4403
- resource: texture.createView()
4404
- },
4508
+ { binding: 0, resource: texture.createView() },
4405
4509
  { binding: 1, resource: fontSampler },
4406
4510
  { binding: 2, resource: { buffer: charsBuffer } }
4407
4511
  ]
@@ -4419,10 +4523,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4419
4523
  }
4420
4524
  }
4421
4525
 
4422
- $._font = new MsdfFont(fontPipeline, fontBindGroup, atlas.common.lineHeight, chars, kernings);
4526
+ $._font = new MsdfFont(fontBindGroup, atlas.common.lineHeight, chars, kernings);
4423
4527
 
4528
+ $._font.index = $._fonts.length;
4529
+ $._fonts.push($._font);
4424
4530
  fonts[fontName] = $._font;
4425
- $.pipelines[2] = $._font.pipeline;
4426
4531
 
4427
4532
  q._preloadCount--;
4428
4533
 
@@ -4451,12 +4556,6 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4451
4556
 
4452
4557
  $.textFont = (fontName) => {
4453
4558
  $._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
4559
  };
4461
4560
  $.textSize = (size) => {
4462
4561
  $._textSize = size;
@@ -4569,7 +4668,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4569
4668
  }
4570
4669
  }
4571
4670
 
4572
- let charsData = new Float32Array((str.length - spaces) * 4);
4671
+ let charsData = [];
4573
4672
 
4574
4673
  let ta = $._textAlign,
4575
4674
  tb = $._textBaseline,
@@ -4615,7 +4714,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4615
4714
  }
4616
4715
  $._charStack.push(charsData);
4617
4716
 
4618
- let text = new Float32Array(6);
4717
+ let text = [];
4619
4718
 
4620
4719
  if ($._matrixDirty) $._saveMatrix();
4621
4720
 
@@ -4623,11 +4722,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4623
4722
  text[1] = -y;
4624
4723
  text[2] = $._textSize / 44;
4625
4724
  text[3] = $._transformIndex;
4626
- text[4] = $._fillIndex;
4725
+ text[4] = $._fillSet ? $._fillIndex : 0;
4627
4726
  text[5] = $._strokeIndex;
4628
4727
 
4629
4728
  $._textStack.push(text);
4630
- $.drawStack.push(2, measurements.printedCharCount);
4729
+ $.drawStack.push(2, measurements.printedCharCount, $._font.index);
4631
4730
  };
4632
4731
 
4633
4732
  $.textWidth = (str) => {
@@ -4640,11 +4739,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4640
4739
 
4641
4740
  if ($._doFill) {
4642
4741
  let fi = $._fillIndex * 4;
4643
- g.fill(colorsStack.slice(fi, fi + 4));
4742
+ g.fill(colorStack.slice(fi, fi + 4));
4644
4743
  }
4645
4744
  if ($._doStroke) {
4646
4745
  let si = $._strokeIndex * 4;
4647
- g.stroke(colorsStack.slice(si, si + 4));
4746
+ g.stroke(colorStack.slice(si, si + 4));
4648
4747
  }
4649
4748
 
4650
4749
  let img = g.createTextImage(str, w, h);
@@ -4695,21 +4794,15 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4695
4794
  totalTextSize += charsData.length * 4;
4696
4795
  }
4697
4796
 
4698
- // Create a single buffer for all text data
4797
+ // Create a single buffer for all char data
4699
4798
  let charBuffer = Q5.device.createBuffer({
4700
- label: 'charBuffer',
4701
4799
  size: totalTextSize,
4702
4800
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4703
4801
  mappedAtCreation: true
4704
4802
  });
4705
4803
 
4706
4804
  // 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
- }
4805
+ new Float32Array(charBuffer.getMappedRange()).set($._charStack.flat());
4713
4806
  charBuffer.unmap();
4714
4807
 
4715
4808
  // Calculate total buffer size for metadata
@@ -4724,12 +4817,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4724
4817
  });
4725
4818
 
4726
4819
  // 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
- }
4820
+ new Float32Array(textBuffer.getMappedRange()).set($._textStack.flat());
4733
4821
  textBuffer.unmap();
4734
4822
 
4735
4823
  // Create a single bind group for the text buffer and metadata buffer
@@ -4737,14 +4825,8 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4737
4825
  label: 'msdf text bind group',
4738
4826
  layout: textBindGroupLayout,
4739
4827
  entries: [
4740
- {
4741
- binding: 0,
4742
- resource: { buffer: charBuffer }
4743
- },
4744
- {
4745
- binding: 1,
4746
- resource: { buffer: textBuffer }
4747
- }
4828
+ { binding: 0, resource: { buffer: charBuffer } },
4829
+ { binding: 1, resource: { buffer: textBuffer } }
4748
4830
  ]
4749
4831
  });
4750
4832
  });