senangwebs-photobooth 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +252 -0
- package/LICENSE.md +21 -0
- package/README.md +297 -0
- package/dist/styles.js +1 -0
- package/dist/swp.css +448 -0
- package/dist/swp.js +1 -0
- package/examples/customization.html +360 -0
- package/examples/index.html +54 -0
- package/package.json +32 -0
- package/spec.md +239 -0
- package/src/css/swp.css +447 -0
- package/src/js/swp.js +726 -0
- package/swp_preview.png +0 -0
- package/webpack.config.js +39 -0
package/src/js/swp.js
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Photobooth (SWP)
|
|
3
|
+
* A lightweight client-side photo editing library
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import '../css/swp.css';
|
|
8
|
+
|
|
9
|
+
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
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
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
|
+
});
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Export
|
|
721
|
+
export default SWP;
|
|
722
|
+
|
|
723
|
+
// Also attach to window for non-module usage
|
|
724
|
+
if (typeof window !== 'undefined') {
|
|
725
|
+
window.SWP = SWP;
|
|
726
|
+
}
|