cinematic-shot
Shoot software UI like a film. A sequence of hero shots, each a steep, oblique, extreme close-up of the interface floating in black, with a razor-thin focal plane, a slow motivated camera drift, and UI that acts on its own, hard-cut on the action, ending on one line of copy and a still logo. Use when asked for a launch video, product film, feature trailer, teaser, promo clip, hero shot, or any "cinematic" footage of an app or UI. This skill covers camera, framing, light, and edit. It is stack-agnostic.
Install
npx skills add https://github.com/1weiho/cinematic-shot --skill cinematic-shotLook recipes
These are the exact values behind the look. They are written as CSS where CSS is the most direct way to describe them. Port them to whatever renderer you use. Pixel values assume a 1920 × 1080 frame; scale them with the frame height.
Camera, depth of field, reveals, and motion are shared. Stage, light, grade, the exit, the outro, and the logo come in two versions: the dark stage for products with a genuine dark theme, and the light stage for light-only or light-first products. Pick one per film and use it for every shot.
Stage
Dark stage
The product's own dark chrome is the only surface. Everything that is not UI is black.
Light stage
The ground must be a little darker than the page, so the white slab separates from it. Never white, never black.
The slab is the page's real viewport (1440 × 900 UI px), white, with rounded corners, floating over the ground. Its cast shadow on the ground is what makes it read as an object in a studio (values in UI px on the slab):
Camera
Put the UI on one plane inside a perspective camera. Rotate in-plane first, then tilt, then yaw. In CSS, transforms apply right to left, so the order below does exactly that:
Check the result: in-focus text should read diagonally, and near text should be about 1.4–1.8× the size of far text. If the frame looks like a flat screenshot, increase pitch before anything else.
Drift
Interpolate the translate, a small scale change, and a small angle change across the shot:
Use linear interpolation, or an ease-out over the first third that hands off to a slow, constant drift. Never ease to zero velocity before the cut.
Depth of field
This is optical depth of field, not a blurred background layer. Blur depends on each point's depth relative to the focal plane, so it grows continuously toward both the near and the far edge of the tilted UI.
The values are the same on both stages. On the light stage, the bokeh becomes a soft haze over white, which is the point of that look.
Implementation in a DOM stack: depth is linear across the plane, so you can approximate it with 6–8 stacked copies of the plane, each with a stronger filter: blur(), each masked to a band at a greater distance from the focal line with mask-image: linear-gradient(...) on the plane itself. Point the gradient along the plane's depth direction, which you get by applying the inverse rotation to the camera's z-axis. Feather each band by about a third of its width so no seams show.
Rack focus: animate depth(focus) (the focal line's position on the plane) over 0.4–0.8 s on an ease-in-out when attention moves.
Bokeh brightness:
- Dark stage: out-of-focus bright text should spread and dim, not glow. Lower its opacity to about 0.6–0.8× as it blurs.
- Light stage: do not dim out-of-focus areas (
bokehDim = 0). Dimming bright bokeh only makes sense on black.
Subject light
Dark stage: a pool
A neutral pool that follows the subject. A single screen-space radial gradient makes it look like a hole cut in a black overlay: always round, always the same, always centred on the same point. Four things make it read as light falling on the slab instead.
1. Put the pool on the plane
Light lands on the UI, so attach the pool to the plane and let the camera project it. Give the light layer the same size, transform-origin, and transform as .plane, and stack it above every depth-of-field copy:
All positions and radii are in UI pixels. The tilt foreshortens the ellipse into an egg that falls off quickly toward the far edge and stretches toward the near edge, and the roll lays it along the text diagonal. Every shot gets a different shape for free, because every shot has a different camera.
Make the pool longer along the line of action than across it: wide for a line of typing or a row, tall for a column that streams downward.
To get UI pixels, divide the on-screen radius by the magnification: at 4×, a 0.42 radius is 0.42 × 1920 / 4 ≈ 200 UI px. Check the result on screen and adjust, since the tilt compresses the across axis.
2. Give it a direction
A real fixture hangs off to one side. Offset the core (--kx, --ky) from the spill centre by 8–15% of the spill radius, toward the key. The two falloffs then stop being concentric: one side of the subject has a long, soft shoulder and the other drops off quickly.
Put the key toward the far side of the slab and a little to one side, and flip that side whenever the roll flips sign, so the light direction alternates with the diagonal. Keep it fixed within a shot.
The two layers stack multiplicatively (each passes 1 − alpha of the light), which gives a hot spot with a gentle spill instead of one linear ramp to black. Keep at least five stops in the spill: three linear stops are what make a gradient read as CSS.
3. Flag it, sometimes
In about a third of the shots, cut one side of the pool with a soft straight edge, like a flag in front of the fixture:
Place the edge 20–30% of the frame width from the subject, at least 25° off the text diagonal, with a 15–25% feather. The subject itself stays on the lit side. The flag rides with the pool, keeping the same offset from it as the camera drifts, so it never sweeps across the frame.
4. Let it react to the action
The light never performs, but it does respond:
- Trail the subject. Drive the spill centre with a lagged copy of the subject's position, so the light arrives just after the subject, like a follow-spot operator. It never leads. To keep each frame a pure function of time, average the path over a short window:
light(t) = mean of subject(τ) for τ in [t − T, t], withT= 0.3–0.5 s andτclamped to the start of the shot. The core follows the same lagged position, plus its offset. - Follow the rack. When focus racks to a new subject, the pool slides there too, through the same lag, so it lands slightly after the focus does.
- Open with the content. When an answer streams in or a document writes itself, grow the spill radii 20–40% over the shot to keep the new lines lit, smoothed with the same window.
- Open up for the payoff. The wide shot uses the widest, flattest pool in the film. The cut back to a close-up returns to a tight one, and that contrast is part of the release.
Frame vignette
The vignette belongs to the lens, not the light, so it stays centred and constant. It only keeps the corners black:
No colour goes into any of these layers.
Light stage: a soft key
No pool mask and nothing falling to black. Keep the .light layer from the dark stage, on the plane and trailing the subject through the same lag, but change what it does:
- Darken by at most about 12% away from the subject. Nothing on the slab goes darker than that.
- Radii about 1.6× the dark-stage spill, still longer along the line of action than across it.
- One layer, no core: a single spill with a long, gentle shoulder. Keep the stops, scaled to the 12% ceiling:
Clip the key to the slab; it never falls on the ground. The slab's own cast shadow does the work of separating it.
Flags are off. Nothing casts a dark edge across the slab.
Lens vignette: about 7% at most, toward grey, never black:
No colour goes into any layer.
Light stage: crop edges must never show
On black, the pool hid crop edges; on a light stage, nothing can. Capture two layers per frame:
Composite the detail over the context with a feather of about 24 UI px. The slab's edge is then always the page's real edge, never a crop boundary. With the Chrome DevTools Protocol, Page.captureScreenshot with clip.scale renders a region at any scale without changing the device pixel ratio or firing resize events, at about 0.1 s for each pair.
Grade
Dark stage
The product slab keeps its real chrome. Add only a faint edge to separate panels from the void:
Light stage
Streaming reveal (stipple dissolve)
Generated text, icons, and avatars resolve line by line out of a fine stipple of dots:
- While
p < 1, draw the visible dots 10–15% brighter (on the light stage, 10–15% stronger ink), and add a 1–1.5 px blur that falls to 0 atp = 1. - Stagger lines by about 0.1 s, top to bottom. Start each line slightly before the previous one finishes.
- Rows that hold icons resolve the icon and the text together.
Other motion
When you record a real site, use the page's own animations for these events instead of the recipes above.
Leaving the last UI shot
- Dark stage: hard-cut to black, or over 0.4 s:
blur 0 → 12pxandbrightness 1 → 0. - Light stage: hard-cut to the ground, or over 0.4 s:
blur 0 → 12pxwhile fading to the ground colour. Never to black.
Outro type
Enter over 0.8 s on an ease-out: filter: blur(16px) → blur(0) and opacity 0 → 1. On the dark stage, an optional soft glow, text-shadow: 0 0 40px rgb(255 255 255 / 0.3), fades to 0 as the type sharpens. On the light stage there is no glow.
A quieter alternative is to fade the colour up from 35% to full strength over 1.2 s, with no blur.
For the name-then-tagline variant, set the name, hold it for about 1.5 s, then fade in the tagline after it on the same line over 0.6 s while the whole line slides to re-centre on an ease-in-out.
Logo
Use the mark alone, or the mark with the wordmark. The mark is about 115 px tall, the gap to the wordmark about 0.4× the mark height, and the wordmark the size of the outro type. On the dark stage, plain white. On the light stage, the brand colour (for example, Laravel red #F53003 for both mark and wordmark) on the ground colour. No bloom and no glow. Cut or dissolve (0.3 s) to it, hold it completely still for 4–5 s, and end the film on it.
Stills
A still uses the stage, light, grade, reveal, and type recipes above, frozen at one moment. It does not use the film's camera: a still gets its own camera and focus, below.
Formats
Pick the format before the angle, and reframe the camera for each one instead of cropping. Render at 2× and deliver at 1×.
Pixel values elsewhere in this file assume a 1080 px frame height; scale them with the delivery height, then double them for the 2× render.
Camera
Add a camera roll after the perspective tilt, so the whole frame rotates and the plane stays level on itself:
Put the perspective origin on the subject (a lens shift), not at the frame centre, so the subject's own geometry stays clean wherever it sits in the frame.
Check the result: near text is about 1.2–1.4× the size of far text, the subject's text lines run straight along the diagonal, and the frame does not look like it is swinging past. No drift, no push, no turn: one transform per still.
Focus
Fit the sharp zone to the whole subject, then fall off fast. Measure defocus as the relative distance to the focal plane, the way a lens does:
- Every corner of the subject is sharp (blur 0).
- Choose
kso that the next line beyond the subject is already soft, about 2–4 px, and the far third of the frame reachesmaxBlur. Use themaxBlurvalues from the depth-of-field table above. - Never widen a fixed band to cover the subject: it sharpens the whole frame and the bokeh is gone.
With camera roll and no in-plane roll, the lines of equal depth run along the text, so a whole line, word, or headline fits in the sharp zone however wide it is.
Light
- Dark stage: the same two-layer pool on the plane, with the spill centred on the subject and the core offset 8–15% toward the key. There is no lag window. Use a flag in about half of the stills in a set rather than a third.
- Light stage: the same soft key, centred on the subject. Still no flags.
- The lens vignette is unchanged.
Frozen moment
Seek every animation to one instant, the same way a film frame is a pure function of time:
Never freeze an Appear, a slide between two states, or a scroll mid-ease.
Render pipeline
- Capture the UI at the still's magnification × 2 (the 2× render), with the Detail and Context layers on the light stage as above.
- Composite stage, slab, depth of field, light, and vignette at the 2× size. No motion blur.
- Downsample to the delivery size with a Lanczos filter.
- Add static luma grain, about half the film's strength:
noisestrength about 2 on the dark stage and 1.5 on the light stage, on the luma plane only. - Write lossless PNG. For the web, also write a JPEG at quality about 90 without chroma subsampling (4:4:4), or a lossless WebP; lossy WebP always subsamples chroma.
With ffmpeg, steps 3–5 are:
Grain is the last step before the delivery file is written, so it also dithers the pool's dark gradients. Without it, the dark stage shows banding.
Contact sheet
Before the full-resolution render, render each still at a quarter of its delivery size, lay them out on one sheet in still-list order with their numbers, and show it to the user. Wait for their OK.