@pictogrammers/components 0.5.15 → 0.5.18

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 (149) 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/inputPill/inputPill.html +6 -0
  13. package/pg/inputPill/inputPill.html.tmp.26852.84ac63f4c46f +6 -0
  14. package/pg/inputPill/inputPill.ts +41 -0
  15. package/pg/inputPills/__examples__/basic/basic.html +9 -0
  16. package/pg/inputPills/__examples__/basic/basic.html.tmp.26852.4771e4cad5b7 +9 -0
  17. package/pg/inputPills/__examples__/basic/basic.ts.tmp.26852.d8fe41df18bd +0 -0
  18. package/pg/inputPixelEditor/README.md +104 -69
  19. package/pg/inputPixelEditor/README.md.tmp.42596.658de5f0a6f5 +0 -0
  20. package/pg/inputPixelEditor/__examples__/basic/basic.ts +6 -5
  21. package/pg/inputPixelEditor/inputPixelEditor.ts +96 -24
  22. package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.38516.96529dd82670 +1703 -0
  23. package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.42596.48ecc3ad9c72 +1724 -0
  24. package/pg/inputPixelEditor/utils/bitmapToMask.ts +113 -79
  25. package/pg/inputPixelEditor/utils/getLassoPixels.ts +51 -0
  26. package/pg/inputSelect/inputSelect.ts +5 -1
  27. package/pg/inputText/README.md +6 -6
  28. package/pg/inputText/__examples__/basic/basic.ts +1 -1
  29. package/pg/inputText/inputText.css +2 -2
  30. package/pg/inputText copy/README.md +38 -0
  31. package/pg/inputText copy/inputText.css +41 -0
  32. package/pg/inputText copy/inputText.html +1 -0
  33. package/pg/inputText copy/inputText.spec.ts +59 -0
  34. package/pg/inputText copy/inputText.ts +67 -0
  35. package/pg/inputTextarea/README.md +39 -0
  36. package/pg/inputTextarea/__examples__/basic/basic.html +9 -0
  37. package/pg/inputTextarea/__examples__/basic/basic.ts +30 -0
  38. package/pg/inputTextarea/inputTextarea.ts +121 -0
  39. package/pg/inputTextarea copy/README.md +39 -0
  40. package/pg/inputTextarea copy/inputTextarea.css +41 -0
  41. package/pg/inputTextarea copy/inputTextarea.html +1 -0
  42. package/pg/inputTextarea copy/inputTextarea.spec.ts +59 -0
  43. package/pg/inputTextarea copy/inputTextarea.ts +79 -0
  44. package/pg/inputTextareaAutocomplete/README.md +70 -0
  45. package/pg/inputTextareaAutocomplete/__examples__/basic/basic.ts +67 -0
  46. package/pg/inputTextareaAutocomplete/inputTextareaAutocomplete.ts +267 -0
  47. package/pg/node/README.md +35 -0
  48. package/pg/node/__examples__/basic/basic.ts +34 -0
  49. package/pg/node/index.ts +3 -0
  50. package/pg/node/node.css +53 -0
  51. package/pg/node/node.html +5 -0
  52. package/pg/node/node.ts +270 -0
  53. package/pg/nodeEditorLink/nodeEditorLink.css +69 -0
  54. package/pg/nodeEditorLink/nodeEditorLink.css.tmp.1408.09e961fd0dcc +69 -0
  55. package/pg/nodeEditorLink/nodeEditorLink.html +9 -0
  56. package/pg/nodeEditorNumber/nodeEditorNumber.css +59 -0
  57. package/pg/nodeEditorNumber/nodeEditorNumber.ts +57 -0
  58. package/pg/nodeEditorRange/nodeEditorRange.css +23 -0
  59. package/pg/nodeEditorRange/nodeEditorRange.ts +69 -0
  60. package/pg/nodeEditorText/__examples__/basic/basic.html +9 -0
  61. package/pg/nodeEditorText/__examples__/basic/basic.ts +30 -0
  62. package/pg/nodeEditorText/nodeEditorText.css +17 -0
  63. package/pg/nodeEditorText/nodeEditorText.html +4 -0
  64. package/pg/nodeEditorText/nodeEditorText.ts +57 -0
  65. package/pg/nodeEditorText copy/README.md +35 -0
  66. package/pg/nodeEditorText copy/nodeEditorText.css +49 -0
  67. package/pg/nodeEditorText copy/nodeEditorText.html +4 -0
  68. package/pg/nodeEditorText copy/nodeEditorText.ts +50 -0
  69. package/pg/nodeEditorTextArray/nodeEditorTextArray.css +68 -0
  70. package/pg/nodeEditorTextArray/nodeEditorTextArray.html +9 -0
  71. package/pg/nodeEditorTextArray/nodeEditorTextArray.ts +162 -0
  72. package/pg/nodeEntry/nodeEntry.css +61 -0
  73. package/pg/nodeEntry/nodeEntry.ts +164 -0
  74. package/pg/nodeEntry/nodeEntry.ts.tmp.28724.dda67762bca5 +164 -0
  75. package/pg/nodeInputText/nodeInputText.css +75 -0
  76. package/pg/nodeInputText/nodeInputText.html +4 -0
  77. package/pg/nodeInputText/nodeInputText.ts +105 -0
  78. package/pg/nodeOutput/nodeOutput.css +32 -0
  79. package/pg/nodeResize/README.md +3 -0
  80. package/pg/nodeResize/THINKING.md +81 -0
  81. package/pg/nodeResize/THINKING.md.tmp.24812.99a70ba81062 +81 -0
  82. package/pg/nodeResize/basic/basic.ts +17 -0
  83. package/pg/nodeResize/dragUtil.ts +105 -0
  84. package/pg/nodeResize/dragUtil.ts.tmp.34372.787464c73309 +75 -0
  85. package/pg/nodeResize/nodeResize.css +187 -0
  86. package/pg/nodeResize/nodeResize.html +9 -0
  87. package/pg/nodeResize/nodeResize.ts +156 -0
  88. package/pg/nodes/README.md +177 -0
  89. package/pg/nodes/__examples__/basic/basic.html +33 -0
  90. package/pg/nodes/__examples__/basic/basic.ts +716 -0
  91. package/pg/nodes/__examples__/basic/exampleNodes.ts +129 -0
  92. package/pg/nodes/__examples__/basic/exampleScripts.ts +129 -0
  93. package/pg/nodes/__examples__/basic/keystore.ts +38 -0
  94. package/pg/nodes/executeInSandbox.ts +32 -0
  95. package/pg/nodes/index.ts +3 -0
  96. package/pg/nodes/nodes.css +69 -0
  97. package/pg/nodes/nodes.html +6 -0
  98. package/pg/nodes/nodes.ts +1240 -0
  99. package/pg/nodes copy/nodes.css +41 -0
  100. package/pg/nodes copy/nodes.html +1 -0
  101. package/pg/overlayContextMenu/__examples__/filter/filter.ts +87 -0
  102. package/pg/overlayContextMenu/overlayContextMenu.css +14 -0
  103. package/pg/overlayContextMenu/overlayContextMenu.html +4 -3
  104. package/pg/overlayContextMenu/overlayContextMenu.ts +6 -2
  105. package/pg/overlayContextMenu/overlayContextMenu.ts.tmp.37088.530ff1c0a321 +0 -0
  106. package/pg/overlayMenu/overlayMenu.css +3 -1
  107. package/pg/overlayMenu/overlayMenu.ts +11 -2
  108. package/pg/overlayMenu/overlayMenu.ts.tmp.44252.9bb22fd59ede +80 -0
  109. package/pg/overlaySelectMenu/overlaySelectMenu.css +2 -0
  110. package/pg/overlaySubMenu/overlaySubMenu.css +8 -6
  111. package/pg/poco/README.md +22 -0
  112. package/pg/poco/__examples__/basic/basic.html +11 -0
  113. package/pg/poco/__examples__/basic/basic.ts +95 -0
  114. package/pg/poco/__examples__/basic/examples.ts +258 -0
  115. package/pg/poco/__examples__/basic/myFont.ts +1300 -0
  116. package/pg/poco/index.ts +3 -0
  117. package/pg/poco/mockPoco.ts +640 -0
  118. package/pg/poco/poco.css +7 -0
  119. package/pg/poco/poco.html +1 -0
  120. package/pg/poco/poco.ts +75 -0
  121. package/pg/popover/README.md +47 -0
  122. package/pg/popover/__examples__/basic/basic.html +3 -0
  123. package/pg/popover/popover.html +3 -0
  124. package/pg/popover/popover.ts +44 -0
  125. package/pg/popoverAutocomplete/__examples__/basic/basic.html +4 -0
  126. package/pg/popoverAutocomplete/__examples__/basic/basic.ts +46 -0
  127. package/pg/popoverAutocomplete/popoverAutocomplete.css +25 -0
  128. package/pg/popoverAutocomplete/popoverAutocomplete.html +1 -0
  129. package/pg/popoverAutocomplete/popoverAutocomplete.ts +134 -0
  130. package/pg/popoverAutocompleteItem/popoverAutocompleteItem.css +69 -0
  131. package/pg/popoverAutocompleteItem/popoverAutocompleteItem.ts +90 -0
  132. package/pg/table/__examples__/basic/basic.ts +1 -0
  133. package/pg/tableCellFileLocal/tableCellFileLocal.html +3 -0
  134. package/pg/tableCellFileLocal/tableCellFileLocal.html.tmp.20484.68ee2e30710e +3 -0
  135. package/pg/tableCellFileLocal/tableCellFileLocal.ts +44 -0
  136. package/pg/tableCellSelect/tableCellSelect.html +3 -0
  137. package/pg/tableCellSelect/tableCellSelect.html.tmp.41808.99c1a6f14340 +3 -0
  138. package/pg/tableCellSelect/tableCellSelect.ts +51 -0
  139. package/pg/tree/__examples__/basic/basic.ts +12 -5
  140. package/pg/tree/tree.html +1 -1
  141. package/pg/tree/tree.ts +385 -227
  142. package/pg/tree/tree.ts.tmp.38376.07d376a04f41 +343 -0
  143. package/pg/tree/tree.ts.tmp.38376.2a04599ade9f +361 -0
  144. package/pg/tree/tree.ts.tmp.38376.6cb2c663d476 +0 -0
  145. package/pg/tree/tree.ts.tmp.38376.ac5af1aff2b0 +393 -0
  146. package/pg/tree/tree.ts.tmp.38376.ac9acfc4f95f +324 -0
  147. package/pg/treeItem/treeItem.ts +536 -558
  148. package/pg/treeItem/treeItem.ts.tmp.38376.145d3121d648 +530 -0
  149. package/pg/treeItem/treeItem.ts.tmp.38376.b5224d48e5c0 +0 -0
