Skip to content
File

Blob: src/templates.ts

typescript139 lines
1import 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 */
7export 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 
120export const DEFAULT_TEMPLATE = TEMPLATES[0];
121 
122export 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. */
127export 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}