113 lines
4.7 KiB
TypeScript
113 lines
4.7 KiB
TypeScript
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<BackgroundAdjustments>): 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<string, unknown>;
|
|
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}%)`;
|
|
}
|