q5 3.0.5 → 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 +712 -382
  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', () => {
@@ -815,6 +815,12 @@ Q5.renderers.c2d.canvas = ($, q) => {
815
815
  $.noStroke = () => ($._doStroke = false);
816
816
  $.opacity = (a) => ($.ctx.globalAlpha = a);
817
817
 
818
+ // polyfill for q5 WebGPU functions (used by q5play)
819
+ $._getFillIdx = () => $._fill;
820
+ $._setFillIdx = (v) => ($._fill = v);
821
+ $._getStrokeIdx = () => $._stroke;
822
+ $._setStrokeIdx = (v) => ($._stroke = v);
823
+
818
824
  $._doShadow = false;
819
825
  $._shadowOffsetX = $._shadowOffsetY = $._shadowBlur = 10;
820
826
 
@@ -1098,6 +1104,27 @@ Q5.renderers.c2d.shapes = ($) => {
1098
1104
  return $.rect(x, y, s, s, tl, tr, br, bl);
1099
1105
  };
1100
1106
 
1107
+ $.capsule = (x1, y1, x2, y2, r) => {
1108
+ const dx = x2 - x1,
1109
+ dy = y2 - y1,
1110
+ len = Math.hypot(dx, dy);
1111
+
1112
+ if (len === 0) return $.circle(x1, y1, r * 2);
1113
+
1114
+ const angle = Math.atan2(dy, dx),
1115
+ px = (-dy / len) * r,
1116
+ py = (dx / len) * r;
1117
+
1118
+ $.ctx.beginPath();
1119
+ $.ctx.moveTo(x1 - px, y1 - py);
1120
+ $.ctx.arc(x1, y1, r, angle - $.HALF_PI, angle + $.HALF_PI, true);
1121
+ $.ctx.lineTo(x2 + px, y2 + py);
1122
+ $.ctx.arc(x2, y2, r, angle + $.HALF_PI, angle - $.HALF_PI, true);
1123
+ $.ctx.closePath();
1124
+
1125
+ ink();
1126
+ };
1127
+
1101
1128
  $.beginShape = () => {
1102
1129
  curveBuff = [];
1103
1130
  $.ctx.beginPath();
@@ -4899,8 +4926,8 @@ struct Q5 {
4899
4926
  frameLayout,
4900
4927
  frameSampler,
4901
4928
  frameBindGroup,
4902
- colorIndex = 1,
4903
- colorStackIndex = 8,
4929
+ colorIndex = 2,
4930
+ colorStackIndex = 12,
4904
4931
  prevFramePL = 0,
4905
4932
  framePL = 0;
4906
4933
 
@@ -4918,6 +4945,7 @@ struct Q5 {
4918
4945
 
4919
4946
  // prettier-ignore
4920
4947
  colorStack.set([
4948
+ 0, 0, 0, 0, // transparent
4921
4949
  0, 0, 0, 1, // black
4922
4950
  1, 1, 1, 1 // white
4923
4951
  ]);
@@ -5139,13 +5167,14 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5139
5167
  doStroke = true,
5140
5168
  fillSet = false,
5141
5169
  strokeSet = false,
5142
- strokeIdx = 0,
5143
- fillIdx = 1,
5144
- tintIdx = 1,
5170
+ strokeIdx = 1,
5171
+ fillIdx = 2,
5172
+ tintIdx = 2,
5145
5173
  globalAlpha = 1,
5146
5174
  sw = 1, // stroke weight
5147
- hsw = 0.5, // half the stroke weight
5148
- scaledSW = 1;
5175
+ hsw = 0.5, // half of the stroke weight
5176
+ qsw = 0.25, // quarter of the stroke weight
5177
+ scaledHSW = 0.5;
5149
5178
 
5150
5179
  $.fill = (r, g, b, a) => {
5151
5180
  addColor(r, g, b, a);
@@ -5168,10 +5197,15 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5168
5197
 
5169
5198
  $.strokeWeight = (v) => {
5170
5199
  if (v === undefined) return sw;
5200
+ if (!v) {
5201
+ doStroke = false;
5202
+ return;
5203
+ }
5171
5204
  v = Math.abs(v);
5172
5205
  sw = v;
5173
- scaledSW = v * _scale;
5174
5206
  hsw = v / 2;
5207
+ qsw = v / 4;
5208
+ scaledHSW = hsw * _scale;
5175
5209
  };
5176
5210
 
5177
5211
  $._getFillIdx = () => fillIdx;
@@ -5254,7 +5288,7 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5254
5288
  y ??= x;
5255
5289
 
5256
5290
  _scale = Math.max(Math.abs(x), Math.abs(y));
5257
- scaledSW = sw * _scale;
5291
+ scaledHSW = sw * 0.5 * _scale;
5258
5292
 
5259
5293
  let m = matrix;
5260
5294
 
@@ -5358,7 +5392,7 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5358
5392
  strokeIdx,
5359
5393
  sw,
5360
5394
  hsw,
5361
- scaledSW,
5395
+ scaledHSW,
5362
5396
  doFill,
5363
5397
  doStroke,
5364
5398
  fillSet,
@@ -5387,7 +5421,7 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5387
5421
  strokeIdx,
5388
5422
  sw,
5389
5423
  hsw,
5390
- scaledSW,
5424
+ scaledHSW,
5391
5425
  doFill,
5392
5426
  doStroke,
5393
5427
  fillSet,
@@ -5529,7 +5563,11 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5529
5563
  $.pop();
5530
5564
  } else {
5531
5565
  addColor(r, g, b, a);
5532
- 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);
5533
5571
  }
5534
5572
  };
5535
5573
 
@@ -5664,6 +5702,8 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5664
5702
  }
5665
5703
  }
