File
Blob: src/templates.ts
| 1 | import type { Template } from "./types"; |
| 2 | |
| 3 | /** |
| 4 | * Built-in presets. Each is just data — selecting one sets the crop aspect, the |
| 5 | * target output size, and sensible encoder defaults. Add rows freely. |
| 6 | */ |
| 7 | export const TEMPLATES: Template[] = [ |
| 8 | { |
| 9 | id: "original", |
| 10 | name: "Original", |
| 11 | group: "General", |
| 12 | description: "Keep the source aspect ratio and full resolution.", |
| 13 | aspect: null, |
| 14 | output: null, |
| 15 | encoder: { format: "jpeg", quality: 92 }, |
| 16 | }, |
| 17 | { |
| 18 | id: "og-image", |
| 19 | name: "OG image", |
| 20 | group: "Social", |
| 21 | description: "Open Graph cover for link previews. 1200×630.", |
| 22 | aspect: 1200 / 630, |
| 23 | output: { width: 1200, height: 630 }, |
| 24 | encoder: { format: "jpeg", quality: 85 }, |
| 25 | }, |
| 26 | { |
| 27 | id: "twitter-card", |
| 28 | name: "Twitter / X card", |
| 29 | group: "Social", |
| 30 | description: "Summary card with large image. 1200×675.", |
| 31 | aspect: 1200 / 675, |
| 32 | output: { width: 1200, height: 675 }, |
| 33 | encoder: { format: "jpeg", quality: 85 }, |
| 34 | }, |
| 35 | { |
| 36 | id: "instagram-square", |
| 37 | name: "Instagram square", |
| 38 | group: "Social", |
| 39 | description: "Square feed post. 1080×1080.", |
| 40 | aspect: 1, |
| 41 | output: { width: 1080, height: 1080 }, |
| 42 | encoder: { format: "jpeg", quality: 90 }, |
| 43 | }, |
| 44 | { |
| 45 | id: "instagram-portrait", |
| 46 | name: "Instagram portrait", |
| 47 | group: "Social", |
| 48 | description: "Tall 4:5 feed post. 1080×1350.", |
| 49 | aspect: 1080 / 1350, |
| 50 | output: { width: 1080, height: 1350 }, |
| 51 | encoder: { format: "jpeg", quality: 90 }, |
| 52 | }, |
| 53 | { |
| 54 | id: "story", |
| 55 | name: "Story / Reel", |
| 56 | group: "Social", |
| 57 | description: "Full-screen 9:16 vertical. 1080×1920.", |
| 58 | aspect: 1080 / 1920, |
| 59 | output: { width: 1080, height: 1920 }, |
| 60 | encoder: { format: "jpeg", quality: 88 }, |
| 61 | }, |
| 62 | { |
| 63 | id: "youtube-thumb", |
| 64 | name: "YouTube thumb", |
| 65 | group: "Social", |
| 66 | description: "Video thumbnail. 1280×720.", |
| 67 | aspect: 1280 / 720, |
| 68 | output: { width: 1280, height: 720 }, |
| 69 | encoder: { format: "jpeg", quality: 88 }, |
| 70 | }, |
| 71 | { |
| 72 | id: "linkedin-cover", |
| 73 | name: "LinkedIn cover", |
| 74 | group: "Social", |
| 75 | description: "Profile banner. 1584×396.", |
| 76 | aspect: 1584 / 396, |
| 77 | output: { width: 1584, height: 396 }, |
| 78 | encoder: { format: "jpeg", quality: 85 }, |
| 79 | }, |
| 80 | { |
| 81 | id: "web-banner", |
| 82 | name: "Web banner", |
| 83 | group: "General", |
| 84 | description: "Full-HD hero / header. 1920×1080 WebP.", |
| 85 | aspect: 1920 / 1080, |
| 86 | output: { width: 1920, height: 1080 }, |
| 87 | encoder: { format: "webp", quality: 82 }, |
| 88 | }, |
| 89 | { |
| 90 | id: "avatar", |
| 91 | name: "Avatar", |
| 92 | group: "Profile", |
| 93 | description: "Square profile picture. 512×512, lossless.", |
| 94 | aspect: 1, |
| 95 | output: { width: 512, height: 512 }, |
| 96 | encoder: { format: "webp", quality: 92 }, |
| 97 | }, |
| 98 | { |
| 99 | id: "favicon", |
| 100 | name: "Favicon", |
| 101 | group: "Profile", |
| 102 | description: "Square site icon. 256×256 PNG.", |
| 103 | aspect: 1, |
| 104 | output: { width: 256, height: 256 }, |
| 105 | encoder: { format: "png", quality: 100 }, |
| 106 | }, |
| 107 | { |
| 108 | id: "passport-us", |
| 109 | name: "US passport", |
| 110 | group: "Identity", |
| 111 | description: "2×2 inch at 300 DPI. 600×600. Position the face manually for now.", |
| 112 | aspect: 1, |
| 113 | output: { width: 600, height: 600 }, |
| 114 | encoder: { format: "jpeg", quality: 95 }, |
| 115 | // Reserved for phase-2 face-aware auto-framing (MediaPipe / @imgly/background-removal). |
| 116 | framing: { faceHeightRatio: [0.5, 0.69], eyeLineFromTop: 0.43 }, |
| 117 | }, |
| 118 | ]; |
| 119 | |
| 120 | export const DEFAULT_TEMPLATE = TEMPLATES[0]; |
| 121 | |
| 122 | export function getTemplate(id: string): Template { |
| 123 | return TEMPLATES.find((t) => t.id === id) ?? DEFAULT_TEMPLATE; |
| 124 | } |
| 125 | |
| 126 | /** Templates grouped for the picker UI, preserving declaration order. */ |
| 127 | export function groupedTemplates(): { group: string; items: Template[] }[] { |
| 128 | const groups: { group: string; items: Template[] }[] = []; |
| 129 | for (const t of TEMPLATES) { |
| 130 | let bucket = groups.find((g) => g.group === t.group); |
| 131 | if (!bucket) { |
| 132 | bucket = { group: t.group, items: [] }; |
| 133 | groups.push(bucket); |
| 134 | } |
| 135 | bucket.items.push(t); |
| 136 | } |
| 137 | return groups; |
| 138 | } |