tokimeki-image-editor 0.1.0 → 0.1.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/dist/components/AdjustTool.svelte +317 -0
- package/dist/components/AdjustTool.svelte.d.ts +9 -0
- package/dist/components/BlurTool.svelte +613 -0
- package/dist/components/BlurTool.svelte.d.ts +15 -0
- package/dist/components/Canvas.svelte +214 -0
- package/dist/components/Canvas.svelte.d.ts +17 -0
- package/dist/components/CropTool.svelte +942 -0
- package/dist/components/CropTool.svelte.d.ts +14 -0
- package/dist/components/ExportTool.svelte +191 -0
- package/dist/components/ExportTool.svelte.d.ts +10 -0
- package/dist/components/FilterTool.svelte +492 -0
- package/dist/components/FilterTool.svelte.d.ts +12 -0
- package/dist/components/ImageEditor.svelte +735 -0
- package/dist/components/ImageEditor.svelte.d.ts +12 -0
- package/dist/components/RotateTool.svelte +157 -0
- package/dist/components/RotateTool.svelte.d.ts +9 -0
- package/dist/components/StampTool.svelte +678 -0
- package/dist/components/StampTool.svelte.d.ts +15 -0
- package/dist/components/Toolbar.svelte +136 -0
- package/dist/components/Toolbar.svelte.d.ts +10 -0
- package/dist/config/stamps.d.ts +2 -0
- package/dist/config/stamps.js +22 -0
- package/dist/i18n/index.d.ts +1 -0
- package/dist/i18n/index.js +9 -0
- package/dist/i18n/locales/en.json +68 -0
- package/dist/i18n/locales/ja.json +68 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +1 -0
- package/dist/utils/adjustments.d.ts +26 -0
- package/dist/utils/adjustments.js +525 -0
- package/dist/utils/canvas.d.ts +30 -0
- package/dist/utils/canvas.js +293 -0
- package/dist/utils/filters.d.ts +18 -0
- package/dist/utils/filters.js +114 -0
- package/dist/utils/history.d.ts +15 -0
- package/dist/utils/history.js +67 -0
- package/package.json +1 -1
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create default adjustments state (all values at 0 = no adjustment)
|
|
3
|
+
*/
|
|
4
|
+
export function createDefaultAdjustments() {
|
|
5
|
+
return {
|
|
6
|
+
exposure: 0,
|
|
7
|
+
contrast: 0,
|
|
8
|
+
highlights: 0,
|
|
9
|
+
shadows: 0,
|
|
10
|
+
brightness: 0,
|
|
11
|
+
saturation: 0,
|
|
12
|
+
hue: 0,
|
|
13
|
+
vignette: 0,
|
|
14
|
+
sepia: 0,
|
|
15
|
+
grayscale: 0
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* RGB to HSL color space conversion
|
|
20
|
+
*/
|
|
21
|
+
function rgbToHsl(r, g, b) {
|
|
22
|
+
r /= 255;
|
|
23
|
+
g /= 255;
|
|
24
|
+
b /= 255;
|
|
25
|
+
const max = Math.max(r, g, b);
|
|
26
|
+
const min = Math.min(r, g, b);
|
|
27
|
+
let h = 0;
|
|
28
|
+
let s = 0;
|
|
29
|
+
const l = (max + min) / 2;
|
|
30
|
+
if (max !== min) {
|
|
31
|
+
const d = max - min;
|
|
32
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
33
|
+
switch (max) {
|
|
34
|
+
case r:
|
|
35
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
36
|
+
break;
|
|
37
|
+
case g:
|
|
38
|
+
h = ((b - r) / d + 2) / 6;
|
|
39
|
+
break;
|
|
40
|
+
case b:
|
|
41
|
+
h = ((r - g) / d + 4) / 6;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [h * 360, s * 100, l * 100];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* HSL to RGB color space conversion
|
|
49
|
+
*/
|
|
50
|
+
function hslToRgb(h, s, l) {
|
|
51
|
+
h /= 360;
|
|
52
|
+
s /= 100;
|
|
53
|
+
l /= 100;
|
|
54
|
+
let r, g, b;
|
|
55
|
+
if (s === 0) {
|
|
56
|
+
r = g = b = l;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const hue2rgb = (p, q, t) => {
|
|
60
|
+
if (t < 0)
|
|
61
|
+
t += 1;
|
|
62
|
+
if (t > 1)
|
|
63
|
+
t -= 1;
|
|
64
|
+
if (t < 1 / 6)
|
|
65
|
+
return p + (q - p) * 6 * t;
|
|
66
|
+
if (t < 1 / 2)
|
|
67
|
+
return q;
|
|
68
|
+
if (t < 2 / 3)
|
|
69
|
+
return p + (q - p) * (2 / 3 - t) * 6;
|
|
70
|
+
return p;
|
|
71
|
+
};
|
|
72
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
73
|
+
const p = 2 * l - q;
|
|
74
|
+
r = hue2rgb(p, q, h + 1 / 3);
|
|
75
|
+
g = hue2rgb(p, q, h);
|
|
76
|
+
b = hue2rgb(p, q, h - 1 / 3);
|
|
77
|
+
}
|
|
78
|
+
return [r * 255, g * 255, b * 255];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Apply adjustments to a canvas context
|
|
82
|
+
* NOTE: For Safari compatibility, we don't use ctx.filter anymore.
|
|
83
|
+
* All adjustments are now applied via pixel manipulation in applyAllAdjustments.
|
|
84
|
+
*/
|
|
85
|
+
export function applyAdjustments(ctx, adjustments) {
|
|
86
|
+
// No-op: All adjustments are now handled via pixel manipulation
|
|
87
|
+
// This function is kept for backwards compatibility
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Apply ALL adjustments via pixel manipulation
|
|
91
|
+
* This works in all browsers including Safari (no ctx.filter needed)
|
|
92
|
+
*/
|
|
93
|
+
export function applyAllAdjustments(canvas, img, viewport, adjustments, cropArea) {
|
|
94
|
+
// Skip if no adjustments needed
|
|
95
|
+
if (adjustments.exposure === 0 &&
|
|
96
|
+
adjustments.contrast === 0 &&
|
|
97
|
+
adjustments.highlights === 0 &&
|
|
98
|
+
adjustments.shadows === 0 &&
|
|
99
|
+
adjustments.brightness === 0 &&
|
|
100
|
+
adjustments.saturation === 0 &&
|
|
101
|
+
adjustments.hue === 0 &&
|
|
102
|
+
adjustments.vignette === 0 &&
|
|
103
|
+
adjustments.sepia === 0 &&
|
|
104
|
+
adjustments.grayscale === 0) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const ctx = canvas.getContext('2d');
|
|
108
|
+
if (!ctx)
|
|
109
|
+
return;
|
|
110
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
111
|
+
const data = imageData.data;
|
|
112
|
+
// Pre-calculate adjustment factors
|
|
113
|
+
const hasExposure = adjustments.exposure !== 0;
|
|
114
|
+
const hasContrast = adjustments.contrast !== 0;
|
|
115
|
+
const hasHighlights = adjustments.highlights !== 0;
|
|
116
|
+
const hasShadows = adjustments.shadows !== 0;
|
|
117
|
+
const hasBrightness = adjustments.brightness !== 0;
|
|
118
|
+
const hasSaturation = adjustments.saturation !== 0;
|
|
119
|
+
const hasHue = adjustments.hue !== 0;
|
|
120
|
+
const hasVignette = adjustments.vignette !== 0;
|
|
121
|
+
const hasSepia = adjustments.sepia !== 0;
|
|
122
|
+
const hasGrayscale = adjustments.grayscale !== 0;
|
|
123
|
+
const exposureFactor = hasExposure ? Math.pow(2, adjustments.exposure / 100) : 1;
|
|
124
|
+
const contrastFactor = hasContrast ? 1 + (adjustments.contrast / 200) : 1;
|
|
125
|
+
const brightnessFactor = hasBrightness ? 1 + (adjustments.brightness / 200) : 1;
|
|
126
|
+
const highlightsFactor = adjustments.highlights / 100;
|
|
127
|
+
const shadowsFactor = adjustments.shadows / 100;
|
|
128
|
+
const saturationFactor = hasSaturation ? adjustments.saturation / 100 : 0;
|
|
129
|
+
const hueShift = adjustments.hue;
|
|
130
|
+
const sepiaAmount = adjustments.sepia / 100;
|
|
131
|
+
const grayscaleAmount = adjustments.grayscale / 100;
|
|
132
|
+
// Vignette pre-calculations
|
|
133
|
+
const imgWidth = cropArea ? cropArea.width : img.width;
|
|
134
|
+
const imgHeight = cropArea ? cropArea.height : img.height;
|
|
135
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
136
|
+
const imageCenterX = canvas.width / 2 + viewport.offsetX;
|
|
137
|
+
const imageCenterY = canvas.height / 2 + viewport.offsetY;
|
|
138
|
+
const scaledImageHalfWidth = (imgWidth * totalScale) / 2;
|
|
139
|
+
const scaledImageHalfHeight = (imgHeight * totalScale) / 2;
|
|
140
|
+
const maxDistanceSquared = scaledImageHalfWidth * scaledImageHalfWidth +
|
|
141
|
+
scaledImageHalfHeight * scaledImageHalfHeight;
|
|
142
|
+
const vignetteFactor = adjustments.vignette / 100;
|
|
143
|
+
const vignetteStrength = 1.5;
|
|
144
|
+
const needsLuminance = hasHighlights || hasShadows;
|
|
145
|
+
const needsHSL = hasSaturation || hasHue;
|
|
146
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
147
|
+
let r = data[i];
|
|
148
|
+
let g = data[i + 1];
|
|
149
|
+
let b = data[i + 2];
|
|
150
|
+
// Apply brightness (multiply all channels)
|
|
151
|
+
if (hasBrightness) {
|
|
152
|
+
r *= brightnessFactor;
|
|
153
|
+
g *= brightnessFactor;
|
|
154
|
+
b *= brightnessFactor;
|
|
155
|
+
}
|
|
156
|
+
// Apply contrast (scale around midpoint)
|
|
157
|
+
if (hasContrast) {
|
|
158
|
+
r = ((r - 128) * contrastFactor) + 128;
|
|
159
|
+
g = ((g - 128) * contrastFactor) + 128;
|
|
160
|
+
b = ((b - 128) * contrastFactor) + 128;
|
|
161
|
+
}
|
|
162
|
+
// Calculate luminance if needed
|
|
163
|
+
let luminance = 0;
|
|
164
|
+
if (needsLuminance) {
|
|
165
|
+
luminance = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
166
|
+
}
|
|
167
|
+
// Apply exposure
|
|
168
|
+
if (hasExposure) {
|
|
169
|
+
r *= exposureFactor;
|
|
170
|
+
g *= exposureFactor;
|
|
171
|
+
b *= exposureFactor;
|
|
172
|
+
}
|
|
173
|
+
// Apply highlights adjustment
|
|
174
|
+
if (hasHighlights) {
|
|
175
|
+
const highlightMask = Math.pow(luminance / 255, 2);
|
|
176
|
+
const highlightAdjust = highlightsFactor * highlightMask * 50;
|
|
177
|
+
r += highlightAdjust;
|
|
178
|
+
g += highlightAdjust;
|
|
179
|
+
b += highlightAdjust;
|
|
180
|
+
}
|
|
181
|
+
// Apply shadows adjustment
|
|
182
|
+
if (hasShadows) {
|
|
183
|
+
const shadowMask = Math.pow(1 - luminance / 255, 2);
|
|
184
|
+
const shadowAdjust = shadowsFactor * shadowMask * 50;
|
|
185
|
+
r -= shadowAdjust;
|
|
186
|
+
g -= shadowAdjust;
|
|
187
|
+
b -= shadowAdjust;
|
|
188
|
+
}
|
|
189
|
+
// Apply saturation and hue via HSL
|
|
190
|
+
if (needsHSL) {
|
|
191
|
+
// Clamp before HSL conversion
|
|
192
|
+
r = Math.max(0, Math.min(255, r));
|
|
193
|
+
g = Math.max(0, Math.min(255, g));
|
|
194
|
+
b = Math.max(0, Math.min(255, b));
|
|
195
|
+
let [h, s, l] = rgbToHsl(r, g, b);
|
|
196
|
+
// Adjust saturation
|
|
197
|
+
if (hasSaturation) {
|
|
198
|
+
s = Math.max(0, Math.min(100, s * (1 + saturationFactor)));
|
|
199
|
+
}
|
|
200
|
+
// Adjust hue
|
|
201
|
+
if (hasHue) {
|
|
202
|
+
h = (h + hueShift + 360) % 360;
|
|
203
|
+
}
|
|
204
|
+
[r, g, b] = hslToRgb(h, s, l);
|
|
205
|
+
}
|
|
206
|
+
// Apply sepia
|
|
207
|
+
if (hasSepia) {
|
|
208
|
+
const tr = (0.393 * r + 0.769 * g + 0.189 * b);
|
|
209
|
+
const tg = (0.349 * r + 0.686 * g + 0.168 * b);
|
|
210
|
+
const tb = (0.272 * r + 0.534 * g + 0.131 * b);
|
|
211
|
+
r = r * (1 - sepiaAmount) + tr * sepiaAmount;
|
|
212
|
+
g = g * (1 - sepiaAmount) + tg * sepiaAmount;
|
|
213
|
+
b = b * (1 - sepiaAmount) + tb * sepiaAmount;
|
|
214
|
+
}
|
|
215
|
+
// Apply grayscale
|
|
216
|
+
if (hasGrayscale) {
|
|
217
|
+
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
218
|
+
r = r * (1 - grayscaleAmount) + gray * grayscaleAmount;
|
|
219
|
+
g = g * (1 - grayscaleAmount) + gray * grayscaleAmount;
|
|
220
|
+
b = b * (1 - grayscaleAmount) + gray * grayscaleAmount;
|
|
221
|
+
}
|
|
222
|
+
// Apply vignette
|
|
223
|
+
if (hasVignette) {
|
|
224
|
+
const pixelIndex = i / 4;
|
|
225
|
+
const x = pixelIndex % canvas.width;
|
|
226
|
+
const y = Math.floor(pixelIndex / canvas.width);
|
|
227
|
+
const dx = x - imageCenterX;
|
|
228
|
+
const dy = y - imageCenterY;
|
|
229
|
+
const distanceSquared = dx * dx + dy * dy;
|
|
230
|
+
const normalizedDistanceSquared = distanceSquared / maxDistanceSquared;
|
|
231
|
+
const vignetteAmount = normalizedDistanceSquared;
|
|
232
|
+
const vignetteMultiplier = 1 + (vignetteFactor * vignetteAmount * vignetteStrength);
|
|
233
|
+
r *= vignetteMultiplier;
|
|
234
|
+
g *= vignetteMultiplier;
|
|
235
|
+
b *= vignetteMultiplier;
|
|
236
|
+
}
|
|
237
|
+
// Clamp final values to 0-255
|
|
238
|
+
data[i] = Math.max(0, Math.min(255, r));
|
|
239
|
+
data[i + 1] = Math.max(0, Math.min(255, g));
|
|
240
|
+
data[i + 2] = Math.max(0, Math.min(255, b));
|
|
241
|
+
}
|
|
242
|
+
ctx.putImageData(imageData, 0, 0);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Apply Gaussian blur to a region of canvas via pixel manipulation (Safari-compatible)
|
|
246
|
+
* Uses optimized separable box blur with running sums for O(n) performance
|
|
247
|
+
*/
|
|
248
|
+
export function applyGaussianBlur(canvas, x, y, width, height, radius) {
|
|
249
|
+
if (radius <= 0)
|
|
250
|
+
return;
|
|
251
|
+
const ctx = canvas.getContext('2d');
|
|
252
|
+
if (!ctx)
|
|
253
|
+
return;
|
|
254
|
+
// Clamp region to canvas bounds
|
|
255
|
+
x = Math.max(0, Math.floor(x));
|
|
256
|
+
y = Math.max(0, Math.floor(y));
|
|
257
|
+
width = Math.min(canvas.width - x, Math.ceil(width));
|
|
258
|
+
height = Math.min(canvas.height - y, Math.ceil(height));
|
|
259
|
+
if (width <= 0 || height <= 0)
|
|
260
|
+
return;
|
|
261
|
+
// Get the region to blur
|
|
262
|
+
const imageData = ctx.getImageData(x, y, width, height);
|
|
263
|
+
const pixels = imageData.data;
|
|
264
|
+
// Increase blur strength by using larger radius and more passes
|
|
265
|
+
const boxRadius = Math.max(1, Math.round(radius / 2)); // Changed from /3 to /2 for stronger blur
|
|
266
|
+
const passes = 3;
|
|
267
|
+
// Use two buffers to avoid allocation in loops
|
|
268
|
+
const buffer1 = new Uint8ClampedArray(pixels);
|
|
269
|
+
const buffer2 = new Uint8ClampedArray(pixels.length);
|
|
270
|
+
let srcBuffer = buffer1;
|
|
271
|
+
let dstBuffer = buffer2;
|
|
272
|
+
for (let pass = 0; pass < passes; pass++) {
|
|
273
|
+
// Horizontal pass - using running sum for O(n) performance
|
|
274
|
+
boxBlurHorizontal(srcBuffer, dstBuffer, width, height, boxRadius);
|
|
275
|
+
// Swap buffers
|
|
276
|
+
[srcBuffer, dstBuffer] = [dstBuffer, srcBuffer];
|
|
277
|
+
// Vertical pass - using running sum for O(n) performance
|
|
278
|
+
boxBlurVertical(srcBuffer, dstBuffer, width, height, boxRadius);
|
|
279
|
+
// Swap buffers
|
|
280
|
+
[srcBuffer, dstBuffer] = [dstBuffer, srcBuffer];
|
|
281
|
+
}
|
|
282
|
+
// Copy result back
|
|
283
|
+
pixels.set(srcBuffer);
|
|
284
|
+
ctx.putImageData(imageData, x, y);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Fast horizontal box blur using running sum (O(n) complexity)
|
|
288
|
+
*/
|
|
289
|
+
function boxBlurHorizontal(src, dst, width, height, radius) {
|
|
290
|
+
const iarr = 1 / (radius + radius + 1);
|
|
291
|
+
for (let row = 0; row < height; row++) {
|
|
292
|
+
const rowOffset = row * width * 4;
|
|
293
|
+
let ti = rowOffset;
|
|
294
|
+
let li = ti;
|
|
295
|
+
let ri = ti + radius * 4;
|
|
296
|
+
// First pixel in row
|
|
297
|
+
const fv_r = src[ti];
|
|
298
|
+
const fv_g = src[ti + 1];
|
|
299
|
+
const fv_b = src[ti + 2];
|
|
300
|
+
const fv_a = src[ti + 3];
|
|
301
|
+
// Last pixel in row
|
|
302
|
+
const lv_r = src[rowOffset + (width - 1) * 4];
|
|
303
|
+
const lv_g = src[rowOffset + (width - 1) * 4 + 1];
|
|
304
|
+
const lv_b = src[rowOffset + (width - 1) * 4 + 2];
|
|
305
|
+
const lv_a = src[rowOffset + (width - 1) * 4 + 3];
|
|
306
|
+
// Initial sum
|
|
307
|
+
let val_r = (radius + 1) * fv_r;
|
|
308
|
+
let val_g = (radius + 1) * fv_g;
|
|
309
|
+
let val_b = (radius + 1) * fv_b;
|
|
310
|
+
let val_a = (radius + 1) * fv_a;
|
|
311
|
+
for (let j = 0; j < radius; j++) {
|
|
312
|
+
const offset = rowOffset + j * 4;
|
|
313
|
+
val_r += src[offset];
|
|
314
|
+
val_g += src[offset + 1];
|
|
315
|
+
val_b += src[offset + 2];
|
|
316
|
+
val_a += src[offset + 3];
|
|
317
|
+
}
|
|
318
|
+
// Process pixels
|
|
319
|
+
for (let col = 0; col <= radius; col++) {
|
|
320
|
+
val_r += src[ri] - fv_r;
|
|
321
|
+
val_g += src[ri + 1] - fv_g;
|
|
322
|
+
val_b += src[ri + 2] - fv_b;
|
|
323
|
+
val_a += src[ri + 3] - fv_a;
|
|
324
|
+
ri += 4;
|
|
325
|
+
dst[ti] = Math.round(val_r * iarr);
|
|
326
|
+
dst[ti + 1] = Math.round(val_g * iarr);
|
|
327
|
+
dst[ti + 2] = Math.round(val_b * iarr);
|
|
328
|
+
dst[ti + 3] = Math.round(val_a * iarr);
|
|
329
|
+
ti += 4;
|
|
330
|
+
}
|
|
331
|
+
for (let col = radius + 1; col < width - radius; col++) {
|
|
332
|
+
val_r += src[ri] - src[li];
|
|
333
|
+
val_g += src[ri + 1] - src[li + 1];
|
|
334
|
+
val_b += src[ri + 2] - src[li + 2];
|
|
335
|
+
val_a += src[ri + 3] - src[li + 3];
|
|
336
|
+
ri += 4;
|
|
337
|
+
li += 4;
|
|
338
|
+
dst[ti] = Math.round(val_r * iarr);
|
|
339
|
+
dst[ti + 1] = Math.round(val_g * iarr);
|
|
340
|
+
dst[ti + 2] = Math.round(val_b * iarr);
|
|
341
|
+
dst[ti + 3] = Math.round(val_a * iarr);
|
|
342
|
+
ti += 4;
|
|
343
|
+
}
|
|
344
|
+
for (let col = width - radius; col < width; col++) {
|
|
345
|
+
val_r += lv_r - src[li];
|
|
346
|
+
val_g += lv_g - src[li + 1];
|
|
347
|
+
val_b += lv_b - src[li + 2];
|
|
348
|
+
val_a += lv_a - src[li + 3];
|
|
349
|
+
li += 4;
|
|
350
|
+
dst[ti] = Math.round(val_r * iarr);
|
|
351
|
+
dst[ti + 1] = Math.round(val_g * iarr);
|
|
352
|
+
dst[ti + 2] = Math.round(val_b * iarr);
|
|
353
|
+
dst[ti + 3] = Math.round(val_a * iarr);
|
|
354
|
+
ti += 4;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Fast vertical box blur using running sum (O(n) complexity)
|
|
360
|
+
*/
|
|
361
|
+
function boxBlurVertical(src, dst, width, height, radius) {
|
|
362
|
+
const iarr = 1 / (radius + radius + 1);
|
|
363
|
+
for (let col = 0; col < width; col++) {
|
|
364
|
+
const colOffset = col * 4;
|
|
365
|
+
let ti = colOffset;
|
|
366
|
+
let li = ti;
|
|
367
|
+
let ri = ti + radius * width * 4;
|
|
368
|
+
// First pixel in column
|
|
369
|
+
const fv_r = src[ti];
|
|
370
|
+
const fv_g = src[ti + 1];
|
|
371
|
+
const fv_b = src[ti + 2];
|
|
372
|
+
const fv_a = src[ti + 3];
|
|
373
|
+
// Last pixel in column
|
|
374
|
+
const lv_r = src[colOffset + (height - 1) * width * 4];
|
|
375
|
+
const lv_g = src[colOffset + (height - 1) * width * 4 + 1];
|
|
376
|
+
const lv_b = src[colOffset + (height - 1) * width * 4 + 2];
|
|
377
|
+
const lv_a = src[colOffset + (height - 1) * width * 4 + 3];
|
|
378
|
+
// Initial sum
|
|
379
|
+
let val_r = (radius + 1) * fv_r;
|
|
380
|
+
let val_g = (radius + 1) * fv_g;
|
|
381
|
+
let val_b = (radius + 1) * fv_b;
|
|
382
|
+
let val_a = (radius + 1) * fv_a;
|
|
383
|
+
for (let j = 0; j < radius; j++) {
|
|
384
|
+
const offset = colOffset + j * width * 4;
|
|
385
|
+
val_r += src[offset];
|
|
386
|
+
val_g += src[offset + 1];
|
|
387
|
+
val_b += src[offset + 2];
|
|
388
|
+
val_a += src[offset + 3];
|
|
389
|
+
}
|
|
390
|
+
// Process pixels
|
|
391
|
+
for (let row = 0; row <= radius; row++) {
|
|
392
|
+
val_r += src[ri] - fv_r;
|
|
393
|
+
val_g += src[ri + 1] - fv_g;
|
|
394
|
+
val_b += src[ri + 2] - fv_b;
|
|
395
|
+
val_a += src[ri + 3] - fv_a;
|
|
396
|
+
ri += width * 4;
|
|
397
|
+
dst[ti] = Math.round(val_r * iarr);
|
|
398
|
+
dst[ti + 1] = Math.round(val_g * iarr);
|
|
399
|
+
dst[ti + 2] = Math.round(val_b * iarr);
|
|
400
|
+
dst[ti + 3] = Math.round(val_a * iarr);
|
|
401
|
+
ti += width * 4;
|
|
402
|
+
}
|
|
403
|
+
for (let row = radius + 1; row < height - radius; row++) {
|
|
404
|
+
val_r += src[ri] - src[li];
|
|
405
|
+
val_g += src[ri + 1] - src[li + 1];
|
|
406
|
+
val_b += src[ri + 2] - src[li + 2];
|
|
407
|
+
val_a += src[ri + 3] - src[li + 3];
|
|
408
|
+
ri += width * 4;
|
|
409
|
+
li += width * 4;
|
|
410
|
+
dst[ti] = Math.round(val_r * iarr);
|
|
411
|
+
dst[ti + 1] = Math.round(val_g * iarr);
|
|
412
|
+
dst[ti + 2] = Math.round(val_b * iarr);
|
|
413
|
+
dst[ti + 3] = Math.round(val_a * iarr);
|
|
414
|
+
ti += width * 4;
|
|
415
|
+
}
|
|
416
|
+
for (let row = height - radius; row < height; row++) {
|
|
417
|
+
val_r += lv_r - src[li];
|
|
418
|
+
val_g += lv_g - src[li + 1];
|
|
419
|
+
val_b += lv_b - src[li + 2];
|
|
420
|
+
val_a += lv_a - src[li + 3];
|
|
421
|
+
li += width * 4;
|
|
422
|
+
dst[ti] = Math.round(val_r * iarr);
|
|
423
|
+
dst[ti + 1] = Math.round(val_g * iarr);
|
|
424
|
+
dst[ti + 2] = Math.round(val_b * iarr);
|
|
425
|
+
dst[ti + 3] = Math.round(val_a * iarr);
|
|
426
|
+
ti += width * 4;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Legacy function for backwards compatibility
|
|
432
|
+
* @deprecated Use applyAllAdjustments instead
|
|
433
|
+
*/
|
|
434
|
+
export function applyPixelAdjustments(canvas, img, viewport, adjustments, cropArea) {
|
|
435
|
+
// Skip if no pixel adjustments needed
|
|
436
|
+
if (adjustments.exposure === 0 &&
|
|
437
|
+
adjustments.highlights === 0 &&
|
|
438
|
+
adjustments.shadows === 0 &&
|
|
439
|
+
adjustments.vignette === 0) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const ctx = canvas.getContext('2d');
|
|
443
|
+
if (!ctx)
|
|
444
|
+
return;
|
|
445
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
446
|
+
const data = imageData.data;
|
|
447
|
+
// Pre-calculate adjustment factors
|
|
448
|
+
const hasExposure = adjustments.exposure !== 0;
|
|
449
|
+
const hasHighlights = adjustments.highlights !== 0;
|
|
450
|
+
const hasShadows = adjustments.shadows !== 0;
|
|
451
|
+
const hasVignette = adjustments.vignette !== 0;
|
|
452
|
+
const exposureFactor = hasExposure ? Math.pow(2, adjustments.exposure / 100) : 1;
|
|
453
|
+
const highlightsFactor = adjustments.highlights / 100;
|
|
454
|
+
const shadowsFactor = adjustments.shadows / 100;
|
|
455
|
+
// Vignette pre-calculations (based on image dimensions, not canvas)
|
|
456
|
+
const imgWidth = cropArea ? cropArea.width : img.width;
|
|
457
|
+
const imgHeight = cropArea ? cropArea.height : img.height;
|
|
458
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
459
|
+
// Image center on canvas (accounting for viewport offset)
|
|
460
|
+
const imageCenterX = canvas.width / 2 + viewport.offsetX;
|
|
461
|
+
const imageCenterY = canvas.height / 2 + viewport.offsetY;
|
|
462
|
+
// Max distance based on scaled image dimensions
|
|
463
|
+
const scaledImageHalfWidth = (imgWidth * totalScale) / 2;
|
|
464
|
+
const scaledImageHalfHeight = (imgHeight * totalScale) / 2;
|
|
465
|
+
const maxDistanceSquared = scaledImageHalfWidth * scaledImageHalfWidth +
|
|
466
|
+
scaledImageHalfHeight * scaledImageHalfHeight;
|
|
467
|
+
const vignetteFactor = adjustments.vignette / 100;
|
|
468
|
+
const vignetteStrength = 1.5;
|
|
469
|
+
const needsLuminance = hasHighlights || hasShadows;
|
|
470
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
471
|
+
let r = data[i];
|
|
472
|
+
let g = data[i + 1];
|
|
473
|
+
let b = data[i + 2];
|
|
474
|
+
// Calculate luminance only if needed for highlights/shadows
|
|
475
|
+
let luminance = 0;
|
|
476
|
+
if (needsLuminance) {
|
|
477
|
+
luminance = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
478
|
+
}
|
|
479
|
+
// Apply exposure (multiply all channels)
|
|
480
|
+
if (hasExposure) {
|
|
481
|
+
r *= exposureFactor;
|
|
482
|
+
g *= exposureFactor;
|
|
483
|
+
b *= exposureFactor;
|
|
484
|
+
}
|
|
485
|
+
// Apply highlights adjustment (affect bright pixels more)
|
|
486
|
+
if (hasHighlights) {
|
|
487
|
+
const highlightMask = Math.pow(luminance / 255, 2);
|
|
488
|
+
const highlightAdjust = highlightsFactor * highlightMask * 50;
|
|
489
|
+
r += highlightAdjust;
|
|
490
|
+
g += highlightAdjust;
|
|
491
|
+
b += highlightAdjust;
|
|
492
|
+
}
|
|
493
|
+
// Apply shadows adjustment (affect dark pixels more)
|
|
494
|
+
if (hasShadows) {
|
|
495
|
+
const shadowMask = Math.pow(1 - luminance / 255, 2);
|
|
496
|
+
const shadowAdjust = shadowsFactor * shadowMask * 50;
|
|
497
|
+
r -= shadowAdjust;
|
|
498
|
+
g -= shadowAdjust;
|
|
499
|
+
b -= shadowAdjust;
|
|
500
|
+
}
|
|
501
|
+
// Apply vignette (darken or brighten edges based on distance from image center)
|
|
502
|
+
if (hasVignette) {
|
|
503
|
+
const pixelIndex = i / 4;
|
|
504
|
+
const x = pixelIndex % canvas.width;
|
|
505
|
+
const y = Math.floor(pixelIndex / canvas.width);
|
|
506
|
+
// Calculate distance from image center (not canvas center)
|
|
507
|
+
const dx = x - imageCenterX;
|
|
508
|
+
const dy = y - imageCenterY;
|
|
509
|
+
const distanceSquared = dx * dx + dy * dy;
|
|
510
|
+
const normalizedDistanceSquared = distanceSquared / maxDistanceSquared;
|
|
511
|
+
// Use smooth falloff curve - already squared, so power of 2 total
|
|
512
|
+
const vignetteAmount = normalizedDistanceSquared;
|
|
513
|
+
// Negative vignette = darken edges, Positive = brighten edges
|
|
514
|
+
const vignetteMultiplier = 1 + (vignetteFactor * vignetteAmount * vignetteStrength);
|
|
515
|
+
r *= vignetteMultiplier;
|
|
516
|
+
g *= vignetteMultiplier;
|
|
517
|
+
b *= vignetteMultiplier;
|
|
518
|
+
}
|
|
519
|
+
// Clamp values to 0-255
|
|
520
|
+
data[i] = Math.max(0, Math.min(255, r));
|
|
521
|
+
data[i + 1] = Math.max(0, Math.min(255, g));
|
|
522
|
+
data[i + 2] = Math.max(0, Math.min(255, b));
|
|
523
|
+
}
|
|
524
|
+
ctx.putImageData(imageData, 0, 0);
|
|
525
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CropArea, TransformState, ExportOptions, Viewport, AdjustmentsState, BlurArea, StampArea } from '../types';
|
|
2
|
+
export declare function preloadStampImage(url: string): Promise<HTMLImageElement>;
|
|
3
|
+
export declare function getStampImage(url: string): HTMLImageElement | null;
|
|
4
|
+
export declare function loadImage(file: File): Promise<HTMLImageElement>;
|
|
5
|
+
export declare function calculateFitScale(imageWidth: number, imageHeight: number, canvasWidth: number, canvasHeight: number): number;
|
|
6
|
+
export declare function drawImage(canvas: HTMLCanvasElement, img: HTMLImageElement, viewport: Viewport, transform: TransformState, adjustments: AdjustmentsState, cropArea?: CropArea | null, blurAreas?: BlurArea[], stampAreas?: StampArea[]): void;
|
|
7
|
+
export declare function exportCanvas(canvas: HTMLCanvasElement, options: ExportOptions): string;
|
|
8
|
+
export declare function downloadImage(dataUrl: string, filename: string): void;
|
|
9
|
+
export declare function applyTransform(img: HTMLImageElement, transform: TransformState, adjustments: AdjustmentsState, cropArea?: CropArea | null, blurAreas?: BlurArea[], stampAreas?: StampArea[]): HTMLCanvasElement;
|
|
10
|
+
export declare function screenToImageCoords(screenX: number, screenY: number, canvas: HTMLCanvasElement, img: HTMLImageElement, viewport: Viewport, transform: TransformState): {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function imageToCanvasCoords(imageX: number, imageY: number, canvas: HTMLCanvasElement, img: HTMLImageElement, viewport: Viewport): {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function imageToScreenCoords(imageX: number, imageY: number, canvas: HTMLCanvasElement, img: HTMLImageElement, viewport: Viewport): {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Apply blur effects to specified areas of the canvas
|
|
24
|
+
* Uses pixel manipulation for Safari compatibility
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyBlurAreas(canvas: HTMLCanvasElement, img: HTMLImageElement, viewport: Viewport, blurAreas: BlurArea[], cropArea?: CropArea | null): void;
|
|
27
|
+
/**
|
|
28
|
+
* Apply stamp decorations to the canvas
|
|
29
|
+
*/
|
|
30
|
+
export declare function applyStamps(canvas: HTMLCanvasElement, img: HTMLImageElement, viewport: Viewport, stampAreas: StampArea[], cropArea?: CropArea | null): void;
|