q5 2.4.5 → 2.5.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.
@@ -1,43 +1,619 @@
1
1
  Q5.renderers.webgpu.text = ($, q) => {
2
- let t = $.createGraphics(1, 1);
3
- t.pixelDensity($._pixelDensity);
4
- t._imageMode = 'corner';
2
+ let textShader = Q5.device.createShaderModule({
3
+ label: 'MSDF text shader',
4
+ code: `
5
+ // Positions for simple quad geometry
6
+ const pos = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0));
5
7
 
6
- $.loadFont = (f) => {
8
+ struct VertexInput {
9
+ @builtin(vertex_index) vertex : u32,
10
+ @builtin(instance_index) instance : u32,
11
+ };
12
+ struct VertexOutput {
13
+ @builtin(position) position : vec4f,
14
+ @location(0) texcoord : vec2f,
15
+ @location(1) colorIndex : f32
16
+ };
17
+ struct Char {
18
+ texOffset: vec2f,
19
+ texExtent: vec2f,
20
+ size: vec2f,
21
+ offset: vec2f,
22
+ };
23
+ struct Text {
24
+ pos: vec2f,
25
+ scale: f32,
26
+ transformIndex: f32,
27
+ fillIndex: f32,
28
+ strokeIndex: f32
29
+ };
30
+ struct Uniforms {
31
+ halfWidth: f32,
32
+ halfHeight: f32
33
+ };
34
+
35
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
36
+ @group(0) @binding(1) var<storage, read> transforms: array<mat4x4<f32>>;
37
+
38
+ @group(1) @binding(0) var<storage, read> colors : array<vec4f>;
39
+
40
+ @group(2) @binding(0) var fontTexture: texture_2d<f32>;
41
+ @group(2) @binding(1) var fontSampler: sampler;
42
+ @group(2) @binding(2) var<storage> fontChars: array<Char>;
43
+
44
+ @group(3) @binding(0) var<storage> textChars: array<vec4f>;
45
+ @group(3) @binding(1) var<storage> textMetadata: array<Text>;
46
+
47
+ @vertex
48
+ fn vertexMain(input : VertexInput) -> VertexOutput {
49
+ let char = textChars[input.instance];
50
+
51
+ let text = textMetadata[i32(char.w)];
52
+
53
+ let fontChar = fontChars[i32(char.z)];
54
+
55
+ let charPos = ((pos[input.vertex] * fontChar.size + char.xy + fontChar.offset) * text.scale) + text.pos;
56
+
57
+ var vert = vec4f(charPos, 0.0, 1.0);
58
+ vert = transforms[i32(text.transformIndex)] * vert;
59
+ vert.x /= uniforms.halfWidth;
60
+ vert.y /= uniforms.halfHeight;
61
+
62
+ var output : VertexOutput;
63
+ output.position = vert;
64
+ output.texcoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;
65
+ output.colorIndex = text.fillIndex;
66
+ return output;
67
+ }
68
+
69
+ fn sampleMsdf(texcoord: vec2f) -> f32 {
70
+ let c = textureSample(fontTexture, fontSampler, texcoord);
71
+ return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
72
+ }
73
+
74
+ @fragment
75
+ fn fragmentMain(input : VertexOutput) -> @location(0) vec4f {
76
+ // pxRange (AKA distanceRange) comes from the msdfgen tool,
77
+ // uses the default which is 4.
78
+ let pxRange = 4.0;
79
+ let sz = vec2f(textureDimensions(fontTexture, 0));
80
+ let dx = sz.x*length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));
81
+ let dy = sz.y*length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));
82
+ let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
83
+ let sigDist = sampleMsdf(input.texcoord) - 0.5;
84
+ let pxDist = sigDist * toPixels;
85
+ let edgeWidth = 0.5;
86
+ let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
87
+ if (alpha < 0.001) {
88
+ discard;
89
+ }
90
+ let fillColor = colors[i32(input.colorIndex)];
91
+ return vec4f(fillColor.rgb, fillColor.a * alpha);
92
+ }
93
+ `
94
+ });
95
+
96
+ class MsdfFont {
97
+ constructor(pipeline, bindGroup, lineHeight, chars, kernings) {
98
+ this.pipeline = pipeline;
99
+ this.bindGroup = bindGroup;
100
+ this.lineHeight = lineHeight;
101
+ this.chars = chars;
102
+ this.kernings = kernings;
103
+ let charArray = Object.values(chars);
104
+ this.charCount = charArray.length;
105
+ this.defaultChar = charArray[0];
106
+ }
107
+ getChar(charCode) {
108
+ return this.chars[charCode] ?? this.defaultChar;
109
+ }
110
+ // Gets the distance in pixels a line should advance for a given character code. If the upcoming
111
+ // character code is given any kerning between the two characters will be taken into account.
112
+ getXAdvance(charCode, nextCharCode = -1) {
113
+ let char = this.getChar(charCode);
114
+ if (nextCharCode >= 0) {
115
+ let kerning = this.kernings.get(charCode);
116
+ if (kerning) {
117
+ return char.xadvance + (kerning.get(nextCharCode) ?? 0);
118
+ }
119
+ }
120
+ return char.xadvance;
121
+ }
122
+ }
123
+
124
+ let textBindGroupLayout = Q5.device.createBindGroupLayout({
125
+ label: 'MSDF text group layout',
126
+ entries: [
127
+ {
128
+ binding: 0,
129
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
130
+ buffer: { type: 'read-only-storage' }
131
+ },
132
+ {
133
+ binding: 1,
134
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
135
+ buffer: { type: 'read-only-storage' }
136
+ }
137
+ ]
138
+ });
139
+
140
+ let fonts = {};
141
+
142
+ let createFont = async (fontJsonUrl, fontName, cb) => {
7
143
  q._preloadCount++;
8
- return t.loadFont(f, () => {
144
+
145
+ let res = await fetch(fontJsonUrl);
146
+ if (res.status == 404) {
9
147
  q._preloadCount--;
148
+ return '';
149
+ }
150
+ let atlas = await res.json();
151
+
152
+ let slashIdx = fontJsonUrl.lastIndexOf('/');
153
+ let baseUrl = slashIdx != -1 ? fontJsonUrl.substring(0, slashIdx + 1) : '';
154
+ // load font image
155
+ res = await fetch(baseUrl + atlas.pages[0]);
156
+ let img = await createImageBitmap(await res.blob());
157
+
158
+ // convert image to texture
159
+ let imgSize = [img.width, img.height, 1];
160
+ let texture = Q5.device.createTexture({
161
+ label: `MSDF ${fontName}`,
162
+ size: imgSize,
163
+ format: 'rgba8unorm',
164
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
165
+ });
166
+ Q5.device.queue.copyExternalImageToTexture({ source: img }, { texture }, imgSize);
167
+
168
+ // to make q5's default font file smaller,
169
+ // the chars and kernings are stored as csv strings
170
+ if (typeof atlas.chars == 'string') {
171
+ atlas.chars = $.CSV.parse(atlas.chars, ' ');
172
+ atlas.kernings = $.CSV.parse(atlas.kernings, ' ');
173
+ }
174
+
175
+ let charCount = atlas.chars.length;
176
+ let charsBuffer = Q5.device.createBuffer({
177
+ size: charCount * 32,
178
+ usage: GPUBufferUsage.STORAGE,
179
+ mappedAtCreation: true
180
+ });
181
+
182
+ let fontChars = new Float32Array(charsBuffer.getMappedRange());
183
+ let u = 1 / atlas.common.scaleW;
184
+ let v = 1 / atlas.common.scaleH;
185
+ let chars = {};
186
+ let o = 0; // offset
187
+ for (let [i, char] of atlas.chars.entries()) {
188
+ chars[char.id] = char;
189
+ chars[char.id].charIndex = i;
190
+ fontChars[o] = char.x * u; // texOffset.x
191
+ fontChars[o + 1] = char.y * v; // texOffset.y
192
+ fontChars[o + 2] = char.width * u; // texExtent.x
193
+ fontChars[o + 3] = char.height * v; // texExtent.y
194
+ fontChars[o + 4] = char.width; // size.x
195
+ fontChars[o + 5] = char.height; // size.y
196
+ fontChars[o + 6] = char.xoffset; // offset.x
197
+ fontChars[o + 7] = -char.yoffset; // offset.y
198
+ o += 8;
199
+ }
200
+ charsBuffer.unmap();
201
+
202
+ let fontSampler = Q5.device.createSampler({
203
+ minFilter: 'linear',
204
+ magFilter: 'linear',
205
+ mipmapFilter: 'linear',
206
+ maxAnisotropy: 16
207
+ });
208
+ let fontBindGroupLayout = Q5.device.createBindGroupLayout({
209
+ label: 'MSDF font group layout',
210
+ entries: [
211
+ {
212
+ binding: 0,
213
+ visibility: GPUShaderStage.FRAGMENT,
214
+ texture: {}
215
+ },
216
+ {
217
+ binding: 1,
218
+ visibility: GPUShaderStage.FRAGMENT,
219
+ sampler: {}
220
+ },
221
+ {
222
+ binding: 2,
223
+ visibility: GPUShaderStage.VERTEX,
224
+ buffer: { type: 'read-only-storage' }
225
+ }
226
+ ]
227
+ });
228
+ let fontPipeline = Q5.device.createRenderPipeline({
229
+ label: 'msdf font pipeline',
230
+ layout: Q5.device.createPipelineLayout({
231
+ bindGroupLayouts: [...$.bindGroupLayouts, fontBindGroupLayout, textBindGroupLayout]
232
+ }),
233
+ vertex: {
234
+ module: textShader,
235
+ entryPoint: 'vertexMain'
236
+ },
237
+ fragment: {
238
+ module: textShader,
239
+ entryPoint: 'fragmentMain',
240
+ targets: [
241
+ {
242
+ format: 'bgra8unorm',
243
+ blend: {
244
+ color: {
245
+ srcFactor: 'src-alpha',
246
+ dstFactor: 'one-minus-src-alpha'
247
+ },
248
+ alpha: {
249
+ srcFactor: 'one',
250
+ dstFactor: 'one'
251
+ }
252
+ }
253
+ }
254
+ ]
255
+ },
256
+ primitive: {
257
+ topology: 'triangle-strip',
258
+ stripIndexFormat: 'uint32'
259
+ }
260
+ });
261
+
262
+ let fontBindGroup = Q5.device.createBindGroup({
263
+ label: 'msdf font bind group',
264
+ layout: fontBindGroupLayout,
265
+ entries: [
266
+ {
267
+ binding: 0,
268
+ resource: texture.createView()
269
+ },
270
+ { binding: 1, resource: fontSampler },
271
+ { binding: 2, resource: { buffer: charsBuffer } }
272
+ ]
273
+ });
274
+
275
+ let kernings = new Map();
276
+ if (atlas.kernings) {
277
+ for (let kerning of atlas.kernings) {
278
+ let charKerning = kernings.get(kerning.first);
279
+ if (!charKerning) {
280
+ charKerning = new Map();
281
+ kernings.set(kerning.first, charKerning);
282
+ }
283
+ charKerning.set(kerning.second, kerning.amount);
284
+ }
285
+ }
286
+
287
+ $._font = new MsdfFont(fontPipeline, fontBindGroup, atlas.common.lineHeight, chars, kernings);
288
+
289
+ fonts[fontName] = $._font;
290
+ $.pipelines[2] = $._font.pipeline;
291
+
292
+ q._preloadCount--;
293
+
294
+ if (cb) cb(fontName);
295
+ };
296
+
297
+ // q2d graphics context to use for text image creation
298
+ let g = $.createGraphics(1, 1);
299
+ g.colorMode($.RGB, 1);
300
+
301
+ $.loadFont = (url, cb) => {
302
+ let ext = url.slice(url.lastIndexOf('.') + 1);
303
+ if (ext != 'json') return g.loadFont(url, cb);
304
+ let fontName = url.slice(url.lastIndexOf('/') + 1, url.lastIndexOf('-'));
305
+ createFont(url, fontName, cb);
306
+ return fontName;
307
+ };
308
+
309
+ $._textSize = 18;
310
+ $._textAlign = 'left';
311
+ $._textBaseline = 'alphabetic';
312
+ let leadingSet = false,
313
+ leading = 22.5,
314
+ leadDiff = 4.5,
315
+ leadPercent = 1.25;
316
+
317
+ $.textFont = (fontName) => {
318
+ $._font = fonts[fontName];
319
+
320
+ // replay the change of font in the draw stack
321
+ $.drawStack.push(-1, () => {
322
+ $._font = fonts[fontName];
323
+ $.pipelines[2] = $._font.pipeline;
10
324
  });
11
325
  };
12
- $.textFont = t.textFont;
13
- $.textSize = t.textSize;
14
- $.textLeading = t.textLeading;
15
- $.textStyle = t.textStyle;
16
- $.textAlign = t.textAlign;
17
- $.textWidth = t.textWidth;
18
- $.textAscent = t.textAscent;
19
- $.textDescent = t.textDescent;
326
+ $.textSize = (size) => {
327
+ $._textSize = size;
328
+ if (!leadingSet) {
329
+ leading = size * leadPercent;
330
+ leadDiff = leading - size;
331
+ }
332
+ };
333
+ $.textLeading = (lineHeight) => {
334
+ $._font.lineHeight = leading = lineHeight;
335
+ leadDiff = leading - $._textSize;
336
+ leadPercent = leading / $._textSize;
337
+ leadingSet = true;
338
+ };
339
+ $.textAlign = (horiz, vert) => {
340
+ $._textAlign = horiz;
341
+ if (vert) $._textBaseline = vert;
342
+ };
343
+
344
+ $._charStack = [];
345
+ $._textStack = [];
346
+
347
+ let measureText = (font, text, charCallback) => {
348
+ let maxWidth = 0,
349
+ offsetX = 0,
350
+ offsetY = 0,
351
+ line = 0,
352
+ printedCharCount = 0,
353
+ lineWidths = [],
354
+ nextCharCode = text.charCodeAt(0);
355
+
356
+ for (let i = 0; i < text.length; ++i) {
357
+ let charCode = nextCharCode;
358
+ nextCharCode = i < text.length - 1 ? text.charCodeAt(i + 1) : -1;
359
+ switch (charCode) {
360
+ case 10: // Newline
361
+ lineWidths.push(offsetX);
362
+ line++;
363
+ maxWidth = Math.max(maxWidth, offsetX);
364
+ offsetX = 0;
365
+ offsetY -= font.lineHeight * leadPercent;
366
+ break;
367
+ case 13: // CR
368
+ break;
369
+ case 32: // Space
370
+ // advance the offset without actually adding a character
371
+ offsetX += font.getXAdvance(charCode);
372
+ break;
373
+ case 9: // Tab
374
+ offsetX += font.getXAdvance(charCode) * 2;
375
+ break;
376
+ default:
377
+ if (charCallback) {
378
+ charCallback(offsetX, offsetY, line, font.getChar(charCode));
379
+ }
380
+ offsetX += font.getXAdvance(charCode, nextCharCode);
381
+ printedCharCount++;
382
+ }
383
+ }
384
+ lineWidths.push(offsetX);
385
+ maxWidth = Math.max(maxWidth, offsetX);
386
+ return {
387
+ width: maxWidth,
388
+ height: lineWidths.length * font.lineHeight * leadPercent,
389
+ lineWidths,
390
+ printedCharCount
391
+ };
392
+ };
20
393
 
21
- $.textFill = (r, g, b, a) => t.fill($.color(r, g, b, a));
22
- $.textStroke = (r, g, b, a) => t.stroke($.color(r, g, b, a));
394
+ let initLoadDefaultFont;
23
395
 
24
396
  $.text = (str, x, y, w, h) => {
25
- let img = t.createTextImage(str, w, h);
397
+ if (!$._font) {
398
+ // check if online and loading the default font hasn't been attempted yet
399
+ if (navigator.onLine && !initLoadDefaultFont) {
400
+ initLoadDefaultFont = true;
401
+ $.loadFont('https://q5js.org/fonts/YaHei-msdf.json');
402
+ }
403
+ return;
404
+ }
26
405
 
27
- if (img.canvas.textureIndex == undefined) $._createTexture(img);
406
+ if (str.length > w) {
407
+ let wrapped = [];
408
+ let i = 0;
409
+ while (i < str.length) {
410
+ let max = i + w;
411
+ if (max >= str.length) {
412
+ wrapped.push(str.slice(i));
413
+ break;
414
+ }
415
+ let end = str.lastIndexOf(' ', max);
416
+ if (end == -1 || end < i) end = max;
417
+ wrapped.push(str.slice(i, end));
418
+ i = end + 1;
419
+ }
420
+ str = wrapped.join('\n');
421
+ }
28
422
 
29
- $.textImage(img, x, y);
423
+ let spaces = 0, // whitespace char count, not literal spaces
424
+ hasNewline;
425
+ for (let i = 0; i < str.length; i++) {
426
+ let c = str[i];
427
+ switch (c) {
428
+ case '\n':
429
+ hasNewline = true;
430
+ case '\r':
431
+ case '\t':
432
+ case ' ':
433
+ spaces++;
434
+ }
435
+ }
436
+
437
+ let charsData = new Float32Array((str.length - spaces) * 4);
438
+
439
+ let ta = $._textAlign,
440
+ tb = $._textBaseline,
441
+ textIndex = $._textStack.length,
442
+ o = 0, // offset
443
+ measurements;
444
+
445
+ if (ta == 'left' && !hasNewline) {
446
+ measurements = measureText($._font, str, (textX, textY, line, char) => {
447
+ charsData[o] = textX;
448
+ charsData[o + 1] = textY;
449
+ charsData[o + 2] = char.charIndex;
450
+ charsData[o + 3] = textIndex;
451
+ o += 4;
452
+ });
453
+
454
+ if (tb == 'alphabetic') y -= $._textSize;
455
+ else if (tb == 'center') y -= $._textSize * 0.5;
456
+ else if (tb == 'bottom') y -= leading;
457
+ } else {
458
+ // measure the text to get the line widths before setting
459
+ // the x position to properly align the text
460
+ measurements = measureText($._font, str);
461
+
462
+ let offsetY = 0;
463
+ if (tb == 'alphabetic') y -= $._textSize;
464
+ else if (tb == 'center') offsetY = measurements.height * 0.5;
465
+ else if (tb == 'bottom') offsetY = measurements.height;
466
+
467
+ measureText($._font, str, (textX, textY, line, char) => {
468
+ let offsetX = 0;
469
+ if (ta == 'center') {
470
+ offsetX = measurements.width * -0.5 - (measurements.width - measurements.lineWidths[line]) * -0.5;
471
+ } else if (ta == 'right') {
472
+ offsetX = measurements.width - measurements.lineWidths[line];
473
+ }
474
+ charsData[o] = textX + offsetX;
475
+ charsData[o + 1] = textY + offsetY;
476
+ charsData[o + 2] = char.charIndex;
477
+ charsData[o + 3] = textIndex;
478
+ o += 4;
479
+ });
480
+ }
481
+ $._charStack.push(charsData);
482
+
483
+ let text = new Float32Array(6);
484
+
485
+ if ($._matrixDirty) $._saveMatrix();
486
+
487
+ text[0] = x;
488
+ text[1] = -y;
489
+ text[2] = $._textSize / 44;
490
+ text[3] = $._transformIndex;
491
+ text[4] = $._fillIndex;
492
+ text[5] = $._strokeIndex;
493
+
494
+ $._textStack.push(text);
495
+ $.drawStack.push(2, measurements.printedCharCount);
30
496
  };
31
497
 
32
- $.createTextImage = t.createTextImage;
498
+ $.textWidth = (str) => {
499
+ if (!$._font) return 0;
500
+ return measureText($._font, str).width;
501
+ };
502
+
503
+ $.createTextImage = (str, w, h) => {
504
+ g.textSize($._textSize);
505
+
506
+ if ($._doFill) {
507
+ let fi = $._fillIndex * 4;
508
+ g.fill(colorsStack.slice(fi, fi + 4));
509
+ }
510
+ if ($._doStroke) {
511
+ let si = $._strokeIndex * 4;
512
+ g.stroke(colorsStack.slice(si, si + 4));
513
+ }
514
+
515
+ let img = g.createTextImage(str, w, h);
516
+
517
+ if (img.canvas.textureIndex == undefined) {
518
+ $._createTexture(img);
519
+ } else if (img.modified) {
520
+ let cnv = img.canvas;
521
+ let textureSize = [cnv.width, cnv.height, 1];
522
+ let texture = $._textures[cnv.textureIndex];
523
+
524
+ Q5.device.queue.copyExternalImageToTexture(
525
+ { source: cnv },
526
+ { texture, colorSpace: $.canvas.colorSpace },
527
+ textureSize
528
+ );
529
+ img.modified = false;
530
+ }
531
+ return img;
532
+ };
33
533
 
34
534
  $.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;
535
+ let og = $._imageMode;
536
+ $._imageMode = 'corner';
537
+
538
+ let ta = $._textAlign;
539
+ if (ta == 'center') x -= img.canvas.hw;
540
+ else if (ta == 'right') x -= img.width;
541
+
542
+ let bl = $._textBaseline;
543
+ if (bl == 'alphabetic') y -= img._leading;
544
+ else if (bl == 'center') y -= img._middle;
545
+ else if (bl == 'bottom') y -= img._bottom;
546
+ else if (bl == 'top') y -= img._top;
547
+
41
548
  $.image(img, x, y);
549
+ $._imageMode = og;
42
550
  };
551
+
552
+ $._hooks.preRender.push(() => {
553
+ if (!$._charStack.length) return;
554
+
555
+ // Calculate total buffer size for text data
556
+ let totalTextSize = 0;
557
+ for (let charsData of $._charStack) {
558
+ totalTextSize += charsData.length * 4;
559
+ }
560
+
561
+ // Create a single buffer for all text data
562
+ let charBuffer = Q5.device.createBuffer({
563
+ label: 'charBuffer',
564
+ size: totalTextSize,
565
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
566
+ mappedAtCreation: true
567
+ });
568
+
569
+ // Copy all text data into the buffer
570
+ let textArray = new Float32Array(charBuffer.getMappedRange());
571
+ let o = 0;
572
+ for (let array of $._charStack) {
573
+ textArray.set(array, o);
574
+ o += array.length;
575
+ }
576
+ charBuffer.unmap();
577
+
578
+ // Calculate total buffer size for metadata
579
+ let totalMetadataSize = $._textStack.length * 6 * 4;
580
+
581
+ // Create a single buffer for all metadata
582
+ let textBuffer = Q5.device.createBuffer({
583
+ label: 'textBuffer',
584
+ size: totalMetadataSize,
585
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
586
+ mappedAtCreation: true
587
+ });
588
+
589
+ // Copy all metadata into the buffer
590
+ let metadataArray = new Float32Array(textBuffer.getMappedRange());
591
+ o = 0;
592
+ for (let array of $._textStack) {
593
+ metadataArray.set(array, o);
594
+ o += array.length;
595
+ }
596
+ textBuffer.unmap();
597
+
598
+ // Create a single bind group for the text buffer and metadata buffer
599
+ $._textBindGroup = Q5.device.createBindGroup({
600
+ label: 'msdf text bind group',
601
+ layout: textBindGroupLayout,
602
+ entries: [
603
+ {
604
+ binding: 0,
605
+ resource: { buffer: charBuffer }
606
+ },
607
+ {
608
+ binding: 1,
609
+ resource: { buffer: textBuffer }
610
+ }
611
+ ]
612
+ });
613
+ });
614
+
615
+ $._hooks.postRender.push(() => {
616
+ $._charStack.length = 0;
617
+ $._textStack.length = 0;
618
+ });
43
619
  };