q5 2.4.5 → 2.4.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "q5",
3
- "version": "2.4.5",
3
+ "version": "2.4.10",
4
4
  "description": "A sequel to p5.js that's smaller and faster",
5
5
  "author": "quinton-ashley",
6
6
  "contributors": [
package/q5.js CHANGED
@@ -596,6 +596,13 @@ Q5.renderers.q2d.canvas = ($, q) => {
596
596
  return c;
597
597
  };
598
598
 
599
+ $.clear = () => {
600
+ $.ctx.save();
601
+ $.ctx.resetTransform();
602
+ $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
603
+ $.ctx.restore();
604
+ };
605
+
599
606
  if ($._scope == 'image') return;
600
607
 
601
608
  $._resizeCanvas = (w, h) => {
@@ -632,7 +639,7 @@ Q5.renderers.q2d.canvas = ($, q) => {
632
639
  }
633
640
  if (c.a <= 0) return ($._doFill = false);
634
641
  }
635
- $.ctx.fillStyle = c.toString();
642
+ $.ctx.fillStyle = $._fill = c.toString();
636
643
  };
637
644
  $.noFill = () => ($._doFill = false);
638
645
  $.stroke = function (c) {
@@ -645,24 +652,17 @@ Q5.renderers.q2d.canvas = ($, q) => {
645
652
  }
646
653
  if (c.a <= 0) return ($._doStroke = false);
647
654
  }
648
- $.ctx.strokeStyle = c.toString();
655
+ $.ctx.strokeStyle = $._stroke = c.toString();
649
656
  };
650
657
  $.strokeWeight = (n) => {
651
658
  if (!n) $._doStroke = false;
652
659
  if ($._da) n *= $._da;
653
- $.ctx.lineWidth = n || 0.0001;
660
+ $.ctx.lineWidth = $._strokeWeight = n || 0.0001;
654
661
  };
655
662
  $.noStroke = () => ($._doStroke = false);
656
663
 
657
664
  $.opacity = (a) => ($.ctx.globalAlpha = a);
658
665
 
659
- $.clear = () => {
660
- $.ctx.save();
661
- $.ctx.resetTransform();
662
- $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
663
- $.ctx.restore();
664
- };
665
-
666
666
  // DRAWING MATRIX
667
667
 
668
668
  $.translate = (x, y) => {
@@ -1422,11 +1422,23 @@ Q5.DILATE = 6;
1422
1422
  Q5.ERODE = 7;
1423
1423
  Q5.BLUR = 8;
1424
1424
  Q5.renderers.q2d.text = ($, q) => {
1425
- $._textFont = 'sans-serif';
1426
- $._textSize = 12;
1427
- $._textLeading = 15;
1428
- $._textLeadDiff = 3;
1429
- $._textStyle = 'normal';
1425
+ $._textAlign = 'left';
1426
+ $._textBaseline = 'alphabetic';
1427
+
1428
+ let font = 'sans-serif',
1429
+ tSize = 12,
1430
+ leading = 15,
1431
+ leadDiff = 3,
1432
+ emphasis = 'normal',
1433
+ fontMod = false,
1434
+ styleHash = 0,
1435
+ styleHashes = [],
1436
+ useCache = false,
1437
+ genTextImage = false,
1438
+ cacheSize = 0,
1439
+ cacheMax = 12000;
1440
+
1441
+ let cache = ($._textCache = {});
1430
1442
 
1431
1443
  $.loadFont = (url, cb) => {
1432
1444
  q._preloadCount++;
@@ -1439,156 +1451,162 @@ Q5.renderers.q2d.text = ($, q) => {
1439
1451
  });
1440
1452
  return name;
1441
1453
  };
1442
- $.textFont = (x) => ($._textFont = x);
1454
+
1455
+ $.textFont = (x) => {
1456
+ font = x;
1457
+ fontMod = true;
1458
+ styleHash = -1;
1459
+ };
1443
1460
  $.textSize = (x) => {
1444
- if (x === undefined) return $._textSize;
1461
+ if (x === undefined) return tSize;
1445
1462
  if ($._da) x *= $._da;
1446
- $._textSize = x;
1463
+ tSize = x;
1464
+ fontMod = true;
1465
+ styleHash = -1;
1447
1466
  if (!$._leadingSet) {
1448
- $._textLeading = x * 1.25;
1449
- $._textLeadDiff = $._textLeading - x;
1467
+ leading = x * 1.25;
1468
+ leadDiff = leading - x;
1450
1469
  }
1451
1470
  };
1471
+ $.textStyle = (x) => {
1472
+ emphasis = x;
1473
+ fontMod = true;
1474
+ styleHash = -1;
1475
+ };
1452
1476
  $.textLeading = (x) => {
1453
- if (x === undefined) return $._textLeading;
1477
+ if (x === undefined) return leading;
1454
1478
  if ($._da) x *= $._da;
1455
- $._textLeading = x;
1456
- $._textLeadDiff = x - $._textSize;
1479
+ leading = x;
1480
+ leadDiff = x - tSize;
1457
1481
  $._leadingSet = true;
1482
+ styleHash = -1;
1458
1483
  };
1459
- $.textStyle = (x) => ($._textStyle = x);
1460
1484
  $.textAlign = (horiz, vert) => {
1461
- $.ctx.textAlign = horiz;
1485
+ $.ctx.textAlign = $._textAlign = horiz;
1462
1486
  if (vert) {
1463
- $.ctx.textBaseline = vert == $.CENTER ? 'middle' : vert;
1487
+ $.ctx.textBaseline = $._textBaseline = vert == $.CENTER ? 'middle' : vert;
1464
1488
  }
1489
+ styleHash = -1;
1465
1490
  };
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
- };
1491
+
1492
+ $.textWidth = (str) => $.ctx.measureText(str).width;
1493
+ $.textAscent = (str) => $.ctx.measureText(str).actualBoundingBoxAscent;
1494
+ $.textDescent = (str) => $.ctx.measureText(str).actualBoundingBoxDescent;
1495
+
1478
1496
  $.textFill = $.fill;
