senangwebs-photobooth 1.0.1 → 2.0.2
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 +219 -235
- package/dist/swp.css +884 -344
- package/dist/swp.js +1 -1
- package/examples/data-attribute.html +69 -0
- package/examples/index.html +56 -51
- package/examples/studio.html +83 -0
- package/package.json +12 -5
- package/src/css/swp.css +884 -344
- package/src/js/core/Canvas.js +398 -0
- package/src/js/core/EventEmitter.js +188 -0
- package/src/js/core/History.js +250 -0
- package/src/js/core/Keyboard.js +323 -0
- package/src/js/filters/FilterManager.js +248 -0
- package/src/js/index.js +48 -0
- package/src/js/io/Clipboard.js +52 -0
- package/src/js/io/FileManager.js +150 -0
- package/src/js/layers/BlendModes.js +342 -0
- package/src/js/layers/Layer.js +415 -0
- package/src/js/layers/LayerManager.js +459 -0
- package/src/js/selection/Selection.js +167 -0
- package/src/js/swp.js +297 -709
- package/src/js/tools/BaseTool.js +264 -0
- package/src/js/tools/BrushTool.js +314 -0
- package/src/js/tools/CropTool.js +400 -0
- package/src/js/tools/EraserTool.js +155 -0
- package/src/js/tools/EyedropperTool.js +184 -0
- package/src/js/tools/FillTool.js +109 -0
- package/src/js/tools/GradientTool.js +141 -0
- package/src/js/tools/HandTool.js +51 -0
- package/src/js/tools/MarqueeTool.js +103 -0
- package/src/js/tools/MoveTool.js +465 -0
- package/src/js/tools/ShapeTool.js +285 -0
- package/src/js/tools/TextTool.js +253 -0
- package/src/js/tools/ToolManager.js +277 -0
- package/src/js/tools/ZoomTool.js +68 -0
- package/src/js/ui/ColorManager.js +71 -0
- package/src/js/ui/UI.js +1211 -0
- package/swp_preview1.png +0 -0
- package/swp_preview2.png +0 -0
- package/webpack.config.js +4 -11
- package/dist/styles.js +0 -1
- package/examples/customization.html +0 -360
- package/spec.md +0 -239
- package/swp_preview.png +0 -0
package/src/js/ui/UI.js
ADDED
|
@@ -0,0 +1,1211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Photobooth - UI Manager
|
|
3
|
+
* TOAST UI-Inspired Simple Layout
|
|
4
|
+
* @version 3.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Events } from '../core/EventEmitter.js';
|
|
8
|
+
import '@bookklik/senangstart-icons';
|
|
9
|
+
|
|
10
|
+
export class UI {
|
|
11
|
+
constructor(app) {
|
|
12
|
+
this.app = app;
|
|
13
|
+
this.container = null;
|
|
14
|
+
this.currentMenu = null;
|
|
15
|
+
this.isFullscreen = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
init(container) {
|
|
19
|
+
this.container = container;
|
|
20
|
+
this.container.classList.add('swp-app');
|
|
21
|
+
this.createLayout();
|
|
22
|
+
this.bindEvents();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
createLayout() {
|
|
26
|
+
this.container.innerHTML = `
|
|
27
|
+
<!-- Header Bar -->
|
|
28
|
+
<div class="swp-header">
|
|
29
|
+
<div class="swp-header-left">
|
|
30
|
+
<button class="swp-header-btn" data-action="load" title="Load Image">
|
|
31
|
+
<ss-icon icon="folder-open" thickness="2"></ss-icon>
|
|
32
|
+
<span>Load</span>
|
|
33
|
+
</button>
|
|
34
|
+
<div class="swp-download-dropdown">
|
|
35
|
+
<button class="swp-header-btn" data-action="toggle-download" title="Download">
|
|
36
|
+
<ss-icon icon="save" thickness="2"></ss-icon>
|
|
37
|
+
<span>Download</span>
|
|
38
|
+
<ss-icon icon="chevron-down" thickness="2" class="swp-dropdown-arrow"></ss-icon>
|
|
39
|
+
</button>
|
|
40
|
+
<div class="swp-dropdown-menu" hidden>
|
|
41
|
+
<button class="swp-dropdown-item" data-format="png">PNG</button>
|
|
42
|
+
<button class="swp-dropdown-item" data-format="jpeg">JPEG</button>
|
|
43
|
+
<button class="swp-dropdown-item" data-format="webp">WebP</button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="swp-header-center">
|
|
48
|
+
<button class="swp-icon-btn" data-action="undo" title="Undo (Ctrl+Z)">
|
|
49
|
+
<ss-icon icon="arrow-rotate-ccw" thickness="2"></ss-icon>
|
|
50
|
+
</button>
|
|
51
|
+
<button class="swp-icon-btn" data-action="redo" title="Redo (Ctrl+Shift+Z)">
|
|
52
|
+
<ss-icon icon="arrow-rotate-cw" thickness="2"></ss-icon>
|
|
53
|
+
</button>
|
|
54
|
+
<div class="swp-divider"></div>
|
|
55
|
+
<button class="swp-icon-btn" data-action="history" title="History">
|
|
56
|
+
<ss-icon icon="clock" thickness="2"></ss-icon>
|
|
57
|
+
</button>
|
|
58
|
+
<button class="swp-icon-btn" data-action="layers" title="Layers">
|
|
59
|
+
<ss-icon icon="layer-stacks" thickness="2"></ss-icon>
|
|
60
|
+
</button>
|
|
61
|
+
<div class="swp-divider"></div>
|
|
62
|
+
<button class="swp-icon-btn" data-action="reset" title="Reset">
|
|
63
|
+
<ss-icon icon="time-reset" thickness="2"></ss-icon>
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="swp-header-right">
|
|
67
|
+
<button class="swp-icon-btn" data-action="center" title="Center Canvas">
|
|
68
|
+
<ss-icon icon="container" thickness="2"></ss-icon>
|
|
69
|
+
</button>
|
|
70
|
+
<button class="swp-icon-btn" data-action="fullscreen" title="Fullscreen">
|
|
71
|
+
<ss-icon icon="focus" thickness="2"></ss-icon>
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- Main Workspace (full width canvas) -->
|
|
77
|
+
<div class="swp-workspace"></div>
|
|
78
|
+
|
|
79
|
+
<!-- Side Panel (History / Layers) -->
|
|
80
|
+
<div class="swp-side-panel" hidden>
|
|
81
|
+
<div class="swp-side-panel-header">
|
|
82
|
+
<span class="swp-side-panel-title">Panel</span>
|
|
83
|
+
<button class="swp-icon-btn swp-side-panel-close" data-action="close-panel">
|
|
84
|
+
<ss-icon icon="cross" thickness="2"></ss-icon>
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="swp-side-panel-content"></div>
|
|
88
|
+
</div>
|
|
89
|
+
<!-- Sub-menu Panel (contextual options) -->
|
|
90
|
+
<div class="swp-submenu" hidden></div>
|
|
91
|
+
|
|
92
|
+
<!-- Bottom Menu Bar -->
|
|
93
|
+
<div class="swp-menu-bar">
|
|
94
|
+
<button class="swp-menu-item" data-menu="crop">
|
|
95
|
+
<ss-icon icon="crop" thickness="2"></ss-icon>
|
|
96
|
+
<span>Crop</span>
|
|
97
|
+
</button>
|
|
98
|
+
<button class="swp-menu-item" data-menu="rotate">
|
|
99
|
+
<ss-icon icon="arrow-path" thickness="2"></ss-icon>
|
|
100
|
+
<span>Rotate</span>
|
|
101
|
+
</button>
|
|
102
|
+
<button class="swp-menu-item" data-menu="flip">
|
|
103
|
+
<ss-icon icon="arrow-left-arrow-right" thickness="2"></ss-icon>
|
|
104
|
+
<span>Flip</span>
|
|
105
|
+
</button>
|
|
106
|
+
<button class="swp-menu-item" data-menu="resize">
|
|
107
|
+
<ss-icon icon="sliders-vertical" thickness="2"></ss-icon>
|
|
108
|
+
<span>Resize</span>
|
|
109
|
+
</button>
|
|
110
|
+
<button class="swp-menu-item" data-menu="draw">
|
|
111
|
+
<ss-icon icon="pencil" thickness="2"></ss-icon>
|
|
112
|
+
<span>Draw</span>
|
|
113
|
+
</button>
|
|
114
|
+
<button class="swp-menu-item" data-menu="shape">
|
|
115
|
+
<ss-icon icon="shapes" thickness="2"></ss-icon>
|
|
116
|
+
<span>Shape</span>
|
|
117
|
+
</button>
|
|
118
|
+
<button class="swp-menu-item" data-menu="text">
|
|
119
|
+
<ss-icon icon="text" thickness="2"></ss-icon>
|
|
120
|
+
<span>Text</span>
|
|
121
|
+
</button>
|
|
122
|
+
<button class="swp-menu-item" data-menu="filter">
|
|
123
|
+
<ss-icon icon="magic-wand" thickness="2"></ss-icon>
|
|
124
|
+
<span>Filter</span>
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
this.bindHeaderActions();
|
|
130
|
+
this.bindMenuActions();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
bindHeaderActions() {
|
|
134
|
+
const header = this.container.querySelector('.swp-header');
|
|
135
|
+
const sidePanel = this.container.querySelector('.swp-side-panel');
|
|
136
|
+
|
|
137
|
+
// Close panel button
|
|
138
|
+
sidePanel?.querySelector('[data-action="close-panel"]')?.addEventListener('click', () => {
|
|
139
|
+
this.closeSidePanel();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Download dropdown handling
|
|
143
|
+
const downloadDropdown = this.container.querySelector('.swp-download-dropdown');
|
|
144
|
+
const dropdownMenu = downloadDropdown?.querySelector('.swp-dropdown-menu');
|
|
145
|
+
|
|
146
|
+
// Format selection
|
|
147
|
+
dropdownMenu?.querySelectorAll('[data-format]').forEach(item => {
|
|
148
|
+
item.addEventListener('click', (e) => {
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
const format = item.dataset.format;
|
|
151
|
+
this.app.file.export(format);
|
|
152
|
+
dropdownMenu.hidden = true;
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Close dropdown when clicking outside
|
|
157
|
+
document.addEventListener('click', (e) => {
|
|
158
|
+
if (dropdownMenu && !downloadDropdown.contains(e.target)) {
|
|
159
|
+
dropdownMenu.hidden = true;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
header.addEventListener('click', (e) => {
|
|
164
|
+
const btn = e.target.closest('[data-action]');
|
|
165
|
+
if (!btn) return;
|
|
166
|
+
|
|
167
|
+
const action = btn.dataset.action;
|
|
168
|
+
switch (action) {
|
|
169
|
+
case 'load':
|
|
170
|
+
this.openFileDialog();
|
|
171
|
+
break;
|
|
172
|
+
case 'toggle-download':
|
|
173
|
+
if (dropdownMenu) {
|
|
174
|
+
dropdownMenu.hidden = !dropdownMenu.hidden;
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
case 'undo':
|
|
178
|
+
this.app.history.undo();
|
|
179
|
+
break;
|
|
180
|
+
case 'redo':
|
|
181
|
+
this.app.history.redo();
|
|
182
|
+
break;
|
|
183
|
+
case 'history':
|
|
184
|
+
this.toggleSidePanel('history');
|
|
185
|
+
break;
|
|
186
|
+
case 'layers':
|
|
187
|
+
this.toggleSidePanel('layers');
|
|
188
|
+
break;
|
|
189
|
+
case 'reset':
|
|
190
|
+
this.resetCanvas();
|
|
191
|
+
break;
|
|
192
|
+
case 'center':
|
|
193
|
+
this.app.canvas.fitToScreen();
|
|
194
|
+
break;
|
|
195
|
+
case 'fullscreen':
|
|
196
|
+
this.toggleFullscreen();
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
bindMenuActions() {
|
|
203
|
+
const menuBar = this.container.querySelector('.swp-menu-bar');
|
|
204
|
+
|
|
205
|
+
menuBar.addEventListener('click', (e) => {
|
|
206
|
+
const menuItem = e.target.closest('.swp-menu-item');
|
|
207
|
+
if (!menuItem) return;
|
|
208
|
+
|
|
209
|
+
const menu = menuItem.dataset.menu;
|
|
210
|
+
this.selectMenu(menu);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
selectMenu(menu) {
|
|
215
|
+
// Toggle off if same menu clicked
|
|
216
|
+
if (this.currentMenu === menu) {
|
|
217
|
+
this.closeSubmenu();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.currentMenu = menu;
|
|
222
|
+
|
|
223
|
+
// Update active state
|
|
224
|
+
this.container.querySelectorAll('.swp-menu-item').forEach(item => {
|
|
225
|
+
item.classList.toggle('active', item.dataset.menu === menu);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Activate corresponding tool
|
|
229
|
+
this.activateToolForMenu(menu);
|
|
230
|
+
|
|
231
|
+
// Show submenu
|
|
232
|
+
this.showSubmenu(menu);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
activateToolForMenu(menu) {
|
|
236
|
+
const toolMap = {
|
|
237
|
+
'crop': 'crop',
|
|
238
|
+
'rotate': 'move',
|
|
239
|
+
'flip': 'move',
|
|
240
|
+
'resize': 'move',
|
|
241
|
+
'draw': 'brush',
|
|
242
|
+
'shape': 'shape',
|
|
243
|
+
'text': 'text',
|
|
244
|
+
'filter': 'move'
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const toolName = toolMap[menu];
|
|
248
|
+
if (toolName) {
|
|
249
|
+
this.app.tools.setTool(toolName);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
showSubmenu(menu) {
|
|
254
|
+
const submenu = this.container.querySelector('.swp-submenu');
|
|
255
|
+
submenu.hidden = false;
|
|
256
|
+
|
|
257
|
+
switch (menu) {
|
|
258
|
+
case 'crop':
|
|
259
|
+
this.renderCropSubmenu(submenu);
|
|
260
|
+
break;
|
|
261
|
+
case 'rotate':
|
|
262
|
+
this.renderRotateSubmenu(submenu);
|
|
263
|
+
break;
|
|
264
|
+
case 'flip':
|
|
265
|
+
this.renderFlipSubmenu(submenu);
|
|
266
|
+
break;
|
|
267
|
+
case 'resize':
|
|
268
|
+
this.renderResizeSubmenu(submenu);
|
|
269
|
+
break;
|
|
270
|
+
case 'draw':
|
|
271
|
+
this.renderDrawSubmenu(submenu);
|
|
272
|
+
break;
|
|
273
|
+
case 'shape':
|
|
274
|
+
this.renderShapeSubmenu(submenu);
|
|
275
|
+
break;
|
|
276
|
+
case 'text':
|
|
277
|
+
this.renderTextSubmenu(submenu);
|
|
278
|
+
break;
|
|
279
|
+
case 'filter':
|
|
280
|
+
this.renderFilterSubmenu(submenu);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
closeSubmenu() {
|
|
286
|
+
this.currentMenu = null;
|
|
287
|
+
this.container.querySelectorAll('.swp-menu-item').forEach(item => {
|
|
288
|
+
item.classList.remove('active');
|
|
289
|
+
});
|
|
290
|
+
const submenu = this.container.querySelector('.swp-submenu');
|
|
291
|
+
submenu.hidden = true;
|
|
292
|
+
submenu.innerHTML = '';
|
|
293
|
+
|
|
294
|
+
// Reset tool to move
|
|
295
|
+
this.app.tools.setTool('move');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
renderCropSubmenu(submenu) {
|
|
299
|
+
submenu.innerHTML = `
|
|
300
|
+
<div class="swp-submenu-content">
|
|
301
|
+
<div class="swp-submenu-title">Crop</div>
|
|
302
|
+
<div class="swp-submenu-group">
|
|
303
|
+
<label class="swp-submenu-label">Preset</label>
|
|
304
|
+
<div class="swp-btn-group">
|
|
305
|
+
<button class="swp-submenu-btn active" data-ratio="free">Free</button>
|
|
306
|
+
<button class="swp-submenu-btn" data-ratio="1:1">1:1</button>
|
|
307
|
+
<button class="swp-submenu-btn" data-ratio="4:3">4:3</button>
|
|
308
|
+
<button class="swp-submenu-btn" data-ratio="16:9">16:9</button>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="swp-submenu-actions">
|
|
312
|
+
<button class="swp-btn" data-action="cancel">Cancel</button>
|
|
313
|
+
<button class="swp-btn swp-btn-primary" data-action="apply">Apply</button>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
`;
|
|
317
|
+
|
|
318
|
+
this.bindCropSubmenuEvents(submenu);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
bindCropSubmenuEvents(submenu) {
|
|
322
|
+
submenu.querySelectorAll('[data-ratio]').forEach(btn => {
|
|
323
|
+
btn.addEventListener('click', () => {
|
|
324
|
+
submenu.querySelectorAll('[data-ratio]').forEach(b => b.classList.remove('active'));
|
|
325
|
+
btn.classList.add('active');
|
|
326
|
+
|
|
327
|
+
const ratio = btn.dataset.ratio;
|
|
328
|
+
const cropTool = this.app.tools.getTool('crop');
|
|
329
|
+
if (cropTool) {
|
|
330
|
+
if (ratio === 'free') {
|
|
331
|
+
cropTool.setAspectRatio(null);
|
|
332
|
+
} else {
|
|
333
|
+
const [w, h] = ratio.split(':').map(Number);
|
|
334
|
+
cropTool.setAspectRatio(w / h);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
submenu.querySelector('[data-action="cancel"]')?.addEventListener('click', () => {
|
|
341
|
+
this.closeSubmenu();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
submenu.querySelector('[data-action="apply"]')?.addEventListener('click', () => {
|
|
345
|
+
const cropTool = this.app.tools.getTool('crop');
|
|
346
|
+
if (cropTool?.applyCrop) {
|
|
347
|
+
cropTool.applyCrop();
|
|
348
|
+
}
|
|
349
|
+
this.closeSubmenu();
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
renderRotateSubmenu(submenu) {
|
|
354
|
+
submenu.innerHTML = `
|
|
355
|
+
<div class="swp-submenu-content">
|
|
356
|
+
<div class="swp-submenu-title">Rotate</div>
|
|
357
|
+
<div class="swp-submenu-group">
|
|
358
|
+
<div class="swp-btn-group" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px;">
|
|
359
|
+
<button class="swp-submenu-btn" data-rotate="-90">
|
|
360
|
+
<ss-icon icon="rotate-minus" thickness="2"></ss-icon>
|
|
361
|
+
<span>-90°</span>
|
|
362
|
+
</button>
|
|
363
|
+
<button class="swp-submenu-btn" data-rotate="90">
|
|
364
|
+
<ss-icon icon="rotate-add" thickness="2"></ss-icon>
|
|
365
|
+
<span>+90°</span>
|
|
366
|
+
</button>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="swp-submenu-group">
|
|
370
|
+
<label class="swp-submenu-label">Custom Angle</label>
|
|
371
|
+
<div class="swp-range-wrap">
|
|
372
|
+
<input type="range" class="swp-slider" id="rotateAngle" min="-180" max="180" value="0">
|
|
373
|
+
<span class="swp-range-value">0°</span>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="swp-submenu-actions">
|
|
377
|
+
<button class="swp-btn" data-action="cancel">Cancel</button>
|
|
378
|
+
<button class="swp-btn swp-btn-primary" data-action="apply">Apply</button>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
`;
|
|
382
|
+
|
|
383
|
+
this.bindRotateSubmenuEvents(submenu);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
bindRotateSubmenuEvents(submenu) {
|
|
387
|
+
let currentAngle = 0;
|
|
388
|
+
|
|
389
|
+
submenu.querySelectorAll('[data-rotate]').forEach(btn => {
|
|
390
|
+
btn.addEventListener('click', () => {
|
|
391
|
+
const angle = parseInt(btn.dataset.rotate);
|
|
392
|
+
this.rotateCanvas(angle);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const slider = submenu.querySelector('#rotateAngle');
|
|
397
|
+
const valueDisplay = submenu.querySelector('.swp-range-value');
|
|
398
|
+
|
|
399
|
+
slider?.addEventListener('input', (e) => {
|
|
400
|
+
currentAngle = parseInt(e.target.value);
|
|
401
|
+
valueDisplay.textContent = `${currentAngle}°`;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
submenu.querySelector('[data-action="cancel"]')?.addEventListener('click', () => {
|
|
405
|
+
this.closeSubmenu();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
submenu.querySelector('[data-action="apply"]')?.addEventListener('click', () => {
|
|
409
|
+
if (currentAngle !== 0) {
|
|
410
|
+
this.rotateCanvas(currentAngle);
|
|
411
|
+
}
|
|
412
|
+
this.closeSubmenu();
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
renderFlipSubmenu(submenu) {
|
|
417
|
+
submenu.innerHTML = `
|
|
418
|
+
<div class="swp-submenu-content">
|
|
419
|
+
<div class="swp-submenu-title">Flip</div>
|
|
420
|
+
<div class="swp-submenu-group">
|
|
421
|
+
<div class="swp-btn-group swp-btn-group-lg">
|
|
422
|
+
<button class="swp-submenu-btn" data-flip="horizontal">
|
|
423
|
+
<ss-icon icon="flip-horizontal" thickness="2"></ss-icon>
|
|
424
|
+
<span>Horizontal</span>
|
|
425
|
+
</button>
|
|
426
|
+
<button class="swp-submenu-btn" data-flip="vertical">
|
|
427
|
+
<ss-icon icon="flip-vertical" thickness="2"></ss-icon>
|
|
428
|
+
<span>Vertical</span>
|
|
429
|
+
</button>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
`;
|
|
434
|
+
|
|
435
|
+
submenu.querySelectorAll('[data-flip]').forEach(btn => {
|
|
436
|
+
btn.addEventListener('click', () => {
|
|
437
|
+
const direction = btn.dataset.flip;
|
|
438
|
+
this.flipCanvas(direction);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
renderResizeSubmenu(submenu) {
|
|
444
|
+
const currentWidth = this.app.canvas.width;
|
|
445
|
+
const currentHeight = this.app.canvas.height;
|
|
446
|
+
|
|
447
|
+
submenu.innerHTML = `
|
|
448
|
+
<div class="swp-submenu-content">
|
|
449
|
+
<div class="swp-submenu-title">Resize Canvas</div>
|
|
450
|
+
<div class="swp-submenu-group">
|
|
451
|
+
<label class="swp-submenu-label">Dimensions</label>
|
|
452
|
+
<div class="swp-resize-inputs">
|
|
453
|
+
<div class="swp-input-group">
|
|
454
|
+
<label>Width</label>
|
|
455
|
+
<input type="number" class="swp-input" id="resizeWidth" value="${currentWidth}" min="1" max="10000">
|
|
456
|
+
</div>
|
|
457
|
+
<div class="swp-input-group">
|
|
458
|
+
<label>Height</label>
|
|
459
|
+
<input type="number" class="swp-input" id="resizeHeight" value="${currentHeight}" min="1" max="10000">
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
<div class="swp-submenu-group">
|
|
464
|
+
<label class="swp-checkbox-label">
|
|
465
|
+
<input type="checkbox" id="lockAspectRatio" checked>
|
|
466
|
+
<span>Lock aspect ratio</span>
|
|
467
|
+
</label>
|
|
468
|
+
</div>
|
|
469
|
+
<div class="swp-submenu-group">
|
|
470
|
+
<label class="swp-submenu-label">Presets</label>
|
|
471
|
+
<div class="swp-btn-group swp-btn-group-wrap">
|
|
472
|
+
<button class="swp-submenu-btn swp-preset-btn" data-width="1920" data-height="1080">1920×1080</button>
|
|
473
|
+
<button class="swp-submenu-btn swp-preset-btn" data-width="1280" data-height="720">1280×720</button>
|
|
474
|
+
<button class="swp-submenu-btn swp-preset-btn" data-width="800" data-height="600">800×600</button>
|
|
475
|
+
<button class="swp-submenu-btn swp-preset-btn" data-width="500" data-height="500">500×500</button>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
<div class="swp-submenu-actions">
|
|
479
|
+
<button class="swp-btn" data-action="cancel">Cancel</button>
|
|
480
|
+
<button class="swp-btn swp-btn-primary" data-action="apply">Apply</button>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
`;
|
|
484
|
+
|
|
485
|
+
this.bindResizeSubmenuEvents(submenu, currentWidth, currentHeight);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
bindResizeSubmenuEvents(submenu, originalWidth, originalHeight) {
|
|
489
|
+
const widthInput = submenu.querySelector('#resizeWidth');
|
|
490
|
+
const heightInput = submenu.querySelector('#resizeHeight');
|
|
491
|
+
const lockCheckbox = submenu.querySelector('#lockAspectRatio');
|
|
492
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
493
|
+
|
|
494
|
+
widthInput?.addEventListener('input', () => {
|
|
495
|
+
if (lockCheckbox?.checked) {
|
|
496
|
+
const newWidth = parseInt(widthInput.value) || 1;
|
|
497
|
+
heightInput.value = Math.round(newWidth / aspectRatio);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
heightInput?.addEventListener('input', () => {
|
|
502
|
+
if (lockCheckbox?.checked) {
|
|
503
|
+
const newHeight = parseInt(heightInput.value) || 1;
|
|
504
|
+
widthInput.value = Math.round(newHeight * aspectRatio);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
submenu.querySelectorAll('.swp-preset-btn').forEach(btn => {
|
|
509
|
+
btn.addEventListener('click', () => {
|
|
510
|
+
widthInput.value = btn.dataset.width;
|
|
511
|
+
heightInput.value = btn.dataset.height;
|
|
512
|
+
lockCheckbox.checked = false; // Uncheck to allow preset change
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
submenu.querySelector('[data-action="cancel"]')?.addEventListener('click', () => {
|
|
517
|
+
this.closeSubmenu();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
submenu.querySelector('[data-action="apply"]')?.addEventListener('click', () => {
|
|
521
|
+
const newWidth = parseInt(widthInput.value) || originalWidth;
|
|
522
|
+
const newHeight = parseInt(heightInput.value) || originalHeight;
|
|
523
|
+
|
|
524
|
+
if (newWidth !== originalWidth || newHeight !== originalHeight) {
|
|
525
|
+
this.resizeCanvas(newWidth, newHeight);
|
|
526
|
+
}
|
|
527
|
+
this.closeSubmenu();
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
resizeCanvas(width, height) {
|
|
532
|
+
this.app.history.pushState(`Resize to ${width}×${height}`);
|
|
533
|
+
this.app.canvas.resize(width, height);
|
|
534
|
+
this.app.canvas.fitToScreen();
|
|
535
|
+
this.app.canvas.render();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
renderDrawSubmenu(submenu) {
|
|
539
|
+
const currentColor = this.app.colors.foreground;
|
|
540
|
+
const brushTool = this.app.tools.getTool('brush');
|
|
541
|
+
const currentSize = brushTool?.size || 10;
|
|
542
|
+
|
|
543
|
+
submenu.innerHTML = `
|
|
544
|
+
<div class="swp-submenu-content">
|
|
545
|
+
<div class="swp-submenu-title">Draw</div>
|
|
546
|
+
<div class="swp-submenu-group">
|
|
547
|
+
<label class="swp-submenu-label">Brush Size</label>
|
|
548
|
+
<div class="swp-range-wrap">
|
|
549
|
+
<input type="range" class="swp-slider" id="brushSize" min="1" max="100" value="${currentSize}">
|
|
550
|
+
<span class="swp-range-value">${currentSize}px</span>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
<div class="swp-submenu-group">
|
|
554
|
+
<label class="swp-submenu-label">Color</label>
|
|
555
|
+
<input type="color" class="swp-color-input" id="brushColor" value="${currentColor}">
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
`;
|
|
559
|
+
|
|
560
|
+
this.bindDrawSubmenuEvents(submenu);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
bindDrawSubmenuEvents(submenu) {
|
|
564
|
+
const sizeSlider = submenu.querySelector('#brushSize');
|
|
565
|
+
const sizeValue = submenu.querySelector('.swp-range-value');
|
|
566
|
+
const colorInput = submenu.querySelector('#brushColor');
|
|
567
|
+
|
|
568
|
+
sizeSlider?.addEventListener('input', (e) => {
|
|
569
|
+
const size = parseInt(e.target.value);
|
|
570
|
+
sizeValue.textContent = `${size}px`;
|
|
571
|
+
const brushTool = this.app.tools.getTool('brush');
|
|
572
|
+
if (brushTool) {
|
|
573
|
+
brushTool.size = size;
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
colorInput?.addEventListener('input', (e) => {
|
|
578
|
+
this.app.colors.setForeground(e.target.value);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
renderShapeSubmenu(submenu) {
|
|
583
|
+
const shapeTool = this.app.tools.getTool('shape');
|
|
584
|
+
const currentShape = shapeTool?.options?.shape || 'rectangle';
|
|
585
|
+
const currentFillColor = shapeTool?.options?.fillColor || this.app.colors.foreground;
|
|
586
|
+
const currentStrokeWidth = shapeTool?.options?.strokeWidth || 2;
|
|
587
|
+
|
|
588
|
+
submenu.innerHTML = `
|
|
589
|
+
<div class="swp-submenu-content">
|
|
590
|
+
<div class="swp-submenu-title">Shape</div>
|
|
591
|
+
<div class="swp-submenu-group">
|
|
592
|
+
<label class="swp-submenu-label">Shape Type</label>
|
|
593
|
+
<div class="swp-btn-group">
|
|
594
|
+
<button class="swp-submenu-btn ${currentShape === 'rectangle' ? 'active' : ''}" data-shape="rectangle">
|
|
595
|
+
<ss-icon icon="square" thickness="2"></ss-icon>
|
|
596
|
+
</button>
|
|
597
|
+
<button class="swp-submenu-btn ${currentShape === 'ellipse' ? 'active' : ''}" data-shape="ellipse">
|
|
598
|
+
<ss-icon icon="circle" thickness="2"></ss-icon>
|
|
599
|
+
</button>
|
|
600
|
+
<button class="swp-submenu-btn ${currentShape === 'line' ? 'active' : ''}" data-shape="line">
|
|
601
|
+
<ss-icon icon="minus" thickness="2"></ss-icon>
|
|
602
|
+
</button>
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
<div class="swp-submenu-group">
|
|
606
|
+
<label class="swp-submenu-label">Fill Color</label>
|
|
607
|
+
<input type="color" class="swp-color-input" id="shapeFill" value="${currentFillColor}">
|
|
608
|
+
</div>
|
|
609
|
+
<div class="swp-submenu-group">
|
|
610
|
+
<label class="swp-submenu-label">Stroke Width</label>
|
|
611
|
+
<div class="swp-range-wrap">
|
|
612
|
+
<input type="range" class="swp-slider" id="shapeStroke" min="0" max="20" value="${currentStrokeWidth}">
|
|
613
|
+
<span class="swp-range-value">2px</span>
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
`;
|
|
618
|
+
|
|
619
|
+
this.bindShapeSubmenuEvents(submenu);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
bindShapeSubmenuEvents(submenu) {
|
|
623
|
+
submenu.querySelectorAll('[data-shape]').forEach(btn => {
|
|
624
|
+
btn.addEventListener('click', () => {
|
|
625
|
+
submenu.querySelectorAll('[data-shape]').forEach(b => b.classList.remove('active'));
|
|
626
|
+
btn.classList.add('active');
|
|
627
|
+
|
|
628
|
+
const shapeTool = this.app.tools.getTool('shape');
|
|
629
|
+
if (shapeTool) {
|
|
630
|
+
shapeTool.options.shape = btn.dataset.shape;
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
const fillInput = submenu.querySelector('#shapeFill');
|
|
636
|
+
fillInput?.addEventListener('input', (e) => {
|
|
637
|
+
const shapeTool = this.app.tools.getTool('shape');
|
|
638
|
+
if (shapeTool) {
|
|
639
|
+
shapeTool.options.fillColor = e.target.value;
|
|
640
|
+
shapeTool.options.filled = true;
|
|
641
|
+
}
|
|
642
|
+
this.app.colors.setForeground(e.target.value);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const strokeSlider = submenu.querySelector('#shapeStroke');
|
|
646
|
+
const strokeValue = submenu.querySelectorAll('.swp-range-value')[0];
|
|
647
|
+
strokeSlider?.addEventListener('input', (e) => {
|
|
648
|
+
const width = parseInt(e.target.value);
|
|
649
|
+
if (strokeValue) strokeValue.textContent = `${width}px`;
|
|
650
|
+
const shapeTool = this.app.tools.getTool('shape');
|
|
651
|
+
if (shapeTool) {
|
|
652
|
+
shapeTool.options.strokeWidth = width;
|
|
653
|
+
if (width > 0) {
|
|
654
|
+
shapeTool.options.stroked = true;
|
|
655
|
+
shapeTool.options.strokeColor = this.app.colors.foreground;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
renderTextSubmenu(submenu) {
|
|
662
|
+
const currentColor = this.app.colors.foreground;
|
|
663
|
+
|
|
664
|
+
submenu.innerHTML = `
|
|
665
|
+
<div class="swp-submenu-content">
|
|
666
|
+
<div class="swp-submenu-title">Text</div>
|
|
667
|
+
<div class="swp-submenu-group">
|
|
668
|
+
<label class="swp-submenu-label">Font Size</label>
|
|
669
|
+
<div class="swp-range-wrap">
|
|
670
|
+
<input type="range" class="swp-slider" id="textSize" min="12" max="120" value="32">
|
|
671
|
+
<span class="swp-range-value">32px</span>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
<div class="swp-submenu-group">
|
|
675
|
+
<label class="swp-submenu-label">Font</label>
|
|
676
|
+
<select class="swp-select" id="textFont">
|
|
677
|
+
<option value="Arial">Arial</option>
|
|
678
|
+
<option value="Helvetica">Helvetica</option>
|
|
679
|
+
<option value="Georgia">Georgia</option>
|
|
680
|
+
<option value="Times New Roman">Times New Roman</option>
|
|
681
|
+
<option value="Courier New">Courier New</option>
|
|
682
|
+
<option value="Impact">Impact</option>
|
|
683
|
+
</select>
|
|
684
|
+
</div>
|
|
685
|
+
<div class="swp-submenu-group">
|
|
686
|
+
<label class="swp-submenu-label">Color</label>
|
|
687
|
+
<input type="color" class="swp-color-input" id="textColor" value="${currentColor}">
|
|
688
|
+
</div>
|
|
689
|
+
<div class="swp-submenu-group">
|
|
690
|
+
<label class="swp-submenu-label">Style</label>
|
|
691
|
+
<div class="swp-btn-group">
|
|
692
|
+
<button class="swp-submenu-btn" data-style="bold">
|
|
693
|
+
<strong>B</strong>
|
|
694
|
+
</button>
|
|
695
|
+
<button class="swp-submenu-btn" data-style="italic">
|
|
696
|
+
<em>I</em>
|
|
697
|
+
</button>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
</div>
|
|
701
|
+
`;
|
|
702
|
+
|
|
703
|
+
this.bindTextSubmenuEvents(submenu);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
bindTextSubmenuEvents(submenu) {
|
|
707
|
+
const sizeSlider = submenu.querySelector('#textSize');
|
|
708
|
+
const sizeValue = submenu.querySelector('.swp-range-value');
|
|
709
|
+
|
|
710
|
+
sizeSlider?.addEventListener('input', (e) => {
|
|
711
|
+
const size = parseInt(e.target.value);
|
|
712
|
+
sizeValue.textContent = `${size}px`;
|
|
713
|
+
const textTool = this.app.tools.getTool('text');
|
|
714
|
+
if (textTool) {
|
|
715
|
+
textTool.fontSize = size;
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const fontSelect = submenu.querySelector('#textFont');
|
|
720
|
+
fontSelect?.addEventListener('change', (e) => {
|
|
721
|
+
const textTool = this.app.tools.getTool('text');
|
|
722
|
+
if (textTool) {
|
|
723
|
+
textTool.fontFamily = e.target.value;
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
const colorInput = submenu.querySelector('#textColor');
|
|
728
|
+
colorInput?.addEventListener('input', (e) => {
|
|
729
|
+
this.app.colors.setForeground(e.target.value);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
submenu.querySelectorAll('[data-style]').forEach(btn => {
|
|
733
|
+
btn.addEventListener('click', () => {
|
|
734
|
+
btn.classList.toggle('active');
|
|
735
|
+
const textTool = this.app.tools.getTool('text');
|
|
736
|
+
if (textTool) {
|
|
737
|
+
if (btn.dataset.style === 'bold') {
|
|
738
|
+
textTool.bold = btn.classList.contains('active');
|
|
739
|
+
} else if (btn.dataset.style === 'italic') {
|
|
740
|
+
textTool.italic = btn.classList.contains('active');
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
renderFilterSubmenu(submenu) {
|
|
748
|
+
submenu.innerHTML = `
|
|
749
|
+
<div class="swp-submenu-content swp-submenu-wide">
|
|
750
|
+
<div class="swp-submenu-title">Filter</div>
|
|
751
|
+
<div class="swp-submenu-group">
|
|
752
|
+
<div class="swp-filter-grid">
|
|
753
|
+
<button class="swp-filter-btn" data-filter="none">
|
|
754
|
+
<div class="swp-filter-preview"></div>
|
|
755
|
+
<span>None</span>
|
|
756
|
+
</button>
|
|
757
|
+
<button class="swp-filter-btn" data-filter="grayscale">
|
|
758
|
+
<div class="swp-filter-preview swp-filter-grayscale"></div>
|
|
759
|
+
<span>Grayscale</span>
|
|
760
|
+
</button>
|
|
761
|
+
<button class="swp-filter-btn" data-filter="sepia">
|
|
762
|
+
<div class="swp-filter-preview swp-filter-sepia"></div>
|
|
763
|
+
<span>Sepia</span>
|
|
764
|
+
</button>
|
|
765
|
+
<button class="swp-filter-btn" data-filter="invert">
|
|
766
|
+
<div class="swp-filter-preview swp-filter-invert"></div>
|
|
767
|
+
<span>Invert</span>
|
|
768
|
+
</button>
|
|
769
|
+
<button class="swp-filter-btn" data-filter="blur">
|
|
770
|
+
<div class="swp-filter-preview swp-filter-blur"></div>
|
|
771
|
+
<span>Blur</span>
|
|
772
|
+
</button>
|
|
773
|
+
<button class="swp-filter-btn" data-filter="brightness">
|
|
774
|
+
<div class="swp-filter-preview swp-filter-brightness"></div>
|
|
775
|
+
<span>Brighten</span>
|
|
776
|
+
</button>
|
|
777
|
+
<button class="swp-filter-btn" data-filter="contrast">
|
|
778
|
+
<div class="swp-filter-preview swp-filter-contrast"></div>
|
|
779
|
+
<span>Contrast</span>
|
|
780
|
+
</button>
|
|
781
|
+
<button class="swp-filter-btn" data-filter="sharpen">
|
|
782
|
+
<div class="swp-filter-preview swp-filter-sharpen"></div>
|
|
783
|
+
<span>Sharpen</span>
|
|
784
|
+
</button>
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
787
|
+
<div class="swp-submenu-group swp-filter-intensity" hidden>
|
|
788
|
+
<label class="swp-submenu-label">Intensity</label>
|
|
789
|
+
<div class="swp-range-wrap">
|
|
790
|
+
<input type="range" class="swp-slider" id="filterIntensity" min="0" max="100" value="50">
|
|
791
|
+
<span class="swp-range-value">50%</span>
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
<div class="swp-submenu-actions">
|
|
795
|
+
<button class="swp-btn" data-action="cancel">Cancel</button>
|
|
796
|
+
<button class="swp-btn swp-btn-primary" data-action="apply">Apply</button>
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
`;
|
|
800
|
+
|
|
801
|
+
this.bindFilterSubmenuEvents(submenu);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
bindFilterSubmenuEvents(submenu) {
|
|
805
|
+
let selectedFilter = 'none';
|
|
806
|
+
let intensity = 50;
|
|
807
|
+
|
|
808
|
+
const intensityGroup = submenu.querySelector('.swp-filter-intensity');
|
|
809
|
+
const intensitySlider = submenu.querySelector('#filterIntensity');
|
|
810
|
+
const intensityValue = submenu.querySelector('.swp-filter-intensity .swp-range-value');
|
|
811
|
+
|
|
812
|
+
submenu.querySelectorAll('.swp-filter-btn').forEach(btn => {
|
|
813
|
+
btn.addEventListener('click', () => {
|
|
814
|
+
submenu.querySelectorAll('.swp-filter-btn').forEach(b => b.classList.remove('active'));
|
|
815
|
+
btn.classList.add('active');
|
|
816
|
+
selectedFilter = btn.dataset.filter;
|
|
817
|
+
|
|
818
|
+
// Show/hide intensity slider
|
|
819
|
+
if (selectedFilter !== 'none') {
|
|
820
|
+
intensityGroup.hidden = false;
|
|
821
|
+
this.previewFilter(selectedFilter, intensity);
|
|
822
|
+
} else {
|
|
823
|
+
intensityGroup.hidden = true;
|
|
824
|
+
this.app.filters.cancelPreview();
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
intensitySlider?.addEventListener('input', (e) => {
|
|
830
|
+
intensity = parseInt(e.target.value);
|
|
831
|
+
intensityValue.textContent = `${intensity}%`;
|
|
832
|
+
if (selectedFilter !== 'none') {
|
|
833
|
+
this.previewFilter(selectedFilter, intensity);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
submenu.querySelector('[data-action="cancel"]')?.addEventListener('click', () => {
|
|
838
|
+
this.app.filters.cancelPreview();
|
|
839
|
+
this.closeSubmenu();
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
submenu.querySelector('[data-action="apply"]')?.addEventListener('click', () => {
|
|
843
|
+
if (selectedFilter !== 'none') {
|
|
844
|
+
this.applyFilter(selectedFilter, intensity);
|
|
845
|
+
}
|
|
846
|
+
this.closeSubmenu();
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Canvas manipulation methods
|
|
851
|
+
rotateCanvas(angle) {
|
|
852
|
+
const layer = this.app.layers.getActiveLayer();
|
|
853
|
+
if (!layer) return;
|
|
854
|
+
|
|
855
|
+
// Save history
|
|
856
|
+
this.app.history.pushState(`Rotate ${angle}°`);
|
|
857
|
+
|
|
858
|
+
// Perform rotation
|
|
859
|
+
const tempCanvas = document.createElement('canvas');
|
|
860
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
861
|
+
|
|
862
|
+
const rad = (angle * Math.PI) / 180;
|
|
863
|
+
const sin = Math.abs(Math.sin(rad));
|
|
864
|
+
const cos = Math.abs(Math.cos(rad));
|
|
865
|
+
|
|
866
|
+
const w = layer.width;
|
|
867
|
+
const h = layer.height;
|
|
868
|
+
const newW = Math.round(w * cos + h * sin);
|
|
869
|
+
const newH = Math.round(w * sin + h * cos);
|
|
870
|
+
|
|
871
|
+
tempCanvas.width = newW;
|
|
872
|
+
tempCanvas.height = newH;
|
|
873
|
+
|
|
874
|
+
tempCtx.translate(newW / 2, newH / 2);
|
|
875
|
+
tempCtx.rotate(rad);
|
|
876
|
+
tempCtx.drawImage(layer.canvas, -w / 2, -h / 2);
|
|
877
|
+
|
|
878
|
+
// Update canvas size if needed (for 90° rotations)
|
|
879
|
+
if (angle === 90 || angle === -90) {
|
|
880
|
+
this.app.canvas.resize(newH, newW);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
layer.clear();
|
|
884
|
+
layer.ctx.drawImage(tempCanvas, 0, 0);
|
|
885
|
+
this.app.canvas.render();
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
flipCanvas(direction) {
|
|
889
|
+
const layer = this.app.layers.getActiveLayer();
|
|
890
|
+
if (!layer) return;
|
|
891
|
+
|
|
892
|
+
this.app.history.pushState(`Flip ${direction}`);
|
|
893
|
+
|
|
894
|
+
const tempCanvas = document.createElement('canvas');
|
|
895
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
896
|
+
tempCanvas.width = layer.width;
|
|
897
|
+
tempCanvas.height = layer.height;
|
|
898
|
+
|
|
899
|
+
if (direction === 'horizontal') {
|
|
900
|
+
tempCtx.translate(layer.width, 0);
|
|
901
|
+
tempCtx.scale(-1, 1);
|
|
902
|
+
} else {
|
|
903
|
+
tempCtx.translate(0, layer.height);
|
|
904
|
+
tempCtx.scale(1, -1);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
tempCtx.drawImage(layer.canvas, 0, 0);
|
|
908
|
+
layer.clear();
|
|
909
|
+
layer.ctx.drawImage(tempCanvas, 0, 0);
|
|
910
|
+
this.app.canvas.render();
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
previewFilter(filterName, intensity) {
|
|
914
|
+
// Convert 0-100 intensity to appropriate filter values
|
|
915
|
+
let options = {};
|
|
916
|
+
switch (filterName) {
|
|
917
|
+
case 'brightness':
|
|
918
|
+
// Convert 0-100 to -128 to 128 range (50 = 0 change)
|
|
919
|
+
options.value = Math.round((intensity - 50) * 2.56);
|
|
920
|
+
break;
|
|
921
|
+
case 'contrast':
|
|
922
|
+
// Convert 0-100 to -128 to 128 range (50 = 0 change)
|
|
923
|
+
options.value = Math.round((intensity - 50) * 2.56);
|
|
924
|
+
break;
|
|
925
|
+
case 'saturation':
|
|
926
|
+
// Convert 0-100 to -100 to 100 range (50 = 0 change)
|
|
927
|
+
options.value = (intensity - 50) * 2;
|
|
928
|
+
break;
|
|
929
|
+
case 'blur':
|
|
930
|
+
// Convert 0-100 to 1-10 radius
|
|
931
|
+
options.radius = Math.max(1, Math.round(intensity / 10));
|
|
932
|
+
break;
|
|
933
|
+
case 'sharpen':
|
|
934
|
+
// Convert 0-100 to 0-2 amount
|
|
935
|
+
options.amount = intensity / 50;
|
|
936
|
+
break;
|
|
937
|
+
case 'hueRotate':
|
|
938
|
+
// Convert 0-100 to 0-360 degrees
|
|
939
|
+
options.angle = intensity * 3.6;
|
|
940
|
+
break;
|
|
941
|
+
default:
|
|
942
|
+
options.value = intensity;
|
|
943
|
+
}
|
|
944
|
+
this.app.filters.previewFilter(filterName, options);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
applyFilter(filterName, intensity) {
|
|
948
|
+
// Convert 0-100 intensity to appropriate filter values
|
|
949
|
+
let options = {};
|
|
950
|
+
switch (filterName) {
|
|
951
|
+
case 'brightness':
|
|
952
|
+
options.value = Math.round((intensity - 50) * 2.56);
|
|
953
|
+
break;
|
|
954
|
+
case 'contrast':
|
|
955
|
+
options.value = Math.round((intensity - 50) * 2.56);
|
|
956
|
+
break;
|
|
957
|
+
case 'saturation':
|
|
958
|
+
options.value = (intensity - 50) * 2;
|
|
959
|
+
break;
|
|
960
|
+
case 'blur':
|
|
961
|
+
options.radius = Math.max(1, Math.round(intensity / 10));
|
|
962
|
+
break;
|
|
963
|
+
case 'sharpen':
|
|
964
|
+
options.amount = intensity / 50;
|
|
965
|
+
break;
|
|
966
|
+
case 'hueRotate':
|
|
967
|
+
options.angle = intensity * 3.6;
|
|
968
|
+
break;
|
|
969
|
+
default:
|
|
970
|
+
options.value = intensity;
|
|
971
|
+
}
|
|
972
|
+
this.app.filters.applyFilter(filterName, options);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
resetCanvas() {
|
|
976
|
+
if (confirm('Reset all changes?')) {
|
|
977
|
+
this.app.file.newDocument({
|
|
978
|
+
width: this.app.options.width,
|
|
979
|
+
height: this.app.options.height
|
|
980
|
+
});
|
|
981
|
+
this.closeSubmenu();
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
openFileDialog() {
|
|
986
|
+
const input = document.createElement('input');
|
|
987
|
+
input.type = 'file';
|
|
988
|
+
input.accept = 'image/*';
|
|
989
|
+
input.onchange = (e) => {
|
|
990
|
+
const file = e.target.files[0];
|
|
991
|
+
if (file) {
|
|
992
|
+
const reader = new FileReader();
|
|
993
|
+
reader.onload = (e) => {
|
|
994
|
+
this.app.loadImage(e.target.result);
|
|
995
|
+
};
|
|
996
|
+
reader.readAsDataURL(file);
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
input.click();
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
toggleFullscreen() {
|
|
1003
|
+
if (!document.fullscreenElement) {
|
|
1004
|
+
this.container.requestFullscreen();
|
|
1005
|
+
this.isFullscreen = true;
|
|
1006
|
+
} else {
|
|
1007
|
+
document.exitFullscreen();
|
|
1008
|
+
this.isFullscreen = false;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Side Panel Methods
|
|
1013
|
+
toggleSidePanel(panelType) {
|
|
1014
|
+
const sidePanel = this.container.querySelector('.swp-side-panel');
|
|
1015
|
+
if (!sidePanel) return;
|
|
1016
|
+
|
|
1017
|
+
// If already showing this panel, close it
|
|
1018
|
+
if (!sidePanel.hidden && this.currentPanel === panelType) {
|
|
1019
|
+
this.closeSidePanel();
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Show panel
|
|
1024
|
+
sidePanel.hidden = false;
|
|
1025
|
+
this.currentPanel = panelType;
|
|
1026
|
+
|
|
1027
|
+
// Update title
|
|
1028
|
+
const title = sidePanel.querySelector('.swp-side-panel-title');
|
|
1029
|
+
if (title) {
|
|
1030
|
+
title.textContent = panelType.charAt(0).toUpperCase() + panelType.slice(1);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Render content
|
|
1034
|
+
const content = sidePanel.querySelector('.swp-side-panel-content');
|
|
1035
|
+
if (panelType === 'history') {
|
|
1036
|
+
this.renderHistorySidePanel(content);
|
|
1037
|
+
} else if (panelType === 'layers') {
|
|
1038
|
+
this.renderLayersSidePanel(content);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Update button active states
|
|
1042
|
+
this.container.querySelectorAll('[data-action="history"], [data-action="layers"]').forEach(btn => {
|
|
1043
|
+
btn.classList.toggle('active', btn.dataset.action === panelType);
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
closeSidePanel() {
|
|
1048
|
+
const sidePanel = this.container.querySelector('.swp-side-panel');
|
|
1049
|
+
if (sidePanel) {
|
|
1050
|
+
sidePanel.hidden = true;
|
|
1051
|
+
}
|
|
1052
|
+
this.currentPanel = null;
|
|
1053
|
+
|
|
1054
|
+
// Remove active states
|
|
1055
|
+
this.container.querySelectorAll('[data-action="history"], [data-action="layers"]').forEach(btn => {
|
|
1056
|
+
btn.classList.remove('active');
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
renderHistorySidePanel(content) {
|
|
1061
|
+
const states = this.app.history.getStates();
|
|
1062
|
+
|
|
1063
|
+
content.innerHTML = `
|
|
1064
|
+
<div class="swp-panel-list">
|
|
1065
|
+
${states.length === 0 ? '<div class="swp-panel-empty">No history yet</div>' : ''}
|
|
1066
|
+
${states.map(state => `
|
|
1067
|
+
<div class="swp-panel-item ${state.isCurrent ? 'active' : ''}" data-index="${state.index}">
|
|
1068
|
+
<ss-icon icon="clock" thickness="2"></ss-icon>
|
|
1069
|
+
<span>${state.name}</span>
|
|
1070
|
+
</div>
|
|
1071
|
+
`).join('')}
|
|
1072
|
+
</div>
|
|
1073
|
+
`;
|
|
1074
|
+
|
|
1075
|
+
content.querySelectorAll('.swp-panel-item').forEach(item => {
|
|
1076
|
+
item.addEventListener('click', () => {
|
|
1077
|
+
this.app.history.goToState(parseInt(item.dataset.index));
|
|
1078
|
+
this.renderHistorySidePanel(content);
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
renderLayersSidePanel(content) {
|
|
1084
|
+
const layers = this.app.layers.getLayers().slice().reverse();
|
|
1085
|
+
const active = this.app.layers.getActiveLayer();
|
|
1086
|
+
|
|
1087
|
+
content.innerHTML = `
|
|
1088
|
+
<div class="swp-panel-list">
|
|
1089
|
+
${layers.length === 0 ? '<div class="swp-panel-empty">No layers</div>' : ''}
|
|
1090
|
+
${layers.map(layer => `
|
|
1091
|
+
<div class="swp-panel-item ${layer.id === active?.id ? 'active' : ''}" data-id="${layer.id}">
|
|
1092
|
+
<button class="swp-layer-vis-btn ${layer.visible ? 'visible' : ''}" data-action="toggle-visibility">
|
|
1093
|
+
<ss-icon icon="${layer.visible ? 'eye' : 'eye-slash'}" thickness="2"></ss-icon>
|
|
1094
|
+
</button>
|
|
1095
|
+
<span class="swp-layer-name">${layer.name}</span>
|
|
1096
|
+
<span class="swp-layer-opacity">${layer.opacity}%</span>
|
|
1097
|
+
</div>
|
|
1098
|
+
`).join('')}
|
|
1099
|
+
</div>
|
|
1100
|
+
<div class="swp-panel-actions">
|
|
1101
|
+
<button class="swp-btn swp-btn-sm" data-action="add-layer">
|
|
1102
|
+
<ss-icon icon="plus" thickness="2"></ss-icon> Add
|
|
1103
|
+
</button>
|
|
1104
|
+
<button class="swp-btn swp-btn-sm swp-btn-danger" data-action="delete-layer">
|
|
1105
|
+
<ss-icon icon="trash" thickness="2"></ss-icon> Delete
|
|
1106
|
+
</button>
|
|
1107
|
+
</div>
|
|
1108
|
+
`;
|
|
1109
|
+
|
|
1110
|
+
// Layer click handlers
|
|
1111
|
+
content.querySelectorAll('.swp-panel-item').forEach(item => {
|
|
1112
|
+
item.addEventListener('click', (e) => {
|
|
1113
|
+
if (!e.target.closest('[data-action]')) {
|
|
1114
|
+
this.app.layers.setActiveLayer(item.dataset.id);
|
|
1115
|
+
this.renderLayersSidePanel(content);
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
item.querySelector('[data-action="toggle-visibility"]')?.addEventListener('click', (e) => {
|
|
1120
|
+
e.stopPropagation();
|
|
1121
|
+
const layer = this.app.layers.getLayer(item.dataset.id);
|
|
1122
|
+
if (layer) {
|
|
1123
|
+
this.app.layers.setLayerVisibility(item.dataset.id, !layer.visible);
|
|
1124
|
+
this.renderLayersSidePanel(content);
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
// Action buttons
|
|
1130
|
+
content.querySelector('[data-action="add-layer"]')?.addEventListener('click', () => {
|
|
1131
|
+
this.app.layers.addLayer();
|
|
1132
|
+
this.renderLayersSidePanel(content);
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
content.querySelector('[data-action="delete-layer"]')?.addEventListener('click', () => {
|
|
1136
|
+
const activeLayer = this.app.layers.getActiveLayer();
|
|
1137
|
+
if (activeLayer) {
|
|
1138
|
+
this.app.layers.removeLayer(activeLayer.id);
|
|
1139
|
+
this.renderLayersSidePanel(content);
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
bindEvents() {
|
|
1145
|
+
// History events - update buttons and panel
|
|
1146
|
+
this.app.events.on(Events.HISTORY_PUSH, () => {
|
|
1147
|
+
this.updateHistoryButtons();
|
|
1148
|
+
this.updateHistoryPanel();
|
|
1149
|
+
});
|
|
1150
|
+
this.app.events.on(Events.HISTORY_UNDO, () => {
|
|
1151
|
+
this.updateHistoryButtons();
|
|
1152
|
+
this.updateHistoryPanel();
|
|
1153
|
+
});
|
|
1154
|
+
this.app.events.on(Events.HISTORY_REDO, () => {
|
|
1155
|
+
this.updateHistoryButtons();
|
|
1156
|
+
this.updateHistoryPanel();
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
// Layer events - update layers panel
|
|
1160
|
+
this.app.events.on(Events.LAYER_ADD, () => this.updateLayersPanel());
|
|
1161
|
+
this.app.events.on(Events.LAYER_REMOVE, () => this.updateLayersPanel());
|
|
1162
|
+
this.app.events.on(Events.LAYER_SELECT, () => this.updateLayersPanel());
|
|
1163
|
+
this.app.events.on(Events.LAYER_VISIBILITY, () => this.updateLayersPanel());
|
|
1164
|
+
this.app.events.on(Events.LAYER_UPDATE, () => this.updateLayersPanel());
|
|
1165
|
+
this.app.events.on(Events.LAYER_RENAME, () => this.updateLayersPanel());
|
|
1166
|
+
this.app.events.on(Events.LAYER_REORDER, () => this.updateLayersPanel());
|
|
1167
|
+
this.app.events.on(Events.LAYER_OPACITY, () => this.updateLayersPanel());
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
updateHistoryButtons() {
|
|
1171
|
+
const undoBtn = this.container.querySelector('[data-action="undo"]');
|
|
1172
|
+
const redoBtn = this.container.querySelector('[data-action="redo"]');
|
|
1173
|
+
|
|
1174
|
+
if (undoBtn) {
|
|
1175
|
+
undoBtn.disabled = !this.app.history.canUndo();
|
|
1176
|
+
}
|
|
1177
|
+
if (redoBtn) {
|
|
1178
|
+
redoBtn.disabled = !this.app.history.canRedo();
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Update layers panel if it's currently visible
|
|
1183
|
+
updateLayersPanel() {
|
|
1184
|
+
if (this.currentPanel === 'layers') {
|
|
1185
|
+
const content = this.container.querySelector('.swp-side-panel-content');
|
|
1186
|
+
if (content) {
|
|
1187
|
+
this.renderLayersSidePanel(content);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Update history panel if it's currently visible
|
|
1193
|
+
updateHistoryPanel() {
|
|
1194
|
+
if (this.currentPanel === 'history') {
|
|
1195
|
+
const content = this.container.querySelector('.swp-side-panel-content');
|
|
1196
|
+
if (content) {
|
|
1197
|
+
this.renderHistorySidePanel(content);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
updateToolbox() {
|
|
1203
|
+
// No-op in simplified UI
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
getWorkspace() {
|
|
1207
|
+
return this.container.querySelector('.swp-workspace');
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
export default UI;
|