@pictogrammers/components 0.5.14 → 0.5.17

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