q5 2.4.10 → 2.5.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/README.md +5 -2
- package/package.json +1 -1
- package/q5.d.ts +68 -11
- package/q5.js +752 -154
- package/q5.min.js +1 -1
- package/src/q5-2d-canvas.js +0 -9
- package/src/q5-2d-image.js +12 -6
- package/src/q5-2d-text.js +19 -16
- package/src/q5-canvas.js +11 -7
- package/src/q5-core.js +2 -2
- package/src/q5-util.js +17 -1
- package/src/q5-webgpu-canvas.js +79 -62
- package/src/q5-webgpu-drawing.js +21 -13
- package/src/q5-webgpu-image.js +10 -12
- package/src/q5-webgpu-text.js +581 -26
- package/src/readme.md +81 -4
package/src/q5-webgpu-text.js
CHANGED
|
@@ -1,32 +1,520 @@
|
|
|
1
1
|
Q5.renderers.webgpu.text = ($, q) => {
|
|
2
|
-
let
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
|
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
|
+
};
|
|
12
343
|
|
|
13
|
-
|
|
14
|
-
$.
|
|
15
|
-
$.textSize = t.textSize;
|
|
16
|
-
$.textLeading = t.textLeading;
|
|
17
|
-
$.textStyle = t.textStyle;
|
|
18
|
-
$.textAlign = t.textAlign;
|
|
19
|
-
$.textWidth = t.textWidth;
|
|
20
|
-
$.textAscent = t.textAscent;
|
|
21
|
-
$.textDescent = t.textDescent;
|
|
344
|
+
$._charStack = [];
|
|
345
|
+
$._textStack = [];
|
|
22
346
|
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
};
|
|
393
|
+
|
|
394
|
+
let initLoadDefaultFont;
|
|
25
395
|
|
|
26
396
|
$.text = (str, x, y, w, h) => {
|
|
27
|
-
|
|
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
|
+
}
|
|
405
|
+
|
|
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
|
+
}
|
|
422
|
+
|
|
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);
|
|
496
|
+
};
|
|
497
|
+
|
|
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
|
+
}
|
|
28
514
|
|
|
29
|
-
|
|
515
|
+
let img = g.createTextImage(str, w, h);
|
|
516
|
+
|
|
517
|
+
if (img.canvas.textureIndex == undefined) {
|
|
30
518
|
$._createTexture(img);
|
|
31
519
|
} else if (img.modified) {
|
|
32
520
|
let cnv = img.canvas;
|
|
@@ -40,27 +528,94 @@ Q5.renderers.webgpu.text = ($, q) => {
|
|
|
40
528
|
);
|
|
41
529
|
img.modified = false;
|
|
42
530
|
}
|
|
43
|
-
|
|
44
|
-
$.textImage(img, x, y);
|
|
531
|
+
return img;
|
|
45
532
|
};
|
|
46
533
|
|
|
47
|
-
$.createTextImage = t.createTextImage;
|
|
48
|
-
|
|
49
534
|
$.textImage = (img, x, y) => {
|
|
535
|
+
if (typeof img == 'string') img = $.createTextImage(img);
|
|
536
|
+
|
|
50
537
|
let og = $._imageMode;
|
|
51
538
|
$._imageMode = 'corner';
|
|
52
539
|
|
|
53
|
-
let ta =
|
|
540
|
+
let ta = $._textAlign;
|
|
54
541
|
if (ta == 'center') x -= img.canvas.hw;
|
|
55
542
|
else if (ta == 'right') x -= img.width;
|
|
56
543
|
|
|
57
|
-
let bl =
|
|
58
|
-
if (bl == 'alphabetic') y -=
|
|
59
|
-
else if (bl == '
|
|
544
|
+
let bl = $._textBaseline;
|
|
545
|
+
if (bl == 'alphabetic') y -= img._leading;
|
|
546
|
+
else if (bl == 'center') y -= img._middle;
|
|
60
547
|
else if (bl == 'bottom') y -= img._bottom;
|
|
61
548
|
else if (bl == 'top') y -= img._top;
|
|
62
549
|
|
|
63
550
|
$.image(img, x, y);
|
|
64
551
|
$._imageMode = og;
|
|
65
552
|
};
|
|
553
|
+
|
|
554
|
+
$._hooks.preRender.push(() => {
|
|
555
|
+
if (!$._charStack.length) return;
|
|
556
|
+
|
|
557
|
+
// Calculate total buffer size for text data
|
|
558
|
+
let totalTextSize = 0;
|
|
559
|
+
for (let charsData of $._charStack) {
|
|
560
|
+
totalTextSize += charsData.length * 4;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Create a single buffer for all text data
|
|
564
|
+
let charBuffer = Q5.device.createBuffer({
|
|
565
|
+
label: 'charBuffer',
|
|
566
|
+
size: totalTextSize,
|
|
567
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
568
|
+
mappedAtCreation: true
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Copy all text data into the buffer
|
|
572
|
+
let textArray = new Float32Array(charBuffer.getMappedRange());
|
|
573
|
+
let o = 0;
|
|
574
|
+
for (let array of $._charStack) {
|
|
575
|
+
textArray.set(array, o);
|
|
576
|
+
o += array.length;
|
|
577
|
+
}
|
|
578
|
+
charBuffer.unmap();
|
|
579
|
+
|
|
580
|
+
// Calculate total buffer size for metadata
|
|
581
|
+
let totalMetadataSize = $._textStack.length * 6 * 4;
|
|
582
|
+
|
|
583
|
+
// Create a single buffer for all metadata
|
|
584
|
+
let textBuffer = Q5.device.createBuffer({
|
|
585
|
+
label: 'textBuffer',
|
|
586
|
+
size: totalMetadataSize,
|
|
587
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
588
|
+
mappedAtCreation: true
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// Copy all metadata into the buffer
|
|
592
|
+
let metadataArray = new Float32Array(textBuffer.getMappedRange());
|
|
593
|
+
o = 0;
|
|
594
|
+
for (let array of $._textStack) {
|
|
595
|
+
metadataArray.set(array, o);
|
|
596
|
+
o += array.length;
|
|
597
|
+
}
|
|
598
|
+
textBuffer.unmap();
|
|
599
|
+
|
|
600
|
+
// Create a single bind group for the text buffer and metadata buffer
|
|
601
|
+
$._textBindGroup = Q5.device.createBindGroup({
|
|
602
|
+
label: 'msdf text bind group',
|
|
603
|
+
layout: textBindGroupLayout,
|
|
604
|
+
entries: [
|
|
605
|
+
{
|
|
606
|
+
binding: 0,
|
|
607
|
+
resource: { buffer: charBuffer }
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
binding: 1,
|
|
611
|
+
resource: { buffer: textBuffer }
|
|
612
|
+
}
|
|
613
|
+
]
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
$._hooks.postRender.push(() => {
|
|
618
|
+
$._charStack.length = 0;
|
|
619
|
+
$._textStack.length = 0;
|
|
620
|
+
});
|
|
66
621
|
};
|