senangwebs-photobooth 1.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -235
- package/dist/swp.css +884 -344
- package/dist/swp.js +1 -1
- package/examples/data-attribute.html +69 -0
- package/examples/index.html +56 -51
- package/examples/studio.html +83 -0
- package/package.json +12 -5
- package/src/css/swp.css +884 -344
- package/src/js/core/Canvas.js +398 -0
- package/src/js/core/EventEmitter.js +188 -0
- package/src/js/core/History.js +250 -0
- package/src/js/core/Keyboard.js +323 -0
- package/src/js/filters/FilterManager.js +248 -0
- package/src/js/index.js +48 -0
- package/src/js/io/Clipboard.js +52 -0
- package/src/js/io/FileManager.js +150 -0
- package/src/js/layers/BlendModes.js +342 -0
- package/src/js/layers/Layer.js +415 -0
- package/src/js/layers/LayerManager.js +459 -0
- package/src/js/selection/Selection.js +167 -0
- package/src/js/swp.js +297 -709
- package/src/js/tools/BaseTool.js +264 -0
- package/src/js/tools/BrushTool.js +314 -0
- package/src/js/tools/CropTool.js +400 -0
- package/src/js/tools/EraserTool.js +155 -0
- package/src/js/tools/EyedropperTool.js +184 -0
- package/src/js/tools/FillTool.js +109 -0
- package/src/js/tools/GradientTool.js +141 -0
- package/src/js/tools/HandTool.js +51 -0
- package/src/js/tools/MarqueeTool.js +103 -0
- package/src/js/tools/MoveTool.js +465 -0
- package/src/js/tools/ShapeTool.js +285 -0
- package/src/js/tools/TextTool.js +253 -0
- package/src/js/tools/ToolManager.js +277 -0
- package/src/js/tools/ZoomTool.js +68 -0
- package/src/js/ui/ColorManager.js +71 -0
- package/src/js/ui/UI.js +1211 -0
- package/swp_preview1.png +0 -0
- package/swp_preview2.png +0 -0
- package/webpack.config.js +4 -11
- package/dist/styles.js +0 -1
- package/examples/customization.html +0 -360
- package/spec.md +0 -239
- package/swp_preview.png +0 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Studio - Blend Modes
|
|
3
|
+
* Custom blend mode implementations for pixel-level operations
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Blend mode functions
|
|
9
|
+
* Each function takes two values (base and blend) and returns the result
|
|
10
|
+
* Values are normalized to 0-1 range
|
|
11
|
+
*/
|
|
12
|
+
export const BlendModes = {
|
|
13
|
+
// Normal
|
|
14
|
+
normal: (base, blend) => blend,
|
|
15
|
+
|
|
16
|
+
// Darken modes
|
|
17
|
+
darken: (base, blend) => Math.min(base, blend),
|
|
18
|
+
|
|
19
|
+
multiply: (base, blend) => base * blend,
|
|
20
|
+
|
|
21
|
+
colorBurn: (base, blend) => {
|
|
22
|
+
if (blend === 0) return 0;
|
|
23
|
+
return Math.max(0, 1 - (1 - base) / blend);
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
linearBurn: (base, blend) => Math.max(0, base + blend - 1),
|
|
27
|
+
|
|
28
|
+
// Lighten modes
|
|
29
|
+
lighten: (base, blend) => Math.max(base, blend),
|
|
30
|
+
|
|
31
|
+
screen: (base, blend) => 1 - (1 - base) * (1 - blend),
|
|
32
|
+
|
|
33
|
+
colorDodge: (base, blend) => {
|
|
34
|
+
if (blend === 1) return 1;
|
|
35
|
+
return Math.min(1, base / (1 - blend));
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
linearDodge: (base, blend) => Math.min(1, base + blend),
|
|
39
|
+
|
|
40
|
+
// Contrast modes
|
|
41
|
+
overlay: (base, blend) => {
|
|
42
|
+
return base < 0.5
|
|
43
|
+
? 2 * base * blend
|
|
44
|
+
: 1 - 2 * (1 - base) * (1 - blend);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
softLight: (base, blend) => {
|
|
48
|
+
if (blend < 0.5) {
|
|
49
|
+
return base - (1 - 2 * blend) * base * (1 - base);
|
|
50
|
+
}
|
|
51
|
+
const d = base <= 0.25
|
|
52
|
+
? ((16 * base - 12) * base + 4) * base
|
|
53
|
+
: Math.sqrt(base);
|
|
54
|
+
return base + (2 * blend - 1) * (d - base);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
hardLight: (base, blend) => {
|
|
58
|
+
return blend < 0.5
|
|
59
|
+
? 2 * base * blend
|
|
60
|
+
: 1 - 2 * (1 - base) * (1 - blend);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
vividLight: (base, blend) => {
|
|
64
|
+
if (blend < 0.5) {
|
|
65
|
+
return blend === 0 ? 0 : Math.max(0, 1 - (1 - base) / (2 * blend));
|
|
66
|
+
}
|
|
67
|
+
return blend === 1 ? 1 : Math.min(1, base / (2 * (1 - blend)));
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
linearLight: (base, blend) => {
|
|
71
|
+
return Math.max(0, Math.min(1, base + 2 * blend - 1));
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
pinLight: (base, blend) => {
|
|
75
|
+
if (blend < 0.5) {
|
|
76
|
+
return Math.min(base, 2 * blend);
|
|
77
|
+
}
|
|
78
|
+
return Math.max(base, 2 * blend - 1);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
hardMix: (base, blend) => {
|
|
82
|
+
return (base + blend >= 1) ? 1 : 0;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Inversion modes
|
|
86
|
+
difference: (base, blend) => Math.abs(base - blend),
|
|
87
|
+
|
|
88
|
+
exclusion: (base, blend) => base + blend - 2 * base * blend,
|
|
89
|
+
|
|
90
|
+
subtract: (base, blend) => Math.max(0, base - blend),
|
|
91
|
+
|
|
92
|
+
divide: (base, blend) => {
|
|
93
|
+
if (blend === 0) return 1;
|
|
94
|
+
return Math.min(1, base / blend);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Component modes (require special handling for HSL)
|
|
98
|
+
hue: null, // Handled separately
|
|
99
|
+
saturation: null,
|
|
100
|
+
color: null,
|
|
101
|
+
luminosity: null
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Apply blend mode to two image data arrays
|
|
106
|
+
* @param {ImageData} baseData - Base image data
|
|
107
|
+
* @param {ImageData} blendData - Blend image data
|
|
108
|
+
* @param {string} mode - Blend mode name
|
|
109
|
+
* @param {number} opacity - Opacity (0-1)
|
|
110
|
+
* @returns {ImageData}
|
|
111
|
+
*/
|
|
112
|
+
export function applyBlendMode(baseData, blendData, mode, opacity = 1) {
|
|
113
|
+
const base = baseData.data;
|
|
114
|
+
const blend = blendData.data;
|
|
115
|
+
const result = new Uint8ClampedArray(base.length);
|
|
116
|
+
|
|
117
|
+
const blendFn = BlendModes[mode];
|
|
118
|
+
|
|
119
|
+
if (!blendFn) {
|
|
120
|
+
// Handle HSL-based blend modes
|
|
121
|
+
if (['hue', 'saturation', 'color', 'luminosity'].includes(mode)) {
|
|
122
|
+
return applyHSLBlendMode(baseData, blendData, mode, opacity);
|
|
123
|
+
}
|
|
124
|
+
// Default to normal
|
|
125
|
+
for (let i = 0; i < base.length; i += 4) {
|
|
126
|
+
const alpha = (blend[i + 3] / 255) * opacity;
|
|
127
|
+
result[i] = base[i] * (1 - alpha) + blend[i] * alpha;
|
|
128
|
+
result[i + 1] = base[i + 1] * (1 - alpha) + blend[i + 1] * alpha;
|
|
129
|
+
result[i + 2] = base[i + 2] * (1 - alpha) + blend[i + 2] * alpha;
|
|
130
|
+
result[i + 3] = Math.min(255, base[i + 3] + blend[i + 3] * opacity);
|
|
131
|
+
}
|
|
132
|
+
return new ImageData(result, baseData.width, baseData.height);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < base.length; i += 4) {
|
|
136
|
+
const blendAlpha = (blend[i + 3] / 255) * opacity;
|
|
137
|
+
|
|
138
|
+
if (blendAlpha === 0) {
|
|
139
|
+
result[i] = base[i];
|
|
140
|
+
result[i + 1] = base[i + 1];
|
|
141
|
+
result[i + 2] = base[i + 2];
|
|
142
|
+
result[i + 3] = base[i + 3];
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Apply blend function to each channel
|
|
147
|
+
for (let c = 0; c < 3; c++) {
|
|
148
|
+
const baseVal = base[i + c] / 255;
|
|
149
|
+
const blendVal = blend[i + c] / 255;
|
|
150
|
+
const blended = blendFn(baseVal, blendVal);
|
|
151
|
+
|
|
152
|
+
// Mix based on blend alpha
|
|
153
|
+
const mixed = baseVal * (1 - blendAlpha) + blended * blendAlpha;
|
|
154
|
+
result[i + c] = Math.round(mixed * 255);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Combine alpha
|
|
158
|
+
result[i + 3] = Math.min(255, base[i + 3] + blend[i + 3] * opacity);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return new ImageData(result, baseData.width, baseData.height);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Apply HSL-based blend mode
|
|
166
|
+
* @param {ImageData} baseData - Base image data
|
|
167
|
+
* @param {ImageData} blendData - Blend image data
|
|
168
|
+
* @param {string} mode - Blend mode (hue, saturation, color, luminosity)
|
|
169
|
+
* @param {number} opacity - Opacity (0-1)
|
|
170
|
+
* @returns {ImageData}
|
|
171
|
+
*/
|
|
172
|
+
function applyHSLBlendMode(baseData, blendData, mode, opacity) {
|
|
173
|
+
const base = baseData.data;
|
|
174
|
+
const blend = blendData.data;
|
|
175
|
+
const result = new Uint8ClampedArray(base.length);
|
|
176
|
+
|
|
177
|
+
for (let i = 0; i < base.length; i += 4) {
|
|
178
|
+
const blendAlpha = (blend[i + 3] / 255) * opacity;
|
|
179
|
+
|
|
180
|
+
if (blendAlpha === 0) {
|
|
181
|
+
result[i] = base[i];
|
|
182
|
+
result[i + 1] = base[i + 1];
|
|
183
|
+
result[i + 2] = base[i + 2];
|
|
184
|
+
result[i + 3] = base[i + 3];
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const baseHSL = rgbToHsl(base[i], base[i + 1], base[i + 2]);
|
|
189
|
+
const blendHSL = rgbToHsl(blend[i], blend[i + 1], blend[i + 2]);
|
|
190
|
+
|
|
191
|
+
let resultHSL;
|
|
192
|
+
switch (mode) {
|
|
193
|
+
case 'hue':
|
|
194
|
+
resultHSL = [blendHSL[0], baseHSL[1], baseHSL[2]];
|
|
195
|
+
break;
|
|
196
|
+
case 'saturation':
|
|
197
|
+
resultHSL = [baseHSL[0], blendHSL[1], baseHSL[2]];
|
|
198
|
+
break;
|
|
199
|
+
case 'color':
|
|
200
|
+
resultHSL = [blendHSL[0], blendHSL[1], baseHSL[2]];
|
|
201
|
+
break;
|
|
202
|
+
case 'luminosity':
|
|
203
|
+
resultHSL = [baseHSL[0], baseHSL[1], blendHSL[2]];
|
|
204
|
+
break;
|
|
205
|
+
default:
|
|
206
|
+
resultHSL = baseHSL;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const rgb = hslToRgb(...resultHSL);
|
|
210
|
+
|
|
211
|
+
// Mix based on blend alpha
|
|
212
|
+
result[i] = Math.round(base[i] * (1 - blendAlpha) + rgb[0] * blendAlpha);
|
|
213
|
+
result[i + 1] = Math.round(base[i + 1] * (1 - blendAlpha) + rgb[1] * blendAlpha);
|
|
214
|
+
result[i + 2] = Math.round(base[i + 2] * (1 - blendAlpha) + rgb[2] * blendAlpha);
|
|
215
|
+
result[i + 3] = Math.min(255, base[i + 3] + blend[i + 3] * opacity);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return new ImageData(result, baseData.width, baseData.height);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Convert RGB to HSL
|
|
223
|
+
* @param {number} r - Red (0-255)
|
|
224
|
+
* @param {number} g - Green (0-255)
|
|
225
|
+
* @param {number} b - Blue (0-255)
|
|
226
|
+
* @returns {number[]} [h, s, l] (0-1 range)
|
|
227
|
+
*/
|
|
228
|
+
function rgbToHsl(r, g, b) {
|
|
229
|
+
r /= 255;
|
|
230
|
+
g /= 255;
|
|
231
|
+
b /= 255;
|
|
232
|
+
|
|
233
|
+
const max = Math.max(r, g, b);
|
|
234
|
+
const min = Math.min(r, g, b);
|
|
235
|
+
const l = (max + min) / 2;
|
|
236
|
+
|
|
237
|
+
if (max === min) {
|
|
238
|
+
return [0, 0, l];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const d = max - min;
|
|
242
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
243
|
+
|
|
244
|
+
let h;
|
|
245
|
+
switch (max) {
|
|
246
|
+
case r:
|
|
247
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
248
|
+
break;
|
|
249
|
+
case g:
|
|
250
|
+
h = ((b - r) / d + 2) / 6;
|
|
251
|
+
break;
|
|
252
|
+
case b:
|
|
253
|
+
h = ((r - g) / d + 4) / 6;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return [h, s, l];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Convert HSL to RGB
|
|
262
|
+
* @param {number} h - Hue (0-1)
|
|
263
|
+
* @param {number} s - Saturation (0-1)
|
|
264
|
+
* @param {number} l - Lightness (0-1)
|
|
265
|
+
* @returns {number[]} [r, g, b] (0-255 range)
|
|
266
|
+
*/
|
|
267
|
+
function hslToRgb(h, s, l) {
|
|
268
|
+
if (s === 0) {
|
|
269
|
+
const v = Math.round(l * 255);
|
|
270
|
+
return [v, v, v];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const hue2rgb = (p, q, t) => {
|
|
274
|
+
if (t < 0) t += 1;
|
|
275
|
+
if (t > 1) t -= 1;
|
|
276
|
+
if (t < 1/6) return p + (q - p) * 6 * t;
|
|
277
|
+
if (t < 1/2) return q;
|
|
278
|
+
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
|
279
|
+
return p;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
283
|
+
const p = 2 * l - q;
|
|
284
|
+
|
|
285
|
+
return [
|
|
286
|
+
Math.round(hue2rgb(p, q, h + 1/3) * 255),
|
|
287
|
+
Math.round(hue2rgb(p, q, h) * 255),
|
|
288
|
+
Math.round(hue2rgb(p, q, h - 1/3) * 255)
|
|
289
|
+
];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get list of available blend modes
|
|
294
|
+
* @returns {Object[]} Blend mode options
|
|
295
|
+
*/
|
|
296
|
+
export function getBlendModeList() {
|
|
297
|
+
return [
|
|
298
|
+
{ group: 'Normal', modes: ['normal'] },
|
|
299
|
+
{ group: 'Darken', modes: ['darken', 'multiply', 'colorBurn', 'linearBurn'] },
|
|
300
|
+
{ group: 'Lighten', modes: ['lighten', 'screen', 'colorDodge', 'linearDodge'] },
|
|
301
|
+
{ group: 'Contrast', modes: ['overlay', 'softLight', 'hardLight', 'vividLight', 'linearLight', 'pinLight', 'hardMix'] },
|
|
302
|
+
{ group: 'Inversion', modes: ['difference', 'exclusion', 'subtract', 'divide'] },
|
|
303
|
+
{ group: 'Component', modes: ['hue', 'saturation', 'color', 'luminosity'] }
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get human-readable blend mode name
|
|
309
|
+
* @param {string} mode - Blend mode key
|
|
310
|
+
* @returns {string} Human-readable name
|
|
311
|
+
*/
|
|
312
|
+
export function getBlendModeName(mode) {
|
|
313
|
+
const names = {
|
|
314
|
+
normal: 'Normal',
|
|
315
|
+
darken: 'Darken',
|
|
316
|
+
multiply: 'Multiply',
|
|
317
|
+
colorBurn: 'Color Burn',
|
|
318
|
+
linearBurn: 'Linear Burn',
|
|
319
|
+
lighten: 'Lighten',
|
|
320
|
+
screen: 'Screen',
|
|
321
|
+
colorDodge: 'Color Dodge',
|
|
322
|
+
linearDodge: 'Linear Dodge (Add)',
|
|
323
|
+
overlay: 'Overlay',
|
|
324
|
+
softLight: 'Soft Light',
|
|
325
|
+
hardLight: 'Hard Light',
|
|
326
|
+
vividLight: 'Vivid Light',
|
|
327
|
+
linearLight: 'Linear Light',
|
|
328
|
+
pinLight: 'Pin Light',
|
|
329
|
+
hardMix: 'Hard Mix',
|
|
330
|
+
difference: 'Difference',
|
|
331
|
+
exclusion: 'Exclusion',
|
|
332
|
+
subtract: 'Subtract',
|
|
333
|
+
divide: 'Divide',
|
|
334
|
+
hue: 'Hue',
|
|
335
|
+
saturation: 'Saturation',
|
|
336
|
+
color: 'Color',
|
|
337
|
+
luminosity: 'Luminosity'
|
|
338
|
+
};
|
|
339
|
+
return names[mode] || mode;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export default BlendModes;
|