q5 3.0.6 → 3.1.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.
Files changed (5) hide show
  1. package/deno.json +1 -1
  2. package/package.json +1 -1
  3. package/q5.d.ts +48 -2
  4. package/q5.js +675 -384
  5. package/q5.min.js +2 -2
package/q5.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * q5.js
3
- * @version 3.0
3
+ * @version 3.1
4
4
  * @author quinton-ashley
5
5
  * @contributors evanalulu, Tezumie, ormaq, Dukemz, LingDong-
6
6
  * @license LGPL-3.0
@@ -385,7 +385,7 @@ function createCanvas(w, h, opt) {
385
385
  }
386
386
  }
387
387
 
388
- Q5.version = Q5.VERSION = '3.0';
388
+ Q5.version = Q5.VERSION = '3.1';
389
389
 
390
390
  if (typeof document == 'object') {
391
391
  document.addEventListener('DOMContentLoaded', () => {
@@ -4926,8 +4926,8 @@ struct Q5 {
4926
4926
  frameLayout,
4927
4927
  frameSampler,
4928
4928
  frameBindGroup,
4929
- colorIndex = 1,
4930
- colorStackIndex = 8,
4929
+ colorIndex = 2,
4930
+ colorStackIndex = 12,
4931
4931
  prevFramePL = 0,
4932
4932
  framePL = 0;
4933
4933
 
@@ -4945,6 +4945,7 @@ struct Q5 {
4945
4945
 
4946
4946
  // prettier-ignore
4947
4947
  colorStack.set([
4948
+ 0, 0, 0, 0, // transparent
4948
4949
  0, 0, 0, 1, // black
4949
4950
  1, 1, 1, 1 // white
4950
4951
  ]);
@@ -5166,12 +5167,13 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5166
5167
  doStroke = true,
5167
5168
  fillSet = false,
5168
5169
  strokeSet = false,
5169
- strokeIdx = 0,
5170
- fillIdx = 1,
5171
- tintIdx = 1,
5170
+ strokeIdx = 1,
5171
+ fillIdx = 2,
5172
+ tintIdx = 2,
5172
5173
  globalAlpha = 1,
5173
5174
  sw = 1, // stroke weight
5174
- hsw = 0.5, // half the stroke weight
5175
+ hsw = 0.5, // half of the stroke weight
5176
+ qsw = 0.25, // quarter of the stroke weight
5175
5177
  scaledHSW = 0.5;
5176
5178
 
5177
5179
  $.fill = (r, g, b, a) => {
@@ -5202,6 +5204,7 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5202
5204
  v = Math.abs(v);
5203
5205
  sw = v;
5204
5206
  hsw = v / 2;
5207
+ qsw = v / 4;
5205
5208
  scaledHSW = hsw * _scale;
5206
5209
  };
5207
5210
 
@@ -5560,7 +5563,11 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5560
5563
  $.pop();
5561
5564
  } else {
5562
5565
  addColor(r, g, b, a);
5563
- addRect(-c.hw, c.hh, c.hw, c.hh, c.hw, -c.hh, -c.hw, -c.hh, colorIndex, 0);
5566
+ let lx = -c.hw,
5567
+ rx = c.hw,
5568
+ ty = -c.hh,
5569
+ by = c.hh;
5570
+ addQuad(lx, ty, rx, ty, rx, by, lx, by, colorIndex, 0);
5564
5571
  }
5565
5572
  };
5566
5573
 
@@ -5695,6 +5702,8 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5695
5702
  }
5696
5703
  }
5697
5704
 
5705
+ // prepare to render text
5706
+
5698
5707
  if (charStack.length) {
5699
5708
  // calculate total buffer size for text data
5700
5709
  let totalTextSize = 0;
@@ -5741,9 +5750,33 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5741
5750
  });
5742
5751
  }
5743
5752
 
5753
+ // prepare to render rectangles
5754
+
5755
+ // prettier-ignore
5756
+ Q5.device.queue.writeBuffer(
5757
+ rectBuffer,
5758
+ 0,
5759
+ rectStack.buffer,
5760
+ rectStack.byteOffset,
5761
+ rectStackIdx * 4
5762
+ );
5763
+
5764
+ // prepare to render ellipses
5765
+
5766
+ // prettier-ignore
5767
+ Q5.device.queue.writeBuffer(
5768
+ ellipseBuffer,
5769
+ 0,
5770
+ ellipseStack.buffer,
5771
+ ellipseStack.byteOffset,
5772
+ ellipseStackIdx * 4
5773
+ );
5774
+
5744
5775
  let drawVertOffset = 0,
5745
5776
  imageVertOffset = 0,
5746
5777
  textCharOffset = 0,
5778
+ rectIdx = 0,
5779
+ ellipseIdx = 0,
5747
5780
  curPipelineIndex = -1;
5748
5781
 
5749
5782
  for (let i = 0; i < drawStack.length; i += 2) {
@@ -5762,9 +5795,25 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5762
5795
 
5763
5796
  curPipelineIndex = drawStack[i];
5764
5797
  pass.setPipeline($._pipelines[curPipelineIndex]);
5798
+
5799
+ if (curPipelineIndex == 5) {
5800
+ pass.setIndexBuffer(rectIndexBuffer, 'uint16');
5801
+ pass.setBindGroup(1, rectBindGroup);
5802
+ } else if (curPipelineIndex == 6) {
5803
+ pass.setIndexBuffer(ellipseIndexBuffer, 'uint16');
5804
+ pass.setBindGroup(1, ellipseBindGroup);
5805
+ }
5765
5806
  }
5766
5807
 
