@multiplekex/shallot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/dist/core/builder.d.ts +25 -0
  2. package/dist/core/builder.d.ts.map +1 -0
  3. package/dist/core/builder.js +88 -0
  4. package/dist/core/builder.js.map +1 -0
  5. package/dist/core/component.d.ts +29 -0
  6. package/dist/core/component.d.ts.map +1 -0
  7. package/dist/core/component.js +36 -0
  8. package/dist/core/component.js.map +1 -0
  9. package/dist/core/index.d.ts +13 -0
  10. package/dist/core/index.d.ts.map +1 -0
  11. package/dist/core/math.d.ts +32 -0
  12. package/dist/core/math.d.ts.map +1 -0
  13. package/dist/core/math.js +39 -0
  14. package/dist/core/math.js.map +1 -0
  15. package/dist/core/relation.d.ts +16 -0
  16. package/dist/core/relation.d.ts.map +1 -0
  17. package/dist/core/relation.js +32 -0
  18. package/dist/core/relation.js.map +1 -0
  19. package/dist/core/resource.d.ts +9 -0
  20. package/dist/core/resource.d.ts.map +1 -0
  21. package/dist/core/resource.js +12 -0
  22. package/dist/core/resource.js.map +1 -0
  23. package/dist/core/runtime.d.ts +13 -0
  24. package/dist/core/runtime.d.ts.map +1 -0
  25. package/dist/core/runtime.js +118 -0
  26. package/dist/core/runtime.js.map +1 -0
  27. package/dist/core/scheduler.d.ts +47 -0
  28. package/dist/core/scheduler.d.ts.map +1 -0
  29. package/dist/core/scheduler.js +138 -0
  30. package/dist/core/scheduler.js.map +1 -0
  31. package/dist/core/state.d.ts +62 -0
  32. package/dist/core/state.d.ts.map +1 -0
  33. package/dist/core/state.js +185 -0
  34. package/dist/core/state.js.map +1 -0
  35. package/dist/core/strings.d.ts +3 -0
  36. package/dist/core/strings.d.ts.map +1 -0
  37. package/dist/core/strings.js +11 -0
  38. package/dist/core/strings.js.map +1 -0
  39. package/dist/core/types.d.ts +33 -0
  40. package/dist/core/types.d.ts.map +1 -0
  41. package/dist/core/xml.d.ts +42 -0
  42. package/dist/core/xml.d.ts.map +1 -0
  43. package/dist/core/xml.js +349 -0
  44. package/dist/core/xml.js.map +1 -0
  45. package/dist/extras/arrows/index.d.ts +33 -0
  46. package/dist/extras/arrows/index.d.ts.map +1 -0
  47. package/dist/extras/arrows/index.js +288 -0
  48. package/dist/extras/arrows/index.js.map +1 -0
  49. package/dist/extras/index.d.ts +5 -0
  50. package/dist/extras/index.d.ts.map +1 -0
  51. package/dist/extras/index.js +31 -0
  52. package/dist/extras/index.js.map +1 -0
  53. package/dist/extras/lines/index.d.ts +36 -0
  54. package/dist/extras/lines/index.d.ts.map +1 -0
  55. package/dist/extras/lines/index.js +288 -0
  56. package/dist/extras/lines/index.js.map +1 -0
  57. package/dist/extras/orbit/index.d.ts +20 -0
  58. package/dist/extras/orbit/index.d.ts.map +1 -0
  59. package/dist/extras/orbit/index.js +93 -0
  60. package/dist/extras/orbit/index.js.map +1 -0
  61. package/dist/extras/text/index.d.ts +64 -0
  62. package/dist/extras/text/index.d.ts.map +1 -0
  63. package/dist/extras/text/index.js +423 -0
  64. package/dist/extras/text/index.js.map +1 -0
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +187 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/rust/transforms/pkg/shallot_transforms.js +107 -0
  70. package/dist/rust/transforms/pkg/shallot_transforms.js.map +1 -0
  71. package/dist/standard/compute/graph.d.ts +37 -0
  72. package/dist/standard/compute/graph.d.ts.map +1 -0
  73. package/dist/standard/compute/graph.js +85 -0
  74. package/dist/standard/compute/graph.js.map +1 -0
  75. package/dist/standard/compute/index.d.ts +21 -0
  76. package/dist/standard/compute/index.d.ts.map +1 -0
  77. package/dist/standard/compute/index.js +81 -0
  78. package/dist/standard/compute/index.js.map +1 -0
  79. package/dist/standard/defaults.d.ts +3 -0
  80. package/dist/standard/defaults.d.ts.map +1 -0
  81. package/dist/standard/defaults.js +18 -0
  82. package/dist/standard/defaults.js.map +1 -0
  83. package/dist/standard/index.d.ts +8 -0
  84. package/dist/standard/index.d.ts.map +1 -0
  85. package/dist/standard/input/index.d.ts +5 -0
  86. package/dist/standard/input/index.d.ts.map +1 -0
  87. package/dist/standard/input/index.js +70 -0
  88. package/dist/standard/input/index.js.map +1 -0
  89. package/dist/standard/loading/index.d.ts +7 -0
  90. package/dist/standard/loading/index.d.ts.map +1 -0
  91. package/dist/standard/loading/index.js +91 -0
  92. package/dist/standard/loading/index.js.map +1 -0
  93. package/dist/standard/render/camera.d.ts +36 -0
  94. package/dist/standard/render/camera.d.ts.map +1 -0
  95. package/dist/standard/render/camera.js +71 -0
  96. package/dist/standard/render/camera.js.map +1 -0
  97. package/dist/standard/render/forward.d.ts +30 -0
  98. package/dist/standard/render/forward.d.ts.map +1 -0
  99. package/dist/standard/render/forward.js +158 -0
  100. package/dist/standard/render/forward.js.map +1 -0
  101. package/dist/standard/render/index.d.ts +22 -0
  102. package/dist/standard/render/index.d.ts.map +1 -0
  103. package/dist/standard/render/index.js +153 -0
  104. package/dist/standard/render/index.js.map +1 -0
  105. package/dist/standard/render/light.d.ts +25 -0
  106. package/dist/standard/render/light.d.ts.map +1 -0
  107. package/dist/standard/render/light.js +48 -0
  108. package/dist/standard/render/light.js.map +1 -0
  109. package/dist/standard/render/mesh/box.d.ts +3 -0
  110. package/dist/standard/render/mesh/box.d.ts.map +1 -0
  111. package/dist/standard/render/mesh/box.js +190 -0
  112. package/dist/standard/render/mesh/box.js.map +1 -0
  113. package/dist/standard/render/mesh/index.d.ts +52 -0
  114. package/dist/standard/render/mesh/index.d.ts.map +1 -0
  115. package/dist/standard/render/mesh/index.js +158 -0
  116. package/dist/standard/render/mesh/index.js.map +1 -0
  117. package/dist/standard/render/mesh/plane.d.ts +3 -0
  118. package/dist/standard/render/mesh/plane.d.ts.map +1 -0
  119. package/dist/standard/render/mesh/plane.js +33 -0
  120. package/dist/standard/render/mesh/plane.js.map +1 -0
  121. package/dist/standard/render/mesh/sphere.d.ts +3 -0
  122. package/dist/standard/render/mesh/sphere.d.ts.map +1 -0
  123. package/dist/standard/render/mesh/sphere.js +25 -0
  124. package/dist/standard/render/mesh/sphere.js.map +1 -0
  125. package/dist/standard/render/postprocess.d.ts +11 -0
  126. package/dist/standard/render/postprocess.d.ts.map +1 -0
  127. package/dist/standard/render/postprocess.js +190 -0
  128. package/dist/standard/render/postprocess.js.map +1 -0
  129. package/dist/standard/render/scene.d.ts +8 -0
  130. package/dist/standard/render/scene.d.ts.map +1 -0
  131. package/dist/standard/render/scene.js +67 -0
  132. package/dist/standard/render/scene.js.map +1 -0
  133. package/dist/standard/transforms/index.d.ts +27 -0
  134. package/dist/standard/transforms/index.d.ts.map +1 -0
  135. package/dist/standard/transforms/index.js +122 -0
  136. package/dist/standard/transforms/index.js.map +1 -0
  137. package/dist/standard/transforms/wasm.d.ts +17 -0
  138. package/dist/standard/transforms/wasm.d.ts.map +1 -0
  139. package/dist/standard/transforms/wasm.js +31 -0
  140. package/dist/standard/transforms/wasm.js.map +1 -0
  141. package/dist/standard/tween/easing.d.ts +5 -0
  142. package/dist/standard/tween/easing.d.ts.map +1 -0
  143. package/dist/standard/tween/easing.js +80 -0
  144. package/dist/standard/tween/easing.js.map +1 -0
  145. package/dist/standard/tween/index.d.ts +4 -0
  146. package/dist/standard/tween/index.d.ts.map +1 -0
  147. package/dist/standard/tween/sequence.d.ts +20 -0
  148. package/dist/standard/tween/sequence.d.ts.map +1 -0
  149. package/dist/standard/tween/sequence.js +95 -0
  150. package/dist/standard/tween/sequence.js.map +1 -0
  151. package/dist/standard/tween/tween.d.ts +28 -0
  152. package/dist/standard/tween/tween.d.ts.map +1 -0
  153. package/dist/standard/tween/tween.js +136 -0
  154. package/dist/standard/tween/tween.js.map +1 -0
  155. package/package.json +63 -0
  156. package/src/core/builder.ts +148 -0
  157. package/src/core/component.ts +71 -0
  158. package/src/core/index.ts +92 -0
  159. package/src/core/math.ts +128 -0
  160. package/src/core/relation.ts +46 -0
  161. package/src/core/resource.ts +18 -0
  162. package/src/core/runtime.ts +185 -0
  163. package/src/core/scheduler.ts +238 -0
  164. package/src/core/state.ts +295 -0
  165. package/src/core/strings.ts +10 -0
  166. package/src/core/types.ts +37 -0
  167. package/src/core/xml.ts +676 -0
  168. package/src/extras/arrows/index.ts +363 -0
  169. package/src/extras/index.ts +4 -0
  170. package/src/extras/lines/index.ts +368 -0
  171. package/src/extras/orbit/index.ts +133 -0
  172. package/src/extras/text/index.ts +641 -0
  173. package/src/index.ts +3 -0
  174. package/src/standard/compute/graph.ts +165 -0
  175. package/src/standard/compute/index.ts +116 -0
  176. package/src/standard/defaults.ts +17 -0
  177. package/src/standard/index.ts +7 -0
  178. package/src/standard/input/index.ts +142 -0
  179. package/src/standard/loading/index.ts +136 -0
  180. package/src/standard/render/camera.ts +87 -0
  181. package/src/standard/render/forward.ts +212 -0
  182. package/src/standard/render/index.ts +175 -0
  183. package/src/standard/render/light.ts +81 -0
  184. package/src/standard/render/mesh/box.ts +20 -0
  185. package/src/standard/render/mesh/index.ts +227 -0
  186. package/src/standard/render/mesh/plane.ts +11 -0
  187. package/src/standard/render/mesh/sphere.ts +40 -0
  188. package/src/standard/render/postprocess.ts +235 -0
  189. package/src/standard/render/scene.ts +116 -0
  190. package/src/standard/transforms/index.ts +184 -0
  191. package/src/standard/transforms/wasm.ts +61 -0
  192. package/src/standard/tween/easing.ts +169 -0
  193. package/src/standard/tween/index.ts +13 -0
  194. package/src/standard/tween/sequence.ts +142 -0
  195. package/src/standard/tween/tween.ts +265 -0
  196. package/src/vite-env.d.ts +6 -0
