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,400 @@
1
+ /**
2
+ * SenangWebs Studio - Crop Tool
3
+ * Crop the canvas
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { BaseTool } from './BaseTool.js';
8
+
9
+ export class CropTool extends BaseTool {
10
+ constructor(app) {
11
+ super(app);
12
+ this.name = 'crop';
13
+ this.icon = 'crop';
14
+ this.cursor = 'crosshair';
15
+ this.shortcut = 'c';
16
+
17
+ this.options = {
18
+ aspectRatio: 'free', // 'free', '1:1', '4:3', '16:9', 'original'
19
+ width: null,
20
+ height: null
21
+ };
22
+ this.defaultOptions = { ...this.options };
23
+
24
+ // Crop state
25
+ this.cropBounds = null;
26
+ this.activeHandle = null;
27
+ }
28
+
29
+ onActivate() {
30
+ super.onActivate();
31
+ // Initialize crop bounds to full canvas
32
+ this.cropBounds = {
33
+ x: 0,
34
+ y: 0,
35
+ width: this.app.canvas.width,
36
+ height: this.app.canvas.height
37
+ };
38
+ this.app.canvas.scheduleRender();
39
+ }
40
+
41
+ onDeactivate() {
42
+ this.cropBounds = null;
43
+ super.onDeactivate();
44
+ }
45
+
46
+ onPointerDown(e) {
47
+ super.onPointerDown(e);
48
+
49
+ // Check if clicking on handle
50
+ this.activeHandle = this.hitTestHandles(this.startPoint);
51
+
52
+ if (!this.activeHandle) {
53
+ // Start new crop selection
54
+ this.cropBounds = {
55
+ x: this.startPoint.x,
56
+ y: this.startPoint.y,
57
+ width: 0,
58
+ height: 0
59
+ };
60
+ }
61
+ }
62
+
63
+ onPointerMove(e) {
64
+ super.onPointerMove(e);
65
+
66
+ if (!this.isDrawing) return;
67
+
68
+ if (this.activeHandle) {
69
+ this.resizeCropBounds(this.activeHandle, this.currentPoint);
70
+ } else {
71
+ // Update crop bounds
72
+ this.updateCropBounds(this.startPoint, this.currentPoint);
73
+ }
74
+
75
+ this.app.canvas.scheduleRender();
76
+ }
77
+
78
+ onPointerUp(e) {
79
+ this.activeHandle = null;
80
+ super.onPointerUp(e);
81
+ }
82
+
83
+ /**
84
+ * Update crop bounds from two points
85
+ * @param {Object} start - Start point
86
+ * @param {Object} end - End point
87
+ */
88
+ updateCropBounds(start, end) {
89
+ let x = Math.min(start.x, end.x);
90
+ let y = Math.min(start.y, end.y);
91
+ let width = Math.abs(end.x - start.x);
92
+ let height = Math.abs(end.y - start.y);
93
+
94
+ // Apply aspect ratio constraint
95
+ const ratio = this.getAspectRatio();
96
+ if (ratio) {
97
+ if (width / height > ratio) {
98
+ width = height * ratio;
99
+ } else {
100
+ height = width / ratio;
101
+ }
102
+ }
103
+
104
+ // Clamp to canvas bounds
105
+ x = Math.max(0, Math.min(x, this.app.canvas.width - width));
106
+ y = Math.max(0, Math.min(y, this.app.canvas.height - height));
107
+ width = Math.min(width, this.app.canvas.width - x);
108
+ height = Math.min(height, this.app.canvas.height - y);
109
+
110
+ this.cropBounds = { x, y, width, height };
111
+ }
112
+
113
+ /**
114
+ * Resize crop bounds using handle
115
+ * @param {string} handle - Handle name
116
+ * @param {Object} point - Current point
117
+ */
118
+ resizeCropBounds(handle, point) {
119
+ const bounds = this.cropBounds;
120
+ const ratio = this.getAspectRatio();
121
+
122
+ switch (handle) {
123
+ case 'nw':
124
+ const newWidth = bounds.x + bounds.width - point.x;
125
+ const newHeight = bounds.y + bounds.height - point.y;
126
+ bounds.x = point.x;
127
+ bounds.y = point.y;
128
+ bounds.width = newWidth;
129
+ bounds.height = newHeight;
130
+ break;
131
+ case 'ne':
132
+ bounds.width = point.x - bounds.x;
133
+ bounds.y = point.y;
134
+ bounds.height = bounds.y + bounds.height - point.y;
135
+ break;
136
+ case 'se':
137
+ bounds.width = point.x - bounds.x;
138
+ bounds.height = point.y - bounds.y;
139
+ break;
140
+ case 'sw':
141
+ bounds.x = point.x;
142
+ bounds.width = bounds.x + bounds.width - point.x;
143
+ bounds.height = point.y - bounds.y;
144
+ break;
145
+ case 'n':
146
+ bounds.height = bounds.y + bounds.height - point.y;
147
+ bounds.y = point.y;
148
+ break;
149
+ case 's':
150
+ bounds.height = point.y - bounds.y;
151
+ break;
152
+ case 'e':
153
+ bounds.width = point.x - bounds.x;
154
+ break;
155
+ case 'w':
156
+ bounds.width = bounds.x + bounds.width - point.x;
157
+ bounds.x = point.x;
158
+ break;
159
+ }
160
+
161
+ // Ensure positive dimensions
162
+ if (bounds.width < 10) bounds.width = 10;
163
+ if (bounds.height < 10) bounds.height = 10;
164
+ }
165
+
166
+ /**
167
+ * Get aspect ratio from options
168
+ * @returns {number|null}
169
+ */
170
+ getAspectRatio() {
171
+ switch (this.options.aspectRatio) {
172
+ case '1:1': return 1;
173
+ case '4:3': return 4 / 3;
174
+ case '3:4': return 3 / 4;
175
+ case '16:9': return 16 / 9;
176
+ case '9:16': return 9 / 16;
177
+ case 'original': return this.app.canvas.width / this.app.canvas.height;
178
+ default: return null;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Set aspect ratio
184
+ * @param {number|null} ratio - Aspect ratio (null for free)
185
+ */
186
+ setAspectRatio(ratio) {
187
+ if (ratio === null) {
188
+ this.options.aspectRatio = 'free';
189
+ } else if (ratio === 1) {
190
+ this.options.aspectRatio = '1:1';
191
+ } else if (Math.abs(ratio - 4/3) < 0.01) {
192
+ this.options.aspectRatio = '4:3';
193
+ } else if (Math.abs(ratio - 16/9) < 0.01) {
194
+ this.options.aspectRatio = '16:9';
195
+ } else {
196
+ // For custom ratios, store as 'custom' and use numeric value
197
+ this.options.aspectRatio = 'custom';
198
+ this.options.customRatio = ratio;
199
+ }
200
+
201
+ // Update crop bounds if already cropping
202
+ if (this.cropBounds && this.cropBounds.width > 0) {
203
+ const bounds = this.cropBounds;
204
+ if (ratio) {
205
+ // Adjust height to match ratio
206
+ const newHeight = bounds.width / ratio;
207
+ bounds.height = Math.min(newHeight, this.app.canvas.height - bounds.y);
208
+ }
209
+ this.app.canvas.scheduleRender();
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Hit test crop handles
215
+ * @param {Object} point - Point
216
+ * @returns {string|null} Handle name
217
+ */
218
+ hitTestHandles(point) {
219
+ if (!this.cropBounds) return null;
220
+
221
+ const b = this.cropBounds;
222
+ const handleSize = 10;
223
+
224
+ const handles = {
225
+ 'nw': { x: b.x, y: b.y },
226
+ 'n': { x: b.x + b.width / 2, y: b.y },
227
+ 'ne': { x: b.x + b.width, y: b.y },
228
+ 'e': { x: b.x + b.width, y: b.y + b.height / 2 },
229
+ 'se': { x: b.x + b.width, y: b.y + b.height },
230
+ 's': { x: b.x + b.width / 2, y: b.y + b.height },
231
+ 'sw': { x: b.x, y: b.y + b.height },
232
+ 'w': { x: b.x, y: b.y + b.height / 2 }
233
+ };
234
+
235
+ for (const [name, pos] of Object.entries(handles)) {
236
+ if (Math.abs(point.x - pos.x) < handleSize &&
237
+ Math.abs(point.y - pos.y) < handleSize) {
238
+ return name;
239
+ }
240
+ }
241
+
242
+ return null;
243
+ }
244
+
245
+ /**
246
+ * Apply the crop
247
+ */
248
+ applyCrop() {
249
+ if (!this.cropBounds || this.cropBounds.width < 1 || this.cropBounds.height < 1) return;
250
+
251
+ const { x, y, width, height } = this.cropBounds;
252
+
253
+ // Crop all layers
254
+ this.app.layers.getLayers().forEach(layer => {
255
+ if (!layer.canvas) return;
256
+
257
+ // Create new canvas with cropped content
258
+ const tempCanvas = document.createElement('canvas');
259
+ tempCanvas.width = width;
260
+ tempCanvas.height = height;
261
+ const tempCtx = tempCanvas.getContext('2d');
262
+
263
+ // Draw cropped portion
264
+ tempCtx.drawImage(
265
+ layer.canvas,
266
+ x - layer.position.x, y - layer.position.y, width, height,
267
+ 0, 0, width, height
268
+ );
269
+
270
+ // Update layer
271
+ layer.canvas.width = width;
272
+ layer.canvas.height = height;
273
+ layer.width = width;
274
+ layer.height = height;
275
+ layer.ctx.drawImage(tempCanvas, 0, 0);
276
+ layer.position = { x: 0, y: 0 };
277
+ });
278
+
279
+ // Update canvas size
280
+ this.app.canvas.resize(width, height);
281
+
282
+ // Reset crop bounds
283
+ this.cropBounds = {
284
+ x: 0,
285
+ y: 0,
286
+ width,
287
+ height
288
+ };
289
+
290
+ this.app.history.pushState('Crop');
291
+ this.app.canvas.scheduleRender();
292
+ }
293
+
294
+ /**
295
+ * Cancel crop
296
+ */
297
+ cancelCrop() {
298
+ this.cropBounds = {
299
+ x: 0,
300
+ y: 0,
301
+ width: this.app.canvas.width,
302
+ height: this.app.canvas.height
303
+ };
304
+ this.app.canvas.scheduleRender();
305
+ }
306
+
307
+ renderOverlay(ctx) {
308
+ if (!this.cropBounds) return;
309
+
310
+ const { x, y, width, height } = this.cropBounds;
311
+ const canvasWidth = this.app.canvas.width;
312
+ const canvasHeight = this.app.canvas.height;
313
+
314
+ // Draw darkened areas outside crop
315
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
316
+
317
+ // Top
318
+ ctx.fillRect(0, 0, canvasWidth, y);
319
+ // Bottom
320
+ ctx.fillRect(0, y + height, canvasWidth, canvasHeight - y - height);
321
+ // Left
322
+ ctx.fillRect(0, y, x, height);
323
+ // Right
324
+ ctx.fillRect(x + width, y, canvasWidth - x - width, height);
325
+
326
+ // Draw crop border
327
+ ctx.strokeStyle = '#ffffff';
328
+ ctx.lineWidth = 1;
329
+ ctx.setLineDash([]);
330
+ ctx.strokeRect(x, y, width, height);
331
+
332
+ // Draw rule of thirds grid
333
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
334
+ ctx.beginPath();
335
+ // Vertical lines
336
+ ctx.moveTo(x + width / 3, y);
337
+ ctx.lineTo(x + width / 3, y + height);
338
+ ctx.moveTo(x + width * 2 / 3, y);
339
+ ctx.lineTo(x + width * 2 / 3, y + height);
340
+ // Horizontal lines
341
+ ctx.moveTo(x, y + height / 3);
342
+ ctx.lineTo(x + width, y + height / 3);
343
+ ctx.moveTo(x, y + height * 2 / 3);
344
+ ctx.lineTo(x + width, y + height * 2 / 3);
345
+ ctx.stroke();
346
+
347
+ // Draw handles
348
+ ctx.fillStyle = '#ffffff';
349
+ ctx.strokeStyle = '#000000';
350
+ ctx.lineWidth = 1;
351
+
352
+ const handleSize = 8;
353
+ const handles = [
354
+ { x, y },
355
+ { x: x + width / 2, y },
356
+ { x: x + width, y },
357
+ { x: x + width, y: y + height / 2 },
358
+ { x: x + width, y: y + height },
359
+ { x: x + width / 2, y: y + height },
360
+ { x, y: y + height },
361
+ { x, y: y + height / 2 }
362
+ ];
363
+
364
+ handles.forEach(pos => {
365
+ ctx.fillRect(pos.x - handleSize / 2, pos.y - handleSize / 2, handleSize, handleSize);
366
+ ctx.strokeRect(pos.x - handleSize / 2, pos.y - handleSize / 2, handleSize, handleSize);
367
+ });
368
+ }
369
+
370
+ getOptionsUI() {
371
+ return {
372
+ aspectRatio: {
373
+ type: 'select',
374
+ label: 'Aspect Ratio',
375
+ options: [
376
+ { value: 'free', label: 'Free' },
377
+ { value: '1:1', label: '1:1 (Square)' },
378
+ { value: '4:3', label: '4:3' },
379
+ { value: '3:4', label: '3:4' },
380
+ { value: '16:9', label: '16:9' },
381
+ { value: '9:16', label: '9:16' },
382
+ { value: 'original', label: 'Original Ratio' }
383
+ ],
384
+ value: this.options.aspectRatio
385
+ },
386
+ apply: {
387
+ type: 'button',
388
+ label: 'Apply Crop',
389
+ action: () => this.applyCrop()
390
+ },
391
+ cancel: {
392
+ type: 'button',
393
+ label: 'Cancel',
394
+ action: () => this.cancelCrop()
395
+ }
396
+ };
397
+ }
398
+ }
399
+
400
+ export default CropTool;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * SenangWebs Studio - Eraser Tool
3
+ * Erase pixels from layer
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { BaseTool } from './BaseTool.js';
8
+
9
+ export class EraserTool extends BaseTool {
10
+ constructor(app) {
11
+ super(app);
12
+ this.name = 'eraser';
13
+ this.icon = 'eraser';
14
+ this.cursor = 'crosshair';
15
+ this.shortcut = 'e';
16
+
17
+ this.options = {
18
+ size: 20,
19
+ hardness: 100,
20
+ opacity: 100,
21
+ mode: 'brush' // 'brush', 'block'
22
+ };
23
+ this.defaultOptions = { ...this.options };
24
+
25
+ this.points = [];
26
+ }
27
+
28
+ onPointerDown(e) {
29
+ super.onPointerDown(e);
30
+
31
+ const layer = this.app.layers.getActiveLayer();
32
+ if (!layer || layer.locked) return;
33
+
34
+ this.points = [this.startPoint];
35
+ this.eraseAt(layer.ctx, this.startPoint);
36
+ this.app.canvas.scheduleRender();
37
+ }
38
+
39
+ onPointerMove(e) {
40
+ super.onPointerMove(e);
41
+
42
+ if (!this.isDrawing) return;
43
+
44
+ const layer = this.app.layers.getActiveLayer();
45
+ if (!layer || layer.locked) return;
46
+
47
+ this.points.push(this.currentPoint);
48
+
49
+ // Interpolate between points
50
+ const spacing = Math.max(1, this.options.size / 4);
51
+ const interpolated = this.interpolatePoints(this.lastPoint, this.currentPoint, spacing);
52
+
53
+ interpolated.forEach(point => {
54
+ this.eraseAt(layer.ctx, point);
55
+ });
56
+
57
+ this.app.canvas.scheduleRender();
58
+ }
59
+
60
+ onPointerUp(e) {
61
+ if (this.isDrawing && this.points.length > 0) {
62
+ this.app.history.pushState('Erase');
63
+ }
64
+
65
+ this.points = [];
66
+ super.onPointerUp(e);
67
+ }
68
+
69
+ /**
70
+ * Erase at position
71
+ * @param {CanvasRenderingContext2D} ctx - Target context
72
+ * @param {Object} point - Erase position
73
+ */
74
+ eraseAt(ctx, point) {
75
+ const size = this.options.size;
76
+ const halfSize = size / 2;
77
+ const opacity = this.options.opacity / 100;
78
+
79
+ ctx.save();
80
+ ctx.globalCompositeOperation = 'destination-out';
81
+ ctx.globalAlpha = opacity;
82
+
83
+ if (this.options.mode === 'block') {
84
+ // Block eraser
85
+ ctx.fillStyle = 'rgba(0, 0, 0, 1)';
86
+ ctx.fillRect(point.x - halfSize, point.y - halfSize, size, size);
87
+ } else {
88
+ // Brush eraser
89
+ if (this.options.hardness < 100) {
90
+ // Soft eraser
91
+ const gradient = ctx.createRadialGradient(
92
+ point.x, point.y, 0,
93
+ point.x, point.y, halfSize
94
+ );
95
+ const hardness = this.options.hardness / 100;
96
+ gradient.addColorStop(0, 'rgba(0, 0, 0, 1)');
97
+ gradient.addColorStop(hardness, 'rgba(0, 0, 0, 1)');
98
+ gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
99
+
100
+ ctx.fillStyle = gradient;
101
+ ctx.beginPath();
102
+ ctx.arc(point.x, point.y, halfSize, 0, Math.PI * 2);
103
+ ctx.fill();
104
+ } else {
105
+ // Hard eraser
106
+ ctx.fillStyle = 'rgba(0, 0, 0, 1)';
107
+ ctx.beginPath();
108
+ ctx.arc(point.x, point.y, halfSize, 0, Math.PI * 2);
109
+ ctx.fill();
110
+ }
111
+ }
112
+
113
+ ctx.restore();
114
+ }
115
+
116
+ getOptionsUI() {
117
+ return {
118
+ size: {
119
+ type: 'slider',
120
+ label: 'Size',
121
+ min: 1,
122
+ max: 500,
123
+ value: this.options.size,
124
+ unit: 'px'
125
+ },
126
+ hardness: {
127
+ type: 'slider',
128
+ label: 'Hardness',
129
+ min: 0,
130
+ max: 100,
131
+ value: this.options.hardness,
132
+ unit: '%'
133
+ },
134
+ opacity: {
135
+ type: 'slider',
136
+ label: 'Opacity',
137
+ min: 1,
138
+ max: 100,
139
+ value: this.options.opacity,
140
+ unit: '%'
141
+ },
142
+ mode: {
143
+ type: 'select',
144
+ label: 'Mode',
145
+ options: [
146
+ { value: 'brush', label: 'Brush' },
147
+ { value: 'block', label: 'Block' }
148
+ ],
149
+ value: this.options.mode
150
+ }
151
+ };
152
+ }
153
+ }
154
+
155
+ export default EraserTool;