# opus-anim — Opus 5.5 làm animation & video "chỉ bằng JS": reverse-engineer + bài học > Research thực hiện ngày 2026-09-24, tức 2 ngày sau khi Claude Opus 5.5 ra mắt (2026-09-22). Lượng post còn mỏng, xem mục độ tin cậy ở cuối. > Mọi ví dụ trong repo này đều chạy được, đã được render thành MP4 và đã qua kiểm tra tất định (determinism). ```bash npm install && npx playwright install chromium # 1 lần npm run serve # http://localhost:5190 → gallery + 13 ví dụ live (scrub được) node tools/render.mjs examples/07-physics-heptagon # → out/07-physics-heptagon.mp4 node tools/render.mjs examples/12-explainer-video --sheet=0.5,3,8,14 --cols=4 # contact sheet để tự kiểm tra node tools/render.mjs examples/12-explainer-video --strip=2.7:3.1 # mọi frame của 1 khoảnh khắc node tools/render-all.mjs # render cả 13 ví dụ node tools/check-determinism.mjs # chứng minh render(t) là hàm thuần node tools/test-export.mjs # test export trong trình duyệt (WebCodecs + MediaRecorder) ``` > **Mở rộng:** [CATALOG-100.md](CATALOG-100.md) / `catalog.html`: 100 ví dụ từ đơn giản → phức tạp (3D, game, hàng nghìn object, mô phỏng agent), kèm nguồn X đã kiểm chứng. --- ## 1. Trên X người ta đang khoe gì? | Demo | Stack | Nguồn | |---|---|---| | **"I'm Upping My P(doom)"**: MV 156.6 s, 9 chương, nhân vật, storyboard, style guide do model tự làm hết | p5.js + p5.brush → headless Chrome chụp từng frame → ffmpeg | [JohnHeibel/PDoomVideo](https://github.com/JohnHeibel/PDoomVideo) (**đã đọc mã nguồn**) | | Starter kit rút ra từ MV trên | như trên + `ANIMATION_GUIDE.md` rất chi tiết | [JohnHeibel/ClaudeAnimationBase](https://github.com/JohnHeibel/ClaudeAnimationBase) (**đã đọc**) | | "small print" (26 s): Claude nhặt "phần con người" trong các request ("one hand. baby's asleep") bỏ vào hũ, ban đêm thành chòm sao. "I didn't write a single line of code… no image assets, it's all JS" | **HyperFrames**, tất cả trong 1 `index.html`; nhạc **synth bằng Python**; sau đó model **xem lại bản render từng giây một và polish thêm** | [@Voxyz_ai](https://x.com/Voxyz_ai/status/2102531681450119426) (**đã đọc bằng browser**) | | Pelican riding a bicycle bằng Three.js (video 1:29, 394K view) | three.js/WebGL | [@addyosmani](https://x.com/addyosmani/status/2102436416437580159) (**đã đọc bằng browser**) | | Opus 5.5 tự vẽ chính mình bằng voxel (14 s): đảo nổi, cái cây, nhân vật ngồi ở mép đảo | 3D procedural | [@blueemi99](https://x.com/blueemi99/status/2102511304456212763) (**đã đọc bằng browser**); trong reply có game 3D đua chó bắn súng làm từ prompt một dòng ([@dustinng_gs](https://x.com/dustinng_gs)) | | Pelican SVG bên bờ biển, có animation | SVG + SMIL | [toankhontech/pelican-test-opus55](https://github.com/toankhontech/pelican-test-opus55) | | Launch video cho Opus 5 | Remotion + WebGL shader + SVG path của brand; một prompt 12 "beat" rất chi tiết | [remocn.dev](https://remocn.dev/blog/claude-opus-5-launch-video) | | Benchmark pelican (dòng benchmark lâu năm), và lỗi "max effort" | SVG | [Simon Willison](https://simonwillison.net/2026/Sep/22/opus-and-sol-and-luna/), [HN](https://news.ycombinator.com/item?id=49804316) | **Nhận xét chung:** Opus 5.5 không "sinh pixel" mà **viết một chương trình vẽ ra hình**, đúng như cách gọi "visual software engineer". Muốn có video thì chương trình đó phải **render được từng frame một cách tất định**. Toàn bộ repo này xoay quanh ý đó. --- ## 2. Nguyên lý gốc (reverse-engineer từ PDoomVideo, ClaudeAnimationBase, Remotion) ``` animation = render(t) // t tính bằng giây, render vẽ LẠI TOÀN BỘ frame video = [render(i / fps) for i in 0..N] → ffmpeg ``` Trích `ANIMATION_GUIDE.md` do Opus tự viết để brief các subagent: > *"Frames render in parallel and out of order. Every shot must be a pure function of `t`: no state that carries between frames, and no `Math.random()`. Use `hash(i)`…"* Hệ quả của nguyên lý này: 1. **Seek được**: tua tới bất kỳ t nào (thanh scrub trong player). 2. **Render video không phụ thuộc tốc độ máy.** Một frame mất 2 s thì video 60 fps vẫn mượt, vì đồng hồ thật không dính vào kết quả. 3. **Render song song, không theo thứ tự** (`--workers=N`), sau đó sắp lại đúng thứ tự trước khi đưa cho ffmpeg. 4. **Model tự kiểm tra được:** chụp contact sheet tại các t chọn trước rồi *nhìn* vào đó (Claude đọc được ảnh). Kiến trúc repo: ``` lib/anim.js easing, bezier, interpolate, spring (closed-form), rng, noise, timeline (stateless), stepper (physics seekable) lib/player.js 1 harness 2 chế độ: dev (play/scrub, audio là master clock) · ?render (1:1, expose __seek/__grab/__wav) tools/render.mjs Playwright → __seek(t) → canvas.toDataURL hoặc screenshot → ffmpeg (+ mux audio). --sheet/--strip/--stills/--workers tools/check-determinism.mjs frame(A) → frame(B) → frame(A), so sánh md5 examples/NN-*/index.html 13 bài, mỗi bài 1 file, comment đầu file = bài học ``` --- ## 3. Lộ trình kỹ thuật: đơn giản → nâng cao ### Tầng 1: Làm cho mọi thứ trở thành f(t) **01 · CSS @keyframes seek được** (`examples/01-css-keyframes`) - Kỹ thuật: `document.getAnimations()` → `a.pause(); a.currentTime = t*1000`. CSS thuần (stagger qua `--i`, `cubic-bezier` overshoot) giờ render ra video được. - Bài học: **không cần bỏ CSS**, chỉ cần tách nó khỏi đồng hồ của trình duyệt. Remotion/Hyperframes cũng làm đúng việc này. - Bug bắt được nhờ contact sheet: CSS `h1{font-size:15px}` của player rò vào scene, làm tiêu đề bé xíu. → Scope CSS của harness (`#hdr`). **02 · SVG + SMIL: pelican đạp xe** (`examples/02-svg-pelican`) - Kỹ thuật: bánh xe, mây, vạch đường dùng SMIL (``, `stroke-dashoffset`). SMIL cũng seek được: `svg.pauseAnimations(); svg.setCurrentTime(t)`. - Chân không khai báo SMIL được vì đầu gối phải theo pedal. → **IK 2 khớp** (giao 2 đường tròn) tính bằng JS mỗi frame. - Bài học: **khai báo cái gì tuần hoàn, tính toán cái gì có ràng buộc.** Thêm follow-through: đầu và túi mỏ nhún trễ pha so với thân. **03 · Canvas f(t), perfect loop, motion blur** (`examples/03-canvas-loop`) - Anti-pattern: `x += speed` trong rAF, vừa phụ thuộc fps vừa không seek được. - Perfect loop: `phase = TAU*t/D` và **chỉ dùng bội số nguyên** của phase. - Motion blur: vẽ N sub-frame trong khoảng shutter rồi cộng lại (`'lighter'` với alpha 1/N). - Bug bắt được nhờ sheet: frame t=3.99 khác t=0. Vòng xoay 1/6 vòng làm lệch pha sóng `sin(3a)` đúng π. Sửa thành 1/3 vòng. **Loop chỉ liền mạch khi mọi thành phần đều khép kín theo D, và phải kiểm bằng mắt.** **04 · Easing & `interpolate()`** (`examples/04-easing`) - 8 đường cong Penner + `bezier()` kiểu CSS + `interpolate(t, [..], [..], ease)` kiểu Remotion. - Bài học: lerp tuyến tính trông như máy. "Slow in, slow out" là nguyên tắc animation số 1 trong guide của Opus. ### Tầng 2: Biên đạo chuyển động **05 · Kinetic typography trên timeline không state** (`examples/05-kinetic-type`) - `timeline().to(key, from, to, {at, dur, ease})`, `.stagger(keys, …)`, `value(key, t)`. Timeline là **dữ liệu** chứ không phải đồng hồ (GSAP nhưng stateless). - Stagger vào từ trái, ra từ phải (thứ tự đảo đọc tự nhiên hơn), circle wipe mang màu của cảnh sau, camera push chậm để khung hình không bao giờ "chết". **06 · Springs** (`examples/06-springs`) - `spring(t, {stiffness, damping})` dạng **closed-form** (dao động tắt dần giải tích), nên vẫn là hàm của t. - Retarget: `x = from + (to-from)*spring(t - tSwitch)`, bắt đầu từ vị trí hiện tại. Follow-through: mỗi mắt xích là cùng một spring nhưng trễ 60 ms. ### Tầng 3: Mô phỏng có state mà vẫn render được **07 · Physics: 20 bóng trong heptagon xoay** (`examples/07-physics-heptagon`), benchmark LLM nổi tiếng - Physics là **stateful**, nên `stepper()` trong lib dùng **fixed dt = 1/240**, **seed** cố định, **snapshot mỗi 0.5 s**; tua lùi thì khôi phục snapshot rồi chạy lại. - Vật lý trông "đúng" là nhờ: trừ **vận tốc thành tại điểm chạm (ω × r)** trước khi nảy (thành xoay "hất" bóng), restitution < 1, ma sát tiếp tuyến, đẩy bóng ra khi chồng lấn. - Bài học từ research: prompt benchmark ghi rõ luật (khối lượng, mất năng lượng, va chạm với gì). **Nói luật vật lý cụ thể**, đừng chỉ ghi "có physics". **08 · Particles không state** (`examples/08-particles-flowfield`) - Trail thường cần "mờ dần canvas mỗi frame", tức là có state. Thay vào đó cho hạt một **vòng đời tất định**: chu kỳ `c = floor((t+off)/LIFE)`, điểm sinh = `hash(i, c)`, đường đi = tích phân flow field (noise) với dt cố định từ lúc sinh; trail = đoạn cuối của chính tích phân đó. - Bài học: **đổi state thành "tính lại từ điểm sinh tất định"**. Tốn CPU hơn, đổi lại seek được và render song song được. ### Tầng 4: GPU **09 · GLSL raymarching** (`examples/09-shader-raymarch`) - Một fragment shader duy nhất vẽ SDF blobs với smooth-union, soft shadow, AO, fog và camera orbit. Input mỗi frame chỉ có `uTime`. - Chi tiết để render được: `preserveDrawingBuffer: true` để `toDataURL()` đọc được frame; vẽ 1 tam giác phủ toàn màn hình. - Đây là thể loại dùng để stress-test model: chất lượng hình đến từ toán gọn, không từ asset. **10 · three.js** (`examples/10-threejs-scene`) - Bỏ `setAnimationLoop/clock.getDelta()`. `render(t)` đặt 2.500 matrix cho `InstancedMesh` (1 draw call), camera dolly + orbit bằng `interpolate`, rồi `renderer.render()` một lần. - Bug bắt được nhờ sheet: có "lỗ đen" trên lưới, do chiều cao âm → scale âm → normal bị lật. → `Math.max(0.15, h)`. ### Tầng 5: Âm thanh, câu chuyện, xuất bản **11 · Audio sync không cần FFT** (`examples/11-audio-sync`) - Nhạc viết thành **DATA (SCORE)**; `audio(ctx)` lên lịch synth (kick sweep, snare noise bandpass, hat highpass, bass saw + lowpass env, arp + delay feedback) vào **OfflineAudioContext**. - Hình vẽ từ **cùng SCORE** (tuổi của kick gần nhất, hợp âm hiện tại, piano roll). Oscilloscope đọc **đúng mẫu âm thanh** sẽ được mux vào MP4 (`anim.audioBuffer`). - Ở chế độ dev, `AudioContext.currentTime` làm master clock. PDoomVideo làm tương tự: `bpOf(t)`, `pulse(t)`, "đặt hành động quan trọng lên beat". **12 · Explainer video** (`examples/12-explainer-video`): tổng hợp mọi thứ theo cấu trúc của PDoomVideo - **Storyboard là dữ liệu**: `SHOTS = [[start, fn]]`, `fn(t, lt, dur)` vẽ toàn frame, cut rơi đúng vào điểm bắt đầu shot. - **Data-driven**: chạy bubble sort **một lần** để ra TRACE, animation chỉ là ánh xạ (chỉ số sự kiện, tiến độ cục bộ) → pixel. - Nguyên tắc animation: swap đi theo **arc** (một thanh nhảy qua, một thanh chui dưới), **anticipation** (nhấc lên trước), **hold** để người xem kịp đọc, stagger thay cho "twinning", **mọi đường nối đều có transition**. - Bug timing bắt được nhờ `--strip` (chụp từng frame): swap đầu tiên diễn ra *trong lúc* transition còn che màn hình, hai "read" chồng lên nhau. → thêm `LEAD = 0.5 s`. Đây đúng là lỗi mà ClaudeAnimationBase cảnh báo: *"Timing is the rule models get wrong most often."* **13 · Export trong trình duyệt** (`examples/13-browser-export`) - WebCodecs `VideoEncoder` + `webm-muxer` chạy **offline**: 150 frame trong 447 ms (**11,2× realtime**), đúng 150 frame (đã kiểm bằng ffprobe). - MediaRecorder + `captureStream`: **realtime** (5,01 s cho 5 s), nên jank và throttle sẽ vào thẳng file. - Bài học: muốn chất lượng như video thật thì dùng render tất định (WebCodecs hoặc headless + ffmpeg). MediaRecorder chỉ hợp để "quay nhanh cái đang thấy". --- ## 4. Workflow agentic: phần "bí mật" không nằm trong code Rút ra từ README, `ANIMATION_GUIDE.md` và `STORYBOARD.md` của PDoomVideo/ClaudeAnimationBase: 1. **Storyboard trước khi code.** Viết logline, world/palette, motif, arc cảm xúc, và danh sách shot kèm transition. Mỗi shot có danh sách **"reads"** (điều người xem cần hiểu) với thời gian bắt đầu/kết thúc; không để hai read quan trọng chồng lên nhau. 2. **Viết guide/API cho chính mình rồi chia subagent.** PDoomVideo: Opus viết `ANIMATION_GUIDE.md` để brief các subagent chạy song song, **mỗi agent một file chương** (IIFE, không sửa file dùng chung, phát hiện bug ở file chung thì báo lại chứ không tự sửa). 3. **Vòng review bằng mắt** (bắt buộc, vì "you can't see motion by reading code"): - `--sheet`: frame đầu/giữa/cuối mỗi shot, để thấy hình dạng tổng thể - `--strip`: mọi frame của một chuyển động hay transition, để thấy pop, snap, twinning - crop full-res cho mặt nhân vật, điểm tiếp xúc - checklist: read, timing, anticipation/follow-through, transitions, không có text thừa, không có 3D (với style 2D) 4. **Hai lượt:** bản nháp bằng Opus 5.5 Medium, bản hoàn thiện bằng Opus 5.5 (PDoomVideo). Tác giả ClaudeAnimationBase nhận xét *"reasoning level corresponds to how extravagant and detail-oriented the scene is"* và dùng xhigh. 5. **Ngân sách render:** ≤ 2,5 s/frame, in ms/frame ra log (render.mjs ở đây cũng in ra). **Kiểm chứng trong repo này:** vòng contact sheet bắt được **6 bug thật** mà đọc code không thấy: CSS rò rỉ (01), loop lệch pha π (03), lỗ đen do normal lật (10), layout chồng chữ (12), text tràn card (12), timing chồng read (12). `check-determinism` pass 13/13. ## 5. Bài học về prompt - **Tư duy "program, not pixels":** yêu cầu model viết `render(t)` thuần, cố định kích thước và fps, cấm `Math.random`/`Date.now`, và yêu cầu có đường render ra MP4. - **Nói rõ ràng buộc:** vật lý (luật cụ thể), số giây, palette, typography, "hai thuộc tính của một phần tử không dùng chung một curve" (prompt launch-video Remotion). - **Ràng buộc môi trường:** "single-file HTML, no build step, no external assets" giúp demo tái lập được và dễ chia sẻ. - **Ít chỉ đạo cũng được:** PDoomVideo chỉ dặn "dùng nhân vật Clawd, mỗi câu hát có hình và transition hay", còn lại model tự làm. - **Luật style mạnh chống "look AI":** ClaudeAnimationBase cấm chữ ("models overuse text", cái biển ghi "I'm lost" là thất bại kinh điển), cấm 3D giả, cấm màu đen/trắng tuyệt đối, "nothing is ever still", "faces act, never snap". Prompt mẫu (dùng với repo này): ``` Read README.md sections 2–4. Create examples/14-/index.html using lib/player.js + lib/anim.js. A 12-second 1280×720 video of . render(t) must be a pure function of t (no Math.random, no Date). First write a storyboard (shots with start–end, transition in/out, the "reads" with times). Then build shot by shot; after each shot run node tools/render.mjs examples/14- --sheet= --cols=3 and --strip on every key motion; open the images and fix what you see. Finish with node tools/check-determinism.mjs 14 and node tools/render.mjs examples/14-. ``` ## 6. Failure modes đã được báo cáo - **"max" effort phản tác dụng:** với task pelican SVG, Opus 5.5 ở max tiêu hết 128K output token mà không ra SVG. Simon Willison bị 2 lần, mỗi lần ~$2.56 và ~20 phút; HN xác nhận. → Dùng medium/high/xhigh. - **"One-shot" thường không thật sự một lượt:** model *viết rồi đạo diễn* một chương trình render, bên trong có nhiều vòng dùng tool. - **Lỗi quen thuộc của animation do model viết** (theo guide): timing đều đều một tốc độ, các sự kiện chồng lên nhau, twinning (mọi thứ đối xứng/đồng pha), lạm dụng chữ, frame đứng im. - **Hiệu năng:** p5.brush watercolor cần GPU rời, "seconds per frame" trên GPU tích hợp. ## 7. Độ tin cậy & giới hạn - **Chắc chắn (nguồn gốc/đọc mã):** stack và workflow của PDoomVideo/ClaudeAnimationBase, benchmark pelican, lỗi max-effort, giá và ngày ra mắt. - **Đã đọc trực tiếp bằng ego-browser (2026-09-24):** post của @Voxyz_ai, @blueemi99, @addyosmani. Không post nào công bố prompt đầy đủ. Tìm kiếm trên X cần đăng nhập nên chưa quét rộng hơn được. Post shader của Ethan Mollick vẫn chưa tìm thấy. - **Khớp với repo này:** vòng "xem lại render từng giây rồi polish" của Voxyz chính là `--sheet`/`--strip`; HyperFrames = HTML → video theo đúng mô hình `render(t)` + headless capture của `tools/render.mjs`. - **Chưa thấy demo Opus-5.5 cụ thể** dùng Motion Canvas, Revideo, GSAP hay WebCodecs. Các bài 05/13 là tái hiện kỹ thuật chung, không phải clone một post cụ thể. - Mọi ví dụ ở đây là **code do tôi viết để tái hiện kỹ thuật**, không phải output gốc của các post.