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.
Files changed (43) hide show
  1. package/README.md +220 -236
  2. package/dist/swp.css +790 -256
  3. package/dist/swp.js +1 -1
  4. package/examples/data-attribute.html +69 -0
  5. package/examples/index.html +56 -51
  6. package/examples/studio.html +83 -0
  7. package/package.json +10 -6
  8. package/src/css/swp.css +790 -256
  9. package/src/js/core/Canvas.js +398 -0
  10. package/src/js/core/EventEmitter.js +188 -0
  11. package/src/js/core/History.js +250 -0
  12. package/src/js/core/Keyboard.js +323 -0
  13. package/src/js/filters/FilterManager.js +248 -0
  14. package/src/js/index.js +48 -0
  15. package/src/js/io/Clipboard.js +52 -0
  16. package/src/js/io/FileManager.js +150 -0
  17. package/src/js/layers/BlendModes.js +342 -0
  18. package/src/js/layers/Layer.js +415 -0
  19. package/src/js/layers/LayerManager.js +459 -0
  20. package/src/js/selection/Selection.js +167 -0
  21. package/src/js/swp.js +247 -761
  22. package/src/js/tools/BaseTool.js +264 -0
  23. package/src/js/tools/BrushTool.js +314 -0
  24. package/src/js/tools/CropTool.js +400 -0
  25. package/src/js/tools/EraserTool.js +155 -0
  26. package/src/js/tools/EyedropperTool.js +184 -0
  27. package/src/js/tools/FillTool.js +109 -0
  28. package/src/js/tools/GradientTool.js +141 -0
  29. package/src/js/tools/HandTool.js +51 -0
  30. package/src/js/tools/MarqueeTool.js +103 -0
  31. package/src/js/tools/MoveTool.js +465 -0
  32. package/src/js/tools/ShapeTool.js +285 -0
  33. package/src/js/tools/TextTool.js +253 -0
  34. package/src/js/tools/ToolManager.js +277 -0
  35. package/src/js/tools/ZoomTool.js +68 -0
  36. package/src/js/ui/ColorManager.js +71 -0
  37. package/src/js/ui/UI.js +1211 -0
  38. package/swp_preview1.png +0 -0
  39. package/swp_preview2.png +0 -0
  40. package/webpack.config.js +4 -11
  41. package/dist/styles.js +0 -1
  42. package/examples/customization.html +0 -360
  43. 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;