1479
1497
  $.textStroke = $.stroke;
1480
1498
 
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);
1499
+ let updateStyleHash = () => {
1500
+ let styleString = font + tSize + emphasis + leading;
1501
+
1502
+ let hash = 5381;
1503
+ for (let i = 0; i < styleString.length; i++) {
1504
+ hash = (hash * 33) ^ styleString.charCodeAt(i);
1517
1505
  }
1506
+ styleHash = hash >>> 0;
1518
1507
  };
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
- );
1508
+
1509
+ $.textCache = (enable, maxSize) => {
1510
+ if (maxSize) cacheMax = maxSize;
1511
+ if (enable !== undefined) useCache = enable;
1512
+ return useCache;
1537
1513
  };
1538
1514
  $.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);
1515
+ genTextImage = true;
1516
+ img = $.text(str, 0, 0, w, h);
1517
+ genTextImage = false;
1518
+ return img;
1549
1519
  };
1520
+
1521
+ let lines = [];
1550
1522
  $.text = (str, x, y, w, h) => {
1551
1523
  if (str === undefined || (!$._doFill && !$._doStroke)) return;
1552
1524
  str = str.toString();
1553
- let lines = str.split('\n');
1554
1525
  if ($._da) {
1555
1526
  x *= $._da;
1556
1527
  y *= $._da;
1557
1528
  }
1558
1529
  let ctx = $.ctx;
1559
- ctx.font = `${$._textStyle} ${$._textSize}px ${$._textFont}`;
1530
+ let img, tX, tY;
1560
1531
 
1561
- let useCache, img, cacheKey, tX, tY, ascent, descent;
1532
+ if (fontMod) {
1533
+ ctx.font = `${emphasis} ${tSize}px ${font}`;
1534
+ fontMod = false;
1535
+ }
1536
+
1537
+ if (useCache || genTextImage) {
1538
+ if (styleHash == -1) updateStyleHash();
1562
1539
 
1563
- if (!(useCache = $._genTextImage) && $._textCache) {
1564
- let transform = $.ctx.getTransform();
1565
- useCache = transform.b != 0 || transform.c != 0;
1540
+ img = cache[str];
1541
+ if (img) img = img[styleHash];
1542
+
1543
+ if (img) {
1544
+ if (img._fill == $._fill && img._stroke == $._stroke && img._strokeWeight == $._strokeWeight) {
1545
+ if (genTextImage) return img;
1546
+ return $.textImage(img, x, y);
1547
+ } else img.clear();
1548
+ }
1566
1549
  }
1567
1550
 
1568
- if (!useCache) {
1551
+ if (str.indexOf('\n') == -1) lines[0] = str;
1552
+ else lines = str.split('\n');
1553
+
1554
+ if (w) {
1555
+ let wrapped = [];
1556
+ for (let line of lines) {
1557
+ let i = 0;
1558
+
1559
+ while (i < line.length) {
1560
+ let max = i + w;
1561
+ if (max >= line.length) {
1562
+ wrapped.push(line.slice(i));
1563
+ break;
1564
+ }
1565
+ let end = line.lastIndexOf(' ', max);
1566
+ if (end === -1 || end < i) {
1567
+ end = max;
1568
+ }
1569
+ wrapped.push(line.slice(i, end));
1570
+ i = end;
1571
+ }
1572
+ }
1573
+ lines = wrapped;
1574
+ }
1575
+
1576
+ if (!useCache && !genTextImage) {
1569
1577
  tX = x;
1570
1578
  tY = y;
1571
1579
  } else {
1572
- cacheKey = $._genTextImageKey(str, w, h);
1573
- img = $._tic.get(cacheKey);
1574
- if (img && !$._genTextImage) return $.textImage(img, x, y);
1575
-
1576
1580
  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
- });
1581
+ tY = leading * lines.length;
1582
+
1583
+ if (!img) {
1584
+ let measure = ctx.measureText(' ');
1585
+ let ascent = measure.fontBoundingBoxAscent;
1586
+ let descent = measure.fontBoundingBoxDescent;
1587
+ h ??= tY + descent;
1588
+
1589
+ img = $.createImage.call($, Math.ceil(ctx.measureText(str).width), Math.ceil(h), {
1590
+ pixelDensity: $._pixelDensity
1591
+ });
1592
+
1593
+ img._ascent = ascent;
1594
+ img._descent = descent;
1595
+ img._top = descent + leadDiff;
1596
+ img._middle = img._top + ascent * 0.5;
1597
+ img._bottom = img._top + ascent;
1598
+ }
1599
+
1600
+ img._fill = $._fill;
1601
+ img._stroke = $._stroke;
1602
+ img._strokeWeight = $._strokeWeight;
1603
+ img.modified = true;
1586
1604
 
1587
1605
  ctx = img.ctx;
1588
1606
 
1589
1607
  ctx.font = $.ctx.font;
1590
- ctx.fillStyle = $.ctx.fillStyle;
1591
- ctx.strokeStyle = $.ctx.strokeStyle;
1608
+ ctx.fillStyle = $._fill;
1609
+ ctx.strokeStyle = $._stroke;
1592
1610
  ctx.lineWidth = $.ctx.lineWidth;
1593
1611
  }
1594
1612
 
@@ -1598,31 +1616,49 @@ Q5.renderers.q2d.text = ($, q) => {
1598
1616
  ctx.fillStyle = 'black';
1599
1617
  }
1600
1618
 
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;
1619
+ for (let line of lines) {
1620
+ if ($._doStroke && $._strokeSet) ctx.strokeText(line, tX, tY);
1621
+ if ($._doFill) ctx.fillText(line, tX, tY);
1622
+ tY += leading;
1605
1623
  if (tY > h) break;
1606
1624
  }
1625
+ lines.length = 0;
1607
1626
 
1608
1627
  if (!$._fillSet) ctx.fillStyle = ogFill;
1609
1628
 
1610
- if (useCache) {
1611
- img._ascent = ascent;
1612
- img._descent = descent;
1613
- $._tic.set(cacheKey, img);
1614
- if (!$._genTextImage) $.textImage(img, x, y);
1629
+ if (useCache || genTextImage) {
1630
+ styleHashes.push(styleHash);
1631
+ (cache[str] ??= {})[styleHash] = img;
1632
+
1633
+ cacheSize++;
1634
+ if (cacheSize > cacheMax) {
1635
+ let half = Math.ceil(cacheSize / 2);
1636
+ let hashes = styleHashes.splice(0, half);
1637
+ for (let s in cache) {
1638
+ s = cache[s];
1639
+ for (let h of hashes) delete s[h];
1640
+ }
1641
+ cacheSize -= half;
1642
+ }
1643
+
1644
+ if (genTextImage) return img;
1645
+ $.textImage(img, x, y);
1615
1646
  }
1616
1647
  };
1617
1648
  $.textImage = (img, x, y) => {
1618
1649
  let og = $._imageMode;
1619
1650
  $._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;
1651
+
1652
+ let ta = $._textAlign;
1653
+ if (ta == 'center') x -= img.canvas.hw;
1654
+ else if (ta == 'right') x -= img.width;
1655
+
1656
+ let bl = $._textBaseline;
1657
+ if (bl == 'alphabetic') y -= leading;
1658
+ else if (bl == 'middle') y -= img._middle;
1659
+ else if (bl == 'bottom') y -= img._bottom;
1660
+ else if (bl == 'top') y -= img._top;
1661
+
1626
1662
  $.image(img, x, y);
1627
1663
  $._imageMode = og;
1628
1664
  };
@@ -3064,8 +3100,8 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3064
3100
  $._createCanvas = (w, h, opt) => {
3065
3101
  q.ctx = q.drawingContext = c.getContext('webgpu');
3066
3102
 
3067
- opt.format = navigator.gpu.getPreferredCanvasFormat();
3068
- opt.device = Q5.device;
3103
+ opt.format ??= navigator.gpu.getPreferredCanvasFormat();
3104
+ opt.device ??= Q5.device;
3069
3105
 
3070
3106
  $.ctx.configure(opt);
3071
3107
 
@@ -3928,20 +3964,30 @@ fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
3928
3964
  minFilter: 'linear'
3929
3965
  });
