q5 3.5.2 → 3.6.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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * q5.js
3
- * @version 3.5
3
+ * @version 3.6
4
4
  * @author quinton-ashley
5
5
  * @contributors evanalulu, Tezumie, ormaq, Dukemz, LingDong-
6
6
  * @license LGPL-3.0
@@ -79,15 +79,19 @@ function Q5(scope, parent, renderer) {
79
79
  $.deviceOrientation = window.screen?.orientation?.type;
80
80
  }
81
81
 
82
- $._preloadPromises = [];
83
- $._usePreload = true;
84
- $.usePromiseLoading = (v = true) => ($._usePreload = !v);
85
- $.usePreloadSystem = (v = true) => ($._usePreload = v);
86
- $.isPreloadSupported = () => $._usePreload;
82
+ $._loaders = [];
83
+ $.loadAll = () => {
84
+ let loaders = $._loaders;
85
+ if ($._g) loaders = loaders.concat($._g._loaders);
86
+ return Promise.all(loaders);
87
+ };
88
+
89
+ $.isPreloadSupported = () => true;
90
+ $.disablePreload = () => ($._disablePreload = true);
87
91
 
88
92
  const resolvers = [];
89
93
  $._incrementPreload = () => {
90
- $._preloadPromises.push(new Promise((resolve) => resolvers.push(resolve)));
94
+ $._loaders.push(new Promise((resolve) => resolvers.push(resolve)));
91
95
  };
