Google Play Screenshots Generator (RIPDPI)

SkillWeb & browsing

Use when generating Google Play Store assets for RIPDPI — phone screenshots (1080x1920), feature graphics (1024x500), tablet screenshots, or marketing images. Also use when updating play-store-screenshots/src/app/page.tsx, refreshing the raw app captures in public/screenshots/, running the Puppeteer batch capture, or aligning slides with DESIGN.md tokens. Triggers on \"Play Store assets\", \"marketing screenshots\", \"feature graphic\", \"screenshot generator\", \"1080x1920\", or \"play-store-screenshots\".

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Google Play Screenshots Generator (RIPDPI) skill

What this skill tells your AI

The instructions your AI receives, as published by po4yka/ripdpi in .agents/skills/play-store-screenshots/SKILL.md and read by ahel’s review.

Overview

Build or update the Next.js page in play-store-screenshots/ that renders Google Play Store screenshots as advertisements (not UI showcases) and exports them via html-to-image + Puppeteer batch capture at Google Play's required resolutions.

Google Play constraints:

  • Max 8 screenshots per device type (phone, 7" tablet, 10" tablet)
  • Text overlay must not exceed 20% of the screenshot area
  • No promotional text (pricing, rankings, awards)
  • 24-bit PNG or JPEG only (no alpha transparency)
  • Max 8 MB per image
  • Minimum 2 screenshots to publish, 4+ recommended for visibility

Core Principle

Screenshots are advertisements, not documentation. Every screenshot sells one idea. If you're showing UI, you're doing it wrong -- you're selling a feeling, an outcome, or killing a pain point.

Existing Project Structure

The generator already exists at play-store-screenshots/. Check if it needs updating rather than scaffolding from scratch.

play-store-screenshots/
├── public/
│   ├── app-icon.png                  # Copied from app/src/main/ic_launcher-playstore.png
│   └── screenshots/                  # High-res app screenshots
│       ├── home-light.png            # From docs/screenshots/main.png (1080x2400)
│       ├── diagnostics.png           # From docs/screenshots/diagnostics.png (1080x2400)
│       └── settings.png             # From docs/screenshots/settings.png (1080x2400)
├── src/app/
│   ├── layout.tsx                    # Geist Sans + Geist Mono font setup
│   └── page.tsx                      # The screenshot generator (single file)
├── capture.mjs                       # Puppeteer batch capture script
└── package.json                      # next, html-to-image, puppeteer (devDep)

Asset Sources

