underpost 2.8.867 → 2.8.871
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -36
- package/bin/build.js +1 -0
- package/bin/deploy.js +4 -0
- package/bin/util.js +1 -56
- package/cli.md +88 -86
- package/conf.js +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/mongo-express/deployment.yaml +12 -12
- package/manifests/maas/nvim.sh +91 -0
- package/package.json +1 -10
- package/src/api/file/file.service.js +28 -8
- package/src/api/user/user.router.js +24 -0
- package/src/api/user/user.service.js +3 -4
- package/src/cli/cluster.js +2 -13
- package/src/cli/cron.js +0 -1
- package/src/cli/db.js +0 -19
- package/src/cli/deploy.js +17 -26
- package/src/cli/fs.js +1 -0
- package/src/cli/index.js +1 -0
- package/src/cli/run.js +9 -2
- package/src/client/components/core/CalendarCore.js +1 -1
- package/src/client/components/core/CssCore.js +12 -0
- package/src/client/components/core/Docs.js +2 -2
- package/src/client/components/core/FullScreen.js +19 -28
- package/src/client/components/core/Input.js +1 -0
- package/src/client/components/core/Modal.js +41 -24
- package/src/client/components/core/ObjectLayerEngine.js +229 -4
- package/src/client/components/core/ObjectLayerEngineModal.js +441 -0
- package/src/client/components/core/Panel.js +4 -1
- package/src/client/components/core/PanelForm.js +1 -1
- package/src/client/components/core/Router.js +29 -25
- package/src/client/components/core/ToggleSwitch.js +15 -1
- package/src/client/components/core/VanillaJs.js +12 -13
- package/src/client/public/default/assets/mailer/api-user-default-avatar.png +0 -0
- package/src/index.js +1 -1
- package/src/server/client-build.js +3 -11
- package/src/server/client-icons.js +6 -78
- package/src/server/conf.js +4 -56
- package/src/server/process.js +2 -1
|
@@ -1535,18 +1535,6 @@ const Modal = {
|
|
|
1535
1535
|
s(`.btn-icon-menu-back`).classList.add('hide');
|
|
1536
1536
|
if (s(`.menu-btn-container-main`)) s(`.menu-btn-container-main`).classList.remove('hide');
|
|
1537
1537
|
};
|
|
1538
|
-
this.onHomeRouterEvent = async () => {
|
|
1539
|
-
for (const keyModal of Object.keys(this.Data)) {
|
|
1540
|
-
if (
|
|
1541
|
-
![idModal, 'main-body-top', 'main-body'].concat(this.Data[idModal]?.homeModals || []).includes(keyModal)
|
|
1542
|
-
)
|
|
1543
|
-
if (s(`.btn-close-${keyModal}`)) s(`.btn-close-${keyModal}`).click();
|
|
1544
|
-
backMenuButtonEvent();
|
|
1545
|
-
}
|
|
1546
|
-
if (s(`.btn-close-modal-menu`)) s(`.btn-close-modal-menu`).click();
|
|
1547
|
-
setPath(getProxyPath());
|
|
1548
|
-
setDocTitle();
|
|
1549
|
-
};
|
|
1550
1538
|
s(`.main-btn-home`).onclick = async () => {
|
|
1551
1539
|
// await this.onHomeRouterEvent();
|
|
1552
1540
|
s(`.action-btn-home`).click();
|
|
@@ -1767,12 +1755,8 @@ const Modal = {
|
|
|
1767
1755
|
if (!s(`.${idModal}`)) return;
|
|
1768
1756
|
this.removeModal(idModal);
|
|
1769
1757
|
// Handle modal route change
|
|
1770
|
-
if (options.route) {
|
|
1771
|
-
closeModalRouteChangeEvent({
|
|
1772
|
-
route: options.route,
|
|
1773
|
-
RouterInstance: options.RouterInstance,
|
|
1774
|
-
homeCid: Modal.homeCid,
|
|
1775
|
-
});
|
|
1758
|
+
if (options.route || options.query) {
|
|
1759
|
+
closeModalRouteChangeEvent({ closedId: idModal, homeCid: Modal.homeCid });
|
|
1776
1760
|
}
|
|
1777
1761
|
}, 300);
|
|
1778
1762
|
};
|
|
@@ -1967,7 +1951,45 @@ const Modal = {
|
|
|
1967
1951
|
...this.Data[idModal],
|
|
1968
1952
|
};
|
|
1969
1953
|
},
|
|
1970
|
-
onHomeRouterEvent: () => {
|
|
1954
|
+
onHomeRouterEvent: async () => {
|
|
1955
|
+
// 1. Get list of modals to close.
|
|
1956
|
+
const modalsToClose = Object.keys(Modal.Data).filter((idModal) => {
|
|
1957
|
+
const modal = Modal.Data[idModal];
|
|
1958
|
+
if (!modal) return false;
|
|
1959
|
+
// Don't close the core UI elements
|
|
1960
|
+
const coreUI = ['modal-menu', 'main-body', 'main-body-top', 'bottom-bar', 'board-notification'];
|
|
1961
|
+
if (coreUI.find((id) => idModal.startsWith(id))) {
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
// Don't close modals that are part of the "home" screen itself
|
|
1965
|
+
const homeModals = Modal.Data['modal-menu']?.homeModals || [];
|
|
1966
|
+
if (homeModals.includes(idModal)) {
|
|
1967
|
+
return false;
|
|
1968
|
+
}
|
|
1969
|
+
return true;
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
// 2. Navigate to home first, creating a new history entry.
|
|
1973
|
+
setPath(getProxyPath());
|
|
1974
|
+
setDocTitle();
|
|
1975
|
+
|
|
1976
|
+
// 3. Close the modals without them affecting the URL.
|
|
1977
|
+
for (const id of modalsToClose) {
|
|
1978
|
+
Modal.removeModal(id);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// 4. Finally, handle UI cleanup for the slide-menu.
|
|
1982
|
+
if (s(`.menu-btn-container-children`)) htmls(`.menu-btn-container-children`, '');
|
|
1983
|
+
if (s(`.nav-title-display-modal-menu`)) htmls(`.nav-title-display-modal-menu`, '');
|
|
1984
|
+
if (s(`.nav-path-display-modal-menu`)) htmls(`.nav-path-display-modal-menu`, '');
|
|
1985
|
+
if (s(`.btn-icon-menu-back`)) s(`.btn-icon-menu-back`).classList.add('hide');
|
|
1986
|
+
if (s(`.menu-btn-container-main`)) s(`.menu-btn-container-main`).classList.remove('hide');
|
|
1987
|
+
|
|
1988
|
+
// And close the slide menu if it's open
|
|
1989
|
+
if (s(`.btn-close-modal-menu`) && !s(`.btn-close-modal-menu`).classList.contains('hide')) {
|
|
1990
|
+
s(`.btn-close-modal-menu`).click();
|
|
1991
|
+
}
|
|
1992
|
+
},
|
|
1971
1993
|
currentTopModalId: '',
|
|
1972
1994
|
zIndexSync: function ({ idModal }) {
|
|
1973
1995
|
setTimeout(() => {
|
|
@@ -1996,11 +2018,6 @@ const Modal = {
|
|
|
1996
2018
|
setTopModalCallback: function (idModal) {
|
|
1997
2019
|
s(`.${idModal}`).style.zIndex = '4';
|
|
1998
2020
|
this.currentTopModalId = `${idModal}`;
|
|
1999
|
-
if (
|
|
2000
|
-
this.Data[idModal].query &&
|
|
2001
|
-
`${location.pathname}${window.location.search}` !== `${location.pathname}${this.Data[idModal].query}`
|
|
2002
|
-
)
|
|
2003
|
-
setPath(`${location.pathname}${this.Data[idModal].query}`);
|
|
2004
2021
|
},
|
|
2005
2022
|
mobileModal: () => window.innerWidth < 600 || window.innerHeight < 600,
|
|
2006
2023
|
writeHTML: ({ idModal, html }) => htmls(`.html-${idModal}`, html),
|
|
@@ -33,6 +33,22 @@ const templateHTML = html`
|
|
|
33
33
|
top: 0;
|
|
34
34
|
pointer-events: none;
|
|
35
35
|
}
|
|
36
|
+
.toolbar {
|
|
37
|
+
display: flex;
|
|
38
|
+
gap: 8px;
|
|
39
|
+
flex-wrap: wrap;
|
|
40
|
+
align-items: center;
|
|
41
|
+
}
|
|
42
|
+
.toolbar label {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
gap: 6px;
|
|
45
|
+
align-items: center;
|
|
46
|
+
}
|
|
47
|
+
.group {
|
|
48
|
+
display: inline-flex;
|
|
49
|
+
gap: 6px;
|
|
50
|
+
align-items: center;
|
|
51
|
+
}
|
|
36
52
|
</style>
|
|
37
53
|
|
|
38
54
|
<div class="wrap">
|
|
@@ -48,8 +64,34 @@ const templateHTML = html`
|
|
|
48
64
|
<label>brush <input type="number" part="brush-size" min="1" value="1" /></label>
|
|
49
65
|
<label>pixel-size <input type="number" part="pixel-size" min="1" value="16" /></label>
|
|
50
66
|
|
|
67
|
+
<!-- New: cell dimensions (width x height) -->
|
|
68
|
+
<label
|
|
69
|
+
>cells <input type="number" part="cell-width" min="1" value="16" style="width:6ch" /> x
|
|
70
|
+
<input type="number" part="cell-height" min="1" value="16" style="width:6ch"
|
|
71
|
+
/></label>
|
|
72
|
+
|
|
51
73
|
<label class="switch"> <input type="checkbox" part="toggle-grid" /> grid </label>
|
|
52
74
|
|
|
75
|
+
<!-- New: transform tools -->
|
|
76
|
+
<div class="group">
|
|
77
|
+
<button part="flip-h" title="Flip horizontally">Flip H</button>
|
|
78
|
+
<button part="flip-v" title="Flip vertically">Flip V</button>
|
|
79
|
+
<button part="rot-ccw" title="Rotate -90°">⟲</button>
|
|
80
|
+
<button part="rot-cw" title="Rotate +90°">⟳</button>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<label
|
|
84
|
+
>opacity <input type="range" part="opacity" min="0" max="255" value="255" style="width:10rem" /><input
|
|
85
|
+
type="number"
|
|
86
|
+
part="opacity-num"
|
|
87
|
+
min="0"
|
|
88
|
+
max="255"
|
|
89
|
+
value="255"
|
|
90
|
+
style="width:5ch;margin-left:4px"
|
|
91
|
+
/></label>
|
|
92
|
+
|
|
93
|
+
<button part="clear" title="Clear (make fully transparent)">Clear</button>
|
|
94
|
+
|
|
53
95
|
<button part="export">Export PNG</button>
|
|
54
96
|
<button part="export-json">Export JSON</button>
|
|
55
97
|
<button part="import-json">Import JSON</button>
|
|
@@ -79,11 +121,24 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
79
121
|
this._importJsonBtn = this.shadowRoot.querySelector('button[part="import-json"]');
|
|
80
122
|
this._toggleGrid = this.shadowRoot.querySelector('input[part="toggle-grid"]');
|
|
81
123
|
|
|
124
|
+
// new controls
|
|
125
|
+
this._widthInput = this.shadowRoot.querySelector('input[part="cell-width"]');
|
|
126
|
+
this._heightInput = this.shadowRoot.querySelector('input[part="cell-height"]');
|
|
127
|
+
this._flipHBtn = this.shadowRoot.querySelector('button[part="flip-h"]');
|
|
128
|
+
this._flipVBtn = this.shadowRoot.querySelector('button[part="flip-v"]');
|
|
129
|
+
this._rotCCWBtn = this.shadowRoot.querySelector('button[part="rot-ccw"]');
|
|
130
|
+
this._rotCWBtn = this.shadowRoot.querySelector('button[part="rot-cw"]');
|
|
131
|
+
this._clearBtn = this.shadowRoot.querySelector('button[part="clear"]');
|
|
132
|
+
this._opacityRange = this.shadowRoot.querySelector('input[part="opacity"]');
|
|
133
|
+
this._opacityNumber = this.shadowRoot.querySelector('input[part="opacity-num"]');
|
|
134
|
+
|
|
82
135
|
// internal state
|
|
83
136
|
this._width = 16;
|
|
84
137
|
this._height = 16;
|
|
85
138
|
this._pixelSize = 16;
|
|
86
139
|
this._brushSize = 1;
|
|
140
|
+
// brush color stored as [r,g,b,a]
|
|
141
|
+
this._brushColor = [0, 0, 0, 255];
|
|
87
142
|
this._matrix = this._createEmptyMatrix(this._width, this._height);
|
|
88
143
|
|
|
89
144
|
this._pixelCtx = null;
|
|
@@ -91,13 +146,18 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
91
146
|
|
|
92
147
|
this._isPointerDown = false;
|
|
93
148
|
this._tool = 'pencil';
|
|
94
|
-
this._brushColor = [0, 0, 0, 255];
|
|
95
149
|
this._showGrid = false;
|
|
96
150
|
|
|
97
151
|
// binds
|
|
98
152
|
this._onPointerDown = this._onPointerDown.bind(this);
|
|
99
153
|
this._onPointerMove = this._onPointerMove.bind(this);
|
|
100
154
|
this._onPointerUp = this._onPointerUp.bind(this);
|
|
155
|
+
|
|
156
|
+
// transform methods bound (useful if passing as callbacks)
|
|
157
|
+
this.flipHorizontal = this.flipHorizontal.bind(this);
|
|
158
|
+
this.flipVertical = this.flipVertical.bind(this);
|
|
159
|
+
this.rotateCW = this.rotateCW.bind(this);
|
|
160
|
+
this.rotateCCW = this.rotateCCW.bind(this);
|
|
101
161
|
}
|
|
102
162
|
|
|
103
163
|
static get observedAttributes() {
|
|
@@ -111,16 +171,29 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
111
171
|
}
|
|
112
172
|
|
|
113
173
|
connectedCallback() {
|
|
174
|
+
// respect attributes if present
|
|
114
175
|
if (this.hasAttribute('width')) this._width = Math.max(1, parseInt(this.getAttribute('width'), 10));
|
|
115
176
|
if (this.hasAttribute('height')) this._height = Math.max(1, parseInt(this.getAttribute('height'), 10));
|
|
116
177
|
if (this.hasAttribute('pixel-size')) this._pixelSize = Math.max(1, parseInt(this.getAttribute('pixel-size'), 10));
|
|
117
178
|
|
|
118
179
|
this._setupContextsAndSize();
|
|
119
180
|
|
|
181
|
+
// set initial UI control values (keeps in sync with attributes)
|
|
182
|
+
if (this._widthInput) this._widthInput.value = String(this._width);
|
|
183
|
+
if (this._heightInput) this._heightInput.value = String(this._height);
|
|
184
|
+
if (this._pixelSizeInput) this._pixelSizeInput.value = String(this._pixelSize);
|
|
185
|
+
if (this._brushSizeInput) this._brushSizeInput.value = String(this._brushSize);
|
|
186
|
+
|
|
187
|
+
// initialize color & opacity UI
|
|
188
|
+
if (this._colorInput) this._colorInput.value = this._rgbaToHex(this._brushColor);
|
|
189
|
+
if (this._opacityRange) this._opacityRange.value = String(this._brushColor[3]);
|
|
190
|
+
if (this._opacityNumber) this._opacityNumber.value = String(this._brushColor[3]);
|
|
191
|
+
|
|
120
192
|
// UI events
|
|
121
193
|
this._colorInput.addEventListener('input', (e) => {
|
|
122
|
-
const
|
|
123
|
-
|
|
194
|
+
const rgb = this._hexToRgba(e.target.value);
|
|
195
|
+
// keep current alpha
|
|
196
|
+
this.setBrushColor([rgb[0], rgb[1], rgb[2], this._brushColor[3]]);
|
|
124
197
|
});
|
|
125
198
|
this._toolSelect.addEventListener('change', (e) => this.setTool(e.target.value));
|
|
126
199
|
this._brushSizeInput.addEventListener('change', (e) => this.setBrushSize(parseInt(e.target.value, 10) || 1));
|
|
@@ -132,6 +205,42 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
132
205
|
this._renderGrid();
|
|
133
206
|
});
|
|
134
207
|
|
|
208
|
+
// opacity controls - keep range and number in sync
|
|
209
|
+
if (this._opacityRange) {
|
|
210
|
+
this._opacityRange.addEventListener('input', (e) => {
|
|
211
|
+
const v = Math.max(0, Math.min(255, parseInt(e.target.value, 10) || 0));
|
|
212
|
+
this.setBrushAlpha(v);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (this._opacityNumber) {
|
|
216
|
+
this._opacityNumber.addEventListener('change', (e) => {
|
|
217
|
+
const v = Math.max(0, Math.min(255, parseInt(e.target.value, 10) || 0));
|
|
218
|
+
this.setBrushAlpha(v);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// width/height change -> resize (preserve existing content)
|
|
223
|
+
if (this._widthInput)
|
|
224
|
+
this._widthInput.addEventListener('change', (e) => {
|
|
225
|
+
const val = Math.max(1, parseInt(e.target.value, 10) || 1);
|
|
226
|
+
// keep value synced (will update input again in resize)
|
|
227
|
+
this.resize(val, this._height, { preserve: true });
|
|
228
|
+
});
|
|
229
|
+
if (this._heightInput)
|
|
230
|
+
this._heightInput.addEventListener('change', (e) => {
|
|
231
|
+
const val = Math.max(1, parseInt(e.target.value, 10) || 1);
|
|
232
|
+
this.resize(this._width, val, { preserve: true });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// transform buttons
|
|
236
|
+
if (this._flipHBtn) this._flipHBtn.addEventListener('click', this.flipHorizontal);
|
|
237
|
+
if (this._flipVBtn) this._flipVBtn.addEventListener('click', this.flipVertical);
|
|
238
|
+
if (this._rotCWBtn) this._rotCWBtn.addEventListener('click', this.rotateCW);
|
|
239
|
+
if (this._rotCCWBtn) this._rotCCWBtn.addEventListener('click', this.rotateCCW);
|
|
240
|
+
|
|
241
|
+
// clear button (makes canvas fully transparent)
|
|
242
|
+
if (this._clearBtn) this._clearBtn.addEventListener('click', () => this.clear([0, 0, 0, 0]));
|
|
243
|
+
|
|
135
244
|
// Export/Import
|
|
136
245
|
this._exportBtn.addEventListener('click', () => this.exportPNG());
|
|
137
246
|
this._exportJsonBtn.addEventListener('click', () => {
|
|
@@ -169,6 +278,14 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
169
278
|
this._pixelCanvas.removeEventListener('pointerdown', this._onPointerDown);
|
|
170
279
|
window.removeEventListener('pointermove', this._onPointerMove);
|
|
171
280
|
window.removeEventListener('pointerup', this._onPointerUp);
|
|
281
|
+
|
|
282
|
+
if (this._flipHBtn) this._flipHBtn.removeEventListener('click', this.flipHorizontal);
|
|
283
|
+
if (this._flipVBtn) this._flipVBtn.removeEventListener('click', this.flipVertical);
|
|
284
|
+
if (this._rotCWBtn) this._rotCWBtn.removeEventListener('click', this.rotateCW);
|
|
285
|
+
if (this._rotCCWBtn) this._rotCCWBtn.removeEventListener('click', this.rotateCCW);
|
|
286
|
+
if (this._clearBtn) this._clearBtn.removeEventListener('click', () => this.clear([0, 0, 0, 0]));
|
|
287
|
+
if (this._opacityRange) this._opacityRange.removeEventListener('input', () => {});
|
|
288
|
+
if (this._opacityNumber) this._opacityNumber.removeEventListener('change', () => {});
|
|
172
289
|
}
|
|
173
290
|
|
|
174
291
|
// ---------------- Matrix helpers ----------------
|
|
@@ -227,6 +344,13 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
227
344
|
this._width = nw;
|
|
228
345
|
this._height = nh;
|
|
229
346
|
this._matrix = newMat;
|
|
347
|
+
|
|
348
|
+
// keep inputs and attributes in sync
|
|
349
|
+
if (this._widthInput) this._widthInput.value = String(this._width);
|
|
350
|
+
if (this._heightInput) this._heightInput.value = String(this._height);
|
|
351
|
+
this.setAttribute('width', String(this._width));
|
|
352
|
+
this.setAttribute('height', String(this._height));
|
|
353
|
+
|
|
230
354
|
this._setupContextsAndSize();
|
|
231
355
|
this.render();
|
|
232
356
|
this.dispatchEvent(new CustomEvent('resize', { detail: { width: nw, height: nh } }));
|
|
@@ -365,10 +489,33 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
365
489
|
this._tool = name;
|
|
366
490
|
if (this._toolSelect) this._toolSelect.value = name;
|
|
367
491
|
}
|
|
492
|
+
|
|
493
|
+
// set full RGBA brush color (alpha optional)
|
|
368
494
|
setBrushColor(rgba) {
|
|
369
|
-
|
|
495
|
+
if (!Array.isArray(rgba) || rgba.length < 3) return;
|
|
496
|
+
const r = this._clampInt(rgba[0]);
|
|
497
|
+
const g = this._clampInt(rgba[1]);
|
|
498
|
+
const b = this._clampInt(rgba[2]);
|
|
499
|
+
const a = typeof rgba[3] === 'number' ? this._clampInt(rgba[3]) : this._brushColor[3];
|
|
500
|
+
this._brushColor = [r, g, b, a];
|
|
370
501
|
if (this._colorInput) this._colorInput.value = this._rgbaToHex(this._brushColor);
|
|
502
|
+
if (this._opacityRange) this._opacityRange.value = String(this._brushColor[3]);
|
|
503
|
+
if (this._opacityNumber) this._opacityNumber.value = String(this._brushColor[3]);
|
|
371
504
|
}
|
|
505
|
+
|
|
506
|
+
// set brush alpha (0-255)
|
|
507
|
+
setBrushAlpha(a) {
|
|
508
|
+
const v = Math.max(0, Math.min(255, Math.floor(Number(a) || 0)));
|
|
509
|
+
this._brushColor[3] = v;
|
|
510
|
+
if (this._opacityRange) this._opacityRange.value = String(v);
|
|
511
|
+
if (this._opacityNumber) this._opacityNumber.value = String(v);
|
|
512
|
+
// keep color input (hex) representing rgb only
|
|
513
|
+
if (this._colorInput) this._colorInput.value = this._rgbaToHex(this._brushColor);
|
|
514
|
+
}
|
|
515
|
+
getBrushAlpha() {
|
|
516
|
+
return this._brushColor[3];
|
|
517
|
+
}
|
|
518
|
+
|
|
372
519
|
setBrushSize(n) {
|
|
373
520
|
this._brushSize = Math.max(1, Math.floor(n));
|
|
374
521
|
if (this._brushSizeInput) this._brushSizeInput.value = this._brushSize;
|
|
@@ -584,6 +731,84 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
584
731
|
.slice(1)}`;
|
|
585
732
|
}
|
|
586
733
|
|
|
734
|
+
// ---------------- Transform helpers (flip/rotate) ----------------
|
|
735
|
+
flipHorizontal() {
|
|
736
|
+
// reverse each row (mirror horizontally)
|
|
737
|
+
for (let y = 0; y < this._height; y++) {
|
|
738
|
+
this._matrix[y].reverse();
|
|
739
|
+
}
|
|
740
|
+
this.render();
|
|
741
|
+
this.dispatchEvent(new CustomEvent('transform', { detail: { type: 'flip-horizontal' } }));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
flipVertical() {
|
|
745
|
+
// reverse the order of rows (mirror vertically)
|
|
746
|
+
this._matrix.reverse();
|
|
747
|
+
this.render();
|
|
748
|
+
this.dispatchEvent(new CustomEvent('transform', { detail: { type: 'flip-vertical' } }));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
rotateCW() {
|
|
752
|
+
// rotate +90 degrees (clockwise)
|
|
753
|
+
const oldH = this._height;
|
|
754
|
+
const oldW = this._width;
|
|
755
|
+
const newW = oldH;
|
|
756
|
+
const newH = oldW;
|
|
757
|
+
const newMat = this._createEmptyMatrix(newW, newH);
|
|
758
|
+
for (let y = 0; y < oldH; y++) {
|
|
759
|
+
for (let x = 0; x < oldW; x++) {
|
|
760
|
+
const px = this._matrix[y][x] ? this._matrix[y][x].slice() : [0, 0, 0, 0];
|
|
761
|
+
const newX = oldH - 1 - y; // column in new matrix
|
|
762
|
+
const newY = x; // row in new matrix
|
|
763
|
+
newMat[newY][newX] = px;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
this._width = newW;
|
|
767
|
+
this._height = newH;
|
|
768
|
+
this._matrix = newMat;
|
|
769
|
+
// keep inputs/attributes in sync
|
|
770
|
+
if (this._widthInput) this._widthInput.value = String(this._width);
|
|
771
|
+
if (this._heightInput) this._heightInput.value = String(this._height);
|
|
772
|
+
this.setAttribute('width', String(this._width));
|
|
773
|
+
this.setAttribute('height', String(this._height));
|
|
774
|
+
|
|
775
|
+
this._setupContextsAndSize();
|
|
776
|
+
this.render();
|
|
777
|
+
this.dispatchEvent(
|
|
778
|
+
new CustomEvent('transform', { detail: { type: 'rotate-cw', width: this._width, height: this._height } }),
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
rotateCCW() {
|
|
783
|
+
// rotate -90 degrees (counter-clockwise)
|
|
784
|
+
const oldH = this._height;
|
|
785
|
+
const oldW = this._width;
|
|
786
|
+
const newW = oldH;
|
|
787
|
+
const newH = oldW;
|
|
788
|
+
const newMat = this._createEmptyMatrix(newW, newH);
|
|
789
|
+
for (let y = 0; y < oldH; y++) {
|
|
790
|
+
for (let x = 0; x < oldW; x++) {
|
|
791
|
+
const px = this._matrix[y][x] ? this._matrix[y][x].slice() : [0, 0, 0, 0];
|
|
792
|
+
const newX = y; // column in new matrix
|
|
793
|
+
const newY = oldW - 1 - x; // row in new matrix
|
|
794
|
+
newMat[newY][newX] = px;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
this._width = newW;
|
|
798
|
+
this._height = newH;
|
|
799
|
+
this._matrix = newMat;
|
|
800
|
+
if (this._widthInput) this._widthInput.value = String(this._width);
|
|
801
|
+
if (this._heightInput) this._heightInput.value = String(this._height);
|
|
802
|
+
this.setAttribute('width', String(this._width));
|
|
803
|
+
this.setAttribute('height', String(this._height));
|
|
804
|
+
|
|
805
|
+
this._setupContextsAndSize();
|
|
806
|
+
this.render();
|
|
807
|
+
this.dispatchEvent(
|
|
808
|
+
new CustomEvent('transform', { detail: { type: 'rotate-ccw', width: this._width, height: this._height } }),
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
587
812
|
// ---------------- Properties ----------------
|
|
588
813
|
get width() {
|
|
589
814
|
return this._width;
|