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
package/src/js/swp.js CHANGED
@@ -1,726 +1,314 @@
1
1
  /**
2
2
  * SenangWebs Photobooth (SWP)
3
- * A lightweight client-side photo editing library
4
- * @version 1.0.0
3
+ * Professional browser-based image editor
4
+ * @version 2.0.0
5
5
  */
6
6
 
7
7
  import '../css/swp.css';
8
+ import { EventEmitter, Events } from './core/EventEmitter.js';
9
+ import { Canvas } from './core/Canvas.js';
10
+ import { History } from './core/History.js';
11
+ import { Keyboard } from './core/Keyboard.js';
12
+ import { LayerManager } from './layers/LayerManager.js';
13
+ import { ToolManager } from './tools/ToolManager.js';
14
+ import { Selection } from './selection/Selection.js';
15
+ import { FilterManager } from './filters/FilterManager.js';
16
+ import { ColorManager } from './ui/ColorManager.js';
17
+ import { UI } from './ui/UI.js';
18
+ import { FileManager } from './io/FileManager.js';
19
+ import { Clipboard } from './io/Clipboard.js';
8
20
 
9
21
  class SWP {
10
- constructor(container, options = {}) {
11
- this.container = container;
12
- this.options = {
13
- imageUrl: options.imageUrl || null,
14
- width: options.width || 800,
15
- height: options.height || 600,
16
- showIcons: options.showIcons !== undefined ? options.showIcons : true,
17
- showLabels: options.showLabels !== undefined ? options.showLabels : true,
18
- labels: {
19
- upload: options.labels?.upload !== undefined ? options.labels.upload : 'Upload',
20
- rotateLeft: options.labels?.rotateLeft !== undefined ? options.labels.rotateLeft : null,
21
- rotateRight: options.labels?.rotateRight !== undefined ? options.labels.rotateRight : null,
22
- flipH: options.labels?.flipH !== undefined ? options.labels.flipH : null,
23
- flipV: options.labels?.flipV !== undefined ? options.labels.flipV : null,
24
- resize: options.labels?.resize !== undefined ? options.labels.resize : 'Resize',
25
- adjust: options.labels?.adjust !== undefined ? options.labels.adjust : 'Adjust',
26
- filters: options.labels?.filters !== undefined ? options.labels.filters : 'Filters',
27
- reset: options.labels?.reset !== undefined ? options.labels.reset : 'Reset',
28
- save: options.labels?.save !== undefined ? options.labels.save : 'Save'
29
- }
30
- };
31
-
32
- this.canvas = null;
33
- this.ctx = null;
34
- this.originalImage = null;
35
- this.currentImage = null;
36
- this.history = [];
37
- this.currentState = {
38
- brightness: 100,
39
- contrast: 100,
40
- saturation: 100,
41
- rotation: 0,
42
- flipH: false,
43
- flipV: false,
44
- filter: 'none'
45
- };
46
- this.eventListeners = {};
47
-
48
- this.init();
49
- }
50
-
51
- init() {
52
- this.createUI();
53
- if (this.options.imageUrl) {
54
- this.loadImage(this.options.imageUrl);
55
- }
56
- }
57
-
58
- createUI() {
59
- // Clear container
60
- this.container.innerHTML = '';
61
- this.container.classList.add('swp-container');
62
-
63
- // Create main structure
64
- const wrapper = document.createElement('div');
65
- wrapper.className = 'swp-wrapper';
66
-
67
- // Create toolbar
68
- const toolbar = document.createElement('div');
69
- toolbar.className = 'swp-toolbar';
70
- toolbar.innerHTML = this.createToolbarHTML();
71
-
72
- // Create layout container
73
- const layoutContainer = document.createElement('div');
74
- layoutContainer.className = 'swp-layout-container';
75
-
76
- // Create control container
77
- const controlContainer = document.createElement('div');
78
- controlContainer.className = 'swp-control-container';
79
-
80
- // Create canvas container
81
- const canvasContainer = document.createElement('div');
82
- canvasContainer.className = 'swp-canvas-container';
83
-
84
- // Create canvas
85
- this.canvas = document.createElement('canvas');
86
- this.canvas.width = this.options.width;
87
- this.canvas.height = this.options.height;
88
- this.canvas.className = 'swp-canvas';
89
- this.ctx = this.canvas.getContext('2d');
90
-
91
- canvasContainer.appendChild(this.canvas);
92
-
93
- // Create adjustments panel
94
- const adjustmentsPanel = document.createElement('div');
95
- adjustmentsPanel.className = 'swp-adjustments-panel';
96
- adjustmentsPanel.innerHTML = this.createAdjustmentsPanelHTML();
97
-
98
- // Create filters panel
99
- const filtersPanel = document.createElement('div');
100
- filtersPanel.className = 'swp-filters-panel';
101
- filtersPanel.innerHTML = this.createFiltersPanelHTML();
102
-
103
- // Create resize panel
104
- const resizePanel = document.createElement('div');
105
- resizePanel.className = 'swp-resize-panel';
106
- resizePanel.innerHTML = this.createResizePanelHTML();
107
-
108
- // Append elements
109
- wrapper.appendChild(toolbar);
110
- wrapper.appendChild(layoutContainer);
111
-
112
- layoutContainer.appendChild(canvasContainer);
113
- layoutContainer.appendChild(controlContainer);
114
-
115
- controlContainer.appendChild(adjustmentsPanel);
116
- controlContainer.appendChild(filtersPanel);
117
- controlContainer.appendChild(resizePanel);
118
- this.container.appendChild(wrapper);
119
-
120
- // Bind events
121
- this.bindEvents();
122
- }
123
-
124
- createToolbarHTML() {
125
- const { showIcons, showLabels, labels } = this.options;
126
-
127
- const createButton = (action, icon, label, title) => {
128
- const showIcon = showIcons ? `<span class="swp-icon"><i class="${icon}"></i></span>` : '';
129
- const showLabel = showLabels && label !== null ? `<span>${label}</span>` : '';
130
- return `<button class="swp-btn${!showLabel ? ' swp-btn-icon-only' : ''}" data-action="${action}" title="${title}">${showIcon}${showLabel}</button>`;
131
- };
132
-
133
- return `
134
- <div class="swp-toolbar-group">
135
- ${createButton('upload', 'fas fa-folder', labels.upload, 'Upload Image')}
136
- <input type="file" id="swp-file-input" accept="image/*" style="display: none;">
137
- </div>
138
-
139
- <div class="swp-toolbar-group" style="margin: 0px auto;">
140
- ${createButton('rotate-left', 'fas fa-undo', labels.rotateLeft, 'Rotate Left')}
141
- ${createButton('rotate-right', 'fas fa-redo', labels.rotateRight, 'Rotate Right')}
142
- ${createButton('flip-h', 'fas fa-arrows-alt-h', labels.flipH, 'Flip Horizontal')}
143
- ${createButton('flip-v', 'fas fa-arrows-alt-v', labels.flipV, 'Flip Vertical')}
144
- </div>
145
-
146
- <div class="swp-toolbar-group">
147
- ${createButton('reset', 'fas fa-history', labels.reset, 'Reset')}
148
- </div>
149
-
150
- <div class="swp-toolbar-group">
151
- ${createButton('toggle-resize', 'fas fa-expand-arrows-alt', labels.resize, 'Resize')}
152
- ${createButton('toggle-adjustments', 'fas fa-sliders-h', labels.adjust, 'Adjustments')}
153
- ${createButton('toggle-filters', 'fas fa-magic', labels.filters, 'Filters')}
154
- </div>
155
- <div class="swp-toolbar-group">
156
- <button class="swp-btn swp-btn-primary${!showLabels || labels.save === null ? ' swp-btn-icon-only' : ''}" data-action="download" title="Download">
157
- ${showIcons ? '<span class="swp-icon"><i class="fas fa-save"></i></span>' : ''}
158
- ${showLabels && labels.save !== null ? `<span>${labels.save}</span>` : ''}
159
- </button>
160
- </div>
161
- `;
162
- }
163
-
164
- createAdjustmentsPanelHTML() {
165
- return `
166
- <h3>Adjustments</h3>
167
- <div class="swp-adjustment">
168
- <label>Brightness</label>
169
- <input type="range" id="swp-brightness" min="0" max="200" value="100">
170
- <span class="swp-value">100%</span>
171
- </div>
172
- <div class="swp-adjustment">
173
- <label>Contrast</label>
174
- <input type="range" id="swp-contrast" min="0" max="200" value="100">
175
- <span class="swp-value">100%</span>
176
- </div>
177
- <div class="swp-adjustment">
178
- <label>Saturation</label>
179
- <input type="range" id="swp-saturation" min="0" max="200" value="100">
180
- <span class="swp-value">100%</span>
181
- </div>
182
- `;
183
- }
184
-
185
- createResizePanelHTML() {
186
- return `
187
- <h3>Resize Image</h3>
188
- <div class="swp-resize-controls">
189
- <div class="swp-adjustment">
190
- <label>Width (px)</label>
191
- <input type="number" id="swp-resize-width" min="1" max="5000" value="800">
192
- </div>
193
- <div class="swp-adjustment">
194
- <label>Height (px)</label>
195
- <input type="number" id="swp-resize-height" min="1" max="5000" value="600">
196
- </div>
197
- <div class="swp-adjustment">
198
- <label>
199
- <input type="checkbox" id="swp-maintain-ratio" checked>
200
- Maintain aspect ratio
201
- </label>
202
- </div>
203
- <button class="swp-btn swp-btn-primary" data-action="apply-resize">Apply Resize</button>
204
- </div>
205
- `;
206
- }
207
-
208
- createFiltersPanelHTML() {
209
- const filters = [
210
- { name: 'none', label: 'None' },
211
- { name: 'grayscale', label: 'Grayscale' },
212
- { name: 'sepia', label: 'Sepia' },
213
- { name: 'invert', label: 'Invert' },
214
- { name: 'blur', label: 'Blur' }
215
- ];
216
-
217
- return `
218
- <h3>Filters</h3>
219
- <div class="swp-filters-grid">
220
- ${filters.map(filter => `
221
- <button class="swp-filter-btn ${filter.name === 'none' ? 'active' : ''}"
222
- data-filter="${filter.name}">
223
- <span class="swp-filter-preview" data-filter="${filter.name}"></span>
224
- <span>${filter.label}</span>
225
- </button>
226
- `).join('')}
227
- </div>
228
- `;
229
- }
230
-
231
- bindEvents() {
232
- // Toolbar buttons
233
- this.container.querySelectorAll('[data-action]').forEach(btn => {
234
- btn.addEventListener('click', (e) => {
235
- const action = e.currentTarget.getAttribute('data-action');
236
- this.handleAction(action);
237
- });
238
- });
239
-
240
- // File input
241
- const fileInput = this.container.querySelector('#swp-file-input');
242
- if (fileInput) {
243
- fileInput.addEventListener('change', (e) => {
244
- const file = e.target.files[0];
245
- if (file) {
246
- const reader = new FileReader();
247
- reader.onload = (event) => {
248
- this.loadImage(event.target.result);
249
- };
250
- reader.readAsDataURL(file);
251
- }
252
- });
253
- }
254
-
255
- // Adjustment sliders
256
- ['brightness', 'contrast', 'saturation'].forEach(adj => {
257
- const slider = this.container.querySelector(`#swp-${adj}`);
258
- if (slider) {
259
- slider.addEventListener('input', (e) => {
260
- const value = parseInt(e.target.value);
261
- e.target.nextElementSibling.textContent = value + '%';
262
- this.setAdjustment(adj, value);
263
- });
264
- }
265
- });
266
-
267
- // Filter buttons
268
- this.container.querySelectorAll('[data-filter]').forEach(btn => {
269
- if (btn.classList.contains('swp-filter-btn')) {
270
- btn.addEventListener('click', (e) => {
271
- const filter = e.currentTarget.getAttribute('data-filter');
272
- this.applyFilter(filter);
273
-
274
- // Update active state
275
- this.container.querySelectorAll('.swp-filter-btn').forEach(b => {
276
- b.classList.remove('active');
277
- });
278
- e.currentTarget.classList.add('active');
279
- });
280
- }
281
- });
282
-
283
- }
284
-
285
- bindResizeInputs() {
286
- const widthInput = this.container.querySelector('#swp-resize-width');
287
- const heightInput = this.container.querySelector('#swp-resize-height');
288
- const maintainRatio = this.container.querySelector('#swp-maintain-ratio');
289
-
290
- if (!widthInput || !heightInput || !this.currentImage) return;
291
-
292
- // Remove old event listeners by cloning and replacing
293
- const newWidthInput = widthInput.cloneNode(true);
294
- const newHeightInput = heightInput.cloneNode(true);
295
- widthInput.parentNode.replaceChild(newWidthInput, widthInput);
296
- heightInput.parentNode.replaceChild(newHeightInput, heightInput);
297
-
298
- const aspectRatio = this.currentImage.width / this.currentImage.height;
299
-
300
- // Real-time width adjustment with aspect ratio
301
- newWidthInput.addEventListener('input', (e) => {
302
- const newWidth = parseInt(e.target.value);
303
-
304
- if (maintainRatio.checked) {
305
- const newHeight = Math.round(newWidth / aspectRatio);
306
- newHeightInput.value = newHeight;
307
- }
308
-
309
- // Real-time preview
310
- this.updateCanvasSize(parseInt(newWidthInput.value), parseInt(newHeightInput.value));
311
- });
312
-
313
- // Real-time height adjustment with aspect ratio
314
- newHeightInput.addEventListener('input', (e) => {
315
- const newHeight = parseInt(e.target.value);
316
-
317
- if (maintainRatio.checked) {
318
- const newWidth = Math.round(newHeight * aspectRatio);
319
- newWidthInput.value = newWidth;
320
- }
321
-
322
- // Real-time preview
323
- this.updateCanvasSize(parseInt(newWidthInput.value), parseInt(newHeightInput.value));
324
- });
325
- }
326
-
327
- updateCanvasSize(width, height) {
328
- if (!this.currentImage || !width || !height || width < 1 || height < 1) return;
329
-
330
- // Update canvas dimensions
331
- this.canvas.width = width;
332
- this.canvas.height = height;
333
-
334
- // Redraw image at new size
335
- this.drawImage();
336
- }
337
-
338
- handleAction(action) {
339
- switch(action) {
340
- case 'upload':
341
- this.container.querySelector('#swp-file-input').click();
342
- break;
343
- case 'rotate-left':
344
- this.rotate(-90);
345
- break;
346
- case 'rotate-right':
347
- this.rotate(90);
348
- break;
349
- case 'flip-h':
350
- this.flip('horizontal');
351
- break;
352
- case 'flip-v':
353
- this.flip('vertical');
354
- break;
355
- case 'toggle-adjustments':
356
- this.togglePanel('.swp-adjustments-panel');
357
- break;
358
- case 'toggle-filters':
359
- this.togglePanel('.swp-filters-panel');
360
- break;
361
- case 'toggle-resize':
362
- this.togglePanel('.swp-resize-panel');
363
- break;
364
- case 'apply-resize':
365
- this.applyResize();
366
- break;
367
- case 'reset':
368
- this.reset();
369
- break;
370
- case 'download':
371
- this.download();
372
- break;
373
- }
374
- }
375
-
376
- togglePanel(selector) {
377
- const panel = this.container.querySelector(selector);
378
- if (panel) {
379
- panel.classList.toggle('active');
380
-
381
- // Close other panels
382
- const panels = ['.swp-adjustments-panel', '.swp-filters-panel', '.swp-resize-panel'];
383
- panels.forEach(p => {
384
- if (p !== selector) {
385
- this.container.querySelector(p)?.classList.remove('active');
386
- }
387
- });
388
- }
389
- }
390
-
391
- loadImage(imageUrl) {
392
- const img = new Image();
393
- img.crossOrigin = 'anonymous';
394
-
395
- img.onload = () => {
396
- this.originalImage = img;
397
- this.currentImage = img;
398
-
399
- // Set canvas to exact image dimensions
400
- this.canvas.width = img.width;
401
- this.canvas.height = img.height;
402
-
403
- // Update resize inputs
404
- const widthInput = this.container.querySelector('#swp-resize-width');
405
- const heightInput = this.container.querySelector('#swp-resize-height');
406
- if (widthInput) widthInput.value = img.width;
407
- if (heightInput) heightInput.value = img.height;
408
-
409
- // Bind resize inputs with aspect ratio
410
- this.bindResizeInputs();
411
-
412
- this.drawImage();
413
- this.emit('load');
414
- };
415
-
416
- img.onerror = () => {
417
- console.error('Failed to load image');
418
- };
419
-
420
- img.src = imageUrl;
421
- }
422
-
423
- drawImage() {
424
- if (!this.currentImage) return;
425
-
426
- const canvas = this.canvas;
427
- const ctx = this.ctx;
428
-
429
- // Clear canvas
430
- ctx.clearRect(0, 0, canvas.width, canvas.height);
431
-
432
- // Apply transformations
433
- ctx.save();
434
-
435
- // Move to center for transformations
436
- ctx.translate(canvas.width / 2, canvas.height / 2);
437
-
438
- // Apply rotation
439
- ctx.rotate(this.currentState.rotation * Math.PI / 180);
440
-
441
- // Apply flips
442
- ctx.scale(
443
- this.currentState.flipH ? -1 : 1,
444
- this.currentState.flipV ? -1 : 1
445
- );
446
-
447
- // Apply CSS filters
448
- ctx.filter = this.getFilterString();
449
-
450
- // Draw image at actual size (canvas matches image dimensions)
451
- ctx.drawImage(
452
- this.currentImage,
453
- -canvas.width / 2,
454
- -canvas.height / 2,
455
- canvas.width,
456
- canvas.height
457
- );
458
-
459
- ctx.restore();
460
-
461
- this.emit('change');
462
- }
463
-
464
- getFilterString() {
465
- let filters = [];
466
-
467
- if (this.currentState.brightness !== 100) {
468
- filters.push(`brightness(${this.currentState.brightness}%)`);
469
- }
470
- if (this.currentState.contrast !== 100) {
471
- filters.push(`contrast(${this.currentState.contrast}%)`);
472
- }
473
- if (this.currentState.saturation !== 100) {
474
- filters.push(`saturate(${this.currentState.saturation}%)`);
475
- }
476
-
477
- switch(this.currentState.filter) {
478
- case 'grayscale':
479
- filters.push('grayscale(100%)');
480
- break;
481
- case 'sepia':
482
- filters.push('sepia(100%)');
483
- break;
484
- case 'invert':
485
- filters.push('invert(100%)');
486
- break;
487
- case 'blur':
488
- filters.push('blur(5px)');
489
- break;
490
- }
491
-
492
- return filters.length > 0 ? filters.join(' ') : 'none';
493
- }
494
-
495
- rotate(degrees) {
496
- if (!this.currentImage) return;
497
-
498
- this.currentState.rotation = (this.currentState.rotation + degrees) % 360;
499
- this.drawImage();
500
- }
501
-
502
- flip(direction) {
503
- if (!this.currentImage) return;
504
-
505
- if (direction === 'horizontal') {
506
- this.currentState.flipH = !this.currentState.flipH;
507
- } else if (direction === 'vertical') {
508
- this.currentState.flipV = !this.currentState.flipV;
509
- }
510
-
511
- this.drawImage();
512
- }
513
-
514
- setAdjustment(adjustment, value) {
515
- if (!this.currentImage) return;
516
-
517
- this.currentState[adjustment] = value;
518
- this.drawImage();
519
- }
520
-
521
- applyFilter(filterName) {
522
- if (!this.currentImage) return;
523
-
524
- this.currentState.filter = filterName;
525
- this.drawImage();
526
- }
527
-
528
- applyResize() {
529
- if (!this.currentImage) return;
530
-
531
- const widthInput = this.container.querySelector('#swp-resize-width');
532
- const heightInput = this.container.querySelector('#swp-resize-height');
533
-
534
- const newWidth = parseInt(widthInput.value);
535
- const newHeight = parseInt(heightInput.value);
536
-
537
- if (!newWidth || !newHeight || newWidth < 1 || newHeight < 1) {
538
- alert('Please enter valid dimensions');
539
- return;
540
- }
541
-
542
- // Canvas size is already updated by real-time preview
543
- // Now we need to make the resize permanent by updating the currentImage
544
-
545
- // Create temporary canvas with current canvas content
546
- const tempCanvas = document.createElement('canvas');
547
- tempCanvas.width = this.canvas.width;
548
- tempCanvas.height = this.canvas.height;
549
- const tempCtx = tempCanvas.getContext('2d');
550
-
551
- // Copy current canvas content
552
- tempCtx.drawImage(this.canvas, 0, 0);
553
-
554
- // Load as new current image
555
- const resizedImage = new Image();
556
- resizedImage.onload = () => {
557
- this.currentImage = resizedImage;
558
- this.originalImage = resizedImage;
559
-
560
- // Rebind resize inputs with new aspect ratio
561
- this.bindResizeInputs();
562
-
563
- this.drawImage();
564
- };
565
- resizedImage.src = tempCanvas.toDataURL();
566
- }
567
-
568
- crop(x, y, width, height) {
569
- if (!this.currentImage) return;
570
-
571
- // Create temporary canvas for cropping
572
- const tempCanvas = document.createElement('canvas');
573
- tempCanvas.width = width;
574
- tempCanvas.height = height;
575
- const tempCtx = tempCanvas.getContext('2d');
576
-
577
- // Draw cropped area
578
- tempCtx.drawImage(this.canvas, x, y, width, height, 0, 0, width, height);
579
-
580
- // Load cropped image
581
- const croppedImage = new Image();
582
- croppedImage.onload = () => {
583
- this.currentImage = croppedImage;
584
- this.drawImage();
585
- };
586
- croppedImage.src = tempCanvas.toDataURL();
587
- }
588
-
589
- reset() {
590
- // Reset all states
591
- this.currentState = {
592
- brightness: 100,
593
- contrast: 100,
594
- saturation: 100,
595
- rotation: 0,
596
- flipH: false,
597
- flipV: false,
598
- filter: 'none'
599
- };
600
-
601
- // Reset sliders
602
- ['brightness', 'contrast', 'saturation'].forEach(adj => {
603
- const slider = this.container.querySelector(`#swp-${adj}`);
604
- if (slider) {
605
- slider.value = 100;
606
- slider.nextElementSibling.textContent = '100%';
607
- }
608
- });
609
-
610
- // Reset filter selection
611
- this.container.querySelectorAll('.swp-filter-btn').forEach(btn => {
612
- btn.classList.remove('active');
613
- if (btn.getAttribute('data-filter') === 'none') {
614
- btn.classList.add('active');
615
- }
616
- });
617
-
618
- // Reset image
619
- if (this.originalImage) {
620
- this.currentImage = this.originalImage;
621
- this.drawImage();
622
- }
623
- }
624
-
625
- getImageData(format = 'jpeg', quality = 0.9) {
626
- if (!this.canvas) return null;
627
-
628
- const mimeType = format === 'png' ? 'image/png' : 'image/jpeg';
629
- return this.canvas.toDataURL(mimeType, quality);
630
- }
631
-
632
- download() {
633
- const dataUrl = this.getImageData('png');
634
- if (!dataUrl) return;
635
-
636
- const link = document.createElement('a');
637
- link.download = `swp-edited-${Date.now()}.png`;
638
- link.href = dataUrl;
639
- link.click();
640
-
641
- this.emit('save');
642
- }
643
-
644
- // Event system
645
- on(event, callback) {
646
- if (!this.eventListeners[event]) {
647
- this.eventListeners[event] = [];
648
- }
649
- this.eventListeners[event].push(callback);
650
- }
651
-
652
- emit(event, data) {
653
- if (this.eventListeners[event]) {
654
- this.eventListeners[event].forEach(callback => {
655
- callback(data);
656
- });
657
- }
658
- }
22
+ constructor(container, options = {}) {
23
+ this.container = typeof container === 'string'
24
+ ? document.querySelector(container)
25
+ : container;
26
+
27
+ if (!this.container) {
28
+ throw new Error('SWP: Container element not found');
659
29
  }
660
30
 
661
- // Auto-initialize for declarative approach
662
- if (typeof document !== 'undefined') {
663
- document.addEventListener('DOMContentLoaded', () => {
664
- const declarativeContainers = document.querySelectorAll('[data-swp]');
665
- declarativeContainers.forEach(container => {
666
- // Parse data attributes for options
667
- const options = {};
668
-
669
- // Parse basic options
670
- if (container.dataset.swpImageUrl) {
671
- options.imageUrl = container.dataset.swpImageUrl;
672
- }
673
- if (container.dataset.swpWidth) {
674
- options.width = parseInt(container.dataset.swpWidth);
675
- }
676
- if (container.dataset.swpHeight) {
677
- options.height = parseInt(container.dataset.swpHeight);
678
- }
679
-
680
- // Parse boolean options
681
- if (container.dataset.swpShowIcons !== undefined) {
682
- options.showIcons = container.dataset.swpShowIcons !== 'false';
683
- }
684
- if (container.dataset.swpShowLabels !== undefined) {
685
- options.showLabels = container.dataset.swpShowLabels !== 'false';
686
- }
687
-
688
- // Parse labels object
689
- if (container.dataset.swpLabels) {
690
- try {
691
- // Support both JSON and simple key:value format
692
- let labelsStr = container.dataset.swpLabels.trim();
693
-
694
- // If it looks like JSON, parse as JSON
695
- if (labelsStr.startsWith('{')) {
696
- options.labels = JSON.parse(labelsStr);
697
- } else {
698
- // Parse simple format: "upload: 'text'; resize: 'text'"
699
- options.labels = {};
700
- const pairs = labelsStr.split(';');
701
- pairs.forEach(pair => {
702
- const [key, value] = pair.split(':').map(s => s.trim());
703
- if (key && value) {
704
- // Remove quotes and parse null
705
- const cleanValue = value.replace(/^['"]|['"]$/g, '');
706
- options.labels[key] = cleanValue === 'null' ? null : cleanValue;
707
- }
708
- });
709
- }
710
- } catch (e) {
711
- console.error('Failed to parse data-swp-labels:', e);
712
- }
713
- }
714
-
715
- new SWP(container, options);
716
- });
31
+ this.options = {
32
+ width: options.width || 1920,
33
+ height: options.height || 1080,
34
+ theme: options.theme || 'dark',
35
+ accentColor: options.accentColor || '#00FF99',
36
+ ...options
37
+ };
38
+
39
+ // Core systems
40
+ this.events = new EventEmitter();
41
+ this.canvas = new Canvas(this, { width: this.options.width, height: this.options.height });
42
+ this.history = new History(this);
43
+ this.keyboard = new Keyboard(this);
44
+
45
+ // Managers
46
+ this.layers = new LayerManager(this);
47
+ this.tools = new ToolManager(this);
48
+ this.selection = new Selection(this);
49
+ this.filters = new FilterManager(this);
50
+ this.colors = new ColorManager(this);
51
+ this.file = new FileManager(this);
52
+ this.clipboard = new Clipboard(this);
53
+
54
+ // UI
55
+ this.ui = new UI(this);
56
+
57
+ this.init();
58
+ }
59
+
60
+ init() {
61
+ // Initialize UI
62
+ this.ui.init(this.container);
63
+
64
+ // Apply theme class
65
+ this.applyTheme(this.options.theme);
66
+
67
+ // Apply accent color
68
+ this.applyAccentColor(this.options.accentColor);
69
+
70
+ // Initialize canvas in workspace
71
+ const workspace = this.ui.getWorkspace();
72
+ this.canvas.init(workspace);
73
+
74
+ // Bind tool events to canvas
75
+ this.tools.bindCanvasEvents(this.canvas.displayCanvas);
76
+
77
+ // Create initial document
78
+ this.file.newDocument({
79
+ width: this.options.width,
80
+ height: this.options.height
717
81
  });
82
+
83
+ // Update UI
84
+ this.ui.updateLayersPanel();
85
+ this.ui.updateHistoryPanel();
86
+ this.ui.updateToolbox();
87
+
88
+ // Emit ready
89
+ this.events.emit(Events.READY);
90
+ }
91
+
92
+ // Public API
93
+
94
+ loadImage(url) {
95
+ return new Promise((resolve, reject) => {
96
+ const img = new Image();
97
+ img.crossOrigin = 'anonymous';
98
+ img.onload = () => {
99
+ this.file.newDocument({ width: img.width, height: img.height });
100
+ const layer = this.layers.getActiveLayer();
101
+ if (layer) {
102
+ layer.ctx.drawImage(img, 0, 0);
103
+ this.canvas.render();
104
+ this.history.pushState('Load Image');
105
+ }
106
+ resolve();
107
+ };
108
+ img.onerror = reject;
109
+ img.src = url;
110
+ });
111
+ }
112
+
113
+ newDocument(width, height, background = '#ffffff') {
114
+ this.file.newDocument({ width, height, background });
115
+ }
116
+
117
+ getImageData(format = 'png', quality = 1) {
118
+ return this.canvas.toDataURL(`image/${format}`, quality);
119
+ }
120
+
121
+ export(format = 'png', quality = 1) {
122
+ return this.file.export(format, quality);
123
+ }
124
+
125
+ undo() {
126
+ return this.history.undo();
127
+ }
128
+
129
+ redo() {
130
+ return this.history.redo();
131
+ }
132
+
133
+ setTool(name) {
134
+ this.tools.setTool(name);
135
+ }
136
+
137
+ applyFilter(name, options) {
138
+ this.filters.applyFilter(name, options);
139
+ }
140
+
141
+ on(event, callback) {
142
+ return this.events.on(event, callback);
143
+ }
144
+
145
+ off(event, callback) {
146
+ this.events.off(event, callback);
147
+ }
148
+
149
+ cancelCurrentAction() {
150
+ this.filters.cancelPreview();
151
+ this.selection.clear();
152
+ }
153
+
154
+ confirmCurrentAction() {
155
+ // Confirm crop, text, etc.
156
+ const tool = this.tools.currentTool;
157
+ if (tool?.name === 'crop' && tool.applyCrop) {
158
+ tool.applyCrop();
159
+ }
160
+ if (tool?.name === 'text' && tool.commitText) {
161
+ tool.commitText();
162
+ }
163
+ }
164
+
165
+ applyTheme(theme) {
166
+ // The container itself has .swp-app class added by UI.init()
167
+ const app = this.container.classList.contains('swp-app')
168
+ ? this.container
169
+ : this.container.querySelector('.swp-app');
170
+ if (app) {
171
+ app.classList.remove('swp-theme-dark', 'swp-theme-light');
172
+ app.classList.add(`swp-theme-${theme}`);
173
+ }
174
+ this.options.theme = theme;
175
+ }
176
+
177
+ applyAccentColor(color) {
178
+ const app = this.container.classList.contains('swp-app')
179
+ ? this.container
180
+ : this.container.querySelector('.swp-app');
181
+ if (app && color) {
182
+ // Parse hex color to RGB for variations
183
+ const hex = color.replace('#', '');
184
+ const r = parseInt(hex.substring(0, 2), 16);
185
+ const g = parseInt(hex.substring(2, 4), 16);
186
+ const b = parseInt(hex.substring(4, 6), 16);
187
+
188
+ // Create lighter hover color (add 20% brightness)
189
+ const lighten = (val) => Math.min(255, Math.round(val + (255 - val) * 0.2));
190
+ const hoverR = lighten(r);
191
+ const hoverG = lighten(g);
192
+ const hoverB = lighten(b);
193
+ const hoverColor = `#${hoverR.toString(16).padStart(2, '0')}${hoverG.toString(16).padStart(2, '0')}${hoverB.toString(16).padStart(2, '0')}`;
194
+
195
+ // Calculate relative luminance for contrast (WCAG formula)
196
+ const toLinear = (val) => {
197
+ const sRGB = val / 255;
198
+ return sRGB <= 0.03928 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4);
199
+ };
200
+ const luminance = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
201
+
202
+ // Choose contrasting text color (black for light backgrounds, white for dark)
203
+ // Threshold 0.179 is the crossover point where black/white have equal contrast
204
+ const contrastColor = luminance > 0.179 ? '#000000' : '#ffffff';
205
+
206
+ // Apply CSS variables
207
+ app.style.setProperty('--swp-accent', color);
208
+ app.style.setProperty('--swp-accent-hover', hoverColor);
209
+ app.style.setProperty('--swp-accent-dim', `rgba(${r}, ${g}, ${b}, 0.2)`);
210
+ app.style.setProperty('--swp-accent-contrast', contrastColor);
211
+ }
212
+ this.options.accentColor = color;
213
+ }
214
+
215
+ setAccentColor(color) {
216
+ this.applyAccentColor(color);
217
+ this.events.emit(Events.CHANGE, { type: 'accentColor', color });
218
+ }
219
+
220
+ setTheme(theme) {
221
+ if (theme === 'light' || theme === 'dark') {
222
+ this.applyTheme(theme);
223
+ this.events.emit(Events.CHANGE, { type: 'theme', theme });
224
+ }
225
+ }
226
+
227
+ destroy() {
228
+ this.keyboard.destroy();
229
+ this.selection.destroy();
230
+ this.canvas.destroy();
231
+ this.container.innerHTML = '';
232
+ }
718
233
  }
719
234
 
235
+ // Static method to parse data attributes
236
+ SWP.parseDataAttributes = function(element) {
237
+ const options = {};
238
+
239
+ // Parse data-swp-* attributes
240
+ const dataset = element.dataset;
241
+
242
+ // Width
243
+ if (dataset.swpWidth) {
244
+ options.width = parseInt(dataset.swpWidth, 10);
245
+ }
246
+
247
+ // Height
248
+ if (dataset.swpHeight) {
249
+ options.height = parseInt(dataset.swpHeight, 10);
250
+ }
251
+
252
+ // Theme
253
+ if (dataset.swpTheme) {
254
+ options.theme = dataset.swpTheme;
255
+ }
256
+
257
+ // Any other data-swp-* attributes (convert kebab-case to camelCase)
258
+ for (const key in dataset) {
259
+ if (key.startsWith('swp') && key !== 'swp') {
260
+ // Remove 'swp' prefix and convert first char to lowercase
261
+ const optionKey = key.slice(3).charAt(0).toLowerCase() + key.slice(4);
262
+ if (!(optionKey in options)) {
263
+ // Try to parse as number or boolean
264
+ let value = dataset[key];
265
+ if (value === 'true') value = true;
266
+ else if (value === 'false') value = false;
267
+ else if (!isNaN(value) && value !== '') value = parseFloat(value);
268
+ options[optionKey] = value;
269
+ }
270
+ }
271
+ }
272
+
273
+ return options;
274
+ };
275
+
276
+ // Auto-initialize elements with data-swp attribute
277
+ SWP.autoInit = function() {
278
+ const elements = document.querySelectorAll('[data-swp]');
279
+ const instances = [];
280
+
281
+ elements.forEach(element => {
282
+ // Skip if already initialized
283
+ if (element.swpInstance) return;
284
+
285
+ const options = SWP.parseDataAttributes(element);
286
+ const instance = new SWP(element, options);
287
+ element.swpInstance = instance;
288
+ instances.push(instance);
289
+ });
290
+
291
+ return instances;
292
+ };
293
+
294
+ // Store all auto-initialized instances
295
+ SWP.instances = [];
296
+
720
297
  // Export
298
+ export { SWP, Events };
721
299
  export default SWP;
722
300
 
723
- // Also attach to window for non-module usage
301
+ // Global access
724
302
  if (typeof window !== 'undefined') {
725
- window.SWP = SWP;
303
+ window.SWP = SWP;
304
+
305
+ // Auto-init on DOMContentLoaded
306
+ if (document.readyState === 'loading') {
307
+ document.addEventListener('DOMContentLoaded', () => {
308
+ SWP.instances = SWP.autoInit();
309
+ });
310
+ } else {
311
+ // DOM already loaded
312
+ SWP.instances = SWP.autoInit();
313
+ }
726
314
  }