5767
- if (curPipelineIndex == 4 || curPipelineIndex >= 4000) {
5808
+ if (curPipelineIndex == 6) {
5809
+ // draw an ellipse
5810
+ pass.drawIndexed(18, v, 0, 0, ellipseIdx);
5811
+ ellipseIdx += v;
5812
+ } else if (curPipelineIndex == 5) {
5813
+ // draw a rectangle
5814
+ pass.drawIndexed(6, v, 0, 0, rectIdx);
5815
+ rectIdx += v;
5816
+ } else if (curPipelineIndex == 4 || curPipelineIndex >= 4000) {
5768
5817
  // draw text
5769
5818
  let o = drawStack[i + 2];
5770
5819
  pass.setBindGroup(1, fontsArr[o].bindGroup);
@@ -5826,8 +5875,8 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5826
5875
 
5827
5876
  // clear the stacks for the next frame
5828
5877
  drawStack.splice(0, drawStack.length);
5829
- colorIndex = 1;
5830
- colorStackIndex = 8;
5878
+ colorIndex = 2;
5879
+ colorStackIndex = 12;
5831
5880
  matrices = [matrices[0]];
5832
5881
  matricesIdxStack = [];
5833
5882
 
@@ -5841,6 +5890,10 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5841
5890
  vidFrames = 0;
5842
5891
  charStack = [];
5843
5892
  textStack = [];
5893
+ rectStack = new Float32Array(Q5.MAX_RECTS * 16);
5894
+ rectStackIdx = 0;
5895
+ ellipseStack = new Float32Array(Q5.MAX_ELLIPSES * 16);
5896
+ ellipseStackIdx = 0;
5844
5897
 
5845
5898
  // destroy buffers
5846
5899
  Q5.device.queue.onSubmittedWorkDone().then(() => {
@@ -5903,8 +5956,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
5903
5956
  let shapesVertStack = new Float32Array($._isGraphics ? 1000 : 1e7),
5904
5957
  shapesVertIdx = 0;
5905
5958
  const TAU = Math.PI * 2,
5906
- HALF_PI = Math.PI / 2,
5907
- THREE_HALF_PI = Math.PI * 1.5;
5959
+ HALF_PI = Math.PI / 2;
5908
5960
 
5909
5961
  let shapesVertBuffLayout = {
5910
5962
  arrayStride: 16, // 4 floats * 4 bytes
@@ -5915,14 +5967,14 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
5915
5967
  ]
5916
5968
  };
5917
5969
 
5918
- let pipelineLayout = Q5.device.createPipelineLayout({
5970
+ let shapesPipelineLayout = Q5.device.createPipelineLayout({
5919
5971
  label: 'shapesPipelineLayout',
5920
5972
  bindGroupLayouts: $._bindGroupLayouts
5921
5973
  });
5922
5974
 
5923
5975
  $._pipelineConfigs[1] = {
5924
5976
  label: 'shapesPipeline',
5925
- layout: pipelineLayout,
5977
+ layout: shapesPipelineLayout,
5926
5978
  vertex: {
5927
5979
  module: shapesShader,
5928
5980
  entryPoint: 'vertexMain',
@@ -5949,344 +6001,6 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
5949
6001
  shapesVertIdx = i;
5950
6002
  };
5951
6003
 
5952
- const addRect = (x1, y1, x2, y2, x3, y3, x4, y4, ci, ti) => {
5953
- let v = shapesVertStack,
5954
- i = shapesVertIdx;
5955
-
5956
- v[i++] = x1;
5957
- v[i++] = y1;
5958
- v[i++] = ci;
5959
- v[i++] = ti;
5960
-
5961
- v[i++] = x2;
5962
- v[i++] = y2;
5963
- v[i++] = ci;
5964
- v[i++] = ti;
5965
-
5966
- v[i++] = x4;
5967
- v[i++] = y4;
5968
- v[i++] = ci;
5969
- v[i++] = ti;
5970
-
5971
- v[i++] = x3;
5972
- v[i++] = y3;
5973
- v[i++] = ci;
5974
- v[i++] = ti;
5975
-
5976
- shapesVertIdx = i;
5977
- drawStack.push(shapesPL, 4);
5978
- };
5979
-
5980
- const addArc = (x, y, a, b, startAngle, endAngle, n, ci, ti) => {
5981
- let angleRange = endAngle - startAngle;
5982
- let angleIncrement = angleRange / n;
5983
- let t = startAngle;
5984
-
5985
- let v = shapesVertStack,
5986
- i = shapesVertIdx;
5987
-
5988
- for (let j = 0; j <= n; j++) {
5989
- // add center vertex
5990
- v[i++] = x;
5991
- v[i++] = y;
5992
- v[i++] = ci;
5993
- v[i++] = ti;
5994
-
5995
- // calculate perimeter vertex
5996
- let vx = x + a * Math.cos(t);
5997
- let vy = y + b * Math.sin(t);
5998
-
5999
- // add perimeter vertex
6000
- v[i++] = vx;
6001
- v[i++] = vy;
6002
- v[i++] = ci;
6003
- v[i++] = ti;
6004
-
6005
- t += angleIncrement;
6006
- }
6007
-
6008
- shapesVertIdx = i;
6009
- drawStack.push(shapesPL, (n + 1) * 2);
6010
- };
6011
-
6012
- const addArcStroke = (x, y, outerA, outerB, innerA, innerB, startAngle, endAngle, n, ci, ti) => {
6013
- let angleRange = endAngle - startAngle;
6014
- let angleIncrement = angleRange / n;
6015
- let t = startAngle;
6016
-
6017
- let v = shapesVertStack,
6018
- i = shapesVertIdx;
6019
-
6020
- for (let j = 0; j <= n; j++) {
6021
- // Outer vertex
6022
- let vxOuter = x + outerA * Math.cos(t);
6023
- let vyOuter = y + outerB * Math.sin(t);
6024
-
6025
- // Inner vertex
6026
- let vxInner = x + innerA * Math.cos(t);
6027
- let vyInner = y + innerB * Math.sin(t);
6028
-
6029
- // Add vertices for triangle strip
6030
- v[i++] = vxOuter;
6031
- v[i++] = vyOuter;
6032
- v[i++] = ci;
6033
- v[i++] = ti;
6034
-
6035
- v[i++] = vxInner;
6036
- v[i++] = vyInner;
6037
- v[i++] = ci;
6038
- v[i++] = ti;
6039
-
6040
- t += angleIncrement;
6041
- }
6042
-
6043
- shapesVertIdx = i;
6044
- drawStack.push(shapesPL, (n + 1) * 2);
6045
- };
6046
-
6047
- let _rectMode = 'corner';
6048
-
6049
- $.rectMode = (x) => (_rectMode = x);
6050
-
6051
- $.rect = (x, y, w, h, rr = 0) => {
6052
- h ??= w;
6053
- let [l, r, t, b] = calcBox(x, y, w, h, _rectMode);
6054
- let ci, ti;
6055
- if (matrixDirty) saveMatrix();
6056
- ti = matrixIdx;
6057
-
6058
- if (!rr) {
6059
- if (doFill) {
6060
- ci = fillIdx;
6061
- addRect(l, t, r, t, r, b, l, b, ci, ti);
6062
- }
6063
-
6064
- if (doStroke) {
6065
- ci = strokeIdx;
6066
-
6067
- // Calculate stroke positions
6068
- let lsw = l - hsw,
6069
- rsw = r + hsw,
6070
- tsw = t + hsw,
6071
- bsw = b - hsw,
6072
- lpsw = l + hsw,
6073
- rpsw = r - hsw,
6074
- tpsw = t - hsw,
6075
- bpsw = b + hsw;
6076
-
6077
- addRect(lsw, tpsw, rsw, tpsw, rsw, tsw, lsw, tsw, ci, ti); // Top
6078
- addRect(lsw, bsw, rsw, bsw, rsw, bpsw, lsw, bpsw, ci, ti); // Bottom
6079
-
6080
- // Adjust side strokes to avoid overlapping corners
6081
- tsw = t - hsw;
6082
- bsw = b + hsw;
6083
-
6084
- addRect(lsw, tsw, lpsw, tsw, lpsw, bsw, lsw, bsw, ci, ti); // Left
6085
- addRect(rpsw, tsw, rsw, tsw, rsw, bsw, rpsw, bsw, ci, ti); // Right
6086
- }
6087
- return;
6088
- }
6089
-
6090
- l += rr;
6091
- r -= rr;
6092
- t += rr;
6093
- b -= rr;
6094
-
6095
- // Clamp radius
6096
- rr = Math.min(rr, Math.min(w, h) / 2);
6097
-
6098
- let n = getArcSegments(rr * _scale);
6099
-
6100
- let trr = t - rr,
6101
- brr = b + rr,
6102
- lrr = l - rr,
6103
- rrr = r + rr;
6104
-
6105
- if (doFill) {
6106
- ci = fillIdx;
6107
- // Corner arcs
6108
- addArc(l, t, rr, -rr, HALF_PI, Math.PI, n, ci, ti); // top-left
6109
- addArc(r, t, rr, -rr, HALF_PI, 0, n, ci, ti); // top-right
6110
- addArc(r, b, rr, -rr, 0, -HALF_PI, n, ci, ti); // bottom-right
6111
- addArc(l, b, rr, -rr, -HALF_PI, -Math.PI, n, ci, ti); // bottom-left
6112
-
6113
- addRect(l, trr, r, trr, r, brr, l, brr, ci, ti); // center
6114
- addRect(l, t, lrr, t, lrr, b, l, b, ci, ti); // Left
6115
- addRect(rrr, t, r, t, r, b, rrr, b, ci, ti); // Right
6116
- }
6117
-
6118
- if (doStroke) {
6119
- ci = strokeIdx;
6120
-
6121
- let outerA = rr + hsw,
6122
- outerB = rr + hsw,
6123
- innerA = rr - hsw,
6124
- innerB = rr - hsw;
6125
-
6126
- // Corner arc strokes
6127
- addArcStroke(l, t, outerA, -outerB, innerA, -innerB, Math.PI, HALF_PI, n, ci, ti); // top-left
6128
- addArcStroke(r, t, outerA, -outerB, innerA, -innerB, HALF_PI, 0, n, ci, ti); // top-right
6129
- addArcStroke(r, b, outerA, -outerB, innerA, -innerB, 0, -HALF_PI, n, ci, ti); // bottom-right
6130
- addArcStroke(l, b, outerA, -outerB, innerA, -innerB, -HALF_PI, -Math.PI, n, ci, ti); // bottom-left
6131
-
6132
- let lrrMin = lrr - hsw,
6133
- lrrMax = lrr + hsw,
6134
- rrrMin = rrr - hsw,
6135
- rrrMax = rrr + hsw,
6136
- trrMin = trr - hsw,
6137
- trrMax = trr + hsw,
6138
- brrMin = brr - hsw,
6139
- brrMax = brr + hsw;
6140
-
6141
- // Side strokes - positioned outside
6142
- addRect(lrrMin, t, lrrMax, t, lrrMax, b, lrrMin, b, ci, ti); // Left
6143
- addRect(rrrMin, t, rrrMax, t, rrrMax, b, rrrMin, b, ci, ti); // Right
6144
- addRect(l, trrMin, r, trrMin, r, trrMax, l, trrMax, ci, ti); // Top
6145
- addRect(l, brrMin, r, brrMin, r, brrMax, l, brrMax, ci, ti); // Bottom
6146
- }
6147
- };
6148
-
6149
- $.square = (x, y, s) => $.rect(x, y, s, s);
6150
-
6151
- $.plane = (x, y, w, h) => {
6152
- h ??= w;
6153
- let [l, r, t, b] = calcBox(x, y, w, h, 'center');
6154
- if (matrixDirty) saveMatrix();
6155
- addRect(l, t, r, t, r, b, l, b, fillIdx, matrixIdx);
6156
- };
6157
-
6158
- // prettier-ignore
6159
- const getArcSegments = (d) =>
6160
- d < 2 ? 6 :
6161
- d < 3 ? 8 :
6162
- d < 5 ? 10 :
6163
- d < 8 ? 12 :
6164
- d < 10 ? 14 :
6165
- d < 11 ? 16 :
6166
- d < 12 ? 18 :
6167
- d < 14 ? 20 :
6168
- d < 17 ? 22 :
6169
- d < 21 ? 24 :
6170
- d < 24 ? 26 :
6171
- d < 28 ? 28 :
6172
- d < 32 ? 30 :
6173
- d < 36 ? 32 :
6174
- d < 42 ? 34 :
6175
- d < 48 ? 36 :
6176
- d < 49 ? 38 :
6177
- d < 57 ? 40 :
6178
- d < 75 ? 44 :
6179
- d < 100 ? 48 :
6180
- d < 130 ? 52 :
6181
- d < 176 ? 56 :
6182
- d < 230 ? 60 :
6183
- d < 292 ? 64 :
6184
- d < 600 ? 70 :
6185
- d < 900 ? 80 :
6186
- d < 1200 ? 90 :
6187
- 100;
6188
-
6189
- let _ellipseMode = 'center';
6190
-
6191
- $.ellipseMode = (x) => (_ellipseMode = x);
6192
-
6193
- function applyEllipseMode(x, y, w, h) {
6194
- h ??= w;
6195
- let a, b;
6196
- if (_ellipseMode == $.CENTER) {
6197
- a = w / 2;
6198
- b = h / 2;
6199
- } else if (_ellipseMode == $.RADIUS) {
6200
- a = w;
6201
- b = h;
6202
- } else if (_ellipseMode == $.CORNER) {
6203
- x += w / 2;
6204
- y += h / 2;
6205
- a = w / 2;
6206
- b = h / 2;
6207
- } else if (_ellipseMode == $.CORNERS) {
6208
- x = (x + w) / 2;
6209
- y = (y + h) / 2;
6210
- a = (w - x) / 2;
6211
- b = (h - y) / 2;
6212
- }
6213
- return [x, y, a, b];
6214
- }
6215
-
6216
- $.ellipse = (x, y, w, h) => {
6217
- let a, b;
6218
- [x, y, a, b] = applyEllipseMode(x, y, w, h);
6219
-
6220
- let n = getArcSegments(Math.max(Math.abs(a), Math.abs(b)) * _scale);
6221
-
6222
- if (matrixDirty) saveMatrix();
6223
- let ti = matrixIdx;
6224
-
6225
- if (doFill) {
6226
- addArc(x, y, a, b, 0, TAU, n, fillIdx, ti);
6227
- }
6228
- if (doStroke) {
6229
- // Draw the stroke as a ring using triangle strips
6230
- addArcStroke(x, y, a + hsw, b + hsw, a - hsw, b - hsw, 0, TAU, n, strokeIdx, ti);
6231
- }
6232
- };
6233
-
6234
- $.circle = (x, y, d) => $.ellipse(x, y, d, d);
6235
-
6236
- $.arc = (x, y, w, h, start, stop) => {
6237
- if (start === stop) return $.ellipse(x, y, w, h);
6238
-
6239
- // Convert angles if needed
6240
- if ($._angleMode) {
6241
- start = $.radians(start);
6242
- stop = $.radians(stop);
6243
- }
6244
-
6245
- // Normalize angles
6246
- start %= TAU;
6247
- stop %= TAU;
6248
- if (start < 0) start += TAU;
6249
- if (stop < 0) stop += TAU;
6250
- if (start > stop) stop += TAU;
6251
- if (start == stop) return $.ellipse(x, y, w, h);
6252
-
6253
- // Calculate position based on ellipseMode
6254
- let a, b;
6255
- [x, y, a, b] = applyEllipseMode(x, y, w, h);
6256
-
6257
- if (matrixDirty) saveMatrix();
6258
- let ti = matrixIdx;
6259
- let n = getArcSegments(Math.max(Math.abs(a), Math.abs(b)) * _scale);
6260
-
6261
- // Draw fill
6262
- if (doFill) {
6263
- addArc(x, y, a, b, start, stop, n, fillIdx, ti);
6264
- }
6265
-
6266
- // Draw stroke
6267
- if (doStroke) {
6268
- addArcStroke(x, y, a + hsw, b + hsw, a - hsw, b - hsw, start, stop, n, strokeIdx, ti);
6269
- if (_strokeCap == 'round') {
6270
- addArc(x + a * Math.cos(start), y + b * Math.sin(start), hsw, hsw, 0, TAU, n, strokeIdx, ti);
6271
- addArc(x + a * Math.cos(stop), y + b * Math.sin(stop), hsw, hsw, 0, TAU, n, strokeIdx, ti);
6272
- }
6273
- }
6274
- };
6275
-
6276
- $.point = (x, y) => {
6277
- if (matrixDirty) saveMatrix();
6278
- let ti = matrixIdx,
6279
- ci = strokeIdx;
6280
-
6281
- if (scaledHSW < 1) {
6282
- let [l, r, t, b] = calcBox(x, y, sw, sw, 'corner');
6283
- addRect(l, t, r, t, r, b, l, b, ci, ti);
6284
- } else {
6285
- let n = getArcSegments(scaledHSW);
6286
- addArc(x, y, hsw, hsw, 0, TAU, n, ci, ti);
6287
- }
6288
- };
6289
-
6290
6004
  let _strokeCap = 'round',
6291
6005
  _strokeJoin = 'round';
6292
6006
 
@@ -6297,29 +6011,6 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6297
6011
  _strokeJoin = 'none';
6298
6012
  };
6299
6013
 
6300
- $.line = (x1, y1, x2, y2) => {
6301
- if (matrixDirty) saveMatrix();
6302
- let ti = matrixIdx,
6303
- ci = strokeIdx;
6304
-
6305
- // calculate the direction vector and length
6306
- let dx = x2 - x1,
6307
- dy = y2 - y1,
6308
- length = Math.hypot(dx, dy);
6309
-
6310
- // calculate the perpendicular vector for line thickness
6311
- let px = -(dy / length) * hsw,
6312
- py = (dx / length) * hsw;
6313
-
6314
- addRect(x1 + px, y1 + py, x1 - px, y1 - py, x2 - px, y2 - py, x2 + px, y2 + py, ci, ti);
6315
-
6316
- if (scaledHSW > 1 && _strokeCap != 'square') {
6317
- let n = getArcSegments(scaledHSW);
6318
- addArc(x1, y1, hsw, hsw, 0, TAU, n, ci, ti);
6319
- addArc(x2, y2, hsw, hsw, 0, TAU, n, ci, ti);
6320
- }
6321
- };
6322
-
6323
6014
  let curveSegments = 20;
6324
6015
  $.curveDetail = (v) => (curveSegments = v);
6325
6016
 
@@ -6482,23 +6173,18 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6482
6173
  }
6483
6174
 
6484
6175
  if (doStroke) {
6485
- let n = getArcSegments(scaledHSW),
6486
- ti = matrixIdx,
6487
- ogStrokeCap = _strokeCap;
6488
- _strokeCap = 'square';
6489
6176
  // draw lines between vertices
6490
6177
  for (let i = 0; i < shapeVertCount - 1; i++) {
6491
6178
  let v1 = i * 4;
6492
6179
  let v2 = (i + 1) * 4;
6493
6180
  $.line(sv[v1], sv[v1 + 1], sv[v2], sv[v2 + 1]);
6494
6181
 
6495
- addArc(sv[v1], sv[v1 + 1], hsw, hsw, 0, TAU, n, strokeIdx, ti);
6182
+ // addEllipse(sv[v1], sv[v1 + 1], qsw, qsw, 0, TAU, hsw, 0);
6496
6183
  }
6497
6184
  let v1 = (shapeVertCount - 1) * 4;
6498
6185
  let v2 = 0;
6499
6186
  if (close) $.line(sv[v1], sv[v1 + 1], sv[v2], sv[v2 + 1]);
6500
- addArc(sv[v1], sv[v1 + 1], hsw, hsw, 0, TAU, n, strokeIdx, ti);
6501
- _strokeCap = ogStrokeCap;
6187
+ // addEllipse(sv[v1], sv[v1 + 1], qsw, qsw, 0, TAU, hsw, 0);
6502
6188
  }
6503
6189
 
6504
6190
  // reset for the next shape
@@ -6540,6 +6226,608 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6540
6226
  $.endShape(true);
6541
6227
  };
6542
6228
 
6229
+ function addQuad(x1, y1, x2, y2, x3, y3, x4, y4, ci, ti) {
6230
+ addVert(x1, y1, ci, ti); // v0
6231
+ addVert(x2, y2, ci, ti); // v1
6232
+ addVert(x4, y4, ci, ti); // v3
6233
+ addVert(x3, y3, ci, ti); // v2
6234
+ drawStack.push(shapesPL, 4);
6235
+ }
6236
+
6237
+ $.plane = (x, y, w, h) => {
6238
+ h ??= w;
6239
+ let [l, r, t, b] = calcBox(x, y, w, h, 'center');
6240
+ if (matrixDirty) saveMatrix();
6241
+ addQuad(l, t, r, t, r, b, l, b, fillIdx, matrixIdx);
6242
+ };
6243
+
6244
+ /* RECT */
6245
+
6246
+ let rectPL = 5;
6247
+
6248
+ $._rectShaderCode =
6249
+ $._baseShaderCode +
6250
+ /* wgsl */ `
6251
+ struct Rect {
6252
+ center: vec2f,
6253
+ extents: vec2f,
6254
+ roundedRadius: f32,
6255
+ strokeWeight: f32,
6256
+ fillIndex: f32,
6257
+ strokeIndex: f32,
6258
+ matrixIndex: f32,
6259
+ padding0: f32, // can't use vec3f for alignment
6260
+ padding1: vec2f,
6261
+ padding2: vec4f
6262
+ };
6263
+
6264
+ struct VertexParams {
6265
+ @builtin(vertex_index) vertIndex: u32,
6266
+ @builtin(instance_index) instIndex: u32
6267
+ };
6268
+
6269
+ struct FragParams {
6270
+ @builtin(position) position: vec4f,
6271
+ @location(0) local: vec2f,
6272
+ @location(1) extents: vec2f,
6273
+ @location(2) roundedRadius: f32,
6274
+ @location(3) strokeWeight: f32,
6275
+ @location(4) fill: vec4f,
6276
+ @location(5) stroke: vec4f,
6277
+ @location(6) blend: vec4f
6278
+ };
6279
+
6280
+ @group(0) @binding(0) var<uniform> q: Q5;
6281
+ @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
6282
+ @group(0) @binding(2) var<storage> colors : array<vec4f>;
6283
+
6284
+ @group(1) @binding(0) var<storage, read> rects: array<Rect>;
6285
+
6286
+ const quad = array(
6287
+ vec2f(-1.0, -1.0),
6288
+ vec2f( 1.0, -1.0),
6289
+ vec2f(-1.0, 1.0),
6290
+ vec2f( 1.0, 1.0)
6291
+ );
6292
+ const transparent = vec4f(0.0);
6293
+
6294
+ fn transformVertex(pos: vec2f, matrixIndex: f32) -> vec4f {
6295
+ var vert = vec4f(pos, 0.0, 1.0);
6296
+ vert = transforms[i32(matrixIndex)] * vert;
6297
+ vert.x /= q.halfWidth;
6298
+ vert.y /= q.halfHeight;
6299
+ return vert;
6300
+ }
6301
+
6302
+ @vertex
6303
+ fn vertexMain(v: VertexParams) -> FragParams {
6304
+ let rect = rects[v.instIndex];
6305
+
6306
+ let halfStrokeSize = vec2f(rect.strokeWeight * 0.5);
6307
+ let quadSize = rect.extents + halfStrokeSize;
6308
+ let pos = (quad[v.vertIndex] * quadSize) + rect.center;
6309
+
6310
+ let local = pos - rect.center;
6311
+
6312
+ var f: FragParams;
6313
+ f.position = transformVertex(pos, rect.matrixIndex);
6314
+
6315
+ f.local = local;
6316
+ f.extents = rect.extents;
6317
+ f.roundedRadius = rect.roundedRadius;
6318
+ f.strokeWeight = rect.strokeWeight;
6319
+
6320
+ let fill = colors[i32(rect.fillIndex)];
6321
+ let stroke = colors[i32(rect.strokeIndex)];
6322
+ f.fill = fill;
6323
+ f.stroke = stroke;
6324
+
6325
+ // Source-over blend: stroke over fill (pre-multiplied alpha)
6326
+ if (fill.a != 0.0 && stroke.a != 1.0) {
6327
+ let outAlpha = stroke.a + fill.a * (1.0 - stroke.a);
6328
+ let outColor = stroke.rgb * stroke.a + fill.rgb * fill.a * (1.0 - stroke.a);
6329
+ f.blend = vec4f(outColor / max(outAlpha, 1e-5), outAlpha);
6330
+ }
6331
+ return f;
6332
+ }
6333
+
6334
+ fn sdRoundRect(p: vec2f, extents: vec2f, radius: f32) -> f32 {
6335
+ let q = abs(p) - extents + vec2f(radius);
6336
+ return length(max(q, vec2f(0.0))) - radius + min(max(q.x, q.y), 0.0);
6337
+ }
6338
+
6339
+ @fragment
6340
+ fn fragMain(f: FragParams) -> @location(0) vec4f {
6341
+ let dist = select(
6342
+ max(abs(f.local.x) - f.extents.x, abs(f.local.y) - f.extents.y), // sharp
6343
+ sdRoundRect(f.local, f.extents, f.roundedRadius), // rounded
6344
+ f.roundedRadius > 0.0
6345
+ );
6346
+
6347
+ // fill only
6348
+ if (f.fill.a != 0.0 && f.strokeWeight == 0.0) {
6349
+ if (dist <= 0.0) {
6350
+ return f.fill;
6351
+ }
6352
+ return transparent;
6353
+ }
6354
+
6355
+ let halfStroke = f.strokeWeight * 0.5;
6356
+ let inner = dist + halfStroke;
6357
+
6358
+ if (f.fill.a != 0.0) {
6359
+ if (inner <= 0.0) {
6360
+ return f.fill;
6361
+ }
6362
+ if (dist <= 0.0 && f.stroke.a != 1.0) {
6363
+ return f.blend;
6364
+ }
6365
+ }
6366
+
6367
+ let outer = dist - halfStroke;
6368
+
6369
+ if (outer <= 0.0 && inner >= 0.0) {
6370
+ return f.stroke;
6371
+ }
6372
+
6373
+ return transparent;
6374
+ }
6375
+ `;
6376
+
6377
+ let rectShader = Q5.device.createShaderModule({
6378
+ label: 'rectShader',
6379
+ code: $._rectShaderCode
6380
+ });
6381
+
6382
+ let rectIndices = new Uint16Array([0, 1, 2, 2, 1, 3]);
6383
+
6384
+ let rectIndexBuffer = Q5.device.createBuffer({
6385
+ size: rectIndices.byteLength,
6386
+ usage: GPUBufferUsage.INDEX,
6387
+ mappedAtCreation: true
6388
+ });
6389
+ new Uint16Array(rectIndexBuffer.getMappedRange()).set(rectIndices);
6390
+ rectIndexBuffer.unmap();
6391
+
6392
+ let rectBindGroupLayout = Q5.device.createBindGroupLayout({
6393
+ entries: [
6394
+ {
6395
+ binding: 0,
6396
+ visibility: GPUShaderStage.VERTEX,
6397
+ buffer: { type: 'read-only-storage' }
6398
+ }
6399
+ ]
6400
+ });
6401
+
6402
+ let rectPipelineLayout = Q5.device.createPipelineLayout({
6403
+ label: 'rectPipelineLayout',
6404
+ bindGroupLayouts: [...$._bindGroupLayouts, rectBindGroupLayout]
6405
+ });
6406
+
6407
+ $._pipelineConfigs[5] = {
6408
+ label: 'rectPipeline',
6409
+ layout: rectPipelineLayout,
6410
+ vertex: {
6411
+ module: rectShader,
6412
+ entryPoint: 'vertexMain',
6413
+ buffers: []
6414
+ },
6415
+ fragment: {
6416
+ module: rectShader,
6417
+ entryPoint: 'fragMain',
6418
+ targets: [
6419
+ {
6420
+ format: 'bgra8unorm',
6421
+ blend: $.blendConfigs['source-over']
6422
+ }
6423
+ ]
6424
+ },
6425
+ primitive: { topology: 'triangle-list' },
6426
+ multisample: { count: 4 }
6427
+ };
6428
+
6429
+ $._pipelines[5] = Q5.device.createRenderPipeline($._pipelineConfigs[5]);
6430
+
6431
+ let rectStack = new Float32Array(Q5.MAX_RECTS * 16);
6432
+ let rectStackIdx = 0;
6433
+
6434
+ let rectBuffer = Q5.device.createBuffer({
6435
+ size: rectStack.byteLength,
6436
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
6437
+ });
6438
+
6439
+ let rectBindGroup = Q5.device.createBindGroup({
6440
+ layout: rectBindGroupLayout,
6441
+ entries: [{ binding: 0, resource: { buffer: rectBuffer } }]
6442
+ });
6443
+
6444
+ function addRect(x, y, hw, hh, roundedRadius, strokeW, fillRect) {
6445
+ let s = rectStack,
6446
+ i = rectStackIdx;
6447
+
6448
+ s[i] = x;
6449
+ s[i + 1] = y;
6450
+ s[i + 2] = hw;
6451
+ s[i + 3] = hh;
6452
+ s[i + 4] = roundedRadius;
6453
+ s[i + 5] = strokeW;
6454
+ s[i + 6] = fillRect;
6455
+ s[i + 7] = strokeIdx;
6456
+ s[i + 8] = matrixIdx;
6457
+
6458
+ rectStackIdx += 16;
6459
+ drawStack.push(rectPL, 1);
6460
+ }
6461
+
6462
+ let _rectMode = 'corner';
6463
+
6464
+ $.rectMode = (x) => (_rectMode = x);
6465
+
6466
+ function applyRectMode(x, y, w, h) {
6467
+ let hw = w / 2,
6468
+ hh = h / 2;
6469
+ if (_rectMode != 'center') {
6470
+ if (_rectMode == 'corner') {
6471
+ x += hw;
6472
+ y += hh;
6473
+ } else if (_rectMode == 'radius') {
6474
+ hw = w;
6475
+ hh = h;
6476
+ } else if (_rectMode == 'corners') {
6477
+ hw = (x - w) / 2;
6478
+ hh = (y - h) / 2;
6479
+ x += hw;
6480
+ y += hh;
6481
+ }
6482
+ }
6483
+ return [x, y, hw, hh];
6484
+ }
6485
+
6486
+ $.rect = (x, y, w, h, rr = 0) => {
6487
+ if (matrixDirty) saveMatrix();
6488
+
6489
+ let hw, hh;
6490
+ [x, y, hw, hh] = applyRectMode(x, y, w, h);
6491
+
6492
+ addRect(x, y, hw, hh, rr, doStroke ? sw : 0, doFill ? fillIdx : 0);
6493
+ };
6494
+
6495
+ $.square = (x, y, s) => $.rect(x, y, s, s);
6496
+
6497
+ function addCapsule(x1, y1, x2, y2, r, strokeW, fillCapsule) {
6498
+ let dx = x2 - x1,
6499
+ dy = y2 - y1,
6500
+ len = Math.hypot(dx, dy);
6501
+
6502
+ if (len === 0) return;
6503
+
6504
+ let angle = Math.atan2(dy, dx),
6505
+ cx = (x1 + x2) / 2,
6506
+ cy = (y1 + y2) / 2;
6507
+
6508
+ if ($._angleMode) angle *= $._RADTODEG;
6509
+
6510
+ $.pushMatrix();
6511
+ $.translate(cx, cy);
6512
+ $.rotate(angle);
6513
+
6514
+ if (matrixDirty) saveMatrix();
6515
+
6516
+ addRect(0, 0, len / 2 + r, r, r, strokeW, fillCapsule);
6517
+
6518
+ $.popMatrix();
6519
+ }
6520
+
6521
+ $.capsule = (x1, y1, x2, y2, r) => {
6522
+ addCapsule(x1, y1, x2, y2, r, doStroke ? sw : 0, doFill ? fillIdx : 0);
6523
+ };
6524
+
6525
+ $.line = (x1, y1, x2, y2) => {
6526
+ if (doStroke) addCapsule(x1, y1, x2, y2, qsw, hsw, 0);
6527
+ };
6528
+
6529
+ /* ELLIPSE */
6530
+
6531
+ let ellipsePL = 6;
6532
+
6533
+ $._ellipseShaderCode =
6534
+ $._baseShaderCode +
6535
+ /* wgsl */ `
6536
+ struct Ellipse {
6537
+ center: vec2f,
6538
+ size: vec2f,
6539
+ startAngle: f32,
6540
+ endAngle: f32,
6541
+ strokeWeight: f32,
6542
+ fillIndex: f32,
6543
+ strokeIndex: f32,
6544
+ matrixIndex: f32,
6545
+ padding0: vec2f,
6546
+ padding1: vec4f
6547
+ };
6548
+
6549
+ struct VertexParams {
6550
+ @builtin(vertex_index) vertIndex: u32,
6551
+ @builtin(instance_index) instIndex: u32
6552
+ };
6553
+
6554
+ struct FragParams {
6555
+ @builtin(position) position: vec4f,
6556
+ @location(0) outerEdge: vec2f,
6557
+ @location(1) fillEdge: vec2f,
6558
+ @location(2) innerEdge: vec2f,
6559
+ @location(3) strokeWeight: f32,
6560
+ @location(4) fill: vec4f,
6561
+ @location(5) stroke: vec4f,
6562
+ @location(6) blend: vec4f
6563
+ };
6564
+
6565
+ @group(0) @binding(0) var<uniform> q: Q5;
6566
+ @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
6567
+ @group(0) @binding(2) var<storage> colors : array<vec4f>;
6568
+
6569
+ @group(1) @binding(0) var<storage, read> ellipses: array<Ellipse>;
6570
+
6571
+ const PI = 3.141592653589793;
6572
+ const segments = 6.0;
6573
+ const expansion = 1.0 / cos(PI / segments);
6574
+ const antiAlias = 0.015625; // 1/64
6575
+ const transparent = vec4f(0.0);
6576
+
6577
+ fn transformVertex(pos: vec2f, matrixIndex: f32) -> vec4f {
6578
+ var vert = vec4f(pos, 0.0, 1.0);
6579
+ vert = transforms[i32(matrixIndex)] * vert;
6580
+ vert.x /= q.halfWidth;
6581
+ vert.y /= q.halfHeight;
6582
+ return vert;
6583
+ }
6584
+
6585
+ @vertex
6586
+ fn vertexMain(v: VertexParams) -> FragParams {
6587
+ let ellipse = ellipses[v.instIndex];
6588
+ var pos = ellipse.center;
6589
+ var local = vec2f(0.0);
6590
+ let start = ellipse.startAngle;
6591
+ let end = ellipse.endAngle;
6592
+ let arc = end - start;
6593
+ let halfStrokeSize = vec2f(ellipse.strokeWeight * 0.5);
6594
+
6595
+ let fanSize = (ellipse.size + halfStrokeSize) * expansion;
6596
+
6597
+ if (v.vertIndex != 0) {
6598
+ let theta = start + (f32(v.vertIndex - 1) / segments) * arc;
6599
+ local = vec2f(cos(theta), sin(theta));
6600
+ pos = ellipse.center + local * fanSize;
6601
+ }
6602
+
6603
+ let dist = pos - ellipse.center;
6604
+
6605
+ var f: FragParams;
6606
+ f.position = transformVertex(pos, ellipse.matrixIndex);
6607
+ f.outerEdge = dist / (ellipse.size + halfStrokeSize);
6608
+ f.fillEdge = dist / ellipse.size;
6609
+ f.innerEdge = dist / (ellipse.size - halfStrokeSize);
6610
+ f.strokeWeight = ellipse.strokeWeight;
6611
+
6612
+ let fill = colors[i32(ellipse.fillIndex)];
6613
+ let stroke = colors[i32(ellipse.strokeIndex)];
6614
+ f.fill = fill;
6615
+ f.stroke = stroke;
6616
+
6617
+ // Source-over blend: stroke over fill (pre-multiplied alpha)
6618
+ if (fill.a != 0.0 && stroke.a != 1.0) {
6619
+ let outAlpha = stroke.a + fill.a * (1.0 - stroke.a);
6620
+ let outColor = stroke.rgb * stroke.a + fill.rgb * fill.a * (1.0 - stroke.a);
6621
+ f.blend = vec4f(outColor / max(outAlpha, 1e-5), outAlpha);
6622
+ }
6623
+ return f;
6624
+ }
6625
+
6626
+ @fragment
6627
+ fn fragMain(f: FragParams) -> @location(0) vec4f {
6628
+ let fillEdge = length(f.fillEdge);
6629
+
6630
+ // disable AA for very thin strokes
6631
+ let aa = select(antiAlias, 0.0, f.strokeWeight <= 1.0);
6632
+
6633
+ if (f.fill.a != 0.0 && f.strokeWeight == 0.0) {
6634
+ if (fillEdge > 1.0) {
6635
+ return transparent;
6636
+ }
6637
+ let fillAlpha = 1.0 - smoothstep(1.0 - aa, 1.0, fillEdge);
6638
+ return vec4f(f.fill.rgb, f.fill.a * fillAlpha);
6639
+ }
6640
+
6641
+ let innerEdge = length(f.innerEdge);
6642
+
6643
+ if (f.fill.a != 0.0 && fillEdge < 1.0) {
6644
+ if (innerEdge < 1.0) {
6645
+ return f.fill;
6646
+ }
6647
+ let tInner = smoothstep(1.0, 1.0 + aa, innerEdge);
6648
+ let tOuter = smoothstep(1.0 - aa, 1.0, fillEdge);
6649
+ if (f.stroke.a != 1.0) {
6650
+ let fillBlend = mix(f.fill, f.blend, tInner);
6651
+ return mix(fillBlend, f.stroke, tOuter);
6652
+ } else {
6653
+ let fillBlend = mix(f.fill, f.stroke, tInner);
6654
+ return mix(fillBlend, f.stroke, tOuter);
6655
+ }
6656
+ }
6657
+
6658
+ if (innerEdge < 1.0) {
6659
+ return transparent;
6660
+ }
6661
+
6662
+ let outerEdge = length(f.outerEdge);
6663
+ let outerAlpha = 1.0 - smoothstep(1.0 - aa, 1.0, outerEdge);
6664
+ let innerAlpha = smoothstep(1.0, 1.0 + aa, innerEdge);
6665
+ let strokeAlpha = innerAlpha * outerAlpha;
6666
+ return vec4f(f.stroke.rgb, f.stroke.a * strokeAlpha);
6667
+ }
6668
+ `;
6669
+
6670
+ let ellipseShader = Q5.device.createShaderModule({
6671
+ label: 'ellipseShader',
6672
+ code: $._ellipseShaderCode
6673
+ });
6674
+
6675
+ let fanIndices = new Uint16Array([0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5, 0, 5, 6, 0, 6, 7]);
6676
+
6677
+ let ellipseIndexBuffer = Q5.device.createBuffer({
6678
+ size: fanIndices.byteLength,
6679
+ usage: GPUBufferUsage.INDEX,
6680
+ mappedAtCreation: true
6681
+ });
6682
+ new Uint16Array(ellipseIndexBuffer.getMappedRange()).set(fanIndices);
6683
+ ellipseIndexBuffer.unmap();
6684
+
6685
+ let ellipseBindGroupLayout = Q5.device.createBindGroupLayout({
6686
+ entries: [
6687
+ {
6688
+ binding: 0,
6689
+ visibility: GPUShaderStage.VERTEX,
6690
+ buffer: { type: 'read-only-storage' }
6691
+ }
6692
+ ]
6693
+ });
6694
+
6695
+ let ellipsePipelineLayout = Q5.device.createPipelineLayout({
6696
+ label: 'ellipsePipelineLayout',
6697
+ bindGroupLayouts: [...$._bindGroupLayouts, ellipseBindGroupLayout]
6698
+ });
6699
+
6700
+ $._pipelineConfigs[6] = {
6701
+ label: 'ellipsePipeline',
6702
+ layout: ellipsePipelineLayout,
6703
+ vertex: {
6704
+ module: ellipseShader,
6705
+ entryPoint: 'vertexMain',
6706
+ buffers: []
6707
+ },
6708
+ fragment: {
6709
+ module: ellipseShader,
6710
+ entryPoint: 'fragMain',
6711
+ targets: [
6712
+ {
6713
+ format: 'bgra8unorm',
6714
+ blend: $.blendConfigs['source-over']
6715
+ }
6716
+ ]
6717
+ },
6718
+ primitive: { topology: 'triangle-list' },
6719
+ multisample: { count: 4 }
6720
+ };
6721
+
6722
+ $._pipelines[6] = Q5.device.createRenderPipeline($._pipelineConfigs[6]);
6723
+
6724
+ let ellipseStack = new Float32Array(Q5.MAX_ELLIPSES * 16);
6725
+ let ellipseStackIdx = 0;
6726
+
6727
+ let ellipseBuffer = Q5.device.createBuffer({
6728
+ size: ellipseStack.byteLength,
6729
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
6730
+ });
6731
+
6732
+ let ellipseBindGroup = Q5.device.createBindGroup({
6733
+ layout: ellipseBindGroupLayout,
6734
+ entries: [{ binding: 0, resource: { buffer: ellipseBuffer } }]
6735
+ });
6736
+
6737
+ function addEllipse(x, y, a, b, start, stop, strokeW, fillEllipse) {
6738
+ let s = ellipseStack,
6739
+ i = ellipseStackIdx;
6740
+
6741
+ s[i] = x;
6742
+ s[i + 1] = y;
6743
+ s[i + 2] = a;
6744
+ s[i + 3] = b;
6745
+ s[i + 4] = start;
6746
+ s[i + 5] = stop;
6747
+ s[i + 6] = strokeW;
6748
+ s[i + 7] = fillEllipse ? fillIdx : 0;
6749
+ s[i + 8] = strokeIdx;
6750
+ s[i + 9] = matrixIdx;
6751
+
6752
+ ellipseStackIdx += 16;
6753
+ drawStack.push(ellipsePL, 1);
6754
+ }
6755
+
6756
+ let _ellipseMode = 'center';
6757
+
6758
+ $.ellipseMode = (x) => (_ellipseMode = x);
6759
+
6760
+ function applyEllipseMode(x, y, w, h) {
6761
+ h ??= w;
6762
+ let a, b;
6763
+ if (_ellipseMode == 'center') {
6764
+ a = w / 2;
6765
+ b = h / 2;
6766
+ } else if (_ellipseMode == 'radius') {
6767
+ a = w;
6768
+ b = h;
6769
+ } else if (_ellipseMode == 'corner') {
6770
+ x += w / 2;
6771
+ y += h / 2;
6772
+ a = w / 2;
6773
+ b = h / 2;
6774
+ } else if (_ellipseMode == 'corners') {
6775
+ x = (x + w) / 2;
6776
+ y = (y + h) / 2;
6777
+ a = (w - x) / 2;
6778
+ b = (h - y) / 2;
6779
+ }
6780
+ return [x, y, a, b];
6781
+ }
6782
+
6783
+ $.ellipse = (x, y, w, h) => {
6784
+ let a, b;
6785
+ [x, y, a, b] = applyEllipseMode(x, y, w, h);
6786
+
6787
+ if (matrixDirty) saveMatrix();
6788
+
6789
+ addEllipse(x, y, a, b, 0, TAU, sw, doFill);
6790
+ };
6791
+
6792
+ $.circle = (x, y, d) => $.ellipse(x, y, d, d);
6793
+
6794
+ $.arc = (x, y, w, h, start, stop) => {
6795
+ if (start === stop) return $.ellipse(x, y, w, h);
6796
+
6797
+ // Convert angles if needed
6798
+ if ($._angleMode) {
6799
+ start = $.radians(start);
6800
+ stop = $.radians(stop);
6801
+ }
6802
+
6803
+ // Normalize angles
6804
+ start %= TAU;
6805
+ stop %= TAU;
6806
+ if (start < 0) start += TAU;
6807
+ if (stop < 0) stop += TAU;
6808
+ if (start > stop) stop += TAU;
6809
+ if (start == stop) return $.ellipse(x, y, w, h);
6810
+
6811
+ // Calculate position based on ellipseMode
6812
+ let a, b;
6813
+ [x, y, a, b] = applyEllipseMode(x, y, w, h);
6814
+
6815
+ if (matrixDirty) saveMatrix();
6816
+
6817
+ addEllipse(x, y, a, b, start, stop, sw, doFill);
6818
+ };
6819
+
6820
+ $.point = (x, y) => {
6821
+ if (matrixDirty) saveMatrix();
6822
+
6823
+ if (scaledHSW < 1) {
6824
+ addRect(x, y, hsw, hsw, 0, sw, 0);
6825
+ } else {
6826
+ // dimensions of the point needs to be set to half the stroke weight
6827
+ addEllipse(x, y, hsw, hsw, 0, TAU, sw, 0);
6828
+ }
6829
+ };
6830
+
6543
6831
  /* IMAGE */
6544
6832
 
6545
6833
  let imagePL = 2,
@@ -7675,6 +7963,9 @@ Q5.DILATE = 6;
7675
7963
  Q5.ERODE = 7;
7676
7964
  Q5.BLUR = 8;
7677
7965
 
7966
+ Q5.MAX_RECTS = 200200;
7967
+ Q5.MAX_ELLIPSES = 200200;
7968
+
7678
7969
  Q5.initWebGPU = async () => {
7679
7970
  if (!navigator.gpu) {
7680
7971
  console.warn('q5 WebGPU not supported on this browser! Use Google Chrome or Edge.');