import { isCanvasState, type CanvasState } from "@dada/shared-contracts"; export type BackgroundAdjustments = CanvasState["background"]["adjustments"]; const elementKeys = new Set([ "colors", "content", "coordinates", "created_at", "dynamic_fields", "element_id", "font_override", "font_size", "formatted_value", "opacity", "position", "resource_version", "rotation", "scale", "style_id", "style_parameters", "template_or_asset_id", "type", "z_index", ]); export function defaultBackgroundAdjustments(): BackgroundAdjustments { return { brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 }; } function pixelsForRatio(ratio: CanvasState["ratio"]) { return ratio === "3:4" ? { pixelHeight: 1440, pixelWidth: 1080 } : ratio === "1:1" ? { pixelHeight: 1080, pixelWidth: 1080 } : ratio === "4:3" ? { pixelHeight: 1080, pixelWidth: 1440 } : { pixelHeight: 1920, pixelWidth: 1080 }; } export function createEditorCanvasState(input: { assetId: string | null; ratio: CanvasState["ratio"] }): CanvasState { const pixels = pixelsForRatio(input.ratio); return { background: { adjustments: defaultBackgroundAdjustments(), asset_id: input.assetId }, elements: [], pixel_height: pixels.pixelHeight, pixel_width: pixels.pixelWidth, ratio: input.ratio, schema_version: 1, }; } function checked(state: CanvasState): CanvasState { if (!isCanvasState(state)) throw new Error("canvas_state_invalid"); return structuredClone(state); } export function updateBackgroundAdjustments(state: CanvasState, patch: Partial): CanvasState { return checked({ ...structuredClone(state), background: { ...state.background, adjustments: { ...state.background.adjustments, ...patch } } }); } export function resetBackgroundAdjustments(state: CanvasState): CanvasState { return checked({ ...structuredClone(state), background: { ...state.background, adjustments: defaultBackgroundAdjustments() } }); } export function switchBackground(state: CanvasState, assetId: string, palette: readonly string[]): CanvasState { if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(assetId)) throw new Error("canvas_background_asset_invalid"); if (palette.length !== 5 || palette.some((color) => !/^#[0-9A-Fa-f]{6}$/.test(color))) throw new Error("canvas_palette_invalid"); const next = structuredClone(state); next.background = { asset_id: assetId, adjustments: defaultBackgroundAdjustments() }; next.elements = next.elements.map((element) => element.type === "color_card" ? { ...element, colors: [...palette] } : element); return checked(next); } export class CanvasEditHistory { private current: CanvasState; private readonly past: CanvasState[] = []; private readonly future: CanvasState[] = []; constructor(initial: CanvasState) { this.current = checked(initial); } get value() { return structuredClone(this.current); } get canUndo() { return this.past.length > 0; } get canRedo() { return this.future.length > 0; } commit(next: CanvasState) { this.past.push(this.value); this.current = checked(next); this.future.length = 0; } undo() { const previous = this.past.pop(); if (!previous) return undefined; this.future.push(this.value); this.current = previous; return this.value; } redo() { const next = this.future.pop(); if (!next) return undefined; this.past.push(this.value); this.current = next; return this.value; } } export function deserializeFabricCanvas(input: unknown): CanvasState | undefined { if (!input || typeof input !== "object") return undefined; const value = input as Record; if (!value.background || typeof value.background !== "object" || !Array.isArray(value.elements)) return undefined; const elements = value.elements.map((entry) => { if (!entry || typeof entry !== "object" || Object.keys(entry).some((key) => !elementKeys.has(key))) return undefined; return structuredClone(entry); }); if (elements.some((entry) => entry === undefined)) return undefined; const candidate = { background: structuredClone(value.background), elements, pixel_height: value.pixel_height, pixel_width: value.pixel_width, ratio: value.ratio, schema_version: value.schema_version, }; return isCanvasState(candidate) ? structuredClone(candidate) : undefined; } export function cssFilterForBackground(adjustments: BackgroundAdjustments) { const filter = adjustments.filter === "grayscale" ? "grayscale(1)" : adjustments.filter === "sepia" ? "sepia(0.75)" : "none"; return `${filter} brightness(${100 + adjustments.brightness}%) contrast(${100 + adjustments.contrast}%) saturate(${100 + adjustments.saturation}%)`; }