q5 2.4.5 → 2.4.10

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/src/q5-2d-text.js CHANGED
@@ -1,9 +1,21 @@
1
1
  Q5.renderers.q2d.text = ($, q) => {
2
- $._textFont = 'sans-serif';
3
- $._textSize = 12;
4
- $._textLeading = 15;
5
- $._textLeadDiff = 3;
6
- $._textStyle = 'normal';
2
+ $._textAlign = 'left';
3
+ $._textBaseline = 'alphabetic';
4
+
5
+ let font = 'sans-serif',
6
+ tSize = 12,
7
+ leading = 15,
8
+ leadDiff = 3,
9
+ emphasis = 'normal',
10
+ fontMod = false,
11
+ styleHash = 0,
12
+ styleHashes = [],
13
+ useCache = false,
14
+ genTextImage = false,
15
+ cacheSize = 0,
16
+ cacheMax = 12000;
17
+
18
+ let cache = ($._textCache = {});
7
19
 
8
20
  $.loadFont = (url, cb) => {
9
21
  q._preloadCount++;
@@ -16,156 +28,162 @@ Q5.renderers.q2d.text = ($, q) => {
16
28
  });
17
29
  return name;
18
30
  };
19
- $.textFont = (x) => ($._textFont = x);
31
+
32
+ $.textFont = (x) => {
33
+ font = x;
34
+ fontMod = true;
35
+ styleHash = -1;
36
+ };
20
37
  $.textSize = (x) => {
21
- if (x === undefined) return $._textSize;
38
+ if (x === undefined) return tSize;
22
39
  if ($._da) x *= $._da;
23
- $._textSize = x;
40
+ tSize = x;
41
+ fontMod = true;
42
+ styleHash = -1;
24
43
  if (!$._leadingSet) {
25
- $._textLeading = x * 1.25;
26
- $._textLeadDiff = $._textLeading - x;
44
+ leading = x * 1.25;
45
+ leadDiff = leading - x;
27
46
  }
28
47
  };
48
+ $.textStyle = (x) => {
49
+ emphasis = x;
50
+ fontMod = true;
51
+ styleHash = -1;
52
+ };
29
53
  $.textLeading = (x) => {
30
- if (x === undefined) return $._textLeading;
54
+ if (x === undefined) return leading;
31
55
  if ($._da) x *= $._da;
32
- $._textLeading = x;
33
- $._textLeadDiff = x - $._textSize;
56
+ leading = x;
57
+ leadDiff = x - tSize;
34
58
  $._leadingSet = true;
59
+ styleHash = -1;
35
60
  };
36
- $.textStyle = (x) => ($._textStyle = x);
37
61
  $.textAlign = (horiz, vert) => {
38
- $.ctx.textAlign = horiz;
62
+ $.ctx.textAlign = $._textAlign = horiz;
39
63
  if (vert) {
40
- $.ctx.textBaseline = vert == $.CENTER ? 'middle' : vert;
64
+ $.ctx.textBaseline = $._textBaseline = vert == $.CENTER ? 'middle' : vert;
41
65
  }
66
+ styleHash = -1;
42
67
  };
43
- $.textWidth = (str) => {
44
- $.ctx.font = `${$._textStyle} ${$._textSize}px ${$._textFont}`;
45
- return $.ctx.measureText(str).width;
46
- };
47
- $.textAscent = (str) => {
48
- $.ctx.font = `${$._textStyle} ${$._textSize}px ${$._textFont}`;
49
- return $.ctx.measureText(str).actualBoundingBoxAscent;
50
- };
51
- $.textDescent = (str) => {
52
- $.ctx.font = `${$._textStyle} ${$._textSize}px ${$._textFont}`;
53
- return $.ctx.measureText(str).actualBoundingBoxDescent;
54
- };
68
+
69
+ $.textWidth = (str) => $.ctx.measureText(str).width;
70
+ $.textAscent = (str) => $.ctx.measureText(str).actualBoundingBoxAscent;
71
+ $.textDescent = (str) => $.ctx.measureText(str).actualBoundingBoxDescent;
72
+
55
73
  $.textFill = $.fill;
56
74
  $.textStroke = $.stroke;
57
75
 
