@bloopjs/toodle 0.1.2 → 0.1.4
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/dist/Toodle.d.ts +1 -1
- package/dist/Toodle.d.ts.map +1 -1
- package/dist/backends/ITextShader.d.ts +15 -0
- package/dist/backends/ITextShader.d.ts.map +1 -0
- package/dist/backends/mod.d.ts +1 -0
- package/dist/backends/mod.d.ts.map +1 -1
- package/dist/backends/webgl2/WebGLFontPipeline.d.ts +26 -0
- package/dist/backends/webgl2/WebGLFontPipeline.d.ts.map +1 -0
- package/dist/backends/webgl2/WebGLTextShader.d.ts +24 -0
- package/dist/backends/webgl2/WebGLTextShader.d.ts.map +1 -0
- package/dist/backends/webgl2/glsl/text.glsl.d.ts +12 -0
- package/dist/backends/webgl2/glsl/text.glsl.d.ts.map +1 -0
- package/dist/backends/webgl2/mod.d.ts +2 -0
- package/dist/backends/webgl2/mod.d.ts.map +1 -1
- package/dist/{text → backends/webgpu}/FontPipeline.d.ts +1 -1
- package/dist/backends/webgpu/FontPipeline.d.ts.map +1 -0
- package/dist/{text/TextShader.d.ts → backends/webgpu/WebGPUTextShader.d.ts} +7 -7
- package/dist/backends/webgpu/WebGPUTextShader.d.ts.map +1 -0
- package/dist/backends/webgpu/mod.d.ts +2 -0
- package/dist/backends/webgpu/mod.d.ts.map +1 -1
- package/dist/backends/webgpu/wgsl/text.wgsl.d.ts.map +1 -0
- package/dist/mod.d.ts +1 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1832 -1460
- package/dist/mod.js.map +17 -14
- package/dist/{text → scene}/TextNode.d.ts +5 -5
- package/dist/scene/TextNode.d.ts.map +1 -0
- package/dist/scene/mod.d.ts +1 -0
- package/dist/scene/mod.d.ts.map +1 -1
- package/dist/text/mod.d.ts +1 -3
- package/dist/text/mod.d.ts.map +1 -1
- package/dist/textures/AssetManager.d.ts +9 -6
- package/dist/textures/AssetManager.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Toodle.ts +1 -1
- package/src/backends/ITextShader.ts +15 -0
- package/src/backends/mod.ts +1 -0
- package/src/backends/webgl2/WebGLFontPipeline.ts +173 -0
- package/src/backends/webgl2/WebGLTextShader.ts +275 -0
- package/src/backends/webgl2/glsl/text.glsl.ts +132 -0
- package/src/backends/webgl2/mod.ts +2 -0
- package/src/{text → backends/webgpu}/FontPipeline.ts +2 -2
- package/src/{text/TextShader.ts → backends/webgpu/WebGPUTextShader.ts} +14 -10
- package/src/backends/webgpu/mod.ts +2 -0
- package/src/mod.ts +1 -1
- package/src/{text → scene}/TextNode.ts +6 -6
- package/src/scene/mod.ts +1 -0
- package/src/text/mod.ts +1 -4
- package/src/textures/AssetManager.ts +46 -31
- package/dist/text/FontPipeline.d.ts.map +0 -1
- package/dist/text/TextNode.d.ts.map +0 -1
- package/dist/text/TextShader.d.ts.map +0 -1
- package/dist/text/text.wgsl.d.ts.map +0 -1
- /package/dist/{text → backends/webgpu/wgsl}/text.wgsl.d.ts +0 -0
- /package/src/{text → backends/webgpu/wgsl}/text.wgsl.ts +0 -0
package/dist/mod.js
CHANGED
|
@@ -17219,150 +17219,414 @@ class WebGPUBackend {
|
|
|
17219
17219
|
return this.#renderPass;
|
|
17220
17220
|
}
|
|
17221
17221
|
}
|
|
17222
|
-
// src/
|
|
17223
|
-
|
|
17224
|
-
|
|
17225
|
-
|
|
17226
|
-
|
|
17227
|
-
|
|
17228
|
-
|
|
17229
|
-
|
|
17230
|
-
|
|
17231
|
-
|
|
17232
|
-
|
|
17233
|
-
|
|
17234
|
-
|
|
17235
|
-
|
|
17236
|
-
|
|
17237
|
-
|
|
17238
|
-
|
|
17239
|
-
}
|
|
17240
|
-
|
|
17241
|
-
|
|
17242
|
-
|
|
17243
|
-
|
|
17244
|
-
|
|
17245
|
-
|
|
17246
|
-
|
|
17247
|
-
|
|
17248
|
-
|
|
17249
|
-
|
|
17250
|
-
|
|
17251
|
-
|
|
17252
|
-
|
|
17253
|
-
|
|
17254
|
-
|
|
17222
|
+
// src/backends/webgl2/WebGLFontPipeline.ts
|
|
17223
|
+
class WebGLFontPipeline {
|
|
17224
|
+
font;
|
|
17225
|
+
fontTexture;
|
|
17226
|
+
charDataTexture;
|
|
17227
|
+
textBufferTexture;
|
|
17228
|
+
maxCharCount;
|
|
17229
|
+
lineHeight;
|
|
17230
|
+
#gl;
|
|
17231
|
+
constructor(gl, font, fontTexture, charDataTexture, textBufferTexture, maxCharCount) {
|
|
17232
|
+
this.#gl = gl;
|
|
17233
|
+
this.font = font;
|
|
17234
|
+
this.fontTexture = fontTexture;
|
|
17235
|
+
this.charDataTexture = charDataTexture;
|
|
17236
|
+
this.textBufferTexture = textBufferTexture;
|
|
17237
|
+
this.maxCharCount = maxCharCount;
|
|
17238
|
+
this.lineHeight = font.lineHeight;
|
|
17239
|
+
}
|
|
17240
|
+
static create(gl, font, maxCharCount) {
|
|
17241
|
+
const fontTexture = gl.createTexture();
|
|
17242
|
+
assert(fontTexture, "Failed to create font texture");
|
|
17243
|
+
gl.bindTexture(gl.TEXTURE_2D, fontTexture);
|
|
17244
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
17245
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
17246
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
17247
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
17248
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, font.imageBitmap);
|
|
17249
|
+
const charDataTexture = gl.createTexture();
|
|
17250
|
+
assert(charDataTexture, "Failed to create char data texture");
|
|
17251
|
+
gl.bindTexture(gl.TEXTURE_2D, charDataTexture);
|
|
17252
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
17253
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
17254
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
17255
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
17256
|
+
const charCount = font.charCount;
|
|
17257
|
+
const charTextureWidth = charCount * 2;
|
|
17258
|
+
const charTextureData = new Float32Array(charTextureWidth * 4);
|
|
17259
|
+
for (let i3 = 0;i3 < charCount; i3++) {
|
|
17260
|
+
const srcOffset = i3 * 8;
|
|
17261
|
+
const dstOffset0 = i3 * 2 * 4;
|
|
17262
|
+
const dstOffset1 = (i3 * 2 + 1) * 4;
|
|
17263
|
+
charTextureData[dstOffset0] = font.charBuffer[srcOffset];
|
|
17264
|
+
charTextureData[dstOffset0 + 1] = font.charBuffer[srcOffset + 1];
|
|
17265
|
+
charTextureData[dstOffset0 + 2] = font.charBuffer[srcOffset + 2];
|
|
17266
|
+
charTextureData[dstOffset0 + 3] = font.charBuffer[srcOffset + 3];
|
|
17267
|
+
charTextureData[dstOffset1] = font.charBuffer[srcOffset + 4];
|
|
17268
|
+
charTextureData[dstOffset1 + 1] = font.charBuffer[srcOffset + 5];
|
|
17269
|
+
charTextureData[dstOffset1 + 2] = font.charBuffer[srcOffset + 6];
|
|
17270
|
+
charTextureData[dstOffset1 + 3] = font.charBuffer[srcOffset + 7];
|
|
17271
|
+
}
|
|
17272
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, charTextureWidth, 1, 0, gl.RGBA, gl.FLOAT, charTextureData);
|
|
17273
|
+
const textBufferTexture = gl.createTexture();
|
|
17274
|
+
assert(textBufferTexture, "Failed to create text buffer texture");
|
|
17275
|
+
gl.bindTexture(gl.TEXTURE_2D, textBufferTexture);
|
|
17276
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
17277
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
17278
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
17279
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
17280
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, maxCharCount, 1, 0, gl.RGBA, gl.FLOAT, null);
|
|
17281
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
17282
|
+
return new WebGLFontPipeline(gl, font, fontTexture, charDataTexture, textBufferTexture, maxCharCount);
|
|
17283
|
+
}
|
|
17284
|
+
updateTextBuffer(data, glyphCount) {
|
|
17285
|
+
const gl = this.#gl;
|
|
17286
|
+
gl.bindTexture(gl.TEXTURE_2D, this.textBufferTexture);
|
|
17287
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, glyphCount, 1, gl.RGBA, gl.FLOAT, data);
|
|
17288
|
+
}
|
|
17289
|
+
destroy() {
|
|
17290
|
+
const gl = this.#gl;
|
|
17291
|
+
gl.deleteTexture(this.fontTexture);
|
|
17292
|
+
gl.deleteTexture(this.charDataTexture);
|
|
17293
|
+
gl.deleteTexture(this.textBufferTexture);
|
|
17294
|
+
}
|
|
17255
17295
|
}
|
|
17256
|
-
|
|
17257
|
-
|
|
17258
|
-
|
|
17259
|
-
|
|
17260
|
-
|
|
17261
|
-
|
|
17296
|
+
|
|
17297
|
+
// src/utils/error.ts
|
|
17298
|
+
var warnings = new Map;
|
|
17299
|
+
function warnOnce(key, msg) {
|
|
17300
|
+
if (warnings.has(key)) {
|
|
17301
|
+
return;
|
|
17302
|
+
}
|
|
17303
|
+
warnings.set(key, true);
|
|
17304
|
+
console.warn(msg ?? key);
|
|
17262
17305
|
}
|
|
17263
17306
|
|
|
17264
|
-
// src/
|
|
17265
|
-
class
|
|
17266
|
-
|
|
17267
|
-
|
|
17268
|
-
|
|
17269
|
-
|
|
17270
|
-
|
|
17271
|
-
|
|
17272
|
-
|
|
17273
|
-
|
|
17274
|
-
|
|
17275
|
-
|
|
17307
|
+
// src/text/MsdfFont.ts
|
|
17308
|
+
class MsdfFont {
|
|
17309
|
+
id;
|
|
17310
|
+
json;
|
|
17311
|
+
imageBitmap;
|
|
17312
|
+
name;
|
|
17313
|
+
charset;
|
|
17314
|
+
charCount;
|
|
17315
|
+
lineHeight;
|
|
17316
|
+
charBuffer;
|
|
17317
|
+
#kernings;
|
|
17318
|
+
#chars;
|
|
17319
|
+
#fallbackCharCode;
|
|
17320
|
+
constructor(id, json, imageBitmap) {
|
|
17321
|
+
this.id = id;
|
|
17322
|
+
this.json = json;
|
|
17323
|
+
this.imageBitmap = imageBitmap;
|
|
17324
|
+
const charArray = Object.values(json.chars);
|
|
17325
|
+
this.charCount = charArray.length;
|
|
17326
|
+
this.lineHeight = json.common.lineHeight;
|
|
17327
|
+
this.charset = json.info.charset;
|
|
17328
|
+
this.name = json.info.face;
|
|
17329
|
+
this.#kernings = new Map;
|
|
17330
|
+
if (json.kernings) {
|
|
17331
|
+
for (const kearning of json.kernings) {
|
|
17332
|
+
let charKerning = this.#kernings.get(kearning.first);
|
|
17333
|
+
if (!charKerning) {
|
|
17334
|
+
charKerning = new Map;
|
|
17335
|
+
this.#kernings.set(kearning.first, charKerning);
|
|
17336
|
+
}
|
|
17337
|
+
charKerning.set(kearning.second, kearning.amount);
|
|
17338
|
+
}
|
|
17276
17339
|
}
|
|
17277
|
-
|
|
17278
|
-
|
|
17340
|
+
this.#chars = new Map;
|
|
17341
|
+
const charCount = Object.values(json.chars).length;
|
|
17342
|
+
this.charBuffer = new Float32Array(charCount * 8);
|
|
17343
|
+
let offset = 0;
|
|
17344
|
+
const u3 = 1 / json.common.scaleW;
|
|
17345
|
+
const v3 = 1 / json.common.scaleH;
|
|
17346
|
+
for (const [i3, char] of json.chars.entries()) {
|
|
17347
|
+
this.#chars.set(char.id, char);
|
|
17348
|
+
this.#chars.get(char.id).charIndex = i3;
|
|
17349
|
+
this.charBuffer[offset] = char.x * u3;
|
|
17350
|
+
this.charBuffer[offset + 1] = char.y * v3;
|
|
17351
|
+
this.charBuffer[offset + 2] = char.width * u3;
|
|
17352
|
+
this.charBuffer[offset + 3] = char.height * v3;
|
|
17353
|
+
this.charBuffer[offset + 4] = char.width;
|
|
17354
|
+
this.charBuffer[offset + 5] = char.height;
|
|
17355
|
+
this.charBuffer[offset + 6] = char.xoffset;
|
|
17356
|
+
this.charBuffer[offset + 7] = -char.yoffset;
|
|
17357
|
+
offset += 8;
|
|
17279
17358
|
}
|
|
17280
17359
|
}
|
|
17281
|
-
|
|
17282
|
-
|
|
17283
|
-
|
|
17284
|
-
|
|
17360
|
+
getChar(charCode) {
|
|
17361
|
+
const char = this.#chars.get(charCode);
|
|
17362
|
+
if (!char) {
|
|
17363
|
+
const fallbackCharacter = this.#chars.get(this.#fallbackCharCode ?? this.#chars.keys().toArray()[0]);
|
|
17364
|
+
warnOnce(`unknown_char_${this.name}`, `Couldn't find character ${charCode} in characters for font ${this.name} -- defaulting to first available character "${fallbackCharacter.char}"`);
|
|
17365
|
+
return fallbackCharacter;
|
|
17366
|
+
}
|
|
17367
|
+
return char;
|
|
17285
17368
|
}
|
|
17286
|
-
|
|
17287
|
-
|
|
17288
|
-
if (
|
|
17289
|
-
|
|
17290
|
-
|
|
17291
|
-
|
|
17369
|
+
getXAdvance(charCode, nextCharCode = -1) {
|
|
17370
|
+
const char = this.getChar(charCode);
|
|
17371
|
+
if (nextCharCode >= 0) {
|
|
17372
|
+
const kerning = this.#kernings.get(charCode);
|
|
17373
|
+
if (kerning) {
|
|
17374
|
+
return char.xadvance + (kerning.get(nextCharCode) ?? 0);
|
|
17375
|
+
}
|
|
17292
17376
|
}
|
|
17293
|
-
return
|
|
17377
|
+
return char.xadvance;
|
|
17294
17378
|
}
|
|
17295
|
-
|
|
17296
|
-
|
|
17297
|
-
|
|
17298
|
-
|
|
17299
|
-
|
|
17300
|
-
|
|
17379
|
+
static async create(id, fontJsonUrl) {
|
|
17380
|
+
const response = await fetch(fontJsonUrl);
|
|
17381
|
+
const json = await response.json();
|
|
17382
|
+
const i3 = fontJsonUrl.href.lastIndexOf("/");
|
|
17383
|
+
const baseUrl = i3 !== -1 ? fontJsonUrl.href.substring(0, i3 + 1) : undefined;
|
|
17384
|
+
if (json.pages.length < 1) {
|
|
17385
|
+
throw new Error(`Can't create an msdf font without a reference to the page url in the json`);
|
|
17386
|
+
}
|
|
17387
|
+
if (json.pages.length > 1) {
|
|
17388
|
+
throw new Error(`Can't create an msdf font with more than one page`);
|
|
17389
|
+
}
|
|
17390
|
+
const textureUrl = baseUrl + json.pages[0];
|
|
17391
|
+
const textureResponse = await fetch(textureUrl);
|
|
17392
|
+
const bitmap = await createImageBitmap(await textureResponse.blob());
|
|
17393
|
+
return new MsdfFont(id, json, bitmap);
|
|
17394
|
+
}
|
|
17395
|
+
set fallbackCharacter(character) {
|
|
17396
|
+
const charCode = character.charCodeAt(0);
|
|
17397
|
+
if (this.#chars.has(charCode)) {
|
|
17398
|
+
this.#fallbackCharCode = charCode;
|
|
17399
|
+
} else {
|
|
17400
|
+
const fallbackCode = this.#chars.keys().toArray()[0];
|
|
17401
|
+
console.warn(`${character} character does not exist in font ${this.name} defaulting to "${this.#chars.get(fallbackCode)?.char}".`);
|
|
17402
|
+
this.#fallbackCharCode = fallbackCode;
|
|
17301
17403
|
}
|
|
17302
|
-
return pipeline;
|
|
17303
17404
|
}
|
|
17304
17405
|
}
|
|
17305
17406
|
|
|
17306
|
-
// src/
|
|
17307
|
-
|
|
17308
|
-
|
|
17309
|
-
|
|
17310
|
-
|
|
17311
|
-
|
|
17312
|
-
|
|
17313
|
-
|
|
17314
|
-
|
|
17315
|
-
|
|
17316
|
-
|
|
17317
|
-
#zoom = 1;
|
|
17318
|
-
#rotation = 0;
|
|
17319
|
-
#isDirty = true;
|
|
17320
|
-
#matrix = mat3.create();
|
|
17321
|
-
get zoom() {
|
|
17322
|
-
return this.#zoom;
|
|
17323
|
-
}
|
|
17324
|
-
set zoom(value) {
|
|
17325
|
-
this.#zoom = value;
|
|
17326
|
-
this.setDirty();
|
|
17327
|
-
}
|
|
17328
|
-
get rotation() {
|
|
17329
|
-
return rad2deg(this.#rotation);
|
|
17407
|
+
// src/text/shaping.ts
|
|
17408
|
+
var TAB_SPACES = 4;
|
|
17409
|
+
function shapeText(font, text, blockSize, fontSize, formatting, textArray, initialFloatOffset = 0, debug = false) {
|
|
17410
|
+
let offset = initialFloatOffset;
|
|
17411
|
+
const measurements = measureText(font, text, formatting.wordWrap);
|
|
17412
|
+
const alignment = formatting.align || "left";
|
|
17413
|
+
const em2px = fontSize / font.lineHeight;
|
|
17414
|
+
const hackHasExplicitBlock = blockSize.width !== measurements.width;
|
|
17415
|
+
let debugData = null;
|
|
17416
|
+
if (debug) {
|
|
17417
|
+
debugData = [];
|
|
17330
17418
|
}
|
|
17331
|
-
|
|
17332
|
-
|
|
17333
|
-
|
|
17419
|
+
for (const word of measurements.words) {
|
|
17420
|
+
for (const glyph of word.glyphs) {
|
|
17421
|
+
let lineOffset = 0;
|
|
17422
|
+
if (alignment === "center") {
|
|
17423
|
+
lineOffset = measurements.width * -0.5 - (measurements.width - measurements.lineWidths[glyph.line]) * -0.5;
|
|
17424
|
+
} else if (alignment === "right") {
|
|
17425
|
+
const blockSizeEm = blockSize.width / em2px;
|
|
17426
|
+
const delta = measurements.width - measurements.lineWidths[glyph.line];
|
|
17427
|
+
lineOffset = (hackHasExplicitBlock ? blockSizeEm / 2 : measurements.width / 2) - measurements.width + delta;
|
|
17428
|
+
} else if (alignment === "left") {
|
|
17429
|
+
const blockSizeEm = blockSize.width / em2px;
|
|
17430
|
+
lineOffset = hackHasExplicitBlock ? -blockSizeEm / 2 : -measurements.width / 2;
|
|
17431
|
+
}
|
|
17432
|
+
if (debug && debugData) {
|
|
17433
|
+
debugData.push({
|
|
17434
|
+
line: glyph.line,
|
|
17435
|
+
word: word.glyphs.map((g3) => g3.char.char).join(""),
|
|
17436
|
+
glyph: glyph.char.char,
|
|
17437
|
+
startX: word.startX,
|
|
17438
|
+
glyphX: glyph.offset[0],
|
|
17439
|
+
advance: glyph.char.xadvance,
|
|
17440
|
+
lineOffset,
|
|
17441
|
+
startY: word.startY,
|
|
17442
|
+
glyphY: glyph.offset[1]
|
|
17443
|
+
});
|
|
17444
|
+
}
|
|
17445
|
+
textArray[offset] = word.startX + glyph.offset[0] + lineOffset;
|
|
17446
|
+
textArray[offset + 1] = word.startY + glyph.offset[1];
|
|
17447
|
+
textArray[offset + 2] = glyph.char.charIndex;
|
|
17448
|
+
offset += 4;
|
|
17449
|
+
}
|
|
17334
17450
|
}
|
|
17335
|
-
|
|
17336
|
-
|
|
17451
|
+
if (debug && debugData) {
|
|
17452
|
+
console.table(debugData);
|
|
17337
17453
|
}
|
|
17338
|
-
|
|
17339
|
-
|
|
17340
|
-
|
|
17454
|
+
}
|
|
17455
|
+
function measureText(font, text, wordWrap) {
|
|
17456
|
+
let maxWidth = 0;
|
|
17457
|
+
const lineWidths = [];
|
|
17458
|
+
let textOffsetX = 0;
|
|
17459
|
+
let textOffsetY = 0;
|
|
17460
|
+
let line = 0;
|
|
17461
|
+
let printedCharCount = 0;
|
|
17462
|
+
let nextCharCode = text.charCodeAt(0);
|
|
17463
|
+
let word = { glyphs: [], width: 0, startX: 0, startY: 0 };
|
|
17464
|
+
const words = [];
|
|
17465
|
+
for (let i3 = 0;i3 < text.length; i3++) {
|
|
17466
|
+
const isLastLetter = i3 === text.length - 1;
|
|
17467
|
+
const charCode = nextCharCode;
|
|
17468
|
+
nextCharCode = i3 < text.length - 1 ? text.charCodeAt(i3 + 1) : -1;
|
|
17469
|
+
switch (charCode) {
|
|
17470
|
+
case 9 /* HorizontalTab */:
|
|
17471
|
+
insertSpaces(TAB_SPACES);
|
|
17472
|
+
break;
|
|
17473
|
+
case 10 /* Newline */:
|
|
17474
|
+
flushLine();
|
|
17475
|
+
flushWord();
|
|
17476
|
+
break;
|
|
17477
|
+
case 13 /* CarriageReturn */:
|
|
17478
|
+
break;
|
|
17479
|
+
case 32 /* Space */:
|
|
17480
|
+
insertSpaces(1);
|
|
17481
|
+
break;
|
|
17482
|
+
default: {
|
|
17483
|
+
const advance = font.getXAdvance(charCode, nextCharCode);
|
|
17484
|
+
if (wordWrap && wordWrap.breakOn === "character" && textOffsetX + advance > wordWrap.emWidth) {
|
|
17485
|
+
if (word.startX === 0) {
|
|
17486
|
+
flushWord();
|
|
17487
|
+
} else {
|
|
17488
|
+
lineWidths.push(textOffsetX - word.width);
|
|
17489
|
+
line++;
|
|
17490
|
+
maxWidth = Math.max(maxWidth, textOffsetX);
|
|
17491
|
+
textOffsetX = word.width;
|
|
17492
|
+
textOffsetY -= font.lineHeight;
|
|
17493
|
+
word.startX = 0;
|
|
17494
|
+
word.startY = textOffsetY;
|
|
17495
|
+
word.glyphs.forEach((g3) => {
|
|
17496
|
+
g3.line = line;
|
|
17497
|
+
});
|
|
17498
|
+
}
|
|
17499
|
+
}
|
|
17500
|
+
word.glyphs.push({
|
|
17501
|
+
char: font.getChar(charCode),
|
|
17502
|
+
offset: [word.width, 0],
|
|
17503
|
+
line
|
|
17504
|
+
});
|
|
17505
|
+
if (isLastLetter) {
|
|
17506
|
+
flushWord();
|
|
17507
|
+
}
|
|
17508
|
+
word.width += advance;
|
|
17509
|
+
textOffsetX += advance;
|
|
17510
|
+
}
|
|
17511
|
+
}
|
|
17341
17512
|
}
|
|
17342
|
-
|
|
17343
|
-
|
|
17513
|
+
lineWidths.push(textOffsetX);
|
|
17514
|
+
maxWidth = Math.max(maxWidth, textOffsetX);
|
|
17515
|
+
const lineCount = lineWidths.length;
|
|
17516
|
+
return {
|
|
17517
|
+
width: maxWidth,
|
|
17518
|
+
height: lineCount * font.lineHeight,
|
|
17519
|
+
lineWidths,
|
|
17520
|
+
lineCount,
|
|
17521
|
+
printedCharCount,
|
|
17522
|
+
words
|
|
17523
|
+
};
|
|
17524
|
+
function flushWord() {
|
|
17525
|
+
printedCharCount += word.glyphs.length;
|
|
17526
|
+
words.push(word);
|
|
17527
|
+
word = {
|
|
17528
|
+
glyphs: [],
|
|
17529
|
+
width: 0,
|
|
17530
|
+
startX: textOffsetX,
|
|
17531
|
+
startY: textOffsetY
|
|
17532
|
+
};
|
|
17344
17533
|
}
|
|
17345
|
-
|
|
17346
|
-
|
|
17534
|
+
function flushLine() {
|
|
17535
|
+
lineWidths.push(textOffsetX);
|
|
17536
|
+
line++;
|
|
17537
|
+
maxWidth = Math.max(maxWidth, textOffsetX);
|
|
17538
|
+
textOffsetX = 0;
|
|
17539
|
+
textOffsetY -= font.lineHeight;
|
|
17347
17540
|
}
|
|
17348
|
-
|
|
17349
|
-
|
|
17350
|
-
|
|
17541
|
+
function insertSpaces(spaces) {
|
|
17542
|
+
if (spaces < 1)
|
|
17543
|
+
spaces = 1;
|
|
17544
|
+
textOffsetX += font.getXAdvance(32 /* Space */) * spaces;
|
|
17545
|
+
if (wordWrap?.breakOn === "word" && textOffsetX >= wordWrap.emWidth) {
|
|
17546
|
+
flushLine();
|
|
17547
|
+
}
|
|
17548
|
+
flushWord();
|
|
17351
17549
|
}
|
|
17352
|
-
|
|
17353
|
-
|
|
17354
|
-
|
|
17550
|
+
}
|
|
17551
|
+
function findLargestFontSize(font, text, size, formatting) {
|
|
17552
|
+
if (!formatting.fontSize) {
|
|
17553
|
+
throw new Error("fontSize is required for shrinkToFit");
|
|
17355
17554
|
}
|
|
17356
|
-
|
|
17357
|
-
|
|
17358
|
-
this.#isDirty = false;
|
|
17359
|
-
this.#matrix = createViewMatrix(this, this.#matrix);
|
|
17360
|
-
}
|
|
17361
|
-
return this.#matrix;
|
|
17555
|
+
if (!formatting.shrinkToFit) {
|
|
17556
|
+
throw new Error("shrinkToFit is required for findLargestFontSize");
|
|
17362
17557
|
}
|
|
17363
|
-
|
|
17364
|
-
|
|
17558
|
+
const minSize = formatting.shrinkToFit.minFontSize;
|
|
17559
|
+
const maxSize = formatting.shrinkToFit.maxFontSize ?? formatting.fontSize;
|
|
17560
|
+
const maxLines = formatting.shrinkToFit.maxLines ?? Number.POSITIVE_INFINITY;
|
|
17561
|
+
const threshold = 0.5;
|
|
17562
|
+
let low = minSize;
|
|
17563
|
+
let high = maxSize;
|
|
17564
|
+
while (high - low > threshold) {
|
|
17565
|
+
const testSize = (low + high) / 2;
|
|
17566
|
+
const testMeasure = measureText(font, text, formatting.wordWrap);
|
|
17567
|
+
const padding = formatting.shrinkToFit.padding ?? 0;
|
|
17568
|
+
const scaledWidth = testMeasure.width * (testSize / font.lineHeight);
|
|
17569
|
+
const scaledHeight = testMeasure.height * (testSize / font.lineHeight);
|
|
17570
|
+
const fitsWidth = scaledWidth <= size.width - size.width * padding;
|
|
17571
|
+
const fitsHeight = scaledHeight <= size.height - size.height * padding;
|
|
17572
|
+
const fitsLines = testMeasure.lineCount <= maxLines;
|
|
17573
|
+
if (fitsWidth && fitsHeight && fitsLines) {
|
|
17574
|
+
low = testSize;
|
|
17575
|
+
} else {
|
|
17576
|
+
high = testSize;
|
|
17577
|
+
}
|
|
17365
17578
|
}
|
|
17579
|
+
return low;
|
|
17580
|
+
}
|
|
17581
|
+
|
|
17582
|
+
// src/math/angle.ts
|
|
17583
|
+
function deg2rad(degrees) {
|
|
17584
|
+
return degrees * (Math.PI / 180);
|
|
17585
|
+
}
|
|
17586
|
+
function rad2deg(radians) {
|
|
17587
|
+
return radians * (180 / Math.PI);
|
|
17588
|
+
}
|
|
17589
|
+
|
|
17590
|
+
// src/math/matrix.ts
|
|
17591
|
+
function createProjectionMatrix(resolution, dst) {
|
|
17592
|
+
const { width, height } = resolution;
|
|
17593
|
+
return mat3.scaling([2 / width, 2 / height], dst);
|
|
17594
|
+
}
|
|
17595
|
+
function createViewMatrix(camera, target) {
|
|
17596
|
+
const matrix = mat3.identity(target);
|
|
17597
|
+
mat3.scale(matrix, [camera.zoom, camera.zoom], matrix);
|
|
17598
|
+
mat3.rotate(matrix, camera.rotationRadians, matrix);
|
|
17599
|
+
mat3.translate(matrix, [-camera.x, -camera.y], matrix);
|
|
17600
|
+
return matrix;
|
|
17601
|
+
}
|
|
17602
|
+
function createModelMatrix(transform, base) {
|
|
17603
|
+
mat3.translate(base, [transform.position.x, transform.position.y], base);
|
|
17604
|
+
mat3.rotate(base, transform.rotation, base);
|
|
17605
|
+
mat3.scale(base, [transform.scale.x, transform.scale.y], base);
|
|
17606
|
+
return base;
|
|
17607
|
+
}
|
|
17608
|
+
function convertScreenToWorld(screenCoordinates, camera, projectionMatrix, resolution) {
|
|
17609
|
+
const inverseViewProjectionMatrix = mat3.mul(mat3.inverse(camera.matrix), mat3.inverse(projectionMatrix));
|
|
17610
|
+
const normalizedDeviceCoordinates = {
|
|
17611
|
+
x: 2 * screenCoordinates.x / resolution.width - 1,
|
|
17612
|
+
y: 1 - 2 * screenCoordinates.y / resolution.height
|
|
17613
|
+
};
|
|
17614
|
+
return transformPoint(normalizedDeviceCoordinates, inverseViewProjectionMatrix);
|
|
17615
|
+
}
|
|
17616
|
+
function convertWorldToScreen(worldCoordinates, camera, projectionMatrix, resolution) {
|
|
17617
|
+
const viewProjectionMatrix = mat3.mul(projectionMatrix, camera.matrix);
|
|
17618
|
+
const ndcPoint = transformPoint(worldCoordinates, viewProjectionMatrix);
|
|
17619
|
+
return {
|
|
17620
|
+
x: (ndcPoint.x + 1) * resolution.width / 2,
|
|
17621
|
+
y: (1 - ndcPoint.y) * resolution.height / 2
|
|
17622
|
+
};
|
|
17623
|
+
}
|
|
17624
|
+
function transformPoint(point, matrix) {
|
|
17625
|
+
const result = vec2.transformMat3([point.x, point.y], matrix);
|
|
17626
|
+
return {
|
|
17627
|
+
x: result[0],
|
|
17628
|
+
y: result[1]
|
|
17629
|
+
};
|
|
17366
17630
|
}
|
|
17367
17631
|
|
|
17368
17632
|
// src/scene/SceneNode.ts
|
|
@@ -17706,1430 +17970,1533 @@ function reviver(key, value) {
|
|
|
17706
17970
|
return value;
|
|
17707
17971
|
}
|
|
17708
17972
|
|
|
17709
|
-
// src/scene/
|
|
17710
|
-
var
|
|
17711
|
-
var RESERVED_PRIMITIVE_INDEX_START = 1000;
|
|
17712
|
-
var CIRCLE_INDEX = 1001;
|
|
17713
|
-
var DEFAULT_REGION = {
|
|
17714
|
-
x: 0,
|
|
17715
|
-
y: 0,
|
|
17716
|
-
width: 0,
|
|
17717
|
-
height: 0
|
|
17718
|
-
};
|
|
17973
|
+
// src/scene/TextNode.ts
|
|
17974
|
+
var DEFAULT_FONT_SIZE = 14;
|
|
17719
17975
|
|
|
17720
|
-
class
|
|
17721
|
-
|
|
17722
|
-
#
|
|
17723
|
-
#
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
#cropRatio;
|
|
17729
|
-
#atlasSize;
|
|
17730
|
-
#textureId;
|
|
17731
|
-
#writeInstance;
|
|
17732
|
-
constructor(options, matrixPool) {
|
|
17733
|
-
assert(options.shader, "QuadNode requires a shader to be explicitly provided");
|
|
17734
|
-
assert(options.size, "QuadNode requires a size to be explicitly provided");
|
|
17735
|
-
assert(options.atlasCoords, "QuadNode requires atlas coords to be explicitly provided");
|
|
17736
|
-
options.render ??= {
|
|
17737
|
-
shader: options.shader,
|
|
17738
|
-
writeInstance: writeQuadInstance
|
|
17739
|
-
};
|
|
17740
|
-
super(options);
|
|
17741
|
-
assert(options.assetManager, "QuadNode requires an asset manager");
|
|
17742
|
-
this.assetManager = options.assetManager;
|
|
17743
|
-
if (options.atlasCoords && options.atlasCoords.atlasIndex >= RESERVED_PRIMITIVE_INDEX_START) {
|
|
17744
|
-
this.#textureId = PRIMITIVE_TEXTURE;
|
|
17745
|
-
this.#region = DEFAULT_REGION;
|
|
17746
|
-
this.#atlasSize = DEFAULT_REGION;
|
|
17747
|
-
} else {
|
|
17748
|
-
assert(options.textureId, "QuadNode requires texture id to be explicitly provided");
|
|
17749
|
-
this.#textureId = options.textureId;
|
|
17750
|
-
assert(options.region, "QuadNode requires a region to be explicitly provided");
|
|
17751
|
-
this.#region = options.region;
|
|
17752
|
-
assert(options.atlasSize, "QuadNode requires atlas size to be explicitly provided");
|
|
17753
|
-
this.#atlasSize = options.atlasSize;
|
|
17976
|
+
class TextNode extends SceneNode {
|
|
17977
|
+
#text;
|
|
17978
|
+
#formatting;
|
|
17979
|
+
#font;
|
|
17980
|
+
constructor(shader, text, opts = {}) {
|
|
17981
|
+
const { width, height } = measureText(shader.font, text, opts.wordWrap);
|
|
17982
|
+
if (text.length > shader.maxCharCount) {
|
|
17983
|
+
throw new Error(`Text: ${text} exceeds ${shader.maxCharCount} characters. Try using fewer characters or increase the limit in Toodle.attach.`);
|
|
17754
17984
|
}
|
|
17755
|
-
|
|
17756
|
-
|
|
17757
|
-
|
|
17758
|
-
this.#flip = { x: options.flipX ? -1 : 1, y: options.flipY ? -1 : 1 };
|
|
17759
|
-
this.#cropOffset = options.cropOffset ?? { x: 0, y: 0 };
|
|
17760
|
-
this.#cropRatio = !this.#atlasCoords.uvScaleCropped ? { width: 1, height: 1 } : {
|
|
17761
|
-
width: this.#atlasCoords.uvScaleCropped.width / this.#atlasCoords.uvScale.width,
|
|
17762
|
-
height: this.#atlasCoords.uvScaleCropped.height / this.#atlasCoords.uvScale.height
|
|
17763
|
-
};
|
|
17764
|
-
this.#writeInstance = options.writeInstance;
|
|
17765
|
-
}
|
|
17766
|
-
get color() {
|
|
17767
|
-
return this.#color;
|
|
17768
|
-
}
|
|
17769
|
-
set color(value) {
|
|
17770
|
-
this.#color = value;
|
|
17771
|
-
}
|
|
17772
|
-
get size() {
|
|
17773
|
-
const size = super.size;
|
|
17774
|
-
if (!size) {
|
|
17775
|
-
throw new Error("QuadNode requires a size");
|
|
17985
|
+
const em2px = shader.font.lineHeight / (opts.fontSize ?? DEFAULT_FONT_SIZE);
|
|
17986
|
+
if (!opts.shrinkToFit && !opts.size) {
|
|
17987
|
+
opts.size = { width: width / em2px, height: height / em2px };
|
|
17776
17988
|
}
|
|
17777
|
-
|
|
17778
|
-
|
|
17779
|
-
|
|
17780
|
-
|
|
17781
|
-
|
|
17782
|
-
|
|
17783
|
-
|
|
17784
|
-
|
|
17785
|
-
|
|
17786
|
-
|
|
17787
|
-
|
|
17788
|
-
|
|
17989
|
+
super({
|
|
17990
|
+
...opts,
|
|
17991
|
+
render: {
|
|
17992
|
+
shader,
|
|
17993
|
+
writeInstance: (_node, _array, _offset) => {
|
|
17994
|
+
throw new Error("not implemented - needs access to text uniform buffer, dimensions and a model matrix");
|
|
17995
|
+
}
|
|
17996
|
+
}
|
|
17997
|
+
});
|
|
17998
|
+
this.#font = shader.font;
|
|
17999
|
+
this.#text = text;
|
|
18000
|
+
this.#formatting = opts;
|
|
17789
18001
|
}
|
|
17790
|
-
get
|
|
17791
|
-
return this.#
|
|
18002
|
+
get text() {
|
|
18003
|
+
return this.#text;
|
|
17792
18004
|
}
|
|
17793
|
-
get
|
|
17794
|
-
return this.#
|
|
18005
|
+
get formatting() {
|
|
18006
|
+
return this.#formatting;
|
|
17795
18007
|
}
|
|
17796
|
-
get
|
|
17797
|
-
return this.#
|
|
18008
|
+
get font() {
|
|
18009
|
+
return this.#font;
|
|
17798
18010
|
}
|
|
17799
|
-
set
|
|
17800
|
-
|
|
18011
|
+
set text(text) {
|
|
18012
|
+
if (!text) {
|
|
18013
|
+
throw new Error("text cannot be empty");
|
|
18014
|
+
}
|
|
18015
|
+
this.#text = text;
|
|
17801
18016
|
this.setDirty();
|
|
17802
18017
|
}
|
|
17803
|
-
get
|
|
17804
|
-
return this.#
|
|
18018
|
+
get tint() {
|
|
18019
|
+
return this.#formatting.color || { r: 1, g: 1, b: 1, a: 1 };
|
|
17805
18020
|
}
|
|
17806
|
-
set
|
|
17807
|
-
this.#
|
|
18021
|
+
set tint(tint) {
|
|
18022
|
+
this.#formatting.color = tint;
|
|
17808
18023
|
this.setDirty();
|
|
17809
18024
|
}
|
|
17810
|
-
|
|
17811
|
-
|
|
18025
|
+
set formatting(formatting) {
|
|
18026
|
+
this.#formatting = formatting;
|
|
18027
|
+
this.setDirty();
|
|
17812
18028
|
}
|
|
17813
|
-
|
|
17814
|
-
|
|
18029
|
+
}
|
|
18030
|
+
|
|
18031
|
+
// src/backends/webgl2/glsl/text.glsl.ts
|
|
18032
|
+
var vertexShader2 = `#version 300 es
|
|
18033
|
+
precision highp float;
|
|
18034
|
+
|
|
18035
|
+
// Engine uniforms
|
|
18036
|
+
uniform mat3 u_viewProjection;
|
|
18037
|
+
|
|
18038
|
+
// Per-text-block uniforms
|
|
18039
|
+
uniform mat3 u_textTransform;
|
|
18040
|
+
uniform vec4 u_textColor;
|
|
18041
|
+
uniform float u_fontSize;
|
|
18042
|
+
uniform float u_blockWidth;
|
|
18043
|
+
uniform float u_blockHeight;
|
|
18044
|
+
uniform float u_lineHeight;
|
|
18045
|
+
|
|
18046
|
+
// Character data texture (RGBA32F, 2 texels per character)
|
|
18047
|
+
// Texel 0: texOffset.xy, texExtent.xy
|
|
18048
|
+
// Texel 1: size.xy, offset.xy
|
|
18049
|
+
uniform sampler2D u_charData;
|
|
18050
|
+
|
|
18051
|
+
// Text buffer texture (RGBA32F, 1 texel per glyph)
|
|
18052
|
+
// Each texel: xy = glyph position, z = char index
|
|
18053
|
+
uniform sampler2D u_textBuffer;
|
|
18054
|
+
|
|
18055
|
+
// Outputs to fragment shader
|
|
18056
|
+
out vec2 v_texcoord;
|
|
18057
|
+
|
|
18058
|
+
// Quad vertex positions for a character (matches WGSL)
|
|
18059
|
+
const vec2 pos[4] = vec2[4](
|
|
18060
|
+
vec2(0.0, -1.0),
|
|
18061
|
+
vec2(1.0, -1.0),
|
|
18062
|
+
vec2(0.0, 0.0),
|
|
18063
|
+
vec2(1.0, 0.0)
|
|
18064
|
+
);
|
|
18065
|
+
|
|
18066
|
+
void main() {
|
|
18067
|
+
// gl_VertexID gives us 0-3 for the quad vertices
|
|
18068
|
+
// gl_InstanceID gives us which glyph we're rendering
|
|
18069
|
+
int vertexIndex = gl_VertexID;
|
|
18070
|
+
int glyphIndex = gl_InstanceID;
|
|
18071
|
+
|
|
18072
|
+
// Fetch glyph data from text buffer texture
|
|
18073
|
+
vec4 glyphData = texelFetch(u_textBuffer, ivec2(glyphIndex, 0), 0);
|
|
18074
|
+
vec2 glyphPos = glyphData.xy;
|
|
18075
|
+
int charIndex = int(glyphData.z);
|
|
18076
|
+
|
|
18077
|
+
// Fetch character metrics (2 texels per char)
|
|
18078
|
+
// Texel 0: texOffset.x, texOffset.y, texExtent.x, texExtent.y
|
|
18079
|
+
// Texel 1: size.x, size.y, offset.x, offset.y
|
|
18080
|
+
vec4 charData0 = texelFetch(u_charData, ivec2(charIndex * 2, 0), 0);
|
|
18081
|
+
vec4 charData1 = texelFetch(u_charData, ivec2(charIndex * 2 + 1, 0), 0);
|
|
18082
|
+
|
|
18083
|
+
vec2 texOffset = charData0.xy;
|
|
18084
|
+
vec2 texExtent = charData0.zw;
|
|
18085
|
+
vec2 charSize = charData1.xy;
|
|
18086
|
+
vec2 charOffset = charData1.zw;
|
|
18087
|
+
|
|
18088
|
+
// Center text vertically; origin is mid-height
|
|
18089
|
+
vec2 offset = vec2(0.0, -u_blockHeight / 2.0);
|
|
18090
|
+
|
|
18091
|
+
// Glyph position in ems (quad pos * size + per-char offset)
|
|
18092
|
+
vec2 emPos = pos[vertexIndex] * charSize + charOffset + glyphPos - offset;
|
|
18093
|
+
vec2 charPos = emPos * (u_fontSize / u_lineHeight);
|
|
18094
|
+
|
|
18095
|
+
// Transform position through model and view-projection matrices
|
|
18096
|
+
vec3 worldPos = u_textTransform * vec3(charPos, 1.0);
|
|
18097
|
+
vec3 clipPos = u_viewProjection * worldPos;
|
|
18098
|
+
|
|
18099
|
+
gl_Position = vec4(clipPos.xy, 0.0, 1.0);
|
|
18100
|
+
|
|
18101
|
+
// Calculate texture coordinates
|
|
18102
|
+
v_texcoord = pos[vertexIndex] * vec2(1.0, -1.0);
|
|
18103
|
+
v_texcoord *= texExtent;
|
|
18104
|
+
v_texcoord += texOffset;
|
|
18105
|
+
}
|
|
18106
|
+
`;
|
|
18107
|
+
var fragmentShader2 = `#version 300 es
|
|
18108
|
+
precision highp float;
|
|
18109
|
+
|
|
18110
|
+
// Font texture (MSDF atlas)
|
|
18111
|
+
uniform sampler2D u_fontTexture;
|
|
18112
|
+
|
|
18113
|
+
// Text color
|
|
18114
|
+
uniform vec4 u_textColor;
|
|
18115
|
+
|
|
18116
|
+
// Input from vertex shader
|
|
18117
|
+
in vec2 v_texcoord;
|
|
18118
|
+
|
|
18119
|
+
// Output color
|
|
18120
|
+
out vec4 fragColor;
|
|
18121
|
+
|
|
18122
|
+
// Signed distance function sampling for MSDF font rendering
|
|
18123
|
+
// Median of three: max(min(r,g), min(max(r,g), b))
|
|
18124
|
+
float sampleMsdf(vec2 texcoord) {
|
|
18125
|
+
vec4 c = texture(u_fontTexture, texcoord);
|
|
18126
|
+
return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
|
|
18127
|
+
}
|
|
18128
|
+
|
|
18129
|
+
void main() {
|
|
18130
|
+
// pxRange (AKA distanceRange) comes from the msdfgen tool
|
|
18131
|
+
float pxRange = 4.0;
|
|
18132
|
+
vec2 texSize = vec2(textureSize(u_fontTexture, 0));
|
|
18133
|
+
|
|
18134
|
+
// Anti-aliasing technique by Paul Houx
|
|
18135
|
+
// https://github.com/Chlumsky/msdfgen/issues/22#issuecomment-234958005
|
|
18136
|
+
float dx = texSize.x * length(vec2(dFdx(v_texcoord.x), dFdy(v_texcoord.x)));
|
|
18137
|
+
float dy = texSize.y * length(vec2(dFdx(v_texcoord.y), dFdy(v_texcoord.y)));
|
|
18138
|
+
|
|
18139
|
+
float toPixels = pxRange * inversesqrt(dx * dx + dy * dy);
|
|
18140
|
+
float sigDist = sampleMsdf(v_texcoord) - 0.5;
|
|
18141
|
+
float pxDist = sigDist * toPixels;
|
|
18142
|
+
|
|
18143
|
+
float edgeWidth = 0.5;
|
|
18144
|
+
float alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
|
|
18145
|
+
|
|
18146
|
+
if (alpha < 0.001) {
|
|
18147
|
+
discard;
|
|
17815
18148
|
}
|
|
17816
|
-
|
|
17817
|
-
|
|
18149
|
+
|
|
18150
|
+
fragColor = vec4(u_textColor.rgb, u_textColor.a * alpha);
|
|
18151
|
+
}
|
|
18152
|
+
`;
|
|
18153
|
+
|
|
18154
|
+
// src/backends/webgl2/WebGLTextShader.ts
|
|
18155
|
+
class WebGLTextShader {
|
|
18156
|
+
label = "text";
|
|
18157
|
+
font;
|
|
18158
|
+
maxCharCount;
|
|
18159
|
+
#backend;
|
|
18160
|
+
#pipeline;
|
|
18161
|
+
#program;
|
|
18162
|
+
#vao;
|
|
18163
|
+
#cpuTextBuffer;
|
|
18164
|
+
#cachedUniform = null;
|
|
18165
|
+
#uViewProjection = null;
|
|
18166
|
+
#uTextTransform = null;
|
|
18167
|
+
#uTextColor = null;
|
|
18168
|
+
#uFontSize = null;
|
|
18169
|
+
#uBlockWidth = null;
|
|
18170
|
+
#uBlockHeight = null;
|
|
18171
|
+
#uLineHeight = null;
|
|
18172
|
+
#uCharData = null;
|
|
18173
|
+
#uTextBuffer = null;
|
|
18174
|
+
#uFontTexture = null;
|
|
18175
|
+
constructor(backend, pipeline) {
|
|
18176
|
+
this.#backend = backend;
|
|
18177
|
+
this.#pipeline = pipeline;
|
|
18178
|
+
this.font = pipeline.font;
|
|
18179
|
+
this.maxCharCount = pipeline.maxCharCount;
|
|
18180
|
+
const gl = backend.gl;
|
|
18181
|
+
const vs = this.#compileShader(gl, gl.VERTEX_SHADER, vertexShader2);
|
|
18182
|
+
const fs = this.#compileShader(gl, gl.FRAGMENT_SHADER, fragmentShader2);
|
|
18183
|
+
const program = gl.createProgram();
|
|
18184
|
+
assert(program, "Failed to create WebGL program");
|
|
18185
|
+
gl.attachShader(program, vs);
|
|
18186
|
+
gl.attachShader(program, fs);
|
|
18187
|
+
gl.linkProgram(program);
|
|
18188
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
18189
|
+
const info = gl.getProgramInfoLog(program);
|
|
18190
|
+
throw new Error(`Failed to link text shader program: ${info}`);
|
|
18191
|
+
}
|
|
18192
|
+
this.#program = program;
|
|
18193
|
+
this.#uViewProjection = gl.getUniformLocation(program, "u_viewProjection");
|
|
18194
|
+
this.#uTextTransform = gl.getUniformLocation(program, "u_textTransform");
|
|
18195
|
+
this.#uTextColor = gl.getUniformLocation(program, "u_textColor");
|
|
18196
|
+
this.#uFontSize = gl.getUniformLocation(program, "u_fontSize");
|
|
18197
|
+
this.#uBlockWidth = gl.getUniformLocation(program, "u_blockWidth");
|
|
18198
|
+
this.#uBlockHeight = gl.getUniformLocation(program, "u_blockHeight");
|
|
18199
|
+
this.#uLineHeight = gl.getUniformLocation(program, "u_lineHeight");
|
|
18200
|
+
this.#uCharData = gl.getUniformLocation(program, "u_charData");
|
|
18201
|
+
this.#uTextBuffer = gl.getUniformLocation(program, "u_textBuffer");
|
|
18202
|
+
this.#uFontTexture = gl.getUniformLocation(program, "u_fontTexture");
|
|
18203
|
+
const vao = gl.createVertexArray();
|
|
18204
|
+
assert(vao, "Failed to create WebGL VAO");
|
|
18205
|
+
this.#vao = vao;
|
|
18206
|
+
this.#cpuTextBuffer = new Float32Array(this.maxCharCount * 4);
|
|
18207
|
+
gl.deleteShader(vs);
|
|
18208
|
+
gl.deleteShader(fs);
|
|
17818
18209
|
}
|
|
17819
|
-
|
|
17820
|
-
|
|
18210
|
+
startFrame(uniform) {
|
|
18211
|
+
this.#cachedUniform = uniform;
|
|
17821
18212
|
}
|
|
17822
|
-
|
|
17823
|
-
|
|
18213
|
+
processBatch(nodes) {
|
|
18214
|
+
if (nodes.length === 0)
|
|
18215
|
+
return 0;
|
|
18216
|
+
const gl = this.#backend.gl;
|
|
18217
|
+
const uniform = this.#cachedUniform;
|
|
18218
|
+
if (!uniform) {
|
|
18219
|
+
throw new Error("Tried to process batch but engine uniform is not set");
|
|
18220
|
+
}
|
|
18221
|
+
gl.useProgram(this.#program);
|
|
18222
|
+
gl.bindVertexArray(this.#vao);
|
|
18223
|
+
if (this.#uViewProjection) {
|
|
18224
|
+
const m3 = uniform.viewProjectionMatrix;
|
|
18225
|
+
const mat3x3 = new Float32Array([
|
|
18226
|
+
m3[0],
|
|
18227
|
+
m3[1],
|
|
18228
|
+
m3[2],
|
|
18229
|
+
m3[4],
|
|
18230
|
+
m3[5],
|
|
18231
|
+
m3[6],
|
|
18232
|
+
m3[8],
|
|
18233
|
+
m3[9],
|
|
18234
|
+
m3[10]
|
|
18235
|
+
]);
|
|
18236
|
+
gl.uniformMatrix3fv(this.#uViewProjection, false, mat3x3);
|
|
18237
|
+
}
|
|
18238
|
+
if (this.#uLineHeight) {
|
|
18239
|
+
gl.uniform1f(this.#uLineHeight, this.#pipeline.lineHeight);
|
|
18240
|
+
}
|
|
18241
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
18242
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.fontTexture);
|
|
18243
|
+
if (this.#uFontTexture) {
|
|
18244
|
+
gl.uniform1i(this.#uFontTexture, 0);
|
|
18245
|
+
}
|
|
18246
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
18247
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.charDataTexture);
|
|
18248
|
+
if (this.#uCharData) {
|
|
18249
|
+
gl.uniform1i(this.#uCharData, 1);
|
|
18250
|
+
}
|
|
18251
|
+
gl.activeTexture(gl.TEXTURE2);
|
|
18252
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.textBufferTexture);
|
|
18253
|
+
if (this.#uTextBuffer) {
|
|
18254
|
+
gl.uniform1i(this.#uTextBuffer, 2);
|
|
18255
|
+
}
|
|
18256
|
+
for (const node of nodes) {
|
|
18257
|
+
if (!(node instanceof TextNode)) {
|
|
18258
|
+
console.error(node);
|
|
18259
|
+
throw new Error(`Tried to use WebGLTextShader on something that isn't a TextNode: ${node}`);
|
|
18260
|
+
}
|
|
18261
|
+
const text = node.text;
|
|
18262
|
+
const formatting = node.formatting;
|
|
18263
|
+
const measurements = measureText(this.font, text, formatting.wordWrap);
|
|
18264
|
+
const size = node.size ?? measurements;
|
|
18265
|
+
const fontSize = formatting.shrinkToFit ? findLargestFontSize(this.font, text, size, formatting) : formatting.fontSize;
|
|
18266
|
+
const actualFontSize = fontSize || DEFAULT_FONT_SIZE;
|
|
18267
|
+
shapeText(this.font, text, size, actualFontSize, formatting, this.#cpuTextBuffer, 0);
|
|
18268
|
+
this.#pipeline.updateTextBuffer(this.#cpuTextBuffer, measurements.printedCharCount);
|
|
18269
|
+
if (this.#uTextTransform) {
|
|
18270
|
+
const m3 = node.matrix;
|
|
18271
|
+
const mat3x3 = new Float32Array([
|
|
18272
|
+
m3[0],
|
|
18273
|
+
m3[1],
|
|
18274
|
+
m3[2],
|
|
18275
|
+
m3[4],
|
|
18276
|
+
m3[5],
|
|
18277
|
+
m3[6],
|
|
18278
|
+
m3[8],
|
|
18279
|
+
m3[9],
|
|
18280
|
+
m3[10]
|
|
18281
|
+
]);
|
|
18282
|
+
gl.uniformMatrix3fv(this.#uTextTransform, false, mat3x3);
|
|
18283
|
+
}
|
|
18284
|
+
if (this.#uTextColor) {
|
|
18285
|
+
const tint = node.tint;
|
|
18286
|
+
gl.uniform4f(this.#uTextColor, tint.r, tint.g, tint.b, tint.a);
|
|
18287
|
+
}
|
|
18288
|
+
if (this.#uFontSize) {
|
|
18289
|
+
gl.uniform1f(this.#uFontSize, actualFontSize);
|
|
18290
|
+
}
|
|
18291
|
+
if (this.#uBlockWidth) {
|
|
18292
|
+
gl.uniform1f(this.#uBlockWidth, formatting.align === "center" ? 0 : measurements.width);
|
|
18293
|
+
}
|
|
18294
|
+
if (this.#uBlockHeight) {
|
|
18295
|
+
gl.uniform1f(this.#uBlockHeight, measurements.height);
|
|
18296
|
+
}
|
|
18297
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, measurements.printedCharCount);
|
|
18298
|
+
}
|
|
18299
|
+
gl.bindVertexArray(null);
|
|
18300
|
+
return nodes.length;
|
|
17824
18301
|
}
|
|
17825
|
-
|
|
17826
|
-
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
18302
|
+
endFrame() {}
|
|
18303
|
+
#compileShader(gl, type, source) {
|
|
18304
|
+
const shader = gl.createShader(type);
|
|
18305
|
+
assert(shader, "Failed to create WebGL shader");
|
|
18306
|
+
gl.shaderSource(shader, source);
|
|
18307
|
+
gl.compileShader(shader);
|
|
18308
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
18309
|
+
const info = gl.getShaderInfoLog(shader);
|
|
18310
|
+
const typeStr = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
|
|
18311
|
+
gl.deleteShader(shader);
|
|
18312
|
+
throw new Error(`Failed to compile ${typeStr} shader: ${info}`);
|
|
17834
18313
|
}
|
|
17835
|
-
|
|
17836
|
-
}
|
|
17837
|
-
function writeQuadInstance(node, array, offset) {
|
|
17838
|
-
if (!(node instanceof QuadNode)) {
|
|
17839
|
-
throw new Error("QuadNode.writeInstance can only be called on QuadNodes");
|
|
18314
|
+
return shader;
|
|
17840
18315
|
}
|
|
17841
|
-
|
|
17842
|
-
|
|
17843
|
-
|
|
17844
|
-
|
|
17845
|
-
|
|
17846
|
-
node.atlasCoords.uvOffset.x,
|
|
17847
|
-
node.atlasCoords.uvOffset.y,
|
|
17848
|
-
node.atlasCoords.uvScale.width,
|
|
17849
|
-
node.atlasCoords.uvScale.height
|
|
17850
|
-
], offset + 16);
|
|
17851
|
-
} else {
|
|
17852
|
-
const atlasSize = node.extra.atlasSize();
|
|
17853
|
-
array.set([
|
|
17854
|
-
node.atlasCoords.uvOffset.x + region.x / atlasSize.width,
|
|
17855
|
-
node.atlasCoords.uvOffset.y + region.y / atlasSize.height,
|
|
17856
|
-
region.width / atlasSize.width,
|
|
17857
|
-
region.height / atlasSize.height
|
|
17858
|
-
], offset + 16);
|
|
18316
|
+
destroy() {
|
|
18317
|
+
const gl = this.#backend.gl;
|
|
18318
|
+
gl.deleteProgram(this.#program);
|
|
18319
|
+
gl.deleteVertexArray(this.#vao);
|
|
18320
|
+
this.#pipeline.destroy();
|
|
17859
18321
|
}
|
|
17860
|
-
array.set([
|
|
17861
|
-
node.cropOffset.x / 2 / (node.atlasCoords.originalSize.width || 1),
|
|
17862
|
-
node.cropOffset.y / 2 / (node.atlasCoords.originalSize.height || 1),
|
|
17863
|
-
node.extra.cropRatio().width,
|
|
17864
|
-
node.extra.cropRatio().height
|
|
17865
|
-
], offset + 20);
|
|
17866
|
-
new DataView(array.buffer).setUint32(array.byteOffset + (offset + 24) * Float32Array.BYTES_PER_ELEMENT, node.atlasCoords.atlasIndex, true);
|
|
17867
|
-
node.writeInstance?.(array, offset + 28);
|
|
17868
|
-
return 1;
|
|
17869
18322
|
}
|
|
17870
18323
|
|
|
17871
|
-
// src/
|
|
17872
|
-
var
|
|
17873
|
-
|
|
18324
|
+
// src/backends/webgpu/wgsl/text.wgsl.ts
|
|
18325
|
+
var text_wgsl_default = `
|
|
18326
|
+
// Adapted from: https://webgpu.github.io/webgpu-samples/?sample=textRenderingMsdf
|
|
17874
18327
|
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
shader: options.shader,
|
|
17883
|
-
writeInstance: writeJumboQuadInstance
|
|
17884
|
-
};
|
|
17885
|
-
super({
|
|
17886
|
-
...options,
|
|
17887
|
-
atlasCoords: options.tiles[0].atlasCoords
|
|
17888
|
-
}, matrixPool);
|
|
17889
|
-
this.#matrixPool = matrixPool;
|
|
17890
|
-
this.#tiles = [];
|
|
17891
|
-
for (const tile of options.tiles) {
|
|
17892
|
-
assert(tile.atlasCoords, "JumboQuadNode requires atlas coords to be provided");
|
|
17893
|
-
assert(tile.size, "JumboQuadNode requires a size to be provided");
|
|
17894
|
-
this.#tiles.push({
|
|
17895
|
-
textureId: tile.textureId,
|
|
17896
|
-
offset: tile.offset,
|
|
17897
|
-
size: tile.size,
|
|
17898
|
-
atlasCoords: tile.atlasCoords
|
|
17899
|
-
});
|
|
17900
|
-
}
|
|
17901
|
-
}
|
|
17902
|
-
get atlasCoords() {
|
|
17903
|
-
throw new Error("JumboQuadNode does not have a single atlas coords");
|
|
17904
|
-
}
|
|
17905
|
-
get tiles() {
|
|
17906
|
-
return this.#tiles;
|
|
17907
|
-
}
|
|
17908
|
-
getTileMatrix(tile) {
|
|
17909
|
-
const matrix = mat3.clone(this.matrix, this.#matrixPool.get());
|
|
17910
|
-
const originalSize = {
|
|
17911
|
-
width: Math.max(...this.#tiles.map((t3) => t3.offset.x + t3.size.width)),
|
|
17912
|
-
height: Math.max(...this.#tiles.map((t3) => t3.offset.y + t3.size.height))
|
|
17913
|
-
};
|
|
17914
|
-
const proportionalSize = {
|
|
17915
|
-
width: this.size.width / originalSize.width,
|
|
17916
|
-
height: this.size.height / originalSize.height
|
|
17917
|
-
};
|
|
17918
|
-
const centerOffset = {
|
|
17919
|
-
x: tile.offset.x + tile.size.width / 2 - originalSize.width / 2,
|
|
17920
|
-
y: -(tile.offset.y + tile.size.height / 2 - originalSize.height / 2)
|
|
17921
|
-
};
|
|
17922
|
-
mat3.translate(matrix, [
|
|
17923
|
-
centerOffset.x * proportionalSize.width,
|
|
17924
|
-
centerOffset.y * proportionalSize.height
|
|
17925
|
-
], matrix);
|
|
17926
|
-
mat3.scale(matrix, [
|
|
17927
|
-
tile.size.width * proportionalSize.width * (this.flipX ? -1 : 1),
|
|
17928
|
-
tile.size.height * proportionalSize.height * (this.flipY ? -1 : 1)
|
|
17929
|
-
], matrix);
|
|
17930
|
-
return matrix;
|
|
17931
|
-
}
|
|
17932
|
-
}
|
|
17933
|
-
function writeJumboQuadInstance(node, array, offset) {
|
|
17934
|
-
if (!(node instanceof JumboQuadNode)) {
|
|
17935
|
-
throw new Error("JumboQuadNode.writeJumboQuadInstance can only be called on JumboQuadNodes");
|
|
17936
|
-
}
|
|
17937
|
-
let tileOffset = 0;
|
|
17938
|
-
for (const tile of node.tiles) {
|
|
17939
|
-
const coord = tile.atlasCoords;
|
|
17940
|
-
const matrix = node.getTileMatrix(tile);
|
|
17941
|
-
array.set(matrix, offset + tileOffset);
|
|
17942
|
-
tileOffset += MAT3_SIZE;
|
|
17943
|
-
array.set([node.color.r, node.color.g, node.color.b, node.color.a], offset + tileOffset);
|
|
17944
|
-
tileOffset += VEC4F_SIZE;
|
|
17945
|
-
array.set([
|
|
17946
|
-
coord.uvOffset.x,
|
|
17947
|
-
coord.uvOffset.y,
|
|
17948
|
-
coord.uvScale.width,
|
|
17949
|
-
coord.uvScale.height
|
|
17950
|
-
], offset + tileOffset);
|
|
17951
|
-
tileOffset += VEC4F_SIZE;
|
|
17952
|
-
const cropRatio = !coord.uvScaleCropped ? { width: 1, height: 1 } : {
|
|
17953
|
-
width: coord.uvScaleCropped.width / coord.uvScale.width,
|
|
17954
|
-
height: coord.uvScaleCropped.height / coord.uvScale.height
|
|
17955
|
-
};
|
|
17956
|
-
array.set([
|
|
17957
|
-
tile.atlasCoords.cropOffset.x / 2 / (tile.atlasCoords.originalSize.width || 1),
|
|
17958
|
-
tile.atlasCoords.cropOffset.y / 2 / (tile.atlasCoords.originalSize.height || 1),
|
|
17959
|
-
cropRatio.width,
|
|
17960
|
-
cropRatio.height
|
|
17961
|
-
], offset + tileOffset);
|
|
17962
|
-
tileOffset += VEC4F_SIZE;
|
|
17963
|
-
new DataView(array.buffer).setUint32(array.byteOffset + (offset + tileOffset) * Float32Array.BYTES_PER_ELEMENT, coord.atlasIndex, true);
|
|
17964
|
-
tileOffset += VEC4F_SIZE;
|
|
17965
|
-
}
|
|
17966
|
-
node.writeInstance?.(array, offset + tileOffset);
|
|
17967
|
-
return node.tiles.length;
|
|
17968
|
-
}
|
|
18328
|
+
// Quad vertex positions for a character
|
|
18329
|
+
const pos = array(
|
|
18330
|
+
vec2f(0, -1),
|
|
18331
|
+
vec2f(1, -1),
|
|
18332
|
+
vec2f(0, 0),
|
|
18333
|
+
vec2f(1, 0),
|
|
18334
|
+
);
|
|
17969
18335
|
|
|
17970
|
-
//
|
|
17971
|
-
|
|
17972
|
-
|
|
17973
|
-
|
|
17974
|
-
|
|
17975
|
-
|
|
17976
|
-
|
|
17977
|
-
console.warn(msg ?? key);
|
|
17978
|
-
}
|
|
18336
|
+
// Debug colors for visualization
|
|
18337
|
+
const debugColors = array(
|
|
18338
|
+
vec4f(1, 0, 0, 1),
|
|
18339
|
+
vec4f(0, 1, 0, 1),
|
|
18340
|
+
vec4f(0, 0, 1, 1),
|
|
18341
|
+
vec4f(1, 1, 1, 1),
|
|
18342
|
+
);
|
|
17979
18343
|
|
|
17980
|
-
//
|
|
17981
|
-
|
|
17982
|
-
|
|
17983
|
-
|
|
17984
|
-
|
|
17985
|
-
name;
|
|
17986
|
-
charset;
|
|
17987
|
-
charCount;
|
|
17988
|
-
lineHeight;
|
|
17989
|
-
charBuffer;
|
|
17990
|
-
#kernings;
|
|
17991
|
-
#chars;
|
|
17992
|
-
#fallbackCharCode;
|
|
17993
|
-
constructor(id, json, imageBitmap) {
|
|
17994
|
-
this.id = id;
|
|
17995
|
-
this.json = json;
|
|
17996
|
-
this.imageBitmap = imageBitmap;
|
|
17997
|
-
const charArray = Object.values(json.chars);
|
|
17998
|
-
this.charCount = charArray.length;
|
|
17999
|
-
this.lineHeight = json.common.lineHeight;
|
|
18000
|
-
this.charset = json.info.charset;
|
|
18001
|
-
this.name = json.info.face;
|
|
18002
|
-
this.#kernings = new Map;
|
|
18003
|
-
if (json.kernings) {
|
|
18004
|
-
for (const kearning of json.kernings) {
|
|
18005
|
-
let charKerning = this.#kernings.get(kearning.first);
|
|
18006
|
-
if (!charKerning) {
|
|
18007
|
-
charKerning = new Map;
|
|
18008
|
-
this.#kernings.set(kearning.first, charKerning);
|
|
18009
|
-
}
|
|
18010
|
-
charKerning.set(kearning.second, kearning.amount);
|
|
18011
|
-
}
|
|
18012
|
-
}
|
|
18013
|
-
this.#chars = new Map;
|
|
18014
|
-
const charCount = Object.values(json.chars).length;
|
|
18015
|
-
this.charBuffer = new Float32Array(charCount * 8);
|
|
18016
|
-
let offset = 0;
|
|
18017
|
-
const u3 = 1 / json.common.scaleW;
|
|
18018
|
-
const v3 = 1 / json.common.scaleH;
|
|
18019
|
-
for (const [i3, char] of json.chars.entries()) {
|
|
18020
|
-
this.#chars.set(char.id, char);
|
|
18021
|
-
this.#chars.get(char.id).charIndex = i3;
|
|
18022
|
-
this.charBuffer[offset] = char.x * u3;
|
|
18023
|
-
this.charBuffer[offset + 1] = char.y * v3;
|
|
18024
|
-
this.charBuffer[offset + 2] = char.width * u3;
|
|
18025
|
-
this.charBuffer[offset + 3] = char.height * v3;
|
|
18026
|
-
this.charBuffer[offset + 4] = char.width;
|
|
18027
|
-
this.charBuffer[offset + 5] = char.height;
|
|
18028
|
-
this.charBuffer[offset + 6] = char.xoffset;
|
|
18029
|
-
this.charBuffer[offset + 7] = -char.yoffset;
|
|
18030
|
-
offset += 8;
|
|
18031
|
-
}
|
|
18032
|
-
}
|
|
18033
|
-
getChar(charCode) {
|
|
18034
|
-
const char = this.#chars.get(charCode);
|
|
18035
|
-
if (!char) {
|
|
18036
|
-
const fallbackCharacter = this.#chars.get(this.#fallbackCharCode ?? this.#chars.keys().toArray()[0]);
|
|
18037
|
-
warnOnce(`unknown_char_${this.name}`, `Couldn't find character ${charCode} in characters for font ${this.name} -- defaulting to first available character "${fallbackCharacter.char}"`);
|
|
18038
|
-
return fallbackCharacter;
|
|
18039
|
-
}
|
|
18040
|
-
return char;
|
|
18041
|
-
}
|
|
18042
|
-
getXAdvance(charCode, nextCharCode = -1) {
|
|
18043
|
-
const char = this.getChar(charCode);
|
|
18044
|
-
if (nextCharCode >= 0) {
|
|
18045
|
-
const kerning = this.#kernings.get(charCode);
|
|
18046
|
-
if (kerning) {
|
|
18047
|
-
return char.xadvance + (kerning.get(nextCharCode) ?? 0);
|
|
18048
|
-
}
|
|
18049
|
-
}
|
|
18050
|
-
return char.xadvance;
|
|
18051
|
-
}
|
|
18052
|
-
static async create(id, fontJsonUrl) {
|
|
18053
|
-
const response = await fetch(fontJsonUrl);
|
|
18054
|
-
const json = await response.json();
|
|
18055
|
-
const i3 = fontJsonUrl.href.lastIndexOf("/");
|
|
18056
|
-
const baseUrl = i3 !== -1 ? fontJsonUrl.href.substring(0, i3 + 1) : undefined;
|
|
18057
|
-
if (json.pages.length < 1) {
|
|
18058
|
-
throw new Error(`Can't create an msdf font without a reference to the page url in the json`);
|
|
18059
|
-
}
|
|
18060
|
-
if (json.pages.length > 1) {
|
|
18061
|
-
throw new Error(`Can't create an msdf font with more than one page`);
|
|
18062
|
-
}
|
|
18063
|
-
const textureUrl = baseUrl + json.pages[0];
|
|
18064
|
-
const textureResponse = await fetch(textureUrl);
|
|
18065
|
-
const bitmap = await createImageBitmap(await textureResponse.blob());
|
|
18066
|
-
return new MsdfFont(id, json, bitmap);
|
|
18067
|
-
}
|
|
18068
|
-
set fallbackCharacter(character) {
|
|
18069
|
-
const charCode = character.charCodeAt(0);
|
|
18070
|
-
if (this.#chars.has(charCode)) {
|
|
18071
|
-
this.#fallbackCharCode = charCode;
|
|
18072
|
-
} else {
|
|
18073
|
-
const fallbackCode = this.#chars.keys().toArray()[0];
|
|
18074
|
-
console.warn(`${character} character does not exist in font ${this.name} defaulting to "${this.#chars.get(fallbackCode)?.char}".`);
|
|
18075
|
-
this.#fallbackCharCode = fallbackCode;
|
|
18076
|
-
}
|
|
18077
|
-
}
|
|
18078
|
-
}
|
|
18344
|
+
// Vertex input from GPU
|
|
18345
|
+
struct VertexInput {
|
|
18346
|
+
@builtin(vertex_index) vertex: u32,
|
|
18347
|
+
@builtin(instance_index) instance: u32,
|
|
18348
|
+
};
|
|
18079
18349
|
|
|
18080
|
-
//
|
|
18081
|
-
|
|
18082
|
-
|
|
18083
|
-
|
|
18084
|
-
|
|
18085
|
-
|
|
18086
|
-
|
|
18087
|
-
const hackHasExplicitBlock = blockSize.width !== measurements.width;
|
|
18088
|
-
let debugData = null;
|
|
18089
|
-
if (debug) {
|
|
18090
|
-
debugData = [];
|
|
18091
|
-
}
|
|
18092
|
-
for (const word of measurements.words) {
|
|
18093
|
-
for (const glyph of word.glyphs) {
|
|
18094
|
-
let lineOffset = 0;
|
|
18095
|
-
if (alignment === "center") {
|
|
18096
|
-
lineOffset = measurements.width * -0.5 - (measurements.width - measurements.lineWidths[glyph.line]) * -0.5;
|
|
18097
|
-
} else if (alignment === "right") {
|
|
18098
|
-
const blockSizeEm = blockSize.width / em2px;
|
|
18099
|
-
const delta = measurements.width - measurements.lineWidths[glyph.line];
|
|
18100
|
-
lineOffset = (hackHasExplicitBlock ? blockSizeEm / 2 : measurements.width / 2) - measurements.width + delta;
|
|
18101
|
-
} else if (alignment === "left") {
|
|
18102
|
-
const blockSizeEm = blockSize.width / em2px;
|
|
18103
|
-
lineOffset = hackHasExplicitBlock ? -blockSizeEm / 2 : -measurements.width / 2;
|
|
18104
|
-
}
|
|
18105
|
-
if (debug && debugData) {
|
|
18106
|
-
debugData.push({
|
|
18107
|
-
line: glyph.line,
|
|
18108
|
-
word: word.glyphs.map((g3) => g3.char.char).join(""),
|
|
18109
|
-
glyph: glyph.char.char,
|
|
18110
|
-
startX: word.startX,
|
|
18111
|
-
glyphX: glyph.offset[0],
|
|
18112
|
-
advance: glyph.char.xadvance,
|
|
18113
|
-
lineOffset,
|
|
18114
|
-
startY: word.startY,
|
|
18115
|
-
glyphY: glyph.offset[1]
|
|
18116
|
-
});
|
|
18117
|
-
}
|
|
18118
|
-
textArray[offset] = word.startX + glyph.offset[0] + lineOffset;
|
|
18119
|
-
textArray[offset + 1] = word.startY + glyph.offset[1];
|
|
18120
|
-
textArray[offset + 2] = glyph.char.charIndex;
|
|
18121
|
-
offset += 4;
|
|
18122
|
-
}
|
|
18123
|
-
}
|
|
18124
|
-
if (debug && debugData) {
|
|
18125
|
-
console.table(debugData);
|
|
18126
|
-
}
|
|
18127
|
-
}
|
|
18128
|
-
function measureText(font, text, wordWrap) {
|
|
18129
|
-
let maxWidth = 0;
|
|
18130
|
-
const lineWidths = [];
|
|
18131
|
-
let textOffsetX = 0;
|
|
18132
|
-
let textOffsetY = 0;
|
|
18133
|
-
let line = 0;
|
|
18134
|
-
let printedCharCount = 0;
|
|
18135
|
-
let nextCharCode = text.charCodeAt(0);
|
|
18136
|
-
let word = { glyphs: [], width: 0, startX: 0, startY: 0 };
|
|
18137
|
-
const words = [];
|
|
18138
|
-
for (let i3 = 0;i3 < text.length; i3++) {
|
|
18139
|
-
const isLastLetter = i3 === text.length - 1;
|
|
18140
|
-
const charCode = nextCharCode;
|
|
18141
|
-
nextCharCode = i3 < text.length - 1 ? text.charCodeAt(i3 + 1) : -1;
|
|
18142
|
-
switch (charCode) {
|
|
18143
|
-
case 9 /* HorizontalTab */:
|
|
18144
|
-
insertSpaces(TAB_SPACES);
|
|
18145
|
-
break;
|
|
18146
|
-
case 10 /* Newline */:
|
|
18147
|
-
flushLine();
|
|
18148
|
-
flushWord();
|
|
18149
|
-
break;
|
|
18150
|
-
case 13 /* CarriageReturn */:
|
|
18151
|
-
break;
|
|
18152
|
-
case 32 /* Space */:
|
|
18153
|
-
insertSpaces(1);
|
|
18154
|
-
break;
|
|
18155
|
-
default: {
|
|
18156
|
-
const advance = font.getXAdvance(charCode, nextCharCode);
|
|
18157
|
-
if (wordWrap && wordWrap.breakOn === "character" && textOffsetX + advance > wordWrap.emWidth) {
|
|
18158
|
-
if (word.startX === 0) {
|
|
18159
|
-
flushWord();
|
|
18160
|
-
} else {
|
|
18161
|
-
lineWidths.push(textOffsetX - word.width);
|
|
18162
|
-
line++;
|
|
18163
|
-
maxWidth = Math.max(maxWidth, textOffsetX);
|
|
18164
|
-
textOffsetX = word.width;
|
|
18165
|
-
textOffsetY -= font.lineHeight;
|
|
18166
|
-
word.startX = 0;
|
|
18167
|
-
word.startY = textOffsetY;
|
|
18168
|
-
word.glyphs.forEach((g3) => {
|
|
18169
|
-
g3.line = line;
|
|
18170
|
-
});
|
|
18171
|
-
}
|
|
18172
|
-
}
|
|
18173
|
-
word.glyphs.push({
|
|
18174
|
-
char: font.getChar(charCode),
|
|
18175
|
-
offset: [word.width, 0],
|
|
18176
|
-
line
|
|
18177
|
-
});
|
|
18178
|
-
if (isLastLetter) {
|
|
18179
|
-
flushWord();
|
|
18180
|
-
}
|
|
18181
|
-
word.width += advance;
|
|
18182
|
-
textOffsetX += advance;
|
|
18183
|
-
}
|
|
18184
|
-
}
|
|
18185
|
-
}
|
|
18186
|
-
lineWidths.push(textOffsetX);
|
|
18187
|
-
maxWidth = Math.max(maxWidth, textOffsetX);
|
|
18188
|
-
const lineCount = lineWidths.length;
|
|
18189
|
-
return {
|
|
18190
|
-
width: maxWidth,
|
|
18191
|
-
height: lineCount * font.lineHeight,
|
|
18192
|
-
lineWidths,
|
|
18193
|
-
lineCount,
|
|
18194
|
-
printedCharCount,
|
|
18195
|
-
words
|
|
18196
|
-
};
|
|
18197
|
-
function flushWord() {
|
|
18198
|
-
printedCharCount += word.glyphs.length;
|
|
18199
|
-
words.push(word);
|
|
18200
|
-
word = {
|
|
18201
|
-
glyphs: [],
|
|
18202
|
-
width: 0,
|
|
18203
|
-
startX: textOffsetX,
|
|
18204
|
-
startY: textOffsetY
|
|
18205
|
-
};
|
|
18206
|
-
}
|
|
18207
|
-
function flushLine() {
|
|
18208
|
-
lineWidths.push(textOffsetX);
|
|
18209
|
-
line++;
|
|
18210
|
-
maxWidth = Math.max(maxWidth, textOffsetX);
|
|
18211
|
-
textOffsetX = 0;
|
|
18212
|
-
textOffsetY -= font.lineHeight;
|
|
18213
|
-
}
|
|
18214
|
-
function insertSpaces(spaces) {
|
|
18215
|
-
if (spaces < 1)
|
|
18216
|
-
spaces = 1;
|
|
18217
|
-
textOffsetX += font.getXAdvance(32 /* Space */) * spaces;
|
|
18218
|
-
if (wordWrap?.breakOn === "word" && textOffsetX >= wordWrap.emWidth) {
|
|
18219
|
-
flushLine();
|
|
18220
|
-
}
|
|
18221
|
-
flushWord();
|
|
18222
|
-
}
|
|
18223
|
-
}
|
|
18224
|
-
function findLargestFontSize(font, text, size, formatting) {
|
|
18225
|
-
if (!formatting.fontSize) {
|
|
18226
|
-
throw new Error("fontSize is required for shrinkToFit");
|
|
18227
|
-
}
|
|
18228
|
-
if (!formatting.shrinkToFit) {
|
|
18229
|
-
throw new Error("shrinkToFit is required for findLargestFontSize");
|
|
18230
|
-
}
|
|
18231
|
-
const minSize = formatting.shrinkToFit.minFontSize;
|
|
18232
|
-
const maxSize = formatting.shrinkToFit.maxFontSize ?? formatting.fontSize;
|
|
18233
|
-
const maxLines = formatting.shrinkToFit.maxLines ?? Number.POSITIVE_INFINITY;
|
|
18234
|
-
const threshold = 0.5;
|
|
18235
|
-
let low = minSize;
|
|
18236
|
-
let high = maxSize;
|
|
18237
|
-
while (high - low > threshold) {
|
|
18238
|
-
const testSize = (low + high) / 2;
|
|
18239
|
-
const testMeasure = measureText(font, text, formatting.wordWrap);
|
|
18240
|
-
const padding = formatting.shrinkToFit.padding ?? 0;
|
|
18241
|
-
const scaledWidth = testMeasure.width * (testSize / font.lineHeight);
|
|
18242
|
-
const scaledHeight = testMeasure.height * (testSize / font.lineHeight);
|
|
18243
|
-
const fitsWidth = scaledWidth <= size.width - size.width * padding;
|
|
18244
|
-
const fitsHeight = scaledHeight <= size.height - size.height * padding;
|
|
18245
|
-
const fitsLines = testMeasure.lineCount <= maxLines;
|
|
18246
|
-
if (fitsWidth && fitsHeight && fitsLines) {
|
|
18247
|
-
low = testSize;
|
|
18248
|
-
} else {
|
|
18249
|
-
high = testSize;
|
|
18250
|
-
}
|
|
18251
|
-
}
|
|
18252
|
-
return low;
|
|
18253
|
-
}
|
|
18350
|
+
// Output from vertex shader to fragment shader
|
|
18351
|
+
struct VertexOutput {
|
|
18352
|
+
@builtin(position) position: vec4f,
|
|
18353
|
+
@location(0) texcoord: vec2f,
|
|
18354
|
+
@location(1) debugColor: vec4f,
|
|
18355
|
+
@location(2) @interpolate(flat) instanceIndex: u32,
|
|
18356
|
+
};
|
|
18254
18357
|
|
|
18255
|
-
//
|
|
18256
|
-
|
|
18358
|
+
// Metadata for a single character glyph
|
|
18359
|
+
struct Char {
|
|
18360
|
+
texOffset: vec2f, // Offset to top-left in MSDF texture (pixels)
|
|
18361
|
+
texExtent: vec2f, // Size in texture (pixels)
|
|
18362
|
+
size: vec2f, // Glyph size in ems
|
|
18363
|
+
offset: vec2f, // Position offset in ems
|
|
18364
|
+
};
|
|
18257
18365
|
|
|
18258
|
-
|
|
18259
|
-
|
|
18260
|
-
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
|
|
18267
|
-
const em2px = shader.font.lineHeight / (opts.fontSize ?? DEFAULT_FONT_SIZE);
|
|
18268
|
-
if (!opts.shrinkToFit && !opts.size) {
|
|
18269
|
-
opts.size = { width: width / em2px, height: height / em2px };
|
|
18270
|
-
}
|
|
18271
|
-
super({
|
|
18272
|
-
...opts,
|
|
18273
|
-
render: {
|
|
18274
|
-
shader,
|
|
18275
|
-
writeInstance: (_node, _array, _offset) => {
|
|
18276
|
-
throw new Error("not implemented - needs access to text uniform buffer, dimensions and a model matrix");
|
|
18277
|
-
}
|
|
18278
|
-
}
|
|
18279
|
-
});
|
|
18280
|
-
this.#font = shader.font;
|
|
18281
|
-
this.#text = text;
|
|
18282
|
-
this.#formatting = opts;
|
|
18283
|
-
}
|
|
18284
|
-
get text() {
|
|
18285
|
-
return this.#text;
|
|
18286
|
-
}
|
|
18287
|
-
get formatting() {
|
|
18288
|
-
return this.#formatting;
|
|
18289
|
-
}
|
|
18290
|
-
get font() {
|
|
18291
|
-
return this.#font;
|
|
18292
|
-
}
|
|
18293
|
-
set text(text) {
|
|
18294
|
-
if (!text) {
|
|
18295
|
-
throw new Error("text cannot be empty");
|
|
18296
|
-
}
|
|
18297
|
-
this.#text = text;
|
|
18298
|
-
this.setDirty();
|
|
18299
|
-
}
|
|
18300
|
-
get tint() {
|
|
18301
|
-
return this.#formatting.color || { r: 1, g: 1, b: 1, a: 1 };
|
|
18302
|
-
}
|
|
18303
|
-
set tint(tint) {
|
|
18304
|
-
this.#formatting.color = tint;
|
|
18305
|
-
this.setDirty();
|
|
18306
|
-
}
|
|
18307
|
-
set formatting(formatting) {
|
|
18308
|
-
this.#formatting = formatting;
|
|
18309
|
-
this.setDirty();
|
|
18310
|
-
}
|
|
18311
|
-
}
|
|
18366
|
+
// Metadata for a text block
|
|
18367
|
+
struct TextBlockDescriptor {
|
|
18368
|
+
transform: mat3x3f, // Text transform matrix (model matrix)
|
|
18369
|
+
color: vec4f, // Text color
|
|
18370
|
+
fontSize: f32, // Font size
|
|
18371
|
+
blockWidth: f32, // Total width of text block
|
|
18372
|
+
blockHeight: f32, // Total height of text block
|
|
18373
|
+
bufferPosition: f32 // Index and length in textBuffer
|
|
18374
|
+
};
|
|
18312
18375
|
|
|
18313
|
-
//
|
|
18314
|
-
var
|
|
18315
|
-
|
|
18316
|
-
|
|
18317
|
-
//
|
|
18376
|
+
// Font bindings
|
|
18377
|
+
@group(0) @binding(0) var fontTexture: texture_2d<f32>;
|
|
18378
|
+
@group(0) @binding(1) var fontSampler: sampler;
|
|
18379
|
+
@group(0) @binding(2) var<storage> chars: array<Char>;
|
|
18380
|
+
@group(0) @binding(3) var<uniform> fontData: vec4f; // Contains line height (x)
|
|
18381
|
+
|
|
18382
|
+
// Text bindings
|
|
18383
|
+
@group(1) @binding(0) var<storage> texts: array<TextBlockDescriptor>;
|
|
18384
|
+
@group(1) @binding(1) var<storage> textBuffer: array<vec4f>; // Each vec4: xy = glyph pos, z = char index
|
|
18318
18385
|
|
|
18319
|
-
//
|
|
18320
|
-
@group(
|
|
18321
|
-
var input_texture: texture_2d<f32>;
|
|
18386
|
+
// Global uniforms
|
|
18387
|
+
@group(2) @binding(0) var<uniform> viewProjectionMatrix: mat3x3f;
|
|
18322
18388
|
|
|
18323
|
-
//
|
|
18324
|
-
|
|
18325
|
-
|
|
18326
|
-
|
|
18327
|
-
|
|
18328
|
-
|
|
18329
|
-
|
|
18389
|
+
// Vertex shader
|
|
18390
|
+
@vertex
|
|
18391
|
+
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
18392
|
+
// Because the instance index is used for character indexing, we are
|
|
18393
|
+
// overloading the vertex index to store the instance of the text metadata.
|
|
18394
|
+
//
|
|
18395
|
+
// I.e...
|
|
18396
|
+
// Vertex 0-4 = Instance 0, Vertex 0-4
|
|
18397
|
+
// Vertex 4-8 = Instance 1, Vertex 0-4
|
|
18398
|
+
// Vertex 8-12 = Instance 2, Vertex 0-4
|
|
18399
|
+
let vertexIndex = input.vertex % 4;
|
|
18400
|
+
let textIndex = input.vertex / 4;
|
|
18330
18401
|
|
|
18331
|
-
|
|
18332
|
-
|
|
18333
|
-
|
|
18402
|
+
let text = texts[textIndex];
|
|
18403
|
+
let textElement = textBuffer[u32(text.bufferPosition) + input.instance];
|
|
18404
|
+
let char = chars[u32(textElement.z)];
|
|
18334
18405
|
|
|
18335
|
-
|
|
18336
|
-
|
|
18337
|
-
|
|
18338
|
-
let size = textureDimensions(input_texture).xy;
|
|
18339
|
-
if (gid.x >= size.x || gid.y >= size.y) {
|
|
18340
|
-
return;
|
|
18341
|
-
}
|
|
18406
|
+
let lineHeight = fontData.x;
|
|
18407
|
+
let textWidth = text.blockWidth;
|
|
18408
|
+
let textHeight = text.blockHeight;
|
|
18342
18409
|
|
|
18343
|
-
|
|
18344
|
-
|
|
18345
|
-
atomicMin(&bounds.min_x, gid.x);
|
|
18346
|
-
atomicMin(&bounds.min_y, gid.y);
|
|
18347
|
-
atomicMax(&bounds.max_x, gid.x);
|
|
18348
|
-
atomicMax(&bounds.max_y, gid.y);
|
|
18349
|
-
}
|
|
18350
|
-
}
|
|
18410
|
+
// Center text vertically; origin is mid-height
|
|
18411
|
+
let offset = vec2f(0, -textHeight / 2);
|
|
18351
18412
|
|
|
18352
|
-
//
|
|
18353
|
-
|
|
18354
|
-
|
|
18413
|
+
// Glyph position in ems (quad pos * size + per-char offset)
|
|
18414
|
+
let emPos = pos[vertexIndex] * char.size + char.offset + textElement.xy - offset;
|
|
18415
|
+
let charPos = emPos * (text.fontSize / lineHeight);
|
|
18355
18416
|
|
|
18356
|
-
|
|
18357
|
-
|
|
18358
|
-
var input_texture_crop: texture_2d<f32>;
|
|
18417
|
+
var output: VertexOutput;
|
|
18418
|
+
let transformedPosition = viewProjectionMatrix * text.transform * vec3f(charPos, 1);
|
|
18359
18419
|
|
|
18360
|
-
|
|
18361
|
-
|
|
18362
|
-
|
|
18420
|
+
output.position = vec4f(transformedPosition, 1);
|
|
18421
|
+
output.texcoord = pos[vertexIndex] * vec2f(1, -1);
|
|
18422
|
+
output.texcoord *= char.texExtent;
|
|
18423
|
+
output.texcoord += char.texOffset;
|
|
18424
|
+
output.debugColor = debugColors[vertexIndex];
|
|
18425
|
+
output.instanceIndex = textIndex;
|
|
18426
|
+
return output;
|
|
18363
18427
|
|
|
18364
|
-
//
|
|
18365
|
-
|
|
18366
|
-
|
|
18367
|
-
min_y: u32,
|
|
18368
|
-
max_x: u32,
|
|
18369
|
-
max_y: u32,
|
|
18370
|
-
};
|
|
18428
|
+
// To debug - hardcode quad in bottom right quarter of the screen:
|
|
18429
|
+
// output.position = vec4f(pos[input.vertex], 0, 1);
|
|
18430
|
+
}
|
|
18371
18431
|
|
|
18372
|
-
|
|
18373
|
-
|
|
18432
|
+
// Signed distance function sampling for MSDF font rendering
|
|
18433
|
+
fn sampleMsdf(texcoord: vec2f) -> f32 {
|
|
18434
|
+
let c = textureSample(fontTexture, fontSampler, texcoord);
|
|
18435
|
+
return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
|
|
18436
|
+
}
|
|
18374
18437
|
|
|
18375
|
-
//
|
|
18376
|
-
|
|
18377
|
-
|
|
18378
|
-
|
|
18379
|
-
|
|
18380
|
-
|
|
18381
|
-
|
|
18438
|
+
// Fragment shader
|
|
18439
|
+
// Anti-aliasing technique by Paul Houx
|
|
18440
|
+
// more details here:
|
|
18441
|
+
// https://github.com/Chlumsky/msdfgen/issues/22#issuecomment-234958005
|
|
18442
|
+
@fragment
|
|
18443
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
18444
|
+
let text = texts[input.instanceIndex];
|
|
18382
18445
|
|
|
18383
|
-
//
|
|
18384
|
-
|
|
18385
|
-
|
|
18446
|
+
// pxRange (AKA distanceRange) comes from the msdfgen tool.
|
|
18447
|
+
let pxRange = 4.0;
|
|
18448
|
+
let texSize = vec2f(textureDimensions(fontTexture, 0));
|
|
18386
18449
|
|
|
18387
|
-
|
|
18388
|
-
|
|
18389
|
-
fn crop_and_output(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
18390
|
-
let size = textureDimensions(input_texture_crop).xy;
|
|
18450
|
+
let dx = texSize.x * length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));
|
|
18451
|
+
let dy = texSize.y * length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));
|
|
18391
18452
|
|
|
18392
|
-
|
|
18393
|
-
|
|
18453
|
+
let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
|
|
18454
|
+
let sigDist = sampleMsdf(input.texcoord) - 0.5;
|
|
18455
|
+
let pxDist = sigDist * toPixels;
|
|
18394
18456
|
|
|
18395
|
-
|
|
18396
|
-
|
|
18397
|
-
}
|
|
18457
|
+
let edgeWidth = 0.5;
|
|
18458
|
+
let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
|
|
18398
18459
|
|
|
18399
|
-
|
|
18400
|
-
|
|
18401
|
-
|
|
18402
|
-
);
|
|
18460
|
+
if (alpha < 0.001) {
|
|
18461
|
+
discard;
|
|
18462
|
+
}
|
|
18403
18463
|
|
|
18404
|
-
|
|
18405
|
-
|
|
18406
|
-
textureStore(output_texture, dst_coord, pixel);
|
|
18464
|
+
let msdfColor = vec4f(text.color.rgb, text.color.a * alpha);
|
|
18465
|
+
return msdfColor;
|
|
18407
18466
|
|
|
18408
|
-
|
|
18409
|
-
|
|
18410
|
-
|
|
18411
|
-
|
|
18412
|
-
|
|
18413
|
-
|
|
18467
|
+
// Debug options:
|
|
18468
|
+
// return text.color;
|
|
18469
|
+
// return input.debugColor;
|
|
18470
|
+
// return vec4f(1, 0, 1, 1); // hardcoded magenta
|
|
18471
|
+
// return textureSample(fontTexture, fontSampler, input.texcoord);
|
|
18472
|
+
}
|
|
18473
|
+
`;
|
|
18474
|
+
|
|
18475
|
+
// src/backends/webgpu/FontPipeline.ts
|
|
18476
|
+
class FontPipeline {
|
|
18477
|
+
pipeline;
|
|
18478
|
+
font;
|
|
18479
|
+
fontBindGroup;
|
|
18480
|
+
maxCharCount;
|
|
18481
|
+
constructor(pipeline, font, fontBindGroup, maxCharCount) {
|
|
18482
|
+
this.pipeline = pipeline;
|
|
18483
|
+
this.font = font;
|
|
18484
|
+
this.fontBindGroup = fontBindGroup;
|
|
18485
|
+
this.maxCharCount = maxCharCount;
|
|
18486
|
+
}
|
|
18487
|
+
static async create(device, font, colorFormat, maxCharCount) {
|
|
18488
|
+
const pipeline = await pipelinePromise(device, colorFormat, font.name);
|
|
18489
|
+
const texture = device.createTexture({
|
|
18490
|
+
label: `MSDF font ${font.name}`,
|
|
18491
|
+
size: [font.imageBitmap.width, font.imageBitmap.height, 1],
|
|
18492
|
+
format: "rgba8unorm",
|
|
18493
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
18494
|
+
});
|
|
18495
|
+
device.queue.copyExternalImageToTexture({ source: font.imageBitmap }, { texture }, [font.imageBitmap.width, font.imageBitmap.height]);
|
|
18496
|
+
const charsGpuBuffer = device.createBuffer({
|
|
18497
|
+
label: `MSDF font ${font.name} character layout buffer`,
|
|
18498
|
+
size: font.charCount * Float32Array.BYTES_PER_ELEMENT * 8,
|
|
18499
|
+
usage: GPUBufferUsage.STORAGE,
|
|
18500
|
+
mappedAtCreation: true
|
|
18501
|
+
});
|
|
18502
|
+
const charsArray = new Float32Array(charsGpuBuffer.getMappedRange());
|
|
18503
|
+
charsArray.set(font.charBuffer, 0);
|
|
18504
|
+
charsGpuBuffer.unmap();
|
|
18505
|
+
const fontDataBuffer = device.createBuffer({
|
|
18506
|
+
label: `MSDF font ${font.name} metadata buffer`,
|
|
18507
|
+
size: Float32Array.BYTES_PER_ELEMENT * 4,
|
|
18508
|
+
usage: GPUBufferUsage.UNIFORM,
|
|
18509
|
+
mappedAtCreation: true
|
|
18510
|
+
});
|
|
18511
|
+
const fontDataArray = new Float32Array(fontDataBuffer.getMappedRange());
|
|
18512
|
+
fontDataArray[0] = font.lineHeight;
|
|
18513
|
+
fontDataBuffer.unmap();
|
|
18514
|
+
const fontBindGroup = device.createBindGroup({
|
|
18515
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
18516
|
+
entries: [
|
|
18517
|
+
{
|
|
18518
|
+
binding: 0,
|
|
18519
|
+
resource: texture.createView()
|
|
18520
|
+
},
|
|
18521
|
+
{
|
|
18522
|
+
binding: 1,
|
|
18523
|
+
resource: device.createSampler(sampler)
|
|
18524
|
+
},
|
|
18525
|
+
{
|
|
18526
|
+
binding: 2,
|
|
18527
|
+
resource: {
|
|
18528
|
+
buffer: charsGpuBuffer
|
|
18529
|
+
}
|
|
18530
|
+
},
|
|
18531
|
+
{
|
|
18532
|
+
binding: 3,
|
|
18533
|
+
resource: {
|
|
18534
|
+
buffer: fontDataBuffer
|
|
18535
|
+
}
|
|
18536
|
+
}
|
|
18537
|
+
]
|
|
18538
|
+
});
|
|
18539
|
+
return new FontPipeline(pipeline, font, fontBindGroup, maxCharCount);
|
|
18540
|
+
}
|
|
18541
|
+
}
|
|
18542
|
+
function pipelinePromise(device, colorFormat, label) {
|
|
18543
|
+
const shader = device.createShaderModule({
|
|
18544
|
+
label: `${label} shader`,
|
|
18545
|
+
code: text_wgsl_default
|
|
18546
|
+
});
|
|
18547
|
+
return device.createRenderPipelineAsync({
|
|
18548
|
+
label: `${label} pipeline`,
|
|
18549
|
+
layout: device.createPipelineLayout({
|
|
18550
|
+
bindGroupLayouts: [
|
|
18551
|
+
device.createBindGroupLayout(fontBindGroupLayout),
|
|
18552
|
+
device.createBindGroupLayout(textUniformBindGroupLayout),
|
|
18553
|
+
device.createBindGroupLayout(engineUniformBindGroupLayout)
|
|
18554
|
+
]
|
|
18555
|
+
}),
|
|
18556
|
+
vertex: {
|
|
18557
|
+
module: shader,
|
|
18558
|
+
entryPoint: "vertexMain"
|
|
18559
|
+
},
|
|
18560
|
+
fragment: {
|
|
18561
|
+
module: shader,
|
|
18562
|
+
entryPoint: "fragmentMain",
|
|
18563
|
+
targets: [
|
|
18564
|
+
{
|
|
18565
|
+
format: colorFormat,
|
|
18566
|
+
blend: {
|
|
18567
|
+
color: {
|
|
18568
|
+
srcFactor: "src-alpha",
|
|
18569
|
+
dstFactor: "one-minus-src-alpha"
|
|
18570
|
+
},
|
|
18571
|
+
alpha: {
|
|
18572
|
+
srcFactor: "one",
|
|
18573
|
+
dstFactor: "one"
|
|
18574
|
+
}
|
|
18575
|
+
}
|
|
18576
|
+
}
|
|
18577
|
+
]
|
|
18578
|
+
},
|
|
18579
|
+
primitive: {
|
|
18580
|
+
topology: "triangle-strip",
|
|
18581
|
+
stripIndexFormat: "uint32"
|
|
18414
18582
|
}
|
|
18583
|
+
});
|
|
18415
18584
|
}
|
|
18416
|
-
|
|
18417
|
-
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
var
|
|
18424
|
-
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18585
|
+
if (typeof GPUShaderStage === "undefined") {
|
|
18586
|
+
globalThis.GPUShaderStage = {
|
|
18587
|
+
VERTEX: 1,
|
|
18588
|
+
FRAGMENT: 2,
|
|
18589
|
+
COMPUTE: 4
|
|
18590
|
+
};
|
|
18591
|
+
}
|
|
18592
|
+
var fontBindGroupLayout = {
|
|
18593
|
+
label: "MSDF font group layout",
|
|
18594
|
+
entries: [
|
|
18595
|
+
{
|
|
18596
|
+
binding: 0,
|
|
18597
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
18598
|
+
texture: {}
|
|
18599
|
+
},
|
|
18600
|
+
{
|
|
18601
|
+
binding: 1,
|
|
18602
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
18603
|
+
sampler: {}
|
|
18604
|
+
},
|
|
18605
|
+
{
|
|
18606
|
+
binding: 2,
|
|
18607
|
+
visibility: GPUShaderStage.VERTEX,
|
|
18608
|
+
buffer: { type: "read-only-storage" }
|
|
18609
|
+
},
|
|
18610
|
+
{
|
|
18611
|
+
binding: 3,
|
|
18612
|
+
visibility: GPUShaderStage.VERTEX,
|
|
18613
|
+
buffer: {}
|
|
18431
18614
|
}
|
|
18432
|
-
|
|
18433
|
-
|
|
18434
|
-
|
|
18435
|
-
|
|
18436
|
-
|
|
18437
|
-
|
|
18438
|
-
|
|
18439
|
-
|
|
18440
|
-
|
|
18441
|
-
|
|
18442
|
-
|
|
18615
|
+
]
|
|
18616
|
+
};
|
|
18617
|
+
var engineUniformBindGroupLayout = {
|
|
18618
|
+
label: "Uniform bind group",
|
|
18619
|
+
entries: [
|
|
18620
|
+
{
|
|
18621
|
+
binding: 0,
|
|
18622
|
+
visibility: GPUShaderStage.VERTEX,
|
|
18623
|
+
buffer: {}
|
|
18624
|
+
}
|
|
18625
|
+
]
|
|
18626
|
+
};
|
|
18627
|
+
var sampler = {
|
|
18628
|
+
label: "MSDF text sampler",
|
|
18629
|
+
minFilter: "linear",
|
|
18630
|
+
magFilter: "linear",
|
|
18631
|
+
mipmapFilter: "linear",
|
|
18632
|
+
maxAnisotropy: 16
|
|
18633
|
+
};
|
|
18634
|
+
var textUniformBindGroupLayout = {
|
|
18635
|
+
label: "MSDF text block uniform",
|
|
18636
|
+
entries: [
|
|
18637
|
+
{
|
|
18638
|
+
binding: 0,
|
|
18639
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
18640
|
+
buffer: { type: "read-only-storage" }
|
|
18641
|
+
},
|
|
18642
|
+
{
|
|
18643
|
+
binding: 1,
|
|
18644
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
18645
|
+
buffer: { type: "read-only-storage" }
|
|
18646
|
+
}
|
|
18647
|
+
]
|
|
18648
|
+
};
|
|
18649
|
+
// src/backends/webgpu/WebGPUTextShader.ts
|
|
18650
|
+
var deets = new _t2(text_wgsl_default);
|
|
18651
|
+
var struct = deets.structs.find((s3) => s3.name === "TextBlockDescriptor");
|
|
18652
|
+
if (!struct) {
|
|
18653
|
+
throw new Error("FormattedText struct not found");
|
|
18443
18654
|
}
|
|
18444
|
-
|
|
18445
|
-
|
|
18446
|
-
// src/backends/webgpu/TextureComputeShader.ts
|
|
18447
|
-
var BOUNDING_BOX_SIZE = 4 * Uint32Array.BYTES_PER_ELEMENT;
|
|
18448
|
-
var WORKGROUP_SIZE = 8;
|
|
18449
|
-
var MAX_BOUND = 4294967295;
|
|
18450
|
-
var MIN_BOUND = 0;
|
|
18655
|
+
var textDescriptorInstanceSize = struct.size;
|
|
18451
18656
|
|
|
18452
|
-
class
|
|
18453
|
-
|
|
18454
|
-
#
|
|
18455
|
-
#
|
|
18456
|
-
#
|
|
18457
|
-
#
|
|
18458
|
-
|
|
18459
|
-
|
|
18460
|
-
|
|
18461
|
-
|
|
18462
|
-
|
|
18463
|
-
|
|
18464
|
-
|
|
18465
|
-
|
|
18466
|
-
|
|
18467
|
-
|
|
18468
|
-
|
|
18469
|
-
|
|
18470
|
-
|
|
18471
|
-
|
|
18472
|
-
|
|
18473
|
-
|
|
18474
|
-
|
|
18475
|
-
|
|
18476
|
-
const dispatchX = Math.ceil(textureWrapper.texture.width / WORKGROUP_SIZE);
|
|
18477
|
-
const dispatchY = Math.ceil(textureWrapper.texture.height / WORKGROUP_SIZE);
|
|
18478
|
-
const boundsInit = new Uint32Array([
|
|
18479
|
-
MAX_BOUND,
|
|
18480
|
-
MAX_BOUND,
|
|
18481
|
-
MIN_BOUND,
|
|
18482
|
-
MIN_BOUND
|
|
18483
|
-
]);
|
|
18484
|
-
this.#device.queue.writeBuffer(this.#boundingBuffer, 0, boundsInit.buffer, 0, BOUNDING_BOX_SIZE);
|
|
18485
|
-
passEncoder.setPipeline(this.#boundPipeline);
|
|
18486
|
-
passEncoder.setBindGroup(0, boundsBindGroup);
|
|
18487
|
-
passEncoder.dispatchWorkgroups(dispatchX, dispatchY);
|
|
18488
|
-
passEncoder.end();
|
|
18489
|
-
this.#device.queue.submit([commandEncoder.finish()]);
|
|
18490
|
-
const { texelX, texelY, texelWidth, texelHeight, computeBuffer } = await this.#getBoundingBox();
|
|
18491
|
-
if (texelX === MAX_BOUND || texelY === MAX_BOUND) {
|
|
18492
|
-
return await this.#createMissingTexture(textureWrapper.texture);
|
|
18493
|
-
}
|
|
18494
|
-
const croppedTexture = await this.#cropTexture(texelWidth, texelHeight, computeBuffer, textureWrapper.texture);
|
|
18495
|
-
const leftCrop = texelX;
|
|
18496
|
-
const rightCrop = textureWrapper.originalSize.width - texelX - texelWidth;
|
|
18497
|
-
const topCrop = texelY;
|
|
18498
|
-
const bottomCrop = textureWrapper.originalSize.height - texelY - texelHeight;
|
|
18499
|
-
textureWrapper = {
|
|
18500
|
-
texture: croppedTexture,
|
|
18501
|
-
cropOffset: { x: leftCrop - rightCrop, y: bottomCrop - topCrop },
|
|
18502
|
-
originalSize: textureWrapper.originalSize
|
|
18503
|
-
};
|
|
18504
|
-
return textureWrapper;
|
|
18505
|
-
}
|
|
18506
|
-
async#getBoundingBox() {
|
|
18507
|
-
const readBuffer = this.#device.createBuffer({
|
|
18508
|
-
label: "AABB Compute Buffer",
|
|
18509
|
-
size: BOUNDING_BOX_SIZE,
|
|
18510
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
18657
|
+
class WebGPUTextShader {
|
|
18658
|
+
label = "text";
|
|
18659
|
+
#backend;
|
|
18660
|
+
#pipeline;
|
|
18661
|
+
#bindGroups = [];
|
|
18662
|
+
#font;
|
|
18663
|
+
#maxCharCount;
|
|
18664
|
+
#engineUniformsBuffer;
|
|
18665
|
+
#descriptorBuffer;
|
|
18666
|
+
#textBlockBuffer;
|
|
18667
|
+
#cpuDescriptorBuffer;
|
|
18668
|
+
#cpuTextBlockBuffer;
|
|
18669
|
+
#instanceIndex = 0;
|
|
18670
|
+
#textBlockOffset = 0;
|
|
18671
|
+
constructor(backend, pipeline, font, _colorFormat, instanceCount) {
|
|
18672
|
+
this.#backend = backend;
|
|
18673
|
+
const device = backend.device;
|
|
18674
|
+
this.#font = font;
|
|
18675
|
+
this.#pipeline = pipeline.pipeline;
|
|
18676
|
+
this.#maxCharCount = pipeline.maxCharCount;
|
|
18677
|
+
this.#descriptorBuffer = device.createBuffer({
|
|
18678
|
+
label: "msdf text descriptor buffer",
|
|
18679
|
+
size: textDescriptorInstanceSize * instanceCount,
|
|
18680
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
18511
18681
|
});
|
|
18512
|
-
|
|
18513
|
-
|
|
18514
|
-
this.#device.
|
|
18515
|
-
|
|
18516
|
-
|
|
18517
|
-
readBuffer.unmap();
|
|
18518
|
-
const [minX, minY, maxX, maxY] = computeBuffer;
|
|
18519
|
-
return {
|
|
18520
|
-
texelX: minX,
|
|
18521
|
-
texelY: minY,
|
|
18522
|
-
texelWidth: maxX - minX + 1,
|
|
18523
|
-
texelHeight: maxY - minY + 1,
|
|
18524
|
-
computeBuffer
|
|
18525
|
-
};
|
|
18526
|
-
}
|
|
18527
|
-
async#cropTexture(croppedWidth, croppedHeight, computeBuffer, inputTexture) {
|
|
18528
|
-
const boundsUniform = this.#device.createBuffer({
|
|
18529
|
-
label: "Cropping Bounds Uniform Buffer",
|
|
18530
|
-
size: BOUNDING_BOX_SIZE,
|
|
18682
|
+
this.#cpuDescriptorBuffer = new Float32Array(instanceCount * textDescriptorInstanceSize / Float32Array.BYTES_PER_ELEMENT);
|
|
18683
|
+
this.#cpuTextBlockBuffer = new Float32Array(instanceCount * this.maxCharCount * 4);
|
|
18684
|
+
this.#engineUniformsBuffer = device.createBuffer({
|
|
18685
|
+
label: "msdf view projection matrix",
|
|
18686
|
+
size: Float32Array.BYTES_PER_ELEMENT * 12,
|
|
18531
18687
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
18532
|
-
});
|
|
18533
|
-
this.#device.
|
|
18534
|
-
|
|
18535
|
-
|
|
18536
|
-
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
size: BOUNDING_BOX_SIZE,
|
|
18543
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
18544
|
-
});
|
|
18545
|
-
const bindGroup = this.#croppingBindGroup(inputTexture, outputTexture, boundsUniform, dimensionsOutBuffer);
|
|
18546
|
-
const encoder = this.#device.createCommandEncoder();
|
|
18547
|
-
const pass = encoder.beginComputePass();
|
|
18548
|
-
pass.setPipeline(this.#cropPipeline);
|
|
18549
|
-
pass.setBindGroup(0, bindGroup);
|
|
18550
|
-
pass.dispatchWorkgroups(Math.ceil(croppedWidth / WORKGROUP_SIZE), Math.ceil(croppedHeight / WORKGROUP_SIZE));
|
|
18551
|
-
pass.end();
|
|
18552
|
-
this.#device.queue.submit([encoder.finish()]);
|
|
18553
|
-
return outputTexture;
|
|
18554
|
-
}
|
|
18555
|
-
async#createMissingTexture(inputTexture) {
|
|
18556
|
-
const placeholder = this.#device.createTexture({
|
|
18557
|
-
label: "Missing Placeholder Texture",
|
|
18558
|
-
size: [inputTexture.width, inputTexture.height],
|
|
18559
|
-
format: "rgba8unorm",
|
|
18560
|
-
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING
|
|
18561
|
-
});
|
|
18562
|
-
const encoder = this.#device.createCommandEncoder();
|
|
18563
|
-
const pass = encoder.beginComputePass();
|
|
18564
|
-
pass.setPipeline(this.#missingTexturePipeline);
|
|
18565
|
-
pass.setBindGroup(0, this.#missingTextureBindGroup(placeholder));
|
|
18566
|
-
pass.dispatchWorkgroups(placeholder.width / 8, placeholder.height / 8);
|
|
18567
|
-
pass.end();
|
|
18568
|
-
this.#device.queue.submit([encoder.finish()]);
|
|
18569
|
-
return {
|
|
18570
|
-
texture: placeholder,
|
|
18571
|
-
cropOffset: { x: 0, y: 0 },
|
|
18572
|
-
originalSize: { width: inputTexture.width, height: inputTexture.height }
|
|
18573
|
-
};
|
|
18574
|
-
}
|
|
18575
|
-
#boundsBindGroup(inputTexture) {
|
|
18576
|
-
return this.#device.createBindGroup({
|
|
18577
|
-
layout: this.#boundPipeline.getBindGroupLayout(0),
|
|
18688
|
+
});
|
|
18689
|
+
this.#textBlockBuffer = device.createBuffer({
|
|
18690
|
+
label: "msdf text buffer",
|
|
18691
|
+
size: instanceCount * this.maxCharCount * 4 * Float32Array.BYTES_PER_ELEMENT,
|
|
18692
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
18693
|
+
});
|
|
18694
|
+
this.#bindGroups.push(pipeline.fontBindGroup);
|
|
18695
|
+
this.#bindGroups.push(device.createBindGroup({
|
|
18696
|
+
label: "msdf text bind group",
|
|
18697
|
+
layout: pipeline.pipeline.getBindGroupLayout(1),
|
|
18578
18698
|
entries: [
|
|
18579
|
-
{
|
|
18580
|
-
|
|
18699
|
+
{
|
|
18700
|
+
binding: 0,
|
|
18701
|
+
resource: { buffer: this.#descriptorBuffer }
|
|
18702
|
+
},
|
|
18703
|
+
{
|
|
18704
|
+
binding: 1,
|
|
18705
|
+
resource: { buffer: this.#textBlockBuffer }
|
|
18706
|
+
}
|
|
18581
18707
|
]
|
|
18582
|
-
});
|
|
18583
|
-
|
|
18584
|
-
|
|
18585
|
-
|
|
18586
|
-
layout: this.#cropPipeline.getBindGroupLayout(0),
|
|
18708
|
+
}));
|
|
18709
|
+
const engineUniformsBindGroup = device.createBindGroup({
|
|
18710
|
+
label: "msdf text uniforms bind group",
|
|
18711
|
+
layout: pipeline.pipeline.getBindGroupLayout(2),
|
|
18587
18712
|
entries: [
|
|
18588
|
-
{
|
|
18589
|
-
|
|
18590
|
-
|
|
18591
|
-
|
|
18713
|
+
{
|
|
18714
|
+
binding: 0,
|
|
18715
|
+
resource: { buffer: this.#engineUniformsBuffer }
|
|
18716
|
+
}
|
|
18592
18717
|
]
|
|
18593
18718
|
});
|
|
18719
|
+
this.#bindGroups.push(engineUniformsBindGroup);
|
|
18594
18720
|
}
|
|
18595
|
-
|
|
18596
|
-
|
|
18597
|
-
|
|
18598
|
-
|
|
18599
|
-
|
|
18721
|
+
startFrame(uniform) {
|
|
18722
|
+
const device = this.#backend.device;
|
|
18723
|
+
device.queue.writeBuffer(this.#engineUniformsBuffer, 0, uniform.viewProjectionMatrix);
|
|
18724
|
+
this.#instanceIndex = 0;
|
|
18725
|
+
this.#textBlockOffset = 0;
|
|
18600
18726
|
}
|
|
18601
|
-
|
|
18602
|
-
|
|
18603
|
-
|
|
18604
|
-
|
|
18605
|
-
|
|
18606
|
-
|
|
18607
|
-
|
|
18608
|
-
|
|
18609
|
-
|
|
18610
|
-
{
|
|
18611
|
-
|
|
18612
|
-
|
|
18613
|
-
texture: { sampleType: "float", viewDimension: "2d" }
|
|
18614
|
-
},
|
|
18615
|
-
{
|
|
18616
|
-
binding: 1,
|
|
18617
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
18618
|
-
buffer: { type: "storage" }
|
|
18619
|
-
}
|
|
18620
|
-
]
|
|
18621
|
-
});
|
|
18622
|
-
const cropBindGroupLayout = device.createBindGroupLayout({
|
|
18623
|
-
label: "Cropping Layout",
|
|
18624
|
-
entries: [
|
|
18625
|
-
{ binding: 0, visibility: GPUShaderStage.COMPUTE, texture: {} },
|
|
18626
|
-
{
|
|
18627
|
-
binding: 1,
|
|
18628
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
18629
|
-
storageTexture: {
|
|
18630
|
-
access: "write-only",
|
|
18631
|
-
format: "rgba8unorm",
|
|
18632
|
-
viewDimension: "2d"
|
|
18633
|
-
}
|
|
18634
|
-
},
|
|
18635
|
-
{
|
|
18636
|
-
binding: 2,
|
|
18637
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
18638
|
-
buffer: { type: "uniform" }
|
|
18639
|
-
},
|
|
18640
|
-
{
|
|
18641
|
-
binding: 3,
|
|
18642
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
18643
|
-
buffer: { type: "storage" }
|
|
18644
|
-
}
|
|
18645
|
-
]
|
|
18646
|
-
});
|
|
18647
|
-
const missingTextureBindGroupLayout = device.createBindGroupLayout({
|
|
18648
|
-
label: "Missing Texture Layout",
|
|
18649
|
-
entries: [
|
|
18650
|
-
{
|
|
18651
|
-
binding: 0,
|
|
18652
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
18653
|
-
storageTexture: {
|
|
18654
|
-
access: "write-only",
|
|
18655
|
-
format: "rgba8unorm",
|
|
18656
|
-
viewDimension: "2d"
|
|
18657
|
-
}
|
|
18727
|
+
processBatch(nodes) {
|
|
18728
|
+
if (nodes.length === 0)
|
|
18729
|
+
return 0;
|
|
18730
|
+
const renderPass = this.#backend.renderPass;
|
|
18731
|
+
renderPass.setPipeline(this.#pipeline);
|
|
18732
|
+
for (let i3 = 0;i3 < this.#bindGroups.length; i3++) {
|
|
18733
|
+
renderPass.setBindGroup(i3, this.#bindGroups[i3]);
|
|
18734
|
+
}
|
|
18735
|
+
for (const node of nodes) {
|
|
18736
|
+
if (!(node instanceof TextNode)) {
|
|
18737
|
+
console.error(node);
|
|
18738
|
+
throw new Error(`Tried to use WebGPUTextShader on something that isn't a TextNode: ${node}`);
|
|
18658
18739
|
}
|
|
18659
|
-
|
|
18660
|
-
|
|
18661
|
-
|
|
18662
|
-
|
|
18663
|
-
|
|
18664
|
-
|
|
18665
|
-
|
|
18666
|
-
|
|
18667
|
-
|
|
18668
|
-
|
|
18669
|
-
|
|
18670
|
-
|
|
18671
|
-
|
|
18672
|
-
|
|
18673
|
-
|
|
18674
|
-
|
|
18675
|
-
|
|
18676
|
-
|
|
18677
|
-
|
|
18678
|
-
|
|
18679
|
-
|
|
18680
|
-
|
|
18681
|
-
|
|
18682
|
-
|
|
18683
|
-
|
|
18684
|
-
|
|
18685
|
-
|
|
18686
|
-
|
|
18687
|
-
|
|
18688
|
-
|
|
18689
|
-
|
|
18690
|
-
// Quad vertex positions for a character
|
|
18691
|
-
const pos = array(
|
|
18692
|
-
vec2f(0, -1),
|
|
18693
|
-
vec2f(1, -1),
|
|
18694
|
-
vec2f(0, 0),
|
|
18695
|
-
vec2f(1, 0),
|
|
18696
|
-
);
|
|
18697
|
-
|
|
18698
|
-
// Debug colors for visualization
|
|
18699
|
-
const debugColors = array(
|
|
18700
|
-
vec4f(1, 0, 0, 1),
|
|
18701
|
-
vec4f(0, 1, 0, 1),
|
|
18702
|
-
vec4f(0, 0, 1, 1),
|
|
18703
|
-
vec4f(1, 1, 1, 1),
|
|
18704
|
-
);
|
|
18705
|
-
|
|
18706
|
-
// Vertex input from GPU
|
|
18707
|
-
struct VertexInput {
|
|
18708
|
-
@builtin(vertex_index) vertex: u32,
|
|
18709
|
-
@builtin(instance_index) instance: u32,
|
|
18710
|
-
};
|
|
18711
|
-
|
|
18712
|
-
// Output from vertex shader to fragment shader
|
|
18713
|
-
struct VertexOutput {
|
|
18714
|
-
@builtin(position) position: vec4f,
|
|
18715
|
-
@location(0) texcoord: vec2f,
|
|
18716
|
-
@location(1) debugColor: vec4f,
|
|
18717
|
-
@location(2) @interpolate(flat) instanceIndex: u32,
|
|
18718
|
-
};
|
|
18719
|
-
|
|
18720
|
-
// Metadata for a single character glyph
|
|
18721
|
-
struct Char {
|
|
18722
|
-
texOffset: vec2f, // Offset to top-left in MSDF texture (pixels)
|
|
18723
|
-
texExtent: vec2f, // Size in texture (pixels)
|
|
18724
|
-
size: vec2f, // Glyph size in ems
|
|
18725
|
-
offset: vec2f, // Position offset in ems
|
|
18726
|
-
};
|
|
18727
|
-
|
|
18728
|
-
// Metadata for a text block
|
|
18729
|
-
struct TextBlockDescriptor {
|
|
18730
|
-
transform: mat3x3f, // Text transform matrix (model matrix)
|
|
18731
|
-
color: vec4f, // Text color
|
|
18732
|
-
fontSize: f32, // Font size
|
|
18733
|
-
blockWidth: f32, // Total width of text block
|
|
18734
|
-
blockHeight: f32, // Total height of text block
|
|
18735
|
-
bufferPosition: f32 // Index and length in textBuffer
|
|
18736
|
-
};
|
|
18737
|
-
|
|
18738
|
-
// Font bindings
|
|
18739
|
-
@group(0) @binding(0) var fontTexture: texture_2d<f32>;
|
|
18740
|
-
@group(0) @binding(1) var fontSampler: sampler;
|
|
18741
|
-
@group(0) @binding(2) var<storage> chars: array<Char>;
|
|
18742
|
-
@group(0) @binding(3) var<uniform> fontData: vec4f; // Contains line height (x)
|
|
18743
|
-
|
|
18744
|
-
// Text bindings
|
|
18745
|
-
@group(1) @binding(0) var<storage> texts: array<TextBlockDescriptor>;
|
|
18746
|
-
@group(1) @binding(1) var<storage> textBuffer: array<vec4f>; // Each vec4: xy = glyph pos, z = char index
|
|
18747
|
-
|
|
18748
|
-
// Global uniforms
|
|
18749
|
-
@group(2) @binding(0) var<uniform> viewProjectionMatrix: mat3x3f;
|
|
18750
|
-
|
|
18751
|
-
// Vertex shader
|
|
18752
|
-
@vertex
|
|
18753
|
-
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
18754
|
-
// Because the instance index is used for character indexing, we are
|
|
18755
|
-
// overloading the vertex index to store the instance of the text metadata.
|
|
18756
|
-
//
|
|
18757
|
-
// I.e...
|
|
18758
|
-
// Vertex 0-4 = Instance 0, Vertex 0-4
|
|
18759
|
-
// Vertex 4-8 = Instance 1, Vertex 0-4
|
|
18760
|
-
// Vertex 8-12 = Instance 2, Vertex 0-4
|
|
18761
|
-
let vertexIndex = input.vertex % 4;
|
|
18762
|
-
let textIndex = input.vertex / 4;
|
|
18763
|
-
|
|
18764
|
-
let text = texts[textIndex];
|
|
18765
|
-
let textElement = textBuffer[u32(text.bufferPosition) + input.instance];
|
|
18766
|
-
let char = chars[u32(textElement.z)];
|
|
18767
|
-
|
|
18768
|
-
let lineHeight = fontData.x;
|
|
18769
|
-
let textWidth = text.blockWidth;
|
|
18770
|
-
let textHeight = text.blockHeight;
|
|
18771
|
-
|
|
18772
|
-
// Center text vertically; origin is mid-height
|
|
18773
|
-
let offset = vec2f(0, -textHeight / 2);
|
|
18774
|
-
|
|
18775
|
-
// Glyph position in ems (quad pos * size + per-char offset)
|
|
18776
|
-
let emPos = pos[vertexIndex] * char.size + char.offset + textElement.xy - offset;
|
|
18777
|
-
let charPos = emPos * (text.fontSize / lineHeight);
|
|
18778
|
-
|
|
18779
|
-
var output: VertexOutput;
|
|
18780
|
-
let transformedPosition = viewProjectionMatrix * text.transform * vec3f(charPos, 1);
|
|
18781
|
-
|
|
18782
|
-
output.position = vec4f(transformedPosition, 1);
|
|
18783
|
-
output.texcoord = pos[vertexIndex] * vec2f(1, -1);
|
|
18784
|
-
output.texcoord *= char.texExtent;
|
|
18785
|
-
output.texcoord += char.texOffset;
|
|
18786
|
-
output.debugColor = debugColors[vertexIndex];
|
|
18787
|
-
output.instanceIndex = textIndex;
|
|
18788
|
-
return output;
|
|
18789
|
-
|
|
18790
|
-
// To debug - hardcode quad in bottom right quarter of the screen:
|
|
18791
|
-
// output.position = vec4f(pos[input.vertex], 0, 1);
|
|
18740
|
+
const text = node.text;
|
|
18741
|
+
const formatting = node.formatting;
|
|
18742
|
+
const measurements = measureText(this.#font, text, formatting.wordWrap);
|
|
18743
|
+
const textBlockSize = 4 * text.length;
|
|
18744
|
+
const textDescriptorOffset = this.#instanceIndex * textDescriptorInstanceSize / Float32Array.BYTES_PER_ELEMENT;
|
|
18745
|
+
this.#cpuDescriptorBuffer.set(node.matrix, textDescriptorOffset);
|
|
18746
|
+
this.#cpuDescriptorBuffer.set([node.tint.r, node.tint.g, node.tint.b, node.tint.a], textDescriptorOffset + 12);
|
|
18747
|
+
const size = node.size ?? measurements;
|
|
18748
|
+
const fontSize = formatting.shrinkToFit ? findLargestFontSize(this.#font, text, size, formatting) : formatting.fontSize;
|
|
18749
|
+
const actualFontSize = fontSize || DEFAULT_FONT_SIZE;
|
|
18750
|
+
this.#cpuDescriptorBuffer[textDescriptorOffset + 16] = actualFontSize;
|
|
18751
|
+
this.#cpuDescriptorBuffer[textDescriptorOffset + 17] = formatting.align === "center" ? 0 : measurements.width;
|
|
18752
|
+
this.#cpuDescriptorBuffer[textDescriptorOffset + 18] = measurements.height;
|
|
18753
|
+
this.#cpuDescriptorBuffer[textDescriptorOffset + 19] = this.#textBlockOffset / 4;
|
|
18754
|
+
shapeText(this.#font, text, size, actualFontSize, formatting, this.#cpuTextBlockBuffer, this.#textBlockOffset);
|
|
18755
|
+
this.#backend.device.queue.writeBuffer(this.#descriptorBuffer, textDescriptorOffset * Float32Array.BYTES_PER_ELEMENT, this.#cpuDescriptorBuffer, textDescriptorOffset, textDescriptorInstanceSize / Float32Array.BYTES_PER_ELEMENT);
|
|
18756
|
+
this.#backend.device.queue.writeBuffer(this.#textBlockBuffer, this.#textBlockOffset * Float32Array.BYTES_PER_ELEMENT, this.#cpuTextBlockBuffer, this.#textBlockOffset, textBlockSize);
|
|
18757
|
+
this.#textBlockOffset += textBlockSize;
|
|
18758
|
+
renderPass.draw(4, measurements.printedCharCount, 4 * this.#instanceIndex, 0);
|
|
18759
|
+
this.#instanceIndex++;
|
|
18760
|
+
}
|
|
18761
|
+
return nodes.length;
|
|
18762
|
+
}
|
|
18763
|
+
endFrame() {}
|
|
18764
|
+
get font() {
|
|
18765
|
+
return this.#font;
|
|
18766
|
+
}
|
|
18767
|
+
get maxCharCount() {
|
|
18768
|
+
return this.#maxCharCount;
|
|
18769
|
+
}
|
|
18792
18770
|
}
|
|
18793
18771
|
|
|
18794
|
-
//
|
|
18795
|
-
|
|
18796
|
-
|
|
18797
|
-
|
|
18772
|
+
// src/scene/Batcher.ts
|
|
18773
|
+
class Batcher {
|
|
18774
|
+
nodes = [];
|
|
18775
|
+
layers = [];
|
|
18776
|
+
pipelines = [];
|
|
18777
|
+
enqueue(node) {
|
|
18778
|
+
if (node.renderComponent && node.isActive) {
|
|
18779
|
+
this.nodes.push(node);
|
|
18780
|
+
const z3 = node.layer;
|
|
18781
|
+
const layer = this.#findOrCreateLayer(z3);
|
|
18782
|
+
const pipeline = this.#findOrCreatePipeline(layer, node.renderComponent.shader);
|
|
18783
|
+
pipeline.nodes.push(node);
|
|
18784
|
+
}
|
|
18785
|
+
for (const kid of node.kids) {
|
|
18786
|
+
this.enqueue(kid);
|
|
18787
|
+
}
|
|
18788
|
+
}
|
|
18789
|
+
flush() {
|
|
18790
|
+
this.nodes = [];
|
|
18791
|
+
this.layers = [];
|
|
18792
|
+
this.pipelines = [];
|
|
18793
|
+
}
|
|
18794
|
+
#findOrCreateLayer(z3) {
|
|
18795
|
+
let layer = this.layers.find((l3) => l3.z === z3);
|
|
18796
|
+
if (!layer) {
|
|
18797
|
+
layer = { z: z3, pipelines: [] };
|
|
18798
|
+
this.layers.push(layer);
|
|
18799
|
+
this.layers.sort((a3, b3) => a3.z - b3.z);
|
|
18800
|
+
}
|
|
18801
|
+
return layer;
|
|
18802
|
+
}
|
|
18803
|
+
#findOrCreatePipeline(layer, shader) {
|
|
18804
|
+
let pipeline = layer.pipelines.find((p3) => p3.shader === shader);
|
|
18805
|
+
if (!pipeline) {
|
|
18806
|
+
pipeline = { shader, nodes: [] };
|
|
18807
|
+
layer.pipelines.push(pipeline);
|
|
18808
|
+
this.pipelines.push(pipeline);
|
|
18809
|
+
}
|
|
18810
|
+
return pipeline;
|
|
18811
|
+
}
|
|
18798
18812
|
}
|
|
18799
18813
|
|
|
18800
|
-
//
|
|
18801
|
-
|
|
18802
|
-
|
|
18803
|
-
|
|
18804
|
-
|
|
18805
|
-
|
|
18806
|
-
|
|
18807
|
-
|
|
18808
|
-
|
|
18809
|
-
|
|
18810
|
-
|
|
18811
|
-
|
|
18812
|
-
|
|
18813
|
-
|
|
18814
|
-
|
|
18815
|
-
|
|
18816
|
-
|
|
18817
|
-
|
|
18818
|
-
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
|
|
18822
|
-
|
|
18823
|
-
|
|
18814
|
+
// src/scene/Camera.ts
|
|
18815
|
+
class Camera {
|
|
18816
|
+
#position = { x: 0, y: 0 };
|
|
18817
|
+
#zoom = 1;
|
|
18818
|
+
#rotation = 0;
|
|
18819
|
+
#isDirty = true;
|
|
18820
|
+
#matrix = mat3.create();
|
|
18821
|
+
get zoom() {
|
|
18822
|
+
return this.#zoom;
|
|
18823
|
+
}
|
|
18824
|
+
set zoom(value) {
|
|
18825
|
+
this.#zoom = value;
|
|
18826
|
+
this.setDirty();
|
|
18827
|
+
}
|
|
18828
|
+
get rotation() {
|
|
18829
|
+
return rad2deg(this.#rotation);
|
|
18830
|
+
}
|
|
18831
|
+
set rotation(value) {
|
|
18832
|
+
this.#rotation = deg2rad(value);
|
|
18833
|
+
this.setDirty();
|
|
18834
|
+
}
|
|
18835
|
+
get rotationRadians() {
|
|
18836
|
+
return this.#rotation;
|
|
18837
|
+
}
|
|
18838
|
+
set rotationRadians(value) {
|
|
18839
|
+
this.#rotation = value;
|
|
18840
|
+
this.setDirty();
|
|
18841
|
+
}
|
|
18842
|
+
get x() {
|
|
18843
|
+
return this.#position.x;
|
|
18844
|
+
}
|
|
18845
|
+
get y() {
|
|
18846
|
+
return this.#position.y;
|
|
18847
|
+
}
|
|
18848
|
+
set x(value) {
|
|
18849
|
+
this.#position.x = value;
|
|
18850
|
+
this.setDirty();
|
|
18851
|
+
}
|
|
18852
|
+
set y(value) {
|
|
18853
|
+
this.#position.y = value;
|
|
18854
|
+
this.setDirty();
|
|
18855
|
+
}
|
|
18856
|
+
get matrix() {
|
|
18857
|
+
if (this.#isDirty) {
|
|
18858
|
+
this.#isDirty = false;
|
|
18859
|
+
this.#matrix = createViewMatrix(this, this.#matrix);
|
|
18860
|
+
}
|
|
18861
|
+
return this.#matrix;
|
|
18862
|
+
}
|
|
18863
|
+
setDirty() {
|
|
18864
|
+
this.#isDirty = true;
|
|
18824
18865
|
}
|
|
18825
|
-
|
|
18826
|
-
let msdfColor = vec4f(text.color.rgb, text.color.a * alpha);
|
|
18827
|
-
return msdfColor;
|
|
18828
|
-
|
|
18829
|
-
// Debug options:
|
|
18830
|
-
// return text.color;
|
|
18831
|
-
// return input.debugColor;
|
|
18832
|
-
// return vec4f(1, 0, 1, 1); // hardcoded magenta
|
|
18833
|
-
// return textureSample(fontTexture, fontSampler, input.texcoord);
|
|
18834
18866
|
}
|
|
18835
|
-
`;
|
|
18836
18867
|
|
|
18837
|
-
// src/
|
|
18838
|
-
|
|
18839
|
-
|
|
18840
|
-
|
|
18841
|
-
|
|
18842
|
-
|
|
18843
|
-
|
|
18844
|
-
|
|
18845
|
-
|
|
18846
|
-
|
|
18847
|
-
|
|
18868
|
+
// src/scene/QuadNode.ts
|
|
18869
|
+
var PRIMITIVE_TEXTURE = "__primitive__";
|
|
18870
|
+
var RESERVED_PRIMITIVE_INDEX_START = 1000;
|
|
18871
|
+
var CIRCLE_INDEX = 1001;
|
|
18872
|
+
var DEFAULT_REGION = {
|
|
18873
|
+
x: 0,
|
|
18874
|
+
y: 0,
|
|
18875
|
+
width: 0,
|
|
18876
|
+
height: 0
|
|
18877
|
+
};
|
|
18878
|
+
|
|
18879
|
+
class QuadNode extends SceneNode {
|
|
18880
|
+
assetManager;
|
|
18881
|
+
#color;
|
|
18882
|
+
#atlasCoords;
|
|
18883
|
+
#region;
|
|
18884
|
+
#matrixPool;
|
|
18885
|
+
#flip;
|
|
18886
|
+
#cropOffset;
|
|
18887
|
+
#cropRatio;
|
|
18888
|
+
#atlasSize;
|
|
18889
|
+
#textureId;
|
|
18890
|
+
#writeInstance;
|
|
18891
|
+
constructor(options, matrixPool) {
|
|
18892
|
+
assert(options.shader, "QuadNode requires a shader to be explicitly provided");
|
|
18893
|
+
assert(options.size, "QuadNode requires a size to be explicitly provided");
|
|
18894
|
+
assert(options.atlasCoords, "QuadNode requires atlas coords to be explicitly provided");
|
|
18895
|
+
options.render ??= {
|
|
18896
|
+
shader: options.shader,
|
|
18897
|
+
writeInstance: writeQuadInstance
|
|
18898
|
+
};
|
|
18899
|
+
super(options);
|
|
18900
|
+
assert(options.assetManager, "QuadNode requires an asset manager");
|
|
18901
|
+
this.assetManager = options.assetManager;
|
|
18902
|
+
if (options.atlasCoords && options.atlasCoords.atlasIndex >= RESERVED_PRIMITIVE_INDEX_START) {
|
|
18903
|
+
this.#textureId = PRIMITIVE_TEXTURE;
|
|
18904
|
+
this.#region = DEFAULT_REGION;
|
|
18905
|
+
this.#atlasSize = DEFAULT_REGION;
|
|
18906
|
+
} else {
|
|
18907
|
+
assert(options.textureId, "QuadNode requires texture id to be explicitly provided");
|
|
18908
|
+
this.#textureId = options.textureId;
|
|
18909
|
+
assert(options.region, "QuadNode requires a region to be explicitly provided");
|
|
18910
|
+
this.#region = options.region;
|
|
18911
|
+
assert(options.atlasSize, "QuadNode requires atlas size to be explicitly provided");
|
|
18912
|
+
this.#atlasSize = options.atlasSize;
|
|
18913
|
+
}
|
|
18914
|
+
this.#atlasCoords = options.atlasCoords;
|
|
18915
|
+
this.#color = options.color ?? { r: 1, g: 1, b: 1, a: 1 };
|
|
18916
|
+
this.#matrixPool = matrixPool;
|
|
18917
|
+
this.#flip = { x: options.flipX ? -1 : 1, y: options.flipY ? -1 : 1 };
|
|
18918
|
+
this.#cropOffset = options.cropOffset ?? { x: 0, y: 0 };
|
|
18919
|
+
this.#cropRatio = !this.#atlasCoords.uvScaleCropped ? { width: 1, height: 1 } : {
|
|
18920
|
+
width: this.#atlasCoords.uvScaleCropped.width / this.#atlasCoords.uvScale.width,
|
|
18921
|
+
height: this.#atlasCoords.uvScaleCropped.height / this.#atlasCoords.uvScale.height
|
|
18922
|
+
};
|
|
18923
|
+
this.#writeInstance = options.writeInstance;
|
|
18924
|
+
}
|
|
18925
|
+
get color() {
|
|
18926
|
+
return this.#color;
|
|
18927
|
+
}
|
|
18928
|
+
set color(value) {
|
|
18929
|
+
this.#color = value;
|
|
18930
|
+
}
|
|
18931
|
+
get size() {
|
|
18932
|
+
const size = super.size;
|
|
18933
|
+
if (!size) {
|
|
18934
|
+
throw new Error("QuadNode requires a size");
|
|
18935
|
+
}
|
|
18936
|
+
return size;
|
|
18937
|
+
}
|
|
18938
|
+
set size(val) {
|
|
18939
|
+
super.size = val;
|
|
18940
|
+
}
|
|
18941
|
+
get matrixWithSize() {
|
|
18942
|
+
const matrix = mat3.clone(this.matrix, this.#matrixPool.get());
|
|
18943
|
+
mat3.scale(matrix, [this.size.width * this.#flip.x, this.size.height * this.#flip.y], matrix);
|
|
18944
|
+
return matrix;
|
|
18945
|
+
}
|
|
18946
|
+
get atlasCoords() {
|
|
18947
|
+
return this.#atlasCoords;
|
|
18948
|
+
}
|
|
18949
|
+
get region() {
|
|
18950
|
+
return this.#region;
|
|
18951
|
+
}
|
|
18952
|
+
get writeInstance() {
|
|
18953
|
+
return this.#writeInstance;
|
|
18954
|
+
}
|
|
18955
|
+
get flipX() {
|
|
18956
|
+
return this.#flip.x === -1;
|
|
18957
|
+
}
|
|
18958
|
+
set flipX(value) {
|
|
18959
|
+
this.#flip.x = value ? -1 : 1;
|
|
18960
|
+
this.setDirty();
|
|
18961
|
+
}
|
|
18962
|
+
get flipY() {
|
|
18963
|
+
return this.#flip.y === -1;
|
|
18848
18964
|
}
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
label: `MSDF font ${font.name}`,
|
|
18853
|
-
size: [font.imageBitmap.width, font.imageBitmap.height, 1],
|
|
18854
|
-
format: "rgba8unorm",
|
|
18855
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
18856
|
-
});
|
|
18857
|
-
device.queue.copyExternalImageToTexture({ source: font.imageBitmap }, { texture }, [font.imageBitmap.width, font.imageBitmap.height]);
|
|
18858
|
-
const charsGpuBuffer = device.createBuffer({
|
|
18859
|
-
label: `MSDF font ${font.name} character layout buffer`,
|
|
18860
|
-
size: font.charCount * Float32Array.BYTES_PER_ELEMENT * 8,
|
|
18861
|
-
usage: GPUBufferUsage.STORAGE,
|
|
18862
|
-
mappedAtCreation: true
|
|
18863
|
-
});
|
|
18864
|
-
const charsArray = new Float32Array(charsGpuBuffer.getMappedRange());
|
|
18865
|
-
charsArray.set(font.charBuffer, 0);
|
|
18866
|
-
charsGpuBuffer.unmap();
|
|
18867
|
-
const fontDataBuffer = device.createBuffer({
|
|
18868
|
-
label: `MSDF font ${font.name} metadata buffer`,
|
|
18869
|
-
size: Float32Array.BYTES_PER_ELEMENT * 4,
|
|
18870
|
-
usage: GPUBufferUsage.UNIFORM,
|
|
18871
|
-
mappedAtCreation: true
|
|
18872
|
-
});
|
|
18873
|
-
const fontDataArray = new Float32Array(fontDataBuffer.getMappedRange());
|
|
18874
|
-
fontDataArray[0] = font.lineHeight;
|
|
18875
|
-
fontDataBuffer.unmap();
|
|
18876
|
-
const fontBindGroup = device.createBindGroup({
|
|
18877
|
-
layout: pipeline.getBindGroupLayout(0),
|
|
18878
|
-
entries: [
|
|
18879
|
-
{
|
|
18880
|
-
binding: 0,
|
|
18881
|
-
resource: texture.createView()
|
|
18882
|
-
},
|
|
18883
|
-
{
|
|
18884
|
-
binding: 1,
|
|
18885
|
-
resource: device.createSampler(sampler)
|
|
18886
|
-
},
|
|
18887
|
-
{
|
|
18888
|
-
binding: 2,
|
|
18889
|
-
resource: {
|
|
18890
|
-
buffer: charsGpuBuffer
|
|
18891
|
-
}
|
|
18892
|
-
},
|
|
18893
|
-
{
|
|
18894
|
-
binding: 3,
|
|
18895
|
-
resource: {
|
|
18896
|
-
buffer: fontDataBuffer
|
|
18897
|
-
}
|
|
18898
|
-
}
|
|
18899
|
-
]
|
|
18900
|
-
});
|
|
18901
|
-
return new FontPipeline(pipeline, font, fontBindGroup, maxCharCount);
|
|
18965
|
+
set flipY(value) {
|
|
18966
|
+
this.#flip.y = value ? -1 : 1;
|
|
18967
|
+
this.setDirty();
|
|
18902
18968
|
}
|
|
18903
|
-
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
}
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
18917
|
-
|
|
18918
|
-
|
|
18919
|
-
|
|
18920
|
-
|
|
18969
|
+
get cropOffset() {
|
|
18970
|
+
return this.#cropOffset;
|
|
18971
|
+
}
|
|
18972
|
+
set cropOffset(value) {
|
|
18973
|
+
this.#cropOffset = value;
|
|
18974
|
+
}
|
|
18975
|
+
get textureId() {
|
|
18976
|
+
return this.#textureId;
|
|
18977
|
+
}
|
|
18978
|
+
get isPrimitive() {
|
|
18979
|
+
return this.#textureId === PRIMITIVE_TEXTURE;
|
|
18980
|
+
}
|
|
18981
|
+
get isCircle() {
|
|
18982
|
+
return this.#atlasCoords.atlasIndex === CIRCLE_INDEX;
|
|
18983
|
+
}
|
|
18984
|
+
extra = {
|
|
18985
|
+
setAtlasCoords: (value) => {
|
|
18986
|
+
this.#atlasCoords = value;
|
|
18921
18987
|
},
|
|
18922
|
-
|
|
18923
|
-
|
|
18924
|
-
entryPoint: "fragmentMain",
|
|
18925
|
-
targets: [
|
|
18926
|
-
{
|
|
18927
|
-
format: colorFormat,
|
|
18928
|
-
blend: {
|
|
18929
|
-
color: {
|
|
18930
|
-
srcFactor: "src-alpha",
|
|
18931
|
-
dstFactor: "one-minus-src-alpha"
|
|
18932
|
-
},
|
|
18933
|
-
alpha: {
|
|
18934
|
-
srcFactor: "one",
|
|
18935
|
-
dstFactor: "one"
|
|
18936
|
-
}
|
|
18937
|
-
}
|
|
18938
|
-
}
|
|
18939
|
-
]
|
|
18988
|
+
cropRatio: () => {
|
|
18989
|
+
return this.#cropRatio;
|
|
18940
18990
|
},
|
|
18941
|
-
|
|
18942
|
-
|
|
18943
|
-
stripIndexFormat: "uint32"
|
|
18991
|
+
atlasSize: () => {
|
|
18992
|
+
return this.#atlasSize;
|
|
18944
18993
|
}
|
|
18945
|
-
});
|
|
18946
|
-
}
|
|
18947
|
-
if (typeof GPUShaderStage === "undefined") {
|
|
18948
|
-
globalThis.GPUShaderStage = {
|
|
18949
|
-
VERTEX: 1,
|
|
18950
|
-
FRAGMENT: 2,
|
|
18951
|
-
COMPUTE: 4
|
|
18952
18994
|
};
|
|
18953
18995
|
}
|
|
18954
|
-
|
|
18955
|
-
|
|
18956
|
-
|
|
18957
|
-
|
|
18958
|
-
|
|
18959
|
-
|
|
18960
|
-
|
|
18961
|
-
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
|
|
18965
|
-
|
|
18966
|
-
|
|
18967
|
-
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
18971
|
-
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
|
|
18996
|
+
function writeQuadInstance(node, array, offset) {
|
|
18997
|
+
if (!(node instanceof QuadNode)) {
|
|
18998
|
+
throw new Error("QuadNode.writeInstance can only be called on QuadNodes");
|
|
18999
|
+
}
|
|
19000
|
+
array.set(node.matrixWithSize, offset);
|
|
19001
|
+
array.set([node.color.r, node.color.g, node.color.b, node.color.a], offset + 12);
|
|
19002
|
+
const region = node.region;
|
|
19003
|
+
if (node.textureId === PRIMITIVE_TEXTURE) {
|
|
19004
|
+
array.set([
|
|
19005
|
+
node.atlasCoords.uvOffset.x,
|
|
19006
|
+
node.atlasCoords.uvOffset.y,
|
|
19007
|
+
node.atlasCoords.uvScale.width,
|
|
19008
|
+
node.atlasCoords.uvScale.height
|
|
19009
|
+
], offset + 16);
|
|
19010
|
+
} else {
|
|
19011
|
+
const atlasSize = node.extra.atlasSize();
|
|
19012
|
+
array.set([
|
|
19013
|
+
node.atlasCoords.uvOffset.x + region.x / atlasSize.width,
|
|
19014
|
+
node.atlasCoords.uvOffset.y + region.y / atlasSize.height,
|
|
19015
|
+
region.width / atlasSize.width,
|
|
19016
|
+
region.height / atlasSize.height
|
|
19017
|
+
], offset + 16);
|
|
19018
|
+
}
|
|
19019
|
+
array.set([
|
|
19020
|
+
node.cropOffset.x / 2 / (node.atlasCoords.originalSize.width || 1),
|
|
19021
|
+
node.cropOffset.y / 2 / (node.atlasCoords.originalSize.height || 1),
|
|
19022
|
+
node.extra.cropRatio().width,
|
|
19023
|
+
node.extra.cropRatio().height
|
|
19024
|
+
], offset + 20);
|
|
19025
|
+
new DataView(array.buffer).setUint32(array.byteOffset + (offset + 24) * Float32Array.BYTES_PER_ELEMENT, node.atlasCoords.atlasIndex, true);
|
|
19026
|
+
node.writeInstance?.(array, offset + 28);
|
|
19027
|
+
return 1;
|
|
19028
|
+
}
|
|
19029
|
+
|
|
19030
|
+
// src/scene/JumboQuadNode.ts
|
|
19031
|
+
var MAT3_SIZE = 12;
|
|
19032
|
+
var VEC4F_SIZE = 4;
|
|
19033
|
+
|
|
19034
|
+
class JumboQuadNode extends QuadNode {
|
|
19035
|
+
#tiles;
|
|
19036
|
+
#matrixPool;
|
|
19037
|
+
constructor(options, matrixPool) {
|
|
19038
|
+
assert(options.shader, "JumboQuadNode requires a shader to be explicitly provided");
|
|
19039
|
+
assert(options.tiles && options.tiles.length > 0, "JumboQuadNode requires at least one tile to be provided");
|
|
19040
|
+
options.render ??= {
|
|
19041
|
+
shader: options.shader,
|
|
19042
|
+
writeInstance: writeJumboQuadInstance
|
|
19043
|
+
};
|
|
19044
|
+
super({
|
|
19045
|
+
...options,
|
|
19046
|
+
atlasCoords: options.tiles[0].atlasCoords
|
|
19047
|
+
}, matrixPool);
|
|
19048
|
+
this.#matrixPool = matrixPool;
|
|
19049
|
+
this.#tiles = [];
|
|
19050
|
+
for (const tile of options.tiles) {
|
|
19051
|
+
assert(tile.atlasCoords, "JumboQuadNode requires atlas coords to be provided");
|
|
19052
|
+
assert(tile.size, "JumboQuadNode requires a size to be provided");
|
|
19053
|
+
this.#tiles.push({
|
|
19054
|
+
textureId: tile.textureId,
|
|
19055
|
+
offset: tile.offset,
|
|
19056
|
+
size: tile.size,
|
|
19057
|
+
atlasCoords: tile.atlasCoords
|
|
19058
|
+
});
|
|
18976
19059
|
}
|
|
18977
|
-
|
|
19060
|
+
}
|
|
19061
|
+
get atlasCoords() {
|
|
19062
|
+
throw new Error("JumboQuadNode does not have a single atlas coords");
|
|
19063
|
+
}
|
|
19064
|
+
get tiles() {
|
|
19065
|
+
return this.#tiles;
|
|
19066
|
+
}
|
|
19067
|
+
getTileMatrix(tile) {
|
|
19068
|
+
const matrix = mat3.clone(this.matrix, this.#matrixPool.get());
|
|
19069
|
+
const originalSize = {
|
|
19070
|
+
width: Math.max(...this.#tiles.map((t3) => t3.offset.x + t3.size.width)),
|
|
19071
|
+
height: Math.max(...this.#tiles.map((t3) => t3.offset.y + t3.size.height))
|
|
19072
|
+
};
|
|
19073
|
+
const proportionalSize = {
|
|
19074
|
+
width: this.size.width / originalSize.width,
|
|
19075
|
+
height: this.size.height / originalSize.height
|
|
19076
|
+
};
|
|
19077
|
+
const centerOffset = {
|
|
19078
|
+
x: tile.offset.x + tile.size.width / 2 - originalSize.width / 2,
|
|
19079
|
+
y: -(tile.offset.y + tile.size.height / 2 - originalSize.height / 2)
|
|
19080
|
+
};
|
|
19081
|
+
mat3.translate(matrix, [
|
|
19082
|
+
centerOffset.x * proportionalSize.width,
|
|
19083
|
+
centerOffset.y * proportionalSize.height
|
|
19084
|
+
], matrix);
|
|
19085
|
+
mat3.scale(matrix, [
|
|
19086
|
+
tile.size.width * proportionalSize.width * (this.flipX ? -1 : 1),
|
|
19087
|
+
tile.size.height * proportionalSize.height * (this.flipY ? -1 : 1)
|
|
19088
|
+
], matrix);
|
|
19089
|
+
return matrix;
|
|
19090
|
+
}
|
|
19091
|
+
}
|
|
19092
|
+
function writeJumboQuadInstance(node, array, offset) {
|
|
19093
|
+
if (!(node instanceof JumboQuadNode)) {
|
|
19094
|
+
throw new Error("JumboQuadNode.writeJumboQuadInstance can only be called on JumboQuadNodes");
|
|
19095
|
+
}
|
|
19096
|
+
let tileOffset = 0;
|
|
19097
|
+
for (const tile of node.tiles) {
|
|
19098
|
+
const coord = tile.atlasCoords;
|
|
19099
|
+
const matrix = node.getTileMatrix(tile);
|
|
19100
|
+
array.set(matrix, offset + tileOffset);
|
|
19101
|
+
tileOffset += MAT3_SIZE;
|
|
19102
|
+
array.set([node.color.r, node.color.g, node.color.b, node.color.a], offset + tileOffset);
|
|
19103
|
+
tileOffset += VEC4F_SIZE;
|
|
19104
|
+
array.set([
|
|
19105
|
+
coord.uvOffset.x,
|
|
19106
|
+
coord.uvOffset.y,
|
|
19107
|
+
coord.uvScale.width,
|
|
19108
|
+
coord.uvScale.height
|
|
19109
|
+
], offset + tileOffset);
|
|
19110
|
+
tileOffset += VEC4F_SIZE;
|
|
19111
|
+
const cropRatio = !coord.uvScaleCropped ? { width: 1, height: 1 } : {
|
|
19112
|
+
width: coord.uvScaleCropped.width / coord.uvScale.width,
|
|
19113
|
+
height: coord.uvScaleCropped.height / coord.uvScale.height
|
|
19114
|
+
};
|
|
19115
|
+
array.set([
|
|
19116
|
+
tile.atlasCoords.cropOffset.x / 2 / (tile.atlasCoords.originalSize.width || 1),
|
|
19117
|
+
tile.atlasCoords.cropOffset.y / 2 / (tile.atlasCoords.originalSize.height || 1),
|
|
19118
|
+
cropRatio.width,
|
|
19119
|
+
cropRatio.height
|
|
19120
|
+
], offset + tileOffset);
|
|
19121
|
+
tileOffset += VEC4F_SIZE;
|
|
19122
|
+
new DataView(array.buffer).setUint32(array.byteOffset + (offset + tileOffset) * Float32Array.BYTES_PER_ELEMENT, coord.atlasIndex, true);
|
|
19123
|
+
tileOffset += VEC4F_SIZE;
|
|
19124
|
+
}
|
|
19125
|
+
node.writeInstance?.(array, offset + tileOffset);
|
|
19126
|
+
return node.tiles.length;
|
|
19127
|
+
}
|
|
19128
|
+
|
|
19129
|
+
// src/backends/webgpu/wgsl/pixel-scraping.wgsl.ts
|
|
19130
|
+
var pixel_scraping_wgsl_default = `
|
|
19131
|
+
// ==============================
|
|
19132
|
+
// === BOUNDING BOX PASS =======
|
|
19133
|
+
// ==============================
|
|
19134
|
+
|
|
19135
|
+
// Input texture from which to compute the non-transparent bounding box
|
|
19136
|
+
@group(0) @binding(0)
|
|
19137
|
+
var input_texture: texture_2d<f32>;
|
|
19138
|
+
|
|
19139
|
+
// Atomic bounding box storage structure
|
|
19140
|
+
struct bounding_box_atomic {
|
|
19141
|
+
min_x: atomic<u32>,
|
|
19142
|
+
min_y: atomic<u32>,
|
|
19143
|
+
max_x: atomic<u32>,
|
|
19144
|
+
max_y: atomic<u32>,
|
|
18978
19145
|
};
|
|
18979
|
-
|
|
18980
|
-
|
|
18981
|
-
|
|
18982
|
-
|
|
18983
|
-
|
|
18984
|
-
|
|
18985
|
-
|
|
19146
|
+
|
|
19147
|
+
// Storage buffer to hold atomic bounding box updates
|
|
19148
|
+
@group(0) @binding(1)
|
|
19149
|
+
var<storage, read_write> bounds: bounding_box_atomic;
|
|
19150
|
+
|
|
19151
|
+
// Compute shader to find the bounding box of non-transparent pixels
|
|
19152
|
+
@compute @workgroup_size(8, 8)
|
|
19153
|
+
fn find_bounds(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
19154
|
+
let size = textureDimensions(input_texture).xy;
|
|
19155
|
+
if (gid.x >= size.x || gid.y >= size.y) {
|
|
19156
|
+
return;
|
|
18986
19157
|
}
|
|
18987
|
-
|
|
19158
|
+
|
|
19159
|
+
let pixel = textureLoad(input_texture, vec2<i32>(gid.xy), 0);
|
|
19160
|
+
if (pixel.a > 0.0) {
|
|
19161
|
+
atomicMin(&bounds.min_x, gid.x);
|
|
19162
|
+
atomicMin(&bounds.min_y, gid.y);
|
|
19163
|
+
atomicMax(&bounds.max_x, gid.x);
|
|
19164
|
+
atomicMax(&bounds.max_y, gid.y);
|
|
19165
|
+
}
|
|
19166
|
+
}
|
|
19167
|
+
|
|
19168
|
+
// ==============================
|
|
19169
|
+
// === CROP + OUTPUT PASS ======
|
|
19170
|
+
// ==============================
|
|
19171
|
+
|
|
19172
|
+
// Input texture from which cropped data is read
|
|
19173
|
+
@group(0) @binding(0)
|
|
19174
|
+
var input_texture_crop: texture_2d<f32>;
|
|
19175
|
+
|
|
19176
|
+
// Output texture where cropped image is written
|
|
19177
|
+
@group(0) @binding(1)
|
|
19178
|
+
var output_texture: texture_storage_2d<rgba8unorm, write>;
|
|
19179
|
+
|
|
19180
|
+
// Bounding box passed in as a uniform (not atomic anymore)
|
|
19181
|
+
struct bounding_box_uniform {
|
|
19182
|
+
min_x: u32,
|
|
19183
|
+
min_y: u32,
|
|
19184
|
+
max_x: u32,
|
|
19185
|
+
max_y: u32,
|
|
18988
19186
|
};
|
|
18989
|
-
|
|
18990
|
-
|
|
18991
|
-
|
|
18992
|
-
|
|
18993
|
-
|
|
18994
|
-
|
|
19187
|
+
|
|
19188
|
+
@group(0) @binding(2)
|
|
19189
|
+
var<uniform> bounds_uniform: bounding_box_uniform;
|
|
19190
|
+
|
|
19191
|
+
// Struct to store both original and cropped texture dimensions
|
|
19192
|
+
struct image_dimensions {
|
|
19193
|
+
original_width: u32,
|
|
19194
|
+
original_height: u32,
|
|
19195
|
+
cropped_width: u32,
|
|
19196
|
+
cropped_height: u32,
|
|
18995
19197
|
};
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
|
|
19002
|
-
|
|
19003
|
-
|
|
19004
|
-
|
|
19005
|
-
|
|
19006
|
-
|
|
19007
|
-
|
|
19198
|
+
|
|
19199
|
+
// Storage buffer to output the result dimensions
|
|
19200
|
+
@group(0) @binding(3)
|
|
19201
|
+
var<storage, read_write> dimensions_out: image_dimensions;
|
|
19202
|
+
|
|
19203
|
+
// Compute shader to crop the input texture to the bounding box and output it
|
|
19204
|
+
@compute @workgroup_size(8, 8)
|
|
19205
|
+
fn crop_and_output(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
19206
|
+
let size = textureDimensions(input_texture_crop).xy;
|
|
19207
|
+
|
|
19208
|
+
let crop_width = bounds_uniform.max_x - bounds_uniform.min_x + 1u;
|
|
19209
|
+
let crop_height = bounds_uniform.max_y - bounds_uniform.min_y + 1u;
|
|
19210
|
+
|
|
19211
|
+
if (gid.x >= crop_width || gid.y >= crop_height) {
|
|
19212
|
+
return;
|
|
19008
19213
|
}
|
|
19009
|
-
]
|
|
19010
|
-
};
|
|
19011
19214
|
|
|
19012
|
-
|
|
19013
|
-
|
|
19014
|
-
|
|
19015
|
-
|
|
19016
|
-
|
|
19215
|
+
let src_coord = vec2<i32>(
|
|
19216
|
+
i32(bounds_uniform.min_x + gid.x),
|
|
19217
|
+
i32(bounds_uniform.min_y + gid.y)
|
|
19218
|
+
);
|
|
19219
|
+
|
|
19220
|
+
let dst_coord = vec2<i32>(i32(gid.x), i32(gid.y));
|
|
19221
|
+
let pixel = textureLoad(input_texture_crop, src_coord, 0);
|
|
19222
|
+
textureStore(output_texture, dst_coord, pixel);
|
|
19223
|
+
|
|
19224
|
+
// Output dimensions from workgroup (0,0) only
|
|
19225
|
+
if (gid.x == 0u && gid.y == 0u) {
|
|
19226
|
+
dimensions_out.original_width = size.x;
|
|
19227
|
+
dimensions_out.original_height = size.y;
|
|
19228
|
+
dimensions_out.cropped_width = crop_width;
|
|
19229
|
+
dimensions_out.cropped_height = crop_height;
|
|
19230
|
+
}
|
|
19017
19231
|
}
|
|
19018
|
-
var textDescriptorInstanceSize = struct.size;
|
|
19019
19232
|
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
19025
|
-
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
|
|
19033
|
-
|
|
19034
|
-
|
|
19035
|
-
|
|
19036
|
-
|
|
19037
|
-
|
|
19038
|
-
|
|
19039
|
-
|
|
19040
|
-
|
|
19041
|
-
|
|
19042
|
-
|
|
19043
|
-
|
|
19233
|
+
// ==============================
|
|
19234
|
+
// === MISSING TEXTURE FILL ====
|
|
19235
|
+
// ==============================
|
|
19236
|
+
|
|
19237
|
+
// Output texture to draw a fallback checkerboard
|
|
19238
|
+
@group(0) @binding(0)
|
|
19239
|
+
var checker_texture: texture_storage_2d<rgba8unorm, write>;
|
|
19240
|
+
|
|
19241
|
+
// Compute shader to fill a texture with a purple & green checkerboard
|
|
19242
|
+
@compute @workgroup_size(8, 8)
|
|
19243
|
+
fn missing_texture(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
19244
|
+
let size = textureDimensions(checker_texture);
|
|
19245
|
+
if (id.x >= size.x || id.y >= size.y) {
|
|
19246
|
+
return;
|
|
19247
|
+
}
|
|
19248
|
+
|
|
19249
|
+
let checker_size = 25u;
|
|
19250
|
+
let on_color = ((id.x / checker_size + id.y / checker_size) % 2u) == 0u;
|
|
19251
|
+
|
|
19252
|
+
let color = select(
|
|
19253
|
+
vec4<f32>(0.5, 0.0, 0.5, 1.0), // Purple
|
|
19254
|
+
vec4<f32>(0.0, 1.0, 0.0, 1.0), // Green
|
|
19255
|
+
on_color
|
|
19256
|
+
);
|
|
19257
|
+
|
|
19258
|
+
textureStore(checker_texture, vec2<i32>(id.xy), color);
|
|
19259
|
+
}
|
|
19260
|
+
`;
|
|
19261
|
+
|
|
19262
|
+
// src/backends/webgpu/TextureComputeShader.ts
|
|
19263
|
+
var BOUNDING_BOX_SIZE = 4 * Uint32Array.BYTES_PER_ELEMENT;
|
|
19264
|
+
var WORKGROUP_SIZE = 8;
|
|
19265
|
+
var MAX_BOUND = 4294967295;
|
|
19266
|
+
var MIN_BOUND = 0;
|
|
19267
|
+
|
|
19268
|
+
class TextureComputeShader {
|
|
19269
|
+
#device;
|
|
19270
|
+
#boundingBuffer;
|
|
19271
|
+
#cropPipeline;
|
|
19272
|
+
#boundPipeline;
|
|
19273
|
+
#missingTexturePipeline;
|
|
19274
|
+
constructor(device, cropPipeline, boundPipeline, missingTexturePipeline) {
|
|
19275
|
+
this.#device = device;
|
|
19276
|
+
this.#boundPipeline = boundPipeline;
|
|
19277
|
+
this.#cropPipeline = cropPipeline;
|
|
19278
|
+
this.#missingTexturePipeline = missingTexturePipeline;
|
|
19279
|
+
this.#boundingBuffer = this.#device.createBuffer({
|
|
19280
|
+
size: BOUNDING_BOX_SIZE,
|
|
19281
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
|
|
19282
|
+
});
|
|
19283
|
+
}
|
|
19284
|
+
static create(device) {
|
|
19285
|
+
const pipelines = createPipelines(device, "TextureComputeShader");
|
|
19286
|
+
return new TextureComputeShader(device, pipelines.cropPipeline, pipelines.boundPipeline, pipelines.missingTexturePipeline);
|
|
19287
|
+
}
|
|
19288
|
+
async processTexture(textureWrapper) {
|
|
19289
|
+
const boundsBindGroup = this.#boundsBindGroup(textureWrapper.texture);
|
|
19290
|
+
const commandEncoder = this.#device.createCommandEncoder();
|
|
19291
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
19292
|
+
const dispatchX = Math.ceil(textureWrapper.texture.width / WORKGROUP_SIZE);
|
|
19293
|
+
const dispatchY = Math.ceil(textureWrapper.texture.height / WORKGROUP_SIZE);
|
|
19294
|
+
const boundsInit = new Uint32Array([
|
|
19295
|
+
MAX_BOUND,
|
|
19296
|
+
MAX_BOUND,
|
|
19297
|
+
MIN_BOUND,
|
|
19298
|
+
MIN_BOUND
|
|
19299
|
+
]);
|
|
19300
|
+
this.#device.queue.writeBuffer(this.#boundingBuffer, 0, boundsInit.buffer, 0, BOUNDING_BOX_SIZE);
|
|
19301
|
+
passEncoder.setPipeline(this.#boundPipeline);
|
|
19302
|
+
passEncoder.setBindGroup(0, boundsBindGroup);
|
|
19303
|
+
passEncoder.dispatchWorkgroups(dispatchX, dispatchY);
|
|
19304
|
+
passEncoder.end();
|
|
19305
|
+
this.#device.queue.submit([commandEncoder.finish()]);
|
|
19306
|
+
const { texelX, texelY, texelWidth, texelHeight, computeBuffer } = await this.#getBoundingBox();
|
|
19307
|
+
if (texelX === MAX_BOUND || texelY === MAX_BOUND) {
|
|
19308
|
+
return await this.#createMissingTexture(textureWrapper.texture);
|
|
19309
|
+
}
|
|
19310
|
+
const croppedTexture = await this.#cropTexture(texelWidth, texelHeight, computeBuffer, textureWrapper.texture);
|
|
19311
|
+
const leftCrop = texelX;
|
|
19312
|
+
const rightCrop = textureWrapper.originalSize.width - texelX - texelWidth;
|
|
19313
|
+
const topCrop = texelY;
|
|
19314
|
+
const bottomCrop = textureWrapper.originalSize.height - texelY - texelHeight;
|
|
19315
|
+
textureWrapper = {
|
|
19316
|
+
texture: croppedTexture,
|
|
19317
|
+
cropOffset: { x: leftCrop - rightCrop, y: bottomCrop - topCrop },
|
|
19318
|
+
originalSize: textureWrapper.originalSize
|
|
19319
|
+
};
|
|
19320
|
+
return textureWrapper;
|
|
19321
|
+
}
|
|
19322
|
+
async#getBoundingBox() {
|
|
19323
|
+
const readBuffer = this.#device.createBuffer({
|
|
19324
|
+
label: "AABB Compute Buffer",
|
|
19325
|
+
size: BOUNDING_BOX_SIZE,
|
|
19326
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
19044
19327
|
});
|
|
19045
|
-
|
|
19046
|
-
this.#
|
|
19047
|
-
this.#
|
|
19048
|
-
|
|
19049
|
-
|
|
19328
|
+
const copyEncoder = this.#device.createCommandEncoder();
|
|
19329
|
+
copyEncoder.copyBufferToBuffer(this.#boundingBuffer, 0, readBuffer, 0, BOUNDING_BOX_SIZE);
|
|
19330
|
+
this.#device.queue.submit([copyEncoder.finish()]);
|
|
19331
|
+
await readBuffer.mapAsync(GPUMapMode.READ);
|
|
19332
|
+
const computeBuffer = new Uint32Array(readBuffer.getMappedRange().slice(0));
|
|
19333
|
+
readBuffer.unmap();
|
|
19334
|
+
const [minX, minY, maxX, maxY] = computeBuffer;
|
|
19335
|
+
return {
|
|
19336
|
+
texelX: minX,
|
|
19337
|
+
texelY: minY,
|
|
19338
|
+
texelWidth: maxX - minX + 1,
|
|
19339
|
+
texelHeight: maxY - minY + 1,
|
|
19340
|
+
computeBuffer
|
|
19341
|
+
};
|
|
19342
|
+
}
|
|
19343
|
+
async#cropTexture(croppedWidth, croppedHeight, computeBuffer, inputTexture) {
|
|
19344
|
+
const boundsUniform = this.#device.createBuffer({
|
|
19345
|
+
label: "Cropping Bounds Uniform Buffer",
|
|
19346
|
+
size: BOUNDING_BOX_SIZE,
|
|
19050
19347
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
19051
19348
|
});
|
|
19052
|
-
this.#
|
|
19053
|
-
|
|
19054
|
-
|
|
19055
|
-
|
|
19349
|
+
this.#device.queue.writeBuffer(boundsUniform, 0, computeBuffer);
|
|
19350
|
+
const outputTexture = this.#device.createTexture({
|
|
19351
|
+
label: "Cropped Texture",
|
|
19352
|
+
size: [croppedWidth, croppedHeight],
|
|
19353
|
+
format: "rgba8unorm",
|
|
19354
|
+
usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
|
|
19056
19355
|
});
|
|
19057
|
-
this.#
|
|
19058
|
-
|
|
19059
|
-
|
|
19060
|
-
|
|
19356
|
+
const dimensionsOutBuffer = this.#device.createBuffer({
|
|
19357
|
+
label: "Cropping Dimensions Output Buffer",
|
|
19358
|
+
size: BOUNDING_BOX_SIZE,
|
|
19359
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
19360
|
+
});
|
|
19361
|
+
const bindGroup = this.#croppingBindGroup(inputTexture, outputTexture, boundsUniform, dimensionsOutBuffer);
|
|
19362
|
+
const encoder = this.#device.createCommandEncoder();
|
|
19363
|
+
const pass = encoder.beginComputePass();
|
|
19364
|
+
pass.setPipeline(this.#cropPipeline);
|
|
19365
|
+
pass.setBindGroup(0, bindGroup);
|
|
19366
|
+
pass.dispatchWorkgroups(Math.ceil(croppedWidth / WORKGROUP_SIZE), Math.ceil(croppedHeight / WORKGROUP_SIZE));
|
|
19367
|
+
pass.end();
|
|
19368
|
+
this.#device.queue.submit([encoder.finish()]);
|
|
19369
|
+
return outputTexture;
|
|
19370
|
+
}
|
|
19371
|
+
async#createMissingTexture(inputTexture) {
|
|
19372
|
+
const placeholder = this.#device.createTexture({
|
|
19373
|
+
label: "Missing Placeholder Texture",
|
|
19374
|
+
size: [inputTexture.width, inputTexture.height],
|
|
19375
|
+
format: "rgba8unorm",
|
|
19376
|
+
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING
|
|
19377
|
+
});
|
|
19378
|
+
const encoder = this.#device.createCommandEncoder();
|
|
19379
|
+
const pass = encoder.beginComputePass();
|
|
19380
|
+
pass.setPipeline(this.#missingTexturePipeline);
|
|
19381
|
+
pass.setBindGroup(0, this.#missingTextureBindGroup(placeholder));
|
|
19382
|
+
pass.dispatchWorkgroups(placeholder.width / 8, placeholder.height / 8);
|
|
19383
|
+
pass.end();
|
|
19384
|
+
this.#device.queue.submit([encoder.finish()]);
|
|
19385
|
+
return {
|
|
19386
|
+
texture: placeholder,
|
|
19387
|
+
cropOffset: { x: 0, y: 0 },
|
|
19388
|
+
originalSize: { width: inputTexture.width, height: inputTexture.height }
|
|
19389
|
+
};
|
|
19390
|
+
}
|
|
19391
|
+
#boundsBindGroup(inputTexture) {
|
|
19392
|
+
return this.#device.createBindGroup({
|
|
19393
|
+
layout: this.#boundPipeline.getBindGroupLayout(0),
|
|
19061
19394
|
entries: [
|
|
19062
|
-
{
|
|
19063
|
-
|
|
19064
|
-
resource: { buffer: this.#descriptorBuffer }
|
|
19065
|
-
},
|
|
19066
|
-
{
|
|
19067
|
-
binding: 1,
|
|
19068
|
-
resource: { buffer: this.#textBlockBuffer }
|
|
19069
|
-
}
|
|
19395
|
+
{ binding: 0, resource: inputTexture.createView() },
|
|
19396
|
+
{ binding: 1, resource: { buffer: this.#boundingBuffer } }
|
|
19070
19397
|
]
|
|
19071
|
-
})
|
|
19072
|
-
|
|
19073
|
-
|
|
19074
|
-
|
|
19398
|
+
});
|
|
19399
|
+
}
|
|
19400
|
+
#croppingBindGroup(inputTexture, outputTexture, boundsUniform, dimensionsOutBuffer) {
|
|
19401
|
+
return this.#device.createBindGroup({
|
|
19402
|
+
layout: this.#cropPipeline.getBindGroupLayout(0),
|
|
19075
19403
|
entries: [
|
|
19076
|
-
{
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
}
|
|
19404
|
+
{ binding: 0, resource: inputTexture.createView() },
|
|
19405
|
+
{ binding: 1, resource: outputTexture.createView() },
|
|
19406
|
+
{ binding: 2, resource: { buffer: boundsUniform } },
|
|
19407
|
+
{ binding: 3, resource: { buffer: dimensionsOutBuffer } }
|
|
19080
19408
|
]
|
|
19081
19409
|
});
|
|
19082
|
-
this.#bindGroups.push(engineUniformsBindGroup);
|
|
19083
19410
|
}
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
|
|
19087
|
-
|
|
19088
|
-
|
|
19411
|
+
#missingTextureBindGroup(outputTexture) {
|
|
19412
|
+
return this.#device.createBindGroup({
|
|
19413
|
+
layout: this.#missingTexturePipeline.getBindGroupLayout(0),
|
|
19414
|
+
entries: [{ binding: 0, resource: outputTexture.createView() }]
|
|
19415
|
+
});
|
|
19089
19416
|
}
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
|
|
19093
|
-
|
|
19094
|
-
|
|
19095
|
-
|
|
19096
|
-
|
|
19097
|
-
|
|
19098
|
-
|
|
19099
|
-
|
|
19100
|
-
|
|
19101
|
-
|
|
19417
|
+
}
|
|
19418
|
+
function createPipelines(device, label) {
|
|
19419
|
+
const shader = device.createShaderModule({
|
|
19420
|
+
label: `${label} Shader`,
|
|
19421
|
+
code: pixel_scraping_wgsl_default
|
|
19422
|
+
});
|
|
19423
|
+
const findBoundsBindGroupLayout = device.createBindGroupLayout({
|
|
19424
|
+
label: "Bounds Detection Layout",
|
|
19425
|
+
entries: [
|
|
19426
|
+
{
|
|
19427
|
+
binding: 0,
|
|
19428
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
19429
|
+
texture: { sampleType: "float", viewDimension: "2d" }
|
|
19430
|
+
},
|
|
19431
|
+
{
|
|
19432
|
+
binding: 1,
|
|
19433
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
19434
|
+
buffer: { type: "storage" }
|
|
19102
19435
|
}
|
|
19103
|
-
|
|
19104
|
-
|
|
19105
|
-
|
|
19106
|
-
|
|
19107
|
-
|
|
19108
|
-
|
|
19109
|
-
|
|
19110
|
-
|
|
19111
|
-
|
|
19112
|
-
|
|
19113
|
-
|
|
19114
|
-
|
|
19115
|
-
|
|
19116
|
-
|
|
19117
|
-
|
|
19118
|
-
|
|
19119
|
-
|
|
19120
|
-
|
|
19121
|
-
|
|
19122
|
-
|
|
19123
|
-
|
|
19124
|
-
|
|
19125
|
-
|
|
19126
|
-
|
|
19127
|
-
|
|
19128
|
-
|
|
19129
|
-
}
|
|
19130
|
-
|
|
19131
|
-
|
|
19132
|
-
|
|
19436
|
+
]
|
|
19437
|
+
});
|
|
19438
|
+
const cropBindGroupLayout = device.createBindGroupLayout({
|
|
19439
|
+
label: "Cropping Layout",
|
|
19440
|
+
entries: [
|
|
19441
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, texture: {} },
|
|
19442
|
+
{
|
|
19443
|
+
binding: 1,
|
|
19444
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
19445
|
+
storageTexture: {
|
|
19446
|
+
access: "write-only",
|
|
19447
|
+
format: "rgba8unorm",
|
|
19448
|
+
viewDimension: "2d"
|
|
19449
|
+
}
|
|
19450
|
+
},
|
|
19451
|
+
{
|
|
19452
|
+
binding: 2,
|
|
19453
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
19454
|
+
buffer: { type: "uniform" }
|
|
19455
|
+
},
|
|
19456
|
+
{
|
|
19457
|
+
binding: 3,
|
|
19458
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
19459
|
+
buffer: { type: "storage" }
|
|
19460
|
+
}
|
|
19461
|
+
]
|
|
19462
|
+
});
|
|
19463
|
+
const missingTextureBindGroupLayout = device.createBindGroupLayout({
|
|
19464
|
+
label: "Missing Texture Layout",
|
|
19465
|
+
entries: [
|
|
19466
|
+
{
|
|
19467
|
+
binding: 0,
|
|
19468
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
19469
|
+
storageTexture: {
|
|
19470
|
+
access: "write-only",
|
|
19471
|
+
format: "rgba8unorm",
|
|
19472
|
+
viewDimension: "2d"
|
|
19473
|
+
}
|
|
19474
|
+
}
|
|
19475
|
+
]
|
|
19476
|
+
});
|
|
19477
|
+
return {
|
|
19478
|
+
boundPipeline: device.createComputePipeline({
|
|
19479
|
+
label: `${label} - Find Bounds Pipeline`,
|
|
19480
|
+
layout: device.createPipelineLayout({
|
|
19481
|
+
bindGroupLayouts: [findBoundsBindGroupLayout]
|
|
19482
|
+
}),
|
|
19483
|
+
compute: { module: shader, entryPoint: "find_bounds" }
|
|
19484
|
+
}),
|
|
19485
|
+
cropPipeline: device.createComputePipeline({
|
|
19486
|
+
label: `${label} - Crop Pipeline`,
|
|
19487
|
+
layout: device.createPipelineLayout({
|
|
19488
|
+
bindGroupLayouts: [cropBindGroupLayout]
|
|
19489
|
+
}),
|
|
19490
|
+
compute: { module: shader, entryPoint: "crop_and_output" }
|
|
19491
|
+
}),
|
|
19492
|
+
missingTexturePipeline: device.createComputePipeline({
|
|
19493
|
+
label: `${label} - Missing Texture Pipeline`,
|
|
19494
|
+
layout: device.createPipelineLayout({
|
|
19495
|
+
bindGroupLayouts: [missingTextureBindGroupLayout]
|
|
19496
|
+
}),
|
|
19497
|
+
compute: { module: shader, entryPoint: "missing_texture" }
|
|
19498
|
+
})
|
|
19499
|
+
};
|
|
19133
19500
|
}
|
|
19134
19501
|
|
|
19135
19502
|
// src/textures/Bundles.ts
|
|
@@ -19697,18 +20064,22 @@ class AssetManager {
|
|
|
19697
20064
|
this.bundles.unloadBundle(bundleId);
|
|
19698
20065
|
}
|
|
19699
20066
|
async loadFont(id, url, fallbackCharacter = "_") {
|
|
19700
|
-
if (this.#backend.type !== "webgpu") {
|
|
19701
|
-
throw new Error("loadFont is only supported with WebGPU backend. Text rendering is not available in WebGL mode.");
|
|
19702
|
-
}
|
|
19703
|
-
const webgpuBackend = this.#backend;
|
|
19704
|
-
const device = webgpuBackend.device;
|
|
19705
|
-
const presentationFormat = webgpuBackend.presentationFormat;
|
|
19706
20067
|
const limits = this.#backend.limits;
|
|
19707
20068
|
const font = await MsdfFont.create(id, url);
|
|
19708
20069
|
font.fallbackCharacter = fallbackCharacter;
|
|
19709
|
-
|
|
19710
|
-
|
|
19711
|
-
|
|
20070
|
+
if (this.#backend.type === "webgpu") {
|
|
20071
|
+
const webgpuBackend = this.#backend;
|
|
20072
|
+
const device = webgpuBackend.device;
|
|
20073
|
+
const presentationFormat = webgpuBackend.presentationFormat;
|
|
20074
|
+
const fontPipeline = await FontPipeline.create(device, font, presentationFormat, limits.maxTextLength);
|
|
20075
|
+
const textShader = new WebGPUTextShader(webgpuBackend, fontPipeline, font, presentationFormat, limits.instanceCount);
|
|
20076
|
+
this.#fonts.set(id, textShader);
|
|
20077
|
+
} else {
|
|
20078
|
+
const webglBackend = this.#backend;
|
|
20079
|
+
const fontPipeline = WebGLFontPipeline.create(webglBackend.gl, font, limits.maxTextLength);
|
|
20080
|
+
const textShader = new WebGLTextShader(webglBackend, fontPipeline);
|
|
20081
|
+
this.#fonts.set(id, textShader);
|
|
20082
|
+
}
|
|
19712
20083
|
return id;
|
|
19713
20084
|
}
|
|
19714
20085
|
getFont(id) {
|
|
@@ -20394,8 +20765,10 @@ __export(exports_mod4, {
|
|
|
20394
20765
|
// src/scene/mod.ts
|
|
20395
20766
|
var exports_mod5 = {};
|
|
20396
20767
|
__export(exports_mod5, {
|
|
20768
|
+
TextNode: () => TextNode,
|
|
20397
20769
|
SceneNode: () => SceneNode,
|
|
20398
20770
|
QuadNode: () => QuadNode,
|
|
20771
|
+
DEFAULT_FONT_SIZE: () => DEFAULT_FONT_SIZE,
|
|
20399
20772
|
Camera: () => Camera
|
|
20400
20773
|
});
|
|
20401
20774
|
// src/screen/mod.ts
|
|
@@ -20403,8 +20776,7 @@ var exports_mod6 = {};
|
|
|
20403
20776
|
// src/text/mod.ts
|
|
20404
20777
|
var exports_mod7 = {};
|
|
20405
20778
|
__export(exports_mod7, {
|
|
20406
|
-
TextShader: () =>
|
|
20407
|
-
TextNode: () => TextNode
|
|
20779
|
+
TextShader: () => WebGPUTextShader
|
|
20408
20780
|
});
|
|
20409
20781
|
// src/textures/mod.ts
|
|
20410
20782
|
var exports_mod8 = {};
|
|
@@ -20426,5 +20798,5 @@ export {
|
|
|
20426
20798
|
AssetManager
|
|
20427
20799
|
};
|
|
20428
20800
|
|
|
20429
|
-
//# debugId=
|
|
20801
|
+
//# debugId=4A88BA8B6CA05C3D64756E2164756E21
|
|
20430
20802
|
//# sourceMappingURL=mod.js.map
|