@pictogrammers/components 0.5.14 → 0.5.17

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 (153) hide show
  1. package/package.json +1 -1
  2. package/pg/autocompleteItem/__examples__/basic/basic.html +3 -0
  3. package/pg/autocompleteItem/autocompleteItem.css +55 -0
  4. package/pg/autocompleteItem/autocompleteItem.html +1 -0
  5. package/pg/autocompleteItem/autocompleteItem.ts +65 -0
  6. package/pg/button/button.css +5 -0
  7. package/pg/buttonMenu/buttonMenu.ts +1 -1
  8. package/pg/inputCombobox/__examples__/basic/basic.ts +33 -0
  9. package/pg/inputCombobox/inputCombobox.ts +158 -0
  10. package/pg/inputCombobox/inputCombobox.ts.tmp.44252.751480cb81cc +158 -0
  11. package/pg/inputCombobox/inputCombobox.ts.tmp.44252.dcea847c40a8 +156 -0
  12. package/pg/inputNumber/inputNumber.ts +31 -13
  13. package/pg/inputPill/inputPill.html +6 -0
  14. package/pg/inputPill/inputPill.html.tmp.26852.84ac63f4c46f +6 -0
  15. package/pg/inputPill/inputPill.ts +41 -0
  16. package/pg/inputPills/__examples__/basic/basic.html +9 -0
  17. package/pg/inputPills/__examples__/basic/basic.html.tmp.26852.4771e4cad5b7 +9 -0
  18. package/pg/inputPills/__examples__/basic/basic.ts.tmp.26852.d8fe41df18bd +0 -0
  19. package/pg/inputPixelEditor/README.md +104 -69
  20. package/pg/inputPixelEditor/README.md.tmp.42596.658de5f0a6f5 +0 -0
  21. package/pg/inputPixelEditor/__examples__/basic/basic.ts +6 -5
  22. package/pg/inputPixelEditor/inputPixelEditor.ts +95 -23
  23. package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.38516.96529dd82670 +1703 -0
  24. package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.42596.48ecc3ad9c72 +1724 -0
  25. package/pg/inputPixelEditor/utils/bitmapToMask.ts +113 -79
  26. package/pg/inputPixelEditor/utils/getLassoPixels.ts +51 -0
  27. package/pg/inputRange/inputRange.css +7 -0
  28. package/pg/inputSelect/__examples__/basic/basic.ts +2 -2
  29. package/pg/inputSelect/inputSelect.css +1 -1
  30. package/pg/inputSelect/inputSelect.ts +62 -5
  31. package/pg/inputText/README.md +6 -6
  32. package/pg/inputText/__examples__/basic/basic.ts +1 -1
  33. package/pg/inputText/inputText.css +2 -2
  34. package/pg/inputText copy/README.md +38 -0
  35. package/pg/inputText copy/inputText.css +41 -0
  36. package/pg/inputText copy/inputText.html +1 -0
  37. package/pg/inputText copy/inputText.spec.ts +59 -0
  38. package/pg/inputText copy/inputText.ts +67 -0
  39. package/pg/inputTextarea/README.md +39 -0
  40. package/pg/inputTextarea/__examples__/basic/basic.html +9 -0
  41. package/pg/inputTextarea/__examples__/basic/basic.ts +30 -0
  42. package/pg/inputTextarea/inputTextarea.ts +121 -0
  43. package/pg/inputTextarea copy/README.md +39 -0
  44. package/pg/inputTextarea copy/inputTextarea.css +41 -0
  45. package/pg/inputTextarea copy/inputTextarea.html +1 -0
  46. package/pg/inputTextarea copy/inputTextarea.spec.ts +59 -0
  47. package/pg/inputTextarea copy/inputTextarea.ts +79 -0
  48. package/pg/inputTextareaAutocomplete/README.md +70 -0
  49. package/pg/inputTextareaAutocomplete/__examples__/basic/basic.ts +67 -0
  50. package/pg/inputTextareaAutocomplete/inputTextareaAutocomplete.ts +267 -0
  51. package/pg/node/README.md +35 -0
  52. package/pg/node/__examples__/basic/basic.ts +34 -0
  53. package/pg/node/index.ts +3 -0
  54. package/pg/node/node.css +53 -0
  55. package/pg/node/node.html +5 -0
  56. package/pg/node/node.ts +270 -0
  57. package/pg/nodeEditorLink/nodeEditorLink.css +69 -0
  58. package/pg/nodeEditorLink/nodeEditorLink.css.tmp.1408.09e961fd0dcc +69 -0
  59. package/pg/nodeEditorLink/nodeEditorLink.html +9 -0
  60. package/pg/nodeEditorNumber/nodeEditorNumber.css +59 -0
  61. package/pg/nodeEditorNumber/nodeEditorNumber.ts +57 -0
  62. package/pg/nodeEditorRange/nodeEditorRange.css +23 -0
  63. package/pg/nodeEditorRange/nodeEditorRange.ts +69 -0
  64. package/pg/nodeEditorText/__examples__/basic/basic.html +9 -0
  65. package/pg/nodeEditorText/__examples__/basic/basic.ts +30 -0
  66. package/pg/nodeEditorText/nodeEditorText.css +17 -0
  67. package/pg/nodeEditorText/nodeEditorText.html +4 -0
  68. package/pg/nodeEditorText/nodeEditorText.ts +57 -0
  69. package/pg/nodeEditorText copy/README.md +35 -0
  70. package/pg/nodeEditorText copy/nodeEditorText.css +49 -0
  71. package/pg/nodeEditorText copy/nodeEditorText.html +4 -0
  72. package/pg/nodeEditorText copy/nodeEditorText.ts +50 -0
  73. package/pg/nodeEditorTextArray/nodeEditorTextArray.css +68 -0
  74. package/pg/nodeEditorTextArray/nodeEditorTextArray.html +9 -0
  75. package/pg/nodeEditorTextArray/nodeEditorTextArray.ts +162 -0
  76. package/pg/nodeEntry/nodeEntry.css +61 -0
  77. package/pg/nodeEntry/nodeEntry.ts +164 -0
  78. package/pg/nodeEntry/nodeEntry.ts.tmp.28724.dda67762bca5 +164 -0
  79. package/pg/nodeInputText/nodeInputText.css +75 -0
  80. package/pg/nodeInputText/nodeInputText.html +4 -0
  81. package/pg/nodeInputText/nodeInputText.ts +105 -0
  82. package/pg/nodeOutput/nodeOutput.css +32 -0
  83. package/pg/nodeResize/README.md +3 -0
  84. package/pg/nodeResize/THINKING.md +81 -0
  85. package/pg/nodeResize/THINKING.md.tmp.24812.99a70ba81062 +81 -0
  86. package/pg/nodeResize/basic/basic.ts +17 -0
  87. package/pg/nodeResize/dragUtil.ts +105 -0
  88. package/pg/nodeResize/dragUtil.ts.tmp.34372.787464c73309 +75 -0
  89. package/pg/nodeResize/nodeResize.css +187 -0
  90. package/pg/nodeResize/nodeResize.html +9 -0
  91. package/pg/nodeResize/nodeResize.ts +156 -0
  92. package/pg/nodes/README.md +177 -0
  93. package/pg/nodes/__examples__/basic/basic.html +33 -0
  94. package/pg/nodes/__examples__/basic/basic.ts +716 -0
  95. package/pg/nodes/__examples__/basic/exampleNodes.ts +129 -0
  96. package/pg/nodes/__examples__/basic/exampleScripts.ts +129 -0
  97. package/pg/nodes/__examples__/basic/keystore.ts +38 -0
  98. package/pg/nodes/executeInSandbox.ts +32 -0
  99. package/pg/nodes/index.ts +3 -0
  100. package/pg/nodes/nodes.css +69 -0
  101. package/pg/nodes/nodes.html +6 -0
  102. package/pg/nodes/nodes.ts +1240 -0
  103. package/pg/nodes copy/nodes.css +41 -0
  104. package/pg/nodes copy/nodes.html +1 -0
  105. package/pg/overlayContextMenu/__examples__/filter/filter.ts +87 -0
  106. package/pg/overlayContextMenu/overlayContextMenu.css +14 -0
  107. package/pg/overlayContextMenu/overlayContextMenu.html +4 -3
  108. package/pg/overlayContextMenu/overlayContextMenu.ts +6 -2
  109. package/pg/overlayContextMenu/overlayContextMenu.ts.tmp.37088.530ff1c0a321 +0 -0
  110. package/pg/overlayMenu/overlayMenu.css +3 -1
  111. package/pg/overlayMenu/overlayMenu.ts +11 -2
  112. package/pg/overlayMenu/overlayMenu.ts.tmp.44252.9bb22fd59ede +80 -0
  113. package/pg/overlaySelectMenu/overlaySelectMenu.css +2 -0
  114. package/pg/overlaySubMenu/overlaySubMenu.css +8 -6
  115. package/pg/poco/README.md +22 -0
  116. package/pg/poco/__examples__/basic/basic.html +11 -0
  117. package/pg/poco/__examples__/basic/basic.ts +95 -0
  118. package/pg/poco/__examples__/basic/examples.ts +258 -0
  119. package/pg/poco/__examples__/basic/myFont.ts +1300 -0
  120. package/pg/poco/index.ts +3 -0
  121. package/pg/poco/mockPoco.ts +640 -0
  122. package/pg/poco/poco.css +7 -0
  123. package/pg/poco/poco.html +1 -0
  124. package/pg/poco/poco.ts +75 -0
  125. package/pg/popover/README.md +47 -0
  126. package/pg/popover/__examples__/basic/basic.html +3 -0
  127. package/pg/popover/popover.html +3 -0
  128. package/pg/popover/popover.ts +44 -0
  129. package/pg/popoverAutocomplete/__examples__/basic/basic.html +4 -0
  130. package/pg/popoverAutocomplete/__examples__/basic/basic.ts +46 -0
  131. package/pg/popoverAutocomplete/popoverAutocomplete.css +25 -0
  132. package/pg/popoverAutocomplete/popoverAutocomplete.html +1 -0
  133. package/pg/popoverAutocomplete/popoverAutocomplete.ts +134 -0
  134. package/pg/popoverAutocompleteItem/popoverAutocompleteItem.css +69 -0
  135. package/pg/popoverAutocompleteItem/popoverAutocompleteItem.ts +90 -0
  136. package/pg/table/__examples__/basic/basic.ts +1 -0
  137. package/pg/tableCellFileLocal/tableCellFileLocal.html +3 -0
  138. package/pg/tableCellFileLocal/tableCellFileLocal.html.tmp.20484.68ee2e30710e +3 -0
  139. package/pg/tableCellFileLocal/tableCellFileLocal.ts +44 -0
  140. package/pg/tableCellSelect/tableCellSelect.html +3 -0
  141. package/pg/tableCellSelect/tableCellSelect.html.tmp.41808.99c1a6f14340 +3 -0
  142. package/pg/tableCellSelect/tableCellSelect.ts +51 -0
  143. package/pg/tree/__examples__/basic/basic.ts +12 -5
  144. package/pg/tree/tree.html +1 -1
  145. package/pg/tree/tree.ts +385 -227
  146. package/pg/tree/tree.ts.tmp.38376.07d376a04f41 +343 -0
  147. package/pg/tree/tree.ts.tmp.38376.2a04599ade9f +361 -0
  148. package/pg/tree/tree.ts.tmp.38376.6cb2c663d476 +0 -0
  149. package/pg/tree/tree.ts.tmp.38376.ac5af1aff2b0 +393 -0
  150. package/pg/tree/tree.ts.tmp.38376.ac9acfc4f95f +324 -0
  151. package/pg/treeItem/treeItem.ts +536 -558
  152. package/pg/treeItem/treeItem.ts.tmp.38376.145d3121d648 +530 -0
  153. package/pg/treeItem/treeItem.ts.tmp.38376.b5224d48e5c0 +0 -0
