q5 2.12.12 → 2.13.2

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.12
3
+ * @version 2.13
4
4
  * @author quinton-ashley, Tezumie, and LingDong-
5
5
  * @license LGPL-3.0
6
6
  * @class Q5
@@ -843,6 +843,7 @@ Q5.renderers.q2d.canvas = ($, q) => {
843
843
  if ($.ctx) {
844
844
  $.ctx.resetTransform();
845
845
  $.scale($._pixelDensity);
846
+ if ($._webgpuFallback) $.translate($.canvas.hw, $.canvas.hh);
846
847
  }
847
848
  };
848
849
 
@@ -3594,11 +3595,13 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3594
3595
  c.width = $.width = 500;
3595
3596
  c.height = $.height = 500;
3596
3597
 
3598
+ // q2d graphics context
3599
+ $._g = $.createGraphics(1, 1);
3600
+
3597
3601
  if ($.colorMode) $.colorMode('rgb', 1);
3598
3602
 
3599
3603
  let pass,
3600
3604
  mainView,
3601
- colorsLayout,
3602
3605
  colorIndex = 1,
3603
3606
  colorStackIndex = 8;
3604
3607
 
@@ -3610,7 +3613,6 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3610
3613
  let drawStack = ($.drawStack = []);
3611
3614
 
3612
3615
  // colors used for each draw call
3613
-
3614
3616
  let colorStack = ($.colorStack = new Float32Array(1e6));
3615
3617
 
3616
3618
  // prettier-ignore
@@ -3619,43 +3621,28 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3619
3621
  1, 1, 1, 1 // white
3620
3622
  ]);
3621
3623
 
3622
- $._transformLayout = Q5.device.createBindGroupLayout({
3623
- label: 'transformLayout',
3624
+ let mainLayout = Q5.device.createBindGroupLayout({
3625
+ label: 'mainLayout',
3624
3626
  entries: [
3625
3627
  {
3626
3628
  binding: 0,
3627
3629
  visibility: GPUShaderStage.VERTEX,
3628
- buffer: {
3629
- type: 'uniform',
3630
- hasDynamicOffset: false
3631
- }
3630
+ buffer: { type: 'uniform' }
3632
3631
  },
3633
3632
  {
3634
3633
  binding: 1,
3635
3634
  visibility: GPUShaderStage.VERTEX,
3636
- buffer: {
3637
- type: 'read-only-storage',
3638
- hasDynamicOffset: false
3639
- }
3640
- }
3641
- ]
3642
- });
3643
-
3644
- colorsLayout = Q5.device.createBindGroupLayout({
3645
- label: 'colorsLayout',
3646
- entries: [
3635
+ buffer: { type: 'read-only-storage' }
3636
+ },
3647
3637
  {
3648
- binding: 0,
3638
+ binding: 2,
3649
3639
  visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
3650
- buffer: {
3651
- type: 'read-only-storage',
3652
- hasDynamicOffset: false
3653
- }
3640
+ buffer: { type: 'read-only-storage' }
3654
3641
  }
3655
3642
  ]
3656
3643
  });
3657
3644
 
3658
- $.bindGroupLayouts = [$._transformLayout, colorsLayout];
3645
+ $.bindGroupLayouts = [mainLayout];
3659
3646
 
3660
3647
  let uniformBuffer = Q5.device.createBuffer({
3661
3648
  size: 8, // Size of two floats
@@ -3685,7 +3672,6 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3685
3672
  Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([$.canvas.hw, $.canvas.hh]));
3686
3673
 
3687
3674
  createMainView();
3688
-
3689
3675
  return c;
3690
3676
  };
3691
3677
 
@@ -3729,7 +3715,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3729
3715
  };
3730
3716
 
3731
3717
  $._stroke = 0;
3732
- $._fill = 1;
3718
+ $._fill = $._tint = $._globalAlpha = 1;
3733
3719
  $._doFill = $._doStroke = true;
3734
3720
 
3735
3721
  $.fill = (r, g, b, a) => {
@@ -3742,42 +3728,53 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3742
3728
  $._doStroke = $._strokeSet = true;
3743
3729
  $._stroke = colorIndex;
3744
3730
  };
3731
+ $.tint = (r, g, b, a) => {
3732
+ addColor(r, g, b, a);
3733
+ $._tint = colorIndex;
3734
+ };
3735
+ $.opacity = (a) => ($._globalAlpha = a);
3745
3736
 
3746
3737
  $.noFill = () => ($._doFill = false);
3747
3738
  $.noStroke = () => ($._doStroke = false);
3739
+ $.noTint = () => ($._tint = 1);
3748
3740
 
3749
3741
  $._strokeWeight = 1;
3750
3742
  $.strokeWeight = (v) => ($._strokeWeight = Math.abs(v));
3751
3743
 
3752
- $.resetMatrix = () => {
3753
- // initialize the transformation matrix as 4x4 identity matrix
3754
-
3755
- // prettier-ignore
3756
- $._matrix = [
3757
- 1, 0, 0, 0,
3758
- 0, 1, 0, 0,
3759
- 0, 0, 1, 0,
3760
- 0, 0, 0, 1
3761
- ];
3762
- $._transformIndex = 0;
3763
- };
3764
- $.resetMatrix();
3744
+ const MAX_TRANSFORMS = 1e7, // or whatever maximum you need
3745
+ MATRIX_SIZE = 16, // 4x4 matrix
3746
+ transforms = new Float32Array(MAX_TRANSFORMS * MATRIX_SIZE),
3747
+ matrices = [],
3748
+ matricesIndexStack = [];
3749
+
3750
+ let matrix;
3765
3751
 
