q5 2.4.10 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/q5.js CHANGED
@@ -70,9 +70,9 @@ function Q5(scope, parent, renderer) {
70
70
  let ts = timestamp || performance.now();
71
71
  $._lastFrameTime ??= ts - $._targetFrameDuration;
72
72
 
73
- if ($._shouldResize) {
73
+ if ($._didResize) {
74
74
  $.windowResized();
75
- $._shouldResize = false;
75
+ $._didResize = false;
76
76
  }
77
77
 
78
78
  if ($._loop) looper = raf($._draw);
@@ -483,7 +483,7 @@ Q5.modules.canvas = ($, q) => {
483
483
 
484
484
  function parentResized() {
485
485
  if ($.frameCount > 1) {
486
- $._shouldResize = true;
486
+ $._didResize = true;
487
487
  $._adjustDisplay();
488
488
  }
489
489
  }
@@ -543,14 +543,9 @@ Q5.modules.canvas = ($, q) => {
543
543
  '_imageMode',
544
544
  '_rectMode',
545
545
  '_ellipseMode',
546
- '_textFont',
547
- '_textLeading',
548
- '_leadingSet',
549
546
  '_textSize',
550
547
  '_textAlign',
551
- '_textBaseline',
552
- '_textStyle',
553
- '_textWrap'
548
+ '_textBaseline'
554
549
  ];
555
550
  $._styles = [];
556
551
 
@@ -563,6 +558,15 @@ Q5.modules.canvas = ($, q) => {
563
558
  let styles = $._styles.pop();
564
559
  for (let s of $._styleNames) $[s] = styles[s];
565
560
  };
561
+
562
+ if (window && $._scope != 'graphics') {
563
+ window.addEventListener('resize', () => {
564
+ $._didResize = true;
565
+ q.windowWidth = window.innerWidth;
566
+ q.windowHeight = window.innerHeight;
567
+ q.deviceOrientation = window.screen?.orientation?.type;
568
+ });
569
+ }
566
570
  };
567
571
 
568
572
  Q5.canvasOptions = {
@@ -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;
@@ -1352,11 +1347,18 @@ Q5.renderers.q2d.image = ($, q) => {
1352
1347
  $.ctx.restore();
1353
1348
  };
1354
1349
 
1350
+ $.inset = (x, y, w, h, dx, dy, dw, dh) => {
1351
+ let pd = $._pixelDensity || 1;
1352
+ $.ctx.drawImage($.canvas, x * pd, y * pd, w * pd, h * pd, dx, dy, dw, dh);
1353
+ };
1354
+
1355
+ $.copy = () => $.get();
1356
+
1355
1357
  $.get = (x, y, w, h) => {
1356
1358
  let pd = $._pixelDensity || 1;
1357
1359
  if (x !== undefined && w === undefined) {
1358
1360
  let c = $._getImageData(x * pd, y * pd, 1, 1).data;
1359
- return new $.Color(c[0], c[1], c[2], c[3] / 255);
1361
+ return [c[0], c[1], c[2], c[3] / 255];
1360
1362
  }
1361
1363
  x = (x || 0) * pd;
1362
1364
  y = (y || 0) * pd;
@@ -1365,8 +1367,7 @@ Q5.renderers.q2d.image = ($, q) => {
1365
1367
  w *= pd;
1366
1368
  h *= pd;
1367
1369
  let img = $.createImage(w, h);
1368
- let imgData = $._getImageData(x, y, w, h);
1369
- img.ctx.putImageData(imgData, 0, 0);
1370
+ img.ctx.drawImage($.canvas, x, y, w, h, 0, 0, w, h);
1370
1371
  img._pixelDensity = pd;
1371
1372
  img.width = _w;
1372
1373
  img.height = _h;
@@ -1386,9 +1387,9 @@ Q5.renderers.q2d.image = ($, q) => {
1386
1387
  for (let i = 0; i < mod; i++) {
1387
1388
  for (let j = 0; j < mod; j++) {
1388
1389
  let idx = 4 * ((y * mod + i) * $.canvas.width + x * mod + j);
1389
- $.pixels[idx] = c.r ?? c.l;
1390
- $.pixels[idx + 1] = c.g ?? c.c;
1391
- $.pixels[idx + 2] = c.b ?? c.h;
1390
+ $.pixels[idx] = c.r;
1391
+ $.pixels[idx + 1] = c.g;
1392
+ $.pixels[idx + 2] = c.b;
1392
1393
  $.pixels[idx + 3] = c.a;
1393
1394
  }
1394
1395
  }
@@ -1424,9 +1425,10 @@ Q5.BLUR = 8;
1424
1425
  Q5.renderers.q2d.text = ($, q) => {
1425
1426
  $._textAlign = 'left';
1426
1427
  $._textBaseline = 'alphabetic';
1428
+ $._textSize = 12;
1427
1429
 
1428
1430
  let font = 'sans-serif',
1429
- tSize = 12,
1431
+ leadingSet = false,
1430
1432
  leading = 15,
1431
1433
  leadDiff = 3,
1432
1434
  emphasis = 'normal',
@@ -1453,32 +1455,34 @@ Q5.renderers.q2d.text = ($, q) => {
1453
1455
  };
1454
1456
 
1455
1457
  $.textFont = (x) => {
1458
+ if (!x || x == font) return font;
1456
1459
  font = x;
1457
1460
  fontMod = true;
1458
1461
  styleHash = -1;
1459
1462
  };
1460
1463
  $.textSize = (x) => {
1461
- if (x === undefined) return tSize;
1464
+ if (x == undefined || x == $._textSize) return $._textSize;
1462
1465
  if ($._da) x *= $._da;
1463
- tSize = x;
1466
+ $._textSize = x;
1464
1467
  fontMod = true;
1465
1468
  styleHash = -1;
1466
- if (!$._leadingSet) {
1469
+ if (!leadingSet) {
1467
1470
  leading = x * 1.25;
1468
1471
  leadDiff = leading - x;
1469
1472
  }
1470
1473
  };
1471
1474
  $.textStyle = (x) => {
1475
+ if (!x || x == emphasis) return emphasis;
1472
1476
  emphasis = x;
1473
1477
  fontMod = true;
1474
1478
  styleHash = -1;
1475
1479
  };
1476
1480
  $.textLeading = (x) => {
1477
- if (x === undefined) return leading;
1481
+ leadingSet = true;
1482
+ if (x == undefined || x == leading) return leading;
1478
1483
  if ($._da) x *= $._da;
1479
1484
  leading = x;
1480
- leadDiff = x - tSize;
1481
- $._leadingSet = true;
1485
+ leadDiff = x - $._textSize;
1482
1486
  styleHash = -1;
1483
1487
  };
1484
1488
  $.textAlign = (horiz, vert) => {
@@ -1486,7 +1490,6 @@ Q5.renderers.q2d.text = ($, q) => {
1486
1490
  if (vert) {
1487
1491
  $.ctx.textBaseline = $._textBaseline = vert == $.CENTER ? 'middle' : vert;
1488
1492
  }
1489
- styleHash = -1;
1490
1493
  };
1491
1494
 
1492
1495
  $.textWidth = (str) => $.ctx.measureText(str).width;
@@ -1497,7 +1500,7 @@ Q5.renderers.q2d.text = ($, q) => {
1497
1500
  $.textStroke = $.stroke;
1498
1501
 
1499
1502
  let updateStyleHash = () => {
1500
- let styleString = font + tSize + emphasis + leading;
1503
+ let styleString = font + $._textSize + emphasis + leading;
1501
1504
 
1502
1505
  let hash = 5381;
1503
1506
  for (let i = 0; i < styleString.length; i++) {
@@ -1530,7 +1533,7 @@ Q5.renderers.q2d.text = ($, q) => {
1530
1533
  let img, tX, tY;
1531
1534
 
1532
1535
  if (fontMod) {
1533
- ctx.font = `${emphasis} ${tSize}px ${font}`;
1536
+ ctx.font = `${emphasis} ${$._textSize}px ${font}`;
1534
1537
  fontMod = false;
1535
1538
  }
1536
1539
 
@@ -1551,7 +1554,7 @@ Q5.renderers.q2d.text = ($, q) => {
1551
1554
  if (str.indexOf('\n') == -1) lines[0] = str;
1552
1555
  else lines = str.split('\n');
1553
1556
 
1554
- if (w) {
1557
+ if (str.length > w) {
1555
1558
  let wrapped = [];
1556
1559
  for (let line of lines) {
1557
1560
  let i = 0;
@@ -1563,11 +1566,9 @@ Q5.renderers.q2d.text = ($, q) => {
1563
1566
  break;
1564
1567
  }
1565
1568
  let end = line.lastIndexOf(' ', max);
1566
- if (end === -1 || end < i) {
1567
- end = max;
1568
- }
1569
+ if (end === -1 || end < i) end = max;
1569
1570
  wrapped.push(line.slice(i, end));
1570
- i = end;
1571
+ i = end + 1;
1571
1572
  }
1572
1573
  }
1573
1574
  lines = wrapped;
@@ -1595,6 +1596,7 @@ Q5.renderers.q2d.text = ($, q) => {
1595
1596
  img._top = descent + leadDiff;
1596
1597
  img._middle = img._top + ascent * 0.5;
1597
1598
  img._bottom = img._top + ascent;
1599
+ img._leading = leading;
1598
1600
  }
1599
1601
 
1600
1602
  img._fill = $._fill;
@@ -1646,6 +1648,8 @@ Q5.renderers.q2d.text = ($, q) => {
1646
1648
  }
1647
1649
  };
1648
1650
  $.textImage = (img, x, y) => {
1651
+ if (typeof img == 'string') img = $.createTextImage(img);
1652
+
1649
1653
  let og = $._imageMode;
1650
1654
  $._imageMode = 'corner';
1651
1655
 
@@ -1654,7 +1658,7 @@ Q5.renderers.q2d.text = ($, q) => {
1654
1658
  else if (ta == 'right') x -= img.width;
1655
1659
 
1656
1660
  let bl = $._textBaseline;
1657
- if (bl == 'alphabetic') y -= leading;
1661
+ if (bl == 'alphabetic') y -= img._leading;
1658
1662
  else if (bl == 'middle') y -= img._middle;
1659
1663
  else if (bl == 'bottom') y -= img._bottom;
1660
1664
  else if (bl == 'top') y -= img._top;
@@ -2740,10 +2744,11 @@ Q5.modules.util = ($, q) => {
2740
2744
  fetch(path)
2741
2745
  .then((r) => {
2742
2746
  if (type == 'json') return r.json();
2743
- if (type == 'text') return r.text();
2747
+ return r.text();
2744
2748
  })
2745
2749
  .then((r) => {
2746
2750
  q._preloadCount--;
2751
+ if (type == 'csv') r = $.CSV.parse(r);
2747
2752
  Object.assign(ret, r);
2748
2753
  if (cb) cb(r);
2749
2754
  });
@@ -2752,6 +2757,21 @@ Q5.modules.util = ($, q) => {
2752
2757
 
2753
2758
  $.loadStrings = (path, cb) => $._loadFile(path, cb, 'text');
2754
2759
  $.loadJSON = (path, cb) => $._loadFile(path, cb, 'json');
2760
+ $.loadCSV = (path, cb) => $._loadFile(path, cb, 'csv');
2761
+
2762
+ $.CSV = {};
2763
+ $.CSV.parse = (csv, sep = ',', lineSep = '\n') => {
2764
+ let a = [],
2765
+ lns = csv.split(lineSep),
2766
+ headers = lns[0].split(sep);
2767
+ for (let i = 1; i < lns.length; i++) {
2768
+ let o = {},
2769
+ ln = lns[i].split(sep);
2770
+ headers.forEach((h, i) => (o[h] = JSON.parse(ln[i])));
2771
+ a.push(o);
2772
+ }
2773
+ return a;
2774
+ };
2755
2775
 
2756
2776
  if (typeof localStorage == 'object') {
2757
2777
  $.storeItem = localStorage.setItem;
@@ -3064,7 +3084,8 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3064
3084
  // colors used for each draw call
3065
3085
  let colorsStack = ($.colorsStack = [1, 1, 1, 1]);
3066
3086
 
3067
- $._envLayout = Q5.device.createBindGroupLayout({
3087
+ $._transformLayout = Q5.device.createBindGroupLayout({
3088
+ label: 'transformLayout',
3068
3089
  entries: [
3069
3090
  {
3070
3091
  binding: 0,
@@ -3073,14 +3094,9 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3073
3094
  type: 'uniform',
3074
3095
  hasDynamicOffset: false
3075
3096
  }
3076
- }
3077
- ]
3078
- });
3079
-
3080
- $._transformLayout = Q5.device.createBindGroupLayout({
3081
- entries: [
3097
+ },
3082
3098
  {
3083
- binding: 0,
3099
+ binding: 1,
3084
3100
  visibility: GPUShaderStage.VERTEX,
3085
3101
  buffer: {
3086
3102
  type: 'read-only-storage',
@@ -3090,9 +3106,9 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3090
3106
  ]
3091
3107
  });
3092
3108
 
3093
- $.bindGroupLayouts = [$._envLayout, $._transformLayout];
3109
+ $.bindGroupLayouts = [$._transformLayout];
3094
3110
 
3095
- const uniformBuffer = Q5.device.createBuffer({
3111
+ let uniformBuffer = Q5.device.createBuffer({
3096
3112
  size: 8, // Size of two floats
3097
3113
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
3098
3114
  });
@@ -3107,18 +3123,6 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3107
3123
 
3108
3124
  Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([$.canvas.hw, $.canvas.hh]));
3109
3125
 
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
3126
  return c;
3123
3127
  };
3124
3128
 
@@ -3128,7 +3132,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3128
3132
 
3129
3133
  // current color index, used to associate a vertex with a color
3130
3134
  let colorIndex = 0;
3131
- const addColor = (r, g, b, a = 1) => {
3135
+ let addColor = (r, g, b, a = 1) => {
3132
3136
  if (typeof r == 'string') r = $.color(r);
3133
3137
  else if (b == undefined) {
3134
3138
  // grayscale mode `fill(1, 0.5)`
@@ -3140,6 +3144,8 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3140
3144
  colorIndex++;
3141
3145
  };
3142
3146
 
3147
+ $._fillIndex = $._strokeIndex = -1;
3148
+
3143
3149
  $.fill = (r, g, b, a) => {
3144
3150
  addColor(r, g, b, a);
3145
3151
  $._doFill = true;
@@ -3171,56 +3177,70 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3171
3177
  };
3172
3178
  $.resetMatrix();
3173
3179
 
3174
- // Boolean to track if the matrix has been modified
3180
+ // tracks if the matrix has been modified
3175
3181
  $._matrixDirty = false;
3176
3182
 
3177
- // Array to store transformation matrices for the render pass
3183
+ // array to store transformation matrices for the render pass
3178
3184
  $.transformStates = [$._matrix.slice()];
3179
3185
 
3180
- // Stack to keep track of transformation matrix indexes
3186
+ // stack to keep track of transformation matrix indexes
3181
3187
  $._transformIndexStack = [];
3182
3188
 
3183
3189
  $.translate = (x, y, z) => {
3184
3190
  if (!x && !y && !z) return;
3185
3191
  // Update the translation values
3186
- $._matrix[3] += x;
3187
- $._matrix[7] -= y;
3188
- $._matrix[11] += z || 0;
3192
+ $._matrix[12] += x;
3193
+ $._matrix[13] -= y;
3194
+ $._matrix[14] += z || 0;
3189
3195
  $._matrixDirty = true;
3190
3196
  };
3191
3197
 
3192
- $.rotate = (r) => {
3193
- if (!r) return;
3194
- if ($._angleMode) r *= $._DEGTORAD;
3198
+ $.rotate = (a) => {
3199
+ if (!a) return;
3200
+ if ($._angleMode) a *= $._DEGTORAD;
3195
3201
 
3196
- let cosR = Math.cos(r);
3197
- let sinR = Math.sin(r);
3202
+ let cosR = Math.cos(a);
3203
+ let sinR = Math.sin(a);
3204
+
3205
+ let m = $._matrix;
3206
+
3207
+ let m0 = m[0],
3208
+ m1 = m[1],
3209
+ m4 = m[4],
3210
+ m5 = m[5];
3198
3211
 
3199
- let m0 = $._matrix[0],
3200
- m1 = $._matrix[1],
3201
- m4 = $._matrix[4],
3202
- m5 = $._matrix[5];
3203
3212
  if (!m0 && !m1 && !m4 && !m5) {
3204
- $._matrix[0] = cosR;
3205
- $._matrix[1] = sinR;
3206
- $._matrix[4] = -sinR;
3207
- $._matrix[5] = cosR;
3213
+ m[0] = cosR;
3214
+ m[1] = sinR;
3215
+ m[4] = -sinR;
3216
+ m[5] = cosR;
3208
3217
  } else {
3209
- $._matrix[0] = m0 * cosR + m4 * sinR;
3210
- $._matrix[1] = m1 * cosR + m5 * sinR;
3211
- $._matrix[4] = m0 * -sinR + m4 * cosR;
3212
- $._matrix[5] = m1 * -sinR + m5 * cosR;
3218
+ m[0] = m0 * cosR + m4 * sinR;
3219
+ m[1] = m1 * cosR + m5 * sinR;
3220
+ m[4] = m4 * cosR - m0 * sinR;
3221
+ m[5] = m5 * cosR - m1 * sinR;
3213
3222
  }
3214
3223
 
3215
3224
  $._matrixDirty = true;
3216
3225
  };
3217
3226
 
3218
- $.scale = (sx = 1, sy, sz = 1) => {
3219
- sy ??= sx;
3227
+ $.scale = (x = 1, y, z = 1) => {
3228
+ y ??= x;
3220
3229
 
3221
- $._matrix[0] *= sx;
3222
- $._matrix[5] *= sy;
3223
- $._matrix[10] *= sz;
3230
+ let m = $._matrix;
3231
+
3232
+ m[0] *= x;
3233
+ m[1] *= x;
3234
+ m[2] *= x;
3235
+ m[3] *= x;
3236
+ m[4] *= y;
3237
+ m[5] *= y;
3238
+ m[6] *= y;
3239
+ m[7] *= y;
3240
+ m[8] *= z;
3241
+ m[9] *= z;
3242
+ m[10] *= z;
3243
+ m[11] *= z;
3224
3244
 
3225
3245
  $._matrixDirty = true;
3226
3246
  };
@@ -3292,7 +3312,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3292
3312
  if (!$._transformIndexStack.length) {
3293
3313
  return console.warn('Matrix index stack is empty!');
3294
3314
  }
3295
- // Pop the last matrix index from the stack and set it as the current matrix index
3315
+ // Pop the last matrix index and set it as the current matrix index
3296
3316
  let idx = $._transformIndexStack.pop();
3297
3317
  $._matrix = $.transformStates[idx].slice();
3298
3318
  $._transformIndex = idx;
@@ -3315,7 +3335,6 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3315
3335
  // left, right, top, bottom
3316
3336
  let l, r, t, b;
3317
3337
  if (!mode || mode == 'corner') {
3318
- // CORNER
3319
3338
  l = x;
3320
3339
  r = x + w;
3321
3340
  t = -y;
@@ -3355,7 +3374,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3355
3374
 
3356
3375
  $._render = () => {
3357
3376
  if (transformStates.length > 1 || !$._transformBindGroup) {
3358
- const transformBuffer = Q5.device.createBuffer({
3377
+ let transformBuffer = Q5.device.createBuffer({
3359
3378
  size: transformStates.length * 64, // Size of 16 floats
3360
3379
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
3361
3380
  });
@@ -3367,6 +3386,12 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3367
3386
  entries: [
3368
3387
  {
3369
3388
  binding: 0,
3389
+ resource: {
3390
+ buffer: uniformBuffer
3391
+ }
3392
+ },
3393
+ {
3394
+ binding: 1,
3370
3395
  resource: {
3371
3396
  buffer: transformBuffer
3372
3397
  }
@@ -3375,35 +3400,47 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3375
3400
  });
3376
3401
  }
3377
3402
 
3378
- pass.setBindGroup(0, $._envBindGroup);
3379
- pass.setBindGroup(1, $._transformBindGroup);
3403
+ pass.setBindGroup(0, $._transformBindGroup);
3380
3404
 
3381
3405
  for (let m of $._hooks.preRender) m();
3382
3406
 
3383
3407
  let drawVertOffset = 0;
3384
3408
  let imageVertOffset = 0;
3409
+ let textCharOffset = 0;
3385
3410
  let curPipelineIndex = -1;
3386
3411
  let curTextureIndex = -1;
3387
3412
 
3388
- pass.setPipeline($.pipelines[0]);
3389
-
3390
3413
  for (let i = 0; i < drawStack.length; i += 2) {
3391
3414
  let v = drawStack[i + 1];
3392
3415
 
3416
+ if (drawStack[i] == -1) {
3417
+ v();
3418
+ continue;
3419
+ }
3420
+
3393
3421
  if (curPipelineIndex != drawStack[i]) {
3394
3422
  curPipelineIndex = drawStack[i];
3395
3423
  pass.setPipeline($.pipelines[curPipelineIndex]);
3396
3424
  }
3397
3425
 
3398
3426
  if (curPipelineIndex == 0) {
3399
- pass.draw(v, 1, drawVertOffset, 0);
3427
+ // v is the number of vertices
3428
+ pass.draw(v, 1, drawVertOffset);
3400
3429
  drawVertOffset += v;
3401
3430
  } else if (curPipelineIndex == 1) {
3402
3431
  if (curTextureIndex != v) {
3403
- pass.setBindGroup(3, $._textureBindGroups[v]);
3432
+ // v is the texture index
3433
+ pass.setBindGroup(2, $._textureBindGroups[v]);
3404
3434
  }
3405
- pass.draw(6, 1, imageVertOffset, 0);
3435
+ pass.draw(6, 1, imageVertOffset);
3406
3436
  imageVertOffset += 6;
3437
+ } else if (curPipelineIndex == 2) {
3438
+ pass.setBindGroup(2, $._font.bindGroup);
3439
+ pass.setBindGroup(3, $._textBindGroup);
3440
+
3441
+ // v is the number of characters in the text
3442
+ pass.draw(4, v, 0, textCharOffset);
3443
+ textCharOffset += v;
3407
3444
  }
3408
3445
  }
3409
3446
 
@@ -3412,7 +3449,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3412
3449
 
3413
3450
  $._finishRender = () => {
3414
3451
  pass.end();
3415
- const commandBuffer = $.encoder.finish();
3452
+ let commandBuffer = $.encoder.finish();
3416
3453
  Q5.device.queue.submit([commandBuffer]);
3417
3454
  q.pass = $.encoder = null;
3418
3455
 
@@ -3455,8 +3492,8 @@ Q5.renderers.webgpu.drawing = ($, q) => {
3455
3492
  label: 'drawingVertexShader',
3456
3493
  code: `
3457
3494
  struct VertexOutput {
3458
- @builtin(position) position: vec4<f32>,
3459
- @location(1) colorIndex: f32
3495
+ @builtin(position) position: vec4f,
3496
+ @location(0) colorIndex: f32
3460
3497
  };
3461
3498
 
3462
3499
  struct Uniforms {
@@ -3465,12 +3502,12 @@ struct Uniforms {
3465
3502
  };
3466
3503
 
3467
3504
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3468
- @group(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
3505
+ @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
3469
3506
 
3470
3507
  @vertex
3471
- fn vertexMain(@location(0) pos: vec2<f32>, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput {
3472
- var vert = vec4<f32>(pos, 0.0, 1.0);
3473
- vert *= transforms[i32(transformIndex)];
3508
+ fn vertexMain(@location(0) pos: vec2f, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput {
3509
+ var vert = vec4f(pos, 0.0, 1.0);
3510
+ vert = transforms[i32(transformIndex)] * vert;
3474
3511
  vert.x /= uniforms.halfWidth;
3475
3512
  vert.y /= uniforms.halfHeight;
3476
3513
 
@@ -3485,17 +3522,18 @@ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) colorIndex: f32, @locati
3485
3522
  let fragmentShader = Q5.device.createShaderModule({
3486
3523
  label: 'drawingFragmentShader',
3487
3524
  code: `
3488
- @group(2) @binding(0) var<storage, read> uColors : array<vec4<f32>>;
3525
+ @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
3489
3526
 
3490
3527
  @fragment
3491
- fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3492
- let index = u32(colorIndex);
3493
- return mix(uColors[index], uColors[index + 1u], fract(colorIndex));
3528
+ fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {
3529
+ let index = i32(colorIndex);
3530
+ return mix(colors[index], colors[index + 1], fract(colorIndex));
3494
3531
  }
3495
3532
  `
3496
3533
  });
3497
3534
 
3498
3535
  colorsLayout = Q5.device.createBindGroupLayout({
3536
+ label: 'colorsLayout',
3499
3537
  entries: [
3500
3538
  {
3501
3539
  binding: 0,
@@ -3726,10 +3764,17 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3726
3764
  $.background = (r, g, b, a) => {
3727
3765
  $.push();
3728
3766
  $.resetMatrix();
3729
- if (r.src) $.image(r, -c.hw, -c.hh, c.w, c.h);
3730
- else {
3767
+ if (r.src) {
3768
+ let og = $._imageMode;
3769
+ $._imageMode = 'corner';
3770
+ $.image(r, -c.hw, -c.hh, c.w, c.h);
3771
+ $._imageMode = og;
3772
+ } else {
3773
+ let og = $._rectMode;
3774
+ $._rectMode = 'corner';
3731
3775
  $.fill(r, g, b, a);
3732
3776
  $.rect(-c.hw, -c.hh, c.w, c.h);
3777
+ $._rectMode = og;
3733
3778
  }
3734
3779
  $.pop();
3735
3780
  };
@@ -3837,7 +3882,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3837
3882
  });
3838
3883
 
3839
3884
  // set the bind group once before rendering
3840
- $.pass.setBindGroup(2, $._colorsBindGroup);
3885
+ $.pass.setBindGroup(1, $._colorsBindGroup);
3841
3886
  });
3842
3887
 
3843
3888
  $._hooks.postRender.push(() => {
@@ -3852,8 +3897,8 @@ Q5.renderers.webgpu.image = ($, q) => {
3852
3897
  label: 'imageVertexShader',
3853
3898
  code: `
3854
3899
  struct VertexOutput {
3855
- @builtin(position) position: vec4<f32>,
3856
- @location(0) texCoord: vec2<f32>
3900
+ @builtin(position) position: vec4f,
3901
+ @location(0) texCoord: vec2f
3857
3902
  };
3858
3903
 
3859
3904
  struct Uniforms {
@@ -3862,12 +3907,12 @@ struct Uniforms {
3862
3907
  };
3863
3908
 
3864
3909
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3865
- @group(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
3910
+ @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
3866
3911
 
3867
3912
  @vertex
3868
- fn vertexMain(@location(0) pos: vec2<f32>, @location(1) texCoord: vec2<f32>, @location(2) transformIndex: f32) -> VertexOutput {
3869
- var vert = vec4<f32>(pos, 0.0, 1.0);
3870
- vert *= transforms[i32(transformIndex)];
3913
+ fn vertexMain(@location(0) pos: vec2f, @location(1) texCoord: vec2f, @location(2) transformIndex: f32) -> VertexOutput {
3914
+ var vert = vec4f(pos, 0.0, 1.0);
3915
+ vert = transforms[i32(transformIndex)] * vert;
3871
3916
  vert.x /= uniforms.halfWidth;
3872
3917
  vert.y /= uniforms.halfHeight;
3873
3918
 
@@ -3882,11 +3927,11 @@ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) texCoord: vec2<f32>, @lo
3882
3927
  let fragmentShader = Q5.device.createShaderModule({
3883
3928
  label: 'imageFragmentShader',
3884
3929
  code: `
3885
- @group(3) @binding(0) var samp: sampler;
3886
- @group(3) @binding(1) var texture: texture_2d<f32>;
3930
+ @group(2) @binding(0) var samp: sampler;
3931
+ @group(2) @binding(1) var texture: texture_2d<f32>;
3887
3932
 
3888
3933
  @fragment
3889
- fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
3934
+ fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {
3890
3935
  // Sample the texture using the interpolated texture coordinate
3891
3936
  return textureSample(texture, samp, texCoord);
3892
3937
  }
@@ -3918,11 +3963,9 @@ fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
3918
3963
  ]
3919
3964
  };
3920
3965
 
3921
- $.bindGroupLayouts.push(textureLayout);
3922
-
3923
3966
  const pipelineLayout = Q5.device.createPipelineLayout({
3924
3967
  label: 'imagePipelineLayout',
3925
- bindGroupLayouts: $.bindGroupLayouts
3968
+ bindGroupLayouts: [...$.bindGroupLayouts, textureLayout]
3926
3969
  });
3927
3970
 
3928
3971
  $.pipelines[1] = Q5.device.createRenderPipeline({
@@ -4079,34 +4122,522 @@ Q5.DILATE = 6;
4079
4122
  Q5.ERODE = 7;
4080
4123
  Q5.BLUR = 8;
4081
4124
  Q5.renderers.webgpu.text = ($, q) => {
4082
- let t = $.createGraphics(1, 1);
4083
- t.pixelDensity($._pixelDensity);
4084
- t._imageMode = 'corner';
4125
+ let textShader = Q5.device.createShaderModule({
4126
+ label: 'MSDF text shader',
4127
+ code: `
4128
+ // Positions for simple quad geometry
4129
+ const pos = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0));
4130
+
4131
+ struct VertexInput {
4132
+ @builtin(vertex_index) vertex : u32,
4133
+ @builtin(instance_index) instance : u32,
4134
+ };
4135
+ struct VertexOutput {
4136
+ @builtin(position) position : vec4f,
4137
+ @location(0) texcoord : vec2f,
4138
+ @location(1) colorIndex : f32
4139
+ };
4140
+ struct Char {
4141
+ texOffset: vec2f,
4142
+ texExtent: vec2f,
4143
+ size: vec2f,
4144
+ offset: vec2f,
4145
+ };
4146
+ struct Text {
4147
+ pos: vec2f,
4148
+ scale: f32,
4149
+ transformIndex: f32,
4150
+ fillIndex: f32,
4151
+ strokeIndex: f32
4152
+ };
4153
+ struct Uniforms {
4154
+ halfWidth: f32,
4155
+ halfHeight: f32
4156
+ };
4157
+
4158
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
4159
+ @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
4160
+
4161
+ @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
4162
+
4163
+ @group(2) @binding(0) var fontTexture: texture_2d<f32>;
4164
+ @group(2) @binding(1) var fontSampler: sampler;
4165
+ @group(2) @binding(2) var<storage> fontChars: array<Char>;
4166
+
4167
+ @group(3) @binding(0) var<storage> textChars: array<vec4f>;
4168
+ @group(3) @binding(1) var<storage> textMetadata: array<Text>;
4169
+
4170
+ @vertex
4171
+ fn vertexMain(input : VertexInput) -> VertexOutput {
4172
+ let char = textChars[input.instance];
4173
+
4174
+ let text = textMetadata[i32(char.w)];
4175
+
4176
+ let fontChar = fontChars[i32(char.z)];
4177
+
4178
+ let charPos = ((pos[input.vertex] * fontChar.size + char.xy + fontChar.offset) * text.scale) + text.pos;
4179
+
4180
+ var vert = vec4f(charPos, 0.0, 1.0);
4181
+ vert = transforms[i32(text.transformIndex)] * vert;
4182
+ vert.x /= uniforms.halfWidth;
4183
+ vert.y /= uniforms.halfHeight;
4184
+
4185
+ var output : VertexOutput;
4186
+ output.position = vert;
4187
+ output.texcoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;
4188
+ output.colorIndex = text.fillIndex;
4189
+ return output;
4190
+ }
4191
+
4192
+ fn sampleMsdf(texcoord: vec2f) -> f32 {
4193
+ let c = textureSample(fontTexture, fontSampler, texcoord);
4194
+ return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
4195
+ }
4196
+
4197
+ @fragment
4198
+ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4199
+ // pxRange (AKA distanceRange) comes from the msdfgen tool,
4200
+ // uses the default which is 4.
4201
+ let pxRange = 4.0;
4202
+ let sz = vec2f(textureDimensions(fontTexture, 0));
4203
+ let dx = sz.x*length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));
4204
+ let dy = sz.y*length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));
4205
+ let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
4206
+ let sigDist = sampleMsdf(input.texcoord) - 0.5;
4207
+ let pxDist = sigDist * toPixels;
4208
+ let edgeWidth = 0.5;
4209
+ let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
4210
+ if (alpha < 0.001) {
4211
+ discard;
4212
+ }
4213
+ let fillColor = colors[i32(input.colorIndex)];
4214
+ return vec4f(fillColor.rgb, fillColor.a * alpha);
4215
+ }
4216
+ `
4217
+ });
4218
+
4219
+ class MsdfFont {
4220
+ constructor(pipeline, bindGroup, lineHeight, chars, kernings) {
4221
+ this.pipeline = pipeline;
4222
+ this.bindGroup = bindGroup;
4223
+ this.lineHeight = lineHeight;
4224
+ this.chars = chars;
4225
+ this.kernings = kernings;
4226
+ let charArray = Object.values(chars);
4227
+ this.charCount = charArray.length;
4228
+ this.defaultChar = charArray[0];
4229
+ }
4230
+ getChar(charCode) {
4231
+ return this.chars[charCode] ?? this.defaultChar;
4232
+ }
4233
+ // Gets the distance in pixels a line should advance for a given character code. If the upcoming
4234
+ // character code is given any kerning between the two characters will be taken into account.
4235
+ getXAdvance(charCode, nextCharCode = -1) {
4236
+ let char = this.getChar(charCode);
4237
+ if (nextCharCode >= 0) {
4238
+ let kerning = this.kernings.get(charCode);
4239
+ if (kerning) {
4240
+ return char.xadvance + (kerning.get(nextCharCode) ?? 0);
4241
+ }
4242
+ }
4243
+ return char.xadvance;
4244
+ }
4245
+ }
4085
4246
 
4086
- $.loadFont = (f) => {
4247
+ let textBindGroupLayout = Q5.device.createBindGroupLayout({
4248
+ label: 'MSDF text group layout',
4249
+ entries: [
4250
+ {
4251
+ binding: 0,
4252
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4253
+ buffer: { type: 'read-only-storage' }
4254
+ },
4255
+ {
4256
+ binding: 1,
4257
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4258
+ buffer: { type: 'read-only-storage' }
4259
+ }
4260
+ ]
4261
+ });
4262
+
4263
+ let fonts = {};
4264
+
4265
+ let createFont = async (fontJsonUrl, fontName, cb) => {
4087
4266
  q._preloadCount++;
4088
- return t.loadFont(f, () => {
4267
+
4268
+ let res = await fetch(fontJsonUrl);
4269
+ if (res.status == 404) {
4089
4270
  q._preloadCount--;
4271
+ return '';
4272
+ }
4273
+ let atlas = await res.json();
4274
+
4275
+ let slashIdx = fontJsonUrl.lastIndexOf('/');
4276
+ let baseUrl = slashIdx != -1 ? fontJsonUrl.substring(0, slashIdx + 1) : '';
4277
+ // load font image
4278
+ res = await fetch(baseUrl + atlas.pages[0]);
4279
+ let img = await createImageBitmap(await res.blob());
4280
+
4281
+ // convert image to texture
4282
+ let imgSize = [img.width, img.height, 1];
4283
+ let texture = Q5.device.createTexture({
4284
+ label: `MSDF ${fontName}`,
4285
+ size: imgSize,
4286
+ format: 'rgba8unorm',
4287
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
4090
4288
  });
4289
+ Q5.device.queue.copyExternalImageToTexture({ source: img }, { texture }, imgSize);
4290
+
4291
+ // to make q5's default font file smaller,
4292
+ // the chars and kernings are stored as csv strings
4293
+ if (typeof atlas.chars == 'string') {
4294
+ atlas.chars = $.CSV.parse(atlas.chars, ' ');
4295
+ atlas.kernings = $.CSV.parse(atlas.kernings, ' ');
4296
+ }
4297
+
4298
+ let charCount = atlas.chars.length;
4299
+ let charsBuffer = Q5.device.createBuffer({
4300
+ size: charCount * 32,
4301
+ usage: GPUBufferUsage.STORAGE,
4302
+ mappedAtCreation: true
4303
+ });
4304
+
4305
+ let fontChars = new Float32Array(charsBuffer.getMappedRange());
4306
+ let u = 1 / atlas.common.scaleW;
4307
+ let v = 1 / atlas.common.scaleH;
4308
+ let chars = {};
4309
+ let o = 0; // offset
4310
+ for (let [i, char] of atlas.chars.entries()) {
4311
+ chars[char.id] = char;
4312
+ chars[char.id].charIndex = i;
4313
+ fontChars[o] = char.x * u; // texOffset.x
4314
+ fontChars[o + 1] = char.y * v; // texOffset.y
4315
+ fontChars[o + 2] = char.width * u; // texExtent.x
4316
+ fontChars[o + 3] = char.height * v; // texExtent.y
4317
+ fontChars[o + 4] = char.width; // size.x
4318
+ fontChars[o + 5] = char.height; // size.y
4319
+ fontChars[o + 6] = char.xoffset; // offset.x
4320
+ fontChars[o + 7] = -char.yoffset; // offset.y
4321
+ o += 8;
4322
+ }
4323
+ charsBuffer.unmap();
4324
+
4325
+ let fontSampler = Q5.device.createSampler({
4326
+ minFilter: 'linear',
4327
+ magFilter: 'linear',
4328
+ mipmapFilter: 'linear',
4329
+ maxAnisotropy: 16
4330
+ });
4331
+ let fontBindGroupLayout = Q5.device.createBindGroupLayout({
4332
+ label: 'MSDF font group layout',
4333
+ entries: [
4334
+ {
4335
+ binding: 0,
4336
+ visibility: GPUShaderStage.FRAGMENT,
4337
+ texture: {}
4338
+ },
4339
+ {
4340
+ binding: 1,
4341
+ visibility: GPUShaderStage.FRAGMENT,
4342
+ sampler: {}
4343
+ },
4344
+ {
4345
+ binding: 2,
4346
+ visibility: GPUShaderStage.VERTEX,
4347
+ buffer: { type: 'read-only-storage' }
4348
+ }
4349
+ ]
4350
+ });
4351
+ let fontPipeline = Q5.device.createRenderPipeline({
4352
+ label: 'msdf font pipeline',
4353
+ layout: Q5.device.createPipelineLayout({
4354
+ bindGroupLayouts: [...$.bindGroupLayouts, fontBindGroupLayout, textBindGroupLayout]
4355
+ }),
4356
+ vertex: {
4357
+ module: textShader,
4358
+ entryPoint: 'vertexMain'
4359
+ },
4360
+ fragment: {
4361
+ module: textShader,
4362
+ entryPoint: 'fragmentMain',
4363
+ targets: [
4364
+ {
4365
+ format: 'bgra8unorm',
4366
+ blend: {
4367
+ color: {
4368
+ srcFactor: 'src-alpha',
4369
+ dstFactor: 'one-minus-src-alpha'
4370
+ },
4371
+ alpha: {
4372
+ srcFactor: 'one',
4373
+ dstFactor: 'one'
4374
+ }
4375
+ }
4376
+ }
4377
+ ]
4378
+ },
4379
+ primitive: {
4380
+ topology: 'triangle-strip',
4381
+ stripIndexFormat: 'uint32'
4382
+ }
4383
+ });
4384
+
4385
+ let fontBindGroup = Q5.device.createBindGroup({
4386
+ label: 'msdf font bind group',
4387
+ layout: fontBindGroupLayout,
4388
+ entries: [
4389
+ {
4390
+ binding: 0,
4391
+ resource: texture.createView()
4392
+ },
4393
+ { binding: 1, resource: fontSampler },
4394
+ { binding: 2, resource: { buffer: charsBuffer } }
4395
+ ]
4396
+ });
4397
+
4398
+ let kernings = new Map();
4399
+ if (atlas.kernings) {
4400
+ for (let kerning of atlas.kernings) {
4401
+ let charKerning = kernings.get(kerning.first);
4402
+ if (!charKerning) {
4403
+ charKerning = new Map();
4404
+ kernings.set(kerning.first, charKerning);
4405
+ }
4406
+ charKerning.set(kerning.second, kerning.amount);
4407
+ }
4408
+ }
4409
+
4410
+ $._font = new MsdfFont(fontPipeline, fontBindGroup, atlas.common.lineHeight, chars, kernings);
4411
+
4412
+ fonts[fontName] = $._font;
4413
+ $.pipelines[2] = $._font.pipeline;
4414
+
4415
+ q._preloadCount--;
4416
+
4417
+ if (cb) cb(fontName);
4091
4418
  };
4092
4419
 
4093
- // directly add these text setting functions to the webgpu renderer
4094
- $.textFont = t.textFont;
4095
- $.textSize = t.textSize;
4096
- $.textLeading = t.textLeading;
4097
- $.textStyle = t.textStyle;
4098
- $.textAlign = t.textAlign;
4099
- $.textWidth = t.textWidth;
4100
- $.textAscent = t.textAscent;
4101
- $.textDescent = t.textDescent;
4420
+ // q2d graphics context to use for text image creation
4421
+ let g = $.createGraphics(1, 1);
4422
+ g.colorMode($.RGB, 1);
4102
4423
 
4103
- $.textFill = (r, g, b, a) => t.fill($.color(r, g, b, a));
4104
- $.textStroke = (r, g, b, a) => t.stroke($.color(r, g, b, a));
4424
+ $.loadFont = (url, cb) => {
4425
+ let ext = url.slice(url.lastIndexOf('.') + 1);
4426
+ if (ext != 'json') return g.loadFont(url, cb);
4427
+ let fontName = url.slice(url.lastIndexOf('/') + 1, url.lastIndexOf('-'));
4428
+ createFont(url, fontName, cb);
4429
+ return fontName;
4430
+ };
4431
+
4432
+ $._textSize = 18;
4433
+ $._textAlign = 'left';
4434
+ $._textBaseline = 'alphabetic';
4435
+ let leadingSet = false,
4436
+ leading = 22.5,
4437
+ leadDiff = 4.5,
4438
+ leadPercent = 1.25;
4439
+
4440
+ $.textFont = (fontName) => {
4441
+ $._font = fonts[fontName];
4442
+
4443
+ // replay the change of font in the draw stack
4444
+ $.drawStack.push(-1, () => {
4445
+ $._font = fonts[fontName];
4446
+ $.pipelines[2] = $._font.pipeline;
4447
+ });
4448
+ };
4449
+ $.textSize = (size) => {
4450
+ $._textSize = size;
4451
+ if (!leadingSet) {
4452
+ leading = size * leadPercent;
4453
+ leadDiff = leading - size;
4454
+ }
4455
+ };
4456
+ $.textLeading = (lineHeight) => {
4457
+ $._font.lineHeight = leading = lineHeight;
4458
+ leadDiff = leading - $._textSize;
4459
+ leadPercent = leading / $._textSize;
4460
+ leadingSet = true;
4461
+ };
4462
+ $.textAlign = (horiz, vert) => {
4463
+ $._textAlign = horiz;
4464
+ if (vert) $._textBaseline = vert;
4465
+ };
4466
+
4467
+ $._charStack = [];
4468
+ $._textStack = [];
4469
+
4470
+ let measureText = (font, text, charCallback) => {
4471
+ let maxWidth = 0,
4472
+ offsetX = 0,
4473
+ offsetY = 0,
4474
+ line = 0,
4475
+ printedCharCount = 0,
4476
+ lineWidths = [],
4477
+ nextCharCode = text.charCodeAt(0);
4478
+
4479
+ for (let i = 0; i < text.length; ++i) {
4480
+ let charCode = nextCharCode;
4481
+ nextCharCode = i < text.length - 1 ? text.charCodeAt(i + 1) : -1;
4482
+ switch (charCode) {
4483
+ case 10: // Newline
4484
+ lineWidths.push(offsetX);
4485
+ line++;
4486
+ maxWidth = Math.max(maxWidth, offsetX);
4487
+ offsetX = 0;
4488
+ offsetY -= font.lineHeight * leadPercent;
4489
+ break;
4490
+ case 13: // CR
4491
+ break;
4492
+ case 32: // Space
4493
+ // advance the offset without actually adding a character
4494
+ offsetX += font.getXAdvance(charCode);
4495
+ break;
4496
+ case 9: // Tab
4497
+ offsetX += font.getXAdvance(charCode) * 2;
4498
+ break;
4499
+ default:
4500
+ if (charCallback) {
4501
+ charCallback(offsetX, offsetY, line, font.getChar(charCode));
4502
+ }
4503
+ offsetX += font.getXAdvance(charCode, nextCharCode);
4504
+ printedCharCount++;
4505
+ }
4506
+ }
4507
+ lineWidths.push(offsetX);
4508
+ maxWidth = Math.max(maxWidth, offsetX);
4509
+ return {
4510
+ width: maxWidth,
4511
+ height: lineWidths.length * font.lineHeight * leadPercent,
4512
+ lineWidths,
4513
+ printedCharCount
4514
+ };
4515
+ };
4516
+
4517
+ let initLoadDefaultFont;
4105
4518
 
4106
4519
  $.text = (str, x, y, w, h) => {
4107
- let img = t.createTextImage(str, w, h);
4520
+ if (!$._font) {
4521
+ // check if online and loading the default font hasn't been attempted yet
4522
+ if (navigator.onLine && !initLoadDefaultFont) {
4523
+ initLoadDefaultFont = true;
4524
+ $.loadFont('https://q5js.org/fonts/YaHei-msdf.json');
4525
+ }
4526
+ return;
4527
+ }
4108
4528
 
4109
- if (img.canvas.textureIndex === undefined) {
4529
+ if (str.length > w) {
4530
+ let wrapped = [];
4531
+ let i = 0;
4532
+ while (i < str.length) {
4533
+ let max = i + w;
4534
+ if (max >= str.length) {
4535
+ wrapped.push(str.slice(i));
4536
+ break;
4537
+ }
4538
+ let end = str.lastIndexOf(' ', max);
4539
+ if (end == -1 || end < i) end = max;
4540
+ wrapped.push(str.slice(i, end));
4541
+ i = end + 1;
4542
+ }
4543
+ str = wrapped.join('\n');
4544
+ }
4545
+
4546
+ let spaces = 0, // whitespace char count, not literal spaces
4547
+ hasNewline;
4548
+ for (let i = 0; i < str.length; i++) {
4549
+ let c = str[i];
4550
+ switch (c) {
4551
+ case '\n':
4552
+ hasNewline = true;
4553
+ case '\r':
4554
+ case '\t':
4555
+ case ' ':
4556
+ spaces++;
4557
+ }
4558
+ }
4559
+
4560
+ let charsData = new Float32Array((str.length - spaces) * 4);
4561
+
4562
+ let ta = $._textAlign,
4563
+ tb = $._textBaseline,
4564
+ textIndex = $._textStack.length,
4565
+ o = 0, // offset
4566
+ measurements;
4567
+
4568
+ if (ta == 'left' && !hasNewline) {
4569
+ measurements = measureText($._font, str, (textX, textY, line, char) => {
4570
+ charsData[o] = textX;
4571
+ charsData[o + 1] = textY;
4572
+ charsData[o + 2] = char.charIndex;
4573
+ charsData[o + 3] = textIndex;
4574
+ o += 4;
4575
+ });
4576
+
4577
+ if (tb == 'alphabetic') y -= $._textSize;
4578
+ else if (tb == 'center') y -= $._textSize * 0.5;
4579
+ else if (tb == 'bottom') y -= leading;
4580
+ } else {
4581
+ // measure the text to get the line widths before setting
4582
+ // the x position to properly align the text
4583
+ measurements = measureText($._font, str);
4584
+
4585
+ let offsetY = 0;
4586
+ if (tb == 'alphabetic') y -= $._textSize;
4587
+ else if (tb == 'center') offsetY = measurements.height * 0.5;
4588
+ else if (tb == 'bottom') offsetY = measurements.height;
4589
+
4590
+ measureText($._font, str, (textX, textY, line, char) => {
4591
+ let offsetX = 0;
4592
+ if (ta == 'center') {
4593
+ offsetX = measurements.width * -0.5 - (measurements.width - measurements.lineWidths[line]) * -0.5;
4594
+ } else if (ta == 'right') {
4595
+ offsetX = measurements.width - measurements.lineWidths[line];
4596
+ }
4597
+ charsData[o] = textX + offsetX;
4598
+ charsData[o + 1] = textY + offsetY;
4599
+ charsData[o + 2] = char.charIndex;
4600
+ charsData[o + 3] = textIndex;
4601
+ o += 4;
4602
+ });
4603
+ }
4604
+ $._charStack.push(charsData);
4605
+
4606
+ let text = new Float32Array(6);
4607
+
4608
+ if ($._matrixDirty) $._saveMatrix();
4609
+
4610
+ text[0] = x;
4611
+ text[1] = -y;
4612
+ text[2] = $._textSize / 44;
4613
+ text[3] = $._transformIndex;
4614
+ text[4] = $._fillIndex;
4615
+ text[5] = $._strokeIndex;
4616
+
4617
+ $._textStack.push(text);
4618
+ $.drawStack.push(2, measurements.printedCharCount);
4619
+ };
4620
+
4621
+ $.textWidth = (str) => {
4622
+ if (!$._font) return 0;
4623
+ return measureText($._font, str).width;
4624
+ };
4625
+
4626
+ $.createTextImage = (str, w, h) => {
4627
+ g.textSize($._textSize);
4628
+
4629
+ if ($._doFill) {
4630
+ let fi = $._fillIndex * 4;
4631
+ g.fill(colorsStack.slice(fi, fi + 4));
4632
+ }
4633
+ if ($._doStroke) {
4634
+ let si = $._strokeIndex * 4;
4635
+ g.stroke(colorsStack.slice(si, si + 4));
4636
+ }
4637
+
4638
+ let img = g.createTextImage(str, w, h);
4639
+
4640
+ if (img.canvas.textureIndex == undefined) {
4110
4641
  $._createTexture(img);
4111
4642
  } else if (img.modified) {
4112
4643
  let cnv = img.canvas;
@@ -4120,27 +4651,94 @@ Q5.renderers.webgpu.text = ($, q) => {
4120
4651
  );
4121
4652
  img.modified = false;
4122
4653
  }
4123
-
4124
- $.textImage(img, x, y);
4654
+ return img;
4125
4655
  };
4126
4656
 
4127
- $.createTextImage = t.createTextImage;
4128
-
4129
4657
  $.textImage = (img, x, y) => {
4658
+ if (typeof img == 'string') img = $.createTextImage(img);
4659
+
4130
4660
  let og = $._imageMode;
4131
4661
  $._imageMode = 'corner';
4132
4662
 
4133
- let ta = t._textAlign;
4663
+ let ta = $._textAlign;
4134
4664
  if (ta == 'center') x -= img.canvas.hw;
4135
4665
  else if (ta == 'right') x -= img.width;
4136
4666
 
4137
- let bl = t._textBaseline;
4138
- if (bl == 'alphabetic') y -= t._textLeading;
4139
- else if (bl == 'middle') y -= img._middle;
4667
+ let bl = $._textBaseline;
4668
+ if (bl == 'alphabetic') y -= img._leading;
4669
+ else if (bl == 'center') y -= img._middle;
4140
4670
  else if (bl == 'bottom') y -= img._bottom;
4141
4671
  else if (bl == 'top') y -= img._top;
4142
4672
 
4143
4673
  $.image(img, x, y);
4144
4674
  $._imageMode = og;
4145
4675
  };
4676
+
4677
+ $._hooks.preRender.push(() => {
4678
+ if (!$._charStack.length) return;
4679
+
4680
+ // Calculate total buffer size for text data
4681
+ let totalTextSize = 0;
4682
+ for (let charsData of $._charStack) {
4683
+ totalTextSize += charsData.length * 4;
4684
+ }
4685
+
4686
+ // Create a single buffer for all text data
4687
+ let charBuffer = Q5.device.createBuffer({
4688
+ label: 'charBuffer',
4689
+ size: totalTextSize,
4690
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4691
+ mappedAtCreation: true
4692
+ });
4693
+
4694
+ // Copy all text data into the buffer
4695
+ let textArray = new Float32Array(charBuffer.getMappedRange());
4696
+ let o = 0;
4697
+ for (let array of $._charStack) {
4698
+ textArray.set(array, o);
4699
+ o += array.length;
4700
+ }
4701
+ charBuffer.unmap();
4702
+
4703
+ // Calculate total buffer size for metadata
4704
+ let totalMetadataSize = $._textStack.length * 6 * 4;
4705
+
4706
+ // Create a single buffer for all metadata
4707
+ let textBuffer = Q5.device.createBuffer({
4708
+ label: 'textBuffer',
4709
+ size: totalMetadataSize,
4710
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
4711
+ mappedAtCreation: true
4712
+ });
4713
+
4714
+ // Copy all metadata into the buffer
4715
+ let metadataArray = new Float32Array(textBuffer.getMappedRange());
4716
+ o = 0;
4717
+ for (let array of $._textStack) {
4718
+ metadataArray.set(array, o);
4719
+ o += array.length;
4720
+ }
4721
+ textBuffer.unmap();
4722
+
4723
+ // Create a single bind group for the text buffer and metadata buffer
4724
+ $._textBindGroup = Q5.device.createBindGroup({
4725
+ label: 'msdf text bind group',
4726
+ layout: textBindGroupLayout,
4727
+ entries: [
4728
+ {
4729
+ binding: 0,
4730
+ resource: { buffer: charBuffer }
4731
+ },
4732
+ {
4733
+ binding: 1,
4734
+ resource: { buffer: textBuffer }
4735
+ }
4736
+ ]
4737
+ });
4738
+ });
4739
+
4740
+ $._hooks.postRender.push(() => {
4741
+ $._charStack.length = 0;
4742
+ $._textStack.length = 0;
4743
+ });
4146
4744
  };