5666
5704
 
5705
+ // prepare to render text
5706
+
5667
5707
  if (charStack.length) {
5668
5708
  // calculate total buffer size for text data
5669
5709
  let totalTextSize = 0;
@@ -5710,9 +5750,33 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5710
5750
  });
5711
5751
  }
5712
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
+
5713
5775
  let drawVertOffset = 0,
5714
5776
  imageVertOffset = 0,
5715
5777
  textCharOffset = 0,
5778
+ rectIdx = 0,
5779
+ ellipseIdx = 0,
5716
5780
  curPipelineIndex = -1;
5717
5781
 
5718
5782
  for (let i = 0; i < drawStack.length; i += 2) {
@@ -5731,9 +5795,25 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5731
5795
 
5732
5796
  curPipelineIndex = drawStack[i];
5733
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
+ }
5734
5806
  }
5735
5807
 
5736
- 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) {
5737
5817
  // draw text
5738
5818
  let o = drawStack[i + 2];
5739
5819
  pass.setBindGroup(1, fontsArr[o].bindGroup);
@@ -5795,8 +5875,8 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5795
5875
 
5796
5876
  // clear the stacks for the next frame
5797
5877
  drawStack.splice(0, drawStack.length);
5798
- colorIndex = 1;
5799
- colorStackIndex = 8;
5878
+ colorIndex = 2;
5879
+ colorStackIndex = 12;
5800
5880
  matrices = [matrices[0]];
5801
5881
  matricesIdxStack = [];
5802
5882
 
@@ -5810,6 +5890,10 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5810
5890
  vidFrames = 0;
5811
5891
  charStack = [];
5812
5892
  textStack = [];
5893
+ rectStack = new Float32Array(Q5.MAX_RECTS * 16);
5894
+ rectStackIdx = 0;
5895
+ ellipseStack = new Float32Array(Q5.MAX_ELLIPSES * 16);
5896
+ ellipseStackIdx = 0;
5813
5897
 
5814
5898
  // destroy buffers
5815
5899
  Q5.device.queue.onSubmittedWorkDone().then(() => {
@@ -5871,8 +5955,8 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
5871
5955
 
5872
5956
  let shapesVertStack = new Float32Array($._isGraphics ? 1000 : 1e7),
5873
5957
  shapesVertIdx = 0;
5874
- const TAU = Math.PI * 2;
5875
- const HALF_PI = Math.PI / 2;
5958
+ const TAU = Math.PI * 2,
5959
+ HALF_PI = Math.PI / 2;
5876
5960
 
5877
5961
  let shapesVertBuffLayout = {
5878
5962
  arrayStride: 16, // 4 floats * 4 bytes
@@ -5883,14 +5967,14 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
5883
5967
  ]
5884
5968
  };
5885
5969
 
5886
- let pipelineLayout = Q5.device.createPipelineLayout({
5970
+ let shapesPipelineLayout = Q5.device.createPipelineLayout({
5887
5971
  label: 'shapesPipelineLayout',
5888
5972
  bindGroupLayouts: $._bindGroupLayouts
5889
5973
  });
5890
5974
 
5891
5975
  $._pipelineConfigs[1] = {
5892
5976
  label: 'shapesPipeline',
5893
- layout: pipelineLayout,
5977
+ layout: shapesPipelineLayout,
5894
5978
  vertex: {
5895
5979
  module: shapesShader,
5896
5980
  entryPoint: 'vertexMain',
@@ -5917,337 +6001,6 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
5917
6001
  shapesVertIdx = i;
5918
6002
  };
5919
6003
 
5920
- const addRect = (x1, y1, x2, y2, x3, y3, x4, y4, ci, ti) => {
5921
- let v = shapesVertStack,
5922
- i = shapesVertIdx;
5923
-
5924
- v[i++] = x1;
5925
- v[i++] = y1;
5926
- v[i++] = ci;
5927
- v[i++] = ti;
5928
-
5929
- v[i++] = x2;
5930
- v[i++] = y2;
5931
- v[i++] = ci;
5932
- v[i++] = ti;
5933
-
5934
- v[i++] = x4;
5935
- v[i++] = y4;
5936
- v[i++] = ci;
5937
- v[i++] = ti;
5938
-
5939
- v[i++] = x3;
5940
- v[i++] = y3;
5941
- v[i++] = ci;
5942
- v[i++] = ti;
5943
-
5944
- shapesVertIdx = i;
5945
- drawStack.push(shapesPL, 4);
5946
- };
5947
-
5948
- const addArc = (x, y, a, b, startAngle, endAngle, n, ci, ti) => {
5949
- let angleRange = endAngle - startAngle;
5950
- let angleIncrement = angleRange / n;
5951
- let t = startAngle;
5952
-
5953
- let v = shapesVertStack,
5954
- i = shapesVertIdx;
5955
-
5956
- for (let j = 0; j <= n; j++) {
5957
- // add center vertex
5958
- v[i++] = x;
5959
- v[i++] = y;
5960
- v[i++] = ci;
5961
- v[i++] = ti;
5962
-
5963
- // calculate perimeter vertex
5964
- let vx = x + a * Math.cos(t);
5965
- let vy = y + b * Math.sin(t);
5966
-
5967
- // add perimeter vertex
5968
- v[i++] = vx;
5969
- v[i++] = vy;
5970
- v[i++] = ci;
5971
- v[i++] = ti;
5972
-
5973
- t += angleIncrement;
5974
- }
5975
-
5976
- shapesVertIdx = i;
5977
- drawStack.push(shapesPL, (n + 1) * 2);
5978
- };
5979
-
5980
- const addArcStroke = (x, y, outerA, outerB, innerA, innerB, 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
- // Outer vertex
5990
- let vxOuter = x + outerA * Math.cos(t);
5991
- let vyOuter = y + outerB * Math.sin(t);
5992
-
5993
- // Inner vertex
5994
- let vxInner = x + innerA * Math.cos(t);
5995
- let vyInner = y + innerB * Math.sin(t);
5996
-
5997
- // Add vertices for triangle strip
5998
- v[i++] = vxOuter;
5999
- v[i++] = vyOuter;
6000
- v[i++] = ci;
6001
- v[i++] = ti;
6002
-
6003
- v[i++] = vxInner;
6004
- v[i++] = vyInner;
6005
- v[i++] = ci;
6006
- v[i++] = ti;
6007
-
6008
- t += angleIncrement;
6009
- }
6010
-
6011
- shapesVertIdx = i;
6012
- drawStack.push(shapesPL, (n + 1) * 2);
6013
- };
6014
-
6015
- let _rectMode = 'corner';
6016
-
6017
- $.rectMode = (x) => (_rectMode = x);
6018
-
6019
- $.rect = (x, y, w, h, rr = 0) => {
6020
- h ??= w;
6021
- let [l, r, t, b] = calcBox(x, y, w, h, _rectMode);
6022
- let ci, ti;
6023
- if (matrixDirty) saveMatrix();
6024
- ti = matrixIdx;
6025
-
6026
- if (!rr) {
6027
- if (doFill) {
6028
- ci = fillIdx;
6029
- addRect(l, t, r, t, r, b, l, b, ci, ti);
6030
- }
6031
-
6032
- if (doStroke) {
6033
- ci = strokeIdx;
6034
-
6035
- // Calculate stroke positions
6036
- let lsw = l - hsw,
6037
- rsw = r + hsw,
6038
- tsw = t + hsw,
6039
- bsw = b - hsw,
6040
- lpsw = l + hsw,
6041
- rpsw = r - hsw,
6042
- tpsw = t - hsw,
6043
- bpsw = b + hsw;
6044
-
6045
- addRect(lsw, tpsw, rsw, tpsw, rsw, tsw, lsw, tsw, ci, ti); // Top
6046
- addRect(lsw, bsw, rsw, bsw, rsw, bpsw, lsw, bpsw, ci, ti); // Bottom
6047
-
6048
- // Adjust side strokes to avoid overlapping corners
6049
- tsw = t - hsw;
6050
- bsw = b + hsw;
6051
-
6052
- addRect(lsw, tsw, lpsw, tsw, lpsw, bsw, lsw, bsw, ci, ti); // Left
6053
- addRect(rpsw, tsw, rsw, tsw, rsw, bsw, rpsw, bsw, ci, ti); // Right
6054
- }
6055
- return;
6056
- }
6057
-
6058
- l += rr;
6059
- r -= rr;
6060
- t += rr;
6061
- b -= rr;
6062
-
6063
- // Clamp radius
6064
- rr = Math.min(rr, Math.min(w, h) / 2);
6065
-
6066
- let n = getArcSegments(rr * _scale);
6067
-
6068
- let trr = t - rr,
6069
- brr = b + rr,
6070
- lrr = l - rr,
6071
- rrr = r + rr;
6072
-
6073
- if (doFill) {
6074
- ci = fillIdx;
6075
- // Corner arcs
6076
- addArc(l, t, rr, -rr, -HALF_PI, Math.PI, n, ci, ti); // top-left
6077
- addArc(r, t, rr, -rr, HALF_PI, 0, n, ci, ti); // top-right
6078
- addArc(r, b, rr, -rr, 0, -HALF_PI, n, ci, ti); // bottom-right
6079
- addArc(l, b, rr, -rr, -HALF_PI, -Math.PI, n, ci, ti); // bottom-left
6080
-
6081
- addRect(l, trr, r, trr, r, brr, l, brr, ci, ti); // center
6082
- addRect(l, t, lrr, t, lrr, b, l, b, ci, ti); // Left
6083
- addRect(rrr, t, r, t, r, b, rrr, b, ci, ti); // Right
6084
- }
6085
-
6086
- if (doStroke) {
6087
- ci = strokeIdx;
6088
-
6089
- let outerA = rr + hsw,
6090
- outerB = rr + hsw,
6091
- innerA = rr - hsw,
6092
- innerB = rr - hsw;
6093
-
6094
- // Corner arc strokes
6095
- addArcStroke(l, t, outerA, -outerB, innerA, -innerB, Math.PI, HALF_PI, n, ci, ti); // top-left
6096
- addArcStroke(r, t, outerA, -outerB, innerA, -innerB, HALF_PI, 0, n, ci, ti); // top-right
6097
- addArcStroke(r, b, outerA, -outerB, innerA, -innerB, 0, -HALF_PI, n, ci, ti); // bottom-right
6098
- addArcStroke(l, b, outerA, -outerB, innerA, -innerB, -HALF_PI, -Math.PI, n, ci, ti); // bottom-left
6099
-
6100
- let lrrMin = lrr - hsw,
6101
- lrrMax = lrr + hsw,
6102
- rrrMin = rrr - hsw,
6103
- rrrMax = rrr + hsw,
6104
- trrMin = trr - hsw,
6105
- trrMax = trr + hsw,
6106
- brrMin = brr - hsw,
6107
- brrMax = brr + hsw;
6108
-
6109
- // Side strokes - positioned outside
6110
- addRect(lrrMin, t, lrrMax, t, lrrMax, b, lrrMin, b, ci, ti); // Left
6111
- addRect(rrrMin, t, rrrMax, t, rrrMax, b, rrrMin, b, ci, ti); // Right
6112
- addRect(l, trrMin, r, trrMin, r, trrMax, l, trrMax, ci, ti); // Top
6113
- addRect(l, brrMin, r, brrMin, r, brrMax, l, brrMax, ci, ti); // Bottom
6114
- }
6115
- };
6116
-
6117
- $.square = (x, y, s) => $.rect(x, y, s, s);
6118
-
6119
- $.plane = (x, y, w, h) => {
6120
- h ??= w;
6121
- let [l, r, t, b] = calcBox(x, y, w, h, 'center');
6122
- if (matrixDirty) saveMatrix();
6123
- addRect(l, t, r, t, r, b, l, b, fillIdx, matrixIdx);
6124
- };
6125
-
6126
- // prettier-ignore
6127
- const getArcSegments = (d) =>
6128
- d < 4 ? 6 :
6129
- d < 6 ? 8 :
6130
- d < 10 ? 10 :
6131
- d < 16 ? 12 :
6132
- d < 20 ? 14 :
6133
- d < 22 ? 16 :
6134
- d < 24 ? 18 :
6135
- d < 28 ? 20 :
6136
- d < 34 ? 22 :
6137
- d < 42 ? 24 :
6138
- d < 48 ? 26 :
6139
- d < 56 ? 28 :
6140
- d < 64 ? 30 :
6141
- d < 72 ? 32 :
6142
- d < 84 ? 34 :
6143
- d < 96 ? 36 :
6144
- d < 98 ? 38 :
6145
- d < 113 ? 40 :
6146
- d < 149 ? 44 :
6147
- d < 199 ? 48 :
6148
- d < 261 ? 52 :
6149
- d < 353 ? 56 :
6150
- d < 461 ? 60 :
6151
- d < 585 ? 64 :
6152
- d < 1200 ? 70 :
6153
- d < 1800 ? 80 :
6154
- d < 2400 ? 90 :
6155
- 100;
6156
-
6157
- let _ellipseMode = 'center';
6158
-
6159
- $.ellipseMode = (x) => (_ellipseMode = x);
6160
-
6161
- $.ellipse = (x, y, w, h) => {
6162
- let n = getArcSegments(Math.max(Math.abs(w), Math.abs(h)) * _scale);
6163
- let a = w / 2;
6164
- let b = w == h ? a : h / 2;
6165
-
6166
- if (matrixDirty) saveMatrix();
6167
- let ti = matrixIdx;
6168
-
6169
- if (doFill) {
6170
- addArc(x, y, a, b, 0, TAU, n, fillIdx, ti);
6171
- }
6172
- if (doStroke) {
6173
- // Draw the stroke as a ring using triangle strips
6174
- addArcStroke(x, y, a + hsw, b + hsw, a - hsw, b - hsw, 0, TAU, n, strokeIdx, ti);
6175
- }
6176
- };
6177
-
6178
- $.circle = (x, y, d) => $.ellipse(x, y, d, d);
6179
-
6180
- $.arc = (x, y, w, h, start, stop) => {
6181
- if (start === stop) return $.ellipse(x, y, w, h);
6182
-
6183
- // Convert angles if needed
6184
- if ($._angleMode) {
6185
- start = $.radians(start);
6186
- stop = $.radians(stop);
6187
- }
6188
-
6189
- // Normalize angles
6190
- start %= TAU;
6191
- stop %= TAU;
6192
- if (start < 0) start += TAU;
6193
- if (stop < 0) stop += TAU;
6194
- if (start > stop) stop += TAU;
6195
- if (start == stop) return $.ellipse(x, y, w, h);
6196
-
6197
- // Calculate position based on ellipseMode
6198
- let a, b;
6199
- if (_ellipseMode == $.CENTER) {
6200
- a = w / 2;
6201
- b = h / 2;
6202
- } else if (_ellipseMode == $.RADIUS) {
6203
- a = w;
6204
- b = h;
6205
- } else if (_ellipseMode == $.CORNER) {
6206
- x += w / 2;
6207
- y += h / 2;
6208
- a = w / 2;
6209
- b = h / 2;
6210
- } else if (_ellipseMode == $.CORNERS) {
6211
- x = (x + w) / 2;
6212
- y = (y + h) / 2;
6213
- a = (w - x) / 2;
6214
- b = (h - y) / 2;
6215
- }
6216
-
6217
- if (matrixDirty) saveMatrix();
6218
- let ti = matrixIdx;
6219
- let n = getArcSegments(Math.max(Math.abs(w), Math.abs(h)) * _scale);
6220
-
6221
- // Draw fill
6222
- if (doFill) {
6223
- addArc(x, y, a, b, start, stop, n, fillIdx, ti);
6224
- }
6225
-
6226
- // Draw stroke
6227
- if (doStroke) {
6228
- addArcStroke(x, y, a + hsw, b + hsw, a - hsw, b - hsw, start, stop, n, strokeIdx, ti);
6229
- if (_strokeCap == 'round') {
6230
- addArc(x + a * Math.cos(start), y + b * Math.sin(start), hsw, hsw, 0, TAU, n, strokeIdx, ti);
6231
- addArc(x + a * Math.cos(stop), y + b * Math.sin(stop), hsw, hsw, 0, TAU, n, strokeIdx, ti);
6232
- }
6233
- }
6234
- };
6235
-
6236
- $.point = (x, y) => {
6237
- if (matrixDirty) saveMatrix();
6238
- let ti = matrixIdx,
6239
- ci = strokeIdx;
6240
-
6241
- if (scaledSW < 2) {
6242
- let [l, r, t, b] = calcBox(x, y, sw, sw, 'corner');
6243
- addRect(l, t, r, t, r, b, l, b, ci, ti);
6244
- } else {
6245
- let n = getArcSegments(scaledSW);
6246
- sw /= 2;
6247
- addArc(x, y, sw, sw, 0, TAU, n, ci, ti);
6248
- }
6249
- };
6250
-
6251
6004
  let _strokeCap = 'round',
6252
6005
  _strokeJoin = 'round';
6253
6006
 
@@ -6258,29 +6011,6 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6258
6011
  _strokeJoin = 'none';
6259
6012
  };
6260
6013
 
6261
- $.line = (x1, y1, x2, y2) => {
6262
- if (matrixDirty) saveMatrix();
6263
- let ti = matrixIdx,
6264
- ci = strokeIdx;
6265
-
6266
- // calculate the direction vector and length
6267
- let dx = x2 - x1,
6268
- dy = y2 - y1,
6269
- length = Math.hypot(dx, dy);
6270
-
6271
- // calculate the perpendicular vector for line thickness
6272
- let px = -(dy / length) * hsw,
6273
- py = (dx / length) * hsw;
6274
-
6275
- addRect(x1 + px, y1 + py, x1 - px, y1 - py, x2 - px, y2 - py, x2 + px, y2 + py, ci, ti);
6276
-
6277
- if (scaledSW > 2 && _strokeCap != 'square') {
6278
- let n = getArcSegments(scaledSW);
6279
- addArc(x1, y1, hsw, hsw, 0, TAU, n, ci, ti);
6280
- addArc(x2, y2, hsw, hsw, 0, TAU, n, ci, ti);
6281
- }
6282
- };
6283
-
6284
6014
  let curveSegments = 20;
6285
6015
  $.curveDetail = (v) => (curveSegments = v);
6286
6016
 
@@ -6443,23 +6173,18 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6443
6173
  }
6444
6174
 
6445
6175
  if (doStroke) {
6446
- let n = getArcSegments(scaledSW),
6447
- ti = matrixIdx,
6448
- ogStrokeCap = _strokeCap;
6449
- _strokeCap = 'square';
6450
6176
  // draw lines between vertices
6451
6177
  for (let i = 0; i < shapeVertCount - 1; i++) {
6452
6178
  let v1 = i * 4;
6453
6179
  let v2 = (i + 1) * 4;
6454
6180
  $.line(sv[v1], sv[v1 + 1], sv[v2], sv[v2 + 1]);
6455
6181
 
6456
- 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);
6457
6183
  }
6458
6184
  let v1 = (shapeVertCount - 1) * 4;
6459
6185
  let v2 = 0;
6460
6186
  if (close) $.line(sv[v1], sv[v1 + 1], sv[v2], sv[v2 + 1]);
6461
- addArc(sv[v1], sv[v1 + 1], hsw, hsw, 0, TAU, n, strokeIdx, ti);
6462
- _strokeCap = ogStrokeCap;
6187
+ // addEllipse(sv[v1], sv[v1 + 1], qsw, qsw, 0, TAU, hsw, 0);
6463
6188
  }
6464
6189
 
6465
6190
  // reset for the next shape
@@ -6501,6 +6226,608 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6501
6226
  $.endShape(true);
6502
6227
  };
6503
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
+
6504
6831
  /* IMAGE */
6505
6832
 
6506
6833
  let imagePL = 2,
@@ -7636,6 +7963,9 @@ Q5.DILATE = 6;
7636
7963
  Q5.ERODE = 7;
7637
7964
  Q5.BLUR = 8;
7638
7965
 
7966
+ Q5.MAX_RECTS = 200200;
7967
+ Q5.MAX_ELLIPSES = 200200;
7968
+
7639
7969
  Q5.initWebGPU = async () => {
7640
7970
  if (!navigator.gpu) {
7641
7971
  console.warn('q5 WebGPU not supported on this browser! Use Google Chrome or Edge.');