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.
Files changed (44) hide show
  1. package/README.md +219 -235
  2. package/dist/swp.css +884 -344
  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 +12 -5
  8. package/src/css/swp.css +884 -344
  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 +297 -709
  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/spec.md +0 -239
  44. package/swp_preview.png +0 -0
@@ -0,0 +1,459 @@
1
+ /**
2
+ * SenangWebs Studio - Layer Manager
3
+ * Managing multiple layers
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { Events } from '../core/EventEmitter.js';
8
+ import { Layer } from './Layer.js';
9
+
10
+ export class LayerManager {
11
+ constructor(app) {
12
+ this.app = app;
13
+ this.layers = [];
14
+ this.activeLayer = null;
15
+ }
16
+
17
+ /**
18
+ * Initialize with default background layer
19
+ * @param {number} width - Canvas width
20
+ * @param {number} height - Canvas height
21
+ */
22
+ init(width, height) {
23
+ this.clear();
24
+
25
+ // Create background layer
26
+ const bgLayer = this.addLayer({
27
+ name: 'Background',
28
+ type: 'raster'
29
+ });
30
+
31
+ bgLayer.initCanvas(width, height);
32
+ bgLayer.fill('#ffffff');
33
+
34
+ this.setActiveLayer(bgLayer.id);
35
+ }
36
+
37
+ /**
38
+ * Add a new layer
39
+ * @param {Object} options - Layer options
40
+ * @returns {Layer}
41
+ */
42
+ addLayer(options = {}) {
43
+ const layer = new Layer(options);
44
+ layer.app = this.app;
45
+
46
+ // Initialize canvas if dimensions available
47
+ if (!layer.canvas && this.app.canvas) {
48
+ layer.initCanvas(this.app.canvas.width, this.app.canvas.height);
49
+ }
50
+
51
+ // Insert at top (or at specified index)
52
+ const insertIndex = options.index !== undefined ? options.index : this.layers.length;
53
+ this.layers.splice(insertIndex, 0, layer);
54
+
55
+ // Set as active if no active layer
56
+ if (!this.activeLayer) {
57
+ this.activeLayer = layer;
58
+ }
59
+
60
+ this.app.events.emit(Events.LAYER_ADD, { layer, index: insertIndex });
61
+
62
+ return layer;
63
+ }
64
+
65
+ /**
66
+ * Remove a layer
67
+ * @param {string} layerId - Layer ID
68
+ * @returns {boolean}
69
+ */
70
+ removeLayer(layerId) {
71
+ const index = this.layers.findIndex(l => l.id === layerId);
72
+ if (index === -1) return false;
73
+
74
+ // Don't remove the last layer
75
+ if (this.layers.length === 1) {
76
+ console.warn('Cannot remove the last layer');
77
+ return false;
78
+ }
79
+
80
+ const removed = this.layers.splice(index, 1)[0];
81
+
82
+ // Update active layer if needed
83
+ if (this.activeLayer?.id === layerId) {
84
+ this.activeLayer = this.layers[Math.min(index, this.layers.length - 1)];
85
+ }
86
+
87
+ this.app.events.emit(Events.LAYER_REMOVE, { layer: removed, index });
88
+ this.app.canvas.scheduleRender();
89
+
90
+ return true;
91
+ }
92
+
93
+ /**
94
+ * Get layer by ID
95
+ * @param {string} layerId - Layer ID
96
+ * @returns {Layer|null}
97
+ */
98
+ getLayer(layerId) {
99
+ return this.layers.find(l => l.id === layerId) || null;
100
+ }
101
+
102
+ /**
103
+ * Get all layers
104
+ * @returns {Layer[]}
105
+ */
106
+ getLayers() {
107
+ return [...this.layers];
108
+ }
109
+
110
+ /**
111
+ * Get visible layers
112
+ * @returns {Layer[]}
113
+ */
114
+ getVisibleLayers() {
115
+ return this.layers.filter(l => l.visible);
116
+ }
117
+
118
+ /**
119
+ * Set active layer
120
+ * @param {string} layerId - Layer ID
121
+ */
122
+ setActiveLayer(layerId) {
123
+ const layer = this.getLayer(layerId);
124
+ if (layer) {
125
+ this.activeLayer = layer;
126
+ this.app.events.emit(Events.LAYER_SELECT, { layer });
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Get active layer
132
+ * @returns {Layer|null}
133
+ */
134
+ getActiveLayer() {
135
+ return this.activeLayer;
136
+ }
137
+
138
+ /**
139
+ * Move layer to new position
140
+ * @param {string} layerId - Layer ID
141
+ * @param {number} newIndex - New index
142
+ */
143
+ moveLayer(layerId, newIndex) {
144
+ const currentIndex = this.layers.findIndex(l => l.id === layerId);
145
+ if (currentIndex === -1) return;
146
+
147
+ const [layer] = this.layers.splice(currentIndex, 1);
148
+ this.layers.splice(newIndex, 0, layer);
149
+
150
+ this.app.events.emit(Events.LAYER_REORDER, { layer, oldIndex: currentIndex, newIndex });
151
+ this.app.canvas.scheduleRender();
152
+ }
153
+
154
+ /**
155
+ * Move layer up
156
+ * @param {string} layerId - Layer ID
157
+ */
158
+ moveLayerUp(layerId) {
159
+ const index = this.layers.findIndex(l => l.id === layerId);
160
+ if (index < this.layers.length - 1) {
161
+ this.moveLayer(layerId, index + 1);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Move layer down
167
+ * @param {string} layerId - Layer ID
168
+ */
169
+ moveLayerDown(layerId) {
170
+ const index = this.layers.findIndex(l => l.id === layerId);
171
+ if (index > 0) {
172
+ this.moveLayer(layerId, index - 1);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Rename layer
178
+ * @param {string} layerId - Layer ID
179
+ * @param {string} name - New name
180
+ */
181
+ renameLayer(layerId, name) {
182
+ const layer = this.getLayer(layerId);
183
+ if (layer) {
184
+ layer.name = name;
185
+ this.app.events.emit(Events.LAYER_RENAME, { layer, name });
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Set layer visibility
191
+ * @param {string} layerId - Layer ID
192
+ * @param {boolean} visible - Visibility
193
+ */
194
+ setLayerVisibility(layerId, visible) {
195
+ const layer = this.getLayer(layerId);
196
+ if (layer) {
197
+ layer.visible = visible;
198
+ this.app.events.emit(Events.LAYER_VISIBILITY, { layer, visible });
199
+ this.app.canvas.scheduleRender();
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Set layer opacity
205
+ * @param {string} layerId - Layer ID
206
+ * @param {number} opacity - Opacity (0-100)
207
+ */
208
+ setLayerOpacity(layerId, opacity) {
209
+ const layer = this.getLayer(layerId);
210
+ if (layer) {
211
+ layer.opacity = Math.max(0, Math.min(100, opacity));
212
+ this.app.events.emit(Events.LAYER_OPACITY, { layer, opacity: layer.opacity });
213
+ this.app.canvas.scheduleRender();
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Set layer blend mode
219
+ * @param {string} layerId - Layer ID
220
+ * @param {string} blendMode - Blend mode
221
+ */
222
+ setLayerBlendMode(layerId, blendMode) {
223
+ const layer = this.getLayer(layerId);
224
+ if (layer) {
225
+ layer.blendMode = blendMode;
226
+ this.app.events.emit(Events.LAYER_BLEND_MODE, { layer, blendMode });
227
+ this.app.canvas.scheduleRender();
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Lock/unlock layer
233
+ * @param {string} layerId - Layer ID
234
+ * @param {boolean} locked - Lock state
235
+ */
236
+ setLayerLocked(layerId, locked) {
237
+ const layer = this.getLayer(layerId);
238
+ if (layer) {
239
+ layer.locked = locked;
240
+ this.app.events.emit(Events.LAYER_LOCK, { layer, locked });
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Duplicate layer
246
+ * @param {string} layerId - Layer ID
247
+ * @returns {Layer|null}
248
+ */
249
+ duplicateLayer(layerId) {
250
+ const layer = this.getLayer(layerId);
251
+ if (!layer) return null;
252
+
253
+ const cloned = layer.clone();
254
+ cloned.app = this.app;
255
+
256
+ const index = this.layers.findIndex(l => l.id === layerId);
257
+ this.layers.splice(index + 1, 0, cloned);
258
+
259
+ this.app.events.emit(Events.LAYER_DUPLICATE, { original: layer, duplicate: cloned });
260
+ this.app.canvas.scheduleRender();
261
+
262
+ return cloned;
263
+ }
264
+
265
+ /**
266
+ * Merge layer with layer below
267
+ * @param {string} layerId - Layer ID
268
+ */
269
+ mergeDown(layerId) {
270
+ const index = this.layers.findIndex(l => l.id === layerId);
271
+ if (index <= 0) return; // Can't merge bottom layer
272
+
273
+ const topLayer = this.layers[index];
274
+ const bottomLayer = this.layers[index - 1];
275
+
276
+ if (topLayer.locked || bottomLayer.locked) {
277
+ console.warn('Cannot merge locked layers');
278
+ return;
279
+ }
280
+
281
+ // Draw top layer onto bottom layer
282
+ bottomLayer.ctx.save();
283
+ bottomLayer.ctx.globalAlpha = topLayer.opacity / 100;
284
+ bottomLayer.ctx.globalCompositeOperation = topLayer.getCompositeOperation();
285
+ bottomLayer.ctx.translate(topLayer.position.x, topLayer.position.y);
286
+ bottomLayer.ctx.drawImage(topLayer.canvas, 0, 0);
287
+ bottomLayer.ctx.restore();
288
+
289
+ // Remove top layer
290
+ this.layers.splice(index, 1);
291
+
292
+ if (this.activeLayer?.id === layerId) {
293
+ this.activeLayer = bottomLayer;
294
+ }
295
+
296
+ this.app.events.emit(Events.LAYER_MERGE, { merged: bottomLayer, removed: topLayer });
297
+ this.app.canvas.scheduleRender();
298
+ }
299
+
300
+ /**
301
+ * Merge all visible layers
302
+ * @returns {Layer}
303
+ */
304
+ mergeVisible() {
305
+ const visibleLayers = this.getVisibleLayers();
306
+ if (visibleLayers.length === 0) return null;
307
+
308
+ // Create new layer for merged result
309
+ const mergedLayer = new Layer({
310
+ name: 'Merged',
311
+ type: 'raster'
312
+ });
313
+ mergedLayer.app = this.app;
314
+ mergedLayer.initCanvas(this.app.canvas.width, this.app.canvas.height);
315
+
316
+ // Composite visible layers
317
+ visibleLayers.forEach(layer => {
318
+ mergedLayer.ctx.save();
319
+ mergedLayer.ctx.globalAlpha = layer.opacity / 100;
320
+ mergedLayer.ctx.globalCompositeOperation = layer.getCompositeOperation();
321
+ mergedLayer.ctx.translate(layer.position.x, layer.position.y);
322
+ if (layer.canvas) {
323
+ mergedLayer.ctx.drawImage(layer.canvas, 0, 0);
324
+ }
325
+ mergedLayer.ctx.restore();
326
+ });
327
+
328
+ // Remove old visible layers and add merged
329
+ this.layers = this.layers.filter(l => !l.visible);
330
+ this.layers.push(mergedLayer);
331
+ this.activeLayer = mergedLayer;
332
+
333
+ this.app.canvas.scheduleRender();
334
+
335
+ return mergedLayer;
336
+ }
337
+
338
+ /**
339
+ * Flatten all layers into one
340
+ * @returns {Layer}
341
+ */
342
+ flatten() {
343
+ const flatLayer = new Layer({
344
+ name: 'Background',
345
+ type: 'raster'
346
+ });
347
+ flatLayer.app = this.app;
348
+ flatLayer.initCanvas(this.app.canvas.width, this.app.canvas.height);
349
+
350
+ // Fill with white background
351
+ flatLayer.fill('#ffffff');
352
+
353
+ // Composite all layers
354
+ this.layers.forEach(layer => {
355
+ if (!layer.visible) return;
356
+
357
+ flatLayer.ctx.save();
358
+ flatLayer.ctx.globalAlpha = layer.opacity / 100;
359
+ flatLayer.ctx.globalCompositeOperation = layer.getCompositeOperation();
360
+ flatLayer.ctx.translate(layer.position.x, layer.position.y);
361
+ if (layer.canvas) {
362
+ flatLayer.ctx.drawImage(layer.canvas, 0, 0);
363
+ }
364
+ flatLayer.ctx.restore();
365
+ });
366
+
367
+ this.layers = [flatLayer];
368
+ this.activeLayer = flatLayer;
369
+
370
+ this.app.canvas.scheduleRender();
371
+
372
+ return flatLayer;
373
+ }
374
+
375
+ /**
376
+ * Render all layers to context
377
+ * @param {CanvasRenderingContext2D} ctx - Target context
378
+ */
379
+ render(ctx) {
380
+ // Render layers from bottom to top
381
+ this.layers.forEach(layer => {
382
+ if (layer.visible) {
383
+ layer.render(ctx);
384
+ }
385
+ });
386
+ }
387
+
388
+ /**
389
+ * Clear all layers
390
+ */
391
+ clear() {
392
+ this.layers = [];
393
+ this.activeLayer = null;
394
+ }
395
+
396
+ /**
397
+ * Delete content in selection on active layer
398
+ */
399
+ deleteSelection() {
400
+ if (!this.activeLayer || this.activeLayer.locked) return;
401
+
402
+ if (!this.app.selection?.hasSelection()) {
403
+ // No selection - remove the active layer entirely (if more than one layer)
404
+ if (this.layers.length > 1) {
405
+ const layerId = this.activeLayer.id;
406
+ this.removeLayer(layerId);
407
+ this.app.history.pushState('Delete Layer');
408
+ } else {
409
+ // Only one layer - just clear its content
410
+ this.activeLayer.clear();
411
+ this.app.canvas.scheduleRender();
412
+ this.app.history.pushState('Clear Layer');
413
+ }
414
+ } else {
415
+ // Clear selection area
416
+ this.app.selection.clearSelection(this.activeLayer);
417
+ this.app.canvas.scheduleRender();
418
+ this.app.history.pushState('Delete Selection');
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Serialize all layers to JSON
424
+ * @returns {Array}
425
+ */
426
+ toJSON() {
427
+ return this.layers.map(l => l.toJSON());
428
+ }
429
+
430
+ /**
431
+ * Load layers from JSON
432
+ * @param {Array} layersData - Layers data
433
+ */
434
+ async fromJSON(layersData) {
435
+ this.clear();
436
+
437
+ for (const data of layersData) {
438
+ const layer = await Layer.fromJSON(data);
439
+ layer.app = this.app;
440
+ this.layers.push(layer);
441
+ }
442
+
443
+ if (this.layers.length > 0) {
444
+ this.activeLayer = this.layers[this.layers.length - 1];
445
+ }
446
+
447
+ this.app.canvas.scheduleRender();
448
+ }
449
+
450
+ /**
451
+ * Get layer count
452
+ * @returns {number}
453
+ */
454
+ get count() {
455
+ return this.layers.length;
456
+ }
457
+ }
458
+
459
+ export default LayerManager;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * SenangWebs Studio - Selection System
3
+ * @version 2.0.0
4
+ */
5
+
6
+ import { Events } from '../core/EventEmitter.js';
7
+
8
+ export class Selection {
9
+ constructor(app) {
10
+ this.app = app;
11
+ this.path = null;
12
+ this.bounds = null;
13
+ this.shape = 'rectangle';
14
+ this.feather = 0;
15
+ this.marchingAntsOffset = 0;
16
+ this.animationId = null;
17
+ }
18
+
19
+ hasSelection() {
20
+ return this.path !== null || this.bounds !== null;
21
+ }
22
+
23
+ setRect(x, y, width, height, shape = 'rectangle') {
24
+ this.bounds = { x, y, width, height };
25
+ this.shape = shape;
26
+ this.path = null;
27
+ this.startMarchingAnts();
28
+ this.app.events.emit(Events.SELECTION_CREATE, { bounds: this.bounds, shape });
29
+ }
30
+
31
+ setPath(points) {
32
+ this.path = points;
33
+ this.bounds = this.calculatePathBounds(points);
34
+ this.shape = 'freeform';
35
+ this.startMarchingAnts();
36
+ this.app.events.emit(Events.SELECTION_CREATE, { bounds: this.bounds, path: this.path });
37
+ }
38
+
39
+ calculatePathBounds(points) {
40
+ if (!points || points.length === 0) return null;
41
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
42
+ points.forEach(p => {
43
+ minX = Math.min(minX, p.x);
44
+ minY = Math.min(minY, p.y);
45
+ maxX = Math.max(maxX, p.x);
46
+ maxY = Math.max(maxY, p.y);
47
+ });
48
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
49
+ }
50
+
51
+ clear() {
52
+ this.path = null;
53
+ this.bounds = null;
54
+ this.stopMarchingAnts();
55
+ this.app.events.emit(Events.SELECTION_CLEAR);
56
+ this.app.canvas.scheduleRender();
57
+ }
58
+
59
+ selectAll() {
60
+ this.setRect(0, 0, this.app.canvas.width, this.app.canvas.height);
61
+ }
62
+
63
+ deselect() {
64
+ this.clear();
65
+ }
66
+
67
+ invert() {
68
+ // TODO: Implement selection inversion
69
+ this.app.events.emit(Events.SELECTION_INVERT);
70
+ }
71
+
72
+ isPointInSelection(x, y) {
73
+ if (!this.hasSelection()) return true;
74
+ if (this.shape === 'ellipse' && this.bounds) {
75
+ const cx = this.bounds.x + this.bounds.width / 2;
76
+ const cy = this.bounds.y + this.bounds.height / 2;
77
+ const rx = this.bounds.width / 2;
78
+ const ry = this.bounds.height / 2;
79
+ return Math.pow(x - cx, 2) / Math.pow(rx, 2) + Math.pow(y - cy, 2) / Math.pow(ry, 2) <= 1;
80
+ }
81
+ if (this.bounds) {
82
+ return x >= this.bounds.x && x <= this.bounds.x + this.bounds.width &&
83
+ y >= this.bounds.y && y <= this.bounds.y + this.bounds.height;
84
+ }
85
+ return true;
86
+ }
87
+
88
+ startMarchingAnts() {
89
+ this.stopMarchingAnts();
90
+ const animate = () => {
91
+ this.marchingAntsOffset = (this.marchingAntsOffset + 0.5) % 10;
92
+ this.app.canvas.scheduleRender();
93
+ this.animationId = requestAnimationFrame(animate);
94
+ };
95
+ this.animationId = requestAnimationFrame(animate);
96
+ }
97
+
98
+ stopMarchingAnts() {
99
+ if (this.animationId) {
100
+ cancelAnimationFrame(this.animationId);
101
+ this.animationId = null;
102
+ }
103
+ }
104
+
105
+ render(ctx) {
106
+ if (!this.hasSelection()) return;
107
+
108
+ ctx.save();
109
+ ctx.strokeStyle = '#000000';
110
+ ctx.lineWidth = 1;
111
+ ctx.setLineDash([5, 5]);
112
+ ctx.lineDashOffset = -this.marchingAntsOffset;
113
+
114
+ if (this.shape === 'ellipse' && this.bounds) {
115
+ ctx.beginPath();
116
+ ctx.ellipse(this.bounds.x + this.bounds.width/2, this.bounds.y + this.bounds.height/2,
117
+ this.bounds.width/2, this.bounds.height/2, 0, 0, Math.PI * 2);
118
+ ctx.stroke();
119
+ } else if (this.path) {
120
+ ctx.beginPath();
121
+ ctx.moveTo(this.path[0].x, this.path[0].y);
122
+ this.path.slice(1).forEach(p => ctx.lineTo(p.x, p.y));
123
+ ctx.closePath();
124
+ ctx.stroke();
125
+ } else if (this.bounds) {
126
+ ctx.strokeRect(this.bounds.x, this.bounds.y, this.bounds.width, this.bounds.height);
127
+ }
128
+
129
+ // White stroke for contrast
130
+ ctx.strokeStyle = '#ffffff';
131
+ ctx.lineDashOffset = -this.marchingAntsOffset + 5;
132
+ if (this.shape === 'ellipse' && this.bounds) {
133
+ ctx.beginPath();
134
+ ctx.ellipse(this.bounds.x + this.bounds.width/2, this.bounds.y + this.bounds.height/2,
135
+ this.bounds.width/2, this.bounds.height/2, 0, 0, Math.PI * 2);
136
+ ctx.stroke();
137
+ } else if (this.bounds) {
138
+ ctx.strokeRect(this.bounds.x, this.bounds.y, this.bounds.width, this.bounds.height);
139
+ }
140
+
141
+ ctx.restore();
142
+ }
143
+
144
+ clearSelection(layer) {
145
+ if (!this.hasSelection() || !layer.ctx) return;
146
+ const ctx = layer.ctx;
147
+ ctx.save();
148
+ ctx.globalCompositeOperation = 'destination-out';
149
+
150
+ if (this.shape === 'ellipse' && this.bounds) {
151
+ ctx.beginPath();
152
+ ctx.ellipse(this.bounds.x + this.bounds.width/2, this.bounds.y + this.bounds.height/2,
153
+ this.bounds.width/2, this.bounds.height/2, 0, 0, Math.PI * 2);
154
+ ctx.fill();
155
+ } else if (this.bounds) {
156
+ ctx.fillRect(this.bounds.x, this.bounds.y, this.bounds.width, this.bounds.height);
157
+ }
158
+
159
+ ctx.restore();
160
+ }
161
+
162
+ destroy() {
163
+ this.stopMarchingAnts();
164
+ }
165
+ }
166
+
167
+ export default Selection;