q5 2.4.5 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/q5.js CHANGED
@@ -70,9 +70,9 @@ function Q5(scope, parent, renderer) {
70
70
  let ts = timestamp || performance.now();
71
71
  $._lastFrameTime ??= ts - $._targetFrameDuration;
72
72
 
73
- if ($._shouldResize) {
73
+ if ($._didResize) {
74
74
  $.windowResized();
75
- $._shouldResize = false;
75
+ $._didResize = false;
76
76
  }
77
77
 
78
78
  if ($._loop) looper = raf($._draw);
@@ -483,7 +483,7 @@ Q5.modules.canvas = ($, q) => {
483
483
 
484
484
  function parentResized() {
485
485
  if ($.frameCount > 1) {
486
- $._shouldResize = true;
486
+ $._didResize = true;
487
487
  $._adjustDisplay();
488
488
  }
489
489
  }
@@ -543,14 +543,9 @@ Q5.modules.canvas = ($, q) => {
543
543
  '_imageMode',
544
544
  '_rectMode',
545
545
  '_ellipseMode',
546
- '_textFont',
547
- '_textLeading',
548
- '_leadingSet',
549
546
  '_textSize',
550
547
  '_textAlign',
551
- '_textBaseline',
552
- '_textStyle',
553
- '_textWrap'
548
+ '_textBaseline'
554
549
  ];
555
550
  $._styles = [];
556
551
 
@@ -563,6 +558,15 @@ Q5.modules.canvas = ($, q) => {
563
558
  let styles = $._styles.pop();
564
559
  for (let s of $._styleNames) $[s] = styles[s];
565
560
  };
561
+
562
+ if (window && $._scope != 'graphics') {
563
+ window.addEventListener('resize', () => {
564
+ $._didResize = true;
565
+ q.windowWidth = window.innerWidth;
566
+ q.windowHeight = window.innerHeight;
567
+ q.deviceOrientation = window.screen?.orientation?.type;
568
+ });
569
+ }
566
570
  };
567
571
 
568
572
  Q5.canvasOptions = {
@@ -596,6 +600,13 @@ Q5.renderers.q2d.canvas = ($, q) => {
596
600
  return c;
597
601
  };
598
602
 
603
+ $.clear = () => {
604
+ $.ctx.save();
605
+ $.ctx.resetTransform();
606
+ $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
607
+ $.ctx.restore();
608
+ };
609
+
599
610
  if ($._scope == 'image') return;
600
611
 
601
612
  $._resizeCanvas = (w, h) => {
@@ -632,7 +643,7 @@ Q5.renderers.q2d.canvas = ($, q) => {
632
643
  }
633
644
  if (c.a <= 0) return ($._doFill = false);
634
645
  }
635
- $.ctx.fillStyle = c.toString();
646
+ $.ctx.fillStyle = $._fill = c.toString();
636
647
  };
637
648
  $.noFill = () => ($._doFill = false);
638
649
  $.stroke = function (c) {
@@ -645,24 +656,17 @@ Q5.renderers.q2d.canvas = ($, q) => {
645
656
  }
646
657
  if (c.a <= 0) return ($._doStroke = false);
647
658
  }
648
- $.ctx.strokeStyle = c.toString();
659
+ $.ctx.strokeStyle = $._stroke = c.toString();
649
660
  };
650
661
  $.strokeWeight = (n) => {
651
662
  if (!n) $._doStroke = false;
652
663
  if ($._da) n *= $._da;
653
- $.ctx.lineWidth = n || 0.0001;
664
+ $.ctx.lineWidth = $._strokeWeight = n || 0.0001;
654
665
  };
655
666
  $.noStroke = () => ($._doStroke = false);
656
667
 
657
668
  $.opacity = (a) => ($.ctx.globalAlpha = a);
658
669
 
659
- $.clear = () => {
660
- $.ctx.save();
661
- $.ctx.resetTransform();
662
- $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
663
- $.ctx.restore();
664
- };
665
-
666
670
  // DRAWING MATRIX
667
671
 
668
672
  $.translate = (x, y) => {
@@ -713,15 +717,6 @@ Q5.renderers.q2d.canvas = ($, q) => {
713
717
  document.body.append(vid);
714
718
  return vid;
715
719
  };
716
-
717
- if (window && $._scope != 'graphics') {
718
- window.addEventListener('resize', () => {
719
- $._shouldResize = true;
720
- q.windowWidth = window.innerWidth;
721
- q.windowHeight = window.innerHeight;
722
- q.deviceOrientation = window.screen?.orientation?.type;
723
- });
724
- }
725
720
  };
726
721
  Q5.renderers.q2d.drawing = ($) => {
727
722
  $._doStroke = true;
@@ -1422,11 +1417,24 @@ Q5.DILATE = 6;
1422
1417
  Q5.ERODE = 7;
1423
1418
  Q5.BLUR = 8;
1424
1419
  Q5.renderers.q2d.text = ($, q) => {
1425
- $._textFont = 'sans-serif';
1420
+ $._textAlign = 'left';
1421
+ $._textBaseline = 'alphabetic';
1426
1422
  $._textSize = 12;
1427
- $._textLeading = 15;
1428
- $._textLeadDiff = 3;
1429
- $._textStyle = 'normal';
1423
+
1424
+ let font = 'sans-serif',
1425
+ leadingSet = false,
1426
+ leading = 15,
1427
+ leadDiff = 3,
1428
+ emphasis = 'normal',
1429
+ fontMod = false,
1430
+ styleHash = 0,
1431
+ styleHashes = [],
1432
+ useCache = false,
1433
+ genTextImage = false,
1434
+ cacheSize = 0,
1435
+ cacheMax = 12000;
1436
+
1437
+ let cache = ($._textCache = {});
1430
1438
 
1431
1439
  $.loadFont = (url, cb) => {
1432
1440
  q._preloadCount++;
@@ -1439,156 +1447,160 @@ Q5.renderers.q2d.text = ($, q) => {
1439
1447
  });
1440
1448
  return name;
1441
1449
  };
1442
- $.textFont = (x) => ($._textFont = x);
1450
+
1451
+ $.textFont = (x) => {
1452
+ font = x;
1453
+ fontMod = true;
1454
+ styleHash = -1;
1455
+ };
1443
1456
  $.textSize = (x) => {
1444
1457
  if (x === undefined) return $._textSize;
1445
1458
  if ($._da) x *= $._da;
1446
1459
  $._textSize = x;
1447
- if (!$._leadingSet) {
1448
- $._textLeading = x * 1.25;
1449
- $._textLeadDiff = $._textLeading - x;
1460
+ fontMod = true;
1461
+ styleHash = -1;
1462
+ if (!leadingSet) {
1463
+ leading = x * 1.25;
1464
+ leadDiff = leading - x;
1450
1465
  }
1451
1466
  };
1467
+ $.textStyle = (x) => {
1468
+ emphasis = x;
1469
+ fontMod = true;
1470
+ styleHash = -1;
1471
+ };
1452
1472
  $.textLeading = (x) => {
1453
- if (x === undefined) return $._textLeading;
1473
+ if (x === undefined) return leading;
1454
1474
  if ($._da) x *= $._da;
1455
- $._textLeading = x;
1456
- $._textLeadDiff = x - $._textSize;
1457
- $._leadingSet = true;
1475
+ leading = x;
1476
+ leadDiff = x - $._textSize;
1477
+ leadingSet = true;
1478
+ styleHash = -1;
1458
1479
  };
1459
- $.textStyle = (x) => ($._textStyle = x);
1460
1480
  $.textAlign = (horiz, vert) => {
1461
- $.ctx.textAlign = horiz;
1481
+ $.ctx.textAlign = $._textAlign = horiz;
1462
1482
  if (vert) {
1463
- $.ctx.textBaseline = vert == $.CENTER ? 'middle' : vert;
1483
+ $.ctx.textBaseline = $._textBaseline = vert == $.CENTER ? 'middle' : vert;
1464
1484
  }
1465
1485
  };
1466
- $.textWidth = (str) => {
1467
- $.ctx.font = `${$._textStyle} ${$._textSize}px ${$._textFont}`;
1468
- return $.ctx.measureText(str).width;
1469
- };
1470
- $.textAscent = (str) => {
1471
- $.ctx.font = `${$._textStyle} ${$._textSize}px ${$._textFont}`;
1472
- return $.ctx.measureText(str).actualBoundingBoxAscent;
1473
- };
1474
- $.textDescent = (str) => {
1475
- $.ctx.font = `${$._textStyle} ${$._textSize}px ${$._textFont}`;
1476
- return $.ctx.measureText(str).actualBoundingBoxDescent;
1477
- };
1486
+
1487
+ $.textWidth = (str) => $.ctx.measureText(str).width;
1488
+ $.textAscent = (str) => $.ctx.measureText(str).actualBoundingBoxAscent;
1489
+ $.textDescent = (str) => $.ctx.measureText(str).actualBoundingBoxDescent;
1490
+
1478
1491
  $.textFill = $.fill;
1479
1492
  $.textStroke = $.stroke;
1480
1493
 
1481
- $._textCache = !!Q5.Image;
1482
- $._TimedCache = class extends Map {
1483
- constructor() {
1484
- super();
1485
- this.maxSize = 50000;
1486
- }
1487
- set(k, v) {
1488
- v.lastAccessed = Date.now();
1489
- super.set(k, v);
1490
- if (this.size > this.maxSize) this.gc();
1491
- }
1492
- get(k) {
1493
- const v = super.get(k);
1494
- if (v) v.lastAccessed = Date.now();
1495
- return v;
1496
- }
1497
- gc() {
1498
- let t = Infinity;
1499
- let oldest;
1500
- let i = 0;
1501
- for (const [k, v] of this.entries()) {
1502
- if (v.lastAccessed < t) {
1503
- t = v.lastAccessed;
1504
- oldest = i;
1505
- }
1506
- i++;
1507
- }
1508
- i = oldest;
1509
- for (const k of this.keys()) {
1510
- if (i == 0) {
1511
- oldest = k;
1512
- break;
1513
- }
1514
- i--;
1515
- }
1516
- this.delete(oldest);
1494
+ let updateStyleHash = () => {
1495
+ let styleString = font + $._textSize + emphasis + leading;
1496
+
1497
+ let hash = 5381;
1498
+ for (let i = 0; i < styleString.length; i++) {
1499
+ hash = (hash * 33) ^ styleString.charCodeAt(i);
1517
1500
  }
1501
+ styleHash = hash >>> 0;
1518
1502
  };
1519
- $._tic = new $._TimedCache();
1520
- $.textCache = (b, maxSize) => {
1521
- if (maxSize) $._tic.maxSize = maxSize;
1522
- if (b !== undefined) $._textCache = b;
1523
- return $._textCache;
1524
- };
1525
- $._genTextImageKey = (str, w, h) => {
1526
- return (
1527
- str.slice(0, 200) +
1528
- $._textStyle +
1529
- $._textSize +
1530
- $._textFont +
1531
- ($._doFill ? $.ctx.fillStyle : '') +
1532
- '_' +
1533
- ($._doStroke && $._strokeSet ? $.ctx.lineWidth + $.ctx.strokeStyle + '_' : '') +
1534
- (w || '') +
1535
- (h ? 'x' + h : '')
1536
- );
1503
+
1504
+ $.textCache = (enable, maxSize) => {
1505
+ if (maxSize) cacheMax = maxSize;
1506
+ if (enable !== undefined) useCache = enable;
1507
+ return useCache;
1537
1508
  };
1538
1509
  $.createTextImage = (str, w, h) => {
1539
- let k = $._genTextImageKey(str, w, h);
1540
- if ($._tic.get(k)) return $._tic.get(k);
1541
-
1542
- let og = $._textCache;
1543
- $._textCache = true;
1544
- $._genTextImage = true;
1545
- $.text(str, 0, 0, w, h);
1546
- $._genTextImage = false;
1547
- $._textCache = og;
1548
- return $._tic.get(k);
1510
+ genTextImage = true;
1511
+ img = $.text(str, 0, 0, w, h);
1512
+ genTextImage = false;
1513
+ return img;
1549
1514
  };
1515
+
1516
+ let lines = [];
1550
1517
  $.text = (str, x, y, w, h) => {
1551
1518
  if (str === undefined || (!$._doFill && !$._doStroke)) return;
1552
1519
  str = str.toString();
1553
- let lines = str.split('\n');
1554
1520
  if ($._da) {
1555
1521
  x *= $._da;
1556
1522
  y *= $._da;
1557
1523
  }
1558
1524
  let ctx = $.ctx;
1559
- ctx.font = `${$._textStyle} ${$._textSize}px ${$._textFont}`;
1525
+ let img, tX, tY;
1560
1526
 
1561
- let useCache, img, cacheKey, tX, tY, ascent, descent;
1527
+ if (fontMod) {
1528
+ ctx.font = `${emphasis} ${$._textSize}px ${font}`;
1529
+ fontMod = false;
1530
+ }
1531
+
1532
+ if (useCache || genTextImage) {
1533
+ if (styleHash == -1) updateStyleHash();
1534
+
1535
+ img = cache[str];
1536
+ if (img) img = img[styleHash];
1562
1537
 
1563
- if (!(useCache = $._genTextImage) && $._textCache) {
1564
- let transform = $.ctx.getTransform();
1565
- useCache = transform.b != 0 || transform.c != 0;
1538
+ if (img) {
1539
+ if (img._fill == $._fill && img._stroke == $._stroke && img._strokeWeight == $._strokeWeight) {
1540
+ if (genTextImage) return img;
1541
+ return $.textImage(img, x, y);
1542
+ } else img.clear();
1543
+ }
1566
1544
  }
1567
1545
 
1568
- if (!useCache) {
1546
+ if (str.indexOf('\n') == -1) lines[0] = str;
1547
+ else lines = str.split('\n');
1548
+
1549
+ if (str.length > w) {
1550
+ let wrapped = [];
1551
+ for (let line of lines) {
1552
+ let i = 0;
1553
+
1554
+ while (i < line.length) {
1555
+ let max = i + w;
1556
+ if (max >= line.length) {
1557
+ wrapped.push(line.slice(i));
1558
+ break;
1559
+ }
1560
+ let end = line.lastIndexOf(' ', max);
1561
+ if (end === -1 || end < i) end = max;
1562
+ wrapped.push(line.slice(i, end));
1563
+ i = end + 1;
1564
+ }
1565
+ }
1566
+ lines = wrapped;
1567
+ }
1568
+
1569
+ if (!useCache && !genTextImage) {
1569
1570
  tX = x;
1570
1571
  tY = y;
1571
1572
  } else {
1572
- cacheKey = $._genTextImageKey(str, w, h);
1573
- img = $._tic.get(cacheKey);
1574
- if (img && !$._genTextImage) return $.textImage(img, x, y);
1575
-
1576
1573
  tX = 0;
1577
- tY = $._textLeading * lines.length;
1578
- let measure = ctx.measureText(' ');
1579
- ascent = measure.fontBoundingBoxAscent;
1580
- descent = measure.fontBoundingBoxDescent;
1581
- h ??= tY + descent;
1582
-
1583
- img = $.createImage.call($, Math.ceil(ctx.measureText(str).width), Math.ceil(h), {
1584
- pixelDensity: $._pixelDensity
1585
- });
1574
+ tY = leading * lines.length;
1575
+
1576
+ if (!img) {
1577
+ let measure = ctx.measureText(' ');
1578
+ let ascent = measure.fontBoundingBoxAscent;
1579
+ let descent = measure.fontBoundingBoxDescent;
1580
+ h ??= tY + descent;
1581
+
1582
+ img = $.createImage.call($, Math.ceil(ctx.measureText(str).width), Math.ceil(h), {
1583
+ pixelDensity: $._pixelDensity
1584
+ });
1585
+
1586
+ img._ascent = ascent;
1587
+ img._descent = descent;
1588
+ img._top = descent + leadDiff;
1589
+ img._middle = img._top + ascent * 0.5;
1590
+ img._bottom = img._top + ascent;
1591
+ img._leading = leading;
1592
+ }
1593
+
1594
+ img._fill = $._fill;
1595
+ img._stroke = $._stroke;
1596
+ img._strokeWeight = $._strokeWeight;
1597
+ img.modified = true;
1586
1598
 
1587
1599
  ctx = img.ctx;
1588
1600
 
1589
1601
  ctx.font = $.ctx.font;
1590
- ctx.fillStyle = $.ctx.fillStyle;
1591
- ctx.strokeStyle = $.ctx.strokeStyle;
1602
+ ctx.fillStyle = $._fill;
1603
+ ctx.strokeStyle = $._stroke;
1592
1604
  ctx.lineWidth = $.ctx.lineWidth;
1593
1605
  }
1594
1606
 
@@ -1598,31 +1610,49 @@ Q5.renderers.q2d.text = ($, q) => {
1598
1610
  ctx.fillStyle = 'black';
1599
1611
  }
1600
1612
 
1601
- for (let i = 0; i < lines.length; i++) {
1602
- if ($._doStroke && $._strokeSet) ctx.strokeText(lines[i], tX, tY);
1603
- if ($._doFill) ctx.fillText(lines[i], tX, tY);
1604
- tY += $._textLeading;
1613
+ for (let line of lines) {
1614
+ if ($._doStroke && $._strokeSet) ctx.strokeText(line, tX, tY);
1615
+ if ($._doFill) ctx.fillText(line, tX, tY);
1616
+ tY += leading;
1605
1617
  if (tY > h) break;
1606
1618
  }
1619
+ lines.length = 0;
1607
1620
 
1608
1621
  if (!$._fillSet) ctx.fillStyle = ogFill;
1609
1622
 
1610
- if (useCache) {
1611
- img._ascent = ascent;
1612
- img._descent = descent;
1613
- $._tic.set(cacheKey, img);
1614
- if (!$._genTextImage) $.textImage(img, x, y);
1623
+ if (useCache || genTextImage) {
1624
+ styleHashes.push(styleHash);
1625
+ (cache[str] ??= {})[styleHash] = img;
1626
+
1627
+ cacheSize++;
1628
+ if (cacheSize > cacheMax) {
1629
+ let half = Math.ceil(cacheSize / 2);
1630
+ let hashes = styleHashes.splice(0, half);
1631
+ for (let s in cache) {
1632
+ s = cache[s];
1633
+ for (let h of hashes) delete s[h];
1634
+ }
1635
+ cacheSize -= half;
1636
+ }
1637
+
1638
+ if (genTextImage) return img;
1639
+ $.textImage(img, x, y);
1615
1640
  }
1616
1641
  };
1617
1642
  $.textImage = (img, x, y) => {
1618
1643
  let og = $._imageMode;
1619
1644
  $._imageMode = 'corner';
1620
- if ($.ctx.textAlign == 'center') x -= img.width * 0.5;
1621
- else if ($.ctx.textAlign == 'right') x -= img.width;
1622
- if ($.ctx.textBaseline == 'alphabetic') y -= $._textLeading;
1623
- if ($.ctx.textBaseline == 'middle') y -= img._descent + img._ascent * 0.5 + $._textLeadDiff;
1624
- else if ($.ctx.textBaseline == 'bottom') y -= img._ascent + img._descent + $._textLeadDiff;
1625
- else if ($.ctx.textBaseline == 'top') y -= img._descent + $._textLeadDiff;
1645
+
1646
+ let ta = $._textAlign;
1647
+ if (ta == 'center') x -= img.canvas.hw;
1648
+ else if (ta == 'right') x -= img.width;
1649
+
1650
+ let bl = $._textBaseline;
1651
+ if (bl == 'alphabetic') y -= img._leading;
1652
+ else if (bl == 'middle') y -= img._middle;
1653
+ else if (bl == 'bottom') y -= img._bottom;
1654
+ else if (bl == 'top') y -= img._top;
1655
+
1626
1656
  $.image(img, x, y);
1627
1657
  $._imageMode = og;
1628
1658
  };
@@ -2704,10 +2734,11 @@ Q5.modules.util = ($, q) => {
2704
2734
  fetch(path)
2705
2735
  .then((r) => {
2706
2736
  if (type == 'json') return r.json();
2707
- if (type == 'text') return r.text();
2737
+ return r.text();
2708
2738
  })
2709
2739
  .then((r) => {
2710
2740
  q._preloadCount--;
2741
+ if (type == 'csv') r = $.CSV.parse(r);
2711
2742
  Object.assign(ret, r);
2712
2743
  if (cb) cb(r);
2713
2744
  });
@@ -2716,6 +2747,21 @@ Q5.modules.util = ($, q) => {
2716
2747
 
2717
2748
  $.loadStrings = (path, cb) => $._loadFile(path, cb, 'text');
2718
2749
  $.loadJSON = (path, cb) => $._loadFile(path, cb, 'json');
2750
+ $.loadCSV = (path, cb) => $._loadFile(path, cb, 'csv');
2751
+
2752
+ $.CSV = {};
2753
+ $.CSV.parse = (csv, sep = ',', lineSep = '\n') => {
2754
+ let a = [],
2755
+ lns = csv.split(lineSep),
2756
+ headers = lns[0].split(sep);
2757
+ for (let i = 1; i < lns.length; i++) {
2758
+ let o = {},
2759
+ ln = lns[i].split(sep);
2760
+ headers.forEach((h, i) => (o[h] = JSON.parse(ln[i])));
2761
+ a.push(o);
2762
+ }
2763
+ return a;
2764
+ };
2719
2765
 
2720
2766
  if (typeof localStorage == 'object') {
2721
2767
  $.storeItem = localStorage.setItem;
@@ -3028,7 +3074,8 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3028
3074
  // colors used for each draw call
3029
3075
  let colorsStack = ($.colorsStack = [1, 1, 1, 1]);
3030
3076
 
3031
- $._envLayout = Q5.device.createBindGroupLayout({
3077
+ $._transformLayout = Q5.device.createBindGroupLayout({
3078
+ label: 'transformLayout',
3032
3079
  entries: [
3033
3080
  {
3034
3081
  binding: 0,
@@ -3037,14 +3084,9 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3037
3084
  type: 'uniform',
3038
3085
  hasDynamicOffset: false
3039
3086
  }
3040
- }
3041
- ]
3042
- });
3043
-
3044
- $._transformLayout = Q5.device.createBindGroupLayout({
3045
- entries: [
3087
+ },
3046
3088
  {
3047
- binding: 0,
3089
+ binding: 1,
3048
3090
  visibility: GPUShaderStage.VERTEX,
3049
3091
  buffer: {
3050
3092
  type: 'read-only-storage',
@@ -3054,9 +3096,9 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3054
3096
  ]
3055
3097
  });
3056
3098
 
3057
- $.bindGroupLayouts = [$._envLayout, $._transformLayout];
3099
+ $.bindGroupLayouts = [$._transformLayout];
3058
3100
 
3059
- const uniformBuffer = Q5.device.createBuffer({
3101
+ let uniformBuffer = Q5.device.createBuffer({
3060
3102
  size: 8, // Size of two floats
3061
3103
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
3062
3104
  });
@@ -3064,25 +3106,13 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3064
3106
  $._createCanvas = (w, h, opt) => {
3065
3107
  q.ctx = q.drawingContext = c.getContext('webgpu');
3066
3108
 
3067
- opt.format = navigator.gpu.getPreferredCanvasFormat();
3068
- opt.device = Q5.device;
3109
+ opt.format ??= navigator.gpu.getPreferredCanvasFormat();
3110
+ opt.device ??= Q5.device;
3069
3111
 
3070
3112
  $.ctx.configure(opt);
3071
3113
 
3072
3114
  Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([$.canvas.hw, $.canvas.hh]));
3073
3115
 
3074
- $._envBindGroup = Q5.device.createBindGroup({
3075
- layout: $._envLayout,
3076
- entries: [
3077
- {
3078
- binding: 0,
3079
- resource: {
3080
- buffer: uniformBuffer
3081
- }
3082
- }
3083
- ]
3084
- });
3085
-
3086
3116
  return c;
3087
3117
  };
3088
3118
 
@@ -3092,7 +3122,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3092
3122
 
3093
3123
  // current color index, used to associate a vertex with a color
3094
3124
  let colorIndex = 0;
3095
- const addColor = (r, g, b, a = 1) => {
3125
+ let addColor = (r, g, b, a = 1) => {
3096
3126
  if (typeof r == 'string') r = $.color(r);
3097
3127
  else if (b == undefined) {
3098
3128
  // grayscale mode `fill(1, 0.5)`
@@ -3104,6 +3134,8 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3104
3134
  colorIndex++;
3105
3135
  };
3106
3136
 
3137
+ $._fillIndex = $._strokeIndex = -1;
3138
+
3107
3139
  $.fill = (r, g, b, a) => {
3108
3140
  addColor(r, g, b, a);
3109
3141
  $._doFill = true;
@@ -3135,56 +3167,70 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3135
3167
  };
3136
3168
  $.resetMatrix();
3137
3169
 
3138
- // Boolean to track if the matrix has been modified
3170
+ // tracks if the matrix has been modified
3139
3171
  $._matrixDirty = false;
3140
3172
 
3141
- // Array to store transformation matrices for the render pass
3173
+ // array to store transformation matrices for the render pass
3142
3174
  $.transformStates = [$._matrix.slice()];
3143
3175
 
3144
- // Stack to keep track of transformation matrix indexes
3176
+ // stack to keep track of transformation matrix indexes
3145
3177
  $._transformIndexStack = [];
3146
3178
 
3147
3179
  $.translate = (x, y, z) => {
3148
3180
  if (!x && !y && !z) return;
3149
3181
  // Update the translation values
3150
- $._matrix[3] += x;
3151
- $._matrix[7] -= y;
3152
- $._matrix[11] += z || 0;
3182
+ $._matrix[12] += x;
3183
+ $._matrix[13] -= y;
3184
+ $._matrix[14] += z || 0;
3153
3185
  $._matrixDirty = true;
3154
3186
  };
3155
3187
 
3156
- $.rotate = (r) => {
3157
- if (!r) return;
3158
- if ($._angleMode) r *= $._DEGTORAD;
3188
+ $.rotate = (a) => {
3189
+ if (!a) return;
3190
+ if ($._angleMode) a *= $._DEGTORAD;
3159
3191
 
3160
- let cosR = Math.cos(r);
3161
- let sinR = Math.sin(r);
3192
+ let cosR = Math.cos(a);
3193
+ let sinR = Math.sin(a);
3194
+
3195
+ let m = $._matrix;
3196
+
3197
+ let m0 = m[0],
3198
+ m1 = m[1],
3199
+ m4 = m[4],
3200
+ m5 = m[5];
3162
3201
 
3163
- let m0 = $._matrix[0],
3164
- m1 = $._matrix[1],
3165
- m4 = $._matrix[4],
3166
- m5 = $._matrix[5];
3167
3202
  if (!m0 && !m1 && !m4 && !m5) {
3168
- $._matrix[0] = cosR;
3169
- $._matrix[1] = sinR;
3170
- $._matrix[4] = -sinR;
3171
- $._matrix[5] = cosR;
3203
+ m[0] = cosR;
3204
+ m[1] = sinR;
3205
+ m[4] = -sinR;
3206
+ m[5] = cosR;
3172
3207
  } else {
3173
- $._matrix[0] = m0 * cosR + m4 * sinR;
3174
- $._matrix[1] = m1 * cosR + m5 * sinR;
3175
- $._matrix[4] = m0 * -sinR + m4 * cosR;
3176
- $._matrix[5] = m1 * -sinR + m5 * cosR;
3208
+ m[0] = m0 * cosR + m4 * sinR;
3209
+ m[1] = m1 * cosR + m5 * sinR;
3210
+ m[4] = m4 * cosR - m0 * sinR;
3211
+ m[5] = m5 * cosR - m1 * sinR;
3177
3212
  }
3178
3213
 
3179
3214
  $._matrixDirty = true;
3180
3215
  };
3181
3216
 
3182
- $.scale = (sx = 1, sy, sz = 1) => {
3183
- sy ??= sx;
3217
+ $.scale = (x = 1, y, z = 1) => {
3218
+ y ??= x;
3184
3219
 
3185
- $._matrix[0] *= sx;
3186
- $._matrix[5] *= sy;
3187
- $._matrix[10] *= sz;
3220
+ let m = $._matrix;
3221
+
3222
+ m[0] *= x;
3223
+ m[1] *= x;
3224
+ m[2] *= x;
3225
+ m[3] *= x;
3226
+ m[4] *= y;
3227
+ m[5] *= y;
3228
+ m[6] *= y;
3229
+ m[7] *= y;
3230
+ m[8] *= z;
3231
+ m[9] *= z;
3232
+ m[10] *= z;
3233
+ m[11] *= z;
3188
3234
 
3189
3235
  $._matrixDirty = true;
3190
3236
  };
@@ -3256,7 +3302,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3256
3302
  if (!$._transformIndexStack.length) {
3257
3303
  return console.warn('Matrix index stack is empty!');
3258
3304
  }
3259
- // Pop the last matrix index from the stack and set it as the current matrix index
3305
+ // Pop the last matrix index and set it as the current matrix index
3260
3306
  let idx = $._transformIndexStack.pop();
3261
3307
  $._matrix = $.transformStates[idx].slice();
3262
3308
  $._transformIndex = idx;
@@ -3279,7 +3325,6 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3279
3325
  // left, right, top, bottom
3280
3326
  let l, r, t, b;
3281
3327
  if (!mode || mode == 'corner') {
3282
- // CORNER
3283
3328
  l = x;
3284
3329
  r = x + w;
3285
3330
  t = -y;
@@ -3319,7 +3364,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3319
3364
 
3320
3365
  $._render = () => {
3321
3366
  if (transformStates.length > 1 || !$._transformBindGroup) {
3322
- const transformBuffer = Q5.device.createBuffer({
3367
+ let transformBuffer = Q5.device.createBuffer({
3323
3368
  size: transformStates.length * 64, // Size of 16 floats
3324
3369
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
3325
3370
  });
@@ -3331,6 +3376,12 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3331
3376
  entries: [
3332
3377
  {
3333
3378
  binding: 0,
3379
+ resource: {
3380
+ buffer: uniformBuffer
3381
+ }
3382
+ },
3383
+ {
3384
+ binding: 1,
3334
3385
  resource: {
3335
3386
  buffer: transformBuffer
3336
3387
  }
@@ -3339,35 +3390,47 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3339
3390
  });
3340
3391
  }
3341
3392
 
3342
- pass.setBindGroup(0, $._envBindGroup);
3343
- pass.setBindGroup(1, $._transformBindGroup);
3393
+ pass.setBindGroup(0, $._transformBindGroup);
3344
3394
 
3345
3395
  for (let m of $._hooks.preRender) m();
3346
3396
 
3347
3397
  let drawVertOffset = 0;
3348
3398
  let imageVertOffset = 0;
3399
+ let textCharOffset = 0;
3349
3400
  let curPipelineIndex = -1;
3350
3401
  let curTextureIndex = -1;
3351
3402
 
3352
- pass.setPipeline($.pipelines[0]);
3353
-
3354
3403
  for (let i = 0; i < drawStack.length; i += 2) {
3355
3404
  let v = drawStack[i + 1];
3356
3405
 
3406
+ if (drawStack[i] == -1) {
3407
+ v();
3408
+ continue;
3409
+ }
3410
+
3357
3411
  if (curPipelineIndex != drawStack[i]) {
3358
3412
  curPipelineIndex = drawStack[i];
3359
3413
  pass.setPipeline($.pipelines[curPipelineIndex]);
3360
3414
  }
3361
3415
 
3362
3416
  if (curPipelineIndex == 0) {
3363
- pass.draw(v, 1, drawVertOffset, 0);
3417
+ // v is the number of vertices
3418
+ pass.draw(v, 1, drawVertOffset);
3364
3419
  drawVertOffset += v;
3365
3420
  } else if (curPipelineIndex == 1) {
3366
3421
  if (curTextureIndex != v) {
3367
- pass.setBindGroup(3, $._textureBindGroups[v]);
3422
+ // v is the texture index
3423
+ pass.setBindGroup(2, $._textureBindGroups[v]);
3368
3424
  }
3369
- pass.draw(6, 1, imageVertOffset, 0);
3425
+ pass.draw(6, 1, imageVertOffset);
3370
3426
  imageVertOffset += 6;
3427
+ } else if (curPipelineIndex == 2) {
3428
+ pass.setBindGroup(2, $._font.bindGroup);
3429
+ pass.setBindGroup(3, $._textBindGroup);
3430
+
3431
+ // v is the number of characters in the text
3432
+ pass.draw(4, v, 0, textCharOffset);
3433
+ textCharOffset += v;
3371
3434
  }
3372
3435
  }
3373
3436
 
@@ -3376,7 +3439,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3376
3439
 
3377
3440
  $._finishRender = () => {
3378
3441
  pass.end();
3379
- const commandBuffer = $.encoder.finish();
3442
+ let commandBuffer = $.encoder.finish();
3380
3443
  Q5.device.queue.submit([commandBuffer]);
3381
3444
  q.pass = $.encoder = null;
3382
3445
 
@@ -3419,8 +3482,8 @@ Q5.renderers.webgpu.drawing = ($, q) => {
3419
3482
  label: 'drawingVertexShader',
3420
3483
  code: `
3421
3484
  struct VertexOutput {
3422
- @builtin(position) position: vec4<f32>,
3423
- @location(1) colorIndex: f32
3485
+ @builtin(position) position: vec4f,
3486
+ @location(0) colorIndex: f32
3424
3487
  };
3425
3488
 
3426
3489
  struct Uniforms {
@@ -3429,12 +3492,12 @@ struct Uniforms {
3429
3492
  };
3430
3493
 
3431
3494
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3432
- @group(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
3495
+ @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
3433
3496
 
3434
3497
  @vertex
3435
- fn vertexMain(@location(0) pos: vec2<f32>, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput {
3436
- var vert = vec4<f32>(pos, 0.0, 1.0);
3437
- vert *= transforms[i32(transformIndex)];
3498
+ fn vertexMain(@location(0) pos: vec2f, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput {
3499
+ var vert = vec4f(pos, 0.0, 1.0);
3500
+ vert = transforms[i32(transformIndex)] * vert;
3438
3501
  vert.x /= uniforms.halfWidth;
3439
3502
  vert.y /= uniforms.halfHeight;
3440
3503
 
@@ -3449,17 +3512,18 @@ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) colorIndex: f32, @locati
3449
3512
  let fragmentShader = Q5.device.createShaderModule({
3450
3513
  label: 'drawingFragmentShader',
3451
3514
  code: `
3452
- @group(2) @binding(0) var<storage, read> uColors : array<vec4<f32>>;
3515
+ @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
3453
3516
 
3454
3517
  @fragment
3455
- fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3456
- let index = u32(colorIndex);
3457
- return mix(uColors[index], uColors[index + 1u], fract(colorIndex));
3518
+ fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3519
+ let index = i32(colorIndex);
3520
+ return mix(colors[index], colors[index + 1], fract(colorIndex));
3458
3521
  }
3459
3522
  `
3460
3523
  });
3461
3524
 
3462
3525
  colorsLayout = Q5.device.createBindGroupLayout({
3526
+ label: 'colorsLayout',
3463
3527
  entries: [
3464
3528
  {
3465
3529
  binding: 0,
@@ -3690,10 +3754,17 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3690
3754
  $.background = (r, g, b, a) => {
3691
3755
  $.push();
3692
3756
  $.resetMatrix();
3693
- if (r.src) $.image(r, -c.hw, -c.hh, c.w, c.h);
3694
- else {
3757
+ if (r.src) {
3758
+ let og = $._imageMode;
3759
+ $._imageMode = 'corner';
3760
+ $.image(r, -c.hw, -c.hh, c.w, c.h);
3761
+ $._imageMode = og;
3762
+ } else {
3763
+ let og = $._rectMode;
3764
+ $._rectMode = 'corner';
3695
3765
  $.fill(r, g, b, a);
3696
3766
  $.rect(-c.hw, -c.hh, c.w, c.h);
3767
+ $._rectMode = og;
3697
3768
  }
3698
3769
  $.pop();
3699
3770
  };
@@ -3801,7 +3872,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3801
3872
  });
3802
3873
 
3803
3874
  // set the bind group once before rendering
3804
- $.pass.setBindGroup(2, $._colorsBindGroup);
3875
+ $.pass.setBindGroup(1, $._colorsBindGroup);
3805
3876
  });
3806
3877
 
3807
3878
  $._hooks.postRender.push(() => {
@@ -3816,8 +3887,8 @@ Q5.renderers.webgpu.image = ($, q) => {
3816
3887
  label: 'imageVertexShader',
3817
3888
  code: `
3818
3889
  struct VertexOutput {
3819
- @builtin(position) position: vec4<f32>,
3820
- @location(0) texCoord: vec2<f32>
3890
+ @builtin(position) position: vec4f,
3891
+ @location(0) texCoord: vec2f
3821
3892
  };
3822
3893
 
3823
3894
  struct Uniforms {
@@ -3826,12 +3897,12 @@ struct Uniforms {
3826
3897
  };
3827
3898
 
3828
3899
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3829
- @group(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
3900
+ @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
3830
3901
 
3831
3902
  @vertex
3832
- fn vertexMain(@location(0) pos: vec2<f32>, @location(1) texCoord: vec2<f32>, @location(2) transformIndex: f32) -> VertexOutput {
3833
- var vert = vec4<f32>(pos, 0.0, 1.0);
3834
- vert *= transforms[i32(transformIndex)];
3903
+ fn vertexMain(@location(0) pos: vec2f, @location(1) texCoord: vec2f, @location(2) transformIndex: f32) -> VertexOutput {
3904
+ var vert = vec4f(pos, 0.0, 1.0);
3905
+ vert = transforms[i32(transformIndex)] * vert;
3835
3906
  vert.x /= uniforms.halfWidth;
3836
3907
  vert.y /= uniforms.halfHeight;
3837
3908
 
@@ -3846,11 +3917,11 @@ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) texCoord: vec2<f32>, @lo
3846
3917
  let fragmentShader = Q5.device.createShaderModule({
3847
3918
  label: 'imageFragmentShader',
3848
3919
  code: `
3849
- @group(3) @binding(0) var samp: sampler;
3850
- @group(3) @binding(1) var texture: texture_2d<f32>;
3920
+ @group(2) @binding(0) var samp: sampler;
3921
+ @group(2) @binding(1) var texture: texture_2d<f32>;
3851
3922
 
3852
3923
  @fragment
3853
- fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
3924
+ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
3854
3925
  // Sample the texture using the interpolated texture coordinate
3855
3926
  return textureSample(texture, samp, texCoord);
3856
3927
  }
@@ -3882,11 +3953,9 @@ fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
3882
3953
  ]
3883
3954
  };
3884
3955
 
3885
- $.bindGroupLayouts.push(textureLayout);
3886
-
3887
3956
  const pipelineLayout = Q5.device.createPipelineLayout({
3888
3957
  label: 'imagePipelineLayout',
3889
- bindGroupLayouts: $.bindGroupLayouts
3958
+ bindGroupLayouts: [...$.bindGroupLayouts, textureLayout]
3890
3959
  });
3891
3960
 
3892
3961
  $.pipelines[1] = Q5.device.createRenderPipeline({
@@ -3928,20 +3997,30 @@ fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
3928
3997
  minFilter: 'linear'
3929
3998
  });
3930
3999
 
4000
+ let MAX_TEXTURES = 12000;
4001
+
4002
+ $._textures = [];
4003
+ let tIdx = 0;
4004
+
3931
4005
  $._createTexture = (img) => {
3932
4006
  if (img.canvas) img = img.canvas;
3933
4007
 
3934
4008
  let textureSize = [img.width, img.height, 1];
3935
4009
 
3936
- const texture = Q5.device.createTexture({
4010
+ let texture = Q5.device.createTexture({
3937
4011
  size: textureSize,
3938
4012
  format: 'bgra8unorm',
3939
4013
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
3940
4014
  });
3941
4015
 
3942
- Q5.device.queue.copyExternalImageToTexture({ source: img }, { texture }, textureSize);
4016
+ Q5.device.queue.copyExternalImageToTexture(
4017
+ { source: img },
4018
+ { texture, colorSpace: $.canvas.colorSpace },
4019
+ textureSize
4020
+ );
3943
4021
 
3944
- img.textureIndex = $._textureBindGroups.length;
4022
+ $._textures[tIdx] = texture;
4023
+ img.textureIndex = tIdx;
3945
4024
 
3946
4025
  const textureBindGroup = Q5.device.createBindGroup({
3947
4026
  layout: textureLayout,
@@ -3950,7 +4029,16 @@ fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
3950
4029
  { binding: 1, resource: texture.createView() }
3951
4030
  ]
3952
4031
  });
3953
- $._textureBindGroups.push(textureBindGroup);
4032
+ $._textureBindGroups[tIdx] = textureBindGroup;
4033
+
4034
+ tIdx = (tIdx + 1) % MAX_TEXTURES;
4035
+
4036
+ // If the texture array is full, destroy the oldest texture
4037
+ if ($._textures[tIdx]) {
4038
+ $._textures[tIdx].destroy();
4039
+ delete $._textures[tIdx];
4040
+ delete $._textureBindGroups[tIdx];
4041
+ }
3954
4042
  };
3955
4043
 
3956
4044
  $.loadImage = $.loadTexture = (src) => {
@@ -4024,45 +4112,621 @@ Q5.DILATE = 6;
4024
4112
  Q5.ERODE = 7;
4025
4113
  Q5.BLUR = 8;
4026
4114
  Q5.renderers.webgpu.text = ($, q) => {
4027
- let t = $.createGraphics(1, 1);
4028
- t.pixelDensity($._pixelDensity);
4029
- t._imageMode = 'corner';
4115
+ let textShader = Q5.device.createShaderModule({
4116
+ label: 'MSDF text shader',
4117
+ code: `
4118
+ // Positions for simple quad geometry
4119
+ const pos = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0));
4030
4120
 
4031
- $.loadFont = (f) => {
4121
+ struct VertexInput {
4122
+ @builtin(vertex_index) vertex : u32,
4123
+ @builtin(instance_index) instance : u32,
4124
+ };
4125
+ struct VertexOutput {
4126
+ @builtin(position) position : vec4f,
4127
+ @location(0) texcoord : vec2f,
4128
+ @location(1) colorIndex : f32
4129
+ };
4130
+ struct Char {
4131
+ texOffset: vec2f,
4132
+ texExtent: vec2f,
4133
+ size: vec2f,
4134
+ offset: vec2f,
4135
+ };
4136
+ struct Text {
4137
+ pos: vec2f,
4138
+ scale: f32,
4139
+ transformIndex: f32,
4140
+ fillIndex: f32,
4141
+ strokeIndex: f32
4142
+ };
4143
+ struct Uniforms {
4144
+ halfWidth: f32,
4145
+ halfHeight: f32
4146
+ };
4147
+
4148
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
4149
+ @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
4150
+
4151
+ @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
4152
+
4153
+ @group(2) @binding(0) var fontTexture: texture_2d<f32>;
4154
+ @group(2) @binding(1) var fontSampler: sampler;
4155
+ @group(2) @binding(2) var<storage> fontChars: array<Char>;
4156
+
4157
+ @group(3) @binding(0) var<storage> textChars: array<vec4f>;
4158
+ @group(3) @binding(1) var<storage> textMetadata: array<Text>;
4159
+
4160
+ @vertex
4161
+ fn vertexMain(input : VertexInput) -> VertexOutput {
4162
+ let char = textChars[input.instance];
4163
+
4164
+ let text = textMetadata[i32(char.w)];
4165
+
4166
+ let fontChar = fontChars[i32(char.z)];
4167
+
4168
+ let charPos = ((pos[input.vertex] * fontChar.size + char.xy + fontChar.offset) * text.scale) + text.pos;
4169
+
4170
+ var vert = vec4f(charPos, 0.0, 1.0);
4171
+ vert = transforms[i32(text.transformIndex)] * vert;
4172
+ vert.x /= uniforms.halfWidth;
4173
+ vert.y /= uniforms.halfHeight;
4174
+
4175
+ var output : VertexOutput;
4176
+ output.position = vert;
4177
+ output.texcoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;
4178
+ output.colorIndex = text.fillIndex;
4179
+ return output;
4180
+ }
4181
+
4182
+ fn sampleMsdf(texcoord: vec2f) -> f32 {
4183
+ let c = textureSample(fontTexture, fontSampler, texcoord);
4184
+ return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
4185
+ }
4186
+
4187
+ @fragment
4188
+ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4189
+ // pxRange (AKA distanceRange) comes from the msdfgen tool,
4190
+ // uses the default which is 4.
4191
+ let pxRange = 4.0;
4192
+ let sz = vec2f(textureDimensions(fontTexture, 0));
4193
+ let dx = sz.x*length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));
4194
+ let dy = sz.y*length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));
4195
+ let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
4196
+ let sigDist = sampleMsdf(input.texcoord) - 0.5;
4197
+ let pxDist = sigDist * toPixels;
4198
+ let edgeWidth = 0.5;
4199
+ let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
4200
+ if (alpha < 0.001) {
4201
+ discard;
4202
+ }
4203
+ let fillColor = colors[i32(input.colorIndex)];
4204
+ return vec4f(fillColor.rgb, fillColor.a * alpha);
4205
+ }
4206
+ `
4207
+ });
4208
+
4209
+ class MsdfFont {
4210
+ constructor(pipeline, bindGroup, lineHeight, chars, kernings) {
4211
+ this.pipeline = pipeline;
4212
+ this.bindGroup = bindGroup;
4213
+ this.lineHeight = lineHeight;
4214
+ this.chars = chars;
4215
+ this.kernings = kernings;
4216
+ let charArray = Object.values(chars);
4217
+ this.charCount = charArray.length;
4218
+ this.defaultChar = charArray[0];
4219
+ }
4220
+ getChar(charCode) {
4221
+ return this.chars[charCode] ?? this.defaultChar;
4222
+ }
4223
+ // Gets the distance in pixels a line should advance for a given character code. If the upcoming
4224
+ // character code is given any kerning between the two characters will be taken into account.
4225
+ getXAdvance(charCode, nextCharCode = -1) {
4226
+ let char = this.getChar(charCode);
4227
+ if (nextCharCode >= 0) {
4228
+ let kerning = this.kernings.get(charCode);
4229
+ if (kerning) {
4230
+ return char.xadvance + (kerning.get(nextCharCode) ?? 0);
4231
+ }
4232
+ }
4233
+ return char.xadvance;
4234
+ }
4235
+ }
4236
+
4237
+ let textBindGroupLayout = Q5.device.createBindGroupLayout({
4238
+ label: 'MSDF text group layout',
4239
+ entries: [
4240
+ {
4241
+ binding: 0,
4242
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4243
+ buffer: { type: 'read-only-storage' }
4244
+ },
4245
+ {
4246
+ binding: 1,
4247
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4248
+ buffer: { type: 'read-only-storage' }
4249
+ }
4250
+ ]
4251
+ });
4252
+
4253
+ let fonts = {};
4254
+
4255
+ let createFont = async (fontJsonUrl, fontName, cb) => {
4032
4256
  q._preloadCount++;
4033
- return t.loadFont(f, () => {
4257
+
4258
+ let res = await fetch(fontJsonUrl);
4259
+ if (res.status == 404) {
4034
4260
  q._preloadCount--;
4261
+ return '';
4262
+ }
4263
+ let atlas = await res.json();
4264
+
4265
+ let slashIdx = fontJsonUrl.lastIndexOf('/');
4266
+ let baseUrl = slashIdx != -1 ? fontJsonUrl.substring(0, slashIdx + 1) : '';
4267
+ // load font image
4268
+ res = await fetch(baseUrl + atlas.pages[0]);
4269
+ let img = await createImageBitmap(await res.blob());
4270
+
4271
+ // convert image to texture
4272
+ let imgSize = [img.width, img.height, 1];
4273
+ let texture = Q5.device.createTexture({
4274
+ label: `MSDF ${fontName}`,
4275
+ size: imgSize,
4276
+ format: 'rgba8unorm',
4277
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
4278
+ });
4279
+ Q5.device.queue.copyExternalImageToTexture({ source: img }, { texture }, imgSize);
4280
+
4281
+ // to make q5's default font file smaller,
4282
+ // the chars and kernings are stored as csv strings
4283
+ if (typeof atlas.chars == 'string') {
4284
+ atlas.chars = $.CSV.parse(atlas.chars, ' ');
4285
+ atlas.kernings = $.CSV.parse(atlas.kernings, ' ');
4286
+ }
4287
+
4288
+ let charCount = atlas.chars.length;
4289
+ let charsBuffer = Q5.device.createBuffer({
4290
+ size: charCount * 32,
4291
+ usage: GPUBufferUsage.STORAGE,
4292
+ mappedAtCreation: true
4293
+ });
4294
+
4295
+ let fontChars = new Float32Array(charsBuffer.getMappedRange());
4296
+ let u = 1 / atlas.common.scaleW;
4297
+ let v = 1 / atlas.common.scaleH;
4298
+ let chars = {};
4299
+ let o = 0; // offset
4300
+ for (let [i, char] of atlas.chars.entries()) {
4301
+ chars[char.id] = char;
4302
+ chars[char.id].charIndex = i;
4303
+ fontChars[o] = char.x * u; // texOffset.x
4304
+ fontChars[o + 1] = char.y * v; // texOffset.y
4305
+ fontChars[o + 2] = char.width * u; // texExtent.x
4306
+ fontChars[o + 3] = char.height * v; // texExtent.y
4307
+ fontChars[o + 4] = char.width; // size.x
4308
+ fontChars[o + 5] = char.height; // size.y
4309
+ fontChars[o + 6] = char.xoffset; // offset.x
4310
+ fontChars[o + 7] = -char.yoffset; // offset.y
4311
+ o += 8;
4312
+ }
4313
+ charsBuffer.unmap();
4314
+
4315
+ let fontSampler = Q5.device.createSampler({
4316
+ minFilter: 'linear',
4317
+ magFilter: 'linear',
4318
+ mipmapFilter: 'linear',
4319
+ maxAnisotropy: 16
4320
+ });
4321
+ let fontBindGroupLayout = Q5.device.createBindGroupLayout({
4322
+ label: 'MSDF font group layout',
4323
+ entries: [
4324
+ {
4325
+ binding: 0,
4326
+ visibility: GPUShaderStage.FRAGMENT,
4327
+ texture: {}
4328
+ },
4329
+ {
4330
+ binding: 1,
4331
+ visibility: GPUShaderStage.FRAGMENT,
4332
+ sampler: {}
4333
+ },
4334
+ {
4335
+ binding: 2,
4336
+ visibility: GPUShaderStage.VERTEX,
4337
+ buffer: { type: 'read-only-storage' }
4338
+ }
4339
+ ]
4340
+ });
4341
+ let fontPipeline = Q5.device.createRenderPipeline({
4342
+ label: 'msdf font pipeline',
4343
+ layout: Q5.device.createPipelineLayout({
4344
+ bindGroupLayouts: [...$.bindGroupLayouts, fontBindGroupLayout, textBindGroupLayout]
4345
+ }),
4346
+ vertex: {
4347
+ module: textShader,
4348
+ entryPoint: 'vertexMain'
4349
+ },
4350
+ fragment: {
4351
+ module: textShader,
4352
+ entryPoint: 'fragmentMain',
4353
+ targets: [
4354
+ {
4355
+ format: 'bgra8unorm',
4356
+ blend: {
4357
+ color: {
4358
+ srcFactor: 'src-alpha',
4359
+ dstFactor: 'one-minus-src-alpha'
4360
+ },
4361
+ alpha: {
4362
+ srcFactor: 'one',
4363
+ dstFactor: 'one'
4364
+ }
4365
+ }
4366
+ }
4367
+ ]
4368
+ },
4369
+ primitive: {
4370
+ topology: 'triangle-strip',
4371
+ stripIndexFormat: 'uint32'
4372
+ }
4035
4373
  });
4374
+
4375
+ let fontBindGroup = Q5.device.createBindGroup({
4376
+ label: 'msdf font bind group',
4377
+ layout: fontBindGroupLayout,
4378
+ entries: [
4379
+ {
4380
+ binding: 0,
4381
+ resource: texture.createView()
4382
+ },
4383
+ { binding: 1, resource: fontSampler },
4384
+ { binding: 2, resource: { buffer: charsBuffer } }
4385
+ ]
4386
+ });
4387
+
4388
+ let kernings = new Map();
4389
+ if (atlas.kernings) {
4390
+ for (let kerning of atlas.kernings) {
4391
+ let charKerning = kernings.get(kerning.first);
4392
+ if (!charKerning) {
4393
+ charKerning = new Map();
4394
+ kernings.set(kerning.first, charKerning);
4395
+ }
4396
+ charKerning.set(kerning.second, kerning.amount);
4397
+ }
4398
+ }
4399
+
4400
+ $._font = new MsdfFont(fontPipeline, fontBindGroup, atlas.common.lineHeight, chars, kernings);
4401
+
4402
+ fonts[fontName] = $._font;
4403
+ $.pipelines[2] = $._font.pipeline;
4404
+
4405
+ q._preloadCount--;
4406
+
4407
+ if (cb) cb(fontName);
4408
+ };
4409
+
4410
+ // q2d graphics context to use for text image creation
4411
+ let g = $.createGraphics(1, 1);
4412
+ g.colorMode($.RGB, 1);
4413
+
4414
+ $.loadFont = (url, cb) => {
4415
+ let ext = url.slice(url.lastIndexOf('.') + 1);
4416
+ if (ext != 'json') return g.loadFont(url, cb);
4417
+ let fontName = url.slice(url.lastIndexOf('/') + 1, url.lastIndexOf('-'));
4418
+ createFont(url, fontName, cb);
4419
+ return fontName;
4420
+ };
4421
+
4422
+ $._textSize = 18;
4423
+ $._textAlign = 'left';
4424
+ $._textBaseline = 'alphabetic';
4425
+ let leadingSet = false,
4426
+ leading = 22.5,
4427
+ leadDiff = 4.5,
4428
+ leadPercent = 1.25;
4429
+
4430
+ $.textFont = (fontName) => {
4431
+ $._font = fonts[fontName];
4432
+
4433
+ // replay the change of font in the draw stack
4434
+ $.drawStack.push(-1, () => {
4435
+ $._font = fonts[fontName];
4436
+ $.pipelines[2] = $._font.pipeline;
4437
+ });
4438
+ };
4439
+ $.textSize = (size) => {
4440
+ $._textSize = size;
4441
+ if (!leadingSet) {
4442
+ leading = size * leadPercent;
4443
+ leadDiff = leading - size;
4444
+ }
4445
+ };
4446
+ $.textLeading = (lineHeight) => {
4447
+ $._font.lineHeight = leading = lineHeight;
4448
+ leadDiff = leading - $._textSize;
4449
+ leadPercent = leading / $._textSize;
4450
+ leadingSet = true;
4451
+ };
4452
+ $.textAlign = (horiz, vert) => {
4453
+ $._textAlign = horiz;
4454
+ if (vert) $._textBaseline = vert;
4455
+ };
4456
+
4457
+ $._charStack = [];
4458
+ $._textStack = [];
4459
+
4460
+ let measureText = (font, text, charCallback) => {
4461
+ let maxWidth = 0,
4462
+ offsetX = 0,
4463
+ offsetY = 0,
4464
+ line = 0,
4465
+ printedCharCount = 0,
4466
+ lineWidths = [],
4467
+ nextCharCode = text.charCodeAt(0);
4468
+
4469
+ for (let i = 0; i < text.length; ++i) {
4470
+ let charCode = nextCharCode;
4471
+ nextCharCode = i < text.length - 1 ? text.charCodeAt(i + 1) : -1;
4472
+ switch (charCode) {
4473
+ case 10: // Newline
4474
+ lineWidths.push(offsetX);
4475
+ line++;
4476
+ maxWidth = Math.max(maxWidth, offsetX);
4477
+ offsetX = 0;
4478
+ offsetY -= font.lineHeight * leadPercent;
4479
+ break;
4480
+ case 13: // CR
4481
+ break;
4482
+ case 32: // Space
4483
+ // advance the offset without actually adding a character
4484
+ offsetX += font.getXAdvance(charCode);
4485
+ break;
4486
+ case 9: // Tab
4487
+ offsetX += font.getXAdvance(charCode) * 2;
4488
+ break;
4489
+ default:
4490
+ if (charCallback) {
4491
+ charCallback(offsetX, offsetY, line, font.getChar(charCode));
4492
+ }
4493
+ offsetX += font.getXAdvance(charCode, nextCharCode);
4494
+ printedCharCount++;
4495
+ }
4496
+ }
4497
+ lineWidths.push(offsetX);
4498
+ maxWidth = Math.max(maxWidth, offsetX);
4499
+ return {
4500
+ width: maxWidth,
4501
+ height: lineWidths.length * font.lineHeight * leadPercent,
4502
+ lineWidths,
4503
+ printedCharCount
4504
+ };
4036
4505
  };
4037
- $.textFont = t.textFont;
4038
- $.textSize = t.textSize;
4039
- $.textLeading = t.textLeading;
4040
- $.textStyle = t.textStyle;
4041
- $.textAlign = t.textAlign;
4042
- $.textWidth = t.textWidth;
4043
- $.textAscent = t.textAscent;
4044
- $.textDescent = t.textDescent;
4045
4506
 
4046
- $.textFill = (r, g, b, a) => t.fill($.color(r, g, b, a));
4047
- $.textStroke = (r, g, b, a) => t.stroke($.color(r, g, b, a));
4507
+ let initLoadDefaultFont;
4048
4508
 
4049
4509
  $.text = (str, x, y, w, h) => {
4050
- let img = t.createTextImage(str, w, h);
4510
+ if (!$._font) {
4511
+ // check if online and loading the default font hasn't been attempted yet
4512
+ if (navigator.onLine && !initLoadDefaultFont) {
4513
+ initLoadDefaultFont = true;
4514
+ $.loadFont('https://q5js.org/fonts/YaHei-msdf.json');
4515
+ }
4516
+ return;
4517
+ }
4051
4518
 
4052
- if (img.canvas.textureIndex == undefined) $._createTexture(img);
4519
+ if (str.length > w) {
4520
+ let wrapped = [];
4521
+ let i = 0;
4522
+ while (i < str.length) {
4523
+ let max = i + w;
4524
+ if (max >= str.length) {
4525
+ wrapped.push(str.slice(i));
4526
+ break;
4527
+ }
4528
+ let end = str.lastIndexOf(' ', max);
4529
+ if (end == -1 || end < i) end = max;
4530
+ wrapped.push(str.slice(i, end));
4531
+ i = end + 1;
4532
+ }
4533
+ str = wrapped.join('\n');
4534
+ }
4053
4535
 
4054
- $.textImage(img, x, y);
4536
+ let spaces = 0, // whitespace char count, not literal spaces
4537
+ hasNewline;
4538
+ for (let i = 0; i < str.length; i++) {
4539
+ let c = str[i];
4540
+ switch (c) {
4541
+ case '\n':
4542
+ hasNewline = true;
4543
+ case '\r':
4544
+ case '\t':
4545
+ case ' ':
4546
+ spaces++;
4547
+ }
4548
+ }
4549
+
4550
+ let charsData = new Float32Array((str.length - spaces) * 4);
4551
+
4552
+ let ta = $._textAlign,
4553
+ tb = $._textBaseline,
4554
+ textIndex = $._textStack.length,
4555
+ o = 0, // offset
4556
+ measurements;
4557
+
4558
+ if (ta == 'left' && !hasNewline) {
4559
+ measurements = measureText($._font, str, (textX, textY, line, char) => {
4560
+ charsData[o] = textX;
4561
+ charsData[o + 1] = textY;
4562
+ charsData[o + 2] = char.charIndex;
4563
+ charsData[o + 3] = textIndex;
4564
+ o += 4;
4565
+ });
4566
+
4567
+ if (tb == 'alphabetic') y -= $._textSize;
4568
+ else if (tb == 'center') y -= $._textSize * 0.5;
4569
+ else if (tb == 'bottom') y -= leading;
4570
+ } else {
4571
+ // measure the text to get the line widths before setting
4572
+ // the x position to properly align the text
4573
+ measurements = measureText($._font, str);
4574
+
4575
+ let offsetY = 0;
4576
+ if (tb == 'alphabetic') y -= $._textSize;
4577
+ else if (tb == 'center') offsetY = measurements.height * 0.5;
4578
+ else if (tb == 'bottom') offsetY = measurements.height;
4579
+
4580
+ measureText($._font, str, (textX, textY, line, char) => {
4581
+ let offsetX = 0;
4582
+ if (ta == 'center') {
4583
+ offsetX = measurements.width * -0.5 - (measurements.width - measurements.lineWidths[line]) * -0.5;
4584
+ } else if (ta == 'right') {
4585
+ offsetX = measurements.width - measurements.lineWidths[line];
4586
+ }
4587
+ charsData[o] = textX + offsetX;
4588
+ charsData[o + 1] = textY + offsetY;
4589
+ charsData[o + 2] = char.charIndex;
4590
+ charsData[o + 3] = textIndex;
4591
+ o += 4;
4592
+ });
4593
+ }
4594
+ $._charStack.push(charsData);
4595
+
4596
+ let text = new Float32Array(6);
4597
+
4598
+ if ($._matrixDirty) $._saveMatrix();
4599
+
4600
+ text[0] = x;
4601
+ text[1] = -y;
4602
+ text[2] = $._textSize / 44;
4603
+ text[3] = $._transformIndex;
4604
+ text[4] = $._fillIndex;
4605
+ text[5] = $._strokeIndex;
4606
+
4607
+ $._textStack.push(text);
4608
+ $.drawStack.push(2, measurements.printedCharCount);
4609
+ };
4610
+
4611
+ $.textWidth = (str) => {
4612
+ if (!$._font) return 0;
4613
+ return measureText($._font, str).width;
4055
4614
  };
4056
4615
 
4057
- $.createTextImage = t.createTextImage;
4616
+ $.createTextImage = (str, w, h) => {
4617
+ g.textSize($._textSize);
4618
+
4619
+ if ($._doFill) {
4620
+ let fi = $._fillIndex * 4;
4621
+ g.fill(colorsStack.slice(fi, fi + 4));
4622
+ }
4623
+ if ($._doStroke) {
4624
+ let si = $._strokeIndex * 4;
4625
+ g.stroke(colorsStack.slice(si, si + 4));
4626
+ }
4627
+
4628
+ let img = g.createTextImage(str, w, h);
4629
+
4630
+ if (img.canvas.textureIndex == undefined) {
4631
+ $._createTexture(img);
4632
+ } else if (img.modified) {
4633
+ let cnv = img.canvas;
4634
+ let textureSize = [cnv.width, cnv.height, 1];
4635
+ let texture = $._textures[cnv.textureIndex];
4636
+
4637
+ Q5.device.queue.copyExternalImageToTexture(
4638
+ { source: cnv },
4639
+ { texture, colorSpace: $.canvas.colorSpace },
4640
+ textureSize
4641
+ );
4642
+ img.modified = false;
4643
+ }
4644
+ return img;
4645
+ };
4058
4646
 
4059
4647
  $.textImage = (img, x, y) => {
4060
- if (t.ctx.textAlign == 'center') x -= img.width * 0.5;
4061
- else if (t.ctx.textAlign == 'right') x -= img.width;
4062
- if (t.ctx.textBaseline == 'alphabetic') y -= t._textLeading;
4063
- if (t.ctx.textBaseline == 'middle') y -= img._descent + img._ascent * 0.5 + t._textLeadDiff;
4064
- else if (t.ctx.textBaseline == 'bottom') y -= img._ascent + img._descent + t._textLeadDiff;
4065
- else if (t.ctx.textBaseline == 'top') y -= img._descent + t._textLeadDiff;
4648
+ let og = $._imageMode;
4649
+ $._imageMode = 'corner';
4650
+
4651
+ let ta = $._textAlign;
4652
+ if (ta == 'center') x -= img.canvas.hw;
4653
+ else if (ta == 'right') x -= img.width;
4654
+
4655
+ let bl = $._textBaseline;
4656
+ if (bl == 'alphabetic') y -= img._leading;
4657
+ else if (bl == 'center') y -= img._middle;
4658
+ else if (bl == 'bottom') y -= img._bottom;
4659
+ else if (bl == 'top') y -= img._top;
4660
+
4066
4661
  $.image(img, x, y);
4662
+ $._imageMode = og;
4067
4663
  };
4664
+
4665
+ $._hooks.preRender.push(() => {
4666
+ if (!$._charStack.length) return;
4667
+
4668
+ // Calculate total buffer size for text data
4669
+ let totalTextSize = 0;
4670
+ for (let charsData of $._charStack) {
4671
+ totalTextSize += charsData.length * 4;
4672
+ }
4673
+
4674
+ // Create a single buffer for all text data
4675
+ let charBuffer = Q5.device.createBuffer({
4676
+ label: 'charBuffer',
4677
+ size: totalTextSize,
4678
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4679
+ mappedAtCreation: true
4680
+ });
4681
+
4682
+ // Copy all text data into the buffer
4683
+ let textArray = new Float32Array(charBuffer.getMappedRange());
4684
+ let o = 0;
4685
+ for (let array of $._charStack) {
4686
+ textArray.set(array, o);
4687
+ o += array.length;
4688
+ }
4689
+ charBuffer.unmap();
4690
+
4691
+ // Calculate total buffer size for metadata
4692
+ let totalMetadataSize = $._textStack.length * 6 * 4;
4693
+
4694
+ // Create a single buffer for all metadata
4695
+ let textBuffer = Q5.device.createBuffer({
4696
+ label: 'textBuffer',
4697
+ size: totalMetadataSize,
4698
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4699
+ mappedAtCreation: true
4700
+ });
4701
+
4702
+ // Copy all metadata into the buffer
4703
+ let metadataArray = new Float32Array(textBuffer.getMappedRange());
4704
+ o = 0;
4705
+ for (let array of $._textStack) {
4706
+ metadataArray.set(array, o);
4707
+ o += array.length;
4708
+ }
4709
+ textBuffer.unmap();
4710
+
4711
+ // Create a single bind group for the text buffer and metadata buffer
4712
+ $._textBindGroup = Q5.device.createBindGroup({
4713
+ label: 'msdf text bind group',
4714
+ layout: textBindGroupLayout,
4715
+ entries: [
4716
+ {
4717
+ binding: 0,
4718
+ resource: { buffer: charBuffer }
4719
+ },
4720
+ {
4721
+ binding: 1,
4722
+ resource: { buffer: textBuffer }
4723
+ }
4724
+ ]
4725
+ });
4726
+ });
4727
+
4728
+ $._hooks.postRender.push(() => {
4729
+ $._charStack.length = 0;
4730
+ $._textStack.length = 0;
4731
+ });
4068
4732
  };