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,465 @@
1
+ /**
2
+ * SenangWebs Studio - Move Tool
3
+ * Move and transform layers
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { BaseTool } from './BaseTool.js';
8
+
9
+ export class MoveTool extends BaseTool {
10
+ constructor(app) {
11
+ super(app);
12
+ this.name = 'move';
13
+ this.icon = 'move';
14
+ this.cursor = 'move';
15
+ this.shortcut = 'v';
16
+
17
+ this.options = {
18
+ autoSelect: false,
19
+ showTransform: true,
20
+ snapToEdges: true,
21
+ snapThreshold: 4
22
+ };
23
+ this.defaultOptions = { ...this.options };
24
+
25
+ // Transform state
26
+ this.transforming = false;
27
+ this.transformHandle = null;
28
+ this.originalBounds = null;
29
+ this.originalImageData = null; // Store original image for quality resize
30
+
31
+ // Snap state
32
+ this.activeSnaps = { x: null, y: null };
33
+ }
34
+
35
+ onPointerDown(e) {
36
+ super.onPointerDown(e);
37
+
38
+ const layer = this.app.layers.getActiveLayer();
39
+ if (!layer || layer.locked) return;
40
+
41
+ // Check if clicking on transform handle
42
+ if (this.options.showTransform) {
43
+ this.transformHandle = this.hitTestTransformHandles(this.startPoint);
44
+ }
45
+
46
+ if (this.transformHandle) {
47
+ this.transforming = true;
48
+ this.originalBounds = this.getLayerBounds(layer);
49
+ // Store original image data for quality resize
50
+ if (layer.canvas) {
51
+ this.originalImageData = layer.ctx.getImageData(0, 0, layer.width, layer.height);
52
+ }
53
+ } else {
54
+ // Auto-select layer at click position if enabled
55
+ if (this.options.autoSelect) {
56
+ this.selectLayerAtPoint(this.startPoint);
57
+ }
58
+ }
59
+
60
+ // Reset snaps
61
+ this.activeSnaps = { x: null, y: null };
62
+ }
63
+
64
+ onPointerMove(e) {
65
+ super.onPointerMove(e);
66
+
67
+ if (!this.isDrawing) return;
68
+
69
+ const layer = this.app.layers.getActiveLayer();
70
+ if (!layer || layer.locked) return;
71
+
72
+ const dx = this.currentPoint.x - this.lastPoint.x;
73
+ const dy = this.currentPoint.y - this.lastPoint.y;
74
+
75
+ if (this.transforming && this.transformHandle) {
76
+ this.applyTransform(layer, this.transformHandle, dx, dy);
77
+ } else {
78
+ // Move layer
79
+ layer.position.x += dx;
80
+ layer.position.y += dy;
81
+
82
+ // Apply snapping
83
+ if (this.options.snapToEdges) {
84
+ const snap = this.calculateSnap(layer);
85
+ if (snap.x !== null) {
86
+ layer.position.x = snap.x;
87
+ }
88
+ if (snap.y !== null) {
89
+ layer.position.y = snap.y;
90
+ }
91
+ this.activeSnaps = { x: snap.snapX, y: snap.snapY };
92
+ } else {
93
+ this.activeSnaps = { x: null, y: null };
94
+ }
95
+ }
96
+
97
+ this.app.canvas.scheduleRender();
98
+ }
99
+
100
+ onPointerUp(e) {
101
+ if (this.isDrawing) {
102
+ this.app.history.pushState('Move Layer');
103
+ }
104
+
105
+ this.transforming = false;
106
+ this.transformHandle = null;
107
+ this.originalBounds = null;
108
+ this.originalImageData = null;
109
+ this.activeSnaps = { x: null, y: null };
110
+
111
+ super.onPointerUp(e);
112
+ this.app.canvas.scheduleRender();
113
+ }
114
+
115
+ /**
116
+ * Calculate snap positions for layer
117
+ * @param {Layer} layer - Layer to snap
118
+ * @returns {Object} Snap result with x, y positions and snap line positions
119
+ */
120
+ calculateSnap(layer) {
121
+ const threshold = this.options.snapThreshold;
122
+ const canvasWidth = this.app.canvas.width;
123
+ const canvasHeight = this.app.canvas.height;
124
+
125
+ const bounds = this.getLayerBounds(layer);
126
+ const layerLeft = bounds.x;
127
+ const layerRight = bounds.x + bounds.width;
128
+ const layerTop = bounds.y;
129
+ const layerBottom = bounds.y + bounds.height;
130
+ const layerCenterX = bounds.x + bounds.width / 2;
131
+ const layerCenterY = bounds.y + bounds.height / 2;
132
+
133
+ const canvasCenterX = canvasWidth / 2;
134
+ const canvasCenterY = canvasHeight / 2;
135
+
136
+ let snapX = null;
137
+ let snapY = null;
138
+ let newX = null;
139
+ let newY = null;
140
+
141
+ // Horizontal snapping (X axis)
142
+ // Left edge to canvas left
143
+ if (Math.abs(layerLeft) < threshold) {
144
+ newX = 0;
145
+ snapX = 0;
146
+ }
147
+ // Right edge to canvas right
148
+ else if (Math.abs(layerRight - canvasWidth) < threshold) {
149
+ newX = canvasWidth - bounds.width;
150
+ snapX = canvasWidth;
151
+ }
152
+ // Center to canvas center
153
+ else if (Math.abs(layerCenterX - canvasCenterX) < threshold) {
154
+ newX = canvasCenterX - bounds.width / 2;
155
+ snapX = canvasCenterX;
156
+ }
157
+
158
+ // Vertical snapping (Y axis)
159
+ // Top edge to canvas top
160
+ if (Math.abs(layerTop) < threshold) {
161
+ newY = 0;
162
+ snapY = 0;
163
+ }
164
+ // Bottom edge to canvas bottom
165
+ else if (Math.abs(layerBottom - canvasHeight) < threshold) {
166
+ newY = canvasHeight - bounds.height;
167
+ snapY = canvasHeight;
168
+ }
169
+ // Center to canvas center
170
+ else if (Math.abs(layerCenterY - canvasCenterY) < threshold) {
171
+ newY = canvasCenterY - bounds.height / 2;
172
+ snapY = canvasCenterY;
173
+ }
174
+
175
+ return { x: newX, y: newY, snapX, snapY };
176
+ }
177
+
178
+ /**
179
+ * Get layer bounds
180
+ * @param {Layer} layer - Layer
181
+ * @returns {Object} Bounds
182
+ */
183
+ getLayerBounds(layer) {
184
+ return {
185
+ x: layer.position.x,
186
+ y: layer.position.y,
187
+ width: layer.width,
188
+ height: layer.height
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Hit test transform handles
194
+ * @param {Object} point - Point to test
195
+ * @returns {string|null} Handle name or null
196
+ */
197
+ hitTestTransformHandles(point) {
198
+ const layer = this.app.layers.getActiveLayer();
199
+ if (!layer) return null;
200
+
201
+ const bounds = this.getLayerBounds(layer);
202
+ const handleSize = 8 / (this.app.canvas.zoom / 100);
203
+
204
+ const handles = {
205
+ 'nw': { x: bounds.x, y: bounds.y },
206
+ 'n': { x: bounds.x + bounds.width / 2, y: bounds.y },
207
+ 'ne': { x: bounds.x + bounds.width, y: bounds.y },
208
+ 'e': { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 },
209
+ 'se': { x: bounds.x + bounds.width, y: bounds.y + bounds.height },
210
+ 's': { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height },
211
+ 'sw': { x: bounds.x, y: bounds.y + bounds.height },
212
+ 'w': { x: bounds.x, y: bounds.y + bounds.height / 2 }
213
+ };
214
+
215
+ for (const [name, pos] of Object.entries(handles)) {
216
+ if (Math.abs(point.x - pos.x) < handleSize &&
217
+ Math.abs(point.y - pos.y) < handleSize) {
218
+ return name;
219
+ }
220
+ }
221
+
222
+ return null;
223
+ }
224
+
225
+ /**
226
+ * Apply transform based on handle
227
+ * @param {Layer} layer - Layer
228
+ * @param {string} handle - Handle name
229
+ * @param {number} dx - Delta X
230
+ * @param {number} dy - Delta Y
231
+ */
232
+ applyTransform(layer, handle, dx, dy) {
233
+ if (!this.originalBounds || !this.originalImageData) return;
234
+
235
+ const minSize = 10;
236
+
237
+ // Calculate new bounds based on delta from start point
238
+ const totalDx = this.currentPoint.x - this.startPoint.x;
239
+ const totalDy = this.currentPoint.y - this.startPoint.y;
240
+
241
+ let newX = this.originalBounds.x;
242
+ let newY = this.originalBounds.y;
243
+ let newWidth = this.originalBounds.width;
244
+ let newHeight = this.originalBounds.height;
245
+
246
+ switch (handle) {
247
+ case 'nw':
248
+ newX += totalDx;
249
+ newY += totalDy;
250
+ newWidth -= totalDx;
251
+ newHeight -= totalDy;
252
+ break;
253
+ case 'n':
254
+ newY += totalDy;
255
+ newHeight -= totalDy;
256
+ break;
257
+ case 'ne':
258
+ newY += totalDy;
259
+ newWidth += totalDx;
260
+ newHeight -= totalDy;
261
+ break;
262
+ case 'e':
263
+ newWidth += totalDx;
264
+ break;
265
+ case 'se':
266
+ newWidth += totalDx;
267
+ newHeight += totalDy;
268
+ break;
269
+ case 's':
270
+ newHeight += totalDy;
271
+ break;
272
+ case 'sw':
273
+ newX += totalDx;
274
+ newWidth -= totalDx;
275
+ newHeight += totalDy;
276
+ break;
277
+ case 'w':
278
+ newX += totalDx;
279
+ newWidth -= totalDx;
280
+ break;
281
+ }
282
+
283
+ // Enforce minimum size
284
+ if (newWidth < minSize) {
285
+ if (handle.includes('w')) {
286
+ newX = this.originalBounds.x + this.originalBounds.width - minSize;
287
+ }
288
+ newWidth = minSize;
289
+ }
290
+ if (newHeight < minSize) {
291
+ if (handle.includes('n')) {
292
+ newY = this.originalBounds.y + this.originalBounds.height - minSize;
293
+ }
294
+ newHeight = minSize;
295
+ }
296
+
297
+ newWidth = Math.round(newWidth);
298
+ newHeight = Math.round(newHeight);
299
+
300
+ // Apply position change
301
+ layer.position.x = newX;
302
+ layer.position.y = newY;
303
+
304
+ // Resize from original image to prevent quality loss
305
+ if (newWidth !== layer.width || newHeight !== layer.height) {
306
+ // Create temp canvas with original image
307
+ const tempCanvas = document.createElement('canvas');
308
+ tempCanvas.width = this.originalImageData.width;
309
+ tempCanvas.height = this.originalImageData.height;
310
+ const tempCtx = tempCanvas.getContext('2d');
311
+ tempCtx.putImageData(this.originalImageData, 0, 0);
312
+
313
+ // Resize layer canvas
314
+ layer.canvas.width = newWidth;
315
+ layer.canvas.height = newHeight;
316
+ layer.width = newWidth;
317
+ layer.height = newHeight;
318
+
319
+ // Draw scaled original to layer
320
+ layer.ctx.imageSmoothingEnabled = true;
321
+ layer.ctx.imageSmoothingQuality = 'high';
322
+ layer.ctx.drawImage(tempCanvas, 0, 0, newWidth, newHeight);
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Select layer at point
328
+ * @param {Object} point - Point
329
+ */
330
+ selectLayerAtPoint(point) {
331
+ // Find topmost non-transparent layer at point
332
+ const layers = this.app.layers.getLayers().reverse();
333
+
334
+ for (const layer of layers) {
335
+ if (!layer.visible || !layer.canvas) continue;
336
+
337
+ const x = Math.floor(point.x - layer.position.x);
338
+ const y = Math.floor(point.y - layer.position.y);
339
+
340
+ if (x >= 0 && x < layer.width && y >= 0 && y < layer.height) {
341
+ const pixel = layer.ctx.getImageData(x, y, 1, 1).data;
342
+ if (pixel[3] > 0) {
343
+ this.app.layers.setActiveLayer(layer.id);
344
+ return;
345
+ }
346
+ }
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Check if layer has any visible content
352
+ * @param {Layer} layer - Layer to check
353
+ * @returns {boolean} True if layer is empty
354
+ */
355
+ isLayerEmpty(layer) {
356
+ if (!layer || !layer.canvas || !layer.ctx) return true;
357
+
358
+ // Sample pixels to check if layer has any content
359
+ // For performance, we check a grid of points rather than every pixel
360
+ const sampleSize = 20;
361
+ const stepX = Math.max(1, Math.floor(layer.width / sampleSize));
362
+ const stepY = Math.max(1, Math.floor(layer.height / sampleSize));
363
+
364
+ for (let y = 0; y < layer.height; y += stepY) {
365
+ for (let x = 0; x < layer.width; x += stepX) {
366
+ const pixel = layer.ctx.getImageData(x, y, 1, 1).data;
367
+ if (pixel[3] > 0) {
368
+ return false; // Found non-transparent pixel
369
+ }
370
+ }
371
+ }
372
+
373
+ return true; // No visible content found
374
+ }
375
+
376
+ renderOverlay(ctx) {
377
+ const layer = this.app.layers.getActiveLayer();
378
+ if (!layer) return;
379
+
380
+ const bounds = this.getLayerBounds(layer);
381
+ const handleSize = 8;
382
+ const canvasWidth = this.app.canvas.width;
383
+ const canvasHeight = this.app.canvas.height;
384
+
385
+ // Draw snap guides
386
+ if (this.options.snapToEdges && this.isDrawing) {
387
+ ctx.save();
388
+ ctx.strokeStyle = '#00FFFF';
389
+ ctx.lineWidth = 1;
390
+ ctx.setLineDash([4, 4]);
391
+
392
+ // Vertical snap guide
393
+ if (this.activeSnaps.x !== null) {
394
+ ctx.beginPath();
395
+ ctx.moveTo(this.activeSnaps.x, 0);
396
+ ctx.lineTo(this.activeSnaps.x, canvasHeight);
397
+ ctx.stroke();
398
+ }
399
+
400
+ // Horizontal snap guide
401
+ if (this.activeSnaps.y !== null) {
402
+ ctx.beginPath();
403
+ ctx.moveTo(0, this.activeSnaps.y);
404
+ ctx.lineTo(canvasWidth, this.activeSnaps.y);
405
+ ctx.stroke();
406
+ }
407
+
408
+ ctx.restore();
409
+ }
410
+
411
+ // Draw transform controls
412
+ if (!this.options.showTransform) return;
413
+
414
+ // Don't show bounding box for empty layers
415
+ if (this.isLayerEmpty(layer)) return;
416
+
417
+ // Draw bounding box
418
+ ctx.strokeStyle = '#0066ff';
419
+ ctx.lineWidth = 1;
420
+ ctx.setLineDash([]);
421
+ ctx.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height);
422
+
423
+ // Draw handles
424
+ ctx.fillStyle = '#ffffff';
425
+ ctx.strokeStyle = '#0066ff';
426
+
427
+ const handles = [
428
+ { x: bounds.x, y: bounds.y },
429
+ { x: bounds.x + bounds.width / 2, y: bounds.y },
430
+ { x: bounds.x + bounds.width, y: bounds.y },
431
+ { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 },
432
+ { x: bounds.x + bounds.width, y: bounds.y + bounds.height },
433
+ { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height },
434
+ { x: bounds.x, y: bounds.y + bounds.height },
435
+ { x: bounds.x, y: bounds.y + bounds.height / 2 }
436
+ ];
437
+
438
+ handles.forEach(pos => {
439
+ ctx.fillRect(pos.x - handleSize / 2, pos.y - handleSize / 2, handleSize, handleSize);
440
+ ctx.strokeRect(pos.x - handleSize / 2, pos.y - handleSize / 2, handleSize, handleSize);
441
+ });
442
+ }
443
+
444
+ getOptionsUI() {
445
+ return {
446
+ autoSelect: {
447
+ type: 'checkbox',
448
+ label: 'Auto-Select Layer',
449
+ value: this.options.autoSelect
450
+ },
451
+ showTransform: {
452
+ type: 'checkbox',
453
+ label: 'Show Transform Controls',
454
+ value: this.options.showTransform
455
+ },
456
+ snapToEdges: {
457
+ type: 'checkbox',
458
+ label: 'Snap to Edges',
459
+ value: this.options.snapToEdges
460
+ }
461
+ };
462
+ }
463
+ }
464
+
465
+ export default MoveTool;