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,184 @@
1
+ /**
2
+ * SenangWebs Studio - Eyedropper Tool
3
+ * Pick color from canvas
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { BaseTool } from './BaseTool.js';
8
+ import { Events } from '../core/EventEmitter.js';
9
+
10
+ export class EyedropperTool extends BaseTool {
11
+ constructor(app) {
12
+ super(app);
13
+ this.name = 'eyedropper';
14
+ this.icon = 'eyedropper';
15
+ this.cursor = 'crosshair';
16
+ this.shortcut = 'i';
17
+
18
+ this.options = {
19
+ sampleSize: 'point', // 'point', '3x3', '5x5'
20
+ sampleLayers: 'current' // 'current', 'all'
21
+ };
22
+ this.defaultOptions = { ...this.options };
23
+
24
+ this.previewColor = null;
25
+ }
26
+
27
+ onPointerDown(e) {
28
+ super.onPointerDown(e);
29
+ this.pickColor(this.startPoint, !e.altKey);
30
+ }
31
+
32
+ onPointerMove(e) {
33
+ super.onPointerMove(e);
34
+
35
+ // Preview color on hover
36
+ if (!this.isDrawing) {
37
+ this.previewColor = this.getColorAt(this.currentPoint);
38
+ } else {
39
+ this.pickColor(this.currentPoint, !e.altKey);
40
+ }
41
+
42
+ this.app.canvas.scheduleRender();
43
+ }
44
+
45
+ /**
46
+ * Pick color at point
47
+ * @param {Object} point - Point
48
+ * @param {boolean} foreground - Set as foreground (true) or background (false)
49
+ */
50
+ pickColor(point, foreground = true) {
51
+ const color = this.getColorAt(point);
52
+
53
+ if (color) {
54
+ if (foreground) {
55
+ this.app.colors.setForeground(color);
56
+ } else {
57
+ this.app.colors.setBackground(color);
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Get color at point
64
+ * @param {Object} point - Point
65
+ * @returns {string|null} Hex color
66
+ */
67
+ getColorAt(point) {
68
+ const x = Math.floor(point.x);
69
+ const y = Math.floor(point.y);
70
+
71
+ if (x < 0 || x >= this.app.canvas.width || y < 0 || y >= this.app.canvas.height) {
72
+ return null;
73
+ }
74
+
75
+ let ctx;
76
+
77
+ if (this.options.sampleLayers === 'current') {
78
+ const layer = this.app.layers.getActiveLayer();
79
+ if (!layer?.ctx) return null;
80
+ ctx = layer.ctx;
81
+ } else {
82
+ // Sample from composite
83
+ ctx = this.app.canvas.workCtx;
84
+ }
85
+
86
+ const sampleSize = this.getSampleSize();
87
+ const halfSize = Math.floor(sampleSize / 2);
88
+
89
+ // Get average color in sample area
90
+ let r = 0, g = 0, b = 0, count = 0;
91
+
92
+ for (let sy = -halfSize; sy <= halfSize; sy++) {
93
+ for (let sx = -halfSize; sx <= halfSize; sx++) {
94
+ const px = x + sx;
95
+ const py = y + sy;
96
+
97
+ if (px >= 0 && px < this.app.canvas.width && py >= 0 && py < this.app.canvas.height) {
98
+ const pixel = ctx.getImageData(px, py, 1, 1).data;
99
+ r += pixel[0];
100
+ g += pixel[1];
101
+ b += pixel[2];
102
+ count++;
103
+ }
104
+ }
105
+ }
106
+
107
+ if (count === 0) return null;
108
+
109
+ r = Math.round(r / count);
110
+ g = Math.round(g / count);
111
+ b = Math.round(b / count);
112
+
113
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
114
+ }
115
+
116
+ /**
117
+ * Get sample size from options
118
+ * @returns {number}
119
+ */
120
+ getSampleSize() {
121
+ switch (this.options.sampleSize) {
122
+ case '3x3': return 3;
123
+ case '5x5': return 5;
124
+ default: return 1;
125
+ }
126
+ }
127
+
128
+ renderOverlay(ctx) {
129
+ if (!this.previewColor || !this.currentPoint) return;
130
+
131
+ const x = this.currentPoint.x;
132
+ const y = this.currentPoint.y;
133
+
134
+ // Draw color preview
135
+ ctx.save();
136
+
137
+ // Magnifier circle
138
+ const radius = 30;
139
+ ctx.beginPath();
140
+ ctx.arc(x, y - 50, radius, 0, Math.PI * 2);
141
+ ctx.fillStyle = this.previewColor;
142
+ ctx.fill();
143
+ ctx.strokeStyle = '#ffffff';
144
+ ctx.lineWidth = 2;
145
+ ctx.stroke();
146
+ ctx.strokeStyle = '#000000';
147
+ ctx.lineWidth = 1;
148
+ ctx.stroke();
149
+
150
+ // Color value
151
+ ctx.fillStyle = '#ffffff';
152
+ ctx.font = '10px monospace';
153
+ ctx.textAlign = 'center';
154
+ ctx.fillText(this.previewColor.toUpperCase(), x, y - 50 + 4);
155
+
156
+ ctx.restore();
157
+ }
158
+
159
+ getOptionsUI() {
160
+ return {
161
+ sampleSize: {
162
+ type: 'select',
163
+ label: 'Sample Size',
164
+ options: [
165
+ { value: 'point', label: 'Point' },
166
+ { value: '3x3', label: '3×3 Average' },
167
+ { value: '5x5', label: '5×5 Average' }
168
+ ],
169
+ value: this.options.sampleSize
170
+ },
171
+ sampleLayers: {
172
+ type: 'select',
173
+ label: 'Sample',
174
+ options: [
175
+ { value: 'current', label: 'Current Layer' },
176
+ { value: 'all', label: 'All Layers' }
177
+ ],
178
+ value: this.options.sampleLayers
179
+ }
180
+ };
181
+ }
182
+ }
183
+
184
+ export default EyedropperTool;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * SenangWebs Studio - Fill Tool (Paint Bucket)
3
+ * @version 2.0.0
4
+ */
5
+
6
+ import { BaseTool } from './BaseTool.js';
7
+
8
+ export class FillTool extends BaseTool {
9
+ constructor(app) {
10
+ super(app);
11
+ this.name = 'fill';
12
+ this.icon = 'fill';
13
+ this.cursor = 'crosshair';
14
+ this.shortcut = 'g';
15
+
16
+ this.options = {
17
+ tolerance: 32,
18
+ contiguous: true,
19
+ opacity: 100
20
+ };
21
+ this.defaultOptions = { ...this.options };
22
+ }
23
+
24
+ onPointerDown(e) {
25
+ super.onPointerDown(e);
26
+
27
+ const layer = this.app.layers.getActiveLayer();
28
+ if (!layer || layer.locked || !layer.ctx) return;
29
+
30
+ const x = Math.floor(this.startPoint.x);
31
+ const y = Math.floor(this.startPoint.y);
32
+
33
+ if (x < 0 || x >= layer.width || y < 0 || y >= layer.height) return;
34
+
35
+ const fillColor = this.app.colors?.foreground || '#000000';
36
+ this.floodFill(layer, x, y, fillColor);
37
+
38
+ this.app.history.pushState('Fill');
39
+ this.app.canvas.scheduleRender();
40
+ }
41
+
42
+ floodFill(layer, startX, startY, fillColor) {
43
+ const ctx = layer.ctx;
44
+ const imageData = ctx.getImageData(0, 0, layer.width, layer.height);
45
+ const data = imageData.data;
46
+ const width = layer.width;
47
+ const height = layer.height;
48
+
49
+ const startIdx = (startY * width + startX) * 4;
50
+ const targetR = data[startIdx];
51
+ const targetG = data[startIdx + 1];
52
+ const targetB = data[startIdx + 2];
53
+ const targetA = data[startIdx + 3];
54
+
55
+ const fillRGB = this.hexToRgb(fillColor);
56
+ const fillA = Math.round((this.options.opacity / 100) * 255);
57
+ const tolerance = this.options.tolerance;
58
+ const visited = new Uint8Array(width * height);
59
+ const stack = [[startX, startY]];
60
+
61
+ while (stack.length > 0) {
62
+ const [x, y] = stack.pop();
63
+ const idx = y * width + x;
64
+
65
+ if (x < 0 || x >= width || y < 0 || y >= height) continue;
66
+ if (visited[idx]) continue;
67
+
68
+ const pixelIdx = idx * 4;
69
+ if (!this.colorsMatch(targetR, targetG, targetB, targetA,
70
+ data[pixelIdx], data[pixelIdx + 1], data[pixelIdx + 2], data[pixelIdx + 3], tolerance)) {
71
+ continue;
72
+ }
73
+
74
+ visited[idx] = 1;
75
+ data[pixelIdx] = fillRGB.r;
76
+ data[pixelIdx + 1] = fillRGB.g;
77
+ data[pixelIdx + 2] = fillRGB.b;
78
+ data[pixelIdx + 3] = fillA;
79
+
80
+ stack.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]);
81
+ }
82
+
83
+ ctx.putImageData(imageData, 0, 0);
84
+ }
85
+
86
+ colorsMatch(r1, g1, b1, a1, r2, g2, b2, a2, tolerance) {
87
+ return Math.abs(r1 - r2) <= tolerance && Math.abs(g1 - g2) <= tolerance &&
88
+ Math.abs(b1 - b2) <= tolerance && Math.abs(a1 - a2) <= tolerance;
89
+ }
90
+
91
+ hexToRgb(hex) {
92
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
93
+ return result ? {
94
+ r: parseInt(result[1], 16),
95
+ g: parseInt(result[2], 16),
96
+ b: parseInt(result[3], 16)
97
+ } : { r: 0, g: 0, b: 0 };
98
+ }
99
+
100
+ getOptionsUI() {
101
+ return {
102
+ tolerance: { type: 'slider', label: 'Tolerance', min: 0, max: 255, value: this.options.tolerance },
103
+ contiguous: { type: 'checkbox', label: 'Contiguous', value: this.options.contiguous },
104
+ opacity: { type: 'slider', label: 'Opacity', min: 1, max: 100, value: this.options.opacity, unit: '%' }
105
+ };
106
+ }
107
+ }
108
+
109
+ export default FillTool;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * SenangWebs Studio - Gradient Tool
3
+ * Draw gradients
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { BaseTool } from './BaseTool.js';
8
+
9
+ export class GradientTool extends BaseTool {
10
+ constructor(app) {
11
+ super(app);
12
+ this.name = 'gradient';
13
+ this.icon = 'gradient';
14
+ this.cursor = 'crosshair';
15
+ this.shortcut = 'g';
16
+
17
+ this.options = {
18
+ type: 'linear', // 'linear', 'radial', 'angle', 'diamond'
19
+ opacity: 100,
20
+ reverse: false,
21
+ dither: false
22
+ };
23
+ this.defaultOptions = { ...this.options };
24
+ }
25
+
26
+ onPointerUp(e) {
27
+ if (this.isDrawing && this.startPoint && this.currentPoint) {
28
+ const layer = this.app.layers.getActiveLayer();
29
+ if (layer && !layer.locked) {
30
+ this.drawGradient(layer.ctx, this.startPoint, this.currentPoint);
31
+ this.app.history.pushState('Gradient');
32
+ this.app.canvas.scheduleRender();
33
+ }
34
+ }
35
+
36
+ super.onPointerUp(e);
37
+ }
38
+
39
+ /**
40
+ * Draw gradient on context
41
+ * @param {CanvasRenderingContext2D} ctx - Target context
42
+ * @param {Object} start - Start point
43
+ * @param {Object} end - End point
44
+ */
45
+ drawGradient(ctx, start, end) {
46
+ const foreground = this.app.colors?.foreground || '#000000';
47
+ const background = this.app.colors?.background || '#ffffff';
48
+
49
+ const color1 = this.options.reverse ? background : foreground;
50
+ const color2 = this.options.reverse ? foreground : background;
51
+
52
+ let gradient;
53
+
54
+ switch (this.options.type) {
55
+ case 'radial':
56
+ const radius = this.distance(start, end);
57
+ gradient = ctx.createRadialGradient(start.x, start.y, 0, start.x, start.y, radius);
58
+ break;
59
+
60
+ case 'angle':
61
+ // Simulate angular gradient with multiple color stops
62
+ gradient = ctx.createConicGradient(this.angle(start, end), start.x, start.y);
63
+ break;
64
+
65
+ default: // linear
66
+ gradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
67
+ }
68
+
69
+ gradient.addColorStop(0, color1);
70
+ gradient.addColorStop(1, color2);
71
+
72
+ ctx.save();
73
+ ctx.globalAlpha = this.options.opacity / 100;
74
+ ctx.fillStyle = gradient;
75
+ ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
76
+ ctx.restore();
77
+ }
78
+
79
+ renderOverlay(ctx) {
80
+ if (!this.isDrawing || !this.startPoint || !this.currentPoint) return;
81
+
82
+ // Draw gradient preview line
83
+ ctx.save();
84
+ ctx.strokeStyle = '#000000';
85
+ ctx.lineWidth = 2;
86
+ ctx.setLineDash([5, 5]);
87
+
88
+ ctx.beginPath();
89
+ ctx.moveTo(this.startPoint.x, this.startPoint.y);
90
+ ctx.lineTo(this.currentPoint.x, this.currentPoint.y);
91
+ ctx.stroke();
92
+
93
+ // Start point
94
+ ctx.fillStyle = this.app.colors?.foreground || '#000000';
95
+ ctx.beginPath();
96
+ ctx.arc(this.startPoint.x, this.startPoint.y, 5, 0, Math.PI * 2);
97
+ ctx.fill();
98
+
99
+ // End point
100
+ ctx.fillStyle = this.app.colors?.background || '#ffffff';
101
+ ctx.beginPath();
102
+ ctx.arc(this.currentPoint.x, this.currentPoint.y, 5, 0, Math.PI * 2);
103
+ ctx.fill();
104
+ ctx.strokeStyle = '#000000';
105
+ ctx.lineWidth = 1;
106
+ ctx.setLineDash([]);
107
+ ctx.stroke();
108
+
109
+ ctx.restore();
110
+ }
111
+
112
+ getOptionsUI() {
113
+ return {
114
+ type: {
115
+ type: 'select',
116
+ label: 'Type',
117
+ options: [
118
+ { value: 'linear', label: 'Linear' },
119
+ { value: 'radial', label: 'Radial' },
120
+ { value: 'angle', label: 'Angle' }
121
+ ],
122
+ value: this.options.type
123
+ },
124
+ opacity: {
125
+ type: 'slider',
126
+ label: 'Opacity',
127
+ min: 1,
128
+ max: 100,
129
+ value: this.options.opacity,
130
+ unit: '%'
131
+ },
132
+ reverse: {
133
+ type: 'checkbox',
134
+ label: 'Reverse',
135
+ value: this.options.reverse
136
+ }
137
+ };
138
+ }
139
+ }
140
+
141
+ export default GradientTool;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * SenangWebs Studio - Hand Tool
3
+ * Pan the canvas viewport
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { BaseTool } from './BaseTool.js';
8
+
9
+ export class HandTool extends BaseTool {
10
+ constructor(app) {
11
+ super(app);
12
+ this.name = 'hand';
13
+ this.icon = 'hand';
14
+ this.cursor = 'grab';
15
+ this.shortcut = 'h';
16
+
17
+ this.lastViewPoint = null;
18
+ }
19
+
20
+ onPointerDown(e) {
21
+ this.isDrawing = true;
22
+ this.lastViewPoint = this.getViewportPoint(e);
23
+ this.cursor = 'grabbing';
24
+ this.updateCursor();
25
+ }
26
+
27
+ onPointerMove(e) {
28
+ if (!this.isDrawing) return;
29
+
30
+ const currentViewPoint = this.getViewportPoint(e);
31
+ const dx = currentViewPoint.x - this.lastViewPoint.x;
32
+ const dy = currentViewPoint.y - this.lastViewPoint.y;
33
+
34
+ this.app.canvas.pan(dx, dy);
35
+
36
+ this.lastViewPoint = currentViewPoint;
37
+ }
38
+
39
+ onPointerUp(e) {
40
+ this.isDrawing = false;
41
+ this.cursor = 'grab';
42
+ this.updateCursor();
43
+ this.lastViewPoint = null;
44
+ }
45
+
46
+ getOptionsUI() {
47
+ return {};
48
+ }
49
+ }
50
+
51
+ export default HandTool;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * SenangWebs Studio - Marquee Selection Tool
3
+ * @version 2.0.0
4
+ */
5
+
6
+ import { BaseTool } from './BaseTool.js';
7
+
8
+ export class MarqueeTool extends BaseTool {
9
+ constructor(app) {
10
+ super(app);
11
+ this.name = 'marquee';
12
+ this.icon = 'square-dashed';
13
+ this.cursor = 'crosshair';
14
+ this.shortcut = 'm';
15
+
16
+ this.options = {
17
+ shape: 'rectangle',
18
+ feather: 0,
19
+ fixed: false,
20
+ fixedWidth: 100,
21
+ fixedHeight: 100
22
+ };
23
+ this.defaultOptions = { ...this.options };
24
+ this.previewBounds = null;
25
+ }
26
+
27
+ onPointerDown(e) {
28
+ super.onPointerDown(e);
29
+ if (!e.shiftKey && !e.altKey) {
30
+ this.app.selection?.clear();
31
+ }
32
+ this.previewBounds = null;
33
+ }
34
+
35
+ onPointerMove(e) {
36
+ super.onPointerMove(e);
37
+ if (!this.isDrawing) return;
38
+
39
+ this.previewBounds = this.calculateBounds(this.startPoint, this.currentPoint, e.shiftKey);
40
+ this.app.canvas.scheduleRender();
41
+ }
42
+
43
+ onPointerUp(e) {
44
+ if (this.isDrawing && this.previewBounds) {
45
+ const bounds = this.previewBounds;
46
+ if (bounds.width > 1 && bounds.height > 1) {
47
+ this.app.selection?.setRect(bounds.x, bounds.y, bounds.width, bounds.height, this.options.shape);
48
+ }
49
+ }
50
+ this.previewBounds = null;
51
+ super.onPointerUp(e);
52
+ this.app.canvas.scheduleRender();
53
+ }
54
+
55
+ calculateBounds(start, end, constrain) {
56
+ let x = Math.min(start.x, end.x);
57
+ let y = Math.min(start.y, end.y);
58
+ let width = Math.abs(end.x - start.x);
59
+ let height = Math.abs(end.y - start.y);
60
+
61
+ if (constrain) {
62
+ const size = Math.max(width, height);
63
+ width = height = size;
64
+ if (end.x < start.x) x = start.x - size;
65
+ if (end.y < start.y) y = start.y - size;
66
+ }
67
+
68
+ return { x, y, width, height };
69
+ }
70
+
71
+ renderOverlay(ctx) {
72
+ const bounds = this.previewBounds;
73
+ if (!bounds) return;
74
+
75
+ ctx.save();
76
+ ctx.strokeStyle = '#000000';
77
+ ctx.lineWidth = 1;
78
+ ctx.setLineDash([5, 5]);
79
+
80
+ if (this.options.shape === 'ellipse') {
81
+ ctx.beginPath();
82
+ ctx.ellipse(bounds.x + bounds.width/2, bounds.y + bounds.height/2,
83
+ bounds.width/2, bounds.height/2, 0, 0, Math.PI * 2);
84
+ ctx.stroke();
85
+ } else {
86
+ ctx.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height);
87
+ }
88
+ ctx.restore();
89
+ }
90
+
91
+ getOptionsUI() {
92
+ return {
93
+ shape: {
94
+ type: 'select', label: 'Shape',
95
+ options: [{ value: 'rectangle', label: 'Rectangle' }, { value: 'ellipse', label: 'Ellipse' }],
96
+ value: this.options.shape
97
+ },
98
+ feather: { type: 'slider', label: 'Feather', min: 0, max: 100, value: this.options.feather, unit: 'px' }
99
+ };
100
+ }
101
+ }
102
+
103
+ export default MarqueeTool;