AssetSourceResolution
App icon (current)app/src/main/ic_launcher-playstore.png -- brutalist black silhouette (bag-in-arch motif) on white512x512
Launcher variants (7)app/src/main/res/drawable/ic_launcher_foreground_ripdpi_{clean,cracked,disintegrate,glitch,rubble,stitch}.xml + ic_launcher_monochrome_ripdpi.xmladaptive XML
High-res screenshotsdocs/screenshots/*.png1080x2400
Low-res test screenshotsapp/src/test/screenshots/com.poyka.ripdpi.ui.screenshot.*.png420x900 to 720x920

Brand shift note (2026-05): The previous logo (dove rising from barbed wire on navy) was replaced with a brutalist black silhouette on white. The app now also ships 7 user-selectable launcher icon variants via the in-app icon picker (customization_icon_* strings). The marketing screenshots use the default clean variant at the top-level ic_launcher-playstore.png. If you want a slide that shows the customization feature, capture each variant from a real device (the variants are XML adaptive icons; there are no PNG rasters in the repo).

Use only high-res screenshots (1080x2400) from docs/screenshots/. The Roborazzi test screenshots are too low-resolution for Play Store quality. If a screen is only available as a test screenshot, use text-focused slides instead.

Refreshing the cached icon: After any logo change, run cp app/src/main/ic_launcher-playstore.png play-store-screenshots/public/app-icon.png and rerun bun run capture:prod. The cached copy is the icon the screenshot generator reads.

Step 1: Confirm RIPDPI Defaults with the User

Pre-Filled from DESIGN.md (confirm, don't ask from scratch)

All colors come from the project's design system (DESIGN.md / RipDpiExtendedColors). The Play Store screenshots use these tokens for brand consistency.

Brand-palette note: the repo-root DESIGN.md (alpha) is monochrome-first light (#FAFAFA background, #1A1A1A foreground). Marketing slides honor the design system: light, monochrome-first, restrained semantic color, no decorative gradients. BRAND_LIGHT is the canonical default; BRAND (the strict dark inversion) is kept for at most 1-2 rhythm-break slides per set and uses the same role mapping, not a separate visual identity.

ItemDefault
App nameRIPDPI
Design philosophyMonochrome-first, semantic color for status only. Technical, precise, utilitarian.
Font familiesGeist Sans (UI text), Geist Mono (values/configs), Geist Pixel Circle (brand mark only)
Light Theme Tokens (canonical default)
const BRAND_LIGHT = {
  bg: "#FAFAFA",           // background
  card: "#FFFFFF",         // card
  text: "#1A1A1A",         // foreground
  muted: "#F5F5F5",        // muted
  mutedFg: "#575757",      // mutedForeground
  accent: "#E8E8E8",       // accent
  border: "#E0E0E0",       // border
  success: "#047857",      // success
  warning: "#B45309",      // warning
  error: "#B91C1C",        // destructive
  info: "#1D4ED8",         // info
  restricted: "#6B7280",   // restricted
} as const;
Dark Theme Tokens (strict inversion — same role mapping)

Use sparingly for at most 1-2 rhythm-break slides. Status colors are slightly lighter than light-theme variants for dark-surface contrast — never bright.

const BRAND = {
  bg: "#1A1A1A",           // background
  card: "#1F1F1F",         // card
  text: "#FAFAFA",         // foreground
  muted: "#262626",        // muted
  mutedFg: "#A3A3A3",      // mutedForeground
  accent: "#2A2A2A",       // accent
  border: "#2A2A2A",       // border
  success: "#10B981",      // success
  warning: "#D97706",      // warning
  error: "#DC2626",        // destructive
  info: "#3B82F6",         // info
  restricted: "#6B7280",   // restricted
} as const;
Shape Tokens (from RipDpiShapeTokens)
TokenRadiusUsage in screenshots
xl16dpButtons, cards, text fields
xlIncreased20dpExpressive interactive surfaces
xxl28dpPills, FABs
full50%Avatars, status dots

Use these for screenshot frame corners (40px = ~3.7% of 1080px width) and UI element pills.

Typography Reference
ElementFamilyWeightPlay Store sizing
Category labelGeist Sans600 (semibold)~35px at 1080w
HeadlineGeist Sans700 (bold)103-108px at 1080w
Pill/badge textGeist Mono600 (semibold)26-30px at 1080w
SubtextGeist Sans400 (normal)28px at 1080w

M3 Expressive principle: Prefer weight promotion (400->500->700) over size increase for emphasis.

| Feature list | 1. path optimization (proxy + VPN modes) 2. No root required 3. Advanced strategy controls (TCP, QUIC, DNS) 4. Encrypted DNS (DoH/DoT/DNSCrypt) 5. Integrated diagnostics & monitoring 6. Per-network policies 7. Works with AdGuard 8. Session telemetry & export | | Style direction | Light, monochrome-first. Restrained semantic color (use info/success/warning only when the state itself matters). No decorative gradients on any slide — DESIGN.md forbids them in-app and the marketing surface honors the same rule. |

Ask the User

  1. Number of slides -- "How many screenshots do you want? (Google Play allows up to 8)"
  2. Feature Graphic -- "Shall I generate a Feature Graphic (1024x500)? It's required for Play Store listings."
  3. Localized screenshots -- "Do you want screenshots in multiple languages? If yes, which languages?"
  4. Additional instructions -- "Any specific requirements or preferences?"

IMPORTANT: If the user says "figure it out" or similar, use the defaults and proceed without asking.

Derived (do NOT ask)

  • Background style: flat solid backgrounds only (no gradients). BRAND_LIGHT.bg (#FAFAFA) for the canonical light slides; BRAND.bg (#1A1A1A) for the rare rhythm-break dark slide.
  • Decorative elements: subtle low-opacity monochrome Grid pattern is the only allowed decoration. No radial glow orbs, no circuit-board, no shield motifs — these read as consumer-dashboard flourish and violate the design system.
  • Light vs dark slides: light-first (matches DESIGN.md). At most 1-2 of 6 slides may use the dark inversion for visual rhythm; never more.
  • Screenshot placement: use top positioning (not bottom + translateY) to precisely control where screenshots start below headlines

Step 2: Set Up / Update the Project

If project already exists

cd play-store-screenshots
bun install  # or npm install

Check if public/screenshots/ has the latest high-res screenshots from docs/screenshots/. Copy any updated ones.

If scaffolding new

Package manager priority: bun > pnpm > yarn > npm

bunx create-next-app@latest play-store-screenshots --typescript --tailwind --app --src-dir --no-eslint --import-alias "@/*"
cd play-store-screenshots
bun add html-to-image
bun add -d puppeteer
bun pm trust puppeteer  # allow postinstall to download Chromium

Copy assets:

mkdir -p public/screenshots
cp ../app/src/main/ic_launcher-playstore.png public/app-icon.png
cp ../docs/screenshots/main.png public/screenshots/home-light.png
cp ../docs/screenshots/diagnostics.png public/screenshots/diagnostics.png
cp ../docs/screenshots/settings.png public/screenshots/settings.png

Font Setup (Next.js 16+)

Next.js 16 ships Geist fonts natively via next/font/google:

// src/app/layout.tsx
import { Geist, Geist_Mono } from "next/font/google";
const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });

Use var(--font-geist-sans) and var(--font-geist-mono) in slide styles.

Next.js 16 Caveats

  • useSearchParams requires Suspense: Wrap the main component in <Suspense> to avoid build failures during static prerendering.
  • Production build for capture: bun run build && bun run start -- the dev server HMR websocket causes Puppeteer and Chrome DevTools navigation timeouts.

Step 3: Plan the Slides

Screenshot Framework (Narrative Arc)

SlotPurposeRIPDPI Suggestion
#1Hero / Main BenefitHome screen on light bg. "Browse without borders"
#2DifferentiatorBrutalist black silhouette logo on light, text-focused. "One tap. No root."
#3Core FeatureSettings screenshot on light bg. "Your privacy. Your rules."
#4Core FeatureProtocol pills, text-focused, light. "Fine-tune every packet"
#5Core FeatureDiagnostics screenshot on light bg; info accent legitimately marks active probes. "See what's really happening"
#6More FeaturesFeature pills + icon, dark inversion for rhythm. "And so much more."

Rules:

  • Each slide sells ONE idea
  • Vary layouts -- never repeat the same template structure in adjacent slides
  • Light-first: at most 1-2 of 6 slides may be the dark inversion, for rhythm only
  • Text overlay must not exceed 20% of the screenshot area
  • Slides with no high-res screenshot available should be text-focused (feature cards, protocol pills, etc.)

Step 4: Write Copy FIRST

The Iron Rules

  1. One idea per headline. Never join two things with "and."
  2. Short, common words. 1-2 syllables. No jargon unless domain-specific.
  3. 3-5 words per line. Readable at thumbnail size.
  4. Line breaks are intentional. Control with <br />.
  5. Max 20% text overlay.

Three Approaches (pick one per slide)

TypeWhat it doesExample
Paint a momentYou picture yourself doing it"Open any site. No extra steps."
State an outcomeWhat your life looks like after"The internet you were promised."
Kill a painName a problem and destroy it"No more blocked pages."

Step 5: Build the Page

Architecture

The entire generator is a single page.tsx file:

page.tsx
├── Constants (PHONE_W/H, FEATURE_GRAPHIC, BRAND tokens)
├── Screenshot component (frameless, 40px border-radius, bgColor prop)
├── Caption component (label + headline)
├── Decorative components (Glow, Grid, Pill)
├── Slide container component
├── Slide1..N components (one per slide)
├── FeatureGraphicSlide component (1024x500)
├── SLIDES array (registry)
├── ScreenshotPreview (ResizeObserver scaling + click-to-export)
├── ScreenshotsPage (grid + export logic)
└── Page wrapper (Suspense boundary)

Key Dimensions

const PHONE_W = 1080;
const PHONE_H = 1920;
const FEATURE_GRAPHIC = { w: 1024, h: 500 };

Screenshot Component (Frameless)

function Screenshot({ src, alt, style, bgColor = "#ffffff" }: {
  src: string; alt: string; style?: React.CSSProperties; bgColor?: string;
}) {
  return (
    <div style={{ position: "relative", ...style }}>
      <div style={{
        width: "100%", height: "100%",
        borderRadius: 40,  // ~3.7% of 1080px, close to xl (16dp) scaled up
        overflow: "hidden",
        boxShadow: "0 12px 60px rgba(0,0,0,0.5)",
        background: bgColor,
      }}>
        <img src={src} alt={alt}
          style={{ display: "block", width: "100%", height: "100%",
            objectFit: "cover", objectPosition: "top" }}
          draggable={false} />
      </div>
    </div>
  );
}

Phone Placement (Critical)

The high-res screenshots are 1080x2400 (taller than the 1080x1920 canvas). Use top positioning to control exactly where the phone starts below the headline:

// 3-line headline (~400px) + gap = top: 520
<Screenshot src="/screenshots/home-light.png" alt="Home"
  style={{
    position: "absolute",
    top: 520,           // precise control -- no overlap
    left: "50%",
    transform: "translateX(-50%)",
    width: "76%",
    aspectRatio: "1080/2400",
  }}
/>

Never use bottom: 0 + translateY(N%) for 1080x2400 screenshots -- the percentage math is hard to get right and leads to overlap or excessive gaps.

Single-Slide Mode

Support ?slide=N (1-6) and ?slide=fg for headless capture:

export default function Page() {
  return (
    <Suspense fallback={<div style={{ background: "#0a0a0a", minHeight: "100vh" }} />}>
      <ScreenshotsPage />
    </Suspense>
  );
}

function ScreenshotsPage() {
  const searchParams = useSearchParams();
  const slideParam = searchParams.get("slide");
  if (slideParam) {
    if (slideParam === "fg") return <FeatureGraphicSlide />;
    const idx = parseInt(slideParam) - 1;
    const slide = SLIDES[idx];
    if (slide) { const C = slide.component; return <C />; }
  }
  // ... grid view with export
}

Step 6: Export

Browser Export (interactive)

import { toPng } from "html-to-image";
const opts = { width: w, height: h, pixelRatio: 1, cacheBust: true, backgroundColor: "#FAFAFA" };
await toPng(el, opts);  // warm-up call
const dataUrl = await toPng(el, opts);  // actual capture

Puppeteer Batch Export (headless)

Use capture.mjs against the production build (dev server HMR causes timeouts):

bun run build && bun run start -- -p 3099 &
node capture.mjs
// capture.mjs
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
for (const slide of SLIDES) {
  await page.setViewport({ width: slide.w, height: slide.h, deviceScaleFactor: 1 });
  await page.goto(`http://localhost:3099/?slide=${slide.param}`, { waitUntil: "load", timeout: 60000 });
  await new Promise(r => setTimeout(r, 2000));  // fonts + images
  await page.screenshot({ path: outPath, type: "png", clip: { x: 0, y: 0, width: slide.w, height: slide.h } });
}

Captured images go to docs/screenshots/ for README usage.

Key Export Rules

  • Double-call trick for html-to-image: first call warms up fonts/images
  • backgroundColor: Always set to strip alpha (Google Play rejects alpha PNGs)
  • Numbered filenames: 01-hero.png, 02-no-root.png, etc.
  • Feature Graphic filename: feature-graphic.png
  • Production server only for Puppeteer -- dev server HMR websocket causes infinite loading

Step 7: Final QA Gate

Google Play Compliance

  • No alpha transparency (backgroundColor set)
  • Text overlay <= 20% of screenshot area
  • No promotional pricing, rankings, or awards
  • Aspect ratio valid (1920/1080 = 1.78, passes max 2:1)
  • Minimum 4 screenshots
  • Feature Graphic exactly 1024x500
  • Each file under 8 MB
  • All exports are 24-bit PNG

Visual Quality

  • No repeated layouts in adjacent slides
  • No text/screenshot overlap
  • Screenshots fully contained (no clipping at edges)
  • At least 1 light contrast slide for rhythm
  • Decorative elements don't cover app UI

Design System Alignment

  • Colors match DESIGN.md tokens (light canonical: #FAFAFA bg, #1A1A1A text, #1D4ED8 info; dark inversion: #1A1A1A bg, #FAFAFA text, #3B82F6 info)
  • Font families are Geist Sans (headlines, labels) and Geist Mono (pills, badges, values)
  • Weight emphasis follows M3 Expressive principle (400->500->700, not size increase)
  • Pill/badge corners use design system radii (12-16px range)

Common Mistakes

MistakeFix
Text overlaps phone screenshotUse top positioning, not bottom + translateY
Screenshot clipped at edgesUse left/right: "4%" not negative values
Blank Puppeteer capturesMust use production build (bun run build && bun run start)
useSearchParams build errorWrap component in <Suspense>
Low-res screenshots look badOnly use 1080x2400 from docs/screenshots/; text-focused slides for others
Decorative gradients on slidesDESIGN.md forbids them in the app and they also break the marketing brand. Use solid backgrounds plus the Grid utility for subtle texture. No linear-gradient(...) or radial-glow orbs anywhere.
Bright saturated status accentsUse restrained DESIGN.md tokens (#1D4ED8 info, #047857 success, #B45309 warning), never bubblegum-bright variants. Reserve status color for slides where the state itself matters.
All slides look the sameVary: centered phone, right-offset, left-offset, text-only, pills-only
Copy too complex"One second at arm's length" test; 3-5 words per line

Signals

GitHub stars
71
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
play-store-screenshots
Source
github.com/po4yka/ripdpi