q5 3.5.1 → 3.6.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.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,16 @@ function createCanvas(w, h, opt) {
437
438
  }
438
439
  }
439
440
 
440
- if (Q5._server) global.p5 ??= global.Q5 = Q5;
441
+ if (Q5._server) global.p5 ??= global.q5 = global.Q5 = Q5;
441
442
 
442
443
  if (typeof window == 'object') {
443
- window.p5 ??= window.Q5 = Q5;
444
+ window.p5 ??= window.q5 = window.Q5 = Q5;
444
445
  window.createCanvas = createCanvas;
445
446
  window.C2D = 'c2d';
446
447
  window.WEBGPU = 'webgpu';
447
448
  } else global.window = 0;
448
449
 
449
- Q5.version = Q5.VERSION = '3.5';
450
+ Q5.version = Q5.VERSION = '3.6';
450
451
 
451
452
  if (typeof document == 'object') {
452
453
  document.addEventListener('DOMContentLoaded', () => {
@@ -1388,11 +1389,10 @@ Q5.renderers.c2d.image = ($, q) => {
1388
1389
  let imgData = null,
1389
1390
  pixels = null;
1390
1391
 
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);
1392
+ $.createImage = (w, h, opt = {}) => {
1393
+ opt.colorSpace ??= $.canvas.colorSpace;
1394
+ opt.defaultImageScale ??= $._defaultImageScale;
1395
+ return new Q5.Image(w, h, opt);
1396
1396
  };
1397
1397
 
1398
1398
  $.loadImage = function (url, cb, opt) {
@@ -1406,7 +1406,7 @@ Q5.renderers.c2d.image = ($, q) => {
1406
1406
  if (typeof last == 'object') {
1407
1407
  opt = last;
1408
1408
  cb = null;
1409
- } else opt = null;
1409
+ } else opt = undefined;
1410
1410
 
1411
1411
  let g = $.createImage(1, 1, opt);
1412
1412
  let pd = g._pixelDensity;
@@ -1416,6 +1416,10 @@ Q5.renderers.c2d.image = ($, q) => {
1416
1416
 
1417
1417
  g.promise = new Promise((resolve, reject) => {
1418
1418
  img.onload = () => {
1419
+ delete g.promise;
1420
+ delete g.then;
1421
+ if (g._usedAwait) g = $.createImage(1, 1, opt);
1422
+
1419
1423
  img._pixelDensity = pd;
1420
1424
  g.defaultWidth = img.width * $._defaultImageScale;
1421
1425
  g.defaultHeight = img.height * $._defaultImageScale;
@@ -1425,16 +1429,19 @@ Q5.renderers.c2d.image = ($, q) => {
1425
1429
 
1426
1430
  g.ctx.drawImage(img, 0, 0);
1427
1431
  if (cb) cb(g);
1428
- delete g.promise;
1429
1432
  resolve(g);
1430
1433
  };
1431
1434
  img.onerror = reject;
1432
1435
  });
1433
- $._preloadPromises.push(g.promise);
1436
+ $._loaders.push(g.promise);
1434
1437
 
1435
- g.src = img.src = url;
1438
+ // then only runs when the user awaits the instance
1439
+ g.then = (resolve, reject) => {
1440
+ g._usedAwait = true;
1441
+ return g.promise.then(resolve, reject);
1442
+ };
1436
1443
 
1437
- if (!$._usePreload) return g.promise;
1444
+ g.src = img.src = url;
1438
1445
  return g;
1439
1446
  };
1440
1447
 
@@ -1695,7 +1702,9 @@ Q5.renderers.c2d.image = ($, q) => {
1695
1702
  };
1696
1703
 
1697
1704
  Q5.Image = class {
1698
- constructor(q, w, h, opt = {}) {
1705
+ constructor(w, h, opt = {}) {
1706
+ opt.alpha ??= true;
1707
+ opt.colorSpace ??= Q5.canvasOptions.colorSpace;
1699
1708
  let $ = this;
1700
1709
  $._isImage = true;
1701
1710
  $.canvas = $.ctx = $.drawingContext = null;
@@ -1706,7 +1715,7 @@ Q5.Image = class {
1706
1715
  if (r[m]) r[m]($, $);
1707
1716
  }
1708
1717
  $._pixelDensity = opt.pixelDensity || 1;
1709
- $._defaultImageScale = opt.defaultImageScale || q._defaultImageScale;
1718
+ $._defaultImageScale = opt.defaultImageScale || 2;
1710
1719
  $.createCanvas(w, h, opt);
1711
1720
  let scale = $._pixelDensity * $._defaultImageScale;
1712
1721
  $.defaultWidth = w * scale;
@@ -1878,13 +1887,11 @@ Q5.renderers.c2d.text = ($, q) => {
1878
1887
  emphasis = 'normal',
1879
1888
  weight = 'normal',
1880
1889
  styleHash = 0,
1881
- styleHashes = [],
1882
1890
  genTextImage = false,
1883
1891
  cacheSize = 0;
1884
1892
  $._fontMod = false;
1885
1893
 
1886
1894
  let cache = ($._textCache = {});
1887
- $._textCacheMaxSize = 12000;
1888
1895
 
1889
1896
  $.loadFont = (url, cb) => {
1890
1897
  let f;
@@ -1894,25 +1901,29 @@ Q5.renderers.c2d.text = ($, q) => {
1894
1901
  } else {
1895
1902
  let name = url.split('/').pop().split('.')[0].replace(' ', '');
1896
1903
 
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
- })();
1904
+ f = { family: name };
1905
+ let ff = new FontFace(name, `url(${encodeURI(url)})`);
1906
+ document.fonts.add(ff);
1907
+
1908
+ f.promise = new Promise((resolve, reject) => {
1909
+ ff.load()
1910
+ .then(() => {
1911
+ delete f.promise;
1912
+ delete f.then;
1913
+ if (cb) cb(ff);
1914
+ resolve(ff);
1915
+ })
1916
+ .catch((err) => {
1917
+ reject(err);
1918
+ });
1919
+ });
1911
1920
  }
1912
-
1913
- $._preloadPromises.push(f.promise);
1921
+ $._loaders.push(f.promise);
1914
1922
  $.textFont(f.family);
1915
- if (!$._usePreload) return f.promise;
1923
+ f.then = (resolve, reject) => {
1924
+ f._usedAwait = true;
1925
+ return f.promise.then(resolve, reject);
1926
+ };
1916
1927
  return f;
1917
1928
  };
1918
1929
 
@@ -1980,8 +1991,13 @@ Q5.renderers.c2d.text = ($, q) => {
1980
1991
  }
1981
1992
  }
1982
1993
 
1994
+ if (f._usedAwait) {
1995
+ f = { family: fontFamily };
1996
+ }
1997
+
1983
1998
  f.faces = loadedFaces;
1984
1999
  delete f.promise;
2000
+ delete f.then;
1985
2001
  if (cb) cb(f);
1986
2002
  return f;
1987
2003
  } catch (e) {
@@ -2086,7 +2102,7 @@ Q5.renderers.c2d.text = ($, q) => {
2086
2102
  if (str === undefined || (!$._doFill && !$._doStroke)) return;
2087
2103
  str = str.toString();
2088
2104
  let ctx = $.ctx;
2089
- let img, colorStyle;
2105
+ let img, colorStyle, styleCache, colorCache, recycling;
2090
2106
 
2091
2107
  if ($._fontMod) $._updateFont();
2092
2108
 
@@ -2094,14 +2110,23 @@ Q5.renderers.c2d.text = ($, q) => {
2094
2110
  if (styleHash == -1) updateStyleHash();
2095
2111
  colorStyle = $._fill + $._stroke + $._strokeWeight;
2096
2112
 
2097
- let cacheLevel1 = cache[str];
2098
- let cacheLevel2;
2099
- if (cacheLevel1) cacheLevel2 = cacheLevel1[styleHash];
2113
+ styleCache = cache[str];
2114
+ if (styleCache) colorCache = styleCache[styleHash];
2115
+ else styleCache = cache[str] = {};
2100
2116
 
2101
- if (cacheLevel2) {
2102
- img = cacheLevel2[colorStyle];
2117
+ if (colorCache) {
2118
+ img = colorCache[colorStyle];
2103
2119
  if (img) return img;
2104
- }
2120
+
2121
+ if (colorCache.size >= 4) {
2122
+ for (let recycleKey in colorCache) {
2123
+ img = colorCache[recycleKey];
2124
+ delete colorCache[recycleKey];
2125
+ break;
2126
+ }
2127
+ recycling = true;
2128
+ }
2129
+ } else colorCache = styleCache[styleHash] = {};
2105
2130
  }
2106
2131
 
2107
2132
  if (str.indexOf('\n') == -1) lines[0] = str;
@@ -2169,6 +2194,8 @@ Q5.renderers.c2d.text = ($, q) => {
2169
2194
  img._bottom = img._top + ascent + leading * (lines.length - 1);
2170
2195
  img._leading = leading;
2171
2196
  } else {
2197
+ let cnv = img.canvas;
2198
+ img.ctx.clearRect(0, 0, cnv.width, cnv.height);
2172
2199
  img.modified = true;
2173
2200
  }
2174
2201
 
@@ -2199,19 +2226,33 @@ Q5.renderers.c2d.text = ($, q) => {
2199
2226
  if (!$._fillSet) ctx.fillStyle = ogFill;
2200
2227
 
2201
2228
  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];
2229
+ colorCache[colorStyle] = img;
2230
+
2231
+ if (!recycling) {
2232
+ if (!colorCache.size) {
2233
+ Object.defineProperty(colorCache, 'size', {
2234
+ writable: true,
2235
+ enumerable: false
2236
+ });
2237
+ colorCache.size = 0;
2213
2238
  }
2214
- cacheSize -= half;
2239
+ colorCache.size++;
2240
+ cacheSize++;
2241
+ }
2242
+
2243
+ if (cacheSize > Q5.MAX_TEXT_IMAGES) {
2244
+ for (const str in cache) {
2245
+ styleCache = cache[str];
2246
+ for (const hash in styleCache) {
2247
+ colorCache = styleCache[hash];
2248
+ for (let c in colorCache) {
2249
+ let _img = colorCache[c];
2250
+ if (_img._texture) _img._texture.destroy();
2251
+ delete colorCache[c];
2252
+ }
2253
+ }
2254
+ }
2255
+ cacheSize = 0;
2215
2256
  }
2216
2257
  return img;
2217
2258
  }
@@ -2239,6 +2280,7 @@ Q5.renderers.c2d.text = ($, q) => {
2239
2280
  };
2240
2281
 
2241
2282
  Q5.fonts = [];
2283
+ Q5.MAX_TEXT_IMAGES = 5000;
2242
2284
  Q5.modules.color = ($, q) => {
2243
2285
  $.RGB = $.RGBA = $.RGBHDR = $._colorMode = 'rgb';
2244
2286
  $.HSL = 'hsl';
@@ -2275,6 +2317,7 @@ Q5.modules.color = ($, q) => {
2275
2317
  black: [0, 0, 0],
2276
2318
  blue: [0, 0, 255],
2277
2319
  brown: [165, 42, 42],
2320
+ coral: [255, 127, 80],
2278
2321
  crimson: [220, 20, 60],
2279
2322
  cyan: [0, 255, 255],
2280
2323
  darkviolet: [148, 0, 211],
@@ -3112,29 +3155,38 @@ Q5.modules.dom = ($, q) => {
3112
3155
 
3113
3156
  $.createSpan = (content) => $.createEl('span', content);
3114
3157
 
3158
+ function initVideo(el) {
3159
+ el.width ||= el.videoWidth;
3160
+ el.height ||= el.videoHeight;
3161
+ el.defaultWidth = el.width * $._defaultImageScale;
3162
+ el.defaultHeight = el.height * $._defaultImageScale;
3163
+ el.ready = true;
3164
+ }
3165
+
3115
3166
  $.createVideo = (src) => {
3116
3167
  let el = $.createEl('video');
3117
3168
  el.crossOrigin = 'anonymous';
3118
3169
 
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
3170
  if (src) {
3128
3171
  el.promise = new Promise((resolve) => {
3129
3172
  el.addEventListener('loadeddata', () => {
3130
- el._load();
3173
+ delete el.promise;
3174
+ delete el.then;
3175
+ if (el._usedAwait) {
3176
+ el = $.createEl('video');
3177
+ el.crossOrigin = 'anonymous';
3178
+ el.src = src;
3179
+ }
3180
+ initVideo(el);
3131
3181
  resolve(el);
3132
3182
  });
3133
3183
  el.src = src;
3134
3184
  });
3135
- $._preloadPromises.push(el.promise);
3136
-
3137
- if (!$._usePreload) return el.promise;
3185
+ $._loaders.push(el.promise);
3186
+ el.then = (resolve, reject) => {
3187
+ el._usedAwait = true;
3188
+ return el.promise.then(resolve, reject);
3189
+ };
3138
3190
  }
3139
3191
  return el;
3140
3192
  };
@@ -3149,18 +3201,22 @@ Q5.modules.dom = ($, q) => {
3149
3201
  constraints.video.facingMode ??= 'user';
3150
3202
 
3151
3203
  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
- };
3204
+
3205
+ function extendVideo(vid) {
3206
+ vid.playsinline = vid.autoplay = true;
3207
+ if (flipped) {
3208
+ vid.flipped = true;
3209
+ vid.style.transform = 'scale(-1, 1)';
3210
+ }
3211
+ vid.loadPixels = () => {
3212
+ let g = $.createGraphics(vid.videoWidth, vid.videoHeight, { renderer: 'c2d' });
3213
+ g.image(vid, 0, 0);
3214
+ g.loadPixels();
3215
+ vid.pixels = g.pixels;
3216
+ g.remove();
3217
+ };
3218
+ }
3219
+
3164
3220
  vid.promise = (async () => {
3165
3221
  let stream;
3166
3222
  try {
@@ -3169,16 +3225,26 @@ Q5.modules.dom = ($, q) => {
3169
3225
  throw e;
3170
3226
  }
3171
3227
 
3228
+ delete vid.promise;
3229
+ delete vid.then;
3230
+ if (vid._usedAwait) {
3231
+ vid = $.createVideo();
3232
+ }
3233
+ extendVideo(vid);
3234
+
3172
3235
  vid.srcObject = stream;
3173
3236
  await new Promise((resolve) => vid.addEventListener('loadeddata', resolve));
3174
3237
 
3175
- vid._load();
3238
+ initVideo(vid);
3176
3239
  if (cb) cb(vid);
3177
3240
  return vid;
3178
3241
  })();
3179
- $._preloadPromises.push(vid.promise);
3242
+ $._loaders.push(vid.promise);
3180
3243
 
3181
- if (!$._usePreload) return vid.promise;
3244
+ vid.then = (resolve, reject) => {
3245
+ vid._usedAwait = true;
3246
+ return vid.promise.then(resolve, reject);
3247
+ };
3182
3248
  return vid;
3183
3249
  };
3184
3250
 
@@ -3258,12 +3324,12 @@ Q5.modules.fes = ($) => {
3258
3324
  }
3259
3325
  }
3260
3326
 
3261
- if (Q5.online != false && typeof navigator != undefined && navigator.onLine) {
3327
+ if ($._isGlobal && Q5.online != false && typeof navigator != undefined && navigator.onLine) {
3262
3328
  async function checkLatestVersion() {
3263
3329
  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();
3330
+ let res = await fetch('https://data.jsdelivr.com/v1/package/npm/q5');
3331
+ if (!res.ok) return;
3332
+ let data = await res.json();
3267
3333
  let l = data.tags.latest;
3268
3334
  l = l.slice(0, l.lastIndexOf('.'));
3269
3335
  if (l != Q5.version) {
@@ -4352,6 +4418,11 @@ Q5.modules.sound = ($, q) => {
4352
4418
  sounds.push(s);
4353
4419
 
4354
4420
  s.promise = (async () => {
4421
+ if (s._usedAwait) {
4422
+ sounds.splice(sounds.indexOf(s), 1);
4423
+ s = new Q5.Sound();
4424
+ sounds.push(s);
4425
+ }
4355
4426
  let err;
4356
4427
  try {
4357
4428
  await s.load(url);
@@ -4359,13 +4430,17 @@ Q5.modules.sound = ($, q) => {
4359
4430
  err = e;
4360
4431
  }
4361
4432
  delete s.promise;
4433
+ delete s.then;
4362
4434
  if (err) throw err;
4363
4435
  if (cb) cb(s);
4364
4436
  return s;
4365
4437
  })();
4366
- $._preloadPromises.push(s.promise);
4438
+ $._loaders.push(s.promise);
4367
4439
 
4368
- if (!$._usePreload) return s.promise;
4440
+ s.then = (resolve, reject) => {
4441
+ s._usedAwait = true;
4442
+ return s.promise.then(resolve, reject);
4443
+ };
4369
4444
  return s;
4370
4445
  };
4371
4446
 
@@ -4374,19 +4449,30 @@ Q5.modules.sound = ($, q) => {
4374
4449
  a._isAudio = true;
4375
4450
  a.crossOrigin = 'Anonymous';
4376
4451
  a.promise = new Promise((resolve, reject) => {
4377
- a.addEventListener('canplay', () => {
4452
+ function loaded() {
4378
4453
  if (!a.loaded) {
4454
+ delete a.promise;
4455
+ delete a.then;
4456
+ if (a._usedAwait) {
4457
+ a = new Audio(url);
4458
+ a._isAudio = true;
4459
+ a.crossOrigin = 'Anonymous';
4460
+ }
4379
4461
  a.loaded = true;
4380
4462
  if (cb) cb(a);
4381
4463
  resolve(a);
4382
4464
  }
4383
- });
4384
- a.addEventListener('suspend', resolve);
4465
+ }
4466
+ a.addEventListener('canplay', loaded);
4467
+ a.addEventListener('suspend', loaded);
4385
4468
  a.addEventListener('error', reject);
4386
4469
  });
4387
- $._preloadPromises.push(a.promise);
4470
+ $._loaders.push(a.promise);
4388
4471
 
4389
- if (!$._usePreload) return a.promise;
4472
+ a.then = (resolve, reject) => {
4473
+ a._usedAwait = true;
4474
+ return a.promise.then(resolve, reject);
4475
+ };
4390
4476
  return a;
4391
4477
  };
4392
4478
 
@@ -4571,27 +4657,30 @@ Q5.Sound = class {
4571
4657
  Q5.modules.util = ($, q) => {
4572
4658
  $._loadFile = (url, cb, type) => {
4573
4659
  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 = $.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;
4660
+ ret.promise = fetch(url)
4661
+ .then((res) => {
4662
+ if (!res.ok) {
4663
+ reject('error loading file');
4664
+ return null;
4665
+ }
4666
+ return type == 'json' ? res.json() : res.text();
4667
+ })
4668
+ .then((f) => {
4669
+ if (type == 'csv') f = Q5.CSV.parse(f);
4670
+
4671
+ if (typeof f == 'string') ret.text = f;
4672
+ else Object.assign(ret, f);
4673
+
4674
+ delete ret.promise;
4675
+ delete ret.then;
4676
+ if (cb) cb(f);
4677
+ return f;
4678
+ });
4679
+ $._loaders.push(ret.promise);
4680
+
4681
+ ret.then = (resolve, reject) => {
4682
+ return ret.promise.then(resolve, reject);
4683
+ };
4595
4684
  return ret;
4596
4685
  };
4597
4686
 
@@ -4607,11 +4696,15 @@ Q5.modules.util = ($, q) => {
4607
4696
  let xml = new DOMParser().parseFromString(text, 'application/xml');
4608
4697
  ret.DOM = xml;
4609
4698
  delete ret.promise;
4699
+ delete ret.then;
4610
4700
  if (cb) cb(xml);
4611
4701
  return xml;
4612
4702
  });
4613
- $._preloadPromises.push(ret.promise);
4614
- if (!$._usePreload) return ret.promise;
4703
+ $._loaders.push(ret.promise);
4704
+
4705
+ ret.then = (resolve, reject) => {
4706
+ return ret.promise.then(resolve, reject);
4707
+ };
4615
4708
  return ret;
4616
4709
  };
4617
4710
 
@@ -4623,7 +4716,7 @@ Q5.modules.util = ($, q) => {
4623
4716
  $.load = function (...urls) {
4624
4717
  if (Array.isArray(urls[0])) urls = urls[0];
4625
4718
 
4626
- let promises = [];
4719
+ let thenables = [];
4627
4720
 
4628
4721
  for (let url of urls) {
4629
4722
  let ext = url.split('.').pop().toLowerCase();
@@ -4644,11 +4737,11 @@ Q5.modules.util = ($, q) => {
4644
4737
  } else {
4645
4738
  obj = $.loadText(url);
4646
4739
  }
4647
- promises.push($._usePreload ? obj.promise : obj);
4740
+ thenables.push(obj);
4648
4741
  }
4649
4742
 
4650
- if (urls.length == 1) return promises[0];
4651
- return Promise.all(promises);
4743
+ if (urls.length == 1) return thenables[0];
4744
+ return Promise.all(thenables);
4652
4745
  };
4653
4746
 
4654
4747
  async function saveFile(data, name, ext) {
@@ -4688,21 +4781,6 @@ Q5.modules.util = ($, q) => {
4688
4781
  } else saveFile(a);
4689
4782
  };
4690
4783
 
4691
- $.CSV = {};
4692
- $.CSV.parse = (csv, sep = ',', lineSep = '\n') => {
4693
- if (!csv.length) return [];
4694
- let a = [],
4695
- lns = csv.split(lineSep),
4696
- headers = lns[0].split(sep).map((h) => h.replaceAll('"', ''));
4697
- for (let i = 1; i < lns.length; i++) {
4698
- let o = {},
4699
- ln = lns[i].split(sep);
4700
- headers.forEach((h, i) => (o[h] = JSON.parse(ln[i])));
4701
- a.push(o);
4702
- }
4703
- return a;
4704
- };
4705
-
4706
4784
  if ($.canvas && !Q5._createServerCanvas) {
4707
4785
  $.canvas.save = $.saveCanvas = $.save;
4708
4786
  }
@@ -4739,6 +4817,21 @@ Q5.modules.util = ($, q) => {
4739
4817
  return a;
4740
4818
  };
4741
4819
  };
4820
+
4821
+ Q5.CSV = {};
4822
+ Q5.CSV.parse = (csv, sep = ',', lineSep = '\n') => {
4823
+ if (!csv.length) return [];
4824
+ let a = [],
4825
+ lns = csv.split(lineSep),
4826
+ headers = lns[0].split(sep).map((h) => h.replaceAll('"', ''));
4827
+ for (let i = 1; i < lns.length; i++) {
4828
+ let o = {},
4829
+ ln = lns[i].split(sep);
4830
+ headers.forEach((h, i) => (o[h] = JSON.parse(ln[i])));
4831
+ a.push(o);
4832
+ }
4833
+ return a;
4834
+ };
4742
4835
  Q5.modules.vector = ($) => {
4743
4836
  $.Vector = Q5.Vector;
4744
4837
  $.createVector = (x, y, z) => new $.Vector(x, y, z, $);
@@ -5532,8 +5625,18 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5532
5625
  if (args.length == 1) m = args[0];
5533
5626
  else m = args;
5534
5627
 
5535
- if (m.length == 9) {
5536
- // convert 3x3 matrix to 4x4 matrix
5628
+ if (m.length <= 6) {
5629
+ const a = m[0],
5630
+ b = m[1],
5631
+ c = m[2],
5632
+ d = m[3],
5633
+ e = m[4] || 0,
5634
+ f = m[5] || 0;
5635
+ // Convert Canvas2D [a,b,c,d,e,f] (column-major 3x3: [a,b,0, c,d,0, e,f,1])
5636
+ m = [a, b, 0, c, d, 0, e, f, 1];
5637
+ }
5638
+ if (m.length <= 9) {
5639
+ // convert 3x3 matrix to 4x4 layout used internally
5537
5640
  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
5641
  } else if (m.length != 16) {
5539
5642
  throw new Error('Matrix must be a 3x3 or 4x4 array.');
@@ -6674,10 +6777,10 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6674
6777
  hw = w;
6675
6778
  hh = h;
6676
6779
  } else if (_rectMode == 'corners') {
6677
- hw = (x - w) / 2;
6678
- hh = (y - h) / 2;
6679
- x += hw;
6680
- y += hh;
6780
+ hw = Math.abs((w - x) / 2);
6781
+ hh = Math.abs((h - y) / 2);
6782
+ x = (x + w) / 2;
6783
+ y = (y + h) / 2;
6681
6784
  }
6682
6785
  }
6683
6786
  rectModeCache[0] = x;
@@ -6696,7 +6799,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6696
6799
  addRect(x, y, hw, hh, rr, doStroke ? sw : 0, doFill ? fillIdx : 0);
6697
6800
  };
6698
6801
 
6699
- $.square = (x, y, s) => $.rect(x, y, s, s);
6802
+ $.square = (x, y, s, rr) => $.rect(x, y, s, s, rr);
6700
6803
 
6701
6804
  function addCapsule(x1, y1, x2, y2, r, strokeW, fillCapsule) {
6702
6805
  let dx = x2 - x1,
@@ -6982,8 +7085,8 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6982
7085
  } else if (_ellipseMode == 'corners') {
6983
7086
  x = (x + w) / 2;
6984
7087
  y = (y + h) / 2;
6985
- a = (w - x) / 2;
6986
- b = (h - y) / 2;
7088
+ a = w - x;
7089
+ b = h - y;
6987
7090
  }
6988
7091
  ellipseModeCache[0] = x;
6989
7092
  ellipseModeCache[1] = y;
@@ -7338,9 +7441,9 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
7338
7441
  };
7339
7442
 
7340
7443
  $.loadImage = (src, cb) => {
7341
- let g = $._g.loadImage(src, () => {
7342
- $._makeDrawable(g);
7343
- if (cb) cb(g);
7444
+ let g = $._g.loadImage(src, (img) => {
7445
+ $._makeDrawable(img);
7446
+ if (cb) cb(img);
7344
7447
  });
7345
7448
  return g;
7346
7449
  };
@@ -7707,18 +7810,27 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7707
7810
 
7708
7811
  let fontsArr = [];
7709
7812
  let fonts = {};
7813
+ let fontSet;
7710
7814
 
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();
7815
+ async function createFont(url, fontName, cb) {
7816
+ let baseUrl = url.substring(0, url.lastIndexOf('-'));
7716
7817
 
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());
7818
+ // load atlas and image in parallel
7819
+ let atlas, img;
7820
+ try {
7821
+ [atlas, img] = await Promise.all([
7822
+ fetch(url).then((res) => {
7823
+ if (res.status == 404) throw new Error('404');
7824
+ return res.json();
7825
+ }),
7826
+ fetch(baseUrl + '.png')
7827
+ .then((res) => res.blob())
7828
+ .then((blob) => createImageBitmap(blob))
7829
+ ]);
7830
+ } catch (error) {
7831
+ console.error('Error loading font:', error);
7832
+ return '';
7833
+ }
7722
7834
 
7723
7835
  // convert image to texture
7724
7836
  let imgSize = [img.width, img.height, 1];
@@ -7733,8 +7845,8 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7733
7845
  // chars and kernings can be stored as csv strings, making the file
7734
7846
  // size smaller, but they need to be parsed into arrays of objects
7735
7847
  if (typeof atlas.chars == 'string') {
7736
- atlas.chars = $.CSV.parse(atlas.chars, ' ');
7737
- atlas.kernings = $.CSV.parse(atlas.kernings, ' ');
7848
+ atlas.chars = Q5.CSV.parse(atlas.chars, ' ');
7849
+ atlas.kernings = Q5.CSV.parse(atlas.kernings, ' ');
7738
7850
  }
7739
7851
 
7740
7852
  let charCount = atlas.chars.length;
@@ -7786,44 +7898,54 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7786
7898
  }
7787
7899
  }
7788
7900
 
7789
- $._font = new MsdfFont(fontBindGroup, atlas.common.lineHeight, chars, kernings);
7790
-
7791
- $._font.index = fontsArr.length;
7792
- fontsArr.push($._font);
7793
- fonts[fontName] = $._font;
7901
+ let _font = new MsdfFont(fontBindGroup, atlas.common.lineHeight, chars, kernings);
7902
+ _font.index = fontsArr.length;
7903
+ fontsArr.push(_font);
7904
+ fonts[fontName] = _font;
7905
+ $._font = _font;
7794
7906
 
7795
7907
  if (cb) cb(fontName);
7908
+ return { family: fontName };
7796
7909
  }
7797
7910
 
7798
- $.loadFont = (url, cb) => {
7911
+ $.loadFont = (url = 'sans-serif', cb) => {
7912
+ fontSet = true;
7799
7913
  if (url.startsWith('https://fonts.googleapis.com/css')) {
7800
7914
  return $._g.loadFont(url, cb);
7801
7915
  }
7802
7916
 
7803
7917
  let ext = url.slice(url.lastIndexOf('.') + 1);
7804
- if (url == ext) return $._loadDefaultFont(url, cb);
7918
+
7919
+ // if not a url, assume it's one of q5's MSDF fonts
7920
+ if (url == ext) {
7921
+ let fontName = url;
7922
+ fonts[fontName] = null;
7923
+ url = `https://q5js.org/fonts/${fontName}-msdf.json`;
7924
+ if (Q5.online == false || !navigator.onLine) {
7925
+ url = `/node_modules/q5/builtinFonts/${fontName}-msdf.json`;
7926
+ }
7927
+ ext = 'json';
7928
+ }
7929
+
7805
7930
  if (ext != 'json') return $._g.loadFont(url, cb);
7931
+
7806
7932
  let fontName = url.slice(url.lastIndexOf('/') + 1, url.lastIndexOf('-'));
7807
7933
  let f = { family: fontName };
7808
7934
  f.promise = createFont(url, fontName, () => {
7809
7935
  delete f.promise;
7936
+ delete f.then;
7937
+ if (f._usedAwait) f = { family: fontName };
7810
7938
  if (cb) cb(f);
7811
7939
  });
7812
- $._preloadPromises.push(f.promise);
7940
+ $._loaders.push(f.promise);
7813
7941
 
7814
- if (!$._usePreload) return f.promise;
7942
+ f.then = (resolve, reject) => {
7943
+ f._usedAwait = true;
7944
+ return f.promise.then(resolve, reject);
7945
+ };
7815
7946
  return f;
7816
7947
  };
7817
7948
 
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
7949
  let _textSize = 18,
7828
7950
  _textAlign = 'left',
7829
7951
  _textBaseline = 'alphabetic',
@@ -7832,12 +7954,16 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7832
7954
  leadDiff = 4.5,
7833
7955
  leadPercent = 1.25;
7834
7956
 
7957
+ let categories = ['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui'];
7958
+
7835
7959
  $.textFont = (fontName) => {
7836
7960
  if (!fontName) return $._font;
7961
+ fontSet = true;
7837
7962
  if (typeof fontName != 'string') fontName = fontName.family;
7838
7963
  let font = fonts[fontName];
7839
7964
  if (font) $._font = font;
7840
- else if (font === undefined) return $._loadDefaultFont(fontName);
7965
+ // if it's a font category or not a WebGPU font, set the Canvas2D font
7966
+ else if (categories[fontName] || font === undefined) $._g.textFont(fontName);
7841
7967
  };
7842
7968
 
7843
7969
  $.textSize = (size) => {
@@ -7895,8 +8021,8 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7895
8021
  let lineWidthsCache = new Array(100);
7896
8022
 
7897
8023
  // 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
8024
+ let charDataBuffer = new Float32Array(Q5.MAX_CHARS * 4); // reusable buffer for char data
8025
+ let textDataBuffer = new Float32Array(Q5.MAX_TEXTS * 8); // reusable buffer for text metadata
7900
8026
 
7901
8027
  let measureText = (font, text, charCallback) => {
7902
8028
  let maxWidth = 0,
@@ -7948,16 +8074,21 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7948
8074
  };
7949
8075
 
7950
8076
  $.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
- return;
7955
- }
8077
+ if (_textSize < 1) return;
7956
8078
 
7957
8079
  let type = typeof str;
7958
8080
  if (type != 'string') {
7959
8081
  if (type == 'object') str = str.toString();
7960
8082
  else str = str + '';
8083
+ } else if (!str.length) return;
8084
+
8085
+ // if not using an MSDF font
8086
+ if (!$._font) {
8087
+ // if no font is set, lazy load the default MSDF font
8088
+ if (!fontSet) $.loadFont();
8089
+ // use Canvas2D text rendering
8090
+ let img = $.createTextImage(str, w, h);
8091
+ return $.textImage(img, x, y);
7961
8092
  }
7962
8093
 
7963
8094
  if (str.length > w) {
@@ -8194,6 +8325,8 @@ Q5.BLUR = 8;
8194
8325
  Q5.MAX_TRANSFORMS = 1e7;
8195
8326
  Q5.MAX_RECTS = 200200;
8196
8327
  Q5.MAX_ELLIPSES = 200200;
8328
+ Q5.MAX_CHARS = 100000;
8329
+ Q5.MAX_TEXTS = 10000;
8197
8330
 
8198
8331
  Q5.initWebGPU = async () => {
8199
8332
  if (!navigator.gpu) {