q5 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/q5-webgpu.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * q5.js
3
- * @version 2.1
3
+ * @version 2.2
4
4
  * @author quinton-ashley, Tezumie, and LingDong-
5
5
  * @license LGPL-3.0
6
6
  * @class Q5
@@ -85,8 +85,8 @@ function Q5(scope, parent, renderer) {
85
85
  $._frameRate = 1000 / $.deltaTime;
86
86
  q.frameCount++;
87
87
  let pre = performance.now();
88
- if ($._beginRender) $._beginRender();
89
88
  if ($.ctx) $.resetMatrix();
89
+ if ($._beginRender) $._beginRender();
90
90
  for (let m of Q5.methods.pre) m.call($);
91
91
  $.draw();
92
92
  if ($._render) $._render();
@@ -524,11 +524,17 @@ Q5.renderers.q2d.canvas = ($, q) => {
524
524
  $.ctx.drawImage(o, 0, 0, o.w, o.h);
525
525
  };
526
526
 
527
- $.strokeWeight = (n) => {
528
- if (!n) $._doStroke = false;
529
- if ($._da) n *= $._da;
530
- $.ctx.lineWidth = n || 0.0001;
527
+ $.fill = function (c) {
528
+ $._doFill = true;
529
+ $._fillSet = true;
530
+ if (Q5.Color) {
531
+ if (!c._q5Color && typeof c != 'string') c = $.color(...arguments);
532
+ else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
533
+ if (c.a <= 0) return ($._doFill = false);
534
+ }
535
+ $.ctx.fillStyle = c.toString();
531
536
  };
537
+ $.noFill = () => ($._doFill = false);
532
538
  $.stroke = function (c) {
533
539
  $._doStroke = true;
534
540
  $._strokeSet = true;
@@ -539,18 +545,13 @@ Q5.renderers.q2d.canvas = ($, q) => {
539
545
  }
540
546
  $.ctx.strokeStyle = c.toString();
541
547
  };
542
- $.noStroke = () => ($._doStroke = false);
543
- $.fill = function (c) {
544
- $._doFill = true;
545
- $._fillSet = true;
546
- if (Q5.Color) {
547
- if (!c._q5Color && typeof c != 'string') c = $.color(...arguments);
548
- else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
549
- if (c.a <= 0) return ($._doFill = false);
550
- }
551
- $.ctx.fillStyle = c.toString();
548
+ $.strokeWeight = (n) => {
549
+ if (!n) $._doStroke = false;
550
+ if ($._da) n *= $._da;
551
+ $.ctx.lineWidth = n || 0.0001;
552
552
  };
553
- $.noFill = () => ($._doFill = false);
553
+ $.noStroke = () => ($._doStroke = false);
554
+ $.clear = () => $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
554
555
 
555
556
  // DRAWING MATRIX
556
557
 
@@ -715,10 +716,6 @@ Q5.renderers.q2d.drawing = ($) => {
715
716
 
716
717
  // DRAWING
717
718
 
718
- $.clear = () => {
719
- $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
720
- };
721
-
722
719
  $.background = function (c) {
723
720
  if (c.canvas) return $.image(c, 0, 0, $.width, $.height);
724
721
  $.ctx.save();
@@ -876,18 +873,7 @@ Q5.renderers.q2d.drawing = ($) => {
876
873
  bl *= $._da;
877
874
  br *= $._da;
878
875
  }
879
- const hh = Math.min(Math.abs(h), Math.abs(w)) / 2;
880
- tl = Math.min(hh, tl);
881
- tr = Math.min(hh, tr);
882
- bl = Math.min(hh, bl);
883
- br = Math.min(hh, br);
884
- $.ctx.beginPath();
885
- $.ctx.moveTo(x + tl, y);
886
- $.ctx.arcTo(x + w, y, x + w, y + h, tr);
887
- $.ctx.arcTo(x + w, y + h, x, y + h, br);
888
- $.ctx.arcTo(x, y + h, x, y, bl);
889
- $.ctx.arcTo(x, y, x + w, y, tl);
890
- $.ctx.closePath();
876
+ $.ctx.roundRect(x, y, w, h, [tl, tr, br, bl]);
891
877
  ink();
892
878
  }
893
879
 
@@ -1676,12 +1662,12 @@ Q5.modules.color = ($, q) => {
1676
1662
  $._colorMode = mode;
1677
1663
  let srgb = $.canvas.colorSpace == 'srgb' || mode == 'srgb';
1678
1664
  format ??= srgb ? 'integer' : 'float';
1679
- $._colorFormat = format;
1665
+ $._colorFormat = format == 'float' || format == 1 ? 1 : 255;
1680
1666
  if (mode == 'oklch') {
1681
1667
  q.Color = Q5.ColorOKLCH;
1682
1668
  } else {
1683
1669
  let srgb = $.canvas.colorSpace == 'srgb';
1684
- if ($._colorFormat == 'integer') {
1670
+ if ($._colorFormat == 255) {
1685
1671
  q.Color = srgb ? Q5.ColorRGBA_8 : Q5.ColorRGBA_P3_8;
1686
1672
  } else {
1687
1673
  q.Color = srgb ? Q5.ColorRGBA : Q5.ColorRGBA_P3;
@@ -1724,48 +1710,49 @@ Q5.modules.color = ($, q) => {
1724
1710
  yellow: [255, 255, 0]
1725
1711
  };
1726
1712
 
1727
- $.color = function (c0, c1, c2, c3) {
1713
+ $.color = (c0, c1, c2, c3) => {
1728
1714
  let C = $.Color;
1729
1715
  if (c0._q5Color) return new C(...c0.levels);
1730
- let args = arguments;
1731
- if (args.length == 1) {
1716
+ if (c1 == undefined) {
1732
1717
  if (typeof c0 == 'string') {
1733
- let r, g, b, a;
1734
1718
  if (c0[0] == '#') {
1735
1719
  if (c0.length <= 5) {
1736
- r = parseInt(c0[1] + c0[1], 16);
1737
- g = parseInt(c0[2] + c0[2], 16);
1738
- b = parseInt(c0[3] + c0[3], 16);
1739
- a = c0.length == 4 ? null : parseInt(c0[4] + c0[4], 16);
1720
+ if (c0.length > 4) c3 = parseInt(c0[4] + c0[4], 16);
1721
+ c2 = parseInt(c0[3] + c0[3], 16);
1722
+ c1 = parseInt(c0[2] + c0[2], 16);
1723
+ c0 = parseInt(c0[1] + c0[1], 16);
1740
1724
  } else {
1741
- r = parseInt(c0.slice(1, 3), 16);
1742
- g = parseInt(c0.slice(3, 5), 16);
1743
- b = parseInt(c0.slice(5, 7), 16);
1744
- a = c0.length == 7 ? null : parseInt(c0.slice(7, 9), 16);
1725
+ if (c0.length > 7) c3 = parseInt(c0.slice(7, 9), 16);
1726
+ c2 = parseInt(c0.slice(5, 7), 16);
1727
+ c1 = parseInt(c0.slice(3, 5), 16);
1728
+ c0 = parseInt(c0.slice(1, 3), 16);
1745
1729
  }
1746
- } else if ($._namedColors[c0]) [r, g, b] = $._namedColors[c0];
1747
- else {
1730
+ } else if ($._namedColors[c0]) {
1731
+ c0 = $._namedColors[c0];
1732
+ } else {
1748
1733
  console.error(
1749
1734
  "q5 can't parse color: " + c0 + '\nOnly numeric input, hex, and common named colors are supported.'
1750
1735
  );
1751
1736
  return new C(0, 0, 0);
1752
1737
  }
1753
- if ($._colorFormat != 'integer') {
1754
- r /= 255;
1755
- g /= 255;
1756
- b /= 255;
1757
- if (a != null) a /= 255;
1758
- }
1759
- return new C(r, g, b, a);
1760
1738
  }
1761
- if (Array.isArray(c0)) return new C(...c0);
1739
+ if (Array.isArray(c0)) {
1740
+ c1 = c0[1];
1741
+ c2 = c0[2];
1742
+ c3 = c0[3];
1743
+ c0 = c0[0];
1744
+ }
1762
1745
  }
1763
- if ($._colorMode == 'rgb') {
1764
- if (args.length == 1) return new C(c0, c0, c0);
1765
- if (args.length == 2) return new C(c0, c0, c0, c1);
1746
+
1747
+ if ($._colorFormat == 1) {
1748
+ c0 /= 255;
1749
+ if (c1) c1 /= 255;
1750
+ if (c2) c2 /= 255;
1751
+ if (c3) c3 /= 255;
1766
1752
  }
1767
- if (args.length == 3) return new C(c0, c1, c2);
1768
- if (args.length == 4) return new C(c0, c1, c2, c3);
1753
+
1754
+ if (c2 == undefined) return new C(c0, c0, c0, c1);
1755
+ return new C(c0, c1, c2, c3);
1769
1756
  };
1770
1757
 
1771
1758
  $.red = (c) => c.r;
@@ -2919,13 +2906,12 @@ Q5.renderers.webgpu.canvas = ($, q) => {
2919
2906
 
2920
2907
  if ($.colorMode) $.colorMode('rgb', 'float');
2921
2908
 
2922
- let colorsStack;
2909
+ let colorsStack, envBindGroup, transformBindGroup;
2923
2910
 
2924
2911
  $._createCanvas = (w, h, opt) => {
2925
2912
  q.ctx = q.drawingContext = c.getContext('webgpu');
2926
2913
 
2927
- $._canvasFormat = navigator.gpu.getPreferredCanvasFormat();
2928
- opt.format = $._canvasFormat;
2914
+ opt.format = navigator.gpu.getPreferredCanvasFormat();
2929
2915
  opt.device = Q5.device;
2930
2916
 
2931
2917
  $.ctx.configure(opt);
@@ -2942,18 +2928,153 @@ Q5.renderers.webgpu.canvas = ($, q) => {
2942
2928
  $.drawStack = [];
2943
2929
 
2944
2930
  // colors used for each draw call
2945
- colorsStack = $.colorsStack = [];
2931
+ colorsStack = $.colorsStack = [1, 1, 1, 1];
2946
2932
 
2947
2933
  // current color index, used to associate a vertex with a color
2948
- $._colorIndex = -1;
2934
+ $._colorIndex = 0;
2935
+
2936
+ let envLayout = Q5.device.createBindGroupLayout({
2937
+ entries: [
2938
+ {
2939
+ binding: 0,
2940
+ visibility: GPUShaderStage.VERTEX,
2941
+ buffer: {
2942
+ type: 'uniform',
2943
+ hasDynamicOffset: false
2944
+ }
2945
+ }
2946
+ ]
2947
+ });
2948
+
2949
+ let transformLayout = Q5.device.createBindGroupLayout({
2950
+ entries: [
2951
+ {
2952
+ binding: 0,
2953
+ visibility: GPUShaderStage.VERTEX,
2954
+ buffer: {
2955
+ type: 'read-only-storage',
2956
+ hasDynamicOffset: false
2957
+ }
2958
+ }
2959
+ ]
2960
+ });
2961
+
2962
+ $.bindGroupLayouts = [envLayout, transformLayout];
2963
+
2964
+ const uniformBuffer = Q5.device.createBuffer({
2965
+ size: 8, // Size of two floats
2966
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
2967
+ });
2968
+
2969
+ Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([$.canvas.hw, $.canvas.hh]));
2970
+
2971
+ envBindGroup = Q5.device.createBindGroup({
2972
+ layout: envLayout,
2973
+ entries: [
2974
+ {
2975
+ binding: 0,
2976
+ resource: {
2977
+ buffer: uniformBuffer
2978
+ }
2979
+ }
2980
+ ]
2981
+ });
2949
2982
  };
2950
2983
 
2951
2984
  $._resizeCanvas = (w, h) => {
2952
2985
  $._setCanvasSize(w, h);
2953
2986
  };
2954
2987
 
2955
- $.resetMatrix = () => {};
2956
- $.translate = () => {};
2988
+ $.resetMatrix = () => {
2989
+ // Initialize the transformation matrix as 4x4 identity matrix
2990
+
2991
+ // prettier-ignore
2992
+ $._matrix = [
2993
+ 1, 0, 0, 0,
2994
+ 0, 1, 0, 0,
2995
+ 0, 0, 1, 0,
2996
+ 0, 0, 0, 1
2997
+ ];
2998
+ $._transformIndex = 0;
2999
+ };
3000
+ $.resetMatrix();
3001
+
3002
+ // Boolean to track if the matrix has been modified
3003
+ $._matrixDirty = false;
3004
+
3005
+ // Array to store transformation matrices for the render pass
3006
+ $.transformStates = [$._matrix.slice()];
3007
+
3008
+ // Stack to keep track of transformation matrix indexes
3009
+ $._transformIndexStack = [];
3010
+
3011
+ $.push = () => {
3012
+ // Push the current matrix index onto the stack
3013
+ $._transformIndexStack.push($._transformIndex);
3014
+ };
3015
+
3016
+ $.pop = () => {
3017
+ if ($._transformIndexStack.length > 0) {
3018
+ // Pop the last matrix index from the stack and set it as the current matrix index
3019
+ let idx = $._transformIndexStack.pop();
3020
+ $._matrix = $.transformStates[idx].slice();
3021
+ $._transformIndex = idx;
3022
+ } else {
3023
+ console.warn('Matrix index stack is empty!');
3024
+ }
3025
+ };
3026
+
3027
+ $.translate = (x, y, z) => {
3028
+ if (!x && !y && !z) return;
3029
+ // Update the translation values
3030
+ $._matrix[3] += x;
3031
+ $._matrix[7] += y;
3032
+ $._matrix[11] += z || 0;
3033
+ $._matrixDirty = true;
3034
+ };
3035
+
3036
+ $.rotate = (r) => {
3037
+ if (!r) return;
3038
+ if ($._angleMode == 'degrees') r = $.radians(r);
3039
+
3040
+ let cosR = Math.cos(r);
3041
+ let sinR = Math.sin(r);
3042
+
3043
+ let m0 = $._matrix[0],
3044
+ m1 = $._matrix[1],
3045
+ m4 = $._matrix[4],
3046
+ m5 = $._matrix[5];
3047
+ if (!m0 && !m1 && !m4 && !m5) {
3048
+ $._matrix[0] = cosR;
3049
+ $._matrix[1] = sinR;
3050
+ $._matrix[4] = -sinR;
3051
+ $._matrix[5] = cosR;
3052
+ } else {
3053
+ $._matrix[0] = m0 * cosR + m4 * sinR;
3054
+ $._matrix[1] = m1 * cosR + m5 * sinR;
3055
+ $._matrix[4] = m0 * -sinR + m4 * cosR;
3056
+ $._matrix[5] = m1 * -sinR + m5 * cosR;
3057
+ }
3058
+
3059
+ $._matrixDirty = true;
3060
+ };
3061
+
3062
+ $.scale = (sx = 1, sy, sz = 1) => {
3063
+ sy ??= sx;
3064
+
3065
+ $._matrix[0] *= sx;
3066
+ $._matrix[5] *= sy;
3067
+ $._matrix[10] *= sz;
3068
+
3069
+ $._matrixDirty = true;
3070
+ };
3071
+
3072
+ // Function to save the current matrix state if dirty
3073
+ $._saveMatrix = () => {
3074
+ $.transformStates.push($._matrix.slice());
3075
+ $._transformIndex = $.transformStates.length - 1;
3076
+ $._matrixDirty = false;
3077
+ };
2957
3078
 
2958
3079
  $._beginRender = () => {
2959
3080
  $.encoder = Q5.device.createCommandEncoder();
@@ -2970,12 +3091,38 @@ Q5.renderers.webgpu.canvas = ($, q) => {
2970
3091
  };
2971
3092
 
2972
3093
  $._render = () => {
3094
+ $.pass.setBindGroup(0, envBindGroup);
3095
+
3096
+ if (transformStates.length > 1 || !transformBindGroup) {
3097
+ const transformBuffer = Q5.device.createBuffer({
3098
+ size: transformStates.length * 64, // Size of 16 floats
3099
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
3100
+ });
3101
+
3102
+ Q5.device.queue.writeBuffer(transformBuffer, 0, new Float32Array(transformStates.flat()));
3103
+
3104
+ transformBindGroup = Q5.device.createBindGroup({
3105
+ layout: $.bindGroupLayouts[1],
3106
+ entries: [
3107
+ {
3108
+ binding: 0,
3109
+ resource: {
3110
+ buffer: transformBuffer
3111
+ }
3112
+ }
3113
+ ]
3114
+ });
3115
+ }
3116
+
3117
+ $.pass.setBindGroup(1, transformBindGroup);
3118
+
2973
3119
  // run pre-render methods
2974
3120
  for (let m of $._hooks.preRender) m();
2975
3121
 
2976
3122
  $.pass.setPipeline($.pipelines[0]);
2977
3123
 
2978
- let drawStack = $.drawStack; // local variables used for performance
3124
+ // local variables used for performance
3125
+ let drawStack = $.drawStack;
2979
3126
  let o = 0; // vertex offset
2980
3127
  for (let i = 0; i < drawStack.length; i++) {
2981
3128
  $.pass.draw(drawStack[i], 1, o, 0);
@@ -2992,30 +3139,41 @@ Q5.renderers.webgpu.canvas = ($, q) => {
2992
3139
  // clear the stacks for the next frame
2993
3140
  $.verticesStack.length = 0;
2994
3141
  $.drawStack.length = 0;
2995
- $.colorsStack.length = 0;
3142
+ $.colorsStack.length = 4;
2996
3143
  $.pipelinesStack.length = 0;
2997
- $._colorIndex = -1;
3144
+ $._colorIndex = 0;
3145
+ rotation = 0;
3146
+ $.resetMatrix();
3147
+ $._matrixDirty = false;
3148
+ $.transformStates.length = 1;
3149
+ $._transformIndexStack.length = 0;
2998
3150
  };
2999
3151
 
3000
3152
  $.fill = (r, g, b, a = 1) => {
3153
+ if (typeof r == 'string') r = Q5.color(r);
3001
3154
  // grayscale mode `fill(1, 0.5)`
3002
3155
  if (b == undefined) {
3003
3156
  a = g;
3004
3157
  g = b = r;
3005
3158
  }
3006
- let levels;
3007
- if (r._q5Color) levels = r.levels;
3008
- else levels = [r, g, b, a];
3009
-
3010
- colorsStack.push(...levels);
3159
+ if (r._q5Color) colorsStack.push(...r.levels);
3160
+ else colorsStack.push(r, g, b, a);
3011
3161
  $._colorIndex++;
3012
3162
  };
3163
+ $.noFill = () => colorsStack.push(0, 0, 0, 0);
3164
+ $.stroke = () => {};
3165
+ $.noStroke = () => {};
3166
+
3167
+ $.clear = () => {};
3013
3168
  };
3014
3169
 
3015
3170
  Q5.webgpu = async function (scope, parent) {
3016
3171
  if (!navigator.gpu) {
3017
3172
  console.error('q5 WebGPU not supported on this browser!');
3018
- return new Q5(scope, parent);
3173
+ let q = new Q5(scope, parent);
3174
+ q.colorMode('rgb', 1);
3175
+ q._beginRender = () => q.translate(q.canvas.hw, q.canvas.hh);
3176
+ return q;
3019
3177
  }
3020
3178
  let adapter = await navigator.gpu.requestAdapter();
3021
3179
  if (!adapter) throw new Error('No appropriate GPUAdapter found.');
@@ -3029,23 +3187,31 @@ Q5.renderers.webgpu.drawing = ($, q) => {
3029
3187
  let verticesStack, drawStack, colorsStack;
3030
3188
 
3031
3189
  $._hooks.postCanvas.push(() => {
3190
+ let colorsLayout = Q5.device.createBindGroupLayout({
3191
+ entries: [
3192
+ {
3193
+ binding: 0,
3194
+ visibility: GPUShaderStage.FRAGMENT,
3195
+ buffer: {
3196
+ type: 'read-only-storage',
3197
+ hasDynamicOffset: false
3198
+ }
3199
+ }
3200
+ ]
3201
+ });
3202
+
3203
+ $.bindGroupLayouts.push(colorsLayout);
3204
+
3032
3205
  verticesStack = $.verticesStack;
3033
3206
  drawStack = $.drawStack;
3034
3207
  colorsStack = $.colorsStack;
3035
3208
 
3036
3209
  let vertexBufferLayout = {
3037
- arrayStride: 12, // 2 coordinates + 1 color index * 4 bytes each
3210
+ arrayStride: 16, // 2 coordinates + 1 color index + 1 transform index * 4 bytes each
3038
3211
  attributes: [
3039
- {
3040
- format: 'float32x2',
3041
- offset: 0,
3042
- shaderLocation: 0 // position
3043
- },
3044
- {
3045
- format: 'float32',
3046
- offset: 8,
3047
- shaderLocation: 1 // colorIndex
3048
- }
3212
+ { format: 'float32x2', offset: 0, shaderLocation: 0 }, // position
3213
+ { format: 'float32', offset: 8, shaderLocation: 1 }, // colorIndex
3214
+ { format: 'float32', offset: 12, shaderLocation: 2 } // transformIndex
3049
3215
  ]
3050
3216
  };
3051
3217
 
@@ -3056,10 +3222,23 @@ struct VertexOutput {
3056
3222
  @location(1) colorIndex: f32
3057
3223
  };
3058
3224
 
3225
+ struct Uniforms {
3226
+ halfWidth: f32,
3227
+ halfHeight: f32
3228
+ };
3229
+
3230
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3231
+ @group(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
3232
+
3059
3233
  @vertex
3060
- fn vertexMain(@location(0) pos: vec2<f32>, @location(1) colorIndex: f32) -> VertexOutput {
3234
+ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput {
3235
+ var vert = vec4<f32>(pos, 0.0, 1.0);
3236
+ vert *= transforms[i32(transformIndex)];
3237
+ vert.x /= uniforms.halfWidth;
3238
+ vert.y /= uniforms.halfHeight;
3239
+
3061
3240
  var output: VertexOutput;
3062
- output.position = vec4<f32>(pos, 0.0, 1.0);
3241
+ output.position = vert;
3063
3242
  output.colorIndex = colorIndex;
3064
3243
  return output;
3065
3244
  }
@@ -3068,7 +3247,7 @@ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) colorIndex: f32) -> Vert
3068
3247
 
3069
3248
  let fragmentShader = Q5.device.createShaderModule({
3070
3249
  code: `
3071
- @group(0) @binding(0) var<storage, read> uColors : array<vec4<f32>>;
3250
+ @group(2) @binding(0) var<storage, read> uColors : array<vec4<f32>>;
3072
3251
 
3073
3252
  @fragment
3074
3253
  fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
@@ -3078,45 +3257,104 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3078
3257
  `
3079
3258
  });
3080
3259
 
3081
- let bindGroupLayout = Q5.device.createBindGroupLayout({
3082
- entries: [
3083
- {
3084
- binding: 0,
3085
- visibility: GPUShaderStage.FRAGMENT,
3086
- buffer: {
3087
- type: 'read-only-storage',
3088
- hasDynamicOffset: false
3089
- }
3090
- }
3091
- ]
3092
- });
3093
-
3094
3260
  let pipelineLayout = Q5.device.createPipelineLayout({
3095
- bindGroupLayouts: [bindGroupLayout]
3261
+ bindGroupLayouts: $.bindGroupLayouts
3096
3262
  });
3097
3263
 
3098
- $.pipelines[0] = Q5.device.createRenderPipeline({
3099
- layout: pipelineLayout,
3100
- vertex: {
3101
- module: vertexShader,
3102
- entryPoint: 'vertexMain',
3103
- buffers: [vertexBufferLayout]
3104
- },
3105
- fragment: {
3106
- module: fragmentShader,
3107
- entryPoint: 'fragmentMain',
3108
- targets: [
3109
- {
3110
- format: $._canvasFormat
3111
- }
3112
- ]
3264
+ $._createPipeline = (blendConfig) => {
3265
+ return Q5.device.createRenderPipeline({
3266
+ layout: pipelineLayout,
3267
+ vertex: {
3268
+ module: vertexShader,
3269
+ entryPoint: 'vertexMain',
3270
+ buffers: [vertexBufferLayout]
3271
+ },
3272
+ fragment: {
3273
+ module: fragmentShader,
3274
+ entryPoint: 'fragmentMain',
3275
+ targets: [
3276
+ {
3277
+ format: 'bgra8unorm',
3278
+ blend: blendConfig
3279
+ }
3280
+ ]
3281
+ },
3282
+ primitive: {
3283
+ topology: 'triangle-list'
3284
+ }
3285
+ });
3286
+ };
3287
+
3288
+ $.pipelines[0] = $._createPipeline(blendConfigs.normal);
3289
+ });
3290
+
3291
+ // prettier-ignore
3292
+ let blendFactors = [
3293
+ 'zero', // 0
3294
+ 'one', // 1
3295
+ 'src-alpha', // 2
3296
+ 'one-minus-src-alpha', // 3
3297
+ 'dst', // 4
3298
+ 'dst-alpha', // 5
3299
+ 'one-minus-dst-alpha', // 6
3300
+ 'one-minus-src' // 7
3301
+ ];
3302
+ let blendOps = [
3303
+ 'add', // 0
3304
+ 'subtract', // 1
3305
+ 'reverse-subtract', // 2
3306
+ 'min', // 3
3307
+ 'max' // 4
3308
+ ];
3309
+
3310
+ const blendModes = {
3311
+ normal: [2, 3, 0, 2, 3, 0],
3312
+ lighter: [2, 1, 0, 2, 1, 0],
3313
+ subtract: [2, 1, 2, 2, 1, 2],
3314
+ multiply: [4, 0, 0, 5, 0, 0],
3315
+ screen: [1, 3, 0, 1, 3, 0],
3316
+ darken: [1, 3, 3, 1, 3, 3],
3317
+ lighten: [1, 3, 4, 1, 3, 4],
3318
+ overlay: [2, 3, 0, 2, 3, 0],
3319
+ hard_light: [2, 3, 0, 2, 3, 0],
3320
+ soft_light: [2, 3, 0, 2, 3, 0],
3321
+ difference: [2, 3, 2, 2, 3, 2],
3322
+ exclusion: [2, 3, 0, 2, 3, 0],
3323
+ color_dodge: [1, 7, 0, 1, 7, 0],
3324
+ color_burn: [6, 1, 0, 6, 1, 0],
3325
+ linear_dodge: [2, 1, 0, 2, 1, 0],
3326
+ linear_burn: [2, 7, 1, 2, 7, 1],
3327
+ vivid_light: [2, 7, 0, 2, 7, 0],
3328
+ pin_light: [2, 7, 0, 2, 7, 0],
3329
+ hard_mix: [2, 7, 0, 2, 7, 0]
3330
+ };
3331
+
3332
+ $.blendConfigs = {};
3333
+
3334
+ Object.entries(blendModes).forEach(([name, mode]) => {
3335
+ $.blendConfigs[name] = {
3336
+ color: {
3337
+ srcFactor: blendFactors[mode[0]],
3338
+ dstFactor: blendFactors[mode[1]],
3339
+ operation: blendOps[mode[2]]
3113
3340
  },
3114
- primitive: {
3115
- topology: 'triangle-list'
3341
+ alpha: {
3342
+ srcFactor: blendFactors[mode[3]],
3343
+ dstFactor: blendFactors[mode[4]],
3344
+ operation: blendOps[mode[5]]
3116
3345
  }
3117
- });
3346
+ };
3118
3347
  });
3119
3348
 
3349
+ $._blendMode = 'normal';
3350
+ $.blendMode = (mode) => {
3351
+ if (mode == $._blendMode) return;
3352
+ if (mode == 'source-over') mode = 'normal';
3353
+ mode = mode.toLowerCase().replace(/[ -]/g, '_');
3354
+ $._blendMode = mode;
3355
+ $.pipelines[0] = $._createPipeline($.blendConfigs[mode]);
3356
+ };
3357
+
3120
3358
  let shapeVertices;
3121
3359
 
3122
3360
  $.beginShape = () => {
@@ -3124,35 +3362,40 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3124
3362
  };
3125
3363
 
3126
3364
  $.vertex = (x, y) => {
3127
- shapeVertices.push(x / $.canvas.hw, -y / $.canvas.hh, $._colorIndex);
3365
+ if ($._matrixDirty) $._saveMatrix();
3366
+ shapeVertices.push(x, -y, $._colorIndex, $._transformIndex);
3128
3367
  };
3129
3368
 
3130
3369
  $.endShape = (close) => {
3131
- if (shapeVertices.length < 6) {
3370
+ let v = shapeVertices;
3371
+ if (v.length < 12) {
3132
3372
  throw new Error('A shape must have at least 3 vertices.');
3133
3373
  }
3134
3374
  if (close) {
3135
3375
  // Close the shape by adding the first vertex at the end
3136
- shapeVertices.push(shapeVertices[0], shapeVertices[1], shapeVertices[2]);
3376
+ v.push(v[0], v[1], v[2], v[3]);
3137
3377
  }
3138
3378
  // Convert the shape to triangles
3139
3379
  let triangles = [];
3140
- for (let i = 3; i < shapeVertices.length; i += 3) {
3380
+ for (let i = 4; i < v.length; i += 4) {
3141
3381
  triangles.push(
3142
- shapeVertices[0],
3143
- shapeVertices[1],
3144
- shapeVertices[2], // First vertex
3145
- shapeVertices[i - 3],
3146
- shapeVertices[i - 2],
3147
- shapeVertices[i - 1], // Previous vertex
3148
- shapeVertices[i],
3149
- shapeVertices[i + 1],
3150
- shapeVertices[i + 2] // Current vertex
3382
+ v[0], // First vertex
3383
+ v[1],
3384
+ v[2],
3385
+ v[3],
3386
+ v[i - 4], // Previous vertex
3387
+ v[i - 3],
3388
+ v[i - 2],
3389
+ v[i - 1],
3390
+ v[i], // Current vertex
3391
+ v[i + 1],
3392
+ v[i + 2],
3393
+ v[i + 3]
3151
3394
  );
3152
3395
  }
3153
3396
 
3154
3397
  verticesStack.push(...triangles);
3155
- drawStack.push(triangles.length / 3);
3398
+ drawStack.push(triangles.length / 4);
3156
3399
  shapeVertices = [];
3157
3400
  };
3158
3401
 
@@ -3167,37 +3410,47 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3167
3410
  $.rect = (x, y, w, h) => {
3168
3411
  let hw = w / 2;
3169
3412
  let hh = h / 2;
3170
- // convert the coordinates from pixel space to NDC space
3171
- let left = (x - hw) / $.canvas.hw;
3172
- let right = (x + hw) / $.canvas.hw;
3173
- let top = -(y - hh) / $.canvas.hh; // y is inverted in WebGPU
3174
- let bottom = -(y + hh) / $.canvas.hh; // y is inverted in WebGPU
3413
+
3414
+ let left = x - hw;
3415
+ let right = x + hw;
3416
+ let top = -(y - hh); // y is inverted in WebGPU
3417
+ let bottom = -(y + hh);
3175
3418
 
3176
3419
  let ci = $._colorIndex;
3420
+ if ($._matrixDirty) $._saveMatrix();
3421
+ let ti = $._transformIndex;
3177
3422
  // two triangles make a rectangle
3178
3423
  verticesStack.push(
3179
3424
  left,
3180
3425
  top,
3181
3426
  ci,
3427
+ ti,
3182
3428
  right,
3183
3429
  top,
3184
3430
  ci,
3431
+ ti,
3185
3432
  left,
3186
3433
  bottom,
3187
3434
  ci,
3435
+ ti,
3188
3436
  right,
3189
3437
  top,
3190
3438
  ci,
3439
+ ti,
3191
3440
  left,
3192
3441
  bottom,
3193
3442
  ci,
3443
+ ti,
3194
3444
  right,
3195
3445
  bottom,
3196
- ci
3446
+ ci,
3447
+ ti
3197
3448
  );
3198
3449
  drawStack.push(6);
3199
3450
  };
3200
3451
 
3452
+ $.background = () => {};
3453
+
3201
3454
  /**
3202
3455
  * Derived from: ceil(Math.log(d) * 7) * 2 - ceil(28)
3203
3456
  * This lookup table is used for better performance.
@@ -3240,14 +3493,11 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3240
3493
  let a = Math.max(w, 1) / 2;
3241
3494
  let b = w == h ? a : Math.max(h, 1) / 2;
3242
3495
 
3243
- x /= $.canvas.hw;
3244
- y /= -$.canvas.hh;
3245
- a /= $.canvas.hw;
3246
- b /= -$.canvas.hh;
3247
-
3248
3496
  let t = 0; // theta
3249
3497
  const angleIncrement = $.TAU / n;
3250
3498
  const ci = $._colorIndex;
3499
+ if ($._matrixDirty) $._saveMatrix();
3500
+ const ti = $._transformIndex;
3251
3501
  let vx1, vy1, vx2, vy2;
3252
3502
  for (let i = 0; i <= n; i++) {
3253
3503
  vx1 = vx2;
@@ -3258,7 +3508,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3258
3508
 
3259
3509
  if (i == 0) continue;
3260
3510
 
3261
- verticesStack.push(x, y, ci, vx1, vy1, ci, vx2, vy2, ci);
3511
+ verticesStack.push(x, y, ci, ti, vx1, vy1, ci, ti, vx2, vy2, ci, ti);
3262
3512
  }
3263
3513
 
3264
3514
  drawStack.push(n * 3);
@@ -3266,10 +3516,6 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3266
3516
 
3267
3517
  $.circle = (x, y, d) => $.ellipse(x, y, d, d);
3268
3518
 
3269
- $.noStroke = () => {};
3270
-
3271
- $.background = () => {};
3272
-
3273
3519
  $._hooks.preRender.push(() => {
3274
3520
  const vertexBuffer = Q5.device.createBuffer({
3275
3521
  size: verticesStack.length * 6,
@@ -3279,20 +3525,20 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3279
3525
  Q5.device.queue.writeBuffer(vertexBuffer, 0, new Float32Array(verticesStack));
3280
3526
  $.pass.setVertexBuffer(0, vertexBuffer);
3281
3527
 
3282
- const colorBuffer = Q5.device.createBuffer({
3528
+ const colorsBuffer = Q5.device.createBuffer({
3283
3529
  size: colorsStack.length * 4,
3284
3530
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
3285
3531
  });
3286
3532
 
3287
- Q5.device.queue.writeBuffer(colorBuffer, 0, new Float32Array(colorsStack));
3533
+ Q5.device.queue.writeBuffer(colorsBuffer, 0, new Float32Array(colorsStack));
3288
3534
 
3289
- const bindGroup = Q5.device.createBindGroup({
3290
- layout: $.pipelines[0].getBindGroupLayout(0),
3535
+ const colorsBindGroup = Q5.device.createBindGroup({
3536
+ layout: $.bindGroupLayouts[2],
3291
3537
  entries: [
3292
3538
  {
3293
3539
  binding: 0,
3294
3540
  resource: {
3295
- buffer: colorBuffer,
3541
+ buffer: colorsBuffer,
3296
3542
  offset: 0,
3297
3543
  size: colorsStack.length * 4
3298
3544
  }
@@ -3301,7 +3547,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3301
3547
  });
3302
3548
 
3303
3549
  // set the bind group once before rendering
3304
- $.pass.setBindGroup(0, bindGroup);
3550
+ $.pass.setBindGroup(2, colorsBindGroup);
3305
3551
  });
3306
3552
  };
3307
3553
  Q5.renderers.webgpu.image = ($, q) => {};