senangwebs-photobooth 1.0.2 → 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 +220 -236
- package/dist/swp.css +790 -256
- 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 +10 -6
- package/src/css/swp.css +790 -256
- 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 +247 -761
- 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/swp_preview.png +0 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Studio - Canvas Manager
|
|
3
|
+
* Multi-layer canvas with viewport management
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Events } from './EventEmitter.js';
|
|
8
|
+
|
|
9
|
+
export class Canvas {
|
|
10
|
+
constructor(app, options = {}) {
|
|
11
|
+
this.app = app;
|
|
12
|
+
this.width = options.width || 1920;
|
|
13
|
+
this.height = options.height || 1080;
|
|
14
|
+
this.zoom = 100;
|
|
15
|
+
this.panX = 0;
|
|
16
|
+
this.panY = 0;
|
|
17
|
+
this.minZoom = 1;
|
|
18
|
+
this.maxZoom = 3200;
|
|
19
|
+
|
|
20
|
+
// Canvas elements
|
|
21
|
+
this.container = null;
|
|
22
|
+
this.viewport = null;
|
|
23
|
+
this.workCanvas = null;
|
|
24
|
+
this.workCtx = null;
|
|
25
|
+
this.displayCanvas = null;
|
|
26
|
+
this.displayCtx = null;
|
|
27
|
+
this.overlayCanvas = null;
|
|
28
|
+
this.overlayCtx = null;
|
|
29
|
+
|
|
30
|
+
// Rendering
|
|
31
|
+
this.needsRender = false;
|
|
32
|
+
this.renderScheduled = false;
|
|
33
|
+
|
|
34
|
+
// Checkerboard pattern for transparency
|
|
35
|
+
this.checkerboardPattern = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initialize canvas system
|
|
40
|
+
* @param {HTMLElement} container - Container element
|
|
41
|
+
*/
|
|
42
|
+
init(container) {
|
|
43
|
+
this.container = container;
|
|
44
|
+
this.createCanvasElements();
|
|
45
|
+
this.createCheckerboardPattern();
|
|
46
|
+
this.bindEvents();
|
|
47
|
+
this.render();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create canvas elements
|
|
52
|
+
*/
|
|
53
|
+
createCanvasElements() {
|
|
54
|
+
// Viewport container
|
|
55
|
+
this.viewport = document.createElement('div');
|
|
56
|
+
this.viewport.className = 'swp-viewport';
|
|
57
|
+
|
|
58
|
+
// Canvas wrapper (for transform)
|
|
59
|
+
this.canvasWrapper = document.createElement('div');
|
|
60
|
+
this.canvasWrapper.className = 'swp-canvas-wrapper';
|
|
61
|
+
|
|
62
|
+
// Work canvas (composited layers)
|
|
63
|
+
this.workCanvas = document.createElement('canvas');
|
|
64
|
+
this.workCanvas.width = this.width;
|
|
65
|
+
this.workCanvas.height = this.height;
|
|
66
|
+
this.workCanvas.className = 'swp-work-canvas';
|
|
67
|
+
this.workCtx = this.workCanvas.getContext('2d');
|
|
68
|
+
|
|
69
|
+
// Display canvas (what user sees, with zoom/pan applied)
|
|
70
|
+
this.displayCanvas = document.createElement('canvas');
|
|
71
|
+
this.displayCanvas.className = 'swp-display-canvas';
|
|
72
|
+
this.displayCtx = this.displayCanvas.getContext('2d');
|
|
73
|
+
|
|
74
|
+
// Overlay canvas (selection, guides, tool feedback)
|
|
75
|
+
this.overlayCanvas = document.createElement('canvas');
|
|
76
|
+
this.overlayCanvas.className = 'swp-overlay-canvas';
|
|
77
|
+
this.overlayCtx = this.overlayCanvas.getContext('2d');
|
|
78
|
+
|
|
79
|
+
// Assemble
|
|
80
|
+
this.canvasWrapper.appendChild(this.displayCanvas);
|
|
81
|
+
this.canvasWrapper.appendChild(this.overlayCanvas);
|
|
82
|
+
this.viewport.appendChild(this.canvasWrapper);
|
|
83
|
+
this.container.appendChild(this.viewport);
|
|
84
|
+
|
|
85
|
+
// Initial size
|
|
86
|
+
this.updateDisplaySize();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create checkerboard pattern for transparency
|
|
91
|
+
*/
|
|
92
|
+
createCheckerboardPattern() {
|
|
93
|
+
const size = 16;
|
|
94
|
+
const patternCanvas = document.createElement('canvas');
|
|
95
|
+
patternCanvas.width = size * 2;
|
|
96
|
+
patternCanvas.height = size * 2;
|
|
97
|
+
const ctx = patternCanvas.getContext('2d');
|
|
98
|
+
|
|
99
|
+
ctx.fillStyle = '#ffffff';
|
|
100
|
+
ctx.fillRect(0, 0, size * 2, size * 2);
|
|
101
|
+
ctx.fillStyle = '#e0e0e0';
|
|
102
|
+
ctx.fillRect(0, 0, size, size);
|
|
103
|
+
ctx.fillRect(size, size, size, size);
|
|
104
|
+
|
|
105
|
+
this.checkerboardPattern = this.workCtx.createPattern(patternCanvas, 'repeat');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Bind viewport events
|
|
110
|
+
*/
|
|
111
|
+
bindEvents() {
|
|
112
|
+
// Wheel zoom
|
|
113
|
+
this.viewport.addEventListener('wheel', this.handleWheel.bind(this), { passive: false });
|
|
114
|
+
|
|
115
|
+
// Resize observer
|
|
116
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
117
|
+
this.updateDisplaySize();
|
|
118
|
+
this.render();
|
|
119
|
+
});
|
|
120
|
+
this.resizeObserver.observe(this.viewport);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Handle mouse wheel for zoom
|
|
125
|
+
* @param {WheelEvent} e - Wheel event
|
|
126
|
+
*/
|
|
127
|
+
handleWheel(e) {
|
|
128
|
+
if (e.ctrlKey || e.metaKey) {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
|
|
131
|
+
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
|
132
|
+
const newZoom = Math.round(this.zoom * delta);
|
|
133
|
+
|
|
134
|
+
// Zoom towards cursor position
|
|
135
|
+
const rect = this.displayCanvas.getBoundingClientRect();
|
|
136
|
+
const x = e.clientX - rect.left;
|
|
137
|
+
const y = e.clientY - rect.top;
|
|
138
|
+
|
|
139
|
+
this.setZoom(newZoom, x, y);
|
|
140
|
+
} else {
|
|
141
|
+
// Pan
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
this.panX -= e.deltaX;
|
|
144
|
+
this.panY -= e.deltaY;
|
|
145
|
+
this.render();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Update display canvas size based on viewport
|
|
151
|
+
*/
|
|
152
|
+
updateDisplaySize() {
|
|
153
|
+
if (!this.viewport) return;
|
|
154
|
+
|
|
155
|
+
const rect = this.viewport.getBoundingClientRect();
|
|
156
|
+
this.displayCanvas.width = rect.width;
|
|
157
|
+
this.displayCanvas.height = rect.height;
|
|
158
|
+
this.overlayCanvas.width = rect.width;
|
|
159
|
+
this.overlayCanvas.height = rect.height;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resize document
|
|
164
|
+
* @param {number} width - New width
|
|
165
|
+
* @param {number} height - New height
|
|
166
|
+
*/
|
|
167
|
+
resize(width, height) {
|
|
168
|
+
this.width = width;
|
|
169
|
+
this.height = height;
|
|
170
|
+
this.workCanvas.width = width;
|
|
171
|
+
this.workCanvas.height = height;
|
|
172
|
+
this.createCheckerboardPattern();
|
|
173
|
+
this.render();
|
|
174
|
+
|
|
175
|
+
this.app.events.emit(Events.DOCUMENT_RESIZE, { width, height });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Set zoom level
|
|
180
|
+
* @param {number} zoom - Zoom percentage (1-3200)
|
|
181
|
+
* @param {number} centerX - Center X for zoom
|
|
182
|
+
* @param {number} centerY - Center Y for zoom
|
|
183
|
+
*/
|
|
184
|
+
setZoom(zoom, centerX = null, centerY = null) {
|
|
185
|
+
const oldZoom = this.zoom;
|
|
186
|
+
this.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, zoom));
|
|
187
|
+
|
|
188
|
+
// Adjust pan to zoom toward point
|
|
189
|
+
if (centerX !== null && centerY !== null) {
|
|
190
|
+
const scale = this.zoom / oldZoom;
|
|
191
|
+
this.panX = centerX - (centerX - this.panX) * scale;
|
|
192
|
+
this.panY = centerY - (centerY - this.panY) * scale;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.render();
|
|
196
|
+
this.app.events.emit(Events.CANVAS_ZOOM, { zoom: this.zoom });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Zoom in by step
|
|
201
|
+
*/
|
|
202
|
+
zoomIn() {
|
|
203
|
+
const zoomSteps = [1, 2, 3, 4, 5, 6.25, 8.33, 12.5, 16.67, 25, 33.33, 50, 66.67, 100, 150, 200, 300, 400, 500, 600, 800, 1200, 1600, 2400, 3200];
|
|
204
|
+
const nextZoom = zoomSteps.find(z => z > this.zoom) || this.maxZoom;
|
|
205
|
+
this.setZoom(nextZoom, this.displayCanvas.width / 2, this.displayCanvas.height / 2);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Zoom out by step
|
|
210
|
+
*/
|
|
211
|
+
zoomOut() {
|
|
212
|
+
const zoomSteps = [1, 2, 3, 4, 5, 6.25, 8.33, 12.5, 16.67, 25, 33.33, 50, 66.67, 100, 150, 200, 300, 400, 500, 600, 800, 1200, 1600, 2400, 3200];
|
|
213
|
+
const prevZoom = [...zoomSteps].reverse().find(z => z < this.zoom) || this.minZoom;
|
|
214
|
+
this.setZoom(prevZoom, this.displayCanvas.width / 2, this.displayCanvas.height / 2);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Fit canvas to screen
|
|
219
|
+
*/
|
|
220
|
+
fitToScreen() {
|
|
221
|
+
const padding = 40;
|
|
222
|
+
const viewportWidth = this.displayCanvas.width - padding * 2;
|
|
223
|
+
const viewportHeight = this.displayCanvas.height - padding * 2;
|
|
224
|
+
|
|
225
|
+
const scaleX = viewportWidth / this.width;
|
|
226
|
+
const scaleY = viewportHeight / this.height;
|
|
227
|
+
const scale = Math.min(scaleX, scaleY);
|
|
228
|
+
|
|
229
|
+
this.zoom = Math.round(scale * 100);
|
|
230
|
+
this.panX = (this.displayCanvas.width - this.width * scale) / 2;
|
|
231
|
+
this.panY = (this.displayCanvas.height - this.height * scale) / 2;
|
|
232
|
+
|
|
233
|
+
this.render();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Pan canvas
|
|
238
|
+
* @param {number} dx - Delta X
|
|
239
|
+
* @param {number} dy - Delta Y
|
|
240
|
+
*/
|
|
241
|
+
pan(dx, dy) {
|
|
242
|
+
this.panX += dx;
|
|
243
|
+
this.panY += dy;
|
|
244
|
+
this.render();
|
|
245
|
+
this.app.events.emit(Events.CANVAS_PAN, { panX: this.panX, panY: this.panY });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Convert viewport coordinates to canvas coordinates
|
|
250
|
+
* @param {number} viewX - Viewport X
|
|
251
|
+
* @param {number} viewY - Viewport Y
|
|
252
|
+
* @returns {Object} Canvas coordinates
|
|
253
|
+
*/
|
|
254
|
+
viewportToCanvas(viewX, viewY) {
|
|
255
|
+
const scale = this.zoom / 100;
|
|
256
|
+
return {
|
|
257
|
+
x: (viewX - this.panX) / scale,
|
|
258
|
+
y: (viewY - this.panY) / scale
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Convert canvas coordinates to viewport coordinates
|
|
264
|
+
* @param {number} canvasX - Canvas X
|
|
265
|
+
* @param {number} canvasY - Canvas Y
|
|
266
|
+
* @returns {Object} Viewport coordinates
|
|
267
|
+
*/
|
|
268
|
+
canvasToViewport(canvasX, canvasY) {
|
|
269
|
+
const scale = this.zoom / 100;
|
|
270
|
+
return {
|
|
271
|
+
x: canvasX * scale + this.panX,
|
|
272
|
+
y: canvasY * scale + this.panY
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Schedule a render
|
|
278
|
+
*/
|
|
279
|
+
scheduleRender() {
|
|
280
|
+
this.needsRender = true;
|
|
281
|
+
if (!this.renderScheduled) {
|
|
282
|
+
this.renderScheduled = true;
|
|
283
|
+
requestAnimationFrame(() => {
|
|
284
|
+
this.renderScheduled = false;
|
|
285
|
+
if (this.needsRender) {
|
|
286
|
+
this.needsRender = false;
|
|
287
|
+
this.render();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Render the canvas
|
|
295
|
+
*/
|
|
296
|
+
render() {
|
|
297
|
+
// Clear work canvas
|
|
298
|
+
this.workCtx.clearRect(0, 0, this.width, this.height);
|
|
299
|
+
|
|
300
|
+
// Draw checkerboard background
|
|
301
|
+
this.workCtx.fillStyle = this.checkerboardPattern;
|
|
302
|
+
this.workCtx.fillRect(0, 0, this.width, this.height);
|
|
303
|
+
|
|
304
|
+
// Composite all layers
|
|
305
|
+
this.app.layers.render(this.workCtx);
|
|
306
|
+
|
|
307
|
+
// Draw to display canvas with zoom/pan
|
|
308
|
+
this.displayCtx.clearRect(0, 0, this.displayCanvas.width, this.displayCanvas.height);
|
|
309
|
+
|
|
310
|
+
// Fill with gray background
|
|
311
|
+
if (this.app.options.theme === 'dark') {
|
|
312
|
+
this.displayCtx.fillStyle = '#18181B';
|
|
313
|
+
} else {
|
|
314
|
+
this.displayCtx.fillStyle = '#f5f5f5';
|
|
315
|
+
}
|
|
316
|
+
this.displayCtx.fillRect(0, 0, this.displayCanvas.width, this.displayCanvas.height);
|
|
317
|
+
|
|
318
|
+
// Draw work canvas with transform
|
|
319
|
+
const scale = this.zoom / 100;
|
|
320
|
+
this.displayCtx.save();
|
|
321
|
+
this.displayCtx.translate(this.panX, this.panY);
|
|
322
|
+
this.displayCtx.scale(scale, scale);
|
|
323
|
+
|
|
324
|
+
// Draw shadow
|
|
325
|
+
this.displayCtx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
326
|
+
this.displayCtx.shadowBlur = 20;
|
|
327
|
+
this.displayCtx.shadowOffsetX = 5;
|
|
328
|
+
this.displayCtx.shadowOffsetY = 5;
|
|
329
|
+
|
|
330
|
+
this.displayCtx.drawImage(this.workCanvas, 0, 0);
|
|
331
|
+
this.displayCtx.restore();
|
|
332
|
+
|
|
333
|
+
// Draw overlay (selection, guides, etc.)
|
|
334
|
+
this.renderOverlay();
|
|
335
|
+
|
|
336
|
+
this.app.events.emit(Events.CANVAS_RENDER);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Render overlay elements
|
|
341
|
+
*/
|
|
342
|
+
renderOverlay() {
|
|
343
|
+
this.overlayCtx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
|
|
344
|
+
|
|
345
|
+
const scale = this.zoom / 100;
|
|
346
|
+
this.overlayCtx.save();
|
|
347
|
+
this.overlayCtx.translate(this.panX, this.panY);
|
|
348
|
+
this.overlayCtx.scale(scale, scale);
|
|
349
|
+
|
|
350
|
+
// Draw selection if any
|
|
351
|
+
if (this.app.selection) {
|
|
352
|
+
this.app.selection.render(this.overlayCtx);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Draw tool overlay
|
|
356
|
+
if (this.app.tools?.currentTool) {
|
|
357
|
+
this.app.tools.currentTool.renderOverlay(this.overlayCtx);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this.overlayCtx.restore();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get image data from work canvas
|
|
365
|
+
* @param {string} format - Image format
|
|
366
|
+
* @param {number} quality - Quality (0-1)
|
|
367
|
+
* @returns {string} Data URL
|
|
368
|
+
*/
|
|
369
|
+
toDataURL(format = 'image/png', quality = 1) {
|
|
370
|
+
return this.workCanvas.toDataURL(format, quality);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Get image blob
|
|
375
|
+
* @param {string} format - Image format
|
|
376
|
+
* @param {number} quality - Quality (0-1)
|
|
377
|
+
* @returns {Promise<Blob>}
|
|
378
|
+
*/
|
|
379
|
+
toBlob(format = 'image/png', quality = 1) {
|
|
380
|
+
return new Promise(resolve => {
|
|
381
|
+
this.workCanvas.toBlob(resolve, format, quality);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Destroy canvas system
|
|
387
|
+
*/
|
|
388
|
+
destroy() {
|
|
389
|
+
if (this.resizeObserver) {
|
|
390
|
+
this.resizeObserver.disconnect();
|
|
391
|
+
}
|
|
392
|
+
if (this.viewport) {
|
|
393
|
+
this.viewport.remove();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export default Canvas;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Studio - EventEmitter
|
|
3
|
+
* Central event bus for module communication
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class EventEmitter {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.events = new Map();
|
|
10
|
+
this.onceEvents = new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Subscribe to an event
|
|
15
|
+
* @param {string} event - Event name
|
|
16
|
+
* @param {Function} callback - Callback function
|
|
17
|
+
* @returns {Function} Unsubscribe function
|
|
18
|
+
*/
|
|
19
|
+
on(event, callback) {
|
|
20
|
+
if (!this.events.has(event)) {
|
|
21
|
+
this.events.set(event, new Set());
|
|
22
|
+
}
|
|
23
|
+
this.events.get(event).add(callback);
|
|
24
|
+
|
|
25
|
+
// Return unsubscribe function
|
|
26
|
+
return () => this.off(event, callback);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Subscribe to an event once
|
|
31
|
+
* @param {string} event - Event name
|
|
32
|
+
* @param {Function} callback - Callback function
|
|
33
|
+
*/
|
|
34
|
+
once(event, callback) {
|
|
35
|
+
if (!this.onceEvents.has(event)) {
|
|
36
|
+
this.onceEvents.set(event, new Set());
|
|
37
|
+
}
|
|
38
|
+
this.onceEvents.get(event).add(callback);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Unsubscribe from an event
|
|
43
|
+
* @param {string} event - Event name
|
|
44
|
+
* @param {Function} callback - Callback function
|
|
45
|
+
*/
|
|
46
|
+
off(event, callback) {
|
|
47
|
+
if (this.events.has(event)) {
|
|
48
|
+
this.events.get(event).delete(callback);
|
|
49
|
+
}
|
|
50
|
+
if (this.onceEvents.has(event)) {
|
|
51
|
+
this.onceEvents.get(event).delete(callback);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Emit an event
|
|
57
|
+
* @param {string} event - Event name
|
|
58
|
+
* @param {*} data - Event data
|
|
59
|
+
*/
|
|
60
|
+
emit(event, data) {
|
|
61
|
+
// Call regular listeners
|
|
62
|
+
if (this.events.has(event)) {
|
|
63
|
+
this.events.get(event).forEach(callback => {
|
|
64
|
+
try {
|
|
65
|
+
callback(data);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`Error in event listener for "${event}":`, error);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Call once listeners and remove them
|
|
73
|
+
if (this.onceEvents.has(event)) {
|
|
74
|
+
this.onceEvents.get(event).forEach(callback => {
|
|
75
|
+
try {
|
|
76
|
+
callback(data);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(`Error in once listener for "${event}":`, error);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
this.onceEvents.delete(event);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Also emit a wildcard event for debugging
|
|
85
|
+
if (event !== '*') {
|
|
86
|
+
this.emit('*', { event, data });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Remove all listeners for an event
|
|
92
|
+
* @param {string} event - Event name (optional, removes all if not provided)
|
|
93
|
+
*/
|
|
94
|
+
removeAllListeners(event) {
|
|
95
|
+
if (event) {
|
|
96
|
+
this.events.delete(event);
|
|
97
|
+
this.onceEvents.delete(event);
|
|
98
|
+
} else {
|
|
99
|
+
this.events.clear();
|
|
100
|
+
this.onceEvents.clear();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get listener count for an event
|
|
106
|
+
* @param {string} event - Event name
|
|
107
|
+
* @returns {number} Listener count
|
|
108
|
+
*/
|
|
109
|
+
listenerCount(event) {
|
|
110
|
+
const regular = this.events.has(event) ? this.events.get(event).size : 0;
|
|
111
|
+
const once = this.onceEvents.has(event) ? this.onceEvents.get(event).size : 0;
|
|
112
|
+
return regular + once;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Event name constants
|
|
117
|
+
export const Events = {
|
|
118
|
+
// Document events
|
|
119
|
+
DOCUMENT_NEW: 'document:new',
|
|
120
|
+
DOCUMENT_OPEN: 'document:open',
|
|
121
|
+
DOCUMENT_SAVE: 'document:save',
|
|
122
|
+
DOCUMENT_EXPORT: 'document:export',
|
|
123
|
+
DOCUMENT_CLOSE: 'document:close',
|
|
124
|
+
DOCUMENT_RESIZE: 'document:resize',
|
|
125
|
+
|
|
126
|
+
// Layer events
|
|
127
|
+
LAYER_ADD: 'layer:add',
|
|
128
|
+
LAYER_REMOVE: 'layer:remove',
|
|
129
|
+
LAYER_SELECT: 'layer:select',
|
|
130
|
+
LAYER_RENAME: 'layer:rename',
|
|
131
|
+
LAYER_REORDER: 'layer:reorder',
|
|
132
|
+
LAYER_VISIBILITY: 'layer:visibility',
|
|
133
|
+
LAYER_OPACITY: 'layer:opacity',
|
|
134
|
+
LAYER_BLEND_MODE: 'layer:blendMode',
|
|
135
|
+
LAYER_LOCK: 'layer:lock',
|
|
136
|
+
LAYER_MERGE: 'layer:merge',
|
|
137
|
+
LAYER_DUPLICATE: 'layer:duplicate',
|
|
138
|
+
LAYER_UPDATE: 'layer:update',
|
|
139
|
+
|
|
140
|
+
// Tool events
|
|
141
|
+
TOOL_SELECT: 'tool:select',
|
|
142
|
+
TOOL_OPTIONS_CHANGE: 'tool:optionsChange',
|
|
143
|
+
TOOL_START: 'tool:start',
|
|
144
|
+
TOOL_MOVE: 'tool:move',
|
|
145
|
+
TOOL_END: 'tool:end',
|
|
146
|
+
|
|
147
|
+
// Canvas events
|
|
148
|
+
CANVAS_DRAW: 'canvas:draw',
|
|
149
|
+
CANVAS_CLEAR: 'canvas:clear',
|
|
150
|
+
CANVAS_RENDER: 'canvas:render',
|
|
151
|
+
CANVAS_ZOOM: 'canvas:zoom',
|
|
152
|
+
CANVAS_PAN: 'canvas:pan',
|
|
153
|
+
|
|
154
|
+
// History events
|
|
155
|
+
HISTORY_PUSH: 'history:push',
|
|
156
|
+
HISTORY_UNDO: 'history:undo',
|
|
157
|
+
HISTORY_REDO: 'history:redo',
|
|
158
|
+
HISTORY_CLEAR: 'history:clear',
|
|
159
|
+
|
|
160
|
+
// Selection events
|
|
161
|
+
SELECTION_CREATE: 'selection:create',
|
|
162
|
+
SELECTION_MODIFY: 'selection:modify',
|
|
163
|
+
SELECTION_CLEAR: 'selection:clear',
|
|
164
|
+
SELECTION_INVERT: 'selection:invert',
|
|
165
|
+
|
|
166
|
+
// Filter events
|
|
167
|
+
FILTER_APPLY: 'filter:apply',
|
|
168
|
+
FILTER_PREVIEW: 'filter:preview',
|
|
169
|
+
FILTER_CANCEL: 'filter:cancel',
|
|
170
|
+
|
|
171
|
+
// Color events
|
|
172
|
+
COLOR_FOREGROUND: 'color:foreground',
|
|
173
|
+
COLOR_BACKGROUND: 'color:background',
|
|
174
|
+
COLOR_SWAP: 'color:swap',
|
|
175
|
+
|
|
176
|
+
// UI events
|
|
177
|
+
UI_PANEL_TOGGLE: 'ui:panelToggle',
|
|
178
|
+
UI_DIALOG_OPEN: 'ui:dialogOpen',
|
|
179
|
+
UI_DIALOG_CLOSE: 'ui:dialogClose',
|
|
180
|
+
UI_MENU_ACTION: 'ui:menuAction',
|
|
181
|
+
|
|
182
|
+
// General
|
|
183
|
+
CHANGE: 'change',
|
|
184
|
+
ERROR: 'error',
|
|
185
|
+
READY: 'ready'
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export default EventEmitter;
|