@hypen-space/web 0.2.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 (195) hide show
  1. package/dist/chunk-2s02mkzs.js +32 -0
  2. package/dist/chunk-2s02mkzs.js.map +9 -0
  3. package/dist/src/canvas/accessibility.js +152 -0
  4. package/dist/src/canvas/accessibility.js.map +10 -0
  5. package/dist/src/canvas/events.js +198 -0
  6. package/dist/src/canvas/events.js.map +10 -0
  7. package/dist/src/canvas/index.js +28 -0
  8. package/dist/src/canvas/index.js.map +9 -0
  9. package/dist/src/canvas/input.js +132 -0
  10. package/dist/src/canvas/input.js.map +10 -0
  11. package/dist/src/canvas/layout.js +309 -0
  12. package/dist/src/canvas/layout.js.map +10 -0
  13. package/dist/src/canvas/paint.js +878 -0
  14. package/dist/src/canvas/paint.js.map +10 -0
  15. package/dist/src/canvas/renderer.js +276 -0
  16. package/dist/src/canvas/renderer.js.map +10 -0
  17. package/dist/src/canvas/text.js +118 -0
  18. package/dist/src/canvas/text.js.map +10 -0
  19. package/dist/src/canvas/types.js +2 -0
  20. package/dist/src/canvas/types.js.map +9 -0
  21. package/dist/src/canvas/utils.js +139 -0
  22. package/dist/src/canvas/utils.js.map +10 -0
  23. package/dist/src/dom/applicators/advanced-layout.js +111 -0
  24. package/dist/src/dom/applicators/advanced-layout.js.map +10 -0
  25. package/dist/src/dom/applicators/background.js +54 -0
  26. package/dist/src/dom/applicators/background.js.map +10 -0
  27. package/dist/src/dom/applicators/border.js +33 -0
  28. package/dist/src/dom/applicators/border.js.map +10 -0
  29. package/dist/src/dom/applicators/color.js +36 -0
  30. package/dist/src/dom/applicators/color.js.map +10 -0
  31. package/dist/src/dom/applicators/display.js +57 -0
  32. package/dist/src/dom/applicators/display.js.map +10 -0
  33. package/dist/src/dom/applicators/effects.js +89 -0
  34. package/dist/src/dom/applicators/effects.js.map +10 -0
  35. package/dist/src/dom/applicators/events.js +518 -0
  36. package/dist/src/dom/applicators/events.js.map +10 -0
  37. package/dist/src/dom/applicators/font.js +39 -0
  38. package/dist/src/dom/applicators/font.js.map +10 -0
  39. package/dist/src/dom/applicators/index.js +296 -0
  40. package/dist/src/dom/applicators/index.js.map +10 -0
  41. package/dist/src/dom/applicators/layout.js +86 -0
  42. package/dist/src/dom/applicators/layout.js.map +10 -0
  43. package/dist/src/dom/applicators/margin.js +32 -0
  44. package/dist/src/dom/applicators/margin.js.map +10 -0
  45. package/dist/src/dom/applicators/padding.js +35 -0
  46. package/dist/src/dom/applicators/padding.js.map +10 -0
  47. package/dist/src/dom/applicators/size.js +42 -0
  48. package/dist/src/dom/applicators/size.js.map +10 -0
  49. package/dist/src/dom/applicators/transform.js +92 -0
  50. package/dist/src/dom/applicators/transform.js.map +10 -0
  51. package/dist/src/dom/applicators/transition.js +66 -0
  52. package/dist/src/dom/applicators/transition.js.map +10 -0
  53. package/dist/src/dom/applicators/typography.js +87 -0
  54. package/dist/src/dom/applicators/typography.js.map +10 -0
  55. package/dist/src/dom/canvas/index.js +50 -0
  56. package/dist/src/dom/canvas/index.js.map +10 -0
  57. package/dist/src/dom/components/audio.js +48 -0
  58. package/dist/src/dom/components/audio.js.map +10 -0
  59. package/dist/src/dom/components/avatar.js +58 -0
  60. package/dist/src/dom/components/avatar.js.map +10 -0
  61. package/dist/src/dom/components/badge.js +55 -0
  62. package/dist/src/dom/components/badge.js.map +10 -0
  63. package/dist/src/dom/components/button.js +29 -0
  64. package/dist/src/dom/components/button.js.map +10 -0
  65. package/dist/src/dom/components/card.js +33 -0
  66. package/dist/src/dom/components/card.js.map +10 -0
  67. package/dist/src/dom/components/center.js +32 -0
  68. package/dist/src/dom/components/center.js.map +10 -0
  69. package/dist/src/dom/components/checkbox.js +54 -0
  70. package/dist/src/dom/components/checkbox.js.map +10 -0
  71. package/dist/src/dom/components/column.js +31 -0
  72. package/dist/src/dom/components/column.js.map +10 -0
  73. package/dist/src/dom/components/container.js +29 -0
  74. package/dist/src/dom/components/container.js.map +10 -0
  75. package/dist/src/dom/components/divider.js +45 -0
  76. package/dist/src/dom/components/divider.js.map +10 -0
  77. package/dist/src/dom/components/grid.js +44 -0
  78. package/dist/src/dom/components/grid.js.map +10 -0
  79. package/dist/src/dom/components/heading.js +47 -0
  80. package/dist/src/dom/components/heading.js.map +10 -0
  81. package/dist/src/dom/components/image.js +39 -0
  82. package/dist/src/dom/components/image.js.map +10 -0
  83. package/dist/src/dom/components/index.js +217 -0
  84. package/dist/src/dom/components/index.js.map +10 -0
  85. package/dist/src/dom/components/input.js +41 -0
  86. package/dist/src/dom/components/input.js.map +10 -0
  87. package/dist/src/dom/components/link.js +42 -0
  88. package/dist/src/dom/components/link.js.map +10 -0
  89. package/dist/src/dom/components/list.js +42 -0
  90. package/dist/src/dom/components/list.js.map +10 -0
  91. package/dist/src/dom/components/paragraph.js +35 -0
  92. package/dist/src/dom/components/paragraph.js.map +10 -0
  93. package/dist/src/dom/components/progressbar.js +57 -0
  94. package/dist/src/dom/components/progressbar.js.map +10 -0
  95. package/dist/src/dom/components/route.js +44 -0
  96. package/dist/src/dom/components/route.js.map +10 -0
  97. package/dist/src/dom/components/router.js +33 -0
  98. package/dist/src/dom/components/router.js.map +10 -0
  99. package/dist/src/dom/components/row.js +31 -0
  100. package/dist/src/dom/components/row.js.map +10 -0
  101. package/dist/src/dom/components/select.js +57 -0
  102. package/dist/src/dom/components/select.js.map +10 -0
  103. package/dist/src/dom/components/slider.js +48 -0
  104. package/dist/src/dom/components/slider.js.map +10 -0
  105. package/dist/src/dom/components/spacer.js +30 -0
  106. package/dist/src/dom/components/spacer.js.map +10 -0
  107. package/dist/src/dom/components/spinner.js +65 -0
  108. package/dist/src/dom/components/spinner.js.map +10 -0
  109. package/dist/src/dom/components/stack.js +45 -0
  110. package/dist/src/dom/components/stack.js.map +10 -0
  111. package/dist/src/dom/components/switch.js +83 -0
  112. package/dist/src/dom/components/switch.js.map +10 -0
  113. package/dist/src/dom/components/text.js +37 -0
  114. package/dist/src/dom/components/text.js.map +10 -0
  115. package/dist/src/dom/components/textarea.js +51 -0
  116. package/dist/src/dom/components/textarea.js.map +10 -0
  117. package/dist/src/dom/components/video.js +51 -0
  118. package/dist/src/dom/components/video.js.map +10 -0
  119. package/dist/src/dom/debug.js +170 -0
  120. package/dist/src/dom/debug.js.map +10 -0
  121. package/dist/src/dom/events.js +112 -0
  122. package/dist/src/dom/events.js.map +10 -0
  123. package/dist/src/dom/index.js +73 -0
  124. package/dist/src/dom/index.js.map +9 -0
  125. package/dist/src/dom/renderer.js +277 -0
  126. package/dist/src/dom/renderer.js.map +10 -0
  127. package/dist/src/index.js +89 -0
  128. package/dist/src/index.js.map +9 -0
  129. package/package.json +84 -0
  130. package/src/canvas/QUICKSTART.md +421 -0
  131. package/src/canvas/README.md +376 -0
  132. package/src/canvas/accessibility.ts +218 -0
  133. package/src/canvas/events.ts +307 -0
  134. package/src/canvas/index.ts +35 -0
  135. package/src/canvas/input.ts +210 -0
  136. package/src/canvas/layout.ts +401 -0
  137. package/src/canvas/paint.ts +1321 -0
  138. package/src/canvas/renderer.ts +422 -0
  139. package/src/canvas/text.ts +182 -0
  140. package/src/canvas/types.ts +137 -0
  141. package/src/canvas/utils.ts +218 -0
  142. package/src/dom/README.md +265 -0
  143. package/src/dom/applicators/advanced-layout.ts +128 -0
  144. package/src/dom/applicators/background.ts +50 -0
  145. package/src/dom/applicators/border.ts +19 -0
  146. package/src/dom/applicators/color.ts +23 -0
  147. package/src/dom/applicators/display.ts +54 -0
  148. package/src/dom/applicators/effects.ts +97 -0
  149. package/src/dom/applicators/events.ts +689 -0
  150. package/src/dom/applicators/font.ts +27 -0
  151. package/src/dom/applicators/index.ts +354 -0
  152. package/src/dom/applicators/layout.ts +92 -0
  153. package/src/dom/applicators/margin.ts +18 -0
  154. package/src/dom/applicators/padding.ts +18 -0
  155. package/src/dom/applicators/size.ts +31 -0
  156. package/src/dom/applicators/transform.ts +93 -0
  157. package/src/dom/applicators/transition.ts +65 -0
  158. package/src/dom/applicators/typography.ts +91 -0
  159. package/src/dom/canvas/index.ts +60 -0
  160. package/src/dom/components/audio.ts +45 -0
  161. package/src/dom/components/avatar.ts +49 -0
  162. package/src/dom/components/badge.ts +45 -0
  163. package/src/dom/components/button.ts +13 -0
  164. package/src/dom/components/card.ts +19 -0
  165. package/src/dom/components/center.ts +16 -0
  166. package/src/dom/components/checkbox.ts +54 -0
  167. package/src/dom/components/column.ts +15 -0
  168. package/src/dom/components/container.ts +13 -0
  169. package/src/dom/components/divider.ts +37 -0
  170. package/src/dom/components/grid.ts +40 -0
  171. package/src/dom/components/heading.ts +41 -0
  172. package/src/dom/components/image.ts +27 -0
  173. package/src/dom/components/index.ts +115 -0
  174. package/src/dom/components/input.ts +29 -0
  175. package/src/dom/components/link.ts +35 -0
  176. package/src/dom/components/list.ts +30 -0
  177. package/src/dom/components/paragraph.ts +23 -0
  178. package/src/dom/components/progressbar.ts +51 -0
  179. package/src/dom/components/route.ts +37 -0
  180. package/src/dom/components/router.ts +22 -0
  181. package/src/dom/components/row.ts +15 -0
  182. package/src/dom/components/select.ts +56 -0
  183. package/src/dom/components/slider.ts +45 -0
  184. package/src/dom/components/spacer.ts +16 -0
  185. package/src/dom/components/spinner.ts +60 -0
  186. package/src/dom/components/stack.ts +34 -0
  187. package/src/dom/components/switch.ts +86 -0
  188. package/src/dom/components/text.ts +24 -0
  189. package/src/dom/components/textarea.ts +50 -0
  190. package/src/dom/components/video.ts +50 -0
  191. package/src/dom/debug.ts +247 -0
  192. package/src/dom/events.ts +168 -0
  193. package/src/dom/index.ts +11 -0
  194. package/src/dom/renderer.ts +327 -0
  195. package/src/index.ts +56 -0
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Canvas Renderer
3
+ *
4
+ * Main renderer class that orchestrates layout, painting, and events
5
+ */
6
+
7
+ import type { Renderer, Patch } from "@hypen/core";
8
+
9
+ // Interface for the engine that canvas renderer needs
10
+ interface IEngine {
11
+ dispatchAction(name: string, payload?: any): void;
12
+ }
13
+ import type {
14
+ VirtualNode,
15
+ CanvasRendererOptions,
16
+ PainterFunction,
17
+ LayoutFunction,
18
+ } from "./types.js";
19
+ import { computeLayout } from "./layout.js";
20
+ import { paintNode, registerPainter } from "./paint.js";
21
+ import { CanvasEventManager } from "./events.js";
22
+ import { InputOverlay } from "./input.js";
23
+ import { AccessibilityLayer } from "./accessibility.js";
24
+ import { findNodeById } from "./utils.js";
25
+
26
+ const DEFAULT_OPTIONS: CanvasRendererOptions = {
27
+ devicePixelRatio: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
28
+ backgroundColor: "#ffffff",
29
+ enableAccessibility: true,
30
+ enableHitTesting: true,
31
+ enableInputOverlay: true,
32
+ enableDirtyRects: false,
33
+ enableLayerCaching: false,
34
+ maxLayerCacheSize: 10,
35
+ showLayoutBounds: false,
36
+ showDirtyRects: false,
37
+ logPerformance: false,
38
+ };
39
+
40
+ /**
41
+ * Canvas Renderer
42
+ */
43
+ export class CanvasRenderer implements Renderer {
44
+ private canvas: HTMLCanvasElement;
45
+ private ctx: CanvasRenderingContext2D;
46
+ private engine: IEngine;
47
+ private options: CanvasRendererOptions;
48
+
49
+ private rootNode: VirtualNode | null = null;
50
+ private nodes = new Map<string, VirtualNode>();
51
+
52
+ private eventManager: CanvasEventManager;
53
+ private inputOverlay: InputOverlay;
54
+ private accessibilityLayer: AccessibilityLayer;
55
+
56
+ private rafId: number | null = null;
57
+ private needsRedraw = false;
58
+
59
+ private frameCount = 0;
60
+ private lastFrameTime = 0;
61
+
62
+ constructor(canvas: HTMLCanvasElement, engine: IEngine, options?: Partial<CanvasRendererOptions>) {
63
+ this.canvas = canvas;
64
+ this.engine = engine;
65
+ this.options = { ...DEFAULT_OPTIONS, ...options };
66
+
67
+ // Get context
68
+ const ctx = canvas.getContext("2d");
69
+ if (!ctx) {
70
+ throw new Error("Failed to get 2D context from canvas");
71
+ }
72
+ this.ctx = ctx;
73
+
74
+ // Setup HiDPI
75
+ this.setupHiDPI();
76
+
77
+ // Initialize subsystems
78
+ this.eventManager = new CanvasEventManager(canvas, engine);
79
+ this.inputOverlay = new InputOverlay(
80
+ (canvas as any).parentElement || (typeof document !== "undefined" ? document.body : null)
81
+ );
82
+ this.accessibilityLayer = new AccessibilityLayer(
83
+ (canvas as any).parentElement || (typeof document !== "undefined" ? document.body : null),
84
+ this.options.enableAccessibility || false
85
+ );
86
+
87
+ // Listen for redraw requests from event manager
88
+ this.canvas.addEventListener("hypen:redraw", () => this.scheduleRedraw());
89
+
90
+ // Don't schedule initial render - wait for patches
91
+ }
92
+
93
+ /**
94
+ * Setup HiDPI rendering
95
+ */
96
+ private setupHiDPI(): void {
97
+ const dpr = this.options.devicePixelRatio || 1;
98
+ const rect = this.canvas.getBoundingClientRect();
99
+
100
+ this.canvas.width = rect.width * dpr;
101
+ this.canvas.height = rect.height * dpr;
102
+
103
+ this.ctx.scale(dpr, dpr);
104
+
105
+ // Update canvas display size
106
+ this.canvas.style.width = `${rect.width}px`;
107
+ this.canvas.style.height = `${rect.height}px`;
108
+ }
109
+
110
+ /**
111
+ * Apply patches from engine
112
+ */
113
+ applyPatches(patches: Patch[]): void {
114
+ for (const patch of patches) {
115
+ this.applyPatch(patch);
116
+ }
117
+
118
+ // Update accessibility layer
119
+ if (this.rootNode) {
120
+ this.accessibilityLayer.syncTree(this.rootNode);
121
+ }
122
+
123
+ // Schedule redraw
124
+ this.scheduleRedraw();
125
+ }
126
+
127
+ /**
128
+ * Apply single patch
129
+ */
130
+ private applyPatch(patch: Patch): void {
131
+ switch (patch.type) {
132
+ case "create":
133
+ this.onCreate(patch.id!, (patch as any).element_type!, patch.props || {});
134
+ break;
135
+
136
+ case "setProp":
137
+ this.onSetProp(patch.id!, patch.name!, patch.value);
138
+ break;
139
+
140
+ case "setText":
141
+ this.onSetText(patch.id!, patch.text!);
142
+ break;
143
+
144
+ case "insert":
145
+ this.onInsert((patch as any).parent_id!, patch.id!, (patch as any).before_id);
146
+ break;
147
+
148
+ case "move":
149
+ this.onMove((patch as any).parent_id!, patch.id!, (patch as any).before_id);
150
+ break;
151
+
152
+ case "remove":
153
+ this.onRemove(patch.id!);
154
+ break;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Create new virtual node
160
+ */
161
+ private onCreate(id: string, elementType: string, props: Record<string, any>): void {
162
+ const node: VirtualNode = {
163
+ id,
164
+ type: elementType,
165
+ props: props instanceof Map ? Object.fromEntries(props) : props,
166
+ children: [],
167
+ parent: null,
168
+ visible: true,
169
+ opacity: parseFloat(props.opacity) || 1,
170
+ clickable: elementType === "button" || !!props.onclick,
171
+ hoverable: true,
172
+ focusable: elementType === "input" || elementType === "textarea" || elementType === "button",
173
+ focused: false,
174
+ hovered: false,
175
+ };
176
+
177
+ this.nodes.set(id, node);
178
+ }
179
+
180
+ /**
181
+ * Set property on node
182
+ */
183
+ private onSetProp(id: string, name: string, value: any): void {
184
+ const node = this.nodes.get(id);
185
+ if (!node) return;
186
+
187
+ node.props[name] = value;
188
+
189
+ // Update computed properties
190
+ if (name === "visible") {
191
+ node.visible = !!value;
192
+ }
193
+ if (name === "opacity") {
194
+ node.opacity = parseFloat(value) || 1;
195
+ }
196
+
197
+ // Update accessibility
198
+ this.accessibilityLayer.updateNode(node);
199
+ }
200
+
201
+ /**
202
+ * Set text on node
203
+ */
204
+ private onSetText(id: string, text: string): void {
205
+ const node = this.nodes.get(id);
206
+ if (!node) return;
207
+
208
+ node.props[0] = text;
209
+
210
+ // Update accessibility
211
+ this.accessibilityLayer.updateNode(node);
212
+ }
213
+
214
+ /**
215
+ * Insert node into tree
216
+ */
217
+ private onInsert(parentId: string, id: string, beforeId?: string): void {
218
+ const child = this.nodes.get(id);
219
+ if (!child) return;
220
+
221
+ // Check if this is setting the root node (parent_id === id === "root" or similar)
222
+ if (parentId === "root" && id === "root") {
223
+ this.rootNode = child;
224
+ this.eventManager.setRootNode(child);
225
+ return;
226
+ }
227
+
228
+ // Otherwise find the parent node
229
+ const parent = this.nodes.get(parentId);
230
+ if (!parent) {
231
+ // If parent is "root", this might be the first real root node
232
+ if (parentId === "root") {
233
+ this.rootNode = child;
234
+ this.eventManager.setRootNode(child);
235
+ }
236
+ return;
237
+ }
238
+
239
+ // Insert child into parent
240
+ child.parent = parent;
241
+
242
+ if (beforeId) {
243
+ const beforeIndex = parent.children.findIndex((c) => c.id === beforeId);
244
+ if (beforeIndex >= 0) {
245
+ parent.children.splice(beforeIndex, 0, child);
246
+ } else {
247
+ parent.children.push(child);
248
+ }
249
+ } else {
250
+ parent.children.push(child);
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Move node in tree
256
+ */
257
+ private onMove(parentId: string, id: string, beforeId?: string): void {
258
+ // Remove from old parent
259
+ const node = this.nodes.get(id);
260
+ if (!node || !node.parent) return;
261
+
262
+ const oldParent = node.parent;
263
+ const oldIndex = oldParent.children.indexOf(node);
264
+ if (oldIndex >= 0) {
265
+ oldParent.children.splice(oldIndex, 1);
266
+ }
267
+
268
+ // Insert into new location
269
+ this.onInsert(parentId, id, beforeId);
270
+ }
271
+
272
+ /**
273
+ * Remove node from tree
274
+ */
275
+ private onRemove(id: string): void {
276
+ const node = this.nodes.get(id);
277
+ if (!node) return;
278
+
279
+ // Remove from parent
280
+ if (node.parent) {
281
+ const index = node.parent.children.indexOf(node);
282
+ if (index >= 0) {
283
+ node.parent.children.splice(index, 1);
284
+ }
285
+ }
286
+
287
+ // Remove from root
288
+ if (this.rootNode === node) {
289
+ this.rootNode = null;
290
+ this.eventManager.setRootNode(null);
291
+ }
292
+
293
+ // Remove from nodes map
294
+ this.nodes.delete(id);
295
+ }
296
+
297
+ /**
298
+ * Schedule redraw
299
+ */
300
+ private scheduleRedraw(): void {
301
+ if (this.rafId !== null) return;
302
+
303
+ // Use requestAnimationFrame if available (browser), otherwise render immediately (tests)
304
+ if (typeof requestAnimationFrame !== "undefined") {
305
+ this.rafId = requestAnimationFrame(() => {
306
+ this.render();
307
+ this.rafId = null;
308
+ });
309
+ } else {
310
+ // In non-browser environments (tests), render immediately
311
+ this.render();
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Main render function
317
+ */
318
+ private render(): void {
319
+ const startTime = performance.now();
320
+
321
+ // Clear canvas
322
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
323
+
324
+ // Draw background
325
+ if (this.options.backgroundColor) {
326
+ this.ctx.fillStyle = this.options.backgroundColor;
327
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
328
+ }
329
+
330
+ // Layout and paint
331
+ if (this.rootNode) {
332
+ // Compute layout
333
+ const dpr = this.options.devicePixelRatio || 1;
334
+ computeLayout(
335
+ this.ctx,
336
+ this.rootNode,
337
+ this.canvas.width / dpr,
338
+ this.canvas.height / dpr
339
+ );
340
+
341
+ // Paint
342
+ paintNode(this.ctx, this.rootNode);
343
+
344
+ // Debug: show layout bounds
345
+ if (this.options.showLayoutBounds) {
346
+ this.drawLayoutBounds(this.rootNode);
347
+ }
348
+ }
349
+
350
+ // Performance logging
351
+ if (this.options.logPerformance) {
352
+ const elapsed = performance.now() - startTime;
353
+ this.frameCount++;
354
+ if (performance.now() - this.lastFrameTime > 1000) {
355
+ console.log(`Canvas FPS: ${this.frameCount}, Last frame: ${elapsed.toFixed(2)}ms`);
356
+ this.frameCount = 0;
357
+ this.lastFrameTime = performance.now();
358
+ }
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Draw layout bounds for debugging
364
+ */
365
+ private drawLayoutBounds(node: VirtualNode): void {
366
+ if (!node.layout) return;
367
+
368
+ const layout = node.layout;
369
+
370
+ this.ctx.strokeStyle = "#ff0000";
371
+ this.ctx.lineWidth = 1;
372
+ this.ctx.strokeRect(layout.x, layout.y, layout.width, layout.height);
373
+
374
+ for (const child of node.children) {
375
+ this.drawLayoutBounds(child);
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Get node by ID
381
+ */
382
+ getNode(id: string): VirtualNode | undefined {
383
+ return this.nodes.get(id);
384
+ }
385
+
386
+ /**
387
+ * Clear renderer
388
+ */
389
+ clear(): void {
390
+ this.rootNode = null;
391
+ this.nodes.clear();
392
+ this.eventManager.setRootNode(null);
393
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
394
+ }
395
+
396
+ /**
397
+ * Register custom painter
398
+ */
399
+ registerPainter(type: string, painter: PainterFunction): void {
400
+ registerPainter(type, painter);
401
+ }
402
+
403
+ /**
404
+ * Set renderer options
405
+ */
406
+ setOptions(options: Partial<CanvasRendererOptions>): void {
407
+ this.options = { ...this.options, ...options };
408
+ this.accessibilityLayer.setEnabled(this.options.enableAccessibility || false);
409
+ }
410
+
411
+ /**
412
+ * Destroy renderer
413
+ */
414
+ destroy(): void {
415
+ if (this.rafId !== null) {
416
+ cancelAnimationFrame(this.rafId);
417
+ }
418
+ this.eventManager.destroy();
419
+ this.accessibilityLayer.destroy();
420
+ }
421
+ }
422
+
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Text Rendering System
3
+ *
4
+ * Text measurement, wrapping, and rendering
5
+ */
6
+
7
+ import type { FontStyle, TextMetrics, TextStyle } from "./types.js";
8
+ import { createFontString } from "./utils.js";
9
+
10
+ /**
11
+ * Text metrics cache
12
+ */
13
+ const textMetricsCache = new Map<string, TextMetrics>();
14
+
15
+ /**
16
+ * Get cache key for text metrics
17
+ */
18
+ function getCacheKey(text: string, fontStyle: FontStyle, maxWidth?: number): string {
19
+ return `${text}|${fontStyle.fontSize}|${fontStyle.fontWeight}|${fontStyle.fontFamily}|${maxWidth || "auto"}`;
20
+ }
21
+
22
+ /**
23
+ * Measure text dimensions
24
+ */
25
+ export function measureText(
26
+ ctx: CanvasRenderingContext2D,
27
+ text: string,
28
+ fontStyle: FontStyle,
29
+ maxWidth?: number
30
+ ): TextMetrics {
31
+ const cacheKey = getCacheKey(text, fontStyle, maxWidth);
32
+ const cached = textMetricsCache.get(cacheKey);
33
+ if (cached) return cached;
34
+
35
+ const font = createFontString(fontStyle.fontSize, fontStyle.fontWeight, fontStyle.fontFamily);
36
+ ctx.save();
37
+ ctx.font = font;
38
+
39
+ const lineHeight = fontStyle.lineHeight || fontStyle.fontSize * 1.2;
40
+
41
+ // No wrapping needed
42
+ if (!maxWidth) {
43
+ const metrics = ctx.measureText(text);
44
+ const result: TextMetrics = {
45
+ width: metrics.width,
46
+ height: lineHeight,
47
+ lines: [text],
48
+ lineHeight,
49
+ };
50
+ ctx.restore();
51
+ textMetricsCache.set(cacheKey, result);
52
+ return result;
53
+ }
54
+
55
+ // Wrap text
56
+ const lines = wrapText(ctx, text, maxWidth);
57
+ const width = Math.max(...lines.map((line) => ctx.measureText(line).width));
58
+ const height = lines.length * lineHeight;
59
+
60
+ const result: TextMetrics = {
61
+ width,
62
+ height,
63
+ lines,
64
+ lineHeight,
65
+ };
66
+
67
+ ctx.restore();
68
+ textMetricsCache.set(cacheKey, result);
69
+ return result;
70
+ }
71
+
72
+ /**
73
+ * Wrap text to fit within maxWidth
74
+ */
75
+ function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number): string[] {
76
+ const lines: string[] = [];
77
+ const paragraphs = text.split("\n");
78
+
79
+ for (const paragraph of paragraphs) {
80
+ const words = paragraph.split(" ");
81
+ let currentLine = "";
82
+
83
+ for (const word of words) {
84
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
85
+ const metrics = ctx.measureText(testLine);
86
+
87
+ if (metrics.width > maxWidth && currentLine) {
88
+ lines.push(currentLine);
89
+ currentLine = word;
90
+ } else {
91
+ currentLine = testLine;
92
+ }
93
+ }
94
+
95
+ if (currentLine) {
96
+ lines.push(currentLine);
97
+ }
98
+ }
99
+
100
+ return lines.length > 0 ? lines : [""];
101
+ }
102
+
103
+ /**
104
+ * Render text with style
105
+ */
106
+ export function renderText(
107
+ ctx: CanvasRenderingContext2D,
108
+ text: string,
109
+ x: number,
110
+ y: number,
111
+ width: number,
112
+ height: number,
113
+ style: TextStyle
114
+ ): void {
115
+ const font = createFontString(style.fontSize, style.fontWeight, style.fontFamily);
116
+ ctx.save();
117
+ ctx.font = font;
118
+ ctx.fillStyle = style.color;
119
+ ctx.textBaseline = "top";
120
+
121
+ const metrics = measureText(ctx, text, style, width);
122
+
123
+ // Calculate starting Y based on vertical alignment
124
+ let startY = y;
125
+ if (style.verticalAlign === "middle") {
126
+ startY = y + (height - metrics.height) / 2;
127
+ } else if (style.verticalAlign === "bottom") {
128
+ startY = y + height - metrics.height;
129
+ }
130
+
131
+ // Render each line
132
+ for (let i = 0; i < metrics.lines.length; i++) {
133
+ const line = metrics.lines[i];
134
+ const lineY = startY + i * metrics.lineHeight;
135
+
136
+ // Calculate X based on text alignment
137
+ let lineX = x;
138
+ if (style.textAlign === "center") {
139
+ const lineWidth = ctx.measureText(line).width;
140
+ lineX = x + (width - lineWidth) / 2;
141
+ } else if (style.textAlign === "right") {
142
+ const lineWidth = ctx.measureText(line).width;
143
+ lineX = x + width - lineWidth;
144
+ }
145
+
146
+ ctx.fillText(line, lineX, lineY);
147
+ }
148
+
149
+ ctx.restore();
150
+ }
151
+
152
+ /**
153
+ * Clear text metrics cache
154
+ */
155
+ export function clearTextCache(): void {
156
+ textMetricsCache.clear();
157
+ }
158
+
159
+ /**
160
+ * Preload font to ensure it's available
161
+ */
162
+ export async function loadFont(fontFamily: string, fontWeight: string | number = "normal"): Promise<void> {
163
+ if (!("fonts" in document)) return;
164
+
165
+ const font = `${fontWeight} 16px ${fontFamily}`;
166
+ try {
167
+ await document.fonts.load(font);
168
+ } catch (error) {
169
+ console.warn(`Failed to load font: ${font}`, error);
170
+ }
171
+ }
172
+
173
+
174
+
175
+
176
+
177
+
178
+
179
+
180
+
181
+
182
+