File
Blob: src/hooks/useEditor.ts
| 1 | import { useReducer } from "react"; |
| 2 | import type { Adjustments, ExportFormat, PixelRect, Template } from "../types"; |
| 3 | import { NEUTRAL_ADJUSTMENTS } from "../types"; |
| 4 | import { DEFAULT_TEMPLATE } from "../templates"; |
| 5 | |
| 6 | export const CUSTOM_TEMPLATE_ID = "custom"; |
| 7 | |
| 8 | export 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 | |
| 24 | function 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 | |
| 42 | export const initialEditorState = fromTemplate(DEFAULT_TEMPLATE); |
| 43 | |
| 44 | function wrap360(deg: number): number { |
| 45 | return ((deg % 360) + 360) % 360; |
| 46 | } |
| 47 | |
| 48 | export 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 | |
| 66 | function 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 | |
| 129 | export function useEditor() { |
| 130 | return useReducer(reducer, initialEditorState); |
| 131 | } |