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/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;
@@ -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
- tSize = 12,
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 tSize;
1457
+ if (x === undefined) return $._textSize;
1462
1458
  if ($._da) x *= $._da;
1463
- tSize = x;
1459
+ $._textSize = x;
1464
1460
  fontMod = true;
1465
1461
  styleHash = -1;
1466
- if (!$._leadingSet) {
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 - tSize;
1481
- $._leadingSet = true;
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 + tSize + emphasis + leading;
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} ${tSize}px ${font}`;
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 -= leading;
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
- if (type == 'text') return r.text();
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
- $._envLayout = Q5.device.createBindGroupLayout({
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: 0,
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 = [$._envLayout, $._transformLayout];
3099
+ $.bindGroupLayouts = [$._transformLayout];
3094
3100
 
3095
- const uniformBuffer = Q5.device.createBuffer({
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
- const addColor = (r, g, b, a = 1) => {
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
- // Boolean to track if the matrix has been modified
3170
+ // tracks if the matrix has been modified
3175
3171
  $._matrixDirty = false;
3176
3172
 
3177
- // Array to store transformation matrices for the render pass
3173
+ // array to store transformation matrices for the render pass
3178
3174
  $.transformStates = [$._matrix.slice()];
3179
3175
 
3180
- // Stack to keep track of transformation matrix indexes
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[3] += x;
3187
- $._matrix[7] -= y;
3188
- $._matrix[11] += z || 0;
3182
+ $._matrix[12] += x;
3183
+ $._matrix[13] -= y;
3184
+ $._matrix[14] += z || 0;
3189
3185
  $._matrixDirty = true;
3190
3186
  };
3191
3187
 
3192
- $.rotate = (r) => {
3193
- if (!r) return;
3194
- if ($._angleMode) r *= $._DEGTORAD;
3188
+ $.rotate = (a) => {
3189
+ if (!a) return;
3190
+ if ($._angleMode) a *= $._DEGTORAD;
3195
3191
 
3196
- let cosR = Math.cos(r);
3197
- let sinR = Math.sin(r);
3192
+ let cosR = Math.cos(a);
3193
+ let sinR = Math.sin(a);
3194
+
3195
+ let m = $._matrix;
3196
+
3197
+ let m0 = m[0],
3198
+ m1 = m[1],
3199
+ m4 = m[4],
3200
+ m5 = m[5];
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
- $._matrix[0] = cosR;
3205
- $._matrix[1] = sinR;
3206
- $._matrix[4] = -sinR;
3207
- $._matrix[5] = cosR;
3203
+ m[0] = cosR;
3204
+ m[1] = sinR;
3205
+ m[4] = -sinR;
3206
+ m[5] = cosR;
3208
3207
  } 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;
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 = (sx = 1, sy, sz = 1) => {
3219
- sy ??= sx;
3217
+ $.scale = (x = 1, y, z = 1) => {
3218
+ y ??= x;
3220
3219
 
3221
- $._matrix[0] *= sx;
3222
- $._matrix[5] *= sy;
3223
- $._matrix[10] *= sz;
3220
+ let m = $._matrix;
3221
+
3222
+ m[0] *= x;
3223
+ m[1] *= x;
3224
+ m[2] *= x;
3225
+ m[3] *= x;
3226
+ m[4] *= y;
3227
+ m[5] *= y;
3228
+ m[6] *= y;
3229
+ m[7] *= y;
3230
+ m[8] *= z;
3231
+ m[9] *= z;
3232
+ m[10] *= z;
3233
+ m[11] *= z;
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 from the stack and set it as the current matrix index
3305
+ // Pop the last matrix index and set it as the current matrix index
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
- const transformBuffer = Q5.device.createBuffer({
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, $._envBindGroup);
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
- pass.draw(v, 1, drawVertOffset, 0);
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
- pass.setBindGroup(3, $._textureBindGroups[v]);
3422
+ // v is the texture index
3423
+ pass.setBindGroup(2, $._textureBindGroups[v]);
3404
3424
  }
3405
- pass.draw(6, 1, imageVertOffset, 0);
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
- const commandBuffer = $.encoder.finish();
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: vec4<f32>,
3459
- @location(1) colorIndex: f32
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(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
3495
+ @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
3469
3496
 
3470
3497
  @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)];
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(2) @binding(0) var<storage, read> uColors : array<vec4<f32>>;
3515
+ @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
3489
3516
 
3490
3517
  @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));
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) $.image(r, -c.hw, -c.hh, c.w, c.h);
3730
- else {
3757
+ if (r.src) {
3758
+ let og = $._imageMode;
3759
+ $._imageMode = 'corner';
3760
+ $.image(r, -c.hw, -c.hh, c.w, c.h);
3761
+ $._imageMode = og;
3762
+ } else {
3763
+ let og = $._rectMode;
3764
+ $._rectMode = 'corner';
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(2, $._colorsBindGroup);
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: vec4<f32>,
3856
- @location(0) texCoord: vec2<f32>
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(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
3900
+ @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
3866
3901
 
3867
3902
  @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)];
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(3) @binding(0) var samp: sampler;
3886
- @group(3) @binding(1) var texture: texture_2d<f32>;
3920
+ @group(2) @binding(0) var samp: sampler;
3921
+ @group(2) @binding(1) var texture: texture_2d<f32>;
3887
3922
 
3888
3923
  @fragment
3889
- fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
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: $.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 t = $.createGraphics(1, 1);
4083
- t.pixelDensity($._pixelDensity);
4084
- t._imageMode = 'corner';
4115
+ let textShader = Q5.device.createShaderModule({
4116
+ label: 'MSDF text shader',
4117
+ code: `
4118
+ // Positions for simple quad geometry
4119
+ const pos = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0));
4085
4120
 
