underpost 2.8.845 → 2.8.847

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 (69) hide show
  1. package/README.md +13 -2
  2. package/bin/build.js +7 -1
  3. package/bin/deploy.js +7 -1
  4. package/cli.md +19 -3
  5. package/docker-compose.yml +1 -1
  6. package/manifests/deployment/dd-template-development/deployment.yaml +2 -2
  7. package/package.json +1 -1
  8. package/src/cli/cluster.js +6 -0
  9. package/src/cli/deploy.js +2 -1
  10. package/src/cli/index.js +8 -7
  11. package/src/cli/run.js +9 -1
  12. package/src/cli/ssh.js +32 -0
  13. package/src/client/Default.index.js +1 -1
  14. package/src/client/components/core/Chat.js +1 -1
  15. package/src/client/components/core/CommonJs.js +24 -22
  16. package/src/client/components/core/Content.js +1 -5
  17. package/src/client/components/core/Css.js +16 -2
  18. package/src/client/components/core/CssCore.js +7 -3
  19. package/src/client/components/core/DropDown.js +21 -12
  20. package/src/client/components/core/Modal.js +137 -15
  21. package/src/client/components/core/ObjectLayerEngine.js +638 -0
  22. package/src/client/components/core/Panel.js +156 -32
  23. package/src/client/components/core/Translate.js +4 -0
  24. package/src/client/components/default/MenuDefault.js +27 -1
  25. package/src/client/public/default/android-chrome-144x144.png +0 -0
  26. package/src/client/public/default/android-chrome-192x192.png +0 -0
  27. package/src/client/public/default/android-chrome-256x256.png +0 -0
  28. package/src/client/public/default/android-chrome-36x36.png +0 -0
  29. package/src/client/public/default/android-chrome-48x48.png +0 -0
  30. package/src/client/public/default/android-chrome-72x72.png +0 -0
  31. package/src/client/public/default/android-chrome-96x96.png +0 -0
  32. package/src/client/public/default/apple-touch-icon-114x114-precomposed.png +0 -0
  33. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  34. package/src/client/public/default/apple-touch-icon-120x120-precomposed.png +0 -0
  35. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  36. package/src/client/public/default/apple-touch-icon-144x144-precomposed.png +0 -0
  37. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  38. package/src/client/public/default/apple-touch-icon-152x152-precomposed.png +0 -0
  39. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  40. package/src/client/public/default/apple-touch-icon-180x180-precomposed.png +0 -0
  41. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  42. package/src/client/public/default/apple-touch-icon-57x57-precomposed.png +0 -0
  43. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  44. package/src/client/public/default/apple-touch-icon-60x60-precomposed.png +0 -0
  45. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  46. package/src/client/public/default/apple-touch-icon-72x72-precomposed.png +0 -0
  47. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  48. package/src/client/public/default/apple-touch-icon-76x76-precomposed.png +0 -0
  49. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  50. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  51. package/src/client/public/default/apple-touch-icon.png +0 -0
  52. package/src/client/public/default/assets/background/dark.jpg +0 -0
  53. package/src/client/public/default/assets/logo/base-icon.png +0 -0
  54. package/src/client/public/default/assets/mailer/api-user-check.png +0 -0
  55. package/src/client/public/default/assets/mailer/api-user-invalid-token.png +0 -0
  56. package/src/client/public/default/assets/mailer/api-user-recover.png +0 -0
  57. package/src/client/public/default/favicon-16x16.png +0 -0
  58. package/src/client/public/default/favicon-32x32.png +0 -0
  59. package/src/client/public/default/favicon.ico +0 -0
  60. package/src/client/public/default/mstile-144x144.png +0 -0
  61. package/src/client/public/default/mstile-150x150.png +0 -0
  62. package/src/client/public/default/mstile-310x150.png +0 -0
  63. package/src/client/public/default/mstile-310x310.png +0 -0
  64. package/src/client/public/default/mstile-70x70.png +0 -0
  65. package/src/client/public/default/safari-pinned-tab.svg +24 -0
  66. package/src/client/ssr/body/DefaultSplashScreen.js +2 -2
  67. package/src/index.js +9 -1
  68. package/src/server/client-build.js +4 -18
  69. package/src/server/conf.js +12 -5
