q5 2.24.4 → 2.25.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 2.24
3
+ * @version 2.25
4
4
  * @author quinton-ashley, Tezumie, and LingDong-
5
5
  * @license LGPL-3.0
6
6
  * @class Q5
@@ -99,7 +99,6 @@ function Q5(scope, parent, renderer) {
99
99
  let nextTS = ts + $._targetFrameDuration;
100
100
  let frameDelay = nextTS - performance.now();
101
101
  while (frameDelay < 0) frameDelay += $._targetFrameDuration;
102
- log(frameDelay);
103
102
  looper = setTimeout(() => $._draw(nextTS), frameDelay);
104
103
  }
105
104
  } else if ($.frameCount && !$._redraw) return;
@@ -223,11 +222,23 @@ function Q5(scope, parent, renderer) {
223
222
  if (n[0] != '_' && typeof $[n] == 'function') $[n] = fn.bind($);
224
223
  }
225
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
+
226
233
  if (scope == 'global') {
227
234
  let props = Object.getOwnPropertyNames($);
228
235
  for (let p of props) {
229
236
  if (p[0] != '_') globalScope[p] = $[p];
230
237
  }
238
+ // to support p5.sound
239
+ for (let p of ['_incrementPreload', '_decrementPreload']) {
240
+ globalScope[p] = $[p];
241
+ }
231
242
  }
232
243
 
233
244
  if (typeof scope == 'function') scope($);
@@ -335,7 +346,9 @@ Q5.methods = {
335
346
  remove: []
336
347
  };
337
348
  Q5.prototype.registerMethod = (m, fn) => Q5.methods[m].push(fn);
338
- 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]);
339
352
 
340
353
  if (Q5._server) global.p5 ??= global.Q5 = Q5;
341
354
 
@@ -349,7 +362,7 @@ function createCanvas(w, h, opt) {
349
362
  }
350
363
  }
351
364
 
352
- Q5.version = Q5.VERSION = '2.24';
365
+ Q5.version = Q5.VERSION = '2.25';
353
366
 
