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