grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: add App Store screenshot template and renders

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+143
marketing/screens/01.png

This is a binary file and will not be displayed.

marketing/screens/02.png

This is a binary file and will not be displayed.

marketing/screens/03.png

This is a binary file and will not be displayed.

marketing/screens/04.png

This is a binary file and will not be displayed.

marketing/screens/05.png

This is a binary file and will not be displayed.

marketing/screens/06.png

This is a binary file and will not be displayed.

+56
marketing/screens/render.mjs
··· 1 + // Render six App Store screenshots from template.html + the source iPhone shots. 2 + // Output: marketing/screens/01.png … 06.png at 1242×2688. 3 + // 4 + // Usage: node marketing/screens/render.mjs 5 + 6 + import { chromium } from "playwright"; 7 + import path from "node:path"; 8 + import { fileURLToPath } from "node:url"; 9 + 10 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 11 + 12 + const SCREENS = [ 13 + { file: "01.png", image: "01.png", title: "Pin the feeds you care about" }, 14 + { file: "02.png", image: "02.png", title: "See the camera behind every shot" }, 15 + { file: "03.png", image: "03.png", title: "Group your photos into galleries" }, 16 + { file: "04.png", image: "04.png", title: "Share moments for the day" }, 17 + { file: "05.png", image: "05.png", title: "Find your photo people" }, 18 + { file: "06.png", image: "06.png", title: "Keep up with the activity" }, 19 + ]; 20 + 21 + const browser = await chromium.launch(); 22 + const context = await browser.newContext({ 23 + viewport: { width: 1242, height: 2688 }, 24 + deviceScaleFactor: 1, 25 + }); 26 + const page = await context.newPage(); 27 + 28 + const baseDir = path.resolve(__dirname); 29 + const templateUrl = `file://${path.join(baseDir, "template.html")}`; 30 + 31 + await page.goto(templateUrl, { waitUntil: "domcontentloaded" }); 32 + 33 + for (const s of SCREENS) { 34 + const sourceUrl = `file://${path.join(baseDir, "source", s.image)}`; 35 + console.log(`→ ${s.file}: ${s.title}`); 36 + await page.evaluate( 37 + ({ title, image }) => 38 + new Promise((resolve) => { 39 + document.getElementById("title").textContent = title; 40 + const img = document.getElementById("device"); 41 + if (img.src === image && img.complete) return resolve(); 42 + img.onload = () => resolve(); 43 + img.onerror = () => resolve(); 44 + img.src = image; 45 + }), 46 + { title: s.title, image: sourceUrl }, 47 + ); 48 + await page.waitForTimeout(200); 49 + await page.screenshot({ 50 + path: path.join(baseDir, s.file), 51 + fullPage: false, 52 + }); 53 + } 54 + 55 + await browser.close(); 56 + console.log(`Done — ${SCREENS.length} screens rendered to ${baseDir}`);
marketing/screens/source/01.png

This is a binary file and will not be displayed.

marketing/screens/source/02.png

This is a binary file and will not be displayed.

marketing/screens/source/03.png

This is a binary file and will not be displayed.

marketing/screens/source/04.png

This is a binary file and will not be displayed.

marketing/screens/source/05.png

This is a binary file and will not be displayed.

marketing/screens/source/06.png

This is a binary file and will not be displayed.

+87
marketing/screens/template.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width,initial-scale=1" /> 6 + <title>grain — App Store screen</title> 7 + <link rel="preconnect" href="https://fonts.googleapis.com" /> 8 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 9 + <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&display=block" rel="stylesheet" /> 10 + <style> 11 + :root { 12 + --bg-root: #080b12; 13 + --grain: #85a1ff; 14 + --text: #f8fafc; 15 + --text-secondary: #a3b3cc; 16 + } 17 + * { box-sizing: border-box; margin: 0; padding: 0; } 18 + html, body { height: 100%; } 19 + body { 20 + width: 1242px; 21 + height: 2688px; 22 + font-family: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, sans-serif; 23 + background: var(--bg-root); 24 + color: var(--text); 25 + -webkit-font-smoothing: antialiased; 26 + overflow: hidden; 27 + position: relative; 28 + background-image: 29 + radial-gradient(ellipse 1100px 800px at 90% -10%, rgba(133, 161, 255, 0.18), transparent 60%), 30 + radial-gradient(ellipse 900px 700px at 5% 110%, rgba(107, 139, 239, 0.10), transparent 60%); 31 + } 32 + 33 + .stage { 34 + width: 100%; 35 + height: 100%; 36 + padding: 180px 90px 0; 37 + display: flex; 38 + flex-direction: column; 39 + align-items: center; 40 + text-align: center; 41 + position: relative; 42 + } 43 + 44 + .title { 45 + font-size: 100px; 46 + font-weight: 700; 47 + letter-spacing: -0.035em; 48 + line-height: 1.05; 49 + color: var(--text); 50 + margin-bottom: 110px; 51 + max-width: 1040px; 52 + } 53 + 54 + .device-wrap { 55 + flex: 1; 56 + width: 100%; 57 + display: flex; 58 + justify-content: center; 59 + align-items: flex-start; 60 + overflow: hidden; 61 + } 62 + 63 + .device { 64 + width: 940px; 65 + height: auto; 66 + border-radius: 68px; 67 + box-shadow: 68 + 0 60px 140px -20px rgba(0, 0, 0, 0.7), 69 + 0 30px 60px -20px rgba(133, 161, 255, 0.20), 70 + 0 0 0 6px rgba(255, 255, 255, 0.05), 71 + 0 0 0 8px rgba(0, 0, 0, 0.6); 72 + background: #000; 73 + object-fit: cover; 74 + object-position: top; 75 + margin-bottom: -120px; 76 + } 77 + </style> 78 + </head> 79 + <body> 80 + <section class="stage"> 81 + <h1 class="title" id="title">Title goes here</h1> 82 + <div class="device-wrap"> 83 + <img class="device" id="device" alt="" /> 84 + </div> 85 + </section> 86 + </body> 87 + </html>