@typecad/ui 0.1.0-alpha.1

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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1443 -0
  3. package/dist/engine-index.d.ts +7 -0
  4. package/dist/engine-index.js +41 -0
  5. package/dist/index.d.ts +165 -0
  6. package/dist/index.js +42 -0
  7. package/dist/preview/build-program.d.ts +7 -0
  8. package/dist/preview/build-program.js +665 -0
  9. package/dist/preview/host-gfx.d.ts +90 -0
  10. package/dist/preview/host-gfx.js +552 -0
  11. package/dist/preview/host-ui-runtime.d.ts +241 -0
  12. package/dist/preview/host-ui-runtime.js +3236 -0
  13. package/dist/preview/types.d.ts +83 -0
  14. package/dist/preview/types.js +1 -0
  15. package/dist/types.d.ts +108 -0
  16. package/dist/types.js +10 -0
  17. package/dist/ui-engine/block-layout.d.ts +8 -0
  18. package/dist/ui-engine/block-layout.js +95 -0
  19. package/dist/ui-engine/color.d.ts +49 -0
  20. package/dist/ui-engine/color.js +273 -0
  21. package/dist/ui-engine/css-parser.d.ts +155 -0
  22. package/dist/ui-engine/css-parser.js +1058 -0
  23. package/dist/ui-engine/default-keyboards.d.ts +3 -0
  24. package/dist/ui-engine/default-keyboards.js +53 -0
  25. package/dist/ui-engine/easing.d.ts +12 -0
  26. package/dist/ui-engine/easing.js +76 -0
  27. package/dist/ui-engine/font-assets.d.ts +50 -0
  28. package/dist/ui-engine/font-assets.js +431 -0
  29. package/dist/ui-engine/html-parser.d.ts +116 -0
  30. package/dist/ui-engine/html-parser.js +284 -0
  31. package/dist/ui-engine/image-assets.d.ts +21 -0
  32. package/dist/ui-engine/image-assets.js +125 -0
  33. package/dist/ui-engine/inline-parser.d.ts +30 -0
  34. package/dist/ui-engine/inline-parser.js +66 -0
  35. package/dist/ui-engine/keyframes.d.ts +3 -0
  36. package/dist/ui-engine/keyframes.js +39 -0
  37. package/dist/ui-engine/layout-engine.d.ts +35 -0
  38. package/dist/ui-engine/layout-engine.js +203 -0
  39. package/dist/ui-engine/model.d.ts +239 -0
  40. package/dist/ui-engine/model.js +987 -0
  41. package/dist/ui-engine/rich-layout.d.ts +51 -0
  42. package/dist/ui-engine/rich-layout.js +220 -0
  43. package/dist/ui-engine/run-types.d.ts +13 -0
  44. package/dist/ui-engine/run-types.js +1 -0
  45. package/dist/ui-engine/runtime-header/antialiasing.d.ts +1 -0
  46. package/dist/ui-engine/runtime-header/antialiasing.js +141 -0
  47. package/dist/ui-engine/runtime-header/blend-bodies.d.ts +1 -0
  48. package/dist/ui-engine/runtime-header/blend-bodies.js +39 -0
  49. package/dist/ui-engine/runtime-header/canvas-helpers.d.ts +1 -0
  50. package/dist/ui-engine/runtime-header/canvas-helpers.js +155 -0
  51. package/dist/ui-engine/runtime-header/canvas-scrollbar.d.ts +1 -0
  52. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +105 -0
  53. package/dist/ui-engine/runtime-header/color-mono-refresh.d.ts +1 -0
  54. package/dist/ui-engine/runtime-header/color-mono-refresh.js +154 -0
  55. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.d.ts +1 -0
  56. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +169 -0
  57. package/dist/ui-engine/runtime-header/display-shim.d.ts +1 -0
  58. package/dist/ui-engine/runtime-header/display-shim.js +104 -0
  59. package/dist/ui-engine/runtime-header/forward-decls.d.ts +1 -0
  60. package/dist/ui-engine/runtime-header/forward-decls.js +137 -0
  61. package/dist/ui-engine/runtime-header/guard-close.d.ts +1 -0
  62. package/dist/ui-engine/runtime-header/guard-close.js +8 -0
  63. package/dist/ui-engine/runtime-header/image-drawing.d.ts +1 -0
  64. package/dist/ui-engine/runtime-header/image-drawing.js +300 -0
  65. package/dist/ui-engine/runtime-header/init-press-input.d.ts +1 -0
  66. package/dist/ui-engine/runtime-header/init-press-input.js +126 -0
  67. package/dist/ui-engine/runtime-header/keyboard.d.ts +1 -0
  68. package/dist/ui-engine/runtime-header/keyboard.js +342 -0
  69. package/dist/ui-engine/runtime-header/node-decoration.d.ts +1 -0
  70. package/dist/ui-engine/runtime-header/node-decoration.js +212 -0
  71. package/dist/ui-engine/runtime-header/paint-order-coords.d.ts +1 -0
  72. package/dist/ui-engine/runtime-header/paint-order-coords.js +190 -0
  73. package/dist/ui-engine/runtime-header/paint-rects-repair.d.ts +1 -0
  74. package/dist/ui-engine/runtime-header/paint-rects-repair.js +553 -0
  75. package/dist/ui-engine/runtime-header/scroll-physics.d.ts +1 -0
  76. package/dist/ui-engine/runtime-header/scroll-physics.js +161 -0
  77. package/dist/ui-engine/runtime-header/state-bindings-nav.d.ts +1 -0
  78. package/dist/ui-engine/runtime-header/state-bindings-nav.js +176 -0
  79. package/dist/ui-engine/runtime-header/structs.d.ts +1 -0
  80. package/dist/ui-engine/runtime-header/structs.js +220 -0
  81. package/dist/ui-engine/runtime-header/text-rendering.d.ts +1 -0
  82. package/dist/ui-engine/runtime-header/text-rendering.js +715 -0
  83. package/dist/ui-engine/runtime-header/tick/bindings-phase.d.ts +1 -0
  84. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +103 -0
  85. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.d.ts +1 -0
  86. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +1051 -0
  87. package/dist/ui-engine/runtime-header/tick/flush-phase.d.ts +1 -0
  88. package/dist/ui-engine/runtime-header/tick/flush-phase.js +17 -0
  89. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.d.ts +1 -0
  90. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +15 -0
  91. package/dist/ui-engine/runtime-header/tick/transitions-phase.d.ts +1 -0
  92. package/dist/ui-engine/runtime-header/tick/transitions-phase.js +213 -0
  93. package/dist/ui-engine/runtime-header/tick-motion-helpers.d.ts +1 -0
  94. package/dist/ui-engine/runtime-header/tick-motion-helpers.js +26 -0
  95. package/dist/ui-engine/runtime-header/tick.d.ts +1 -0
  96. package/dist/ui-engine/runtime-header/tick.js +17 -0
  97. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.d.ts +1 -0
  98. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +406 -0
  99. package/dist/ui-engine/runtime-header/types-defines.d.ts +1 -0
  100. package/dist/ui-engine/runtime-header/types-defines.js +79 -0
  101. package/dist/ui-engine/runtime-header.d.ts +1 -0
  102. package/dist/ui-engine/runtime-header.js +69 -0
  103. package/dist/ui-engine/scroll-memory-diagnostics.d.ts +16 -0
  104. package/dist/ui-engine/scroll-memory-diagnostics.js +58 -0
  105. package/dist/ui-engine/select-engine.d.ts +4 -0
  106. package/dist/ui-engine/select-engine.js +19 -0
  107. package/dist/ui-engine/style-resolver.d.ts +77 -0
  108. package/dist/ui-engine/style-resolver.js +341 -0
  109. package/dist/ui-engine/text-layout.d.ts +19 -0
  110. package/dist/ui-engine/text-layout.js +92 -0
  111. package/dist/ui-engine/transpile-ui.d.ts +11 -0
  112. package/dist/ui-engine/transpile-ui.js +33 -0
  113. package/dist/ui-engine/ua-stylesheet.d.ts +3 -0
  114. package/dist/ui-engine/ua-stylesheet.js +56 -0
  115. package/dist/ui-engine/ui-file-splitter.d.ts +14 -0
  116. package/dist/ui-engine/ui-file-splitter.js +47 -0
  117. package/dist/ui-engine/ui-lowering.d.ts +33 -0
  118. package/dist/ui-engine/ui-lowering.js +390 -0
  119. package/dist/ui-engine/ui-registry.d.ts +81 -0
  120. package/dist/ui-engine/ui-registry.js +429 -0
  121. package/dist/ui-engine/yoga-layout.d.ts +12 -0
  122. package/dist/ui-engine/yoga-layout.js +314 -0
  123. package/package.json +86 -0
  124. package/src/engine-index.ts +51 -0
  125. package/src/index.ts +232 -0
  126. package/src/preview/build-program.ts +734 -0
  127. package/src/preview/host-gfx.ts +580 -0
  128. package/src/preview/host-ui-runtime.ts +3245 -0
  129. package/src/preview/types.ts +96 -0
  130. package/src/types.ts +127 -0
  131. package/src/ui-engine/block-layout.ts +104 -0
  132. package/src/ui-engine/color.ts +263 -0
  133. package/src/ui-engine/css-parser.ts +1003 -0
  134. package/src/ui-engine/default-keyboards.ts +58 -0
  135. package/src/ui-engine/easing.ts +53 -0
  136. package/src/ui-engine/font-assets.ts +512 -0
  137. package/src/ui-engine/html-parser.ts +397 -0
  138. package/src/ui-engine/image-assets.ts +170 -0
  139. package/src/ui-engine/inline-parser.ts +78 -0
  140. package/src/ui-engine/keyframes.ts +47 -0
  141. package/src/ui-engine/layout-engine.ts +228 -0
  142. package/src/ui-engine/model.ts +1225 -0
  143. package/src/ui-engine/rich-layout.ts +257 -0
  144. package/src/ui-engine/run-types.ts +14 -0
  145. package/src/ui-engine/runtime-header/antialiasing.ts +141 -0
  146. package/src/ui-engine/runtime-header/blend-bodies.ts +39 -0
  147. package/src/ui-engine/runtime-header/canvas-helpers.ts +155 -0
  148. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +105 -0
  149. package/src/ui-engine/runtime-header/color-mono-refresh.ts +154 -0
  150. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +169 -0
  151. package/src/ui-engine/runtime-header/display-shim.ts +104 -0
  152. package/src/ui-engine/runtime-header/forward-decls.ts +137 -0
  153. package/src/ui-engine/runtime-header/guard-close.ts +8 -0
  154. package/src/ui-engine/runtime-header/image-drawing.ts +300 -0
  155. package/src/ui-engine/runtime-header/init-press-input.ts +126 -0
  156. package/src/ui-engine/runtime-header/keyboard.ts +342 -0
  157. package/src/ui-engine/runtime-header/node-decoration.ts +212 -0
  158. package/src/ui-engine/runtime-header/paint-order-coords.ts +190 -0
  159. package/src/ui-engine/runtime-header/paint-rects-repair.ts +553 -0
  160. package/src/ui-engine/runtime-header/scroll-physics.ts +161 -0
  161. package/src/ui-engine/runtime-header/state-bindings-nav.ts +176 -0
  162. package/src/ui-engine/runtime-header/structs.ts +220 -0
  163. package/src/ui-engine/runtime-header/text-rendering.ts +715 -0
  164. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +103 -0
  165. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +1051 -0
  166. package/src/ui-engine/runtime-header/tick/flush-phase.ts +17 -0
  167. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +15 -0
  168. package/src/ui-engine/runtime-header/tick/transitions-phase.ts +213 -0
  169. package/src/ui-engine/runtime-header/tick-motion-helpers.ts +26 -0
  170. package/src/ui-engine/runtime-header/tick.ts +18 -0
  171. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +406 -0
  172. package/src/ui-engine/runtime-header/types-defines.ts +79 -0
  173. package/src/ui-engine/runtime-header.ts +71 -0
  174. package/src/ui-engine/scroll-memory-diagnostics.ts +79 -0
  175. package/src/ui-engine/select-engine.ts +21 -0
  176. package/src/ui-engine/style-resolver.ts +416 -0
  177. package/src/ui-engine/text-layout.ts +114 -0
  178. package/src/ui-engine/transpile-ui.ts +45 -0
  179. package/src/ui-engine/ua-stylesheet.ts +60 -0
  180. package/src/ui-engine/ui-file-splitter.ts +59 -0
  181. package/src/ui-engine/ui-lowering.ts +459 -0
  182. package/src/ui-engine/ui-registry.ts +530 -0
  183. package/src/ui-engine/yoga-layout.ts +309 -0
