q5 2.24.3 → 2.25.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 2.24
3
+ * @version 2.25
4
4
  * @author quinton-ashley, Tezumie, and LingDong-
5
5
  * @license LGPL-3.0
6
6
  * @class Q5
@@ -71,14 +71,19 @@ function Q5(scope, parent, renderer) {
71
71
  $.deviceOrientation = window.screen?.orientation?.type;
72
72
  }
73
73
 
74
- $._preloadCount = 0;
75
- $._incrementPreload = () => q._preloadCount++;
76
- $._decrementPreload = () => q._preloadCount--;
77
-
74
+ $._preloadPromises = [];
78
75
  $._usePreload = true;
79
76
  $.usePreloadSystem = (v) => ($._usePreload = v);
80
77
  $.isPreloadSupported = () => $._usePreload;
81
78
 
79
+ const resolvers = [];
80
+ $._incrementPreload = () => {
81
+ $._preloadPromises.push(new Promise((resolve) => resolvers.push(resolve)));
82
+ };
83
+ $._decrementPreload = () => {
84
+ if (resolvers.length) resolvers.pop()();
85
+ };
86
+
82
87
  $._draw = (timestamp) => {
83
88
  let ts = timestamp || performance.now();
84
89
  $._lastFrameTime ??= ts - $._targetFrameDuration;
@@ -94,7 +99,6 @@ function Q5(scope, parent, renderer) {
94
99
  let nextTS = ts + $._targetFrameDuration;
95
100
  let frameDelay = nextTS - performance.now();
96
101
  while (frameDelay < 0) frameDelay += $._targetFrameDuration;
97
- log(frameDelay);
98
102
  looper = setTimeout(() => $._draw(nextTS), frameDelay);
99
103
  }
100
104
  } else if ($.frameCount && !$._redraw) return;
@@ -131,7 +135,7 @@ function Q5(scope, parent, renderer) {
131
135
  };
