q5 3.9.1 → 4.0.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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * q5.js
3
- * @version 3.9
3
+ * @version 4.0
4
4
  * @author quinton-ashley
5
5
  * @contributors evanalulu, Tezumie, ormaq, Dukemz, LingDong-
6
6
  * @license LGPL-3.0
@@ -131,14 +131,16 @@ function Q5(scope, parent, renderer) {
131
131
  $.resetMatrix();
132
132
 
133
133
  if ($._beginRender) $._beginRender();
134
- await runHooks('predraw');
134
+
135
135
  try {
136
+ await runHooks('predraw');
136
137
  await $.draw();
137
138
  } catch (e) {
138
139
  if (!Q5.errorTolerant) $.noLoop();
139
140
  if ($._fes) $._fes(e);
140
141
  throw e;
141
142
  }
143
+
142
144
  await runHooks('postdraw');
143
145
  await $.postProcess();
144
146
  if ($._render) $._render();
@@ -315,11 +317,10 @@ function Q5(scope, parent, renderer) {
315
317
  $.preload();
316
318
  }
317
319
 
318
- // wait for the user to define setup, update, or draw
319
320
  await Promise.race([
320
321
  new Promise((resolve) => {
321
322
  function checkUserFns() {
322
- if ($.setup || $.update || $.draw || t.setup || t.update || t.draw) {
323
+ if ($._disablePreload || $.setup || $.update || $.draw || t.setup || t.update || t.draw) {
323
324
  resolve();
324
325
  } else if (!$._setupDone) {
325
326
  // render during loading
@@ -336,7 +337,7 @@ function Q5(scope, parent, renderer) {
336
337
  new Promise((resolve) => {
337
338
  setTimeout(() => {
338
339
  // if not loading
339
- if (!$._loaders.length) resolve();
340
+ if (!$._loaders.length && !$._g?._loaders.length) resolve();
340
341
  }, 500);
341
342
  })
342
343
  ]);
@@ -468,7 +469,7 @@ if (typeof window == 'object') {
468
469
  window.WEBGPU = 'webgpu';
469
470
  } else global.window = 0;
470
471
 
471
- Q5.version = Q5.VERSION = '3.9';
472
+ Q5.version = Q5.VERSION = '4.0';
472
473
 
473
474
  if (typeof document == 'object') {
474
475
  document.addEventListener('DOMContentLoaded', () => {
@@ -637,7 +638,7 @@ Q5.modules.canvas = ($, q) => {
637
638
 
638
639
  let m = Q5._libMap;
639
640
 
640
- if (m.width) {
641
+ if (m?.width) {
641
642
  q[m.width] = w;
642
643
  q[m.height] = h;
643
644
  q[m.halfWidth] = q.halfWidth;
@@ -1602,7 +1603,11 @@ Q5.renderers.c2d.image = ($, q) => {
1602
1603
  $._retint = true;
1603
1604
 
1604
1605
  if ($._owner?._makeDrawable) {
1605
- $._texture.destroy();
1606
+ if ($._owner._texturesToDestroy && $._texture) {
1607
+ $._owner._texturesToDestroy.push($._texture);
1608
+ } else {
1609
+ $._texture.destroy();
1610
+ }
1606
1611
  delete $._texture;
1607
1612
  $._owner._makeDrawable($);
1608
1613
  }
@@ -1775,24 +1780,26 @@ Q5.Image = class {
1775
1780
  delete $.createCanvas;
1776
1781
  $._loop = false;
1777
1782
 
1778
- let libMap = Q5._libMap;
1779
- let imgFns = [
1780
- 'copy',
1781
- 'filter',
1782
- 'get',
1783
- 'set',
1784
- 'resize',
1785
- 'mask',
1786
- 'trim',
1787
- 'inset',
1788
- 'pixels',
1789
- 'loadPixels',
1790
- 'updatePixels',
1791
- 'smooth',
1792
- 'noSmooth'
1793
- ];
1794
- for (let name of imgFns) {
1795
- if (libMap[name]) $[libMap[name]] = $[name];
1783
+ let m = Q5._libMap;
1784
+ if (m) {
1785
+ let imgFns = [
1786
+ 'copy',
1787
+ 'filter',
1788
+ 'get',
1789
+ 'set',
1790
+ 'resize',
1791
+ 'mask',
1792
+ 'trim',
1793
+ 'inset',
1794
+ 'pixels',
1795
+ 'loadPixels',
1796
+ 'updatePixels',
1797
+ 'smooth',
1798
+ 'noSmooth'
1799
+ ];
1800
+ for (let name of imgFns) {
1801
+ if (m[name]) $[m[name]] = $[name];
1802
+ }
1796
1803
  }
1797
1804
  }
1798
1805
  get w() {
@@ -2100,7 +2107,7 @@ Q5.renderers.c2d.text = ($, q) => {
2100
2107
 
2101
2108
  $.textStyle = (x) => {
2102
2109
  if (!x) return emphasis;
2103
- emphasis = x;
2110
+ $._textStyle = emphasis = x;
2104
2111
  $._fontMod = true;
2105
2112
  styleHash = -1;
2106
2113
  };
@@ -2230,7 +2237,6 @@ Q5.renderers.c2d.text = ($, q) => {
2230
2237
  if ($._textBaseline == 'middle') tY -= leading * (lines.length - 1) * 0.5;
2231
2238
  else if ($._textBaseline == 'bottom') tY -= leading * (lines.length - 1);
2232
2239
  } else {
2233
- tX = 0;
2234
2240
  tY = leading;
2235
2241
 
2236
2242
  if (!img) {
@@ -2271,9 +2277,14 @@ Q5.renderers.c2d.text = ($, q) => {
2271
2277
 
2272
2278
  ctx = img.ctx;
2273
2279
 
2280
+ ctx.textAlign = $._textAlign;
2281
+ if ($._textAlign == 'center') tX = img.width / 2;
2282
+ else if ($._textAlign == 'right') tX = img.width;
2283
+ else tX = 0;
2284
+
2274
2285
  ctx.font = $.ctx.font;
2275
- ctx.fillStyle = $._fill;
2276
- ctx.strokeStyle = $._stroke;
2286
+ if ($._doFill && $._fillSet) ctx.fillStyle = $._fill;
2287
+ if ($._doStroke && $._strokeSet) ctx.strokeStyle = $._stroke;
2277
2288
  ctx.lineWidth = $.ctx.lineWidth;
2278
2289
  }
2279
2290
 
@@ -2317,7 +2328,11 @@ Q5.renderers.c2d.text = ($, q) => {
2317
2328
  colorCache = styleCache[hash];
2318
2329
  for (let c in colorCache) {
2319
2330
  let _img = colorCache[c];
2320
- if (_img._texture) _img._texture.destroy();
2331
+ if (_img._texture) {
2332
+ let owner = _img._owner || $;
2333
+ if (owner._texturesToDestroy) owner._texturesToDestroy.push(_img._texture);
2334
+ else _img._texture.destroy();
2335
+ }
2321
2336
  delete colorCache[c];
2322
2337
  }
2323
2338
  }
@@ -2347,6 +2362,79 @@ Q5.renderers.c2d.text = ($, q) => {
2347
2362
  $.image(img, x, y);
2348
2363
  $._imageMode = og;
2349
2364
  };
2365
+
2366
+ $.textToPoints = (str, x = 0, y = 0, sampleRate = 0.1, density = 1) => {
2367
+ let pd = $._pixelDensity;
2368
+ $._pixelDensity = density;
2369
+ let img = $.createTextImage(str);
2370
+ $._pixelDensity = pd;
2371
+
2372
+ img.loadPixels();
2373
+
2374
+ let w = img.canvas.width,
2375
+ h = img.canvas.height;
2376
+
2377
+ let points = [];
2378
+
2379
+ let ta = $._textAlign,
2380
+ bl = $._textBaseline,
2381
+ offsetX = 0,
2382
+ offsetY = 0;
2383
+
2384
+ if (ta == 'center') offsetX = -w / 2;
2385
+ else if (ta == 'right') offsetX = -w;
2386
+
2387
+ if (bl == 'alphabetic') offsetY = -img._leading;
2388
+ else if (bl == 'middle') offsetY = -img._middle;
2389
+ else if (bl == 'bottom') offsetY = -img._bottom;
2390
+ else if (bl == 'top') offsetY = -img._top;
2391
+
2392
+ offsetY *= density;
2393
+
2394
+ let allPoints = [];
2395
+
2396
+ // Z-order curve (Morton code)
2397
+ const part1by1 = (n) => {
2398
+ n &= 0x0000ffff;
2399
+ n = (n | (n << 8)) & 0x00ff00ff;
2400
+ n = (n | (n << 4)) & 0x0f0f0f0f;
2401
+ n = (n | (n << 2)) & 0x33333333;
2402
+ n = (n | (n << 1)) & 0x55555555;
2403
+ return n;
2404
+ };
2405
+
2406
+ let r = Math.max(0.5, sampleRate);
2407
+
2408
+ for (let py = 0; py < h; py++) {
2409
+ for (let px = 0; px < w; px++) {
2410
+ let index = (py * w + px) * 4;
2411
+
2412
+ if ((r == 1 || $.random() < r) && img.pixels[index + 3] > 128) {
2413
+ allPoints.push({
2414
+ x: px,
2415
+ y: py,
2416
+ z: part1by1(px) | (part1by1(py) << 1)
2417
+ });
2418
+ }
2419
+ }
2420
+ }
2421
+
2422
+ let total = allPoints.length;
2423
+ let numPoints = total * sampleRate * (1 / r);
2424
+
2425
+ if (sampleRate < 1) allPoints.sort((a, b) => a.z - b.z);
2426
+
2427
+ let step = total / numPoints;
2428
+ for (let i = 0; i < total; i += step) {
2429
+ let p = allPoints[Math.floor(i)];
2430
+ points.push({
2431
+ x: (p.x + offsetX) / density + x,
2432
+ y: (p.y + offsetY) / density + y
2433
+ });
2434
+ }
2435
+
2436
+ return points;
2437
+ };
2350
2438
  };
2351
2439
 
2352
2440
  Q5.fonts = [];
@@ -3316,8 +3404,8 @@ Q5.modules.dom = ($, q) => {
3316
3404
  return vid;
3317
3405
  };
3318
3406
 
3319
- $.findElement = (selector) => document.querySelector(selector);
3320
- $.findElements = (selector) => document.querySelectorAll(selector);
3407
+ $.findEl = (selector) => document.querySelector(selector);
3408
+ $.findEls = (selector) => document.querySelectorAll(selector);
3321
3409
  };
3322
3410
  Q5.modules.fes = ($) => {
3323
3411
  $._fes = async (e) => {
@@ -3334,7 +3422,7 @@ Q5.modules.fes = ($) => {
3334
3422
  idx = 0;
3335
3423
  sep = '@';
3336
3424
  }
3337
- while (stackLines[idx].indexOf('q5') >= 0) idx++;
3425
+ while (idx > stackLines.length && stackLines[idx].indexOf('q5') >= 0) idx++;
3338
3426
 
3339
3427
  let errFile = stackLines[idx].split(sep).at(-1);
3340
3428
  if (errFile.startsWith('blob:')) errFile = errFile.slice(5);
@@ -3469,6 +3557,7 @@ Q5.modules.input = ($, q) => {
3469
3557
  if ($._webgpu) {
3470
3558
  x -= c.hw;
3471
3559
  y -= c.hh;
3560
+ if (!$._flippedY) y *= -1;
3472
3561
  }
3473
3562
  } else {
3474
3563
  x = e.clientX;
@@ -3582,18 +3671,24 @@ Q5.modules.input = ($, q) => {
3582
3671
  $.keyIsDown = (v) => !!keysHeld[typeof v == 'string' ? v.toLowerCase() : v];
3583
3672
 
3584
3673
  function getTouchInfo(touch) {
3585
- const rect = $.canvas.getBoundingClientRect();
3586
- const sx = $.canvas.scrollWidth / $.width || 1;
3587
- const sy = $.canvas.scrollHeight / $.height || 1;
3674
+ const rect = $.canvas.getBoundingClientRect(),
3675
+ sx = $.canvas.scrollWidth / $.width || 1,
3676
+ sy = $.canvas.scrollHeight / $.height || 1;
3588
3677
  let modX = 0,
3589
3678
  modY = 0;
3590
3679
  if ($._webgpu) {
3591
3680
  modX = $.halfWidth;
3592
3681
  modY = $.halfHeight;
3593
3682
  }
3683
+
3684
+ let x = (touch.clientX - rect.left) / sx - modX,
3685
+ y = (touch.clientY - rect.top) / sy - modY;
3686
+
3687
+ if ($._webgpu && !$._flippedY) y *= -1;
3688
+
3594
3689
  return {
3595
- x: (touch.clientX - rect.left) / sx - modX,
3596
- y: (touch.clientY - rect.top) / sy - modY,
3690
+ x,
3691
+ y,
3597
3692
  id: touch.identifier
3598
3693
  };
3599
3694
  }
@@ -3781,6 +3876,7 @@ Q5.modules.math = ($, q) => {
3781
3876
  return {
3782
3877
  setSeed(val) {
3783
3878
  jsr = seed = (val == null ? Math.random() * m : val) >>> 0;
3879
+ if (jsr === 0) jsr = 1;
3784
3880
  },
3785
3881
  getSeed() {
3786
3882
  return seed;
@@ -5276,7 +5372,8 @@ struct Q5 {
5276
5372
  mouseY: f32,
5277
5373
  mouseIsPressed: f32,
5278
5374
  keyCode: f32,
5279
- keyIsPressed: f32
5375
+ keyIsPressed: f32,
5376
+ yUp: f32
5280
5377
  }`;
5281
5378
 
5282
5379
  $._g = $.createGraphics(1, 1, 'c2d');
@@ -5300,6 +5397,7 @@ struct Q5 {
5300
5397
  $._pipelineConfigs = [];
5301
5398
  $._pipelines = [];
5302
5399
  $._buffers = [];
5400
+ $._texturesToDestroy = [];
5303
5401
 
5304
5402
  // local variables used for slightly better performance
5305
5403
 
@@ -5398,7 +5496,8 @@ fn vertexMain(v: VertexParams) -> FragParams {
5398
5496
  @fragment
5399
5497
  fn fragMain(f: FragParams ) -> @location(0) vec4f {
5400
5498
  return textureSample(tex, samp, f.texCoord);
5401
- }`;
5499
+ }
5500
+ `;
5402
5501
 
5403
5502
  let frameShader = Q5.device.createShaderModule({
5404
5503
  label: 'frameShader',
@@ -5619,17 +5718,32 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5619
5718
  matrixIdx = 0,
5620
5719
  matrixDirty = false; // tracks if the matrix has been modified
5621
5720
 
5622
- // 4x4 identity matrix with y axis flipped
5721
+ // 4x4 identity matrix
5623
5722
  // prettier-ignore
5624
5723
  matrices.push([
5625
5724
  1, 0, 0, 0,
5626
- 0, -1, 0, 0, // -1 here flips the y axis
5725
+ 0, 1, 0, 0,
5627
5726
  0, 0, 1, 0,
5628
5727
  0, 0, 0, 1
5629
5728
  ]);
5630
5729
 
5631
5730
  transforms.set(matrices[0]);
5632
5731
 
5732
+ let flippedY = false,
5733
+ yDir = 1;
5734
+
5735
+ $.flipY = () => {
5736
+ $._flippedY = flippedY = !flippedY;
5737
+ yDir *= -1;
5738
+
5739
+ // edit the identity matrix to flip Y axis
5740
+ matrices[0][5] *= -1;
5741
+ transforms.set(matrices[0], 0);
5742
+ };
5743
+
5744
+ // current default is y-down for q5 WebGPU
5745
+ $.flipY();
5746
+
5633
5747
  $.translate = (x, y) => {
5634
5748
  if (!x && !y) return;
5635
5749
  let m = matrix;
@@ -5887,20 +6001,20 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5887
6001
  l = x;
5888
6002
  r = x + w;
5889
6003
  t = y;
5890
- b = y + h;
6004
+ b = y - h * yDir;
5891
6005
  } else if (mode == 'center') {
5892
6006
  let hw = w / 2,
5893
6007
  hh = h / 2;
5894
6008
  l = x - hw;
5895
6009
  r = x + hw;
5896
- t = y - hh;
5897
- b = y + hh;
6010
+ t = y + hh * yDir;
6011
+ b = y - hh * yDir;
5898
6012
  } else {
5899
6013
  // CORNERS
5900
6014
  l = x;
5901
6015
  r = w;
5902
6016
  t = y;
5903
- b = h;
6017
+ b = -h * yDir;
5904
6018
  }
5905
6019
 
5906
6020
  boxCache[0] = l;
@@ -5930,7 +6044,7 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5930
6044
  ];
5931
6045
 
5932
6046
  const blendModes = {
5933
- 'source-over': [2, 3, 0, 2, 3, 0],
6047
+ 'source-over': [2, 3, 0, 1, 3, 0],
5934
6048
  'destination-over': [6, 1, 0, 6, 1, 0],
5935
6049
  'source-in': [5, 0, 0, 5, 0, 0],
5936
6050
  'destination-in': [0, 2, 0, 0, 2, 0],
@@ -5939,8 +6053,8 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5939
6053
  'source-atop': [5, 3, 0, 5, 3, 0],
5940
6054
  'destination-atop': [6, 2, 0, 6, 2, 0],
5941
6055
  lighter: [1, 1, 0, 1, 1, 0],
5942
- darken: [1, 1, 3, 3, 5, 0],
5943
- lighten: [1, 1, 4, 3, 5, 0],
6056
+ darken: [1, 1, 3, 1, 3, 0],
6057
+ lighten: [1, 1, 4, 1, 3, 0],
5944
6058
  replace: [1, 0, 0, 1, 0, 0]
5945
6059
  };
5946
6060
 
@@ -6076,6 +6190,7 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
6076
6190
  $._uniforms[10] = $.mouseIsPressed ? 1 : 0;
6077
6191
  $._uniforms[11] = $.keyCode;
6078
6192
  $._uniforms[12] = $.keyIsPressed ? 1 : 0;
6193
+ $._uniforms[13] = yDir;
6079
6194
 
6080
6195
  Q5.device.queue.writeBuffer(uniformBuffer, 0, $._uniforms);
6081
6196
 
@@ -6329,9 +6444,14 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
6329
6444
  ellipseStackIdx = 0;
6330
6445
 
6331
6446
  // destroy buffers
6447
+ let bufs = $._buffers;
6448
+ let texs = $._texturesToDestroy;
6449
+ $._buffers = [];
6450
+ $._texturesToDestroy = [];
6451
+
6332
6452
  Q5.device.queue.onSubmittedWorkDone().then(() => {
6333
- for (let b of $._buffers) b.destroy();
6334
- $._buffers = [];
6453
+ for (let b of bufs) b.destroy();
6454
+ for (let t of texs) t.destroy();
6335
6455
  });
6336
6456
  };
6337
6457
 
@@ -6903,7 +7023,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6903
7023
  if (_rectMode != 'center') {
6904
7024
  if (_rectMode == 'corner') {
6905
7025
  x += hw;
6906
- y += hh;
7026
+ y += hh * -yDir;
6907
7027
  hw = Math.abs(hw);
6908
7028
  hh = Math.abs(hh);
6909
7029
  } else if (_rectMode == 'radius') {
@@ -6936,7 +7056,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6936
7056
 
6937
7057
  function addCapsule(x1, y1, x2, y2, r, strokeW, fillCapsule) {
6938
7058
  let dx = x2 - x1,
6939
- dy = y2 - y1,
7059
+ dy = flippedY ? y2 - y1 : y1 - y2,
6940
7060
  len = Math.hypot(dx, dy);
6941
7061
 
6942
7062
  if (len === 0) return;
@@ -7046,7 +7166,12 @@ fn vertexMain(v: VertexParams) -> FragParams {
7046
7166
  f.position = transformVertex(pos, ellipse.matrixIndex);
7047
7167
  f.outerEdge = dist / (ellipse.size + halfStrokeSize);
7048
7168
  f.fillEdge = dist / ellipse.size;
7049
- f.innerEdge = dist / (ellipse.size - halfStrokeSize);
7169
+ let innerSize = ellipse.size - halfStrokeSize;
7170
+ if (innerSize.x <= 0.0 || innerSize.y <= 0.0) {
7171
+ f.innerEdge = vec2f(2.0, 0.0);
7172
+ } else {
7173
+ f.innerEdge = dist / innerSize;
7174
+ }
7050
7175
  f.strokeWeight = ellipse.strokeWeight;
7051
7176
 
7052
7177
  let fill = colors[i32(ellipse.fillIndex)];
@@ -7105,7 +7230,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
7105
7230
  let strokeAlpha = innerAlpha * outerAlpha;
7106
7231
  return vec4f(f.stroke.rgb, f.stroke.a * strokeAlpha);
7107
7232
  }
7108
- `;
7233
+ `;
7109
7234
 
7110
7235
  let ellipseShader = Q5.device.createShaderModule({
7111
7236
  label: 'ellipseShader',
@@ -7285,61 +7410,61 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
7285
7410
  $._imageShaderCode =
7286
7411
  $._baseShaderCode +
7287
7412
  /* wgsl */ `
7288
- struct VertexParams {
7289
- @builtin(vertex_index) vertexIndex : u32,
7290
- @location(0) pos: vec2f,
7291
- @location(1) texCoord: vec2f,
7292
- @location(2) tintIndex: f32,
7293
- @location(3) matrixIndex: f32,
7294
- @location(4) imageAlpha: f32
7295
- }
7296
- struct FragParams {
7297
- @builtin(position) position: vec4f,
7298
- @location(0) texCoord: vec2f,
7299
- @location(1) tintColor: vec4f,
7300
- @location(2) imageAlpha: f32
7301
- }
7302
-
7303
- @group(0) @binding(0) var<uniform> q: Q5;
7304
- @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
7305
- @group(0) @binding(2) var<storage> colors : array<vec4f>;
7306
-
7307
- @group(1) @binding(0) var samp: sampler;
7308
- @group(1) @binding(1) var tex: texture_2d<f32>;
7309
-
7310
- fn transformVertex(pos: vec2f, matrixIndex: f32) -> vec4f {
7311
- var vert = vec4f(pos, 0f, 1f);
7312
- vert = transforms[i32(matrixIndex)] * vert;
7313
- vert.x /= q.halfWidth;
7314
- vert.y /= q.halfHeight;
7315
- return vert;
7316
- }
7317
-
7318
- fn applyTint(texColor: vec4f, tintColor: vec4f) -> vec4f {
7319
- // apply the tint color to the sampled texture color at full strength
7320
- let tinted = vec4f(texColor.rgb * tintColor.rgb, texColor.a);
7321
- // mix in the tint using the tint alpha as the blend strength
7322
- return mix(texColor, tinted, tintColor.a);
7323
- }
7324
-
7325
- @vertex
7326
- fn vertexMain(v: VertexParams) -> FragParams {
7327
- var vert = transformVertex(v.pos, v.matrixIndex);
7328
-
7329
- var f: FragParams;
7330
- f.position = vert;
7331
- f.texCoord = v.texCoord;
7332
- f.tintColor = colors[i32(v.tintIndex)];
7333
- f.imageAlpha = v.imageAlpha;
7334
- return f;
7335
- }
7336
-
7337
- @fragment
7338
- fn fragMain(f: FragParams) -> @location(0) vec4f {
7339
- var texColor = textureSample(tex, samp, f.texCoord);
7340
- texColor.a *= f.imageAlpha;
7341
- return applyTint(texColor, f.tintColor);
7342
- }
7413
+ struct VertexParams {
7414
+ @builtin(vertex_index) vertexIndex : u32,
7415
+ @location(0) pos: vec2f,
7416
+ @location(1) texCoord: vec2f,
7417
+ @location(2) tintIndex: f32,
7418
+ @location(3) matrixIndex: f32,
7419
+ @location(4) imageAlpha: f32
7420
+ }
7421
+ struct FragParams {
7422
+ @builtin(position) position: vec4f,
7423
+ @location(0) texCoord: vec2f,
7424
+ @location(1) tintColor: vec4f,
7425
+ @location(2) imageAlpha: f32
7426
+ }
7427
+
7428
+ @group(0) @binding(0) var<uniform> q: Q5;
7429
+ @group(0) @binding(1) var<storage> transforms: array<mat4x4<f32>>;
7430
+ @group(0) @binding(2) var<storage> colors : array<vec4f>;
7431
+
7432
+ @group(1) @binding(0) var samp: sampler;
7433
+ @group(1) @binding(1) var tex: texture_2d<f32>;
7434
+
7435
+ fn transformVertex(pos: vec2f, matrixIndex: f32) -> vec4f {
7436
+ var vert = vec4f(pos, 0f, 1f);
7437
+ vert = transforms[i32(matrixIndex)] * vert;
7438
+ vert.x /= q.halfWidth;
7439
+ vert.y /= q.halfHeight;
7440
+ return vert;
7441
+ }
7442
+
7443
+ fn applyTint(texColor: vec4f, tintColor: vec4f) -> vec4f {
7444
+ // apply the tint color to the sampled texture color at full strength
7445
+ let tinted = vec4f(texColor.rgb * tintColor.rgb, texColor.a);
7446
+ // mix in the tint using the tint alpha as the blend strength
7447
+ return mix(texColor, tinted, tintColor.a);
7448
+ }
7449
+
7450
+ @vertex
7451
+ fn vertexMain(v: VertexParams) -> FragParams {
7452
+ var vert = transformVertex(v.pos, v.matrixIndex);
7453
+
7454
+ var f: FragParams;
7455
+ f.position = vert;
7456
+ f.texCoord = v.texCoord;
7457
+ f.tintColor = colors[i32(v.tintIndex)];
7458
+ f.imageAlpha = v.imageAlpha;
7459
+ return f;
7460
+ }
7461
+
7462
+ @fragment
7463
+ fn fragMain(f: FragParams) -> @location(0) vec4f {
7464
+ var texColor = textureSample(tex, samp, f.texCoord);
7465
+ texColor.a *= f.imageAlpha;
7466
+ return applyTint(texColor, f.tintColor);
7467
+ }
7343
7468
  `;
7344
7469
 
7345
7470
  let imageShader = Q5.device.createShaderModule({
@@ -7548,8 +7673,11 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
7548
7673
  GPUTextureUsage.RENDER_ATTACHMENT
7549
7674
  });
7550
7675
 
7676
+ let src = { source: cnv };
7677
+ if (cnv.tagName == 'IMG') src.colorSpace = $.canvas.colorSpace;
7678
+
7551
7679
  Q5.device.queue.copyExternalImageToTexture(
7552
- { source: cnv },
7680
+ src,
7553
7681
  {
7554
7682
  texture,
7555
7683
  colorSpace: $.canvas.colorSpace
@@ -7627,7 +7755,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
7627
7755
  $._getImageMode = () => _imageMode;
7628
7756
 
7629
7757
  // Reusable uniform buffer array to avoid GC
7630
- $._uniforms = new Float32Array(13);
7758
+ $._uniforms = new Float32Array(14);
7631
7759
 
7632
7760
  const addImgVert = (x, y, u, v, ci, ti, ia) => {
7633
7761
  let s = imgVertStack,
@@ -7647,7 +7775,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
7647
7775
  let isVideo;
7648
7776
  if (img._texture == undefined) {
7649
7777
  isVideo = img.tagName == 'VIDEO';
7650
- if (!img.width || (isVideo && !img.currentTime)) return;
7778
+ if (!isVideo || !img.currentTime) return;
7651
7779
  if (img.flipped) $.scale(-1, 1);
7652
7780
  }
7653
7781
 
@@ -7686,8 +7814,9 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
7686
7814
  let u0 = sx / w,
7687
7815
  v0 = sy / h,
7688
7816
  u1 = (sx + sw) / w,
7689
- v1 = (sy + sh) / h,
7690
- ti = matrixIdx,
7817
+ v1 = (sy + sh) / h;
7818
+
7819
+ let ti = matrixIdx,
7691
7820
  ci = tintIdx,
7692
7821
  ia = globalAlpha;
7693
7822
 
@@ -7772,12 +7901,11 @@ struct Text {
7772
7901
  @group(2) @binding(0) var<storage> textChars: array<vec4f>;
7773
7902
  @group(2) @binding(1) var<storage> textMetadata: array<Text>;
7774
7903
 
7775
- const quad = array(vec2f(0, 1), vec2f(1, 1), vec2f(0, 0), vec2f(1, 0));
7904
+ const quad = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0));
7776
7905
  const uvs = array(vec2f(0, 1), vec2f(1, 1), vec2f(0, 0), vec2f(1, 0));
7777
7906
 
7778
7907
  fn calcPos(i: u32, char: vec4f, fontChar: Char, text: Text) -> vec2f {
7779
- return ((quad[i] * fontChar.size + char.xy + fontChar.offset) *
7780
- text.scale) + text.pos;
7908
+ return ((vec2f(quad[i].x, quad[i].y * q.yUp) * fontChar.size + char.xy + fontChar.offset) * text.scale) + text.pos;
7781
7909
  }
7782
7910
 
7783
7911
  fn calcUV(i: u32, fontChar: Char) -> vec2f {
@@ -8008,7 +8136,7 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8008
8136
  fontChars[o + 4] = char.width; // size.x
8009
8137
  fontChars[o + 5] = char.height; // size.y
8010
8138
  fontChars[o + 6] = char.xoffset; // offset.x
8011
- fontChars[o + 7] = char.yoffset; // offset.y
8139
+ fontChars[o + 7] = -char.yoffset * yDir; // offset.y
8012
8140
  o += 8;
8013
8141
  }
8014
8142
  charsBuffer.unmap();
@@ -8084,9 +8212,10 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8084
8212
 
8085
8213
  let _textSize = 18,
8086
8214
  _textAlign = 'left',
8215
+ _textStyle = 'normal',
8087
8216
  _textBaseline = 'alphabetic',
8088
8217
  leadingSet = false,
8089
- leading = 22.5,
8218
+ _textLeading = 22.5,
8090
8219
  leadDiff = 4.5,
8091
8220
  leadPercent = 1.25;
8092
8221
 
@@ -8107,8 +8236,8 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8107
8236
  if (size == undefined) return _textSize;
8108
8237
  _textSize = size;
8109
8238
  if (!leadingSet) {
8110
- leading = size * leadPercent;
8111
- leadDiff = leading - size;
8239
+ _textLeading = size * leadPercent;
8240
+ leadDiff = _textLeading - size;
8112
8241
  }
8113
8242
  };
8114
8243
 
@@ -8141,29 +8270,29 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8141
8270
 
8142
8271
  $.textLeading = (lineHeight) => {
8143
8272
  if (!$._font) return $._g.textLeading(lineHeight);
8144
- if (!lineHeight) return leading;
8145
- $._font.lineHeight = leading = lineHeight;
8146
- leadDiff = leading - _textSize;
8147
- leadPercent = leading / _textSize;
8273
+ if (!lineHeight) return _textLeading;
8274
+ $._font.lineHeight = _textLeading = lineHeight;
8275
+ leadDiff = _textLeading - _textSize;
8276
+ leadPercent = _textLeading / _textSize;
8148
8277
  leadingSet = true;
8149
8278
  };
8150
8279
 
8151
8280
  $.textAlign = (horiz, vert) => {
8152
8281
  _textAlign = horiz;
8153
- if (vert) _textBaseline = vert;
8282
+ if (vert) _textBaseline = vert[0] == 'c' ? 'middle' : vert;
8154
8283
  };
8155
8284
 
8156
8285
  $.textStyle = (style) => {
8157
- // stub
8286
+ _textStyle = style;
8158
8287
  };
8159
8288
 
8160
8289
  let charStack = [],
8161
8290
  textStack = [];
8162
8291
 
8163
8292
  // Reusable array for line widths to avoid GC
8164
- let lineWidthsCache = new Array(100);
8293
+ let lineWidths = new Array(100);
8165
8294
 
8166
- // Reusable buffers for text data to avoid creating new arrays
8295
+ // Reusable buffers for text data to avoid GC
8167
8296
  let charDataBuffer = new Float32Array(Q5.MAX_CHARS * 4); // reusable buffer for char data
8168
8297
  let textDataBuffer = new Float32Array(Q5.MAX_TEXTS * 8); // reusable buffer for text metadata
8169
8298
 
@@ -8173,7 +8302,6 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8173
8302
  offsetY = 0,
8174
8303
  line = 0,
8175
8304
  printedCharCount = 0,
8176
- lineWidths = lineWidthsCache, // reuse array
8177
8305
  nextCharCode = text.charCodeAt(0);
8178
8306
 
8179
8307
  for (let i = 0; i < text.length; ++i) {
@@ -8181,7 +8309,7 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8181
8309
  nextCharCode = i < text.length - 1 ? text.charCodeAt(i + 1) : -1;
8182
8310
  switch (charCode) {
8183
8311
  case 10: // newline
8184
- lineWidths.push(offsetX);
8312
+ lineWidths[line] = offsetX;
8185
8313
  line++;
8186
8314
  maxWidth = Math.max(maxWidth, offsetX);
8187
8315
  offsetX = 0;
@@ -8217,7 +8345,7 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8217
8345
  };
8218
8346
 
8219
8347
  $.text = (str, x, y, w, h) => {
8220
- if (_textSize < 1) return;
8348
+ if (_textSize * _scale < 1) return;
8221
8349
 
8222
8350
  let type = typeof str;
8223
8351
  if (type != 'string') {
@@ -8273,17 +8401,17 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8273
8401
  o += 4;
8274
8402
  });
8275
8403
 
8276
- if (tb == 'alphabetic') y -= _textSize;
8277
- else if (tb == 'center') y -= _textSize * 0.5;
8278
- else if (tb == 'bottom') y -= leading;
8404
+ if (tb == 'alphabetic') y += _textSize * yDir;
8405
+ else if (tb == 'middle') y += _textSize * 0.5 * yDir;
8406
+ else if (tb == 'bottom') y += _textLeading * yDir;
8279
8407
  } else {
8280
8408
  // measure the text to get the line height before setting
8281
8409
  // the x position to properly align the text
8282
8410
  measurements = measureText($._font, str);
8283
8411
 
8284
8412
  let offsetY = 0;
8285
- if (tb == 'alphabetic') y -= _textSize;
8286
- else if (tb == 'center') offsetY = measurements.height * 0.5;
8413
+ if (tb == 'alphabetic') y += _textSize * yDir;
8414
+ else if (tb == 'middle') offsetY = measurements.height * 0.5;
8287
8415
  else if (tb == 'bottom') offsetY = measurements.height;
8288
8416
 
8289
8417
  measureText($._font, str, (textX, textY, line, char) => {
@@ -8294,7 +8422,7 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8294
8422
  offsetX = -measurements.lineWidths[line];
8295
8423
  }
8296
8424
  charsData[o] = textX + offsetX;
8297
- charsData[o + 1] = -(textY + offsetY);
8425
+ charsData[o + 1] = (textY + offsetY) * yDir;
8298
8426
  charsData[o + 2] = char.charIndex;
8299
8427
  charsData[o + 3] = textIndex;
8300
8428
  o += 4;
@@ -8332,7 +8460,7 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8332
8460
  $._g.textSize(_textSize);
8333
8461
  return $._g.textAscent(str);
8334
8462
  }
8335
- return leading - leadDiff;
8463
+ return _textLeading - leadDiff;
8336
8464
  };
8337
8465
 
8338
8466
  $.textDescent = (str) => {
@@ -8343,18 +8471,29 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8343
8471
  return leadDiff;
8344
8472
  };
8345
8473
 
8346
- $.createTextImage = (str, w, h) => {
8347
- $._g.textSize(_textSize);
8348
-
8349
- if (doFill && fillSet) {
8474
+ $._applyTextStylesToC2D = () => {
8475
+ if (!doFill) $._g.noFill();
8476
+ else if (fillSet) {
8350
8477
  let fi = fillIdx * 4;
8351
8478
  $._g.fill(colorStack.slice(fi, fi + 4));
8352
8479
  }
8353
- if (doStroke && strokeSet) {
8480
+
8481
+ if (!doStroke) $._g.noStroke();
8482
+ else if (strokeSet) {
8354
8483
  let si = strokeIdx * 4;
8355
8484
  $._g.stroke(colorStack.slice(si, si + 4));
8356
8485
  }
8357
8486
 
8487
+ if (sw != $._g._strokeWeight) $._g.strokeWeight(sw);
8488
+ if (_textSize != $._g._textSize) $._g.textSize(_textSize);
8489
+ if (_textStyle != $._g._textStyle) $._g.textStyle(_textStyle);
8490
+ if (_textLeading != $._g.textLeading()) $._g.textLeading(_textLeading);
8491
+ $._g.textAlign(_textAlign, _textBaseline);
8492
+ };
8493
+
8494
+ $.createTextImage = (str, w, h) => {
8495
+ $._applyTextStylesToC2D();
8496
+
8358
8497
  let g = $._g.createTextImage(str, w, h);
8359
8498
  $._makeDrawable(g);
8360
8499
  return g;
@@ -8371,15 +8510,20 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8371
8510
  else if (ta == 'right') x -= img.width;
8372
8511
 
8373
8512
  let bl = _textBaseline;
8374
- if (bl == 'alphabetic') y -= img._leading;
8375
- else if (bl == 'center') y -= img._middle;
8376
- else if (bl == 'bottom') y -= img._bottom;
8377
- else if (bl == 'top') y -= img._top;
8513
+ if (bl == 'alphabetic') y += img._leading * yDir;
8514
+ else if (bl == 'middle') y += img._middle * yDir;
8515
+ else if (bl == 'bottom') y += img._bottom * yDir;
8516
+ else if (bl == 'top') y += img._top * yDir;
8378
8517
 
8379
8518
  $.image(img, x, y);
8380
8519
  _imageMode = og;
8381
8520
  };
8382
8521
 
8522
+ $.textToPoints = (str, x, y, sampleRate, density) => {
8523
+ $._applyTextStylesToC2D();
8524
+ return $._g.textToPoints(str, x, y, sampleRate, density);
8525
+ };
8526
+
8383
8527
  /* SHADERS */
8384
8528
 
8385
8529
  let pipelineTypes = ['frame', 'shapes', 'image', 'video', 'text'];
@@ -8464,6 +8608,28 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
8464
8608
  videoPL = 3;
8465
8609
  textPL = 4;
8466
8610
  };
8611
+
8612
+ const _remove = $.remove;
8613
+ $.remove = () => {
8614
+ $._frameA?.destroy();
8615
+ $._frameB?.destroy();
8616
+ uniformBuffer?.destroy();
8617
+ transformsBuffer?.destroy();
8618
+ colorsBuffer?.destroy();
8619
+ shapesVertBuff?.destroy();
8620
+ imgVertBuff?.destroy();
8621
+ charBuffer?.destroy();
8622
+ textBuffer?.destroy();
8623
+ rectBuffer?.destroy();
8624
+ rectIndexBuffer?.destroy();
8625
+ ellipseBuffer?.destroy();
8626
+ ellipseIndexBuffer?.destroy();
8627
+
8628
+ for (let b of $._buffers) b.destroy();
8629
+ $._buffers = [];
8630
+
8631
+ _remove();
8632
+ };
8467
8633
  };
8468
8634
 
8469
8635
  Q5.THRESHOLD = 1;
@@ -8475,7 +8641,7 @@ Q5.DILATE = 6;
8475
8641
  Q5.ERODE = 7;
8476
8642
  Q5.BLUR = 8;
8477
8643
 
8478
- Q5.MAX_TRANSFORMS = 1e7;
8644
+ Q5.MAX_TRANSFORMS = 2097152;
8479
8645
  Q5.MAX_RECTS = 200200;
8480
8646
  Q5.MAX_ELLIPSES = 200200;
8481
8647
  Q5.MAX_CHARS = 100000;
@@ -8486,20 +8652,50 @@ Q5.initWebGPU = async () => {
8486
8652
  console.warn('q5 WebGPU not supported on this browser! Use Google Chrome or Edge.');
8487
8653
  return false;
8488
8654
  }
8489
- if (!Q5.requestedGPU) {
8490
- Q5.requestedGPU = true;
8491
- let adapter = await navigator.gpu.requestAdapter();
8492
- if (!adapter) {
8493
- console.warn('q5 WebGPU could not start! No appropriate GPUAdapter found, Vulkan may need to be enabled.');
8494
- return false;
8495
- }
8496
- Q5.device = await adapter.requestDevice();
8497
8655
 
8498
- Q5.device.lost.then((e) => {
8499
- console.error('WebGPU crashed!');
8500
- console.error(e);
8501
- });
8656
+ // fn can only be called once
8657
+ if (Q5.requestedGPU) return;
8658
+ Q5.requestedGPU = true;
8659
+
8660
+ let adapter = await navigator.gpu.requestAdapter();
8661
+
8662
+ adapter ??= await navigator.gpu.requestAdapter({
8663
+ featureLevel: 'compatibility'
8664
+ });
8665
+
8666
+ if (!adapter) {
8667
+ console.warn('q5 WebGPU could not start! No appropriate GPUAdapter found, Vulkan may need to be enabled.');
8668
+ return false;
8669
+ }
8670
+
8671
+ let device = await adapter.requestDevice();
8672
+
8673
+ const vertexStorageLimit =
8674
+ device.limits.maxStorageBuffersInVertexStage ?? device.limits.maxStorageBuffersPerShaderStage;
8675
+ if (vertexStorageLimit < 3) {
8676
+ console.warn('q5 WebGPU requires vertex storage buffers, which are not supported by this device.');
8677
+ return false;
8502
8678
  }
8679
+
8680
+ // Update to fit device limits
8681
+ const maxStorage = device.limits.maxStorageBufferBindingSize;
8682
+
8683
+ let min = Math.min,
8684
+ floor = Math.floor;
8685
+
8686
+ Q5.MAX_TRANSFORMS = min(Q5.MAX_TRANSFORMS, floor(maxStorage / 64));
8687
+ Q5.MAX_RECTS = min(Q5.MAX_RECTS, floor(maxStorage / 64));
8688
+ Q5.MAX_ELLIPSES = min(Q5.MAX_ELLIPSES, floor(maxStorage / 64));
8689
+ Q5.MAX_CHARS = min(Q5.MAX_CHARS, floor(maxStorage / 16));
8690
+ Q5.MAX_TEXTS = min(Q5.MAX_TEXTS, floor(maxStorage / 32));
8691
+
8692
+ device.lost.then((e) => {
8693
+ console.error('WebGPU crashed!');
8694
+ console.error(e);
8695
+ });
8696
+
8697
+ Q5.device = device;
8698
+
8503
8699
  return true;
8504
8700
  };
8505
8701