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/README.md +5 -2
- package/package.json +1 -1
- package/q5.d.ts +27 -4
- package/q5.js +939 -275
- package/q5.min.js +1 -1
- package/src/q5-2d-canvas.js +10 -19
- package/src/q5-2d-text.js +162 -127
- package/src/q5-canvas.js +11 -7
- package/src/q5-core.js +2 -2
- package/src/q5-util.js +17 -1
- package/src/q5-webgpu-canvas.js +81 -64
- package/src/q5-webgpu-drawing.js +21 -13
- package/src/q5-webgpu-image.js +33 -16
- package/src/q5-webgpu-text.js +601 -25
- package/src/readme.md +76 -9
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 ($.
|
|
73
|
+
if ($._didResize) {
|
|
74
74
|
$.windowResized();
|
|
75
|
-
$.
|
|
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
|
-
$.
|
|
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
|
-
$.
|
|
1420
|
+
$._textAlign = 'left';
|
|
1421
|
+
$._textBaseline = 'alphabetic';
|
|
1426
1422
|
$._textSize = 12;
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
|
1473
|
+
if (x === undefined) return leading;
|
|
1454
1474
|
if ($._da) x *= $._da;
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
-
|
|
1520
|
-
$.textCache = (
|
|
1521
|
-
if (maxSize)
|
|
1522
|
-
if (
|
|
1523
|
-
return
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
-
|
|
1525
|
+
let img, tX, tY;
|
|
1560
1526
|
|
|
1561
|
-
|
|
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
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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 (
|
|
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 =
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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 = $.
|
|
1591
|
-
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
|
|
1602
|
-
if ($._doStroke && $._strokeSet) ctx.strokeText(
|
|
1603
|
-
if ($._doFill) ctx.fillText(
|
|
1604
|
-
tY +=
|
|
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
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
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
|
-
|
|
1621
|
-
|
|
1622
|
-
if (
|
|
1623
|
-
if (
|
|
1624
|
-
|
|
1625
|
-
|
|
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
|
-
|
|
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
|
-
$.
|
|
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:
|
|
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 = [$.
|
|
3099
|
+
$.bindGroupLayouts = [$._transformLayout];
|
|
3058
3100
|
|
|
3059
|
-
|
|
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
|
|
3068
|
-
opt.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
|
-
|
|
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
|
-
//
|
|
3170
|
+
// tracks if the matrix has been modified
|
|
3139
3171
|
$._matrixDirty = false;
|
|
3140
3172
|
|
|
3141
|
-
//
|
|
3173
|
+
// array to store transformation matrices for the render pass
|
|
3142
3174
|
$.transformStates = [$._matrix.slice()];
|
|
3143
3175
|
|
|
3144
|
-
//
|
|
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[
|
|
3151
|
-
$._matrix[
|
|
3152
|
-
$._matrix[
|
|
3182
|
+
$._matrix[12] += x;
|
|
3183
|
+
$._matrix[13] -= y;
|
|
3184
|
+
$._matrix[14] += z || 0;
|
|
3153
3185
|
$._matrixDirty = true;
|
|
3154
3186
|
};
|
|
3155
3187
|
|
|
3156
|
-
$.rotate = (
|
|
3157
|
-
if (!
|
|
3158
|
-
if ($._angleMode)
|
|
3188
|
+
$.rotate = (a) => {
|
|
3189
|
+
if (!a) return;
|
|
3190
|
+
if ($._angleMode) a *= $._DEGTORAD;
|
|
3159
3191
|
|
|
3160
|
-
let cosR = Math.cos(
|
|
3161
|
-
let sinR = Math.sin(
|
|
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
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3203
|
+
m[0] = cosR;
|
|
3204
|
+
m[1] = sinR;
|
|
3205
|
+
m[4] = -sinR;
|
|
3206
|
+
m[5] = cosR;
|
|
3172
3207
|
} else {
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
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 = (
|
|
3183
|
-
|
|
3217
|
+
$.scale = (x = 1, y, z = 1) => {
|
|
3218
|
+
y ??= x;
|
|
3184
3219
|
|
|
3185
|
-
$._matrix
|
|
3186
|
-
|
|
3187
|
-
|
|
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
|
|
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
|
-
|
|
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, $.
|
|
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
|
-
|
|
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
|
-
|
|
3422
|
+
// v is the texture index
|
|
3423
|
+
pass.setBindGroup(2, $._textureBindGroups[v]);
|
|
3368
3424
|
}
|
|
3369
|
-
pass.draw(6, 1, imageVertOffset
|
|
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
|
-
|
|
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:
|
|
3423
|
-
@location(
|
|
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(
|
|
3495
|
+
@group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
|
|
3433
3496
|
|
|
3434
3497
|
@vertex
|
|
3435
|
-
fn vertexMain(@location(0) pos:
|
|
3436
|
-
var vert =
|
|
3437
|
-
vert
|
|
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(
|
|
3515
|
+
@group(1) @binding(0) var<storage, read> colors : array<vec4f>;
|
|
3453
3516
|
|
|
3454
3517
|
@fragment
|
|
3455
|
-
fn fragmentMain(@location(
|
|
3456
|
-
let index =
|
|
3457
|
-
return mix(
|
|
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)
|
|
3694
|
-
|
|
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(
|
|
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:
|
|
3820
|
-
@location(0) texCoord:
|
|
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(
|
|
3900
|
+
@group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
|
|
3830
3901
|
|
|
3831
3902
|
@vertex
|
|
3832
|
-
fn vertexMain(@location(0) pos:
|
|
3833
|
-
var vert =
|
|
3834
|
-
vert
|
|
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(
|
|
3850
|
-
@group(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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(
|
|
4016
|
+
Q5.device.queue.copyExternalImageToTexture(
|
|
4017
|
+
{ source: img },
|
|
4018
|
+
{ texture, colorSpace: $.canvas.colorSpace },
|
|
4019
|
+
textureSize
|
|
4020
|
+
);
|
|
3943
4021
|
|
|
3944
|
-
|
|
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
|
|
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
|
|
4028
|
-
|
|
4029
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
else if (
|
|
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
|
};
|