58
- $._textCache = !!Q5.Image;
59
- $._TimedCache = class extends Map {
60
- constructor() {
61
- super();
62
- this.maxSize = 50000;
63
- }
64
- set(k, v) {
65
- v.lastAccessed = Date.now();
66
- super.set(k, v);
67
- if (this.size > this.maxSize) this.gc();
68
- }
69
- get(k) {
70
- const v = super.get(k);
71
- if (v) v.lastAccessed = Date.now();
72
- return v;
73
- }
74
- gc() {
75
- let t = Infinity;
76
- let oldest;
77
- let i = 0;
78
- for (const [k, v] of this.entries()) {
79
- if (v.lastAccessed < t) {
80
- t = v.lastAccessed;
81
- oldest = i;
82
- }
83
- i++;
84
- }
85
- i = oldest;
86
- for (const k of this.keys()) {
87
- if (i == 0) {
88
- oldest = k;
89
- break;
90
- }
91
- i--;
92
- }
93
- this.delete(oldest);
76
+ let updateStyleHash = () => {
77
+ let styleString = font + tSize + emphasis + leading;
78
+
79
+ let hash = 5381;
80
+ for (let i = 0; i < styleString.length; i++) {
81
+ hash = (hash * 33) ^ styleString.charCodeAt(i);
94
82
  }
83
+ styleHash = hash >>> 0;
95
84
  };
96
- $._tic = new $._TimedCache();
97
- $.textCache = (b, maxSize) => {
98
- if (maxSize) $._tic.maxSize = maxSize;
99
- if (b !== undefined) $._textCache = b;
100
- return $._textCache;
101
- };
102
- $._genTextImageKey = (str, w, h) => {
103
- return (
104
- str.slice(0, 200) +
105
- $._textStyle +
106
- $._textSize +
107
- $._textFont +
108
- ($._doFill ? $.ctx.fillStyle : '') +
109
- '_' +
110
- ($._doStroke && $._strokeSet ? $.ctx.lineWidth + $.ctx.strokeStyle + '_' : '') +
111
- (w || '') +
112
- (h ? 'x' + h : '')
113
- );
85
+
86
+ $.textCache = (enable, maxSize) => {
87
+ if (maxSize) cacheMax = maxSize;
88
+ if (enable !== undefined) useCache = enable;
89
+ return useCache;
114
90
  };
115
91
  $.createTextImage = (str, w, h) => {
116
- let k = $._genTextImageKey(str, w, h);
117
- if ($._tic.get(k)) return $._tic.get(k);
118
-
119
- let og = $._textCache;
120
- $._textCache = true;
121
- $._genTextImage = true;
122
- $.text(str, 0, 0, w, h);
123
- $._genTextImage = false;
124
- $._textCache = og;
125
- return $._tic.get(k);
92
+ genTextImage = true;
93
+ img = $.text(str, 0, 0, w, h);
94
+ genTextImage = false;
95
+ return img;
126
96
  };
97
+
98
+ let lines = [];
127
99
  $.text = (str, x, y, w, h) => {
128
100
  if (str === undefined || (!$._doFill && !$._doStroke)) return;
129
101
  str = str.toString();
130
- let lines = str.split('\n');
131
102
  if ($._da) {
132
103
  x *= $._da;
133
104
  y *= $._da;
134
105
  }
135
106
  let ctx = $.ctx;
136
- ctx.font = `${$._textStyle} ${$._textSize}px ${$._textFont}`;
107
+ let img, tX, tY;
137
108
 
138
- let useCache, img, cacheKey, tX, tY, ascent, descent;
109
+ if (fontMod) {
110
+ ctx.font = `${emphasis} ${tSize}px ${font}`;
111
+ fontMod = false;
112
+ }
139
113
 
140
- if (!(useCache = $._genTextImage) && $._textCache) {
141
- let transform = $.ctx.getTransform();
142
- useCache = transform.b != 0 || transform.c != 0;
114
+ if (useCache || genTextImage) {
115
+ if (styleHash == -1) updateStyleHash();
116
+
117
+ img = cache[str];
118
+ if (img) img = img[styleHash];
119
+
120
+ if (img) {
121
+ if (img._fill == $._fill && img._stroke == $._stroke && img._strokeWeight == $._strokeWeight) {
122
+ if (genTextImage) return img;
123
+ return $.textImage(img, x, y);
124
+ } else img.clear();
125
+ }
143
126
  }
144
127
 
145
- if (!useCache) {
128
+ if (str.indexOf('\n') == -1) lines[0] = str;
129
+ else lines = str.split('\n');
130
+
131
+ if (w) {
132
+ let wrapped = [];
133
+ for (let line of lines) {
134
+ let i = 0;
135
+
136
+ while (i < line.length) {
137
+ let max = i + w;
138
+ if (max >= line.length) {
139
+ wrapped.push(line.slice(i));
140
+ break;
141
+ }
142
+ let end = line.lastIndexOf(' ', max);
143
+ if (end === -1 || end < i) {
144
+ end = max;
145
+ }
146
+ wrapped.push(line.slice(i, end));
147
+ i = end;
148
+ }
149
+ }
150
+ lines = wrapped;
151
+ }
152
+
153
+ if (!useCache && !genTextImage) {
146
154
  tX = x;
147
155
  tY = y;
148
156
  } else {
149
- cacheKey = $._genTextImageKey(str, w, h);
150
- img = $._tic.get(cacheKey);
151
- if (img && !$._genTextImage) return $.textImage(img, x, y);
152
-
153
157
  tX = 0;
154
- tY = $._textLeading * lines.length;
155
- let measure = ctx.measureText(' ');
156
- ascent = measure.fontBoundingBoxAscent;
157
- descent = measure.fontBoundingBoxDescent;
158
- h ??= tY + descent;
158
+ tY = leading * lines.length;
159
+
160
+ if (!img) {
161
+ let measure = ctx.measureText(' ');
162
+ let ascent = measure.fontBoundingBoxAscent;
163
+ let descent = measure.fontBoundingBoxDescent;
164
+ h ??= tY + descent;
159
165
 
160
- img = $.createImage.call($, Math.ceil(ctx.measureText(str).width), Math.ceil(h), {
161
- pixelDensity: $._pixelDensity
162
- });
166
+ img = $.createImage.call($, Math.ceil(ctx.measureText(str).width), Math.ceil(h), {
167
+ pixelDensity: $._pixelDensity
168
+ });
169
+
170
+ img._ascent = ascent;
171
+ img._descent = descent;
172
+ img._top = descent + leadDiff;
173
+ img._middle = img._top + ascent * 0.5;
174
+ img._bottom = img._top + ascent;
175
+ }
176
+
177
+ img._fill = $._fill;
178
+ img._stroke = $._stroke;
179
+ img._strokeWeight = $._strokeWeight;
180
+ img.modified = true;
163
181
 
164
182
  ctx = img.ctx;
165
183
 
166
184
  ctx.font = $.ctx.font;
167
- ctx.fillStyle = $.ctx.fillStyle;
168
- ctx.strokeStyle = $.ctx.strokeStyle;
185
+ ctx.fillStyle = $._fill;
186
+ ctx.strokeStyle = $._stroke;
169
187
  ctx.lineWidth = $.ctx.lineWidth;
170
188
  }
171
189
 
@@ -175,31 +193,49 @@ Q5.renderers.q2d.text = ($, q) => {
175
193
  ctx.fillStyle = 'black';
176
194
  }
177
195
 
178
- for (let i = 0; i < lines.length; i++) {
179
- if ($._doStroke && $._strokeSet) ctx.strokeText(lines[i], tX, tY);
180
- if ($._doFill) ctx.fillText(lines[i], tX, tY);
181
- tY += $._textLeading;
196
+ for (let line of lines) {
197
+ if ($._doStroke && $._strokeSet) ctx.strokeText(line, tX, tY);
198
+ if ($._doFill) ctx.fillText(line, tX, tY);
199
+ tY += leading;
182
200
  if (tY > h) break;
183
201
  }
202
+ lines.length = 0;
184
203
 
185
204
  if (!$._fillSet) ctx.fillStyle = ogFill;
186
205
 
187
- if (useCache) {
188
- img._ascent = ascent;
189
- img._descent = descent;
190
- $._tic.set(cacheKey, img);
191
- if (!$._genTextImage) $.textImage(img, x, y);
206
+ if (useCache || genTextImage) {
207
+ styleHashes.push(styleHash);
208
+ (cache[str] ??= {})[styleHash] = img;
209
+
210
+ cacheSize++;
211
+ if (cacheSize > cacheMax) {
212
+ let half = Math.ceil(cacheSize / 2);
213
+ let hashes = styleHashes.splice(0, half);
214
+ for (let s in cache) {
215
+ s = cache[s];
216
+ for (let h of hashes) delete s[h];
217
+ }
218
+ cacheSize -= half;
219
+ }
220
+
221
+ if (genTextImage) return img;
222
+ $.textImage(img, x, y);
192
223
  }
193
224
  };
194
225
  $.textImage = (img, x, y) => {
195
226
  let og = $._imageMode;
196
227
  $._imageMode = 'corner';
197
- if ($.ctx.textAlign == 'center') x -= img.width * 0.5;
198
- else if ($.ctx.textAlign == 'right') x -= img.width;
199
- if ($.ctx.textBaseline == 'alphabetic') y -= $._textLeading;
200
- if ($.ctx.textBaseline == 'middle') y -= img._descent + img._ascent * 0.5 + $._textLeadDiff;
201
- else if ($.ctx.textBaseline == 'bottom') y -= img._ascent + img._descent + $._textLeadDiff;
202
- else if ($.ctx.textBaseline == 'top') y -= img._descent + $._textLeadDiff;
228
+
229
+ let ta = $._textAlign;
230
+ if (ta == 'center') x -= img.canvas.hw;
231
+ else if (ta == 'right') x -= img.width;
232
+
233
+ let bl = $._textBaseline;
234
+ if (bl == 'alphabetic') y -= leading;
235
+ else if (bl == 'middle') y -= img._middle;
236
+ else if (bl == 'bottom') y -= img._bottom;
237
+ else if (bl == 'top') y -= img._top;
238
+
203
239
  $.image(img, x, y);
204
240
  $._imageMode = og;
205
241
  };
@@ -60,8 +60,8 @@ Q5.renderers.webgpu.canvas = ($, q) => {
60
60
  $._createCanvas = (w, h, opt) => {
61
61
  q.ctx = q.drawingContext = c.getContext('webgpu');
62
62
 
63
- opt.format = navigator.gpu.getPreferredCanvasFormat();
64
- opt.device = Q5.device;
63
+ opt.format ??= navigator.gpu.getPreferredCanvasFormat();
64
+ opt.device ??= Q5.device;
65
65
 
66
66
  $.ctx.configure(opt);
67
67
 
@@ -118,20 +118,30 @@ fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
118
118
  minFilter: 'linear'
119
119
  });
120
120
 
121
+ let MAX_TEXTURES = 12000;
122
+
123
+ $._textures = [];
124
+ let tIdx = 0;
125
+
121
126
  $._createTexture = (img) => {
122
127
  if (img.canvas) img = img.canvas;
123
128
 
124
129
  let textureSize = [img.width, img.height, 1];
125
130
 
126
- const texture = Q5.device.createTexture({
131
+ let texture = Q5.device.createTexture({
127
132
  size: textureSize,
128
133
  format: 'bgra8unorm',
129
134
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
130
135
  });
131
136
 
132
- Q5.device.queue.copyExternalImageToTexture({ source: img }, { texture }, textureSize);
137
+ Q5.device.queue.copyExternalImageToTexture(
138
+ { source: img },
139
+ { texture, colorSpace: $.canvas.colorSpace },
140
+ textureSize
141
+ );
133
142
 
134
- img.textureIndex = $._textureBindGroups.length;
143
+ $._textures[tIdx] = texture;
144
+ img.textureIndex = tIdx;
135
145
 
136
146
  const textureBindGroup = Q5.device.createBindGroup({
137
147
  layout: textureLayout,
@@ -140,7 +150,16 @@ fn fragmentMain(@location(0) texCoord: vec2<f32>) -> @location(0) vec4<f32> {
140
150
  { binding: 1, resource: texture.createView() }
141
151
  ]
142
152
  });
143
- $._textureBindGroups.push(textureBindGroup);
153
+ $._textureBindGroups[tIdx] = textureBindGroup;
154
+
155
+ tIdx = (tIdx + 1) % MAX_TEXTURES;
156
+
157
+ // If the texture array is full, destroy the oldest texture
158
+ if ($._textures[tIdx]) {
159
+ $._textures[tIdx].destroy();
160
+ delete $._textures[tIdx];
161
+ delete $._textureBindGroups[tIdx];
162
+ }
144
163
  };
145
164
 
146
165
  $.loadImage = $.loadTexture = (src) => {
@@ -9,6 +9,8 @@ Q5.renderers.webgpu.text = ($, q) => {
9
9
  q._preloadCount--;
10
10
  });
11
11
  };
12
+
13
+ // directly add these text setting functions to the webgpu renderer
12
14
  $.textFont = t.textFont;
13
15
  $.textSize = t.textSize;
14
16
  $.textLeading = t.textLeading;
@@ -24,7 +26,20 @@ Q5.renderers.webgpu.text = ($, q) => {
24
26
  $.text = (str, x, y, w, h) => {
25
27
  let img = t.createTextImage(str, w, h);
26
28
 
27
- if (img.canvas.textureIndex == undefined) $._createTexture(img);
29
+ if (img.canvas.textureIndex === undefined) {
30
+ $._createTexture(img);
31
+ } else if (img.modified) {
32
+ let cnv = img.canvas;
33
+ let textureSize = [cnv.width, cnv.height, 1];
34
+ let texture = $._textures[cnv.textureIndex];
35
+
36
+ Q5.device.queue.copyExternalImageToTexture(
37
+ { source: cnv },
38
+ { texture, colorSpace: $.canvas.colorSpace },
39
+ textureSize
40
+ );
41
+ img.modified = false;
42
+ }
28
43
 
29
44
  $.textImage(img, x, y);
30
45
  };
@@ -32,12 +47,20 @@ Q5.renderers.webgpu.text = ($, q) => {
32
47
  $.createTextImage = t.createTextImage;
33
48
 
34
49
  $.textImage = (img, x, y) => {
35
- if (t.ctx.textAlign == 'center') x -= img.width * 0.5;
36
- else if (t.ctx.textAlign == 'right') x -= img.width;
37
- if (t.ctx.textBaseline == 'alphabetic') y -= t._textLeading;
38
- if (t.ctx.textBaseline == 'middle') y -= img._descent + img._ascent * 0.5 + t._textLeadDiff;
39
- else if (t.ctx.textBaseline == 'bottom') y -= img._ascent + img._descent + t._textLeadDiff;
40
- else if (t.ctx.textBaseline == 'top') y -= img._descent + t._textLeadDiff;
50
+ let og = $._imageMode;
51
+ $._imageMode = 'corner';
52
+
53
+ let ta = t._textAlign;
54
+ if (ta == 'center') x -= img.canvas.hw;
55
+ else if (ta == 'right') x -= img.width;
56
+
57
+ let bl = t._textBaseline;
58
+ if (bl == 'alphabetic') y -= t._textLeading;
59
+ else if (bl == 'middle') y -= img._middle;
60
+ else if (bl == 'bottom') y -= img._bottom;
61
+ else if (bl == 'top') y -= img._top;
62
+
41
63
  $.image(img, x, y);
64
+ $._imageMode = og;
42
65
  };
43
66
  };
package/src/readme.md CHANGED
@@ -105,7 +105,7 @@ Image based features in this module require the q5-2d-image module.
105
105
 
106
106
  `textImage(img, x, y)` displays text images, complying with the user's text position settings instead of their image position settings. The idea is that text will appear in the same place as it would if it were drawn with the `text` function.
107
107
 
108
- `textCache(bool, maxSize)` enables or disables text caching. As of June 2024, drawing rotated text is super slow in all browsers, so q5 creates and stores images of text and rotates that instead. Can improve rendering performance 90x but uses more memory. `maxSize` param determines the maximum number of text images to cache, default is 500 since these images will typically be quite small. The text image cache (tic) is a timed cache, so the oldest images are removed first.
108
+ `textCache(bool, maxSize)` enables or disables text caching.
109
109
 
110
110
  ## webgpu-canvas
111
111
 
@@ -181,17 +181,9 @@ Implemented functions:
181
181
 
182
182
  > Use `textFill` and `textStroke` to set text colors.
183
183
 
184
- WebGPU (and WebGL) don't have fast HTML5 based text rasterization functionality, like Canvas2D does.
185
-
186
- In p5.js WebGL mode, text is drawn directly to the canvas. This is a complex task, since letters have intricate geometry: thus many triangles must be used to render text at high resolution. Unless a user wants to render a lot of text, the performance cost is actually negligible. Yet since p5.js depends on opentype.js for this, which is 528kb (171kb minified), a different approach was needed to keep q5 lightweight.
187
-
188
184
  Internally, q5's WebGPU renderer uses a q5 graphics object to draw text to a Canvas2D canvas via `createTextImage`, then converts that canvas to a WebGPU texture. Each texture is cached, so it doesn't have to be recreated every frame that users want to display the same text.
189
185
 
190
- Creating a text image is slower than rendering text if it's only displayed for one frame, but displaying static text multiple frames from a cached image is way faster than re-rendering the text. So just try not to have long strings of text that change every frame.
191
-
192
- For typical use cases, this is a great trade-off!
193
-
194
- Complete implementation of text rendering in WebGPU.
186
+ Implemented functions:
195
187
 
196
188
  `loadFont`,`textFont`, `textSize`, `textLeading`, `textStyle`, `textAlign`, `textWidth`, `textAscent`, `textDescent`, `textFill`, `textStroke`, `text`
197
189