3766
3752
  // tracks if the matrix has been modified
3767
3753
  $._matrixDirty = false;
3768
3754
 
3769
- // array to store transformation matrices for the render pass
3770
- let transformStates = [$._matrix.slice()];
3755
+ // initialize with a 4x4 identity matrix
3756
+ // prettier-ignore
3757
+ matrices.push([
3758
+ 1, 0, 0, 0,
3759
+ 0, 1, 0, 0,
3760
+ 0, 0, 1, 0,
3761
+ 0, 0, 0, 1
3762
+ ]);
3771
3763
 
3772
- // stack to keep track of transformation matrix indexes
3773
- $._transformIndexStack = [];
3764
+ transforms.set(matrices[0]);
3765
+
3766
+ $.resetMatrix = () => {
3767
+ matrix = matrices[0].slice();
3768
+ $._matrixIndex = 0;
3769
+ };
3770
+ $.resetMatrix();
3774
3771
 
3775
3772
  $.translate = (x, y, z) => {
3776
3773
  if (!x && !y && !z) return;
3777
3774
  // update the translation values
3778
- $._matrix[12] += x;
3779
- $._matrix[13] -= y;
3780
- $._matrix[14] += z || 0;
3775
+ matrix[12] += x;
3776
+ matrix[13] -= y;
3777
+ matrix[14] += z || 0;
3781
3778
  $._matrixDirty = true;
3782
3779
  };
3783
3780
 
@@ -3785,12 +3782,10 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3785
3782
  if (!a) return;
3786
3783
  if ($._angleMode) a *= $._DEGTORAD;
3787
3784
 
3788
- let cosR = Math.cos(a);
3789
- let sinR = Math.sin(a);
3790
-
3791
- let m = $._matrix;
3792
-
3793
- let m0 = m[0],
3785
+ let cosR = Math.cos(a),
3786
+ sinR = Math.sin(a),
3787
+ m = matrix,
3788
+ m0 = m[0],
3794
3789
  m1 = m[1],
3795
3790
  m4 = m[4],
3796
3791
  m5 = m[5];
@@ -3815,7 +3810,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3815
3810
  $.scale = (x = 1, y, z = 1) => {
3816
3811
  y ??= x;
3817
3812
 
3818
- let m = $._matrix;
3813
+ let m = matrix;
3819
3814
 
3820
3815
  m[0] *= x;
3821
3816
  m[1] *= x;
@@ -3837,15 +3832,15 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3837
3832
  if (!ang) return;
3838
3833
  if ($._angleMode) ang *= $._DEGTORAD;
3839
3834
 
3840
- let tanAng = Math.tan(ang);
3841
-
3842
- let m0 = $._matrix[0],
3843
- m1 = $._matrix[1],
3844
- m4 = $._matrix[4],
3845
- m5 = $._matrix[5];
3835
+ let tanAng = Math.tan(ang),
3836
+ m = matrix,
3837
+ m0 = m[0],
3838
+ m1 = m[1],
3839
+ m4 = m[4],
3840
+ m5 = m[5];
3846
3841
 
3847
- $._matrix[0] = m0 + m4 * tanAng;
3848
- $._matrix[1] = m1 + m5 * tanAng;
3842
+ m[0] = m0 + m4 * tanAng;
3843
+ m[1] = m1 + m5 * tanAng;
3849
3844
 
3850
3845
  $._matrixDirty = true;
3851
3846
  };
@@ -3854,15 +3849,15 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3854
3849
  if (!ang) return;
3855
3850
  if ($._angleMode) ang *= $._DEGTORAD;
3856
3851
 
3857
- let tanAng = Math.tan(ang);
3858
-
3859
- let m0 = $._matrix[0],
3860
- m1 = $._matrix[1],
3861
- m4 = $._matrix[4],
3862
- m5 = $._matrix[5];
3852
+ let tanAng = Math.tan(ang),
3853
+ m = matrix,
3854
+ m0 = m[0],
3855
+ m1 = m[1],
3856
+ m4 = m[4],
3857
+ m5 = m[5];
3863
3858
 
3864
- $._matrix[4] = m4 + m0 * tanAng;
3865
- $._matrix[5] = m5 + m1 * tanAng;
3859
+ m[4] = m4 + m0 * tanAng;
3860
+ m[5] = m5 + m1 * tanAng;
3866
3861
 
3867
3862
  $._matrixDirty = true;
3868
3863
  };
@@ -3880,31 +3875,32 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3880
3875
  }
3881
3876
 
3882
3877
  // overwrite the current transformation matrix
3883
- $._matrix = m.slice();
3878
+ matrix = m.slice();
3884
3879
  $._matrixDirty = true;
3885
3880
  };
3886
3881
 
3887
3882
  // function to save the current matrix state if dirty
3888
3883
  $._saveMatrix = () => {
3889
- transformStates.push($._matrix.slice());
3890
- $._transformIndex = transformStates.length - 1;
3884
+ transforms.set(matrix, matrices.length * MATRIX_SIZE);
3885
+ $._matrixIndex = matrices.length;
3886
+ matrices.push(matrix.slice());
3891
3887
  $._matrixDirty = false;
3892
3888
  };
