From 4254dec19f4024e33cdedeee936864c233c66cf4 Mon Sep 17 00:00:00 2001 From: kseungyong Date: Mon, 18 May 2026 17:48:53 +0900 Subject: [PATCH] feat: add named canvas presets with YouTube Shorts entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces NAMED_CANVAS_PRESETS with id/label/aspectRatio/projectType metadata so the UI can show meaningful preset names (e.g. "YouTube Shorts (1080×1920)") and so downstream code can resolve a preset by intent rather than dimensions. - NAMED_CANVAS_PRESETS: landscape 16:9, YouTube Shorts 9:16, square, landscape 4:3 - SHORTS_CANVAS_PRESET convenience export for 9:16 vertical - DEFAULT_CANVAS_PRESETS preserved (derived from NAMED) so existing editor-store consumers keep working - Unit tests verify shape, ordering, and back-compat derivation --- apps/web/src/canvas/__tests__/sizes.test.ts | 58 ++++++++++++++++++ apps/web/src/canvas/sizes.ts | 67 ++++++++++++++++++--- 2 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/canvas/__tests__/sizes.test.ts diff --git a/apps/web/src/canvas/__tests__/sizes.test.ts b/apps/web/src/canvas/__tests__/sizes.test.ts new file mode 100644 index 00000000..ff7191e2 --- /dev/null +++ b/apps/web/src/canvas/__tests__/sizes.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from "bun:test"; +import { + DEFAULT_CANVAS_PRESETS, + DEFAULT_CANVAS_SIZE, + NAMED_CANVAS_PRESETS, + SHORTS_CANVAS_PRESET, +} from "@/canvas/sizes"; + +describe("NAMED_CANVAS_PRESETS", () => { + test("has the four canonical presets in declared order", () => { + const ids = NAMED_CANVAS_PRESETS.map((p) => p.id); + expect(ids).toEqual([ + "landscape-1080p", + "shorts-1080x1920", + "square-1080", + "landscape-1440", + ]); + }); + + test("every preset has consistent shape", () => { + for (const preset of NAMED_CANVAS_PRESETS) { + expect(preset.id.length).toBeGreaterThan(0); + expect(preset.label.length).toBeGreaterThan(0); + expect(preset.size.width).toBeGreaterThan(0); + expect(preset.size.height).toBeGreaterThan(0); + expect(preset.aspectRatio).toMatch(/^\d+:\d+$/); + } + }); +}); + +describe("SHORTS_CANVAS_PRESET", () => { + test("is 1080x1920 vertical 9:16", () => { + expect(SHORTS_CANVAS_PRESET.size).toEqual({ width: 1080, height: 1920 }); + expect(SHORTS_CANVAS_PRESET.aspectRatio).toBe("9:16"); + expect(SHORTS_CANVAS_PRESET.projectType).toBe("shorts"); + }); +}); + +describe("DEFAULT_CANVAS_PRESETS (back-compat)", () => { + test("derives from NAMED_CANVAS_PRESETS in order", () => { + expect(DEFAULT_CANVAS_PRESETS).toEqual( + NAMED_CANVAS_PRESETS.map((p) => p.size), + ); + }); + + test("contains the Shorts preset 1080x1920", () => { + expect(DEFAULT_CANVAS_PRESETS).toContainEqual({ + width: 1080, + height: 1920, + }); + }); +}); + +describe("DEFAULT_CANVAS_SIZE", () => { + test("remains 1920x1080 (landscape default)", () => { + expect(DEFAULT_CANVAS_SIZE).toEqual({ width: 1920, height: 1080 }); + }); +}); diff --git a/apps/web/src/canvas/sizes.ts b/apps/web/src/canvas/sizes.ts index 76361336..a49d55b3 100644 --- a/apps/web/src/canvas/sizes.ts +++ b/apps/web/src/canvas/sizes.ts @@ -1,10 +1,63 @@ -import type { TCanvasSize } from "@/project/types"; +import type { TCanvasSize, TProjectType } from "@/project/types"; -export const DEFAULT_CANVAS_PRESETS: TCanvasSize[] = [ - { width: 1920, height: 1080 }, - { width: 1080, height: 1920 }, - { width: 1080, height: 1080 }, - { width: 1440, height: 1080 }, +/** + * A canvas preset bundles the raw size with display metadata (label, + * aspect ratio) and the project intent it implies. UIs should prefer + * NAMED_CANVAS_PRESETS when rendering preset pickers; consumers that + * only need the size array continue to use DEFAULT_CANVAS_PRESETS. + */ +export interface TCanvasPreset { + id: string; + label: string; + size: TCanvasSize; + aspectRatio: string; + projectType?: TProjectType; +} + +export const NAMED_CANVAS_PRESETS: readonly TCanvasPreset[] = [ + { + id: "landscape-1080p", + label: "Landscape 1920×1080", + size: { width: 1920, height: 1080 }, + aspectRatio: "16:9", + projectType: "standard", + }, + { + id: "shorts-1080x1920", + label: "YouTube Shorts (1080×1920)", + size: { width: 1080, height: 1920 }, + aspectRatio: "9:16", + projectType: "shorts", + }, + { + id: "square-1080", + label: "Square 1080×1080", + size: { width: 1080, height: 1080 }, + aspectRatio: "1:1", + projectType: "standard", + }, + { + id: "landscape-1440", + label: "Landscape 1440×1080", + size: { width: 1440, height: 1080 }, + aspectRatio: "4:3", + projectType: "standard", + }, ]; -export const DEFAULT_CANVAS_SIZE: TCanvasSize = { width: 1920, height: 1080 }; +/** + * Convenience reference to the YouTube Shorts preset. + */ +export const SHORTS_CANVAS_PRESET: TCanvasPreset = NAMED_CANVAS_PRESETS.find( + (p) => p.id === "shorts-1080x1920", +)!; + +/** + * Back-compat: existing consumers (editor-store, etc.) read a bare + * TCanvasSize[]. Derived from NAMED_CANVAS_PRESETS to keep them in sync. + */ +export const DEFAULT_CANVAS_PRESETS: TCanvasSize[] = NAMED_CANVAS_PRESETS.map( + (p) => p.size, +); + +export const DEFAULT_CANVAS_SIZE: TCanvasSize = NAMED_CANVAS_PRESETS[0].size;