picture-it 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,457 @@
1
+ import type { Overlay, CompositionPlan } from "../types.ts";
2
+
3
+ export interface TemplateResult {
4
+ overlays: Overlay[];
5
+ background: string; // CSS gradient
6
+ }
7
+
8
+ export type TemplateFunction = (
9
+ data: Record<string, unknown>,
10
+ width: number,
11
+ height: number
12
+ ) => TemplateResult;
13
+
14
+ export const TEMPLATES: Record<string, TemplateFunction> = {
15
+ "vs-comparison": vsComparison,
16
+ "feature-hero": featureHero,
17
+ "text-hero": textHero,
18
+ "social-card": socialCard,
19
+ };
20
+
21
+ function vsComparison(data: Record<string, unknown>, w: number, h: number): TemplateResult {
22
+ const leftLogo = (data["leftLogo"] || data["left-logo"]) as string;
23
+ const rightLogo = (data["rightLogo"] || data["right-logo"]) as string;
24
+ const vsText = (data["vsText"] as string) || "VS";
25
+ const glowLeft = (data["glowColorLeft"] || data["glow-color"] || "#7c3aed") as string;
26
+ const glowRight = (data["glowColorRight"] || "#ef4444") as string;
27
+ const leftLabel = data["leftLabel"] as string | undefined;
28
+ const rightLabel = data["rightLabel"] as string | undefined;
29
+ const bg = (data["background"] as string) || `linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #0f0f23 100%)`;
30
+
31
+ const logoSize = Math.round(Math.min(w, h) * 0.25);
32
+ const overlays: Overlay[] = [];
33
+
34
+ // Left logo
35
+ if (leftLogo) {
36
+ overlays.push({
37
+ type: "image",
38
+ src: leftLogo,
39
+ zone: "left-third",
40
+ width: logoSize,
41
+ height: logoSize,
42
+ anchor: "center",
43
+ glow: { color: glowLeft, blur: 30, spread: 10 },
44
+ shadow: "auto",
45
+ depth: "foreground",
46
+ });
47
+ }
48
+
49
+ // Right logo
50
+ if (rightLogo) {
51
+ overlays.push({
52
+ type: "image",
53
+ src: rightLogo,
54
+ zone: "right-third",
55
+ width: logoSize,
56
+ height: logoSize,
57
+ anchor: "center",
58
+ glow: { color: glowRight, blur: 30, spread: 10 },
59
+ shadow: "auto",
60
+ depth: "foreground",
61
+ });
62
+ }
63
+
64
+ // VS text
65
+ overlays.push({
66
+ type: "satori-text",
67
+ jsx: {
68
+ tag: "div",
69
+ props: {
70
+ style: {
71
+ display: "flex",
72
+ alignItems: "center",
73
+ justifyContent: "center",
74
+ width: "100%",
75
+ height: "100%",
76
+ },
77
+ },
78
+ children: [
79
+ {
80
+ tag: "span",
81
+ props: {
82
+ style: {
83
+ fontSize: Math.round(h * 0.15),
84
+ fontFamily: "Space Grotesk",
85
+ fontWeight: 700,
86
+ color: "white",
87
+ textShadow: "0 0 40px rgba(255,255,255,0.3)",
88
+ letterSpacing: "0.05em",
89
+ },
90
+ },
91
+ children: [vsText],
92
+ },
93
+ ],
94
+ },
95
+ zone: "hero-center",
96
+ width: Math.round(w * 0.2),
97
+ height: Math.round(h * 0.3),
98
+ anchor: "center",
99
+ depth: "overlay",
100
+ });
101
+
102
+ // Labels
103
+ if (leftLabel) {
104
+ overlays.push({
105
+ type: "satori-text",
106
+ jsx: {
107
+ tag: "div",
108
+ props: {
109
+ style: { display: "flex", alignItems: "center", justifyContent: "center", width: "100%", height: "100%" },
110
+ },
111
+ children: [{
112
+ tag: "span",
113
+ props: {
114
+ style: {
115
+ fontSize: Math.round(h * 0.04),
116
+ fontFamily: "Inter",
117
+ fontWeight: 600,
118
+ color: "rgba(255,255,255,0.8)",
119
+ textShadow: "0 2px 4px rgba(0,0,0,0.5)",
120
+ },
121
+ },
122
+ children: [leftLabel],
123
+ }],
124
+ },
125
+ zone: { x: 25, y: 75 },
126
+ width: Math.round(w * 0.3),
127
+ height: Math.round(h * 0.08),
128
+ anchor: "center",
129
+ depth: "overlay",
130
+ });
131
+ }
132
+
133
+ if (rightLabel) {
134
+ overlays.push({
135
+ type: "satori-text",
136
+ jsx: {
137
+ tag: "div",
138
+ props: {
139
+ style: { display: "flex", alignItems: "center", justifyContent: "center", width: "100%", height: "100%" },
140
+ },
141
+ children: [{
142
+ tag: "span",
143
+ props: {
144
+ style: {
145
+ fontSize: Math.round(h * 0.04),
146
+ fontFamily: "Inter",
147
+ fontWeight: 600,
148
+ color: "rgba(255,255,255,0.8)",
149
+ textShadow: "0 2px 4px rgba(0,0,0,0.5)",
150
+ },
151
+ },
152
+ children: [rightLabel],
153
+ }],
154
+ },
155
+ zone: { x: 75, y: 75 },
156
+ width: Math.round(w * 0.3),
157
+ height: Math.round(h * 0.08),
158
+ anchor: "center",
159
+ depth: "overlay",
160
+ });
161
+ }
162
+
163
+ return { overlays, background: bg };
164
+ }
165
+
166
+ function featureHero(data: Record<string, unknown>, w: number, h: number): TemplateResult {
167
+ const logo = data["logo"] as string | undefined;
168
+ const title = (data["title"] as string) || "";
169
+ const subtitle = data["subtitle"] as string | undefined;
170
+ const glowColor = (data["glowColor"] || data["glow-color"] || "#3b82f6") as string;
171
+ const position = (data["position"] as string) || "center";
172
+ const bg = (data["background"] as string) || `linear-gradient(135deg, #0a0a1a 0%, #1a1a3e 100%)`;
173
+
174
+ const overlays: Overlay[] = [];
175
+
176
+ const xZone = position === "left" ? "center-left" : position === "right" ? "center-right" : "hero-center";
177
+
178
+ if (logo) {
179
+ const logoSize = Math.round(Math.min(w, h) * 0.3);
180
+ overlays.push({
181
+ type: "image",
182
+ src: logo,
183
+ zone: xZone as any,
184
+ width: logoSize,
185
+ height: logoSize,
186
+ anchor: "center",
187
+ glow: { color: glowColor, blur: 40, spread: 15 },
188
+ shadow: "auto",
189
+ depth: "foreground",
190
+ });
191
+ }
192
+
193
+ // Title + subtitle grouped
194
+ const textChildren: any[] = [
195
+ {
196
+ tag: "span",
197
+ props: {
198
+ style: {
199
+ fontSize: Math.round(h * 0.08),
200
+ fontFamily: "Space Grotesk",
201
+ fontWeight: 700,
202
+ color: "white",
203
+ textShadow: "0 2px 10px rgba(0,0,0,0.5)",
204
+ textAlign: "center",
205
+ },
206
+ },
207
+ children: [title],
208
+ },
209
+ ];
210
+
211
+ if (subtitle) {
212
+ textChildren.push({
213
+ tag: "span",
214
+ props: {
215
+ style: {
216
+ fontSize: Math.round(h * 0.04),
217
+ fontFamily: "Inter",
218
+ fontWeight: 400,
219
+ color: "rgba(255,255,255,0.7)",
220
+ textShadow: "0 1px 4px rgba(0,0,0,0.5)",
221
+ textAlign: "center",
222
+ marginTop: 12,
223
+ },
224
+ },
225
+ children: [subtitle],
226
+ });
227
+ }
228
+
229
+ overlays.push({
230
+ type: "satori-text",
231
+ jsx: {
232
+ tag: "div",
233
+ props: {
234
+ style: {
235
+ display: "flex",
236
+ flexDirection: "column",
237
+ alignItems: "center",
238
+ justifyContent: "center",
239
+ width: "100%",
240
+ height: "100%",
241
+ },
242
+ },
243
+ children: textChildren,
244
+ },
245
+ zone: "title-area",
246
+ width: Math.round(w * 0.8),
247
+ height: Math.round(h * 0.25),
248
+ anchor: "center",
249
+ depth: "overlay",
250
+ });
251
+
252
+ return { overlays, background: bg };
253
+ }
254
+
255
+ function textHero(data: Record<string, unknown>, w: number, h: number): TemplateResult {
256
+ const title = (data["title"] as string) || "";
257
+ const subtitle = data["subtitle"] as string | undefined;
258
+ const badge = data["badge"] as string | undefined;
259
+ const textColor = (data["textColor"] as string) || "white";
260
+ const bg = (data["background"] as string) || `linear-gradient(135deg, #667eea 0%, #764ba2 100%)`;
261
+
262
+ const overlays: Overlay[] = [];
263
+ const children: any[] = [];
264
+
265
+ if (badge) {
266
+ children.push({
267
+ tag: "div",
268
+ props: {
269
+ style: {
270
+ display: "flex",
271
+ backgroundColor: "rgba(255,255,255,0.15)",
272
+ borderRadius: 20,
273
+ padding: "6px 16px",
274
+ marginBottom: 16,
275
+ },
276
+ },
277
+ children: [{
278
+ tag: "span",
279
+ props: {
280
+ style: {
281
+ fontSize: Math.round(h * 0.03),
282
+ fontFamily: "Inter",
283
+ fontWeight: 600,
284
+ color: textColor,
285
+ },
286
+ },
287
+ children: [badge],
288
+ }],
289
+ });
290
+ }
291
+
292
+ children.push({
293
+ tag: "span",
294
+ props: {
295
+ style: {
296
+ fontSize: Math.round(h * 0.1),
297
+ fontFamily: "Space Grotesk",
298
+ fontWeight: 700,
299
+ color: textColor,
300
+ textShadow: "0 2px 10px rgba(0,0,0,0.3)",
301
+ textAlign: "center",
302
+ lineHeight: 1.1,
303
+ },
304
+ },
305
+ children: [title],
306
+ });
307
+
308
+ if (subtitle) {
309
+ children.push({
310
+ tag: "span",
311
+ props: {
312
+ style: {
313
+ fontSize: Math.round(h * 0.04),
314
+ fontFamily: "Inter",
315
+ fontWeight: 400,
316
+ color: textColor,
317
+ opacity: 0.8,
318
+ textAlign: "center",
319
+ marginTop: 16,
320
+ },
321
+ },
322
+ children: [subtitle],
323
+ });
324
+ }
325
+
326
+ overlays.push({
327
+ type: "satori-text",
328
+ jsx: {
329
+ tag: "div",
330
+ props: {
331
+ style: {
332
+ display: "flex",
333
+ flexDirection: "column",
334
+ alignItems: "center",
335
+ justifyContent: "center",
336
+ width: "100%",
337
+ height: "100%",
338
+ },
339
+ },
340
+ children,
341
+ },
342
+ zone: "hero-center",
343
+ width: Math.round(w * 0.8),
344
+ height: Math.round(h * 0.7),
345
+ anchor: "center",
346
+ depth: "overlay",
347
+ });
348
+
349
+ return { overlays, background: bg };
350
+ }
351
+
352
+ function socialCard(data: Record<string, unknown>, w: number, h: number): TemplateResult {
353
+ const title = (data["title"] as string) || "";
354
+ const description = data["description"] as string | undefined;
355
+ const logo = data["logo"] as string | undefined;
356
+ const siteName = data["siteName"] as string | undefined;
357
+ const authorName = data["authorName"] as string | undefined;
358
+ const bg = (data["background"] as string) || `linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)`;
359
+
360
+ const overlays: Overlay[] = [];
361
+
362
+ // Logo in top-left
363
+ if (logo) {
364
+ overlays.push({
365
+ type: "image",
366
+ src: logo,
367
+ zone: "top-left-safe",
368
+ width: Math.round(h * 0.08),
369
+ height: Math.round(h * 0.08),
370
+ anchor: "top-left",
371
+ depth: "overlay",
372
+ });
373
+ }
374
+
375
+ const children: any[] = [];
376
+
377
+ children.push({
378
+ tag: "span",
379
+ props: {
380
+ style: {
381
+ fontSize: Math.round(h * 0.08),
382
+ fontFamily: "Space Grotesk",
383
+ fontWeight: 700,
384
+ color: "white",
385
+ textShadow: "0 2px 8px rgba(0,0,0,0.4)",
386
+ lineHeight: 1.2,
387
+ },
388
+ },
389
+ children: [title],
390
+ });
391
+
392
+ if (description) {
393
+ children.push({
394
+ tag: "span",
395
+ props: {
396
+ style: {
397
+ fontSize: Math.round(h * 0.035),
398
+ fontFamily: "Inter",
399
+ fontWeight: 400,
400
+ color: "rgba(255,255,255,0.7)",
401
+ marginTop: 12,
402
+ lineHeight: 1.4,
403
+ },
404
+ },
405
+ children: [description],
406
+ });
407
+ }
408
+
409
+ const bottomParts: string[] = [];
410
+ if (siteName) bottomParts.push(siteName);
411
+ if (authorName) bottomParts.push(authorName);
412
+
413
+ if (bottomParts.length > 0) {
414
+ children.push({
415
+ tag: "span",
416
+ props: {
417
+ style: {
418
+ fontSize: Math.round(h * 0.03),
419
+ fontFamily: "Inter",
420
+ fontWeight: 600,
421
+ color: "rgba(255,255,255,0.5)",
422
+ marginTop: 24,
423
+ },
424
+ },
425
+ children: [bottomParts.join(" · ")],
426
+ });
427
+ }
428
+
429
+ overlays.push({
430
+ type: "satori-text",
431
+ jsx: {
432
+ tag: "div",
433
+ props: {
434
+ style: {
435
+ display: "flex",
436
+ flexDirection: "column",
437
+ justifyContent: "center",
438
+ padding: Math.round(w * 0.08),
439
+ width: "100%",
440
+ height: "100%",
441
+ },
442
+ },
443
+ children,
444
+ },
445
+ zone: "hero-center",
446
+ width: Math.round(w * 0.9),
447
+ height: Math.round(h * 0.8),
448
+ anchor: "center",
449
+ depth: "overlay",
450
+ });
451
+
452
+ return { overlays, background: bg };
453
+ }
454
+
455
+ export function getTemplate(name: string): TemplateFunction | undefined {
456
+ return TEMPLATES[name];
457
+ }
package/src/types.ts ADDED
@@ -0,0 +1,226 @@
1
+ // Core types for picture-it v2 — composable operations architecture
2
+
3
+ export type FalModel =
4
+ | "seedream"
5
+ | "banana2"
6
+ | "banana-pro"
7
+ | "flux-dev"
8
+ | "flux-schnell";
9
+
10
+ export type ColorGrade =
11
+ | "cinematic"
12
+ | "moody"
13
+ | "vibrant"
14
+ | "clean"
15
+ | "warm-editorial"
16
+ | "cool-tech";
17
+
18
+ export type DepthLayer =
19
+ | "background"
20
+ | "midground"
21
+ | "foreground"
22
+ | "overlay"
23
+ | "frame";
24
+
25
+ export type BlendMode = "normal" | "multiply" | "screen" | "overlay";
26
+
27
+ export type MaskShape = "circle" | "rounded" | "hexagon" | "diamond" | "blob" | string;
28
+
29
+ export type DeviceFrame = "iphone" | "macbook" | "browser" | "ipad";
30
+
31
+ export type AnchorPosition = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right";
32
+
33
+ export type CropPosition =
34
+ | "attention" | "entropy" | "center"
35
+ | "top" | "bottom" | "left" | "right"
36
+ | { left: number; top: number };
37
+
38
+ export type ZoneName =
39
+ | "hero-center" | "title-area" | "top-bar" | "bottom-bar"
40
+ | "left-third" | "right-third"
41
+ | "top-left-safe" | "top-right-safe" | "bottom-left-safe" | "bottom-right-safe"
42
+ | "center-left" | "center-right";
43
+
44
+ export interface Zone {
45
+ x: number;
46
+ y: number;
47
+ }
48
+
49
+ export const ZONES: Record<ZoneName, Zone> = {
50
+ "hero-center": { x: 50, y: 45 },
51
+ "title-area": { x: 50, y: 75 },
52
+ "top-bar": { x: 50, y: 8 },
53
+ "bottom-bar": { x: 50, y: 92 },
54
+ "left-third": { x: 25, y: 50 },
55
+ "right-third": { x: 75, y: 50 },
56
+ "top-left-safe": { x: 15, y: 12 },
57
+ "top-right-safe": { x: 85, y: 12 },
58
+ "bottom-left-safe": { x: 15, y: 88 },
59
+ "bottom-right-safe": { x: 85, y: 88 },
60
+ "center-left": { x: 30, y: 50 },
61
+ "center-right": { x: 70, y: 50 },
62
+ };
63
+
64
+ // --- Overlay types (used by compose/text commands) ---
65
+
66
+ export interface ShadowConfig {
67
+ blur: number;
68
+ color: string;
69
+ offsetX: number;
70
+ offsetY: number;
71
+ opacity?: number;
72
+ }
73
+
74
+ export interface GlowConfig {
75
+ color: string;
76
+ blur: number;
77
+ spread: number;
78
+ }
79
+
80
+ export interface ReflectionConfig {
81
+ opacity: number;
82
+ fadeHeight: number;
83
+ }
84
+
85
+ export interface ImageOverlay {
86
+ type: "image";
87
+ src: string;
88
+ zone?: ZoneName | { x: number; y: number };
89
+ width?: number | string;
90
+ height?: number | string;
91
+ anchor?: AnchorPosition;
92
+ opacity?: number;
93
+ borderRadius?: number;
94
+ shadow?: ShadowConfig | "auto";
95
+ glow?: GlowConfig;
96
+ reflection?: ReflectionConfig;
97
+ rotation?: number;
98
+ mask?: MaskShape;
99
+ deviceFrame?: DeviceFrame;
100
+ depth?: DepthLayer;
101
+ }
102
+
103
+ export interface SatoriTextOverlay {
104
+ type: "satori-text";
105
+ jsx: SatoriJSX;
106
+ zone?: ZoneName | { x: number; y: number };
107
+ width?: number;
108
+ height?: number;
109
+ anchor?: AnchorPosition;
110
+ opacity?: number;
111
+ depth?: DepthLayer;
112
+ }
113
+
114
+ export interface ShapeOverlay {
115
+ type: "shape";
116
+ shape: "rect" | "circle" | "line" | "arrow";
117
+ zone?: ZoneName | { x: number; y: number };
118
+ width?: number;
119
+ height?: number;
120
+ fill?: string;
121
+ stroke?: string;
122
+ strokeWidth?: number;
123
+ borderRadius?: number;
124
+ opacity?: number;
125
+ from?: { x: number; y: number };
126
+ to?: { x: number; y: number };
127
+ headSize?: number;
128
+ curve?: number;
129
+ depth?: DepthLayer;
130
+ }
131
+
132
+ export interface GradientOverlay {
133
+ type: "gradient-overlay";
134
+ gradient: string;
135
+ opacity?: number;
136
+ blend?: BlendMode;
137
+ depth?: DepthLayer;
138
+ }
139
+
140
+ export interface WatermarkOverlay {
141
+ type: "watermark";
142
+ src: string;
143
+ position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
144
+ margin?: number;
145
+ opacity?: number;
146
+ size?: number;
147
+ depth?: DepthLayer;
148
+ }
149
+
150
+ export type Overlay =
151
+ | ImageOverlay
152
+ | SatoriTextOverlay
153
+ | ShapeOverlay
154
+ | GradientOverlay
155
+ | WatermarkOverlay;
156
+
157
+ export interface SatoriJSX {
158
+ tag: string;
159
+ props?: Record<string, unknown>;
160
+ children?: (SatoriJSX | string)[];
161
+ }
162
+
163
+ // --- Pipeline types ---
164
+
165
+ export type PipelineStep =
166
+ | { op: "generate"; prompt: string; model?: FalModel; size?: string; platform?: string }
167
+ | { op: "edit"; prompt: string; model?: FalModel; assets?: string[]; size?: string }
168
+ | { op: "remove-bg" }
169
+ | { op: "replace-bg"; prompt: string; model?: FalModel }
170
+ | { op: "crop"; size: string; position?: string }
171
+ | { op: "grade"; name: ColorGrade }
172
+ | { op: "grain"; intensity?: number }
173
+ | { op: "vignette"; opacity?: number }
174
+ | { op: "text"; title: string; font?: string; color?: string; fontSize?: number; zone?: string }
175
+ | { op: "compose"; overlays: string | Overlay[] }
176
+ | { op: "upscale"; scale?: number };
177
+
178
+ // --- Config ---
179
+
180
+ export interface PictureItConfig {
181
+ fal_key?: string;
182
+ default_model?: FalModel;
183
+ default_platform?: string;
184
+ default_grade?: ColorGrade;
185
+ }
186
+
187
+ // --- Platform/style presets ---
188
+
189
+ export interface PlatformPreset {
190
+ width: number;
191
+ height: number;
192
+ safeZone: string;
193
+ minHeading?: number;
194
+ defaultGrade?: ColorGrade;
195
+ notes?: string;
196
+ }
197
+
198
+ export interface StylePreset {
199
+ falPromptStyle: string;
200
+ font: string;
201
+ defaultGrade: ColorGrade;
202
+ glowDefault?: string;
203
+ }
204
+
205
+ // --- Asset info ---
206
+
207
+ export interface ImageInfo {
208
+ path: string;
209
+ filename: string;
210
+ width: number;
211
+ height: number;
212
+ aspectRatio: number;
213
+ hasTransparency: boolean;
214
+ dominantColors: string[];
215
+ contentType: "icon" | "logo" | "screenshot" | "avatar" | "cutout" | "photo";
216
+ format: string;
217
+ sizeBytes: number;
218
+ }
219
+
220
+ // --- Batch ---
221
+
222
+ export interface BatchEntry {
223
+ id: string;
224
+ pipeline: PipelineStep[];
225
+ output?: string;
226
+ }