132
136
  $.noLoop = () => {
133
137
  $._loop = false;
134
- if (looper) {
138
+ if (looper != null) {
135
139
  if (useRAF) cancelAnimationFrame(looper);
136
140
  else clearTimeout(looper);
137
141
  }
@@ -155,17 +159,20 @@ function Q5(scope, parent, renderer) {
155
159
  };
156
160
 
157
161
  $.frameRate = (hz) => {
158
- if (hz) {
162
+ if (hz != $._targetFrameRate) {
159
163
  $._targetFrameRate = hz;
160
164
  $._targetFrameDuration = 1000 / hz;
161
165
 
162
- if ($._loop && $._setupDone && looper != null) {
166
+ if ($._loop && looper != null) {
163
167
  if (useRAF) cancelAnimationFrame(looper);
164
168
  else clearTimeout(looper);
165
169
  looper = null;
166
170
  }
167
171
  useRAF = hz <= 60;
168
- setTimeout(() => $._draw(), $._targetFrameDuration);
172
+ if ($._setupDone) {
173
+ if (useRAF) looper = raf($._draw);
174
+ else looper = setTimeout(() => $._draw(), $._targetFrameDuration);
175
+ }
169
176
  }
170
177
  return $._frameRate;
171
178
  };
@@ -215,11 +222,23 @@ function Q5(scope, parent, renderer) {
215
222
  if (n[0] != '_' && typeof $[n] == 'function') $[n] = fn.bind($);
216
223
  }
217
224
 
225
+ for (let [n, fn] of Object.entries(Q5.preloadMethods)) {
226
+ $[n] = function () {
227
+ $._incrementPreload();
228
+ return fn.apply($, arguments);
229
+ // fn is responsible for calling $._decrementPreload
230
+ };
231
+ }
232
+
218
233
  if (scope == 'global') {
219
234
  let props = Object.getOwnPropertyNames($);
220
235
  for (let p of props) {
221
236
  if (p[0] != '_') globalScope[p] = $[p];
222
237
  }
238
+ // to support p5.sound
239
+ for (let p of ['_incrementPreload', '_decrementPreload']) {
240
+ globalScope[p] = $[p];
241
+ }
223
242
  }
224
243
 
225
244
  if (typeof scope == 'function') scope($);
@@ -282,7 +301,8 @@ function Q5(scope, parent, renderer) {
282
301
 
283
302
  async function _setup() {
284
303
  $._startDone = true;
285
- if ($._preloadCount > 0 || $._g?._preloadCount > 0) return raf(_setup);
304
+ await Promise.all($._preloadPromises);
305
+ if ($._g) await Promise.all($._g._preloadPromises);
286
306
  millisStart = performance.now();
287
307
  await $.setup();
288
308
  $._setupDone = true;
@@ -326,7 +346,9 @@ Q5.methods = {
326
346
  remove: []
327
347
  };
328
348
  Q5.prototype.registerMethod = (m, fn) => Q5.methods[m].push(fn);
329
- Q5.prototype.registerPreloadMethod = (n, fn) => (Q5.prototype[n] = fn[n]);
349
+
350
+ Q5.preloadMethods = {};
351
+ Q5.prototype.registerPreloadMethod = (n, fn) => (Q5.preloadMethods[n] = fn[n]);
330
352
 
331
353
  if (Q5._server) global.p5 ??= global.Q5 = Q5;
332
354
 
@@ -340,7 +362,7 @@ function createCanvas(w, h, opt) {
340
362
  }
341
363
  }
342
364
 
343
- Q5.version = Q5.VERSION = '2.24';
365
+ Q5.version = Q5.VERSION = '2.25';
344
366
 
345
367
  if (typeof document == 'object') {
346
368
  document.addEventListener('DOMContentLoaded', () => {
@@ -934,6 +956,9 @@ Q5.renderers.c2d.shapes = ($) => {
934
956
  w /= 2;
935
957
  h /= 2;
936
958
 
959
+ w = Math.abs(w);
960
+ h = Math.abs(h);
961
+
937
962
  if (!$._doFill && mode == $.PIE_OPEN) mode = $.CHORD_OPEN;
938
963
 
939
964
  $.ctx.beginPath();
@@ -975,7 +1000,7 @@ Q5.renderers.c2d.shapes = ($) => {
975
1000
 
976
1001
  function ellipse(x, y, w, h) {
977
1002
  $.ctx.beginPath();
978
- $.ctx.ellipse(x, y, w / 2, h / 2, 0, 0, TAU);
1003
+ $.ctx.ellipse(x, y, Math.abs(w / 2), Math.abs(h / 2), 0, 0, TAU);
979
1004
  ink();
980
1005
  }
981
1006
 
@@ -1006,7 +1031,7 @@ Q5.renderers.c2d.shapes = ($) => {
1006
1031
  d *= $._da;
1007
1032
  }
1008
1033
  $.ctx.beginPath();
1009
- $.ctx.arc(x, y, d / 2, 0, TAU);
1034
+ $.ctx.arc(x, y, Math.abs(d / 2), 0, TAU);
1010
1035
  ink();
1011
1036
  } else $.ellipse(x, y, d, d);
1012
1037
  };
@@ -1310,7 +1335,6 @@ Q5.renderers.c2d.image = ($, q) => {
1310
1335
  `q5 doesn't support GIFs. Use a video or p5play animation instead. https://github.com/q5js/q5.js/issues/84`
1311
1336
  );
1312
1337
  }
1313
- q._preloadCount++;
1314
1338
  let last = [...arguments].at(-1);
1315
1339
  if (typeof last == 'object') {
1316
1340
  opt = last;
@@ -1323,7 +1347,7 @@ Q5.renderers.c2d.image = ($, q) => {
1323
1347
  let img = new window.Image();
1324
1348
  img.crossOrigin = 'Anonymous';
1325
1349
 
1326
- g._loader = new Promise((resolve, reject) => {
1350
+ g.promise = new Promise((resolve, reject) => {
1327
1351
  img.onload = () => {
1328
1352
  img._pixelDensity = pd;
1329
1353
  g.defaultWidth = img.width * $._defaultImageScale;
@@ -1333,20 +1357,17 @@ Q5.renderers.c2d.image = ($, q) => {
1333
1357
  g._setImageSize(Math.ceil(g.naturalWidth / pd), Math.ceil(g.naturalHeight / pd));
1334
1358
 
1335
1359
  g.ctx.drawImage(img, 0, 0);
1336
- q._preloadCount--;
1337
1360
  if (cb) cb(g);
1338
- delete g._loader;
1361
+ delete g.promise;
1339
1362
  resolve(g);
1340
1363
  };
1341
- img.onerror = (e) => {
1342
- q._preloadCount--;
1343
- reject(e);
1344
- };
1364
+ img.onerror = reject;
1345
1365
  });
1366
+ $._preloadPromises.push(g.promise);
1346
1367
 
1347
1368
  g.src = img.src = url;
1348
1369
 
1349
- if (!$._usePreload) return g._loader;
1370
+ if (!$._usePreload) return g.promise;
1350
1371
  return g;
1351
1372
  };
1352
1373
 
@@ -1783,34 +1804,32 @@ Q5.renderers.c2d.text = ($, q) => {
1783
1804
  fontMod = false,
1784
1805
  styleHash = 0,
1785
1806
  styleHashes = [],
1786
- useCache = false,
1787
1807
  genTextImage = false,
1788
- cacheSize = 0,
1789
- cacheMax = 12000;
1808
+ cacheSize = 0;
1790
1809
 
1791
1810
  let cache = ($._textCache = {});
1811
+ $._textCacheMaxSize = 12000;
1792
1812
 
1793
1813
  $.loadFont = (url, cb) => {
1794
- q._preloadCount++;
1795
1814
  let name = url.split('/').pop().split('.')[0].replace(' ', '');
1796
1815
 
1797
1816
  let f = new FontFace(name, `url(${url})`);
1798
1817
  document.fonts.add(f);
1799
- f._loader = (async () => {
1818
+ f.promise = (async () => {
1800
1819
  let err;
1801
1820
  try {
1802
1821
  await f.load();
1803
1822
  } catch (e) {
1804
1823
  err = e;
1805
1824
  }
1806
- q._preloadCount--;
1807
- delete f._loader;
1825
+ delete f.promise;
1808
1826
  if (err) throw err;
1809
1827
  if (cb) cb(f);
1810
1828
  return f;
1811
1829
  })();
1830
+ $._preloadPromises.push(f.promise);
1812
1831
  $.textFont(name);
1813
- if (!$._usePreload) return f._loader;
1832
+ if (!$._usePreload) return f.promise;
1814
1833
  return f;
1815
1834
  };
1816
1835
 
@@ -1896,12 +1915,6 @@ Q5.renderers.c2d.text = ($, q) => {
1896
1915
  styleHash = hash >>> 0;
1897
1916
  };
1898
1917
 
1899
- $.textCache = (enable, maxSize) => {
1900
- if (maxSize) cacheMax = maxSize;
1901
- if (enable !== undefined) useCache = enable;
1902
- return useCache;
1903
- };
1904
-
1905
1918
  $.createTextImage = (str, w, h) => {
1906
1919
  genTextImage = true;
1907
1920
  let img = $.text(str, 0, 0, w, h);
@@ -1923,7 +1936,7 @@ Q5.renderers.c2d.text = ($, q) => {
1923
1936
 
1924
1937
  if (fontMod) updateFont();
1925
1938
 
1926
- if (useCache || genTextImage) {
1939
+ if (genTextImage) {
1927
1940
  if (styleHash == -1) updateStyleHash();
1928
1941
 
1929
1942
  img = cache[str];
@@ -1931,8 +1944,7 @@ Q5.renderers.c2d.text = ($, q) => {
1931
1944
 
1932
1945
  if (img) {
1933
1946
  if (img._fill == $._fill && img._stroke == $._stroke && img._strokeWeight == $._strokeWeight) {
1934
- if (genTextImage) return img;
1935
- return $.textImage(img, x, y);
1947
+ return img;
1936
1948
  } else img.clear();
1937
1949
  }
1938
1950
  }
@@ -1960,7 +1972,7 @@ Q5.renderers.c2d.text = ($, q) => {
1960
1972
  lines = wrapped;
1961
1973
  }
1962
1974
 
1963
- if (!useCache && !genTextImage) {
1975
+ if (!genTextImage) {
1964
1976
  tX = x;
1965
1977
  tY = y;
1966
1978
  } else {
@@ -2021,12 +2033,12 @@ Q5.renderers.c2d.text = ($, q) => {
2021
2033
 
2022
2034
  if (!$._fillSet) ctx.fillStyle = ogFill;
2023
2035
 
2024
- if (useCache || genTextImage) {
2036
+ if (genTextImage) {
2025
2037
  styleHashes.push(styleHash);
2026
2038
  (cache[str] ??= {})[styleHash] = img;
2027
2039
 
2028
2040
  cacheSize++;
2029
- if (cacheSize > cacheMax) {
2041
+ if (cacheSize > $._textCacheMaxSize) {
2030
2042
  let half = Math.ceil(cacheSize / 2);
2031
2043
  let hashes = styleHashes.splice(0, half);
2032
2044
  for (let s in cache) {
@@ -2036,8 +2048,7 @@ Q5.renderers.c2d.text = ($, q) => {
2036
2048
  cacheSize -= half;
2037
2049
  }
2038
2050
 
2039
- if (genTextImage) return img;
2040
- $.textImage(img, x, y);
2051
+ return img;
2041
2052
  }
2042
2053
  };
2043
2054
 
@@ -2932,24 +2943,21 @@ Q5.modules.dom = ($, q) => {
2932
2943
  };
2933
2944
 
2934
2945
  if (src) {
2935
- q._preloadCount++;
2936
- el._loader = new Promise((resolve) => {
2946
+ el.promise = new Promise((resolve) => {
2937
2947
  el.addEventListener('loadeddata', () => {
2938
2948
  el._load();
2939
- q._preloadCount--;
2940
2949
  resolve(el);
2941
2950
  });
2942
2951
  el.src = src;
2943
2952
  });
2953
+ $._preloadPromises.push(el.promise);
2944
2954
 
2945
- if (!$._usePreload) return el._loader;
2955
+ if (!$._usePreload) return el.promise;
2946
2956
  }
2947
2957
  return el;
2948
2958
  };
2949
2959
 
2950
2960
  $.createCapture = function (type, flipped = true, cb) {
2951
- q._preloadCount++;
2952
-
2953
2961
  let constraints = typeof type == 'string' ? { [type]: true } : type || { video: true, audio: true };
2954
2962
 
2955
2963
  if (constraints.video === true) {
@@ -2971,12 +2979,11 @@ Q5.modules.dom = ($, q) => {
2971
2979
  vid.pixels = g.pixels;
2972
2980
  g.remove();
2973
2981
  };
2974
- vid._loader = (async () => {
2982
+ vid.promise = (async () => {
2975
2983
  let stream;
2976
2984
  try {
2977
2985
  stream = await navigator.mediaDevices.getUserMedia(constraints);
2978
2986
  } catch (e) {
2979
- q._preloadCount--;
2980
2987
  throw e;
2981
2988
  }
2982
2989
 
@@ -2985,11 +2992,11 @@ Q5.modules.dom = ($, q) => {
2985
2992
 
2986
2993
  vid._load();
2987
2994
  if (cb) cb(vid);
2988
- q._preloadCount--;
2989
2995
  return vid;
2990
2996
  })();
2997
+ $._preloadPromises.push(vid.promise);
2991
2998
 
2992
- if (!$._usePreload) return vid._loader;
2999
+ if (!$._usePreload) return vid.promise;
2993
3000
  return vid;
2994
3001
  };
2995
3002
 
@@ -4065,49 +4072,44 @@ Q5.modules.sound = ($, q) => {
4065
4072
  let sounds = [];
4066
4073
 
4067
4074
  $.loadSound = (url, cb) => {
4068
- q._preloadCount++;
4069
-
4070
4075
  let s = new Q5.Sound();
4071
4076
  sounds.push(s);
4072
4077
 
4073
- s._loader = (async () => {
4078
+ s.promise = (async () => {
4074
4079
  let err;
4075
4080
  try {
4076
4081
  await s.load(url);
4077
4082
  } catch (e) {
4078
4083
  err = e;
4079
4084
  }
4080
- q._preloadCount--;
4081
- delete s._loader;
4085
+ delete s.promise;
4082
4086
  if (err) throw err;
4083
4087
  if (cb) cb(s);
4084
4088
  return s;
4085
4089
  })();
4090
+ $._preloadPromises.push(s.promise);
4086
4091
 
4087
- if (!$._usePreload) return s._loader;
4092
+ if (!$._usePreload) return s.promise;
4088
4093
  return s;
4089
4094
  };
4090
4095
 
4091
4096
  $.loadAudio = (url, cb) => {
4092
- q._preloadCount++;
4093
4097
  let a = new Audio(url);
4094
4098
  a.crossOrigin = 'Anonymous';
4095
- a.addEventListener('canplaythrough', () => {
4096
- if (!a.loaded) {
4097
- q._preloadCount--;
4098
- a.loaded = true;
4099
- if (cb) cb(a);
4100
- }
4101
- });
4102
- let preloadSkip = () => {
4103
- a._preloadSkip = true;
4104
- q._preloadCount--;
4105
- };
4106
- a.addEventListener('suspend', preloadSkip);
4107
- a.addEventListener('error', (e) => {
4108
- preloadSkip();
4109
- throw e;
4099
+ a.promise = new Promise((resolve, reject) => {
4100
+ a.addEventListener('canplaythrough', () => {
4101
+ if (!a.loaded) {
4102
+ a.loaded = true;
4103
+ if (cb) cb(a);
4104
+ resolve(a);
4105
+ }
4106
+ });
4107
+ a.addEventListener('suspend', resolve);
4108
+ a.addEventListener('error', reject);
4110
4109
  });
4110
+ $._preloadPromises.push(a.promise);
4111
+
4112
+ if (!$._usePreload) return a.promise;
4111
4113
  return a;
4112
4114
  };
4113
4115
 
@@ -4273,9 +4275,8 @@ Q5.Sound = class {
4273
4275
  };
4274
4276
  Q5.modules.util = ($, q) => {
4275
4277
  $._loadFile = (url, cb, type) => {
4276
- q._preloadCount++;
4277
4278
  let ret = {};
4278
- ret._loader = new Promise((resolve, reject) => {
4279
+ ret.promise = new Promise((resolve, reject) => {
4279
4280
  fetch(url)
4280
4281
  .then((res) => {
4281
4282
  if (!res.ok) {
@@ -4289,9 +4290,8 @@ Q5.modules.util = ($, q) => {
4289
4290
  if (type == 'csv') f = $.CSV.parse(f);
4290
4291
  if (typeof f == 'string') ret.text = f;
4291
4292
  else Object.assign(ret, f);
4292
- delete ret._loader;
4293
+ delete ret.promise;
4293
4294
  if (cb) cb(f);
4294
- q._preloadCount--;
4295
4295
  resolve(f);
4296
4296
  });
4297
4297
  });
@@ -4302,14 +4302,15 @@ Q5.modules.util = ($, q) => {
4302
4302
  $.loadJSON = (url, cb) => $._loadFile(url, cb, 'json');
4303
4303
  $.loadCSV = (url, cb) => $._loadFile(url, cb, 'csv');
4304
4304
 
4305
- const imgRegex = /(jpe?g|png|gif|webp|avif|svg)/,
4306
- fontRegex = /(ttf|otf|woff2?|eot|json)/,
4307
- audioRegex = /(wav|flac|mp3|ogg|m4a|aac|aiff|weba)/;
4305
+ const imgRegex = /(jpe?g|png|gif|webp|avif|svg)/i,
4306
+ fontRegex = /(ttf|otf|woff2?|eot|json)/i,
4307
+ fontCategoryRegex = /(serif|sans-serif|monospace|cursive|fantasy)/i,
4308
+ audioRegex = /(wav|flac|mp3|ogg|m4a|aac|aiff|weba)/i;
4308
4309
 
4309
4310
  $.load = function (...urls) {
4310
4311
  if (Array.isArray(urls[0])) urls = urls[0];
4311
4312
 
4312
- let loaders = [];
4313
+ let promises = [];
4313
4314
 
4314
4315
  for (let url of urls) {
4315
4316
  let ext = url.split('.').pop().toLowerCase();
@@ -4321,18 +4322,18 @@ Q5.modules.util = ($, q) => {
4321
4322
  obj = $.loadCSV(url);
4322
4323
  } else if (imgRegex.test(ext)) {
4323
4324
  obj = $.loadImage(url);
4324
- } else if (fontRegex.test(ext)) {
4325
+ } else if (fontRegex.test(ext) || fontCategoryRegex.test(url)) {
4325
4326
  obj = $.loadFont(url);
4326
4327
  } else if (audioRegex.test(ext)) {
4327
4328
  obj = $.loadSound(url);
4328
4329
  } else {
4329
4330
  obj = $.loadText(url);
4330
4331
  }
4331
- loaders.push(obj._loader);
4332
+ promises.push(obj.promise);
4332
4333
  }
4333
4334
 
4334
- if (urls.length == 1) return loaders[0];
4335
- return Promise.all(loaders);
4335
+ if (urls.length == 1) return promises[0];
4336
+ return Promise.all(promises);
4336
4337
  };
4337
4338
 
4338
4339
  async function saveFile(data, name, ext) {
@@ -4837,6 +4838,8 @@ struct Q5 {
4837
4838
  $._frameA = frameA = Q5.device.createTexture({ size, format, usage });
4838
4839
  $._frameB = frameB = Q5.device.createTexture({ size, format, usage });
4839
4840
 
4841
+ $.canvas.texture = frameA;
4842
+
4840
4843
  $._frameShaderCode =
4841
4844
  $._baseShaderCode +
4842
4845
  /* wgsl */ `
@@ -5727,6 +5730,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
5727
5730
  $.rectMode = (x) => ($._rectMode = x);
5728
5731
 
5729
5732
  $.rect = (x, y, w, h, rr = 0) => {
5733
+ h ??= w;
5730
5734
  let [l, r, t, b] = $._calcBox(x, y, w, h, $._rectMode);
5731
5735
  let ci, ti;
5732
5736
  if ($._matrixDirty) $._saveMatrix();
@@ -6526,10 +6530,8 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6526
6530
  };
6527
6531
 
6528
6532
  $.loadImage = (src, cb) => {
6529
- q._preloadCount++;
6530
6533
  let g = $._g.loadImage(src, (img) => {
6531
6534
  $._addTexture(img);
6532
- q._preloadCount--;
6533
6535
  if (cb) cb(g);
6534
6536
  });
6535
6537
  return g;
@@ -6703,7 +6705,8 @@ struct FragParams {
6703
6705
  @location(0) texCoord : vec2f,
6704
6706
  @location(1) fillColor : vec4f,
6705
6707
  @location(2) strokeColor : vec4f,
6706
- @location(3) strokeWeight : f32
6708
+ @location(3) strokeWeight : f32,
6709
+ @location(4) edge : f32
6707
6710
  }
6708
6711
  struct Char {
6709
6712
  texOffset: vec2f,
@@ -6717,7 +6720,8 @@ struct Text {
6717
6720
  matrixIndex: f32,
6718
6721
  fillIndex: f32,
6719
6722
  strokeIndex: f32,
6720
- strokeWeight: f32
6723
+ strokeWeight: f32,
6724
+ edge: f32
6721
6725
  }
6722
6726
 
6723
6727
  @group(0) @binding(0) var<uniform> q: Q5;
@@ -6778,12 +6782,13 @@ fn vertexMain(v : VertexParams) -> FragParams {
6778
6782
  f.fillColor = colors[i32(text.fillIndex)];
6779
6783
  f.strokeColor = colors[i32(text.strokeIndex)];
6780
6784
  f.strokeWeight = text.strokeWeight;
6785
+ f.edge = text.edge;
6781
6786
  return f;
6782
6787
  }
6783
6788
 
6784
6789
  @fragment
6785
6790
  fn fragMain(f : FragParams) -> @location(0) vec4f {
6786
- let edge = 0.5;
6791
+ let edge = f.edge;
6787
6792
  let dist = calcDist(f.texCoord, edge);
6788
6793
 
6789
6794
  if (f.strokeWeight == 0.0) {
@@ -6906,14 +6911,10 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
6906
6911
  $._fonts = [];
6907
6912
  let fonts = {};
6908
6913
 
6909
- let createFont = async (fontJsonUrl, fontName, cb) => {
6910
- q._preloadCount++;
6911
-
6914
+ async function createFont(fontJsonUrl, fontName, cb) {
6912
6915
  let res = await fetch(fontJsonUrl);
6913
- if (res.status == 404) {
6914
- q._preloadCount--;
6915
- return '';
6916
- }
6916
+ if (res.status == 404) return '';
6917
+
6917
6918
  let atlas = await res.json();
6918
6919
 
6919
6920
  let slashIdx = fontJsonUrl.lastIndexOf('/');
@@ -6994,31 +6995,32 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
6994
6995
  $._fonts.push($._font);
6995
6996
  fonts[fontName] = $._font;
6996
6997
 
6997
- q._preloadCount--;
6998
-
6999
6998
  if (cb) cb(fontName);
7000
- };
6999
+ }
7001
7000
 
7002
7001
  $.loadFont = (url, cb) => {
7003
7002
  let ext = url.slice(url.lastIndexOf('.') + 1);
7003
+ if (url == ext) return $._loadDefaultFont(url, cb);
7004
7004
  if (ext != 'json') return $._g.loadFont(url, cb);
7005
7005
  let fontName = url.slice(url.lastIndexOf('/') + 1, url.lastIndexOf('-'));
7006
7006
  let f = { family: fontName };
7007
- f._loader = createFont(url, fontName, () => {
7008
- delete f._loader;
7007
+ f.promise = createFont(url, fontName, () => {
7008
+ delete f.promise;
7009
7009
  if (cb) cb(f);
7010
7010
  });
7011
- if (!$._usePreload) return f._loader;
7011
+ $._preloadPromises.push(f.promise);
7012
+
7013
+ if (!$._usePreload) return f.promise;
7012
7014
  return f;
7013
7015
  };
7014
7016
 
7015
- $._loadDefaultFont = (fontName) => {
7017
+ $._loadDefaultFont = (fontName, cb) => {
7016
7018
  fonts[fontName] = null;
7017
- if (navigator.onLine) {
7018
- $.loadFont(`https://q5js.org/fonts/${fontName}-msdf.json`);
7019
- } else {
7020
- $.loadFont(`/node_modules/q5/builtinFonts/${fontName}-msdf.json`);
7019
+ let url = `https://q5js.org/fonts/${fontName}-msdf.json`;
7020
+ if (!navigator.onLine) {
7021
+ url = `/node_modules/q5/builtinFonts/${fontName}-msdf.json`;
7021
7022
  }
7023
+ return $.loadFont(url, cb);
7022
7024
  };
7023
7025
 
7024
7026
  $._textSize = 18;
@@ -7034,7 +7036,7 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7034
7036
  if (typeof fontName != 'string') fontName = fontName.family;
7035
7037
  let font = fonts[fontName];
7036
7038
  if (font) $._font = font;
7037
- else if (font === undefined) $._loadDefaultFont(fontName);
7039
+ else if (font === undefined) return $._loadDefaultFont(fontName);
7038
7040
  };
7039
7041
 
7040
7042
  $.textSize = (size) => {
@@ -7046,6 +7048,33 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7046
7048
  }
7047
7049
  };
7048
7050
 
7051
+ let weights = {
7052
+ thin: 100,
7053
+ extralight: 200,
7054
+ light: 300,
7055
+ normal: 400,
7056
+ regular: 400,
7057
+ medium: 500,
7058
+ semibold: 600,
7059
+ bold: 700,
7060
+ bolder: 800,
7061
+ extrabold: 800,
7062
+ black: 900,
7063
+ heavy: 900
7064
+ };
7065
+
7066
+ // ranges from 0.35 (black) to 0.65 (thin)
7067
+ $._textEdge = 0.5;
7068
+
7069
+ $.textWeight = (weight) => {
7070
+ if (!weight) return $._textWeight;
7071
+ if (typeof weight == 'string') {
7072
+ weight = weights[weight.toLowerCase().replace(/[ _-]/g, '')];
7073
+ if (!weight) throw new Error(`Invalid font weight: ${weight}`);
7074
+ }
7075
+ $._textEdge = 0.6875 - weight * 0.000375;
7076
+ };
7077
+
7049
7078
  $.textLeading = (lineHeight) => {
7050
7079
  $._font.lineHeight = leading = lineHeight;
7051
7080
  leadDiff = leading - $._textSize;
@@ -7204,12 +7233,12 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7204
7233
 
7205
7234
  txt[0] = x;
7206
7235
  txt[1] = -y;
7207
- txt[2] = $._textSize / 44;
7236
+ txt[2] = $._textSize / 42;
7208
7237
  txt[3] = $._matrixIndex;
7209
7238
  txt[4] = $._doFill && $._fillSet ? $._fill : 0;
7210
7239
  txt[5] = $._stroke;
7211
7240
  txt[6] = $._doStroke && $._strokeSet ? $._strokeWeight : 0;
7212
- txt[7] = 0; // padding
7241
+ txt[7] = $._textEdge;
7213
7242
 
7214
7243
  textStack.push(txt);
7215
7244
  $._drawStack.push($._textPL, measurements.printedCharCount, $._font.index);