Skip to content
File

Blob: src/hooks/useEditor.ts

typescript132 lines
1import { useReducer } from "react";
2import type { Adjustments, ExportFormat, PixelRect, Template } from "../types";
3import { NEUTRAL_ADJUSTMENTS } from "../types";
4import { DEFAULT_TEMPLATE } from "../templates";
5 
6export const CUSTOM_TEMPLATE_ID = "custom";
7 
8export interface EditorState {
9 crop: { x: number; y: number };
10 zoom: number;
11 rotation: number;
12 flipH: boolean;
13 flipV: boolean;
14 aspect: number | null;
15 output: { width: number; height: number } | null;
16 maxEdge: number | null;
17 adjustments: Adjustments;
18 templateId: string;
19 format: ExportFormat;
20 quality: number;
21 croppedAreaPixels: PixelRect | null;
22}
23 
24function fromTemplate(t: Template): EditorState {
25 return {
26 crop: { x: 0, y: 0 },
27 zoom: 1,
28 rotation: 0,
29 flipH: false,
30 flipV: false,
31 aspect: t.aspect,
32 output: t.output,
33 maxEdge: null,
34 adjustments: { ...NEUTRAL_ADJUSTMENTS },
35 templateId: t.id,
36 format: t.encoder.format,
37 quality: t.encoder.quality,
38 croppedAreaPixels: null,
39 };
40}
41 
42export const initialEditorState = fromTemplate(DEFAULT_TEMPLATE);
43 
44function wrap360(deg: number): number {
45 return ((deg % 360) + 360) % 360;
46}
47 
48export type EditorAction =
49 | { type: "selectTemplate"; template: Template }
50 | { type: "setAspect"; aspect: number | null }
51 | { type: "setCrop"; crop: { x: number; y: number } }
52 | { type: "setZoom"; zoom: number }
53 | { type: "setRotation"; rotation: number }
54 | { type: "rotateBy"; delta: number }
55 | { type: "toggleFlipH" }
56 | { type: "toggleFlipV" }
57 | { type: "setAdjustment"; key: keyof Adjustments; value: number }
58 | { type: "setAdjustments"; adjustments: Adjustments }
59 | { type: "resetAdjustments" }
60 | { type: "setMaxEdge"; maxEdge: number | null }
61 | { type: "setFormat"; format: ExportFormat }
62 | { type: "setQuality"; quality: number }
63 | { type: "setCroppedAreaPixels"; rect: PixelRect }
64 | { type: "resetEdits" };
65 
66function reducer(state: EditorState, action: EditorAction): EditorState {
67 switch (action.type) {
68 case "selectTemplate": {
69 const t = action.template;
70 return {
71 ...state,
72 templateId: t.id,
73 aspect: t.aspect,
74 output: t.output,
75 format: t.encoder.format,
76 quality: t.encoder.quality,
77 crop: { x: 0, y: 0 },
78 zoom: 1,
79 };
80 }
81 case "setAspect":
82 return {
83 ...state,
84 aspect: action.aspect,
85 output: null,
86 templateId: CUSTOM_TEMPLATE_ID,
87 crop: { x: 0, y: 0 },
88 zoom: 1,
89 };
90 case "setCrop":
91 return { ...state, crop: action.crop };
92 case "setZoom":
93 return { ...state, zoom: action.zoom };
94 case "setRotation":
95 return { ...state, rotation: wrap360(action.rotation) };
96 case "rotateBy":
97 return { ...state, rotation: wrap360(state.rotation + action.delta) };
98 case "toggleFlipH":
99 return { ...state, flipH: !state.flipH };
100 case "toggleFlipV":
101 return { ...state, flipV: !state.flipV };
102 case "setAdjustment":
103 return { ...state, adjustments: { ...state.adjustments, [action.key]: action.value } };
104 case "setAdjustments":
105 return { ...state, adjustments: action.adjustments };
106 case "resetAdjustments":
107 return { ...state, adjustments: { ...NEUTRAL_ADJUSTMENTS } };
108 case "setMaxEdge":
109 return { ...state, maxEdge: action.maxEdge };
110 case "setFormat":
111 return { ...state, format: action.format };
112 case "setQuality":
113 return { ...state, quality: action.quality };
114 case "setCroppedAreaPixels":
115 return { ...state, croppedAreaPixels: action.rect };
116 case "resetEdits":
117 return {
118 ...state,
119 crop: { x: 0, y: 0 },
120 zoom: 1,
121 rotation: 0,
122 flipH: false,
123 flipV: false,
124 adjustments: { ...NEUTRAL_ADJUSTMENTS },
125 };
126 }
127}
128 
129export function useEditor() {
130 return useReducer(reducer, initialEditorState);
131}