@@ -0,0 +1,641 @@
1
+ import TinySDF from "@mapbox/tiny-sdf";
2
+ import { MAX_ENTITIES, resource, type Plugin, type State, type System } from "../../core";
3
+ import { setTraits, type FieldAccessor } from "../../core/component";
4
+ import {
5
+ Compute,
6
+ ComputePlugin,
7
+ type ComputeNode,
8
+ type ExecutionContext,
9
+ } from "../../standard/compute";
10
+ import { Render, RenderPlugin } from "../../standard/render";
11
+ import { Transform } from "../../standard/transforms";
12
+
13
+ const MAX_GLYPHS = 50000;
14
+ const GLYPH_FLOATS = 16;
15
+
16
+ export const TextData = {
17
+ data: new Float32Array(MAX_ENTITIES * 12),
18
+ };
19
+
20
+ interface TextProxy extends Array<number>, FieldAccessor {}
21
+
22
+ function textProxy(offset: number): TextProxy {
23
+ const data = TextData.data;
24
+
25
+ function getValue(eid: number): number {
26
+ return data[eid * 12 + offset];
27
+ }
28
+
29
+ function setValue(eid: number, value: number): void {
30
+ data[eid * 12 + offset] = value;
31
+ }
32
+
33
+ return new Proxy([] as unknown as TextProxy, {
34
+ get(_, prop) {
35
+ if (prop === "get") return getValue;
36
+ if (prop === "set") return setValue;
37
+ const eid = Number(prop);
38
+ if (Number.isNaN(eid)) return undefined;
39
+ return getValue(eid);
40
+ },
41
+ set(_, prop, value) {
42
+ const eid = Number(prop);
43
+ if (Number.isNaN(eid)) return false;
44
+ setValue(eid, value);
45
+ return true;
46
+ },
47
+ });
48
+ }
49
+
50
+ function colorProxy(): TextProxy {
51
+ const data = TextData.data;
52
+
53
+ function getValue(eid: number): number {
54
+ const offset = eid * 12 + 8;
55
+ const r = Math.round(data[offset] * 255);
56
+ const g = Math.round(data[offset + 1] * 255);
57
+ const b = Math.round(data[offset + 2] * 255);
58
+ return (r << 16) | (g << 8) | b;
59
+ }
60
+
61
+ function setValue(eid: number, value: number): void {
62
+ const offset = eid * 12 + 8;
63
+ data[offset] = ((value >> 16) & 0xff) / 255;
64
+ data[offset + 1] = ((value >> 8) & 0xff) / 255;
65
+ data[offset + 2] = (value & 0xff) / 255;
66
+ data[offset + 3] = 1;
67
+ }
68
+
69
+ return new Proxy([] as unknown as TextProxy, {
70
+ get(_, prop) {
71
+ if (prop === "get") return getValue;
72
+ if (prop === "set") return setValue;
73
+ const eid = Number(prop);
74
+ if (Number.isNaN(eid)) return undefined;
75
+ return getValue(eid);
76
+ },
77
+ set(_, prop, value) {
78
+ const eid = Number(prop);
79
+ if (Number.isNaN(eid)) return false;
80
+ setValue(eid, value);
81
+ return true;
82
+ },
83
+ });
84
+ }
85
+
86
+ export const Text: {
87
+ fontSize: TextProxy;
88
+ opacity: TextProxy;
89
+ visible: TextProxy;
90
+ anchorX: TextProxy;
91
+ anchorY: TextProxy;
92
+ color: TextProxy;
93
+ } = {
94
+ fontSize: textProxy(0),
95
+ opacity: textProxy(1),
96
+ visible: textProxy(2),
97
+ anchorX: textProxy(3),
98
+ anchorY: textProxy(4),
99
+ color: colorProxy(),
100
+ };
101
+
102
+ setTraits(Text, {
103
+ defaults: () => ({
104
+ fontSize: 1,
105
+ opacity: 1,
106
+ visible: 1,
107
+ anchorX: 0,
108
+ anchorY: 0,
109
+ color: 0xffffff,
110
+ }),
111
+ accessors: {
112
+ fontSize: Text.fontSize,
113
+ opacity: Text.opacity,
114
+ visible: Text.visible,
115
+ anchorX: Text.anchorX,
116
+ anchorY: Text.anchorY,
117
+ color: Text.color,
118
+ },
119
+ });
120
+
121
+ interface GlyphMetrics {
122
+ width: number;
123
+ height: number;
124
+ glyphWidth: number;
125
+ glyphHeight: number;
126
+ glyphTop: number;
127
+ glyphLeft: number;
128
+ advance: number;
129
+ u0: number;
130
+ v0: number;
131
+ u1: number;
132
+ v1: number;
133
+ }
134
+
135
+ interface GlyphAtlas {
136
+ texture: GPUTexture;
137
+ textureView: GPUTextureView;
138
+ width: number;
139
+ height: number;
140
+ glyphs: Map<string, GlyphMetrics>;
141
+ rowHeight: number;
142
+ cursorX: number;
143
+ cursorY: number;
144
+ sdf: TinySDF;
145
+ sdfFontSize: number;
146
+ }
147
+
148
+ function createGlyphAtlas(device: GPUDevice): GlyphAtlas {
149
+ const width = 1024;
150
+ const height = 1024;
151
+ const fontSize = 32;
152
+
153
+ const texture = device.createTexture({
154
+ size: { width, height },
155
+ format: "r8unorm",
156
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
157
+ label: "glyphAtlas",
158
+ });
159
+
160
+ const sdf = new TinySDF({
161
+ fontSize,
162
+ fontFamily: "monospace",
163
+ fontWeight: "normal",
164
+ fontStyle: "normal",
165
+ buffer: 3,
166
+ radius: 8,
167
+ cutoff: 0.5,
168
+ });
169
+
170
+ return {
171
+ texture,
172
+ textureView: texture.createView(),
173
+ width,
174
+ height,
175
+ glyphs: new Map(),
176
+ rowHeight: 0,
177
+ cursorX: 0,
178
+ cursorY: 0,
179
+ sdf,
180
+ sdfFontSize: fontSize,
181
+ };
182
+ }
183
+
184
+ function ensureGlyph(device: GPUDevice, atlas: GlyphAtlas, char: string): GlyphMetrics {
185
+ const existing = atlas.glyphs.get(char);
186
+ if (existing) return existing;
187
+
188
+ const glyph = atlas.sdf.draw(char);
189
+
190
+ if (atlas.cursorX + glyph.width > atlas.width) {
191
+ atlas.cursorX = 0;
192
+ atlas.cursorY += atlas.rowHeight;
193
+ atlas.rowHeight = 0;
194
+ }
195
+
196
+ if (atlas.cursorY + glyph.height > atlas.height) {
197
+ throw new Error("Glyph atlas full");
198
+ }
199
+
200
+ const glyphData = new Uint8Array(glyph.data.length);
201
+ glyphData.set(glyph.data);
202
+
203
+ device.queue.writeTexture(
204
+ {
205
+ texture: atlas.texture,
206
+ origin: { x: atlas.cursorX, y: atlas.cursorY },
207
+ },
208
+ glyphData,
209
+ { bytesPerRow: glyph.width },
210
+ { width: glyph.width, height: glyph.height }
211
+ );
212
+
213
+ const metrics: GlyphMetrics = {
214
+ width: glyph.width,
215
+ height: glyph.height,
216
+ glyphWidth: glyph.glyphWidth,
217
+ glyphHeight: glyph.glyphHeight,
218
+ glyphTop: glyph.glyphTop,
219
+ glyphLeft: glyph.glyphLeft,
220
+ advance: glyph.glyphAdvance,
221
+ u0: atlas.cursorX / atlas.width,
222
+ v0: atlas.cursorY / atlas.height,
223
+ u1: (atlas.cursorX + glyph.width) / atlas.width,
224
+ v1: (atlas.cursorY + glyph.height) / atlas.height,
225
+ };
226
+
227
+ atlas.glyphs.set(char, metrics);
228
+ atlas.cursorX += glyph.width;
229
+ atlas.rowHeight = Math.max(atlas.rowHeight, glyph.height);
230
+
231
+ return metrics;
232
+ }
233
+
234
+ function ensureString(device: GPUDevice, atlas: GlyphAtlas, text: string): void {
235
+ for (const char of text) {
236
+ ensureGlyph(device, atlas, char);
237
+ }
238
+ }
239
+
240
+ interface LayoutGlyph {
241
+ x: number;
242
+ y: number;
243
+ width: number;
244
+ height: number;
245
+ u0: number;
246
+ v0: number;
247
+ u1: number;
248
+ v1: number;
249
+ }
250
+
251
+ interface LayoutResult {
252
+ glyphs: LayoutGlyph[];
253
+ width: number;
254
+ height: number;
255
+ }
256
+
257
+ function layoutText(text: string, atlas: GlyphAtlas, fontSize: number): LayoutResult {
258
+ const glyphs: LayoutGlyph[] = [];
259
+ const scale = fontSize / atlas.sdfFontSize;
260
+
261
+ let cursorX = 0;
262
+ let maxHeight = 0;
263
+
264
+ for (const char of text) {
265
+ const metrics = atlas.glyphs.get(char);
266
+ if (!metrics) continue;
267
+
268
+ const glyphW = metrics.glyphWidth * scale;
269
+ const glyphH = metrics.glyphHeight * scale;
270
+ const advance = metrics.advance * scale;
271
+
272
+ const x = cursorX + metrics.glyphLeft * scale;
273
+ const y = (metrics.glyphTop - metrics.glyphHeight) * scale;
274
+
275
+ glyphs.push({
276
+ x,
277
+ y,
278
+ width: glyphW,
279
+ height: glyphH,
280
+ u0: metrics.u0,
281
+ v0: metrics.v0,
282
+ u1: metrics.u1,
283
+ v1: metrics.v1,
284
+ });
285
+
286
+ cursorX += advance;
287
+ maxHeight = Math.max(maxHeight, glyphH);
288
+ }
289
+
290
+ return {
291
+ glyphs,
292
+ width: cursorX,
293
+ height: maxHeight,
294
+ };
295
+ }
296
+
297
+ const textShader = /* wgsl */ `
298
+ struct Scene {
299
+ viewProj: mat4x4<f32>,
300
+ cameraWorld: mat4x4<f32>,
301
+ }
302
+
303
+ struct GlyphInstance {
304
+ posX: f32,
305
+ posY: f32,
306
+ posZ: f32,
307
+ _pad0: f32,
308
+ width: f32,
309
+ height: f32,
310
+ _pad1: vec2<f32>,
311
+ u0: f32,
312
+ v0: f32,
313
+ u1: f32,
314
+ v1: f32,
315
+ color: vec4<f32>,
316
+ }
317
+
318
+ @group(0) @binding(0) var<uniform> scene: Scene;
319
+ @group(0) @binding(1) var<storage, read> glyphs: array<GlyphInstance>;
320
+ @group(0) @binding(2) var atlasTexture: texture_2d<f32>;
321
+ @group(0) @binding(3) var atlasSampler: sampler;
322
+
323
+ struct VertexOutput {
324
+ @builtin(position) position: vec4<f32>,
325
+ @location(0) uv: vec2<f32>,
326
+ @location(1) color: vec4<f32>,
327
+ }
328
+
329
+ const SDF_SMOOTHING: f32 = 0.1;
330
+
331
+ @vertex
332
+ fn vs(@builtin(vertex_index) vid: u32) -> VertexOutput {
333
+ let glyphIdx = vid / 6u;
334
+ let cornerIdx = vid % 6u;
335
+
336
+ let glyph = glyphs[glyphIdx];
337
+
338
+ var localPos: vec2<f32>;
339
+ var uv: vec2<f32>;
340
+
341
+ switch cornerIdx {
342
+ case 0u: {
343
+ localPos = vec2(0.0, 0.0);
344
+ uv = vec2(glyph.u0, glyph.v1);
345
+ }
346
+ case 1u: {
347
+ localPos = vec2(1.0, 0.0);
348
+ uv = vec2(glyph.u1, glyph.v1);
349
+ }
350
+ case 2u: {
351
+ localPos = vec2(1.0, 1.0);
352
+ uv = vec2(glyph.u1, glyph.v0);
353
+ }
354
+ case 3u: {
355
+ localPos = vec2(0.0, 0.0);
356
+ uv = vec2(glyph.u0, glyph.v1);
357
+ }
358
+ case 4u: {
359
+ localPos = vec2(1.0, 1.0);
360
+ uv = vec2(glyph.u1, glyph.v0);
361
+ }
362
+ case 5u: {
363
+ localPos = vec2(0.0, 1.0);
364
+ uv = vec2(glyph.u0, glyph.v0);
365
+ }
366
+ default: {
367
+ localPos = vec2(0.0);
368
+ uv = vec2(0.0);
369
+ }
370
+ }
371
+
372
+ let worldPos = vec3(
373
+ glyph.posX + localPos.x * glyph.width,
374
+ glyph.posY + localPos.y * glyph.height,
375
+ glyph.posZ
376
+ );
377
+
378
+ var out: VertexOutput;
379
+ out.position = scene.viewProj * vec4(worldPos, 1.0);
380
+ out.uv = uv;
381
+ out.color = glyph.color;
382
+ return out;
383
+ }
384
+
385
+ @fragment
386
+ fn fs(input: VertexOutput) -> @location(0) vec4<f32> {
387
+ let sdfValue = textureSample(atlasTexture, atlasSampler, input.uv).r;
388
+ let alpha = smoothstep(0.5 - SDF_SMOOTHING, 0.5 + SDF_SMOOTHING, sdfValue);
389
+
390
+ if alpha < 0.01 {
391
+ discard;
392
+ }
393
+
394
+ return vec4(input.color.rgb, input.color.a * alpha);
395
+ }
396
+ `;
397
+
398
+ function createTextPipeline(device: GPUDevice, format: GPUTextureFormat): GPURenderPipeline {
399
+ const module = device.createShaderModule({ code: textShader });
400
+
401
+ return device.createRenderPipeline({
402
+ layout: "auto",
403
+ vertex: {
404
+ module,
405
+ entryPoint: "vs",
406
+ },
407
+ fragment: {
408
+ module,
409
+ entryPoint: "fs",
410
+ targets: [
411
+ {
412
+ format,
413
+ blend: {
414
+ color: {
415
+ srcFactor: "src-alpha",
416
+ dstFactor: "one-minus-src-alpha",
417
+ operation: "add",
418
+ },
419
+ alpha: {
420
+ srcFactor: "one",
421
+ dstFactor: "one-minus-src-alpha",
422
+ operation: "add",
423
+ },
424
+ },
425
+ },
426
+ ],
427
+ },
428
+ primitive: {
429
+ topology: "triangle-list",
430
+ cullMode: "none",
431
+ },
432
+ });
433
+ }
434
+
435
+ export interface TextConfig {
436
+ scene: GPUBuffer;
437
+ glyphs: GPUBuffer;
438
+ atlas: GPUTextureView;
439
+ sampler: GPUSampler;
440
+ getCount: () => number;
441
+ }
442
+
443
+ export function createTextNode(config: TextConfig): ComputeNode {
444
+ let pipeline: GPURenderPipeline | null = null;
445
+ let bindGroup: GPUBindGroup | null = null;
446
+
447
+ return {
448
+ id: "text",
449
+ phase: "overlay",
450
+ inputs: [],
451
+ outputs: [],
452
+
453
+ execute(ctx: ExecutionContext) {
454
+ const count = config.getCount();
455
+ if (count === 0) return;
456
+
457
+ const { device, encoder, format } = ctx;
458
+ const targetView = ctx.getTextureView("scene") ?? ctx.canvasView;
459
+
460
+ if (!pipeline) {
461
+ pipeline = createTextPipeline(device, format);
462
+ }
463
+
464
+ if (!bindGroup) {
465
+ bindGroup = device.createBindGroup({
466
+ layout: pipeline.getBindGroupLayout(0),
467
+ entries: [
468
+ { binding: 0, resource: { buffer: config.scene } },
469
+ { binding: 1, resource: { buffer: config.glyphs } },
470
+ { binding: 2, resource: config.atlas },
471
+ { binding: 3, resource: config.sampler },
472
+ ],
473
+ });
474
+ }
475
+
476
+ const pass = encoder.beginRenderPass({
477
+ colorAttachments: [
478
+ {
479
+ view: targetView,
480
+ loadOp: "load" as const,
481
+ storeOp: "store" as const,
482
+ },
483
+ ],
484
+ });
485
+
486
+ pass.setPipeline(pipeline);
487
+ pass.setBindGroup(0, bindGroup);
488
+ pass.draw(count * 6);
489
+ pass.end();
490
+ },
491
+ };
492
+ }
493
+
494
+ export interface TextState {
495
+ content: Map<number, string>;
496
+ atlas: GlyphAtlas;
497
+ sampler: GPUSampler;
498
+ buffer: GPUBuffer;
499
+ staging: Float32Array;
500
+ count: number;
501
+ }
502
+
503
+ export const TextResource = resource<TextState>("text");
504
+
505
+ export function setTextContent(state: State, eid: number, content: string): void {
506
+ const text = TextResource.from(state);
507
+ if (!text) return;
508
+ text.content.set(eid, content);
509
+ }
510
+
511
+ export function getTextContent(state: State, eid: number): string | undefined {
512
+ const text = TextResource.from(state);
513
+ return text?.content.get(eid);
514
+ }
515
+
516
+ const TextSystem: System = {
517
+ group: "draw",
518
+
519
+ update(state: State) {
520
+ const compute = Compute.from(state);
521
+ const text = TextResource.from(state);
522
+ if (!compute || !text) return;
523
+
524
+ const { device } = compute;
525
+ const { atlas, staging, content } = text;
526
+
527
+ let glyphCount = 0;
528
+
529
+ for (const eid of state.query([Text, Transform])) {
530
+ if (!Text.visible[eid]) continue;
531
+
532
+ const textContent = content.get(eid);
533
+ if (!textContent) continue;
534
+
535
+ ensureString(device, atlas, textContent);
536
+
537
+ const fontSize = Text.fontSize[eid];
538
+ const layout = layoutText(textContent, atlas, fontSize);
539
+
540
+ const anchorX = Text.anchorX[eid];
541
+ const anchorY = Text.anchorY[eid];
542
+ const offsetX = -layout.width * anchorX;
543
+ const offsetY = -layout.height * anchorY;
544
+
545
+ const baseX = Transform.posX[eid] + offsetX;
546
+ const baseY = Transform.posY[eid] + offsetY;
547
+ const baseZ = Transform.posZ[eid];
548
+
549
+ const color = Text.color[eid];
550
+ const r = ((color >> 16) & 0xff) / 255;
551
+ const g = ((color >> 8) & 0xff) / 255;
552
+ const b = (color & 0xff) / 255;
553
+ const a = Text.opacity[eid];
554
+
555
+ for (const glyph of layout.glyphs) {
556
+ if (glyphCount >= MAX_GLYPHS) break;
557
+
558
+ const offset = glyphCount * GLYPH_FLOATS;
559
+
560
+ staging[offset + 0] = baseX + glyph.x;
561
+ staging[offset + 1] = baseY + glyph.y;
562
+ staging[offset + 2] = baseZ;
563
+ staging[offset + 3] = 0;
564
+
565
+ staging[offset + 4] = glyph.width;
566
+ staging[offset + 5] = glyph.height;
567
+ staging[offset + 6] = 0;
568
+ staging[offset + 7] = 0;
569
+
570
+ staging[offset + 8] = glyph.u0;
571
+ staging[offset + 9] = glyph.v0;
572
+ staging[offset + 10] = glyph.u1;
573
+ staging[offset + 11] = glyph.v1;
574
+
575
+ staging[offset + 12] = r;
576
+ staging[offset + 13] = g;
577
+ staging[offset + 14] = b;
578
+ staging[offset + 15] = a;
579
+
580
+ glyphCount++;
581
+ }
582
+ }
583
+
584
+ text.count = glyphCount;
585
+
586
+ if (glyphCount > 0) {
587
+ device.queue.writeBuffer(
588
+ text.buffer,
589
+ 0,
590
+ staging.buffer,
591
+ 0,
592
+ glyphCount * GLYPH_FLOATS * 4
593
+ );
594
+ }
595
+ },
596
+ };
597
+
598
+ export const TextPlugin: Plugin = {
599
+ systems: [TextSystem],
600
+ components: { Text },
601
+ dependencies: [ComputePlugin, RenderPlugin],
602
+
603
+ initialize(state: State) {
604
+ const compute = Compute.from(state);
605
+ const render = Render.from(state);
606
+ if (!compute || !render) return;
607
+
608
+ const { device } = compute;
609
+
610
+ const atlas = createGlyphAtlas(device);
611
+ const sampler = device.createSampler({
612
+ magFilter: "linear",
613
+ minFilter: "linear",
614
+ });
615
+
616
+ const textState: TextState = {
617
+ content: new Map(),
618
+ atlas,
619
+ sampler,
620
+ buffer: device.createBuffer({
621
+ label: "glyphs",
622
+ size: MAX_GLYPHS * GLYPH_FLOATS * 4,
623
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
624
+ }),
625
+ staging: new Float32Array(MAX_GLYPHS * GLYPH_FLOATS),
626
+ count: 0,
627
+ };
628
+
629
+ state.setResource(TextResource, textState);
630
+
631
+ compute.graph.add(
632
+ createTextNode({
633
+ scene: render.scene,
634
+ glyphs: textState.buffer,
635
+ atlas: atlas.textureView,
636
+ sampler,
637
+ getCount: () => textState.count,
638
+ })
639
+ );
640
+ },
641
+ };
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./core";
2
+ export * from "./standard";
3
+ export * from "./extras";