diff --git a/.claude/skills/video-film/SKILL.md b/.claude/skills/video-film/SKILL.md new file mode 100644 index 0000000..a6d352b --- /dev/null +++ b/.claude/skills/video-film/SKILL.md @@ -0,0 +1,95 @@ +--- +name: video-film +description: Make or change a social media film in apps/video (Remotion) in the approved mintel.me style - one continuous shot, minimal, with a camera pull-back as the reveal, in German and English. Use when asked for a new video, a new topic, changes to an existing film, new formats, or to re-render the video library. +--- + +# Making a film for mintel.me + +The videos in `apps/video` are short looping films for LinkedIn and ads. The owner approved this +style after rejecting several others. Read `apps/video/CLAUDE.md` first; this skill is the procedure. + +## What the owner rejected, so do not go back to it + +- Slides with a panel, a progress bar, labels and a frame around everything. +- Much text, small text, text sizes that change from line to line. +- Hectic motion: hops of a third of a second, six statements in 15 seconds. +- Busy drawings: icons, labels, counters, stacks, several effects at once. +- A tiny logo in a black box. +- German that reads translated. + +## The style + +One continuous shot on a dark stage. Only squares, dots, lines and bars. One statement at a time, +two short lines, large and centred under the drawing. Calm: nothing moves faster than it has to, every +statement stays for about three seconds. + +Every film tells the same kind of story in six beats, on the shared timeline `T` in +`src/films/shot.ts` (690 frames at 30 fps, 23 seconds): + +| Beat | Caption id | What happens | +| :--- | :--------- | :----------- | +| Hook | `hook` | Close on something small and abstract that looks like something else. "Looks like ping-pong." | +| Reveal | `reveal` | The camera pulls back and shows what it really is. "It's your office." This is the aha moment. | +| Problem | `problem` | The whole picture keeps going, and it is clearly too much. | +| Turn | `turn` | Everything slows to a halt, then the form changes: arcs become a line, a pile leaves by itself. | +| Calm | `calm` | The result at rest, with one quiet movement. | +| Call to action | `cta` | A question that picks up the hook ("Done with ping-pong?") and the website's button. | + +Then the picture folds back into its opening, so the last frame equals the first and the film loops. +Frame 0 is the preview image: it must already look good and carry the hook line. + +## Procedure + +1. **Bring the worktree up to date with `main`** and read the service in + `apps/web/src/content/de.ts` and `en.ts`. Claims, prices and terms come from there and nowhere else. +2. **Find the picture before writing code.** Answer in one line each: + - What does the viewer see close up, and what harmless thing does it look like? + - What does the camera reveal when it pulls back? + - Which single change of form resolves it? + If the reveal does not make you think "oh", look for another picture. Existing ones: + ping-pong / office, skyscraper / pile of invoices, star / unanswered enquiries, + blinking cursor / empty website, Jenga / fragile app, loading bar / backlog. +3. **Write the six statements** in `src/content/videos.ts`, German and English, plus the post text. + Two lines each, as short as possible: one font size is used for the whole film, set by the longest + line. The hook is "Sieht aus wie ..." / "Looks like ...". A little sarcasm is welcome. German uses + "Sie" and has to sound like a native wrote it; write it first, do not translate it from English. + The post text opens with the hook. Copy rules of the root `CLAUDE.md` apply. +4. **Write the film** as `src/films/Film.tsx`. Copy the structure of `AppFilm.tsx` (simplest) + or `OrderFilm.tsx` (several moving things): + - `cameraAt(frame, zoomIn, zoomOut, focus)` gives `transform`, `zoom`, `hair` and `others`. + Everything that is not part of the opening picture gets `opacity={others}`. + - Draw in stage coordinates, (0, 0) in the centre, 1000 x 700. Use `hair` for stroke widths and + divide glow sizes by `zoom`, so lines look the same at every zoom. + - All motion is a pure function of the frame: `progress`, `inOut`, `out`, `mix` from + `src/illustrations/motion.ts`. No timers, no randomness, no CSS animations. + - For the loop: after `T.fold` the opening picture has to be rebuilt exactly. Anything periodic + needs a period that divides 690 (23, 30, 46). Compare frame 0 and frame 689. + - Logic worth testing goes into `src/lib` with a spec, written first (see `hop.ts`). +5. **Register it**: `src/compositions/.tsx` exporting a `VideoEntry` with `Film`, and the + list in `src/Root.tsx`. A new id also goes into `VIDEO_IDS` in `videos.ts`. +6. **Check**: `pnpm typecheck` and `pnpm test` in `apps/video`. +7. **Render and look**: `node scripts/export.ts --only `, then pull frames from the MP4 + (start, middle of the pull-back, problem, turn, calm, call to action, last frame) in `feed` and + `wide`, and read them. Check: nothing overlaps the text, the drawing fades at the stage edges + during the camera move, frame 689 equals frame 0, German lines fit. +8. **Report** what was rendered, what you looked at, and what you did not. You cannot play video: + say so, and leave the judgement of pace and feel to the owner. + +## Traps that have cost time here + +- **A render can silently not happen.** Redirect logs with `>|`, not `>` (the shell refuses to + overwrite an existing file and the command never starts). After every export, compare the file times + with the clock before sending anything. Run one export at a time. +- **Glows**: `drop-shadow` on SVG shapes is fine. Blur filters under a CSS mask, blend modes and dashed + strokes came out with hard edges; build soft light from gradients that fade to transparent. +- **The stage is not clipped by itself.** While the camera is close, the drawing is larger than the + stage; `FilmStage` fades it at the edges. Do not rely on things being "off screen". +- **Do not name a helper script `copy.py`, `json.py` or similar**: it shadows the Python module. +- **Text from an old worktree is stale.** The website changed language and wording during this work. + +## Formats and output + +Compositions are `--`. `node scripts/export.ts` renders +`feed` (4:5, LinkedIn) and `wide` (16:9, YouTube and ads) in both languages into +`out/library///`, with `post.txt` and an overview page `out/library/index.html`. +`out/` is not committed. diff --git a/CLAUDE.md b/CLAUDE.md index 5b2a7f7..d460d4a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # mintel.me Personal website of a senior freelance engineer. Next.js 16 (App Router), pnpm monorepo. -App lives in `apps/web`. Concept and scope: `apps/web/plans/freelance-relaunch.md` (read it first). +App lives in `apps/web`. Social media videos live in `apps/video` (Remotion, standalone, own `CLAUDE.md`). Concept and scope: `apps/web/plans/freelance-relaunch.md` (read it first). ## Commands (run from repo root) @@ -54,8 +54,9 @@ App lives in `apps/web`. Concept and scope: `apps/web/plans/freelance-relaunch.m - Building blocks live in `apps/web/src/components/bits/`: `DecodeText` (mono labels resolving from digits), `BitWipe` (headline revealed behind a column of digits), `BitReveal` (picture assembling from bits), - `BinaryTicker`, `ScrollBits`, `BitBurst`, `BitCurtain` (the language switch transition). Reuse them - instead of inventing new effects. + `BinaryTicker`, `ScrollBits`, `BitBurst`, `BitCurtain` (the language switch transition), `BitAura` (light + along the edge of an element, digits drifting off it), `BitSeam` (lit top edge of a dark section), `BitGlow` + (light following the pointer on `data-glow` surfaces). Reuse them instead of inventing new effects. - `DecodeText` is for monospaced text only; in proportional type the changing glyph widths make text jump. - Never put an in-view trigger on an element that clips itself away (clip-path): a fully clipped element never counts as visible. Observe an unclipped wrapper (see `BitWipe`). diff --git a/apps/video/.gitignore b/apps/video/.gitignore new file mode 100644 index 0000000..2fb2ff3 --- /dev/null +++ b/apps/video/.gitignore @@ -0,0 +1,3 @@ +node_modules +out +.remotion diff --git a/apps/video/CLAUDE.md b/apps/video/CLAUDE.md new file mode 100644 index 0000000..8010c26 --- /dev/null +++ b/apps/video/CLAUDE.md @@ -0,0 +1,89 @@ +# apps/video + +Social media videos (LinkedIn first, 16:9 for YouTube and Google Ads) in the mintel.me look, made with Remotion. +Standalone package: it is **not** in `pnpm-workspace.yaml`, so it never ends up in the web build, the Dockerfile +or `pnpm -r`. `apps/web` must never import from here, and this package imports nothing from `apps/web`. + +## Commands (run from this folder) + +- Install: `pnpm install --ignore-workspace` +- Studio (the playground, http://localhost:3000): `pnpm studio` +- One frame as PNG: `pnpm still WebsiteDone-de-feed out/check.png --frame=40` +- One video: `pnpm render WebsiteDone-de-feed out/website.mp4` +- The whole library: `pnpm library` (= `node scripts/export.ts`; `--only `, `--formats feed,wide`, + `--page` to rewrite only post texts and the overview). Writes `out/library/index.html` (watch, copy the post + text, download) and `out/library/