3930
3966
 
3967
+ let MAX_TEXTURES = 12000;
3968
+
3969
+ $._textures = [];
3970
+ let tIdx = 0;
3971
+
3931
3972
  $._createTexture = (img) => {
3932
3973
  if (img.canvas) img = img.canvas;
3933
3974
 
3934
3975
  let textureSize = [img.width, img.height, 1];
3935
3976
 
3936
- const texture = Q5.device.createTexture({
3977
+ let texture = Q5.device.createTexture({
3937
3978
  size: textureSize,
3938
3979
  format: 'bgra8unorm',
3939
3980
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
3940
3981
  });
3941
3982
 
3942
- Q5.device.queue.copyExternalImageToTexture({ source: img }, { texture }, textureSize);
3983
+ Q5.device.queue.copyExternalImageToTexture(
3984
+ { source: img },
3985
+ { texture, colorSpace: $.canvas.colorSpace },
3986
+ textureSize
3987
+ );
3943
3988
 
3944
- img.textureIndex = $._textureBindGroups.length;
3989
+ $._textures[tIdx] = texture;
3990
+ img.textureIndex = tIdx;
3945
3991
 
3946
3992
  const textureBindGroup = Q5.device.createBindGroup({
3947
3993
  layout: textureLayout,
@@ -3950,7 +3996,16 @@ fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
3950
3996
  { binding: 1, resource: texture.createView() }
3951
3997
  ]
3952
3998
  });
3953
- $._textureBindGroups.push(textureBindGroup);
3999
+ $._textureBindGroups[tIdx] = textureBindGroup;
4000
+
4001
+ tIdx = (tIdx + 1) % MAX_TEXTURES;
4002
+
4003
+ // If the texture array is full, destroy the oldest texture
4004
+ if ($._textures[tIdx]) {
4005
+ $._textures[tIdx].destroy();
4006
+ delete $._textures[tIdx];
4007
+ delete $._textureBindGroups[tIdx];
4008
+ }
3954
4009
  };
3955
4010
 
3956
4011
  $.loadImage = $.loadTexture = (src) => {
@@ -4034,6 +4089,8 @@ Q5.renderers.webgpu.text = ($, q) => {
4034
4089
  q._preloadCount--;
4035
4090
  });
4036
4091
  };
