q5 2.4.10 → 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 +733 -147
- package/q5.min.js +1 -1
- package/src/q5-2d-canvas.js +0 -9
- package/src/q5-2d-text.js +14 -15
- 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 +79 -62
- package/src/q5-webgpu-drawing.js +21 -13
- package/src/q5-webgpu-image.js +10 -12
- package/src/q5-webgpu-text.js +579 -26
- package/src/readme.md +79 -4
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 = {
|
|
@@ -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;
|
|
@@ -1424,9 +1419,10 @@ Q5.BLUR = 8;
|
|
|
1424
1419
|
Q5.renderers.q2d.text = ($, q) => {
|
|
1425
1420
|
$._textAlign = 'left';
|
|
1426
1421
|
$._textBaseline = 'alphabetic';
|
|
1422
|
+
$._textSize = 12;
|
|
1427
1423
|
|
|
1428
1424
|
let font = 'sans-serif',
|
|
1429
|
-
|
|
1425
|
+
leadingSet = false,
|
|
1430
1426
|
leading = 15,
|
|
1431
1427
|
leadDiff = 3,
|
|
1432
1428
|
emphasis = 'normal',
|
|
@@ -1458,12 +1454,12 @@ Q5.renderers.q2d.text = ($, q) => {
|
|
|
1458
1454
|
styleHash = -1;
|
|
1459
1455
|
};
|
|
1460
1456
|
$.textSize = (x) => {
|
|
1461
|
-
if (x === undefined) return
|
|
1457
|
+
if (x === undefined) return $._textSize;
|
|
1462
1458
|
if ($._da) x *= $._da;
|
|
1463
|
-
|
|
1459
|
+
$._textSize = x;
|
|
1464
1460
|
fontMod = true;
|
|
1465
1461
|
styleHash = -1;
|
|
1466
|
-
if (
|
|
1462
|
+
if (!leadingSet) {
|
|
1467
1463
|
leading = x * 1.25;
|
|
1468
1464
|
leadDiff = leading - x;
|
|
1469
1465
|
}
|
|
@@ -1477,8 +1473,8 @@ Q5.renderers.q2d.text = ($, q) => {
|
|
|
1477
1473
|
if (x === undefined) return leading;
|
|
1478
1474
|
if ($._da) x *= $._da;
|
|
1479
1475
|
leading = x;
|
|
1480
|
-
leadDiff = x -
|
|
1481
|
-
|
|
1476
|
+
leadDiff = x - $._textSize;
|
|
1477
|
+
leadingSet = true;
|
|
1482
1478
|
styleHash = -1;
|
|
1483
1479
|
};
|
|
1484
1480
|
$.textAlign = (horiz, vert) => {
|
|
@@ -1486,7 +1482,6 @@ Q5.renderers.q2d.text = ($, q) => {
|
|
|
1486
1482
|
if (vert) {
|
|
1487
1483
|
$.ctx.textBaseline = $._textBaseline = vert == $.CENTER ? 'middle' : vert;
|
|
1488
1484
|
}
|
|
1489
|
-
styleHash = -1;
|
|
1490
1485
|
};
|
|
1491
1486
|
|
|
1492
1487
|
$.textWidth = (str) => $.ctx.measureText(str).width;
|
|
@@ -1497,7 +1492,7 @@ Q5.renderers.q2d.text = ($, q) => {
|
|
|
1497
1492
|
$.textStroke = $.stroke;
|
|
1498
1493
|
|
|
1499
1494
|
let updateStyleHash = () => {
|
|
1500
|
-
let styleString = font +
|
|
1495
|
+
let styleString = font + $._textSize + emphasis + leading;
|
|
1501
1496
|
|
|
1502
1497
|
let hash = 5381;
|
|
1503
1498
|
for (let i = 0; i < styleString.length; i++) {
|
|
@@ -1530,7 +1525,7 @@ Q5.renderers.q2d.text = ($, q) => {
|
|
|
1530
1525
|
let img, tX, tY;
|
|
1531
1526
|
|
|
1532
1527
|
if (fontMod) {
|
|
1533
|
-
ctx.font = `${emphasis} ${
|
|
1528
|
+
ctx.font = `${emphasis} ${$._textSize}px ${font}`;
|
|
1534
1529
|
fontMod = false;
|
|
1535
1530
|
}
|
|
1536
1531
|
|
|
@@ -1551,7 +1546,7 @@ Q5.renderers.q2d.text = ($, q) => {
|
|
|
1551
1546
|
if (str.indexOf('\n') == -1) lines[0] = str;
|
|
1552
1547
|
else lines = str.split('\n');
|
|
1553
1548
|
|
|
1554
|
-
if (w) {
|
|
1549
|
+
if (str.length > w) {
|
|
1555
1550
|
let wrapped = [];
|
|
1556
1551
|
for (let line of lines) {
|
|
1557
1552
|
let i = 0;
|
|
@@ -1563,11 +1558,9 @@ Q5.renderers.q2d.text = ($, q) => {
|
|
|
1563
1558
|
break;
|
|
1564
1559
|
}
|
|
1565
1560
|
let end = line.lastIndexOf(' ', max);
|
|
1566
|
-
if (end === -1 || end < i)
|
|
1567
|
-
end = max;
|
|
1568
|
-
}
|
|
1561
|
+
if (end === -1 || end < i) end = max;
|
|
1569
1562
|
wrapped.push(line.slice(i, end));
|
|
1570
|
-
i = end;
|
|
1563
|
+
i = end + 1;
|
|
1571
1564
|
}
|
|
1572
1565
|
}
|
|
1573
1566
|
lines = wrapped;
|
|
@@ -1595,6 +1588,7 @@ Q5.renderers.q2d.text = ($, q) => {
|
|
|
1595
1588
|
img._top = descent + leadDiff;
|
|
1596
1589
|
img._middle = img._top + ascent * 0.5;
|
|
1597
1590
|
img._bottom = img._top + ascent;
|
|
1591
|
+
img._leading = leading;
|
|
1598
1592
|
}
|
|
1599
1593
|
|
|
1600
1594
|
img._fill = $._fill;
|
|
@@ -1654,7 +1648,7 @@ Q5.renderers.q2d.text = ($, q) => {
|
|
|
1654
1648
|
else if (ta == 'right') x -= img.width;
|
|
1655
1649
|
|
|
1656
1650
|
let bl = $._textBaseline;
|
|
1657
|
-
if (bl == 'alphabetic') y -=
|
|
1651
|
+
if (bl == 'alphabetic') y -= img._leading;
|
|
1658
1652
|
else if (bl == 'middle') y -= img._middle;
|
|
1659
1653
|
else if (bl == 'bottom') y -= img._bottom;
|
|
1660
1654
|
else if (bl == 'top') y -= img._top;
|
|
@@ -2740,10 +2734,11 @@ Q5.modules.util = ($, q) => {
|
|
|
2740
2734
|
fetch(path)
|
|
2741
2735
|
.then((r) => {
|
|
2742
2736
|
if (type == 'json') return r.json();
|
|
2743
|
-
|
|
2737
|
+
return r.text();
|
|
2744
2738
|
})
|
|
2745
2739
|
.then((r) => {
|
|
2746
2740
|
q._preloadCount--;
|
|
2741
|
+
if (type == 'csv') r = $.CSV.parse(r);
|
|
2747
2742
|
Object.assign(ret, r);
|
|
2748
2743
|
if (cb) cb(r);
|
|
2749
2744
|
});
|
|
@@ -2752,6 +2747,21 @@ Q5.modules.util = ($, q) => {
|
|
|
2752
2747
|
|
|
2753
2748
|
$.loadStrings = (path, cb) => $._loadFile(path, cb, 'text');
|
|
2754
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
|
+
};
|
|
2755
2765
|
|
|
2756
2766
|
if (typeof localStorage == 'object') {
|
|
2757
2767
|
$.storeItem = localStorage.setItem;
|
|
@@ -3064,7 +3074,8 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3064
3074
|
// colors used for each draw call
|
|
3065
3075
|
let colorsStack = ($.colorsStack = [1, 1, 1, 1]);
|
|
3066
3076
|
|
|
3067
|
-
$.
|
|
3077
|
+
$._transformLayout = Q5.device.createBindGroupLayout({
|
|
3078
|
+
label: 'transformLayout',
|
|
3068
3079
|
entries: [
|
|
3069
3080
|
{
|
|
3070
3081
|
binding: 0,
|
|
@@ -3073,14 +3084,9 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3073
3084
|
type: 'uniform',
|
|
3074
3085
|
hasDynamicOffset: false
|
|
3075
3086
|
}
|
|
3076
|
-
}
|
|
3077
|
-
]
|
|
3078
|
-
});
|
|
3079
|
-
|
|
3080
|
-
$._transformLayout = Q5.device.createBindGroupLayout({
|
|
3081
|
-
entries: [
|
|
3087
|
+
},
|
|
3082
3088
|
{
|
|
3083
|
-
binding:
|
|
3089
|
+
binding: 1,
|
|
3084
3090
|
visibility: GPUShaderStage.VERTEX,
|
|
3085
3091
|
buffer: {
|
|
3086
3092
|
type: 'read-only-storage',
|
|
@@ -3090,9 +3096,9 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3090
3096
|
]
|
|
3091
3097
|
});
|
|
3092
3098
|
|
|
3093
|
-
$.bindGroupLayouts = [$.
|
|
3099
|
+
$.bindGroupLayouts = [$._transformLayout];
|
|
3094
3100
|
|
|
3095
|
-
|
|
3101
|
+
let uniformBuffer = Q5.device.createBuffer({
|
|
3096
3102
|
size: 8, // Size of two floats
|
|
3097
3103
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
3098
3104
|
});
|
|
@@ -3107,18 +3113,6 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3107
3113
|
|
|
3108
3114
|
Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([$.canvas.hw, $.canvas.hh]));
|
|
3109
3115
|
|
|
3110
|
-
$._envBindGroup = Q5.device.createBindGroup({
|
|
3111
|
-
layout: $._envLayout,
|
|
3112
|
-
entries: [
|
|
3113
|
-
{
|
|
3114
|
-
binding: 0,
|
|
3115
|
-
resource: {
|
|
3116
|
-
buffer: uniformBuffer
|
|
3117
|
-
}
|
|
3118
|
-
}
|
|
3119
|
-
]
|
|
3120
|
-
});
|
|
3121
|
-
|
|
3122
3116
|
return c;
|
|
3123
3117
|
};
|
|
3124
3118
|
|
|
@@ -3128,7 +3122,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3128
3122
|
|
|
3129
3123
|
// current color index, used to associate a vertex with a color
|
|
3130
3124
|
let colorIndex = 0;
|
|
3131
|
-
|
|
3125
|
+
let addColor = (r, g, b, a = 1) => {
|
|
3132
3126
|
if (typeof r == 'string') r = $.color(r);
|
|
3133
3127
|
else if (b == undefined) {
|
|
3134
3128
|
// grayscale mode `fill(1, 0.5)`
|
|
@@ -3140,6 +3134,8 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3140
3134
|
colorIndex++;
|
|
3141
3135
|
};
|
|
3142
3136
|
|
|
3137
|
+
$._fillIndex = $._strokeIndex = -1;
|
|
3138
|
+
|
|
3143
3139
|
$.fill = (r, g, b, a) => {
|
|
3144
3140
|
addColor(r, g, b, a);
|
|
3145
3141
|
$._doFill = true;
|
|
@@ -3171,56 +3167,70 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3171
3167
|
};
|
|
3172
3168
|
$.resetMatrix();
|
|
3173
3169
|
|
|
3174
|
-
//
|
|
3170
|
+
// tracks if the matrix has been modified
|
|
3175
3171
|
$._matrixDirty = false;
|
|
3176
3172
|
|
|
3177
|
-
//
|
|
3173
|
+
// array to store transformation matrices for the render pass
|
|
3178
3174
|
$.transformStates = [$._matrix.slice()];
|
|
3179
3175
|
|
|
3180
|
-
//
|
|
3176
|
+
// stack to keep track of transformation matrix indexes
|
|
3181
3177
|
$._transformIndexStack = [];
|
|
3182
3178
|
|
|
3183
3179
|
$.translate = (x, y, z) => {
|
|
3184
3180
|
if (!x && !y && !z) return;
|
|
3185
3181
|
// Update the translation values
|
|
3186
|
-
$._matrix[
|
|
3187
|
-
$._matrix[
|
|
3188
|
-
$._matrix[
|
|
3182
|
+
$._matrix[12] += x;
|
|
3183
|
+
$._matrix[13] -= y;
|
|
3184
|
+
$._matrix[14] += z || 0;
|
|
3189
3185
|
$._matrixDirty = true;
|
|
3190
3186
|
};
|
|
3191
3187
|
|
|
3192
|
-
$.rotate = (
|
|
3193
|
-
if (!
|
|
3194
|
-
if ($._angleMode)
|
|
3188
|
+
$.rotate = (a) => {
|
|
3189
|
+
if (!a) return;
|
|
3190
|
+
if ($._angleMode) a *= $._DEGTORAD;
|
|
3195
3191
|
|
|
3196
|
-
let cosR = Math.cos(
|
|
3197
|
-
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];
|
|
3198
3201
|
|
|
3199
|
-
let m0 = $._matrix[0],
|
|
3200
|
-
m1 = $._matrix[1],
|
|
3201
|
-
m4 = $._matrix[4],
|
|
3202
|
-
m5 = $._matrix[5];
|
|
3203
3202
|
if (!m0 && !m1 && !m4 && !m5) {
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3203
|
+
m[0] = cosR;
|
|
3204
|
+
m[1] = sinR;
|
|
3205
|
+
m[4] = -sinR;
|
|
3206
|
+
m[5] = cosR;
|
|
3208
3207
|
} else {
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
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;
|
|
3213
3212
|
}
|
|
3214
3213
|
|
|
3215
3214
|
$._matrixDirty = true;
|
|
3216
3215
|
};
|
|
3217
3216
|
|
|
3218
|
-
$.scale = (
|
|
3219
|
-
|
|
3217
|
+
$.scale = (x = 1, y, z = 1) => {
|
|
3218
|
+
y ??= x;
|
|
3220
3219
|
|
|
3221
|
-
$._matrix
|
|
3222
|
-
|
|
3223
|
-
|
|
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;
|
|
3224
3234
|
|
|
3225
3235
|
$._matrixDirty = true;
|
|
3226
3236
|
};
|
|
@@ -3292,7 +3302,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3292
3302
|
if (!$._transformIndexStack.length) {
|
|
3293
3303
|
return console.warn('Matrix index stack is empty!');
|
|
3294
3304
|
}
|
|
3295
|
-
// Pop the last matrix index
|
|
3305
|
+
// Pop the last matrix index and set it as the current matrix index
|
|
3296
3306
|
let idx = $._transformIndexStack.pop();
|
|
3297
3307
|
$._matrix = $.transformStates[idx].slice();
|
|
3298
3308
|
$._transformIndex = idx;
|
|
@@ -3315,7 +3325,6 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3315
3325
|
// left, right, top, bottom
|
|
3316
3326
|
let l, r, t, b;
|
|
3317
3327
|
if (!mode || mode == 'corner') {
|
|
3318
|
-
// CORNER
|
|
3319
3328
|
l = x;
|
|
3320
3329
|
r = x + w;
|
|
3321
3330
|
t = -y;
|
|
@@ -3355,7 +3364,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3355
3364
|
|
|
3356
3365
|
$._render = () => {
|
|
3357
3366
|
if (transformStates.length > 1 || !$._transformBindGroup) {
|
|
3358
|
-
|
|
3367
|
+
let transformBuffer = Q5.device.createBuffer({
|
|
3359
3368
|
size: transformStates.length * 64, // Size of 16 floats
|
|
3360
3369
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
3361
3370
|
});
|
|
@@ -3367,6 +3376,12 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3367
3376
|
entries: [
|
|
3368
3377
|
{
|
|
3369
3378
|
binding: 0,
|
|
3379
|
+
resource: {
|
|
3380
|
+
buffer: uniformBuffer
|
|
3381
|
+
}
|
|
3382
|
+
},
|
|
3383
|
+
{
|
|
3384
|
+
binding: 1,
|
|
3370
3385
|
resource: {
|
|
3371
3386
|
buffer: transformBuffer
|
|
3372
3387
|
}
|
|
@@ -3375,35 +3390,47 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3375
3390
|
});
|
|
3376
3391
|
}
|
|
3377
3392
|
|
|
3378
|
-
pass.setBindGroup(0, $.
|
|
3379
|
-
pass.setBindGroup(1, $._transformBindGroup);
|
|
3393
|
+
pass.setBindGroup(0, $._transformBindGroup);
|
|
3380
3394
|
|
|
3381
3395
|
for (let m of $._hooks.preRender) m();
|
|
3382
3396
|
|
|
3383
3397
|
let drawVertOffset = 0;
|
|
3384
3398
|
let imageVertOffset = 0;
|
|
3399
|
+
let textCharOffset = 0;
|
|
3385
3400
|
let curPipelineIndex = -1;
|
|
3386
3401
|
let curTextureIndex = -1;
|
|
3387
3402
|
|
|
3388
|
-
pass.setPipeline($.pipelines[0]);
|
|
3389
|
-
|
|
3390
3403
|
for (let i = 0; i < drawStack.length; i += 2) {
|
|
3391
3404
|
let v = drawStack[i + 1];
|
|
3392
3405
|
|
|
3406
|
+
if (drawStack[i] == -1) {
|
|
3407
|
+
v();
|
|
3408
|
+
continue;
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3393
3411
|
if (curPipelineIndex != drawStack[i]) {
|
|
3394
3412
|
curPipelineIndex = drawStack[i];
|
|
3395
3413
|
pass.setPipeline($.pipelines[curPipelineIndex]);
|
|
3396
3414
|
}
|
|
3397
3415
|
|
|
3398
3416
|
if (curPipelineIndex == 0) {
|
|
3399
|
-
|
|
3417
|
+
// v is the number of vertices
|
|
3418
|
+
pass.draw(v, 1, drawVertOffset);
|
|
3400
3419
|
drawVertOffset += v;
|
|
3401
3420
|
} else if (curPipelineIndex == 1) {
|
|
3402
3421
|
if (curTextureIndex != v) {
|
|
3403
|
-
|
|
3422
|
+
// v is the texture index
|
|
3423
|
+
pass.setBindGroup(2, $._textureBindGroups[v]);
|
|
3404
3424
|
}
|
|
3405
|
-
pass.draw(6, 1, imageVertOffset
|
|
3425
|
+
pass.draw(6, 1, imageVertOffset);
|
|
3406
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;
|
|
3407
3434
|
}
|
|
3408
3435
|
}
|
|
3409
3436
|
|
|
@@ -3412,7 +3439,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
|
|
|
3412
3439
|
|
|
3413
3440
|
$._finishRender = () => {
|
|
3414
3441
|
pass.end();
|
|
3415
|
-
|
|
3442
|
+
let commandBuffer = $.encoder.finish();
|
|
3416
3443
|
Q5.device.queue.submit([commandBuffer]);
|
|
3417
3444
|
q.pass = $.encoder = null;
|
|
3418
3445
|
|
|
@@ -3455,8 +3482,8 @@ Q5.renderers.webgpu.drawing = ($, q) => {
|
|
|
3455
3482
|
label: 'drawingVertexShader',
|
|
3456
3483
|
code: `
|
|
3457
3484
|
struct VertexOutput {
|
|
3458
|
-
@builtin(position) position:
|
|
3459
|
-
@location(
|
|
3485
|
+
@builtin(position) position: vec4f,
|
|
3486
|
+
@location(0) colorIndex: f32
|
|
3460
3487
|
};
|
|
3461
3488
|
|
|
3462
3489
|
struct Uniforms {
|
|
@@ -3465,12 +3492,12 @@ struct Uniforms {
|
|
|
3465
3492
|
};
|
|
3466
3493
|
|
|
3467
3494
|
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
3468
|
-
@group(
|
|
3495
|
+
@group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
|
|
3469
3496
|
|
|
3470
3497
|
@vertex
|
|
3471
|
-
fn vertexMain(@location(0) pos:
|
|
3472
|
-
var vert =
|
|
3473
|
-
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;
|
|
3474
3501
|
vert.x /= uniforms.halfWidth;
|
|
3475
3502
|
vert.y /= uniforms.halfHeight;
|
|
3476
3503
|
|
|
@@ -3485,17 +3512,18 @@ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) colorIndex: f32, @locati
|
|
|
3485
3512
|
let fragmentShader = Q5.device.createShaderModule({
|
|
3486
3513
|
label: 'drawingFragmentShader',
|
|
3487
3514
|
code: `
|
|
3488
|
-
@group(
|
|
3515
|
+
@group(1) @binding(0) var<storage, read> colors : array<vec4f>;
|
|
3489
3516
|
|
|
3490
3517
|
@fragment
|
|
3491
|
-
fn fragmentMain(@location(
|
|
3492
|
-
let index =
|
|
3493
|
-
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));
|
|
3494
3521
|
}
|
|
3495
3522
|
`
|
|
3496
3523
|
});
|
|
3497
3524
|
|
|
3498
3525
|
colorsLayout = Q5.device.createBindGroupLayout({
|
|
3526
|
+
label: 'colorsLayout',
|
|
3499
3527
|
entries: [
|
|
3500
3528
|
{
|
|
3501
3529
|
binding: 0,
|
|
@@ -3726,10 +3754,17 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
|
|
|
3726
3754
|
$.background = (r, g, b, a) => {
|
|
3727
3755
|
$.push();
|
|
3728
3756
|
$.resetMatrix();
|
|
3729
|
-
if (r.src)
|
|
3730
|
-
|
|
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';
|
|
3731
3765
|
$.fill(r, g, b, a);
|
|
3732
3766
|
$.rect(-c.hw, -c.hh, c.w, c.h);
|
|
3767
|
+
$._rectMode = og;
|
|
3733
3768
|
}
|
|
3734
3769
|
$.pop();
|
|
3735
3770
|
};
|
|
@@ -3837,7 +3872,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
|
|
|
3837
3872
|
});
|
|
3838
3873
|
|
|
3839
3874
|
// set the bind group once before rendering
|
|
3840
|
-
$.pass.setBindGroup(
|
|
3875
|
+
$.pass.setBindGroup(1, $._colorsBindGroup);
|
|
3841
3876
|
});
|
|
3842
3877
|
|
|
3843
3878
|
$._hooks.postRender.push(() => {
|
|
@@ -3852,8 +3887,8 @@ Q5.renderers.webgpu.image = ($, q) => {
|
|
|
3852
3887
|
label: 'imageVertexShader',
|
|
3853
3888
|
code: `
|
|
3854
3889
|
struct VertexOutput {
|
|
3855
|
-
@builtin(position) position:
|
|
3856
|
-
@location(0) texCoord:
|
|
3890
|
+
@builtin(position) position: vec4f,
|
|
3891
|
+
@location(0) texCoord: vec2f
|
|
3857
3892
|
};
|
|
3858
3893
|
|
|
3859
3894
|
struct Uniforms {
|
|
@@ -3862,12 +3897,12 @@ struct Uniforms {
|
|
|
3862
3897
|
};
|
|
3863
3898
|
|
|
3864
3899
|
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
3865
|
-
@group(
|
|
3900
|
+
@group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
|
|
3866
3901
|
|
|
3867
3902
|
@vertex
|
|
3868
|
-
fn vertexMain(@location(0) pos:
|
|
3869
|
-
var vert =
|
|
3870
|
-
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;
|
|
3871
3906
|
vert.x /= uniforms.halfWidth;
|
|
3872
3907
|
vert.y /= uniforms.halfHeight;
|
|
3873
3908
|
|
|
@@ -3882,11 +3917,11 @@ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) texCoord: vec2<f32>, @lo
|
|
|
3882
3917
|
let fragmentShader = Q5.device.createShaderModule({
|
|
3883
3918
|
label: 'imageFragmentShader',
|
|
3884
3919
|
code: `
|
|
3885
|
-
@group(
|
|
3886
|
-
@group(
|
|
3920
|
+
@group(2) @binding(0) var samp: sampler;
|
|
3921
|
+
@group(2) @binding(1) var texture: texture_2d<f32>;
|
|
3887
3922
|
|
|
3888
3923
|
@fragment
|
|
3889
|
-
fn fragmentMain(@location(0) texCoord:
|
|
3924
|
+
fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
|
|
3890
3925
|
// Sample the texture using the interpolated texture coordinate
|
|
3891
3926
|
return textureSample(texture, samp, texCoord);
|
|
3892
3927
|
}
|
|
@@ -3918,11 +3953,9 @@ fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
|
|
|
3918
3953
|
]
|
|
3919
3954
|
};
|
|
3920
3955
|
|
|
3921
|
-
$.bindGroupLayouts.push(textureLayout);
|
|
3922
|
-
|
|
3923
3956
|
const pipelineLayout = Q5.device.createPipelineLayout({
|
|
3924
3957
|
label: 'imagePipelineLayout',
|
|
3925
|
-
bindGroupLayouts:
|
|
3958
|
+
bindGroupLayouts: [...$.bindGroupLayouts, textureLayout]
|
|
3926
3959
|
});
|
|
3927
3960
|
|
|
3928
3961
|
$.pipelines[1] = Q5.device.createRenderPipeline({
|
|
@@ -4079,34 +4112,522 @@ Q5.DILATE = 6;
|
|
|
4079
4112
|
Q5.ERODE = 7;
|
|
4080
4113
|
Q5.BLUR = 8;
|
|
4081
4114
|
Q5.renderers.webgpu.text = ($, q) => {
|
|
4082
|
-
let
|
|
4083
|
-
|
|
4084
|
-
|
|
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));
|
|
4085
4120
|
|
|
4086
|
-
|
|
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) => {
|
|
4087
4256
|
q._preloadCount++;
|
|
4088
|
-
|
|
4257
|
+
|
|
4258
|
+
let res = await fetch(fontJsonUrl);
|
|
4259
|
+
if (res.status == 404) {
|
|
4089
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
|
|
4090
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
|
+
}
|
|
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);
|
|
4091
4408
|
};
|
|
4092
4409
|
|
|
4093
|
-
//
|
|
4094
|
-
|
|
4095
|
-
$.
|
|
4096
|
-
|
|
4097
|
-
$.
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
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
|
+
};
|
|
4505
|
+
};
|
|
4102
4506
|
|
|
4103
|
-
|
|
4104
|
-
$.textStroke = (r, g, b, a) => t.stroke($.color(r, g, b, a));
|
|
4507
|
+
let initLoadDefaultFont;
|
|
4105
4508
|
|
|
4106
4509
|
$.text = (str, x, y, w, h) => {
|
|
4107
|
-
|
|
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
|
+
}
|
|
4108
4518
|
|
|
4109
|
-
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
|
+
}
|
|
4535
|
+
|
|
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;
|
|
4614
|
+
};
|
|
4615
|
+
|
|
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) {
|
|
4110
4631
|
$._createTexture(img);
|
|
4111
4632
|
} else if (img.modified) {
|
|
4112
4633
|
let cnv = img.canvas;
|
|
@@ -4120,27 +4641,92 @@ Q5.renderers.webgpu.text = ($, q) => {
|
|
|
4120
4641
|
);
|
|
4121
4642
|
img.modified = false;
|
|
4122
4643
|
}
|
|
4123
|
-
|
|
4124
|
-
$.textImage(img, x, y);
|
|
4644
|
+
return img;
|
|
4125
4645
|
};
|
|
4126
4646
|
|
|
4127
|
-
$.createTextImage = t.createTextImage;
|
|
4128
|
-
|
|
4129
4647
|
$.textImage = (img, x, y) => {
|
|
4130
4648
|
let og = $._imageMode;
|
|
4131
4649
|
$._imageMode = 'corner';
|
|
4132
4650
|
|
|
4133
|
-
let ta =
|
|
4651
|
+
let ta = $._textAlign;
|
|
4134
4652
|
if (ta == 'center') x -= img.canvas.hw;
|
|
4135
4653
|
else if (ta == 'right') x -= img.width;
|
|
4136
4654
|
|
|
4137
|
-
let bl =
|
|
4138
|
-
if (bl == 'alphabetic') y -=
|
|
4139
|
-
else if (bl == '
|
|
4655
|
+
let bl = $._textBaseline;
|
|
4656
|
+
if (bl == 'alphabetic') y -= img._leading;
|
|
4657
|
+
else if (bl == 'center') y -= img._middle;
|
|
4140
4658
|
else if (bl == 'bottom') y -= img._bottom;
|
|
4141
4659
|
else if (bl == 'top') y -= img._top;
|
|
4142
4660
|
|
|
4143
4661
|
$.image(img, x, y);
|
|
4144
4662
|
$._imageMode = og;
|
|
4145
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
|
+
});
|
|
4146
4732
|
};
|