@rajeev02/camera 0.1.0

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.
@@ -0,0 +1,451 @@
1
+ /**
2
+ * @rajeev02/camera — Photo Editor
3
+ * Full photo editing: crop, rotate, adjust, draw, text, stickers, blur, frames, heal, collage
4
+ */
5
+
6
+ export type EditTool =
7
+ | "crop"
8
+ | "rotate"
9
+ | "flip"
10
+ | "adjust"
11
+ | "filter"
12
+ | "draw"
13
+ | "text"
14
+ | "sticker"
15
+ | "blur"
16
+ | "frame"
17
+ | "vignette"
18
+ | "sharpen"
19
+ | "heal"
20
+ | "red_eye"
21
+ | "perspective"
22
+ | "collage"
23
+ | "background_remove"
24
+ | "resize";
25
+
26
+ export interface CropPreset {
27
+ id: string;
28
+ label: string;
29
+ aspectRatio: number | null; // null = freeform
30
+ }
31
+
32
+ export interface AdjustmentValues {
33
+ brightness: number; // -100 to 100
34
+ contrast: number; // -100 to 100
35
+ saturation: number; // -100 to 100
36
+ exposure: number; // -100 to 100
37
+ highlights: number; // -100 to 100
38
+ shadows: number; // -100 to 100
39
+ warmth: number; // -100 to 100 (temperature)
40
+ tint: number; // -100 to 100
41
+ sharpness: number; // 0 to 100
42
+ grain: number; // 0 to 100
43
+ vignette: number; // 0 to 100
44
+ clarity: number; // -100 to 100
45
+ fadeAmount: number; // 0 to 100
46
+ hue: number; // -180 to 180
47
+ }
48
+
49
+ export interface DrawConfig {
50
+ color: string;
51
+ brushSize: number; // 1-50
52
+ brushType:
53
+ | "pen"
54
+ | "marker"
55
+ | "pencil"
56
+ | "spray"
57
+ | "neon"
58
+ | "eraser"
59
+ | "blur_brush";
60
+ opacity: number; // 0-100
61
+ }
62
+
63
+ export interface TextOverlay {
64
+ id: string;
65
+ text: string;
66
+ fontFamily: string;
67
+ fontSize: number;
68
+ color: string;
69
+ backgroundColor?: string;
70
+ bold: boolean;
71
+ italic: boolean;
72
+ underline: boolean;
73
+ alignment: "left" | "center" | "right";
74
+ rotation: number;
75
+ position: { x: number; y: number };
76
+ opacity: number;
77
+ shadow?: { color: string; offsetX: number; offsetY: number; blur: number };
78
+ outline?: { color: string; width: number };
79
+ }
80
+
81
+ export interface StickerOverlay {
82
+ id: string;
83
+ source: string; // URI or asset name
84
+ position: { x: number; y: number };
85
+ scale: number;
86
+ rotation: number;
87
+ opacity: number;
88
+ flipHorizontal: boolean;
89
+ }
90
+
91
+ export interface BlurRegion {
92
+ id: string;
93
+ type: "circle" | "rectangle" | "freeform";
94
+ center: { x: number; y: number };
95
+ radius?: number;
96
+ width?: number;
97
+ height?: number;
98
+ intensity: number; // 1-100
99
+ blurType: "gaussian" | "motion" | "radial" | "pixelate";
100
+ /** Freeform path points */
101
+ path?: { x: number; y: number }[];
102
+ }
103
+
104
+ export interface FrameConfig {
105
+ id: string;
106
+ type:
107
+ | "solid"
108
+ | "gradient"
109
+ | "pattern"
110
+ | "rounded"
111
+ | "polaroid"
112
+ | "film_strip"
113
+ | "torn_paper";
114
+ color?: string;
115
+ width: number; // pixels
116
+ cornerRadius?: number;
117
+ innerPadding?: number;
118
+ }
119
+
120
+ export interface EditAction {
121
+ id: string;
122
+ tool: EditTool;
123
+ timestamp: number;
124
+ params: Record<string, unknown>;
125
+ }
126
+
127
+ /**
128
+ * Photo Editor Controller — manages edit state, history, undo/redo
129
+ */
130
+ export class PhotoEditorController {
131
+ private sourceUri: string;
132
+ private adjustments: AdjustmentValues;
133
+ private textOverlays: TextOverlay[] = [];
134
+ private stickerOverlays: StickerOverlay[] = [];
135
+ private blurRegions: BlurRegion[] = [];
136
+ private drawPaths: {
137
+ config: DrawConfig;
138
+ points: { x: number; y: number }[];
139
+ }[] = [];
140
+ private frame: FrameConfig | null = null;
141
+ private cropRegion: {
142
+ x: number;
143
+ y: number;
144
+ width: number;
145
+ height: number;
146
+ } | null = null;
147
+ private rotation: number = 0; // degrees
148
+ private flipH: boolean = false;
149
+ private flipV: boolean = false;
150
+ private history: EditAction[] = [];
151
+ private historyIndex: number = -1;
152
+ private activeFilter: string | null = null;
153
+ private listeners: Set<(event: string) => void> = new Set();
154
+
155
+ constructor(sourceUri: string) {
156
+ this.sourceUri = sourceUri;
157
+ this.adjustments = this.getDefaultAdjustments();
158
+ }
159
+
160
+ // ---- CROP & TRANSFORM ----
161
+
162
+ setCrop(x: number, y: number, width: number, height: number): void {
163
+ this.cropRegion = { x, y, width, height };
164
+ this.addToHistory("crop", { x, y, width, height });
165
+ }
166
+
167
+ rotate(degrees: number): void {
168
+ this.rotation = (this.rotation + degrees) % 360;
169
+ this.addToHistory("rotate", { degrees: this.rotation });
170
+ }
171
+
172
+ rotateLeft(): void {
173
+ this.rotate(-90);
174
+ }
175
+ rotateRight(): void {
176
+ this.rotate(90);
177
+ }
178
+
179
+ flipHorizontal(): void {
180
+ this.flipH = !this.flipH;
181
+ this.addToHistory("flip", { horizontal: true });
182
+ }
183
+ flipVertical(): void {
184
+ this.flipV = !this.flipV;
185
+ this.addToHistory("flip", { vertical: true });
186
+ }
187
+
188
+ straighten(degrees: number): void {
189
+ this.rotation = Math.max(-45, Math.min(45, degrees));
190
+ this.addToHistory("rotate", { degrees });
191
+ }
192
+
193
+ setPerspective(
194
+ topLeft: { x: number; y: number },
195
+ topRight: { x: number; y: number },
196
+ bottomLeft: { x: number; y: number },
197
+ bottomRight: { x: number; y: number },
198
+ ): void {
199
+ this.addToHistory("perspective", {
200
+ topLeft,
201
+ topRight,
202
+ bottomLeft,
203
+ bottomRight,
204
+ });
205
+ }
206
+
207
+ // ---- ADJUSTMENTS ----
208
+
209
+ setAdjustment(key: keyof AdjustmentValues, value: number): void {
210
+ (this.adjustments as unknown as Record<string, number>)[key] = value;
211
+ this.addToHistory("adjust", { key, value });
212
+ }
213
+
214
+ resetAdjustments(): void {
215
+ this.adjustments = this.getDefaultAdjustments();
216
+ }
217
+ getAdjustments(): AdjustmentValues {
218
+ return { ...this.adjustments };
219
+ }
220
+
221
+ // ---- FILTER ----
222
+
223
+ applyFilter(filterId: string): void {
224
+ this.activeFilter = filterId;
225
+ this.addToHistory("filter", { filterId });
226
+ }
227
+
228
+ removeFilter(): void {
229
+ this.activeFilter = null;
230
+ }
231
+ getActiveFilter(): string | null {
232
+ return this.activeFilter;
233
+ }
234
+
235
+ // ---- TEXT ----
236
+
237
+ addText(overlay: Omit<TextOverlay, "id">): string {
238
+ const id = `text_${Date.now()}`;
239
+ this.textOverlays.push({ ...overlay, id });
240
+ this.addToHistory("text", { id, action: "add" });
241
+ return id;
242
+ }
243
+
244
+ updateText(id: string, updates: Partial<TextOverlay>): void {
245
+ const t = this.textOverlays.find((o) => o.id === id);
246
+ if (t) Object.assign(t, updates);
247
+ }
248
+
249
+ removeText(id: string): void {
250
+ this.textOverlays = this.textOverlays.filter((o) => o.id !== id);
251
+ }
252
+
253
+ getTextOverlays(): TextOverlay[] {
254
+ return [...this.textOverlays];
255
+ }
256
+
257
+ // ---- STICKERS ----
258
+
259
+ addSticker(sticker: Omit<StickerOverlay, "id">): string {
260
+ const id = `sticker_${Date.now()}`;
261
+ this.stickerOverlays.push({ ...sticker, id });
262
+ this.addToHistory("sticker", { id, action: "add" });
263
+ return id;
264
+ }
265
+
266
+ updateSticker(id: string, updates: Partial<StickerOverlay>): void {
267
+ const s = this.stickerOverlays.find((o) => o.id === id);
268
+ if (s) Object.assign(s, updates);
269
+ }
270
+
271
+ removeSticker(id: string): void {
272
+ this.stickerOverlays = this.stickerOverlays.filter((o) => o.id !== id);
273
+ }
274
+
275
+ getStickers(): StickerOverlay[] {
276
+ return [...this.stickerOverlays];
277
+ }
278
+
279
+ // ---- DRAWING ----
280
+
281
+ startDrawPath(config: DrawConfig): void {
282
+ this.drawPaths.push({ config: { ...config }, points: [] });
283
+ }
284
+
285
+ addDrawPoint(x: number, y: number): void {
286
+ const current = this.drawPaths[this.drawPaths.length - 1];
287
+ if (current) current.points.push({ x, y });
288
+ }
289
+
290
+ endDrawPath(): void {
291
+ this.addToHistory("draw", { pathIndex: this.drawPaths.length - 1 });
292
+ }
293
+
294
+ clearDrawing(): void {
295
+ this.drawPaths = [];
296
+ }
297
+ getDrawPaths(): typeof this.drawPaths {
298
+ return [...this.drawPaths];
299
+ }
300
+
301
+ // ---- BLUR ----
302
+
303
+ addBlurRegion(region: Omit<BlurRegion, "id">): string {
304
+ const id = `blur_${Date.now()}`;
305
+ this.blurRegions.push({ ...region, id });
306
+ return id;
307
+ }
308
+
309
+ removeBlurRegion(id: string): void {
310
+ this.blurRegions = this.blurRegions.filter((r) => r.id !== id);
311
+ }
312
+
313
+ getBlurRegions(): BlurRegion[] {
314
+ return [...this.blurRegions];
315
+ }
316
+
317
+ // ---- FRAME ----
318
+
319
+ setFrame(frame: FrameConfig): void {
320
+ this.frame = frame;
321
+ }
322
+ removeFrame(): void {
323
+ this.frame = null;
324
+ }
325
+ getFrame(): FrameConfig | null {
326
+ return this.frame;
327
+ }
328
+
329
+ // ---- HISTORY (Undo/Redo) ----
330
+
331
+ undo(): boolean {
332
+ if (this.historyIndex < 0) return false;
333
+ this.historyIndex--;
334
+ this.emit("undo");
335
+ return true;
336
+ }
337
+
338
+ redo(): boolean {
339
+ if (this.historyIndex >= this.history.length - 1) return false;
340
+ this.historyIndex++;
341
+ this.emit("redo");
342
+ return true;
343
+ }
344
+
345
+ canUndo(): boolean {
346
+ return this.historyIndex >= 0;
347
+ }
348
+ canRedo(): boolean {
349
+ return this.historyIndex < this.history.length - 1;
350
+ }
351
+ getHistoryCount(): number {
352
+ return this.history.length;
353
+ }
354
+
355
+ // ---- EXPORT ----
356
+
357
+ getEditState(): Record<string, unknown> {
358
+ return {
359
+ sourceUri: this.sourceUri,
360
+ adjustments: this.adjustments,
361
+ textOverlays: this.textOverlays,
362
+ stickerOverlays: this.stickerOverlays,
363
+ blurRegions: this.blurRegions,
364
+ drawPaths: this.drawPaths,
365
+ frame: this.frame,
366
+ cropRegion: this.cropRegion,
367
+ rotation: this.rotation,
368
+ flipH: this.flipH,
369
+ flipV: this.flipV,
370
+ activeFilter: this.activeFilter,
371
+ };
372
+ }
373
+
374
+ /** Reset all edits */
375
+ resetAll(): void {
376
+ this.adjustments = this.getDefaultAdjustments();
377
+ this.textOverlays = [];
378
+ this.stickerOverlays = [];
379
+ this.blurRegions = [];
380
+ this.drawPaths = [];
381
+ this.frame = null;
382
+ this.cropRegion = null;
383
+ this.rotation = 0;
384
+ this.flipH = false;
385
+ this.flipV = false;
386
+ this.activeFilter = null;
387
+ this.history = [];
388
+ this.historyIndex = -1;
389
+ }
390
+
391
+ on(listener: (event: string) => void): () => void {
392
+ this.listeners.add(listener);
393
+ return () => this.listeners.delete(listener);
394
+ }
395
+
396
+ private addToHistory(tool: string, params: Record<string, unknown>): void {
397
+ // Remove any redo history
398
+ this.history = this.history.slice(0, this.historyIndex + 1);
399
+ this.history.push({
400
+ id: `action_${Date.now()}`,
401
+ tool: tool as EditTool,
402
+ timestamp: Date.now(),
403
+ params,
404
+ });
405
+ this.historyIndex = this.history.length - 1;
406
+ }
407
+
408
+ private emit(event: string): void {
409
+ for (const l of this.listeners) {
410
+ try {
411
+ l(event);
412
+ } catch {}
413
+ }
414
+ }
415
+
416
+ private getDefaultAdjustments(): AdjustmentValues {
417
+ return {
418
+ brightness: 0,
419
+ contrast: 0,
420
+ saturation: 0,
421
+ exposure: 0,
422
+ highlights: 0,
423
+ shadows: 0,
424
+ warmth: 0,
425
+ tint: 0,
426
+ sharpness: 0,
427
+ grain: 0,
428
+ vignette: 0,
429
+ clarity: 0,
430
+ fadeAmount: 0,
431
+ hue: 0,
432
+ };
433
+ }
434
+ }
435
+
436
+ /** Standard crop presets */
437
+ export function getCropPresets(): CropPreset[] {
438
+ return [
439
+ { id: "free", label: "Free", aspectRatio: null },
440
+ { id: "original", label: "Original", aspectRatio: null },
441
+ { id: "1:1", label: "Square", aspectRatio: 1 },
442
+ { id: "4:3", label: "4:3", aspectRatio: 4 / 3 },
443
+ { id: "3:4", label: "3:4", aspectRatio: 3 / 4 },
444
+ { id: "16:9", label: "16:9", aspectRatio: 16 / 9 },
445
+ { id: "9:16", label: "9:16 (Story)", aspectRatio: 9 / 16 },
446
+ { id: "3:2", label: "3:2", aspectRatio: 3 / 2 },
447
+ { id: "2:3", label: "2:3", aspectRatio: 2 / 3 },
448
+ { id: "aadhaar", label: "Aadhaar Size", aspectRatio: 3.5 / 4.5 },
449
+ { id: "passport", label: "Passport Size", aspectRatio: 3.5 / 4.5 },
450
+ ];
451
+ }