tokimeki-image-editor 0.1.1 → 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,293 @@
|
|
|
1
|
+
import { applyAllAdjustments, applyGaussianBlur } from './adjustments';
|
|
2
|
+
// Image cache for stamp images
|
|
3
|
+
const stampImageCache = new Map();
|
|
4
|
+
export function preloadStampImage(url) {
|
|
5
|
+
// Return cached image if available
|
|
6
|
+
if (stampImageCache.has(url)) {
|
|
7
|
+
return Promise.resolve(stampImageCache.get(url));
|
|
8
|
+
}
|
|
9
|
+
// Load new image
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const img = new Image();
|
|
12
|
+
img.crossOrigin = 'anonymous'; // Allow CORS for external images
|
|
13
|
+
img.onload = () => {
|
|
14
|
+
stampImageCache.set(url, img);
|
|
15
|
+
resolve(img);
|
|
16
|
+
};
|
|
17
|
+
img.onerror = (error) => {
|
|
18
|
+
console.error(`Failed to load stamp image: ${url}`, error);
|
|
19
|
+
reject(error);
|
|
20
|
+
};
|
|
21
|
+
img.src = url;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function getStampImage(url) {
|
|
25
|
+
return stampImageCache.get(url) || null;
|
|
26
|
+
}
|
|
27
|
+
export function loadImage(file) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const reader = new FileReader();
|
|
30
|
+
reader.onload = (e) => {
|
|
31
|
+
const img = new Image();
|
|
32
|
+
img.onload = () => resolve(img);
|
|
33
|
+
img.onerror = reject;
|
|
34
|
+
img.src = e.target?.result;
|
|
35
|
+
};
|
|
36
|
+
reader.onerror = reject;
|
|
37
|
+
reader.readAsDataURL(file);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function calculateFitScale(imageWidth, imageHeight, canvasWidth, canvasHeight) {
|
|
41
|
+
const scaleX = canvasWidth / imageWidth;
|
|
42
|
+
const scaleY = canvasHeight / imageHeight;
|
|
43
|
+
return Math.min(scaleX, scaleY, 1); // Don't scale up, only down
|
|
44
|
+
}
|
|
45
|
+
export function drawImage(canvas, img, viewport, transform, adjustments, cropArea, blurAreas, stampAreas) {
|
|
46
|
+
const ctx = canvas.getContext('2d');
|
|
47
|
+
if (!ctx)
|
|
48
|
+
return;
|
|
49
|
+
// Clear canvas
|
|
50
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
51
|
+
// Ensure filter is reset before starting
|
|
52
|
+
ctx.filter = 'none';
|
|
53
|
+
ctx.save();
|
|
54
|
+
// Apply viewport transformations
|
|
55
|
+
const centerX = canvas.width / 2;
|
|
56
|
+
const centerY = canvas.height / 2;
|
|
57
|
+
ctx.translate(centerX + viewport.offsetX, centerY + viewport.offsetY);
|
|
58
|
+
// Apply zoom
|
|
59
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
60
|
+
ctx.scale(totalScale, totalScale);
|
|
61
|
+
// Rotation
|
|
62
|
+
ctx.rotate((transform.rotation * Math.PI) / 180);
|
|
63
|
+
// Flip
|
|
64
|
+
ctx.scale(transform.flipHorizontal ? -1 : 1, transform.flipVertical ? -1 : 1);
|
|
65
|
+
// Draw image (with crop if specified)
|
|
66
|
+
if (cropArea) {
|
|
67
|
+
// Draw only the cropped area
|
|
68
|
+
ctx.drawImage(img, cropArea.x, cropArea.y, cropArea.width, cropArea.height, -cropArea.width / 2, -cropArea.height / 2, cropArea.width, cropArea.height);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Draw full image centered
|
|
72
|
+
ctx.drawImage(img, -img.width / 2, -img.height / 2);
|
|
73
|
+
}
|
|
74
|
+
ctx.restore();
|
|
75
|
+
// Apply all adjustments via pixel manipulation (Safari-compatible)
|
|
76
|
+
// This modifies the canvas pixels after drawing
|
|
77
|
+
applyAllAdjustments(canvas, img, viewport, adjustments, cropArea);
|
|
78
|
+
// Apply blur areas
|
|
79
|
+
if (blurAreas && blurAreas.length > 0) {
|
|
80
|
+
applyBlurAreas(canvas, img, viewport, blurAreas, cropArea);
|
|
81
|
+
}
|
|
82
|
+
// Apply stamps
|
|
83
|
+
if (stampAreas && stampAreas.length > 0) {
|
|
84
|
+
applyStamps(canvas, img, viewport, stampAreas, cropArea);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function exportCanvas(canvas, options) {
|
|
88
|
+
if (options.format === 'jpeg') {
|
|
89
|
+
return canvas.toDataURL('image/jpeg', options.quality);
|
|
90
|
+
}
|
|
91
|
+
return canvas.toDataURL('image/png');
|
|
92
|
+
}
|
|
93
|
+
export function downloadImage(dataUrl, filename) {
|
|
94
|
+
const link = document.createElement('a');
|
|
95
|
+
link.download = filename;
|
|
96
|
+
link.href = dataUrl;
|
|
97
|
+
link.click();
|
|
98
|
+
}
|
|
99
|
+
export function applyTransform(img, transform, adjustments, cropArea = null, blurAreas = [], stampAreas = []) {
|
|
100
|
+
const canvas = document.createElement('canvas');
|
|
101
|
+
const ctx = canvas.getContext('2d');
|
|
102
|
+
if (!ctx)
|
|
103
|
+
return canvas;
|
|
104
|
+
// Calculate source dimensions
|
|
105
|
+
const sourceWidth = cropArea ? cropArea.width : img.width;
|
|
106
|
+
const sourceHeight = cropArea ? cropArea.height : img.height;
|
|
107
|
+
// Calculate canvas size based on rotation
|
|
108
|
+
const needsSwap = transform.rotation === 90 || transform.rotation === 270;
|
|
109
|
+
canvas.width = needsSwap ? sourceHeight : sourceWidth;
|
|
110
|
+
canvas.height = needsSwap ? sourceWidth : sourceHeight;
|
|
111
|
+
// Ensure filter is reset before starting
|
|
112
|
+
ctx.filter = 'none';
|
|
113
|
+
ctx.save();
|
|
114
|
+
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
115
|
+
ctx.rotate((transform.rotation * Math.PI) / 180);
|
|
116
|
+
ctx.scale(transform.flipHorizontal ? -1 : 1, transform.flipVertical ? -1 : 1);
|
|
117
|
+
if (cropArea) {
|
|
118
|
+
ctx.drawImage(img, cropArea.x, cropArea.y, cropArea.width, cropArea.height, -sourceWidth / 2, -sourceHeight / 2, sourceWidth, sourceHeight);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
ctx.drawImage(img, -sourceWidth / 2, -sourceHeight / 2);
|
|
122
|
+
}
|
|
123
|
+
ctx.restore();
|
|
124
|
+
// Apply all adjustments via pixel manipulation (Safari-compatible)
|
|
125
|
+
// For export, create a centered viewport with no offset
|
|
126
|
+
const exportViewport = {
|
|
127
|
+
zoom: 1,
|
|
128
|
+
offsetX: 0,
|
|
129
|
+
offsetY: 0,
|
|
130
|
+
scale: 1
|
|
131
|
+
};
|
|
132
|
+
applyAllAdjustments(canvas, img, exportViewport, adjustments, cropArea);
|
|
133
|
+
// Apply blur areas for export
|
|
134
|
+
if (blurAreas.length > 0) {
|
|
135
|
+
applyBlurAreas(canvas, img, exportViewport, blurAreas, cropArea);
|
|
136
|
+
}
|
|
137
|
+
// Apply stamps for export
|
|
138
|
+
if (stampAreas.length > 0) {
|
|
139
|
+
applyStamps(canvas, img, exportViewport, stampAreas, cropArea);
|
|
140
|
+
}
|
|
141
|
+
return canvas;
|
|
142
|
+
}
|
|
143
|
+
export function screenToImageCoords(screenX, screenY, canvas, img, viewport, transform) {
|
|
144
|
+
const rect = canvas.getBoundingClientRect();
|
|
145
|
+
// Convert screen coordinates to canvas coordinates
|
|
146
|
+
const scaleX = canvas.width / rect.width;
|
|
147
|
+
const scaleY = canvas.height / rect.height;
|
|
148
|
+
const canvasX = (screenX - rect.left) * scaleX;
|
|
149
|
+
const canvasY = (screenY - rect.top) * scaleY;
|
|
150
|
+
const centerX = canvas.width / 2;
|
|
151
|
+
const centerY = canvas.height / 2;
|
|
152
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
153
|
+
// Inverse transform
|
|
154
|
+
const x = (canvasX - centerX - viewport.offsetX) / totalScale + img.width / 2;
|
|
155
|
+
const y = (canvasY - centerY - viewport.offsetY) / totalScale + img.height / 2;
|
|
156
|
+
return { x, y };
|
|
157
|
+
}
|
|
158
|
+
export function imageToCanvasCoords(imageX, imageY, canvas, img, viewport) {
|
|
159
|
+
const centerX = canvas.width / 2;
|
|
160
|
+
const centerY = canvas.height / 2;
|
|
161
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
162
|
+
const x = (imageX - img.width / 2) * totalScale + centerX + viewport.offsetX;
|
|
163
|
+
const y = (imageY - img.height / 2) * totalScale + centerY + viewport.offsetY;
|
|
164
|
+
return { x, y };
|
|
165
|
+
}
|
|
166
|
+
// Deprecated: use imageToCanvasCoords instead
|
|
167
|
+
export function imageToScreenCoords(imageX, imageY, canvas, img, viewport) {
|
|
168
|
+
return imageToCanvasCoords(imageX, imageY, canvas, img, viewport);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Apply blur effects to specified areas of the canvas
|
|
172
|
+
* Uses pixel manipulation for Safari compatibility
|
|
173
|
+
*/
|
|
174
|
+
export function applyBlurAreas(canvas, img, viewport, blurAreas, cropArea) {
|
|
175
|
+
const ctx = canvas.getContext('2d');
|
|
176
|
+
if (!ctx)
|
|
177
|
+
return;
|
|
178
|
+
const centerX = canvas.width / 2;
|
|
179
|
+
const centerY = canvas.height / 2;
|
|
180
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
181
|
+
// Create a temporary canvas to extract regions for blurring
|
|
182
|
+
const tempCanvas = document.createElement('canvas');
|
|
183
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
184
|
+
if (!tempCtx)
|
|
185
|
+
return;
|
|
186
|
+
blurAreas.forEach(blurArea => {
|
|
187
|
+
// Determine source dimensions based on crop
|
|
188
|
+
const sourceWidth = cropArea ? cropArea.width : img.width;
|
|
189
|
+
const sourceHeight = cropArea ? cropArea.height : img.height;
|
|
190
|
+
const offsetX = cropArea ? cropArea.x : 0;
|
|
191
|
+
const offsetY = cropArea ? cropArea.y : 0;
|
|
192
|
+
// Convert blur area to crop-relative coordinates
|
|
193
|
+
const relativeX = blurArea.x - offsetX;
|
|
194
|
+
const relativeY = blurArea.y - offsetY;
|
|
195
|
+
// Calculate blur area in canvas coordinates
|
|
196
|
+
const canvasBlurX = (relativeX - sourceWidth / 2) * totalScale + centerX + viewport.offsetX;
|
|
197
|
+
const canvasBlurY = (relativeY - sourceHeight / 2) * totalScale + centerY + viewport.offsetY;
|
|
198
|
+
const canvasBlurWidth = blurArea.width * totalScale;
|
|
199
|
+
const canvasBlurHeight = blurArea.height * totalScale;
|
|
200
|
+
// Calculate image bounds on canvas
|
|
201
|
+
const imgCanvasLeft = (0 - sourceWidth / 2) * totalScale + centerX + viewport.offsetX;
|
|
202
|
+
const imgCanvasTop = (0 - sourceHeight / 2) * totalScale + centerY + viewport.offsetY;
|
|
203
|
+
const imgCanvasRight = imgCanvasLeft + sourceWidth * totalScale;
|
|
204
|
+
const imgCanvasBottom = imgCanvasTop + sourceHeight * totalScale;
|
|
205
|
+
// Clip blur area to image bounds
|
|
206
|
+
const clippedX = Math.max(imgCanvasLeft, canvasBlurX);
|
|
207
|
+
const clippedY = Math.max(imgCanvasTop, canvasBlurY);
|
|
208
|
+
const clippedRight = Math.min(imgCanvasRight, canvasBlurX + canvasBlurWidth);
|
|
209
|
+
const clippedBottom = Math.min(imgCanvasBottom, canvasBlurY + canvasBlurHeight);
|
|
210
|
+
const clippedWidth = clippedRight - clippedX;
|
|
211
|
+
const clippedHeight = clippedBottom - clippedY;
|
|
212
|
+
if (clippedWidth <= 0 || clippedHeight <= 0)
|
|
213
|
+
return;
|
|
214
|
+
// Calculate blur radius in pixels
|
|
215
|
+
const imageBlurPx = (blurArea.blurStrength / 100) * 100;
|
|
216
|
+
const blurRadius = imageBlurPx * totalScale;
|
|
217
|
+
// Add padding for blur to work properly at edges
|
|
218
|
+
const padding = Math.ceil(blurRadius * 2);
|
|
219
|
+
const paddedX = Math.max(0, clippedX - padding);
|
|
220
|
+
const paddedY = Math.max(0, clippedY - padding);
|
|
221
|
+
const paddedRight = Math.min(canvas.width, clippedRight + padding);
|
|
222
|
+
const paddedBottom = Math.min(canvas.height, clippedBottom + padding);
|
|
223
|
+
const paddedWidth = paddedRight - paddedX;
|
|
224
|
+
const paddedHeight = paddedBottom - paddedY;
|
|
225
|
+
// Extract the padded region from the canvas
|
|
226
|
+
tempCanvas.width = paddedWidth;
|
|
227
|
+
tempCanvas.height = paddedHeight;
|
|
228
|
+
tempCtx.clearRect(0, 0, paddedWidth, paddedHeight);
|
|
229
|
+
tempCtx.drawImage(canvas, paddedX, paddedY, paddedWidth, paddedHeight, 0, 0, paddedWidth, paddedHeight);
|
|
230
|
+
// Apply blur to the temporary canvas
|
|
231
|
+
applyGaussianBlur(tempCanvas, 0, 0, paddedWidth, paddedHeight, blurRadius);
|
|
232
|
+
// Calculate the portion to draw back (excluding padding)
|
|
233
|
+
const srcX = clippedX - paddedX;
|
|
234
|
+
const srcY = clippedY - paddedY;
|
|
235
|
+
// Draw only the non-padded portion back to the main canvas
|
|
236
|
+
ctx.drawImage(tempCanvas, srcX, srcY, clippedWidth, clippedHeight, clippedX, clippedY, clippedWidth, clippedHeight);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Apply stamp decorations to the canvas
|
|
241
|
+
*/
|
|
242
|
+
export function applyStamps(canvas, img, viewport, stampAreas, cropArea) {
|
|
243
|
+
const ctx = canvas.getContext('2d');
|
|
244
|
+
if (!ctx)
|
|
245
|
+
return;
|
|
246
|
+
const centerX = canvas.width / 2;
|
|
247
|
+
const centerY = canvas.height / 2;
|
|
248
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
249
|
+
stampAreas.forEach(stamp => {
|
|
250
|
+
// Determine source dimensions based on crop
|
|
251
|
+
const sourceWidth = cropArea ? cropArea.width : img.width;
|
|
252
|
+
const sourceHeight = cropArea ? cropArea.height : img.height;
|
|
253
|
+
const offsetX = cropArea ? cropArea.x : 0;
|
|
254
|
+
const offsetY = cropArea ? cropArea.y : 0;
|
|
255
|
+
// Convert stamp area to crop-relative coordinates
|
|
256
|
+
const relativeX = stamp.x - offsetX;
|
|
257
|
+
const relativeY = stamp.y - offsetY;
|
|
258
|
+
// Calculate stamp center in canvas coordinates
|
|
259
|
+
const canvasCenterX = (relativeX - sourceWidth / 2) * totalScale + centerX + viewport.offsetX;
|
|
260
|
+
const canvasCenterY = (relativeY - sourceHeight / 2) * totalScale + centerY + viewport.offsetY;
|
|
261
|
+
const canvasWidth = stamp.width * totalScale;
|
|
262
|
+
const canvasHeight = stamp.height * totalScale;
|
|
263
|
+
// Save context
|
|
264
|
+
ctx.save();
|
|
265
|
+
// Apply transformation
|
|
266
|
+
ctx.translate(canvasCenterX, canvasCenterY);
|
|
267
|
+
ctx.rotate((stamp.rotation || 0) * Math.PI / 180);
|
|
268
|
+
// Render stamp based on type
|
|
269
|
+
if (stamp.stampType === 'emoji') {
|
|
270
|
+
// Render emoji
|
|
271
|
+
ctx.font = `${canvasHeight}px Arial`;
|
|
272
|
+
ctx.textAlign = 'center';
|
|
273
|
+
ctx.textBaseline = 'middle';
|
|
274
|
+
ctx.fillText(stamp.stampContent, 0, 0);
|
|
275
|
+
}
|
|
276
|
+
else if (stamp.stampType === 'image' || stamp.stampType === 'svg') {
|
|
277
|
+
// Get image from cache
|
|
278
|
+
const stampImg = getStampImage(stamp.stampContent);
|
|
279
|
+
if (stampImg) {
|
|
280
|
+
ctx.drawImage(stampImg, -canvasWidth / 2, -canvasHeight / 2, canvasWidth, canvasHeight);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// Draw placeholder while loading
|
|
284
|
+
ctx.fillStyle = '#ccc';
|
|
285
|
+
ctx.fillRect(-canvasWidth / 2, -canvasHeight / 2, canvasWidth, canvasHeight);
|
|
286
|
+
// Start loading if not already loading
|
|
287
|
+
preloadStampImage(stamp.stampContent).catch(console.error);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Restore context
|
|
291
|
+
ctx.restore();
|
|
292
|
+
});
|
|
293
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FilterPreset, AdjustmentsState } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Built-in filter presets
|
|
4
|
+
* Each filter is a combination of adjustment values
|
|
5
|
+
*/
|
|
6
|
+
export declare const FILTER_PRESETS: FilterPreset[];
|
|
7
|
+
/**
|
|
8
|
+
* Get a filter preset by ID
|
|
9
|
+
*/
|
|
10
|
+
export declare function getFilterPreset(id: string): FilterPreset | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Apply a filter preset to adjustments
|
|
13
|
+
*/
|
|
14
|
+
export declare function applyFilterPreset(preset: FilterPreset, baseAdjustments?: AdjustmentsState): AdjustmentsState;
|
|
15
|
+
/**
|
|
16
|
+
* Check if current adjustments match a filter preset
|
|
17
|
+
*/
|
|
18
|
+
export declare function matchesFilterPreset(adjustments: AdjustmentsState, preset: FilterPreset): boolean;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createDefaultAdjustments } from './adjustments';
|
|
2
|
+
/**
|
|
3
|
+
* Built-in filter presets
|
|
4
|
+
* Each filter is a combination of adjustment values
|
|
5
|
+
*/
|
|
6
|
+
export const FILTER_PRESETS = [
|
|
7
|
+
{
|
|
8
|
+
id: 'none',
|
|
9
|
+
name: 'None',
|
|
10
|
+
adjustments: createDefaultAdjustments()
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'vivid',
|
|
14
|
+
name: 'Vivid',
|
|
15
|
+
adjustments: {
|
|
16
|
+
saturation: 40,
|
|
17
|
+
contrast: 20,
|
|
18
|
+
brightness: 5
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'sepia',
|
|
23
|
+
name: 'Sepia',
|
|
24
|
+
adjustments: {
|
|
25
|
+
sepia: 80,
|
|
26
|
+
brightness: -10,
|
|
27
|
+
contrast: -30,
|
|
28
|
+
highlights: -32,
|
|
29
|
+
shadows: 30,
|
|
30
|
+
vignette: -20
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'monochrome',
|
|
35
|
+
name: 'Monochrome',
|
|
36
|
+
adjustments: {
|
|
37
|
+
grayscale: 100,
|
|
38
|
+
contrast: 15
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'vintage',
|
|
43
|
+
name: 'Vintage',
|
|
44
|
+
adjustments: {
|
|
45
|
+
sepia: 50,
|
|
46
|
+
brightness: -15,
|
|
47
|
+
contrast: -10,
|
|
48
|
+
vignette: -40,
|
|
49
|
+
saturation: -20
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'warm',
|
|
54
|
+
name: 'Warm',
|
|
55
|
+
adjustments: {
|
|
56
|
+
sepia: -10,
|
|
57
|
+
saturation: 15,
|
|
58
|
+
brightness: 5,
|
|
59
|
+
exposure: 10,
|
|
60
|
+
hue: -10,
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'cool',
|
|
65
|
+
name: 'Cool',
|
|
66
|
+
adjustments: {
|
|
67
|
+
saturation: 10,
|
|
68
|
+
brightness: -5,
|
|
69
|
+
contrast: 10,
|
|
70
|
+
hue: 15,
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'film',
|
|
75
|
+
name: 'Film',
|
|
76
|
+
adjustments: {
|
|
77
|
+
contrast: 60,
|
|
78
|
+
highlights: -45,
|
|
79
|
+
shadows: -100,
|
|
80
|
+
saturation: 2,
|
|
81
|
+
vignette: -24,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
/**
|
|
86
|
+
* Get a filter preset by ID
|
|
87
|
+
*/
|
|
88
|
+
export function getFilterPreset(id) {
|
|
89
|
+
return FILTER_PRESETS.find(preset => preset.id === id);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Apply a filter preset to adjustments
|
|
93
|
+
*/
|
|
94
|
+
export function applyFilterPreset(preset, baseAdjustments = createDefaultAdjustments()) {
|
|
95
|
+
// Start with base adjustments or defaults
|
|
96
|
+
const result = { ...baseAdjustments };
|
|
97
|
+
// Apply preset values
|
|
98
|
+
Object.entries(preset.adjustments).forEach(([key, value]) => {
|
|
99
|
+
if (value !== undefined) {
|
|
100
|
+
result[key] = value;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if current adjustments match a filter preset
|
|
107
|
+
*/
|
|
108
|
+
export function matchesFilterPreset(adjustments, preset) {
|
|
109
|
+
const presetAdjustments = applyFilterPreset(preset);
|
|
110
|
+
// Compare all adjustment values
|
|
111
|
+
return Object.keys(presetAdjustments).every(key => {
|
|
112
|
+
return adjustments[key] === presetAdjustments[key];
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HistorySnapshot, EditorHistory } from '../types';
|
|
2
|
+
export declare const MAX_HISTORY_SIZE = 50;
|
|
3
|
+
export declare function createEmptyHistory(): EditorHistory;
|
|
4
|
+
export declare function createSnapshot(cropArea: any, transform: any, adjustments: any, viewport: any, blurAreas?: any[], stampAreas?: any[]): HistorySnapshot;
|
|
5
|
+
export declare function addToHistory(history: EditorHistory, snapshot: HistorySnapshot): EditorHistory;
|
|
6
|
+
export declare function undo(history: EditorHistory): {
|
|
7
|
+
history: EditorHistory;
|
|
8
|
+
snapshot: HistorySnapshot | null;
|
|
9
|
+
};
|
|
10
|
+
export declare function redo(history: EditorHistory): {
|
|
11
|
+
history: EditorHistory;
|
|
12
|
+
snapshot: HistorySnapshot | null;
|
|
13
|
+
};
|
|
14
|
+
export declare function canUndo(history: EditorHistory): boolean;
|
|
15
|
+
export declare function canRedo(history: EditorHistory): boolean;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Maximum number of history states to keep
|
|
2
|
+
export const MAX_HISTORY_SIZE = 50;
|
|
3
|
+
export function createEmptyHistory() {
|
|
4
|
+
return {
|
|
5
|
+
past: [],
|
|
6
|
+
present: null,
|
|
7
|
+
future: []
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function createSnapshot(cropArea, transform, adjustments, viewport, blurAreas = [], stampAreas = []) {
|
|
11
|
+
return {
|
|
12
|
+
cropArea: cropArea ? { ...cropArea } : null,
|
|
13
|
+
transform: { ...transform },
|
|
14
|
+
adjustments: { ...adjustments },
|
|
15
|
+
viewport: { ...viewport },
|
|
16
|
+
blurAreas: blurAreas.map(area => ({ ...area })),
|
|
17
|
+
stampAreas: stampAreas.map(area => ({ ...area }))
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function addToHistory(history, snapshot) {
|
|
21
|
+
const newPast = history.present
|
|
22
|
+
? [...history.past, history.present]
|
|
23
|
+
: history.past;
|
|
24
|
+
// Limit history size
|
|
25
|
+
const limitedPast = newPast.slice(-MAX_HISTORY_SIZE);
|
|
26
|
+
return {
|
|
27
|
+
past: limitedPast,
|
|
28
|
+
present: snapshot,
|
|
29
|
+
future: [] // Clear future when adding new state
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function undo(history) {
|
|
33
|
+
if (history.past.length === 0 || !history.present) {
|
|
34
|
+
return { history, snapshot: null };
|
|
35
|
+
}
|
|
36
|
+
const previous = history.past[history.past.length - 1];
|
|
37
|
+
const newPast = history.past.slice(0, -1);
|
|
38
|
+
return {
|
|
39
|
+
history: {
|
|
40
|
+
past: newPast,
|
|
41
|
+
present: previous,
|
|
42
|
+
future: [history.present, ...history.future]
|
|
43
|
+
},
|
|
44
|
+
snapshot: previous
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function redo(history) {
|
|
48
|
+
if (history.future.length === 0 || !history.present) {
|
|
49
|
+
return { history, snapshot: null };
|
|
50
|
+
}
|
|
51
|
+
const next = history.future[0];
|
|
52
|
+
const newFuture = history.future.slice(1);
|
|
53
|
+
return {
|
|
54
|
+
history: {
|
|
55
|
+
past: [...history.past, history.present],
|
|
56
|
+
present: next,
|
|
57
|
+
future: newFuture
|
|
58
|
+
},
|
|
59
|
+
snapshot: next
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function canUndo(history) {
|
|
63
|
+
return history.past.length > 0;
|
|
64
|
+
}
|
|
65
|
+
export function canRedo(history) {
|
|
66
|
+
return history.future.length > 0;
|
|
67
|
+
}
|