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,250 @@
1
+ /**
2
+ * SenangWebs Studio - History Manager
3
+ * Undo/redo system with state snapshots
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { Events } from './EventEmitter.js';
8
+
9
+ export class History {
10
+ constructor(app, options = {}) {
11
+ this.app = app;
12
+ this.maxStates = options.maxStates || 50;
13
+ this.states = [];
14
+ this.currentIndex = -1;
15
+ this.isPerformingAction = false;
16
+ }
17
+
18
+ /**
19
+ * Initialize history with first state
20
+ */
21
+ init() {
22
+ this.clear();
23
+ this.pushState('Initial State');
24
+ }
25
+
26
+ /**
27
+ * Push a new state to history
28
+ * @param {string} actionName - Description of the action
29
+ * @param {Object} state - State snapshot (optional, will capture current if not provided)
30
+ */
31
+ pushState(actionName, state = null) {
32
+ if (this.isPerformingAction) return;
33
+
34
+ // Remove any states after current index (for redo overwrite)
35
+ if (this.currentIndex < this.states.length - 1) {
36
+ this.states = this.states.slice(0, this.currentIndex + 1);
37
+ }
38
+
39
+ // Capture current state if not provided
40
+ const snapshot = state || this.captureState();
41
+
42
+ // Add new state
43
+ this.states.push({
44
+ name: actionName,
45
+ timestamp: Date.now(),
46
+ state: snapshot
47
+ });
48
+
49
+ // Limit history size
50
+ if (this.states.length > this.maxStates) {
51
+ this.states.shift();
52
+ } else {
53
+ this.currentIndex++;
54
+ }
55
+
56
+ this.app.events.emit(Events.HISTORY_PUSH, {
57
+ actionName,
58
+ index: this.currentIndex,
59
+ total: this.states.length
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Capture current application state
65
+ * @returns {Object} State snapshot
66
+ */
67
+ captureState() {
68
+ const layers = this.app.layers.getLayers().map(layer => ({
69
+ id: layer.id,
70
+ name: layer.name,
71
+ type: layer.type,
72
+ visible: layer.visible,
73
+ locked: layer.locked,
74
+ opacity: layer.opacity,
75
+ blendMode: layer.blendMode,
76
+ position: { ...layer.position },
77
+ imageData: layer.canvas ? layer.canvas.toDataURL() : null
78
+ }));
79
+
80
+ return {
81
+ layers,
82
+ activeLayerId: this.app.layers.activeLayer?.id,
83
+ canvasWidth: this.app.canvas.width,
84
+ canvasHeight: this.app.canvas.height,
85
+ zoom: this.app.canvas.zoom,
86
+ panX: this.app.canvas.panX,
87
+ panY: this.app.canvas.panY
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Restore a state snapshot
93
+ * @param {Object} snapshot - State to restore
94
+ */
95
+ async restoreState(snapshot) {
96
+ this.isPerformingAction = true;
97
+
98
+ try {
99
+ // Clear existing layers
100
+ this.app.layers.clear();
101
+
102
+ // Restore canvas dimensions
103
+ this.app.canvas.resize(snapshot.canvasWidth, snapshot.canvasHeight);
104
+ this.app.canvas.zoom = snapshot.zoom;
105
+ this.app.canvas.panX = snapshot.panX;
106
+ this.app.canvas.panY = snapshot.panY;
107
+
108
+ // Restore layers
109
+ for (const layerData of snapshot.layers) {
110
+ const layer = await this.app.layers.addLayer({
111
+ id: layerData.id,
112
+ name: layerData.name,
113
+ type: layerData.type,
114
+ visible: layerData.visible,
115
+ locked: layerData.locked,
116
+ opacity: layerData.opacity,
117
+ blendMode: layerData.blendMode,
118
+ position: layerData.position
119
+ });
120
+
121
+ // Restore image data
122
+ if (layerData.imageData) {
123
+ await layer.loadFromDataURL(layerData.imageData);
124
+ }
125
+ }
126
+
127
+ // Restore active layer
128
+ if (snapshot.activeLayerId) {
129
+ this.app.layers.setActiveLayer(snapshot.activeLayerId);
130
+ }
131
+
132
+ // Re-render
133
+ this.app.canvas.render();
134
+
135
+ } finally {
136
+ this.isPerformingAction = false;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Undo last action
142
+ * @returns {boolean} Success
143
+ */
144
+ async undo() {
145
+ if (!this.canUndo()) return false;
146
+
147
+ this.currentIndex--;
148
+ const state = this.states[this.currentIndex];
149
+
150
+ await this.restoreState(state.state);
151
+
152
+ this.app.events.emit(Events.HISTORY_UNDO, {
153
+ actionName: state.name,
154
+ index: this.currentIndex
155
+ });
156
+
157
+ return true;
158
+ }
159
+
160
+ /**
161
+ * Redo last undone action
162
+ * @returns {boolean} Success
163
+ */
164
+ async redo() {
165
+ if (!this.canRedo()) return false;
166
+
167
+ this.currentIndex++;
168
+ const state = this.states[this.currentIndex];
169
+
170
+ await this.restoreState(state.state);
171
+
172
+ this.app.events.emit(Events.HISTORY_REDO, {
173
+ actionName: state.name,
174
+ index: this.currentIndex
175
+ });
176
+
177
+ return true;
178
+ }
179
+
180
+ /**
181
+ * Check if undo is available
182
+ * @returns {boolean}
183
+ */
184
+ canUndo() {
185
+ return this.currentIndex > 0;
186
+ }
187
+
188
+ /**
189
+ * Check if redo is available
190
+ * @returns {boolean}
191
+ */
192
+ canRedo() {
193
+ return this.currentIndex < this.states.length - 1;
194
+ }
195
+
196
+ /**
197
+ * Go to specific history state
198
+ * @param {number} index - State index
199
+ */
200
+ async goToState(index) {
201
+ if (index < 0 || index >= this.states.length) return false;
202
+
203
+ this.currentIndex = index;
204
+ const state = this.states[this.currentIndex];
205
+
206
+ await this.restoreState(state.state);
207
+
208
+ return true;
209
+ }
210
+
211
+ /**
212
+ * Get all history states
213
+ * @returns {Array} History states
214
+ */
215
+ getStates() {
216
+ return this.states.map((state, index) => ({
217
+ index,
218
+ name: state.name,
219
+ timestamp: state.timestamp,
220
+ isCurrent: index === this.currentIndex
221
+ }));
222
+ }
223
+
224
+ /**
225
+ * Get current state index
226
+ * @returns {number}
227
+ */
228
+ getCurrentIndex() {
229
+ return this.currentIndex;
230
+ }
231
+
232
+ /**
233
+ * Clear all history
234
+ */
235
+ clear() {
236
+ this.states = [];
237
+ this.currentIndex = -1;
238
+ this.app.events.emit(Events.HISTORY_CLEAR);
239
+ }
240
+
241
+ /**
242
+ * Get history count
243
+ * @returns {number}
244
+ */
245
+ get count() {
246
+ return this.states.length;
247
+ }
248
+ }
249
+
250
+ export default History;
@@ -0,0 +1,323 @@
1
+ /**
2
+ * SenangWebs Studio - Keyboard Manager
3
+ * Keyboard shortcuts and hotkey management
4
+ * @version 2.0.0
5
+ */
6
+
7
+ export class Keyboard {
8
+ constructor(app) {
9
+ this.app = app;
10
+ this.shortcuts = new Map();
11
+ this.enabled = true;
12
+ this.modifiers = {
13
+ ctrl: false,
14
+ shift: false,
15
+ alt: false,
16
+ meta: false
17
+ };
18
+ this.spacePressed = false;
19
+
20
+ this.init();
21
+ }
22
+
23
+ /**
24
+ * Initialize keyboard listeners
25
+ */
26
+ init() {
27
+ this.registerDefaultShortcuts();
28
+
29
+ document.addEventListener('keydown', this.handleKeyDown.bind(this));
30
+ document.addEventListener('keyup', this.handleKeyUp.bind(this));
31
+ window.addEventListener('blur', this.resetModifiers.bind(this));
32
+ }
33
+
34
+ /**
35
+ * Register default Photoshop-like shortcuts
36
+ */
37
+ registerDefaultShortcuts() {
38
+ // Tool shortcuts
39
+ this.register('v', () => this.app.tools.setTool('move'));
40
+ this.register('m', () => this.app.tools.setTool('marquee'));
41
+ this.register('l', () => this.app.tools.setTool('lasso'));
42
+ this.register('w', () => this.app.tools.setTool('magicWand'));
43
+ this.register('b', () => this.app.tools.setTool('brush'));
44
+ this.register('e', () => this.app.tools.setTool('eraser'));
45
+ this.register('g', () => this.app.tools.setTool('gradient'));
46
+ this.register('t', () => this.app.tools.setTool('text'));
47
+ this.register('u', () => this.app.tools.setTool('shape'));
48
+ this.register('i', () => this.app.tools.setTool('eyedropper'));
49
+ this.register('z', () => this.app.tools.setTool('zoom'));
50
+ this.register('h', () => this.app.tools.setTool('hand'));
51
+ this.register('s', () => this.app.tools.setTool('cloneStamp'));
52
+ this.register('c', () => this.app.tools.setTool('crop'));
53
+
54
+ // File operations
55
+ this.register('ctrl+n', () => this.app.file.newDocument());
56
+ this.register('ctrl+o', () => this.app.file.open());
57
+ this.register('ctrl+s', () => this.app.file.save());
58
+ this.register('ctrl+shift+s', () => this.app.file.saveAs());
59
+ this.register('ctrl+e', () => this.app.file.export());
60
+ this.register('ctrl+shift+e', () => this.app.file.exportAs());
61
+
62
+ // Edit operations
63
+ this.register('ctrl+z', () => this.app.history.undo());
64
+ this.register('ctrl+shift+z', () => this.app.history.redo());
65
+ this.register('ctrl+y', () => this.app.history.redo());
66
+ this.register('ctrl+a', () => this.app.selection.selectAll());
67
+ this.register('ctrl+d', () => this.app.selection.deselect());
68
+ this.register('ctrl+shift+i', () => this.app.selection.invert());
69
+ this.register('ctrl+c', () => this.app.clipboard.copy());
70
+ this.register('ctrl+v', () => this.app.clipboard.paste());
71
+ this.register('ctrl+x', () => this.app.clipboard.cut());
72
+ this.register('ctrl+t', () => this.app.tools.startTransform());
73
+ this.register('delete', () => this.app.layers.deleteSelection());
74
+ this.register('backspace', () => this.app.layers.deleteSelection());
75
+
76
+ // View operations
77
+ this.register('ctrl+0', () => this.app.canvas.fitToScreen());
78
+ this.register('ctrl+1', () => this.app.canvas.setZoom(100));
79
+ this.register('ctrl+plus', () => this.app.canvas.zoomIn());
80
+ this.register('ctrl+minus', () => this.app.canvas.zoomOut());
81
+ this.register('tab', () => this.app.ui.togglePanels());
82
+ this.register('f', () => this.app.ui.toggleFullscreen());
83
+
84
+ // Brush size
85
+ this.register('[', () => this.app.tools.decreaseBrushSize());
86
+ this.register(']', () => this.app.tools.increaseBrushSize());
87
+ this.register('shift+[', () => this.app.tools.decreaseBrushHardness());
88
+ this.register('shift+]', () => this.app.tools.increaseBrushHardness());
89
+
90
+ // Layer operations
91
+ this.register('ctrl+shift+n', () => this.app.layers.addLayer());
92
+ this.register('ctrl+j', () => this.app.layers.duplicateLayer());
93
+ this.register('ctrl+shift+e', () => this.app.layers.mergeVisible());
94
+ this.register('ctrl+e', () => this.app.layers.mergeDown());
95
+
96
+ // Color
97
+ this.register('x', () => this.app.colors.swap());
98
+ this.register('d', () => this.app.colors.reset());
99
+
100
+ // Escape
101
+ this.register('escape', () => this.app.cancelCurrentAction());
102
+ this.register('enter', () => this.app.confirmCurrentAction());
103
+ }
104
+
105
+ /**
106
+ * Register a keyboard shortcut
107
+ * @param {string} shortcut - Shortcut string (e.g., 'ctrl+s', 'shift+a')
108
+ * @param {Function} callback - Callback function
109
+ * @param {Object} options - Options
110
+ */
111
+ register(shortcut, callback, options = {}) {
112
+ const key = this.normalizeShortcut(shortcut);
113
+ this.shortcuts.set(key, {
114
+ callback,
115
+ preventDefault: options.preventDefault !== false,
116
+ allowInInput: options.allowInInput || false,
117
+ description: options.description || ''
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Unregister a keyboard shortcut
123
+ * @param {string} shortcut - Shortcut string
124
+ */
125
+ unregister(shortcut) {
126
+ const key = this.normalizeShortcut(shortcut);
127
+ this.shortcuts.delete(key);
128
+ }
129
+
130
+ /**
131
+ * Normalize shortcut string
132
+ * @param {string} shortcut - Shortcut string
133
+ * @returns {string} Normalized shortcut
134
+ */
135
+ normalizeShortcut(shortcut) {
136
+ return shortcut
137
+ .toLowerCase()
138
+ .split('+')
139
+ .sort((a, b) => {
140
+ const order = ['ctrl', 'alt', 'shift', 'meta'];
141
+ const aIndex = order.indexOf(a);
142
+ const bIndex = order.indexOf(b);
143
+ if (aIndex === -1 && bIndex === -1) return a.localeCompare(b);
144
+ if (aIndex === -1) return 1;
145
+ if (bIndex === -1) return -1;
146
+ return aIndex - bIndex;
147
+ })
148
+ .join('+');
149
+ }
150
+
151
+ /**
152
+ * Build shortcut string from event
153
+ * @param {KeyboardEvent} e - Keyboard event
154
+ * @returns {string} Shortcut string
155
+ */
156
+ buildShortcutFromEvent(e) {
157
+ const parts = [];
158
+
159
+ if (e.ctrlKey || e.metaKey) parts.push('ctrl');
160
+ if (e.altKey) parts.push('alt');
161
+ if (e.shiftKey) parts.push('shift');
162
+
163
+ let key = e.key.toLowerCase();
164
+
165
+ // Normalize special keys
166
+ const keyMap = {
167
+ ' ': 'space',
168
+ 'arrowup': 'up',
169
+ 'arrowdown': 'down',
170
+ 'arrowleft': 'left',
171
+ 'arrowright': 'right',
172
+ '=': 'plus',
173
+ '-': 'minus'
174
+ };
175
+
176
+ key = keyMap[key] || key;
177
+
178
+ // Don't add modifier keys as the main key
179
+ if (!['control', 'alt', 'shift', 'meta'].includes(key)) {
180
+ parts.push(key);
181
+ }
182
+
183
+ return this.normalizeShortcut(parts.join('+'));
184
+ }
185
+
186
+ /**
187
+ * Handle keydown event
188
+ * @param {KeyboardEvent} e - Keyboard event
189
+ */
190
+ handleKeyDown(e) {
191
+ // Update modifier state
192
+ this.modifiers.ctrl = e.ctrlKey || e.metaKey;
193
+ this.modifiers.shift = e.shiftKey;
194
+ this.modifiers.alt = e.altKey;
195
+ this.modifiers.meta = e.metaKey;
196
+
197
+ // Handle space for temporary hand tool
198
+ if (e.code === 'Space' && !this.spacePressed) {
199
+ // Don't activate hand tool if text tool is editing
200
+ const currentTool = this.app.tools?.currentTool;
201
+ if (currentTool?.name === 'text' && currentTool?.editingLayer) {
202
+ return; // Allow space in text
203
+ }
204
+ this.spacePressed = true;
205
+ this.app.tools.activateTemporaryTool('hand');
206
+ e.preventDefault();
207
+ return;
208
+ }
209
+
210
+ if (!this.enabled) return;
211
+
212
+ // Check if text tool is actively editing - allow all typing
213
+ const currentTool = this.app.tools?.currentTool;
214
+ if (currentTool?.name === 'text' && currentTool?.editingLayer) {
215
+ // Only process shortcuts with modifiers (Ctrl, Alt, Meta) during text editing
216
+ if (!e.ctrlKey && !e.altKey && !e.metaKey) {
217
+ // Allow normal typing, don't process tool shortcuts
218
+ return;
219
+ }
220
+ }
221
+
222
+ // Check if in input field
223
+ const target = e.target;
224
+ const isInputField = target.tagName === 'INPUT' ||
225
+ target.tagName === 'TEXTAREA' ||
226
+ target.contentEditable === 'true';
227
+
228
+ // Build shortcut string
229
+ const shortcut = this.buildShortcutFromEvent(e);
230
+
231
+ // Look up shortcut
232
+ const handler = this.shortcuts.get(shortcut);
233
+
234
+ if (handler) {
235
+ // Skip if in input field and not allowed
236
+ if (isInputField && !handler.allowInInput) return;
237
+
238
+ if (handler.preventDefault) {
239
+ e.preventDefault();
240
+ }
241
+
242
+ try {
243
+ handler.callback(e);
244
+ } catch (error) {
245
+ console.error(`Error executing shortcut "${shortcut}":`, error);
246
+ }
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Handle keyup event
252
+ * @param {KeyboardEvent} e - Keyboard event
253
+ */
254
+ handleKeyUp(e) {
255
+ // Update modifier state
256
+ this.modifiers.ctrl = e.ctrlKey || e.metaKey;
257
+ this.modifiers.shift = e.shiftKey;
258
+ this.modifiers.alt = e.altKey;
259
+ this.modifiers.meta = e.metaKey;
260
+
261
+ // Handle space release
262
+ if (e.code === 'Space') {
263
+ this.spacePressed = false;
264
+ this.app.tools.deactivateTemporaryTool();
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Reset modifier states
270
+ */
271
+ resetModifiers() {
272
+ this.modifiers.ctrl = false;
273
+ this.modifiers.shift = false;
274
+ this.modifiers.alt = false;
275
+ this.modifiers.meta = false;
276
+ this.spacePressed = false;
277
+ this.app.tools.deactivateTemporaryTool();
278
+ }
279
+
280
+ /**
281
+ * Enable/disable keyboard shortcuts
282
+ * @param {boolean} enabled - Enable state
283
+ */
284
+ setEnabled(enabled) {
285
+ this.enabled = enabled;
286
+ }
287
+
288
+ /**
289
+ * Check if a modifier is pressed
290
+ * @param {string} modifier - Modifier name
291
+ * @returns {boolean}
292
+ */
293
+ isModifierPressed(modifier) {
294
+ return this.modifiers[modifier] || false;
295
+ }
296
+
297
+ /**
298
+ * Get all registered shortcuts
299
+ * @returns {Array} Shortcuts with descriptions
300
+ */
301
+ getShortcuts() {
302
+ const shortcuts = [];
303
+ this.shortcuts.forEach((handler, key) => {
304
+ shortcuts.push({
305
+ shortcut: key,
306
+ description: handler.description
307
+ });
308
+ });
309
+ return shortcuts;
310
+ }
311
+
312
+ /**
313
+ * Destroy keyboard manager
314
+ */
315
+ destroy() {
316
+ document.removeEventListener('keydown', this.handleKeyDown);
317
+ document.removeEventListener('keyup', this.handleKeyUp);
318
+ window.removeEventListener('blur', this.resetModifiers);
319
+ this.shortcuts.clear();
320
+ }
321
+ }
322
+
323
+ export default Keyboard;