22bc903041
Deploy / deploy (push) Successful in 1m5s
- nginx basic auth on /previz/ (login file lives on the server, mounted from docker/auth/, see DEPLOY.md step 8); private, no-store so Cloudflare never caches it - Print styles for previz pages: dark, A4 landscape, one section a page; videos print as poster frames with a link - scripts/previz-pdf.mjs (npm run previz:pdf) prints the page to public/previz/crowd/crowd-previz.pdf via Edge/Chrome DevTools - "PDF" download link on the page Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
431 lines
20 KiB
TypeScript
431 lines
20 KiB
TypeScript
import type { Metadata, Viewport } from "next";
|
|
import Fox from "@/components/Fox";
|
|
import SectionLabel from "@/components/SectionLabel";
|
|
import ShotList, { type Shot } from "@/components/previz/ShotList";
|
|
import { site } from "@/content/site";
|
|
|
|
// Pre-shoot explainer for production: how the drone crowd shot gets built.
|
|
// Shared by link only. Not linked from the site, kept out of search and the sitemap.
|
|
export const metadata: Metadata = {
|
|
title: "Drone crowd shot: previz",
|
|
description: "How the drone crowd shot is planned, shot and composited.",
|
|
robots: { index: false, follow: false },
|
|
};
|
|
|
|
export const viewport: Viewport = {
|
|
themeColor: "#0a0a0b",
|
|
};
|
|
|
|
const dir = "/previz/crowd";
|
|
|
|
// Drop the renders in public/previz/crowd/ and fill these in when they're done.
|
|
const videos: { hero?: string; heroPoster?: string; final?: string; finalPoster?: string } = {
|
|
hero: `${dir}/hero-previz.mp4`,
|
|
heroPoster: `${dir}/hero-previz.jpg`,
|
|
final: `${dir}/final-comp.mp4`,
|
|
finalPoster: `${dir}/final-comp.jpg`,
|
|
};
|
|
|
|
// Plate A sits nearest the stage, D furthest back (order in the 3D layout).
|
|
const plates = [
|
|
{ id: "A", src: `${dir}/plate-a.jpg`, where: "Nearest the stage, just behind the real crowd" },
|
|
{ id: "B", src: `${dir}/plate-b.jpg`, where: "Second row back" },
|
|
{ id: "C", src: `${dir}/plate-c.jpg`, where: "Third row back" },
|
|
{ id: "D", src: `${dir}/plate-d.jpg`, where: "Back of the crowd, where the drone starts" },
|
|
];
|
|
|
|
// Labels pinned to the front edge of each block in layout-3d.jpg (percent of the image).
|
|
const layoutLabels = [
|
|
{ id: "Hero", x: 68, y: 52, hero: true },
|
|
{ id: "A", x: 64, y: 61 },
|
|
{ id: "B", x: 54, y: 75 },
|
|
{ id: "C", x: 45.5, y: 80 },
|
|
{ id: "D", x: 34, y: 79 },
|
|
];
|
|
|
|
// In shooting order: hero first, then the plate drone setup.
|
|
const shots: Shot[] = [
|
|
{
|
|
id: "hero",
|
|
name: "Hero drone move",
|
|
setup: "Hero drone",
|
|
notes: "Behind the crowd to the truck stage. Constant height and tilt. Real crowd placed close to the stage.",
|
|
},
|
|
{
|
|
id: "clean",
|
|
name: "Clean plate",
|
|
setup: "Plate position",
|
|
notes: "Empty block area, no people, from the locked plate position. Roll a few seconds before the crowd walks in.",
|
|
},
|
|
...plates.map((p) => ({
|
|
id: `plate-${p.id.toLowerCase()}`,
|
|
name: `Crowd plate ${p.id}`,
|
|
setup: "Plate position",
|
|
notes: `Locked off, crowd fills the marked block. ${p.id === "A" ? "Mix people up from their hero shot spots" : "Everyone swaps places from the last plate"}. Roll longer than the hero shot.`,
|
|
})),
|
|
];
|
|
|
|
type Tip = { title: string; body: string };
|
|
|
|
// For the 1st AD to brief the extras before each plate
|
|
const crowdTips: Tip[] = [
|
|
{
|
|
title: "Front two rows: arms below the shoulders",
|
|
body: "The front of each block (the stage side) overlaps the block in front of it in the comp. Arms up there cut across other people and are very hard to cut out. Clap, fist pump, bounce, but keep hands low. Everyone behind can go big.",
|
|
},
|
|
{
|
|
title: "Face the stage, not the drone",
|
|
body: "Same direction as the real crowd in the hero shot. Nobody looks up at the drone or points at it.",
|
|
},
|
|
{
|
|
title: "Stay inside the cones",
|
|
body: "Fill the marked block and don't drift out of it. Don't line up neatly on the edges either; a slightly ragged edge blends better.",
|
|
},
|
|
{
|
|
title: "Keep it going until cut",
|
|
body: "Same energy as the hero crowd from action to cut. No easing off when it feels long; we need the full plate.",
|
|
},
|
|
{
|
|
title: "New spot every plate",
|
|
body: "Nobody keeps their place or their neighbours. Mix front to back and side to side between plates.",
|
|
},
|
|
{
|
|
title: "Swap the memorable stuff",
|
|
body: "Bright hats, jackets, scarves and bags get swapped or taken off between plates, so the same person doesn't show up four times.",
|
|
},
|
|
{
|
|
title: "No flags, signs or poles",
|
|
body: "Anything held up high crosses into the next block. Keep them for the real crowd in the hero shot only.",
|
|
},
|
|
{
|
|
title: "Leave nothing on the grass",
|
|
body: "No bags, bottles or jackets on the ground inside or near the block, and clear it fully for the clean plate.",
|
|
},
|
|
];
|
|
|
|
// For the DOP and drone operator
|
|
const cameraTips: Tip[] = [
|
|
{
|
|
title: "Plates: fast shutter, minimal motion blur",
|
|
body: "Around 1/100 to 1/200 (90° or narrower) on the crowd plates so waving arms stay sharp and can be cut out. We add matching motion blur in comp. Use ND to hold exposure.",
|
|
},
|
|
{
|
|
title: "Hero: normal shutter, slow and steady",
|
|
body: "180° is fine for the hero move. Keep the move slow, straight and constant speed with no yaw, so the track is clean and blur stays low.",
|
|
},
|
|
{
|
|
title: "Everything manual and locked",
|
|
body: "Same lens, resolution, frame rate, ISO, white balance and colour profile for the hero and every plate. No auto exposure, auto ISO or auto white balance.",
|
|
},
|
|
{
|
|
title: "Same image settings throughout",
|
|
body: "Keep lens distortion correction, digital zoom and stabilisation crop in the same mode for every take. Changing them changes the framing.",
|
|
},
|
|
{
|
|
title: "Best quality the drone can record",
|
|
body: "Highest resolution and bitrate, 10-bit log. The plates get scaled down into the shot, so extra resolution keeps people crisp.",
|
|
},
|
|
{
|
|
title: "Lock the plate position",
|
|
body: "One spot, one height, one gimbal tilt for every plate and the clean plate. Tilt matches the hero shot. Let the drone settle before calling action.",
|
|
},
|
|
{
|
|
title: "Roll long",
|
|
body: "Each plate runs longer than the hero shot so we can offset timing and stop blocks moving in sync. Pre-roll and post-roll on the hero too.",
|
|
},
|
|
{
|
|
title: "Log the numbers",
|
|
body: "Height, gimbal tilt, lens and position for the hero start, hero end and the plate spot. A screenshot of the drone screen is perfect.",
|
|
},
|
|
];
|
|
|
|
// Production and VFX on the day
|
|
const setTips: Tip[] = [
|
|
{
|
|
title: "Mark the block on the ground",
|
|
body: "Cones or tape at the four corners of the crowd rectangle so every plate fills the same area.",
|
|
},
|
|
{
|
|
title: "Grab a clean plate",
|
|
body: "A few seconds of the empty block area from the same spot, so we can blend the ground under the crowd.",
|
|
},
|
|
{
|
|
title: "Watch the sun",
|
|
body: "Shoot the plates straight after the hero shot. Shadows must fall the same way, so no big gap in time and ideally no passing cloud.",
|
|
},
|
|
{
|
|
title: "Measure the field",
|
|
body: "Ten minutes with a tape before we wrap: stage, real crowd and block corners, plus the hero drone height. It lets us line the plates up in the tracked shot.",
|
|
},
|
|
];
|
|
|
|
function PreviewVideo({ src, poster, label }: { src?: string; poster?: string; label: string }) {
|
|
if (src) {
|
|
return (
|
|
<div>
|
|
<video
|
|
src={src}
|
|
poster={poster}
|
|
className="aspect-[2.39/1] w-full bg-black object-cover print:hidden"
|
|
controls
|
|
autoPlay
|
|
muted
|
|
loop
|
|
playsInline
|
|
preload="metadata"
|
|
/>
|
|
{/* Paper/PDF: the poster frame and a pointer to the moving version */}
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
{poster && <img src={poster} alt={label} className="hidden aspect-[2.39/1] w-full object-cover print:block" />}
|
|
<p className="mt-2 hidden font-mono text-[10px] tracking-[0.2em] text-muted uppercase print:block">
|
|
▶ Video: {site.url.replace(/^https?:\/\//, "")}/previz/crowd/
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<div className="relative flex aspect-[2.39/1] w-full items-center justify-center overflow-hidden border border-dashed border-line bg-ink-2">
|
|
<div className="absolute inset-x-0 top-0 h-px animate-scan bg-accent/30" aria-hidden="true" />
|
|
<div className="px-6 text-center">
|
|
<p className="font-mono text-[11px] tracking-[0.25em] text-accent uppercase">Rendering</p>
|
|
<p className="mt-2 text-sm text-muted md:text-base">{label}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TipGrid({ tips, className = "" }: { tips: Tip[]; className?: string }) {
|
|
return (
|
|
<ol className={`mt-10 grid gap-px border border-line bg-line md:grid-cols-2 ${className}`}>
|
|
{tips.map((c, i) => (
|
|
<li key={c.title} className="flex gap-4 bg-ink p-5 md:p-6">
|
|
<span className="font-mono text-xs text-accent">{String(i + 1).padStart(2, "0")}</span>
|
|
<div>
|
|
<p className="text-lg">{c.title}</p>
|
|
<p className="mt-1.5 text-sm leading-relaxed text-muted md:text-base">{c.body}</p>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
);
|
|
}
|
|
|
|
function Step({
|
|
index,
|
|
label,
|
|
title,
|
|
children,
|
|
}: {
|
|
index: string;
|
|
label: string;
|
|
title: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div>
|
|
<SectionLabel index={index}>{label}</SectionLabel>
|
|
<h2 className="font-display text-3xl leading-none font-thin uppercase md:text-5xl">{title}</h2>
|
|
<div className="mt-6 max-w-3xl space-y-4 text-lg leading-relaxed font-light text-muted md:text-xl">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function CrowdPrevizPage() {
|
|
return (
|
|
<main className="previz grain min-h-dvh bg-ink pb-24">
|
|
{/* Print/PDF: landscape A4, edge to edge. Inline so it beats the global @page rule. */}
|
|
<style>{"@page { size: A4 landscape; margin: 0; }"}</style>
|
|
<header className="mx-auto flex max-w-6xl items-center gap-3 px-4 pt-6 md:px-8">
|
|
<Fox className="h-6 w-6 text-accent" />
|
|
<span className="font-mono text-[11px] tracking-[0.25em] text-muted uppercase">{site.name} · Previz</span>
|
|
{/* Regenerate with `npm run previz:pdf` after editing this page */}
|
|
<a
|
|
href={`${dir}/crowd-previz.pdf`}
|
|
download
|
|
className="-my-2 ml-auto py-2 font-mono text-[11px] tracking-[0.2em] text-accent uppercase hover:text-fg print:hidden"
|
|
>
|
|
PDF ↓
|
|
</a>
|
|
</header>
|
|
|
|
<section className="mx-auto max-w-6xl px-4 pt-16 md:px-8 md:pt-24">
|
|
<p className="font-mono text-xs tracking-[0.3em] text-accent uppercase">VFX plan for production</p>
|
|
<h1 className="mt-5 font-display text-[clamp(2.2rem,8vw,5.5rem)] leading-[0.9] font-thin uppercase">
|
|
Drone crowd shot
|
|
</h1>
|
|
<p className="mt-8 max-w-3xl text-xl leading-relaxed font-light md:text-2xl">
|
|
One continuous drone move from behind the crowd toward the truck stage. We film one real crowd near the stage,
|
|
then build the rest of the audience from four locked-off plates of the same people, rearranged each time.
|
|
</p>
|
|
<dl className="mt-12 grid grid-cols-2 gap-px border border-line bg-line md:grid-cols-4">
|
|
{[
|
|
{ k: "Hero shot", v: "1 drone move" },
|
|
{ k: "Crowd plates", v: "4, locked off" },
|
|
{ k: "Drone setups", v: "2" },
|
|
{ k: "Extras needed", v: "One crowd block" },
|
|
].map((m) => (
|
|
<div key={m.k} className="bg-ink px-4 py-5">
|
|
<dt className="font-mono text-[10px] tracking-[0.2em] text-muted uppercase">{m.k}</dt>
|
|
<dd className="mt-1.5 text-lg">{m.v}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</section>
|
|
|
|
<section className="mx-auto mt-20 max-w-6xl px-4 md:mt-28 md:px-8 print:!mt-0 print:break-before-page">
|
|
<figure>
|
|
<div className="relative">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img src={`${dir}/layout-3d.jpg`} alt="3D layout: real crowd at the stage, four plate blocks behind it" className="w-full" />
|
|
{layoutLabels.map((l) => (
|
|
<span
|
|
key={l.id}
|
|
className={`absolute -translate-x-1/2 -translate-y-1/2 border px-1.5 py-0.5 font-mono text-[10px] font-semibold tracking-[0.15em] uppercase shadow-lg md:px-2.5 md:py-1 md:text-xs ${
|
|
l.hero ? "border-fg bg-fg text-ink" : "border-fg/70 bg-ink/85 text-fg"
|
|
}`}
|
|
style={{ left: `${l.x}%`, top: `${l.y}%` }}
|
|
>
|
|
{/* Phones: short tags so they don't collide */}
|
|
<span className="md:hidden">{l.hero ? "Real" : l.id}</span>
|
|
<span className="hidden md:inline">{l.hero ? "Real crowd" : `Plate ${l.id}`}</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
<figcaption className="mt-4 max-w-3xl text-sm leading-relaxed text-muted md:text-base">
|
|
The full layout from above. The block at the stage is the real crowd, filmed in the hero shot. The four
|
|
blocks behind it are added in post, each one a separate plate. The orange frames are those plates sitting in
|
|
the scene.
|
|
</figcaption>
|
|
</figure>
|
|
</section>
|
|
|
|
<div className="mx-auto mt-24 max-w-6xl space-y-24 px-4 md:mt-32 md:space-y-32 md:px-8">
|
|
<section className="space-y-10 print:!mt-0 print:break-before-page">
|
|
<Step index="01" label="Hero shot" title="One drone move, real crowd at the stage">
|
|
<p>
|
|
The drone starts behind the audience and flies forward toward the truck stage. Height and camera tilt stay
|
|
constant for the whole move, which keeps the angle on the crowd consistent and makes the added blocks hold
|
|
up.
|
|
</p>
|
|
<p>
|
|
The real crowd stands close to the stage, where the camera ends up closest and sees the most detail. The
|
|
empty ground behind them is where the plates go.
|
|
</p>
|
|
</Step>
|
|
<PreviewVideo src={videos.hero} poster={videos.heroPoster} label="Hero shot previz coming soon" />
|
|
</section>
|
|
|
|
<section className="space-y-10 print:!mt-0 print:break-before-page">
|
|
<Step index="02" label="Crowd plates" title="Same spot, same angle, new arrangement">
|
|
<p>
|
|
After the hero shot, the drone flies to a single marked position and holds still. No camera movement. The
|
|
same group of people fills a rectangle on the ground and we roll.
|
|
</p>
|
|
<p>
|
|
Between takes, everyone swaps places. Each take becomes one block of crowd, so four takes make four
|
|
different-looking blocks from the same people.
|
|
</p>
|
|
</Step>
|
|
<div className="grid gap-2 md:grid-cols-2">
|
|
{plates.map((p) => (
|
|
<figure key={p.id} className="relative">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img src={p.src} alt={`Plate ${p.id}`} loading="lazy" className="w-full" />
|
|
<figcaption className="absolute inset-x-0 bottom-0 flex items-baseline gap-3 bg-gradient-to-t from-ink/90 to-transparent px-4 pt-10 pb-3">
|
|
<span className="font-mono text-xs font-semibold tracking-[0.2em] text-fg uppercase">Plate {p.id}</span>
|
|
<span className="text-xs text-muted md:text-sm">{p.where}</span>
|
|
</figcaption>
|
|
</figure>
|
|
))}
|
|
</div>
|
|
<p className="max-w-3xl text-sm leading-relaxed text-muted md:text-base">
|
|
Colours are only there to tell the blocks apart in the previz. On the day it's the same extras every
|
|
time; only their positions change.
|
|
</p>
|
|
</section>
|
|
|
|
<section className="space-y-10 print:!mt-0 print:break-before-page">
|
|
<Step index="03" label="Composite" title="Plates placed into the moving shot">
|
|
<p>
|
|
We track the hero drone move in 3D, then place each plate as a flat layer on the ground behind the real
|
|
crowd, back to front. The drone moves, the layers stay put in the scene, and the crowd fills the field.
|
|
</p>
|
|
<p>
|
|
The proof of concept below was rendered exactly this way: the four plates projected onto cards inside the
|
|
tracked shot.
|
|
</p>
|
|
</Step>
|
|
<PreviewVideo src={videos.final} poster={videos.finalPoster} label="Final comp proof of concept coming soon" />
|
|
</section>
|
|
|
|
<section className="print:!mt-0 print:break-before-page">
|
|
<SectionLabel index="04">Shot list</SectionLabel>
|
|
<h2 className="font-display text-3xl leading-none font-thin uppercase md:text-5xl">Six shots, two setups</h2>
|
|
<p className="mt-6 max-w-3xl text-lg leading-relaxed font-light text-muted md:text-xl">
|
|
Tick them off as they're in the can.
|
|
</p>
|
|
<div className="mt-10">
|
|
<ShotList shots={shots} storeKey="ttvfx-previz-crowd-shots" />
|
|
</div>
|
|
</section>
|
|
|
|
<section className="print:!mt-0 print:break-before-page">
|
|
<SectionLabel index="05">Crowd direction</SectionLabel>
|
|
<h2 className="font-display text-3xl leading-none font-thin uppercase md:text-5xl">Briefing the extras</h2>
|
|
<p className="mt-6 max-w-3xl text-lg leading-relaxed font-light text-muted md:text-xl">
|
|
For the 1st AD to run through before the first plate, and again between plates.
|
|
</p>
|
|
<figure className="mt-10">
|
|
<div className="relative">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img src={`${dir}/plate-a.jpg`} alt="Plate A with the front two rows of the crowd marked" loading="lazy" className="w-full" />
|
|
{/* Front two rows of the block in plate-a.jpg (percent of the image) */}
|
|
<div
|
|
className="absolute border-2 border-dashed border-fg"
|
|
style={{
|
|
left: "18.5%",
|
|
top: "46%",
|
|
width: "62.5%",
|
|
height: "12%",
|
|
backgroundImage:
|
|
"repeating-linear-gradient(135deg, rgb(255 255 255 / 0.28) 0 2px, transparent 2px 9px)",
|
|
}}
|
|
/>
|
|
<span className="absolute top-[46%] left-1/2 -translate-x-1/2 -translate-y-[calc(100%+6px)] border border-fg bg-ink/90 px-2 py-1 font-mono text-[9px] font-semibold tracking-[0.15em] whitespace-nowrap text-fg uppercase md:text-xs">
|
|
Front 2 rows · hands low
|
|
</span>
|
|
<span className="absolute top-[5%] left-1/2 -translate-x-1/2 font-mono text-[9px] tracking-[0.2em] whitespace-nowrap text-fg/80 uppercase md:text-[11px]">
|
|
↑ Stage this way
|
|
</span>
|
|
</div>
|
|
<figcaption className="mt-4 max-w-3xl text-sm leading-relaxed text-muted md:text-base">
|
|
The crowd faces away from the drone, toward the stage. The rows nearest the stage overlap the next block in
|
|
the comp, so that's where arms need to stay down.
|
|
</figcaption>
|
|
</figure>
|
|
<TipGrid tips={crowdTips} className="print:break-before-page" />
|
|
</section>
|
|
|
|
<section className="print:!mt-0 print:break-before-page">
|
|
<SectionLabel index="06">Camera</SectionLabel>
|
|
<h2 className="font-display text-3xl leading-none font-thin uppercase md:text-5xl">Notes for DOP and drone op</h2>
|
|
<TipGrid tips={cameraTips} />
|
|
</section>
|
|
|
|
<section className="print:!mt-0 print:break-before-page">
|
|
<SectionLabel index="07">On set</SectionLabel>
|
|
<h2 className="font-display text-3xl leading-none font-thin uppercase md:text-5xl">Production and VFX</h2>
|
|
<TipGrid tips={setTips} />
|
|
</section>
|
|
|
|
<footer className="border-t border-line pt-8 font-mono text-[11px] tracking-[0.2em] text-muted uppercase">
|
|
{site.name} · Questions:{" "}
|
|
<a href={`mailto:${site.email}`} className="text-accent hover:text-fg">
|
|
{site.email}
|
|
</a>
|
|
</footer>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|