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,285 @@
1
+ /**
2
+ * SenangWebs Studio - Shape Tool
3
+ * Draw geometric shapes
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { BaseTool } from './BaseTool.js';
8
+
9
+ export class ShapeTool extends BaseTool {
10
+ constructor(app) {
11
+ super(app);
12
+ this.name = 'shape';
13
+ this.icon = 'square';
14
+ this.cursor = 'crosshair';
15
+ this.shortcut = 'u';
16
+
17
+ this.options = {
18
+ shape: 'rectangle', // rectangle, ellipse, line, polygon
19
+ fillColor: '#000000',
20
+ strokeColor: 'transparent',
21
+ strokeWidth: 2,
22
+ filled: true,
23
+ stroked: false,
24
+ createNewLayer: true,
25
+ cornerRadius: 0
26
+ };
27
+ this.defaultOptions = { ...this.options };
28
+
29
+ // Preview state
30
+ this.previewShape = null;
31
+ }
32
+
33
+ onPointerDown(e) {
34
+ super.onPointerDown(e);
35
+ this.previewShape = null;
36
+ }
37
+
38
+ onPointerMove(e) {
39
+ super.onPointerMove(e);
40
+
41
+ if (!this.isDrawing) return;
42
+
43
+ // Update preview
44
+ this.previewShape = this.calculateShape(this.startPoint, this.currentPoint, e.shiftKey);
45
+ this.app.canvas.scheduleRender();
46
+ }
47
+
48
+ onPointerUp(e) {
49
+ if (this.isDrawing && this.startPoint) {
50
+ const shape = this.calculateShape(this.startPoint, this.currentPoint, e.shiftKey);
51
+
52
+ if (this.options.createNewLayer) {
53
+ // Create shape layer
54
+ const layer = this.app.layers.addLayer({
55
+ name: `${this.options.shape} layer`,
56
+ type: 'shape'
57
+ });
58
+ layer.shapeType = this.options.shape;
59
+ layer.shapeData = shape;
60
+ layer.fillColor = this.options.filled ? this.options.fillColor : 'transparent';
61
+ layer.strokeColor = this.options.stroked ? this.options.strokeColor : 'transparent';
62
+ layer.strokeWidth = this.options.strokeWidth;
63
+
64
+ this.app.layers.setActiveLayer(layer.id);
65
+ } else {
66
+ // Draw directly on current layer
67
+ const layer = this.app.layers.getActiveLayer();
68
+ if (layer && !layer.locked) {
69
+ this.drawShape(layer.ctx, shape);
70
+ }
71
+ }
72
+
73
+ this.app.history.pushState(`Draw ${this.options.shape}`);
74
+ this.app.canvas.scheduleRender();
75
+ }
76
+
77
+ this.previewShape = null;
78
+ super.onPointerUp(e);
79
+ }
80
+
81
+ /**
82
+ * Calculate shape bounds
83
+ * @param {Object} start - Start point
84
+ * @param {Object} end - End point
85
+ * @param {boolean} constrain - Constrain proportions
86
+ * @returns {Object} Shape data
87
+ */
88
+ calculateShape(start, end, constrain = false) {
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
+ // Constrain to square/circle
95
+ if (constrain) {
96
+ const size = Math.max(width, height);
97
+ width = size;
98
+ height = size;
99
+
100
+ if (end.x < start.x) x = start.x - size;
101
+ if (end.y < start.y) y = start.y - size;
102
+ }
103
+
104
+ if (this.options.shape === 'line') {
105
+ return {
106
+ points: [
107
+ { x: start.x, y: start.y },
108
+ { x: constrain ? this.constrainLine(start, end).x : end.x, y: constrain ? this.constrainLine(start, end).y : end.y }
109
+ ]
110
+ };
111
+ }
112
+
113
+ return { x, y, width, height };
114
+ }
115
+
116
+ /**
117
+ * Constrain line to 45-degree angles
118
+ * @param {Object} start - Start point
119
+ * @param {Object} end - End point
120
+ * @returns {Object} Constrained end point
121
+ */
122
+ constrainLine(start, end) {
123
+ const dx = end.x - start.x;
124
+ const dy = end.y - start.y;
125
+ const angle = Math.atan2(dy, dx);
126
+ const length = Math.sqrt(dx * dx + dy * dy);
127
+
128
+ // Snap to 45-degree increments
129
+ const snappedAngle = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4);
130
+
131
+ return {
132
+ x: start.x + Math.cos(snappedAngle) * length,
133
+ y: start.y + Math.sin(snappedAngle) * length
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Draw shape on context
139
+ * @param {CanvasRenderingContext2D} ctx - Target context
140
+ * @param {Object} shape - Shape data
141
+ */
142
+ drawShape(ctx, shape) {
143
+ ctx.save();
144
+
145
+ ctx.fillStyle = this.options.filled ? this.options.fillColor : 'transparent';
146
+ ctx.strokeStyle = this.options.stroked ? this.options.strokeColor : 'transparent';
147
+ ctx.lineWidth = this.options.strokeWidth;
148
+
149
+ switch (this.options.shape) {
150
+ case 'rectangle':
151
+ this.drawRectangle(ctx, shape);
152
+ break;
153
+ case 'ellipse':
154
+ this.drawEllipse(ctx, shape);
155
+ break;
156
+ case 'line':
157
+ this.drawLine(ctx, shape);
158
+ break;
159
+ }
160
+
161
+ ctx.restore();
162
+ }
163
+
164
+ drawRectangle(ctx, shape) {
165
+ const { x, y, width, height } = shape;
166
+ const radius = this.options.cornerRadius;
167
+
168
+ ctx.beginPath();
169
+
170
+ if (radius > 0) {
171
+ // Rounded rectangle
172
+ const r = Math.min(radius, width / 2, height / 2);
173
+ ctx.moveTo(x + r, y);
174
+ ctx.lineTo(x + width - r, y);
175
+ ctx.quadraticCurveTo(x + width, y, x + width, y + r);
176
+ ctx.lineTo(x + width, y + height - r);
177
+ ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
178
+ ctx.lineTo(x + r, y + height);
179
+ ctx.quadraticCurveTo(x, y + height, x, y + height - r);
180
+ ctx.lineTo(x, y + r);
181
+ ctx.quadraticCurveTo(x, y, x + r, y);
182
+ } else {
183
+ ctx.rect(x, y, width, height);
184
+ }
185
+
186
+ ctx.closePath();
187
+
188
+ if (this.options.filled) ctx.fill();
189
+ if (this.options.stroked) ctx.stroke();
190
+ }
191
+
192
+ drawEllipse(ctx, shape) {
193
+ const { x, y, width, height } = shape;
194
+ const centerX = x + width / 2;
195
+ const centerY = y + height / 2;
196
+
197
+ ctx.beginPath();
198
+ ctx.ellipse(centerX, centerY, width / 2, height / 2, 0, 0, Math.PI * 2);
199
+
200
+ if (this.options.filled) ctx.fill();
201
+ if (this.options.stroked) ctx.stroke();
202
+ }
203
+
204
+ drawLine(ctx, shape) {
205
+ const { points } = shape;
206
+
207
+ ctx.beginPath();
208
+ ctx.moveTo(points[0].x, points[0].y);
209
+ ctx.lineTo(points[1].x, points[1].y);
210
+ ctx.stroke();
211
+ }
212
+
213
+ renderOverlay(ctx) {
214
+ if (!this.previewShape) return;
215
+
216
+ ctx.save();
217
+ ctx.fillStyle = this.options.filled ? this.options.fillColor : 'transparent';
218
+ ctx.strokeStyle = this.options.stroked ? this.options.strokeColor : '#0066ff';
219
+ ctx.lineWidth = this.options.strokeWidth || 1;
220
+ ctx.setLineDash([5, 5]);
221
+ ctx.globalAlpha = 0.7;
222
+
223
+ this.drawShape(ctx, this.previewShape);
224
+
225
+ ctx.restore();
226
+ }
227
+
228
+ getOptionsUI() {
229
+ return {
230
+ shape: {
231
+ type: 'select',
232
+ label: 'Shape',
233
+ options: [
234
+ { value: 'rectangle', label: 'Rectangle' },
235
+ { value: 'ellipse', label: 'Ellipse' },
236
+ { value: 'line', label: 'Line' }
237
+ ],
238
+ value: this.options.shape
239
+ },
240
+ filled: {
241
+ type: 'checkbox',
242
+ label: 'Fill',
243
+ value: this.options.filled
244
+ },
245
+ fillColor: {
246
+ type: 'color',
247
+ label: 'Fill Color',
248
+ value: this.options.fillColor
249
+ },
250
+ stroked: {
251
+ type: 'checkbox',
252
+ label: 'Stroke',
253
+ value: this.options.stroked
254
+ },
255
+ strokeColor: {
256
+ type: 'color',
257
+ label: 'Stroke Color',
258
+ value: this.options.strokeColor
259
+ },
260
+ strokeWidth: {
261
+ type: 'slider',
262
+ label: 'Stroke Width',
263
+ min: 1,
264
+ max: 50,
265
+ value: this.options.strokeWidth,
266
+ unit: 'px'
267
+ },
268
+ cornerRadius: {
269
+ type: 'slider',
270
+ label: 'Corner Radius',
271
+ min: 0,
272
+ max: 100,
273
+ value: this.options.cornerRadius,
274
+ unit: 'px'
275
+ },
276
+ createNewLayer: {
277
+ type: 'checkbox',
278
+ label: 'Create Shape Layer',
279
+ value: this.options.createNewLayer
280
+ }
281
+ };
282
+ }
283
+ }
284
+
285
+ export default ShapeTool;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * SenangWebs Photobooth - Text Tool
3
+ * Add and edit text layers
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { BaseTool } from './BaseTool.js';
8
+
9
+ export class TextTool extends BaseTool {
10
+ constructor(app) {
11
+ super(app);
12
+ this.name = 'text';
13
+ this.icon = 'font';
14
+ this.cursor = 'text';
15
+ this.shortcut = 't';
16
+
17
+ this.options = {
18
+ fontFamily: 'Arial',
19
+ fontSize: 48,
20
+ fontWeight: 'normal',
21
+ fontStyle: 'normal',
22
+ textAlign: 'left',
23
+ color: '#000000',
24
+ lineHeight: 1.2
25
+ };
26
+ this.defaultOptions = { ...this.options };
27
+
28
+ // Text editing state
29
+ this.editingLayer = null;
30
+ this.handleKeyDown = this.handleKeyDown.bind(this);
31
+ }
32
+
33
+ onActivate() {
34
+ super.onActivate();
35
+ document.addEventListener('keydown', this.handleKeyDown);
36
+ }
37
+
38
+ onDeactivate() {
39
+ this.commitText();
40
+ document.removeEventListener('keydown', this.handleKeyDown);
41
+ super.onDeactivate();
42
+ }
43
+
44
+ handleKeyDown(e) {
45
+ if (!this.editingLayer) return;
46
+
47
+ // Don't process if modifier keys are pressed (let shortcuts work)
48
+ if (e.ctrlKey || e.altKey || e.metaKey) return;
49
+
50
+ e.preventDefault();
51
+ e.stopPropagation();
52
+
53
+ const key = e.key;
54
+
55
+ if (key === 'Backspace') {
56
+ this.editingLayer.textContent = this.editingLayer.textContent.slice(0, -1);
57
+ } else if (key === 'Enter') {
58
+ this.editingLayer.textContent += '\n';
59
+ } else if (key === 'Escape') {
60
+ this.commitText();
61
+ return;
62
+ } else if (key.length === 1) {
63
+ // Single character - add to text
64
+ this.editingLayer.textContent += key;
65
+ }
66
+
67
+ this.app.canvas.scheduleRender();
68
+ }
69
+
70
+ onPointerDown(e) {
71
+ super.onPointerDown(e);
72
+
73
+ // Check if clicking on existing text layer
74
+ const existingLayer = this.findTextLayerAt(this.startPoint);
75
+
76
+ if (existingLayer) {
77
+ // Edit existing text
78
+ this.startEditingLayer(existingLayer);
79
+ } else {
80
+ // Create new text layer
81
+ this.commitText(); // Commit any existing edit first
82
+ this.createTextLayer(this.startPoint);
83
+ }
84
+ }
85
+
86
+ findTextLayerAt(point) {
87
+ const layers = this.app.layers.getLayers().reverse();
88
+
89
+ for (const layer of layers) {
90
+ if (layer.type !== 'text' || !layer.visible) continue;
91
+
92
+ // Simple bounds check
93
+ const bounds = this.getTextBounds(layer);
94
+ if (point.x >= bounds.x && point.x <= bounds.x + bounds.width &&
95
+ point.y >= bounds.y && point.y <= bounds.y + bounds.height) {
96
+ return layer;
97
+ }
98
+ }
99
+
100
+ return null;
101
+ }
102
+
103
+ getTextBounds(layer) {
104
+ const ctx = this.app.canvas.workCtx;
105
+ if (!ctx) {
106
+ return { x: layer.position.x, y: layer.position.y, width: 100, height: layer.textStyle.fontSize };
107
+ }
108
+
109
+ ctx.font = `${layer.textStyle.fontStyle} ${layer.textStyle.fontWeight} ${layer.textStyle.fontSize}px ${layer.textStyle.fontFamily}`;
110
+
111
+ const lines = (layer.textContent || '').split('\n');
112
+ let maxWidth = 100; // Minimum width
113
+
114
+ lines.forEach(line => {
115
+ const metrics = ctx.measureText(line || ' ');
116
+ maxWidth = Math.max(maxWidth, metrics.width);
117
+ });
118
+
119
+ const height = Math.max(1, lines.length) * layer.textStyle.fontSize * layer.textStyle.lineHeight;
120
+
121
+ return {
122
+ x: layer.position.x,
123
+ y: layer.position.y,
124
+ width: maxWidth,
125
+ height
126
+ };
127
+ }
128
+
129
+ createTextLayer(position) {
130
+ const layer = this.app.layers.addLayer({
131
+ name: 'Text Layer',
132
+ type: 'text'
133
+ });
134
+
135
+ layer.position = { x: position.x, y: position.y };
136
+ layer.textContent = '';
137
+ layer.textStyle = {
138
+ ...this.options,
139
+ color: this.app.colors?.foreground || this.options.color
140
+ };
141
+
142
+ this.startEditingLayer(layer);
143
+ this.app.layers.setActiveLayer(layer.id);
144
+ }
145
+
146
+ startEditingLayer(layer) {
147
+ this.editingLayer = layer;
148
+ this.app.canvas.scheduleRender();
149
+ }
150
+
151
+ commitText() {
152
+ if (!this.editingLayer) return;
153
+
154
+ // Remove empty text layer
155
+ if (!this.editingLayer.textContent || !this.editingLayer.textContent.trim()) {
156
+ this.app.layers.removeLayer(this.editingLayer.id);
157
+ } else {
158
+ this.app.history.pushState('Add Text');
159
+ }
160
+
161
+ this.editingLayer = null;
162
+ this.app.canvas.scheduleRender();
163
+ }
164
+
165
+ renderOverlay(ctx) {
166
+ if (!this.editingLayer) return;
167
+
168
+ const bounds = this.getTextBounds(this.editingLayer);
169
+
170
+ // Draw text cursor/bounding box
171
+ ctx.strokeStyle = '#3b82f6';
172
+ ctx.lineWidth = 2;
173
+ ctx.setLineDash([5, 5]);
174
+ ctx.strokeRect(bounds.x - 4, bounds.y - 4, bounds.width + 8, bounds.height + 8);
175
+
176
+ // Draw blinking cursor
177
+ const now = Date.now();
178
+ if (Math.floor(now / 500) % 2 === 0) {
179
+ ctx.setLineDash([]);
180
+ ctx.strokeStyle = this.editingLayer.textStyle.color;
181
+ ctx.lineWidth = 2;
182
+ const cursorX = bounds.x + bounds.width + 2;
183
+ const cursorY = bounds.y;
184
+ const cursorHeight = this.editingLayer.textStyle.fontSize;
185
+ ctx.beginPath();
186
+ ctx.moveTo(cursorX, cursorY);
187
+ ctx.lineTo(cursorX, cursorY + cursorHeight);
188
+ ctx.stroke();
189
+ }
190
+ }
191
+
192
+ getOptionsUI() {
193
+ return {
194
+ fontFamily: {
195
+ type: 'select',
196
+ label: 'Font',
197
+ options: [
198
+ { value: 'Arial', label: 'Arial' },
199
+ { value: 'Helvetica', label: 'Helvetica' },
200
+ { value: 'Times New Roman', label: 'Times New Roman' },
201
+ { value: 'Georgia', label: 'Georgia' },
202
+ { value: 'Verdana', label: 'Verdana' },
203
+ { value: 'Courier New', label: 'Courier New' },
204
+ { value: 'Impact', label: 'Impact' }
205
+ ],
206
+ value: this.options.fontFamily
207
+ },
208
+ fontSize: {
209
+ type: 'slider',
210
+ label: 'Size',
211
+ min: 8,
212
+ max: 200,
213
+ value: this.options.fontSize,
214
+ unit: 'px'
215
+ },
216
+ fontWeight: {
217
+ type: 'select',
218
+ label: 'Weight',
219
+ options: [
220
+ { value: 'normal', label: 'Regular' },
221
+ { value: 'bold', label: 'Bold' }
222
+ ],
223
+ value: this.options.fontWeight
224
+ },
225
+ fontStyle: {
226
+ type: 'select',
227
+ label: 'Style',
228
+ options: [
229
+ { value: 'normal', label: 'Normal' },
230
+ { value: 'italic', label: 'Italic' }
231
+ ],
232
+ value: this.options.fontStyle
233
+ },
234
+ textAlign: {
235
+ type: 'select',
236
+ label: 'Align',
237
+ options: [
238
+ { value: 'left', label: 'Left' },
239
+ { value: 'center', label: 'Center' },
240
+ { value: 'right', label: 'Right' }
241
+ ],
242
+ value: this.options.textAlign
243
+ },
244
+ color: {
245
+ type: 'color',
246
+ label: 'Color',
247
+ value: this.options.color
248
+ }
249
+ };
250
+ }
251
+ }
252
+
253
+ export default TextTool;