@bloopjs/toodle 0.1.6 → 0.1.7

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/mod.js CHANGED
@@ -1,14 +1,3 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, {
5
- get: all[name],
6
- enumerable: true,
7
- configurable: true,
8
- set: (newValue) => all[name] = () => newValue
9
- });
10
- };
11
-
12
1
  // src/limits.ts
13
2
  var DEFAULT_LIMITS = {
14
3
  instanceCount: 1024 * 2,
@@ -3445,18 +3434,6 @@ var {
3445
3434
  vec4: vec4n
3446
3435
  } = wgpuMatrixAPI(ZeroArray, Array, Array, Array, Array, Array);
3447
3436
 
3448
- // src/backends/mod.ts
3449
- var exports_mod = {};
3450
- __export(exports_mod, {
3451
- isWebGPUAvailable: () => isWebGPUAvailable,
3452
- isWebGL2Available: () => isWebGL2Available,
3453
- detectBackend: () => detectBackend,
3454
- defaultGLSLFragmentShader: () => defaultFragmentShader,
3455
- WebGPUBackend: () => WebGPUBackend,
3456
- WebGLBackend: () => WebGLBackend,
3457
- PostProcessDefaults: () => PostProcessDefaults
3458
- });
3459
-
3460
3437
  // src/backends/detection.ts
3461
3438
  async function detectBackend() {
3462
3439
  if (typeof navigator !== "undefined" && "gpu" in navigator) {
@@ -3469,21 +3446,13 @@ async function detectBackend() {
3469
3446
  }
3470
3447
  return "webgl2";
3471
3448
  }
3472
- function isWebGPUAvailable() {
3473
- return typeof navigator !== "undefined" && "gpu" in navigator;
3474
- }
3475
- function isWebGL2Available() {
3476
- if (typeof document === "undefined") {
3477
- return false;
3478
- }
3479
- try {
3480
- const canvas = document.createElement("canvas");
3481
- const gl = canvas.getContext("webgl2");
3482
- return gl !== null;
3483
- } catch {
3484
- return false;
3449
+ // src/utils/assert.ts
3450
+ function assert(condition, message) {
3451
+ if (!condition) {
3452
+ throw new Error(message);
3485
3453
  }
3486
3454
  }
3455
+
3487
3456
  // src/backends/webgl2/glsl/quad.glsl.ts
3488
3457
  var vertexShader = `#version 300 es
3489
3458
  precision highp float;
@@ -3588,13 +3557,6 @@ void main() {
3588
3557
  }
3589
3558
  }
3590
3559
  `;
3591
- var defaultFragmentShader = fragmentShader;
3592
- // src/utils/assert.ts
3593
- function assert(condition, message) {
3594
- if (!condition) {
3595
- throw new Error(message);
3596
- }
3597
- }
3598
3560
 
3599
3561
  // src/backends/webgl2/WebGLQuadShader.ts
3600
3562
  var INSTANCE_FLOATS = 28;
@@ -3754,7 +3716,7 @@ class WebGLQuadShader {
3754
3716
  }
3755
3717
 
3756
3718
  // src/backends/webgl2/WebGLBackend.ts
3757
- class WebGLBackend {
3719
+ class WebGLBackend2 {
3758
3720
  type = "webgl2";
3759
3721
  limits;
3760
3722
  atlasSize;
@@ -3784,7 +3746,7 @@ class WebGLBackend {
3784
3746
  ...DEFAULT_LIMITS,
3785
3747
  ...options.limits
3786
3748
  };
3787
- const backend = new WebGLBackend(gl, canvas, limits);
3749
+ const backend = new WebGLBackend2(gl, canvas, limits);
3788
3750
  backend.createTextureAtlas("default", {
3789
3751
  format: options.format ?? "rgba8unorm",
3790
3752
  layers: limits.textureArrayLayers,
@@ -3873,58 +3835,6 @@ class WebGLBackend {
3873
3835
  return this.getTextureAtlas("default")?.format ?? "rgba8unorm";
3874
3836
  }
3875
3837
  }
3876
- // src/backends/webgpu/postprocess/mod.ts
3877
- var PostProcessDefaults = {
3878
- sampler(device) {
3879
- return device.createSampler({
3880
- label: "toodle post process sampler",
3881
- magFilter: "linear",
3882
- minFilter: "linear",
3883
- mipmapFilter: "linear",
3884
- addressModeU: "clamp-to-edge",
3885
- addressModeV: "clamp-to-edge"
3886
- });
3887
- },
3888
- vertexBufferLayout(_device) {
3889
- return {
3890
- arrayStride: 4 * 4,
3891
- attributes: [{ shaderLocation: 0, offset: 0, format: "float32x2" }]
3892
- };
3893
- },
3894
- vertexShader(device) {
3895
- return device.createShaderModule({
3896
- label: "toodle post process vertex shader",
3897
- code: `
3898
- struct VertexOut {
3899
- @builtin(position) position: vec4<f32>,
3900
- @location(0) uv: vec2<f32>,
3901
- };
3902
-
3903
- const enginePosLookup = array(vec2f(-1, 1), vec2f(-1, -1), vec2f(1, 1), vec2f(1, -1));
3904
- const engineUvLookup = array(vec2f(0, 0), vec2f(0, 1), vec2f(1, 0), vec2f(1, 1));
3905
-
3906
- @vertex
3907
- fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOut {
3908
- var out: VertexOut;
3909
- out.position = vec4(enginePosLookup[vertexIndex], 0.0, 1.0);
3910
- out.uv = engineUvLookup[vertexIndex];
3911
- return out;
3912
- }
3913
- `
3914
- });
3915
- },
3916
- pipelineDescriptor(device) {
3917
- return {
3918
- label: "toodle post process pipeline descriptor",
3919
- layout: "auto",
3920
- primitive: { topology: "triangle-strip" },
3921
- vertex: {
3922
- buffers: [PostProcessDefaults.vertexBufferLayout(device)],
3923
- module: PostProcessDefaults.vertexShader(device)
3924
- }
3925
- };
3926
- }
3927
- };
3928
3838
  // node_modules/webgpu-utils/dist/1.x/webgpu-utils.module.js
3929
3839
  var roundUpToMultipleOf = (v, multiple) => ((v + multiple - 1) / multiple | 0) * multiple;
3930
3840
  function keysOf(obj) {
@@ -17031,7 +16941,7 @@ function convertBlendMode(mode) {
17031
16941
  }
17032
16942
 
17033
16943
  // src/backends/webgpu/WebGPUBackend.ts
17034
- class WebGPUBackend {
16944
+ class WebGPUBackend2 {
17035
16945
  type = "webgpu";
17036
16946
  limits;
17037
16947
  atlasSize;
@@ -17076,7 +16986,7 @@ class WebGPUBackend {
17076
16986
  ...DEFAULT_LIMITS,
17077
16987
  ...options.limits
17078
16988
  };
17079
- const backend = new WebGPUBackend(device, context, presentationFormat, limits, canvas);
16989
+ const backend = new WebGPUBackend2(device, context, presentationFormat, limits, canvas);
17080
16990
  backend.createTextureAtlas("default", {
17081
16991
  format: options.format ?? "rgba8unorm",
17082
16992
  layers: limits.textureArrayLayers,
@@ -17221,414 +17131,150 @@ class WebGPUBackend {
17221
17131
  return this.#renderPass;
17222
17132
  }
17223
17133
  }
17224
- // src/backends/webgl2/WebGLFontPipeline.ts
17225
- class WebGLFontPipeline {
17226
- font;
17227
- fontTexture;
17228
- charDataTexture;
17229
- textBufferTexture;
17230
- maxCharCount;
17231
- lineHeight;
17232
- #gl;
17233
- constructor(gl, font, fontTexture, charDataTexture, textBufferTexture, maxCharCount) {
17234
- this.#gl = gl;
17235
- this.font = font;
17236
- this.fontTexture = fontTexture;
17237
- this.charDataTexture = charDataTexture;
17238
- this.textBufferTexture = textBufferTexture;
17239
- this.maxCharCount = maxCharCount;
17240
- this.lineHeight = font.lineHeight;
17241
- }
17242
- static create(gl, font, maxCharCount) {
17243
- const fontTexture = gl.createTexture();
17244
- assert(fontTexture, "Failed to create font texture");
17245
- gl.bindTexture(gl.TEXTURE_2D, fontTexture);
17246
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
17247
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
17248
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
17249
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
17250
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, font.imageBitmap);
17251
- const charDataTexture = gl.createTexture();
17252
- assert(charDataTexture, "Failed to create char data texture");
17253
- gl.bindTexture(gl.TEXTURE_2D, charDataTexture);
17254
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
17255
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
17256
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
17257
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
17258
- const charCount = font.charCount;
17259
- const charTextureWidth = charCount * 2;
17260
- const charTextureData = new Float32Array(charTextureWidth * 4);
17261
- for (let i3 = 0;i3 < charCount; i3++) {
17262
- const srcOffset = i3 * 8;
17263
- const dstOffset0 = i3 * 2 * 4;
17264
- const dstOffset1 = (i3 * 2 + 1) * 4;
17265
- charTextureData[dstOffset0] = font.charBuffer[srcOffset];
17266
- charTextureData[dstOffset0 + 1] = font.charBuffer[srcOffset + 1];
17267
- charTextureData[dstOffset0 + 2] = font.charBuffer[srcOffset + 2];
17268
- charTextureData[dstOffset0 + 3] = font.charBuffer[srcOffset + 3];
17269
- charTextureData[dstOffset1] = font.charBuffer[srcOffset + 4];
17270
- charTextureData[dstOffset1 + 1] = font.charBuffer[srcOffset + 5];
17271
- charTextureData[dstOffset1 + 2] = font.charBuffer[srcOffset + 6];
17272
- charTextureData[dstOffset1 + 3] = font.charBuffer[srcOffset + 7];
17273
- }
17274
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, charTextureWidth, 1, 0, gl.RGBA, gl.FLOAT, charTextureData);
17275
- const textBufferTexture = gl.createTexture();
17276
- assert(textBufferTexture, "Failed to create text buffer texture");
17277
- gl.bindTexture(gl.TEXTURE_2D, textBufferTexture);
17278
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
17279
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
17280
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
17281
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
17282
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, maxCharCount, 1, 0, gl.RGBA, gl.FLOAT, null);
17283
- gl.bindTexture(gl.TEXTURE_2D, null);
17284
- return new WebGLFontPipeline(gl, font, fontTexture, charDataTexture, textBufferTexture, maxCharCount);
17285
- }
17286
- updateTextBuffer(data, glyphCount) {
17287
- const gl = this.#gl;
17288
- gl.bindTexture(gl.TEXTURE_2D, this.textBufferTexture);
17289
- gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, glyphCount, 1, gl.RGBA, gl.FLOAT, data);
17290
- }
17291
- destroy() {
17292
- const gl = this.#gl;
17293
- gl.deleteTexture(this.fontTexture);
17294
- gl.deleteTexture(this.charDataTexture);
17295
- gl.deleteTexture(this.textBufferTexture);
17296
- }
17134
+ // src/math/matrix.ts
17135
+ function createProjectionMatrix(resolution, dst) {
17136
+ const { width, height } = resolution;
17137
+ return mat3.scaling([2 / width, 2 / height], dst);
17297
17138
  }
17298
-
17299
- // src/utils/error.ts
17300
- var warnings = new Map;
17301
- function warnOnce(key, msg) {
17302
- if (warnings.has(key)) {
17303
- return;
17304
- }
17305
- warnings.set(key, true);
17306
- console.warn(msg ?? key);
17139
+ function createViewMatrix(camera, target) {
17140
+ const matrix = mat3.identity(target);
17141
+ mat3.scale(matrix, [camera.zoom, camera.zoom], matrix);
17142
+ mat3.rotate(matrix, camera.rotationRadians, matrix);
17143
+ mat3.translate(matrix, [-camera.x, -camera.y], matrix);
17144
+ return matrix;
17145
+ }
17146
+ function createModelMatrix(transform, base) {
17147
+ mat3.translate(base, [transform.position.x, transform.position.y], base);
17148
+ mat3.rotate(base, transform.rotation, base);
17149
+ mat3.scale(base, [transform.scale.x, transform.scale.y], base);
17150
+ return base;
17151
+ }
17152
+ function convertScreenToWorld(screenCoordinates, camera, projectionMatrix, resolution) {
17153
+ const inverseViewProjectionMatrix = mat3.mul(mat3.inverse(camera.matrix), mat3.inverse(projectionMatrix));
17154
+ const normalizedDeviceCoordinates = {
17155
+ x: 2 * screenCoordinates.x / resolution.width - 1,
17156
+ y: 1 - 2 * screenCoordinates.y / resolution.height
17157
+ };
17158
+ return transformPoint(normalizedDeviceCoordinates, inverseViewProjectionMatrix);
17159
+ }
17160
+ function convertWorldToScreen(worldCoordinates, camera, projectionMatrix, resolution) {
17161
+ const viewProjectionMatrix = mat3.mul(projectionMatrix, camera.matrix);
17162
+ const ndcPoint = transformPoint(worldCoordinates, viewProjectionMatrix);
17163
+ return {
17164
+ x: (ndcPoint.x + 1) * resolution.width / 2,
17165
+ y: (1 - ndcPoint.y) * resolution.height / 2
17166
+ };
17167
+ }
17168
+ function transformPoint(point, matrix) {
17169
+ const result = vec2.transformMat3([point.x, point.y], matrix);
17170
+ return {
17171
+ x: result[0],
17172
+ y: result[1]
17173
+ };
17307
17174
  }
17308
17175
 
17309
- // src/text/MsdfFont.ts
17310
- class MsdfFont {
17311
- id;
17312
- json;
17313
- imageBitmap;
17314
- name;
17315
- charset;
17316
- charCount;
17317
- lineHeight;
17318
- charBuffer;
17319
- #kernings;
17320
- #chars;
17321
- #fallbackCharCode;
17322
- constructor(id, json, imageBitmap) {
17323
- this.id = id;
17324
- this.json = json;
17325
- this.imageBitmap = imageBitmap;
17326
- const charArray = Object.values(json.chars);
17327
- this.charCount = charArray.length;
17328
- this.lineHeight = json.common.lineHeight;
17329
- this.charset = json.info.charset;
17330
- this.name = json.info.face;
17331
- this.#kernings = new Map;
17332
- if (json.kernings) {
17333
- for (const kearning of json.kernings) {
17334
- let charKerning = this.#kernings.get(kearning.first);
17335
- if (!charKerning) {
17336
- charKerning = new Map;
17337
- this.#kernings.set(kearning.first, charKerning);
17338
- }
17339
- charKerning.set(kearning.second, kearning.amount);
17340
- }
17176
+ // src/scene/Batcher.ts
17177
+ class Batcher {
17178
+ nodes = [];
17179
+ layers = [];
17180
+ pipelines = [];
17181
+ enqueue(node) {
17182
+ if (node.renderComponent && node.isActive) {
17183
+ this.nodes.push(node);
17184
+ const z3 = node.layer;
17185
+ const layer = this.#findOrCreateLayer(z3);
17186
+ const pipeline = this.#findOrCreatePipeline(layer, node.renderComponent.shader);
17187
+ pipeline.nodes.push(node);
17341
17188
  }
17342
- this.#chars = new Map;
17343
- const charCount = Object.values(json.chars).length;
17344
- this.charBuffer = new Float32Array(charCount * 8);
17345
- let offset = 0;
17346
- const u3 = 1 / json.common.scaleW;
17347
- const v3 = 1 / json.common.scaleH;
17348
- for (const [i3, char] of json.chars.entries()) {
17349
- this.#chars.set(char.id, char);
17350
- this.#chars.get(char.id).charIndex = i3;
17351
- this.charBuffer[offset] = char.x * u3;
17352
- this.charBuffer[offset + 1] = char.y * v3;
17353
- this.charBuffer[offset + 2] = char.width * u3;
17354
- this.charBuffer[offset + 3] = char.height * v3;
17355
- this.charBuffer[offset + 4] = char.width;
17356
- this.charBuffer[offset + 5] = char.height;
17357
- this.charBuffer[offset + 6] = char.xoffset;
17358
- this.charBuffer[offset + 7] = -char.yoffset;
17359
- offset += 8;
17189
+ for (const kid of node.kids) {
17190
+ this.enqueue(kid);
17360
17191
  }
17361
17192
  }
17362
- getChar(charCode) {
17363
- const char = this.#chars.get(charCode);
17364
- if (!char) {
17365
- const fallbackCharacter = this.#chars.get(this.#fallbackCharCode ?? this.#chars.keys().toArray()[0]);
17366
- warnOnce(`unknown_char_${this.name}`, `Couldn't find character ${charCode} in characters for font ${this.name} -- defaulting to first available character "${fallbackCharacter.char}"`);
17367
- return fallbackCharacter;
17193
+ flush() {
17194
+ this.nodes = [];
17195
+ this.layers = [];
17196
+ this.pipelines = [];
17197
+ }
17198
+ #findOrCreateLayer(z3) {
17199
+ let layer = this.layers.find((l3) => l3.z === z3);
17200
+ if (!layer) {
17201
+ layer = { z: z3, pipelines: [] };
17202
+ this.layers.push(layer);
17203
+ this.layers.sort((a3, b3) => a3.z - b3.z);
17368
17204
  }
17369
- return char;
17205
+ return layer;
17370
17206
  }
17371
- getXAdvance(charCode, nextCharCode = -1) {
17372
- const char = this.getChar(charCode);
17373
- if (nextCharCode >= 0) {
17374
- const kerning = this.#kernings.get(charCode);
17375
- if (kerning) {
17376
- return char.xadvance + (kerning.get(nextCharCode) ?? 0);
17377
- }
17378
- }
17379
- return char.xadvance;
17380
- }
17381
- static async create(id, fontJsonUrl) {
17382
- const response = await fetch(fontJsonUrl);
17383
- const json = await response.json();
17384
- const i3 = fontJsonUrl.href.lastIndexOf("/");
17385
- const baseUrl = i3 !== -1 ? fontJsonUrl.href.substring(0, i3 + 1) : undefined;
17386
- if (json.pages.length < 1) {
17387
- throw new Error(`Can't create an msdf font without a reference to the page url in the json`);
17388
- }
17389
- if (json.pages.length > 1) {
17390
- throw new Error(`Can't create an msdf font with more than one page`);
17391
- }
17392
- const textureUrl = baseUrl + json.pages[0];
17393
- const textureResponse = await fetch(textureUrl);
17394
- const bitmap = await createImageBitmap(await textureResponse.blob());
17395
- return new MsdfFont(id, json, bitmap);
17396
- }
17397
- set fallbackCharacter(character) {
17398
- const charCode = character.charCodeAt(0);
17399
- if (this.#chars.has(charCode)) {
17400
- this.#fallbackCharCode = charCode;
17401
- } else {
17402
- const fallbackCode = this.#chars.keys().toArray()[0];
17403
- console.warn(`${character} character does not exist in font ${this.name} defaulting to "${this.#chars.get(fallbackCode)?.char}".`);
17404
- this.#fallbackCharCode = fallbackCode;
17207
+ #findOrCreatePipeline(layer, shader) {
17208
+ let pipeline = layer.pipelines.find((p3) => p3.shader === shader);
17209
+ if (!pipeline) {
17210
+ pipeline = { shader, nodes: [] };
17211
+ layer.pipelines.push(pipeline);
17212
+ this.pipelines.push(pipeline);
17405
17213
  }
17214
+ return pipeline;
17406
17215
  }
17407
17216
  }
17408
17217
 
17409
- // src/text/shaping.ts
17410
- var TAB_SPACES = 4;
17411
- function shapeText(font, text, blockSize, fontSize, formatting, textArray, initialFloatOffset = 0, debug = false) {
17412
- let offset = initialFloatOffset;
17413
- const measurements = measureText(font, text, formatting.wordWrap);
17414
- const alignment = formatting.align || "left";
17415
- const em2px = fontSize / font.lineHeight;
17416
- const hackHasExplicitBlock = blockSize.width !== measurements.width;
17417
- let debugData = null;
17418
- if (debug) {
17419
- debugData = [];
17218
+ // src/math/angle.ts
17219
+ function deg2rad(degrees) {
17220
+ return degrees * (Math.PI / 180);
17221
+ }
17222
+ function rad2deg(radians) {
17223
+ return radians * (180 / Math.PI);
17224
+ }
17225
+
17226
+ // src/scene/Camera.ts
17227
+ class Camera {
17228
+ #position = { x: 0, y: 0 };
17229
+ #zoom = 1;
17230
+ #rotation = 0;
17231
+ #isDirty = true;
17232
+ #matrix = mat3.create();
17233
+ get zoom() {
17234
+ return this.#zoom;
17420
17235
  }
17421
- for (const word of measurements.words) {
17422
- for (const glyph of word.glyphs) {
17423
- let lineOffset = 0;
17424
- if (alignment === "center") {
17425
- lineOffset = measurements.width * -0.5 - (measurements.width - measurements.lineWidths[glyph.line]) * -0.5;
17426
- } else if (alignment === "right") {
17427
- const blockSizeEm = blockSize.width / em2px;
17428
- const delta = measurements.width - measurements.lineWidths[glyph.line];
17429
- lineOffset = (hackHasExplicitBlock ? blockSizeEm / 2 : measurements.width / 2) - measurements.width + delta;
17430
- } else if (alignment === "left") {
17431
- const blockSizeEm = blockSize.width / em2px;
17432
- lineOffset = hackHasExplicitBlock ? -blockSizeEm / 2 : -measurements.width / 2;
17433
- }
17434
- if (debug && debugData) {
17435
- debugData.push({
17436
- line: glyph.line,
17437
- word: word.glyphs.map((g3) => g3.char.char).join(""),
17438
- glyph: glyph.char.char,
17439
- startX: word.startX,
17440
- glyphX: glyph.offset[0],
17441
- advance: glyph.char.xadvance,
17442
- lineOffset,
17443
- startY: word.startY,
17444
- glyphY: glyph.offset[1]
17445
- });
17446
- }
17447
- textArray[offset] = word.startX + glyph.offset[0] + lineOffset;
17448
- textArray[offset + 1] = word.startY + glyph.offset[1];
17449
- textArray[offset + 2] = glyph.char.charIndex;
17450
- offset += 4;
17451
- }
17236
+ set zoom(value) {
17237
+ this.#zoom = value;
17238
+ this.setDirty();
17452
17239
  }
17453
- if (debug && debugData) {
17454
- console.table(debugData);
17240
+ get rotation() {
17241
+ return rad2deg(this.#rotation);
17455
17242
  }
17456
- }
17457
- function measureText(font, text, wordWrap) {
17458
- let maxWidth = 0;
17459
- const lineWidths = [];
17460
- let textOffsetX = 0;
17461
- let textOffsetY = 0;
17462
- let line = 0;
17463
- let printedCharCount = 0;
17464
- let nextCharCode = text.charCodeAt(0);
17465
- let word = { glyphs: [], width: 0, startX: 0, startY: 0 };
17466
- const words = [];
17467
- for (let i3 = 0;i3 < text.length; i3++) {
17468
- const isLastLetter = i3 === text.length - 1;
17469
- const charCode = nextCharCode;
17470
- nextCharCode = i3 < text.length - 1 ? text.charCodeAt(i3 + 1) : -1;
17471
- switch (charCode) {
17472
- case 9 /* HorizontalTab */:
17473
- insertSpaces(TAB_SPACES);
17474
- break;
17475
- case 10 /* Newline */:
17476
- flushLine();
17477
- flushWord();
17478
- break;
17479
- case 13 /* CarriageReturn */:
17480
- break;
17481
- case 32 /* Space */:
17482
- insertSpaces(1);
17483
- break;
17484
- default: {
17485
- const advance = font.getXAdvance(charCode, nextCharCode);
17486
- if (wordWrap && wordWrap.breakOn === "character" && textOffsetX + advance > wordWrap.emWidth) {
17487
- if (word.startX === 0) {
17488
- flushWord();
17489
- } else {
17490
- lineWidths.push(textOffsetX - word.width);
17491
- line++;
17492
- maxWidth = Math.max(maxWidth, textOffsetX);
17493
- textOffsetX = word.width;
17494
- textOffsetY -= font.lineHeight;
17495
- word.startX = 0;
17496
- word.startY = textOffsetY;
17497
- word.glyphs.forEach((g3) => {
17498
- g3.line = line;
17499
- });
17500
- }
17501
- }
17502
- word.glyphs.push({
17503
- char: font.getChar(charCode),
17504
- offset: [word.width, 0],
17505
- line
17506
- });
17507
- if (isLastLetter) {
17508
- flushWord();
17509
- }
17510
- word.width += advance;
17511
- textOffsetX += advance;
17512
- }
17513
- }
17243
+ set rotation(value) {
17244
+ this.#rotation = deg2rad(value);
17245
+ this.setDirty();
17514
17246
  }
17515
- lineWidths.push(textOffsetX);
17516
- maxWidth = Math.max(maxWidth, textOffsetX);
17517
- const lineCount = lineWidths.length;
17518
- return {
17519
- width: maxWidth,
17520
- height: lineCount * font.lineHeight,
17521
- lineWidths,
17522
- lineCount,
17523
- printedCharCount,
17524
- words
17525
- };
17526
- function flushWord() {
17527
- printedCharCount += word.glyphs.length;
17528
- words.push(word);
17529
- word = {
17530
- glyphs: [],
17531
- width: 0,
17532
- startX: textOffsetX,
17533
- startY: textOffsetY
17534
- };
17247
+ get rotationRadians() {
17248
+ return this.#rotation;
17535
17249
  }
17536
- function flushLine() {
17537
- lineWidths.push(textOffsetX);
17538
- line++;
17539
- maxWidth = Math.max(maxWidth, textOffsetX);
17540
- textOffsetX = 0;
17541
- textOffsetY -= font.lineHeight;
17250
+ set rotationRadians(value) {
17251
+ this.#rotation = value;
17252
+ this.setDirty();
17542
17253
  }
17543
- function insertSpaces(spaces) {
17544
- if (spaces < 1)
17545
- spaces = 1;
17546
- textOffsetX += font.getXAdvance(32 /* Space */) * spaces;
17547
- if (wordWrap?.breakOn === "word" && textOffsetX >= wordWrap.emWidth) {
17548
- flushLine();
17549
- }
17550
- flushWord();
17254
+ get x() {
17255
+ return this.#position.x;
17551
17256
  }
17552
- }
17553
- function findLargestFontSize(font, text, size, formatting) {
17554
- if (!formatting.fontSize) {
17555
- throw new Error("fontSize is required for shrinkToFit");
17257
+ get y() {
17258
+ return this.#position.y;
17556
17259
  }
17557
- if (!formatting.shrinkToFit) {
17558
- throw new Error("shrinkToFit is required for findLargestFontSize");
17260
+ set x(value) {
17261
+ this.#position.x = value;
17262
+ this.setDirty();
17559
17263
  }
17560
- const minSize = formatting.shrinkToFit.minFontSize;
17561
- const maxSize = formatting.shrinkToFit.maxFontSize ?? formatting.fontSize;
17562
- const maxLines = formatting.shrinkToFit.maxLines ?? Number.POSITIVE_INFINITY;
17563
- const threshold = 0.5;
17564
- let low = minSize;
17565
- let high = maxSize;
17566
- while (high - low > threshold) {
17567
- const testSize = (low + high) / 2;
17568
- const testMeasure = measureText(font, text, formatting.wordWrap);
17569
- const padding = formatting.shrinkToFit.padding ?? 0;
17570
- const scaledWidth = testMeasure.width * (testSize / font.lineHeight);
17571
- const scaledHeight = testMeasure.height * (testSize / font.lineHeight);
17572
- const fitsWidth = scaledWidth <= size.width - size.width * padding;
17573
- const fitsHeight = scaledHeight <= size.height - size.height * padding;
17574
- const fitsLines = testMeasure.lineCount <= maxLines;
17575
- if (fitsWidth && fitsHeight && fitsLines) {
17576
- low = testSize;
17577
- } else {
17578
- high = testSize;
17264
+ set y(value) {
17265
+ this.#position.y = value;
17266
+ this.setDirty();
17267
+ }
17268
+ get matrix() {
17269
+ if (this.#isDirty) {
17270
+ this.#isDirty = false;
17271
+ this.#matrix = createViewMatrix(this, this.#matrix);
17579
17272
  }
17273
+ return this.#matrix;
17274
+ }
17275
+ setDirty() {
17276
+ this.#isDirty = true;
17580
17277
  }
17581
- return low;
17582
- }
17583
-
17584
- // src/math/angle.ts
17585
- function deg2rad(degrees) {
17586
- return degrees * (Math.PI / 180);
17587
- }
17588
- function rad2deg(radians) {
17589
- return radians * (180 / Math.PI);
17590
- }
17591
-
17592
- // src/math/matrix.ts
17593
- function createProjectionMatrix(resolution, dst) {
17594
- const { width, height } = resolution;
17595
- return mat3.scaling([2 / width, 2 / height], dst);
17596
- }
17597
- function createViewMatrix(camera, target) {
17598
- const matrix = mat3.identity(target);
17599
- mat3.scale(matrix, [camera.zoom, camera.zoom], matrix);
17600
- mat3.rotate(matrix, camera.rotationRadians, matrix);
17601
- mat3.translate(matrix, [-camera.x, -camera.y], matrix);
17602
- return matrix;
17603
- }
17604
- function createModelMatrix(transform, base) {
17605
- mat3.translate(base, [transform.position.x, transform.position.y], base);
17606
- mat3.rotate(base, transform.rotation, base);
17607
- mat3.scale(base, [transform.scale.x, transform.scale.y], base);
17608
- return base;
17609
- }
17610
- function convertScreenToWorld(screenCoordinates, camera, projectionMatrix, resolution) {
17611
- const inverseViewProjectionMatrix = mat3.mul(mat3.inverse(camera.matrix), mat3.inverse(projectionMatrix));
17612
- const normalizedDeviceCoordinates = {
17613
- x: 2 * screenCoordinates.x / resolution.width - 1,
17614
- y: 1 - 2 * screenCoordinates.y / resolution.height
17615
- };
17616
- return transformPoint(normalizedDeviceCoordinates, inverseViewProjectionMatrix);
17617
- }
17618
- function convertWorldToScreen(worldCoordinates, camera, projectionMatrix, resolution) {
17619
- const viewProjectionMatrix = mat3.mul(projectionMatrix, camera.matrix);
17620
- const ndcPoint = transformPoint(worldCoordinates, viewProjectionMatrix);
17621
- return {
17622
- x: (ndcPoint.x + 1) * resolution.width / 2,
17623
- y: (1 - ndcPoint.y) * resolution.height / 2
17624
- };
17625
- }
17626
- function transformPoint(point, matrix) {
17627
- const result = vec2.transformMat3([point.x, point.y], matrix);
17628
- return {
17629
- x: result[0],
17630
- y: result[1]
17631
- };
17632
17278
  }
17633
17279
 
17634
17280
  // src/scene/SceneNode.ts
@@ -17972,1163 +17618,1304 @@ function reviver(key, value) {
17972
17618
  return value;
17973
17619
  }
17974
17620
 
17975
- // src/scene/TextNode.ts
17976
- var DEFAULT_FONT_SIZE = 14;
17621
+ // src/scene/QuadNode.ts
17622
+ var PRIMITIVE_TEXTURE = "__primitive__";
17623
+ var RESERVED_PRIMITIVE_INDEX_START = 1000;
17624
+ var CIRCLE_INDEX = 1001;
17625
+ var DEFAULT_REGION = {
17626
+ x: 0,
17627
+ y: 0,
17628
+ width: 0,
17629
+ height: 0
17630
+ };
17977
17631
 
17978
- class TextNode extends SceneNode {
17979
- #text;
17980
- #formatting;
17981
- #font;
17982
- constructor(shader, text, opts = {}) {
17983
- const { width, height } = measureText(shader.font, text, opts.wordWrap);
17984
- if (text.length > shader.maxCharCount) {
17985
- throw new Error(`Text: ${text} exceeds ${shader.maxCharCount} characters. Try using fewer characters or increase the limit in Toodle.attach.`);
17632
+ class QuadNode extends SceneNode {
17633
+ assetManager;
17634
+ #color;
17635
+ #atlasCoords;
17636
+ #region;
17637
+ #matrixPool;
17638
+ #flip;
17639
+ #cropOffset;
17640
+ #cropRatio;
17641
+ #atlasSize;
17642
+ #textureId;
17643
+ #writeInstance;
17644
+ constructor(options, matrixPool) {
17645
+ assert(options.shader, "QuadNode requires a shader to be explicitly provided");
17646
+ assert(options.size, "QuadNode requires a size to be explicitly provided");
17647
+ assert(options.atlasCoords, "QuadNode requires atlas coords to be explicitly provided");
17648
+ options.render ??= {
17649
+ shader: options.shader,
17650
+ writeInstance: writeQuadInstance
17651
+ };
17652
+ super(options);
17653
+ assert(options.assetManager, "QuadNode requires an asset manager");
17654
+ this.assetManager = options.assetManager;
17655
+ if (options.atlasCoords && options.atlasCoords.atlasIndex >= RESERVED_PRIMITIVE_INDEX_START) {
17656
+ this.#textureId = PRIMITIVE_TEXTURE;
17657
+ this.#region = DEFAULT_REGION;
17658
+ this.#atlasSize = DEFAULT_REGION;
17659
+ } else {
17660
+ assert(options.textureId, "QuadNode requires texture id to be explicitly provided");
17661
+ this.#textureId = options.textureId;
17662
+ assert(options.region, "QuadNode requires a region to be explicitly provided");
17663
+ this.#region = options.region;
17664
+ assert(options.atlasSize, "QuadNode requires atlas size to be explicitly provided");
17665
+ this.#atlasSize = options.atlasSize;
17986
17666
  }
17987
- const em2px = shader.font.lineHeight / (opts.fontSize ?? DEFAULT_FONT_SIZE);
17988
- if (!opts.shrinkToFit && !opts.size) {
17989
- opts.size = { width: width / em2px, height: height / em2px };
17667
+ this.#atlasCoords = options.atlasCoords;
17668
+ this.#color = options.color ?? { r: 1, g: 1, b: 1, a: 1 };
17669
+ this.#matrixPool = matrixPool;
17670
+ this.#flip = { x: options.flipX ? -1 : 1, y: options.flipY ? -1 : 1 };
17671
+ this.#cropOffset = options.cropOffset ?? { x: 0, y: 0 };
17672
+ this.#cropRatio = !this.#atlasCoords.uvScaleCropped ? { width: 1, height: 1 } : {
17673
+ width: this.#atlasCoords.uvScaleCropped.width / this.#atlasCoords.uvScale.width,
17674
+ height: this.#atlasCoords.uvScaleCropped.height / this.#atlasCoords.uvScale.height
17675
+ };
17676
+ this.#writeInstance = options.writeInstance;
17677
+ }
17678
+ get color() {
17679
+ return this.#color;
17680
+ }
17681
+ set color(value) {
17682
+ this.#color = value;
17683
+ }
17684
+ get size() {
17685
+ const size = super.size;
17686
+ if (!size) {
17687
+ throw new Error("QuadNode requires a size");
17990
17688
  }
17991
- super({
17992
- ...opts,
17993
- render: {
17994
- shader,
17995
- writeInstance: (_node, _array, _offset) => {
17996
- throw new Error("not implemented - needs access to text uniform buffer, dimensions and a model matrix");
17997
- }
17998
- }
17999
- });
18000
- this.#font = shader.font;
18001
- this.#text = text;
18002
- this.#formatting = opts;
17689
+ return size;
18003
17690
  }
18004
- get text() {
18005
- return this.#text;
17691
+ set size(val) {
17692
+ super.size = val;
18006
17693
  }
18007
- get formatting() {
18008
- return this.#formatting;
17694
+ get matrixWithSize() {
17695
+ const matrix = mat3.clone(this.matrix, this.#matrixPool.get());
17696
+ mat3.scale(matrix, [this.size.width * this.#flip.x, this.size.height * this.#flip.y], matrix);
17697
+ return matrix;
18009
17698
  }
18010
- get font() {
18011
- return this.#font;
17699
+ get atlasCoords() {
17700
+ return this.#atlasCoords;
18012
17701
  }
18013
- set text(text) {
18014
- if (!text) {
18015
- throw new Error("text cannot be empty");
18016
- }
18017
- this.#text = text;
18018
- this.setDirty();
17702
+ get region() {
17703
+ return this.#region;
18019
17704
  }
18020
- get tint() {
18021
- return this.#formatting.color || { r: 1, g: 1, b: 1, a: 1 };
17705
+ get writeInstance() {
17706
+ return this.#writeInstance;
18022
17707
  }
18023
- set tint(tint) {
18024
- this.#formatting.color = tint;
17708
+ get flipX() {
17709
+ return this.#flip.x === -1;
17710
+ }
17711
+ set flipX(value) {
17712
+ this.#flip.x = value ? -1 : 1;
18025
17713
  this.setDirty();
18026
17714
  }
18027
- set formatting(formatting) {
18028
- this.#formatting = formatting;
17715
+ get flipY() {
17716
+ return this.#flip.y === -1;
17717
+ }
17718
+ set flipY(value) {
17719
+ this.#flip.y = value ? -1 : 1;
18029
17720
  this.setDirty();
18030
17721
  }
17722
+ get cropOffset() {
17723
+ return this.#cropOffset;
17724
+ }
17725
+ set cropOffset(value) {
17726
+ this.#cropOffset = value;
17727
+ }
17728
+ get textureId() {
17729
+ return this.#textureId;
17730
+ }
17731
+ get isPrimitive() {
17732
+ return this.#textureId === PRIMITIVE_TEXTURE;
17733
+ }
17734
+ get isCircle() {
17735
+ return this.#atlasCoords.atlasIndex === CIRCLE_INDEX;
17736
+ }
17737
+ extra = {
17738
+ setAtlasCoords: (value) => {
17739
+ this.#atlasCoords = value;
17740
+ },
17741
+ cropRatio: () => {
17742
+ return this.#cropRatio;
17743
+ },
17744
+ atlasSize: () => {
17745
+ return this.#atlasSize;
17746
+ }
17747
+ };
17748
+ }
17749
+ function writeQuadInstance(node, array, offset) {
17750
+ if (!(node instanceof QuadNode)) {
17751
+ throw new Error("QuadNode.writeInstance can only be called on QuadNodes");
17752
+ }
17753
+ array.set(node.matrixWithSize, offset);
17754
+ array.set([node.color.r, node.color.g, node.color.b, node.color.a], offset + 12);
17755
+ const region = node.region;
17756
+ if (node.textureId === PRIMITIVE_TEXTURE) {
17757
+ array.set([
17758
+ node.atlasCoords.uvOffset.x,
17759
+ node.atlasCoords.uvOffset.y,
17760
+ node.atlasCoords.uvScale.width,
17761
+ node.atlasCoords.uvScale.height
17762
+ ], offset + 16);
17763
+ } else {
17764
+ const atlasSize = node.extra.atlasSize();
17765
+ array.set([
17766
+ node.atlasCoords.uvOffset.x + region.x / atlasSize.width,
17767
+ node.atlasCoords.uvOffset.y + region.y / atlasSize.height,
17768
+ region.width / atlasSize.width,
17769
+ region.height / atlasSize.height
17770
+ ], offset + 16);
17771
+ }
17772
+ array.set([
17773
+ node.cropOffset.x / 2 / (node.atlasCoords.originalSize.width || 1),
17774
+ node.cropOffset.y / 2 / (node.atlasCoords.originalSize.height || 1),
17775
+ node.extra.cropRatio().width,
17776
+ node.extra.cropRatio().height
17777
+ ], offset + 20);
17778
+ new DataView(array.buffer).setUint32(array.byteOffset + (offset + 24) * Float32Array.BYTES_PER_ELEMENT, node.atlasCoords.atlasIndex, true);
17779
+ node.writeInstance?.(array, offset + 28);
17780
+ return 1;
18031
17781
  }
18032
17782
 
18033
- // src/backends/webgl2/glsl/text.glsl.ts
18034
- var vertexShader2 = `#version 300 es
18035
- precision highp float;
18036
-
18037
- // Engine uniforms
18038
- uniform mat3 u_viewProjection;
18039
-
18040
- // Per-text-block uniforms
18041
- uniform mat3 u_textTransform;
18042
- uniform vec4 u_textColor;
18043
- uniform float u_fontSize;
18044
- uniform float u_blockWidth;
18045
- uniform float u_blockHeight;
18046
- uniform float u_lineHeight;
18047
-
18048
- // Character data texture (RGBA32F, 2 texels per character)
18049
- // Texel 0: texOffset.xy, texExtent.xy
18050
- // Texel 1: size.xy, offset.xy
18051
- uniform sampler2D u_charData;
18052
-
18053
- // Text buffer texture (RGBA32F, 1 texel per glyph)
18054
- // Each texel: xy = glyph position, z = char index
18055
- uniform sampler2D u_textBuffer;
18056
-
18057
- // Outputs to fragment shader
18058
- out vec2 v_texcoord;
18059
-
18060
- // Quad vertex positions for a character (matches WGSL)
18061
- const vec2 pos[4] = vec2[4](
18062
- vec2(0.0, -1.0),
18063
- vec2(1.0, -1.0),
18064
- vec2(0.0, 0.0),
18065
- vec2(1.0, 0.0)
18066
- );
17783
+ // src/scene/JumboQuadNode.ts
17784
+ var MAT3_SIZE = 12;
17785
+ var VEC4F_SIZE = 4;
18067
17786
 
18068
- void main() {
18069
- // gl_VertexID gives us 0-3 for the quad vertices
18070
- // gl_InstanceID gives us which glyph we're rendering
18071
- int vertexIndex = gl_VertexID;
18072
- int glyphIndex = gl_InstanceID;
18073
-
18074
- // Fetch glyph data from text buffer texture
18075
- vec4 glyphData = texelFetch(u_textBuffer, ivec2(glyphIndex, 0), 0);
18076
- vec2 glyphPos = glyphData.xy;
18077
- int charIndex = int(glyphData.z);
18078
-
18079
- // Fetch character metrics (2 texels per char)
18080
- // Texel 0: texOffset.x, texOffset.y, texExtent.x, texExtent.y
18081
- // Texel 1: size.x, size.y, offset.x, offset.y
18082
- vec4 charData0 = texelFetch(u_charData, ivec2(charIndex * 2, 0), 0);
18083
- vec4 charData1 = texelFetch(u_charData, ivec2(charIndex * 2 + 1, 0), 0);
18084
-
18085
- vec2 texOffset = charData0.xy;
18086
- vec2 texExtent = charData0.zw;
18087
- vec2 charSize = charData1.xy;
18088
- vec2 charOffset = charData1.zw;
18089
-
18090
- // Center text vertically; origin is mid-height
18091
- vec2 offset = vec2(0.0, -u_blockHeight / 2.0);
18092
-
18093
- // Glyph position in ems (quad pos * size + per-char offset)
18094
- vec2 emPos = pos[vertexIndex] * charSize + charOffset + glyphPos - offset;
18095
- vec2 charPos = emPos * (u_fontSize / u_lineHeight);
18096
-
18097
- // Transform position through model and view-projection matrices
18098
- vec3 worldPos = u_textTransform * vec3(charPos, 1.0);
18099
- vec3 clipPos = u_viewProjection * worldPos;
18100
-
18101
- gl_Position = vec4(clipPos.xy, 0.0, 1.0);
18102
-
18103
- // Calculate texture coordinates
18104
- v_texcoord = pos[vertexIndex] * vec2(1.0, -1.0);
18105
- v_texcoord *= texExtent;
18106
- v_texcoord += texOffset;
17787
+ class JumboQuadNode extends QuadNode {
17788
+ #tiles;
17789
+ #matrixPool;
17790
+ constructor(options, matrixPool) {
17791
+ assert(options.shader, "JumboQuadNode requires a shader to be explicitly provided");
17792
+ assert(options.tiles && options.tiles.length > 0, "JumboQuadNode requires at least one tile to be provided");
17793
+ options.render ??= {
17794
+ shader: options.shader,
17795
+ writeInstance: writeJumboQuadInstance
17796
+ };
17797
+ super({
17798
+ ...options,
17799
+ atlasCoords: options.tiles[0].atlasCoords
17800
+ }, matrixPool);
17801
+ this.#matrixPool = matrixPool;
17802
+ this.#tiles = [];
17803
+ for (const tile of options.tiles) {
17804
+ assert(tile.atlasCoords, "JumboQuadNode requires atlas coords to be provided");
17805
+ assert(tile.size, "JumboQuadNode requires a size to be provided");
17806
+ this.#tiles.push({
17807
+ textureId: tile.textureId,
17808
+ offset: tile.offset,
17809
+ size: tile.size,
17810
+ atlasCoords: tile.atlasCoords
17811
+ });
17812
+ }
17813
+ }
17814
+ get atlasCoords() {
17815
+ throw new Error("JumboQuadNode does not have a single atlas coords");
17816
+ }
17817
+ get tiles() {
17818
+ return this.#tiles;
17819
+ }
17820
+ getTileMatrix(tile) {
17821
+ const matrix = mat3.clone(this.matrix, this.#matrixPool.get());
17822
+ const originalSize = {
17823
+ width: Math.max(...this.#tiles.map((t3) => t3.offset.x + t3.size.width)),
17824
+ height: Math.max(...this.#tiles.map((t3) => t3.offset.y + t3.size.height))
17825
+ };
17826
+ const proportionalSize = {
17827
+ width: this.size.width / originalSize.width,
17828
+ height: this.size.height / originalSize.height
17829
+ };
17830
+ const centerOffset = {
17831
+ x: tile.offset.x + tile.size.width / 2 - originalSize.width / 2,
17832
+ y: -(tile.offset.y + tile.size.height / 2 - originalSize.height / 2)
17833
+ };
17834
+ mat3.translate(matrix, [
17835
+ centerOffset.x * proportionalSize.width,
17836
+ centerOffset.y * proportionalSize.height
17837
+ ], matrix);
17838
+ mat3.scale(matrix, [
17839
+ tile.size.width * proportionalSize.width * (this.flipX ? -1 : 1),
17840
+ tile.size.height * proportionalSize.height * (this.flipY ? -1 : 1)
17841
+ ], matrix);
17842
+ return matrix;
17843
+ }
18107
17844
  }
18108
- `;
18109
- var fragmentShader2 = `#version 300 es
18110
- precision highp float;
18111
-
18112
- // Font texture (MSDF atlas)
18113
- uniform sampler2D u_fontTexture;
18114
-
18115
- // Text color
18116
- uniform vec4 u_textColor;
18117
-
18118
- // Input from vertex shader
18119
- in vec2 v_texcoord;
18120
-
18121
- // Output color
18122
- out vec4 fragColor;
18123
-
18124
- // Signed distance function sampling for MSDF font rendering
18125
- // Median of three: max(min(r,g), min(max(r,g), b))
18126
- float sampleMsdf(vec2 texcoord) {
18127
- vec4 c = texture(u_fontTexture, texcoord);
18128
- return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
17845
+ function writeJumboQuadInstance(node, array, offset) {
17846
+ if (!(node instanceof JumboQuadNode)) {
17847
+ throw new Error("JumboQuadNode.writeJumboQuadInstance can only be called on JumboQuadNodes");
17848
+ }
17849
+ let tileOffset = 0;
17850
+ for (const tile of node.tiles) {
17851
+ const coord = tile.atlasCoords;
17852
+ const matrix = node.getTileMatrix(tile);
17853
+ array.set(matrix, offset + tileOffset);
17854
+ tileOffset += MAT3_SIZE;
17855
+ array.set([node.color.r, node.color.g, node.color.b, node.color.a], offset + tileOffset);
17856
+ tileOffset += VEC4F_SIZE;
17857
+ array.set([
17858
+ coord.uvOffset.x,
17859
+ coord.uvOffset.y,
17860
+ coord.uvScale.width,
17861
+ coord.uvScale.height
17862
+ ], offset + tileOffset);
17863
+ tileOffset += VEC4F_SIZE;
17864
+ const cropRatio = !coord.uvScaleCropped ? { width: 1, height: 1 } : {
17865
+ width: coord.uvScaleCropped.width / coord.uvScale.width,
17866
+ height: coord.uvScaleCropped.height / coord.uvScale.height
17867
+ };
17868
+ array.set([
17869
+ tile.atlasCoords.cropOffset.x / 2 / (tile.atlasCoords.originalSize.width || 1),
17870
+ tile.atlasCoords.cropOffset.y / 2 / (tile.atlasCoords.originalSize.height || 1),
17871
+ cropRatio.width,
17872
+ cropRatio.height
17873
+ ], offset + tileOffset);
17874
+ tileOffset += VEC4F_SIZE;
17875
+ new DataView(array.buffer).setUint32(array.byteOffset + (offset + tileOffset) * Float32Array.BYTES_PER_ELEMENT, coord.atlasIndex, true);
17876
+ tileOffset += VEC4F_SIZE;
17877
+ }
17878
+ node.writeInstance?.(array, offset + tileOffset);
17879
+ return node.tiles.length;
18129
17880
  }
18130
17881
 
18131
- void main() {
18132
- // pxRange (AKA distanceRange) comes from the msdfgen tool
18133
- float pxRange = 4.0;
18134
- vec2 texSize = vec2(textureSize(u_fontTexture, 0));
18135
-
18136
- // Anti-aliasing technique by Paul Houx
18137
- // https://github.com/Chlumsky/msdfgen/issues/22#issuecomment-234958005
18138
- float dx = texSize.x * length(vec2(dFdx(v_texcoord.x), dFdy(v_texcoord.x)));
18139
- float dy = texSize.y * length(vec2(dFdx(v_texcoord.y), dFdy(v_texcoord.y)));
18140
-
18141
- float toPixels = pxRange * inversesqrt(dx * dx + dy * dy);
18142
- float sigDist = sampleMsdf(v_texcoord) - 0.5;
18143
- float pxDist = sigDist * toPixels;
18144
-
18145
- float edgeWidth = 0.5;
18146
- float alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
18147
-
18148
- if (alpha < 0.001) {
18149
- discard;
17882
+ // src/utils/error.ts
17883
+ var warnings = new Map;
17884
+ function warnOnce(key, msg) {
17885
+ if (warnings.has(key)) {
17886
+ return;
18150
17887
  }
17888
+ warnings.set(key, true);
17889
+ console.warn(msg ?? key);
17890
+ }
18151
17891
 
18152
- fragColor = vec4(u_textColor.rgb, u_textColor.a * alpha);
17892
+ // src/text/MsdfFont.ts
17893
+ class MsdfFont {
17894
+ id;
17895
+ json;
17896
+ imageBitmap;
17897
+ name;
17898
+ charset;
17899
+ charCount;
17900
+ lineHeight;
17901
+ charBuffer;
17902
+ #kernings;
17903
+ #chars;
17904
+ #fallbackCharCode;
17905
+ constructor(id, json, imageBitmap) {
17906
+ this.id = id;
17907
+ this.json = json;
17908
+ this.imageBitmap = imageBitmap;
17909
+ const charArray = Object.values(json.chars);
17910
+ this.charCount = charArray.length;
17911
+ this.lineHeight = json.common.lineHeight;
17912
+ this.charset = json.info.charset;
17913
+ this.name = json.info.face;
17914
+ this.#kernings = new Map;
17915
+ if (json.kernings) {
17916
+ for (const kearning of json.kernings) {
17917
+ let charKerning = this.#kernings.get(kearning.first);
17918
+ if (!charKerning) {
17919
+ charKerning = new Map;
17920
+ this.#kernings.set(kearning.first, charKerning);
17921
+ }
17922
+ charKerning.set(kearning.second, kearning.amount);
17923
+ }
17924
+ }
17925
+ this.#chars = new Map;
17926
+ const charCount = Object.values(json.chars).length;
17927
+ this.charBuffer = new Float32Array(charCount * 8);
17928
+ let offset = 0;
17929
+ const u3 = 1 / json.common.scaleW;
17930
+ const v3 = 1 / json.common.scaleH;
17931
+ for (const [i3, char] of json.chars.entries()) {
17932
+ this.#chars.set(char.id, char);
17933
+ this.#chars.get(char.id).charIndex = i3;
17934
+ this.charBuffer[offset] = char.x * u3;
17935
+ this.charBuffer[offset + 1] = char.y * v3;
17936
+ this.charBuffer[offset + 2] = char.width * u3;
17937
+ this.charBuffer[offset + 3] = char.height * v3;
17938
+ this.charBuffer[offset + 4] = char.width;
17939
+ this.charBuffer[offset + 5] = char.height;
17940
+ this.charBuffer[offset + 6] = char.xoffset;
17941
+ this.charBuffer[offset + 7] = -char.yoffset;
17942
+ offset += 8;
17943
+ }
17944
+ }
17945
+ getChar(charCode) {
17946
+ const char = this.#chars.get(charCode);
17947
+ if (!char) {
17948
+ const fallbackCharacter = this.#chars.get(this.#fallbackCharCode ?? this.#chars.keys().toArray()[0]);
17949
+ warnOnce(`unknown_char_${this.name}`, `Couldn't find character ${charCode} in characters for font ${this.name} -- defaulting to first available character "${fallbackCharacter.char}"`);
17950
+ return fallbackCharacter;
17951
+ }
17952
+ return char;
17953
+ }
17954
+ getXAdvance(charCode, nextCharCode = -1) {
17955
+ const char = this.getChar(charCode);
17956
+ if (nextCharCode >= 0) {
17957
+ const kerning = this.#kernings.get(charCode);
17958
+ if (kerning) {
17959
+ return char.xadvance + (kerning.get(nextCharCode) ?? 0);
17960
+ }
17961
+ }
17962
+ return char.xadvance;
17963
+ }
17964
+ static async create(id, fontJsonUrl) {
17965
+ const response = await fetch(fontJsonUrl);
17966
+ const json = await response.json();
17967
+ const i3 = fontJsonUrl.href.lastIndexOf("/");
17968
+ const baseUrl = i3 !== -1 ? fontJsonUrl.href.substring(0, i3 + 1) : undefined;
17969
+ if (json.pages.length < 1) {
17970
+ throw new Error(`Can't create an msdf font without a reference to the page url in the json`);
17971
+ }
17972
+ if (json.pages.length > 1) {
17973
+ throw new Error(`Can't create an msdf font with more than one page`);
17974
+ }
17975
+ const textureUrl = baseUrl + json.pages[0];
17976
+ const textureResponse = await fetch(textureUrl);
17977
+ const bitmap = await createImageBitmap(await textureResponse.blob());
17978
+ return new MsdfFont(id, json, bitmap);
17979
+ }
17980
+ set fallbackCharacter(character) {
17981
+ const charCode = character.charCodeAt(0);
17982
+ if (this.#chars.has(charCode)) {
17983
+ this.#fallbackCharCode = charCode;
17984
+ } else {
17985
+ const fallbackCode = this.#chars.keys().toArray()[0];
17986
+ console.warn(`${character} character does not exist in font ${this.name} defaulting to "${this.#chars.get(fallbackCode)?.char}".`);
17987
+ this.#fallbackCharCode = fallbackCode;
17988
+ }
17989
+ }
18153
17990
  }
18154
- `;
18155
17991
 
18156
- // src/backends/webgl2/WebGLTextShader.ts
18157
- class WebGLTextShader {
18158
- label = "text";
18159
- code = fragmentShader2;
18160
- font;
18161
- maxCharCount;
18162
- #backend;
18163
- #pipeline;
18164
- #program;
18165
- #vao;
18166
- #cpuTextBuffer;
18167
- #cachedUniform = null;
18168
- #uViewProjection = null;
18169
- #uTextTransform = null;
18170
- #uTextColor = null;
18171
- #uFontSize = null;
18172
- #uBlockWidth = null;
18173
- #uBlockHeight = null;
18174
- #uLineHeight = null;
18175
- #uCharData = null;
18176
- #uTextBuffer = null;
18177
- #uFontTexture = null;
18178
- constructor(backend, pipeline) {
18179
- this.#backend = backend;
18180
- this.#pipeline = pipeline;
18181
- this.font = pipeline.font;
18182
- this.maxCharCount = pipeline.maxCharCount;
18183
- const gl = backend.gl;
18184
- const vs = this.#compileShader(gl, gl.VERTEX_SHADER, vertexShader2);
18185
- const fs = this.#compileShader(gl, gl.FRAGMENT_SHADER, fragmentShader2);
18186
- const program = gl.createProgram();
18187
- assert(program, "Failed to create WebGL program");
18188
- gl.attachShader(program, vs);
18189
- gl.attachShader(program, fs);
18190
- gl.linkProgram(program);
18191
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
18192
- const info = gl.getProgramInfoLog(program);
18193
- throw new Error(`Failed to link text shader program: ${info}`);
17992
+ // src/text/shaping.ts
17993
+ var TAB_SPACES = 4;
17994
+ function shapeText(font, text, blockSize, fontSize, formatting, textArray, initialFloatOffset = 0, debug = false) {
17995
+ let offset = initialFloatOffset;
17996
+ const measurements = measureText(font, text, formatting.wordWrap);
17997
+ const alignment = formatting.align || "left";
17998
+ const em2px = fontSize / font.lineHeight;
17999
+ const hackHasExplicitBlock = blockSize.width !== measurements.width;
18000
+ let debugData = null;
18001
+ if (debug) {
18002
+ debugData = [];
18003
+ }
18004
+ for (const word of measurements.words) {
18005
+ for (const glyph of word.glyphs) {
18006
+ let lineOffset = 0;
18007
+ if (alignment === "center") {
18008
+ lineOffset = measurements.width * -0.5 - (measurements.width - measurements.lineWidths[glyph.line]) * -0.5;
18009
+ } else if (alignment === "right") {
18010
+ const blockSizeEm = blockSize.width / em2px;
18011
+ const delta = measurements.width - measurements.lineWidths[glyph.line];
18012
+ lineOffset = (hackHasExplicitBlock ? blockSizeEm / 2 : measurements.width / 2) - measurements.width + delta;
18013
+ } else if (alignment === "left") {
18014
+ const blockSizeEm = blockSize.width / em2px;
18015
+ lineOffset = hackHasExplicitBlock ? -blockSizeEm / 2 : -measurements.width / 2;
18016
+ }
18017
+ if (debug && debugData) {
18018
+ debugData.push({
18019
+ line: glyph.line,
18020
+ word: word.glyphs.map((g3) => g3.char.char).join(""),
18021
+ glyph: glyph.char.char,
18022
+ startX: word.startX,
18023
+ glyphX: glyph.offset[0],
18024
+ advance: glyph.char.xadvance,
18025
+ lineOffset,
18026
+ startY: word.startY,
18027
+ glyphY: glyph.offset[1]
18028
+ });
18029
+ }
18030
+ textArray[offset] = word.startX + glyph.offset[0] + lineOffset;
18031
+ textArray[offset + 1] = word.startY + glyph.offset[1];
18032
+ textArray[offset + 2] = glyph.char.charIndex;
18033
+ offset += 4;
18194
18034
  }
18195
- this.#program = program;
18196
- this.#uViewProjection = gl.getUniformLocation(program, "u_viewProjection");
18197
- this.#uTextTransform = gl.getUniformLocation(program, "u_textTransform");
18198
- this.#uTextColor = gl.getUniformLocation(program, "u_textColor");
18199
- this.#uFontSize = gl.getUniformLocation(program, "u_fontSize");
18200
- this.#uBlockWidth = gl.getUniformLocation(program, "u_blockWidth");
18201
- this.#uBlockHeight = gl.getUniformLocation(program, "u_blockHeight");
18202
- this.#uLineHeight = gl.getUniformLocation(program, "u_lineHeight");
18203
- this.#uCharData = gl.getUniformLocation(program, "u_charData");
18204
- this.#uTextBuffer = gl.getUniformLocation(program, "u_textBuffer");
18205
- this.#uFontTexture = gl.getUniformLocation(program, "u_fontTexture");
18206
- const vao = gl.createVertexArray();
18207
- assert(vao, "Failed to create WebGL VAO");
18208
- this.#vao = vao;
18209
- this.#cpuTextBuffer = new Float32Array(this.maxCharCount * 4);
18210
- gl.deleteShader(vs);
18211
- gl.deleteShader(fs);
18212
18035
  }
18213
- startFrame(uniform) {
18214
- this.#cachedUniform = uniform;
18036
+ if (debug && debugData) {
18037
+ console.table(debugData);
18215
18038
  }
18216
- processBatch(nodes) {
18217
- if (nodes.length === 0)
18218
- return 0;
18219
- const gl = this.#backend.gl;
18220
- const uniform = this.#cachedUniform;
18221
- if (!uniform) {
18222
- throw new Error("Tried to process batch but engine uniform is not set");
18223
- }
18224
- gl.useProgram(this.#program);
18225
- gl.bindVertexArray(this.#vao);
18226
- if (this.#uViewProjection) {
18227
- const m3 = uniform.viewProjectionMatrix;
18228
- const mat3x3 = new Float32Array([
18229
- m3[0],
18230
- m3[1],
18231
- m3[2],
18232
- m3[4],
18233
- m3[5],
18234
- m3[6],
18235
- m3[8],
18236
- m3[9],
18237
- m3[10]
18238
- ]);
18239
- gl.uniformMatrix3fv(this.#uViewProjection, false, mat3x3);
18039
+ }
18040
+ function measureText(font, text, wordWrap) {
18041
+ let maxWidth = 0;
18042
+ const lineWidths = [];
18043
+ let textOffsetX = 0;
18044
+ let textOffsetY = 0;
18045
+ let line = 0;
18046
+ let printedCharCount = 0;
18047
+ let nextCharCode = text.charCodeAt(0);
18048
+ let word = { glyphs: [], width: 0, startX: 0, startY: 0 };
18049
+ const words = [];
18050
+ for (let i3 = 0;i3 < text.length; i3++) {
18051
+ const isLastLetter = i3 === text.length - 1;
18052
+ const charCode = nextCharCode;
18053
+ nextCharCode = i3 < text.length - 1 ? text.charCodeAt(i3 + 1) : -1;
18054
+ switch (charCode) {
18055
+ case 9 /* HorizontalTab */:
18056
+ insertSpaces(TAB_SPACES);
18057
+ break;
18058
+ case 10 /* Newline */:
18059
+ flushLine();
18060
+ flushWord();
18061
+ break;
18062
+ case 13 /* CarriageReturn */:
18063
+ break;
18064
+ case 32 /* Space */:
18065
+ insertSpaces(1);
18066
+ break;
18067
+ default: {
18068
+ const advance = font.getXAdvance(charCode, nextCharCode);
18069
+ if (wordWrap && wordWrap.breakOn === "character" && textOffsetX + advance > wordWrap.emWidth) {
18070
+ if (word.startX === 0) {
18071
+ flushWord();
18072
+ } else {
18073
+ lineWidths.push(textOffsetX - word.width);
18074
+ line++;
18075
+ maxWidth = Math.max(maxWidth, textOffsetX);
18076
+ textOffsetX = word.width;
18077
+ textOffsetY -= font.lineHeight;
18078
+ word.startX = 0;
18079
+ word.startY = textOffsetY;
18080
+ word.glyphs.forEach((g3) => {
18081
+ g3.line = line;
18082
+ });
18083
+ }
18084
+ }
18085
+ word.glyphs.push({
18086
+ char: font.getChar(charCode),
18087
+ offset: [word.width, 0],
18088
+ line
18089
+ });
18090
+ if (isLastLetter) {
18091
+ flushWord();
18092
+ }
18093
+ word.width += advance;
18094
+ textOffsetX += advance;
18095
+ }
18240
18096
  }
18241
- if (this.#uLineHeight) {
18242
- gl.uniform1f(this.#uLineHeight, this.#pipeline.lineHeight);
18097
+ }
18098
+ lineWidths.push(textOffsetX);
18099
+ maxWidth = Math.max(maxWidth, textOffsetX);
18100
+ const lineCount = lineWidths.length;
18101
+ return {
18102
+ width: maxWidth,
18103
+ height: lineCount * font.lineHeight,
18104
+ lineWidths,
18105
+ lineCount,
18106
+ printedCharCount,
18107
+ words
18108
+ };
18109
+ function flushWord() {
18110
+ printedCharCount += word.glyphs.length;
18111
+ words.push(word);
18112
+ word = {
18113
+ glyphs: [],
18114
+ width: 0,
18115
+ startX: textOffsetX,
18116
+ startY: textOffsetY
18117
+ };
18118
+ }
18119
+ function flushLine() {
18120
+ lineWidths.push(textOffsetX);
18121
+ line++;
18122
+ maxWidth = Math.max(maxWidth, textOffsetX);
18123
+ textOffsetX = 0;
18124
+ textOffsetY -= font.lineHeight;
18125
+ }
18126
+ function insertSpaces(spaces) {
18127
+ if (spaces < 1)
18128
+ spaces = 1;
18129
+ textOffsetX += font.getXAdvance(32 /* Space */) * spaces;
18130
+ if (wordWrap?.breakOn === "word" && textOffsetX >= wordWrap.emWidth) {
18131
+ flushLine();
18243
18132
  }
18244
- gl.activeTexture(gl.TEXTURE0);
18245
- gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.fontTexture);
18246
- if (this.#uFontTexture) {
18247
- gl.uniform1i(this.#uFontTexture, 0);
18133
+ flushWord();
18134
+ }
18135
+ }
18136
+ function findLargestFontSize(font, text, size, formatting) {
18137
+ if (!formatting.fontSize) {
18138
+ throw new Error("fontSize is required for shrinkToFit");
18139
+ }
18140
+ if (!formatting.shrinkToFit) {
18141
+ throw new Error("shrinkToFit is required for findLargestFontSize");
18142
+ }
18143
+ const minSize = formatting.shrinkToFit.minFontSize;
18144
+ const maxSize = formatting.shrinkToFit.maxFontSize ?? formatting.fontSize;
18145
+ const maxLines = formatting.shrinkToFit.maxLines ?? Number.POSITIVE_INFINITY;
18146
+ const threshold = 0.5;
18147
+ let low = minSize;
18148
+ let high = maxSize;
18149
+ while (high - low > threshold) {
18150
+ const testSize = (low + high) / 2;
18151
+ const testMeasure = measureText(font, text, formatting.wordWrap);
18152
+ const padding = formatting.shrinkToFit.padding ?? 0;
18153
+ const scaledWidth = testMeasure.width * (testSize / font.lineHeight);
18154
+ const scaledHeight = testMeasure.height * (testSize / font.lineHeight);
18155
+ const fitsWidth = scaledWidth <= size.width - size.width * padding;
18156
+ const fitsHeight = scaledHeight <= size.height - size.height * padding;
18157
+ const fitsLines = testMeasure.lineCount <= maxLines;
18158
+ if (fitsWidth && fitsHeight && fitsLines) {
18159
+ low = testSize;
18160
+ } else {
18161
+ high = testSize;
18248
18162
  }
18249
- gl.activeTexture(gl.TEXTURE1);
18250
- gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.charDataTexture);
18251
- if (this.#uCharData) {
18252
- gl.uniform1i(this.#uCharData, 1);
18163
+ }
18164
+ return low;
18165
+ }
18166
+
18167
+ // src/scene/TextNode.ts
18168
+ var DEFAULT_FONT_SIZE = 14;
18169
+
18170
+ class TextNode extends SceneNode {
18171
+ #text;
18172
+ #formatting;
18173
+ #font;
18174
+ constructor(shader, text, opts = {}) {
18175
+ const { width, height } = measureText(shader.font, text, opts.wordWrap);
18176
+ if (text.length > shader.maxCharCount) {
18177
+ throw new Error(`Text: ${text} exceeds ${shader.maxCharCount} characters. Try using fewer characters or increase the limit in Toodle.attach.`);
18253
18178
  }
18254
- gl.activeTexture(gl.TEXTURE2);
18255
- gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.textBufferTexture);
18256
- if (this.#uTextBuffer) {
18257
- gl.uniform1i(this.#uTextBuffer, 2);
18179
+ const em2px = shader.font.lineHeight / (opts.fontSize ?? DEFAULT_FONT_SIZE);
18180
+ if (!opts.shrinkToFit && !opts.size) {
18181
+ opts.size = { width: width / em2px, height: height / em2px };
18258
18182
  }
18259
- for (const node of nodes) {
18260
- if (!(node instanceof TextNode)) {
18261
- console.error(node);
18262
- throw new Error(`Tried to use WebGLTextShader on something that isn't a TextNode: ${node}`);
18263
- }
18264
- const text = node.text;
18265
- const formatting = node.formatting;
18266
- const measurements = measureText(this.font, text, formatting.wordWrap);
18267
- const size = node.size ?? measurements;
18268
- const fontSize = formatting.shrinkToFit ? findLargestFontSize(this.font, text, size, formatting) : formatting.fontSize;
18269
- const actualFontSize = fontSize || DEFAULT_FONT_SIZE;
18270
- shapeText(this.font, text, size, actualFontSize, formatting, this.#cpuTextBuffer, 0);
18271
- this.#pipeline.updateTextBuffer(this.#cpuTextBuffer, measurements.printedCharCount);
18272
- if (this.#uTextTransform) {
18273
- const m3 = node.matrix;
18274
- const mat3x3 = new Float32Array([
18275
- m3[0],
18276
- m3[1],
18277
- m3[2],
18278
- m3[4],
18279
- m3[5],
18280
- m3[6],
18281
- m3[8],
18282
- m3[9],
18283
- m3[10]
18284
- ]);
18285
- gl.uniformMatrix3fv(this.#uTextTransform, false, mat3x3);
18286
- }
18287
- if (this.#uTextColor) {
18288
- const tint = node.tint;
18289
- gl.uniform4f(this.#uTextColor, tint.r, tint.g, tint.b, tint.a);
18290
- }
18291
- if (this.#uFontSize) {
18292
- gl.uniform1f(this.#uFontSize, actualFontSize);
18293
- }
18294
- if (this.#uBlockWidth) {
18295
- gl.uniform1f(this.#uBlockWidth, formatting.align === "center" ? 0 : measurements.width);
18296
- }
18297
- if (this.#uBlockHeight) {
18298
- gl.uniform1f(this.#uBlockHeight, measurements.height);
18183
+ super({
18184
+ ...opts,
18185
+ render: {
18186
+ shader,
18187
+ writeInstance: (_node, _array, _offset) => {
18188
+ throw new Error("not implemented - needs access to text uniform buffer, dimensions and a model matrix");
18189
+ }
18299
18190
  }
18300
- gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, measurements.printedCharCount);
18191
+ });
18192
+ this.#font = shader.font;
18193
+ this.#text = text;
18194
+ this.#formatting = opts;
18195
+ }
18196
+ get text() {
18197
+ return this.#text;
18198
+ }
18199
+ get formatting() {
18200
+ return this.#formatting;
18201
+ }
18202
+ get font() {
18203
+ return this.#font;
18204
+ }
18205
+ set text(text) {
18206
+ if (!text) {
18207
+ throw new Error("text cannot be empty");
18301
18208
  }
18302
- gl.bindVertexArray(null);
18303
- return nodes.length;
18209
+ this.#text = text;
18210
+ this.setDirty();
18304
18211
  }
18305
- endFrame() {}
18306
- #compileShader(gl, type, source) {
18307
- const shader = gl.createShader(type);
18308
- assert(shader, "Failed to create WebGL shader");
18309
- gl.shaderSource(shader, source);
18310
- gl.compileShader(shader);
18311
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
18312
- const info = gl.getShaderInfoLog(shader);
18313
- const typeStr = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
18314
- gl.deleteShader(shader);
18315
- throw new Error(`Failed to compile ${typeStr} shader: ${info}`);
18212
+ get tint() {
18213
+ return this.#formatting.color || { r: 1, g: 1, b: 1, a: 1 };
18214
+ }
18215
+ set tint(tint) {
18216
+ this.#formatting.color = tint;
18217
+ this.setDirty();
18218
+ }
18219
+ set formatting(formatting) {
18220
+ this.#formatting = formatting;
18221
+ this.setDirty();
18222
+ }
18223
+ }
18224
+
18225
+ // src/backends/webgl2/WebGLFontPipeline.ts
18226
+ class WebGLFontPipeline2 {
18227
+ font;
18228
+ fontTexture;
18229
+ charDataTexture;
18230
+ textBufferTexture;
18231
+ maxCharCount;
18232
+ lineHeight;
18233
+ #gl;
18234
+ constructor(gl, font, fontTexture, charDataTexture, textBufferTexture, maxCharCount) {
18235
+ this.#gl = gl;
18236
+ this.font = font;
18237
+ this.fontTexture = fontTexture;
18238
+ this.charDataTexture = charDataTexture;
18239
+ this.textBufferTexture = textBufferTexture;
18240
+ this.maxCharCount = maxCharCount;
18241
+ this.lineHeight = font.lineHeight;
18242
+ }
18243
+ static create(gl, font, maxCharCount) {
18244
+ const fontTexture = gl.createTexture();
18245
+ assert(fontTexture, "Failed to create font texture");
18246
+ gl.bindTexture(gl.TEXTURE_2D, fontTexture);
18247
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
18248
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
18249
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
18250
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
18251
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, font.imageBitmap);
18252
+ const charDataTexture = gl.createTexture();
18253
+ assert(charDataTexture, "Failed to create char data texture");
18254
+ gl.bindTexture(gl.TEXTURE_2D, charDataTexture);
18255
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
18256
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
18257
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
18258
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
18259
+ const charCount = font.charCount;
18260
+ const charTextureWidth = charCount * 2;
18261
+ const charTextureData = new Float32Array(charTextureWidth * 4);
18262
+ for (let i3 = 0;i3 < charCount; i3++) {
18263
+ const srcOffset = i3 * 8;
18264
+ const dstOffset0 = i3 * 2 * 4;
18265
+ const dstOffset1 = (i3 * 2 + 1) * 4;
18266
+ charTextureData[dstOffset0] = font.charBuffer[srcOffset];
18267
+ charTextureData[dstOffset0 + 1] = font.charBuffer[srcOffset + 1];
18268
+ charTextureData[dstOffset0 + 2] = font.charBuffer[srcOffset + 2];
18269
+ charTextureData[dstOffset0 + 3] = font.charBuffer[srcOffset + 3];
18270
+ charTextureData[dstOffset1] = font.charBuffer[srcOffset + 4];
18271
+ charTextureData[dstOffset1 + 1] = font.charBuffer[srcOffset + 5];
18272
+ charTextureData[dstOffset1 + 2] = font.charBuffer[srcOffset + 6];
18273
+ charTextureData[dstOffset1 + 3] = font.charBuffer[srcOffset + 7];
18316
18274
  }
18317
- return shader;
18275
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, charTextureWidth, 1, 0, gl.RGBA, gl.FLOAT, charTextureData);
18276
+ const textBufferTexture = gl.createTexture();
18277
+ assert(textBufferTexture, "Failed to create text buffer texture");
18278
+ gl.bindTexture(gl.TEXTURE_2D, textBufferTexture);
18279
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
18280
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
18281
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
18282
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
18283
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, maxCharCount, 1, 0, gl.RGBA, gl.FLOAT, null);
18284
+ gl.bindTexture(gl.TEXTURE_2D, null);
18285
+ return new WebGLFontPipeline2(gl, font, fontTexture, charDataTexture, textBufferTexture, maxCharCount);
18286
+ }
18287
+ updateTextBuffer(data, glyphCount) {
18288
+ const gl = this.#gl;
18289
+ gl.bindTexture(gl.TEXTURE_2D, this.textBufferTexture);
18290
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, glyphCount, 1, gl.RGBA, gl.FLOAT, data);
18318
18291
  }
18319
18292
  destroy() {
18320
- const gl = this.#backend.gl;
18321
- gl.deleteProgram(this.#program);
18322
- gl.deleteVertexArray(this.#vao);
18323
- this.#pipeline.destroy();
18293
+ const gl = this.#gl;
18294
+ gl.deleteTexture(this.fontTexture);
18295
+ gl.deleteTexture(this.charDataTexture);
18296
+ gl.deleteTexture(this.textBufferTexture);
18324
18297
  }
18325
18298
  }
18326
18299
 
18327
- // src/backends/webgpu/wgsl/text.wgsl.ts
18328
- var text_wgsl_default = `
18329
- // Adapted from: https://webgpu.github.io/webgpu-samples/?sample=textRenderingMsdf
18300
+ // src/backends/webgl2/glsl/text.glsl.ts
18301
+ var vertexShader2 = `#version 300 es
18302
+ precision highp float;
18330
18303
 
18331
- // Quad vertex positions for a character
18332
- const pos = array(
18333
- vec2f(0, -1),
18334
- vec2f(1, -1),
18335
- vec2f(0, 0),
18336
- vec2f(1, 0),
18337
- );
18304
+ // Engine uniforms
18305
+ uniform mat3 u_viewProjection;
18338
18306
 
18339
- // Debug colors for visualization
18340
- const debugColors = array(
18341
- vec4f(1, 0, 0, 1),
18342
- vec4f(0, 1, 0, 1),
18343
- vec4f(0, 0, 1, 1),
18344
- vec4f(1, 1, 1, 1),
18345
- );
18307
+ // Per-text-block uniforms
18308
+ uniform mat3 u_textTransform;
18309
+ uniform vec4 u_textColor;
18310
+ uniform float u_fontSize;
18311
+ uniform float u_blockWidth;
18312
+ uniform float u_blockHeight;
18313
+ uniform float u_lineHeight;
18346
18314
 
18347
- // Vertex input from GPU
18348
- struct VertexInput {
18349
- @builtin(vertex_index) vertex: u32,
18350
- @builtin(instance_index) instance: u32,
18351
- };
18315
+ // Character data texture (RGBA32F, 2 texels per character)
18316
+ // Texel 0: texOffset.xy, texExtent.xy
18317
+ // Texel 1: size.xy, offset.xy
18318
+ uniform sampler2D u_charData;
18352
18319
 
18353
- // Output from vertex shader to fragment shader
18354
- struct VertexOutput {
18355
- @builtin(position) position: vec4f,
18356
- @location(0) texcoord: vec2f,
18357
- @location(1) debugColor: vec4f,
18358
- @location(2) @interpolate(flat) instanceIndex: u32,
18359
- };
18320
+ // Text buffer texture (RGBA32F, 1 texel per glyph)
18321
+ // Each texel: xy = glyph position, z = char index
18322
+ uniform sampler2D u_textBuffer;
18360
18323
 
18361
- // Metadata for a single character glyph
18362
- struct Char {
18363
- texOffset: vec2f, // Offset to top-left in MSDF texture (pixels)
18364
- texExtent: vec2f, // Size in texture (pixels)
18365
- size: vec2f, // Glyph size in ems
18366
- offset: vec2f, // Position offset in ems
18367
- };
18324
+ // Outputs to fragment shader
18325
+ out vec2 v_texcoord;
18368
18326
 
18369
- // Metadata for a text block
18370
- struct TextBlockDescriptor {
18371
- transform: mat3x3f, // Text transform matrix (model matrix)
18372
- color: vec4f, // Text color
18373
- fontSize: f32, // Font size
18374
- blockWidth: f32, // Total width of text block
18375
- blockHeight: f32, // Total height of text block
18376
- bufferPosition: f32 // Index and length in textBuffer
18377
- };
18327
+ // Quad vertex positions for a character (matches WGSL)
18328
+ const vec2 pos[4] = vec2[4](
18329
+ vec2(0.0, -1.0),
18330
+ vec2(1.0, -1.0),
18331
+ vec2(0.0, 0.0),
18332
+ vec2(1.0, 0.0)
18333
+ );
18378
18334
 
18379
- // Font bindings
18380
- @group(0) @binding(0) var fontTexture: texture_2d<f32>;
18381
- @group(0) @binding(1) var fontSampler: sampler;
18382
- @group(0) @binding(2) var<storage> chars: array<Char>;
18383
- @group(0) @binding(3) var<uniform> fontData: vec4f; // Contains line height (x)
18335
+ void main() {
18336
+ // gl_VertexID gives us 0-3 for the quad vertices
18337
+ // gl_InstanceID gives us which glyph we're rendering
18338
+ int vertexIndex = gl_VertexID;
18339
+ int glyphIndex = gl_InstanceID;
18384
18340
 
18385
- // Text bindings
18386
- @group(1) @binding(0) var<storage> texts: array<TextBlockDescriptor>;
18387
- @group(1) @binding(1) var<storage> textBuffer: array<vec4f>; // Each vec4: xy = glyph pos, z = char index
18341
+ // Fetch glyph data from text buffer texture
18342
+ vec4 glyphData = texelFetch(u_textBuffer, ivec2(glyphIndex, 0), 0);
18343
+ vec2 glyphPos = glyphData.xy;
18344
+ int charIndex = int(glyphData.z);
18388
18345
 
18389
- // Global uniforms
18390
- @group(2) @binding(0) var<uniform> viewProjectionMatrix: mat3x3f;
18346
+ // Fetch character metrics (2 texels per char)
18347
+ // Texel 0: texOffset.x, texOffset.y, texExtent.x, texExtent.y
18348
+ // Texel 1: size.x, size.y, offset.x, offset.y
18349
+ vec4 charData0 = texelFetch(u_charData, ivec2(charIndex * 2, 0), 0);
18350
+ vec4 charData1 = texelFetch(u_charData, ivec2(charIndex * 2 + 1, 0), 0);
18391
18351
 
18392
- // Vertex shader
18393
- @vertex
18394
- fn vertexMain(input: VertexInput) -> VertexOutput {
18395
- // Because the instance index is used for character indexing, we are
18396
- // overloading the vertex index to store the instance of the text metadata.
18397
- //
18398
- // I.e...
18399
- // Vertex 0-4 = Instance 0, Vertex 0-4
18400
- // Vertex 4-8 = Instance 1, Vertex 0-4
18401
- // Vertex 8-12 = Instance 2, Vertex 0-4
18402
- let vertexIndex = input.vertex % 4;
18403
- let textIndex = input.vertex / 4;
18352
+ vec2 texOffset = charData0.xy;
18353
+ vec2 texExtent = charData0.zw;
18354
+ vec2 charSize = charData1.xy;
18355
+ vec2 charOffset = charData1.zw;
18404
18356
 
18405
- let text = texts[textIndex];
18406
- let textElement = textBuffer[u32(text.bufferPosition) + input.instance];
18407
- let char = chars[u32(textElement.z)];
18357
+ // Center text vertically; origin is mid-height
18358
+ vec2 offset = vec2(0.0, -u_blockHeight / 2.0);
18408
18359
 
18409
- let lineHeight = fontData.x;
18410
- let textWidth = text.blockWidth;
18411
- let textHeight = text.blockHeight;
18360
+ // Glyph position in ems (quad pos * size + per-char offset)
18361
+ vec2 emPos = pos[vertexIndex] * charSize + charOffset + glyphPos - offset;
18362
+ vec2 charPos = emPos * (u_fontSize / u_lineHeight);
18412
18363
 
18413
- // Center text vertically; origin is mid-height
18414
- let offset = vec2f(0, -textHeight / 2);
18364
+ // Transform position through model and view-projection matrices
18365
+ vec3 worldPos = u_textTransform * vec3(charPos, 1.0);
18366
+ vec3 clipPos = u_viewProjection * worldPos;
18367
+
18368
+ gl_Position = vec4(clipPos.xy, 0.0, 1.0);
18369
+
18370
+ // Calculate texture coordinates
18371
+ v_texcoord = pos[vertexIndex] * vec2(1.0, -1.0);
18372
+ v_texcoord *= texExtent;
18373
+ v_texcoord += texOffset;
18374
+ }
18375
+ `;
18376
+ var fragmentShader2 = `#version 300 es
18377
+ precision highp float;
18415
18378
 
18416
- // Glyph position in ems (quad pos * size + per-char offset)
18417
- let emPos = pos[vertexIndex] * char.size + char.offset + textElement.xy - offset;
18418
- let charPos = emPos * (text.fontSize / lineHeight);
18379
+ // Font texture (MSDF atlas)
18380
+ uniform sampler2D u_fontTexture;
18419
18381
 
18420
- var output: VertexOutput;
18421
- let transformedPosition = viewProjectionMatrix * text.transform * vec3f(charPos, 1);
18382
+ // Text color
18383
+ uniform vec4 u_textColor;
18422
18384
 
18423
- output.position = vec4f(transformedPosition, 1);
18424
- output.texcoord = pos[vertexIndex] * vec2f(1, -1);
18425
- output.texcoord *= char.texExtent;
18426
- output.texcoord += char.texOffset;
18427
- output.debugColor = debugColors[vertexIndex];
18428
- output.instanceIndex = textIndex;
18429
- return output;
18385
+ // Input from vertex shader
18386
+ in vec2 v_texcoord;
18430
18387
 
18431
- // To debug - hardcode quad in bottom right quarter of the screen:
18432
- // output.position = vec4f(pos[input.vertex], 0, 1);
18433
- }
18388
+ // Output color
18389
+ out vec4 fragColor;
18434
18390
 
18435
18391
  // Signed distance function sampling for MSDF font rendering
18436
- fn sampleMsdf(texcoord: vec2f) -> f32 {
18437
- let c = textureSample(fontTexture, fontSampler, texcoord);
18392
+ // Median of three: max(min(r,g), min(max(r,g), b))
18393
+ float sampleMsdf(vec2 texcoord) {
18394
+ vec4 c = texture(u_fontTexture, texcoord);
18438
18395
  return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
18439
18396
  }
18440
18397
 
18441
- // Fragment shader
18442
- // Anti-aliasing technique by Paul Houx
18443
- // more details here:
18444
- // https://github.com/Chlumsky/msdfgen/issues/22#issuecomment-234958005
18445
- @fragment
18446
- fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
18447
- let text = texts[input.instanceIndex];
18448
-
18449
- // pxRange (AKA distanceRange) comes from the msdfgen tool.
18450
- let pxRange = 4.0;
18451
- let texSize = vec2f(textureDimensions(fontTexture, 0));
18398
+ void main() {
18399
+ // pxRange (AKA distanceRange) comes from the msdfgen tool
18400
+ float pxRange = 4.0;
18401
+ vec2 texSize = vec2(textureSize(u_fontTexture, 0));
18452
18402
 
18453
- let dx = texSize.x * length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));
18454
- let dy = texSize.y * length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));
18403
+ // Anti-aliasing technique by Paul Houx
18404
+ // https://github.com/Chlumsky/msdfgen/issues/22#issuecomment-234958005
18405
+ float dx = texSize.x * length(vec2(dFdx(v_texcoord.x), dFdy(v_texcoord.x)));
18406
+ float dy = texSize.y * length(vec2(dFdx(v_texcoord.y), dFdy(v_texcoord.y)));
18455
18407
 
18456
- let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
18457
- let sigDist = sampleMsdf(input.texcoord) - 0.5;
18458
- let pxDist = sigDist * toPixels;
18408
+ float toPixels = pxRange * inversesqrt(dx * dx + dy * dy);
18409
+ float sigDist = sampleMsdf(v_texcoord) - 0.5;
18410
+ float pxDist = sigDist * toPixels;
18459
18411
 
18460
- let edgeWidth = 0.5;
18461
- let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
18412
+ float edgeWidth = 0.5;
18413
+ float alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
18462
18414
 
18463
18415
  if (alpha < 0.001) {
18464
18416
  discard;
18465
18417
  }
18466
18418
 
18467
- let msdfColor = vec4f(text.color.rgb, text.color.a * alpha);
18468
- return msdfColor;
18469
-
18470
- // Debug options:
18471
- // return text.color;
18472
- // return input.debugColor;
18473
- // return vec4f(1, 0, 1, 1); // hardcoded magenta
18474
- // return textureSample(fontTexture, fontSampler, input.texcoord);
18419
+ fragColor = vec4(u_textColor.rgb, u_textColor.a * alpha);
18475
18420
  }
18476
18421
  `;
18477
18422
 
18478
- // src/backends/webgpu/FontPipeline.ts
18479
- class FontPipeline {
18480
- pipeline;
18481
- font;
18482
- fontBindGroup;
18483
- maxCharCount;
18484
- constructor(pipeline, font, fontBindGroup, maxCharCount) {
18485
- this.pipeline = pipeline;
18486
- this.font = font;
18487
- this.fontBindGroup = fontBindGroup;
18488
- this.maxCharCount = maxCharCount;
18489
- }
18490
- static async create(device, font, colorFormat, maxCharCount) {
18491
- const pipeline = await pipelinePromise(device, colorFormat, font.name);
18492
- const texture = device.createTexture({
18493
- label: `MSDF font ${font.name}`,
18494
- size: [font.imageBitmap.width, font.imageBitmap.height, 1],
18495
- format: "rgba8unorm",
18496
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
18497
- });
18498
- device.queue.copyExternalImageToTexture({ source: font.imageBitmap }, { texture }, [font.imageBitmap.width, font.imageBitmap.height]);
18499
- const charsGpuBuffer = device.createBuffer({
18500
- label: `MSDF font ${font.name} character layout buffer`,
18501
- size: font.charCount * Float32Array.BYTES_PER_ELEMENT * 8,
18502
- usage: GPUBufferUsage.STORAGE,
18503
- mappedAtCreation: true
18504
- });
18505
- const charsArray = new Float32Array(charsGpuBuffer.getMappedRange());
18506
- charsArray.set(font.charBuffer, 0);
18507
- charsGpuBuffer.unmap();
18508
- const fontDataBuffer = device.createBuffer({
18509
- label: `MSDF font ${font.name} metadata buffer`,
18510
- size: Float32Array.BYTES_PER_ELEMENT * 4,
18511
- usage: GPUBufferUsage.UNIFORM,
18512
- mappedAtCreation: true
18513
- });
18514
- const fontDataArray = new Float32Array(fontDataBuffer.getMappedRange());
18515
- fontDataArray[0] = font.lineHeight;
18516
- fontDataBuffer.unmap();
18517
- const fontBindGroup = device.createBindGroup({
18518
- layout: pipeline.getBindGroupLayout(0),
18519
- entries: [
18520
- {
18521
- binding: 0,
18522
- resource: texture.createView()
18523
- },
18524
- {
18525
- binding: 1,
18526
- resource: device.createSampler(sampler)
18527
- },
18528
- {
18529
- binding: 2,
18530
- resource: {
18531
- buffer: charsGpuBuffer
18532
- }
18533
- },
18534
- {
18535
- binding: 3,
18536
- resource: {
18537
- buffer: fontDataBuffer
18538
- }
18539
- }
18540
- ]
18541
- });
18542
- return new FontPipeline(pipeline, font, fontBindGroup, maxCharCount);
18543
- }
18544
- }
18545
- function pipelinePromise(device, colorFormat, label) {
18546
- const shader = device.createShaderModule({
18547
- label: `${label} shader`,
18548
- code: text_wgsl_default
18549
- });
18550
- return device.createRenderPipelineAsync({
18551
- label: `${label} pipeline`,
18552
- layout: device.createPipelineLayout({
18553
- bindGroupLayouts: [
18554
- device.createBindGroupLayout(fontBindGroupLayout),
18555
- device.createBindGroupLayout(textUniformBindGroupLayout),
18556
- device.createBindGroupLayout(engineUniformBindGroupLayout)
18557
- ]
18558
- }),
18559
- vertex: {
18560
- module: shader,
18561
- entryPoint: "vertexMain"
18562
- },
18563
- fragment: {
18564
- module: shader,
18565
- entryPoint: "fragmentMain",
18566
- targets: [
18567
- {
18568
- format: colorFormat,
18569
- blend: {
18570
- color: {
18571
- srcFactor: "src-alpha",
18572
- dstFactor: "one-minus-src-alpha"
18573
- },
18574
- alpha: {
18575
- srcFactor: "one",
18576
- dstFactor: "one"
18577
- }
18578
- }
18579
- }
18580
- ]
18581
- },
18582
- primitive: {
18583
- topology: "triangle-strip",
18584
- stripIndexFormat: "uint32"
18585
- }
18586
- });
18587
- }
18588
- if (typeof GPUShaderStage === "undefined") {
18589
- globalThis.GPUShaderStage = {
18590
- VERTEX: 1,
18591
- FRAGMENT: 2,
18592
- COMPUTE: 4
18593
- };
18594
- }
18595
- var fontBindGroupLayout = {
18596
- label: "MSDF font group layout",
18597
- entries: [
18598
- {
18599
- binding: 0,
18600
- visibility: GPUShaderStage.FRAGMENT,
18601
- texture: {}
18602
- },
18603
- {
18604
- binding: 1,
18605
- visibility: GPUShaderStage.FRAGMENT,
18606
- sampler: {}
18607
- },
18608
- {
18609
- binding: 2,
18610
- visibility: GPUShaderStage.VERTEX,
18611
- buffer: { type: "read-only-storage" }
18612
- },
18613
- {
18614
- binding: 3,
18615
- visibility: GPUShaderStage.VERTEX,
18616
- buffer: {}
18617
- }
18618
- ]
18619
- };
18620
- var engineUniformBindGroupLayout = {
18621
- label: "Uniform bind group",
18622
- entries: [
18623
- {
18624
- binding: 0,
18625
- visibility: GPUShaderStage.VERTEX,
18626
- buffer: {}
18627
- }
18628
- ]
18629
- };
18630
- var sampler = {
18631
- label: "MSDF text sampler",
18632
- minFilter: "linear",
18633
- magFilter: "linear",
18634
- mipmapFilter: "linear",
18635
- maxAnisotropy: 16
18636
- };
18637
- var textUniformBindGroupLayout = {
18638
- label: "MSDF text block uniform",
18639
- entries: [
18640
- {
18641
- binding: 0,
18642
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
18643
- buffer: { type: "read-only-storage" }
18644
- },
18645
- {
18646
- binding: 1,
18647
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
18648
- buffer: { type: "read-only-storage" }
18649
- }
18650
- ]
18651
- };
18652
- // src/backends/webgpu/WebGPUTextShader.ts
18653
- var deets = new _t2(text_wgsl_default);
18654
- var struct = deets.structs.find((s3) => s3.name === "TextBlockDescriptor");
18655
- if (!struct) {
18656
- throw new Error("FormattedText struct not found");
18657
- }
18658
- var textDescriptorInstanceSize = struct.size;
18659
-
18660
- class WebGPUTextShader {
18423
+ // src/backends/webgl2/WebGLTextShader.ts
18424
+ class WebGLTextShader2 {
18661
18425
  label = "text";
18662
- code = text_wgsl_default;
18426
+ code = fragmentShader2;
18427
+ font;
18428
+ maxCharCount;
18663
18429
  #backend;
18664
18430
  #pipeline;
18665
- #bindGroups = [];
18666
- #font;
18667
- #maxCharCount;
18668
- #engineUniformsBuffer;
18669
- #descriptorBuffer;
18670
- #textBlockBuffer;
18671
- #cpuDescriptorBuffer;
18672
- #cpuTextBlockBuffer;
18673
- #instanceIndex = 0;
18674
- #textBlockOffset = 0;
18675
- constructor(backend, pipeline, font, _colorFormat, instanceCount) {
18431
+ #program;
18432
+ #vao;
18433
+ #cpuTextBuffer;
18434
+ #cachedUniform = null;
18435
+ #uViewProjection = null;
18436
+ #uTextTransform = null;
18437
+ #uTextColor = null;
18438
+ #uFontSize = null;
18439
+ #uBlockWidth = null;
18440
+ #uBlockHeight = null;
18441
+ #uLineHeight = null;
18442
+ #uCharData = null;
18443
+ #uTextBuffer = null;
18444
+ #uFontTexture = null;
18445
+ constructor(backend, pipeline) {
18676
18446
  this.#backend = backend;
18677
- const device = backend.device;
18678
- this.#font = font;
18679
- this.#pipeline = pipeline.pipeline;
18680
- this.#maxCharCount = pipeline.maxCharCount;
18681
- this.#descriptorBuffer = device.createBuffer({
18682
- label: "msdf text descriptor buffer",
18683
- size: textDescriptorInstanceSize * instanceCount,
18684
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
18685
- });
18686
- this.#cpuDescriptorBuffer = new Float32Array(instanceCount * textDescriptorInstanceSize / Float32Array.BYTES_PER_ELEMENT);
18687
- this.#cpuTextBlockBuffer = new Float32Array(instanceCount * this.maxCharCount * 4);
18688
- this.#engineUniformsBuffer = device.createBuffer({
18689
- label: "msdf view projection matrix",
18690
- size: Float32Array.BYTES_PER_ELEMENT * 12,
18691
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
18692
- });
18693
- this.#textBlockBuffer = device.createBuffer({
18694
- label: "msdf text buffer",
18695
- size: instanceCount * this.maxCharCount * 4 * Float32Array.BYTES_PER_ELEMENT,
18696
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
18697
- });
18698
- this.#bindGroups.push(pipeline.fontBindGroup);
18699
- this.#bindGroups.push(device.createBindGroup({
18700
- label: "msdf text bind group",
18701
- layout: pipeline.pipeline.getBindGroupLayout(1),
18702
- entries: [
18703
- {
18704
- binding: 0,
18705
- resource: { buffer: this.#descriptorBuffer }
18706
- },
18707
- {
18708
- binding: 1,
18709
- resource: { buffer: this.#textBlockBuffer }
18710
- }
18711
- ]
18712
- }));
18713
- const engineUniformsBindGroup = device.createBindGroup({
18714
- label: "msdf text uniforms bind group",
18715
- layout: pipeline.pipeline.getBindGroupLayout(2),
18716
- entries: [
18717
- {
18718
- binding: 0,
18719
- resource: { buffer: this.#engineUniformsBuffer }
18720
- }
18721
- ]
18722
- });
18723
- this.#bindGroups.push(engineUniformsBindGroup);
18447
+ this.#pipeline = pipeline;
18448
+ this.font = pipeline.font;
18449
+ this.maxCharCount = pipeline.maxCharCount;
18450
+ const gl = backend.gl;
18451
+ const vs = this.#compileShader(gl, gl.VERTEX_SHADER, vertexShader2);
18452
+ const fs = this.#compileShader(gl, gl.FRAGMENT_SHADER, fragmentShader2);
18453
+ const program = gl.createProgram();
18454
+ assert(program, "Failed to create WebGL program");
18455
+ gl.attachShader(program, vs);
18456
+ gl.attachShader(program, fs);
18457
+ gl.linkProgram(program);
18458
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
18459
+ const info = gl.getProgramInfoLog(program);
18460
+ throw new Error(`Failed to link text shader program: ${info}`);
18461
+ }
18462
+ this.#program = program;
18463
+ this.#uViewProjection = gl.getUniformLocation(program, "u_viewProjection");
18464
+ this.#uTextTransform = gl.getUniformLocation(program, "u_textTransform");
18465
+ this.#uTextColor = gl.getUniformLocation(program, "u_textColor");
18466
+ this.#uFontSize = gl.getUniformLocation(program, "u_fontSize");
18467
+ this.#uBlockWidth = gl.getUniformLocation(program, "u_blockWidth");
18468
+ this.#uBlockHeight = gl.getUniformLocation(program, "u_blockHeight");
18469
+ this.#uLineHeight = gl.getUniformLocation(program, "u_lineHeight");
18470
+ this.#uCharData = gl.getUniformLocation(program, "u_charData");
18471
+ this.#uTextBuffer = gl.getUniformLocation(program, "u_textBuffer");
18472
+ this.#uFontTexture = gl.getUniformLocation(program, "u_fontTexture");
18473
+ const vao = gl.createVertexArray();
18474
+ assert(vao, "Failed to create WebGL VAO");
18475
+ this.#vao = vao;
18476
+ this.#cpuTextBuffer = new Float32Array(this.maxCharCount * 4);
18477
+ gl.deleteShader(vs);
18478
+ gl.deleteShader(fs);
18724
18479
  }
18725
18480
  startFrame(uniform) {
18726
- const device = this.#backend.device;
18727
- device.queue.writeBuffer(this.#engineUniformsBuffer, 0, uniform.viewProjectionMatrix);
18728
- this.#instanceIndex = 0;
18729
- this.#textBlockOffset = 0;
18481
+ this.#cachedUniform = uniform;
18730
18482
  }
18731
18483
  processBatch(nodes) {
18732
18484
  if (nodes.length === 0)
18733
18485
  return 0;
18734
- const renderPass = this.#backend.renderPass;
18735
- renderPass.setPipeline(this.#pipeline);
18736
- for (let i3 = 0;i3 < this.#bindGroups.length; i3++) {
18737
- renderPass.setBindGroup(i3, this.#bindGroups[i3]);
18486
+ const gl = this.#backend.gl;
18487
+ const uniform = this.#cachedUniform;
18488
+ if (!uniform) {
18489
+ throw new Error("Tried to process batch but engine uniform is not set");
18490
+ }
18491
+ gl.useProgram(this.#program);
18492
+ gl.bindVertexArray(this.#vao);
18493
+ if (this.#uViewProjection) {
18494
+ const m3 = uniform.viewProjectionMatrix;
18495
+ const mat3x3 = new Float32Array([
18496
+ m3[0],
18497
+ m3[1],
18498
+ m3[2],
18499
+ m3[4],
18500
+ m3[5],
18501
+ m3[6],
18502
+ m3[8],
18503
+ m3[9],
18504
+ m3[10]
18505
+ ]);
18506
+ gl.uniformMatrix3fv(this.#uViewProjection, false, mat3x3);
18507
+ }
18508
+ if (this.#uLineHeight) {
18509
+ gl.uniform1f(this.#uLineHeight, this.#pipeline.lineHeight);
18510
+ }
18511
+ gl.activeTexture(gl.TEXTURE0);
18512
+ gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.fontTexture);
18513
+ if (this.#uFontTexture) {
18514
+ gl.uniform1i(this.#uFontTexture, 0);
18515
+ }
18516
+ gl.activeTexture(gl.TEXTURE1);
18517
+ gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.charDataTexture);
18518
+ if (this.#uCharData) {
18519
+ gl.uniform1i(this.#uCharData, 1);
18520
+ }
18521
+ gl.activeTexture(gl.TEXTURE2);
18522
+ gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.textBufferTexture);
18523
+ if (this.#uTextBuffer) {
18524
+ gl.uniform1i(this.#uTextBuffer, 2);
18738
18525
  }
18739
18526
  for (const node of nodes) {
18740
18527
  if (!(node instanceof TextNode)) {
18741
18528
  console.error(node);
18742
- throw new Error(`Tried to use WebGPUTextShader on something that isn't a TextNode: ${node}`);
18529
+ throw new Error(`Tried to use WebGLTextShader on something that isn't a TextNode: ${node}`);
18743
18530
  }
18744
18531
  const text = node.text;
18745
18532
  const formatting = node.formatting;
18746
- const measurements = measureText(this.#font, text, formatting.wordWrap);
18747
- const textBlockSize = 4 * text.length;
18748
- const textDescriptorOffset = this.#instanceIndex * textDescriptorInstanceSize / Float32Array.BYTES_PER_ELEMENT;
18749
- this.#cpuDescriptorBuffer.set(node.matrix, textDescriptorOffset);
18750
- this.#cpuDescriptorBuffer.set([node.tint.r, node.tint.g, node.tint.b, node.tint.a], textDescriptorOffset + 12);
18533
+ const measurements = measureText(this.font, text, formatting.wordWrap);
18751
18534
  const size = node.size ?? measurements;
18752
- const fontSize = formatting.shrinkToFit ? findLargestFontSize(this.#font, text, size, formatting) : formatting.fontSize;
18535
+ const fontSize = formatting.shrinkToFit ? findLargestFontSize(this.font, text, size, formatting) : formatting.fontSize;
18753
18536
  const actualFontSize = fontSize || DEFAULT_FONT_SIZE;
18754
- this.#cpuDescriptorBuffer[textDescriptorOffset + 16] = actualFontSize;
18755
- this.#cpuDescriptorBuffer[textDescriptorOffset + 17] = formatting.align === "center" ? 0 : measurements.width;
18756
- this.#cpuDescriptorBuffer[textDescriptorOffset + 18] = measurements.height;
18757
- this.#cpuDescriptorBuffer[textDescriptorOffset + 19] = this.#textBlockOffset / 4;
18758
- shapeText(this.#font, text, size, actualFontSize, formatting, this.#cpuTextBlockBuffer, this.#textBlockOffset);
18759
- this.#backend.device.queue.writeBuffer(this.#descriptorBuffer, textDescriptorOffset * Float32Array.BYTES_PER_ELEMENT, this.#cpuDescriptorBuffer, textDescriptorOffset, textDescriptorInstanceSize / Float32Array.BYTES_PER_ELEMENT);
18760
- this.#backend.device.queue.writeBuffer(this.#textBlockBuffer, this.#textBlockOffset * Float32Array.BYTES_PER_ELEMENT, this.#cpuTextBlockBuffer, this.#textBlockOffset, textBlockSize);
18761
- this.#textBlockOffset += textBlockSize;
18762
- renderPass.draw(4, measurements.printedCharCount, 4 * this.#instanceIndex, 0);
18763
- this.#instanceIndex++;
18764
- }
18765
- return nodes.length;
18766
- }
18767
- endFrame() {}
18768
- get font() {
18769
- return this.#font;
18770
- }
18771
- get maxCharCount() {
18772
- return this.#maxCharCount;
18773
- }
18774
- }
18775
-
18776
- // src/scene/Batcher.ts
18777
- class Batcher {
18778
- nodes = [];
18779
- layers = [];
18780
- pipelines = [];
18781
- enqueue(node) {
18782
- if (node.renderComponent && node.isActive) {
18783
- this.nodes.push(node);
18784
- const z3 = node.layer;
18785
- const layer = this.#findOrCreateLayer(z3);
18786
- const pipeline = this.#findOrCreatePipeline(layer, node.renderComponent.shader);
18787
- pipeline.nodes.push(node);
18788
- }
18789
- for (const kid of node.kids) {
18790
- this.enqueue(kid);
18791
- }
18792
- }
18793
- flush() {
18794
- this.nodes = [];
18795
- this.layers = [];
18796
- this.pipelines = [];
18797
- }
18798
- #findOrCreateLayer(z3) {
18799
- let layer = this.layers.find((l3) => l3.z === z3);
18800
- if (!layer) {
18801
- layer = { z: z3, pipelines: [] };
18802
- this.layers.push(layer);
18803
- this.layers.sort((a3, b3) => a3.z - b3.z);
18804
- }
18805
- return layer;
18806
- }
18807
- #findOrCreatePipeline(layer, shader) {
18808
- let pipeline = layer.pipelines.find((p3) => p3.shader === shader);
18809
- if (!pipeline) {
18810
- pipeline = { shader, nodes: [] };
18811
- layer.pipelines.push(pipeline);
18812
- this.pipelines.push(pipeline);
18537
+ shapeText(this.font, text, size, actualFontSize, formatting, this.#cpuTextBuffer, 0);
18538
+ this.#pipeline.updateTextBuffer(this.#cpuTextBuffer, measurements.printedCharCount);
18539
+ if (this.#uTextTransform) {
18540
+ const m3 = node.matrix;
18541
+ const mat3x3 = new Float32Array([
18542
+ m3[0],
18543
+ m3[1],
18544
+ m3[2],
18545
+ m3[4],
18546
+ m3[5],
18547
+ m3[6],
18548
+ m3[8],
18549
+ m3[9],
18550
+ m3[10]
18551
+ ]);
18552
+ gl.uniformMatrix3fv(this.#uTextTransform, false, mat3x3);
18553
+ }
18554
+ if (this.#uTextColor) {
18555
+ const tint = node.tint;
18556
+ gl.uniform4f(this.#uTextColor, tint.r, tint.g, tint.b, tint.a);
18557
+ }
18558
+ if (this.#uFontSize) {
18559
+ gl.uniform1f(this.#uFontSize, actualFontSize);
18560
+ }
18561
+ if (this.#uBlockWidth) {
18562
+ gl.uniform1f(this.#uBlockWidth, formatting.align === "center" ? 0 : measurements.width);
18563
+ }
18564
+ if (this.#uBlockHeight) {
18565
+ gl.uniform1f(this.#uBlockHeight, measurements.height);
18566
+ }
18567
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, measurements.printedCharCount);
18813
18568
  }
18814
- return pipeline;
18815
- }
18816
- }
18817
-
18818
- // src/scene/Camera.ts
18819
- class Camera {
18820
- #position = { x: 0, y: 0 };
18821
- #zoom = 1;
18822
- #rotation = 0;
18823
- #isDirty = true;
18824
- #matrix = mat3.create();
18825
- get zoom() {
18826
- return this.#zoom;
18827
- }
18828
- set zoom(value) {
18829
- this.#zoom = value;
18830
- this.setDirty();
18831
- }
18832
- get rotation() {
18833
- return rad2deg(this.#rotation);
18834
- }
18835
- set rotation(value) {
18836
- this.#rotation = deg2rad(value);
18837
- this.setDirty();
18838
- }
18839
- get rotationRadians() {
18840
- return this.#rotation;
18841
- }
18842
- set rotationRadians(value) {
18843
- this.#rotation = value;
18844
- this.setDirty();
18845
- }
18846
- get x() {
18847
- return this.#position.x;
18848
- }
18849
- get y() {
18850
- return this.#position.y;
18851
- }
18852
- set x(value) {
18853
- this.#position.x = value;
18854
- this.setDirty();
18855
- }
18856
- set y(value) {
18857
- this.#position.y = value;
18858
- this.setDirty();
18569
+ gl.bindVertexArray(null);
18570
+ return nodes.length;
18859
18571
  }
18860
- get matrix() {
18861
- if (this.#isDirty) {
18862
- this.#isDirty = false;
18863
- this.#matrix = createViewMatrix(this, this.#matrix);
18572
+ endFrame() {}
18573
+ #compileShader(gl, type, source) {
18574
+ const shader = gl.createShader(type);
18575
+ assert(shader, "Failed to create WebGL shader");
18576
+ gl.shaderSource(shader, source);
18577
+ gl.compileShader(shader);
18578
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
18579
+ const info = gl.getShaderInfoLog(shader);
18580
+ const typeStr = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
18581
+ gl.deleteShader(shader);
18582
+ throw new Error(`Failed to compile ${typeStr} shader: ${info}`);
18864
18583
  }
18865
- return this.#matrix;
18584
+ return shader;
18866
18585
  }
18867
- setDirty() {
18868
- this.#isDirty = true;
18586
+ destroy() {
18587
+ const gl = this.#backend.gl;
18588
+ gl.deleteProgram(this.#program);
18589
+ gl.deleteVertexArray(this.#vao);
18590
+ this.#pipeline.destroy();
18869
18591
  }
18870
18592
  }
18871
18593
 
18872
- // src/scene/QuadNode.ts
18873
- var PRIMITIVE_TEXTURE = "__primitive__";
18874
- var RESERVED_PRIMITIVE_INDEX_START = 1000;
18875
- var CIRCLE_INDEX = 1001;
18876
- var DEFAULT_REGION = {
18877
- x: 0,
18878
- y: 0,
18879
- width: 0,
18880
- height: 0
18594
+ // src/backends/webgpu/wgsl/text.wgsl.ts
18595
+ var text_wgsl_default = `
18596
+ // Adapted from: https://webgpu.github.io/webgpu-samples/?sample=textRenderingMsdf
18597
+
18598
+ // Quad vertex positions for a character
18599
+ const pos = array(
18600
+ vec2f(0, -1),
18601
+ vec2f(1, -1),
18602
+ vec2f(0, 0),
18603
+ vec2f(1, 0),
18604
+ );
18605
+
18606
+ // Debug colors for visualization
18607
+ const debugColors = array(
18608
+ vec4f(1, 0, 0, 1),
18609
+ vec4f(0, 1, 0, 1),
18610
+ vec4f(0, 0, 1, 1),
18611
+ vec4f(1, 1, 1, 1),
18612
+ );
18613
+
18614
+ // Vertex input from GPU
18615
+ struct VertexInput {
18616
+ @builtin(vertex_index) vertex: u32,
18617
+ @builtin(instance_index) instance: u32,
18881
18618
  };
18882
18619
 
18883
- class QuadNode extends SceneNode {
18884
- assetManager;
18885
- #color;
18886
- #atlasCoords;
18887
- #region;
18888
- #matrixPool;
18889
- #flip;
18890
- #cropOffset;
18891
- #cropRatio;
18892
- #atlasSize;
18893
- #textureId;
18894
- #writeInstance;
18895
- constructor(options, matrixPool) {
18896
- assert(options.shader, "QuadNode requires a shader to be explicitly provided");
18897
- assert(options.size, "QuadNode requires a size to be explicitly provided");
18898
- assert(options.atlasCoords, "QuadNode requires atlas coords to be explicitly provided");
18899
- options.render ??= {
18900
- shader: options.shader,
18901
- writeInstance: writeQuadInstance
18902
- };
18903
- super(options);
18904
- assert(options.assetManager, "QuadNode requires an asset manager");
18905
- this.assetManager = options.assetManager;
18906
- if (options.atlasCoords && options.atlasCoords.atlasIndex >= RESERVED_PRIMITIVE_INDEX_START) {
18907
- this.#textureId = PRIMITIVE_TEXTURE;
18908
- this.#region = DEFAULT_REGION;
18909
- this.#atlasSize = DEFAULT_REGION;
18910
- } else {
18911
- assert(options.textureId, "QuadNode requires texture id to be explicitly provided");
18912
- this.#textureId = options.textureId;
18913
- assert(options.region, "QuadNode requires a region to be explicitly provided");
18914
- this.#region = options.region;
18915
- assert(options.atlasSize, "QuadNode requires atlas size to be explicitly provided");
18916
- this.#atlasSize = options.atlasSize;
18917
- }
18918
- this.#atlasCoords = options.atlasCoords;
18919
- this.#color = options.color ?? { r: 1, g: 1, b: 1, a: 1 };
18920
- this.#matrixPool = matrixPool;
18921
- this.#flip = { x: options.flipX ? -1 : 1, y: options.flipY ? -1 : 1 };
18922
- this.#cropOffset = options.cropOffset ?? { x: 0, y: 0 };
18923
- this.#cropRatio = !this.#atlasCoords.uvScaleCropped ? { width: 1, height: 1 } : {
18924
- width: this.#atlasCoords.uvScaleCropped.width / this.#atlasCoords.uvScale.width,
18925
- height: this.#atlasCoords.uvScaleCropped.height / this.#atlasCoords.uvScale.height
18926
- };
18927
- this.#writeInstance = options.writeInstance;
18928
- }
18929
- get color() {
18930
- return this.#color;
18931
- }
18932
- set color(value) {
18933
- this.#color = value;
18934
- }
18935
- get size() {
18936
- const size = super.size;
18937
- if (!size) {
18938
- throw new Error("QuadNode requires a size");
18939
- }
18940
- return size;
18941
- }
18942
- set size(val) {
18943
- super.size = val;
18944
- }
18945
- get matrixWithSize() {
18946
- const matrix = mat3.clone(this.matrix, this.#matrixPool.get());
18947
- mat3.scale(matrix, [this.size.width * this.#flip.x, this.size.height * this.#flip.y], matrix);
18948
- return matrix;
18949
- }
18950
- get atlasCoords() {
18951
- return this.#atlasCoords;
18952
- }
18953
- get region() {
18954
- return this.#region;
18955
- }
18956
- get writeInstance() {
18957
- return this.#writeInstance;
18958
- }
18959
- get flipX() {
18960
- return this.#flip.x === -1;
18961
- }
18962
- set flipX(value) {
18963
- this.#flip.x = value ? -1 : 1;
18964
- this.setDirty();
18965
- }
18966
- get flipY() {
18967
- return this.#flip.y === -1;
18968
- }
18969
- set flipY(value) {
18970
- this.#flip.y = value ? -1 : 1;
18971
- this.setDirty();
18972
- }
18973
- get cropOffset() {
18974
- return this.#cropOffset;
18975
- }
18976
- set cropOffset(value) {
18977
- this.#cropOffset = value;
18978
- }
18979
- get textureId() {
18980
- return this.#textureId;
18981
- }
18982
- get isPrimitive() {
18983
- return this.#textureId === PRIMITIVE_TEXTURE;
18984
- }
18985
- get isCircle() {
18986
- return this.#atlasCoords.atlasIndex === CIRCLE_INDEX;
18987
- }
18988
- extra = {
18989
- setAtlasCoords: (value) => {
18990
- this.#atlasCoords = value;
18991
- },
18992
- cropRatio: () => {
18993
- return this.#cropRatio;
18994
- },
18995
- atlasSize: () => {
18996
- return this.#atlasSize;
18997
- }
18998
- };
18620
+ // Output from vertex shader to fragment shader
18621
+ struct VertexOutput {
18622
+ @builtin(position) position: vec4f,
18623
+ @location(0) texcoord: vec2f,
18624
+ @location(1) debugColor: vec4f,
18625
+ @location(2) @interpolate(flat) instanceIndex: u32,
18626
+ };
18627
+
18628
+ // Metadata for a single character glyph
18629
+ struct Char {
18630
+ texOffset: vec2f, // Offset to top-left in MSDF texture (pixels)
18631
+ texExtent: vec2f, // Size in texture (pixels)
18632
+ size: vec2f, // Glyph size in ems
18633
+ offset: vec2f, // Position offset in ems
18634
+ };
18635
+
18636
+ // Metadata for a text block
18637
+ struct TextBlockDescriptor {
18638
+ transform: mat3x3f, // Text transform matrix (model matrix)
18639
+ color: vec4f, // Text color
18640
+ fontSize: f32, // Font size
18641
+ blockWidth: f32, // Total width of text block
18642
+ blockHeight: f32, // Total height of text block
18643
+ bufferPosition: f32 // Index and length in textBuffer
18644
+ };
18645
+
18646
+ // Font bindings
18647
+ @group(0) @binding(0) var fontTexture: texture_2d<f32>;
18648
+ @group(0) @binding(1) var fontSampler: sampler;
18649
+ @group(0) @binding(2) var<storage> chars: array<Char>;
18650
+ @group(0) @binding(3) var<uniform> fontData: vec4f; // Contains line height (x)
18651
+
18652
+ // Text bindings
18653
+ @group(1) @binding(0) var<storage> texts: array<TextBlockDescriptor>;
18654
+ @group(1) @binding(1) var<storage> textBuffer: array<vec4f>; // Each vec4: xy = glyph pos, z = char index
18655
+
18656
+ // Global uniforms
18657
+ @group(2) @binding(0) var<uniform> viewProjectionMatrix: mat3x3f;
18658
+
18659
+ // Vertex shader
18660
+ @vertex
18661
+ fn vertexMain(input: VertexInput) -> VertexOutput {
18662
+ // Because the instance index is used for character indexing, we are
18663
+ // overloading the vertex index to store the instance of the text metadata.
18664
+ //
18665
+ // I.e...
18666
+ // Vertex 0-4 = Instance 0, Vertex 0-4
18667
+ // Vertex 4-8 = Instance 1, Vertex 0-4
18668
+ // Vertex 8-12 = Instance 2, Vertex 0-4
18669
+ let vertexIndex = input.vertex % 4;
18670
+ let textIndex = input.vertex / 4;
18671
+
18672
+ let text = texts[textIndex];
18673
+ let textElement = textBuffer[u32(text.bufferPosition) + input.instance];
18674
+ let char = chars[u32(textElement.z)];
18675
+
18676
+ let lineHeight = fontData.x;
18677
+ let textWidth = text.blockWidth;
18678
+ let textHeight = text.blockHeight;
18679
+
18680
+ // Center text vertically; origin is mid-height
18681
+ let offset = vec2f(0, -textHeight / 2);
18682
+
18683
+ // Glyph position in ems (quad pos * size + per-char offset)
18684
+ let emPos = pos[vertexIndex] * char.size + char.offset + textElement.xy - offset;
18685
+ let charPos = emPos * (text.fontSize / lineHeight);
18686
+
18687
+ var output: VertexOutput;
18688
+ let transformedPosition = viewProjectionMatrix * text.transform * vec3f(charPos, 1);
18689
+
18690
+ output.position = vec4f(transformedPosition, 1);
18691
+ output.texcoord = pos[vertexIndex] * vec2f(1, -1);
18692
+ output.texcoord *= char.texExtent;
18693
+ output.texcoord += char.texOffset;
18694
+ output.debugColor = debugColors[vertexIndex];
18695
+ output.instanceIndex = textIndex;
18696
+ return output;
18697
+
18698
+ // To debug - hardcode quad in bottom right quarter of the screen:
18699
+ // output.position = vec4f(pos[input.vertex], 0, 1);
18999
18700
  }
19000
- function writeQuadInstance(node, array, offset) {
19001
- if (!(node instanceof QuadNode)) {
19002
- throw new Error("QuadNode.writeInstance can only be called on QuadNodes");
19003
- }
19004
- array.set(node.matrixWithSize, offset);
19005
- array.set([node.color.r, node.color.g, node.color.b, node.color.a], offset + 12);
19006
- const region = node.region;
19007
- if (node.textureId === PRIMITIVE_TEXTURE) {
19008
- array.set([
19009
- node.atlasCoords.uvOffset.x,
19010
- node.atlasCoords.uvOffset.y,
19011
- node.atlasCoords.uvScale.width,
19012
- node.atlasCoords.uvScale.height
19013
- ], offset + 16);
19014
- } else {
19015
- const atlasSize = node.extra.atlasSize();
19016
- array.set([
19017
- node.atlasCoords.uvOffset.x + region.x / atlasSize.width,
19018
- node.atlasCoords.uvOffset.y + region.y / atlasSize.height,
19019
- region.width / atlasSize.width,
19020
- region.height / atlasSize.height
19021
- ], offset + 16);
19022
- }
19023
- array.set([
19024
- node.cropOffset.x / 2 / (node.atlasCoords.originalSize.width || 1),
19025
- node.cropOffset.y / 2 / (node.atlasCoords.originalSize.height || 1),
19026
- node.extra.cropRatio().width,
19027
- node.extra.cropRatio().height
19028
- ], offset + 20);
19029
- new DataView(array.buffer).setUint32(array.byteOffset + (offset + 24) * Float32Array.BYTES_PER_ELEMENT, node.atlasCoords.atlasIndex, true);
19030
- node.writeInstance?.(array, offset + 28);
19031
- return 1;
18701
+
18702
+ // Signed distance function sampling for MSDF font rendering
18703
+ fn sampleMsdf(texcoord: vec2f) -> f32 {
18704
+ let c = textureSample(fontTexture, fontSampler, texcoord);
18705
+ return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
19032
18706
  }
19033
18707
 
19034
- // src/scene/JumboQuadNode.ts
19035
- var MAT3_SIZE = 12;
19036
- var VEC4F_SIZE = 4;
18708
+ // Fragment shader
18709
+ // Anti-aliasing technique by Paul Houx
18710
+ // more details here:
18711
+ // https://github.com/Chlumsky/msdfgen/issues/22#issuecomment-234958005
18712
+ @fragment
18713
+ fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
18714
+ let text = texts[input.instanceIndex];
19037
18715
 
19038
- class JumboQuadNode extends QuadNode {
19039
- #tiles;
19040
- #matrixPool;
19041
- constructor(options, matrixPool) {
19042
- assert(options.shader, "JumboQuadNode requires a shader to be explicitly provided");
19043
- assert(options.tiles && options.tiles.length > 0, "JumboQuadNode requires at least one tile to be provided");
19044
- options.render ??= {
19045
- shader: options.shader,
19046
- writeInstance: writeJumboQuadInstance
19047
- };
19048
- super({
19049
- ...options,
19050
- atlasCoords: options.tiles[0].atlasCoords
19051
- }, matrixPool);
19052
- this.#matrixPool = matrixPool;
19053
- this.#tiles = [];
19054
- for (const tile of options.tiles) {
19055
- assert(tile.atlasCoords, "JumboQuadNode requires atlas coords to be provided");
19056
- assert(tile.size, "JumboQuadNode requires a size to be provided");
19057
- this.#tiles.push({
19058
- textureId: tile.textureId,
19059
- offset: tile.offset,
19060
- size: tile.size,
19061
- atlasCoords: tile.atlasCoords
19062
- });
19063
- }
19064
- }
19065
- get atlasCoords() {
19066
- throw new Error("JumboQuadNode does not have a single atlas coords");
19067
- }
19068
- get tiles() {
19069
- return this.#tiles;
19070
- }
19071
- getTileMatrix(tile) {
19072
- const matrix = mat3.clone(this.matrix, this.#matrixPool.get());
19073
- const originalSize = {
19074
- width: Math.max(...this.#tiles.map((t3) => t3.offset.x + t3.size.width)),
19075
- height: Math.max(...this.#tiles.map((t3) => t3.offset.y + t3.size.height))
19076
- };
19077
- const proportionalSize = {
19078
- width: this.size.width / originalSize.width,
19079
- height: this.size.height / originalSize.height
19080
- };
19081
- const centerOffset = {
19082
- x: tile.offset.x + tile.size.width / 2 - originalSize.width / 2,
19083
- y: -(tile.offset.y + tile.size.height / 2 - originalSize.height / 2)
19084
- };
19085
- mat3.translate(matrix, [
19086
- centerOffset.x * proportionalSize.width,
19087
- centerOffset.y * proportionalSize.height
19088
- ], matrix);
19089
- mat3.scale(matrix, [
19090
- tile.size.width * proportionalSize.width * (this.flipX ? -1 : 1),
19091
- tile.size.height * proportionalSize.height * (this.flipY ? -1 : 1)
19092
- ], matrix);
19093
- return matrix;
18716
+ // pxRange (AKA distanceRange) comes from the msdfgen tool.
18717
+ let pxRange = 4.0;
18718
+ let texSize = vec2f(textureDimensions(fontTexture, 0));
18719
+
18720
+ let dx = texSize.x * length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));
18721
+ let dy = texSize.y * length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));
18722
+
18723
+ let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
18724
+ let sigDist = sampleMsdf(input.texcoord) - 0.5;
18725
+ let pxDist = sigDist * toPixels;
18726
+
18727
+ let edgeWidth = 0.5;
18728
+ let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
18729
+
18730
+ if (alpha < 0.001) {
18731
+ discard;
19094
18732
  }
18733
+
18734
+ let msdfColor = vec4f(text.color.rgb, text.color.a * alpha);
18735
+ return msdfColor;
18736
+
18737
+ // Debug options:
18738
+ // return text.color;
18739
+ // return input.debugColor;
18740
+ // return vec4f(1, 0, 1, 1); // hardcoded magenta
18741
+ // return textureSample(fontTexture, fontSampler, input.texcoord);
19095
18742
  }
19096
- function writeJumboQuadInstance(node, array, offset) {
19097
- if (!(node instanceof JumboQuadNode)) {
19098
- throw new Error("JumboQuadNode.writeJumboQuadInstance can only be called on JumboQuadNodes");
18743
+ `;
18744
+
18745
+ // src/backends/webgpu/FontPipeline.ts
18746
+ class FontPipeline2 {
18747
+ pipeline;
18748
+ font;
18749
+ fontBindGroup;
18750
+ maxCharCount;
18751
+ constructor(pipeline, font, fontBindGroup, maxCharCount) {
18752
+ this.pipeline = pipeline;
18753
+ this.font = font;
18754
+ this.fontBindGroup = fontBindGroup;
18755
+ this.maxCharCount = maxCharCount;
19099
18756
  }
19100
- let tileOffset = 0;
19101
- for (const tile of node.tiles) {
19102
- const coord = tile.atlasCoords;
19103
- const matrix = node.getTileMatrix(tile);
19104
- array.set(matrix, offset + tileOffset);
19105
- tileOffset += MAT3_SIZE;
19106
- array.set([node.color.r, node.color.g, node.color.b, node.color.a], offset + tileOffset);
19107
- tileOffset += VEC4F_SIZE;
19108
- array.set([
19109
- coord.uvOffset.x,
19110
- coord.uvOffset.y,
19111
- coord.uvScale.width,
19112
- coord.uvScale.height
19113
- ], offset + tileOffset);
19114
- tileOffset += VEC4F_SIZE;
19115
- const cropRatio = !coord.uvScaleCropped ? { width: 1, height: 1 } : {
19116
- width: coord.uvScaleCropped.width / coord.uvScale.width,
19117
- height: coord.uvScaleCropped.height / coord.uvScale.height
19118
- };
19119
- array.set([
19120
- tile.atlasCoords.cropOffset.x / 2 / (tile.atlasCoords.originalSize.width || 1),
19121
- tile.atlasCoords.cropOffset.y / 2 / (tile.atlasCoords.originalSize.height || 1),
19122
- cropRatio.width,
19123
- cropRatio.height
19124
- ], offset + tileOffset);
19125
- tileOffset += VEC4F_SIZE;
19126
- new DataView(array.buffer).setUint32(array.byteOffset + (offset + tileOffset) * Float32Array.BYTES_PER_ELEMENT, coord.atlasIndex, true);
19127
- tileOffset += VEC4F_SIZE;
18757
+ static async create(device, font, colorFormat, maxCharCount) {
18758
+ const pipeline = await pipelinePromise(device, colorFormat, font.name);
18759
+ const texture = device.createTexture({
18760
+ label: `MSDF font ${font.name}`,
18761
+ size: [font.imageBitmap.width, font.imageBitmap.height, 1],
18762
+ format: "rgba8unorm",
18763
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
18764
+ });
18765
+ device.queue.copyExternalImageToTexture({ source: font.imageBitmap }, { texture }, [font.imageBitmap.width, font.imageBitmap.height]);
18766
+ const charsGpuBuffer = device.createBuffer({
18767
+ label: `MSDF font ${font.name} character layout buffer`,
18768
+ size: font.charCount * Float32Array.BYTES_PER_ELEMENT * 8,
18769
+ usage: GPUBufferUsage.STORAGE,
18770
+ mappedAtCreation: true
18771
+ });
18772
+ const charsArray = new Float32Array(charsGpuBuffer.getMappedRange());
18773
+ charsArray.set(font.charBuffer, 0);
18774
+ charsGpuBuffer.unmap();
18775
+ const fontDataBuffer = device.createBuffer({
18776
+ label: `MSDF font ${font.name} metadata buffer`,
18777
+ size: Float32Array.BYTES_PER_ELEMENT * 4,
18778
+ usage: GPUBufferUsage.UNIFORM,
18779
+ mappedAtCreation: true
18780
+ });
18781
+ const fontDataArray = new Float32Array(fontDataBuffer.getMappedRange());
18782
+ fontDataArray[0] = font.lineHeight;
18783
+ fontDataBuffer.unmap();
18784
+ const fontBindGroup = device.createBindGroup({
18785
+ layout: pipeline.getBindGroupLayout(0),
18786
+ entries: [
18787
+ {
18788
+ binding: 0,
18789
+ resource: texture.createView()
18790
+ },
18791
+ {
18792
+ binding: 1,
18793
+ resource: device.createSampler(sampler)
18794
+ },
18795
+ {
18796
+ binding: 2,
18797
+ resource: {
18798
+ buffer: charsGpuBuffer
18799
+ }
18800
+ },
18801
+ {
18802
+ binding: 3,
18803
+ resource: {
18804
+ buffer: fontDataBuffer
18805
+ }
18806
+ }
18807
+ ]
18808
+ });
18809
+ return new FontPipeline2(pipeline, font, fontBindGroup, maxCharCount);
19128
18810
  }
19129
- node.writeInstance?.(array, offset + tileOffset);
19130
- return node.tiles.length;
19131
18811
  }
18812
+ function pipelinePromise(device, colorFormat, label) {
18813
+ const shader = device.createShaderModule({
18814
+ label: `${label} shader`,
18815
+ code: text_wgsl_default
18816
+ });
18817
+ return device.createRenderPipelineAsync({
18818
+ label: `${label} pipeline`,
18819
+ layout: device.createPipelineLayout({
18820
+ bindGroupLayouts: [
18821
+ device.createBindGroupLayout(fontBindGroupLayout),
18822
+ device.createBindGroupLayout(textUniformBindGroupLayout),
18823
+ device.createBindGroupLayout(engineUniformBindGroupLayout)
18824
+ ]
18825
+ }),
18826
+ vertex: {
18827
+ module: shader,
18828
+ entryPoint: "vertexMain"
18829
+ },
18830
+ fragment: {
18831
+ module: shader,
18832
+ entryPoint: "fragmentMain",
18833
+ targets: [
18834
+ {
18835
+ format: colorFormat,
18836
+ blend: {
18837
+ color: {
18838
+ srcFactor: "src-alpha",
18839
+ dstFactor: "one-minus-src-alpha"
18840
+ },
18841
+ alpha: {
18842
+ srcFactor: "one",
18843
+ dstFactor: "one"
18844
+ }
18845
+ }
18846
+ }
18847
+ ]
18848
+ },
18849
+ primitive: {
18850
+ topology: "triangle-strip",
18851
+ stripIndexFormat: "uint32"
18852
+ }
18853
+ });
18854
+ }
18855
+ if (typeof GPUShaderStage === "undefined") {
18856
+ globalThis.GPUShaderStage = {
18857
+ VERTEX: 1,
18858
+ FRAGMENT: 2,
18859
+ COMPUTE: 4
18860
+ };
18861
+ }
18862
+ var fontBindGroupLayout = {
18863
+ label: "MSDF font group layout",
18864
+ entries: [
18865
+ {
18866
+ binding: 0,
18867
+ visibility: GPUShaderStage.FRAGMENT,
18868
+ texture: {}
18869
+ },
18870
+ {
18871
+ binding: 1,
18872
+ visibility: GPUShaderStage.FRAGMENT,
18873
+ sampler: {}
18874
+ },
18875
+ {
18876
+ binding: 2,
18877
+ visibility: GPUShaderStage.VERTEX,
18878
+ buffer: { type: "read-only-storage" }
18879
+ },
18880
+ {
18881
+ binding: 3,
18882
+ visibility: GPUShaderStage.VERTEX,
18883
+ buffer: {}
18884
+ }
18885
+ ]
18886
+ };
18887
+ var engineUniformBindGroupLayout = {
18888
+ label: "Uniform bind group",
18889
+ entries: [
18890
+ {
18891
+ binding: 0,
18892
+ visibility: GPUShaderStage.VERTEX,
18893
+ buffer: {}
18894
+ }
18895
+ ]
18896
+ };
18897
+ var sampler = {
18898
+ label: "MSDF text sampler",
18899
+ minFilter: "linear",
18900
+ magFilter: "linear",
18901
+ mipmapFilter: "linear",
18902
+ maxAnisotropy: 16
18903
+ };
18904
+ var textUniformBindGroupLayout = {
18905
+ label: "MSDF text block uniform",
18906
+ entries: [
18907
+ {
18908
+ binding: 0,
18909
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
18910
+ buffer: { type: "read-only-storage" }
18911
+ },
18912
+ {
18913
+ binding: 1,
18914
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
18915
+ buffer: { type: "read-only-storage" }
18916
+ }
18917
+ ]
18918
+ };
19132
18919
 
19133
18920
  // src/backends/webgpu/wgsl/pixel-scraping.wgsl.ts
19134
18921
  var pixel_scraping_wgsl_default = `
@@ -19503,6 +19290,130 @@ function createPipelines(device, label) {
19503
19290
  };
19504
19291
  }
19505
19292
 
19293
+ // src/backends/webgpu/WebGPUTextShader.ts
19294
+ var deets = new _t2(text_wgsl_default);
19295
+ var struct = deets.structs.find((s3) => s3.name === "TextBlockDescriptor");
19296
+ if (!struct) {
19297
+ throw new Error("FormattedText struct not found");
19298
+ }
19299
+ var textDescriptorInstanceSize = struct.size;
19300
+
19301
+ class WebGPUTextShader2 {
19302
+ label = "text";
19303
+ code = text_wgsl_default;
19304
+ #backend;
19305
+ #pipeline;
19306
+ #bindGroups = [];
19307
+ #font;
19308
+ #maxCharCount;
19309
+ #engineUniformsBuffer;
19310
+ #descriptorBuffer;
19311
+ #textBlockBuffer;
19312
+ #cpuDescriptorBuffer;
19313
+ #cpuTextBlockBuffer;
19314
+ #instanceIndex = 0;
19315
+ #textBlockOffset = 0;
19316
+ constructor(backend, pipeline, font, _colorFormat, instanceCount) {
19317
+ this.#backend = backend;
19318
+ const device = backend.device;
19319
+ this.#font = font;
19320
+ this.#pipeline = pipeline.pipeline;
19321
+ this.#maxCharCount = pipeline.maxCharCount;
19322
+ this.#descriptorBuffer = device.createBuffer({
19323
+ label: "msdf text descriptor buffer",
19324
+ size: textDescriptorInstanceSize * instanceCount,
19325
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
19326
+ });
19327
+ this.#cpuDescriptorBuffer = new Float32Array(instanceCount * textDescriptorInstanceSize / Float32Array.BYTES_PER_ELEMENT);
19328
+ this.#cpuTextBlockBuffer = new Float32Array(instanceCount * this.maxCharCount * 4);
19329
+ this.#engineUniformsBuffer = device.createBuffer({
19330
+ label: "msdf view projection matrix",
19331
+ size: Float32Array.BYTES_PER_ELEMENT * 12,
19332
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
19333
+ });
19334
+ this.#textBlockBuffer = device.createBuffer({
19335
+ label: "msdf text buffer",
19336
+ size: instanceCount * this.maxCharCount * 4 * Float32Array.BYTES_PER_ELEMENT,
19337
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
19338
+ });
19339
+ this.#bindGroups.push(pipeline.fontBindGroup);
19340
+ this.#bindGroups.push(device.createBindGroup({
19341
+ label: "msdf text bind group",
19342
+ layout: pipeline.pipeline.getBindGroupLayout(1),
19343
+ entries: [
19344
+ {
19345
+ binding: 0,
19346
+ resource: { buffer: this.#descriptorBuffer }
19347
+ },
19348
+ {
19349
+ binding: 1,
19350
+ resource: { buffer: this.#textBlockBuffer }
19351
+ }
19352
+ ]
19353
+ }));
19354
+ const engineUniformsBindGroup = device.createBindGroup({
19355
+ label: "msdf text uniforms bind group",
19356
+ layout: pipeline.pipeline.getBindGroupLayout(2),
19357
+ entries: [
19358
+ {
19359
+ binding: 0,
19360
+ resource: { buffer: this.#engineUniformsBuffer }
19361
+ }
19362
+ ]
19363
+ });
19364
+ this.#bindGroups.push(engineUniformsBindGroup);
19365
+ }
19366
+ startFrame(uniform) {
19367
+ const device = this.#backend.device;
19368
+ device.queue.writeBuffer(this.#engineUniformsBuffer, 0, uniform.viewProjectionMatrix);
19369
+ this.#instanceIndex = 0;
19370
+ this.#textBlockOffset = 0;
19371
+ }
19372
+ processBatch(nodes) {
19373
+ if (nodes.length === 0)
19374
+ return 0;
19375
+ const renderPass = this.#backend.renderPass;
19376
+ renderPass.setPipeline(this.#pipeline);
19377
+ for (let i3 = 0;i3 < this.#bindGroups.length; i3++) {
19378
+ renderPass.setBindGroup(i3, this.#bindGroups[i3]);
19379
+ }
19380
+ for (const node of nodes) {
19381
+ if (!(node instanceof TextNode)) {
19382
+ console.error(node);
19383
+ throw new Error(`Tried to use WebGPUTextShader on something that isn't a TextNode: ${node}`);
19384
+ }
19385
+ const text = node.text;
19386
+ const formatting = node.formatting;
19387
+ const measurements = measureText(this.#font, text, formatting.wordWrap);
19388
+ const textBlockSize = 4 * text.length;
19389
+ const textDescriptorOffset = this.#instanceIndex * textDescriptorInstanceSize / Float32Array.BYTES_PER_ELEMENT;
19390
+ this.#cpuDescriptorBuffer.set(node.matrix, textDescriptorOffset);
19391
+ this.#cpuDescriptorBuffer.set([node.tint.r, node.tint.g, node.tint.b, node.tint.a], textDescriptorOffset + 12);
19392
+ const size = node.size ?? measurements;
19393
+ const fontSize = formatting.shrinkToFit ? findLargestFontSize(this.#font, text, size, formatting) : formatting.fontSize;
19394
+ const actualFontSize = fontSize || DEFAULT_FONT_SIZE;
19395
+ this.#cpuDescriptorBuffer[textDescriptorOffset + 16] = actualFontSize;
19396
+ this.#cpuDescriptorBuffer[textDescriptorOffset + 17] = formatting.align === "center" ? 0 : measurements.width;
19397
+ this.#cpuDescriptorBuffer[textDescriptorOffset + 18] = measurements.height;
19398
+ this.#cpuDescriptorBuffer[textDescriptorOffset + 19] = this.#textBlockOffset / 4;
19399
+ shapeText(this.#font, text, size, actualFontSize, formatting, this.#cpuTextBlockBuffer, this.#textBlockOffset);
19400
+ this.#backend.device.queue.writeBuffer(this.#descriptorBuffer, textDescriptorOffset * Float32Array.BYTES_PER_ELEMENT, this.#cpuDescriptorBuffer, textDescriptorOffset, textDescriptorInstanceSize / Float32Array.BYTES_PER_ELEMENT);
19401
+ this.#backend.device.queue.writeBuffer(this.#textBlockBuffer, this.#textBlockOffset * Float32Array.BYTES_PER_ELEMENT, this.#cpuTextBlockBuffer, this.#textBlockOffset, textBlockSize);
19402
+ this.#textBlockOffset += textBlockSize;
19403
+ renderPass.draw(4, measurements.printedCharCount, 4 * this.#instanceIndex, 0);
19404
+ this.#instanceIndex++;
19405
+ }
19406
+ return nodes.length;
19407
+ }
19408
+ endFrame() {}
19409
+ get font() {
19410
+ return this.#font;
19411
+ }
19412
+ get maxCharCount() {
19413
+ return this.#maxCharCount;
19414
+ }
19415
+ }
19416
+
19506
19417
  // src/textures/Bundles.ts
19507
19418
  class Bundles {
19508
19419
  #bundles = new Map;
@@ -19720,6 +19631,8 @@ class Bundles {
19720
19631
  const rightCrop = frame.sourceSize.w - frame.spriteSourceSize.x - frame.spriteSourceSize.w;
19721
19632
  const topCrop = frame.spriteSourceSize.y;
19722
19633
  const bottomCrop = frame.sourceSize.h - frame.spriteSourceSize.y - frame.spriteSourceSize.h;
19634
+ const halfTexelX = 0.5 / atlasWidth;
19635
+ const halfTexelY = 0.5 / atlasHeight;
19723
19636
  return {
19724
19637
  cropOffset: {
19725
19638
  x: leftCrop - rightCrop,
@@ -19730,16 +19643,16 @@ class Bundles {
19730
19643
  height: frame.sourceSize.h
19731
19644
  },
19732
19645
  uvOffset: {
19733
- x: frame.frame.x / atlasWidth,
19734
- y: frame.frame.y / atlasHeight
19646
+ x: frame.frame.x / atlasWidth + halfTexelX,
19647
+ y: frame.frame.y / atlasHeight + halfTexelY
19735
19648
  },
19736
19649
  uvScale: {
19737
19650
  width: frame.sourceSize.w / atlasWidth,
19738
19651
  height: frame.sourceSize.h / atlasHeight
19739
19652
  },
19740
19653
  uvScaleCropped: {
19741
- width: frame.frame.w / atlasWidth,
19742
- height: frame.frame.h / atlasHeight
19654
+ width: (frame.frame.w - 1) / atlasWidth,
19655
+ height: (frame.frame.h - 1) / atlasHeight
19743
19656
  }
19744
19657
  };
19745
19658
  }
@@ -19849,18 +19762,19 @@ async function packBitmapsToAtlas(images, textureSize, device) {
19849
19762
  height: space.height - texture.height
19850
19763
  });
19851
19764
  }
19765
+ const halfTexel = 0.5 / textureSize;
19852
19766
  atlasRegionMap.set(id, {
19853
19767
  uvOffset: {
19854
- x: space.x / textureSize,
19855
- y: space.y / textureSize
19768
+ x: space.x / textureSize + halfTexel,
19769
+ y: space.y / textureSize + halfTexel
19856
19770
  },
19857
19771
  uvScale: {
19858
19772
  width: originalSize.width / textureSize,
19859
19773
  height: originalSize.height / textureSize
19860
19774
  },
19861
19775
  uvScaleCropped: {
19862
- width: texture.width / textureSize,
19863
- height: texture.height / textureSize
19776
+ width: (texture.width - 1) / textureSize,
19777
+ height: (texture.height - 1) / textureSize
19864
19778
  },
19865
19779
  cropOffset: offset,
19866
19780
  originalSize
@@ -20174,13 +20088,13 @@ class AssetManager {
20174
20088
  const webgpuBackend = this.#backend;
20175
20089
  const device = webgpuBackend.device;
20176
20090
  const presentationFormat = webgpuBackend.presentationFormat;
20177
- const fontPipeline = await FontPipeline.create(device, font, presentationFormat, limits.maxTextLength);
20178
- const textShader = new WebGPUTextShader(webgpuBackend, fontPipeline, font, presentationFormat, limits.instanceCount);
20091
+ const fontPipeline = await FontPipeline2.create(device, font, presentationFormat, limits.maxTextLength);
20092
+ const textShader = new WebGPUTextShader2(webgpuBackend, fontPipeline, font, presentationFormat, limits.instanceCount);
20179
20093
  this.#fonts.set(id, textShader);
20180
20094
  } else {
20181
20095
  const webglBackend = this.#backend;
20182
- const fontPipeline = WebGLFontPipeline.create(webglBackend.gl, font, limits.maxTextLength);
20183
- const textShader = new WebGLTextShader(webglBackend, fontPipeline);
20096
+ const fontPipeline = WebGLFontPipeline2.create(webglBackend.gl, font, limits.maxTextLength);
20097
+ const textShader = new WebGLTextShader2(webglBackend, fontPipeline);
20184
20098
  this.#fonts.set(id, textShader);
20185
20099
  }
20186
20100
  return id;
@@ -20323,13 +20237,6 @@ class AssetManager {
20323
20237
  }
20324
20238
  }
20325
20239
 
20326
- // src/utils/mod.ts
20327
- var exports_mod2 = {};
20328
- __export(exports_mod2, {
20329
- assert: () => assert,
20330
- Pool: () => Pool
20331
- });
20332
-
20333
20240
  // src/utils/pool.ts
20334
20241
  class Pool {
20335
20242
  #items = [];
@@ -20351,6 +20258,7 @@ class Pool {
20351
20258
  this.#index = 0;
20352
20259
  }
20353
20260
  }
20261
+
20354
20262
  // src/Toodle.ts
20355
20263
  class Toodle {
20356
20264
  assets;
@@ -20687,11 +20595,11 @@ class Toodle {
20687
20595
  }
20688
20596
  let backend;
20689
20597
  if (backendType === "webgpu") {
20690
- backend = await WebGPUBackend.create(canvas, {
20598
+ backend = await WebGPUBackend2.create(canvas, {
20691
20599
  limits: options?.limits
20692
20600
  });
20693
20601
  } else {
20694
- backend = await WebGLBackend.create(canvas, {
20602
+ backend = await WebGLBackend2.create(canvas, {
20695
20603
  limits: options?.limits
20696
20604
  });
20697
20605
  }
@@ -20709,204 +20617,20 @@ class Toodle {
20709
20617
  return this.#backend;
20710
20618
  }
20711
20619
  }
20712
- // src/colors/mod.ts
20713
- var exports_mod3 = {};
20714
- __export(exports_mod3, {
20715
- web: () => web
20716
- });
20717
- var web = Object.freeze({
20718
- aliceBlue: { r: 0.941176, g: 0.972549, b: 1, a: 1 },
20719
- antiqueWhite: { r: 0.980392, g: 0.921569, b: 0.843137, a: 1 },
20720
- aqua: { r: 0, g: 1, b: 1, a: 1 },
20721
- aquamarine: { r: 0.498039, g: 1, b: 0.831373, a: 1 },
20722
- azure: { r: 0.941176, g: 1, b: 1, a: 1 },
20723
- beige: { r: 0.960784, g: 0.960784, b: 0.862745, a: 1 },
20724
- bisque: { r: 1, g: 0.894118, b: 0.768627, a: 1 },
20725
- black: { r: 0, g: 0, b: 0, a: 1 },
20726
- blanchedAlmond: { r: 1, g: 0.921569, b: 0.803922, a: 1 },
20727
- blue: { r: 0, g: 0, b: 1, a: 1 },
20728
- blueViolet: { r: 0.541176, g: 0.168627, b: 0.886275, a: 1 },
20729
- brown: { r: 0.647059, g: 0.164706, b: 0.164706, a: 1 },
20730
- burlywood: { r: 0.870588, g: 0.721569, b: 0.529412, a: 1 },
20731
- cadetBlue: { r: 0.372549, g: 0.619608, b: 0.627451, a: 1 },
20732
- chartreuse: { r: 0.498039, g: 1, b: 0, a: 1 },
20733
- chocolate: { r: 0.823529, g: 0.411765, b: 0.117647, a: 1 },
20734
- coral: { r: 1, g: 0.498039, b: 0.313726, a: 1 },
20735
- cornflowerBlue: { r: 0.392157, g: 0.584314, b: 0.929412, a: 1 },
20736
- cornsilk: { r: 1, g: 0.972549, b: 0.862745, a: 1 },
20737
- crimson: { r: 0.862745, g: 0.0784314, b: 0.235294, a: 1 },
20738
- cyan: { r: 0, g: 1, b: 1, a: 1 },
20739
- darkBlue: { r: 0, g: 0, b: 0.545098, a: 1 },
20740
- darkCyan: { r: 0, g: 0.545098, b: 0.545098, a: 1 },
20741
- darkGoldenrod: { r: 0.721569, g: 0.52549, b: 0.0431373, a: 1 },
20742
- darkGray: { r: 0.662745, g: 0.662745, b: 0.662745, a: 1 },
20743
- darkGreen: { r: 0, g: 0.392157, b: 0, a: 1 },
20744
- darkKhaki: { r: 0.741176, g: 0.717647, b: 0.419608, a: 1 },
20745
- darkMagenta: { r: 0.545098, g: 0, b: 0.545098, a: 1 },
20746
- darkOliveGreen: { r: 0.333333, g: 0.419608, b: 0.184314, a: 1 },
20747
- darkOrange: { r: 1, g: 0.54902, b: 0, a: 1 },
20748
- darkOrchid: { r: 0.6, g: 0.196078, b: 0.8, a: 1 },
20749
- darkRed: { r: 0.545098, g: 0, b: 0, a: 1 },
20750
- darkSalmon: { r: 0.913725, g: 0.588235, b: 0.478431, a: 1 },
20751
- darkSeaGreen: { r: 0.560784, g: 0.737255, b: 0.560784, a: 1 },
20752
- darkSlateBlue: { r: 0.282353, g: 0.239216, b: 0.545098, a: 1 },
20753
- darkSlateGray: { r: 0.184314, g: 0.309804, b: 0.309804, a: 1 },
20754
- darkTurquoise: { r: 0, g: 0.807843, b: 0.819608, a: 1 },
20755
- darkViolet: { r: 0.580392, g: 0, b: 0.827451, a: 1 },
20756
- deepPink: { r: 1, g: 0.0784314, b: 0.576471, a: 1 },
20757
- deepSkyBlue: { r: 0, g: 0.74902, b: 1, a: 1 },
20758
- dimGray: { r: 0.411765, g: 0.411765, b: 0.411765, a: 1 },
20759
- dodgerBlue: { r: 0.117647, g: 0.564706, b: 1, a: 1 },
20760
- firebrick: { r: 0.698039, g: 0.133333, b: 0.133333, a: 1 },
20761
- floralWhite: { r: 1, g: 0.980392, b: 0.941176, a: 1 },
20762
- forestGreen: { r: 0.133333, g: 0.545098, b: 0.133333, a: 1 },
20763
- fuchsia: { r: 1, g: 0, b: 1, a: 1 },
20764
- gainsboro: { r: 0.862745, g: 0.862745, b: 0.862745, a: 1 },
20765
- ghostWhite: { r: 0.972549, g: 0.972549, b: 1, a: 1 },
20766
- gold: { r: 1, g: 0.843137, b: 0, a: 1 },
20767
- goldenrod: { r: 0.854902, g: 0.647059, b: 0.12549, a: 1 },
20768
- gray: { r: 0.745098, g: 0.745098, b: 0.745098, a: 1 },
20769
- green: { r: 0, g: 1, b: 0, a: 1 },
20770
- greenYellow: { r: 0.678431, g: 1, b: 0.184314, a: 1 },
20771
- honeydew: { r: 0.941176, g: 1, b: 0.941176, a: 1 },
20772
- hotPink: { r: 1, g: 0.411765, b: 0.705882, a: 1 },
20773
- indigo: { r: 0.294118, g: 0, b: 0.509804, a: 1 },
20774
- ivory: { r: 1, g: 1, b: 0.941176, a: 1 },
20775
- khaki: { r: 0.941176, g: 0.901961, b: 0.54902, a: 1 },
20776
- lavender: { r: 0.901961, g: 0.901961, b: 0.980392, a: 1 },
20777
- lavenderBlush: { r: 1, g: 0.941176, b: 0.960784, a: 1 },
20778
- lawnGreen: { r: 0.486275, g: 0.988235, b: 0, a: 1 },
20779
- lemonChiffon: { r: 1, g: 0.980392, b: 0.803922, a: 1 },
20780
- lightBlue: { r: 0.678431, g: 0.847059, b: 0.901961, a: 1 },
20781
- lightCoral: { r: 0.941176, g: 0.501961, b: 0.501961, a: 1 },
20782
- lightCyan: { r: 0.878431, g: 1, b: 1, a: 1 },
20783
- lightGoldenrod: { r: 0.980392, g: 0.980392, b: 0.823529, a: 1 },
20784
- lightGray: { r: 0.827451, g: 0.827451, b: 0.827451, a: 1 },
20785
- lightGreen: { r: 0.564706, g: 0.933333, b: 0.564706, a: 1 },
20786
- lightPink: { r: 1, g: 0.713726, b: 0.756863, a: 1 },
20787
- lightSalmon: { r: 1, g: 0.627451, b: 0.478431, a: 1 },
20788
- lightSeaGreen: { r: 0.12549, g: 0.698039, b: 0.666667, a: 1 },
20789
- lightSkyBlue: { r: 0.529412, g: 0.807843, b: 0.980392, a: 1 },
20790
- lightSlateGray: { r: 0.466667, g: 0.533333, b: 0.6, a: 1 },
20791
- lightSteelBlue: { r: 0.690196, g: 0.768627, b: 0.870588, a: 1 },
20792
- lightYellow: { r: 1, g: 1, b: 0.878431, a: 1 },
20793
- lime: { r: 0, g: 1, b: 0, a: 1 },
20794
- limeGreen: { r: 0.196078, g: 0.803922, b: 0.196078, a: 1 },
20795
- linen: { r: 0.980392, g: 0.941176, b: 0.901961, a: 1 },
20796
- magenta: { r: 1, g: 0, b: 1, a: 1 },
20797
- maroon: { r: 0.690196, g: 0.188235, b: 0.376471, a: 1 },
20798
- mediumAquamarine: { r: 0.4, g: 0.803922, b: 0.666667, a: 1 },
20799
- mediumBlue: { r: 0, g: 0, b: 0.803922, a: 1 },
20800
- mediumOrchid: { r: 0.729412, g: 0.333333, b: 0.827451, a: 1 },
20801
- mediumPurple: { r: 0.576471, g: 0.439216, b: 0.858824, a: 1 },
20802
- mediumSeaGreen: { r: 0.235294, g: 0.701961, b: 0.443137, a: 1 },
20803
- mediumSlateBlue: { r: 0.482353, g: 0.407843, b: 0.933333, a: 1 },
20804
- mediumSpringGreen: { r: 0, g: 0.980392, b: 0.603922, a: 1 },
20805
- mediumTurquoise: { r: 0.282353, g: 0.819608, b: 0.8, a: 1 },
20806
- mediumVioletRed: { r: 0.780392, g: 0.0823529, b: 0.521569, a: 1 },
20807
- midnightBlue: { r: 0.0980392, g: 0.0980392, b: 0.439216, a: 1 },
20808
- mintCream: { r: 0.960784, g: 1, b: 0.980392, a: 1 },
20809
- mistyRose: { r: 1, g: 0.894118, b: 0.882353, a: 1 },
20810
- moccasin: { r: 1, g: 0.894118, b: 0.709804, a: 1 },
20811
- navyBlue: { r: 0, g: 0, b: 0.501961, a: 1 },
20812
- oldLace: { r: 0.992157, g: 0.960784, b: 0.901961, a: 1 },
20813
- olive: { r: 0.501961, g: 0.501961, b: 0, a: 1 },
20814
- oliveDrab: { r: 0.419608, g: 0.556863, b: 0.137255, a: 1 },
20815
- orange: { r: 1, g: 0.647059, b: 0, a: 1 },
20816
- orangeRed: { r: 1, g: 0.270588, b: 0, a: 1 },
20817
- orchid: { r: 0.854902, g: 0.439216, b: 0.839216, a: 1 },
20818
- paleGoldenrod: { r: 0.933333, g: 0.909804, b: 0.666667, a: 1 },
20819
- paleGreen: { r: 0.596078, g: 0.984314, b: 0.596078, a: 1 },
20820
- paleTurquoise: { r: 0.686275, g: 0.933333, b: 0.933333, a: 1 },
20821
- paleVioletRed: { r: 0.858824, g: 0.439216, b: 0.576471, a: 1 },
20822
- papayaWhip: { r: 1, g: 0.937255, b: 0.835294, a: 1 },
20823
- peachPuff: { r: 1, g: 0.854902, b: 0.72549, a: 1 },
20824
- peru: { r: 0.803922, g: 0.521569, b: 0.247059, a: 1 },
20825
- pink: { r: 1, g: 0.752941, b: 0.796078, a: 1 },
20826
- plum: { r: 0.866667, g: 0.627451, b: 0.866667, a: 1 },
20827
- powderBlue: { r: 0.690196, g: 0.878431, b: 0.901961, a: 1 },
20828
- purple: { r: 0.627451, g: 0.12549, b: 0.941176, a: 1 },
20829
- rebeccaPurple: { r: 0.4, g: 0.2, b: 0.6, a: 1 },
20830
- red: { r: 1, g: 0, b: 0, a: 1 },
20831
- rosyBrown: { r: 0.737255, g: 0.560784, b: 0.560784, a: 1 },
20832
- royalBlue: { r: 0.254902, g: 0.411765, b: 0.882353, a: 1 },
20833
- saddleBrown: { r: 0.545098, g: 0.270588, b: 0.0745098, a: 1 },
20834
- salmon: { r: 0.980392, g: 0.501961, b: 0.447059, a: 1 },
20835
- sandyBrown: { r: 0.956863, g: 0.643137, b: 0.376471, a: 1 },
20836
- seaGreen: { r: 0.180392, g: 0.545098, b: 0.341176, a: 1 },
20837
- seashell: { r: 1, g: 0.960784, b: 0.933333, a: 1 },
20838
- sienna: { r: 0.627451, g: 0.321569, b: 0.176471, a: 1 },
20839
- silver: { r: 0.752941, g: 0.752941, b: 0.752941, a: 1 },
20840
- skyBlue: { r: 0.529412, g: 0.807843, b: 0.921569, a: 1 },
20841
- slateBlue: { r: 0.415686, g: 0.352941, b: 0.803922, a: 1 },
20842
- slateGray: { r: 0.439216, g: 0.501961, b: 0.564706, a: 1 },
20843
- snow: { r: 1, g: 0.980392, b: 0.980392, a: 1 },
20844
- springGreen: { r: 0, g: 1, b: 0.498039, a: 1 },
20845
- steelBlue: { r: 0.27451, g: 0.509804, b: 0.705882, a: 1 },
20846
- tan: { r: 0.823529, g: 0.705882, b: 0.54902, a: 1 },
20847
- teal: { r: 0, g: 0.501961, b: 0.501961, a: 1 },
20848
- thistle: { r: 0.847059, g: 0.74902, b: 0.847059, a: 1 },
20849
- tomato: { r: 1, g: 0.388235, b: 0.278431, a: 1 },
20850
- transparent: { r: 1, g: 1, b: 1, a: 0 },
20851
- turquoise: { r: 0.25098, g: 0.878431, b: 0.815686, a: 1 },
20852
- violet: { r: 0.933333, g: 0.509804, b: 0.933333, a: 1 },
20853
- webGray: { r: 0.501961, g: 0.501961, b: 0.501961, a: 1 },
20854
- webGreen: { r: 0, g: 0.501961, b: 0, a: 1 },
20855
- webMaroon: { r: 0.501961, g: 0, b: 0, a: 1 },
20856
- webPurple: { r: 0.501961, g: 0, b: 0.501961, a: 1 },
20857
- wheat: { r: 0.960784, g: 0.870588, b: 0.701961, a: 1 },
20858
- white: { r: 1, g: 1, b: 1, a: 1 },
20859
- whiteSmoke: { r: 0.960784, g: 0.960784, b: 0.960784, a: 1 },
20860
- yellow: { r: 1, g: 1, b: 0, a: 1 },
20861
- yellowGreen: { r: 0.603922, g: 0.803922, b: 0.196078, a: 1 }
20862
- });
20863
- // src/math/mod.ts
20864
- var exports_mod4 = {};
20865
- __export(exports_mod4, {
20866
- transformPoint: () => transformPoint,
20867
- rad2deg: () => rad2deg,
20868
- deg2rad: () => deg2rad,
20869
- createViewMatrix: () => createViewMatrix,
20870
- createProjectionMatrix: () => createProjectionMatrix,
20871
- createModelMatrix: () => createModelMatrix,
20872
- convertWorldToScreen: () => convertWorldToScreen,
20873
- convertScreenToWorld: () => convertScreenToWorld
20874
- });
20875
- // src/scene/mod.ts
20876
- var exports_mod5 = {};
20877
- __export(exports_mod5, {
20878
- TextNode: () => TextNode,
20879
- SceneNode: () => SceneNode,
20880
- QuadNode: () => QuadNode,
20881
- DEFAULT_FONT_SIZE: () => DEFAULT_FONT_SIZE,
20882
- Camera: () => Camera
20883
- });
20884
- // src/screen/mod.ts
20885
- var exports_mod6 = {};
20886
- // src/text/mod.ts
20887
- var exports_mod7 = {};
20888
- __export(exports_mod7, {
20889
- TextShader: () => WebGPUTextShader
20890
- });
20891
- // src/textures/mod.ts
20892
- var exports_mod8 = {};
20893
- __export(exports_mod8, {
20894
- Bundles: () => Bundles
20895
- });
20896
20620
  export {
20897
- exports_mod2 as Utils,
20621
+ Utils,
20898
20622
  Toodle,
20899
- exports_mod8 as Textures,
20900
- exports_mod7 as Text,
20901
- exports_mod6 as Screen,
20902
- exports_mod5 as Scene,
20903
- exports_mod4 as GfxMath,
20623
+ Textures,
20624
+ Text,
20625
+ Screen,
20626
+ Scene,
20627
+ GfxMath,
20904
20628
  DEFAULT_LIMITS,
20905
- exports_mod3 as Colors,
20906
- Bundles,
20907
- exports_mod as Backends,
20908
- AssetManager
20629
+ Colors,
20630
+ Bundles2 as Bundles,
20631
+ Backends,
20632
+ AssetManager2 as AssetManager
20909
20633
  };
20910
20634
 
20911
- //# debugId=8FA83A386B693B6064756E2164756E21
20635
+ //# debugId=F5D1111C12CFF04864756E2164756E21
20912
20636
  //# sourceMappingURL=mod.js.map