fake-screencast — screenshots → simulated screen recording skill
Turn static SCREENSHOTS into a simulated screen recording (TSX) — a fake screencast with an animated cursor that eases to targets and clicks, a browser URL bar that updates per page, hard-cut navigations, in-page filter crossfades, smooth scroll, and a ken-burns zoom onto the payoff.
by hassancs91·MIT license·★ 330 Stars on the repo·GitHub ↗
npx degit hassancs91/claude-youtube-editor/.claude/skills/fake-screencast#main ~/.claude/skills/fake-screencastChecked ·commit main
Inside this skill
SKILL.md · the author's own words# fake-screencast — screenshots → simulated screen recording Make a screenshot walkthrough look like a real screen recording: persistent browser chrome with a **URL bar that changes per page**, a **cu… This is a beat-building technique inside **`make-tsx`** (step 2). Use `make-tsx` for the timeline.json / render / bake mechanics and the sy… ## When this is the right tool (decision gate) - **Fake screencast (this skill)** — you have or can capture screenshots of the states, and you want a *controlled* walkthrough (cursor pat… - **Real screen recording** — only when the point is genuine **proof**: real latency, a live result appearing, something you can't fake cre… - **Single static page clone** — if it's one page with no navigation/cursor (e.g. a pricing page you just highlight/scroll), use `WebBrowse… ## Step 1 — get the screenshots (ask the user) Tell the user the exact shot list you need (one screenshot per **page or state** the cursor lands on), then wait for them. Requirements:
Word-for-word excerpt, not a result. Read the full SKILL.md ↓
Files of fake-screencast — screenshots → simulated screen recording
Show the full text93 lines
fake-screencast — screenshots → simulated screen recording
Make a screenshot walkthrough look like a real screen recording: persistent browser chrome with a URL bar that changes per page, a custom cursor that eases along a bezier path to each click target and ripples on click, page navigation as a hard cut (URL path changes), an in-page filter as a short crossfade (URL query changes), smooth scroll, a slow ken-burns zoom onto the payoff element, and a constant drift so it never looks like a slideshow. TSX-first: controllable, re-renderable, reusable across videos.
This is a beat-building technique inside make-tsx (step 2). Use make-tsx for the timeline.json / render / bake mechanics and the sync-to-words principles; use vidtsx-2d-generator for the low-level rules that keep a Remotion file from crashing (frame-based only, monotonic interpolate, Easing.bezier, no useState/useEffect). The shot you write here follows those rules.
When this is the right tool (decision gate)
- Fake screencast (this skill) — you have or can capture screenshots of the states, and you want a controlled walkthrough (cursor path, highlight, zoom, timing synced to narration). This is P4 "real UI for config", just simulated. Best when the walkthrough is about where things are / the steps, not raw speed.
- Real screen recording — only when the point is genuine proof: real latency, a live result appearing, something you can't fake credibly. Leave a clearly-noted
pending_recordingplaceholder slot until it lands (seemake-tsx). - Single static page clone — if it's one page with no navigation/cursor (e.g. a pricing page you just highlight/scroll), use
WebBrowserFramefromlib/browser.tsxdirectly. Don't reach for the screencast machinery.
Step 1 — get the screenshots (ask the user)
Tell the user the exact shot list you need (one screenshot per page or state the cursor lands on), then wait for them. Requirements:
- Logged in / real data, the actual product UI, captured at a consistent window size (ideally ~1920-wide).
- One per state, including intermediate states: an unfiltered list AND its filtered result count as two screenshots (the crossfade sells the "search applied").
- For a real scroll-through of a long page, you need a tall capture (full page, below the fold) — a single-viewport screenshot can't truly scroll (you can only fake a few px of drift). Say so and ask for the tall one if the beat needs it.
- Ask for any missing view by name (e.g. "the models list filtered to X", "the settings page after toggling Y").
Save them to media/library/images/<service>/ with clear names (account-home.png, workers-ai.png, models-all.png, models-flux.png). Reference in a page as library/images/<service>/<name>.png (that's the staticFile-relative path the component expects).
Step 2 — the library API (remotion/src/lib/screencast.tsx)
<Screencast pages cursor clicks box glow favicon appearAt />. Coordinates are FRACTIONS so they survive any resize:
- cursor
x/yand click ripples → VIEWPORT fraction (0..1 of the page area under the URL bar). - zoom
fx/fy→ IMAGE fraction (transform-origin of the push).
import { Screencast, ScreencastPage, CursorKey } from '../../lib/screencast';
const PAGES: ScreencastPage[] = [
{ img: 'library/images/svc/home.png', url: 'app.svc.com/…/home', tabTitle: 'Home | Svc', enterAt: 0 },
{ img: 'library/images/svc/list.png', url: 'app.svc.com/…/items', tabTitle: 'Items | Svc', enterAt: 130, transition: 'cut' }, // navigation → hard cut + new path
{ img: 'library/images/svc/filtered.png', url: 'app.svc.com/…/items?q=x', tabTitle: 'Items | Svc', enterAt: 262, transition: 'crossfade', transitionFrames: 5, // in-page filter → crossfade + same path + query
drift: 0.01, zoom: { from: 1.0, to: 1.4, fx: 0.32, fy: 0.52, range: [266, 289] } }, // ken-burns onto the payoff card
];
const CURSOR: CursorKey[] = [ // {frame, x, y} in viewport fractions; eased per segment
{ frame: 0, x: 0.60, y: 0.42 }, { frame: 128, x: 0.09, y: 0.68 }, /* … */ { frame: 294, x: 0.36, y: 0.52 },
];
const CLICKS = [130, 206, 255]; // frames a ripple fires (place at the arrival keyframe)
export const compositionConfig = { id: 'SvcRecording', durationInSeconds: 10.0, fps: 30, width: 1920, height: 1080 };
const SvcRecording = () => <Screencast pages={PAGES} cursor={CURSOR} clicks={CLICKS} />;
export default SvcRecording;
Page fields: img, url, tabTitle, enterAt (required); transition?: 'cut'|'crossfade' (default cut), transitionFrames? (default 5), scroll?: {to, range:[a,b], from?} (image px translateY), zoom?: {from,to,fx,fy,range:[a,b]}, drift? (default 0.02 — the constant slow "alive" push). Screencast props: box? (default {x:60,y:63,w:1800,h:954}), glow?, appearAt?, favicon? (default Cloudflare cloud — pass your own for other services).
Step 3 — timing + coordinates
- Frame 0 = the shot's
master_in_s. For a narration cue attseconds:frame = round((t − master_in_s) × fps). Grepedited-transcript.jsonfor the exact word start/end (ms). Put the click / pageenterAta few frames before the word so the result lands on the word; end a ken-burnszoom.rangeon the payoff word. - Find a click-target fraction from the screenshot: element's pixel position ÷ image width/height ≈ the viewport fraction (the image fills the page region, top-aligned, fit-to-width). Set the cursor's arrival keyframe there and add the click frame ~2f after arrival. Then render and look (Step 4) — nudge the fraction until the pointer sits on the element.
- Sequence each page/cursor move to its cue: dashboard hold on the intro line → cursor eases to a sidebar item and clicks as that item is named → next page appears → zoom to the payoff as the payoff word is spoken.
Gotchas (each of these cost a render to find)
- Zero-height containing block.
WebBrowserFramewraps children in atransformed div, which becomes the containing block for absolute descendants and collapses to 0 height. Page layers therefore need explicit region dims (width/heightfrom the box), notinset:0— the lib already does this; keep it if you extend it. - Navigation vs filter is the realism tell. A page change = hard cut + different URL path. An in-page search/filter = short crossfade + same path (query appended). Mixing these up reads as fake.
- Single-viewport screenshots can't scroll — fake "aliveness" with
drift+ a ken-burnszoominstead. Only use a realscrollwhen you have a tall capture. - Render
--scale=1for the preview (1080p, correct forbake.py); re-render--scale=2for the 4K60 final. - Cursor hotspot (the tip) is near the pointer SVG's top-left; the lib nudges for it. If a pointer looks a few px off, adjust the target fraction, not the SVG.
Step 4 — verify, then hand back to make-tsx
- Render + screenshot at EACH cue (never one still):
node remotion/scripts/render-all.mjs --scale=1 <Id>, then pull frames with ffmpeg at the cursor-on-target frame, each click frame, the page-change frame, and the zoom payoff frame. Confirm the pointer lands on the element, URLs change correctly, and the zoom frames the right thing. Iterate the fractions/frames and re-render. - Then follow
make-tsxfor the rest: updatetimeline.json(swap/retime the span, drop anypending_recordingnote),python tools/bake.py, and spot-check composited frames of the shot over the real master at the beat + both boundaries. - If you improve the component (per-page favicons, real typed-text-in-field, a caption layer), promote it back into
lib/screencast.tsxso future videos inherit it.
The canonical shape — copy this choreography
The move that reads as a real screen recording, over ~4 captured pages and ~10s:
- Hold on the landing page while the narration sets it up (a beat of
drift, never a frozen frame). - Cursor eases to a sidebar item and clicks on the word that names it — the click lands on the noun, not before it.
- Hard cut to that page, URL path changes. This is a navigation.
- Type a filter → short crossfade, same path with the query appended. This is not a navigation, and rendering it as one is the single biggest tell.
- Ken-burns zoom onto the payoff exactly as the payoff word is spoken.
Read remotion/src/lib/screencast.tsx before building one — it's the engine, and its props are
the documentation for every move above. The pages array, the cursor keyframes, the per-page URL and
zoom are all typed there.
| 1 | |
| 2 | name fake-screencast |
| 3 | description Turn static SCREENSHOTS into a simulated screen recording (TSX) — a fake screencast with an animated cursor that eases to targets and clicks, a browser URL bar that updates per page, hard-cut navigations, in-page filter crossfades, smooth scroll, and a ken-burns zoom onto the payoff. Use when a beat needs to show a walkthrough of a web app / dashboard / settings page and you'd rather build it from screenshots than get (or ask for) a real screen recording — "fake a screen recording", "turn these screenshots into a screencast", "simulate clicking through the dashboard", "animate this UI walkthrough", "cursor moving through the app". Built on remotion/src/lib/screencast.tsx. This is a technique WITHIN step 2 (make-tsx): defer timeline/render/bake orchestration to make-tsx and raw crash-free TSX rules to vidtsx-2d-generator. Not for a single static page clone (use WebBrowserFrame directly) and not when the point is genuine proof of real speed/output (use a real recording). |
| 4 | |
| 5 | |
| 6 | # fake-screencast — screenshots → simulated screen recording |
| 7 | |
| 8 | Make a screenshot walkthrough look like a real screen recording: persistent browser chrome with a **URL bar that changes per page**, a **custom cursor** that eases along a bezier path to each click target and **ripples on click**, page **navigation as a hard cut** (URL path changes), an in-page **filter as a short crossfade** (URL query changes), smooth **scroll**, a slow **ken-burns zoom** onto the payoff element, and a constant **drift** so it never looks like a slideshow. TSX-first: controllable, re-renderable, reusable across videos. |
| 9 | |
| 10 | This is a beat-building technique inside **`make-tsx`** (step 2). Use `make-tsx` for the timeline.json / render / bake mechanics and the sync-to-words principles; use **`vidtsx-2d-generator`** for the low-level rules that keep a Remotion file from crashing (frame-based only, monotonic `interpolate`, `Easing.bezier`, no `useState`/`useEffect`). The shot you write here follows those rules. |
| 11 | |
| 12 | ## When this is the right tool (decision gate) |
| 13 | |
| 14 | **Fake screencast (this skill)** — you have or can capture screenshots of the states, and you want a *controlled* walkthrough (cursor path, highlight, zoom, timing synced to narration). This is P4 "real UI for config", just simulated. Best when the walkthrough is about *where things are* / *the steps*, not raw speed. |
| 15 | **Real screen recording** — only when the point is genuine **proof**: real latency, a live result appearing, something you can't fake credibly. Leave a clearly-noted `pending_recording` placeholder slot until it lands (see `make-tsx`). |
| 16 | **Single static page clone** — if it's one page with no navigation/cursor (e.g. a pricing page you just highlight/scroll), use `WebBrowserFrame` from `lib/browser.tsx` directly. Don't reach for the screencast machinery. |
| 17 | |
| 18 | ## Step 1 — get the screenshots (ask the user) |
| 19 | |
| 20 | Tell the user the exact shot list you need (one screenshot per **page or state** the cursor lands on), then wait for them. Requirements: |
| 21 | |
| 22 | **Logged in / real data**, the actual product UI, captured at a consistent window size (ideally ~1920-wide). |
| 23 | **One per state**, including intermediate states: an unfiltered list AND its filtered result count as two screenshots (the crossfade sells the "search applied"). |
| 24 | For a **real scroll-through of a long page**, you need a **tall** capture (full page, below the fold) — a single-viewport screenshot can't truly scroll (you can only fake a few px of drift). Say so and ask for the tall one if the beat needs it. |
| 25 | Ask for any **missing view** by name (e.g. "the models list filtered to X", "the settings page after toggling Y"). |
| 26 | |
| 27 | Save them to `media/library/images/<service>/` with clear names (`account-home.png`, `workers-ai.png`, `models-all.png`, `models-flux.png`). Reference in a page as `library/images/<service>/<name>.png` (that's the `staticFile`-relative path the component expects). |
| 28 | |
| 29 | ## Step 2 — the library API (`remotion/src/lib/screencast.tsx`) |
| 30 | |
| 31 | `<Screencast pages cursor clicks box glow favicon appearAt />`. **Coordinates are FRACTIONS** so they survive any resize: |
| 32 | **cursor `x`/`y` and click ripples → VIEWPORT fraction** (0..1 of the page area *under* the URL bar). |
| 33 | **zoom `fx`/`fy` → IMAGE fraction** (transform-origin of the push). |
| 34 | |
| 35 | |
| 36 | import { Screencast, ScreencastPage, CursorKey } from '../../lib/screencast'; |
| 37 | |
| 38 | const PAGES: ScreencastPage[] = [ |
| 39 | { img: 'library/images/svc/home.png', url: 'app.svc.com/…/home', tabTitle: 'Home | Svc', enterAt: 0 }, |
| 40 | { img: 'library/images/svc/list.png', url: 'app.svc.com/…/items', tabTitle: 'Items | Svc', enterAt: 130, transition: 'cut' }, // navigation → hard cut + new path |
| 41 | { img: 'library/images/svc/filtered.png', url: 'app.svc.com/…/items?q=x', tabTitle: 'Items | Svc', enterAt: 262, transition: 'crossfade', transitionFrames: 5, // in-page filter → crossfade + same path + query |
| 42 | drift: 0.01, zoom: { from: 1.0, to: 1.4, fx: 0.32, fy: 0.52, range: [266, 289] } }, // ken-burns onto the payoff card |
| 43 | ]; |
| 44 | |
| 45 | const CURSOR: CursorKey[] = [ // {frame, x, y} in viewport fractions; eased per segment |
| 46 | { frame: 0, x: 0.60, y: 0.42 }, { frame: 128, x: 0.09, y: 0.68 }, /* … */ { frame: 294, x: 0.36, y: 0.52 }, |
| 47 | ]; |
| 48 | const CLICKS = [130, 206, 255]; // frames a ripple fires (place at the arrival keyframe) |
| 49 | |
| 50 | export const compositionConfig = { id: 'SvcRecording', durationInSeconds: 10.0, fps: 30, width: 1920, height: 1080 }; |
| 51 | const SvcRecording = () => <Screencast pages={PAGES} cursor={CURSOR} clicks={CLICKS} />; |
| 52 | export default SvcRecording; |
| 53 | |
| 54 | |
| 55 | Page fields: `img, url, tabTitle, enterAt` (required); `transition?: 'cut'|'crossfade'` (default `cut`), `transitionFrames?` (default 5), `scroll?: {to, range:[a,b], from?}` (image px translateY), `zoom?: {from,to,fx,fy,range:[a,b]}`, `drift?` (default 0.02 — the constant slow "alive" push). Screencast props: `box?` (default `{x:60,y:63,w:1800,h:954}`), `glow?`, `appearAt?`, `favicon?` (default Cloudflare cloud — **pass your own for other services**). |
| 56 | |
| 57 | ## Step 3 — timing + coordinates |
| 58 | |
| 59 | **Frame 0 = the shot's `master_in_s`.** For a narration cue at `t` seconds: `frame = round((t − master_in_s) × fps)`. Grep `edited-transcript.json` for the exact word start/end (ms). Put the click / page `enterAt` a few frames **before** the word so the *result* lands on the word; end a ken-burns `zoom.range` **on** the payoff word. |
| 60 | **Find a click-target fraction** from the screenshot: element's pixel position ÷ image width/height ≈ the viewport fraction (the image fills the page region, top-aligned, fit-to-width). Set the cursor's arrival keyframe there and add the click frame ~2f after arrival. Then **render and look** (Step 4) — nudge the fraction until the pointer sits on the element. |
| 61 | Sequence each page/cursor move to its cue: dashboard hold on the intro line → cursor eases to a sidebar item and clicks as that item is named → next page appears → zoom to the payoff as the payoff word is spoken. |
| 62 | |
| 63 | ## Gotchas (each of these cost a render to find) |
| 64 | |
| 65 | **Zero-height containing block.** `WebBrowserFrame` wraps children in a `transform`ed div, which becomes the containing block for absolute descendants and collapses to 0 height. Page layers therefore need **explicit region dims** (`width/height` from the box), not `inset:0` — the lib already does this; keep it if you extend it. |
| 66 | **Navigation vs filter is the realism tell.** A page change = **hard cut + different URL path**. An in-page search/filter = **short crossfade + same path (query appended)**. Mixing these up reads as fake. |
| 67 | **Single-viewport screenshots can't scroll** — fake "aliveness" with `drift` + a ken-burns `zoom` instead. Only use a real `scroll` when you have a tall capture. |
| 68 | **Render `--scale=1` for the preview** (1080p, correct for `bake.py`); re-render `--scale=2` for the 4K60 final. |
| 69 | Cursor hotspot (the tip) is near the pointer SVG's top-left; the lib nudges for it. If a pointer looks a few px off, adjust the target fraction, not the SVG. |
| 70 | |
| 71 | ## Step 4 — verify, then hand back to make-tsx |
| 72 | |
| 73 | **Render + screenshot at EACH cue** (never one still): `node remotion/scripts/render-all.mjs --scale=1 <Id>`, then pull frames with ffmpeg at the **cursor-on-target** frame, each **click** frame, the **page-change** frame, and the **zoom payoff** frame. Confirm the pointer lands on the element, URLs change correctly, and the zoom frames the right thing. Iterate the fractions/frames and re-render. |
| 74 | Then follow `make-tsx` for the rest: update `timeline.json` (swap/retime the span, drop any `pending_recording` note), `python tools/bake.py`, and **spot-check composited frames** of the shot over the real master at the beat + both boundaries. |
| 75 | If you improve the component (per-page favicons, real typed-text-in-field, a caption layer), promote it back into `lib/screencast.tsx` so future videos inherit it. |
| 76 | |
| 77 | ## The canonical shape — copy this choreography |
| 78 | |
| 79 | The move that reads as a real screen recording, over ~4 captured pages and ~10s: |
| 80 | |
| 81 | **Hold** on the landing page while the narration sets it up (a beat of `drift`, never a frozen frame). |
| 82 | **Cursor eases** to a sidebar item and **clicks on the word that names it** — the click lands on the |
| 83 | noun, not before it. |
| 84 | **Hard cut** to that page, **URL path changes**. This is a navigation. |
| 85 | **Type a filter** → **short crossfade**, **same path with the query appended**. This is not a |
| 86 | navigation, and rendering it as one is the single biggest tell. |
| 87 | **Ken-burns zoom** onto the payoff exactly as the payoff word is spoken. |
| 88 | |
| 89 | **Read `remotion/src/lib/screencast.tsx` before building one** — it's the engine, and its props are |
| 90 | the documentation for every move above. The pages array, the cursor keyframes, the per-page URL and |
| 91 | zoom are all typed there. |
| 92 | |
| 93 |
Discussion
Alternatives
Browse more free Claude skills or everything in Content creator.