354
367
  if (typeof document == 'object') {
355
368
  document.addEventListener('DOMContentLoaded', () => {
@@ -943,6 +956,9 @@ Q5.renderers.c2d.shapes = ($) => {
943
956
  w /= 2;
944
957
  h /= 2;
945
958
 
959
+ w = Math.abs(w);
960
+ h = Math.abs(h);
961
+
946
962
  if (!$._doFill && mode == $.PIE_OPEN) mode = $.CHORD_OPEN;
947
963
 
948
964
  $.ctx.beginPath();
@@ -984,7 +1000,7 @@ Q5.renderers.c2d.shapes = ($) => {
984
1000
 
985
1001
  function ellipse(x, y, w, h) {
986
1002
  $.ctx.beginPath();
987
- $.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);
988
1004
  ink();
989
1005
  }
990
1006
 
@@ -1015,7 +1031,7 @@ Q5.renderers.c2d.shapes = ($) => {
1015
1031
  d *= $._da;
1016
1032
  }
1017
1033
  $.ctx.beginPath();
1018
- $.ctx.arc(x, y, d / 2, 0, TAU);
1034
+ $.ctx.arc(x, y, Math.abs(d / 2), 0, TAU);
1019
1035
  ink();
1020
1036
  } else $.ellipse(x, y, d, d);
1021
1037
  };
@@ -1331,7 +1347,7 @@ Q5.renderers.c2d.image = ($, q) => {
1331
1347
  let img = new window.Image();
1332
1348
  img.crossOrigin = 'Anonymous';
1333
1349
 
1334
- g._loader = new Promise((resolve, reject) => {
1350
+ g.promise = new Promise((resolve, reject) => {
1335
1351
  img.onload = () => {
1336
1352
  img._pixelDensity = pd;
1337
1353
  g.defaultWidth = img.width * $._defaultImageScale;
@@ -1342,16 +1358,16 @@ Q5.renderers.c2d.image = ($, q) => {
1342
1358
 
1343
1359
  g.ctx.drawImage(img, 0, 0);
1344
1360
  if (cb) cb(g);
1345
- delete g._loader;
1361
+ delete g.promise;
1346
1362
  resolve(g);
1347
1363
  };
1348
1364
  img.onerror = reject;
1349
1365
  });
1350
- $._preloadPromises.push(g._loader);
1366
+ $._preloadPromises.push(g.promise);
1351
1367
 
1352
1368
  g.src = img.src = url;
1353
1369
 
1354
- if (!$._usePreload) return g._loader;
1370
+ if (!$._usePreload) return g.promise;
1355
1371
  return g;
1356
1372
  };
1357
1373
 
@@ -1788,33 +1804,32 @@ Q5.renderers.c2d.text = ($, q) => {
1788
1804
  fontMod = false,
1789
1805
  styleHash = 0,
1790
1806
  styleHashes = [],
1791
- useCache = false,
1792
1807
  genTextImage = false,
1793
- cacheSize = 0,
1794
- cacheMax = 12000;
1808
+ cacheSize = 0;
1795
1809
 
1796
1810
  let cache = ($._textCache = {});
1811
+ $._textCacheMaxSize = 12000;
1797
1812
 
1798
1813
  $.loadFont = (url, cb) => {
1799
1814
  let name = url.split('/').pop().split('.')[0].replace(' ', '');
1800
1815
 
1801
1816
  let f = new FontFace(name, `url(${url})`);
1802
1817
  document.fonts.add(f);
1803
- f._loader = (async () => {
1818
+ f.promise = (async () => {
1804
1819
  let err;
1805
1820
  try {
1806
1821
  await f.load();
1807
1822
  } catch (e) {
1808
1823
  err = e;
1809
1824
  }
1810
- delete f._loader;
1825
+ delete f.promise;
1811
1826
  if (err) throw err;
1812
1827
  if (cb) cb(f);
1813
1828
  return f;
1814
1829
  })();
1815
- $._preloadPromises.push(f._loader);
1830
+ $._preloadPromises.push(f.promise);
1816
1831
  $.textFont(name);
1817
- if (!$._usePreload) return f._loader;
1832
+ if (!$._usePreload) return f.promise;
1818
1833
  return f;
1819
1834
  };
1820
1835
 
@@ -1900,12 +1915,6 @@ Q5.renderers.c2d.text = ($, q) => {
1900
1915
  styleHash = hash >>> 0;
1901
1916
  };
1902
1917
 
1903
- $.textCache = (enable, maxSize) => {
1904
- if (maxSize) cacheMax = maxSize;
1905
- if (enable !== undefined) useCache = enable;
1906
- return useCache;
1907
- };
1908
-
1909
1918
  $.createTextImage = (str, w, h) => {
1910
1919
  genTextImage = true;
1911
1920
  let img = $.text(str, 0, 0, w, h);
@@ -1927,7 +1936,7 @@ Q5.renderers.c2d.text = ($, q) => {
1927
1936
 
1928
1937
  if (fontMod) updateFont();
1929
1938
 
1930
- if (useCache || genTextImage) {
1939
+ if (genTextImage) {
1931
1940
  if (styleHash == -1) updateStyleHash();
1932
1941
 
1933
1942
  img = cache[str];
@@ -1935,8 +1944,7 @@ Q5.renderers.c2d.text = ($, q) => {
1935
1944
 
1936
1945
  if (img) {
1937
1946
  if (img._fill == $._fill && img._stroke == $._stroke && img._strokeWeight == $._strokeWeight) {
1938
- if (genTextImage) return img;
1939
- return $.textImage(img, x, y);
1947
+ return img;
1940
1948
  } else img.clear();
1941
1949
  }
1942
1950
  }
@@ -1964,12 +1972,14 @@ Q5.renderers.c2d.text = ($, q) => {
1964
1972
  lines = wrapped;
1965
1973
  }
1966
1974
 
1967
- if (!useCache && !genTextImage) {
1975
+ if (!genTextImage) {
1968
1976
  tX = x;
1969
1977
  tY = y;
1978
+ if ($._textBaseline == 'middle') tY -= leading * (lines.length - 1) * 0.5;
1979
+ else if ($._textBaseline == 'bottom') tY -= leading * (lines.length - 1);
1970
1980
  } else {
1971
1981
  tX = 0;
1972
- tY = leading * lines.length;
1982
+ tY = leading;
1973
1983
 
1974
1984
  if (!img) {
1975
1985
  let ogBaseline = $.ctx.textBaseline;
@@ -1981,15 +1991,24 @@ Q5.renderers.c2d.text = ($, q) => {
1981
1991
 
1982
1992
  $.ctx.textBaseline = ogBaseline;
1983
1993
 
1984
- img = $.createImage.call($, Math.ceil(ctx.measureText(str).width), Math.ceil(tY + descent), {
1994
+ let maxWidth = 0;
1995
+ for (let line of lines) {
1996
+ let lineWidth = ctx.measureText(line).width;
1997
+ if (lineWidth > maxWidth) maxWidth = lineWidth;
1998
+ }
1999
+
2000
+ let imgW = Math.ceil(maxWidth),
2001
+ imgH = Math.ceil(leading * lines.length + descent);
2002
+
2003
+ img = $.createImage.call($, imgW, imgH, {
1985
2004
  pixelDensity: $._pixelDensity
1986
2005
  });
1987
2006
 
1988
2007
  img._ascent = ascent;
1989
2008
  img._descent = descent;
1990
2009
  img._top = descent + leadDiff;
1991
- img._middle = img._top + ascent * 0.5;
1992
- img._bottom = img._top + ascent;
2010
+ img._middle = img._top + ascent * 0.5 + leading * (lines.length - 1) * 0.5;
2011
+ img._bottom = img._top + ascent + leading * (lines.length - 1);
1993
2012
  img._leading = leading;
1994
2013
  } else {
1995
2014
  img.modified = true;
@@ -2025,12 +2044,12 @@ Q5.renderers.c2d.text = ($, q) => {
2025
2044
 
2026
2045
  if (!$._fillSet) ctx.fillStyle = ogFill;
2027
2046
 
2028
- if (useCache || genTextImage) {
2047
+ if (genTextImage) {
2029
2048
  styleHashes.push(styleHash);
2030
2049
  (cache[str] ??= {})[styleHash] = img;
2031
2050
 
2032
2051
  cacheSize++;
2033
- if (cacheSize > cacheMax) {
2052
+ if (cacheSize > $._textCacheMaxSize) {
2034
2053
  let half = Math.ceil(cacheSize / 2);
2035
2054
  let hashes = styleHashes.splice(0, half);
2036
2055
  for (let s in cache) {
@@ -2040,8 +2059,8 @@ Q5.renderers.c2d.text = ($, q) => {
2040
2059
  cacheSize -= half;
2041
2060
  }
2042
2061
 
2043
- if (genTextImage) return img;
2044
- $.textImage(img, x, y);
2062
+ $.save(img);
2063
+ return img;
2045
2064
  }
2046
2065
  };
2047
2066
 
@@ -2556,10 +2575,8 @@ Q5.modules.display = ($) => {
2556
2575
 
2557
2576
  let c = $.canvas;
2558
2577
 
2559
- $.CENTERED = 'centered';
2560
- $.FULLSCREEN = 'fullscreen';
2561
2578
  $.MAXED = 'maxed';
2562
-
2579
+ $.SMOOTH = 'smooth';
2563
2580
  $.PIXELATED = 'pixelated';
2564
2581
 
2565
2582
  if (Q5._instanceCount == 0 && !Q5._server) {
@@ -2583,15 +2600,13 @@ html, body {
2583
2600
  -webkit-font-smoothing: none;
2584
2601
  }
2585
2602
  .q5-centered,
2586
- .q5-maxed,
2587
- .q5-fullscreen {
2603
+ .q5-maxed {
2588
2604
  display: flex;
2589
2605
  align-items: center;
2590
2606
  justify-content: center;
2591
2607
  }
2592
2608
  main.q5-centered,
2593
- main.q5-maxed,
2594
- .q5-fullscreen {
2609
+ main.q5-maxed {
2595
2610
  height: 100vh;
2596
2611
  }
2597
2612
  main {
@@ -2612,8 +2627,8 @@ main {
2612
2627
  if ($.noSmooth) $.noSmooth();
2613
2628
  if ($.textFont) $.textFont('monospace');
2614
2629
  }
2615
- if (c.displayMode == 'default' || c.displayMode == 'normal') {
2616
- p.classList.remove('q5-centered', 'q5-maxed', 'q5-fullscreen');
2630
+ if (c.displayMode == 'normal') {
2631
+ p.classList.remove('q5-centered', 'q5-maxed');
2617
2632
  s.width = c.w * c.displayScale + 'px';
2618
2633
  s.height = c.h * c.displayScale + 'px';
2619
2634
  } else {
@@ -2641,6 +2656,7 @@ main {
2641
2656
  if (typeof displayScale == 'string') {
2642
2657
  displayScale = parseFloat(displayScale.slice(1));
2643
2658
  }
2659
+ if (displayMode == 'fullscreen') displayMode = 'maxed';
2644
2660
  if (displayMode == 'center') displayMode = 'centered';
2645
2661
  Object.assign(c, { displayMode, renderQuality, displayScale });
2646
2662
  if ($.ctx) $.pushStyles();
@@ -2649,7 +2665,7 @@ main {
2649
2665
  };
2650
2666
 
2651
2667
  $.fullscreen = (v) => {
2652
- if (v === undefined) return document.fullscreenElement;
2668
+ if (v == undefined) return document.fullscreenElement;
2653
2669
  if (v) document.body.requestFullscreen();
2654
2670
  else document.body.exitFullscreen();
2655
2671
  };
@@ -2936,16 +2952,16 @@ Q5.modules.dom = ($, q) => {
2936
2952
  };
2937
2953
 
2938
2954
  if (src) {
2939
- el._loader = new Promise((resolve) => {
2955
+ el.promise = new Promise((resolve) => {
2940
2956
  el.addEventListener('loadeddata', () => {
2941
2957
  el._load();
2942
2958
  resolve(el);
2943
2959
  });
2944
2960
  el.src = src;
2945
2961
  });
2946
- $._preloadPromises.push(el._loader);
2962
+ $._preloadPromises.push(el.promise);
2947
2963
 
2948
- if (!$._usePreload) return el._loader;
2964
+ if (!$._usePreload) return el.promise;
2949
2965
  }
2950
2966
  return el;
2951
2967
  };
@@ -2972,7 +2988,7 @@ Q5.modules.dom = ($, q) => {
2972
2988
  vid.pixels = g.pixels;
2973
2989
  g.remove();
2974
2990
  };
2975
- vid._loader = (async () => {
2991
+ vid.promise = (async () => {
2976
2992
  let stream;
2977
2993
  try {
2978
2994
  stream = await navigator.mediaDevices.getUserMedia(constraints);
@@ -2987,9 +3003,9 @@ Q5.modules.dom = ($, q) => {
2987
3003
  if (cb) cb(vid);
2988
3004
  return vid;
2989
3005
  })();
2990
- $._preloadPromises.push(vid._loader);
3006
+ $._preloadPromises.push(vid.promise);
2991
3007
 
2992
- if (!$._usePreload) return vid._loader;
3008
+ if (!$._usePreload) return vid.promise;
2993
3009
  return vid;
2994
3010
  };
2995
3011
 
@@ -4068,28 +4084,28 @@ Q5.modules.sound = ($, q) => {
4068
4084
  let s = new Q5.Sound();
4069
4085
  sounds.push(s);
4070
4086
 
4071
- s._loader = (async () => {
4087
+ s.promise = (async () => {
4072
4088
  let err;
4073
4089
  try {
4074
4090
  await s.load(url);
4075
4091
  } catch (e) {
4076
4092
  err = e;
4077
4093
  }
4078
- delete s._loader;
4094
+ delete s.promise;
4079
4095
  if (err) throw err;
4080
4096
  if (cb) cb(s);
4081
4097
  return s;
4082
4098
  })();
4083
- $._preloadPromises.push(s._loader);
4099
+ $._preloadPromises.push(s.promise);
4084
4100
 
4085
- if (!$._usePreload) return s._loader;
4101
+ if (!$._usePreload) return s.promise;
4086
4102
  return s;
4087
4103
  };
4088
4104
 
4089
4105
  $.loadAudio = (url, cb) => {
4090
4106
  let a = new Audio(url);
4091
4107
  a.crossOrigin = 'Anonymous';
4092
- a._loader = new Promise((resolve, reject) => {
4108
+ a.promise = new Promise((resolve, reject) => {
4093
4109
  a.addEventListener('canplaythrough', () => {
4094
4110
  if (!a.loaded) {
4095
4111
  a.loaded = true;
@@ -4100,9 +4116,9 @@ Q5.modules.sound = ($, q) => {
4100
4116
  a.addEventListener('suspend', resolve);
4101
4117
  a.addEventListener('error', reject);
4102
4118
  });
4103
- $._preloadPromises.push(a._loader);
4119
+ $._preloadPromises.push(a.promise);
4104
4120
 
4105
- if (!$._usePreload) return a._loader;
4121
+ if (!$._usePreload) return a.promise;
4106
4122
  return a;
4107
4123
  };
4108
4124
 
@@ -4269,7 +4285,7 @@ Q5.Sound = class {
4269
4285
  Q5.modules.util = ($, q) => {
4270
4286
  $._loadFile = (url, cb, type) => {
4271
4287
  let ret = {};
4272
- ret._loader = new Promise((resolve, reject) => {
4288
+ ret.promise = new Promise((resolve, reject) => {
4273
4289
  fetch(url)
4274
4290
  .then((res) => {
4275
4291
  if (!res.ok) {
@@ -4283,7 +4299,7 @@ Q5.modules.util = ($, q) => {
4283
4299
  if (type == 'csv') f = $.CSV.parse(f);
4284
4300
  if (typeof f == 'string') ret.text = f;
4285
4301
  else Object.assign(ret, f);
4286
- delete ret._loader;
4302
+ delete ret.promise;
4287
4303
  if (cb) cb(f);
4288
4304
  resolve(f);
4289
4305
  });
@@ -4295,14 +4311,15 @@ Q5.modules.util = ($, q) => {
4295
4311
  $.loadJSON = (url, cb) => $._loadFile(url, cb, 'json');
4296
4312
  $.loadCSV = (url, cb) => $._loadFile(url, cb, 'csv');
4297
4313
 
4298
- const imgRegex = /(jpe?g|png|gif|webp|avif|svg)/,
4299
- fontRegex = /(ttf|otf|woff2?|eot|json)/,
4300
- audioRegex = /(wav|flac|mp3|ogg|m4a|aac|aiff|weba)/;
4314
+ const imgRegex = /(jpe?g|png|gif|webp|avif|svg)/i,
4315
+ fontRegex = /(ttf|otf|woff2?|eot|json)/i,
4316
+ fontCategoryRegex = /(serif|sans-serif|monospace|cursive|fantasy)/i,
4317
+ audioRegex = /(wav|flac|mp3|ogg|m4a|aac|aiff|weba)/i;
4301
4318
 
4302
4319
  $.load = function (...urls) {
4303
4320
  if (Array.isArray(urls[0])) urls = urls[0];
4304
4321
 
4305
- let loaders = [];
4322
+ let promises = [];
4306
4323
 
4307
4324
  for (let url of urls) {
4308
4325
  let ext = url.split('.').pop().toLowerCase();
@@ -4314,25 +4331,29 @@ Q5.modules.util = ($, q) => {
4314
4331
  obj = $.loadCSV(url);
4315
4332
  } else if (imgRegex.test(ext)) {
4316
4333
  obj = $.loadImage(url);
4317
- } else if (fontRegex.test(ext)) {
4334
+ } else if (fontRegex.test(ext) || fontCategoryRegex.test(url)) {
4318
4335
  obj = $.loadFont(url);
4319
4336
  } else if (audioRegex.test(ext)) {
4320
4337
  obj = $.loadSound(url);
4321
4338
  } else {
4322
4339
  obj = $.loadText(url);
4323
4340
  }
4324
- loaders.push(obj._loader);
4341
+ promises.push(obj.promise);
4325
4342
  }
4326
4343
 
4327
- if (urls.length == 1) return loaders[0];
4328
- return Promise.all(loaders);
4344
+ if (urls.length == 1) return promises[0];
4345
+ return Promise.all(promises);
4329
4346
  };
4330
4347
 
4331
4348
  async function saveFile(data, name, ext) {
4332
4349
  name = name || 'untitled';
4333
4350
  ext = ext || 'png';
4334
4351
  if (imgRegex.test(ext)) {
4335
- data = await $._saveCanvas(data, ext);
4352
+ if ($.canvas?.renderer == 'webgpu' && data.canvas.renderer == 'c2d') {
4353
+ data = await $._g._saveCanvas(data, ext);
4354
+ } else {
4355
+ data = await $._saveCanvas(data, ext);
4356
+ }
4336
4357
  } else {
4337
4358
  let type = 'text/plain';
4338
4359
  if (ext == 'json') {
@@ -4830,6 +4851,8 @@ struct Q5 {
4830
4851
  $._frameA = frameA = Q5.device.createTexture({ size, format, usage });
4831
4852
  $._frameB = frameB = Q5.device.createTexture({ size, format, usage });
4832
4853
 
4854
+ $.canvas.texture = frameA;
4855
+
4833
4856
  $._frameShaderCode =
4834
4857
  $._baseShaderCode +
4835
4858
  /* wgsl */ `
@@ -4930,16 +4953,25 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
4930
4953
  createMainView();
4931
4954
  };
4932
4955
 
4956
+ let usingRGB = true,
4957
+ colorFormat = 1;
4958
+
4959
+ let cm = $.colorMode;
4960
+ $.colorMode = function () {
4961
+ cm(...arguments);
4962
+ usingRGB = $._colorMode == 'rgb';
4963
+ colorFormat = $._colorFormat;
4964
+ };
4965
+
4933
4966
  let addColor = (r, g, b, a) => {
4934
- let cf = $._colorFormat;
4935
- if (typeof r == 'string' || $._colorMode != 'rgb') {
4967
+ if (typeof r === 'string' || usingRGB === false) {
4936
4968
  r = $.color(r, g, b, a);
4937
- } else if (b == undefined) {
4969
+ } else if (b === undefined) {
4938
4970
  // grayscale mode `fill(1, 0.5)`
4939
- a = g ?? cf;
4971
+ a = g ?? colorFormat;
4940
4972
  g = b = r;
4941
4973
  }
4942
- a ??= cf;
4974
+ a ??= colorFormat;
4943
4975
  if (r._q5Color) {
4944
4976
  let c = r;
4945
4977
  if (c.r != undefined) ({ r, g, b, a } = c);
@@ -4952,7 +4984,7 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
4952
4984
  }
4953
4985
  }
4954
4986
 
4955
- if (cf == 255) {
4987
+ if (colorFormat === 255) {
4956
4988
  r /= 255;
4957
4989
  g /= 255;
4958
4990
  b /= 255;
@@ -5720,6 +5752,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
5720
5752
  $.rectMode = (x) => ($._rectMode = x);
5721
5753
 
5722
5754
  $.rect = (x, y, w, h, rr = 0) => {
5755
+ h ??= w;
5723
5756
  let [l, r, t, b] = $._calcBox(x, y, w, h, $._rectMode);
5724
5757
  let ci, ti;
5725
5758
  if ($._matrixDirty) $._saveMatrix();
@@ -6396,8 +6429,10 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6396
6429
 
6397
6430
  $._textureBindGroups = [];
6398
6431
 
6399
- $._saveCanvas = async (data, ext) => {
6400
- let texture = data.texture,
6432
+ $._saveCanvas = async (img, ext) => {
6433
+ if (img._graphics && img._drawStack?.length) img._completeFrame();
6434
+
6435
+ let texture = img.texture,
6401
6436
  w = texture.width,
6402
6437
  h = texture.height,
6403
6438
  bytesPerRow = Math.ceil((w * 4) / 256) * 256;
@@ -6418,7 +6453,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6418
6453
  await buffer.mapAsync(GPUMapMode.READ);
6419
6454
 
6420
6455
  let pad = new Uint8Array(buffer.getMappedRange());
6421
- data = new Uint8Array(w * h * 4); // unpadded data
6456
+ let data = new Uint8Array(w * h * 4); // unpadded data
6422
6457
 
6423
6458
  // Remove padding from each row and swap BGR to RGB
6424
6459
  for (let y = 0; y < h; y++) {
@@ -6513,7 +6548,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6513
6548
  let g = new Q5Image(...arguments);
6514
6549
  if (w > 1 && h > 1) {
6515
6550
  $._addTexture(g);
6516
- g.modified = true;
6551
+ img.modified = true;
6517
6552
  }
6518
6553
  return g;
6519
6554
  };
@@ -6536,6 +6571,8 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6536
6571
  $._addTexture(g, g._frameA);
6537
6572
  $._addTexture(g, g._frameB);
6538
6573
  g._beginRender();
6574
+ } else {
6575
+ g.modified = true;
6539
6576
  }
6540
6577
  return g;
6541
6578
  };
@@ -6571,17 +6608,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6571
6608
  h = cnv.height,
6572
6609
  pd = img._pixelDensity || 1;
6573
6610
 
6574
- if (img._graphics) {
6575
- let g = img;
6576
- if (g.drawStack.length) {
6577
- g._render();
6578
- g._finishRender();
6579
- g.textureIndex += g.frameCount % 2 == 0 ? -1 : 1;
6580
- g.resetMatrix();
6581
- g._beginRender();
6582
- g.frameCount++;
6583
- }
6584
- }
6611
+ if (img._graphics && img._drawStack?.length) img._completeFrame();
6585
6612
 
6586
6613
  if (img.modified) {
6587
6614
  Q5.device.queue.copyExternalImageToTexture(
@@ -6638,6 +6665,15 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6638
6665
  }
6639
6666
  };
6640
6667
 
6668
+ $._completeFrame = () => {
6669
+ $._render();
6670
+ $._finishRender();
6671
+ $.textureIndex += $.frameCount % 2 == 0 ? -1 : 1;
6672
+ $.resetMatrix();
6673
+ $._beginRender();
6674
+ $.frameCount++;
6675
+ };
6676
+
6641
6677
  $._hooks.preRender.push(() => {
6642
6678
  if (!vertIndex) return;
6643
6679
 
@@ -6694,7 +6730,8 @@ struct FragParams {
6694
6730
  @location(0) texCoord : vec2f,
6695
6731
  @location(1) fillColor : vec4f,
6696
6732
  @location(2) strokeColor : vec4f,
6697
- @location(3) strokeWeight : f32
6733
+ @location(3) strokeWeight : f32,
6734
+ @location(4) edge : f32
6698
6735
  }
6699
6736
  struct Char {
6700
6737
  texOffset: vec2f,
@@ -6708,7 +6745,8 @@ struct Text {
6708
6745
  matrixIndex: f32,
6709
6746
  fillIndex: f32,
6710
6747
  strokeIndex: f32,
6711
- strokeWeight: f32
6748
+ strokeWeight: f32,
6749
+ edge: f32
6712
6750
  }
6713
6751
 
6714
6752
  @group(0) @binding(0) var<uniform> q: Q5;
@@ -6769,12 +6807,13 @@ fn vertexMain(v : VertexParams) -> FragParams {
6769
6807
  f.fillColor = colors[i32(text.fillIndex)];
6770
6808
  f.strokeColor = colors[i32(text.strokeIndex)];
6771
6809
  f.strokeWeight = text.strokeWeight;
6810
+ f.edge = text.edge;
6772
6811
  return f;
6773
6812
  }
6774
6813
 
6775
6814
  @fragment
6776
6815
  fn fragMain(f : FragParams) -> @location(0) vec4f {
6777
- let edge = 0.5;
6816
+ let edge = f.edge;
6778
6817
  let dist = calcDist(f.texCoord, edge);
6779
6818
 
6780
6819
  if (f.strokeWeight == 0.0) {
@@ -6986,26 +7025,27 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
6986
7025
 
6987
7026
  $.loadFont = (url, cb) => {
6988
7027
  let ext = url.slice(url.lastIndexOf('.') + 1);
7028
+ if (url == ext) return $._loadDefaultFont(url, cb);
6989
7029
  if (ext != 'json') return $._g.loadFont(url, cb);
6990
7030
  let fontName = url.slice(url.lastIndexOf('/') + 1, url.lastIndexOf('-'));
6991
7031
  let f = { family: fontName };
6992
- f._loader = createFont(url, fontName, () => {
6993
- delete f._loader;
7032
+ f.promise = createFont(url, fontName, () => {
7033
+ delete f.promise;
6994
7034
  if (cb) cb(f);
6995
7035
  });
6996
- $._preloadPromises.push(f._loader);
7036
+ $._preloadPromises.push(f.promise);
6997
7037
 
6998
- if (!$._usePreload) return f._loader;
7038
+ if (!$._usePreload) return f.promise;
6999
7039
  return f;
7000
7040
  };
7001
7041
 
7002
- $._loadDefaultFont = (fontName) => {
7042
+ $._loadDefaultFont = (fontName, cb) => {
7003
7043
  fonts[fontName] = null;
7004
- if (navigator.onLine) {
7005
- $.loadFont(`https://q5js.org/fonts/${fontName}-msdf.json`);
7006
- } else {
7007
- $.loadFont(`/node_modules/q5/builtinFonts/${fontName}-msdf.json`);
7044
+ let url = `https://q5js.org/fonts/${fontName}-msdf.json`;
7045
+ if (!navigator.onLine) {
7046
+ url = `/node_modules/q5/builtinFonts/${fontName}-msdf.json`;
7008
7047
  }
7048
+ return $.loadFont(url, cb);
7009
7049
  };
7010
7050
 
7011
7051
  $._textSize = 18;
@@ -7021,7 +7061,7 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7021
7061
  if (typeof fontName != 'string') fontName = fontName.family;
7022
7062
  let font = fonts[fontName];
7023
7063
  if (font) $._font = font;
7024
- else if (font === undefined) $._loadDefaultFont(fontName);
7064
+ else if (font === undefined) return $._loadDefaultFont(fontName);
7025
7065
  };
7026
7066
 
7027
7067
  $.textSize = (size) => {
@@ -7033,6 +7073,33 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7033
7073
  }
7034
7074
  };
7035
7075
 
7076
+ let weights = {
7077
+ thin: 100,
7078
+ extralight: 200,
7079
+ light: 300,
7080
+ normal: 400,
7081
+ regular: 400,
7082
+ medium: 500,
7083
+ semibold: 600,
7084
+ bold: 700,
7085
+ bolder: 800,
7086
+ extrabold: 800,
7087
+ black: 900,
7088
+ heavy: 900
7089
+ };
7090
+
7091
+ // ranges from 0.35 (black) to 0.65 (thin)
7092
+ $._textEdge = 0.5;
7093
+
7094
+ $.textWeight = (weight) => {
7095
+ if (!weight) return $._textWeight;
7096
+ if (typeof weight == 'string') {
7097
+ weight = weights[weight.toLowerCase().replace(/[ _-]/g, '')];
7098
+ if (!weight) throw new Error(`Invalid font weight: ${weight}`);
7099
+ }
7100
+ $._textEdge = 0.6875 - weight * 0.000375;
7101
+ };
7102
+
7036
7103
  $.textLeading = (lineHeight) => {
7037
7104
  $._font.lineHeight = leading = lineHeight;
7038
7105
  leadDiff = leading - $._textSize;
@@ -7191,12 +7258,12 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7191
7258
 
7192
7259
  txt[0] = x;
7193
7260
  txt[1] = -y;
7194
- txt[2] = $._textSize / 44;
7261
+ txt[2] = $._textSize / 42;
7195
7262
  txt[3] = $._matrixIndex;
7196
7263
  txt[4] = $._doFill && $._fillSet ? $._fill : 0;
7197
7264
  txt[5] = $._stroke;
7198
7265
  txt[6] = $._doStroke && $._strokeSet ? $._strokeWeight : 0;
7199
- txt[7] = 0; // padding
7266
+ txt[7] = $._textEdge;
7200
7267
 
7201
7268
  textStack.push(txt);
7202
7269
  $._drawStack.push($._textPL, measurements.printedCharCount, $._font.index);