@@ -0,0 +1,3 @@
1
+ import PgPoco from './poco';
2
+
3
+ export default PgPoco;
@@ -0,0 +1,640 @@
1
+ import { readFnt } from '@pictogrammers/fnt';
2
+
3
+ type FntFont = ReturnType<typeof readFnt>;
4
+ type FntChar = FntFont['chars'][number];
5
+
6
+ type StackEntry =
7
+ | { type: 'clip'; x: number; y: number; w: number; h: number }
8
+ | { type: 'origin'; x: number; y: number };
9
+
10
+ function parseCSSColor(css: string): [number, number, number] {
11
+ const m = css.match(/rgb\((\d+),(\d+),(\d+)\)/);
12
+ if (m) return [+m[1], +m[2], +m[3]];
13
+ if (css === 'white' || css === '#fff' || css === '#ffffff') return [255, 255, 255];
14
+ if (css === 'black' || css === '#000' || css === '#000000') return [0, 0, 0];
15
+ return [128, 128, 128];
16
+ }
17
+
18
+ export class MockFont {
19
+ readonly fnt: FntFont;
20
+ readonly atlas: HTMLCanvasElement;
21
+ readonly charMap: Map<number, FntChar>;
22
+ readonly kerningMap: Map<string, number>;
23
+ readonly isGrayAtlas: boolean;
24
+
25
+ constructor(fnt: FntFont, atlas: HTMLCanvasElement, isGrayAtlas: boolean) {
26
+ this.fnt = fnt;
27
+ this.atlas = atlas;
28
+ this.isGrayAtlas = isGrayAtlas;
29
+
30
+ this.charMap = new Map();
31
+ for (const ch of fnt.chars) {
32
+ this.charMap.set(ch.id, ch);
33
+ }
34
+
35
+ this.kerningMap = new Map();
36
+ for (const k of fnt.kernings) {
37
+ this.kerningMap.set(`${k.first},${k.second}`, k.amount);
38
+ }
39
+ }
40
+
41
+ get name(): string {
42
+ return this.fnt.info.face;
43
+ }
44
+
45
+ get height(): number {
46
+ return this.fnt.common.lineHeight;
47
+ }
48
+
49
+ static async load(fntUrl: string): Promise<MockFont> {
50
+ const buffer = await fetch(fntUrl).then(r => r.arrayBuffer());
51
+ const fnt = readFnt(buffer);
52
+ const bmpUrl = fnt.pages[0].replace('.png', '.bmp');
53
+ // Load from resourceBMP if found
54
+ if (resourcesBMP.has(bmpUrl)) {
55
+ const mockBitmap = resourcesBMP.get(bmpUrl);
56
+ const { canvas, isGray } = MockFont._processAtlas(mockBitmap?._canvas!);
57
+ return new MockFont(fnt, canvas, isGray);
58
+ }
59
+ /*
60
+ // Backup request image
61
+ const dir = fntUrl.substring(0, fntUrl.lastIndexOf('/') + 1);
62
+ const image = await new Promise<HTMLImageElement>((resolve, reject) => {
63
+ const img = new Image();
64
+ img.onload = () => resolve(img);
65
+ img.onerror = () => reject(new Error(`Failed to load font texture: ${fnt.pages[0]}`));
66
+ img.src = dir + fnt.pages[0];
67
+ });
68
+
69
+ const { canvas, isGray } = MockFont._processAtlas(image);
70
+ return new MockFont(fnt, canvas, isGray);*/
71
+ // @ts-ignore
72
+ return new MockFont(fnt, null, true);
73
+ }
74
+
75
+ static from(fnt: FntFont): MockFont {
76
+ const bmpUrl = fnt.pages[0].replace('.png', '.bmp');
77
+ if (resourcesBMP.has(bmpUrl)) {
78
+ const mockBitmap = resourcesBMP.get(bmpUrl);
79
+ const { canvas, isGray } = MockFont._processAtlas(mockBitmap?._canvas!);
80
+ return new MockFont(fnt, canvas, isGray);
81
+ }
82
+
83
+ // Backup request image
84
+ /*const image = await new Promise<HTMLImageElement>((resolve, reject) => {
85
+ const img = new Image();
86
+ img.onload = () => resolve(img);
87
+ img.onerror = () => reject(new Error(`Failed to load font texture: ${bmpUrl}`));
88
+ img.src = bmpUrl;
89
+ });
90
+
91
+ const { canvas, isGray } = MockFont._processAtlas(image);
92
+ return new MockFont(fnt, canvas, isGray);*/
93
+ // @ts-ignore
94
+ return new MockFont(fnt, null, false);
95
+ }
96
+
97
+ // For opaque black-background atlases that are purely grayscale (R≈G≈B),
98
+ // converts pixel luminance to the alpha channel so source-in colorization works.
99
+ // Color atlases are stored as-is; the caller uses isGray to choose the draw path.
100
+ private static _processAtlas(canvas: HTMLCanvasElement): { canvas: HTMLCanvasElement; isGray: boolean } {
101
+ const ctx = canvas.getContext('2d')!;
102
+
103
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
104
+ const d = imageData.data;
105
+
106
+ if (d[3] > 128 && d[0] < 128) {
107
+ let isGray = true;
108
+ for (let i = 0; i < d.length; i += 4) {
109
+ if (Math.abs(d[i] - d[i + 1]) > 8 || Math.abs(d[i + 1] - d[i + 2]) > 8) {
110
+ isGray = false;
111
+ break;
112
+ }
113
+ }
114
+ if (isGray) {
115
+ for (let i = 0; i < d.length; i += 4) {
116
+ const luma = (d[i] * 299 + d[i + 1] * 587 + d[i + 2] * 114) / 1000;
117
+ d[i] = 255;
118
+ d[i + 1] = 255;
119
+ d[i + 2] = 255;
120
+ d[i + 3] = luma;
121
+ }
122
+ ctx.putImageData(imageData, 0, 0);
123
+ return { canvas, isGray: true };
124
+ }
125
+ }
126
+
127
+ return { canvas, isGray: false };
128
+ }
129
+ }
130
+
131
+ function drawToCanvas(bytes: Uint8Array, canvas: HTMLCanvasElement): void {
132
+ const view = new DataView(bytes.buffer);
133
+
134
+ const isPng = bytes[0] === 0x89 && bytes[1] === 0x50; // \x89P
135
+ const isBmp = bytes[0] === 0x42 && bytes[1] === 0x4d; // "BM"
136
+
137
+ let width: number;
138
+ let height: number;
139
+ let type: string;
140
+
141
+ if (isPng) {
142
+ width = view.getUint32(16, false); // big-endian
143
+ height = view.getUint32(20, false);
144
+ type = "image/png";
145
+ } else if (isBmp) {
146
+ width = view.getInt32(18, true); // little-endian
147
+ height = Math.abs(view.getInt32(22, true)); // can be negative (top-down BMP)
148
+ type = "image/bmp";
149
+ } else {
150
+ throw new Error("Unsupported format");
151
+ }
152
+
153
+ canvas.width = width;
154
+ canvas.height = height;
155
+
156
+ const blob = new Blob([bytes.buffer as ArrayBuffer], { type });
157
+ const url = URL.createObjectURL(blob);
158
+ const img = new Image();
159
+
160
+ img.onload = () => {
161
+ canvas.getContext("2d")!.drawImage(img, 0, 0);
162
+ URL.revokeObjectURL(url);
163
+ };
164
+
165
+ img.src = url;
166
+ }
167
+
168
+ export class MockBitmap {
169
+ readonly _canvas: HTMLCanvasElement;
170
+ readonly _imageData: ImageData;
171
+ readonly width: number;
172
+ readonly height: number;
173
+
174
+ constructor(canvas: HTMLCanvasElement) {
175
+ this._canvas = canvas;
176
+ this.width = canvas.width;
177
+ this.height = canvas.height;
178
+ const ctx = canvas.getContext('2d')!;
179
+ this._imageData = ctx.getImageData(0, 0, this.width, this.height);
180
+ }
181
+
182
+ static create(
183
+ width: number,
184
+ height: number,
185
+ drawFn: (ctx: CanvasRenderingContext2D, w: number, h: number) => void
186
+ ): MockBitmap {
187
+ const c = document.createElement('canvas');
188
+ c.width = width;
189
+ c.height = height;
190
+ drawFn(c.getContext('2d')!, width, height);
191
+ return new MockBitmap(c);
192
+ }
193
+
194
+ static from(
195
+ buffer: Uint8Array,
196
+ ) {
197
+ const c = document.createElement('canvas');
198
+ drawToCanvas(buffer, c);
199
+ return new MockBitmap(c);
200
+ }
201
+ }
202
+
203
+ export class MockPoco {
204
+ readonly canvas: HTMLCanvasElement;
205
+ readonly ctx: CanvasRenderingContext2D;
206
+ readonly width: number;
207
+ readonly height: number;
208
+
209
+ private _stack: StackEntry[];
210
+ private _originX: number;
211
+ private _originY: number;
212
+ private _clipX: number;
213
+ private _clipY: number;
214
+ private _clipW: number;
215
+ private _clipH: number;
216
+
217
+ constructor(canvas: HTMLCanvasElement) {
218
+ this.canvas = canvas;
219
+ this.ctx = canvas.getContext('2d')!;
220
+ this.width = canvas.width;
221
+ this.height = canvas.height;
222
+ this._stack = [];
223
+ this._originX = 0;
224
+ this._originY = 0;
225
+ this._clipX = 0;
226
+ this._clipY = 0;
227
+ this._clipW = this.width;
228
+ this._clipH = this.height;
229
+ }
230
+
231
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
232
+
233
+ begin(x = 0, y = 0, w = this.width, h = this.height): void {
234
+ this._originX = 0;
235
+ this._originY = 0;
236
+ this._clipX = x;
237
+ this._clipY = y;
238
+ this._clipW = w;
239
+ this._clipH = h;
240
+ this._stack = [];
241
+ this.ctx.save();
242
+ this.ctx.beginPath();
243
+ this.ctx.rect(x, y, w, h);
244
+ this.ctx.clip();
245
+ }
246
+
247
+ end(): void {
248
+ this.ctx.restore();
249
+ }
250
+
251
+ close(): void { }
252
+
253
+ // ── Color ──────────────────────────────────────────────────────────────────
254
+
255
+ makeColor(r: number, g: number, b: number): string {
256
+ return `rgb(${r},${g},${b})`;
257
+ }
258
+
259
+ // ── Clip & Origin ──────────────────────────────────────────────────────────
260
+
261
+ clip(): void;
262
+ clip(x: number, y: number, w: number, h: number): true | undefined;
263
+ clip(x?: number, y?: number, w?: number, h?: number): true | undefined | void {
264
+ if (x === undefined) {
265
+ const prev = this._stack.pop();
266
+ if (prev && prev.type === 'clip') {
267
+ this._clipX = prev.x;
268
+ this._clipY = prev.y;
269
+ this._clipW = prev.w;
270
+ this._clipH = prev.h;
271
+ this._applyClip();
272
+ }
273
+ return;
274
+ }
275
+
276
+ this._stack.push({ type: 'clip', x: this._clipX, y: this._clipY, w: this._clipW, h: this._clipH });
277
+
278
+ const ax = this._originX + x;
279
+ const ay = this._originY + (y ?? 0);
280
+ const bx = Math.max(this._clipX, ax);
281
+ const by = Math.max(this._clipY, ay);
282
+ const bx2 = Math.min(this._clipX + this._clipW, ax + (w ?? 0));
283
+ const by2 = Math.min(this._clipY + this._clipH, ay + (h ?? 0));
284
+
285
+ this._clipX = bx;
286
+ this._clipY = by;
287
+ this._clipW = Math.max(0, bx2 - bx);
288
+ this._clipH = Math.max(0, by2 - by);
289
+
290
+ this._applyClip();
291
+ return this._clipW > 0 && this._clipH > 0 ? true : undefined;
292
+ }
293
+
294
+ private _applyClip(): void {
295
+ this.ctx.restore();
296
+ this.ctx.save();
297
+ this.ctx.beginPath();
298
+ this.ctx.rect(this._clipX, this._clipY, this._clipW, this._clipH);
299
+ this.ctx.clip();
300
+ }
301
+
302
+ origin(): void;
303
+ origin(x: number, y: number): void;
304
+ origin(x?: number, y?: number): void {
305
+ if (x === undefined) {
306
+ const prev = this._stack.pop();
307
+ if (prev && prev.type === 'origin') {
308
+ this._originX = prev.x;
309
+ this._originY = prev.y;
310
+ }
311
+ return;
312
+ }
313
+ this._stack.push({ type: 'origin', x: this._originX, y: this._originY });
314
+ this._originX += x;
315
+ this._originY += (y ?? 0);
316
+ }
317
+
318
+ // ── Drawing ────────────────────────────────────────────────────────────────
319
+
320
+ fillRectangle(color: string, x: number, y: number, w: number, h: number): void {
321
+ this.ctx.fillStyle = color;
322
+ this.ctx.fillRect(this._originX + x, this._originY + y, w, h);
323
+ }
324
+
325
+ blendRectangle(color: string, blend: number, x: number, y: number, w: number, h: number): void {
326
+ this.ctx.save();
327
+ this.ctx.globalAlpha = blend / 255;
328
+ this.ctx.fillStyle = color;
329
+ this.ctx.fillRect(this._originX + x, this._originY + y, w, h);
330
+ this.ctx.restore();
331
+ }
332
+
333
+ drawPixel(color: string, x: number, y: number): void {
334
+ this.ctx.fillStyle = color;
335
+ this.ctx.fillRect(this._originX + x, this._originY + y, 1, 1);
336
+ }
337
+
338
+ drawBitmap(bits: MockBitmap, x: number, y: number, sx = 0, sy = 0, sw?: number, sh?: number): void {
339
+ if (!bits) return;
340
+ sw = sw ?? bits.width;
341
+ sh = sh ?? bits.height;
342
+ this.ctx.drawImage(bits._canvas, sx, sy, sw, sh, this._originX + x, this._originY + y, sw, sh);
343
+ }
344
+
345
+ drawMonochrome(
346
+ bits: MockBitmap,
347
+ fore: string | undefined,
348
+ back: string | undefined,
349
+ x: number,
350
+ y: number,
351
+ sx = 0,
352
+ sy = 0,
353
+ sw?: number,
354
+ sh?: number
355
+ ): void {
356
+ if (!bits) return;
357
+ sw = sw ?? bits.width;
358
+ sh = sh ?? bits.height;
359
+
360
+ const src = bits._imageData;
361
+ const data = src.data;
362
+ const stride = src.width * 4;
363
+
364
+ const tmp = document.createElement('canvas');
365
+ tmp.width = sw;
366
+ tmp.height = sh;
367
+ const tctx = tmp.getContext('2d')!;
368
+ const out = tctx.createImageData(sw, sh);
369
+ const od = out.data;
370
+
371
+ for (let row = 0; row < sh; row++) {
372
+ for (let col = 0; col < sw; col++) {
373
+ const si = (sy + row) * stride + (sx + col) * 4;
374
+ const bright = data[si] > 127;
375
+ const color = bright ? fore : back;
376
+ if (!color) continue;
377
+ const [r, g, b] = parseCSSColor(color);
378
+ const di = (row * sw + col) * 4;
379
+ od[di] = r; od[di + 1] = g; od[di + 2] = b; od[di + 3] = 255;
380
+ }
381
+ }
382
+ tctx.putImageData(out, 0, 0);
383
+ this.ctx.drawImage(tmp, this._originX + x, this._originY + y);
384
+ }
385
+
386
+ drawGray(
387
+ bits: MockBitmap,
388
+ color: string,
389
+ x: number,
390
+ y: number,
391
+ sx = 0,
392
+ sy = 0,
393
+ sw?: number,
394
+ sh?: number,
395
+ blend = 255
396
+ ): void {
397
+ if (!bits) return;
398
+ sw = sw ?? bits.width;
399
+ sh = sh ?? bits.height;
400
+
401
+ const src = bits._imageData;
402
+ const data = src.data;
403
+ const stride = src.width * 4;
404
+ const [r, g, b] = parseCSSColor(color);
405
+ const blendF = blend / 255;
406
+
407
+ const tmp = document.createElement('canvas');
408
+ tmp.width = sw;
409
+ tmp.height = sh;
410
+ const tctx = tmp.getContext('2d')!;
411
+ const out = tctx.createImageData(sw, sh);
412
+ const od = out.data;
413
+
414
+ for (let row = 0; row < sh; row++) {
415
+ for (let col = 0; col < sw; col++) {
416
+ const si = (sy + row) * stride + (sx + col) * 4;
417
+ const alpha = (data[si] / 255) * blendF;
418
+ const di = (row * sw + col) * 4;
419
+ od[di] = r; od[di + 1] = g; od[di + 2] = b;
420
+ od[di + 3] = Math.round(alpha * 255);
421
+ }
422
+ }
423
+ tctx.putImageData(out, 0, 0);
424
+ this.ctx.drawImage(tmp, this._originX + x, this._originY + y);
425
+ }
426
+
427
+ drawMasked(
428
+ bits: MockBitmap,
429
+ x: number,
430
+ y: number,
431
+ sx: number,
432
+ sy: number,
433
+ sw: number,
434
+ sh: number,
435
+ mask: MockBitmap,
436
+ mask_sx: number,
437
+ mask_sy: number,
438
+ blend = 255
439
+ ): void {
440
+ if (!bits || !mask) return;
441
+
442
+ const imgData = bits._imageData;
443
+ const maskData = mask._imageData;
444
+ const iStride = imgData.width * 4;
445
+ const mStride = maskData.width * 4;
446
+ const blendF = blend / 255;
447
+
448
+ const tmp = document.createElement('canvas');
449
+ tmp.width = sw;
450
+ tmp.height = sh;
451
+ const tctx = tmp.getContext('2d')!;
452
+ const out = tctx.createImageData(sw, sh);
453
+ const od = out.data;
454
+ const id = imgData.data;
455
+ const md = maskData.data;
456
+
457
+ for (let row = 0; row < sh; row++) {
458
+ for (let col = 0; col < sw; col++) {
459
+ const ii = (sy + row) * iStride + (sx + col) * 4;
460
+ const mi = (mask_sy + row) * mStride + (mask_sx + col) * 4;
461
+ const alpha = (md[mi] / 255) * blendF;
462
+ const di = (row * sw + col) * 4;
463
+ od[di] = id[ii]; od[di + 1] = id[ii + 1]; od[di + 2] = id[ii + 2];
464
+ od[di + 3] = Math.round(alpha * 255);
465
+ }
466
+ }
467
+ tctx.putImageData(out, 0, 0);
468
+ this.ctx.drawImage(tmp, this._originX + x, this._originY + y);
469
+ }
470
+
471
+ fillPattern(bits: MockBitmap, x: number, y: number, w: number, h: number, sx = 0, sy = 0, sw?: number, sh?: number): void {
472
+ if (!bits) return;
473
+ sw = sw ?? bits.width;
474
+ sh = sh ?? bits.height;
475
+
476
+ const pat = document.createElement('canvas');
477
+ pat.width = sw;
478
+ pat.height = sh;
479
+ pat.getContext('2d')!.drawImage(bits._canvas, sx, sy, sw, sh, 0, 0, sw, sh);
480
+
481
+ const pattern = this.ctx.createPattern(pat, 'repeat')!;
482
+ this.ctx.save();
483
+ this.ctx.translate(this._originX + x, this._originY + y);
484
+ this.ctx.fillStyle = pattern;
485
+ this.ctx.fillRect(0, 0, w, h);
486
+ this.ctx.restore();
487
+ }
488
+
489
+ // ── Text ───────────────────────────────────────────────────────────────────
490
+
491
+ getTextWidth(text: string, font: MockFont): number {
492
+ let width = 0;
493
+ let lastId = -1;
494
+
495
+ for (const ch of [...text]) {
496
+ const id = ch.codePointAt(0)!;
497
+ const fntChar = font.charMap.get(id);
498
+ if (!fntChar) { lastId = -1; continue; }
499
+
500
+ if (lastId >= 0) {
501
+ const kern = font.kerningMap.get(`${lastId},${id}`);
502
+ if (kern) width += kern;
503
+ }
504
+
505
+ width += fntChar.xadvance;
506
+ lastId = id;
507
+ }
508
+
509
+ return width;
510
+ }
511
+
512
+ drawText(text: string, font: MockFont, color: string, x: number, y: number, width?: number): void {
513
+ if (width !== undefined) {
514
+ const totalW = this.getTextWidth(text, font);
515
+ if (totalW > width) {
516
+ const ellipsis = '…';
517
+ const ellW = this.getTextWidth(ellipsis, font);
518
+ let truncated = '';
519
+ let w = 0;
520
+ for (const ch of [...text]) {
521
+ const id = ch.codePointAt(0)!;
522
+ const fntChar = font.charMap.get(id);
523
+ const cw = fntChar ? fntChar.xadvance : 0;
524
+ if (w + cw + ellW > width) break;
525
+ truncated += ch;
526
+ w += cw;
527
+ }
528
+ text = truncated + ellipsis;
529
+ }
530
+ }
531
+
532
+ let cx = this._originX + x;
533
+ const cy = this._originY + y;
534
+ let lastId = -1;
535
+
536
+ for (const ch of [...text]) {
537
+ const id = ch.codePointAt(0)!;
538
+ const fntChar = font.charMap.get(id);
539
+ if (!fntChar) { lastId = -1; continue; }
540
+
541
+ if (lastId >= 0) {
542
+ const kern = font.kerningMap.get(`${lastId},${id}`);
543
+ if (kern) cx += kern;
544
+ }
545
+
546
+ this._drawFntChar(fntChar, font, color, cx, cy);
547
+ cx += fntChar.xadvance;
548
+ lastId = id;
549
+ }
550
+ }
551
+
552
+ private _drawFntChar(char: FntChar, font: MockFont, color: string, x: number, y: number): void {
553
+ if (char.width === 0 || char.height === 0) return;
554
+
555
+ const tmp = document.createElement('canvas');
556
+ tmp.width = char.width;
557
+ tmp.height = char.height;
558
+ const tmpCtx = tmp.getContext('2d')!;
559
+ tmpCtx.drawImage(font.atlas, char.x, char.y, char.width, char.height, 0, 0, char.width, char.height);
560
+
561
+ if (font.isGrayAtlas) {
562
+ // Grayscale atlas: luminance was converted to alpha; colorize with the requested color.
563
+ tmpCtx.globalCompositeOperation = 'source-in';
564
+ tmpCtx.fillStyle = color;
565
+ tmpCtx.fillRect(0, 0, char.width, char.height);
566
+ }
567
+ // Color atlas: draw raw atlas pixels as-is; text color is not applied.
568
+
569
+ this.ctx.drawImage(tmp, x + char.xoffset, y + char.yoffset);
570
+ }
571
+
572
+ // ── Frame ──────────────────────────────────────────────────────────────────
573
+
574
+ drawFrame(frame: unknown, dictionary: { width?: number; height?: number }, x: number, y: number): void {
575
+ const w = dictionary.width ?? 40;
576
+ const h = dictionary.height ?? 40;
577
+ this.ctx.save();
578
+ this.ctx.strokeStyle = 'rgb(0,0,0)';
579
+ this.ctx.lineWidth = 1;
580
+ this.ctx.strokeRect(this._originX + x + 0.5, this._originY + y + 0.5, w - 1, h - 1);
581
+ this.ctx.fillStyle = 'rgb(200,200,200)';
582
+ this.ctx.fillRect(this._originX + x + 1, this._originY + y + 1, w - 2, h - 2);
583
+ this.ctx.fillStyle = 'rgb(0,0,0)';
584
+ this.ctx.font = '10px myFont';
585
+ this.ctx.textBaseline = 'middle';
586
+ this.ctx.fillText('frame', this._originX + x + 4, this._originY + y + h / 2);
587
+ this.ctx.restore();
588
+ }
589
+ }
590
+
591
+ // PNG and BMP are both handled in resourceBMP
592
+ const resourcesBMP = new Map<string, MockBitmap>();
593
+ const resourcesBMF = new Map<string, FntFont>();
594
+
595
+ export class MockResource {
596
+ static setBMP(fileName: string, bitmap: MockBitmap) {
597
+ if (!(fileName.endsWith('.bmp') || fileName.endsWith('.png'))) {
598
+ throw new Error(`setBMP only accepts ".png" and ".bmp"`);
599
+ }
600
+ // Scripts always reference with `.bmp` extension
601
+ resourcesBMP.set(fileName.replace('.png', '.bmp'), bitmap);
602
+ }
603
+
604
+ static setBMF(fileName: string, buffer: ArrayBuffer | Uint8Array<ArrayBufferLike>) {
605
+ if (!fileName.endsWith('.fnt')) {
606
+ throw new Error(`setBMP only accepts ".fnt"`);
607
+ }
608
+ resourcesBMF.set(fileName, readFnt(buffer));
609
+ }
610
+
611
+ // if you've already stored the json
612
+ static setBMFJSON(fileName: string, json: FntFont) {
613
+ if (!fileName.endsWith('.fnt')) {
614
+ throw new Error(`setBMP only accepts ".fnt"`);
615
+ }
616
+ resourcesBMF.set(fileName, json);
617
+ }
618
+
619
+ #file;
620
+ constructor(fileName: string) {
621
+ if (!(resourcesBMP.has(fileName) || resourcesBMF.has(fileName))) {
622
+ throw new Error(`Unknown mock resource "${fileName}". Add with Resource.set(fileName, MockBitmap.create()).`);
623
+ }
624
+ this.#file = fileName;
625
+ }
626
+
627
+ get file() {
628
+ return this.#file;
629
+ }
630
+ }
631
+
632
+ export function MockParseBMP(buffer: MockResource) {
633
+ // this is a mock, so lookup stored file key
634
+ return resourcesBMP.get(buffer.file);
635
+ }
636
+
637
+ export function MockParseBMF(buffer: MockResource) {
638
+ // this is a mock, so lookup stored file key
639
+ return MockFont.from(resourcesBMF.get(buffer.file)!);
640
+ }
@@ -0,0 +1,7 @@
1
+ :host {
2
+ display: contents;
3
+ }
4
+
5
+ [part="canvas"] {
6
+ image-rendering: pixelated;
7
+ }
@@ -0,0 +1 @@
1
+ <canvas part="canvas"></canvas>