// Generates the product images shown next to each plan in Stripe Checkout. // // Each plan is a 4-slot isometric "release stack": lit layers = the plan tier, // unlit slots show what the next plan unlocks. No text in the image, since // Stripe already renders the plan name and the thumbnail is ~48px. // // Usage: bun scripts/generate_stripe_plan_images.ts // Output: assets/stripe-plans/.svg and .png (1024x1024, needs rsvg-convert) import { execFileSync } from 'node:child_process' import { mkdirSync, writeFileSync } from 'node:fs' import { join } from 'node:path' const OUT_DIR = join(import.meta.dirname, '..', 'assets', 'stripe-plans') const SIZE = 1024 const SLOTS = 4 const HALF_W = 230 const HALF_H = 115 const DEPTH = 46 const GAP = 112 const BASE_Y = 668 const plans = [ { name: 'solo', lit: 1, accent: false }, { name: 'maker', lit: 2, accent: false }, { name: 'team', lit: 3, accent: false }, { name: 'enterprise', lit: 4, accent: true }, ] function layer(cy: number, state: 'lit' | 'top' | 'ghost', accent: boolean) { const cx = SIZE / 2 const top = `${cx},${cy - HALF_H} ${cx + HALF_W},${cy} ${cx},${cy + HALF_H} ${cx - HALF_W},${cy}` const left = `${cx - HALF_W},${cy} ${cx},${cy + HALF_H} ${cx},${cy + HALF_H + DEPTH} ${cx - HALF_W},${cy + DEPTH}` const right = `${cx},${cy + HALF_H} ${cx + HALF_W},${cy} ${cx + HALF_W},${cy + DEPTH} ${cx},${cy + HALF_H + DEPTH}` if (state === 'ghost') { return ` ` } const topFill = state === 'top' ? (accent ? 'url(#topAccent)' : 'url(#topLit)') : (accent ? 'url(#topBaseAccent)' : 'url(#topBase)') return ` ` } function planSvg(lit: number, accent: boolean) { // Ghost slots are painted first so the lit top face stays fully visible. const ghosts: string[] = [] const solids: string[] = [] for (let i = 0; i < SLOTS; i++) { const cy = BASE_Y - i * GAP if (i >= lit) ghosts.unshift(layer(cy, 'ghost', accent)) else solids.push(layer(cy, i === lit - 1 ? 'top' : 'lit', accent)) } const layers = [...ghosts, ...solids] const glowY = BASE_Y - (lit - 1) * GAP return ` ${layers.join('\n ')} ` } mkdirSync(OUT_DIR, { recursive: true }) for (const plan of plans) { const svgPath = join(OUT_DIR, `${plan.name}.svg`) writeFileSync(svgPath, planSvg(plan.lit, plan.accent)) execFileSync('rsvg-convert', ['-w', String(SIZE), '-h', String(SIZE), svgPath, '-o', join(OUT_DIR, `${plan.name}.png`)]) console.log(`wrote ${plan.name}`) }