| name | render-ios-lockscreen |
| description | Render a pixel-accurate iOS lockscreen mockup (status bar + clock + date + stacked notification banners + optional decorative tear/glow overlay) from a JSON config. Outputs HTML + PNG. Sibling of create-imessage-mockup — use this for ads whose payoff is the notification stack (FOMO / popularity / demand), not a chat thread. |
render-ios-lockscreen
Generate iOS lockscreen screenshots that look like real captures — translucent grouped notification banners, app icons with avatar thumbnails, "now" / "2m ago" timestamps, the centered clock + date stack, status bar (signal, wifi, battery), optional Dynamic Island, and an optional decorative tear/glow overlay that sells the "too many notifications to handle" payoff.
Purpose
Produce ONE static lockscreen PNG (or its underlying HTML) from a JSON config. Use this as a hyperframe inside a video ad — typically as the climax of a "notification flood" mechanic where the lockscreen reveals an avalanche of app banners from one brand.
This atom is intentionally static. Animation (banners sliding in one by one, the stack tearing apart) lives in the calling molecule (notification-flood-ad), which drives this atom's HTML through Playwright with a TIMELINE — same pattern as create-imessage-video-ad drives create-imessage-mockup.
When to use
- "Notification flood" ad payoff (N messages from the same app raining onto the screen)
- "Lockscreen reveal" — the brand's notification is the punchline
- Hyperframe inside a UGC ad where the camera pretends to look at someone's phone
- Storyboard frame showing what an app's push looks like in context
If you need a chat thread instead, use skills/atoms/messaging/create-imessage-mockup. If you need a full motion-graphics composition, use skills/atoms/motion-graphics/create-motion-graphics-hyperframes.
Inputs
The skill is deterministic — it does not embed an LLM. The orchestrating agent composes a JSON config matching the schema below, then invokes the renderer.
Lockscreen JSON schema
{
"device": "iphone-15-pro" | "bare",
"wallpaper": {
"type": "color" | "gradient" | "image",
"value": "#0a1a3a" | ["#0a1a3a","#1a4a8a"] | "path/to/wallpaper.jpg"
},
"statusBar": {
"show": true,
"time": "9:41",
"signal": 4,
"wifi": 3,
"battery": 100,
"dynamicIsland": true
},
"clock": {
Field notes:
device: "bare" produces just the lockscreen content with no phone bezel (use when the lockscreen will be composited into a larger frame, e.g. someone holding a phone). iphone-15-pro adds the full bezel + Dynamic Island.
wallpaper.type:
color — solid hex
gradient — array of 2+ stops, top→bottom unless direction set
image — path to a JPG/PNG (used at cover)
clock.translucent: true renders the clock with the iOS-17 frosted depth-effect look (faint outline + slight backdrop blur). Default true.
headerOverlay — optional emoji/text band sitting between clock and notifications. Used for the "😭😭😭😭" beat in the Yubo reference.
notifications[] — top-to-bottom in render order. The first item is the most recent (newest banner sits at the top of the stack, just under the clock).
transform lets the molecule push individual banners around per-frame (the avalanche needs each new banner to slide in from y: -120 to y: 0).
style.opacity / style.blur are how new banners fade in.
pinned: true floats this banner above the others with a small drop-shadow (the "Yubo • Max sent you a message" duplicate at the top of the reference).
decoration.tear — optional translucent overlay (the glitchy blue tear in the Yubo reference). The molecule animates its rotate and scale over the avalanche to sell the "too much to hold" feel.
Workflow
- Receive a JSON config matching the schema above.
- Run
node render.js --config <path> to produce HTML + PNG.
- Open the PNG to verify layout. If banners clip, status bar mis-renders, or the wallpaper color is wrong, edit the config and re-render — never patch the templates unless adding a new field for the molecule.
CLI
node render.js --config examples/yubo-flood-final.json
node render.js --config examples/empty-lockscreen.json --name empty
node render.js --config my-config.json --output ./lockscreen-exports/ --name yubo-frame-12
Flags
| Flag | Effect |
|---|
--config <path> | (required) JSON file matching the schema above |
--name <slug> | override the output folder slug |
--output <dir> | parent dir for the dated output folder; default ./lockscreen-exports/ |
--width <px> | viewport width (default 390 — iPhone 15 Pro CSS width; 525 with bezel) |
--dpr <n> | device pixel ratio (default 3 for retina) |
--html-only | skip the screenshot pass, just emit the HTML (useful when the molecule will drive it via Playwright) |
Output
<output>/<YYYY-MM-DD>-<slug>/
index.html # full standalone HTML
screenshot.png # rendered PNG (DPR 3 by default)
config.json # copy of the input for reproducibility
Default <output> is ./lockscreen-exports/ in the cwd.
Quality Checks
- The status bar carrier slot, wifi, and battery glyphs render (not as broken-image squares).
- The clock + date center horizontally, not pushed right by the notification stack.
- Each notification banner shows the app icon at the left, app name + body inline, timestamp ("now") at the right — NOT vertically stacked.
- App icon for the brand is the real PNG, not styled CSS text (see auto-memory: end-card-needs-real-product). Use the brand's actual app icon PNG (e.g.
<your-project>/<brand>/brand-assets/app-icon.png).
- Avatar thumbnail (small circle on the right side of the icon — the "from-person" badge) shows on banners with
avatar.src set.
- Wallpaper is darker than the notification banners — if banners disappear into the wallpaper, raise the wallpaper saturation or pick a darker base.
- DPR 3 PNG is sharp at the natural iPhone width (1170×2532 px on iPhone 15 Pro). Don't render at DPR 1.
Failure Modes
| Symptom | Cause | Fix |
|---|
| App icon renders as broken-image square | appIcon path doesn't resolve from the cwd | Use absolute path OR copy the icon into the config's directory before render |
| Banners stack on top of each other (no vertical gap) | CSS gap got overridden by an inline transform: translateY(0) on all banners | The molecule should only set transform when a banner is mid-animation. At rest, leave the transform unset. |
| Banner text wraps to 3 lines instead of truncating | Body string too long (>~38 chars in DM-style banner) | Trim the body OR set truncate: true on the notification |
| Clock looks blocky / not translucent | clock.translucent: false OR Chromium older than 110 | Bump Playwright (npm install playwright@latest) — the depth-effect CSS uses backdrop-filter: blur() |
| Status bar time and clock time disagree | Two separate fields — statusBar.time and clock.time | Set both to the same value, or omit statusBar.time to mirror clock.time automatically |
| PNG comes out at low DPI / small | Forgot --dpr 3 | Default is 3; check you didn't override it |
| Wallpaper image doesn't show | Path not resolved at render time OR file is git-LFS pointer | Verify the file is real bytes (file path/to/wallpaper.jpg → JPEG image data); run git lfs pull --include="<path>" if it's a 130-byte pointer |
| Notification icons look squished | Some app icons aren't square — atom assumes 1:1 | Pre-crop icons to square before passing |
| Avatar thumbnail not showing | avatar.src path unresolved OR avatar field forgotten on a "from-person" notification (Yubo's whole point is the avatar) | Always set avatar.src for sender-style notifications |
Setup (one-time)
This atom shares the Playwright install with create-imessage-mockup. If that atom's node_modules is already populated, you can symlink it:
cd skills/atoms/messaging/render-ios-lockscreen
ln -s ../create-imessage-mockup/node_modules node_modules
Or do a fresh install:
cd skills/atoms/messaging/render-ios-lockscreen
npm install
npx playwright install chromium
Files
| File | Purpose |
|---|
render.js | CLI entry — parses flags, generates HTML, takes screenshot, writes outputs |
generate.js | Config JSON → standalone HTML page |
screenshot.js | HTML → PNG via Playwright (chromium headless) |
templates/lockscreen.css | All visual styling (status bar, clock, banners, depth-effect, tear overlay) |
templates/icons.js | Inline SVG icons (signal, wifi, battery, lock, chevron) |
examples/*.json | Reference configs (empty lockscreen, single notification, full Yubo flood) |
tests/run-all.sh | Render every example into tests/output/ for visual review |