@@ -0,0 +1,580 @@
1
+ export const GFX_FONT_BYTES = 1280;
2
+
3
+ export interface ImageDataLike {
4
+ readonly data: Uint8ClampedArray;
5
+ readonly width: number;
6
+ readonly height: number;
7
+ }
8
+
9
+ export function rgb565ToRgb888(color: number): { r: number; g: number; b: number } {
10
+ const r5 = (color >> 11) & 0x1f;
11
+ const g6 = (color >> 5) & 0x3f;
12
+ const b5 = color & 0x1f;
13
+ return {
14
+ r: (r5 << 3) | (r5 >> 2),
15
+ g: (g6 << 2) | (g6 >> 4),
16
+ b: (b5 << 3) | (b5 >> 2),
17
+ };
18
+ }
19
+
20
+ /** Quantize a packed RGB888 value to RGB565 (uint16). */
21
+ export function rgb888To565(color: number): number {
22
+ const r = (color >> 16) & 0xff, g = (color >> 8) & 0xff, b = color & 0xff;
23
+ return ((r & 0xf8) << 8) | ((g & 0xfc) << 3) | (b >> 3);
24
+ }
25
+
26
+ /** Reconstruct a packed RGB888 value from a 565 value (lossy). */
27
+ export function rgb565To888(color: number): number {
28
+ const { r, g, b } = rgb565ToRgb888(color);
29
+ return (r << 16) | (g << 8) | b;
30
+ }
31
+
32
+ export function blendRgb565(fg: number, bg: number, opacity: number): number {
33
+ if (opacity >= 100) return fg & 0xffff;
34
+ if (opacity <= 0) return bg & 0xffff;
35
+ // Blend in 888 internally for higher precision (parity with the device's
36
+ // ui_blend565): unpack 565→888, blend at 8-bit, re-quantize to 565.
37
+ const fr5 = (fg >> 11) & 0x1f, fg6 = (fg >> 5) & 0x3f, fb5 = fg & 0x1f;
38
+ const br5 = (bg >> 11) & 0x1f, bg6 = (bg >> 5) & 0x3f, bb5 = bg & 0x1f;
39
+ const fr8 = (fr5 << 3) | (fr5 >> 2), fg8 = (fg6 << 2) | (fg6 >> 4), fb8 = (fb5 << 3) | (fb5 >> 2);
40
+ const br8 = (br5 << 3) | (br5 >> 2), bg8 = (bg6 << 2) | (bg6 >> 4), bb8 = (bb5 << 3) | (bb5 >> 2);
41
+ const r = Math.trunc((fr8 * opacity + br8 * (100 - opacity)) / 100);
42
+ const g = Math.trunc((fg8 * opacity + bg8 * (100 - opacity)) / 100);
43
+ const b = Math.trunc((fb8 * opacity + bb8 * (100 - opacity)) / 100);
44
+ return (((r >> 3) & 0x1f) << 11) | (((g >> 2) & 0x3f) << 5) | ((b >> 3) & 0x1f);
45
+ }
46
+
47
+ /** Blend two RGB888 colors by opacity (0-100). Added for Phase 2 (RGB888/RGB666
48
+ * targets); unused in Phase 1, whose TFT path keeps 565 values and blends via
49
+ * blendRgb565 above for byte-identity with the device runtime. */
50
+ export function blendRgb888(fg: number, bg: number, opacity: number): number {
51
+ if (opacity >= 100) return fg & 0xffffff;
52
+ if (opacity <= 0) return bg & 0xffffff;
53
+ const fr = (fg >> 16) & 0xff, fg8 = (fg >> 8) & 0xff, fb = fg & 0xff;
54
+ const br = (bg >> 16) & 0xff, bg8 = (bg >> 8) & 0xff, bb = bg & 0xff;
55
+ const r = Math.trunc((fr * opacity + br * (100 - opacity)) / 100);
56
+ const g = Math.trunc((fg8 * opacity + bg8 * (100 - opacity)) / 100);
57
+ const b = Math.trunc((fb * opacity + bb * (100 - opacity)) / 100);
58
+ return ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff);
59
+ }
60
+
61
+ export class HostAdafruitGFX {
62
+ readonly buffer: Uint32Array;
63
+ private cursorX = 0;
64
+ private cursorY = 0;
65
+ private textColor = 0xffffff;
66
+ private textBgColor = 0xffffff;
67
+ // When true (mono/e-ink target), draw primitives snap color values to black/
68
+ // white by luminance — mirrors the device's UI_MAYBE_SNAP_MONO565 choke-point.
69
+ private monoSnap = false;
70
+ private textSizeX = 1;
71
+ private textSizeY = 1;
72
+ private wrap = true;
73
+ private clipRect: { x: number; y: number; w: number; h: number } | undefined;
74
+
75
+ constructor(
76
+ readonly width: number,
77
+ readonly height: number,
78
+ private readonly font: Uint8Array = new Uint8Array(GFX_FONT_BYTES),
79
+ ) {
80
+ this.buffer = new Uint32Array(width * height);
81
+ }
82
+
83
+ begin(): void {
84
+ // Hardware driver compatibility hook.
85
+ }
86
+
87
+ /** Enable 1-bit mono snapping (e-ink targets). When on, draw primitives snap
88
+ * every color to black/white by luminance, matching the device's
89
+ * UI_MAYBE_SNAP_MONO565. Uses the same threshold as toMono (0.27). */
90
+ setMonoSnap(on: boolean): void {
91
+ this.monoSnap = on;
92
+ }
93
+
94
+ private snap(c: number): number {
95
+ if (!this.monoSnap) return c;
96
+ // Pre-snapped mono values (0=black, 1=white from the transpiler's
97
+ // rgb888ToMono) pass through directly — the luminance threshold would
98
+ // misclassify 1 as black (r=0,g=0,b=1 → luminance=114 < 68850).
99
+ if (c === 0 || c === 0x0000) return 0x000000;
100
+ if (c === 1 || c === 0x0001 || c === 0xffff || c === 0xffffff) return 0xffffff;
101
+ const r = (c >> 16) & 0xff, g = (c >> 8) & 0xff, b = c & 0xff;
102
+ return (299 * r + 587 * g + 114 * b >= 68850) ? 0xffffff : 0x000000;
103
+ }
104
+
105
+ setRotation(_rotation: number): void {
106
+ // The preview framebuffer is already in display-profile coordinates.
107
+ }
108
+
109
+ setClipRect(rect: { x: number; y: number; w: number; h: number } | undefined): void {
110
+ this.clipRect = rect ? {
111
+ x: Math.trunc(rect.x),
112
+ y: Math.trunc(rect.y),
113
+ w: Math.max(0, Math.trunc(rect.w)),
114
+ h: Math.max(0, Math.trunc(rect.h)),
115
+ } : undefined;
116
+ }
117
+
118
+ getClipRect(): { x: number; y: number; w: number; h: number } | undefined {
119
+ return this.clipRect ? { ...this.clipRect } : undefined;
120
+ }
121
+
122
+ withClipRect<T>(rect: { x: number; y: number; w: number; h: number } | undefined, fn: () => T): T {
123
+ const previous = this.clipRect;
124
+ this.setClipRect(rect);
125
+ try {
126
+ return fn();
127
+ } finally {
128
+ this.clipRect = previous;
129
+ }
130
+ }
131
+
132
+ private insideClip(x: number, y: number): boolean {
133
+ const clip = this.clipRect;
134
+ return !clip || (x >= clip.x && y >= clip.y && x < clip.x + clip.w && y < clip.y + clip.h);
135
+ }
136
+
137
+ drawPixel(x: number, y: number, color: number): void {
138
+ x = Math.trunc(x);
139
+ y = Math.trunc(y);
140
+ if (x < 0 || y < 0 || x >= this.width || y >= this.height) return;
141
+ if (!this.insideClip(x, y)) return;
142
+ this.buffer[y * this.width + x] = this.snap(color) & 0xffffff;
143
+ }
144
+
145
+ writePixel(x: number, y: number, color: number): void {
146
+ this.drawPixel(x, y, color);
147
+ }
148
+
149
+ writeFillRect(x: number, y: number, w: number, h: number, color: number): void {
150
+ this.fillRect(x, y, w, h, color);
151
+ }
152
+
153
+ fillRect(x: number, y: number, w: number, h: number, color: number): void {
154
+ x = Math.trunc(x);
155
+ y = Math.trunc(y);
156
+ w = Math.trunc(w);
157
+ h = Math.trunc(h);
158
+ if (w <= 0 || h <= 0) return;
159
+
160
+ let x0 = x;
161
+ let y0 = y;
162
+ let x1 = x + w;
163
+ let y1 = y + h;
164
+ if (x0 < 0) x0 = 0;
165
+ if (y0 < 0) y0 = 0;
166
+ if (x1 > this.width) x1 = this.width;
167
+ if (y1 > this.height) y1 = this.height;
168
+ if (this.clipRect) {
169
+ x0 = Math.max(x0, this.clipRect.x);
170
+ y0 = Math.max(y0, this.clipRect.y);
171
+ x1 = Math.min(x1, this.clipRect.x + this.clipRect.w);
172
+ y1 = Math.min(y1, this.clipRect.y + this.clipRect.h);
173
+ }
174
+ if (x0 >= x1 || y0 >= y1) return;
175
+
176
+ const c = this.snap(color) & 0xffffff;
177
+ for (let yy = y0; yy < y1; yy++) {
178
+ this.buffer.fill(c, yy * this.width + x0, yy * this.width + x1);
179
+ }
180
+ }
181
+
182
+ fillScreen(color: number): void {
183
+ this.buffer.fill(this.snap(color) & 0xffffff);
184
+ }
185
+
186
+ drawFastHLine(x: number, y: number, w: number, color: number): void {
187
+ this.fillRect(x, y, w, 1, color);
188
+ }
189
+
190
+ writeFastHLine(x: number, y: number, w: number, color: number): void {
191
+ this.drawFastHLine(x, y, w, color);
192
+ }
193
+
194
+ drawFastVLine(x: number, y: number, h: number, color: number): void {
195
+ this.fillRect(x, y, 1, h, color);
196
+ }
197
+
198
+ writeFastVLine(x: number, y: number, h: number, color: number): void {
199
+ this.drawFastVLine(x, y, h, color);
200
+ }
201
+
202
+ drawRect(x: number, y: number, w: number, h: number, color: number): void {
203
+ if (w <= 0 || h <= 0) return;
204
+ this.drawFastHLine(x, y, w, color);
205
+ this.drawFastHLine(x, y + h - 1, w, color);
206
+ this.drawFastVLine(x, y, h, color);
207
+ this.drawFastVLine(x + w - 1, y, h, color);
208
+ }
209
+
210
+ drawRoundRect(x: number, y: number, w: number, h: number, r: number, color: number): void {
211
+ x = Math.trunc(x);
212
+ y = Math.trunc(y);
213
+ w = Math.trunc(w);
214
+ h = Math.trunc(h);
215
+ r = Math.max(0, Math.trunc(r));
216
+ if (w <= 0 || h <= 0) return;
217
+ if (r <= 0) {
218
+ this.drawRect(x, y, w, h, color);
219
+ return;
220
+ }
221
+ r = Math.min(r, Math.trunc(Math.min(w, h) / 2));
222
+ this.drawFastHLine(x + r, y, w - 2 * r, color);
223
+ this.drawFastHLine(x + r, y + h - 1, w - 2 * r, color);
224
+ this.drawFastVLine(x, y + r, h - 2 * r, color);
225
+ this.drawFastVLine(x + w - 1, y + r, h - 2 * r, color);
226
+ this.drawCircleHelper(x + r, y + r, r, 1, color);
227
+ this.drawCircleHelper(x + w - r - 1, y + r, r, 2, color);
228
+ this.drawCircleHelper(x + w - r - 1, y + h - r - 1, r, 4, color);
229
+ this.drawCircleHelper(x + r, y + h - r - 1, r, 8, color);
230
+ }
231
+
232
+ fillRoundRect(x: number, y: number, w: number, h: number, r: number, color: number): void {
233
+ x = Math.trunc(x);
234
+ y = Math.trunc(y);
235
+ w = Math.trunc(w);
236
+ h = Math.trunc(h);
237
+ r = Math.max(0, Math.trunc(r));
238
+ if (w <= 0 || h <= 0) return;
239
+ if (r <= 0) {
240
+ this.fillRect(x, y, w, h, color);
241
+ return;
242
+ }
243
+ r = Math.min(r, Math.trunc(Math.min(w, h) / 2));
244
+ this.fillRect(x + r, y, w - 2 * r, h, color);
245
+ const delta = Math.max(0, h - 2 * r - 1);
246
+ this.fillCircleHelper(x + w - r - 1, y + r, r, 1, delta, color);
247
+ this.fillCircleHelper(x + r, y + r, r, 2, delta, color);
248
+ }
249
+
250
+ drawCircle(x0: number, y0: number, r: number, color: number): void {
251
+ x0 = Math.trunc(x0);
252
+ y0 = Math.trunc(y0);
253
+ r = Math.trunc(r);
254
+ let f = 1 - r;
255
+ let ddFx = 1;
256
+ let ddFy = -2 * r;
257
+ let x = 0;
258
+ let y = r;
259
+
260
+ this.drawPixel(x0, y0 + r, color);
261
+ this.drawPixel(x0, y0 - r, color);
262
+ this.drawPixel(x0 + r, y0, color);
263
+ this.drawPixel(x0 - r, y0, color);
264
+
265
+ while (x < y) {
266
+ if (f >= 0) {
267
+ y--;
268
+ ddFy += 2;
269
+ f += ddFy;
270
+ }
271
+ x++;
272
+ ddFx += 2;
273
+ f += ddFx;
274
+
275
+ this.drawPixel(x0 + x, y0 + y, color);
276
+ this.drawPixel(x0 - x, y0 + y, color);
277
+ this.drawPixel(x0 + x, y0 - y, color);
278
+ this.drawPixel(x0 - x, y0 - y, color);
279
+ this.drawPixel(x0 + y, y0 + x, color);
280
+ this.drawPixel(x0 - y, y0 + x, color);
281
+ this.drawPixel(x0 + y, y0 - x, color);
282
+ this.drawPixel(x0 - y, y0 - x, color);
283
+ }
284
+ }
285
+
286
+ private drawCircleHelper(x0: number, y0: number, r: number, cornername: number, color: number): void {
287
+ x0 = Math.trunc(x0);
288
+ y0 = Math.trunc(y0);
289
+ r = Math.trunc(r);
290
+ let f = 1 - r;
291
+ let ddFx = 1;
292
+ let ddFy = -2 * r;
293
+ let x = 0;
294
+ let y = r;
295
+
296
+ while (x < y) {
297
+ if (f >= 0) {
298
+ y--;
299
+ ddFy += 2;
300
+ f += ddFy;
301
+ }
302
+ x++;
303
+ ddFx += 2;
304
+ f += ddFx;
305
+
306
+ if (cornername & 0x4) {
307
+ this.drawPixel(x0 + x, y0 + y, color);
308
+ this.drawPixel(x0 + y, y0 + x, color);
309
+ }
310
+ if (cornername & 0x2) {
311
+ this.drawPixel(x0 + x, y0 - y, color);
312
+ this.drawPixel(x0 + y, y0 - x, color);
313
+ }
314
+ if (cornername & 0x8) {
315
+ this.drawPixel(x0 - y, y0 + x, color);
316
+ this.drawPixel(x0 - x, y0 + y, color);
317
+ }
318
+ if (cornername & 0x1) {
319
+ this.drawPixel(x0 - y, y0 - x, color);
320
+ this.drawPixel(x0 - x, y0 - y, color);
321
+ }
322
+ }
323
+ }
324
+
325
+ fillCircle(x0: number, y0: number, r: number, color: number): void {
326
+ x0 = Math.trunc(x0);
327
+ y0 = Math.trunc(y0);
328
+ r = Math.trunc(r);
329
+ this.drawFastVLine(x0, y0 - r, 2 * r + 1, color);
330
+ this.fillCircleHelper(x0, y0, r, 3, 0, color);
331
+ }
332
+
333
+ private fillCircleHelper(x0: number, y0: number, r: number, corners: number, delta: number, color: number): void {
334
+ let f = 1 - r;
335
+ let ddFx = 1;
336
+ let ddFy = -2 * r;
337
+ let x = 0;
338
+ let y = r;
339
+ let px = x;
340
+ let py = y;
341
+
342
+ delta++;
343
+ while (x < y) {
344
+ if (f >= 0) {
345
+ y--;
346
+ ddFy += 2;
347
+ f += ddFy;
348
+ }
349
+ x++;
350
+ ddFx += 2;
351
+ f += ddFx;
352
+ if (x < y + 1) {
353
+ if (corners & 1) this.drawFastVLine(x0 + x, y0 - y, 2 * y + delta, color);
354
+ if (corners & 2) this.drawFastVLine(x0 - x, y0 - y, 2 * y + delta, color);
355
+ }
356
+ if (y !== py) {
357
+ if (corners & 1) this.drawFastVLine(x0 + py, y0 - px, 2 * px + delta, color);
358
+ if (corners & 2) this.drawFastVLine(x0 - py, y0 - px, 2 * px + delta, color);
359
+ py = y;
360
+ }
361
+ px = x;
362
+ }
363
+ }
364
+
365
+ drawLine(x0: number, y0: number, x1: number, y1: number, color: number): void {
366
+ x0 = Math.trunc(x0);
367
+ y0 = Math.trunc(y0);
368
+ x1 = Math.trunc(x1);
369
+ y1 = Math.trunc(y1);
370
+
371
+ const steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
372
+ if (steep) {
373
+ [x0, y0] = [y0, x0];
374
+ [x1, y1] = [y1, x1];
375
+ }
376
+ if (x0 > x1) {
377
+ [x0, x1] = [x1, x0];
378
+ [y0, y1] = [y1, y0];
379
+ }
380
+
381
+ const dx = x1 - x0;
382
+ const dy = Math.abs(y1 - y0);
383
+ let err = Math.trunc(dx / 2);
384
+ const ystep = y0 < y1 ? 1 : -1;
385
+
386
+ for (; x0 <= x1; x0++) {
387
+ if (steep) this.drawPixel(y0, x0, color);
388
+ else this.drawPixel(x0, y0, color);
389
+ err -= dy;
390
+ if (err < 0) {
391
+ y0 += ystep;
392
+ err += dx;
393
+ }
394
+ }
395
+ }
396
+
397
+ setCursor(x: number, y: number): void {
398
+ this.cursorX = Math.trunc(x);
399
+ this.cursorY = Math.trunc(y);
400
+ }
401
+
402
+ setTextColor(color: number, bg?: number): void {
403
+ this.textColor = color & 0xffffff;
404
+ this.textBgColor = bg === undefined ? this.textColor : bg & 0xffffff;
405
+ }
406
+
407
+ setTextSize(size: number, sy?: number): void {
408
+ this.textSizeX = Math.max(1, Math.trunc(size));
409
+ this.textSizeY = Math.max(1, Math.trunc(sy ?? size));
410
+ }
411
+
412
+ setTextWrap(wrap: boolean): void {
413
+ this.wrap = wrap;
414
+ }
415
+
416
+ print(value: unknown): void {
417
+ const text = String(value ?? "");
418
+ for (let i = 0; i < text.length; i++) {
419
+ this.write(text.charCodeAt(i) & 0xff);
420
+ }
421
+ }
422
+
423
+ textWidth(value: unknown, size = this.textSizeX): number {
424
+ return String(value ?? "").length * Math.max(1, Math.trunc(size)) * 6;
425
+ }
426
+
427
+ textHeight(size = this.textSizeY): number {
428
+ return Math.max(1, Math.trunc(size)) * 8;
429
+ }
430
+
431
+ drawAntialiasedText(value: unknown, x: number, y: number, color: number, bg: number, size: number): void {
432
+ const text = String(value ?? "");
433
+ if (!text) return;
434
+ size = Math.max(1, Math.trunc(size));
435
+ const w = this.textWidth(text, size);
436
+ const h = this.textHeight(size);
437
+ if (w <= 0 || h <= 0 || (color & 0xffff) === (bg & 0xffff)) {
438
+ this.setCursor(x, y);
439
+ this.setTextColor(color, bg);
440
+ this.setTextSize(size);
441
+ this.setTextWrap(false);
442
+ this.print(text);
443
+ return;
444
+ }
445
+ const destX = Math.trunc(x);
446
+ const destY = Math.trunc(y);
447
+ const activeClip = this.clipRect ?? { x: 0, y: 0, w: this.width, h: this.height };
448
+ const clipX0 = Math.max(destX, activeClip.x, 0);
449
+ const clipY0 = Math.max(destY, activeClip.y, 0);
450
+ const clipX1 = Math.min(destX + w, activeClip.x + activeClip.w, this.width);
451
+ const clipY1 = Math.min(destY + h, activeClip.y + activeClip.h, this.height);
452
+ if (clipX0 >= clipX1 || clipY0 >= clipY1) return;
453
+ const localX0 = clipX0 - destX;
454
+ const localY0 = clipY0 - destY;
455
+ const localX1 = clipX1 - destX;
456
+ const localY1 = clipY1 - destY;
457
+
458
+ const src = new HostAdafruitGFX(w, h, this.font);
459
+ src.fillScreen(bg);
460
+ src.setCursor(0, 0);
461
+ src.setTextColor(color, bg);
462
+ src.setTextSize(size);
463
+ src.setTextWrap(false);
464
+ src.print(text);
465
+
466
+ const fg = color & 0xffff;
467
+ const background = bg & 0xffff;
468
+ const out = new Uint16Array(w * h);
469
+ out.fill(background);
470
+ const foregroundNeighbors = (px: number, py: number, radius = 1): number => {
471
+ let count = 0;
472
+ for (let dy = -radius; dy <= radius; dy++) {
473
+ const yy = py + dy;
474
+ if (yy < 0 || yy >= h) continue;
475
+ for (let dx = -radius; dx <= radius; dx++) {
476
+ const xx = px + dx;
477
+ if (xx < 0 || xx >= w) continue;
478
+ if (src.buffer[yy * w + xx] === fg) count++;
479
+ }
480
+ }
481
+ return count;
482
+ };
483
+ const coverageFor = (neighbors: number, outerNeighbors: number, isFg: boolean): number => {
484
+ if (isFg) {
485
+ if (size <= 1) return neighbors >= 4 ? 100 : 96;
486
+ if (size === 2) return neighbors >= 8 ? 100 : neighbors >= 5 ? 96 : 92;
487
+ return neighbors >= 8 ? 100 : neighbors >= 6 ? 96 : neighbors >= 4 ? 90 : 84;
488
+ }
489
+ if (neighbors === 0) {
490
+ return size >= 3 && outerNeighbors > 0 ? Math.min(14, outerNeighbors * 2) : 0;
491
+ }
492
+ const step = size <= 1 ? 4 : size === 2 ? 6 : 8;
493
+ const cap = size <= 1 ? 18 : size === 2 ? 28 : 38;
494
+ return Math.min(cap, neighbors * step);
495
+ };
496
+
497
+ for (let yy = localY0; yy < localY1; yy++) {
498
+ for (let xx = localX0; xx < localX1; xx++) {
499
+ const px = src.buffer[yy * w + xx];
500
+ const neighbors = foregroundNeighbors(xx, yy);
501
+ const outerNeighbors = size >= 3 && px !== fg && neighbors === 0 ? foregroundNeighbors(xx, yy, 2) : 0;
502
+ const coverage = coverageFor(neighbors, outerNeighbors, px === fg);
503
+ out[yy * w + xx] = coverage === 0 ? background : blendRgb565(fg, background, coverage);
504
+ }
505
+ }
506
+
507
+ for (let yy = localY0; yy < localY1; yy++) {
508
+ for (let xx = localX0; xx < localX1; xx++) {
509
+ this.drawPixel(destX + xx, destY + yy, out[yy * w + xx]);
510
+ }
511
+ }
512
+ }
513
+
514
+ write(c: number): void {
515
+ if (c === 10) {
516
+ this.cursorX = 0;
517
+ this.cursorY += this.textSizeY * 8;
518
+ return;
519
+ }
520
+ if (c === 13) return;
521
+ if (this.wrap && this.cursorX + this.textSizeX * 6 > this.width) {
522
+ this.cursorX = 0;
523
+ this.cursorY += this.textSizeY * 8;
524
+ }
525
+ this.drawChar(this.cursorX, this.cursorY, c, this.textColor, this.textBgColor, this.textSizeX, this.textSizeY);
526
+ this.cursorX += this.textSizeX * 6;
527
+ }
528
+
529
+ drawChar(
530
+ x: number,
531
+ y: number,
532
+ c: number,
533
+ color: number,
534
+ bg: number,
535
+ sizeX: number,
536
+ sizeY: number = sizeX,
537
+ ): void {
538
+ if (x >= this.width || y >= this.height || x + 6 * sizeX - 1 < 0 || y + 8 * sizeY - 1 < 0) return;
539
+ if (c >= 176) c++;
540
+
541
+ for (let i = 0; i < 5; i++) {
542
+ let line = this.font[c * 5 + i] ?? 0;
543
+ for (let j = 0; j < 8; j++, line >>= 1) {
544
+ if (line & 1) {
545
+ if (sizeX === 1 && sizeY === 1) this.writePixel(x + i, y + j, color);
546
+ else this.writeFillRect(x + i * sizeX, y + j * sizeY, sizeX, sizeY, color);
547
+ } else if (bg !== color) {
548
+ if (sizeX === 1 && sizeY === 1) this.writePixel(x + i, y + j, bg);
549
+ else this.writeFillRect(x + i * sizeX, y + j * sizeY, sizeX, sizeY, bg);
550
+ }
551
+ }
552
+ }
553
+ if (bg !== color) {
554
+ if (sizeX === 1 && sizeY === 1) this.writeFastVLine(x + 5, y, 8, bg);
555
+ else this.writeFillRect(x + 5 * sizeX, y, sizeX, 8 * sizeY, bg);
556
+ }
557
+ }
558
+
559
+ toRgbaBytes(): Uint8ClampedArray {
560
+ const out = new Uint8ClampedArray(this.buffer.length * 4);
561
+ for (let i = 0; i < this.buffer.length; i++) {
562
+ const c = this.buffer[i];
563
+ const p = i * 4;
564
+ if (this.monoSnap) {
565
+ // Mono: values are already 0xffffff/0x000000 — emit directly.
566
+ out[p] = (c >> 16) & 0xff;
567
+ out[p + 1] = (c >> 8) & 0xff;
568
+ out[p + 2] = c & 0xff;
569
+ } else {
570
+ // 565 target: unpack to 888.
571
+ const { r, g, b } = rgb565ToRgb888(c);
572
+ out[p] = r;
573
+ out[p + 1] = g;
574
+ out[p + 2] = b;
575
+ }
576
+ out[p + 3] = 255;
577
+ }
578
+ return out;
579
+ }
580
+ }