From 4bfbcbcacd42612c640373b6376dbb1d477674ed Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 4 Oct 2026 21:23:01 +0200 Subject: [PATCH] feat: video films for social media, with the skill to make more Adds apps/video, a standalone Remotion package (not part of the pnpm workspace, so it never reaches the web build or the Docker image). It renders six looping films, one per service, in German and English, as 4:5 for LinkedIn and 16:9 for YouTube and ads, together with the text to post and an overview page. Each film is one continuous shot: close on something abstract, the camera pulls back to show what it really is, the form changes, and the picture folds back into its opening. The slide deck code from the earlier attempts is still in the package and no longer used. Also adds the Google Ads concept, brings the relaunch concept in line with the site as built, and a project skill that describes how to make further films. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/video-film/SKILL.md | 95 + CLAUDE.md | 7 +- apps/video/.gitignore | 3 + apps/video/CLAUDE.md | 89 + apps/video/package.json | 27 + apps/video/pnpm-lock.yaml | 3632 +++++++++++++++++ apps/video/public/logo-mark.svg | 12 + apps/video/public/logo.svg | 1 + apps/video/remotion.config.ts | 7 + apps/video/scripts/export.ts | 137 + apps/video/src/Root.tsx | 43 + apps/video/src/bits/BitWipe.tsx | 70 + apps/video/src/bits/DecodeLabel.tsx | 39 + apps/video/src/brand/BrandButton.tsx | 102 + apps/video/src/brand/BrandFrame.tsx | 94 + apps/video/src/brand/Panel.tsx | 134 + apps/video/src/brand/fonts.ts | 14 + apps/video/src/brand/tokens.ts | 42 + apps/video/src/compositions/AppRescue.tsx | 6 + .../src/compositions/AutomationDocuments.tsx | 6 + .../src/compositions/AutomationEnquiry.tsx | 6 + .../src/compositions/AutomationOrder.tsx | 9 + .../src/compositions/ProductDeveloper.tsx | 6 + apps/video/src/compositions/WebsiteDone.tsx | 6 + apps/video/src/content/CopyContext.tsx | 23 + apps/video/src/content/videos.spec.ts | 51 + apps/video/src/content/videos.ts | 380 ++ apps/video/src/films/AppFilm.tsx | 106 + apps/video/src/films/DeveloperFilm.tsx | 93 + apps/video/src/films/DocumentsFilm.tsx | 128 + apps/video/src/films/EnquiryFilm.tsx | 124 + apps/video/src/films/FilmStage.tsx | 181 + apps/video/src/films/OrderFilm.tsx | 195 + apps/video/src/films/WebsiteFilm.tsx | 132 + apps/video/src/films/shot.ts | 75 + apps/video/src/illustrations/motion.ts | 20 + apps/video/src/illustrations/parts.tsx | 311 ++ apps/video/src/illustrations/scenes.tsx | 259 ++ apps/video/src/index.ts | 4 + apps/video/src/lib/aura.spec.ts | 82 + apps/video/src/lib/aura.ts | 82 + apps/video/src/lib/bits.spec.ts | 60 + apps/video/src/lib/bits.ts | 34 + apps/video/src/lib/curtain.spec.ts | 64 + apps/video/src/lib/curtain.ts | 35 + apps/video/src/lib/hop.spec.ts | 49 + apps/video/src/lib/hop.ts | 24 + apps/video/src/slides/ServiceVideo.tsx | 32 + apps/video/src/slides/SlideDeck.tsx | 306 ++ apps/video/src/slides/ctaSlide.tsx | 19 + apps/video/tsconfig.json | 21 + apps/web/plans/freelance-relaunch.md | 27 +- apps/web/plans/google-ads.md | 158 + 53 files changed, 7649 insertions(+), 13 deletions(-) create mode 100644 .claude/skills/video-film/SKILL.md create mode 100644 apps/video/.gitignore create mode 100644 apps/video/CLAUDE.md create mode 100644 apps/video/package.json create mode 100644 apps/video/pnpm-lock.yaml create mode 100644 apps/video/public/logo-mark.svg create mode 100644 apps/video/public/logo.svg create mode 100644 apps/video/remotion.config.ts create mode 100644 apps/video/scripts/export.ts create mode 100644 apps/video/src/Root.tsx create mode 100644 apps/video/src/bits/BitWipe.tsx create mode 100644 apps/video/src/bits/DecodeLabel.tsx create mode 100644 apps/video/src/brand/BrandButton.tsx create mode 100644 apps/video/src/brand/BrandFrame.tsx create mode 100644 apps/video/src/brand/Panel.tsx create mode 100644 apps/video/src/brand/fonts.ts create mode 100644 apps/video/src/brand/tokens.ts create mode 100644 apps/video/src/compositions/AppRescue.tsx create mode 100644 apps/video/src/compositions/AutomationDocuments.tsx create mode 100644 apps/video/src/compositions/AutomationEnquiry.tsx create mode 100644 apps/video/src/compositions/AutomationOrder.tsx create mode 100644 apps/video/src/compositions/ProductDeveloper.tsx create mode 100644 apps/video/src/compositions/WebsiteDone.tsx create mode 100644 apps/video/src/content/CopyContext.tsx create mode 100644 apps/video/src/content/videos.spec.ts create mode 100644 apps/video/src/content/videos.ts create mode 100644 apps/video/src/films/AppFilm.tsx create mode 100644 apps/video/src/films/DeveloperFilm.tsx create mode 100644 apps/video/src/films/DocumentsFilm.tsx create mode 100644 apps/video/src/films/EnquiryFilm.tsx create mode 100644 apps/video/src/films/FilmStage.tsx create mode 100644 apps/video/src/films/OrderFilm.tsx create mode 100644 apps/video/src/films/WebsiteFilm.tsx create mode 100644 apps/video/src/films/shot.ts create mode 100644 apps/video/src/illustrations/motion.ts create mode 100644 apps/video/src/illustrations/parts.tsx create mode 100644 apps/video/src/illustrations/scenes.tsx create mode 100644 apps/video/src/index.ts create mode 100644 apps/video/src/lib/aura.spec.ts create mode 100644 apps/video/src/lib/aura.ts create mode 100644 apps/video/src/lib/bits.spec.ts create mode 100644 apps/video/src/lib/bits.ts create mode 100644 apps/video/src/lib/curtain.spec.ts create mode 100644 apps/video/src/lib/curtain.ts create mode 100644 apps/video/src/lib/hop.spec.ts create mode 100644 apps/video/src/lib/hop.ts create mode 100644 apps/video/src/slides/ServiceVideo.tsx create mode 100644 apps/video/src/slides/SlideDeck.tsx create mode 100644 apps/video/src/slides/ctaSlide.tsx create mode 100644 apps/video/tsconfig.json create mode 100644 apps/web/plans/google-ads.md 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/