q5 2.2.3 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/q5.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * q5.js
3
- * @version 2.2
3
+ * @version 2.4
4
4
  * @author quinton-ashley, Tezumie, and LingDong-
5
5
  * @license LGPL-3.0
6
6
  * @class Q5
@@ -44,7 +44,8 @@ function Q5(scope, parent, renderer) {
44
44
  $._loop = true;
45
45
  $._hooks = {
46
46
  postCanvas: [],
47
- preRender: []
47
+ preRender: [],
48
+ postRender: []
48
49
  };
49
50
 
50
51
  let millisStart = 0;
@@ -219,7 +220,7 @@ function Q5(scope, parent, renderer) {
219
220
  return t[k]();
220
221
  } catch (e) {
221
222
  if ($._aiErrorAssistance) $._aiErrorAssistance(e);
222
- else console.error(e);
223
+ throw e;
223
224
  }
224
225
  };
225
226
  }
@@ -280,6 +281,58 @@ if (typeof document == 'object') {
280
281
  });
281
282
  }
282
283
  Q5.modules.canvas = ($, q) => {
284
+ $.CENTER = 'center';
285
+ $.LEFT = 'left';
286
+ $.RIGHT = 'right';
287
+ $.TOP = 'top';
288
+ $.BOTTOM = 'bottom';
289
+
290
+ $.BASELINE = 'alphabetic';
291
+
292
+ $.NORMAL = 'normal';
293
+ $.ITALIC = 'italic';
294
+ $.BOLD = 'bold';
295
+ $.BOLDITALIC = 'italic bold';
296
+
297
+ $.ROUND = 'round';
298
+ $.SQUARE = 'butt';
299
+ $.PROJECT = 'square';
300
+ $.MITER = 'miter';
301
+ $.BEVEL = 'bevel';
302
+
303
+ $.CHORD = 0;
304
+ $.PIE = 1;
305
+ $.OPEN = 2;
306
+
307
+ $.RADIUS = 'radius';
308
+ $.CORNER = 'corner';
309
+ $.CORNERS = 'corners';
310
+
311
+ $.CLOSE = 1;
312
+
313
+ $.LANDSCAPE = 'landscape';
314
+ $.PORTRAIT = 'portrait';
315
+
316
+ $.BLEND = 'source-over';
317
+ $.REMOVE = 'destination-out';
318
+ $.ADD = 'lighter';
319
+ $.DARKEST = 'darken';
320
+ $.LIGHTEST = 'lighten';
321
+ $.DIFFERENCE = 'difference';
322
+ $.SUBTRACT = 'subtract';
323
+ $.EXCLUSION = 'exclusion';
324
+ $.MULTIPLY = 'multiply';
325
+ $.SCREEN = 'screen';
326
+ $.REPLACE = 'copy';
327
+ $.OVERLAY = 'overlay';
328
+ $.HARD_LIGHT = 'hard-light';
329
+ $.SOFT_LIGHT = 'soft-light';
330
+ $.DODGE = 'color-dodge';
331
+ $.BURN = 'color-burn';
332
+
333
+ $.P2D = '2d';
334
+ $.WEBGL = 'webgl';
335
+
283
336
  $._OffscreenCanvas =
284
337
  window.OffscreenCanvas ||
285
338
  function () {
@@ -309,7 +362,7 @@ Q5.modules.canvas = ($, q) => {
309
362
  c.renderer = $._renderer;
310
363
  c[$._renderer] = true;
311
364
  }
312
- $._pixelDensity = 1;
365
+ $._pixelDensity = window?.devicePixelRatio || 1;
313
366
 
314
367
  $._adjustDisplay = () => {
315
368
  if (c.style) {
@@ -346,6 +399,15 @@ Q5.modules.canvas = ($, q) => {
346
399
  return rend;
347
400
  };
348
401
 
402
+ $.createGraphics = function (w, h, opt) {
403
+ let g = new Q5('graphics');
404
+ opt ??= {};
405
+ opt.alpha ??= true;
406
+ opt.colorSpace ??= $.canvas.colorSpace;
407
+ g.createCanvas.call($, w, h, opt);
408
+ return g;
409
+ };
410
+
349
411
  $._save = async (data, name, ext) => {
350
412
  name = name || 'untitled';
351
413
  ext = ext || 'png';
@@ -454,7 +516,7 @@ Q5.modules.canvas = ($, q) => {
454
516
  $.canvas.resize = $.resizeCanvas;
455
517
  $.canvas.save = $.saveCanvas = $.save;
456
518
 
457
- $.displayDensity = () => window.devicePixelRatio;
519
+ $.displayDensity = () => window.devicePixelRatio || 1;
458
520
  $.pixelDensity = (v) => {
459
521
  if (!v || v == $._pixelDensity) return $._pixelDensity;
460
522
  $._pixelDensity = v;
@@ -528,8 +590,10 @@ Q5.renderers.q2d.canvas = ($, q) => {
528
590
  $._doFill = true;
529
591
  $._fillSet = true;
530
592
  if (Q5.Color) {
531
- if (!c._q5Color && typeof c != 'string') c = $.color(...arguments);
532
- else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
593
+ if (!c._q5Color) {
594
+ if (typeof c != 'string') c = $.color(...arguments);
595
+ else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
596
+ }
533
597
  if (c.a <= 0) return ($._doFill = false);
534
598
  }
535
599
  $.ctx.fillStyle = c.toString();
@@ -539,8 +603,10 @@ Q5.renderers.q2d.canvas = ($, q) => {
539
603
  $._doStroke = true;
540
604
  $._strokeSet = true;
541
605
  if (Q5.Color) {
542
- if (!c._q5Color && typeof c != 'string') c = $.color(...arguments);
543
- else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
606
+ if (!c._q5Color) {
607
+ if (typeof c != 'string') c = $.color(...arguments);
608
+ else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
609
+ }
544
610
  if (c.a <= 0) return ($._doStroke = false);
545
611
  }
546
612
  $.ctx.strokeStyle = c.toString();
@@ -568,7 +634,7 @@ Q5.renderers.q2d.canvas = ($, q) => {
568
634
  $.ctx.translate(x, y);
569
635
  };
570
636
  $.rotate = (r) => {
571
- if ($._angleMode == 'degrees') r = $.radians(r);
637
+ if ($._angleMode) r = $.radians(r);
572
638
  $.ctx.rotate(r);
573
639
  };
574
640
  $.scale = (x, y) => {
@@ -630,15 +696,6 @@ Q5.renderers.q2d.canvas = ($, q) => {
630
696
  return vid;
631
697
  };
632
698
 
633
- $.createGraphics = function (w, h, opt) {
634
- let g = new Q5('graphics');
635
- opt ??= {};
636
- opt.alpha ??= true;
637
- opt.colorSpace ??= $.canvas.colorSpace;
638
- g.createCanvas.call($, w, h, opt);
639
- return g;
640
- };
641
-
642
699
  if (window && $._scope != 'graphics') {
643
700
  window.addEventListener('resize', () => {
644
701
  $._shouldResize = true;
@@ -649,48 +706,6 @@ Q5.renderers.q2d.canvas = ($, q) => {
649
706
  }
650
707
  };
651
708
  Q5.renderers.q2d.drawing = ($) => {
652
- $.CHORD = 0;
653
- $.PIE = 1;
654
- $.OPEN = 2;
655
-
656
- $.RADIUS = 'radius';
657
- $.CORNER = 'corner';
658
- $.CORNERS = 'corners';
659
-
660
- $.ROUND = 'round';
661
- $.SQUARE = 'butt';
662
- $.PROJECT = 'square';
663
- $.MITER = 'miter';
664
- $.BEVEL = 'bevel';
665
-
666
- $.CLOSE = 1;
667
-
668
- $.CENTER = 'center';
669
- $.LEFT = 'left';
670
- $.RIGHT = 'right';
671
- $.TOP = 'top';
672
- $.BOTTOM = 'bottom';
673
-
674
- $.LANDSCAPE = 'landscape';
675
- $.PORTRAIT = 'portrait';
676
-
677
- $.BLEND = 'source-over';
678
- $.REMOVE = 'destination-out';
679
- $.ADD = 'lighter';
680
- $.DARKEST = 'darken';
681
- $.LIGHTEST = 'lighten';
682
- $.DIFFERENCE = 'difference';
683
- $.SUBTRACT = 'subtract';
684
- $.EXCLUSION = 'exclusion';
685
- $.MULTIPLY = 'multiply';
686
- $.SCREEN = 'screen';
687
- $.REPLACE = 'copy';
688
- $.OVERLAY = 'overlay';
689
- $.HARD_LIGHT = 'hard-light';
690
- $.SOFT_LIGHT = 'soft-light';
691
- $.DODGE = 'color-dodge';
692
- $.BURN = 'color-burn';
693
-
694
709
  $._doStroke = true;
695
710
  $._doFill = true;
696
711
  $._strokeSet = false;
@@ -726,8 +741,8 @@ Q5.renderers.q2d.drawing = ($) => {
726
741
  $.ctx.resetTransform();
727
742
  if (c.canvas) $.image(c, 0, 0, $.width, $.height);
728
743
  else {
729
- if (Q5.Color) {
730
- if (!c._q5Color && typeof c != 'string') c = $.color(...arguments);
744
+ if (Q5.Color && !c._q5Color) {
745
+ if (typeof c != 'string') c = $.color(...arguments);
731
746
  else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
732
747
  }
733
748
  $.ctx.fillStyle = c.toString();
@@ -753,7 +768,7 @@ Q5.renderers.q2d.drawing = ($) => {
753
768
 
754
769
  function arc(x, y, w, h, lo, hi, mode, detail) {
755
770
  if (!$._doFill && !$._doStroke) return;
756
- let d = $._angleMode == 'degrees';
771
+ let d = $._angleMode;
757
772
  let full = d ? 360 : $.TAU;
758
773
  lo %= full;
759
774
  hi %= full;
@@ -1274,11 +1289,15 @@ Q5.renderers.q2d.image = ($, q) => {
1274
1289
  };
1275
1290
  }
1276
1291
 
1292
+ $._getImageData = (x, y, w, h) => {
1293
+ return $.ctx.getImageData(x, y, w, h, { colorSpace: $.canvas.colorSpace });
1294
+ };
1295
+
1277
1296
  $.trim = () => {
1278
1297
  let pd = $._pixelDensity || 1;
1279
1298
  let w = $.canvas.width;
1280
1299
  let h = $.canvas.height;
1281
- let data = $.ctx.getImageData(0, 0, w, h).data;
1300
+ let data = $._getImageData(0, 0, w, h).data;
1282
1301
  let left = w,
1283
1302
  right = 0,
1284
1303
  top = h,
@@ -1317,7 +1336,7 @@ Q5.renderers.q2d.image = ($, q) => {
1317
1336
  $.get = (x, y, w, h) => {
1318
1337
  let pd = $._pixelDensity || 1;
1319
1338
  if (x !== undefined && w === undefined) {
1320
- let c = $.ctx.getImageData(x * pd, y * pd, 1, 1).data;
1339
+ let c = $._getImageData(x * pd, y * pd, 1, 1).data;
1321
1340
  return new $.Color(c[0], c[1], c[2], c[3] / 255);
1322
1341
  }
1323
1342
  x = (x || 0) * pd;
@@ -1327,7 +1346,7 @@ Q5.renderers.q2d.image = ($, q) => {
1327
1346
  w *= pd;
1328
1347
  h *= pd;
1329
1348
  let img = $.createImage(w, h);
1330
- let imgData = $.ctx.getImageData(x, y, w, h);
1349
+ let imgData = $._getImageData(x, y, w, h);
1331
1350
  img.ctx.putImageData(imgData, 0, 0);
1332
1351
  img._pixelDensity = pd;
1333
1352
  img.width = _w;
@@ -1357,7 +1376,7 @@ Q5.renderers.q2d.image = ($, q) => {
1357
1376
  };
1358
1377
 
1359
1378
  $.loadPixels = () => {
1360
- imgData = $.ctx.getImageData(0, 0, $.canvas.width, $.canvas.height);
1379
+ imgData = $._getImageData(0, 0, $.canvas.width, $.canvas.height);
1361
1380
  q.pixels = imgData.data;
1362
1381
  };
1363
1382
  $.updatePixels = () => {
@@ -1384,18 +1403,6 @@ Q5.DILATE = 6;
1384
1403
  Q5.ERODE = 7;
1385
1404
  Q5.BLUR = 8;
1386
1405
  Q5.renderers.q2d.text = ($, q) => {
1387
- $.NORMAL = 'normal';
1388
- $.ITALIC = 'italic';
1389
- $.BOLD = 'bold';
1390
- $.BOLDITALIC = 'italic bold';
1391
-
1392
- $.CENTER = 'center';
1393
- $.LEFT = 'left';
1394
- $.RIGHT = 'right';
1395
- $.TOP = 'top';
1396
- $.BOTTOM = 'bottom';
1397
- $.BASELINE = 'alphabetic';
1398
-
1399
1406
  $._textFont = 'sans-serif';
1400
1407
  $._textSize = 12;
1401
1408
  $._textLeading = 15;
@@ -1507,12 +1514,14 @@ Q5.renderers.q2d.text = ($, q) => {
1507
1514
  );
1508
1515
  };
1509
1516
  $.createTextImage = (str, w, h) => {
1517
+ let k = $._genTextImageKey(str, w, h);
1518
+ if ($._tic.get(k)) return $._tic.get(k);
1519
+
1510
1520
  let og = $._textCache;
1511
1521
  $._textCache = true;
1512
1522
  $._genTextImage = true;
1513
1523
  $.text(str, 0, 0, w, h);
1514
1524
  $._genTextImage = false;
1515
- let k = $._genTextImageKey(str, w, h);
1516
1525
  $._textCache = og;
1517
1526
  return $._tic.get(k);
1518
1527
  };
@@ -1566,7 +1575,7 @@ Q5.renderers.q2d.text = ($, q) => {
1566
1575
  }
1567
1576
  if (!$._fillSet) c.fillStyle = f;
1568
1577
  if (useCache) {
1569
- ti = tg.get();
1578
+ ti = tg.canvas;
1570
1579
  ti._ascent = _ascent;
1571
1580
  ti._descent = _descent;
1572
1581
  $._tic.set(k, ti);
@@ -1603,8 +1612,7 @@ Q5.modules.ai = ($) => {
1603
1612
 
1604
1613
  $._aiErrorAssistance = async (e) => {
1605
1614
  let askAI = e.message?.includes('Ask AI ✨');
1606
- if (!askAI) console.error(e);
1607
- if (Q5.disableFriendlyErrors) return;
1615
+ if (Q5.disableFriendlyErrors && !askAI) return;
1608
1616
  if (askAI || !Q5.errorTolerant) $.noLoop();
1609
1617
  let stackLines = e.stack?.split('\n');
1610
1618
  if (!e.stack || stackLines.length <= 1) return;
@@ -1615,7 +1623,7 @@ Q5.modules.ai = ($) => {
1615
1623
  idx = 0;
1616
1624
  sep = '@';
1617
1625
  }
1618
- while (stackLines[idx].indexOf('q5.js:') >= 0) idx++;
1626
+ while (stackLines[idx].indexOf('q5') >= 0) idx++;
1619
1627
 
1620
1628
  let parts = stackLines[idx].split(sep).at(-1);
1621
1629
  parts = parts.split(':');
@@ -1653,11 +1661,11 @@ Q5.modules.ai = ($) => {
1653
1661
  '%0A%0AExcerpt+for+context%3A%0A%0A' +
1654
1662
  encodeURIComponent(context);
1655
1663
 
1656
- if (!askAI) console.log('Error in ' + fileBase + ' on line ' + lineNum + ':\n\n' + errLine);
1664
+ console.warn('Error in ' + fileBase + ' on line ' + lineNum + ':\n\n' + errLine);
1657
1665
 
1658
1666
  console.warn('Ask AI ✨ ' + url);
1659
1667
 
1660
- if (askAI) window.open(url, '_blank');
1668
+ if (askAI) return window.open(url, '_blank');
1661
1669
  } catch (err) {}
1662
1670
  };
1663
1671
  };
@@ -1750,12 +1758,7 @@ Q5.modules.color = ($, q) => {
1750
1758
  if (c3) c3 /= 255;
1751
1759
  }
1752
1760
  }
1753
- if (Array.isArray(c0)) {
1754
- c1 = c0[1];
1755
- c2 = c0[2];
1756
- c3 = c0[3];
1757
- c0 = c0[0];
1758
- }
1761
+ if (Array.isArray(c0)) [c0, c1, c2, c3] = c0;
1759
1762
  }
1760
1763
 
1761
1764
  if (c2 == undefined) return new C(c0, c0, c0, c1);
@@ -1767,11 +1770,11 @@ Q5.modules.color = ($, q) => {
1767
1770
  $.blue = (c) => c.b;
1768
1771
  $.alpha = (c) => c.a;
1769
1772
  $.lightness = (c) => {
1770
- if ($._colorMode == 'oklch') return c.l;
1773
+ if (c.l) return c.l;
1771
1774
  return ((0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b) * 100) / 255;
1772
1775
  };
1773
1776
  $.hue = (c) => {
1774
- if ($._colorMode == 'oklch') return c.h;
1777
+ if (c.h) return c.h;
1775
1778
  let r = c.r;
1776
1779
  let g = c.g;
1777
1780
  let b = c.b;
@@ -1930,6 +1933,12 @@ Q5.modules.display = ($) => {
1930
1933
 
1931
1934
  let c = $.canvas;
1932
1935
 
1936
+ $.CENTERED = 'centered';
1937
+ $.FULLSCREEN = 'fullscreen';
1938
+ $.MAXED = 'maxed';
1939
+
1940
+ $.PIXELATED = 'pixelated';
1941
+
1933
1942
  if (Q5._instanceCount == 0 && !Q5._nodejs) {
1934
1943
  document.head.insertAdjacentHTML(
1935
1944
  'beforeend',
@@ -2195,8 +2204,8 @@ Q5.modules.input = ($, q) => {
2195
2204
  }
2196
2205
  };
2197
2206
  Q5.modules.math = ($, q) => {
2198
- $.DEGREES = 'degrees';
2199
- $.RADIANS = 'radians';
2207
+ $.RADIANS = 0;
2208
+ $.DEGREES = 1;
2200
2209
 
2201
2210
  $.PI = Math.PI;
2202
2211
  $.HALF_PI = Math.PI / 2;
@@ -2217,9 +2226,14 @@ Q5.modules.math = ($, q) => {
2217
2226
  $.SHR3 = 1;
2218
2227
  $.LCG = 2;
2219
2228
 
2220
- $.angleMode = (mode) => ($._angleMode = mode);
2221
- $._DEGTORAD = Math.PI / 180;
2222
- $._RADTODEG = 180 / Math.PI;
2229
+ let angleMode = 0;
2230
+
2231
+ $.angleMode = (mode) => {
2232
+ if (mode == 'radians') mode = 0;
2233
+ angleMode = $._angleMode = mode;
2234
+ };
2235
+ let DEGTORAD = ($._DEGTORAD = Math.PI / 180);
2236
+ let RADTODEG = ($._RADTODEG = 180 / Math.PI);
2223
2237
  $.degrees = (x) => x * $._RADTODEG;
2224
2238
  $.radians = (x) => x * $._DEGTORAD;
2225
2239
 
@@ -2245,24 +2259,26 @@ Q5.modules.math = ($, q) => {
2245
2259
  $.sq = (x) => x * x;
2246
2260
  $.fract = (x) => x - Math.floor(x);
2247
2261
 
2248
- for (let fn of ['sin', 'cos', 'tan']) {
2249
- $[fn] = (a) => {
2250
- if ($._angleMode == 'degrees') a = $.radians(a);
2251
- return Math[fn](a);
2252
- };
2253
- }
2262
+ $.sin = (a) => Math.sin(!angleMode ? a : a * DEGTORAD);
2263
+ $.cos = (a) => Math.cos(!angleMode ? a : a * DEGTORAD);
2264
+ $.tan = (a) => Math.tan(!angleMode ? a : a * DEGTORAD);
2265
+
2266
+ $.asin = (x) => {
2267
+ let a = Math.asin(x);
2268
+ return !angleMode ? a : a * RADTODEG;
2269
+ };
2270
+ $.acos = (x) => {
2271
+ let a = Math.acos(x);
2272
+ return !angleMode ? a : a * RADTODEG;
2273
+ };
2274
+ $.atan = (x) => {
2275
+ let a = Math.atan(x);
2276
+ return !angleMode ? a : a * RADTODEG;
2277
+ };
2254
2278
 
2255
- for (let fn of ['asin', 'acos', 'atan']) {
2256
- $[fn] = (x) => {
2257
- let a = Math[fn](x);
2258
- if ($._angleMode == 'degrees') a = $.degrees(a);
2259
- return a;
2260
- };
2261
- }
2262
2279
  $.atan2 = (y, x) => {
2263
2280
  let a = Math.atan2(y, x);
2264
- if ($._angleMode == 'degrees') a = $.degrees(a);
2265
- return a;
2281
+ return !angleMode ? a : a * RADTODEG;
2266
2282
  };
2267
2283
 
2268
2284
  function lcg() {
@@ -2687,18 +2703,19 @@ Q5.modules.vector = ($) => {
2687
2703
  };
2688
2704
 
2689
2705
  Q5.Vector = class {
2690
- constructor(_x, _y, _z, _$) {
2691
- this.x = _x || 0;
2692
- this.y = _y || 0;
2693
- this.z = _z || 0;
2694
- this._$ = _$ || window;
2706
+ constructor(x, y, z, $) {
2707
+ this.x = x || 0;
2708
+ this.y = y || 0;
2709
+ this.z = z || 0;
2710
+ this._$ = $ || window;
2695
2711
  this._cn = null;
2696
2712
  this._cnsq = null;
2697
2713
  }
2698
- set(_x, _y, _z) {
2699
- this.x = _x || 0;
2700
- this.y = _y || 0;
2701
- this.z = _z || 0;
2714
+ set(x, y, z) {
2715
+ this.x = x?.x || x || 0;
2716
+ this.y = x?.y || y || 0;
2717
+ this.z = x?.z || z || 0;
2718
+ return this;
2702
2719
  }
2703
2720
  copy() {
2704
2721
  return new Q5.Vector(this.x, this.y, this.z);
@@ -2820,6 +2837,12 @@ Q5.Vector = class {
2820
2837
  heading() {
2821
2838
  return this._$.atan2(this.y, this.x);
2822
2839
  }
2840
+ setHeading(ang) {
2841
+ let mag = this.mag(); // Calculate the magnitude of the vector
2842
+ this.x = mag * this._$.cos(ang); // Set the new x component
2843
+ this.y = mag * this._$.sin(ang); // Set the new y component
2844
+ return this;
2845
+ }
2823
2846
  rotate(ang) {
2824
2847
  let costh = this._$.cos(ang);
2825
2848
  let sinth = this._$.sin(ang);
@@ -2837,13 +2860,52 @@ Q5.Vector = class {
2837
2860
  }
2838
2861
  lerp() {
2839
2862
  let args = [...arguments];
2840
- let u = this._arg2v(...args.slice(0, -1));
2841
2863
  let amt = args.at(-1);
2864
+ if (amt == 0) return this;
2865
+ let u = this._arg2v(...args.slice(0, -1));
2842
2866
  this.x += (u.x - this.x) * amt;
2843
2867
  this.y += (u.y - this.y) * amt;
2844
2868
  this.z += (u.z - this.z) * amt;
2845
2869
  return this;
2846
2870
  }
2871
+ slerp() {
2872
+ let args = [...arguments];
2873
+ let amt = args.at(-1);
2874
+ if (amt == 0) return this;
2875
+ let u = this._arg2v(...args.slice(0, -1));
2876
+ if (amt == 1) return this.set(u);
2877
+
2878
+ let v0Mag = this.mag();
2879
+ let v1Mag = u.mag();
2880
+
2881
+ if (v0Mag == 0 || v1Mag == 0) {
2882
+ return this.mult(1 - amt).add(u.mult(amt));
2883
+ }
2884
+
2885
+ let axis = Q5.Vector.cross(this, u);
2886
+ let axisMag = axis.mag();
2887
+ let theta = Math.atan2(axisMag, this.dot(u));
2888
+
2889
+ if (axisMag > 0) {
2890
+ axis.div(axisMag);
2891
+ } else if (theta < this._$.HALF_PI) {
2892
+ return this.mult(1 - amt).add(u.mult(amt));
2893
+ } else {
2894
+ if (this.z == 0 && u.z == 0) axis.set(0, 0, 1);
2895
+ else if (this.x != 0) axis.set(this.y, -this.x, 0).normalize();
2896
+ else axis.set(1, 0, 0);
2897
+ }
2898
+
2899
+ let ey = axis.cross(this);
2900
+ let lerpedMagFactor = 1 - amt + (amt * v1Mag) / v0Mag;
2901
+ let cosMultiplier = lerpedMagFactor * Math.cos(amt * theta);
2902
+ let sinMultiplier = lerpedMagFactor * Math.sin(amt * theta);
2903
+
2904
+ this.x = this.x * cosMultiplier + ey.x * sinMultiplier;
2905
+ this.y = this.y * cosMultiplier + ey.y * sinMultiplier;
2906
+ this.z = this.z * cosMultiplier + ey.z * sinMultiplier;
2907
+ return this;
2908
+ }
2847
2909
  reflect(n) {
2848
2910
  n.normalize();
2849
2911
  return this.sub(n.mult(2 * this.dot(n)));
@@ -2896,6 +2958,7 @@ Q5.Vector.div = (v, u) => v.copy().div(u);
2896
2958
  Q5.Vector.dot = (v, u) => v.copy().dot(u);
2897
2959
  Q5.Vector.equals = (v, u, epsilon) => v.equals(u, epsilon);
2898
2960
  Q5.Vector.lerp = (v, u, amt) => v.copy().lerp(u, amt);
2961
+ Q5.Vector.slerp = (v, u, amt) => v.copy().slerp(u, amt);
2899
2962
  Q5.Vector.limit = (v, m) => v.copy().limit(m);
2900
2963
  Q5.Vector.heading = (v) => this._$.atan2(v.y, v.x);
2901
2964
  Q5.Vector.magSq = (v) => v.x * v.x + v.y * v.y + v.z * v.z;
@@ -2922,70 +2985,62 @@ Q5.renderers.webgpu.canvas = ($, q) => {
2922
2985
 
2923
2986
  if ($.colorMode) $.colorMode('rgb', 'float');
2924
2987
 
2925
- let pass, colorsStack, envBindGroup, transformBindGroup;
2926
-
2927
- $._createCanvas = (w, h, opt) => {
2928
- q.ctx = q.drawingContext = c.getContext('webgpu');
2929
-
2930
- opt.format = navigator.gpu.getPreferredCanvasFormat();
2931
- opt.device = Q5.device;
2988
+ let pass;
2932
2989
 
2933
- $.ctx.configure(opt);
2990
+ $.pipelines = [];
2934
2991
 
2935
- $.pipelines = [];
2992
+ // local variables used for slightly better performance
2993
+ // stores pipeline shifts and vertex counts/image indices
2994
+ let drawStack = ($.drawStack = []);
2936
2995
 
2937
- // pipeline changes for each draw call
2938
- $.pipelinesStack = [];
2996
+ // colors used for each draw call
2997
+ let colorsStack = ($.colorsStack = [1, 1, 1, 1]);
2939
2998
 
2940
- // vertices for each draw call
2941
- $.verticesStack = [];
2942
-
2943
- // number of vertices for each draw call
2944
- $.drawStack = [];
2999
+ $._envLayout = Q5.device.createBindGroupLayout({
3000
+ entries: [
3001
+ {
3002
+ binding: 0,
3003
+ visibility: GPUShaderStage.VERTEX,
3004
+ buffer: {
3005
+ type: 'uniform',
3006
+ hasDynamicOffset: false
3007
+ }
3008
+ }
3009
+ ]
3010
+ });
2945
3011
 
2946
- // colors used for each draw call
2947
- colorsStack = $.colorsStack = [1, 1, 1, 1];
3012
+ $._transformLayout = Q5.device.createBindGroupLayout({
3013
+ entries: [
3014
+ {
3015
+ binding: 0,
3016
+ visibility: GPUShaderStage.VERTEX,
3017
+ buffer: {
3018
+ type: 'read-only-storage',
3019
+ hasDynamicOffset: false
3020
+ }
3021
+ }
3022
+ ]
3023
+ });
2948
3024
 
2949
- // current color index, used to associate a vertex with a color
2950
- $._colorIndex = 0;
3025
+ $.bindGroupLayouts = [$._envLayout, $._transformLayout];
2951
3026
 
2952
- let envLayout = Q5.device.createBindGroupLayout({
2953
- entries: [
2954
- {
2955
- binding: 0,
2956
- visibility: GPUShaderStage.VERTEX,
2957
- buffer: {
2958
- type: 'uniform',
2959
- hasDynamicOffset: false
2960
- }
2961
- }
2962
- ]
2963
- });
3027
+ const uniformBuffer = Q5.device.createBuffer({
3028
+ size: 8, // Size of two floats
3029
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
3030
+ });
2964
3031
 
2965
- let transformLayout = Q5.device.createBindGroupLayout({
2966
- entries: [
2967
- {
2968
- binding: 0,
2969
- visibility: GPUShaderStage.VERTEX,
2970
- buffer: {
2971
- type: 'read-only-storage',
2972
- hasDynamicOffset: false
2973
- }
2974
- }
2975
- ]
2976
- });
3032
+ $._createCanvas = (w, h, opt) => {
3033
+ q.ctx = q.drawingContext = c.getContext('webgpu');
2977
3034
 
2978
- $.bindGroupLayouts = [envLayout, transformLayout];
3035
+ opt.format = navigator.gpu.getPreferredCanvasFormat();
3036
+ opt.device = Q5.device;
2979
3037
 
2980
- const uniformBuffer = Q5.device.createBuffer({
2981
- size: 8, // Size of two floats
2982
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
2983
- });
3038
+ $.ctx.configure(opt);
2984
3039
 
2985
3040
  Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([$.canvas.hw, $.canvas.hh]));
2986
3041
 
2987
- envBindGroup = Q5.device.createBindGroup({
2988
- layout: envLayout,
3042
+ $._envBindGroup = Q5.device.createBindGroup({
3043
+ layout: $._envLayout,
2989
3044
  entries: [
2990
3045
  {
2991
3046
  binding: 0,
@@ -3044,14 +3099,14 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3044
3099
  if (!x && !y && !z) return;
3045
3100
  // Update the translation values
3046
3101
  $._matrix[3] += x;
3047
- $._matrix[7] += y;
3102
+ $._matrix[7] -= y;
3048
3103
  $._matrix[11] += z || 0;
3049
3104
  $._matrixDirty = true;
3050
3105
  };
3051
3106
 
3052
3107
  $.rotate = (r) => {
3053
3108
  if (!r) return;
3054
- if ($._angleMode == 'degrees') r = $.radians(r);
3109
+ if ($._angleMode) r *= $._DEGTORAD;
3055
3110
 
3056
3111
  let cosR = Math.cos(r);
3057
3112
  let sinR = Math.sin(r);
@@ -3092,10 +3147,72 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3092
3147
  $._matrixDirty = false;
3093
3148
  };
3094
3149
 
3150
+ // current color index, used to associate a vertex with a color
3151
+ let colorIndex = 0;
3152
+ const addColor = (r, g, b, a = 1) => {
3153
+ if (typeof r == 'string') r = $.color(r);
3154
+ else if (b == undefined) {
3155
+ // grayscale mode `fill(1, 0.5)`
3156
+ a = g ?? 1;
3157
+ g = b = r;
3158
+ }
3159
+ if (r._q5Color) colorsStack.push(r.r, r.g, r.b, r.a);
3160
+ else colorsStack.push(r, g, b, a);
3161
+ colorIndex++;
3162
+ };
3163
+
3164
+ $.fill = (r, g, b, a) => {
3165
+ addColor(r, g, b, a);
3166
+ $._doFill = true;
3167
+ $._fillIndex = colorIndex;
3168
+ };
3169
+ $.stroke = (r, g, b, a) => {
3170
+ addColor(r, g, b, a);
3171
+ $._doStroke = true;
3172
+ $._strokeIndex = colorIndex;
3173
+ };
3174
+
3175
+ $.noFill = () => ($._doFill = false);
3176
+ $.noStroke = () => ($._doStroke = false);
3177
+
3178
+ $._strokeWeight = 1;
3179
+ $.strokeWeight = (v) => ($._strokeWeight = Math.abs(v));
3180
+
3181
+ $._calcBox = (x, y, w, h, mode) => {
3182
+ let hw = w / 2;
3183
+ let hh = h / 2;
3184
+
3185
+ // left, right, top, bottom
3186
+ let l, r, t, b;
3187
+ if (!mode || mode == 'corner') {
3188
+ // CORNER
3189
+ l = x;
3190
+ r = x + w;
3191
+ t = -y;
3192
+ b = -(y + h);
3193
+ } else if (mode == 'center') {
3194
+ l = x - hw;
3195
+ r = x + hw;
3196
+ t = -(y - hh);
3197
+ b = -(y + hh);
3198
+ } else {
3199
+ // CORNERS
3200
+ l = x;
3201
+ r = w;
3202
+ t = -y;
3203
+ b = -h;
3204
+ }
3205
+
3206
+ return [l, r, t, b];
3207
+ };
3208
+
3209
+ $.clear = () => {};
3210
+
3095
3211
  $._beginRender = () => {
3096
3212
  $.encoder = Q5.device.createCommandEncoder();
3097
3213
 
3098
3214
  pass = q.pass = $.encoder.beginRenderPass({
3215
+ label: 'q5-webgpu',
3099
3216
  colorAttachments: [
3100
3217
  {
3101
3218
  view: ctx.getCurrentTexture().createView(),
@@ -3107,9 +3224,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3107
3224
  };
3108
3225
 
3109
3226
  $._render = () => {
3110
- pass.setBindGroup(0, envBindGroup);
3111
-
3112
- if (transformStates.length > 1 || !transformBindGroup) {
3227
+ if (transformStates.length > 1 || !$._transformBindGroup) {
3113
3228
  const transformBuffer = Q5.device.createBuffer({
3114
3229
  size: transformStates.length * 64, // Size of 16 floats
3115
3230
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
@@ -3117,8 +3232,8 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3117
3232
 
3118
3233
  Q5.device.queue.writeBuffer(transformBuffer, 0, new Float32Array(transformStates.flat()));
3119
3234
 
3120
- transformBindGroup = Q5.device.createBindGroup({
3121
- layout: $.bindGroupLayouts[1],
3235
+ $._transformBindGroup = Q5.device.createBindGroup({
3236
+ layout: $._transformLayout,
3122
3237
  entries: [
3123
3238
  {
3124
3239
  binding: 0,
@@ -3130,27 +3245,39 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3130
3245
  });
3131
3246
  }
3132
3247
 
3133
- pass.setBindGroup(1, transformBindGroup);
3248
+ pass.setBindGroup(0, $._envBindGroup);
3249
+ pass.setBindGroup(1, $._transformBindGroup);
3134
3250
 
3135
- // run pre-render methods
3136
3251
  for (let m of $._hooks.preRender) m();
3137
3252
 
3138
- // local variables used for performance
3139
- let drawStack = $.drawStack;
3140
-
3141
3253
  let drawVertOffset = 0;
3254
+ let imageVertOffset = 0;
3142
3255
  let curPipelineIndex = -1;
3256
+ let curTextureIndex = -1;
3257
+
3258
+ pass.setPipeline($.pipelines[0]);
3143
3259
 
3144
3260
  for (let i = 0; i < drawStack.length; i += 2) {
3261
+ let v = drawStack[i + 1];
3262
+
3145
3263
  if (curPipelineIndex != drawStack[i]) {
3146
3264
  curPipelineIndex = drawStack[i];
3147
3265
  pass.setPipeline($.pipelines[curPipelineIndex]);
3148
3266
  }
3149
3267
 
3150
- let vertCount = drawStack[i + 1];
3151
- pass.draw(vertCount, 1, drawVertOffset, 0);
3152
- drawVertOffset += vertCount;
3268
+ if (curPipelineIndex == 0) {
3269
+ pass.draw(v, 1, drawVertOffset, 0);
3270
+ drawVertOffset += v;
3271
+ } else if (curPipelineIndex == 1) {
3272
+ if (curTextureIndex != v) {
3273
+ pass.setBindGroup(3, $._textureBindGroups[v]);
3274
+ }
3275
+ pass.draw(6, 1, imageVertOffset, 0);
3276
+ imageVertOffset += 6;
3277
+ }
3153
3278
  }
3279
+
3280
+ for (let m of $._hooks.postRender) m();
3154
3281
  };
3155
3282
 
3156
3283
  $._finishRender = () => {
@@ -3160,48 +3287,15 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3160
3287
  q.pass = $.encoder = null;
3161
3288
 
3162
3289
  // clear the stacks for the next frame
3163
- $.verticesStack.length = 0;
3164
3290
  $.drawStack.length = 0;
3165
3291
  $.colorsStack.length = 4;
3166
- $.pipelinesStack.length = 0;
3167
- $._colorIndex = 0;
3292
+ colorIndex = 0;
3168
3293
  rotation = 0;
3169
3294
  $.resetMatrix();
3170
3295
  $._matrixDirty = false;
3171
3296
  $.transformStates.length = 1;
3172
3297
  $._transformIndexStack.length = 0;
3173
3298
  };
3174
-
3175
- function addColor(r, g, b, a = 1) {
3176
- if (typeof r == 'string') r = Q5.color(r);
3177
- // grayscale mode `fill(1, 0.5)`
3178
- if (b == undefined) {
3179
- a = g;
3180
- g = b = r;
3181
- }
3182
- if (r._q5Color) colorsStack.push(...r.levels);
3183
- else colorsStack.push(r, g, b, a);
3184
- $._colorIndex++;
3185
- }
3186
-
3187
- $.fill = function () {
3188
- addColor(...arguments);
3189
- $._doFill = true;
3190
- $._fillIndex = $._colorIndex;
3191
- };
3192
- $.stroke = function () {
3193
- addColor(...arguments);
3194
- $._doStroke = true;
3195
- $._fillIndex = $._colorIndex;
3196
- };
3197
-
3198
- $.noFill = () => ($._doFill = false);
3199
- $.noStroke = () => ($._doStroke = false);
3200
-
3201
- $._strokeWeight = 1;
3202
- $.strokeWeight = (v) => ($._strokeWeight = v);
3203
-
3204
- $.clear = () => {};
3205
3299
  };
3206
3300
 
3207
3301
  Q5.webgpu = async function (scope, parent) {
@@ -3220,41 +3314,18 @@ Q5.webgpu = async function (scope, parent) {
3220
3314
  return new Q5(scope, parent, 'webgpu');
3221
3315
  };
3222
3316
  Q5.renderers.webgpu.drawing = ($, q) => {
3223
- $.CLOSE = 1;
3224
-
3225
- let verticesStack, drawStack, colorsStack;
3226
-
3227
- $._hooks.postCanvas.push(() => {
3228
- let colorsLayout = Q5.device.createBindGroupLayout({
3229
- entries: [
3230
- {
3231
- binding: 0,
3232
- visibility: GPUShaderStage.FRAGMENT,
3233
- buffer: {
3234
- type: 'read-only-storage',
3235
- hasDynamicOffset: false
3236
- }
3237
- }
3238
- ]
3239
- });
3317
+ let c = $.canvas;
3240
3318
 
3241
- $.bindGroupLayouts.push(colorsLayout);
3319
+ let drawStack = $.drawStack;
3320
+ let colorsStack = $.colorsStack;
3242
3321
 
3243
- verticesStack = $.verticesStack;
3244
- drawStack = $.drawStack;
3245
- colorsStack = $.colorsStack;
3322
+ let verticesStack = [];
3246
3323
 
3247
- let vertexBufferLayout = {
3248
- arrayStride: 16, // 2 coordinates + 1 color index + 1 transform index * 4 bytes each
3249
- attributes: [
3250
- { format: 'float32x2', offset: 0, shaderLocation: 0 }, // position
3251
- { format: 'float32', offset: 8, shaderLocation: 1 }, // colorIndex
3252
- { format: 'float32', offset: 12, shaderLocation: 2 } // transformIndex
3253
- ]
3254
- };
3324
+ let colorIndex, colorsLayout;
3255
3325
 
3256
- let vertexShader = Q5.device.createShaderModule({
3257
- code: `
3326
+ let vertexShader = Q5.device.createShaderModule({
3327
+ label: 'drawingVertexShader',
3328
+ code: `
3258
3329
  struct VertexOutput {
3259
3330
  @builtin(position) position: vec4<f32>,
3260
3331
  @location(1) colorIndex: f32
@@ -3281,10 +3352,11 @@ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) colorIndex: f32, @locati
3281
3352
  return output;
3282
3353
  }
3283
3354
  `
3284
- });
3355
+ });
3285
3356
 
3286
- let fragmentShader = Q5.device.createShaderModule({
3287
- code: `
3357
+ let fragmentShader = Q5.device.createShaderModule({
3358
+ label: 'drawingFragmentShader',
3359
+ code: `
3288
3360
  @group(2) @binding(0) var<storage, read> uColors : array<vec4<f32>>;
3289
3361
 
3290
3362
  @fragment
@@ -3293,31 +3365,31 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3293
3365
  return mix(uColors[index], uColors[index + 1u], fract(colorIndex));
3294
3366
  }
3295
3367
  `
3296
- });
3368
+ });
3297
3369
 
3298
- let pipelineLayout = Q5.device.createPipelineLayout({
3299
- bindGroupLayouts: $.bindGroupLayouts
3300
- });
3370
+ colorsLayout = Q5.device.createBindGroupLayout({
3371
+ entries: [
3372
+ {
3373
+ binding: 0,
3374
+ visibility: GPUShaderStage.FRAGMENT,
3375
+ buffer: {
3376
+ type: 'read-only-storage',
3377
+ hasDynamicOffset: false
3378
+ }
3379
+ }
3380
+ ]
3381
+ });
3301
3382
 
3302
- $._createPipeline = (blendConfig) => {
3303
- return Q5.device.createRenderPipeline({
3304
- layout: pipelineLayout,
3305
- vertex: {
3306
- module: vertexShader,
3307
- entryPoint: 'vertexMain',
3308
- buffers: [vertexBufferLayout]
3309
- },
3310
- fragment: {
3311
- module: fragmentShader,
3312
- entryPoint: 'fragmentMain',
3313
- targets: [{ format: 'bgra8unorm', blend: blendConfig }]
3314
- },
3315
- primitive: { topology: 'triangle-list' }
3316
- });
3317
- };
3383
+ $.bindGroupLayouts.push(colorsLayout);
3318
3384
 
3319
- $.pipelines[0] = $._createPipeline(blendConfigs.normal);
3320
- });
3385
+ let vertexBufferLayout = {
3386
+ arrayStride: 16, // 2 coordinates + 1 color index + 1 transform index * 4 bytes each
3387
+ attributes: [
3388
+ { format: 'float32x2', offset: 0, shaderLocation: 0 }, // position
3389
+ { format: 'float32', offset: 8, shaderLocation: 1 }, // colorIndex
3390
+ { format: 'float32', offset: 12, shaderLocation: 2 } // transformIndex
3391
+ ]
3392
+ };
3321
3393
 
3322
3394
  // prettier-ignore
3323
3395
  let blendFactors = [
@@ -3362,7 +3434,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3362
3434
 
3363
3435
  $.blendConfigs = {};
3364
3436
 
3365
- Object.entries(blendModes).forEach(([name, mode]) => {
3437
+ for (const [name, mode] of Object.entries(blendModes)) {
3366
3438
  $.blendConfigs[name] = {
3367
3439
  color: {
3368
3440
  srcFactor: blendFactors[mode[0]],
@@ -3375,7 +3447,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3375
3447
  operation: blendOps[mode[5]]
3376
3448
  }
3377
3449
  };
3378
- });
3450
+ }
3379
3451
 
3380
3452
  $._blendMode = 'normal';
3381
3453
  $.blendMode = (mode) => {
@@ -3386,6 +3458,31 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3386
3458
  $.pipelines[0] = $._createPipeline($.blendConfigs[mode]);
3387
3459
  };
3388
3460
 
3461
+ let pipelineLayout = Q5.device.createPipelineLayout({
3462
+ label: 'drawingPipelineLayout',
3463
+ bindGroupLayouts: $.bindGroupLayouts
3464
+ });
3465
+
3466
+ $._createPipeline = (blendConfig) => {
3467
+ return Q5.device.createRenderPipeline({
3468
+ label: 'drawingPipeline',
3469
+ layout: pipelineLayout,
3470
+ vertex: {
3471
+ module: vertexShader,
3472
+ entryPoint: 'vertexMain',
3473
+ buffers: [vertexBufferLayout]
3474
+ },
3475
+ fragment: {
3476
+ module: fragmentShader,
3477
+ entryPoint: 'fragmentMain',
3478
+ targets: [{ format: 'bgra8unorm', blend: blendConfig }]
3479
+ },
3480
+ primitive: { topology: 'triangle-list' }
3481
+ });
3482
+ };
3483
+
3484
+ $.pipelines[0] = $._createPipeline($.blendConfigs.normal);
3485
+
3389
3486
  let shapeVertices;
3390
3487
 
3391
3488
  $.beginShape = () => {
@@ -3394,7 +3491,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3394
3491
 
3395
3492
  $.vertex = (x, y) => {
3396
3493
  if ($._matrixDirty) $._saveMatrix();
3397
- shapeVertices.push(x, -y, $._colorIndex, $._transformIndex);
3494
+ shapeVertices.push(x, -y, $._fillIndex, $._transformIndex);
3398
3495
  };
3399
3496
 
3400
3497
  $.endShape = (close) => {
@@ -3438,38 +3535,61 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3438
3535
  $.endShape(1);
3439
3536
  };
3440
3537
 
3441
- $.rect = (x, y, w, h) => {
3442
- let hw = w / 2;
3443
- let hh = h / 2;
3538
+ $.rectMode = (x) => ($._rectMode = x);
3444
3539
 
3445
- let left = x - hw;
3446
- let right = x + hw;
3447
- let top = -(y - hh); // y is inverted in WebGPU
3448
- let bottom = -(y + hh);
3540
+ $.rect = (x, y, w, h) => {
3541
+ let [l, r, t, b] = $._calcBox(x, y, w, h, $._rectMode);
3449
3542
 
3450
- let ci = $._colorIndex;
3543
+ let ci = colorIndex ?? $._fillIndex;
3451
3544
  if ($._matrixDirty) $._saveMatrix();
3452
3545
  let ti = $._transformIndex;
3453
3546
  // two triangles make a rectangle
3454
3547
  // prettier-ignore
3455
3548
  verticesStack.push(
3456
- left, top, ci, ti,
3457
- right, top, ci, ti,
3458
- left, bottom, ci, ti,
3459
- right, top, ci, ti,
3460
- left, bottom, ci, ti,
3461
- right, bottom, ci, ti
3549
+ l, t, ci, ti,
3550
+ r, t, ci, ti,
3551
+ l, b, ci, ti,
3552
+ r, t, ci, ti,
3553
+ l, b, ci, ti,
3554
+ r, b, ci, ti
3462
3555
  );
3463
3556
  drawStack.push(0, 6);
3464
3557
  };
3465
3558
 
3466
3559
  $.point = (x, y) => {
3560
+ colorIndex = $._strokeIndex;
3467
3561
  let sw = $._strokeWeight;
3468
- if (sw == 1) $.rect(x, y, 1, 1);
3469
- else $.ellipse(x, y, sw, sw);
3562
+ if (sw < 2) {
3563
+ sw = Math.round(sw);
3564
+ $.rect(x, y, sw, sw);
3565
+ } else $.ellipse(x, y, sw, sw);
3566
+ colorIndex = null;
3470
3567
  };
3471
3568
 
3472
- $.background = () => {};
3569
+ $.line = (x1, y1, x2, y2) => {
3570
+ colorIndex = $._strokeIndex;
3571
+
3572
+ $.push();
3573
+ $.translate(x1, y1);
3574
+ $.rotate($.atan2(y2 - y1, x2 - x1));
3575
+ let length = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
3576
+ let sw = $._strokeWeight;
3577
+ $.rect(0, -sw / 2, length, sw);
3578
+ $.pop();
3579
+
3580
+ colorIndex = null;
3581
+ };
3582
+
3583
+ $.background = (r, g, b, a) => {
3584
+ $.push();
3585
+ $.resetMatrix();
3586
+ if (r.src) $.image(r, -c.hw, -c.hh, c.w, c.h);
3587
+ else {
3588
+ $.fill(r, g, b, a);
3589
+ $.rect(-c.hw, -c.hh, c.w, c.h);
3590
+ }
3591
+ $.pop();
3592
+ };
3473
3593
 
3474
3594
  /**
3475
3595
  * Derived from: ceil(Math.log(d) * 7) * 2 - ceil(28)
@@ -3479,9 +3599,10 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3479
3599
  */
3480
3600
  // prettier-ignore
3481
3601
  const getArcSegments = (d) =>
3482
- d < 14 ? 8 :
3483
- d < 16 ? 10 :
3484
- d < 18 ? 12 :
3602
+ d < 4 ? 6 :
3603
+ d < 6 ? 8 :
3604
+ d < 10 ? 10 :
3605
+ d < 16 ? 12 :
3485
3606
  d < 20 ? 14 :
3486
3607
  d < 22 ? 16 :
3487
3608
  d < 24 ? 18 :
@@ -3515,7 +3636,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3515
3636
 
3516
3637
  let t = 0; // theta
3517
3638
  const angleIncrement = $.TAU / n;
3518
- const ci = $._colorIndex;
3639
+ const ci = colorIndex ?? $._fillIndex;
3519
3640
  if ($._matrixDirty) $._saveMatrix();
3520
3641
  const ti = $._transformIndex;
3521
3642
  let vx1, vy1, vx2, vy2;
@@ -3537,12 +3658,16 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3537
3658
  $.circle = (x, y, d) => $.ellipse(x, y, d, d);
3538
3659
 
3539
3660
  $._hooks.preRender.push(() => {
3661
+ $.pass.setPipeline($.pipelines[0]);
3662
+
3663
+ const vertices = new Float32Array(verticesStack);
3664
+
3540
3665
  const vertexBuffer = Q5.device.createBuffer({
3541
- size: verticesStack.length * 6,
3666
+ size: vertices.byteLength,
3542
3667
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
3543
3668
  });
3544
3669
 
3545
- Q5.device.queue.writeBuffer(vertexBuffer, 0, new Float32Array(verticesStack));
3670
+ Q5.device.queue.writeBuffer(vertexBuffer, 0, vertices);
3546
3671
  $.pass.setVertexBuffer(0, vertexBuffer);
3547
3672
 
3548
3673
  const colorsBuffer = Q5.device.createBuffer({
@@ -3552,8 +3677,8 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3552
3677
 
3553
3678
  Q5.device.queue.writeBuffer(colorsBuffer, 0, new Float32Array(colorsStack));
3554
3679
 
3555
- const colorsBindGroup = Q5.device.createBindGroup({
3556
- layout: $.bindGroupLayouts[2],
3680
+ $._colorsBindGroup = Q5.device.createBindGroup({
3681
+ layout: colorsLayout,
3557
3682
  entries: [
3558
3683
  {
3559
3684
  binding: 0,
@@ -3567,136 +3692,206 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3567
3692
  });
3568
3693
 
3569
3694
  // set the bind group once before rendering
3570
- $.pass.setBindGroup(2, colorsBindGroup);
3695
+ $.pass.setBindGroup(2, $._colorsBindGroup);
3696
+ });
3697
+
3698
+ $._hooks.postRender.push(() => {
3699
+ verticesStack.length = 0;
3571
3700
  });
3572
3701
  };
3573
3702
  Q5.renderers.webgpu.image = ($, q) => {
3574
- $.imageStack = [];
3575
- $.textures = [];
3703
+ $._textureBindGroups = [];
3704
+ let verticesStack = [];
3576
3705
 
3577
- $._hooks.postCanvas.push(() => {
3578
- let imageVertexShader = Q5.device.createShaderModule({
3579
- code: `
3706
+ let vertexShader = Q5.device.createShaderModule({
3707
+ label: 'imageVertexShader',
3708
+ code: `
3580
3709
  struct VertexOutput {
3581
- @builtin(position) position: vec4<f32>,
3582
- @location(0) texCoord: vec2<f32>,
3583
- @location(1) textureIndex: f32
3710
+ @builtin(position) position: vec4<f32>,
3711
+ @location(0) texCoord: vec2<f32>
3584
3712
  };
3585
3713
 
3586
3714
  struct Uniforms {
3587
- halfWidth: f32,
3588
- halfHeight: f32
3715
+ halfWidth: f32,
3716
+ halfHeight: f32
3589
3717
  };
3590
3718
 
3591
3719
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3592
3720
  @group(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
3593
3721
 
3594
3722
  @vertex
3595
- fn vertexMain(@location(0) pos: vec2<f32>, @location(1) texCoord: vec2<f32>, @location(2) transformIndex: f32, @location(3) textureIndex: f32) -> VertexOutput {
3596
- var vert = vec4<f32>(pos, 0.0, 1.0);
3597
- vert *= transforms[i32(transformIndex)];
3598
- vert.x /= uniforms.halfWidth;
3599
- vert.y /= uniforms.halfHeight;
3600
-
3601
- var output: VertexOutput;
3602
- output.position = vert;
3603
- output.texCoord = texCoord;
3604
- output.textureIndex = textureIndex;
3605
- return output;
3723
+ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) texCoord: vec2<f32>, @location(2) transformIndex: f32) -> VertexOutput {
3724
+ var vert = vec4<f32>(pos, 0.0, 1.0);
3725
+ vert *= transforms[i32(transformIndex)];
3726
+ vert.x /= uniforms.halfWidth;
3727
+ vert.y /= uniforms.halfHeight;
3728
+
3729
+ var output: VertexOutput;
3730
+ output.position = vert;
3731
+ output.texCoord = texCoord;
3732
+ return output;
3606
3733
  }
3607
- `
3608
- });
3734
+ `
3735
+ });
3609
3736
 
3610
- let imageFragmentShader = Q5.device.createShaderModule({
3611
- code: `
3612
- @group(0) @binding(0) var samp: sampler;
3613
- @group(0) @binding(1) var textures: array<texture_2d<f32>>;
3737
+ let fragmentShader = Q5.device.createShaderModule({
3738
+ label: 'imageFragmentShader',
3739
+ code: `
3740
+ @group(3) @binding(0) var samp: sampler;
3741
+ @group(3) @binding(1) var texture: texture_2d<f32>;
3614
3742
 
3615
3743
  @fragment
3616
- fn fragmentMain(@location(0) texCoord: vec2<f32>, @location(1) textureIndex: f32) -> @location(0) vec4<f32> {
3617
- return textureSample(textures[i32(textureIndex)], samp, texCoord);
3744
+ fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
3745
+ // Sample the texture using the interpolated texture coordinate
3746
+ return textureSample(texture, samp, texCoord);
3618
3747
  }
3619
- `
3620
- });
3748
+ `
3749
+ });
3621
3750
 
3622
- const bindGroupLayouts = [
3623
- Q5.device.createBindGroupLayout({
3624
- entries: [
3625
- { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
3626
- { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
3627
- { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: '2d', sampleType: 'float' } }
3628
- ]
3629
- }),
3630
- Q5.device.createBindGroupLayout({
3631
- entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }]
3632
- })
3633
- ];
3751
+ let textureLayout = Q5.device.createBindGroupLayout({
3752
+ label: 'textureLayout',
3753
+ entries: [
3754
+ {
3755
+ binding: 0,
3756
+ visibility: GPUShaderStage.FRAGMENT,
3757
+ sampler: { type: 'filtering' }
3758
+ },
3759
+ {
3760
+ binding: 1,
3761
+ visibility: GPUShaderStage.FRAGMENT,
3762
+ texture: { viewDimension: '2d', sampleType: 'float' }
3763
+ }
3764
+ ]
3765
+ });
3634
3766
 
3635
- const pipelineLayout = Q5.device.createPipelineLayout({
3636
- bindGroupLayouts: bindGroupLayouts
3637
- });
3767
+ const vertexBufferLayout = {
3768
+ arrayStride: 20,
3769
+ attributes: [
3770
+ { shaderLocation: 0, offset: 0, format: 'float32x2' },
3771
+ { shaderLocation: 1, offset: 8, format: 'float32x2' },
3772
+ { shaderLocation: 2, offset: 16, format: 'float32' } // transformIndex
3773
+ ]
3774
+ };
3638
3775
 
3639
- $.pipelines[1] = Q5.device.createRenderPipeline({
3640
- layout: pipelineLayout,
3641
- vertex: {
3642
- module: imageVertexShader,
3643
- entryPoint: 'vertexMain',
3644
- buffers: [
3645
- {
3646
- arrayStride: 5 * 4, // 4 floats for position and texCoord, 1 float for textureIndex
3647
- attributes: [
3648
- { shaderLocation: 0, offset: 0, format: 'float32x2' },
3649
- { shaderLocation: 1, offset: 2 * 4, format: 'float32x2' },
3650
- { shaderLocation: 2, offset: 4 * 4, format: 'float32' } // textureIndex
3651
- ]
3776
+ $.bindGroupLayouts.push(textureLayout);
3777
+
3778
+ const pipelineLayout = Q5.device.createPipelineLayout({
3779
+ label: 'imagePipelineLayout',
3780
+ bindGroupLayouts: $.bindGroupLayouts
3781
+ });
3782
+
3783
+ $.pipelines[1] = Q5.device.createRenderPipeline({
3784
+ label: 'imagePipeline',
3785
+ layout: pipelineLayout,
3786
+ vertex: {
3787
+ module: vertexShader,
3788
+ entryPoint: 'vertexMain',
3789
+ buffers: [{ arrayStride: 0, attributes: [] }, vertexBufferLayout]
3790
+ },
3791
+ fragment: {
3792
+ module: fragmentShader,
3793
+ entryPoint: 'fragmentMain',
3794
+ targets: [
3795
+ {
3796
+ format: 'bgra8unorm',
3797
+ blend: $.blendConfigs?.normal || {
3798
+ color: {
3799
+ srcFactor: 'src-alpha',
3800
+ dstFactor: 'one-minus-src-alpha',
3801
+ operation: 'add'
3802
+ },
3803
+ alpha: {
3804
+ srcFactor: 'src-alpha',
3805
+ dstFactor: 'one-minus-src-alpha',
3806
+ operation: 'add'
3807
+ }
3652
3808
  }
3653
- ]
3654
- },
3655
- fragment: {
3656
- module: imageFragmentShader,
3657
- entryPoint: 'fragmentMain',
3658
- targets: [{ format: 'bgra8unorm' }]
3659
- },
3660
- primitive: {
3661
- topology: 'triangle-list'
3662
- }
3663
- });
3809
+ }
3810
+ ]
3811
+ },
3812
+ primitive: {
3813
+ topology: 'triangle-list'
3814
+ }
3815
+ });
3664
3816
 
3665
- $.sampler = Q5.device.createSampler({
3666
- magFilter: 'linear',
3667
- minFilter: 'linear'
3668
- });
3817
+ let sampler = Q5.device.createSampler({
3818
+ magFilter: 'linear',
3819
+ minFilter: 'linear'
3669
3820
  });
3670
3821
 
3671
- $.loadImage = async (src) => {
3672
- const img = new Image();
3673
- img.onload = async () => {
3674
- const imageBitmap = await createImageBitmap(img);
3675
- const texture = Q5.device.createTexture({
3676
- size: [img.width, img.height, 1],
3677
- format: 'bgra8unorm',
3678
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
3679
- });
3822
+ $._createTexture = (img) => {
3823
+ let textureSize = [img.width, img.height, 1];
3824
+
3825
+ const texture = Q5.device.createTexture({
3826
+ size: textureSize,
3827
+ format: 'bgra8unorm',
3828
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
3829
+ });
3680
3830
 
3681
- Q5.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [img.width, img.height, 1]);
3831
+ Q5.device.queue.copyExternalImageToTexture({ source: img }, { texture }, textureSize);
3682
3832
 
3683
- img.texture = texture;
3684
- img.index = $.textures.length;
3685
- $.textures.push(texture);
3833
+ img.textureIndex = $._textureBindGroups.length;
3834
+
3835
+ const textureBindGroup = Q5.device.createBindGroup({
3836
+ layout: textureLayout,
3837
+ entries: [
3838
+ { binding: 0, resource: sampler },
3839
+ { binding: 1, resource: texture.createView() }
3840
+ ]
3841
+ });
3842
+ $._textureBindGroups.push(textureBindGroup);
3843
+ };
3844
+
3845
+ $.loadImage = $.loadTexture = (src) => {
3846
+ q._preloadCount++;
3847
+ const img = new Image();
3848
+ img.crossOrigin = 'Anonymous';
3849
+ img.onload = () => {
3850
+ $._createTexture(img);
3851
+ q._preloadCount--;
3686
3852
  };
3687
- img.onerror = reject;
3688
3853
  img.src = src;
3689
3854
  return img;
3690
3855
  };
3691
3856
 
3857
+ $.imageMode = (x) => ($._imageMode = x);
3858
+
3859
+ $.image = (img, x, y, w, h) => {
3860
+ if (img.canvas) img = img.canvas;
3861
+ if (img.textureIndex == undefined) return;
3862
+
3863
+ if ($._matrixDirty) $._saveMatrix();
3864
+ let ti = $._transformIndex;
3865
+
3866
+ w ??= img.width;
3867
+ h ??= img.height;
3868
+
3869
+ w /= $._pixelDensity;
3870
+ h /= $._pixelDensity;
3871
+
3872
+ let [l, r, t, b] = $._calcBox(x, y, w, h, $._imageMode);
3873
+
3874
+ // prettier-ignore
3875
+ verticesStack.push(
3876
+ l, t, 0, 0, ti,
3877
+ r, t, 1, 0, ti,
3878
+ l, b, 0, 1, ti,
3879
+ r, t, 1, 0, ti,
3880
+ l, b, 0, 1, ti,
3881
+ r, b, 1, 1, ti
3882
+ );
3883
+
3884
+ $.drawStack.push(1, img.textureIndex);
3885
+ };
3886
+
3692
3887
  $._hooks.preRender.push(() => {
3693
- if (!$.imageStack.length) return;
3888
+ if (!$._textureBindGroups.length) return;
3694
3889
 
3695
3890
  // Switch to image pipeline
3696
3891
  $.pass.setPipeline($.pipelines[1]);
3697
3892
 
3698
3893
  // Create a vertex buffer for the image quads
3699
- const vertices = new Float32Array($.vertexStack);
3894
+ const vertices = new Float32Array(verticesStack);
3700
3895
 
3701
3896
  const vertexBuffer = Q5.device.createBuffer({
3702
3897
  size: vertices.byteLength,
@@ -3704,57 +3899,48 @@ fn fragmentMain(@location(0) texCoord: vec2<f32>, @location(1) textureIndex: f32
3704
3899
  });
3705
3900
 
3706
3901
  Q5.device.queue.writeBuffer(vertexBuffer, 0, vertices);
3707
- $.pass.setVertexBuffer(0, vertexBuffer);
3708
-
3709
- // Set the bind group for the sampler and textures
3710
- if ($.textures.length !== previousTextureCount) {
3711
- previousTextureCount = $.textures.length;
3712
-
3713
- // Create the bind group for all textures
3714
- const textureViews = $.textures.map((tex) => tex.createView());
3715
-
3716
- $.textureBindGroup = Q5.device.createBindGroup({
3717
- layout: $.pipelines[1].getBindGroupLayout(0),
3718
- entries: [
3719
- { binding: 0, resource: $.sampler },
3720
- ...textureViews.map((view, i) => ({ binding: i + 1, resource: view }))
3721
- ]
3722
- });
3723
- }
3724
-
3725
- // Set the bind group for the sampler and textures
3726
- $.pass.setBindGroup(0, $.textureBindGroup);
3902
+ $.pass.setVertexBuffer(1, vertexBuffer);
3727
3903
  });
3728
3904
 
3729
- $.image = (img, x, y, w, h) => {
3730
- if ($._matrixDirty) $._saveMatrix();
3731
- let ti = $._transformIndex;
3732
-
3733
- $.imageStack.push(img.index);
3734
-
3735
- // Calculate half-width and half-height
3736
- let hw = w / 2;
3737
- let hh = h / 2;
3738
-
3739
- // Calculate vertices positions
3740
- let left = x - hw;
3741
- let right = x + hw;
3742
- let top = -(y - hh); // y is inverted in WebGPU
3743
- let bottom = -(y + hh);
3905
+ $._hooks.postRender.push(() => {
3906
+ verticesStack.length = 0;
3907
+ });
3908
+ };
3909
+ Q5.renderers.webgpu.text = ($, q) => {
3910
+ let t = $.createGraphics(1, 1);
3744
3911
 
3745
- let ii = img.index;
3912
+ $.loadFont = (f) => {
3913
+ q._preloadCount++;
3914
+ return t.loadFont(f, () => {
3915
+ q._preloadCount--;
3916
+ });
3917
+ };
3918
+ $.textFont = t.textFont;
3919
+ $.textSize = t.textSize;
3920
+ $.textLeading = t.textLeading;
3921
+ $.textStyle = t.textStyle;
3922
+ $.textAlign = t.textAlign;
3923
+ $.textWidth = t.textWidth;
3924
+ $.textAscent = t.textAscent;
3925
+ $.textDescent = t.textDescent;
3746
3926
 
3747
- // prettier-ignore
3748
- $.vertexStack.push(
3749
- left, top, 0, 0, ti, ii,
3750
- right, top, 1, 0, ti, ii,
3751
- left, bottom, 0, 1, ti, ii,
3752
- right, top, 1, 0, ti, ii,
3753
- left, bottom, 0, 1, ti, ii,
3754
- right, bottom, 1, 1, ti, ii
3755
- );
3927
+ $.textFill = (r, g, b, a) => t.fill($.color(r, g, b, a));
3928
+ $.textStroke = (r, g, b, a) => t.stroke($.color(r, g, b, a));
3756
3929
 
3757
- $.drawStack.push(6);
3930
+ $.text = (str, x, y, w, h) => {
3931
+ let img = t.createTextImage(str, w, h);
3932
+
3933
+ if (img.textureIndex == undefined) $._createTexture(img);
3934
+
3935
+ let og = t._imageMode;
3936
+ t._imageMode = 'corner';
3937
+ if (t.ctx.textAlign == 'center') x -= img.width * 0.5;
3938
+ else if (t.ctx.textAlign == 'right') x -= img.width;
3939
+ if (t.ctx.textBaseline == 'alphabetic') y -= t._textLeading;
3940
+ if (t.ctx.textBaseline == 'middle') y -= img._descent + img._ascent * 0.5 + t._textLeadDiff;
3941
+ else if (t.ctx.textBaseline == 'bottom') y -= img._ascent + img._descent + t._textLeadDiff;
3942
+ else if (t.ctx.textBaseline == 'top') y -= img._descent + t._textLeadDiff;
3943
+ $.image(img, x, y);
3944
+ t._imageMode = og;
3758
3945
  };
3759
3946
  };
3760
- Q5.renderers.webgpu.text = ($, q) => {};