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.
- package/README.md +219 -235
- package/dist/swp.css +884 -344
- package/dist/swp.js +1 -1
- package/examples/data-attribute.html +69 -0
- package/examples/index.html +56 -51
- package/examples/studio.html +83 -0
- package/package.json +12 -5
- package/src/css/swp.css +884 -344
- package/src/js/core/Canvas.js +398 -0
- package/src/js/core/EventEmitter.js +188 -0
- package/src/js/core/History.js +250 -0
- package/src/js/core/Keyboard.js +323 -0
- package/src/js/filters/FilterManager.js +248 -0
- package/src/js/index.js +48 -0
- package/src/js/io/Clipboard.js +52 -0
- package/src/js/io/FileManager.js +150 -0
- package/src/js/layers/BlendModes.js +342 -0
- package/src/js/layers/Layer.js +415 -0
- package/src/js/layers/LayerManager.js +459 -0
- package/src/js/selection/Selection.js +167 -0
- package/src/js/swp.js +297 -709
- package/src/js/tools/BaseTool.js +264 -0
- package/src/js/tools/BrushTool.js +314 -0
- package/src/js/tools/CropTool.js +400 -0
- package/src/js/tools/EraserTool.js +155 -0
- package/src/js/tools/EyedropperTool.js +184 -0
- package/src/js/tools/FillTool.js +109 -0
- package/src/js/tools/GradientTool.js +141 -0
- package/src/js/tools/HandTool.js +51 -0
- package/src/js/tools/MarqueeTool.js +103 -0
- package/src/js/tools/MoveTool.js +465 -0
- package/src/js/tools/ShapeTool.js +285 -0
- package/src/js/tools/TextTool.js +253 -0
- package/src/js/tools/ToolManager.js +277 -0
- package/src/js/tools/ZoomTool.js +68 -0
- package/src/js/ui/ColorManager.js +71 -0
- package/src/js/ui/UI.js +1211 -0
- package/swp_preview1.png +0 -0
- package/swp_preview2.png +0 -0
- package/webpack.config.js +4 -11
- package/dist/styles.js +0 -1
- package/examples/customization.html +0 -360
- package/spec.md +0 -239
- package/swp_preview.png +0 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Studio - Tool Manager
|
|
3
|
+
* Manages tool selection and events
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Events } from '../core/EventEmitter.js';
|
|
8
|
+
import { MoveTool } from './MoveTool.js';
|
|
9
|
+
import { BrushTool } from './BrushTool.js';
|
|
10
|
+
import { EraserTool } from './EraserTool.js';
|
|
11
|
+
import { ShapeTool } from './ShapeTool.js';
|
|
12
|
+
import { TextTool } from './TextTool.js';
|
|
13
|
+
import { CropTool } from './CropTool.js';
|
|
14
|
+
import { ZoomTool } from './ZoomTool.js';
|
|
15
|
+
import { HandTool } from './HandTool.js';
|
|
16
|
+
import { EyedropperTool } from './EyedropperTool.js';
|
|
17
|
+
import { GradientTool } from './GradientTool.js';
|
|
18
|
+
import { FillTool } from './FillTool.js';
|
|
19
|
+
import { MarqueeTool } from './MarqueeTool.js';
|
|
20
|
+
|
|
21
|
+
export class ToolManager {
|
|
22
|
+
constructor(app) {
|
|
23
|
+
this.app = app;
|
|
24
|
+
this.tools = new Map();
|
|
25
|
+
this.currentTool = null;
|
|
26
|
+
this.previousTool = null;
|
|
27
|
+
this.temporaryTool = null;
|
|
28
|
+
|
|
29
|
+
this.init();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initialize tools
|
|
34
|
+
*/
|
|
35
|
+
init() {
|
|
36
|
+
// Register all tools
|
|
37
|
+
this.registerTool('move', new MoveTool(this.app));
|
|
38
|
+
this.registerTool('marquee', new MarqueeTool(this.app));
|
|
39
|
+
this.registerTool('brush', new BrushTool(this.app));
|
|
40
|
+
this.registerTool('eraser', new EraserTool(this.app));
|
|
41
|
+
this.registerTool('shape', new ShapeTool(this.app));
|
|
42
|
+
this.registerTool('text', new TextTool(this.app));
|
|
43
|
+
this.registerTool('crop', new CropTool(this.app));
|
|
44
|
+
this.registerTool('zoom', new ZoomTool(this.app));
|
|
45
|
+
this.registerTool('hand', new HandTool(this.app));
|
|
46
|
+
this.registerTool('eyedropper', new EyedropperTool(this.app));
|
|
47
|
+
this.registerTool('gradient', new GradientTool(this.app));
|
|
48
|
+
this.registerTool('fill', new FillTool(this.app));
|
|
49
|
+
|
|
50
|
+
// Set default tool
|
|
51
|
+
this.setTool('move');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Register a tool
|
|
56
|
+
* @param {string} name - Tool name
|
|
57
|
+
* @param {BaseTool} tool - Tool instance
|
|
58
|
+
*/
|
|
59
|
+
registerTool(name, tool) {
|
|
60
|
+
this.tools.set(name, tool);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get tool by name
|
|
65
|
+
* @param {string} name - Tool name
|
|
66
|
+
* @returns {BaseTool|null}
|
|
67
|
+
*/
|
|
68
|
+
getTool(name) {
|
|
69
|
+
return this.tools.get(name) || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set active tool
|
|
74
|
+
* @param {string} name - Tool name
|
|
75
|
+
*/
|
|
76
|
+
setTool(name) {
|
|
77
|
+
const tool = this.getTool(name);
|
|
78
|
+
if (!tool) {
|
|
79
|
+
console.warn(`Tool "${name}" not found`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Deactivate current tool
|
|
84
|
+
if (this.currentTool) {
|
|
85
|
+
this.currentTool.deactivate();
|
|
86
|
+
this.previousTool = this.currentTool;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Activate new tool
|
|
90
|
+
this.currentTool = tool;
|
|
91
|
+
this.currentTool.activate();
|
|
92
|
+
|
|
93
|
+
this.app.events.emit(Events.TOOL_SELECT, { tool: name });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Activate temporary tool (e.g., hand tool when space is pressed)
|
|
98
|
+
* @param {string} name - Tool name
|
|
99
|
+
*/
|
|
100
|
+
activateTemporaryTool(name) {
|
|
101
|
+
if (this.temporaryTool) return;
|
|
102
|
+
|
|
103
|
+
const tool = this.getTool(name);
|
|
104
|
+
if (!tool) return;
|
|
105
|
+
|
|
106
|
+
this.temporaryTool = this.currentTool;
|
|
107
|
+
this.currentTool.deactivate();
|
|
108
|
+
this.currentTool = tool;
|
|
109
|
+
this.currentTool.activate();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Deactivate temporary tool
|
|
114
|
+
*/
|
|
115
|
+
deactivateTemporaryTool() {
|
|
116
|
+
if (!this.temporaryTool) return;
|
|
117
|
+
|
|
118
|
+
this.currentTool.deactivate();
|
|
119
|
+
this.currentTool = this.temporaryTool;
|
|
120
|
+
this.currentTool.activate();
|
|
121
|
+
this.temporaryTool = null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get all tools
|
|
126
|
+
* @returns {Map} Tools map
|
|
127
|
+
*/
|
|
128
|
+
getAllTools() {
|
|
129
|
+
return this.tools;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get current tool name
|
|
134
|
+
* @returns {string|null}
|
|
135
|
+
*/
|
|
136
|
+
getCurrentToolName() {
|
|
137
|
+
for (const [name, tool] of this.tools) {
|
|
138
|
+
if (tool === this.currentTool) return name;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Switch to previous tool
|
|
145
|
+
*/
|
|
146
|
+
switchToPreviousTool() {
|
|
147
|
+
if (this.previousTool) {
|
|
148
|
+
for (const [name, tool] of this.tools) {
|
|
149
|
+
if (tool === this.previousTool) {
|
|
150
|
+
this.setTool(name);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Increase brush size
|
|
159
|
+
*/
|
|
160
|
+
increaseBrushSize() {
|
|
161
|
+
if (this.currentTool?.options?.size !== undefined) {
|
|
162
|
+
const size = this.currentTool.options.size;
|
|
163
|
+
const newSize = Math.min(500, size + (size < 10 ? 1 : size < 100 ? 5 : 20));
|
|
164
|
+
this.currentTool.setOption('size', newSize);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Decrease brush size
|
|
170
|
+
*/
|
|
171
|
+
decreaseBrushSize() {
|
|
172
|
+
if (this.currentTool?.options?.size !== undefined) {
|
|
173
|
+
const size = this.currentTool.options.size;
|
|
174
|
+
const newSize = Math.max(1, size - (size <= 10 ? 1 : size <= 100 ? 5 : 20));
|
|
175
|
+
this.currentTool.setOption('size', newSize);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Increase brush hardness
|
|
181
|
+
*/
|
|
182
|
+
increaseBrushHardness() {
|
|
183
|
+
if (this.currentTool?.options?.hardness !== undefined) {
|
|
184
|
+
const hardness = this.currentTool.options.hardness;
|
|
185
|
+
this.currentTool.setOption('hardness', Math.min(100, hardness + 10));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Decrease brush hardness
|
|
191
|
+
*/
|
|
192
|
+
decreaseBrushHardness() {
|
|
193
|
+
if (this.currentTool?.options?.hardness !== undefined) {
|
|
194
|
+
const hardness = this.currentTool.options.hardness;
|
|
195
|
+
this.currentTool.setOption('hardness', Math.max(0, hardness - 10));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Start free transform on active layer
|
|
201
|
+
*/
|
|
202
|
+
startTransform() {
|
|
203
|
+
// TODO: Implement transform mode
|
|
204
|
+
console.log('Transform mode');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Bind canvas events
|
|
209
|
+
* @param {HTMLCanvasElement} canvas - Display canvas
|
|
210
|
+
*/
|
|
211
|
+
bindCanvasEvents(canvas) {
|
|
212
|
+
canvas.addEventListener('pointerdown', this.handlePointerDown.bind(this));
|
|
213
|
+
canvas.addEventListener('pointermove', this.handlePointerMove.bind(this));
|
|
214
|
+
canvas.addEventListener('pointerup', this.handlePointerUp.bind(this));
|
|
215
|
+
canvas.addEventListener('pointerleave', this.handlePointerLeave.bind(this));
|
|
216
|
+
|
|
217
|
+
// Prevent context menu on right-click
|
|
218
|
+
canvas.addEventListener('contextmenu', e => e.preventDefault());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Handle pointer down
|
|
223
|
+
* @param {PointerEvent} e - Pointer event
|
|
224
|
+
*/
|
|
225
|
+
handlePointerDown(e) {
|
|
226
|
+
if (!this.currentTool) return;
|
|
227
|
+
|
|
228
|
+
// Set pointer capture
|
|
229
|
+
e.target.setPointerCapture(e.pointerId);
|
|
230
|
+
|
|
231
|
+
this.currentTool.onPointerDown(e);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Handle pointer move
|
|
236
|
+
* @param {PointerEvent} e - Pointer event
|
|
237
|
+
*/
|
|
238
|
+
handlePointerMove(e) {
|
|
239
|
+
if (!this.currentTool) return;
|
|
240
|
+
this.currentTool.onPointerMove(e);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Handle pointer up
|
|
245
|
+
* @param {PointerEvent} e - Pointer event
|
|
246
|
+
*/
|
|
247
|
+
handlePointerUp(e) {
|
|
248
|
+
if (!this.currentTool) return;
|
|
249
|
+
|
|
250
|
+
e.target.releasePointerCapture(e.pointerId);
|
|
251
|
+
|
|
252
|
+
this.currentTool.onPointerUp(e);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Handle pointer leave
|
|
257
|
+
* @param {PointerEvent} e - Pointer event
|
|
258
|
+
*/
|
|
259
|
+
handlePointerLeave(e) {
|
|
260
|
+
if (!this.currentTool) return;
|
|
261
|
+
this.currentTool.onPointerLeave(e);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Destroy tool manager
|
|
266
|
+
*/
|
|
267
|
+
destroy() {
|
|
268
|
+
this.tools.forEach(tool => {
|
|
269
|
+
if (tool.destroy) tool.destroy();
|
|
270
|
+
});
|
|
271
|
+
this.tools.clear();
|
|
272
|
+
this.currentTool = null;
|
|
273
|
+
this.previousTool = null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default ToolManager;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Studio - Zoom Tool
|
|
3
|
+
* Zoom the canvas viewport
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseTool } from './BaseTool.js';
|
|
8
|
+
|
|
9
|
+
export class ZoomTool extends BaseTool {
|
|
10
|
+
constructor(app) {
|
|
11
|
+
super(app);
|
|
12
|
+
this.name = 'zoom';
|
|
13
|
+
this.icon = 'zoom-in';
|
|
14
|
+
this.cursor = 'zoom-in';
|
|
15
|
+
this.shortcut = 'z';
|
|
16
|
+
|
|
17
|
+
this.options = {
|
|
18
|
+
zoomIn: true // false = zoom out
|
|
19
|
+
};
|
|
20
|
+
this.defaultOptions = { ...this.options };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
onPointerDown(e) {
|
|
24
|
+
super.onPointerDown(e);
|
|
25
|
+
|
|
26
|
+
const viewPoint = this.getViewportPoint(e);
|
|
27
|
+
|
|
28
|
+
if (this.options.zoomIn) {
|
|
29
|
+
this.app.canvas.zoomIn();
|
|
30
|
+
} else {
|
|
31
|
+
this.app.canvas.zoomOut();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onPointerMove(e) {
|
|
36
|
+
super.onPointerMove(e);
|
|
37
|
+
|
|
38
|
+
// Update cursor based on Alt key
|
|
39
|
+
if (e.altKey || !this.options.zoomIn) {
|
|
40
|
+
this.cursor = 'zoom-out';
|
|
41
|
+
} else {
|
|
42
|
+
this.cursor = 'zoom-in';
|
|
43
|
+
}
|
|
44
|
+
this.updateCursor();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getOptionsUI() {
|
|
48
|
+
return {
|
|
49
|
+
zoomIn: {
|
|
50
|
+
type: 'checkbox',
|
|
51
|
+
label: 'Zoom In',
|
|
52
|
+
value: this.options.zoomIn
|
|
53
|
+
},
|
|
54
|
+
fitToScreen: {
|
|
55
|
+
type: 'button',
|
|
56
|
+
label: 'Fit to Screen',
|
|
57
|
+
action: () => this.app.canvas.fitToScreen()
|
|
58
|
+
},
|
|
59
|
+
actualSize: {
|
|
60
|
+
type: 'button',
|
|
61
|
+
label: '100%',
|
|
62
|
+
action: () => this.app.canvas.setZoom(100)
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default ZoomTool;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Studio - Color Manager
|
|
3
|
+
* @version 2.0.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Events } from '../core/EventEmitter.js';
|
|
7
|
+
|
|
8
|
+
export class ColorManager {
|
|
9
|
+
constructor(app) {
|
|
10
|
+
this.app = app;
|
|
11
|
+
this.foreground = '#000000';
|
|
12
|
+
this.background = '#ffffff';
|
|
13
|
+
this.swatches = [
|
|
14
|
+
'#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff',
|
|
15
|
+
'#ffff00', '#00ffff', '#ff00ff', '#ff8800', '#8800ff',
|
|
16
|
+
'#888888', '#444444', '#cccccc', '#880000', '#008800'
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setForeground(color) {
|
|
21
|
+
this.foreground = color;
|
|
22
|
+
this.app.events.emit(Events.COLOR_FOREGROUND, { color });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setBackground(color) {
|
|
26
|
+
this.background = color;
|
|
27
|
+
this.app.events.emit(Events.COLOR_BACKGROUND, { color });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
swap() {
|
|
31
|
+
const temp = this.foreground;
|
|
32
|
+
this.foreground = this.background;
|
|
33
|
+
this.background = temp;
|
|
34
|
+
this.app.events.emit(Events.COLOR_SWAP, {
|
|
35
|
+
foreground: this.foreground,
|
|
36
|
+
background: this.background
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
reset() {
|
|
41
|
+
this.foreground = '#000000';
|
|
42
|
+
this.background = '#ffffff';
|
|
43
|
+
this.app.events.emit(Events.COLOR_FOREGROUND, { color: this.foreground });
|
|
44
|
+
this.app.events.emit(Events.COLOR_BACKGROUND, { color: this.background });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
addSwatch(color) {
|
|
48
|
+
if (!this.swatches.includes(color)) {
|
|
49
|
+
this.swatches.push(color);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
removeSwatch(index) {
|
|
54
|
+
this.swatches.splice(index, 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
hexToRgb(hex) {
|
|
58
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
59
|
+
return result ? {
|
|
60
|
+
r: parseInt(result[1], 16),
|
|
61
|
+
g: parseInt(result[2], 16),
|
|
62
|
+
b: parseInt(result[3], 16)
|
|
63
|
+
} : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
rgbToHex(r, g, b) {
|
|
67
|
+
return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default ColorManager;
|