4092
+
4093
+ // directly add these text setting functions to the webgpu renderer
4037
4094
  $.textFont = t.textFont;
4038
4095
  $.textSize = t.textSize;
4039
4096
  $.textLeading = t.textLeading;
@@ -4049,7 +4106,20 @@ Q5.renderers.webgpu.text = ($, q) => {
4049
4106
  $.text = (str, x, y, w, h) => {
4050
4107
  let img = t.createTextImage(str, w, h);
4051
4108
 
4052
- if (img.canvas.textureIndex == undefined) $._createTexture(img);
4109
+ if (img.canvas.textureIndex === undefined) {
4110
+ $._createTexture(img);
4111
+ } else if (img.modified) {
4112
+ let cnv = img.canvas;
4113
+ let textureSize = [cnv.width, cnv.height, 1];
4114
+ let texture = $._textures[cnv.textureIndex];
4115
+
4116
+ Q5.device.queue.copyExternalImageToTexture(
4117
+ { source: cnv },
4118
+ { texture, colorSpace: $.canvas.colorSpace },
4119
+ textureSize
4120
+ );
4121
+ img.modified = false;
4122
+ }
4053
4123
 
4054
4124
  $.textImage(img, x, y);
4055
4125
  };
@@ -4057,12 +4127,20 @@ Q5.renderers.webgpu.text = ($, q) => {
4057
4127
  $.createTextImage = t.createTextImage;
4058
4128
 
4059
4129
  $.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;
4130
+ let og = $._imageMode;
4131
+ $._imageMode = 'corner';
4132
+
4133
+ let ta = t._textAlign;
4134
+ if (ta == 'center') x -= img.canvas.hw;
4135
+ else if (ta == 'right') x -= img.width;
4136
+
4137
+ let bl = t._textBaseline;
4138
+ if (bl == 'alphabetic') y -= t._textLeading;
4139
+ else if (bl == 'middle') y -= img._middle;
4140
+ else if (bl == 'bottom') y -= img._bottom;
4141
+ else if (bl == 'top') y -= img._top;
4142
+
4066
4143
  $.image(img, x, y);
4144
+ $._imageMode = og;
4067
4145
  };
4068
4146
  };