MarkdyScript: Khi Developer Văn Bản Tự Xây Animation Studio Bằng 50 Dòng Kịch Bản
TL;DR
- Giải quyết gì: Không có animation nào text-based, commit được, review PR được, và AI sinh được
- Là gì: DSL animation line-based: khai báo nhân vật, đặt mốc thời gian, chạy trong browser
- Phù hợp nhất với: Dev text-brain không muốn mở Figma hay học Framer Motion chỉ để một animation nhỏ
- Use case tốt nhất: Stick-figure scene trong MDX, cùng file với prose,
git diffđược - Đánh đổi chính: 2D only, bốn loại easing, v0.5.7 mới ra ngày 11/04/2026, chưa battle-tested production
Tối Hôm Ấy
11 giờ tối. Bài tutorial API retry đã xong xuôi, chỉ còn thiếu một animation minh họa cái flow: request bay ra, server trả 500, client chờ 2 giây, thử lại, xong. Không phức tạp hơn vậy.
Mở Framer Motion docs. Thấy useAnimation, AnimatePresence, LayoutGroup, useSpring, useMotionValue. Đọc 15 phút rồi tắt tab, không phải thứ cần làm tối nay. Thử ScreenToGif thay: quay 3 phút, xuất ra file 2.8MB, upload lên repo. Reviewer để lại đúng một comment: “Sửa caption đi.” Mở ScreenToGif, quay lại từ đầu.
Cái khổ không phải animation khó. Cái khổ là không có cách nào để animation nằm cùng file với đoạn text đang giải thích nó.
GIF là file riêng, JS là code riêng, Lottie là JSON binary mà designer làm. Bản thân chỉ có text, và text không chạy được. Tôi muốn thứ gì đó như Mermaid nhưng cho chuyển động: viết trong file, diff được, sửa bằng editor, AI hiểu cấu trúc. Câu trả lời lúc đó tôi không biết, nằm ở 50 dòng kịch bản.
Khi Animation Có Địa Chỉ Git
MarkdyScript là một DSL line-based: mỗi dòng là một câu lệnh, một khai báo actor, hay một event trên timeline. Thay vì viết JavaScript keyframe bằng tay, bạn viết “lúc 0.5 giây, hero đi vào từ trái, mất 0.8 giây” rồi parser lo phần còn lại.
Câu neo cho intern mới: repo này nhận một file text, render animated scene trong browser bằng Web Animations API, không Canvas, không SVG phức tạp, không một dòng JavaScript phải viết tay. So sánh nhanh cùng một animation entrance:
// JavaScript (GSAP): entrance + speech bubble + đổi mặt (~16 dòng)
gsap.from("#hero", { x: -400, opacity: 0, duration: 0.8 });
setTimeout(() => {
const bubble = document.querySelector("#bubble");
bubble.textContent = "Chạy rồi!";
bubble.style.opacity = "1";
setTimeout(() => {
document.querySelector("#face").textContent = "😄";
}, 100);
}, 800);
// ... thêm responsive handling, cleanup, fade-out
# MarkdyScript: cùng kết quả, 4 dòng, git diff được
actor hero = figure(#c68642, m, 😎) at (400, 200)
@0.0: hero.enter(from=left, dur=0.8)
@1.0: hero.say("Chạy rồi!", dur=1.2)
@1.0: hero.face("😄")
Cái hay không nằm ở số dòng. Nó nằm ở chỗ MarkdyScript là text có cấu trúc mà GitHub render được dưới dạng diff, AI hiểu grammar và sinh được, và parser báo lỗi theo đúng số dòng chứ không phải “Something went wrong.” Điểm khác biệt với GIF, Lottie, hay JS thuần là điểm này.
Nhưng nắm concept rồi vẫn còn một bước nữa trước khi scene đầu tiên thực sự chạy.
Từ Terminal Đến Scene Đầu Tiên
Repo chia ba package. @markdy/core (~12KB) parse và validate script. @markdy/renderer-dom (~22KB) tạo player với play, pause, seek, destroy. @markdy/astro (~2KB) là Astro Island wrapper, hydrate bằng IntersectionObserver khi scene hiện đủ trong viewport. Tổng bundle ~30KB, so sánh: GSAP ~70KB, Framer Motion 150KB+.
Đây là con đường ngắn nhất cho dự án Astro:
-
Cài package. Một lệnh.
pnpm add @markdy/astro -
Viết scene đầu tiên.
vartrước,scenetrướcactor,actortrước event. Thứ tự quan trọng, parser enforce nghiêm.scene width=800 height=400 bg=#f5f5ff actor hero = figure(#c68642, m, 😎) at (400, 200) @0.0: hero.enter(from=left, dur=0.8) @1.0: hero.say("Chạy rồi!", dur=1.2) @1.0: hero.face("😄") -
Render trong MDX.
<Markdy />đã pre-registered ở route layer, không cần import.<Markdy code={String.raw` scene width=800 height=400 bg=#f5f5ff actor hero = figure(#c68642, m, 😎) at (400, 200) @0.0: hero.enter(from=left, dur=0.8) @1.0: hero.say("Chạy rồi!", dur=1.2) @1.0: hero.face("😄") `} width={800} height={400} bg="#f5f5ff" /> -
Chạy
pnpm dev, mở browser. Click vào scene để play. Playwright hoặc Cypress test đượcplayer.seek(1.5)vì playback control public.
💡 Mẹo:
durationtrongscenelà optional. Parser tự tính từ event có end-time muộn nhất,startTime + dur. Chỉ đặt tay khi muốn cố ý chừa thêm khoảng nghỉ sau event cuối cùng.
Nếu dự án không phải Astro, dùng @markdy/core + @markdy/renderer-dom trực tiếp: createPlayer({ container, code }) trả về { play, pause, seek, destroy }. Vanilla TS, React, Vue đều được. Nhưng cái parser cần nói kỹ hơn, vì nó không nhẹ tay.
Parser Không Tha Thứ Dòng Nào
Parser MarkdyScript là loại cửa khẩu nghiêm ngặt nhất tôi từng gặp. Nó đọc từng dòng từ trên xuống, validate tức thì, throw ParseError kèm số dòng ngay khi gặp vi phạm, không warn-and-continue.
Thử đặt event trước khi khai báo actor:
@0.5: hero.enter(from=left, dur=0.8) # dòng 1: hero chưa tồn tại
actor hero = figure(#c68642, m, 😎) at (400, 200)
Parser nói thẳng: ParseError line 1: actor 'hero' referenced before declaration. Di chuyển actor lên trên là xong. Không phải feature phiền phức, đây là feature tốt nhất của DSL: lỗi xác định, sửa được, không mò mẫm runtime.
Hành trình đó, đầy đủ hơn - ba nhân vật, ba seq tái sử dụng, parser không nhượng bộ, và mentor phá vỡ thế bí:
Parser không tha. Nhưng cũng không để bạn đoán mò lâu.
Bốn Loại Diễn Viên, Một Trái Tim
MarkdyScript có bốn loại actor built-in:
text("label")nhãn văn bản, nhậnsize,opacity,rotate; tốt cho tiêu đề và captionbox()hình chữ nhật 100x100px màu xám, nhậnscale,rotate; tốt cho diagram flowssprite(assetName)ảnh URL hoặc icon Iconify từassetdeclaration; tốt cho icon sản phẩm thựcfigure(skinColor, gender, face)stick figure emoji với khớp tay chân animate riêng lẻ
figure là kiểu hay nhất. Cấu trúc DOM flexbox, body parts là emoji riêng lẻ có transform-origin tại khớp, scale tốt trên mọi màn hình không pixel blur. face("emoji") là swap textContent tức thì, không WAAPI, seek-safe hoàn toàn: scrub ngược về giây nào cũng hiển thị đúng khuôn mặt.
actor dev = figure(#c68642, m, 😎) at (200, 200) # male, custom face
actor gal = figure(#fad4c0, f, 😊) at (400, 200) # female, starting face
actor boss = figure(#8d5524, m) at (600, 200) # default face 😶
Actions chỉ figure mới dùng được: punch, kick, rotate_part, face. Gọi punch trên text hay box thì parser từ chối ngay khi parse, không phải lúc chạy. Nhưng viết đi viết lại figure(#c68642, m, 😎) mỗi lần khai báo actor mới thì vừa dài vừa dễ sai skin color. Đây là lúc def vào việc.
def và seq: Khuôn Đúc Và Vũ Đạo
def là khuôn đúc nhân vật. Bạn đặt tên cho một template actor, parser expand tại parse time, renderer không biết template đó tồn tại, không overhead runtime:
def fighter(skin, face) {
figure(\${skin}, m, \${face})
}
def heroine(skin, face) {
figure(\${skin}, f, \${face})
}
# Dùng như built-in type
actor dev = fighter(#c68642, 😏) at (200, 220)
actor boss = fighter(#8d5524, 😤) at (600, 220)
actor pm = heroine(#fad4c0, 😊) at (400, 220)
seq là vũ đạo có tên. Dạy parser một chuỗi chuyển động tái sử dụng được. $ là placeholder cho actor sẽ gọi nó, @+offset là thời gian tương đối từ lúc play() được invoke:
seq wave {
@+0.0: $.rotate_part(part=arm_right, to=-80, dur=0.3)
@+0.3: $.rotate_part(part=arm_right, to=-25, dur=0.3)
}
seq celebrate(msg) {
@+0.0: $.rotate_part(part=arm_right, to=-130, dur=0.3)
@+0.4: $.rotate_part(part=arm_right, to=-25, dur=0.4)
@+0.0: $.say(${msg}, dur=1.5)
}
@2.0: dev.play(wave)
@3.5: dev.play(celebrate, msg="Ship it!")
@3.5: boss.play(wave)
@3.5: dev.play(celebrate, msg="Ship it!") expand thành ba events độc lập trong AST cuối. Rendered không biết seq tồn tại. Kết hợp var, def, seq với nhau là điểm MarkdyScript thoát khỏi flat timeline script: thay vì 50 dòng events lặp đi lặp lại, bạn có character system tái sử dụng trong vài chục dòng, ~30KB bundle bỗng có lý do tồn tại.
Nó Vừa Khít Ở Đâu
MarkdyScript fit nhất khi animation phải sống cùng với text, không tách ra file ngoài. Một vài trường hợp thực tế:
- Blog tutorial: Thay GIF giải thích retry flow, auth handshake, hay state machine bằng scene có
say(),shake(),face(), diff cùng với prose - Docs site Astro:
<Markdy />sát đoạn giải thích, hydrate lazy khi scroll đến, không blocking render - Landing page onboarding: Figure người dùng bước qua 3 step, face từ 😐 sang 😊 sang 🎉, không designer, không JS phức tạp
- Team review animation spec: Design review kịch bản animation trong PR như file text, không cần Figma export video
- AI-generated illustration: AI hiểu grammar DSL, sinh script hợp lệ, reviewer đọc được bằng mắt người và sửa tay khi cần
Một chỗ nó không fit: animation interactive hay conditional theo input người dùng. Timeline là linear, không có “if user clicks thì branch” trong MarkdyScript. Đây không phải Lottie interactive, không phải Rive. Nó là kịch bản tuyến tính, như phim ngắn, không phải game.
Trung Thực Về Giới Hạn
MarkdyScript v0.5.7 mới ra ngày 11/04/2026. Cần nói thẳng trước khi ai đó dùng vào production thật.
2D only. Không perspective, không z-axis animate, không 3D transform. Tọa độ là pixel tuyến tính trong mặt phẳng.
Easing chỉ bốn loại: linear, in, out, inout. Không bounce, không elastic, không cubic-bezier tùy chỉnh. Animation phức tạp về cảm giác vật lý là không tới sân.
throw cần target là actor có tên. Không thể ném projectile đến tọa độ tùy ý, chỉ đến một actor đã khai báo.
figure là emoji stick figure. Không SVG character phức tạp, không morphing shape. Đẹp và clean, nhưng đừng mang kỳ vọng của Rive vào đây.
Không audio. Timeline visual only. Muốn sync animation với voice-over thì tự lo phần đó ngoài Markdy.
⚠️ Cảnh báo: Thứ tự khai báo là luật sắt:
vartrước${ref},defvàseqtrước actor dùng template đó,actortrước event tham chiếu đến nó. Parser throwParseErrortức thì khi vi phạm, không có grace period, không warn-and-continue. Gặp lỗi thứ tự thì di chuyển dòng lên là xong, nhưng phải biết luật này trước.
0 stars, ecosystem trống. Không có community plugin, không có third-party component, không có showcase production. Bạn đang là người dùng sớm theo nghĩa đen nhất. API v0.6 có thể thay đổi.
Quay Lại Tối Hôm Ấy
Animation retry flow đó cuối cùng tôi viết trong 14 dòng MarkdyScript. Có say() cho lời request, face("😵") cho lúc server trả 500, shake() cho thằng client bị loop, rồi face("😎") khi retry thành công lần hai. Toàn bộ nằm liền trong file MDX, cạnh cái prose đang giải thích nó. Reviewer sửa caption thì git diff hiện đúng hai dòng thay đổi, không cần quay lại ScreenToGif.
30KB toàn bộ. 14 dòng text. Không một dòng JavaScript.
Mermaid làm điều tương tự cho diagram, và người ta vẫn dùng Mermaid sau cả chục năm, ngay cả khi có hàng chục công cụ diagram fancy hơn. MarkdyScript đang hỏi câu hỏi tương tự nhưng cho chuyển động, đúng thời điểm AI bắt đầu viết docs và tutorial thay người, và DSL text-first là thứ AI sinh ra tốt hơn bất kỳ công cụ GUI nào. Câu trả lời về ecosystem và production-readiness chưa có. Nhưng câu hỏi thì đúng hướng.
HoangYell/markdy-com · MIT · 0★ · markdy.com
Hoang Yell
Một nhà phát triển phần mềm và là người kể chuyện kỹ thuật. Tôi dành thời gian để khám phá những repository mã nguồn mở thú vị nhất trên GitHub và trình bày chúng dưới dạng những câu chuyện dễ hiểu cho mọi người.