92
96
  $._decrementPreload = () => {
93
97
  if (resolvers.length) resolvers.pop()();
@@ -289,7 +293,7 @@ function Q5(scope, parent, renderer) {
289
293
  for (let name of userFns) $[name] ??= () => {};
290
294
 
291
295
  if ($._isGlobal) {
292
- for (let name of ['setup', 'update', 'draw', ...userFns]) {
296
+ for (let name of ['setup', 'update', 'draw', 'drawFrame', ...userFns]) {
293
297
  if (Q5[name]) $[name] = Q5[name];
294
298
  else {
295
299
  Object.defineProperty(Q5, name, {
@@ -341,16 +345,13 @@ function Q5(scope, parent, renderer) {
341
345
  new Promise((resolve) => {
342
346
  setTimeout(() => {
343
347
  // if not loading
344
- if (!$._preloadPromises.length) resolve();
348
+ if (!$._loaders.length) resolve();
345
349
  }, 500);
346
350
  })
347
351
  ]);
348
352
 
349
- await Promise.all($._preloadPromises);
350
- if ($._g) await Promise.all($._g._preloadPromises);
351
-
352
- if (t.setup?.constructor.name == 'AsyncFunction') {
353
- $.usePromiseLoading();
353
+ if (!$._disablePreload) {
354
+ await $.loadAll();
354
355
  }
355
356
 
356
357
  $.setup ??= t.setup || (() => {});
@@ -374,7 +375,7 @@ function Q5(scope, parent, renderer) {
374
375
 
375
376
  Q5.instances.push($);
376
377
 
377
- if (autoLoaded) start();
378
+ if (autoLoaded || Q5._esm) start();
378
379
  else setTimeout(start, 32);
379
380
  }
380
381
 
@@ -400,14 +401,14 @@ Q5.hooks = {
400
401
  remove: []
401
402
  };
402
403
 
403
- Q5.addHook = (name, fn) => Q5.hooks[name].push(fn);
404
+ Q5.addHook = (lifecycle, fn) => Q5.hooks[lifecycle].push(fn);
404
405
 
405
406
  // p5 v2 compat
406
407
  Q5.registerAddon = (addon) => {
407
408
  let lifecycles = {};
408
409
  addon(Q5, Q5.prototype, lifecycles);
409
- for (let name in lifecycles) {
410
- Q5.hooks[name].push(lifecycles[name]);
410
+ for (let l in lifecycles) {
411
+ Q5.hooks[l].push(lifecycles[l]);
411
412
  }
412
413
  };
413
414
 
@@ -437,16 +438,20 @@ function createCanvas(w, h, opt) {
437
438
  }
438
439
  }
439
440
 
440
- if (Q5._server) global.p5 ??= global.Q5 = Q5;
441
+ if (Q5._server) {
442
+ global.q5 = global.Q5 = Q5;
443
+ global.p5 ??= Q5;
444
+ }
441
445
 
442
446
  if (typeof window == 'object') {
443
- window.p5 ??= window.Q5 = Q5;
447
+ window.q5 = window.Q5 = Q5;
448
+ window.p5 ??= Q5;
444
449
  window.createCanvas = createCanvas;
445
450
  window.C2D = 'c2d';
446
451
  window.WEBGPU = 'webgpu';
447
452
  } else global.window = 0;
448
453
 
449
- Q5.version = Q5.VERSION = '3.5';
454
+ Q5.version = Q5.VERSION = '3.6';
450
455
 
451
456
  if (typeof document == 'object') {
452
457
  document.addEventListener('DOMContentLoaded', () => {
@@ -1388,11 +1393,10 @@ Q5.renderers.c2d.image = ($, q) => {
1388
1393
  let imgData = null,
1389
1394
  pixels = null;
1390
1395
 
1391
- $.createImage = (w, h, opt) => {
1392
- opt ??= {};
1393
- opt.alpha ??= true;
1394
- opt.colorSpace ??= c.colorSpace || Q5.canvasOptions.colorSpace;
1395
- return new Q5.Image($, w, h, opt);
1396
+ $.createImage = (w, h, opt = {}) => {
1397
+ opt.colorSpace ??= $.canvas.colorSpace;
1398
+ opt.defaultImageScale ??= $._defaultImageScale;
1399
+ return new Q5.Image(w, h, opt);
1396
1400
  };
1397
1401
 
1398
1402
  $.loadImage = function (url, cb, opt) {
@@ -1406,7 +1410,7 @@ Q5.renderers.c2d.image = ($, q) => {
1406
1410
  if (typeof last == 'object') {
1407
1411
  opt = last;
1408
1412
  cb = null;
1409
- } else opt = null;
1413
+ } else opt = undefined;
1410
1414
 
1411
1415
  let g = $.createImage(1, 1, opt);
1412
1416
  let pd = g._pixelDensity;
@@ -1416,6 +1420,10 @@ Q5.renderers.c2d.image = ($, q) => {
1416
1420
 
1417
1421
  g.promise = new Promise((resolve, reject) => {
1418
1422
  img.onload = () => {
1423
+ delete g.promise;
1424
+ delete g.then;
1425
+ if (g._usedAwait) g = $.createImage(1, 1, opt);
1426
+
1419
1427
  img._pixelDensity = pd;
1420
1428
  g.defaultWidth = img.width * $._defaultImageScale;
1421
1429
  g.defaultHeight = img.height * $._defaultImageScale;
@@ -1425,16 +1433,19 @@ Q5.renderers.c2d.image = ($, q) => {
1425
1433
 
1426
1434
  g.ctx.drawImage(img, 0, 0);
1427
1435
  if (cb) cb(g);
1428
- delete g.promise;
1429
1436
  resolve(g);
1430
1437
  };
1431
1438
  img.onerror = reject;
1432
1439
  });
1433
- $._preloadPromises.push(g.promise);
1440
+ $._loaders.push(g.promise);
1434
1441
 
1435
- g.src = img.src = url;
1442
+ // then only runs when the user awaits the instance
1443
+ g.then = (resolve, reject) => {
1444
+ g._usedAwait = true;
1445
+ return g.promise.then(resolve, reject);
1446
+ };
1436
1447
 
1437
- if (!$._usePreload) return g.promise;
1448
+ g.src = img.src = url;
1438
1449
  return g;
1439
1450
  };
1440
1451
 
@@ -1695,7 +1706,9 @@ Q5.renderers.c2d.image = ($, q) => {
1695
1706
  };
1696
1707
 
1697
1708
  Q5.Image = class {
1698
- constructor(q, w, h, opt = {}) {
1709
+ constructor(w, h, opt = {}) {
1710
+ opt.alpha ??= true;
1711
+ opt.colorSpace ??= Q5.canvasOptions.colorSpace;
1699
1712
  let $ = this;
1700
1713
  $._isImage = true;
1701
1714
  $.canvas = $.ctx = $.drawingContext = null;
@@ -1706,7 +1719,7 @@ Q5.Image = class {
1706
1719
  if (r[m]) r[m]($, $);
1707
1720
  }
1708
1721
  $._pixelDensity = opt.pixelDensity || 1;
1709
- $._defaultImageScale = opt.defaultImageScale || q._defaultImageScale;
1722
+ $._defaultImageScale = opt.defaultImageScale || 2;
1710
1723
  $.createCanvas(w, h, opt);
1711
1724
  let scale = $._pixelDensity * $._defaultImageScale;
1712
1725
  $.defaultWidth = w * scale;
@@ -1878,13 +1891,11 @@ Q5.renderers.c2d.text = ($, q) => {
1878
1891
  emphasis = 'normal',
1879
1892
  weight = 'normal',
1880
1893
  styleHash = 0,
1881
- styleHashes = [],
1882
1894
  genTextImage = false,
1883
1895
  cacheSize = 0;
1884
1896
  $._fontMod = false;
1885
1897
 
1886
1898
  let cache = ($._textCache = {});
1887
- $._textCacheMaxSize = 12000;
1888
1899
 
1889
1900
  $.loadFont = (url, cb) => {
1890
1901
  let f;
@@ -1894,25 +1905,29 @@ Q5.renderers.c2d.text = ($, q) => {
1894
1905
  } else {
1895
1906
  let name = url.split('/').pop().split('.')[0].replace(' ', '');
1896
1907
 
1897
- f = new FontFace(name, `url(${encodeURI(url)})`);
1898
- document.fonts.add(f);
1899
- f.promise = (async () => {
1900
- let err;
1901
- try {
1902
- await f.load();
1903
- } catch (e) {
1904
- err = e;
1905
- }
1906
- delete f.promise;
1907
- if (err) throw err;
1908
- if (cb) cb(f);
1909
- return f;
1910
- })();
1908
+ f = { family: name };
1909
+ let ff = new FontFace(name, `url(${encodeURI(url)})`);
1910
+ document.fonts.add(ff);
1911
+
1912
+ f.promise = new Promise((resolve, reject) => {
1913
+ ff.load()
1914
+ .then(() => {
1915
+ delete f.promise;
1916
+ delete f.then;
1917
+ if (cb) cb(ff);
1918
+ resolve(ff);
1919
+ })
1920
+ .catch((err) => {
1921
+ reject(err);
1922
+ });
1923
+ });
1911
1924
  }
1912
-
1913
- $._preloadPromises.push(f.promise);
1925
+ $._loaders.push(f.promise);
1914
1926
  $.textFont(f.family);
1915
- if (!$._usePreload) return f.promise;
1927
+ f.then = (resolve, reject) => {
1928
+ f._usedAwait = true;
1929
+ return f.promise.then(resolve, reject);
1930
+ };
1916
1931
  return f;
1917
1932
  };
1918
1933
 
@@ -1980,8 +1995,13 @@ Q5.renderers.c2d.text = ($, q) => {
1980
1995
  }
1981
1996
  }
1982
1997
 
1998
+ if (f._usedAwait) {
1999
+ f = { family: fontFamily };
2000
+ }
2001
+
1983
2002
  f.faces = loadedFaces;
1984
2003
  delete f.promise;
2004
+ delete f.then;
1985
2005
  if (cb) cb(f);
1986
2006
  return f;
1987
2007
  } catch (e) {
@@ -2086,7 +2106,7 @@ Q5.renderers.c2d.text = ($, q) => {
2086
2106
  if (str === undefined || (!$._doFill && !$._doStroke)) return;
2087
2107
  str = str.toString();
2088
2108
  let ctx = $.ctx;
2089
- let img, colorStyle;
2109
+ let img, colorStyle, styleCache, colorCache, recycling;
2090
2110
 
2091
2111
  if ($._fontMod) $._updateFont();
2092
2112
 
@@ -2094,14 +2114,23 @@ Q5.renderers.c2d.text = ($, q) => {
2094
2114
  if (styleHash == -1) updateStyleHash();
2095
2115
  colorStyle = $._fill + $._stroke + $._strokeWeight;
2096
2116
 
2097
- let cacheLevel1 = cache[str];
2098
- let cacheLevel2;
2099
- if (cacheLevel1) cacheLevel2 = cacheLevel1[styleHash];
2117
+ styleCache = cache[str];
2118
+ if (styleCache) colorCache = styleCache[styleHash];
2119
+ else styleCache = cache[str] = {};
2100
2120
 
2101
- if (cacheLevel2) {
2102
- img = cacheLevel2[colorStyle];
2121
+ if (colorCache) {
2122
+ img = colorCache[colorStyle];
2103
2123
  if (img) return img;
2104
- }
2124
+
2125
+ if (colorCache.size >= 4) {
2126
+ for (let recycleKey in colorCache) {
2127
+ img = colorCache[recycleKey];
2128
+ delete colorCache[recycleKey];
2129
+ break;
2130
+ }
2131
+ recycling = true;
2132
+ }
2133
+ } else colorCache = styleCache[styleHash] = {};
2105
2134
  }
2106
2135
 
2107
2136
  if (str.indexOf('\n') == -1) lines[0] = str;
@@ -2169,6 +2198,8 @@ Q5.renderers.c2d.text = ($, q) => {
2169
2198
  img._bottom = img._top + ascent + leading * (lines.length - 1);
2170
2199
  img._leading = leading;
2171
2200
  } else {
2201
+ let cnv = img.canvas;
2202
+ img.ctx.clearRect(0, 0, cnv.width, cnv.height);
2172
2203
  img.modified = true;
2173
2204
  }
2174
2205
 
@@ -2199,19 +2230,33 @@ Q5.renderers.c2d.text = ($, q) => {
2199
2230
  if (!$._fillSet) ctx.fillStyle = ogFill;
2200
2231
 
2201
2232
  if (genTextImage) {
2202
- styleHashes.push(styleHash);
2203
- (cache[str] ??= {})[styleHash] ??= {};
2204
- cache[str][styleHash][colorStyle] = img;
2205
-
2206
- cacheSize++;
2207
- if (cacheSize > $._textCacheMaxSize) {
2208
- let half = Math.ceil(cacheSize / 2);
2209
- let hashes = styleHashes.splice(0, half);
2210
- for (let s in cache) {
2211
- s = cache[s];
2212
- for (let h of hashes) delete s[h];
2233
+ colorCache[colorStyle] = img;
2234
+
2235
+ if (!recycling) {
2236
+ if (!colorCache.size) {
2237
+ Object.defineProperty(colorCache, 'size', {
2238
+ writable: true,
2239
+ enumerable: false
2240
+ });
2241
+ colorCache.size = 0;
2242
+ }
2243
+ colorCache.size++;
2244
+ cacheSize++;
2245
+ }
2246
+
2247
+ if (cacheSize > Q5.MAX_TEXT_IMAGES) {
2248
+ for (const str in cache) {
2249
+ styleCache = cache[str];
2250
+ for (const hash in styleCache) {
2251
+ colorCache = styleCache[hash];
2252
+ for (let c in colorCache) {
2253
+ let _img = colorCache[c];
2254
+ if (_img._texture) _img._texture.destroy();
2255
+ delete colorCache[c];
2256
+ }
2257
+ }
2213
2258
  }
2214
- cacheSize -= half;
2259
+ cacheSize = 0;
2215
2260
  }
2216
2261
  return img;
2217
2262
  }
@@ -2239,6 +2284,7 @@ Q5.renderers.c2d.text = ($, q) => {
2239
2284
  };
2240
2285
 
2241
2286
  Q5.fonts = [];
2287
+ Q5.MAX_TEXT_IMAGES = 5000;
2242
2288
  Q5.modules.color = ($, q) => {
2243
2289
  $.RGB = $.RGBA = $.RGBHDR = $._colorMode = 'rgb';
2244
2290
  $.HSL = 'hsl';
@@ -2275,6 +2321,7 @@ Q5.modules.color = ($, q) => {
2275
2321
  black: [0, 0, 0],
2276
2322
  blue: [0, 0, 255],
2277
2323
  brown: [165, 42, 42],
2324
+ coral: [255, 127, 80],
2278
2325
  crimson: [220, 20, 60],
2279
2326
  cyan: [0, 255, 255],
2280
2327
  darkviolet: [148, 0, 211],
@@ -3112,29 +3159,38 @@ Q5.modules.dom = ($, q) => {
3112
3159
 
3113
3160
  $.createSpan = (content) => $.createEl('span', content);
3114
3161
 
3162
+ function initVideo(el) {
3163
+ el.width ||= el.videoWidth;
3164
+ el.height ||= el.videoHeight;
3165
+ el.defaultWidth = el.width * $._defaultImageScale;
3166
+ el.defaultHeight = el.height * $._defaultImageScale;
3167
+ el.ready = true;
3168
+ }
3169
+
3115
3170
  $.createVideo = (src) => {
3116
3171
  let el = $.createEl('video');
3117
3172
  el.crossOrigin = 'anonymous';
3118
3173
 
3119
- el._load = () => {
3120
- el.width ||= el.videoWidth;
3121
- el.height ||= el.videoHeight;
3122
- el.defaultWidth = el.width * $._defaultImageScale;
3123
- el.defaultHeight = el.height * $._defaultImageScale;
3124
- el.ready = true;
3125
- };
3126
-
3127
3174
  if (src) {
3128
3175
  el.promise = new Promise((resolve) => {
3129
3176
  el.addEventListener('loadeddata', () => {
3130
- el._load();
3177
+ delete el.promise;
3178
+ delete el.then;
3179
+ if (el._usedAwait) {
3180
+ el = $.createEl('video');
3181
+ el.crossOrigin = 'anonymous';
3182
+ el.src = src;
3183
+ }
3184
+ initVideo(el);
3131
3185
  resolve(el);
3132
3186
  });
3133
3187
  el.src = src;
3134
3188
  });
3135
- $._preloadPromises.push(el.promise);
3136
-
3137
- if (!$._usePreload) return el.promise;
3189
+ $._loaders.push(el.promise);
3190
+ el.then = (resolve, reject) => {
3191
+ el._usedAwait = true;
3192
+ return el.promise.then(resolve, reject);
3193
+ };
3138
3194
  }
3139
3195
  return el;
3140
3196
  };
@@ -3149,18 +3205,22 @@ Q5.modules.dom = ($, q) => {
3149
3205
  constraints.video.facingMode ??= 'user';
3150
3206
 
3151
3207
  let vid = $.createVideo();
3152
- vid.playsinline = vid.autoplay = true;
3153
- if (flipped) {
3154
- vid.flipped = true;
3155
- vid.style.transform = 'scale(-1, 1)';
3156
- }
3157
- vid.loadPixels = () => {
3158
- let g = $.createGraphics(vid.videoWidth, vid.videoHeight, { renderer: 'c2d' });
3159
- g.image(vid, 0, 0);
3160
- g.loadPixels();
3161
- vid.pixels = g.pixels;
3162
- g.remove();
3163
- };
3208
+
3209
+ function extendVideo(vid) {
3210
+ vid.playsinline = vid.autoplay = true;
3211
+ if (flipped) {
3212
+ vid.flipped = true;
3213
+ vid.style.transform = 'scale(-1, 1)';
3214
+ }
3215
+ vid.loadPixels = () => {
3216
+ let g = $.createGraphics(vid.videoWidth, vid.videoHeight, { renderer: 'c2d' });
3217
+ g.image(vid, 0, 0);
3218
+ g.loadPixels();
3219
+ vid.pixels = g.pixels;
3220
+ g.remove();
3221
+ };
3222
+ }
3223
+
3164
3224
  vid.promise = (async () => {
3165
3225
  let stream;
3166
3226
  try {
@@ -3169,16 +3229,26 @@ Q5.modules.dom = ($, q) => {
3169
3229
  throw e;
3170
3230
  }
3171
3231
 
3232
+ delete vid.promise;
3233
+ delete vid.then;
3234
+ if (vid._usedAwait) {
3235
+ vid = $.createVideo();
3236
+ }
3237
+ extendVideo(vid);
3238
+
3172
3239
  vid.srcObject = stream;
3173
3240
  await new Promise((resolve) => vid.addEventListener('loadeddata', resolve));
3174
3241
 
3175
- vid._load();
3242
+ initVideo(vid);
3176
3243
  if (cb) cb(vid);
3177
3244
  return vid;
3178
3245
  })();
3179
- $._preloadPromises.push(vid.promise);
3246
+ $._loaders.push(vid.promise);
3180
3247
 
3181
- if (!$._usePreload) return vid.promise;
3248
+ vid.then = (resolve, reject) => {
3249
+ vid._usedAwait = true;
3250
+ return vid.promise.then(resolve, reject);
3251
+ };
3182
3252
  return vid;
3183
3253
  };
3184
3254
 
@@ -3258,12 +3328,12 @@ Q5.modules.fes = ($) => {
3258
3328
  }
3259
3329
  }
3260
3330
 
3261
- if (Q5.online != false && typeof navigator != undefined && navigator.onLine) {
3331
+ if ($._isGlobal && Q5.online != false && typeof navigator != undefined && navigator.onLine) {
3262
3332
  async function checkLatestVersion() {
3263
3333
  try {
3264
- let response = await fetch('https://data.jsdelivr.com/v1/package/npm/q5');
3265
- if (!response.ok) return;
3266
- let data = await response.json();
3334
+ let res = await fetch('https://data.jsdelivr.com/v1/package/npm/q5');
3335
+ if (!res.ok) return;
3336
+ let data = await res.json();
3267
3337
  let l = data.tags.latest;
3268
3338
  l = l.slice(0, l.lastIndexOf('.'));
3269
3339
  if (l != Q5.version) {
@@ -4352,6 +4422,11 @@ Q5.modules.sound = ($, q) => {
4352
4422
  sounds.push(s);
4353
4423
 
4354
4424
  s.promise = (async () => {
4425
+ if (s._usedAwait) {
4426
+ sounds.splice(sounds.indexOf(s), 1);
4427
+ s = new Q5.Sound();
4428
+ sounds.push(s);
4429
+ }
4355
4430
  let err;
4356
4431
  try {
4357
4432
  await s.load(url);
@@ -4359,13 +4434,17 @@ Q5.modules.sound = ($, q) => {
4359
4434
  err = e;
4360
4435
  }
4361
4436
  delete s.promise;
4437
+ delete s.then;
4362
4438
  if (err) throw err;
4363
4439
  if (cb) cb(s);
4364
4440
  return s;
4365
4441
  })();
4366
- $._preloadPromises.push(s.promise);
4442
+ $._loaders.push(s.promise);
4367
4443
 
4368
- if (!$._usePreload) return s.promise;
4444
+ s.then = (resolve, reject) => {
4445
+ s._usedAwait = true;
4446
+ return s.promise.then(resolve, reject);
4447
+ };
4369
4448
  return s;
4370
4449
  };
4371
4450
 
@@ -4374,19 +4453,30 @@ Q5.modules.sound = ($, q) => {
4374
4453
  a._isAudio = true;
4375
4454
  a.crossOrigin = 'Anonymous';
4376
4455
  a.promise = new Promise((resolve, reject) => {
4377
- a.addEventListener('canplay', () => {
4456
+ function loaded() {
4378
4457
  if (!a.loaded) {
4458
+ delete a.promise;
4459
+ delete a.then;
4460
+ if (a._usedAwait) {
4461
+ a = new Audio(url);
4462
+ a._isAudio = true;
4463
+ a.crossOrigin = 'Anonymous';
4464
+ }
4379
4465
  a.loaded = true;
4380
4466
  if (cb) cb(a);
4381
4467
  resolve(a);
4382
4468
  }
4383
- });
4384
- a.addEventListener('suspend', resolve);
4469
+ }
4470
+ a.addEventListener('canplay', loaded);
4471
+ a.addEventListener('suspend', loaded);
4385
4472
  a.addEventListener('error', reject);
4386
4473
  });
4387
- $._preloadPromises.push(a.promise);
4474
+ $._loaders.push(a.promise);
4388
4475
 
4389
- if (!$._usePreload) return a.promise;
4476
+ a.then = (resolve, reject) => {
4477
+ a._usedAwait = true;
4478
+ return a.promise.then(resolve, reject);
4479
+ };
4390
4480
  return a;
4391
4481
  };
4392
4482
 
@@ -4571,27 +4661,30 @@ Q5.Sound = class {
4571
4661
  Q5.modules.util = ($, q) => {
4572
4662
  $._loadFile = (url, cb, type) => {
4573
4663
  let ret = {};
4574
- ret.promise = new Promise((resolve, reject) => {
4575
- fetch(url)
4576
- .then((res) => {
4577
- if (!res.ok) {
4578
- reject('error loading file');
4579
- return null;
4580
- }
4581
- if (type == 'json') return res.json();
4582
- return res.text();
4583
- })
4584
- .then((f) => {
4585
- if (type == 'csv') f = Q5.CSV.parse(f);
4586
- if (typeof f == 'string') ret.text = f;
4587
- else Object.assign(ret, f);
4588
- delete ret.promise;
4589
- if (cb) cb(f);
4590
- resolve(f);
4591
- });
4592
- });
4593
- $._preloadPromises.push(ret.promise);
4594
- if (!$._usePreload) return ret.promise;
4664
+ ret.promise = fetch(url)
4665
+ .then((res) => {
4666
+ if (!res.ok) {
4667
+ reject('error loading file');
4668
+ return null;
4669
+ }
4670
+ return type == 'json' ? res.json() : res.text();
4671
+ })
4672
+ .then((f) => {
4673
+ if (type == 'csv') f = Q5.CSV.parse(f);
4674
+
4675
+ if (typeof f == 'string') ret.text = f;
4676
+ else Object.assign(ret, f);
4677
+
4678
+ delete ret.promise;
4679
+ delete ret.then;
4680
+ if (cb) cb(f);
4681
+ return f;
4682
+ });
4683
+ $._loaders.push(ret.promise);
4684
+
4685
+ ret.then = (resolve, reject) => {
4686
+ return ret.promise.then(resolve, reject);
4687
+ };
4595
4688
  return ret;
4596
4689
  };
4597
4690
 
@@ -4607,11 +4700,15 @@ Q5.modules.util = ($, q) => {
4607
4700
  let xml = new DOMParser().parseFromString(text, 'application/xml');
4608
4701
  ret.DOM = xml;
4609
4702
  delete ret.promise;
4703
+ delete ret.then;
4610
4704
  if (cb) cb(xml);
4611
4705
  return xml;
4612
4706
  });
4613
- $._preloadPromises.push(ret.promise);
4614
- if (!$._usePreload) return ret.promise;
4707
+ $._loaders.push(ret.promise);
4708
+
4709
+ ret.then = (resolve, reject) => {
4710
+ return ret.promise.then(resolve, reject);
4711
+ };
4615
4712
  return ret;
4616
4713
  };
4617
4714
 
@@ -4623,7 +4720,7 @@ Q5.modules.util = ($, q) => {
4623
4720
  $.load = function (...urls) {
4624
4721
  if (Array.isArray(urls[0])) urls = urls[0];
4625
4722
 
4626
- let promises = [];
4723
+ let thenables = [];
4627
4724
 
4628
4725
  for (let url of urls) {
4629
4726
  let ext = url.split('.').pop().toLowerCase();
@@ -4644,11 +4741,11 @@ Q5.modules.util = ($, q) => {
4644
4741
  } else {
4645
4742
  obj = $.loadText(url);
4646
4743
  }
4647
- promises.push($._usePreload ? obj.promise : obj);
4744
+ thenables.push(obj);
4648
4745
  }
4649
4746
 
4650
- if (urls.length == 1) return promises[0];
4651
- return Promise.all(promises);
4747
+ if (urls.length == 1) return thenables[0];
4748
+ return Promise.all(thenables);
4652
4749
  };
4653
4750
 
4654
4751
  async function saveFile(data, name, ext) {
@@ -4751,8 +4848,13 @@ Q5.Vector = class {
4751
4848
  this.z = z || 0;
4752
4849
  this._isVector = true;
4753
4850
  this._$ = $ || window;
4754
- this._cn = null;
4755
- this._cnsq = null;
4851
+
4852
+ // managed by the user to avoid redundant calculations
4853
+ this._useCache = false;
4854
+ this._mag = 0;
4855
+ this._magCached = false;
4856
+ this._direction = 0;
4857
+ this._directionCached = false;
4756
4858
  }
4757
4859
 
4758
4860
  set(x, y, z) {
@@ -4774,11 +4876,6 @@ Q5.Vector = class {
4774
4876
  return { x: x, y: x, z: x };
4775
4877
  }
4776
4878
 
4777
- _calcNorm() {
4778
- this._cnsq = this.x * this.x + this.y * this.y + this.z * this.z;
4779
- this._cn = Math.sqrt(this._cnsq);
4780
- }
4781
-
4782
4879
  add() {
4783
4880
  let u = this._arg2v(...arguments);
4784
4881
  this.x += u.x;
@@ -4822,14 +4919,71 @@ Q5.Vector = class {
4822
4919
  return this;
4823
4920
  }
4824
4921
 
4922
+ _calcMag() {
4923
+ const x = this.x,
4924
+ y = this.y,
4925
+ z = this.z;
4926
+ this._mag = Math.sqrt(x * x + y * y + z * z);
4927
+ this._magCached = this._useCache;
4928
+ }
4929
+
4825
4930
  mag() {
4826
- this._calcNorm();
4827
- return this._cn;
4931
+ if (!this._magCached) this._calcMag();
4932
+ return this._mag;
4828
4933
  }
4829
4934
 
4830
4935
  magSq() {
4831
- this._calcNorm();
4832
- return this._cnsq;
4936
+ if (this._magCached) return this._mag * this._mag;
4937
+ const x = this.x,
4938
+ y = this.y,
4939
+ z = this.z;
4940
+ return x * x + y * y + z * z;
4941
+ }
4942
+ setMag(m) {
4943
+ if (!this._magCached) this._calcMag();
4944
+ let n = this._mag;
4945
+ if (n == 0) {
4946
+ const dir = this.direction();
4947
+ this.x = m * this._$.cos(dir);
4948
+ this.y = m * this._$.sin(dir);
4949
+ } else {
4950
+ let t = m / n;
4951
+ this.x *= t;
4952
+ this.y *= t;
4953
+ this.z *= t;
4954
+ }
4955
+ this._mag = m;
4956
+ this._magCached = this._useCache;
4957
+ return this;
4958
+ }
4959
+
4960
+ direction() {
4961
+ if (!this._directionCached) {
4962
+ const x = this.x,
4963
+ y = this.y;
4964
+ if (x || y) this._direction = this._$.atan2(this.y, this.x);
4965
+ this._directionCached = this._useCache;
4966
+ }
4967
+ return this._direction;
4968
+ }
4969
+
4970
+ setDirection(ang) {
4971
+ let mag = this.mag();
4972
+ if (mag) {
4973
+ this.x = mag * this._$.cos(ang);
4974
+ this.y = mag * this._$.sin(ang);
4975
+ }
4976
+ this._direction = ang;
4977
+ this._directionCached = this._useCache;
4978
+ return this;
4979
+ }
4980
+
4981
+ heading() {
4982
+ return this.direction();
4983
+ }
4984
+
4985
+ setHeading(ang) {
4986
+ return this.setDirection(ang);
4833
4987
  }
4834
4988
 
4835
4989
  dot() {
@@ -4857,55 +5011,32 @@ Q5.Vector = class {
4857
5011
  }
4858
5012
 
4859
5013
  normalize() {
4860
- this._calcNorm();
4861
- let n = this._cn;
5014
+ if (!this._magCached) this._calcMag();
5015
+ let n = this._mag;
4862
5016
  if (n != 0) {
4863
5017
  this.x /= n;
4864
5018
  this.y /= n;
4865
5019
  this.z /= n;
4866
5020
  }
4867
- this._cn = 1;
4868
- this._cnsq = 1;
5021
+ this._mag = 1;
5022
+ this._magCached = this._useCache;
4869
5023
  return this;
4870
5024
  }
4871
5025
 
4872
5026
  limit(m) {
4873
- this._calcNorm();
4874
- let n = this._cn;
5027
+ if (!this._magCached) this._calcMag();
5028
+ let n = this._mag;
4875
5029
  if (n > m) {
4876
5030
  let t = m / n;
4877
5031
  this.x *= t;
4878
5032
  this.y *= t;
4879
5033
  this.z *= t;
4880
- this._cn = m;
4881
- this._cnsq = m * m;
5034
+ this._mag = m;
5035
+ this._magCached = this._useCache;
4882
5036
  }
4883
5037
  return this;
4884
5038
  }
4885
5039
 
4886
- setMag(m) {
4887
- this._calcNorm();
4888
- let n = this._cn;
4889
- let t = m / n;
4890
- this.x *= t;
4891
- this.y *= t;
4892
- this.z *= t;
4893
- this._cn = m;
4894
- this._cnsq = m * m;
4895
- return this;
4896
- }
4897
-
4898
- heading() {
4899
- return this._$.atan2(this.y, this.x);
4900
- }
4901
-
4902
- setHeading(ang) {
4903
- let mag = this.mag();
4904
- this.x = mag * this._$.cos(ang);
4905
- this.y = mag * this._$.sin(ang);
4906
- return this;
4907
- }
4908
-
4909
5040
  rotate(ang) {
4910
5041
  let costh = this._$.cos(ang);
4911
5042
  let sinth = this._$.sin(ang);
@@ -4989,8 +5120,8 @@ Q5.Vector = class {
4989
5120
 
4990
5121
  fromAngle(th, l) {
4991
5122
  if (l === undefined) l = 1;
4992
- this._cn = l;
4993
- this._cnsq = l * l;
5123
+ this._mag = l;
5124
+ this._magCached = this._useCache;
4994
5125
  this.x = l * this._$.cos(th);
4995
5126
  this.y = l * this._$.sin(th);
4996
5127
  this.z = 0;
@@ -4999,8 +5130,8 @@ Q5.Vector = class {
4999
5130
 
5000
5131
  fromAngles(th, ph, l) {
5001
5132
  if (l === undefined) l = 1;
5002
- this._cn = l;
5003
- this._cnsq = l * l;
5133
+ this._mag = l;
5134
+ this._magCached = this._useCache;
5004
5135
  const cosph = this._$.cos(ph);
5005
5136
  const sinph = this._$.sin(ph);
5006
5137
  const costh = this._$.cos(th);
@@ -5012,12 +5143,14 @@ Q5.Vector = class {
5012
5143
  }
5013
5144
 
5014
5145
  random2D() {
5015
- this._cn = this._cnsq = 1;
5146
+ this._mag = 1;
5147
+ this._magCached = this._useCache;
5016
5148
  return this.fromAngle(Math.random() * Math.PI * 2);
5017
5149
  }
5018
5150
 
5019
5151
  random3D() {
5020
- this._cn = this._cnsq = 1;
5152
+ this._mag = 1;
5153
+ this._magCached = this._useCache;
5021
5154
  return this.fromAngles(Math.random() * Math.PI * 2, Math.random() * Math.PI * 2);
5022
5155
  }
5023
5156
 
@@ -5035,7 +5168,7 @@ Q5.Vector.equals = (v, u, epsilon) => v.equals(u, epsilon);
5035
5168
  Q5.Vector.lerp = (v, u, amt) => v.copy().lerp(u, amt);
5036
5169
  Q5.Vector.slerp = (v, u, amt) => v.copy().slerp(u, amt);
5037
5170
  Q5.Vector.limit = (v, m) => v.copy().limit(m);
5038
- Q5.Vector.heading = (v) => this._$.atan2(v.y, v.x);
5171
+ Q5.Vector.direction = (v) => this._$.atan2(v.y, v.x);
5039
5172
  Q5.Vector.magSq = (v) => v.x * v.x + v.y * v.y + v.z * v.z;
5040
5173
  Q5.Vector.mag = (v) => Math.sqrt(Q5.Vector.magSq(v));
5041
5174
  Q5.Vector.mult = (v, u) => v.copy().mult(u);
@@ -5532,8 +5665,18 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5532
5665
  if (args.length == 1) m = args[0];
5533
5666
  else m = args;
5534
5667
 
5535
- if (m.length == 9) {
5536
- // convert 3x3 matrix to 4x4 matrix
5668
+ if (m.length <= 6) {
5669
+ const a = m[0],
5670
+ b = m[1],
5671
+ c = m[2],
5672
+ d = m[3],
5673
+ e = m[4] || 0,
5674
+ f = m[5] || 0;
5675
+ // Convert Canvas2D [a,b,c,d,e,f] (column-major 3x3: [a,b,0, c,d,0, e,f,1])
5676
+ m = [a, b, 0, c, d, 0, e, f, 1];
5677
+ }
5678
+ if (m.length <= 9) {
5679
+ // convert 3x3 matrix to 4x4 layout used internally
5537
5680
  m = [m[0], m[1], 0, m[2], m[3], m[4], 0, m[5], 0, 0, 1, 0, m[6], m[7], 0, m[8]];
5538
5681
  } else if (m.length != 16) {
5539
5682
  throw new Error('Matrix must be a 3x3 or 4x4 array.');
@@ -6674,10 +6817,10 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6674
6817
  hw = w;
6675
6818
  hh = h;
6676
6819
  } else if (_rectMode == 'corners') {
6677
- hw = (x - w) / 2;
6678
- hh = (y - h) / 2;
6679
- x += hw;
6680
- y += hh;
6820
+ hw = Math.abs((w - x) / 2);
6821
+ hh = Math.abs((h - y) / 2);
6822
+ x = (x + w) / 2;
6823
+ y = (y + h) / 2;
6681
6824
  }
6682
6825
  }
6683
6826
  rectModeCache[0] = x;
@@ -6696,7 +6839,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6696
6839
  addRect(x, y, hw, hh, rr, doStroke ? sw : 0, doFill ? fillIdx : 0);
6697
6840
  };
6698
6841
 
6699
- $.square = (x, y, s) => $.rect(x, y, s, s);
6842
+ $.square = (x, y, s, rr) => $.rect(x, y, s, s, rr);
6700
6843
 
6701
6844
  function addCapsule(x1, y1, x2, y2, r, strokeW, fillCapsule) {
6702
6845
  let dx = x2 - x1,
@@ -6982,8 +7125,8 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6982
7125
  } else if (_ellipseMode == 'corners') {
6983
7126
  x = (x + w) / 2;
6984
7127
  y = (y + h) / 2;
6985
- a = (w - x) / 2;
6986
- b = (h - y) / 2;
7128
+ a = w - x;
7129
+ b = h - y;
6987
7130
  }
6988
7131
  ellipseModeCache[0] = x;
6989
7132
  ellipseModeCache[1] = y;
@@ -7338,9 +7481,9 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
7338
7481
  };
7339
7482
 
7340
7483
  $.loadImage = (src, cb) => {
7341
- let g = $._g.loadImage(src, () => {
7342
- $._makeDrawable(g);
7343
- if (cb) cb(g);
7484
+ let g = $._g.loadImage(src, (img) => {
7485
+ $._makeDrawable(img);
7486
+ if (cb) cb(img);
7344
7487
  });
7345
7488
  return g;
7346
7489
  };
@@ -7707,18 +7850,27 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7707
7850
 
7708
7851
  let fontsArr = [];
7709
7852
  let fonts = {};
7853
+ let fontSet;
7710
7854
 
7711
- async function createFont(fontJsonUrl, fontName, cb) {
7712
- let res = await fetch(fontJsonUrl);
7713
- if (res.status == 404) return '';
7714
-
7715
- let atlas = await res.json();
7855
+ async function createFont(url, fontName, cb) {
7856
+ let baseUrl = url.substring(0, url.lastIndexOf('-'));
7716
7857
 
7717
- let slashIdx = fontJsonUrl.lastIndexOf('/');
7718
- let baseUrl = slashIdx != -1 ? fontJsonUrl.substring(0, slashIdx + 1) : '';
7719
- // load font image
7720
- res = await fetch(baseUrl + atlas.pages[0]);
7721
- let img = await createImageBitmap(await res.blob());
7858
+ // load atlas and image in parallel
7859
+ let atlas, img;
7860
+ try {
7861
+ [atlas, img] = await Promise.all([
7862
+ fetch(url).then((res) => {
7863
+ if (res.status == 404) throw new Error('404');
7864
+ return res.json();
7865
+ }),
7866
+ fetch(baseUrl + '.png')
7867
+ .then((res) => res.blob())
7868
+ .then((blob) => createImageBitmap(blob))
7869
+ ]);
7870
+ } catch (error) {
7871
+ console.error('Error loading font:', error);
7872
+ return '';
7873
+ }
7722
7874
 
7723
7875
  // convert image to texture
7724
7876
  let imgSize = [img.width, img.height, 1];
@@ -7786,44 +7938,54 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7786
7938
  }
7787
7939
  }
7788
7940
 
7789
- $._font = new MsdfFont(fontBindGroup, atlas.common.lineHeight, chars, kernings);
7790
-
7791
- $._font.index = fontsArr.length;
7792
- fontsArr.push($._font);
7793
- fonts[fontName] = $._font;
7941
+ let _font = new MsdfFont(fontBindGroup, atlas.common.lineHeight, chars, kernings);
7942
+ _font.index = fontsArr.length;
7943
+ fontsArr.push(_font);
7944
+ fonts[fontName] = _font;
7945
+ $._font = _font;
7794
7946
 
7795
7947
  if (cb) cb(fontName);
7948
+ return { family: fontName };
7796
7949
  }
7797
7950
 
7798
- $.loadFont = (url, cb) => {
7951
+ $.loadFont = (url = 'sans-serif', cb) => {
7952
+ fontSet = true;
7799
7953
  if (url.startsWith('https://fonts.googleapis.com/css')) {
7800
7954
  return $._g.loadFont(url, cb);
7801
7955
  }
7802
7956
 
7803
7957
  let ext = url.slice(url.lastIndexOf('.') + 1);
7804
- if (url == ext) return $._loadDefaultFont(url, cb);
7958
+
7959
+ // if not a url, assume it's one of q5's MSDF fonts
7960
+ if (url == ext) {
7961
+ let fontName = url;
7962
+ fonts[fontName] = null;
7963
+ url = `https://q5js.org/fonts/${fontName}-msdf.json`;
7964
+ if (Q5.online == false || !navigator.onLine) {
7965
+ url = `/node_modules/q5/builtinFonts/${fontName}-msdf.json`;
7966
+ }
7967
+ ext = 'json';
7968
+ }
7969
+
7805
7970
  if (ext != 'json') return $._g.loadFont(url, cb);
7971
+
7806
7972
  let fontName = url.slice(url.lastIndexOf('/') + 1, url.lastIndexOf('-'));
7807
7973
  let f = { family: fontName };
7808
7974
  f.promise = createFont(url, fontName, () => {
7809
7975
  delete f.promise;
7976
+ delete f.then;
7977
+ if (f._usedAwait) f = { family: fontName };
7810
7978
  if (cb) cb(f);
7811
7979
  });
7812
- $._preloadPromises.push(f.promise);
7980
+ $._loaders.push(f.promise);
7813
7981
 
7814
- if (!$._usePreload) return f.promise;
7982
+ f.then = (resolve, reject) => {
7983
+ f._usedAwait = true;
7984
+ return f.promise.then(resolve, reject);
7985
+ };
7815
7986
  return f;
7816
7987
  };
7817
7988
 
7818
- $._loadDefaultFont = (fontName, cb) => {
7819
- fonts[fontName] = null;
7820
- let url = `https://q5js.org/fonts/${fontName}-msdf.json`;
7821
- if (Q5.online == false || !navigator.onLine) {
7822
- url = `/node_modules/q5/builtinFonts/${fontName}-msdf.json`;
7823
- }
7824
- return $.loadFont(url, cb);
7825
- };
7826
-
7827
7989
  let _textSize = 18,
7828
7990
  _textAlign = 'left',
7829
7991
  _textBaseline = 'alphabetic',
@@ -7832,12 +7994,16 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7832
7994
  leadDiff = 4.5,
7833
7995
  leadPercent = 1.25;
7834
7996
 
7997
+ let categories = ['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui'];
7998
+
7835
7999
  $.textFont = (fontName) => {
7836
8000
  if (!fontName) return $._font;
8001
+ fontSet = true;
7837
8002
  if (typeof fontName != 'string') fontName = fontName.family;
7838
8003
  let font = fonts[fontName];
7839
8004
  if (font) $._font = font;
7840
- else if (font === undefined) return $._loadDefaultFont(fontName);
8005
+ // if it's a font category or not a WebGPU font, set the Canvas2D font
8006
+ else if (categories[fontName] || font === undefined) $._g.textFont(fontName);
7841
8007
  };
7842
8008
 
7843
8009
  $.textSize = (size) => {
@@ -7895,8 +8061,8 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7895
8061
  let lineWidthsCache = new Array(100);
7896
8062
 
7897
8063
  // Reusable buffers for text data to avoid creating new arrays
7898
- let charDataBuffer = new Float32Array(100000); // reusable buffer for char data
7899
- let textDataBuffer = new Float32Array(10000); // reusable buffer for text metadata
8064
+ let charDataBuffer = new Float32Array(Q5.MAX_CHARS * 4); // reusable buffer for char data
8065
+ let textDataBuffer = new Float32Array(Q5.MAX_TEXTS * 8); // reusable buffer for text metadata
7900
8066
 
7901
8067
  let measureText = (font, text, charCallback) => {
7902
8068
  let maxWidth = 0,
@@ -7948,18 +8114,21 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7948
8114
  };
7949
8115
 
7950
8116
  $.text = (str, x, y, w, h) => {
7951
- if (!$._font) {
7952
- // if the default font hasn't been loaded yet, try to load it
7953
- if ($._font !== null) $.textFont('sans-serif');
7954
- if (_textSize >= 1) return $.textImage(str, x, y, w, h);
7955
- }
7956
-
7957
8117
  if (_textSize < 1) return;
7958
8118
 
7959
8119
  let type = typeof str;
7960
8120
  if (type != 'string') {
7961
8121
  if (type == 'object') str = str.toString();
7962
8122
  else str = str + '';
8123
+ } else if (!str.length) return;
8124
+
8125
+ // if not using an MSDF font
8126
+ if (!$._font) {
8127
+ // if no font is set, lazy load the default MSDF font
8128
+ if (!fontSet) $.loadFont();
8129
+ // use Canvas2D text rendering
8130
+ let img = $.createTextImage(str, w, h);
8131
+ return $.textImage(img, x, y);
7963
8132
  }
7964
8133
 
7965
8134
  if (str.length > w) {
@@ -8196,6 +8365,8 @@ Q5.BLUR = 8;
8196
8365
  Q5.MAX_TRANSFORMS = 1e7;
8197
8366
  Q5.MAX_RECTS = 200200;
8198
8367
  Q5.MAX_ELLIPSES = 200200;
8368
+ Q5.MAX_CHARS = 100000;
8369
+ Q5.MAX_TEXTS = 10000;
8199
8370
 
8200
8371
  Q5.initWebGPU = async () => {
8201
8372
  if (!navigator.gpu) {