3893
3889
 
3894
3890
  // push the current matrix index onto the stack
3895
3891
  $.pushMatrix = () => {
3896
3892
  if ($._matrixDirty) $._saveMatrix();
3897
- $._transformIndexStack.push($._transformIndex);
3893
+ matricesIndexStack.push($._matrixIndex);
3898
3894
  };
3899
3895
 
3900
3896
  $.popMatrix = () => {
3901
- if (!$._transformIndexStack.length) {
3897
+ if (!matricesIndexStack.length) {
3902
3898
  return console.warn('Matrix index stack is empty!');
3903
3899
  }
3904
3900
  // pop the last matrix index and set it as the current matrix index
3905
- let idx = $._transformIndexStack.pop();
3906
- $._matrix = transformStates[idx].slice();
3907
- $._transformIndex = idx;
3901
+ let idx = matricesIndexStack.pop();
3902
+ matrix = matrices[idx].slice();
3903
+ $._matrixIndex = idx;
3908
3904
  $._matrixDirty = false;
3909
3905
  };
3910
3906
 
@@ -4029,26 +4025,14 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4029
4025
  };
4030
4026
 
4031
4027
  $._render = () => {
4032
- if (transformStates.length > 1 || !$._transformBindGroup) {
4033
- let transformBuffer = Q5.device.createBuffer({
4034
- size: transformStates.length * 64, // 64 is the size of 16 floats
4035
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4036
- mappedAtCreation: true
4037
- });
4038
-
4039
- new Float32Array(transformBuffer.getMappedRange()).set(transformStates.flat());
4040
- transformBuffer.unmap();
4041
-
4042
- $._transformBindGroup = Q5.device.createBindGroup({
4043
- layout: $._transformLayout,
4044
- entries: [
4045
- { binding: 0, resource: { buffer: uniformBuffer } },
4046
- { binding: 1, resource: { buffer: transformBuffer } }
4047
- ]
4048
- });
4049
- }
4028
+ let transformBuffer = Q5.device.createBuffer({
4029
+ size: matrices.length * MATRIX_SIZE * 4, // 4 bytes per float
4030
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4031
+ mappedAtCreation: true
4032
+ });
4050
4033
 
4051
- pass.setBindGroup(0, $._transformBindGroup);
4034
+ new Float32Array(transformBuffer.getMappedRange()).set(transforms.slice(0, matrices.length * MATRIX_SIZE));
4035
+ transformBuffer.unmap();
4052
4036
 
4053
4037
  let colorsBuffer = Q5.device.createBuffer({
4054
4038
  size: colorStackIndex * 4,
@@ -4059,12 +4043,16 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4059
4043
  new Float32Array(colorsBuffer.getMappedRange()).set(colorStack.slice(0, colorStackIndex));
4060
4044
  colorsBuffer.unmap();
4061
4045
 
4062
- $._colorsBindGroup = Q5.device.createBindGroup({
4063
- layout: colorsLayout,
4064
- entries: [{ binding: 0, resource: { buffer: colorsBuffer } }]
4046
+ mainBindGroup = Q5.device.createBindGroup({
4047
+ layout: mainLayout,
4048
+ entries: [
4049
+ { binding: 0, resource: { buffer: uniformBuffer } },
4050
+ { binding: 1, resource: { buffer: transformBuffer } },
4051
+ { binding: 2, resource: { buffer: colorsBuffer } }
4052
+ ]
4065
4053
  });
4066
4054
 
4067
- $.pass.setBindGroup(1, $._colorsBindGroup);
4055
+ pass.setBindGroup(0, mainBindGroup);
4068
4056
 
4069
4057
  for (let m of $._hooks.preRender) m();
4070
4058
 
@@ -4088,18 +4076,20 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4088
4076
  pass.draw(v, 1, drawVertOffset);
4089
4077
  drawVertOffset += v;
4090
4078
  } else if (curPipelineIndex == 1) {
4079
+ // let instanceCount = drawStack[i + 2];
4091
4080
  // draw images
4092
4081
  if (curTextureIndex != v) {
4093
4082
  // v is the texture index
4094
- pass.setBindGroup(2, $._textureBindGroups[v]);
4083
+ pass.setBindGroup(1, $._textureBindGroups[v]);
4095
4084
  }
4096
4085
  pass.draw(4, 1, imageVertOffset);
4097
4086
  imageVertOffset += 4;
4087
+ // i++;
4098
4088
  } else if (curPipelineIndex == 2) {
4099
4089
  // draw text
4100
4090
  let o = drawStack[i + 2];
4101
- pass.setBindGroup(2, $._fonts[o].bindGroup);
4102
- pass.setBindGroup(3, $._textBindGroup);
4091
+ pass.setBindGroup(1, $._fonts[o].bindGroup);
4092
+ pass.setBindGroup(2, $._textBindGroup);
4103
4093
 
4104
4094
  // v is the number of characters in the text
4105
4095
  pass.draw(4, v, 0, textCharOffset);
@@ -4123,8 +4113,9 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4123
4113
  colorIndex = 1;
4124
4114
  colorStackIndex = 8;
4125
4115
  rotation = 0;
4126
- transformStates.length = 1;
4127
- $._transformIndexStack.length = 0;
4116
+ transforms.length = MATRIX_SIZE;
4117
+ matrices.length = 1;
4118
+ matricesIndexStack.length = 0;
4128
4119
  };
4129
4120
  };
4130
4121
 
@@ -4135,7 +4126,10 @@ Q5.initWebGPU = async () => {
4135
4126
  }
4136
4127
  if (!Q5.device) {
4137
4128
  let adapter = await navigator.gpu.requestAdapter();
4138
- if (!adapter) throw new Error('No appropriate GPUAdapter found.');
4129
+ if (!adapter) {
4130
+ console.warn('q5 WebGPU could not start! No appropriate GPUAdapter found, vulkan may need to be enabled.');
4131
+ return false;
4132
+ }
4139
4133
  Q5.device = await adapter.requestDevice();
4140
4134
  }
4141
4135
  return true;
@@ -4154,46 +4148,40 @@ Q5.renderers.webgpu.drawing = ($, q) => {
4154
4148
  vertexStack = new Float32Array(1e7),
4155
4149
  vertIndex = 0;
4156
4150
 
4157
- let vertexShader = Q5.device.createShaderModule({
4158
- label: 'drawingVertexShader',
4151
+ let drawingShader = Q5.device.createShaderModule({
4152
+ label: 'drawingShader',
4159
4153
  code: `
4160
- struct VertexInput {
4154
+ struct Uniforms {
4155
+ halfWidth: f32,
4156
+ halfHeight: f32
4157
+ }
4158
+ struct VertexParams {
4161
4159
  @location(0) pos: vec2f,
4162
4160
  @location(1) colorIndex: f32,
4163
- @location(2) transformIndex: f32
4161
+ @location(2) matrixIndex: f32
4164
4162
  }
4165
- struct VertexOutput {
4163
+ struct FragmentParams {
4166
4164
  @builtin(position) position: vec4f,
4167
4165
  @location(0) color: vec4f
4168
4166
  }
4169
- struct Uniforms {
4170
- halfWidth: f32,
4171
- halfHeight: f32
4172
- }
4173
4167
 
4174
4168
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
4175
4169
  @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
4176
-
4177
- @group(1) @binding(0) var<storage> colors : array<vec4f>;
4170
+ @group(0) @binding(2) var<storage> colors : array<vec4f>;
4178
4171
 
4179
4172
  @vertex
4180
- fn vertexMain(input: VertexInput) -> VertexOutput {
4181
- var vert = vec4f(input.pos, 0.0, 1.0);
4182
- vert = transforms[i32(input.transformIndex)] * vert;
4173
+ fn vertexMain(v: VertexParams) -> FragmentParams {
4174
+ var vert = vec4f(v.pos, 0.0, 1.0);
4175
+ vert = transforms[i32(v.matrixIndex)] * vert;
4183
4176
  vert.x /= uniforms.halfWidth;
4184
4177
  vert.y /= uniforms.halfHeight;
4185
4178
 
4186
- var output: VertexOutput;
4187
- output.position = vert;
4188
- output.color = colors[i32(input.colorIndex)];
4189
- return output;
4179
+ var f: FragmentParams;
4180
+ f.position = vert;
4181
+ f.color = colors[i32(v.colorIndex)];
4182
+ return f;
4190
4183
  }
4191
- `
4192
- });
4193
4184
 
4194
- let fragmentShader = Q5.device.createShaderModule({
4195
- label: 'drawingFragmentShader',
4196
- code: `
4197
4185
  @fragment
4198
4186
  fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
4199
4187
  return color;
@@ -4206,7 +4194,7 @@ fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
4206
4194
  attributes: [
4207
4195
  { format: 'float32x2', offset: 0, shaderLocation: 0 }, // position
4208
4196
  { format: 'float32', offset: 8, shaderLocation: 1 }, // colorIndex
4209
- { format: 'float32', offset: 12, shaderLocation: 2 } // transformIndex
4197
+ { format: 'float32', offset: 12, shaderLocation: 2 } // matrixIndex
4210
4198
  ]
4211
4199
  };
4212
4200
 
@@ -4219,19 +4207,17 @@ fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
4219
4207
  label: 'drawingPipeline',
4220
4208
  layout: pipelineLayout,
4221
4209
  vertex: {
4222
- module: vertexShader,
4210
+ module: drawingShader,
4223
4211
  entryPoint: 'vertexMain',
4224
4212
  buffers: [vertexBufferLayout]
4225
4213
  },
4226
4214
  fragment: {
4227
- module: fragmentShader,
4215
+ module: drawingShader,
4228
4216
  entryPoint: 'fragmentMain',
4229
4217
  targets: [{ format: 'bgra8unorm', blend: $.blendConfigs.normal }]
4230
4218
  },
4231
4219
  primitive: { topology: 'triangle-strip', stripIndexFormat: 'uint32' },
4232
- multisample: {
4233
- count: 4
4234
- }
4220
+ multisample: { count: 4 }
4235
4221
  };
4236
4222
 
4237
4223
  $._pipelines[0] = Q5.device.createRenderPipeline($._pipelineConfigs[0]);
@@ -4360,7 +4346,7 @@ fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
4360
4346
  let [l, r, t, b] = $._calcBox(x, y, w, h, $._rectMode);
4361
4347
  let ci, ti;
4362
4348
  if ($._matrixDirty) $._saveMatrix();
4363
- ti = $._transformIndex;
4349
+ ti = $._matrixIndex;
4364
4350
 
4365
4351
  if ($._doFill) {
4366
4352
  ci = $._fill;
@@ -4434,7 +4420,7 @@ fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
4434
4420
  let b = w == h ? a : Math.max(h, 1) / 2;
4435
4421
 
4436
4422
  if ($._matrixDirty) $._saveMatrix();
4437
- let ti = $._transformIndex;
4423
+ let ti = $._matrixIndex;
4438
4424
 
4439
4425
  if ($._doFill) {
4440
4426
  addEllipse(x, y, a, b, n, $._fill, ti);
@@ -4450,7 +4436,7 @@ fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
4450
4436
 
4451
4437
  $.point = (x, y) => {
4452
4438
  if ($._matrixDirty) $._saveMatrix();
4453
- let ti = $._transformIndex,
4439
+ let ti = $._matrixIndex,
4454
4440
  ci = $._stroke,
4455
4441
  sw = $._strokeWeight;
4456
4442
 
@@ -4470,7 +4456,7 @@ fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
4470
4456
 
4471
4457
  $.line = (x1, y1, x2, y2) => {
4472
4458
  if ($._matrixDirty) $._saveMatrix();
4473
- let ti = $._transformIndex,
4459
+ let ti = $._matrixIndex,
4474
4460
  ci = $._stroke,
4475
4461
  sw = $._strokeWeight,
4476
4462
  hsw = sw / 2;
@@ -4505,7 +4491,7 @@ fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
4505
4491
 
4506
4492
  $.vertex = (x, y) => {
4507
4493
  if ($._matrixDirty) $._saveMatrix();
4508
- sv.push(x, -y, $._fill, $._transformIndex);
4494
+ sv.push(x, -y, $._fill, $._matrixIndex);
4509
4495
  shapeVertCount++;
4510
4496
  };
4511
4497
 
@@ -4550,7 +4536,7 @@ fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
4550
4536
  (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
4551
4537
  (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3);
4552
4538
 
4553
- sv.push(x, y, $._fill, $._transformIndex);
4539
+ sv.push(x, y, $._fill, $._matrixIndex);
4554
4540
  shapeVertCount++;
4555
4541
  }
4556
4542
  }
@@ -4667,53 +4653,66 @@ fn fragmentMain(@location(0) color: vec4f) -> @location(0) vec4f {
4667
4653
  });
4668
4654
  };
4669
4655
  Q5.renderers.webgpu.image = ($, q) => {
4670
- $._textureBindGroups = [];
4671
- let vertexStack = [];
4656
+ let vertexStack = new Float32Array(1e7),
4657
+ vertIndex = 0;
4672
4658
 
4673
- let vertexShader = Q5.device.createShaderModule({
4674
- label: 'imageVertexShader',
4659
+ let imageShader = Q5.device.createShaderModule({
4660
+ label: 'imageShader',
4675
4661
  code: `
4676
- struct VertexOutput {
4677
- @builtin(position) position: vec4f,
4678
- @location(0) texCoord: vec2f
4679
- }
4680
4662
  struct Uniforms {
4681
4663
  halfWidth: f32,
4682
4664
  halfHeight: f32
4683
4665
  }
4666
+ struct VertexParams {
4667
+ @location(0) pos: vec2f,
4668
+ @location(1) texCoord: vec2f,
4669
+ @location(2) tintIndex: f32,
4670
+ @location(3) matrixIndex: f32,
4671
+ @location(4) globalAlpha: f32
4672
+ }
4673
+ struct FragmentParams {
4674
+ @builtin(position) position: vec4f,
4675
+ @location(0) texCoord: vec2f,
4676
+ @location(1) tintIndex: f32,
4677
+ @location(2) globalAlpha: f32
4678
+ }
4684
4679
 
4685
4680
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
4686
4681
  @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
4682
+ @group(0) @binding(2) var<storage> colors : array<vec4f>;
4683
+
4684
+ @group(1) @binding(0) var samp: sampler;
4685
+ @group(1) @binding(1) var texture: texture_2d<f32>;
4687
4686
 
4688
4687
  @vertex
4689
- fn vertexMain(@location(0) pos: vec2f, @location(1) texCoord: vec2f, @location(2) transformIndex: f32) -> VertexOutput {
4690
- var vert = vec4f(pos, 0.0, 1.0);
4691
- vert = transforms[i32(transformIndex)] * vert;
4688
+ fn vertexMain(v: VertexParams) -> FragmentParams {
4689
+ var vert = vec4f(v.pos, 0.0, 1.0);
4690
+ vert = transforms[i32(v.matrixIndex)] * vert;
4692
4691
  vert.x /= uniforms.halfWidth;
4693
4692
  vert.y /= uniforms.halfHeight;
4694
4693
 
4695
- var output: VertexOutput;
4696
- output.position = vert;
4697
- output.texCoord = texCoord;
4698
- return output;
4694
+ var f: FragmentParams;
4695
+ f.position = vert;
4696
+ f.texCoord = v.texCoord;
4697
+ f.tintIndex = v.tintIndex;
4698
+ f.globalAlpha = v.globalAlpha;
4699
+ return f;
4699
4700
  }
4700
- `
4701
- });
4702
-
4703
- let fragmentShader = Q5.device.createShaderModule({
4704
- label: 'imageFragmentShader',
4705
- code: `
4706
- @group(2) @binding(0) var samp: sampler;
4707
- @group(2) @binding(1) var texture: texture_2d<f32>;
4708
4701
 
4709
4702
  @fragment
4710
- fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4711
- // Sample the texture using the interpolated texture coordinate
4712
- return textureSample(texture, samp, texCoord);
4703
+ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
4704
+ let texColor = textureSample(texture, samp, f.texCoord);
4705
+ let tintColor = colors[i32(f.tintIndex)];
4706
+
4707
+ // Mix original and tinted colors using tint alpha as blend factor
4708
+ let tinted = vec4f(texColor.rgb * tintColor.rgb, texColor.a * f.globalAlpha);
4709
+ return mix(texColor, tinted, tintColor.a);
4713
4710
  }
4714
4711
  `
4715
4712
  });
4716
4713
 
4714
+ $._textureBindGroups = [];
4715
+
4717
4716
  let textureLayout = Q5.device.createBindGroupLayout({
4718
4717
  label: 'textureLayout',
4719
4718
  entries: [
@@ -4731,11 +4730,13 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4731
4730
  });
4732
4731
 
4733
4732
  const vertexBufferLayout = {
4734
- arrayStride: 20,
4733
+ arrayStride: 28,
4735
4734
  attributes: [
4736
4735
  { shaderLocation: 0, offset: 0, format: 'float32x2' },
4737
4736
  { shaderLocation: 1, offset: 8, format: 'float32x2' },
4738
- { shaderLocation: 2, offset: 16, format: 'float32' } // transformIndex
4737
+ { shaderLocation: 2, offset: 16, format: 'float32' }, // tintIndex
4738
+ { shaderLocation: 3, offset: 20, format: 'float32' }, // matrixIndex
4739
+ { shaderLocation: 4, offset: 24, format: 'float32' } // globalAlpha
4739
4740
  ]
4740
4741
  };
4741
4742
 
@@ -4748,12 +4749,12 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4748
4749
  label: 'imagePipeline',
4749
4750
  layout: pipelineLayout,
4750
4751
  vertex: {
4751
- module: vertexShader,
4752
+ module: imageShader,
4752
4753
  entryPoint: 'vertexMain',
4753
4754
  buffers: [{ arrayStride: 0, attributes: [] }, vertexBufferLayout]
4754
4755
  },
4755
4756
  fragment: {
4756
- module: fragmentShader,
4757
+ module: imageShader,
4757
4758
  entryPoint: 'fragmentMain',
4758
4759
  targets: [{ format: 'bgra8unorm', blend: $.blendConfigs.normal }]
4759
4760
  },
@@ -4829,58 +4830,64 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4829
4830
 
4830
4831
  $.loadImage = (src, cb) => {
4831
4832
  q._preloadCount++;
4832
- const img = new Image();
4833
- img.crossOrigin = 'Anonymous';
4834
- img.onload = () => {
4835
- // calculate the default width and height that the image
4836
- // should be drawn at if the user doesn't specify a display size
4837
- img.defaultWidth = img.width * $._defaultImageScale;
4838
- img.defaultHeight = img.height * $._defaultImageScale;
4839
- img.pixelDensity = 1;
4840
-
4833
+ let g = $._g.loadImage(src, (img) => {
4834
+ g.defaultWidth = img.width * $._defaultImageScale;
4835
+ g.defaultHeight = img.height * $._defaultImageScale;
4841
4836
  $._createTexture(img);
4842
4837
  q._preloadCount--;
4843
4838
  if (cb) cb(img);
4844
- };
4845
- img.src = src;
4846
- return img;
4839
+ });
4840
+ return g;
4847
4841
  };
4848
4842
 
4849
4843
  $.imageMode = (x) => ($._imageMode = x);
4850
4844
 
4851
- $.image = (img, dx, dy, dw, dh, sx = 0, sy = 0, sw, sh) => {
4845
+ const addVert = (x, y, u, v, ci, ti, ga) => {
4846
+ let s = vertexStack,
4847
+ i = vertIndex;
4848
+ s[i++] = x;
4849
+ s[i++] = y;
4850
+ s[i++] = u;
4851
+ s[i++] = v;
4852
+ s[i++] = ci;
4853
+ s[i++] = ti;
4854
+ s[i++] = ga;
4855
+ vertIndex = i;
4856
+ };
4857
+
4858
+ $.image = (img, dx = 0, dy = 0, dw, dh, sx = 0, sy = 0, sw, sh) => {
4859
+ let g = img;
4852
4860
  if (img.canvas) img = img.canvas;
4853
4861
  if (img.textureIndex == undefined) return;
4854
4862
 
4855
4863
  if ($._matrixDirty) $._saveMatrix();
4856
- let ti = $._transformIndex;
4857
4864
 
4858
- let w = img.width;
4859
- let h = img.height;
4865
+ let w = img.width,
4866
+ h = img.height,
4867
+ pd = g._pixelDensity || 1;
4860
4868
 
4861
- dw ??= img.defaultWidth;
4862
- dh ??= img.defaultHeight;
4869
+ dw ??= g.defaultWidth;
4870
+ dh ??= g.defaultHeight;
4863
4871
  sw ??= w;
4864
4872
  sh ??= h;
4865
4873
 
4866
- let pd = img.pixelDensity || 1;
4867
4874
  dw *= pd;
4868
4875
  dh *= pd;
4869
4876
 
4870
4877
  let [l, r, t, b] = $._calcBox(dx, dy, dw, dh, $._imageMode);
4871
4878
 
4872
- let u0 = sx / w;
4873
- let v0 = sy / h;
4874
- let u1 = (sx + sw) / w;
4875
- let v1 = (sy + sh) / h;
4876
-
4877
- // prettier-ignore
4878
- vertexStack.push(
4879
- l, t, u0, v0, ti,
4880
- r, t, u1, v0, ti,
4881
- l, b, u0, v1, ti,
4882
- r, b, u1, v1, ti
4883
- );
4879
+ let u0 = sx / w,
4880
+ v0 = sy / h,
4881
+ u1 = (sx + sw) / w,
4882
+ v1 = (sy + sh) / h,
4883
+ ti = $._matrixIndex,
4884
+ ci = $._tint,
4885
+ ga = $._globalAlpha;
4886
+
4887
+ addVert(l, t, u0, v0, ci, ti, ga);
4888
+ addVert(r, t, u1, v0, ci, ti, ga);
4889
+ addVert(l, b, u0, v1, ci, ti, ga);
4890
+ addVert(r, b, u1, v1, ci, ti, ga);
4884
4891
 
4885
4892
  $.drawStack.push(1, img.textureIndex);
4886
4893
  };
@@ -4891,20 +4898,20 @@ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
4891
4898
  // Switch to image pipeline
4892
4899
  $.pass.setPipeline($._pipelines[1]);
4893
4900
 
4894
- const vertexBuffer = Q5.device.createBuffer({
4895
- size: vertexStack.length * 4,
4901
+ let vertexBuffer = Q5.device.createBuffer({
4902
+ size: vertIndex * 5,
4896
4903
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
4897
4904
  mappedAtCreation: true
4898
4905
  });
4899
4906
 
4900
- new Float32Array(vertexBuffer.getMappedRange()).set(vertexStack);
4907
+ new Float32Array(vertexBuffer.getMappedRange()).set(vertexStack.slice(0, vertIndex));
4901
4908
  vertexBuffer.unmap();
4902
4909
 
4903
4910
  $.pass.setVertexBuffer(1, vertexBuffer);
4904
4911
  });
4905
4912
 
4906
4913
  $._hooks.postRender.push(() => {
4907
- vertexStack.length = 0;
4914
+ vertIndex = 0;
4908
4915
  });
4909
4916
  };
4910
4917
 
@@ -4920,14 +4927,15 @@ Q5.renderers.webgpu.text = ($, q) => {
4920
4927
  let textShader = Q5.device.createShaderModule({
4921
4928
  label: 'MSDF text shader',
4922
4929
  code: `
4923
- // Positions for simple quad geometry
4924
- const pos = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0));
4925
-
4926
- struct VertexInput {
4930
+ struct Uniforms {
4931
+ halfWidth: f32,
4932
+ halfHeight: f32
4933
+ }
4934
+ struct VertexParams {
4927
4935
  @builtin(vertex_index) vertex : u32,
4928
4936
  @builtin(instance_index) instance : u32
4929
4937
  }
4930
- struct VertexOutput {
4938
+ struct FragmentParams {
4931
4939
  @builtin(position) position : vec4f,
4932
4940
  @location(0) texCoord : vec2f,
4933
4941
  @location(1) fillColor : vec4f
@@ -4941,47 +4949,44 @@ struct Char {
4941
4949
  struct Text {
4942
4950
  pos: vec2f,
4943
4951
  scale: f32,
4944
- transformIndex: f32,
4952
+ matrixIndex: f32,
4945
4953
  fillIndex: f32,
4946
4954
  strokeIndex: f32
4947
4955
  }
4948
- struct Uniforms {
4949
- halfWidth: f32,
4950
- halfHeight: f32
4951
- }
4952
4956
 
4953
4957
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
4954
4958
  @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
4959
+ @group(0) @binding(2) var<storage> colors : array<vec4f>;
4955
4960
 
4956
- @group(1) @binding(0) var<storage> colors : array<vec4f>;
4961
+ @group(1) @binding(0) var fontTexture: texture_2d<f32>;
4962
+ @group(1) @binding(1) var fontSampler: sampler;
4963
+ @group(1) @binding(2) var<storage> fontChars: array<Char>;
4957
4964
 
4958
- @group(2) @binding(0) var fontTexture: texture_2d<f32>;
4959
- @group(2) @binding(1) var fontSampler: sampler;
4960
- @group(2) @binding(2) var<storage> fontChars: array<Char>;
4965
+ @group(2) @binding(0) var<storage> textChars: array<vec4f>;
4966
+ @group(2) @binding(1) var<storage> textMetadata: array<Text>;
4961
4967
 
4962
- @group(3) @binding(0) var<storage> textChars: array<vec4f>;
4963
- @group(3) @binding(1) var<storage> textMetadata: array<Text>;
4968
+ const quad = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0));
4964
4969
 
4965
4970
  @vertex
4966
- fn vertexMain(input : VertexInput) -> VertexOutput {
4967
- let char = textChars[input.instance];
4971
+ fn vertexMain(v : VertexParams) -> FragmentParams {
4972
+ let char = textChars[v.instance];
4968
4973
 
4969
4974
  let text = textMetadata[i32(char.w)];
4970
4975
 
4971
4976
  let fontChar = fontChars[i32(char.z)];
4972
4977
 
4973
- let charPos = ((pos[input.vertex] * fontChar.size + char.xy + fontChar.offset) * text.scale) + text.pos;
4978
+ let charPos = ((quad[v.vertex] * fontChar.size + char.xy + fontChar.offset) * text.scale) + text.pos;
4974
4979
 
4975
4980
  var vert = vec4f(charPos, 0.0, 1.0);
4976
- vert = transforms[i32(text.transformIndex)] * vert;
4981
+ vert = transforms[i32(text.matrixIndex)] * vert;
4977
4982
  vert.x /= uniforms.halfWidth;
4978
4983
  vert.y /= uniforms.halfHeight;
4979
4984
 
4980
- var output : VertexOutput;
4981
- output.position = vert;
4982
- output.texCoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;
4983
- output.fillColor = colors[i32(text.fillIndex)];
4984
- return output;
4985
+ var f : FragmentParams;
4986
+ f.position = vert;
4987
+ f.texCoord = (quad[v.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;
4988
+ f.fillColor = colors[i32(text.fillIndex)];
4989
+ return f;
4985
4990
  }
4986
4991
 
4987
4992
  fn sampleMsdf(texCoord: vec2f) -> f32 {
@@ -4990,22 +4995,22 @@ fn sampleMsdf(texCoord: vec2f) -> f32 {
4990
4995
  }
4991
4996
 
4992
4997
  @fragment
4993
- fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4998
+ fn fragmentMain(f : FragmentParams) -> @location(0) vec4f {
4994
4999
  // pxRange (AKA distanceRange) comes from the msdfgen tool,
4995
5000
  // uses the default which is 4.
4996
5001
  let pxRange = 4.0;
4997
5002
  let sz = vec2f(textureDimensions(fontTexture, 0));
4998
- let dx = sz.x*length(vec2f(dpdxFine(input.texCoord.x), dpdyFine(input.texCoord.x)));
4999
- let dy = sz.y*length(vec2f(dpdxFine(input.texCoord.y), dpdyFine(input.texCoord.y)));
5003
+ let dx = sz.x*length(vec2f(dpdxFine(f.texCoord.x), dpdyFine(f.texCoord.x)));
5004
+ let dy = sz.y*length(vec2f(dpdxFine(f.texCoord.y), dpdyFine(f.texCoord.y)));
5000
5005
  let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
5001
- let sigDist = sampleMsdf(input.texCoord) - 0.5;
5006
+ let sigDist = sampleMsdf(f.texCoord) - 0.5;
5002
5007
  let pxDist = sigDist * toPixels;
5003
5008
  let edgeWidth = 0.5;
5004
5009
  let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
5005
5010
  if (alpha < 0.001) {
5006
5011
  discard;
5007
5012
  }
5008
- return vec4f(input.fillColor.rgb, input.fillColor.a * alpha);
5013
+ return vec4f(f.fillColor.rgb, f.fillColor.a * alpha);
5009
5014
  }
5010
5015
  `
5011
5016
  });
@@ -5197,13 +5202,11 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
5197
5202
  if (cb) cb(fontName);
5198
5203
  };
5199
5204
 
5200
- // q2d graphics context to use for text image creation
5201
- let g = $.createGraphics(1, 1);
5202
- g.colorMode($.RGB, 1);
5205
+ $._g.colorMode($.RGB, 1);
5203
5206
 
5204
5207
  $.loadFont = (url, cb) => {
5205
5208
  let ext = url.slice(url.lastIndexOf('.') + 1);
5206
- if (ext != 'json') return g.loadFont(url, cb);
5209
+ if (ext != 'json') return $._g.loadFont(url, cb);
5207
5210
  let fontName = url.slice(url.lastIndexOf('/') + 1, url.lastIndexOf('-'));
5208
5211
  createFont(url, fontName, cb);
5209
5212
  return fontName;
@@ -5388,7 +5391,7 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
5388
5391
  text[0] = x;
5389
5392
  text[1] = -y;
5390
5393
  text[2] = $._textSize / 44;
5391
- text[3] = $._transformIndex;
5394
+ text[3] = $._matrixIndex;
5392
5395
  text[4] = $._fillSet ? $._fill : 0;
5393
5396
  text[5] = $._stroke;
5394
5397
 
@@ -5402,18 +5405,18 @@ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
5402
5405
  };
5403
5406
 
5404
5407
  $.createTextImage = (str, w, h) => {
5405
- g.textSize($._textSize);
5408
+ $._g.textSize($._textSize);
5406
5409
 
5407
5410
  if ($._doFill) {
5408
5411
  let fi = $._fill * 4;
5409
- g.fill(colorStack.slice(fi, fi + 4));
5412
+ $._g.fill(colorStack.slice(fi, fi + 4));
5410
5413
  }
5411
5414
  if ($._doStroke) {
5412
5415
  let si = $._stroke * 4;
5413
- g.stroke(colorStack.slice(si, si + 4));
5416
+ $._g.stroke(colorStack.slice(si, si + 4));
5414
5417
  }
5415
5418
 
5416
- let img = g.createTextImage(str, w, h);
5419
+ let img = $._g.createTextImage(str, w, h);
5417
5420
 
5418
5421
  if (img.canvas.textureIndex == undefined) {
5419
5422
  $._createTexture(img);