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