senangwebs-photobooth 1.0.2 → 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 (43) hide show
  1. package/README.md +220 -236
  2. package/dist/swp.css +790 -256
  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 +10 -6
  8. package/src/css/swp.css +790 -256
  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 +247 -761
  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/swp_preview.png +0 -0
@@ -0,0 +1,248 @@
1
+ /**
2
+ * SenangWebs Studio - Filter Manager
3
+ * @version 2.0.0
4
+ */
5
+
6
+ import { Events } from '../core/EventEmitter.js';
7
+
8
+ export class FilterManager {
9
+ constructor(app) {
10
+ this.app = app;
11
+ this.previewActive = false;
12
+ this.originalImageData = null;
13
+ }
14
+
15
+ startPreview() {
16
+ const layer = this.app.layers.getActiveLayer();
17
+ if (!layer?.ctx) return;
18
+ this.originalImageData = layer.ctx.getImageData(0, 0, layer.width, layer.height);
19
+ this.previewActive = true;
20
+ }
21
+
22
+ cancelPreview() {
23
+ if (!this.previewActive || !this.originalImageData) return;
24
+ const layer = this.app.layers.getActiveLayer();
25
+ if (layer?.ctx) {
26
+ layer.ctx.putImageData(this.originalImageData, 0, 0);
27
+ this.app.canvas.scheduleRender();
28
+ }
29
+ this.originalImageData = null;
30
+ this.previewActive = false;
31
+ this.app.events.emit(Events.FILTER_CANCEL);
32
+ }
33
+
34
+ applyFilter(filterName, options = {}) {
35
+ const layer = this.app.layers.getActiveLayer();
36
+ if (!layer?.ctx) return;
37
+
38
+ // Use original image data if preview was active, otherwise get current
39
+ const imageData = this.originalImageData
40
+ ? new ImageData(
41
+ new Uint8ClampedArray(this.originalImageData.data),
42
+ this.originalImageData.width,
43
+ this.originalImageData.height
44
+ )
45
+ : layer.ctx.getImageData(0, 0, layer.width, layer.height);
46
+
47
+ const filtered = this.processFilter(filterName, imageData, options);
48
+
49
+ layer.ctx.putImageData(filtered, 0, 0);
50
+ this.originalImageData = null;
51
+ this.previewActive = false;
52
+
53
+ this.app.history.pushState(`Filter: ${filterName}`);
54
+ this.app.canvas.scheduleRender();
55
+ this.app.events.emit(Events.FILTER_APPLY, { filter: filterName, options });
56
+ }
57
+
58
+ previewFilter(filterName, options = {}) {
59
+ if (!this.previewActive) this.startPreview();
60
+ const layer = this.app.layers.getActiveLayer();
61
+ if (!layer?.ctx || !this.originalImageData) return;
62
+
63
+ const filtered = this.processFilter(filterName, new ImageData(
64
+ new Uint8ClampedArray(this.originalImageData.data),
65
+ this.originalImageData.width, this.originalImageData.height
66
+ ), options);
67
+
68
+ layer.ctx.putImageData(filtered, 0, 0);
69
+ this.app.canvas.scheduleRender();
70
+ this.app.events.emit(Events.FILTER_PREVIEW, { filter: filterName, options });
71
+ }
72
+
73
+ processFilter(filterName, imageData, options) {
74
+ switch (filterName) {
75
+ case 'brightness': return this.adjustBrightness(imageData, options.value || 0);
76
+ case 'contrast': return this.adjustContrast(imageData, options.value || 0);
77
+ case 'saturation': return this.adjustSaturation(imageData, options.value || 0);
78
+ case 'grayscale': return this.grayscale(imageData);
79
+ case 'sepia': return this.sepia(imageData);
80
+ case 'invert': return this.invert(imageData);
81
+ case 'blur': return this.blur(imageData, options.radius || 5);
82
+ case 'sharpen': return this.sharpen(imageData, options.amount || 1);
83
+ case 'hueRotate': return this.hueRotate(imageData, options.angle || 0);
84
+ default: return imageData;
85
+ }
86
+ }
87
+
88
+ adjustBrightness(imageData, value) {
89
+ const data = imageData.data;
90
+ for (let i = 0; i < data.length; i += 4) {
91
+ data[i] = Math.min(255, Math.max(0, data[i] + value));
92
+ data[i + 1] = Math.min(255, Math.max(0, data[i + 1] + value));
93
+ data[i + 2] = Math.min(255, Math.max(0, data[i + 2] + value));
94
+ }
95
+ return imageData;
96
+ }
97
+
98
+ adjustContrast(imageData, value) {
99
+ const data = imageData.data;
100
+ const factor = (259 * (value + 255)) / (255 * (259 - value));
101
+ for (let i = 0; i < data.length; i += 4) {
102
+ data[i] = Math.min(255, Math.max(0, factor * (data[i] - 128) + 128));
103
+ data[i + 1] = Math.min(255, Math.max(0, factor * (data[i + 1] - 128) + 128));
104
+ data[i + 2] = Math.min(255, Math.max(0, factor * (data[i + 2] - 128) + 128));
105
+ }
106
+ return imageData;
107
+ }
108
+
109
+ adjustSaturation(imageData, value) {
110
+ const data = imageData.data;
111
+ const factor = 1 + value / 100;
112
+ for (let i = 0; i < data.length; i += 4) {
113
+ const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
114
+ data[i] = Math.min(255, Math.max(0, gray + factor * (data[i] - gray)));
115
+ data[i + 1] = Math.min(255, Math.max(0, gray + factor * (data[i + 1] - gray)));
116
+ data[i + 2] = Math.min(255, Math.max(0, gray + factor * (data[i + 2] - gray)));
117
+ }
118
+ return imageData;
119
+ }
120
+
121
+ grayscale(imageData) {
122
+ const data = imageData.data;
123
+ for (let i = 0; i < data.length; i += 4) {
124
+ const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
125
+ data[i] = data[i + 1] = data[i + 2] = gray;
126
+ }
127
+ return imageData;
128
+ }
129
+
130
+ sepia(imageData) {
131
+ const data = imageData.data;
132
+ for (let i = 0; i < data.length; i += 4) {
133
+ const r = data[i], g = data[i + 1], b = data[i + 2];
134
+ data[i] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189);
135
+ data[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168);
136
+ data[i + 2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131);
137
+ }
138
+ return imageData;
139
+ }
140
+
141
+ invert(imageData) {
142
+ const data = imageData.data;
143
+ for (let i = 0; i < data.length; i += 4) {
144
+ data[i] = 255 - data[i];
145
+ data[i + 1] = 255 - data[i + 1];
146
+ data[i + 2] = 255 - data[i + 2];
147
+ }
148
+ return imageData;
149
+ }
150
+
151
+ blur(imageData, radius) {
152
+ // Simple box blur
153
+ const data = imageData.data;
154
+ const width = imageData.width;
155
+ const height = imageData.height;
156
+ const result = new Uint8ClampedArray(data);
157
+
158
+ for (let y = 0; y < height; y++) {
159
+ for (let x = 0; x < width; x++) {
160
+ let r = 0, g = 0, b = 0, count = 0;
161
+ for (let dy = -radius; dy <= radius; dy++) {
162
+ for (let dx = -radius; dx <= radius; dx++) {
163
+ const nx = x + dx, ny = y + dy;
164
+ if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
165
+ const idx = (ny * width + nx) * 4;
166
+ r += data[idx]; g += data[idx + 1]; b += data[idx + 2];
167
+ count++;
168
+ }
169
+ }
170
+ }
171
+ const idx = (y * width + x) * 4;
172
+ result[idx] = r / count;
173
+ result[idx + 1] = g / count;
174
+ result[idx + 2] = b / count;
175
+ }
176
+ }
177
+
178
+ return new ImageData(result, width, height);
179
+ }
180
+
181
+ sharpen(imageData, amount) {
182
+ const kernel = [0, -amount, 0, -amount, 1 + 4 * amount, -amount, 0, -amount, 0];
183
+ return this.convolve(imageData, kernel);
184
+ }
185
+
186
+ hueRotate(imageData, angle) {
187
+ const data = imageData.data;
188
+ const cos = Math.cos(angle * Math.PI / 180);
189
+ const sin = Math.sin(angle * Math.PI / 180);
190
+
191
+ for (let i = 0; i < data.length; i += 4) {
192
+ const r = data[i], g = data[i + 1], b = data[i + 2];
193
+ data[i] = Math.min(255, Math.max(0, r * (0.213 + cos * 0.787 - sin * 0.213) +
194
+ g * (0.715 - cos * 0.715 - sin * 0.715) + b * (0.072 - cos * 0.072 + sin * 0.928)));
195
+ data[i + 1] = Math.min(255, Math.max(0, r * (0.213 - cos * 0.213 + sin * 0.143) +
196
+ g * (0.715 + cos * 0.285 + sin * 0.140) + b * (0.072 - cos * 0.072 - sin * 0.283)));
197
+ data[i + 2] = Math.min(255, Math.max(0, r * (0.213 - cos * 0.213 - sin * 0.787) +
198
+ g * (0.715 - cos * 0.715 + sin * 0.715) + b * (0.072 + cos * 0.928 + sin * 0.072)));
199
+ }
200
+ return imageData;
201
+ }
202
+
203
+ convolve(imageData, kernel) {
204
+ const data = imageData.data;
205
+ const width = imageData.width;
206
+ const height = imageData.height;
207
+ const result = new Uint8ClampedArray(data);
208
+ const kSize = Math.sqrt(kernel.length);
209
+ const half = Math.floor(kSize / 2);
210
+
211
+ for (let y = half; y < height - half; y++) {
212
+ for (let x = half; x < width - half; x++) {
213
+ let r = 0, g = 0, b = 0;
214
+ for (let ky = 0; ky < kSize; ky++) {
215
+ for (let kx = 0; kx < kSize; kx++) {
216
+ const idx = ((y + ky - half) * width + (x + kx - half)) * 4;
217
+ const k = kernel[ky * kSize + kx];
218
+ r += data[idx] * k;
219
+ g += data[idx + 1] * k;
220
+ b += data[idx + 2] * k;
221
+ }
222
+ }
223
+ const idx = (y * width + x) * 4;
224
+ result[idx] = Math.min(255, Math.max(0, r));
225
+ result[idx + 1] = Math.min(255, Math.max(0, g));
226
+ result[idx + 2] = Math.min(255, Math.max(0, b));
227
+ }
228
+ }
229
+
230
+ return new ImageData(result, width, height);
231
+ }
232
+
233
+ getAvailableFilters() {
234
+ return [
235
+ { name: 'brightness', label: 'Brightness', hasOptions: true },
236
+ { name: 'contrast', label: 'Contrast', hasOptions: true },
237
+ { name: 'saturation', label: 'Saturation', hasOptions: true },
238
+ { name: 'hueRotate', label: 'Hue/Saturation', hasOptions: true },
239
+ { name: 'grayscale', label: 'Grayscale', hasOptions: false },
240
+ { name: 'sepia', label: 'Sepia', hasOptions: false },
241
+ { name: 'invert', label: 'Invert', hasOptions: false },
242
+ { name: 'blur', label: 'Blur', hasOptions: true },
243
+ { name: 'sharpen', label: 'Sharpen', hasOptions: true }
244
+ ];
245
+ }
246
+ }
247
+
248
+ export default FilterManager;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * SenangWebs Photobooth (SWP) - Module Exports
3
+ * @version 2.0.0
4
+ */
5
+
6
+ // Core
7
+ export { EventEmitter, Events } from './core/EventEmitter.js';
8
+ export { Canvas } from './core/Canvas.js';
9
+ export { History } from './core/History.js';
10
+ export { Keyboard } from './core/Keyboard.js';
11
+
12
+ // Layers
13
+ export { Layer } from './layers/Layer.js';
14
+ export { LayerManager } from './layers/LayerManager.js';
15
+ export { BlendModes, applyBlendMode, getBlendModeList } from './layers/BlendModes.js';
16
+
17
+ // Tools
18
+ export { BaseTool } from './tools/BaseTool.js';
19
+ export { ToolManager } from './tools/ToolManager.js';
20
+ export { MoveTool } from './tools/MoveTool.js';
21
+ export { BrushTool } from './tools/BrushTool.js';
22
+ export { EraserTool } from './tools/EraserTool.js';
23
+ export { ShapeTool } from './tools/ShapeTool.js';
24
+ export { TextTool } from './tools/TextTool.js';
25
+ export { CropTool } from './tools/CropTool.js';
26
+ export { ZoomTool } from './tools/ZoomTool.js';
27
+ export { HandTool } from './tools/HandTool.js';
28
+ export { EyedropperTool } from './tools/EyedropperTool.js';
29
+ export { GradientTool } from './tools/GradientTool.js';
30
+ export { FillTool } from './tools/FillTool.js';
31
+ export { MarqueeTool } from './tools/MarqueeTool.js';
32
+
33
+ // Selection
34
+ export { Selection } from './selection/Selection.js';
35
+
36
+ // Filters
37
+ export { FilterManager } from './filters/FilterManager.js';
38
+
39
+ // UI
40
+ export { UI } from './ui/UI.js';
41
+ export { ColorManager } from './ui/ColorManager.js';
42
+
43
+ // IO
44
+ export { FileManager } from './io/FileManager.js';
45
+ export { Clipboard } from './io/Clipboard.js';
46
+
47
+ // Main
48
+ export { default as SWP } from './swp.js';
@@ -0,0 +1,52 @@
1
+ /**
2
+ * SenangWebs Studio - Clipboard Manager
3
+ * @version 2.0.0
4
+ */
5
+
6
+ export class Clipboard {
7
+ constructor(app) {
8
+ this.app = app;
9
+ this.data = null;
10
+ this.type = null;
11
+ }
12
+
13
+ copy() {
14
+ const layer = this.app.layers.getActiveLayer();
15
+ if (!layer?.canvas) return;
16
+
17
+ const selection = this.app.selection;
18
+
19
+ if (selection?.hasSelection() && selection.bounds) {
20
+ const { x, y, width, height } = selection.bounds;
21
+ this.data = layer.ctx.getImageData(x, y, width, height);
22
+ this.type = 'imageData';
23
+ } else {
24
+ this.data = layer.canvas.toDataURL();
25
+ this.type = 'dataURL';
26
+ }
27
+ }
28
+
29
+ cut() {
30
+ this.copy();
31
+ this.app.layers.deleteSelection();
32
+ }
33
+
34
+ async paste() {
35
+ if (!this.data) return;
36
+
37
+ const layer = this.app.layers.addLayer({ name: 'Pasted Layer' });
38
+
39
+ if (this.type === 'imageData') {
40
+ layer.initCanvas(this.data.width, this.data.height);
41
+ layer.ctx.putImageData(this.data, 0, 0);
42
+ } else if (this.type === 'dataURL') {
43
+ await layer.loadFromDataURL(this.data);
44
+ }
45
+
46
+ this.app.layers.setActiveLayer(layer.id);
47
+ this.app.history.pushState('Paste');
48
+ this.app.canvas.scheduleRender();
49
+ }
50
+ }
51
+
52
+ export default Clipboard;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * SenangWebs Studio - File Manager
3
+ * @version 2.0.0
4
+ */
5
+
6
+ import { Events } from '../core/EventEmitter.js';
7
+
8
+ export class FileManager {
9
+ constructor(app) {
10
+ this.app = app;
11
+ this.projectName = 'Untitled';
12
+ this.projectData = null;
13
+ this.hasUnsavedChanges = false;
14
+
15
+ this.app.events.on(Events.CHANGE, () => {
16
+ this.hasUnsavedChanges = true;
17
+ });
18
+ }
19
+
20
+ newDocument(options = {}) {
21
+ const width = options.width || 1920;
22
+ const height = options.height || 1080;
23
+ const background = options.background || '#ffffff';
24
+
25
+ this.app.canvas.resize(width, height);
26
+ this.app.layers.init(width, height);
27
+
28
+ if (background !== 'transparent') {
29
+ const bgLayer = this.app.layers.getLayers()[0];
30
+ if (bgLayer) bgLayer.fill(background);
31
+ }
32
+
33
+ this.app.history.init();
34
+ this.projectName = 'Untitled';
35
+ this.hasUnsavedChanges = false;
36
+
37
+ this.app.canvas.fitToScreen();
38
+ this.app.events.emit(Events.DOCUMENT_NEW, { width, height });
39
+ }
40
+
41
+ async open() {
42
+ const input = document.createElement('input');
43
+ input.type = 'file';
44
+ input.accept = 'image/*,.sws';
45
+
46
+ return new Promise((resolve) => {
47
+ input.onchange = async (e) => {
48
+ const file = e.target.files[0];
49
+ if (!file) return resolve(null);
50
+
51
+ if (file.name.endsWith('.sws')) {
52
+ await this.openProject(file);
53
+ } else {
54
+ await this.openImage(file);
55
+ }
56
+ resolve(file);
57
+ };
58
+ input.click();
59
+ });
60
+ }
61
+
62
+ async openImage(file) {
63
+ const url = URL.createObjectURL(file);
64
+ const img = new Image();
65
+
66
+ return new Promise((resolve, reject) => {
67
+ img.onload = () => {
68
+ this.newDocument({ width: img.width, height: img.height });
69
+ const layer = this.app.layers.getActiveLayer();
70
+ if (layer) {
71
+ layer.ctx.drawImage(img, 0, 0);
72
+ this.app.history.pushState('Open Image');
73
+ }
74
+ URL.revokeObjectURL(url);
75
+ this.projectName = file.name.replace(/\.[^/.]+$/, '');
76
+ this.app.events.emit(Events.DOCUMENT_OPEN, { name: this.projectName });
77
+ resolve();
78
+ };
79
+ img.onerror = reject;
80
+ img.src = url;
81
+ });
82
+ }
83
+
84
+ async openProject(file) {
85
+ const text = await file.text();
86
+ const project = JSON.parse(text);
87
+
88
+ this.app.canvas.resize(project.width, project.height);
89
+ await this.app.layers.fromJSON(project.layers);
90
+
91
+ this.projectName = project.name || file.name.replace('.sws', '');
92
+ this.hasUnsavedChanges = false;
93
+ this.app.history.init();
94
+ this.app.canvas.fitToScreen();
95
+ this.app.events.emit(Events.DOCUMENT_OPEN, { name: this.projectName });
96
+ }
97
+
98
+ async save() {
99
+ const project = {
100
+ name: this.projectName,
101
+ version: '2.0.0',
102
+ width: this.app.canvas.width,
103
+ height: this.app.canvas.height,
104
+ layers: this.app.layers.toJSON()
105
+ };
106
+
107
+ const blob = new Blob([JSON.stringify(project)], { type: 'application/json' });
108
+ this.downloadBlob(blob, `${this.projectName}.sws`);
109
+
110
+ this.hasUnsavedChanges = false;
111
+ this.app.events.emit(Events.DOCUMENT_SAVE, { name: this.projectName });
112
+ }
113
+
114
+ async saveAs() {
115
+ const name = prompt('Project name:', this.projectName);
116
+ if (name) {
117
+ this.projectName = name;
118
+ await this.save();
119
+ }
120
+ }
121
+
122
+ async export(format = 'png', quality = 1) {
123
+ const dataURL = this.app.canvas.toDataURL(`image/${format}`, quality);
124
+ const link = document.createElement('a');
125
+ link.download = `${this.projectName}.${format}`;
126
+ link.href = dataURL;
127
+ link.click();
128
+
129
+ this.app.events.emit(Events.DOCUMENT_EXPORT, { format, name: this.projectName });
130
+ }
131
+
132
+ async exportAs() {
133
+ const format = prompt('Format (png, jpeg, webp):', 'png');
134
+ if (format && ['png', 'jpeg', 'webp'].includes(format)) {
135
+ const quality = format === 'png' ? 1 : parseFloat(prompt('Quality (0.1-1.0):', '0.9')) || 0.9;
136
+ await this.export(format, quality);
137
+ }
138
+ }
139
+
140
+ downloadBlob(blob, filename) {
141
+ const url = URL.createObjectURL(blob);
142
+ const link = document.createElement('a');
143
+ link.download = filename;
144
+ link.href = url;
145
+ link.click();
146
+ URL.revokeObjectURL(url);
147
+ }
148
+ }
149
+
150
+ export default FileManager;