senangwebs-photobooth 1.0.1 → 1.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.
- package/README.md +72 -72
- package/dist/swp.css +261 -255
- package/dist/swp.js +1 -1
- package/package.json +4 -1
- package/src/css/swp.css +261 -255
- package/src/js/swp.js +713 -611
- package/swp_preview.png +0 -0
- package/spec.md +0 -239
package/src/js/swp.js
CHANGED
|
@@ -1,168 +1,252 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SenangWebs Photobooth (SWP)
|
|
3
3
|
* A lightweight client-side photo editing library
|
|
4
|
-
* @version 1.0.
|
|
4
|
+
* @version 1.0.2
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import "../css/swp.css";
|
|
8
|
+
import "@bookklik/senangstart-icons/dist/senangstart-icon.min.js";
|
|
8
9
|
|
|
9
10
|
class SWP {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
11
|
+
constructor(container, options = {}) {
|
|
12
|
+
this.container = container;
|
|
13
|
+
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
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
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 = {};
|
|
69
|
+
|
|
70
|
+
this.init();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
init() {
|
|
74
|
+
this.createUI();
|
|
75
|
+
if (this.options.imageUrl) {
|
|
76
|
+
this.loadImage(this.options.imageUrl);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
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();
|
|
144
|
+
}
|
|
145
|
+
|
|
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 `
|
|
134
161
|
<div class="swp-toolbar-group">
|
|
135
|
-
${createButton(
|
|
162
|
+
${createButton(
|
|
163
|
+
"upload",
|
|
164
|
+
"folder",
|
|
165
|
+
labels.upload,
|
|
166
|
+
"Upload Image"
|
|
167
|
+
)}
|
|
136
168
|
<input type="file" id="swp-file-input" accept="image/*" style="display: none;">
|
|
137
169
|
</div>
|
|
138
170
|
|
|
139
171
|
<div class="swp-toolbar-group" style="margin: 0px auto;">
|
|
140
|
-
${createButton(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
)}
|
|
144
196
|
</div>
|
|
145
197
|
|
|
146
198
|
<div class="swp-toolbar-group">
|
|
147
|
-
${createButton(
|
|
199
|
+
${createButton(
|
|
200
|
+
"reset",
|
|
201
|
+
"time-reset",
|
|
202
|
+
labels.reset,
|
|
203
|
+
"Reset"
|
|
204
|
+
)}
|
|
148
205
|
</div>
|
|
149
206
|
|
|
150
207
|
<div class="swp-toolbar-group">
|
|
151
|
-
${createButton(
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
)}
|
|
154
226
|
</div>
|
|
155
227
|
<div class="swp-toolbar-group">
|
|
156
|
-
<button class="swp-btn swp-btn-primary${
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
}
|
|
159
243
|
</button>
|
|
160
244
|
</div>
|
|
161
245
|
`;
|
|
162
|
-
|
|
246
|
+
}
|
|
163
247
|
|
|
164
|
-
|
|
165
|
-
|
|
248
|
+
createAdjustmentsPanelHTML() {
|
|
249
|
+
return `
|
|
166
250
|
<h3>Adjustments</h3>
|
|
167
251
|
<div class="swp-adjustment">
|
|
168
252
|
<label>Brightness</label>
|
|
@@ -180,10 +264,10 @@ class SWP {
|
|
|
180
264
|
<span class="swp-value">100%</span>
|
|
181
265
|
</div>
|
|
182
266
|
`;
|
|
183
|
-
|
|
267
|
+
}
|
|
184
268
|
|
|
185
|
-
|
|
186
|
-
|
|
269
|
+
createResizePanelHTML() {
|
|
270
|
+
return `
|
|
187
271
|
<h3>Resize Image</h3>
|
|
188
272
|
<div class="swp-resize-controls">
|
|
189
273
|
<div class="swp-adjustment">
|
|
@@ -203,524 +287,542 @@ class SWP {
|
|
|
203
287
|
<button class="swp-btn swp-btn-primary" data-action="apply-resize">Apply Resize</button>
|
|
204
288
|
</div>
|
|
205
289
|
`;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
290
|
+
}
|
|
291
|
+
|
|
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 `
|
|
218
302
|
<h3>Filters</h3>
|
|
219
303
|
<div class="swp-filters-grid">
|
|
220
|
-
${filters
|
|
221
|
-
|
|
304
|
+
${filters
|
|
305
|
+
.map(
|
|
306
|
+
(filter) => `
|
|
307
|
+
<button class="swp-filter-btn ${
|
|
308
|
+
filter.name === "none" ? "active" : ""
|
|
309
|
+
}"
|
|
222
310
|
data-filter="${filter.name}">
|
|
223
|
-
<span class="swp-filter-preview" data-filter="${
|
|
311
|
+
<span class="swp-filter-preview" data-filter="${
|
|
312
|
+
filter.name
|
|
313
|
+
}"></span>
|
|
224
314
|
<span>${filter.label}</span>
|
|
225
315
|
</button>
|
|
226
|
-
`
|
|
316
|
+
`
|
|
317
|
+
)
|
|
318
|
+
.join("")}
|
|
227
319
|
</div>
|
|
228
320
|
`;
|
|
321
|
+
}
|
|
322
|
+
|
|
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);
|
|
229
343
|
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
230
346
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
});
|
|
239
358
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
});
|
|
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
|
+
}
|
|
266
375
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
});
|
|
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");
|
|
282
380
|
|
|
283
|
-
|
|
381
|
+
if (!widthInput || !heightInput || !this.currentImage) return;
|
|
284
382
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
}
|
|
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);
|
|
326
388
|
|
|
327
|
-
|
|
328
|
-
if (!this.currentImage || !width || !height || width < 1 || height < 1) return;
|
|
389
|
+
const aspectRatio = this.currentImage.width / this.currentImage.height;
|
|
329
390
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
// Redraw image at new size
|
|
335
|
-
this.drawImage();
|
|
336
|
-
}
|
|
391
|
+
// Real-time width adjustment with aspect ratio
|
|
392
|
+
newWidthInput.addEventListener("input", (e) => {
|
|
393
|
+
const newWidth = parseInt(e.target.value);
|
|
337
394
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
}
|
|
395
|
+
if (maintainRatio.checked) {
|
|
396
|
+
const newHeight = Math.round(newWidth / aspectRatio);
|
|
397
|
+
newHeightInput.value = newHeight;
|
|
398
|
+
}
|
|
375
399
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
}
|
|
400
|
+
// Real-time preview
|
|
401
|
+
this.updateCanvasSize(
|
|
402
|
+
parseInt(newWidthInput.value),
|
|
403
|
+
parseInt(newHeightInput.value)
|
|
404
|
+
);
|
|
405
|
+
});
|
|
390
406
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
};
|
|
407
|
+
// Real-time height adjustment with aspect ratio
|
|
408
|
+
newHeightInput.addEventListener("input", (e) => {
|
|
409
|
+
const newHeight = parseInt(e.target.value);
|
|
415
410
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
411
|
+
if (maintainRatio.checked) {
|
|
412
|
+
const newWidth = Math.round(newHeight * aspectRatio);
|
|
413
|
+
newWidthInput.value = newWidth;
|
|
414
|
+
}
|
|
419
415
|
|
|
420
|
-
|
|
416
|
+
// Real-time preview
|
|
417
|
+
this.updateCanvasSize(
|
|
418
|
+
parseInt(newWidthInput.value),
|
|
419
|
+
parseInt(newHeightInput.value)
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
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();
|
|
434
|
+
}
|
|
435
|
+
|
|
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
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
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");
|
|
421
488
|
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
422
492
|
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
}
|
|
493
|
+
loadImage(imageUrl) {
|
|
494
|
+
const img = new Image();
|
|
495
|
+
img.crossOrigin = "anonymous";
|
|
463
496
|
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
}
|
|
497
|
+
img.onload = () => {
|
|
498
|
+
this.originalImage = img;
|
|
499
|
+
this.currentImage = img;
|
|
494
500
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
this.currentState.rotation = (this.currentState.rotation + degrees) % 360;
|
|
499
|
-
this.drawImage();
|
|
500
|
-
}
|
|
501
|
+
// Set canvas to exact image dimensions
|
|
502
|
+
this.canvas.width = img.width;
|
|
503
|
+
this.canvas.height = img.height;
|
|
501
504
|
|
|
502
|
-
|
|
503
|
-
|
|
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;
|
|
504
510
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
} else if (direction === 'vertical') {
|
|
508
|
-
this.currentState.flipV = !this.currentState.flipV;
|
|
509
|
-
}
|
|
511
|
+
// Bind resize inputs with aspect ratio
|
|
512
|
+
this.bindResizeInputs();
|
|
510
513
|
|
|
511
|
-
|
|
512
|
-
|
|
514
|
+
this.drawImage();
|
|
515
|
+
this.emit("load");
|
|
516
|
+
};
|
|
513
517
|
|
|
514
|
-
|
|
515
|
-
|
|
518
|
+
img.onerror = () => {
|
|
519
|
+
console.error("Failed to load image");
|
|
520
|
+
};
|
|
516
521
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
522
|
+
img.src = imageUrl;
|
|
523
|
+
}
|
|
520
524
|
|
|
521
|
-
|
|
522
|
-
|
|
525
|
+
drawImage() {
|
|
526
|
+
if (!this.currentImage) return;
|
|
523
527
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
528
|
+
const canvas = this.canvas;
|
|
529
|
+
const ctx = this.ctx;
|
|
527
530
|
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
}
|
|
531
|
+
// Clear canvas
|
|
532
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
567
533
|
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
}
|
|
534
|
+
// Apply transformations
|
|
535
|
+
ctx.save();
|
|
588
536
|
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
});
|
|
537
|
+
// Move to center for transformations
|
|
538
|
+
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
609
539
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
btn.classList.remove('active');
|
|
613
|
-
if (btn.getAttribute('data-filter') === 'none') {
|
|
614
|
-
btn.classList.add('active');
|
|
615
|
-
}
|
|
616
|
-
});
|
|
540
|
+
// Apply rotation
|
|
541
|
+
ctx.rotate((this.currentState.rotation * Math.PI) / 180);
|
|
617
542
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
543
|
+
// Apply flips
|
|
544
|
+
ctx.scale(
|
|
545
|
+
this.currentState.flipH ? -1 : 1,
|
|
546
|
+
this.currentState.flipV ? -1 : 1
|
|
547
|
+
);
|
|
624
548
|
|
|
625
|
-
|
|
626
|
-
|
|
549
|
+
// Apply CSS filters
|
|
550
|
+
ctx.filter = this.getFilterString();
|
|
627
551
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
+
);
|
|
631
560
|
|
|
632
|
-
|
|
633
|
-
const dataUrl = this.getImageData('png');
|
|
634
|
-
if (!dataUrl) return;
|
|
561
|
+
ctx.restore();
|
|
635
562
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
link.href = dataUrl;
|
|
639
|
-
link.click();
|
|
563
|
+
this.emit("change");
|
|
564
|
+
}
|
|
640
565
|
|
|
641
|
-
|
|
642
|
-
|
|
566
|
+
getFilterString() {
|
|
567
|
+
let filters = [];
|
|
643
568
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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}%)`);
|
|
574
|
+
}
|
|
575
|
+
if (this.currentState.saturation !== 100) {
|
|
576
|
+
filters.push(`saturate(${this.currentState.saturation}%)`);
|
|
577
|
+
}
|
|
651
578
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return filters.length > 0 ? filters.join(" ") : "none";
|
|
595
|
+
}
|
|
596
|
+
|
|
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;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
this.drawImage();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
setAdjustment(adjustment, value) {
|
|
617
|
+
if (!this.currentImage) return;
|
|
618
|
+
|
|
619
|
+
this.currentState[adjustment] = value;
|
|
620
|
+
this.drawImage();
|
|
621
|
+
}
|
|
622
|
+
|
|
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;
|
|
642
|
+
}
|
|
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();
|
|
668
|
+
}
|
|
669
|
+
|
|
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();
|
|
689
|
+
}
|
|
690
|
+
|
|
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();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
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);
|
|
732
|
+
}
|
|
733
|
+
|
|
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");
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Event system
|
|
747
|
+
on(event, callback) {
|
|
748
|
+
if (!this.eventListeners[event]) {
|
|
749
|
+
this.eventListeners[event] = [];
|
|
659
750
|
}
|
|
751
|
+
this.eventListeners[event].push(callback);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
emit(event, data) {
|
|
755
|
+
if (this.eventListeners[event]) {
|
|
756
|
+
this.eventListeners[event].forEach((callback) => {
|
|
757
|
+
callback(data);
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
660
762
|
|
|
661
763
|
// Auto-initialize for declarative approach
|
|
662
|
-
if (typeof document !==
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
});
|
|
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
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
new SWP(container, options);
|
|
717
818
|
});
|
|
819
|
+
});
|
|
718
820
|
}
|
|
719
821
|
|
|
720
822
|
// Export
|
|
721
823
|
export default SWP;
|
|
722
824
|
|
|
723
825
|
// Also attach to window for non-module usage
|
|
724
|
-
if (typeof window !==
|
|
725
|
-
|
|
826
|
+
if (typeof window !== "undefined") {
|
|
827
|
+
window.SWP = SWP;
|
|
726
828
|
}
|