@@ -0,0 +1,1724 @@
1
+ import { Component, Prop, Part, normalizeInt, normalizeBoolean } from '@pictogrammers/element';
2
+
3
+ import template from './inputPixelEditor.html';
4
+ import style from './inputPixelEditor.css';
5
+ import { InputMode } from './utils/inputMode';
6
+ import cloneGrid from './utils/cloneGrid';
7
+ import getEllipsePixels from './utils/getEllipsePixels';
8
+ import getEllipseOutlinePixels from './utils/getEllipseOutlinePixels';
9
+ import { WHITE, Pixel } from './utils/constants';
10
+ import getLinePixels from './utils/getLinePixels';
11
+ import getRectanglePixels from './utils/getRectanglePixels';
12
+ import getLassoPixels from './utils/getLassoPixels';
13
+ import getRectangleOutlinePixels from './utils/getRectangleOutlinePixels';
14
+ import fillGrid from './utils/fillGrid';
15
+ import iterateGrid from './utils/interateGrid';
16
+ import bitmaskToPath from './utils/bitmapToMask';
17
+ import createLayer from './utils/createLayer';
18
+ import diffGrid from './utils/diffGrid';
19
+ import { getGuides } from './utils/getGuides';
20
+ import { getOutline } from './utils/getOutline';
21
+ import { getGridColorIndexes } from './utils/getGridColorIndexes';
22
+ import { getFloodFill } from './utils/getFloodFill';
23
+ import { readMetadata, textEncode, writeMetadata } from './utils/pngMetadata';
24
+ import { canvasToPngBuffer } from './utils/canvasToPngBuffer';
25
+ import { blobToImage } from './utils/blobToImage';
26
+ import { diffLeftMapPixels } from './utils/diffMap';
27
+ import { maskToBitmap } from './utils/maskToBitmap';
28
+
29
+ type Color = [number, number, number, number];
30
+
31
+ export enum HistoryType {
32
+ Group,
33
+ Pixel,
34
+ ColorUpdate,
35
+ ColorAdd,
36
+ ColorRemove,
37
+ LayerAdd,
38
+ LayerRemove,
39
+ LayerName,
40
+ LayerLock,
41
+ LayerUnlock,
42
+ LayerExport,
43
+ LayerVisible,
44
+ LayerOpacity
45
+ }
46
+
47
+ type HistoryGroupType = {
48
+ name: string
49
+ }
50
+
51
+ type HistoryPixelType = {
52
+ pixels: [number, number, number][],
53
+ layer: number
54
+ }
55
+
56
+ type HistoryColorUpdateType = {
57
+ color: Color,
58
+ index: number
59
+ }
60
+
61
+ type HistoryItem = {
62
+ type: HistoryType
63
+ data: HistoryGroupType | HistoryPixelType | HistoryColorUpdateType
64
+ }
65
+
66
+ type History = HistoryItem[];
67
+
68
+ type Layer = {
69
+ name: string
70
+ type: 'pixel' | 'reference'
71
+ visible: boolean
72
+ locked: boolean
73
+ opacity: number
74
+ export: boolean
75
+ }
76
+
77
+ interface FileOptions {
78
+ history?: boolean
79
+ }
80
+
81
+ interface File {
82
+ width: number
83
+ height: number
84
+ transparent: boolean
85
+ colors: Color[]
86
+ layers: Layer[]
87
+ data: number[][][]
88
+ undo?: History[]
89
+ redo?: History[]
90
+ }
91
+
92
+ interface Export {
93
+ scale?: number
94
+ x?: number
95
+ y?: number
96
+ width?: number
97
+ height?: number
98
+ }
99
+
100
+ interface AddLayerOptions {
101
+ name: string
102
+ type: Layer['type']
103
+ exclude?: boolean
104
+ locked?: boolean
105
+ hidden?: boolean
106
+ opacity?: number
107
+ data?: any[]
108
+ }
109
+
110
+ interface GridItem {
111
+ value: number
112
+ color: number
113
+ }
114
+
115
+ interface Grid {
116
+ vertical: GridItem[]
117
+ horizontal: GridItem[]
118
+ }
119
+
120
+ function toColor([r, g, b, a]: Color) {
121
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
122
+ }
123
+
124
+ @Component({
125
+ selector: 'pg-input-pixel-editor',
126
+ style,
127
+ template
128
+ })
129
+ export default class PgInputPixelEditor extends HTMLElement {
130
+ @Prop(normalizeInt) width: number = 10;
131
+ @Prop(normalizeInt) height: number = 10;
132
+ @Prop(normalizeInt) size: number = 10;
133
+ @Prop(normalizeInt) gridSize: number = 1;
134
+ @Prop(normalizeBoolean) transparent: boolean = false;
135
+ @Prop() placeholder: string = '';
136
+
137
+ @Part() $wrapper: HTMLDivElement;
138
+ @Part() $canvas: HTMLCanvasElement;
139
+ @Part() $selection: SVGSVGElement;
140
+ @Part() $selectionPath: SVGPathElement;
141
+ @Part() $selectionPathPreview: SVGPathElement;
142
+
143
+ // Internal State
144
+ #inputStamp: number[][] = [];
145
+ #inputMode: InputMode = InputMode.Pixel;
146
+ #isPressed: boolean = false;
147
+ #isEditing: boolean = false;
148
+ #startColor: number = -1;
149
+ #startX: number = -1;
150
+ #startY: number = -1;
151
+ #startClientX: number = -1;
152
+ #startClientY: number = -1;
153
+ #lassoPath: [number, number][] = [];
154
+ #lassoOutlinePixels: Set<string> = new Set();
155
+ #lassoOutlineList: { x: number; y: number }[] = [];
156
+ #x: number = -1;
157
+ #y: number = -1;
158
+ #layer: number[] = [0];
159
+ #layers: Layer[] = [];
160
+ #isCtrl: boolean = false;
161
+ #isShift: boolean = false;
162
+ #isAlt: boolean = false;
163
+ #data: number[][][] = [];
164
+ #selectionPreview: number[][] = [];
165
+ #selectionPixels = new Map<string, number[]>(); // 'x,y', [x, y]
166
+ #selection: number[][] = [];
167
+ #export: number[][] = [];
168
+ #grid: Grid = { vertical: [], horizontal: [] };
169
+ #undoHistory: History[] = [];
170
+ #redoHistory: History[] = [];
171
+ #context: CanvasRenderingContext2D;
172
+ #color: number = 1;
173
+ #colors: Color[] = [
174
+ [0, 0, 0, 0],
175
+ [0, 0, 0, 1],
176
+ [255, 0, 255, 1]
177
+ ];
178
+ #baseLayer: HTMLCanvasElement;
179
+ #baseLayerContext: CanvasRenderingContext2D;
180
+ #editLayer: HTMLCanvasElement;
181
+ #editLayerContext: CanvasRenderingContext2D;
182
+ #noEditLayer: HTMLCanvasElement;
183
+ #noEditLayerContext: CanvasRenderingContext2D;
184
+ #previewLayer: HTMLCanvasElement;
185
+ #previewLayerContext: CanvasRenderingContext2D;
186
+
187
+ connectedCallback() {
188
+ // Init
189
+ const context = this.$canvas.getContext('2d');
190
+ if (context === null) { return; }
191
+ this.#context = context;
192
+ // Wire Up Events
193
+ this.$canvas.addEventListener(
194
+ 'contextmenu',
195
+ this.handleContextMenu.bind(this)
196
+ );
197
+ this.$canvas.addEventListener(
198
+ 'doubleclick',
199
+ this.handleDoubleClick.bind(this)
200
+ );
201
+ this.$canvas.addEventListener(
202
+ 'pointerdown',
203
+ this.handlePointerDown.bind(this)
204
+ );
205
+ this.$canvas.addEventListener(
206
+ 'pointerenter',
207
+ this.handlePointerEnter.bind(this)
208
+ );
209
+ this.$canvas.addEventListener(
210
+ 'pointerleave',
211
+ this.handlePointerLeave.bind(this)
212
+ );
213
+ this.$wrapper.addEventListener(
214
+ 'focus',
215
+ this.handleFocus.bind(this)
216
+ );
217
+ this.$wrapper.addEventListener(
218
+ 'blur',
219
+ this.handleBlur.bind(this)
220
+ );
221
+ this.$wrapper.addEventListener(
222
+ 'keypress',
223
+ this.handleKeyPress.bind(this)
224
+ );
225
+ this.$wrapper.addEventListener(
226
+ 'keydown',
227
+ this.handleKeyDown.bind(this)
228
+ );
229
+ this.$wrapper.addEventListener(
230
+ 'keyup',
231
+ this.handleKeyUp.bind(this)
232
+ );
233
+ this.$wrapper.addEventListener('paste', async (e: any) => {
234
+ e.preventDefault();
235
+ const clipboardContents = await navigator.clipboard.read();
236
+ for (const item of clipboardContents) {
237
+ if (item.types.includes("image/png")) {
238
+ const pngBlob = await item.getType('image/png');
239
+ console.log('read', pngBlob.size);
240
+ const arrayBuffer = await pngBlob.arrayBuffer();
241
+ console.log(readMetadata(new Uint8Array(arrayBuffer)));
242
+ }
243
+ if (item.types.includes('web application/easel+json')) {
244
+ const textBlob = await item.getType('web application/easel+json');
245
+ const text = await textBlob.text();
246
+ console.log('Text content:', text);
247
+ }
248
+ }
249
+ });
250
+ }
251
+
252
+ render(changes) {
253
+ if (changes.width || changes.height || changes.size || changes.transparent) {
254
+ this.#init();
255
+ }
256
+ }
257
+
258
+ #reset = true;
259
+ #init() {
260
+ const totalSize = this.size + this.gridSize;
261
+ const actualWidth = this.width * totalSize - this.gridSize;
262
+ const actualHeight = this.height * totalSize - this.gridSize;
263
+ this.$canvas.width = actualWidth;
264
+ this.$canvas.height = actualHeight;
265
+ this.#context.clearRect(0, 0, actualWidth, actualHeight);
266
+ [this.#baseLayer, this.#baseLayerContext] = createLayer(actualWidth, actualHeight);
267
+ [this.#editLayer, this.#editLayerContext] = createLayer(actualWidth, actualHeight);
268
+ [this.#noEditLayer, this.#noEditLayerContext] = createLayer(actualWidth, actualHeight);
269
+ [this.#previewLayer, this.#previewLayerContext] = createLayer(actualWidth, actualHeight);
270
+ if (this.transparent) {
271
+ for (let y = 0; y < this.height; y++) {
272
+ for (let x = 0; x < this.width; x++) {
273
+ this.#baseLayerContext.fillStyle = WHITE;
274
+ this.#baseLayerContext.fillRect(x * totalSize, y * totalSize, this.size + 1, this.size + 1);
275
+ this.#baseLayerContext.fillStyle = '#DDD';
276
+ this.#baseLayerContext.fillRect(x * totalSize + Math.ceil(this.size / 2), y * totalSize, Math.floor(this.size / 2), Math.floor(this.size / 2));
277
+ this.#baseLayerContext.fillRect(x * totalSize, y * totalSize + Math.floor(this.size / 2), Math.ceil(this.size / 2), Math.ceil(this.size / 2));
278
+ }
279
+ }
280
+ } else {
281
+ this.#baseLayerContext.clearRect(0, 0, actualWidth, actualHeight);
282
+ }
283
+ this.$selection.setAttribute('viewBox', `0 0 ${this.width * this.size} ${this.height * this.size}`);
284
+ if (this.#reset) {
285
+ this.#layer = [0];
286
+ this.#layers = [{
287
+ name: 'Layer 1',
288
+ type: 'pixel',
289
+ export: true,
290
+ locked: false,
291
+ visible: true,
292
+ opacity: 1
293
+ }];
294
+ this.#data = [fillGrid(this.width, this.height)];
295
+ this.#export = fillGrid(this.width, this.height);
296
+ this.#selectionPreview = fillGrid(this.width, this.height);
297
+ this.#selection = fillGrid(this.width, this.height);
298
+ this.#reset = false;
299
+ this.#undoHistory = [];
300
+ this.#redoHistory = [];
301
+ } else {
302
+ this.#redraw();
303
+ }
304
+ this.#updateGrid();
305
+ }
306
+
307
+ #redraw() {
308
+ // Render individual pixels
309
+ const data = this.#data.toReversed();
310
+ const layerCount = data.length;
311
+ for (let y = 0; y < this.height; y++) {
312
+ if (y >= data[0].length) {
313
+ for (let l = 0; l < layerCount; l++) {
314
+ data[l].push(new Array(this.width).fill(0));
315
+ this.#export.push(new Array(this.width).fill(0));
316
+ this.#selection.push(new Array(this.width).fill(0));
317
+ this.#selectionPreview.push(new Array(this.width).fill(0));
318
+ }
319
+ }
320
+ for (let x = 0; x < this.width; x++) {
321
+ if (x >= data[0][y].length) {
322
+ for (let l = 0; l < layerCount; l++) {
323
+ data[l][y].push(0);
324
+ this.#export[y].push(0);
325
+ this.#selection[y].push(0);
326
+ this.#selectionPreview[y].push(0);
327
+ }
328
+ }
329
+ for (let l = 0; l < layerCount; l++) {
330
+ if (data[l][y][x] !== 0) {
331
+ this.#setPixel(x, y, data[l][y][x]);
332
+ break;
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+
339
+ #handleChange() {
340
+ // Due to perf, don't compute anything here
341
+ // use this.$input.get... methods
342
+ this.dispatchEvent(new CustomEvent('change', {
343
+ detail: { export: this.#export }
344
+ }));
345
+ };
346
+
347
+ #delayTimerId: number = 0;
348
+ #delayedChange() {
349
+ clearInterval(this.#delayTimerId);
350
+ this.#delayTimerId = window.setTimeout(this.#handleChange.bind(this), 1000);
351
+ };
352
+
353
+ #previousPreview: Pixel[] = [];
354
+ #setSelectionPreview(pixels: Pixel[]) {
355
+ // Undo previous selection
356
+ this.#previousPreview.forEach(({x, y}) => {
357
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) { return; }
358
+ this.#selectionPreview[y][x] = 0;
359
+ });
360
+ this.#previousPreview = pixels;
361
+ pixels.forEach(({ x, y }) => {
362
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) { return; }
363
+ this.#selectionPreview[y][x] = 1;
364
+ });
365
+ this.$selectionPathPreview.classList.toggle('hide', false);
366
+ this.$selectionPathPreview.setAttribute('d', bitmaskToPath(this.#selectionPreview, { scale: this.size })[0]);
367
+ }
368
+
369
+ #clearSelectionPreview() {
370
+ this.$selectionPathPreview.classList.toggle('hide', true);
371
+ this.#previousPreview.forEach(({x, y}) => {
372
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) { return; }
373
+ this.#selectionPreview[y][x] = 0;
374
+ });
375
+ }
376
+
377
+ clearSelection() {
378
+ this.#selectionPixels.forEach(([x, y]) => {
379
+ this.#selection[y][x] = 0;
380
+ });
381
+ this.#selectionPixels.clear();
382
+ this.$selectionPath.classList.toggle('hide', true);
383
+ }
384
+
385
+ hasSelection() {
386
+ return this.#selectionPixels.size !== 0;
387
+ }
388
+
389
+ /**
390
+ * Get a JSON contents of the selected region
391
+ */
392
+ getSelection() {
393
+
394
+ }
395
+
396
+ #setSelectionPixel(x: number, y: number) {
397
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) { return; }
398
+ this.#selectionPixels.set(`${x},${y}`, [x, y]);
399
+ this.#selection[y][x] = 1;
400
+ }
401
+
402
+ #setPixel(x: number, y: number, color: number, layers = this.#layer) {
403
+ if (x >= this.width || x < 0) {
404
+ return;
405
+ // throw new Error(`Invalid x; ${x} > ${this.width} or ${x} < 0`);
406
+ }
407
+ if (y >= this.height || y < 0) {
408
+ return;
409
+ // throw new Error(`Invalid y; ${y} > ${this.height} or ${x} < 0`);
410
+ }
411
+ const totalSize = this.size + this.gridSize;
412
+ // Edit Layer
413
+ this.#context.clearRect(x * totalSize, y * totalSize, this.size, this.size);
414
+ this.#editLayerContext.clearRect(x * totalSize, y * totalSize, this.size, this.size);
415
+ this.#noEditLayerContext.clearRect(x * totalSize, y * totalSize, this.size, this.size);
416
+ // Edit layer
417
+ if (this.#colors[color][3] !== 0) {
418
+ this.#editLayerContext.fillStyle = WHITE;
419
+ this.#editLayerContext.fillRect(
420
+ x * totalSize - (this.gridSize) + 1,
421
+ y * totalSize - (this.gridSize) + 1,
422
+ this.size + (this.gridSize * 2) - 2,
423
+ this.size + (this.gridSize * 2) - 2
424
+ );
425
+ this.#editLayerContext.fillStyle = toColor(this.#colors[color]);
426
+ this.#editLayerContext.fillRect(x * totalSize + 1, y * totalSize + 1, this.size - 2, this.size - 2);
427
+ }
428
+ // No Edit layer
429
+ if (this.#colors[color][3] !== 0) {
430
+ this.#noEditLayerContext.fillStyle = toColor(this.#colors[color]);
431
+ this.#noEditLayerContext.fillRect(x * totalSize, y * totalSize, this.size, this.size);
432
+ }
433
+ // base layer to main canvas
434
+ this.#context.drawImage(
435
+ this.#baseLayer,
436
+ x * totalSize, y * totalSize, this.size + 2, this.size + 2,
437
+ x * totalSize, y * totalSize, this.size + 2, this.size + 2
438
+ );
439
+ // editing layer to main canvas
440
+ this.#context.drawImage(
441
+ this.#isEditing ? this.#editLayer : this.#noEditLayer,
442
+ x * totalSize, y * totalSize, this.size + 2, this.size + 2,
443
+ x * totalSize, y * totalSize, this.size + 2, this.size + 2
444
+ );
445
+ // Verify this is the only place setting pixel data!
446
+ layers.forEach((layer) => {
447
+ this.#data[layer][y][x] = color;
448
+ });
449
+ this.#updateExport(x, y);
450
+ this.#delayedChange();
451
+ }
452
+
453
+ #setPixelAll() {
454
+ for (let y = 0; y < this.height; y++) {
455
+ for (let x = 0; x < this.width; x++) {
456
+ this.#setPixel(x, y, this.#data[this.#layer[0]][y][x]);
457
+ }
458
+ }
459
+ }
460
+
461
+ getExportLayerIndexes() {
462
+ return this.#layers.reduce((arr: any, layer: any, index: number) => {
463
+ if (layer.export) {
464
+ arr.push(index);
465
+ }
466
+ return arr;
467
+ }, []);
468
+ }
469
+
470
+ /**
471
+ * Update cached export grid for performance.
472
+ * @param x X
473
+ * @param y Y
474
+ */
475
+ #updateExport(x: number, y: number) {
476
+ let color = 0;
477
+ let layers = this.getExportLayerIndexes();
478
+ for (let i = layers.length - 1; i >= 0; i--) {
479
+ const layer = layers[i];
480
+ if (this.#data[layer][y][x] !== 0) {
481
+ color = this.#data[layer][y][x];
482
+ break;
483
+ }
484
+ }
485
+ this.#export[y][x] = color;
486
+ }
487
+
488
+ #setPreview(pixels: Pixel[], previousX: number, previousY: number) {
489
+ const totalSize = this.size + this.gridSize;
490
+ const actualWidth = this.width * totalSize - this.gridSize;
491
+ const actualHeight = this.height * totalSize - this.gridSize;
492
+ const { minX, maxX, minY, maxY } = pixels.reduce((previous, current) => {
493
+ return {
494
+ minX: Math.min(previous.minX, current.x, previousX),
495
+ maxX: Math.max(previous.maxX, current.x, previousX),
496
+ minY: Math.min(previous.minY, current.y, previousY),
497
+ maxY: Math.max(previous.maxY, current.y, previousY)
498
+ };
499
+ }, { minX: this.width, maxX: 0, minY: this.height, maxY: 0 });
500
+ const x = minX * totalSize;
501
+ const y = minY * totalSize;
502
+ const width = (maxX - minX + 1) * totalSize;
503
+ const height = (maxY - minY + 1) * totalSize;
504
+ this.#context.clearRect(x, y, width, height);
505
+ // base layer to main canvas
506
+ this.#context.drawImage(
507
+ this.#baseLayer,
508
+ x, y, width, height,
509
+ x, y, width, height
510
+ );
511
+ // edit to main canvas
512
+ this.#context.drawImage(
513
+ this.#editLayer,
514
+ x, y, width, height,
515
+ x, y, width, height
516
+ );
517
+ // preview layer
518
+ this.#previewLayerContext.clearRect(0, 0, actualWidth, actualHeight);
519
+ pixels.forEach(({ x, y }) => {
520
+ this.#previewLayerContext.fillStyle = WHITE;
521
+ this.#previewLayerContext.beginPath();
522
+ this.#previewLayerContext.arc(x * totalSize + 5, y * totalSize + 5, 3, 0, 2 * Math.PI);
523
+ this.#previewLayerContext.closePath();
524
+ this.#previewLayerContext.fill();
525
+ this.#previewLayerContext.fillStyle = '#1B79C8';
526
+ this.#previewLayerContext.beginPath();
527
+ this.#previewLayerContext.arc(x * totalSize + 5, y * totalSize + 5, 2, 0, 2 * Math.PI);
528
+ this.#previewLayerContext.closePath();
529
+ this.#previewLayerContext.fill();
530
+ });
531
+ // preview layer to main canvas
532
+ this.#context.drawImage(
533
+ this.#previewLayer,
534
+ x, y, width, height,
535
+ x, y, width, height
536
+ );
537
+ // Debug
538
+ this.dispatchEvent(new CustomEvent('debug', {
539
+ detail: {
540
+ x,
541
+ y,
542
+ width,
543
+ height,
544
+ canvas: this.$canvas,
545
+ context: this.#context,
546
+ editLayer: this.#editLayer,
547
+ noEditLayer: this.#noEditLayer,
548
+ baseLayer: this.#baseLayer,
549
+ previewLayer: this.#previewLayer
550
+ }
551
+ }));
552
+ }
553
+
554
+ #previousPreviewRect = { x: 0, y: 0, width: 1, height: 1 };
555
+ #clearStampPreview() {
556
+ const { x, y, width, height } = this.#previousPreviewRect;
557
+ this.#context.clearRect(x, y, width, height);
558
+ // base layer to main canvas
559
+ this.#context.drawImage(
560
+ this.#baseLayer,
561
+ x, y, width, height,
562
+ x, y, width, height
563
+ );
564
+ // edit to main canvas
565
+ this.#context.drawImage(
566
+ this.#noEditLayer,
567
+ x, y, width, height,
568
+ x, y, width, height
569
+ );
570
+ }
571
+
572
+ #setStampPreview(pixels: Pixel[], centerX: number, centerY: number, previousX: number, previousY: number) {
573
+ const totalSize = this.size + this.gridSize;
574
+ const { minX, maxX, minY, maxY } = pixels.reduce((previous, current) => {
575
+ return {
576
+ minX: Math.min(previous.minX, current.x, previousX),
577
+ maxX: Math.max(previous.maxX, current.x, previousX),
578
+ minY: Math.min(previous.minY, current.y, previousY),
579
+ maxY: Math.max(previous.maxY, current.y, previousY)
580
+ };
581
+ }, { minX: this.width, maxX: 0, minY: this.height, maxY: 0 });
582
+ const offsetX = previousX - centerX;
583
+ const offsetY = previousY - centerY;
584
+ const x = offsetX < 0 ? (minX + offsetX) * totalSize : minX * totalSize;
585
+ const y = offsetY < 0 ? (minY + offsetY) * totalSize : minY * totalSize;
586
+ const width = (maxX - minX + 1 + Math.abs(offsetX)) * totalSize;
587
+ const height = (maxY - minY + 1 + Math.abs(offsetY)) * totalSize;
588
+ this.#context.clearRect(x, y, width, height);
589
+ // base layer to main canvas
590
+ this.#context.drawImage(
591
+ this.#baseLayer,
592
+ x, y, width, height,
593
+ x, y, width, height
594
+ );
595
+ // edit to main canvas
596
+ this.#context.drawImage(
597
+ this.#editLayer,
598
+ x, y, width, height,
599
+ x, y, width, height
600
+ );
601
+ // preview layer
602
+ this.#previewLayerContext.clearRect(x, y, width, height);
603
+ pixels.forEach(({ x, y }) => {
604
+ this.#previewLayerContext.fillStyle = '#1B79C8';
605
+ this.#previewLayerContext.fillRect(x * totalSize + 2, y * totalSize + 2, this.size - 4, this.size - 4);
606
+ });
607
+ this.#context.drawImage(
608
+ this.#previewLayer,
609
+ x, y, width, height,
610
+ x, y, width, height
611
+ );
612
+ // Store to clear
613
+ this.#previousPreviewRect = { x, y, width, height };
614
+ // Debug
615
+ this.dispatchEvent(new CustomEvent('debug', {
616
+ detail: {
617
+ x,
618
+ y,
619
+ width,
620
+ height,
621
+ canvas: this.$canvas,
622
+ context: this.#context,
623
+ editLayer: this.#editLayer,
624
+ noEditLayer: this.#noEditLayer,
625
+ baseLayer: this.#baseLayer,
626
+ previewLayer: this.#previewLayer
627
+ }
628
+ }));
629
+ }
630
+
631
+ async copyPngToClipboard(blob, data) {
632
+ try {
633
+ // Ensure the blob is of type image/png if necessary
634
+ // In many cases, the fetched blob's type is already correct
635
+ if (blob.type !== "image/png") {
636
+ console.error("Fetched resource is not a PNG image.");
637
+ // Optional: convert to PNG using a canvas if needed
638
+ // For conversion logic, see Stack Overflow links
639
+ return;
640
+ }
641
+
642
+ // 3. Create a new ClipboardItem with the Blob
643
+ const clipboardItem = new ClipboardItem({
644
+ [blob.type]: blob,
645
+ 'web application/easel+json': new Blob([JSON.stringify(data)], { type: 'web application/easel+json' }),
646
+ });
647
+
648
+ // 4. Write the ClipboardItem to the clipboard
649
+ await navigator.clipboard.write([clipboardItem]);
650
+
651
+ } catch (err) {
652
+ console.error('Failed to copy image: ', err.name, err.message);
653
+ }
654
+ }
655
+
656
+ handleFocus() {
657
+ if(!this.$wrapper.matches(':focus-visible')) {
658
+ this.$wrapper.classList.toggle('ignore', true);
659
+ }
660
+ }
661
+
662
+ handleBlur() {
663
+ this.$wrapper.classList.toggle('ignore', false);
664
+ }
665
+
666
+ handleKeyPress(event: KeyboardEvent) {
667
+ if (event.key === 'Delete') {
668
+ event.preventDefault();
669
+ }
670
+ }
671
+
672
+ async handleKeyDown(event: KeyboardEvent) {
673
+ this.#isShift = true;
674
+ switch (event.key) {
675
+ case ' ':
676
+ // stop page scroll
677
+ event.preventDefault();
678
+ break;
679
+ case 'Escape':
680
+ if (this.hasSelection()) {
681
+ this.clearSelection();
682
+ }
683
+ break;
684
+ case 'Delete':
685
+ this.#selectionPixels.forEach(([x, y]) => {
686
+ this.#setPixel(x, y, 0);
687
+ });
688
+ this.clearSelection();
689
+ break;
690
+ case 'ArrowLeft':
691
+ if (this.hasSelection()) {
692
+ this.moveSelection(event.shiftKey ? -10 : -1, 0);
693
+ event.preventDefault();
694
+ }
695
+ break;
696
+ case 'ArrowRight':
697
+ if (this.hasSelection()) {
698
+ this.moveSelection(event.shiftKey ? 10 : 1, 0);
699
+ event.preventDefault();
700
+ }
701
+ break;
702
+ case 'ArrowUp':
703
+ if (this.hasSelection()) {
704
+ this.moveSelection(0, event.shiftKey ? -10 : -1);
705
+ event.preventDefault();
706
+ }
707
+ break;
708
+ case 'ArrowDown':
709
+ if (this.hasSelection()) {
710
+ this.moveSelection(0, event.shiftKey ? 10 : 1);
711
+ event.preventDefault();
712
+ }
713
+ break;
714
+ }
715
+ if (event.ctrlKey) {
716
+ switch (event.key) {
717
+ case 'c':
718
+ if (this.hasSelection()) {
719
+ const image = await this.getSelectionPng();
720
+ this.copyPngToClipboard(image, { x: 2, y: 2 });
721
+ } else {
722
+ const image = await this.getExportPng();
723
+ this.copyPngToClipboard(image, {});
724
+ }
725
+ break;
726
+ case 'z':
727
+ if (event.shiftKey) {
728
+ // modern redo keybinding
729
+ this.redo();
730
+ } else {
731
+ this.undo();
732
+ }
733
+ break;
734
+ case 'y':
735
+ // legacy redo keybinding
736
+ this.redo();
737
+ break;
738
+ }
739
+ }
740
+ }
741
+
742
+ handleKeyUp(event: KeyboardEvent) {
743
+ this.#isShift = false;
744
+ }
745
+
746
+ handleContextMenu(event: MouseEvent) {
747
+ event?.preventDefault();
748
+ }
749
+
750
+ handleDoubleClick(event: MouseEvent) {
751
+ event?.preventDefault();
752
+ }
753
+
754
+ #handlePointerMoveCache;
755
+ #handlePointerUpCache;
756
+
757
+ handlePointerDown(event: MouseEvent) {
758
+ if (event.buttons !== 1 && event.buttons !== 32) {
759
+ event.preventDefault();
760
+ event.stopPropagation();
761
+ return;
762
+ }
763
+ // Update Modifiers
764
+ this.#isAlt = event.altKey;
765
+ this.#isCtrl = event.ctrlKey;
766
+ this.#isShift = event.shiftKey;
767
+ // Drawing
768
+ const rect = this.$canvas.getBoundingClientRect();
769
+ const totalSize = this.size + this.gridSize;
770
+ let newX = Math.floor((event.clientX - rect.left) / totalSize);
771
+ let newY = Math.floor((event.clientY - rect.top) / totalSize);
772
+ if (newX === this.#x && newY === this.#y) { return; }
773
+ if (newX >= this.width) { newX = this.width - 1; }
774
+ if (newY >= this.height) { newY = this.height - 1; }
775
+ this.#isPressed = true;
776
+ this.#startColor = this.#data[this.#layer[0]][newY][newX];
777
+ this.#startX = newX;
778
+ this.#startY = newY;
779
+ this.#x = newX;
780
+ this.#y = newY;
781
+ const color = event.buttons === 32 ? 0 : this.#color;
782
+ switch (this.#inputMode) {
783
+ case InputMode.Cursor:
784
+ const currentColor = this.getColorAt(newX, newY);
785
+ const index = this.getLayerAt(newX, newY);
786
+ this.dispatchEvent(new CustomEvent('selectlayer', {
787
+ detail: {
788
+ color: currentColor,
789
+ index
790
+ }
791
+ }));
792
+ break;
793
+ case InputMode.SelectLasso:
794
+ this.#lassoPath = [[newX, newY]];
795
+ this.#lassoOutlinePixels = new Set();
796
+ this.#lassoOutlineList = [];
797
+ break;
798
+ case InputMode.Pixel:
799
+ this.#setPixel(newX, newY, color);
800
+ break;
801
+ case InputMode.Stamp:
802
+ this.#inputStamp.forEach((point) => {
803
+ this.#setPixel(newX + point[0], newY + point[1], color);
804
+ });
805
+ break;
806
+ }
807
+ // track movement
808
+ this.#handlePointerMoveCache = this.handlePointerMove.bind(this);
809
+ document.addEventListener('pointermove', this.#handlePointerMoveCache);
810
+ // pointer outside
811
+ this.#handlePointerUpCache = this.handlePointerUp.bind(this);
812
+ document.addEventListener('pointerup', this.#handlePointerUpCache);
813
+ }
814
+
815
+ #pointerOutside = false;
816
+
817
+ cleanupPointerGlobal() {
818
+ document.removeEventListener('pointermove', this.#handlePointerMoveCache);
819
+ document.removeEventListener('pointerup', this.#handlePointerUpCache);
820
+ }
821
+
822
+ handlePointerUp(event: MouseEvent) {
823
+ const rect = this.$canvas.getBoundingClientRect();
824
+ const totalSize = this.size + this.gridSize;
825
+ let newX = Math.floor((event.clientX - rect.left) / totalSize);
826
+ let newY = Math.floor((event.clientY - rect.top) / totalSize);
827
+ if (newX >= this.width) { newX = this.width - 1; }
828
+ if (newY >= this.height) { newY = this.height - 1; }
829
+ if (newX < 0) { newX = 0; }
830
+ if (newY < 0) { newY = 0; }
831
+ //if (this.#startX === -1 && this.#startY === -1) {
832
+ // return;
833
+ //}
834
+ // Single Tap
835
+ if (newX === this.#startX && newY === this.#startY) {
836
+ switch (this.#inputMode) {
837
+ case InputMode.SelectMagicWand:
838
+ if (!event.shiftKey) {
839
+ this.clearSelection();
840
+ }
841
+ const color = this.#data[this.#layer[0]][newY][newX];
842
+ const pixels = getFloodFill(this.#data[this.#layer[0]], newX, newY, [color]);
843
+ pixels.forEach(([x, y]) => {
844
+ this.#setSelectionPixel(x, y);
845
+ });
846
+ this.$selectionPathPreview.classList.toggle('hide', true);
847
+ this.$selectionPath.classList.toggle('hide', false);
848
+ this.$selectionPath.setAttribute('d', bitmaskToPath(this.#selection, { scale: this.size })[0]);
849
+ break;
850
+ case InputMode.Pixel:
851
+ // Same as current color
852
+ if (this.#startColor === this.#color) {
853
+ this.#setPixel(newX, newY, 0);
854
+ }
855
+ break;
856
+ case InputMode.Stamp:
857
+ // Same as current color
858
+ if (this.#startColor === this.#color) {
859
+ this.#inputStamp.forEach((point) => {
860
+ this.#setPixel(newX + point[0], newY + point[1], 0);
861
+ });
862
+ }
863
+ break;
864
+ }
865
+ } else {
866
+ switch (this.#inputMode) {
867
+ case InputMode.SelectRectangle:
868
+ this.#clearSelectionPreview();
869
+ if (!event.shiftKey) {
870
+ this.clearSelection();
871
+ }
872
+ getRectanglePixels(this.#startX, this.#startY, newX, newY).forEach(({ x, y }) => {
873
+ this.#setSelectionPixel(x, y);
874
+ });
875
+ this.$selectionPathPreview.classList.toggle('hide', true);
876
+ this.$selectionPath.classList.toggle('hide', false);
877
+ this.$selectionPath.setAttribute('d', bitmaskToPath(this.#selection, { scale: this.size })[0]);
878
+ break;
879
+ case InputMode.SelectEllipse:
880
+ this.#clearSelectionPreview();
881
+ if (!event.shiftKey) {
882
+ this.clearSelection();
883
+ }
884
+ getEllipsePixels(this.#startX, this.#startY, newX, newY).forEach(({ x, y }) => {
885
+ this.#setSelectionPixel(x, y);
886
+ });
887
+ this.$selectionPathPreview.classList.toggle('hide', true);
888
+ this.$selectionPath.classList.toggle('hide', false);
889
+ this.$selectionPath.setAttribute('d', bitmaskToPath(this.#selection, { scale: this.size })[0]);
890
+ break;
891
+ case InputMode.SelectLasso:
892
+ this.#clearSelectionPreview();
893
+ if (!event.shiftKey) {
894
+ this.clearSelection();
895
+ }
896
+ getLassoPixels(this.#lassoPath).forEach(({ x, y }) => {
897
+ this.#setSelectionPixel(x, y);
898
+ });
899
+ this.$selectionPathPreview.classList.toggle('hide', true);
900
+ this.$selectionPath.classList.toggle('hide', false);
901
+ this.$selectionPath.setAttribute('d', bitmaskToPath(this.#selection, { scale: this.size })[0]);
902
+ break;
903
+ case InputMode.Line:
904
+ getLinePixels(this.#startX, this.#startY, newX, newY).forEach(({ x, y }) => {
905
+ this.#setPixel(x, y, 1);
906
+ });
907
+ break;
908
+ case InputMode.Rectangle:
909
+ getRectanglePixels(this.#startX, this.#startY, newX, newY).forEach(({ x, y }) => {
910
+ this.#setPixel(x, y, 1);
911
+ });
912
+ break;
913
+ case InputMode.RectangleOutline:
914
+ getRectangleOutlinePixels(this.#startX, this.#startY, newX, newY).forEach(({ x, y }) => {
915
+ this.#setPixel(x, y, 1);
916
+ });
917
+ break;
918
+ case InputMode.Ellipse:
919
+ getEllipsePixels(this.#startX, this.#startY, newX, newY).forEach(({ x, y }) => {
920
+ this.#setPixel(x, y, 1);
921
+ });
922
+ break;
923
+ case InputMode.EllipseOutline:
924
+ getEllipseOutlinePixels(this.#startX, this.#startY, newX, newY).forEach(({ x, y }) => {
925
+ this.#setPixel(x, y, 1);
926
+ });
927
+ break;
928
+ }
929
+ }
930
+ this.#x = -1;
931
+ this.#y = -1;
932
+ this.#isPressed = false;
933
+ this.cleanupPointerGlobal();
934
+ if (this.#pointerOutside) {
935
+ this.#isEditing = false;
936
+ // base layer to main canvas
937
+ this.#context.drawImage(this.#baseLayer, 0, 0);
938
+ // editing layer to main canvas
939
+ this.#context.drawImage(this.#isEditing ? this.#editLayer : this.#noEditLayer, 0, 0);
940
+ }
941
+ }
942
+
943
+ handlePointerMove(event: PointerEvent) {
944
+ const canvas = this.$canvas;
945
+ if (this.#isPressed) {
946
+ this.#isAlt = event.altKey;
947
+ this.#isCtrl = event.ctrlKey;
948
+ this.#isShift = event.shiftKey;
949
+ const data = this.#data;
950
+ const rect = canvas.getBoundingClientRect();
951
+ const totalSize = this.size + this.gridSize;
952
+ const points: [number, number][] = [];
953
+ const startX = this.#startX;
954
+ const startY = this.#startY;
955
+ const x = this.#x;
956
+ const y = this.#y;
957
+ // If supported get all the inbetween points
958
+ // really noticable for pen support + pencil tool
959
+ if (typeof event.getCoalescedEvents === 'function') {
960
+ const events = event.getCoalescedEvents();
961
+ for (const evt of events) {
962
+ let tX = Math.floor((evt.clientX - rect.left) / totalSize);
963
+ let tY = Math.floor((evt.clientY - rect.top) / totalSize);
964
+ if (tX === x && tY === y) {
965
+ continue;
966
+ }
967
+ points.push([tX, tY]);
968
+ }
969
+ } else {
970
+ let newX = Math.floor((event.clientX - rect.left) / totalSize);
971
+ let newY = Math.floor((event.clientY - rect.top) / totalSize);
972
+ if (newX === x && newY === y) { return; }
973
+ points.push([newX, newY]);
974
+ }
975
+ // Is Eraser
976
+ const color = event.buttons === 32 ? 0 : this.#color;
977
+ // Shape tools only care about the last point
978
+ if (points.length === 0) { return; }
979
+ const [lastX, lastY] = points.at(-1) as [number, number];
980
+ // This is not ideal, but might be good enough,
981
+ // really it should be finding the point furthest absolute
982
+ // point from startX/startY.
983
+ this.#x = lastX;
984
+ this.#y = lastY;
985
+ switch (this.#inputMode) {
986
+ case InputMode.SelectRectangle:
987
+ this.#setSelectionPreview(getRectanglePixels(startX, startY, lastX, lastY));
988
+ break;
989
+ case InputMode.SelectEllipse:
990
+ this.#setSelectionPreview(getEllipsePixels(startX, startY, lastX, lastY));
991
+ break;
992
+ case InputMode.SelectLasso: {
993
+ const [sx, sy] = this.#lassoPath[0];
994
+ for (const [px, py] of points) {
995
+ const [prevX, prevY] = this.#lassoPath.at(-1)!;
996
+ for (const { x: lx, y: ly } of getLinePixels(prevX, prevY, px, py)) {
997
+ const key = `${lx},${ly}`;
998
+ if (!this.#lassoOutlinePixels.has(key)) {
999
+ this.#lassoOutlinePixels.add(key);
1000
+ this.#lassoOutlineList.push({ x: lx, y: ly });
1001
+ }
1002
+ }
1003
+ this.#lassoPath.push([px, py]);
1004
+ }
1005
+ const [cx, cy] = this.#lassoPath.at(-1)!;
1006
+ const closing = getLinePixels(cx, cy, sx, sy)
1007
+ .filter(({ x: lx, y: ly }) => !this.#lassoOutlinePixels.has(`${lx},${ly}`));
1008
+ this.#setSelectionPreview([...this.#lassoOutlineList, ...closing]);
1009
+ break;
1010
+ }
1011
+ case InputMode.Pixel:
1012
+ for (var point of points) {
1013
+ this.#setPixel(point[0], point[1], color);
1014
+ }
1015
+ break;
1016
+ case InputMode.Stamp:
1017
+ for (var point of points) {
1018
+ this.#inputStamp.forEach((arr) => {
1019
+ this.#setPixel(point[0] + arr[0], point[1] + arr[1], color);
1020
+ });
1021
+ }
1022
+ break;
1023
+ case InputMode.Line:
1024
+ this.#setPreview(getLinePixels(startX, startY, lastX, lastY), x, y);
1025
+ break;
1026
+ case InputMode.Rectangle:
1027
+ this.#setPreview(getRectanglePixels(startX, startY, lastX, lastY), x, y);
1028
+ break;
1029
+ case InputMode.RectangleOutline:
1030
+ this.#setPreview(getRectangleOutlinePixels(startX, startY, lastX, lastY), x, y);
1031
+ break;
1032
+ case InputMode.Ellipse:
1033
+ this.#setPreview(getEllipsePixels(startX, startY, lastX, lastY), x, y);
1034
+ break;
1035
+ case InputMode.EllipseOutline:
1036
+ this.#setPreview(getEllipseOutlinePixels(startX, startY, lastX, lastY), x, y);
1037
+ break;
1038
+ }
1039
+ }
1040
+ }
1041
+
1042
+ handlePointerEnter(event: MouseEvent) {
1043
+ if (!this.#isPressed && !this.#isEditing) {
1044
+ this.#isEditing = true;
1045
+ // base layer to main canvas
1046
+ this.#context.drawImage(this.#baseLayer, 0, 0);
1047
+ // editing layer to main canvas
1048
+ this.#context.drawImage(this.#isEditing ? this.#editLayer : this.#noEditLayer, 0, 0);
1049
+ // track pointer movement
1050
+ if (this.#inputMode === InputMode.Stamp) {
1051
+ this.#handlePointerMovePreviewCache = this.handlePointerMovePreview.bind(this);
1052
+ this.$canvas.addEventListener('pointermove', this.#handlePointerMovePreviewCache);
1053
+ this.#handlePointerLeavePreviewCache = this.handlePointerLeavePreview.bind(this);
1054
+ this.$canvas.addEventListener('pointerleave', this.#handlePointerLeavePreviewCache);
1055
+ }
1056
+ }
1057
+ this.#pointerOutside = false;
1058
+ }
1059
+
1060
+ #moveX = -1;
1061
+ #moveY = -1;
1062
+ #handlePointerMovePreviewCache;
1063
+ handlePointerMovePreview(event: MouseEvent) {
1064
+ const rect = this.$canvas.getBoundingClientRect();
1065
+ const totalSize = this.size + this.gridSize;
1066
+ let newX = Math.floor((event.clientX - rect.left) / totalSize);
1067
+ let newY = Math.floor((event.clientY - rect.top) / totalSize);
1068
+ if (newX === this.#moveX && newY === this.#moveY) { return; }
1069
+ if (newX < 0 || newY < 0) { return; }
1070
+ this.#setStampPreview(
1071
+ this.#inputStamp.map(arr => ({ x: arr[0] + newX, y: arr[1] + newY })),
1072
+ newX,
1073
+ newY,
1074
+ this.#moveX === -1 ? newX : this.#moveX,
1075
+ this.#moveY === -1 ? newY : this.#moveY,
1076
+ );
1077
+ this.#moveX = newX;
1078
+ this.#moveY = newY;
1079
+ }
1080
+
1081
+ #handlePointerLeavePreviewCache;
1082
+ handlePointerLeavePreview() {
1083
+ this.#clearStampPreview();
1084
+ this.$canvas.removeEventListener('pointermove', this.#handlePointerMovePreviewCache);
1085
+ this.$canvas.removeEventListener('pointerleave', this.#handlePointerLeavePreviewCache);
1086
+ }
1087
+
1088
+ handlePointerLeave(event: MouseEvent) {
1089
+ if (!this.#isPressed) {
1090
+ this.#isEditing = false;
1091
+ // base layer to main canvas
1092
+ this.#context.drawImage(this.#baseLayer, 0, 0);
1093
+ // editing layer to main canvas
1094
+ this.#context.drawImage(this.#isEditing ? this.#editLayer : this.#noEditLayer, 0, 0);
1095
+ // remove preview tracking
1096
+ this.$canvas.removeEventListener('pointermove', this.#handlePointerMovePreviewCache);
1097
+ } else if (this.#isEditing) {
1098
+ this.#pointerOutside = true;
1099
+ }
1100
+ }
1101
+
1102
+ #updateGrid() {
1103
+ // base layer to main canvas
1104
+ this.#context.drawImage(this.#baseLayer, 0, 0);
1105
+ // editing layer to main canvas
1106
+ this.#context.drawImage(this.#noEditLayer, 0, 0);
1107
+ }
1108
+
1109
+ mergeColor(fromIndex: number, toIndex: number) {
1110
+ // ToDo: Code this
1111
+ }
1112
+
1113
+ /**
1114
+ * ToDo: Delete this method
1115
+ */
1116
+ clear() {
1117
+ const gridEmpty = fillGrid(this.width, this.height);
1118
+ const diff = diffGrid(this.#data[this.#layer[0]], gridEmpty);
1119
+ this.#data = [fillGrid(this.width, this.height)];
1120
+ this.#export = fillGrid(this.width, this.height);
1121
+ this.#selectionPreview = fillGrid(this.width, this.height);
1122
+ this.#selection = fillGrid(this.width, this.height);
1123
+ this.#updateGrid();
1124
+ }
1125
+
1126
+ reset() {
1127
+ this.#reset = true;
1128
+ this.#init();
1129
+ }
1130
+
1131
+ clearHistory() {
1132
+ this.#undoHistory = [];
1133
+ this.#redoHistory = [];
1134
+ }
1135
+
1136
+ getHistory() {
1137
+ return this.#undoHistory;
1138
+ }
1139
+
1140
+ applyTemplate(template: number[][]) {
1141
+ this.#data = [template];
1142
+ this.#setPixelAll();
1143
+ }
1144
+
1145
+ flipHorizontal() {
1146
+ const cloned = cloneGrid(this.#data[this.#layer[0]]);
1147
+ const w = cloned[0].length - 1;
1148
+ iterateGrid(this.#data[this.#layer[0]], (x, y) => {
1149
+ cloned[y][x] = this.#data[this.#layer[0]][y][w - x];
1150
+ });
1151
+ this.#data[this.#layer[0]] = cloned;
1152
+ }
1153
+
1154
+ flipVertical() {
1155
+ const cloned = cloneGrid(this.#data[this.#layer[0]]);
1156
+ const h = cloned.length - 1;
1157
+ iterateGrid(this.#data[this.#layer[0]], (x, y) => {
1158
+ cloned[y][x] = this.#data[this.#layer[0]][h - y][x];
1159
+ });
1160
+ this.#data[this.#layer[0]] = cloned;
1161
+ }
1162
+
1163
+ move(translateX: number, translateY: number) {
1164
+ const cloned = fillGrid(this.width, this.height);
1165
+ for (let iy = 0; iy < this.height; iy++) {
1166
+ cloned[iy].fill(0);
1167
+ }
1168
+ iterateGrid(this.#data[this.#layer[0]], (x, y) => {
1169
+ if (y - translateY < 0
1170
+ || x - translateX < 0
1171
+ || y - translateY >= this.height
1172
+ || x - translateX >= this.width) {
1173
+ return;
1174
+ }
1175
+ cloned[y][x] = this.#data[this.#layer[0]][y - translateY][x - translateX];
1176
+ });
1177
+ this.#data[this.#layer[0]] = cloned;
1178
+ }
1179
+
1180
+ invert() {
1181
+ // Only works with 2 colors
1182
+ if (this.#colors.length > 2) {
1183
+ return;
1184
+ }
1185
+ iterateGrid(this.#data[this.#layer[0]], (x, y) => {
1186
+ this.#data[this.#layer[0]][y][x] = this.#data[this.#layer[0]][y][x] === 0 ? 1 : 0;
1187
+ });
1188
+ this.#setPixelAll();
1189
+ }
1190
+
1191
+ applyGuides() {
1192
+ const guides = getGuides(this.width, this.height, this.size, this.gridSize);
1193
+ this.#baseLayerContext.drawImage(guides, 0, 0);
1194
+ }
1195
+
1196
+ clearGuides() {
1197
+
1198
+ }
1199
+
1200
+ undo() {
1201
+ // ToDo: Rewrite to use new history api
1202
+ const historyItems = this.#undoHistory.pop();
1203
+ if (!historyItems) { return; }
1204
+ this.#redoHistory.push(historyItems);
1205
+ historyItems.forEach((historyItem) => {
1206
+ switch (historyItem.type) {
1207
+ case HistoryType.Pixel:
1208
+ (historyItem.data as HistoryPixelType).pixels.forEach((item) => {
1209
+ const [x, y] = item;
1210
+ this.#data[this.#layer[0]][y][x] = item[2];
1211
+ // redraw canvas
1212
+ });
1213
+ break;
1214
+ }
1215
+ });
1216
+ }
1217
+
1218
+ redo() {
1219
+ // ToDo: Rewrite to use new history api
1220
+ /*const revert = this.#redoHistory.pop();
1221
+ if (!revert) { return; }
1222
+ this.#undoHistory.push(revert);
1223
+ revert?.forEach((item) => {
1224
+ const [x, y] = item;
1225
+ this.#data[y][x] = item[2];
1226
+ // redraw canvas
1227
+ });*/
1228
+ }
1229
+
1230
+ rotateClockwise() {
1231
+ this.#rotate(false);
1232
+ }
1233
+
1234
+ rotateCounterclockwise() {
1235
+ this.#rotate(true);
1236
+ }
1237
+
1238
+ #rotate(counterClockwise: boolean = false) {
1239
+ const cloned = cloneGrid(this.#data[this.#layer[0]]);
1240
+ if (counterClockwise) {
1241
+ const newData = this.#data[0].map((val, index) => this.#data.map(row => row[row.length - 1 - index]));
1242
+ for (let iy = 0; iy < this.height; iy++) {
1243
+ for (let ix = 0; ix < this.width; ix++) {
1244
+ cloned[iy][ix] = newData[this.#layer[0]][iy][ix];
1245
+ }
1246
+ }
1247
+ } else {
1248
+ const newData = this.#data[0].map((val, index) => this.#data.map(row => row[index]).reverse());
1249
+ for (let iy = 0; iy < this.height; iy++) {
1250
+ for (let ix = 0; ix < this.width; ix++) {
1251
+ cloned[iy][ix] = newData[this.#layer[0]][iy][ix];
1252
+ }
1253
+ }
1254
+ }
1255
+ this.#data[this.#layer[0]] = cloned;
1256
+ }
1257
+
1258
+ hasUndo() {
1259
+ return this.#undoHistory.length !== 0;
1260
+ }
1261
+
1262
+ hasRedo() {
1263
+ return this.#redoHistory.length !== 0;
1264
+ }
1265
+
1266
+ #inputModePixelSize = 1;
1267
+ inputModePixelSize(size = 1) {
1268
+ this.#inputModePixelSize = size;
1269
+ }
1270
+
1271
+ inputModeStamp(stamp: number[][]) {
1272
+ this.#inputStamp = stamp;
1273
+ this.#inputMode = InputMode.Stamp;
1274
+ // Clear previous pixel
1275
+ this.#clearStampPreview();
1276
+ }
1277
+
1278
+ inputModeCursor() {
1279
+ this.#inputMode = InputMode.Cursor;
1280
+ }
1281
+
1282
+ inputModeSelectRectangle() {
1283
+ this.#inputMode = InputMode.SelectRectangle;
1284
+ }
1285
+
1286
+ inputModeSelectEllipse() {
1287
+ this.#inputMode = InputMode.SelectEllipse;
1288
+ }
1289
+
1290
+ inputModeSelectLasso() {
1291
+ this.#inputMode = InputMode.SelectLasso;
1292
+ }
1293
+
1294
+ inputModeSelectMagicWand() {
1295
+ this.#inputMode = InputMode.SelectMagicWand;
1296
+ }
1297
+
1298
+ inputModePixel() {
1299
+ this.#inputMode = InputMode.Pixel;
1300
+ }
1301
+
1302
+ inputModeLine() {
1303
+ this.#inputMode = InputMode.Line;
1304
+ }
1305
+
1306
+ inputModeRectangle() {
1307
+ this.#inputMode = InputMode.Rectangle;
1308
+ }
1309
+
1310
+ inputModeRectangleOutline() {
1311
+ this.#inputMode = InputMode.RectangleOutline;
1312
+ }
1313
+
1314
+ inputModeEllipse() {
1315
+ this.#inputMode = InputMode.Ellipse;
1316
+ }
1317
+
1318
+ inputModeEllipseOutline() {
1319
+ this.#inputMode = InputMode.EllipseOutline;
1320
+ }
1321
+
1322
+ async getJson(options: FileOptions = {}): Promise<File> {
1323
+ const file: File = {
1324
+ width: this.width,
1325
+ height: this.height,
1326
+ transparent: this.transparent,
1327
+ colors: this.#colors,
1328
+ layers: this.#layers,
1329
+ data: this.#data
1330
+ };
1331
+ if (options.history === true) {
1332
+ file.undo = this.#undoHistory;
1333
+ file.redo = this.#redoHistory;
1334
+ }
1335
+ // Trim data
1336
+ for (let l = 0; l < file.data.length; l++) {
1337
+ for (let y = file.data[l].length - 1; y >= 0; y--) {
1338
+ if (y >= this.height) {
1339
+ file.data[l].pop();
1340
+ continue;
1341
+ }
1342
+ for (let x = file.data[l][y].length - 1; x >= 0; x--) {
1343
+ if (x >= this.width) {
1344
+ file.data[l][y].pop();
1345
+ }
1346
+ }
1347
+ }
1348
+ }
1349
+ // Output
1350
+ return file;
1351
+ }
1352
+
1353
+ async setJson(json: File) {
1354
+ if (typeof json !== 'object') {
1355
+ return ['json must be type object'];
1356
+ }
1357
+ const errors: string[] = [];
1358
+ // Validate 6 properties exist
1359
+ const keys = Object.keys(json);
1360
+ const required = ['width', 'height', 'transparent', 'colors', 'layers', 'data'];
1361
+ required.forEach((key) => {
1362
+ if (!keys.includes(key)) {
1363
+ errors.push(`JSON key '${key}' required.`);
1364
+ }
1365
+ });
1366
+ // Set props
1367
+ this.width = json.width;
1368
+ this.height = json.height;
1369
+ this.transparent = json.transparent;
1370
+ this.#colors = json.colors;
1371
+ this.#layers = json.layers;
1372
+ this.#data = json.data;
1373
+ if (json.undo) {
1374
+ this.#undoHistory = json.undo;
1375
+ }
1376
+ if (json.redo) {
1377
+ this.#redoHistory = json.redo;
1378
+ }
1379
+ this.#init();
1380
+ }
1381
+
1382
+ selectColor(index) {
1383
+ this.#color = index;
1384
+ }
1385
+
1386
+ getColors() {
1387
+ return this.#colors;
1388
+ }
1389
+
1390
+ addColor(r: number, g: number, b: number, a: number) {
1391
+ this.#colors.push([r, g, b, a]);
1392
+ }
1393
+
1394
+ removeColor(index) {
1395
+ this.#colors.splice(index, 1);
1396
+ }
1397
+
1398
+ getColor(index) {
1399
+ return this.#colors[index];
1400
+ }
1401
+
1402
+ setColor(index, r, g, b, a) {
1403
+ this.#colors[index] = [r, g, b, a];
1404
+ }
1405
+
1406
+ moveColor(startIndex, endIndex) {
1407
+
1408
+ }
1409
+
1410
+ getColorAt(x: number, y: number) {
1411
+ return this.#export[y][x];
1412
+ }
1413
+
1414
+ /**
1415
+ * Get layer index with first valid color.
1416
+ * @param x x
1417
+ * @param y y
1418
+ */
1419
+ getLayerAt(x: number, y: number) {
1420
+ for (let i = this.#data.length - 1; i >= 0; i--) {
1421
+ const layer = this.#data[i];
1422
+ if (layer[y][x] !== 0) {
1423
+ return i;
1424
+ }
1425
+ }
1426
+ return null;
1427
+ }
1428
+
1429
+ getLayers() {
1430
+ return this.#data.map((data, index) => index);
1431
+ }
1432
+
1433
+ selectLayers(indexes: number[]) {
1434
+ this.#layer = indexes;
1435
+ }
1436
+
1437
+ addLayer({ name, type, ...optional }: AddLayerOptions) {
1438
+ this.#data.push(fillGrid(this.width, this.height));
1439
+ this.#layers.push({
1440
+ name,
1441
+ type,
1442
+ export: optional.exclude ?? true,
1443
+ locked: optional.locked ?? false,
1444
+ visible: optional.hidden ?? true,
1445
+ opacity: optional.opacity ?? 1,
1446
+ });
1447
+ // short hand for initial data
1448
+ if (optional.data) {
1449
+ this.setData(optional.data);
1450
+ }
1451
+ }
1452
+
1453
+ /**
1454
+ * Remove layer.
1455
+ * @param index Layer index to remove
1456
+ */
1457
+ removeLayer(index) {
1458
+ this.#data.splice(index, 1);
1459
+ }
1460
+
1461
+ /**
1462
+ * Outline.
1463
+ */
1464
+ outline(include: number[] = []) {
1465
+ const pixels = getOutline(this.#data[this.#layer[0]], true, include);
1466
+ pixels.forEach(([x, y]) => {
1467
+ this.#setPixel(x, y, this.#color);
1468
+ });
1469
+ }
1470
+
1471
+ /**
1472
+ * Glow.
1473
+ */
1474
+ glow(include: number[] = []) {
1475
+ const pixels = getOutline(this.#data[this.#layer[0]], false, include);
1476
+ pixels.forEach(([x, y]) => {
1477
+ this.#setPixel(x, y, this.#color);
1478
+ });
1479
+ }
1480
+
1481
+ /**
1482
+ * Draw pixel.
1483
+ * @param grid 2d grid of colors
1484
+ * @param layer Layer to apply the pixel.
1485
+ */
1486
+ drawGrid(grid: number[][], layer = this.#layer) {
1487
+ grid.forEach((row, y) => {
1488
+ row.forEach((color, x) => {
1489
+ this.#setPixel(x, y, color, layer);
1490
+ });
1491
+ })
1492
+ }
1493
+
1494
+ /**
1495
+ * Draw a single pixel.
1496
+ */
1497
+ drawPixel(x: number, y: number, color: number, layer = this.#layer) {
1498
+ this.#setPixel(x, y, color, layer);
1499
+ }
1500
+
1501
+ // todo: add other draw commands
1502
+
1503
+ /**
1504
+ * Flatten layers, always merges to lowest layer
1505
+ * Note 0 is the lowest layer.
1506
+ */
1507
+ flattenLayers(indexes: number[]) {
1508
+ // ${x},${y} = [x, y, color]
1509
+ const changedPixels = new Map<string, number[]>();
1510
+ //this.#data[index]
1511
+ // Update preview
1512
+ }
1513
+
1514
+ getLayerColorIndexes(layerIndex = this.#layer[0]) {
1515
+ return getGridColorIndexes(this.#data[layerIndex]).sort();
1516
+ }
1517
+
1518
+ /**
1519
+ * Every unique color gets a path
1520
+ */
1521
+ #getLayerPaths() {
1522
+ return this.#data.map((layer, layerIndex) => {
1523
+ const colors = this.getLayerColorIndexes(layerIndex);
1524
+ const paths = bitmaskToPath(layer, {
1525
+ scale: 1,
1526
+ include: colors.map((color) => ([color])),
1527
+ });
1528
+ return colors.map((color, i) => {
1529
+ return [color, paths[i]];
1530
+ });
1531
+ });
1532
+ }
1533
+
1534
+ getLayerPaths() {
1535
+ return this.#getLayerPaths() as any;
1536
+ }
1537
+
1538
+ /**
1539
+ * Save schema for `data`.
1540
+ */
1541
+ getData() {
1542
+ return this.#layers.map((layer, layerIndex) => {
1543
+ switch (layer.type) {
1544
+ case 'pixel':
1545
+ const colors = this.getLayerColorIndexes(layerIndex);
1546
+ return colors.map((color) => {
1547
+ return {
1548
+ color,
1549
+ path: bitmaskToPath(this.#data[layerIndex], {
1550
+ scale: 1,
1551
+ include: [[color]]
1552
+ }),
1553
+ };
1554
+ });
1555
+ case 'reference':
1556
+ return { "id": "uuid", "position": [0, 0] }
1557
+ default:
1558
+ throw new Error('Not implemented');
1559
+ }
1560
+ });
1561
+ }
1562
+
1563
+ /**
1564
+ * Set data
1565
+ */
1566
+ setData(data: any[]) {
1567
+ data.forEach((layerData, layerIndex) => {
1568
+ const { type } = this.#layers[layerIndex];
1569
+ switch(type) {
1570
+ case 'pixel':
1571
+ const { color, path } = layerData;
1572
+ const temp = maskToBitmap(path, this.width, this.height);
1573
+ temp.forEach((tempY, y) => {
1574
+ tempY.forEach((tempX, x) => {
1575
+ if (tempX !== 0) {
1576
+ //this.#data[layerIndex][y][x] = color;
1577
+ this.#setPixel(x, y, color, [layerIndex]);
1578
+ }
1579
+ });
1580
+ });
1581
+ break;
1582
+ case 'reference':
1583
+ console.log('reference', layerData);
1584
+ break;
1585
+ default:
1586
+ throw new Error(`unknown type ${type}`);
1587
+ }
1588
+ });
1589
+ }
1590
+
1591
+ getExport() {
1592
+ return this.#export;
1593
+ }
1594
+
1595
+ getExportPath() {
1596
+ return bitmaskToPath(this.#export, { scale: 1 });
1597
+ }
1598
+
1599
+ /**
1600
+ * Include metadata.
1601
+ */
1602
+ getExportCanvas(options: Export = {}) {
1603
+ const canvas = document.createElement('canvas') as HTMLCanvasElement;
1604
+ const context = canvas.getContext('2d') as CanvasRenderingContext2D;
1605
+ const scale = options.scale ?? 1;
1606
+ const offsetX = options.x ?? 0;
1607
+ const offsetY = options.y ?? 0;
1608
+ canvas.width = (options.width ?? this.width) * scale;
1609
+ canvas.height = (options.height ?? this.height) * scale;
1610
+ const rows = options.height ?? this.#export.length;
1611
+ const columns = options.width ?? this.#export[0].length;
1612
+ for (let y = 0; y < rows; y++) {
1613
+ for (let x = 0; x < columns; x++) {
1614
+ const relativeX = x + offsetX;
1615
+ const relativeY = y + offsetY;
1616
+ const color = this.#export[relativeY][relativeX];
1617
+ const [ r, g, b, a ] = this.#colors[color];
1618
+ context.fillStyle = `rgba(${r},${g},${b},${a})`;
1619
+ context.fillRect(x * scale, y * scale, scale, scale);
1620
+ }
1621
+ }
1622
+ return canvas;
1623
+ }
1624
+
1625
+ async #getExportPng(options: Export = {}) {
1626
+ return new Promise<Blob>((resolve, reject) => {
1627
+ this.getExportCanvas(options).toBlob((blob) => {
1628
+ if (blob) {
1629
+ resolve(blob);
1630
+ } else {
1631
+ reject();
1632
+ }
1633
+ }, 'image/png');
1634
+ });
1635
+ }
1636
+
1637
+ async getExportPng(options: Export = {}, meta: any = null) {
1638
+ const blob = await this.#getExportPng(options);
1639
+ if (meta) {
1640
+ const arrayBuffer = await blob.arrayBuffer();
1641
+ const file = new Uint8Array(arrayBuffer);
1642
+ const data = { tEXt: meta };
1643
+ const blobWithMeta = writeMetadata(file, data);
1644
+ return blobWithMeta.buffer;
1645
+ } else {
1646
+ return blob;
1647
+ }
1648
+ }
1649
+
1650
+ async getSelectionPng(options: Export = {}, meta: any = null) {
1651
+ const xList: number[] = [];
1652
+ const yList: number[] = [];
1653
+ this.#selectionPixels.forEach(([x, y]) => {
1654
+ xList.push(x);
1655
+ yList.push(y);
1656
+ });
1657
+ let minX = Math.min(...xList),
1658
+ minY = Math.min(...yList),
1659
+ maxX = Math.max(...xList),
1660
+ maxY = Math.max(...yList);
1661
+ return await this.getExportPng({
1662
+ ...options,
1663
+ x: minX,
1664
+ y: minY,
1665
+ width: maxX - minX + 1,
1666
+ height: maxY - minY + 1,
1667
+ }, meta);
1668
+ }
1669
+
1670
+ /**
1671
+ * Move selection. Only move changed pixels.
1672
+ * @param x X
1673
+ * @param y Y
1674
+ */
1675
+ moveSelection(x: number, y: number) {
1676
+ const changes = new Map<string, number[]>();
1677
+ const newSelection = new Map<string, number[]>();
1678
+ this.#selectionPixels.forEach(([currentX, currentY]) => {
1679
+ const newX = currentX + x;
1680
+ const newY = currentY + y;
1681
+ if (newX < 0 || newX >= this.width || newY < 0 || newY >= this.height) { return; }
1682
+ this.#layer.forEach((layer) => {
1683
+ const color = this.#data[layer][currentY][currentX];
1684
+ const newColor = this.#data[layer][newY][newX];
1685
+ if (color !== 0) {
1686
+ if (color !== newColor) {
1687
+ changes.set(`${layer},${newX},${newY}`, [layer, newX, newY, color]);
1688
+ }
1689
+ newSelection.set(`${newX},${newY}`, [newX, newY]);
1690
+ }
1691
+ });
1692
+ });
1693
+ const clearSelection = diffLeftMapPixels(this.#selectionPixels, newSelection);
1694
+ // Move pixels
1695
+ changes.forEach(([layer, newX, newY, color]) => {
1696
+ this.#data[layer][newY][newX] = color;
1697
+ this.#setPixel(newX, newY, color, [layer]);
1698
+ this.#selectionPixels.set(`${newX},${newY}`, [newX, newY]);
1699
+ this.#selection[newY][newX] = 1;
1700
+ });
1701
+ // Select new selection
1702
+ newSelection.forEach(([newX, newY]) => {
1703
+ this.#selectionPixels.set(`${newX},${newY}`, [newX, newY]);
1704
+ this.#selection[newY][newX] = 1;
1705
+ });
1706
+ // Add pixels no longer in the selection as these are all now 0
1707
+ clearSelection.forEach(([clearX, clearY]) => {
1708
+ this.#layer.forEach((layer) => {
1709
+ const color = this.#data[layer][clearY][clearX];
1710
+ if (color !== 0) { // already empty ignore
1711
+ this.#setPixel(clearX, clearY, 0);
1712
+ }
1713
+ });
1714
+ this.#selectionPixels.delete(`${clearX},${clearY}`);
1715
+ this.#selection[clearY][clearX] = 0;
1716
+ });
1717
+
1718
+ // Update render
1719
+ this.$selectionPath.setAttribute('d', bitmaskToPath(this.#selection, { scale: this.size })[0]);
1720
+ }
1721
+ }
1722
+
1723
+ export { InputMode } from './utils/inputMode';
1724
+ export { LayerType } from './utils/constants';