underpost 2.8.866 → 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 +52 -36
- package/bin/build.js +1 -0
- package/bin/deploy.js +30 -1
- package/bin/file.js +3 -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 +66 -63
- 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 +20 -64
- package/src/server/process.js +2 -1
- package/test/api.test.js +3 -2
- package/bin/cyberia0.js +0 -78
|
@@ -51,6 +51,7 @@ const Modal = {
|
|
|
51
51
|
RouterInstance: {},
|
|
52
52
|
disableTools: [],
|
|
53
53
|
observer: false,
|
|
54
|
+
disableBoxShadow: false,
|
|
54
55
|
},
|
|
55
56
|
) {
|
|
56
57
|
if (options.heightBottomBar === undefined) options.heightBottomBar = 50;
|
|
@@ -89,6 +90,11 @@ const Modal = {
|
|
|
89
90
|
onHome: {},
|
|
90
91
|
homeModals: options.homeModals ? options.homeModals : [],
|
|
91
92
|
query: options.query ? `${window.location.search}` : undefined,
|
|
93
|
+
getTop: () => window.innerHeight - (options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar),
|
|
94
|
+
getHeight: () =>
|
|
95
|
+
window.innerHeight -
|
|
96
|
+
(options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) -
|
|
97
|
+
(options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar),
|
|
92
98
|
};
|
|
93
99
|
|
|
94
100
|
if (idModal !== 'main-body' && options.mode !== 'view') {
|
|
@@ -117,12 +123,7 @@ const Modal = {
|
|
|
117
123
|
|
|
118
124
|
Responsive.Event[`view-${idModal}`] = () => {
|
|
119
125
|
if (!this.Data[idModal]) return delete Responsive.Event[`view-${idModal}`];
|
|
120
|
-
if (this.Data[idModal].slideMenu)
|
|
121
|
-
s(`.${idModal}`).style.height = `${
|
|
122
|
-
window.innerHeight -
|
|
123
|
-
(options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) -
|
|
124
|
-
(options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
|
|
125
|
-
}px`;
|
|
126
|
+
if (this.Data[idModal].slideMenu) s(`.${idModal}`).style.height = `${this.Data[idModal].getHeight()}px`;
|
|
126
127
|
};
|
|
127
128
|
Responsive.Event[`view-${idModal}`]();
|
|
128
129
|
|
|
@@ -207,11 +208,7 @@ const Modal = {
|
|
|
207
208
|
const { barConfig } = options;
|
|
208
209
|
options.style = {
|
|
209
210
|
position: 'absolute',
|
|
210
|
-
height: `${
|
|
211
|
-
window.innerHeight -
|
|
212
|
-
(options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) -
|
|
213
|
-
(options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
|
|
214
|
-
}px`,
|
|
211
|
+
height: `${Modal.Data[idModal].getHeight()}px`,
|
|
215
212
|
width: `${slideMenuWidth}px`,
|
|
216
213
|
// 'overflow-x': 'hidden',
|
|
217
214
|
// overflow: 'visible', // required for tooltip
|
|
@@ -239,11 +236,7 @@ const Modal = {
|
|
|
239
236
|
if (this.Data[_idModal].slideMenu && this.Data[_idModal].slideMenu.id === idModal)
|
|
240
237
|
this.Data[_idModal].slideMenu.callBack();
|
|
241
238
|
}
|
|
242
|
-
s(`.${idModal}`).style.height = `${
|
|
243
|
-
window.innerHeight -
|
|
244
|
-
(options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) -
|
|
245
|
-
(options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
|
|
246
|
-
}px`;
|
|
239
|
+
s(`.${idModal}`).style.height = `${Modal.Data[idModal].getHeight()}px`;
|
|
247
240
|
if (s(`.main-body-top`)) {
|
|
248
241
|
if (Modal.mobileModal()) {
|
|
249
242
|
if (s(`.btn-menu-${idModal}`).classList.contains('hide') && collapseSlideMenuWidth !== slideMenuWidth)
|
|
@@ -753,9 +746,12 @@ const Modal = {
|
|
|
753
746
|
s(`.main-btn-${results[currentKeyBoardSearchBoxIndex].routerId}`).click();
|
|
754
747
|
Modal.removeModal(searchBoxHistoryId);
|
|
755
748
|
};
|
|
756
|
-
|
|
749
|
+
let boxHistoryDelayRender = 0;
|
|
757
750
|
const searchBoxHistoryOpen = async () => {
|
|
758
|
-
if (
|
|
751
|
+
if (boxHistoryDelayRender) return;
|
|
752
|
+
boxHistoryDelayRender = 1000;
|
|
753
|
+
setTimeout(() => (boxHistoryDelayRender = 0));
|
|
754
|
+
if (!s(`.${searchBoxHistoryId}`)) {
|
|
759
755
|
const { barConfig } = await Themes[Css.currentTheme]();
|
|
760
756
|
barConfig.buttons.maximize.disabled = true;
|
|
761
757
|
barConfig.buttons.minimize.disabled = true;
|
|
@@ -763,7 +759,7 @@ const Modal = {
|
|
|
763
759
|
barConfig.buttons.menu.disabled = true;
|
|
764
760
|
barConfig.buttons.close.disabled = false;
|
|
765
761
|
await Modal.Render({
|
|
766
|
-
id,
|
|
762
|
+
id: searchBoxHistoryId,
|
|
767
763
|
barConfig,
|
|
768
764
|
title: html`<div class="search-box-recent-title">
|
|
769
765
|
${renderViewTitle({
|
|
@@ -964,6 +960,7 @@ const Modal = {
|
|
|
964
960
|
heightBottomBar: originHeightBottomBar,
|
|
965
961
|
barMode: options.barMode,
|
|
966
962
|
observer: true,
|
|
963
|
+
disableBoxShadow: true,
|
|
967
964
|
});
|
|
968
965
|
const maxWidthInputSearchBox = 450;
|
|
969
966
|
const paddingInputSearchBox = 5;
|
|
@@ -985,12 +982,7 @@ const Modal = {
|
|
|
985
982
|
s(`.top-bar-search-box`).style.top = `${
|
|
986
983
|
(originHeightTopBar - s(`.top-bar-search-box`).clientHeight) / 2
|
|
987
984
|
}px`;
|
|
988
|
-
if (this.Data[id].slideMenu)
|
|
989
|
-
s(`.${id}`).style.height = `${
|
|
990
|
-
window.innerHeight -
|
|
991
|
-
(options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) -
|
|
992
|
-
(options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
|
|
993
|
-
}px`;
|
|
985
|
+
if (this.Data[id].slideMenu) s(`.${id}`).style.height = `${Modal.Data[id].getHeight()}px`;
|
|
994
986
|
};
|
|
995
987
|
Responsive.Event[`view-${id}`]();
|
|
996
988
|
Keyboard.instanceMultiPressKey({
|
|
@@ -1119,9 +1111,7 @@ const Modal = {
|
|
|
1119
1111
|
if (!this.Data[id] || !s(`.${id}`)) return delete Responsive.Event[`view-${id}`];
|
|
1120
1112
|
// <div class="in fll right-offset-menu-bottom-bar" style="height: 100%"></div>
|
|
1121
1113
|
// s(`.right-offset-menu-bottom-bar`).style.width = `${window.innerWidth - slideMenuWidth}px`;
|
|
1122
|
-
s(`.${id}`).style.top = `${
|
|
1123
|
-
window.innerHeight - (options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
|
|
1124
|
-
}px`;
|
|
1114
|
+
s(`.${id}`).style.top = `${Modal.Data[id].getTop()}px`;
|
|
1125
1115
|
};
|
|
1126
1116
|
Responsive.Event[`view-${id}`]();
|
|
1127
1117
|
}
|
|
@@ -1293,11 +1283,7 @@ const Modal = {
|
|
|
1293
1283
|
s(`.main-body-btn-ui-close`).classList.contains('hide') &&
|
|
1294
1284
|
s(`.btn-restore-${id}`).style.display !== 'none'
|
|
1295
1285
|
? `${window.innerHeight}px`
|
|
1296
|
-
: `${
|
|
1297
|
-
window.innerHeight -
|
|
1298
|
-
(options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) -
|
|
1299
|
-
(options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
|
|
1300
|
-
}px`;
|
|
1286
|
+
: `${Modal.Data[id].getHeight()}px`;
|
|
1301
1287
|
|
|
1302
1288
|
if (
|
|
1303
1289
|
s(`.main-body-btn-ui-close`).classList.contains('hide') &&
|
|
@@ -1405,7 +1391,11 @@ const Modal = {
|
|
|
1405
1391
|
}
|
|
1406
1392
|
</style>
|
|
1407
1393
|
${renderStyleTag(`style-${idModal}`, `.${idModal}`, options)}
|
|
1408
|
-
<div
|
|
1394
|
+
<div
|
|
1395
|
+
class="fix ${options && options.class ? options.class : ''} modal ${options.disableBoxShadow
|
|
1396
|
+
? ''
|
|
1397
|
+
: 'box-shadow'} ${idModal}"
|
|
1398
|
+
>
|
|
1409
1399
|
<div class="abs modal-handle-${idModal}"></div>
|
|
1410
1400
|
<div class="in modal-html-${idModal}">
|
|
1411
1401
|
<div class="stq bar-default-modal bar-default-modal-${idModal}">
|
|
@@ -1545,18 +1535,6 @@ const Modal = {
|
|
|
1545
1535
|
s(`.btn-icon-menu-back`).classList.add('hide');
|
|
1546
1536
|
if (s(`.menu-btn-container-main`)) s(`.menu-btn-container-main`).classList.remove('hide');
|
|
1547
1537
|
};
|
|
1548
|
-
this.onHomeRouterEvent = async () => {
|
|
1549
|
-
for (const keyModal of Object.keys(this.Data)) {
|
|
1550
|
-
if (
|
|
1551
|
-
![idModal, 'main-body-top', 'main-body'].concat(this.Data[idModal]?.homeModals || []).includes(keyModal)
|
|
1552
|
-
)
|
|
1553
|
-
if (s(`.btn-close-${keyModal}`)) s(`.btn-close-${keyModal}`).click();
|
|
1554
|
-
backMenuButtonEvent();
|
|
1555
|
-
}
|
|
1556
|
-
if (s(`.btn-close-modal-menu`)) s(`.btn-close-modal-menu`).click();
|
|
1557
|
-
setPath(getProxyPath());
|
|
1558
|
-
setDocTitle();
|
|
1559
|
-
};
|
|
1560
1538
|
s(`.main-btn-home`).onclick = async () => {
|
|
1561
1539
|
// await this.onHomeRouterEvent();
|
|
1562
1540
|
s(`.action-btn-home`).click();
|
|
@@ -1777,12 +1755,8 @@ const Modal = {
|
|
|
1777
1755
|
if (!s(`.${idModal}`)) return;
|
|
1778
1756
|
this.removeModal(idModal);
|
|
1779
1757
|
// Handle modal route change
|
|
1780
|
-
if (options.route) {
|
|
1781
|
-
closeModalRouteChangeEvent({
|
|
1782
|
-
route: options.route,
|
|
1783
|
-
RouterInstance: options.RouterInstance,
|
|
1784
|
-
homeCid: Modal.homeCid,
|
|
1785
|
-
});
|
|
1758
|
+
if (options.route || options.query) {
|
|
1759
|
+
closeModalRouteChangeEvent({ closedId: idModal, homeCid: Modal.homeCid });
|
|
1786
1760
|
}
|
|
1787
1761
|
}, 300);
|
|
1788
1762
|
};
|
|
@@ -1918,11 +1892,7 @@ const Modal = {
|
|
|
1918
1892
|
if (s(`.btn-restore-${idModal}`) && s(`.btn-restore-${idModal}`).style.display !== 'none') {
|
|
1919
1893
|
s(`.${idModal}`).style.height = s(`.main-body-btn-ui-close`).classList.contains('hide')
|
|
1920
1894
|
? `${window.innerHeight}px`
|
|
1921
|
-
: `${
|
|
1922
|
-
window.innerHeight -
|
|
1923
|
-
(options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) -
|
|
1924
|
-
(options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
|
|
1925
|
-
}px`;
|
|
1895
|
+
: `${Modal.Data[idModal].getHeight()}px`;
|
|
1926
1896
|
}
|
|
1927
1897
|
s(`.${idModal}`).style.top = s(`.main-body-btn-ui-close`).classList.contains('hide')
|
|
1928
1898
|
? `0px`
|
|
@@ -1981,7 +1951,45 @@ const Modal = {
|
|
|
1981
1951
|
...this.Data[idModal],
|
|
1982
1952
|
};
|
|
1983
1953
|
},
|
|
1984
|
-
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
|
+
},
|
|
1985
1993
|
currentTopModalId: '',
|
|
1986
1994
|
zIndexSync: function ({ idModal }) {
|
|
1987
1995
|
setTimeout(() => {
|
|
@@ -2010,11 +2018,6 @@ const Modal = {
|
|
|
2010
2018
|
setTopModalCallback: function (idModal) {
|
|
2011
2019
|
s(`.${idModal}`).style.zIndex = '4';
|
|
2012
2020
|
this.currentTopModalId = `${idModal}`;
|
|
2013
|
-
if (
|
|
2014
|
-
this.Data[idModal].query &&
|
|
2015
|
-
`${location.pathname}${window.location.search}` !== `${location.pathname}${this.Data[idModal].query}`
|
|
2016
|
-
)
|
|
2017
|
-
setPath(`${location.pathname}${this.Data[idModal].query}`);
|
|
2018
2021
|
},
|
|
2019
2022
|
mobileModal: () => window.innerWidth < 600 || window.innerHeight < 600,
|
|
2020
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;
|