55// welcome cards use), rounded corners, shadow, optional feature highlight.
66// Fully local: no network, no upload (safe for shots with internal data).
77//
8- // Usage: bun beautify-screenshot.ts <in.png> <out.png> [tilt|flat] [x,y,w,h] [flags]
8+ // Usage: bun beautify-screenshot.ts <in.png> <out.png> [card|flat|tilt] [x,y,w,h] [flags]
9+ // card the changelog default (Linear-style): flat product card
10+ // with a hairline border on a near-black backdrop, no
11+ // dither, no perspective. Pair with --bleed for heroes.
12+ // flat / tilt the older dithered-backdrop looks; tilt is retired for
13+ // heroes (Kiet, 2026-08-23: no slanted heroes).
914// x,y,w,h optional crop rectangle in source pixels — zoom into the
1015// feature instead of framing the whole window.
1116//
2732// the frame — points the reader at the feature.
2833// --hl-dim <n> how much to dim outside the highlight, 0-1 (default
2934// 0.42; use ~0.2 when the dimmed area must stay readable)
35+ // --bleed (card only) anchor the card near the top and let it run
36+ // off the bottom edge under a fade, the way Linear frames
37+ // a tall surface; the crop should be taller than 16:10
3038//
3139// Needs: Google Chrome (or set CHROME=/path/to/chrome). Compress the output
3240// afterwards, e.g. `pngquant --quality=58-84 out.png`.
@@ -45,10 +53,12 @@ for (let i = 0; i < args.length; i++) {
4553 positional . push ( a ) ;
4654 }
4755}
48- const [ inPath , outPathArg , style = "tilt" , cropArg ] = positional ;
56+ const [ inPath , outPathArg , style = "card" , cropArg ] = positional ;
57+ const isCard = style === "card" ;
58+ const bleed = isCard && flags . has ( "bleed" ) ;
4959if ( ! inPath || ! outPathArg ) {
5060 console . error (
51- "usage: bun beautify-screenshot.ts <in.png> <out.png> [tilt |flat] [x,y,w,h] [--bg preset] [--seed n] [--highlight x,y,w,h]" ,
61+ "usage: bun beautify-screenshot.ts <in.png> <out.png> [card |flat|tilt ] [x,y,w,h] [--bleed ] [--bg preset] [--seed n] [--highlight x,y,w,h]" ,
5262 ) ;
5363 process . exit ( 2 ) ;
5464}
@@ -59,8 +69,8 @@ const CHROME =
5969const SCALE = 2 ;
6070// Bigger frame + tighter canvas = the UI reads larger, less dead background.
6171const CANVAS_W = 1600 ;
62- const CANVAS_H = style === "tilt" ? 1040 : 980 ;
63- const FRAME_FRAC = style === "tilt" ? 0.86 : 0.92 ;
72+ const CANVAS_H = isCard ? 925 : style === "tilt" ? 1040 : 980 ;
73+ const FRAME_FRAC = isCard ? 0.84 : style === "tilt" ? 0.86 : 0.92 ;
6474const tilt =
6575 style === "tilt"
6676 ? "perspective(2600px) rotateY(-8deg) rotateX(3deg) rotateZ(-0.6deg)"
@@ -89,7 +99,7 @@ if (bgName === "random") {
8999 const names = Object . keys ( PRESETS ) ;
90100 bgName = names [ Math . floor ( rand ( ) * names . length ) ] ;
91101}
92- const usePlain = bgName === "plain" ;
102+ const usePlain = bgName === "plain" || isCard ;
93103if ( ! usePlain && ! PRESETS [ bgName ] ) {
94104 console . error (
95105 `unknown --bg "${ bgName } " — use ${ Object . keys ( PRESETS ) . join ( "|" ) } |plain|random` ,
@@ -147,7 +157,9 @@ if (cropArg) {
147157// crops float as a bordered card on the backdrop instead of overflowing the
148158// canvas and getting cut off top/bottom.
149159const frameW = Math . round (
150- Math . min ( CANVAS_W * FRAME_FRAC , CANVAS_H * FRAME_FRAC * aspect ) ,
160+ bleed
161+ ? CANVAS_W * FRAME_FRAC
162+ : Math . min ( CANVAS_W * FRAME_FRAC , CANVAS_H * FRAME_FRAC * aspect ) ,
151163) ;
152164
153165// Optional highlight: accent ring + dim outside, in source-pixel coordinates.
@@ -246,6 +258,24 @@ const html = `<!doctype html><html><head><meta charset="utf8"><style>
246258 0 80px 160px -40px rgba(0,0,0,.65),
247259 0 0 0 1px rgba(255,255,255,.06) inset;
248260 }
261+ /* card: Linear-style flat product card. Near-black stage with a faint top
262+ glow, hairline border, quiet shadow. --bleed pins it high and fades the
263+ bottom edge into the backdrop. */
264+ body.card .stage { background:#0a0a0b; align-items:${ bleed ? "flex-start" : "center" } ; }
265+ body.card .glow, body.card .tint, body.card .vignette, body.card #dither { display:none; }
266+ body.card .sheen { position:absolute; inset:0; background:
267+ radial-gradient(ellipse 70% 55% at 50% 0%, rgba(255,255,255,.045), transparent 70%); }
268+ body.card .frame {
269+ margin-top:${ bleed ? Math . round ( CANVAS_H * 0.09 ) : 0 } px;
270+ border-radius:14px; background:#111113;
271+ border:1px solid rgba(255,255,255,.09);
272+ box-shadow: 0 1px 0 rgba(255,255,255,.04) inset,
273+ 0 24px 60px -24px rgba(0,0,0,.8),
274+ 0 0 0 1px rgba(0,0,0,.6);
275+ }
276+ body.card .fade { position:absolute; left:0; right:0; bottom:0; height:${ Math . round ( CANVAS_H * 0.32 ) } px;
277+ display:${ bleed ? "block" : "none" } ;
278+ background:linear-gradient(to bottom, rgba(10,10,11,0), rgba(10,10,11,.92) 70%, #0a0a0b); }
249279 .frame img, .frame .shot { display:block; width:100%; height:auto; }
250280 /* highlight ring: dim everything outside via a huge spread shadow (clipped
251281 by the frame's overflow:hidden), accent ring + soft glow on the region */
@@ -256,14 +286,16 @@ const html = `<!doctype html><html><head><meta charset="utf8"><style>
256286 0 0 22px 2px ${ accent } 66,
257287 inset 0 0 14px ${ accent } 22;
258288 }
259- </style></head><body>
289+ </style></head><body class=" ${ isCard ? "card" : "" } " >
260290 <div class="stage">
291+ <div class="sheen"></div>
261292 <div id="dither"></div>
262293 <div class="tint"></div>
263294 <div class="vignette"></div>
264295 <div class="glow g1"></div><div class="glow g2"></div>
265296 <div class="glow g3"></div><div class="glow g4"></div>
266297 <div class="frame">${ shot } ${ highlight } </div>
298+ <div class="fade"></div>
267299 </div>
268300 ${ shaderScript }
269301</body></html>` ;
@@ -305,5 +337,7 @@ if (proc.exitCode !== 0) {
305337console . log (
306338 "wrote" ,
307339 outPath ,
308- `(bg=${ usePlain ? "plain" : bgName } , seed=${ seed } )` ,
340+ isCard
341+ ? `(card${ bleed ? ", bleed" : "" } )`
342+ : `(bg=${ usePlain ? "plain" : bgName } , seed=${ seed } )` ,
309343) ;
0 commit comments