@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.
- package/package.json +1 -1
- package/pg/autocompleteItem/__examples__/basic/basic.html +3 -0
- package/pg/autocompleteItem/autocompleteItem.css +55 -0
- package/pg/autocompleteItem/autocompleteItem.html +1 -0
- package/pg/autocompleteItem/autocompleteItem.ts +65 -0
- package/pg/button/button.css +5 -0
- package/pg/buttonMenu/buttonMenu.ts +1 -1
- package/pg/inputCombobox/__examples__/basic/basic.ts +33 -0
- package/pg/inputCombobox/inputCombobox.ts +158 -0
- package/pg/inputCombobox/inputCombobox.ts.tmp.44252.751480cb81cc +158 -0
- package/pg/inputCombobox/inputCombobox.ts.tmp.44252.dcea847c40a8 +156 -0
- package/pg/inputNumber/inputNumber.ts +31 -13
- package/pg/inputPill/inputPill.html +6 -0
- package/pg/inputPill/inputPill.html.tmp.26852.84ac63f4c46f +6 -0
- package/pg/inputPill/inputPill.ts +41 -0
- package/pg/inputPills/__examples__/basic/basic.html +9 -0
- package/pg/inputPills/__examples__/basic/basic.html.tmp.26852.4771e4cad5b7 +9 -0
- package/pg/inputPills/__examples__/basic/basic.ts.tmp.26852.d8fe41df18bd +0 -0
- package/pg/inputPixelEditor/README.md +104 -69
- package/pg/inputPixelEditor/README.md.tmp.42596.658de5f0a6f5 +0 -0
- package/pg/inputPixelEditor/__examples__/basic/basic.ts +6 -5
- package/pg/inputPixelEditor/inputPixelEditor.ts +95 -23
- package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.38516.96529dd82670 +1703 -0
- package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.42596.48ecc3ad9c72 +1724 -0
- package/pg/inputPixelEditor/utils/bitmapToMask.ts +113 -79
- package/pg/inputPixelEditor/utils/getLassoPixels.ts +51 -0
- package/pg/inputRange/inputRange.css +7 -0
- package/pg/inputSelect/__examples__/basic/basic.ts +2 -2
- package/pg/inputSelect/inputSelect.css +1 -1
- package/pg/inputSelect/inputSelect.ts +62 -5
- package/pg/inputText/README.md +6 -6
- package/pg/inputText/__examples__/basic/basic.ts +1 -1
- package/pg/inputText/inputText.css +2 -2
- package/pg/inputText copy/README.md +38 -0
- package/pg/inputText copy/inputText.css +41 -0
- package/pg/inputText copy/inputText.html +1 -0
- package/pg/inputText copy/inputText.spec.ts +59 -0
- package/pg/inputText copy/inputText.ts +67 -0
- package/pg/inputTextarea/README.md +39 -0
- package/pg/inputTextarea/__examples__/basic/basic.html +9 -0
- package/pg/inputTextarea/__examples__/basic/basic.ts +30 -0
- package/pg/inputTextarea/inputTextarea.ts +121 -0
- package/pg/inputTextarea copy/README.md +39 -0
- package/pg/inputTextarea copy/inputTextarea.css +41 -0
- package/pg/inputTextarea copy/inputTextarea.html +1 -0
- package/pg/inputTextarea copy/inputTextarea.spec.ts +59 -0
- package/pg/inputTextarea copy/inputTextarea.ts +79 -0
- package/pg/inputTextareaAutocomplete/README.md +70 -0
- package/pg/inputTextareaAutocomplete/__examples__/basic/basic.ts +67 -0
- package/pg/inputTextareaAutocomplete/inputTextareaAutocomplete.ts +267 -0
- package/pg/node/README.md +35 -0
- package/pg/node/__examples__/basic/basic.ts +34 -0
- package/pg/node/index.ts +3 -0
- package/pg/node/node.css +53 -0
- package/pg/node/node.html +5 -0
- package/pg/node/node.ts +270 -0
- package/pg/nodeEditorLink/nodeEditorLink.css +69 -0
- package/pg/nodeEditorLink/nodeEditorLink.css.tmp.1408.09e961fd0dcc +69 -0
- package/pg/nodeEditorLink/nodeEditorLink.html +9 -0
- package/pg/nodeEditorNumber/nodeEditorNumber.css +59 -0
- package/pg/nodeEditorNumber/nodeEditorNumber.ts +57 -0
- package/pg/nodeEditorRange/nodeEditorRange.css +23 -0
- package/pg/nodeEditorRange/nodeEditorRange.ts +69 -0
- package/pg/nodeEditorText/__examples__/basic/basic.html +9 -0
- package/pg/nodeEditorText/__examples__/basic/basic.ts +30 -0
- package/pg/nodeEditorText/nodeEditorText.css +17 -0
- package/pg/nodeEditorText/nodeEditorText.html +4 -0
- package/pg/nodeEditorText/nodeEditorText.ts +57 -0
- package/pg/nodeEditorText copy/README.md +35 -0
- package/pg/nodeEditorText copy/nodeEditorText.css +49 -0
- package/pg/nodeEditorText copy/nodeEditorText.html +4 -0
- package/pg/nodeEditorText copy/nodeEditorText.ts +50 -0
- package/pg/nodeEditorTextArray/nodeEditorTextArray.css +68 -0
- package/pg/nodeEditorTextArray/nodeEditorTextArray.html +9 -0
- package/pg/nodeEditorTextArray/nodeEditorTextArray.ts +162 -0
- package/pg/nodeEntry/nodeEntry.css +61 -0
- package/pg/nodeEntry/nodeEntry.ts +164 -0
- package/pg/nodeEntry/nodeEntry.ts.tmp.28724.dda67762bca5 +164 -0
- package/pg/nodeInputText/nodeInputText.css +75 -0
- package/pg/nodeInputText/nodeInputText.html +4 -0
- package/pg/nodeInputText/nodeInputText.ts +105 -0
- package/pg/nodeOutput/nodeOutput.css +32 -0
- package/pg/nodeResize/README.md +3 -0
- package/pg/nodeResize/THINKING.md +81 -0
- package/pg/nodeResize/THINKING.md.tmp.24812.99a70ba81062 +81 -0
- package/pg/nodeResize/basic/basic.ts +17 -0
- package/pg/nodeResize/dragUtil.ts +105 -0
- package/pg/nodeResize/dragUtil.ts.tmp.34372.787464c73309 +75 -0
- package/pg/nodeResize/nodeResize.css +187 -0
- package/pg/nodeResize/nodeResize.html +9 -0
- package/pg/nodeResize/nodeResize.ts +156 -0
- package/pg/nodes/README.md +177 -0
- package/pg/nodes/__examples__/basic/basic.html +33 -0
- package/pg/nodes/__examples__/basic/basic.ts +716 -0
- package/pg/nodes/__examples__/basic/exampleNodes.ts +129 -0
- package/pg/nodes/__examples__/basic/exampleScripts.ts +129 -0
- package/pg/nodes/__examples__/basic/keystore.ts +38 -0
- package/pg/nodes/executeInSandbox.ts +32 -0
- package/pg/nodes/index.ts +3 -0
- package/pg/nodes/nodes.css +69 -0
- package/pg/nodes/nodes.html +6 -0
- package/pg/nodes/nodes.ts +1240 -0
- package/pg/nodes copy/nodes.css +41 -0
- package/pg/nodes copy/nodes.html +1 -0
- package/pg/overlayContextMenu/__examples__/filter/filter.ts +87 -0
- package/pg/overlayContextMenu/overlayContextMenu.css +14 -0
- package/pg/overlayContextMenu/overlayContextMenu.html +4 -3
- package/pg/overlayContextMenu/overlayContextMenu.ts +6 -2
- package/pg/overlayContextMenu/overlayContextMenu.ts.tmp.37088.530ff1c0a321 +0 -0
- package/pg/overlayMenu/overlayMenu.css +3 -1
- package/pg/overlayMenu/overlayMenu.ts +11 -2
- package/pg/overlayMenu/overlayMenu.ts.tmp.44252.9bb22fd59ede +80 -0
- package/pg/overlaySelectMenu/overlaySelectMenu.css +2 -0
- package/pg/overlaySubMenu/overlaySubMenu.css +8 -6
- package/pg/poco/README.md +22 -0
- package/pg/poco/__examples__/basic/basic.html +11 -0
- package/pg/poco/__examples__/basic/basic.ts +95 -0
- package/pg/poco/__examples__/basic/examples.ts +258 -0
- package/pg/poco/__examples__/basic/myFont.ts +1300 -0
- package/pg/poco/index.ts +3 -0
- package/pg/poco/mockPoco.ts +640 -0
- package/pg/poco/poco.css +7 -0
- package/pg/poco/poco.html +1 -0
- package/pg/poco/poco.ts +75 -0
- package/pg/popover/README.md +47 -0
- package/pg/popover/__examples__/basic/basic.html +3 -0
- package/pg/popover/popover.html +3 -0
- package/pg/popover/popover.ts +44 -0
- package/pg/popoverAutocomplete/__examples__/basic/basic.html +4 -0
- package/pg/popoverAutocomplete/__examples__/basic/basic.ts +46 -0
- package/pg/popoverAutocomplete/popoverAutocomplete.css +25 -0
- package/pg/popoverAutocomplete/popoverAutocomplete.html +1 -0
- package/pg/popoverAutocomplete/popoverAutocomplete.ts +134 -0
- package/pg/popoverAutocompleteItem/popoverAutocompleteItem.css +69 -0
- package/pg/popoverAutocompleteItem/popoverAutocompleteItem.ts +90 -0
- package/pg/table/__examples__/basic/basic.ts +1 -0
- package/pg/tableCellFileLocal/tableCellFileLocal.html +3 -0
- package/pg/tableCellFileLocal/tableCellFileLocal.html.tmp.20484.68ee2e30710e +3 -0
- package/pg/tableCellFileLocal/tableCellFileLocal.ts +44 -0
- package/pg/tableCellSelect/tableCellSelect.html +3 -0
- package/pg/tableCellSelect/tableCellSelect.html.tmp.41808.99c1a6f14340 +3 -0
- package/pg/tableCellSelect/tableCellSelect.ts +51 -0
- package/pg/tree/__examples__/basic/basic.ts +12 -5
- package/pg/tree/tree.html +1 -1
- package/pg/tree/tree.ts +385 -227
- package/pg/tree/tree.ts.tmp.38376.07d376a04f41 +343 -0
- package/pg/tree/tree.ts.tmp.38376.2a04599ade9f +361 -0
- package/pg/tree/tree.ts.tmp.38376.6cb2c663d476 +0 -0
- package/pg/tree/tree.ts.tmp.38376.ac5af1aff2b0 +393 -0
- package/pg/tree/tree.ts.tmp.38376.ac9acfc4f95f +324 -0
- package/pg/treeItem/treeItem.ts +536 -558
- package/pg/treeItem/treeItem.ts.tmp.38376.145d3121d648 +530 -0
- package/pg/treeItem/treeItem.ts.tmp.38376.b5224d48e5c0 +0 -0
package/pg/poco/index.ts
ADDED
|
@@ -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
|
+
}
|
package/pg/poco/poco.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<canvas part="canvas"></canvas>
|