@@ -0,0 +1,638 @@
1
+ import { darkTheme, renderChessPattern } from './Css.js';
2
+
3
+ const templateHTML = html`
4
+ <style>
5
+ :host {
6
+ --border: 1px solid #bbb;
7
+ --gap: 8px;
8
+ display: inline-block;
9
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
10
+ }
11
+ .wrap {
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: var(--gap);
15
+ align-items: flex-start;
16
+ }
17
+ .canvas-frame {
18
+ border: var(--border);
19
+ display: inline-block;
20
+ line-height: 0;
21
+ position: relative;
22
+ background: transparent;
23
+ }
24
+ canvas.canvas-layer {
25
+ display: block;
26
+ image-rendering: pixelated;
27
+ touch-action: none;
28
+ cursor: crosshair;
29
+ }
30
+ canvas.grid-layer {
31
+ position: absolute;
32
+ left: 0;
33
+ top: 0;
34
+ pointer-events: none;
35
+ }
36
+ </style>
37
+
38
+ <div class="wrap">
39
+ <div class="toolbar">
40
+ <input type="color" part="color" title="Brush color" value="#000000" />
41
+ <select part="tool">
42
+ <option value="pencil">pencil</option>
43
+ <option value="eraser">eraser</option>
44
+ <option value="fill">fill</option>
45
+ <option value="eyedropper">eyedropper</option>
46
+ </select>
47
+
48
+ <label>brush <input type="number" part="brush-size" min="1" value="1" /></label>
49
+ <label>pixel-size <input type="number" part="pixel-size" min="1" value="16" /></label>
50
+
51
+ <label class="switch"> <input type="checkbox" part="toggle-grid" /> grid </label>
52
+
53
+ <button part="export">Export PNG</button>
54
+ <button part="export-json">Export JSON</button>
55
+ <button part="import-json">Import JSON</button>
56
+ </div>
57
+ <div class="canvas-frame" style="${renderChessPattern()}">
58
+ <canvas part="canvas" class="canvas-layer"></canvas>
59
+ <canvas part="grid" class="grid-layer"></canvas>
60
+ </div>
61
+ </div>
62
+ `;
63
+
64
+ class ObjectLayerEngineElement extends HTMLElement {
65
+ constructor() {
66
+ super();
67
+ this.attachShadow({ mode: 'open' });
68
+ this.shadowRoot.innerHTML = templateHTML;
69
+
70
+ // DOM
71
+ this._pixelCanvas = this.shadowRoot.querySelector('canvas[part="canvas"]');
72
+ this._gridCanvas = this.shadowRoot.querySelector('canvas[part="grid"]');
73
+ this._colorInput = this.shadowRoot.querySelector('input[part="color"]');
74
+ this._toolSelect = this.shadowRoot.querySelector('select[part="tool"]');
75
+ this._brushSizeInput = this.shadowRoot.querySelector('input[part="brush-size"]');
76
+ this._pixelSizeInput = this.shadowRoot.querySelector('input[part="pixel-size"]');
77
+ this._exportBtn = this.shadowRoot.querySelector('button[part="export"]');
78
+ this._exportJsonBtn = this.shadowRoot.querySelector('button[part="export-json"]');
79
+ this._importJsonBtn = this.shadowRoot.querySelector('button[part="import-json"]');
80
+ this._toggleGrid = this.shadowRoot.querySelector('input[part="toggle-grid"]');
81
+
82
+ // internal state
83
+ this._width = 16;
84
+ this._height = 16;
85
+ this._pixelSize = 16;
86
+ this._brushSize = 1;
87
+ this._matrix = this._createEmptyMatrix(this._width, this._height);
88
+
89
+ this._pixelCtx = null;
90
+ this._gridCtx = null;
91
+
92
+ this._isPointerDown = false;
93
+ this._tool = 'pencil';
94
+ this._brushColor = [0, 0, 0, 255];
95
+ this._showGrid = false;
96
+
97
+ // binds
98
+ this._onPointerDown = this._onPointerDown.bind(this);
99
+ this._onPointerMove = this._onPointerMove.bind(this);
100
+ this._onPointerUp = this._onPointerUp.bind(this);
101
+ }
102
+
103
+ static get observedAttributes() {
104
+ return ['width', 'height', 'pixel-size'];
105
+ }
106
+ attributeChangedCallback(name, oldV, newV) {
107
+ if (oldV === newV) return;
108
+ if (name === 'width') this.width = parseInt(newV, 10) || this._width;
109
+ if (name === 'height') this.height = parseInt(newV, 10) || this._height;
110
+ if (name === 'pixel-size') this.pixelSize = parseInt(newV, 10) || this._pixelSize;
111
+ }
112
+
113
+ connectedCallback() {
114
+ if (this.hasAttribute('width')) this._width = Math.max(1, parseInt(this.getAttribute('width'), 10));
115
+ if (this.hasAttribute('height')) this._height = Math.max(1, parseInt(this.getAttribute('height'), 10));
116
+ if (this.hasAttribute('pixel-size')) this._pixelSize = Math.max(1, parseInt(this.getAttribute('pixel-size'), 10));
117
+
118
+ this._setupContextsAndSize();
119
+
120
+ // UI events
121
+ this._colorInput.addEventListener('input', (e) => {
122
+ const rgba = this._hexToRgba(e.target.value);
123
+ this.setBrushColor([rgba[0], rgba[1], rgba[2], 255]);
124
+ });
125
+ this._toolSelect.addEventListener('change', (e) => this.setTool(e.target.value));
126
+ this._brushSizeInput.addEventListener('change', (e) => this.setBrushSize(parseInt(e.target.value, 10) || 1));
127
+ this._pixelSizeInput.addEventListener('change', (e) => {
128
+ this.pixelSize = Math.max(1, parseInt(e.target.value, 10) || 1);
129
+ });
130
+ this._toggleGrid.addEventListener('change', (e) => {
131
+ this._showGrid = !!e.target.checked;
132
+ this._renderGrid();
133
+ });
134
+
135
+ // Export/Import
136
+ this._exportBtn.addEventListener('click', () => this.exportPNG());
137
+ this._exportJsonBtn.addEventListener('click', () => {
138
+ const json = this.exportMatrixJSON();
139
+ const blob = new Blob([json], { type: 'application/json' });
140
+ const url = URL.createObjectURL(blob);
141
+ const a = document.createElement('a');
142
+ a.href = url;
143
+ a.download = 'object-layer.json';
144
+ a.click();
145
+ URL.revokeObjectURL(url);
146
+ });
147
+
148
+ this._importJsonBtn.addEventListener('click', async () => {
149
+ const file = await this._pickFile();
150
+ if (!file) return;
151
+ const text = await file.text();
152
+ try {
153
+ this.importMatrixJSON(text);
154
+ } catch (err) {
155
+ console.error(err);
156
+ alert('Invalid JSON');
157
+ }
158
+ });
159
+
160
+ // Pointer events
161
+ this._pixelCanvas.addEventListener('pointerdown', this._onPointerDown);
162
+ window.addEventListener('pointermove', this._onPointerMove);
163
+ window.addEventListener('pointerup', this._onPointerUp);
164
+
165
+ this.render();
166
+ }
167
+
168
+ disconnectedCallback() {
169
+ this._pixelCanvas.removeEventListener('pointerdown', this._onPointerDown);
170
+ window.removeEventListener('pointermove', this._onPointerMove);
171
+ window.removeEventListener('pointerup', this._onPointerUp);
172
+ }
173
+
174
+ // ---------------- Matrix helpers ----------------
175
+ _createEmptyMatrix(w, h) {
176
+ const mat = new Array(h);
177
+ for (let y = 0; y < h; y++) {
178
+ mat[y] = new Array(w);
179
+ for (let x = 0; x < w; x++) mat[y][x] = [0, 0, 0, 0];
180
+ }
181
+ return mat;
182
+ }
183
+
184
+ createMatrix(width, height, fill = [0, 0, 0, 0]) {
185
+ const w = Math.max(1, Math.floor(width));
186
+ const h = Math.max(1, Math.floor(height));
187
+ const mat = this._createEmptyMatrix(w, h);
188
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) mat[y][x] = fill.slice();
189
+ return mat;
190
+ }
191
+
192
+ loadMatrix(matrix) {
193
+ if (!Array.isArray(matrix) || matrix.length === 0) throw new TypeError('matrix must be non-empty 2D array');
194
+ const h = matrix.length;
195
+ const w = matrix[0].length;
196
+ for (let y = 0; y < h; y++) {
197
+ if (!Array.isArray(matrix[y]) || matrix[y].length !== w) throw new TypeError('matrix must be rectangular');
198
+ for (let x = 0; x < w; x++) {
199
+ const v = matrix[y][x];
200
+ if (!Array.isArray(v) || v.length !== 4) throw new TypeError('each cell must be [r,g,b,a]');
201
+ matrix[y][x] = v.map((n) => this._clampInt(n));
202
+ }
203
+ }
204
+ this._width = w;
205
+ this._height = h;
206
+ this._matrix = matrix.map((r) => r.map((c) => c.slice()));
207
+ this._setupContextsAndSize();
208
+ this.render();
209
+ this.dispatchEvent(new CustomEvent('matrixload', { detail: { width: w, height: h } }));
210
+ }
211
+
212
+ clear(fill = [0, 0, 0, 0]) {
213
+ for (let y = 0; y < this._height; y++) for (let x = 0; x < this._width; x++) this._matrix[y][x] = fill.slice();
214
+ this.render();
215
+ this.dispatchEvent(new CustomEvent('clear'));
216
+ }
217
+
218
+ resize(w, h, { preserve = true } = {}) {
219
+ const nw = Math.max(1, Math.floor(w));
220
+ const nh = Math.max(1, Math.floor(h));
221
+ const newMat = this._createEmptyMatrix(nw, nh);
222
+ if (preserve) {
223
+ const minW = Math.min(nw, this._width);
224
+ const minH = Math.min(nh, this._height);
225
+ for (let y = 0; y < minH; y++) for (let x = 0; x < minW; x++) newMat[y][x] = this._matrix[y][x].slice();
226
+ }
227
+ this._width = nw;
228
+ this._height = nh;
229
+ this._matrix = newMat;
230
+ this._setupContextsAndSize();
231
+ this.render();
232
+ this.dispatchEvent(new CustomEvent('resize', { detail: { width: nw, height: nh } }));
233
+ }
234
+
235
+ setPixel(x, y, rgba, renderNow = true) {
236
+ if (!this._inBounds(x, y)) return false;
237
+ this._matrix[y][x] = rgba.map((n) => this._clampInt(n));
238
+ if (renderNow) this.render();
239
+ this.dispatchEvent(new CustomEvent('pixelchange', { detail: { x, y, rgba: this._matrix[y][x].slice() } }));
240
+ return true;
241
+ }
242
+
243
+ getPixel(x, y) {
244
+ return this._inBounds(x, y) ? this._matrix[y][x].slice() : null;
245
+ }
246
+ _inBounds(x, y) {
247
+ return x >= 0 && y >= 0 && x < this._width && y < this._height;
248
+ }
249
+ _clampInt(v) {
250
+ const n = Number(v) || 0;
251
+ return Math.min(255, Math.max(0, Math.floor(n)));
252
+ }
253
+
254
+ // ---------------- Canvas sizing and contexts ----------------
255
+ _setupContextsAndSize() {
256
+ // logical canvas (one logical pixel per image pixel). CSS scales by pixelSize.
257
+ this._pixelCanvas.width = this._width;
258
+ this._pixelCanvas.height = this._height;
259
+ this._pixelCanvas.style.width = `${this._width * this._pixelSize}px`;
260
+ this._pixelCanvas.style.height = `${this._height * this._pixelSize}px`;
261
+
262
+ // grid overlay uses CSS pixel coordinates
263
+ this._gridCanvas.width = this._width * this._pixelSize;
264
+ this._gridCanvas.height = this._height * this._pixelSize;
265
+ this._gridCanvas.style.width = this._pixelCanvas.style.width;
266
+ this._gridCanvas.style.height = this._pixelCanvas.style.height;
267
+
268
+ this._pixelCtx = this._pixelCanvas.getContext('2d');
269
+ this._gridCtx = this._gridCanvas.getContext('2d');
270
+ try {
271
+ this._pixelCtx.imageSmoothingEnabled = false;
272
+ this._gridCtx.imageSmoothingEnabled = false;
273
+ } catch (e) {}
274
+ this._renderGrid();
275
+ }
276
+
277
+ render() {
278
+ // sanity: ensure matrix shape matches
279
+ if (
280
+ !Array.isArray(this._matrix) ||
281
+ this._matrix.length !== this._height ||
282
+ !Array.isArray(this._matrix[0]) ||
283
+ this._matrix[0].length !== this._width
284
+ ) {
285
+ this._matrix = this._createEmptyMatrix(this._width, this._height);
286
+ }
287
+
288
+ // detect transparency (fast bailout)
289
+ let hasTransparent = false;
290
+ for (let y = 0; y < this._height && !hasTransparent; y++) {
291
+ for (let x = 0; x < this._width; x++) {
292
+ const a = this._matrix[y] && this._matrix[y][x] ? this._matrix[y][x][3] : 0;
293
+ if (a !== 255) {
294
+ hasTransparent = true;
295
+ break;
296
+ }
297
+ }
298
+ }
299
+
300
+ // clear and optionally draw checkerboard (visual only)
301
+ this._pixelCtx.clearRect(0, 0, this._pixelCanvas.width, this._pixelCanvas.height);
302
+ if (hasTransparent) this._drawCheckerboard();
303
+
304
+ // write image data
305
+ const img = this._pixelCtx.createImageData(this._width, this._height);
306
+ const data = img.data;
307
+ let p = 0;
308
+ for (let y = 0; y < this._height; y++) {
309
+ for (let x = 0; x < this._width; x++) {
310
+ const cell = this._matrix[y] && this._matrix[y][x] ? this._matrix[y][x] : [0, 0, 0, 0];
311
+ data[p++] = this._clampInt(cell[0]);
312
+ data[p++] = this._clampInt(cell[1]);
313
+ data[p++] = this._clampInt(cell[2]);
314
+ data[p++] = this._clampInt(cell[3]);
315
+ }
316
+ }
317
+ this._pixelCtx.putImageData(img, 0, 0);
318
+
319
+ if (this._showGrid) this._renderGrid();
320
+ }
321
+
322
+ _drawCheckerboard() {
323
+ const ctx = this._pixelCtx;
324
+ const w = this._width;
325
+ const h = this._height;
326
+ const light = '#e9e9e9',
327
+ dark = '#cfcfcf';
328
+ // draw one logical pixel per matrix cell
329
+ for (let y = 0; y < h; y++) {
330
+ for (let x = 0; x < w; x++) {
331
+ ctx.fillStyle = ((x + y) & 1) === 0 ? light : dark;
332
+ ctx.fillRect(x, y, 1, 1);
333
+ }
334
+ }
335
+ }
336
+
337
+ _renderGrid() {
338
+ const ctx = this._gridCtx;
339
+ if (!ctx) return;
340
+ const w = this._gridCanvas.width;
341
+ const h = this._gridCanvas.height;
342
+ ctx.clearRect(0, 0, w, h);
343
+ if (!this._showGrid) return;
344
+ const ps = this._pixelSize;
345
+ ctx.save();
346
+ ctx.strokeStyle = darkTheme ? '#e1e1e1' : '#272727';
347
+ ctx.lineWidth = 2;
348
+ ctx.beginPath();
349
+ for (let x = 0; x <= this._width; x++) {
350
+ const xx = x * ps + 0.5;
351
+ ctx.moveTo(xx, 0);
352
+ ctx.lineTo(xx, h);
353
+ }
354
+ for (let y = 0; y <= this._height; y++) {
355
+ const yy = y * ps + 0.5;
356
+ ctx.moveTo(0, yy);
357
+ ctx.lineTo(w, yy);
358
+ }
359
+ ctx.stroke();
360
+ ctx.restore();
361
+ }
362
+
363
+ // ---------------- Tools & painting ----------------
364
+ setTool(name) {
365
+ this._tool = name;
366
+ if (this._toolSelect) this._toolSelect.value = name;
367
+ }
368
+ setBrushColor(rgba) {
369
+ this._brushColor = rgba.map((n) => this._clampInt(n));
370
+ if (this._colorInput) this._colorInput.value = this._rgbaToHex(this._brushColor);
371
+ }
372
+ setBrushSize(n) {
373
+ this._brushSize = Math.max(1, Math.floor(n));
374
+ if (this._brushSizeInput) this._brushSizeInput.value = this._brushSize;
375
+ }
376
+
377
+ _applyBrush(x, y, color, renderAfter = false) {
378
+ const half = Math.floor(this._brushSize / 2);
379
+ for (let oy = -half; oy <= half; oy++)
380
+ for (let ox = -half; ox <= half; ox++) {
381
+ const tx = x + ox,
382
+ ty = y + oy;
383
+ if (this._inBounds(tx, ty)) this._matrix[ty][tx] = color.slice();
384
+ }
385
+ if (renderAfter) this.render();
386
+ }
387
+
388
+ fillBucket(x, y, targetColor = null) {
389
+ if (!this._inBounds(x, y)) return;
390
+ const src = this.getPixel(x, y);
391
+ const newColor = targetColor ? targetColor.slice() : this._brushColor.slice();
392
+ if (this._colorsEqual(src, newColor)) return;
393
+ const stack = [[x, y]];
394
+ while (stack.length) {
395
+ const [cx, cy] = stack.pop();
396
+ if (!this._inBounds(cx, cy)) continue;
397
+ const cur = this.getPixel(cx, cy);
398
+ if (!this._colorsEqual(cur, src)) continue;
399
+ this._matrix[cy][cx] = newColor.slice();
400
+ stack.push([cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1]);
401
+ }
402
+ this.render();
403
+ this.dispatchEvent(new CustomEvent('fill', { detail: { x, y } }));
404
+ }
405
+
406
+ _colorsEqual(a, b) {
407
+ if (!a || !b) return false;
408
+ return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
409
+ }
410
+
411
+ // ---------------- Pointer handling ----------------
412
+ _toGridCoords(evt) {
413
+ const rect = this._pixelCanvas.getBoundingClientRect();
414
+ const cssX = evt.clientX - rect.left;
415
+ const cssY = evt.clientY - rect.top;
416
+ const scaleX = this._pixelCanvas.width / rect.width;
417
+ const scaleY = this._pixelCanvas.height / rect.height;
418
+ const x = Math.floor(cssX * scaleX);
419
+ const y = Math.floor(cssY * scaleY);
420
+ return [x, y];
421
+ }
422
+
423
+ _onPointerDown(evt) {
424
+ evt.preventDefault();
425
+ this._isPointerDown = true;
426
+ try {
427
+ this._pixelCanvas.setPointerCapture(evt.pointerId);
428
+ } catch (e) {}
429
+ const [x, y] = this._toGridCoords(evt);
430
+ this._applyToolAt(x, y, evt);
431
+ }
432
+ _onPointerMove(evt) {
433
+ if (!this._isPointerDown) return;
434
+ const [x, y] = this._toGridCoords(evt);
435
+ this._applyToolAt(x, y, evt, true);
436
+ }
437
+ _onPointerUp(evt) {
438
+ this._isPointerDown = false;
439
+ try {
440
+ this._pixelCanvas.releasePointerCapture(evt.pointerId);
441
+ } catch (e) {}
442
+ }
443
+
444
+ _applyToolAt(x, y, evt, continuous = false) {
445
+ if (!this._inBounds(x, y)) return;
446
+ switch (this._tool) {
447
+ case 'pencil':
448
+ this._applyBrush(x, y, this._brushColor, true);
449
+ break;
450
+ case 'eraser':
451
+ this._applyBrush(x, y, [0, 0, 0, 0], true);
452
+ break;
453
+ case 'fill':
454
+ if (!continuous) this.fillBucket(x, y);
455
+ break;
456
+ case 'eyedropper':
457
+ const picked = this.getPixel(x, y);
458
+ if (picked) this.setBrushColor(picked);
459
+ break;
460
+ }
461
+ }
462
+
463
+ // ---------------- Import / Export ----------------
464
+ exportMatrixJSON() {
465
+ return JSON.stringify({ width: this._width, height: this._height, matrix: this._matrix });
466
+ }
467
+ importMatrixJSON(json) {
468
+ const data = typeof json === 'string' ? JSON.parse(json) : json;
469
+ if (!data || !Array.isArray(data.matrix)) throw new TypeError('Invalid matrix JSON');
470
+ this.loadMatrix(data.matrix);
471
+ }
472
+
473
+ async _pickFile() {
474
+ return new Promise((resolve) => {
475
+ const input = document.createElement('input');
476
+ input.type = 'file';
477
+ input.accept = 'application/json';
478
+ input.addEventListener('change', () => {
479
+ resolve(input.files && input.files[0] ? input.files[0] : null);
480
+ });
481
+ input.click();
482
+ });
483
+ }
484
+
485
+ // Create a PNG data URL at the requested scale (scale = number of CSS pixels per logical pixel)
486
+ toDataURL(scale = this._pixelSize) {
487
+ const w = this._width,
488
+ h = this._height;
489
+ const outW = Math.max(1, Math.floor(w * scale));
490
+ const outH = Math.max(1, Math.floor(h * scale));
491
+
492
+ // create logical image at native resolution
493
+ const src = document.createElement('canvas');
494
+ src.width = w;
495
+ src.height = h;
496
+ const sctx = src.getContext('2d');
497
+ const img = sctx.createImageData(w, h);
498
+ const data = img.data;
499
+ let p = 0;
500
+ for (let y = 0; y < h; y++)
501
+ for (let x = 0; x < w; x++) {
502
+ const c = this._matrix[y][x] || [0, 0, 0, 0];
503
+ data[p++] = this._clampInt(c[0]);
504
+ data[p++] = this._clampInt(c[1]);
505
+ data[p++] = this._clampInt(c[2]);
506
+ data[p++] = this._clampInt(c[3]);
507
+ }
508
+ sctx.putImageData(img, 0, 0);
509
+
510
+ // scale into output canvas (nearest-neighbor)
511
+ const out = document.createElement('canvas');
512
+ out.width = outW;
513
+ out.height = outH;
514
+ const octx = out.getContext('2d');
515
+ try {
516
+ octx.imageSmoothingEnabled = false;
517
+ } catch (e) {}
518
+ octx.drawImage(src, 0, 0, outW, outH);
519
+ return out.toDataURL('image/png');
520
+ }
521
+
522
+ // Async blob version (recommended for large images)
523
+ toBlob(scale = this._pixelSize) {
524
+ return new Promise((resolve) => {
525
+ const w = this._width,
526
+ h = this._height;
527
+ const outW = Math.max(1, Math.floor(w * scale));
528
+ const outH = Math.max(1, Math.floor(h * scale));
529
+ const src = document.createElement('canvas');
530
+ src.width = w;
531
+ src.height = h;
532
+ const sctx = src.getContext('2d');
533
+ const img = sctx.createImageData(w, h);
534
+ const data = img.data;
535
+ let p = 0;
536
+ for (let y = 0; y < h; y++)
537
+ for (let x = 0; x < w; x++) {
538
+ const c = this._matrix[y][x] || [0, 0, 0, 0];
539
+ data[p++] = this._clampInt(c[0]);
540
+ data[p++] = this._clampInt(c[1]);
541
+ data[p++] = this._clampInt(c[2]);
542
+ data[p++] = this._clampInt(c[3]);
543
+ }
544
+ sctx.putImageData(img, 0, 0);
545
+ const out = document.createElement('canvas');
546
+ out.width = outW;
547
+ out.height = outH;
548
+ const octx = out.getContext('2d');
549
+ try {
550
+ octx.imageSmoothingEnabled = false;
551
+ } catch (e) {}
552
+ octx.drawImage(src, 0, 0, outW, outH);
553
+ out.toBlob((b) => resolve(b), 'image/png');
554
+ });
555
+ }
556
+
557
+ // Trigger download of PNG (uses blob to avoid huge data URLs on big exports)
558
+ async exportPNG(filename = 'object-layer.png', scale = this._pixelSize) {
559
+ const blob = await this.toBlob(scale);
560
+ const url = URL.createObjectURL(blob);
561
+ const a = document.createElement('a');
562
+ a.href = url;
563
+ a.download = filename;
564
+ a.click();
565
+ // revoke after a tick to ensure download started
566
+ setTimeout(() => URL.revokeObjectURL(url), 5000);
567
+ }
568
+
569
+ // ---------------- Helpers ----------------
570
+ _hexToRgba(hex) {
571
+ const h = (hex || '').replace('#', '');
572
+ if (h.length === 3) {
573
+ return [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16), 255];
574
+ }
575
+ if (h.length === 6) {
576
+ return [parseInt(h.substring(0, 2), 16), parseInt(h.substring(2, 4), 16), parseInt(h.substring(4, 6), 16), 255];
577
+ }
578
+ return [0, 0, 0, 255];
579
+ }
580
+ _rgbaToHex(rgba) {
581
+ const [r, g, b] = rgba;
582
+ return `#${((1 << 24) + (this._clampInt(r) << 16) + (this._clampInt(g) << 8) + this._clampInt(b))
583
+ .toString(16)
584
+ .slice(1)}`;
585
+ }
586
+
587
+ // ---------------- Properties ----------------
588
+ get width() {
589
+ return this._width;
590
+ }
591
+ set width(v) {
592
+ this._width = Math.max(1, Math.floor(v));
593
+ this.setAttribute('width', String(this._width));
594
+ this._setupContextsAndSize();
595
+ this.render();
596
+ }
597
+ get height() {
598
+ return this._height;
599
+ }
600
+ set height(v) {
601
+ this._height = Math.max(1, Math.floor(v));
602
+ this.setAttribute('height', String(this._height));
603
+ this._setupContextsAndSize();
604
+ this.render();
605
+ }
606
+ get pixelSize() {
607
+ return this._pixelSize;
608
+ }
609
+ set pixelSize(v) {
610
+ this._pixelSize = Math.max(1, Math.floor(v));
611
+ this.setAttribute('pixel-size', String(this._pixelSize));
612
+ this._setupContextsAndSize();
613
+ this.render();
614
+ }
615
+ get brushSize() {
616
+ return this._brushSize;
617
+ }
618
+ set brushSize(v) {
619
+ this.setBrushSize(v);
620
+ }
621
+ get matrix() {
622
+ return this._matrix.map((row) => row.map((c) => c.slice()));
623
+ }
624
+
625
+ exportJSON() {
626
+ return this.exportMatrixJSON();
627
+ }
628
+ importJSON(json) {
629
+ return this.importMatrixJSON(json);
630
+ }
631
+ }
632
+
633
+ customElements.define('object-layer-engine', ObjectLayerEngineElement);
634
+
635
+ /*
636
+ Example usage:
637
+ <object-layer-engine id="ole" width="20" height="12" pixel-size="20"></object-layer-engine>
638
+ */