4086
- $.loadFont = (f) => {
4121
+ struct VertexInput {
4122
+ @builtin(vertex_index) vertex : u32,
4123
+ @builtin(instance_index) instance : u32,
4124
+ };
4125
+ struct VertexOutput {
4126
+ @builtin(position) position : vec4f,
4127
+ @location(0) texcoord : vec2f,
4128
+ @location(1) colorIndex : f32
4129
+ };
4130
+ struct Char {
4131
+ texOffset: vec2f,
4132
+ texExtent: vec2f,
4133
+ size: vec2f,
4134
+ offset: vec2f,
4135
+ };
4136
+ struct Text {
4137
+ pos: vec2f,
4138
+ scale: f32,
4139
+ transformIndex: f32,
4140
+ fillIndex: f32,
4141
+ strokeIndex: f32
4142
+ };
4143
+ struct Uniforms {
4144
+ halfWidth: f32,
4145
+ halfHeight: f32
4146
+ };
4147
+
4148
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
4149
+ @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
4150
+
4151
+ @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
4152
+
4153
+ @group(2) @binding(0) var fontTexture: texture_2d<f32>;
4154
+ @group(2) @binding(1) var fontSampler: sampler;
4155
+ @group(2) @binding(2) var<storage> fontChars: array<Char>;
4156
+
4157
+ @group(3) @binding(0) var<storage> textChars: array<vec4f>;
4158
+ @group(3) @binding(1) var<storage> textMetadata: array<Text>;
4159
+
4160
+ @vertex
4161
+ fn vertexMain(input : VertexInput) -> VertexOutput {
4162
+ let char = textChars[input.instance];
4163
+
4164
+ let text = textMetadata[i32(char.w)];
4165
+
4166
+ let fontChar = fontChars[i32(char.z)];
4167
+
4168
+ let charPos = ((pos[input.vertex] * fontChar.size + char.xy + fontChar.offset) * text.scale) + text.pos;
4169
+
4170
+ var vert = vec4f(charPos, 0.0, 1.0);
4171
+ vert = transforms[i32(text.transformIndex)] * vert;
4172
+ vert.x /= uniforms.halfWidth;
4173
+ vert.y /= uniforms.halfHeight;
4174
+
4175
+ var output : VertexOutput;
4176
+ output.position = vert;
4177
+ output.texcoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;
4178
+ output.colorIndex = text.fillIndex;
4179
+ return output;
4180
+ }
4181
+
4182
+ fn sampleMsdf(texcoord: vec2f) -> f32 {
4183
+ let c = textureSample(fontTexture, fontSampler, texcoord);
4184
+ return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
4185
+ }
4186
+
4187
+ @fragment
4188
+ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
4189
+ // pxRange (AKA distanceRange) comes from the msdfgen tool,
4190
+ // uses the default which is 4.
4191
+ let pxRange = 4.0;
4192
+ let sz = vec2f(textureDimensions(fontTexture, 0));
4193
+ let dx = sz.x*length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));
4194
+ let dy = sz.y*length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));
4195
+ let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
4196
+ let sigDist = sampleMsdf(input.texcoord) - 0.5;
4197
+ let pxDist = sigDist * toPixels;
4198
+ let edgeWidth = 0.5;
4199
+ let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
4200
+ if (alpha < 0.001) {
4201
+ discard;
4202
+ }
4203
+ let fillColor = colors[i32(input.colorIndex)];
4204
+ return vec4f(fillColor.rgb, fillColor.a * alpha);
4205
+ }
4206
+ `
4207
+ });
4208
+
4209
+ class MsdfFont {
4210
+ constructor(pipeline, bindGroup, lineHeight, chars, kernings) {
4211
+ this.pipeline = pipeline;
4212
+ this.bindGroup = bindGroup;
4213
+ this.lineHeight = lineHeight;
4214
+ this.chars = chars;
4215
+ this.kernings = kernings;
4216
+ let charArray = Object.values(chars);
4217
+ this.charCount = charArray.length;
4218
+ this.defaultChar = charArray[0];
4219
+ }
4220
+ getChar(charCode) {
4221
+ return this.chars[charCode] ?? this.defaultChar;
4222
+ }
4223
+ // Gets the distance in pixels a line should advance for a given character code. If the upcoming
4224
+ // character code is given any kerning between the two characters will be taken into account.
4225
+ getXAdvance(charCode, nextCharCode = -1) {
4226
+ let char = this.getChar(charCode);
4227
+ if (nextCharCode >= 0) {
4228
+ let kerning = this.kernings.get(charCode);
4229
+ if (kerning) {
4230
+ return char.xadvance + (kerning.get(nextCharCode) ?? 0);
4231
+ }
4232
+ }
4233
+ return char.xadvance;
4234
+ }
4235
+ }
4236
+
4237
+ let textBindGroupLayout = Q5.device.createBindGroupLayout({
4238
+ label: 'MSDF text group layout',
4239
+ entries: [
4240
+ {
4241
+ binding: 0,
4242
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4243
+ buffer: { type: 'read-only-storage' }
4244
+ },
4245
+ {
4246
+ binding: 1,
4247
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
4248
+ buffer: { type: 'read-only-storage' }
4249
+ }
4250
+ ]
4251
+ });
4252
+
4253
+ let fonts = {};
4254
+
4255
+ let createFont = async (fontJsonUrl, fontName, cb) => {
4087
4256
  q._preloadCount++;
4088
- return t.loadFont(f, () => {
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
- // 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;
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
- $.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));
4507
+ let initLoadDefaultFont;
4105
4508
 
4106
4509
  $.text = (str, x, y, w, h) => {
4107
- let img = t.createTextImage(str, w, h);
4510
+ if (!$._font) {
4511
+ // check if online and loading the default font hasn't been attempted yet
4512
+ if (navigator.onLine && !initLoadDefaultFont) {
4513
+ initLoadDefaultFont = true;
4514
+ $.loadFont('https://q5js.org/fonts/YaHei-msdf.json');
4515
+ }
4516
+ return;
4517
+ }
4108
4518
 
4109
- if (img.canvas.textureIndex === undefined) {
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 = t._textAlign;
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 = t._textBaseline;
4138
- if (bl == 'alphabetic') y -= t._textLeading;
4139
- else if (bl == 'middle') y -= img._middle;
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
  };