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,264 @@
1
+ /**
2
+ * SenangWebs Studio - Base Tool
3
+ * Abstract base class for all tools
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { Events } from '../core/EventEmitter.js';
8
+
9
+ export class BaseTool {
10
+ constructor(app) {
11
+ this.app = app;
12
+ this.name = 'base';
13
+ this.icon = 'cursor';
14
+ this.cursor = 'default';
15
+ this.shortcut = null;
16
+
17
+ // Tool state
18
+ this.isActive = false;
19
+ this.isDrawing = false;
20
+ this.startPoint = null;
21
+ this.lastPoint = null;
22
+ this.currentPoint = null;
23
+
24
+ // Options (to be overridden by subclasses)
25
+ this.options = {};
26
+ this.defaultOptions = {};
27
+ }
28
+
29
+ /**
30
+ * Activate tool
31
+ */
32
+ activate() {
33
+ this.isActive = true;
34
+ this.updateCursor();
35
+ this.onActivate();
36
+ }
37
+
38
+ /**
39
+ * Deactivate tool
40
+ */
41
+ deactivate() {
42
+ this.isActive = false;
43
+ this.isDrawing = false;
44
+ this.onDeactivate();
45
+ }
46
+
47
+ /**
48
+ * Called when tool is activated (override in subclass)
49
+ */
50
+ onActivate() {}
51
+
52
+ /**
53
+ * Called when tool is deactivated (override in subclass)
54
+ */
55
+ onDeactivate() {}
56
+
57
+ /**
58
+ * Update cursor
59
+ */
60
+ updateCursor() {
61
+ if (this.app.canvas?.displayCanvas) {
62
+ this.app.canvas.displayCanvas.style.cursor = this.cursor;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Handle pointer down
68
+ * @param {PointerEvent} e - Pointer event
69
+ */
70
+ onPointerDown(e) {
71
+ this.isDrawing = true;
72
+ this.startPoint = this.getCanvasPoint(e);
73
+ this.lastPoint = this.startPoint;
74
+ this.currentPoint = this.startPoint;
75
+
76
+ this.app.events.emit(Events.TOOL_START, {
77
+ tool: this.name,
78
+ point: this.startPoint
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Handle pointer move
84
+ * @param {PointerEvent} e - Pointer event
85
+ */
86
+ onPointerMove(e) {
87
+ this.lastPoint = this.currentPoint;
88
+ this.currentPoint = this.getCanvasPoint(e);
89
+
90
+ if (this.isDrawing) {
91
+ this.app.events.emit(Events.TOOL_MOVE, {
92
+ tool: this.name,
93
+ point: this.currentPoint,
94
+ delta: {
95
+ x: this.currentPoint.x - this.lastPoint.x,
96
+ y: this.currentPoint.y - this.lastPoint.y
97
+ }
98
+ });
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Handle pointer up
104
+ * @param {PointerEvent} e - Pointer event
105
+ */
106
+ onPointerUp(e) {
107
+ if (this.isDrawing) {
108
+ this.currentPoint = this.getCanvasPoint(e);
109
+
110
+ this.app.events.emit(Events.TOOL_END, {
111
+ tool: this.name,
112
+ startPoint: this.startPoint,
113
+ endPoint: this.currentPoint
114
+ });
115
+ }
116
+
117
+ this.isDrawing = false;
118
+ this.startPoint = null;
119
+ this.lastPoint = null;
120
+ }
121
+
122
+ /**
123
+ * Handle pointer leave
124
+ * @param {PointerEvent} e - Pointer event
125
+ */
126
+ onPointerLeave(e) {
127
+ // Optional: handle when pointer leaves canvas
128
+ }
129
+
130
+ /**
131
+ * Get point in canvas coordinates
132
+ * @param {PointerEvent} e - Pointer event
133
+ * @returns {Object} Point {x, y}
134
+ */
135
+ getCanvasPoint(e) {
136
+ const rect = this.app.canvas.displayCanvas.getBoundingClientRect();
137
+ const viewX = e.clientX - rect.left;
138
+ const viewY = e.clientY - rect.top;
139
+ return this.app.canvas.viewportToCanvas(viewX, viewY);
140
+ }
141
+
142
+ /**
143
+ * Get viewport point
144
+ * @param {PointerEvent} e - Pointer event
145
+ * @returns {Object} Point {x, y}
146
+ */
147
+ getViewportPoint(e) {
148
+ const rect = this.app.canvas.displayCanvas.getBoundingClientRect();
149
+ return {
150
+ x: e.clientX - rect.left,
151
+ y: e.clientY - rect.top
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Get pressure from pointer event (for tablet support)
157
+ * @param {PointerEvent} e - Pointer event
158
+ * @returns {number} Pressure (0-1)
159
+ */
160
+ getPressure(e) {
161
+ return e.pressure !== undefined ? e.pressure : 0.5;
162
+ }
163
+
164
+ /**
165
+ * Render tool overlay (selection boxes, guides, etc.)
166
+ * @param {CanvasRenderingContext2D} ctx - Overlay context
167
+ */
168
+ renderOverlay(ctx) {
169
+ // Override in subclass
170
+ }
171
+
172
+ /**
173
+ * Set tool option
174
+ * @param {string} key - Option key
175
+ * @param {*} value - Option value
176
+ */
177
+ setOption(key, value) {
178
+ if (key in this.options) {
179
+ this.options[key] = value;
180
+ this.onOptionChange(key, value);
181
+ this.app.events.emit(Events.TOOL_OPTIONS_CHANGE, {
182
+ tool: this.name,
183
+ option: key,
184
+ value
185
+ });
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Get tool option
191
+ * @param {string} key - Option key
192
+ * @returns {*} Option value
193
+ */
194
+ getOption(key) {
195
+ return this.options[key];
196
+ }
197
+
198
+ /**
199
+ * Called when option changes (override in subclass)
200
+ * @param {string} key - Option key
201
+ * @param {*} value - New value
202
+ */
203
+ onOptionChange(key, value) {}
204
+
205
+ /**
206
+ * Reset options to defaults
207
+ */
208
+ resetOptions() {
209
+ this.options = { ...this.defaultOptions };
210
+ }
211
+
212
+ /**
213
+ * Get options for UI
214
+ * @returns {Object} Options definition
215
+ */
216
+ getOptionsUI() {
217
+ return {};
218
+ }
219
+
220
+ /**
221
+ * Calculate distance between two points
222
+ * @param {Object} p1 - Point 1
223
+ * @param {Object} p2 - Point 2
224
+ * @returns {number} Distance
225
+ */
226
+ distance(p1, p2) {
227
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
228
+ }
229
+
230
+ /**
231
+ * Calculate angle between two points
232
+ * @param {Object} p1 - Point 1
233
+ * @param {Object} p2 - Point 2
234
+ * @returns {number} Angle in radians
235
+ */
236
+ angle(p1, p2) {
237
+ return Math.atan2(p2.y - p1.y, p2.x - p1.x);
238
+ }
239
+
240
+ /**
241
+ * Interpolate points between two positions
242
+ * @param {Object} p1 - Start point
243
+ * @param {Object} p2 - End point
244
+ * @param {number} spacing - Spacing between points
245
+ * @returns {Object[]} Array of interpolated points
246
+ */
247
+ interpolatePoints(p1, p2, spacing) {
248
+ const points = [];
249
+ const dist = this.distance(p1, p2);
250
+ const steps = Math.ceil(dist / spacing);
251
+
252
+ for (let i = 0; i <= steps; i++) {
253
+ const t = i / steps;
254
+ points.push({
255
+ x: p1.x + (p2.x - p1.x) * t,
256
+ y: p1.y + (p2.y - p1.y) * t
257
+ });
258
+ }
259
+
260
+ return points;
261
+ }
262
+ }
263
+
264
+ export default BaseTool;
@@ -0,0 +1,314 @@
1
+ /**
2
+ * SenangWebs Studio - Brush Tool
3
+ * Freehand drawing with customizable brush
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { BaseTool } from './BaseTool.js';
8
+
9
+ export class BrushTool extends BaseTool {
10
+ constructor(app) {
11
+ super(app);
12
+ this.name = 'brush';
13
+ this.icon = 'brush';
14
+ this.cursor = 'crosshair';
15
+ this.shortcut = 'b';
16
+
17
+ this.options = {
18
+ size: 20,
19
+ hardness: 100,
20
+ opacity: 100,
21
+ flow: 100,
22
+ spacing: 25,
23
+ smoothing: 50,
24
+ pressureSize: true,
25
+ pressureOpacity: false
26
+ };
27
+ this.defaultOptions = { ...this.options };
28
+
29
+ // Drawing state
30
+ this.points = [];
31
+ this.brushCanvas = null;
32
+ this.brushCtx = null;
33
+ }
34
+
35
+ onActivate() {
36
+ this.createBrushTip();
37
+ this.updateCursor();
38
+ }
39
+
40
+ /**
41
+ * Create brush tip canvas
42
+ */
43
+ createBrushTip() {
44
+ const size = Math.ceil(this.options.size);
45
+ this.brushCanvas = document.createElement('canvas');
46
+ this.brushCanvas.width = size;
47
+ this.brushCanvas.height = size;
48
+ this.brushCtx = this.brushCanvas.getContext('2d');
49
+ this.updateBrushTip();
50
+ }
51
+
52
+ /**
53
+ * Update brush tip based on options
54
+ */
55
+ updateBrushTip() {
56
+ const size = this.options.size;
57
+ const hardness = this.options.hardness / 100;
58
+
59
+ this.brushCanvas.width = Math.ceil(size);
60
+ this.brushCanvas.height = Math.ceil(size);
61
+
62
+ const ctx = this.brushCtx;
63
+ const center = size / 2;
64
+
65
+ // Create gradient for soft brush
66
+ const gradient = ctx.createRadialGradient(center, center, 0, center, center, center);
67
+
68
+ if (hardness >= 1) {
69
+ // Hard brush
70
+ gradient.addColorStop(0, 'rgba(0, 0, 0, 1)');
71
+ gradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
72
+ } else {
73
+ // Soft brush with hardness control
74
+ const hardnessStop = hardness * 0.9;
75
+ gradient.addColorStop(0, 'rgba(0, 0, 0, 1)');
76
+ gradient.addColorStop(hardnessStop, 'rgba(0, 0, 0, 1)');
77
+ gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
78
+ }
79
+
80
+ ctx.clearRect(0, 0, size, size);
81
+ ctx.fillStyle = gradient;
82
+ ctx.beginPath();
83
+ ctx.arc(center, center, center, 0, Math.PI * 2);
84
+ ctx.fill();
85
+ }
86
+
87
+ onPointerDown(e) {
88
+ super.onPointerDown(e);
89
+
90
+ const layer = this.app.layers.getActiveLayer();
91
+ if (!layer || layer.locked) return;
92
+
93
+ this.points = [{ ...this.startPoint, pressure: this.getPressure(e) }];
94
+
95
+ // Draw initial point
96
+ this.drawStroke(layer.ctx, [this.startPoint], this.getPressure(e));
97
+ this.app.canvas.scheduleRender();
98
+ }
99
+
100
+ onPointerMove(e) {
101
+ super.onPointerMove(e);
102
+
103
+ if (!this.isDrawing) return;
104
+
105
+ const layer = this.app.layers.getActiveLayer();
106
+ if (!layer || layer.locked) return;
107
+
108
+ const point = { ...this.currentPoint, pressure: this.getPressure(e) };
109
+ this.points.push(point);
110
+
111
+ // Draw line from last point to current
112
+ this.drawStroke(layer.ctx, [this.lastPoint, this.currentPoint], this.getPressure(e));
113
+ this.app.canvas.scheduleRender();
114
+ }
115
+
116
+ onPointerUp(e) {
117
+ if (this.isDrawing && this.points.length > 0) {
118
+ this.app.history.pushState('Brush Stroke');
119
+ }
120
+
121
+ this.points = [];
122
+ super.onPointerUp(e);
123
+ }
124
+
125
+ /**
126
+ * Draw stroke on context
127
+ * @param {CanvasRenderingContext2D} ctx - Target context
128
+ * @param {Array} points - Points to draw
129
+ * @param {number} pressure - Current pressure
130
+ */
131
+ drawStroke(ctx, points, pressure = 0.5) {
132
+ if (points.length < 1) return;
133
+
134
+ const color = this.app.colors?.foreground || '#000000';
135
+ const size = this.options.pressureSize
136
+ ? this.options.size * pressure
137
+ : this.options.size;
138
+ const opacity = this.options.pressureOpacity
139
+ ? (this.options.opacity / 100) * pressure
140
+ : this.options.opacity / 100;
141
+ const flow = this.options.flow / 100;
142
+ const spacing = Math.max(1, (this.options.size * this.options.spacing) / 100);
143
+
144
+ ctx.save();
145
+ ctx.globalAlpha = opacity * flow;
146
+ ctx.globalCompositeOperation = 'source-over';
147
+
148
+ if (points.length === 1) {
149
+ // Single point
150
+ this.drawBrushDab(ctx, points[0].x, points[0].y, size, color);
151
+ } else {
152
+ // Interpolate between points
153
+ for (let i = 1; i < points.length; i++) {
154
+ const p1 = points[i - 1];
155
+ const p2 = points[i];
156
+ const interpolated = this.interpolatePoints(p1, p2, spacing);
157
+
158
+ interpolated.forEach(p => {
159
+ this.drawBrushDab(ctx, p.x, p.y, size, color);
160
+ });
161
+ }
162
+ }
163
+
164
+ ctx.restore();
165
+ }
166
+
167
+ /**
168
+ * Draw single brush dab
169
+ * @param {CanvasRenderingContext2D} ctx - Target context
170
+ * @param {number} x - X position
171
+ * @param {number} y - Y position
172
+ * @param {number} size - Brush size
173
+ * @param {string} color - Brush color
174
+ */
175
+ drawBrushDab(ctx, x, y, size, color) {
176
+ const halfSize = size / 2;
177
+
178
+ // Use brush tip canvas for soft brushes
179
+ if (this.options.hardness < 100) {
180
+ // Scale brush tip to current size
181
+ ctx.save();
182
+ ctx.globalCompositeOperation = 'source-over';
183
+
184
+ // Create colored version of brush tip
185
+ const colorCanvas = document.createElement('canvas');
186
+ colorCanvas.width = this.brushCanvas.width;
187
+ colorCanvas.height = this.brushCanvas.height;
188
+ const colorCtx = colorCanvas.getContext('2d');
189
+
190
+ colorCtx.fillStyle = color;
191
+ colorCtx.fillRect(0, 0, colorCanvas.width, colorCanvas.height);
192
+ colorCtx.globalCompositeOperation = 'destination-in';
193
+ colorCtx.drawImage(this.brushCanvas, 0, 0);
194
+
195
+ ctx.drawImage(colorCanvas, x - halfSize, y - halfSize, size, size);
196
+ ctx.restore();
197
+ } else {
198
+ // Hard brush - simple circle
199
+ ctx.fillStyle = color;
200
+ ctx.beginPath();
201
+ ctx.arc(x, y, halfSize, 0, Math.PI * 2);
202
+ ctx.fill();
203
+ }
204
+ }
205
+
206
+ onOptionChange(key, value) {
207
+ if (key === 'size' || key === 'hardness') {
208
+ this.updateBrushTip();
209
+ }
210
+ }
211
+
212
+ updateCursor() {
213
+ // Custom cursor showing brush size
214
+ const size = Math.max(4, this.options.size * (this.app.canvas?.zoom || 100) / 100);
215
+
216
+ if (size > 4) {
217
+ const canvas = document.createElement('canvas');
218
+ canvas.width = size + 2;
219
+ canvas.height = size + 2;
220
+ const ctx = canvas.getContext('2d');
221
+
222
+ ctx.strokeStyle = '#000000';
223
+ ctx.lineWidth = 1;
224
+ ctx.beginPath();
225
+ ctx.arc(size / 2 + 1, size / 2 + 1, size / 2, 0, Math.PI * 2);
226
+ ctx.stroke();
227
+
228
+ ctx.strokeStyle = '#ffffff';
229
+ ctx.setLineDash([2, 2]);
230
+ ctx.beginPath();
231
+ ctx.arc(size / 2 + 1, size / 2 + 1, size / 2, 0, Math.PI * 2);
232
+ ctx.stroke();
233
+
234
+ const dataURL = canvas.toDataURL();
235
+ this.cursor = `url(${dataURL}) ${size / 2 + 1} ${size / 2 + 1}, crosshair`;
236
+ } else {
237
+ this.cursor = 'crosshair';
238
+ }
239
+
240
+ if (this.isActive && this.app.canvas?.displayCanvas) {
241
+ this.app.canvas.displayCanvas.style.cursor = this.cursor;
242
+ }
243
+ }
244
+
245
+ renderOverlay(ctx) {
246
+ // Optional: Show brush preview on hover
247
+ if (this.currentPoint && !this.isDrawing) {
248
+ const size = this.options.size;
249
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
250
+ ctx.lineWidth = 1;
251
+ ctx.setLineDash([2, 2]);
252
+ ctx.beginPath();
253
+ ctx.arc(this.currentPoint.x, this.currentPoint.y, size / 2, 0, Math.PI * 2);
254
+ ctx.stroke();
255
+ }
256
+ }
257
+
258
+ getOptionsUI() {
259
+ return {
260
+ size: {
261
+ type: 'slider',
262
+ label: 'Size',
263
+ min: 1,
264
+ max: 500,
265
+ value: this.options.size,
266
+ unit: 'px'
267
+ },
268
+ hardness: {
269
+ type: 'slider',
270
+ label: 'Hardness',
271
+ min: 0,
272
+ max: 100,
273
+ value: this.options.hardness,
274
+ unit: '%'
275
+ },
276
+ opacity: {
277
+ type: 'slider',
278
+ label: 'Opacity',
279
+ min: 1,
280
+ max: 100,
281
+ value: this.options.opacity,
282
+ unit: '%'
283
+ },
284
+ flow: {
285
+ type: 'slider',
286
+ label: 'Flow',
287
+ min: 1,
288
+ max: 100,
289
+ value: this.options.flow,
290
+ unit: '%'
291
+ },
292
+ smoothing: {
293
+ type: 'slider',
294
+ label: 'Smoothing',
295
+ min: 0,
296
+ max: 100,
297
+ value: this.options.smoothing,
298
+ unit: '%'
299
+ },
300
+ pressureSize: {
301
+ type: 'checkbox',
302
+ label: 'Pressure affects Size',
303
+ value: this.options.pressureSize
304
+ },
305
+ pressureOpacity: {
306
+ type: 'checkbox',
307
+ label: 'Pressure affects Opacity',
308
+ value: this.options.pressureOpacity
309
+ }
310
+ };
311
+ }
312
+ }
313
+
314
+ export default BrushTool;