tokimeki-image-editor 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/components/AdjustTool.svelte +317 -0
  2. package/dist/components/AdjustTool.svelte.d.ts +9 -0
  3. package/dist/components/BlurTool.svelte +613 -0
  4. package/dist/components/BlurTool.svelte.d.ts +15 -0
  5. package/dist/components/Canvas.svelte +214 -0
  6. package/dist/components/Canvas.svelte.d.ts +17 -0
  7. package/dist/components/CropTool.svelte +942 -0
  8. package/dist/components/CropTool.svelte.d.ts +14 -0
  9. package/dist/components/ExportTool.svelte +191 -0
  10. package/dist/components/ExportTool.svelte.d.ts +10 -0
  11. package/dist/components/FilterTool.svelte +492 -0
  12. package/dist/components/FilterTool.svelte.d.ts +12 -0
  13. package/dist/components/ImageEditor.svelte +735 -0
  14. package/dist/components/ImageEditor.svelte.d.ts +12 -0
  15. package/dist/components/RotateTool.svelte +157 -0
  16. package/dist/components/RotateTool.svelte.d.ts +9 -0
  17. package/dist/components/StampTool.svelte +678 -0
  18. package/dist/components/StampTool.svelte.d.ts +15 -0
  19. package/dist/components/Toolbar.svelte +136 -0
  20. package/dist/components/Toolbar.svelte.d.ts +10 -0
  21. package/dist/config/stamps.d.ts +2 -0
  22. package/dist/config/stamps.js +22 -0
  23. package/dist/i18n/index.d.ts +1 -0
  24. package/dist/i18n/index.js +9 -0
  25. package/dist/i18n/locales/en.json +68 -0
  26. package/dist/i18n/locales/ja.json +68 -0
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.js +5 -0
  29. package/dist/types.d.ts +97 -0
  30. package/dist/types.js +1 -0
  31. package/dist/utils/adjustments.d.ts +26 -0
  32. package/dist/utils/adjustments.js +525 -0
  33. package/dist/utils/canvas.d.ts +30 -0
  34. package/dist/utils/canvas.js +293 -0
  35. package/dist/utils/filters.d.ts +18 -0
  36. package/dist/utils/filters.js +114 -0
  37. package/dist/utils/history.d.ts +15 -0
  38. package/dist/utils/history.js +67 -0
  39. 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;