SVG from Zero to Senior · Part 23 — Character Rigging & Jointed Animation
Part 23: bring a character to life. Build a skeleton from nested <g> joints, rotate each around its pivot, and let transform inheritance swing the whole limb — then drive a wave and a walk cycle. With a live demo.
So far things have moved as rigid blobs. {Tới giờ mọi thứ chuyển động như khối cứng.} A character feels alive because its parts move relative to each other — a forearm swings from the elbow, which swings from the shoulder. {Một nhân vật sống động vì các bộ phận chuyển động tương đối với nhau — cẳng tay đung đưa từ khuỷu, khuỷu đung đưa từ vai.} That hierarchy is a rig (a skeleton), and SVG models it natively with nested groups. {Phân cấp đó là một rig (bộ xương), và SVG mô hình hóa nó nguyên bản bằng nhóm lồng nhau.}
Switch between Idle, Wave, and Walk, and tick “show joints” to see the pivots. {Chuyển giữa Idle, Wave, Walk, và tích “show joints” để thấy các điểm xoay.}
Open the full demo {Mở demo đầy đủ}: /tools/svg-rigging-demo/.
A joint is a translated, rotated group {Một khớp là một nhóm được dịch và xoay}
The whole technique rests on one idea from Part 5: child transforms compose with their parent’s. {Toàn bộ kỹ thuật dựa trên một ý từ Phần 5: transform con kết hợp với transform cha.} Place a group at the pivot, draw the limb pointing down from (0,0), then nest the next joint at the limb’s far end: {Đặt một nhóm ở điểm xoay, vẽ chi hướng xuống từ (0,0), rồi lồng khớp kế tiếp ở đầu xa của chi:}
<g id="shoulder" transform="translate(88 98) rotate(θ_shoulder)">
<line x1="0" y1="0" x2="0" y2="26"/> <!-- upper arm -->
<g id="elbow" transform="translate(0 26) rotate(θ_elbow)">
<line x1="0" y1="0" x2="0" y2="24"/> <!-- forearm -->
</g>
</g>
Rotate the shoulder and the elbow group (and the forearm inside it) comes along — you only ever specify each joint’s angle relative to its parent. {Xoay vai thì nhóm khuỷu (và cẳng tay bên trong) đi theo — bạn chỉ luôn chỉ định góc mỗi khớp tương đối với cha của nó.} That’s forward kinematics, the foundation of skeletal animation. {Đó là forward kinematics, nền tảng của animation xương.}
The pivot is the group’s origin {Điểm xoay là gốc của nhóm}
This is the senior subtlety. {Đây là điểm tinh tế của senior.} rotate(θ) rotates around (0,0) of the current coordinate system. {rotate(θ) xoay quanh (0,0) của hệ tọa độ hiện tại.} Because we translate the joint to its pivot first, that pivot becomes (0,0) for everything inside — so the limb rotates around the shoulder, not the canvas corner. {Vì ta translate khớp tới điểm xoay trước, điểm xoay đó trở thành (0,0) cho mọi thứ bên trong — nên chi xoay quanh vai, không phải góc canvas.} Order matters: translate, then rotate. {Thứ tự quan trọng: translate rồi rotate.}
The CSS alternative is
transform-box: fill-box; transform-origin: <x> <y>;but for a multi-joint rig the translate-then-rotate group pattern is clearer and composes naturally. {Cách CSS làtransform-box: fill-box; transform-origin, nhưng cho rig nhiều khớp thì mẫu nhóm translate-rồi-rotate rõ hơn và kết hợp tự nhiên.}
Driving the rig: angles over time {Điều khiển rig: góc theo thời gian}
Animation is now just “what angle is each joint at time t?” {Animation giờ chỉ là “mỗi khớp ở góc nào tại thời điểm t?”} Cyclic motion = sine waves. {Chuyển động chu kỳ = sóng sin.} A wave is a raised shoulder plus an oscillating forearm: {Một cú vẫy là vai giơ cao cộng cẳng tay dao động:}
shoulder.setAttribute('transform', `translate(112 98) rotate(-165)`); // arm up
const wob = Math.sin(t * 1.6 * 2 * Math.PI);
elbow.setAttribute('transform', `translate(0 26) rotate(${-25 + wob * 28})`);
A walk cycle swings the two legs in opposite phase (a half-period apart), bends each knee on its back-swing, counter-swings the arms, and bobs the body up on each step: {Một chu kỳ bước đung đưa hai chân ngược pha (lệch nửa chu kỳ), gập gối khi chân ra sau, vung tay ngược lại, và nhún người lên mỗi bước:}
const ph = Math.sin(t * 1.2 * 2 * Math.PI);
const ph2 = Math.sin(t * 1.2 * 2 * Math.PI + Math.PI); // opposite phase
hipL.rotate(ph * 26); kneeL.rotate(Math.max(0, -ph) * 36 + 4); // bend only when behind
hipR.rotate(ph2 * 26); kneeR.rotate(Math.max(0, -ph2) * 36 + 4);
root.translateY(-Math.abs(ph) * 3); // step bob
The opposite-phase legs and counter-swinging arms are what read as “walking” rather than “twitching.” {Chân ngược pha và tay vung ngược chính là cái khiến mắt đọc ra “đang đi” thay vì “giật giật”.}
Why JS here instead of SMIL/CSS {Vì sao dùng JS thay vì SMIL/CSS ở đây}
A rig has many joints whose angles are computed (phase relationships, max() for knee bend, blend between poses). {Một rig có nhiều khớp mà góc được tính toán (quan hệ pha, max() cho gập gối, trộn giữa các tư thế).} A single requestAnimationFrame loop writing transform strings is the cleanest way to express that math, and it lets you blend Idle→Walk or react to input. {Một vòng requestAnimationFrame ghi chuỗi transform là cách sạch nhất để diễn đạt toán đó, và cho bạn trộn Idle→Walk hay phản ứng đầu vào.} For a fixed, self-contained gesture, SMIL animateTransform on each joint works too. {Cho một cử chỉ cố định, độc lập, animateTransform của SMIL trên mỗi khớp cũng được.}
The master’s warnings {Lời cảnh báo của sư phụ}
- Translate before rotate. Reverse the order and the limb orbits the wrong point — the #1 rigging bug. {Translate trước rotate. Đảo thứ tự là chi quay quanh điểm sai — lỗi rig số 1.}
- Draw each limb from its own origin. A forearm group should start at
(0,0); its parent’s translate puts it at the elbow. {Vẽ mỗi chi từ gốc của chính nó.} - Animate angles, not endpoints. Moving line endpoints by hand throws away the hierarchy that makes a rig a rig. {Animate góc, không phải đầu mút.}
- Offer a calm pose for
prefers-reduced-motion. Looping idle motion can bother some users — freeze a neutral frame. {Cho một tư thế tĩnh khiprefers-reduced-motion.}
Practice {Thực hành}
- Add a second arm gesture {Thêm cử chỉ tay thứ hai}: a two-handed cheer by raising both shoulders. {một cú reo hai tay.}
- Tune the walk {Tinh chỉnh dáng đi}: change the leg frequency and knee-bend amount until it reads naturally. {đổi tần số chân và độ gập gối tới khi nhìn tự nhiên.}
- Add a head bob {Thêm nhún đầu}: nest the head in a small group and rotate it slightly with the step phase. {lồng đầu trong nhóm nhỏ và xoay nhẹ theo pha bước.}
What’s next {Phần tiếp theo}
Your character animates on its own clock. {Nhân vật của bạn tự animate theo đồng hồ riêng.} Next we hand the clock to the reader. {Tiếp theo ta trao đồng hồ cho người đọc.} In Part 24 we build scroll-driven SVG storytelling — the modern Scroll-driven Animations API (animation-timeline: scroll() and view()), drawing a path as the page scrolls, and parallax layers that turn a long article into an unfolding scene. {Ở Phần 24 ta dựng kể chuyện SVG theo cuộn — API Scroll-driven Animations hiện đại (animation-timeline: scroll() và view()), vẽ một đường khi trang cuộn, và các lớp parallax biến bài viết dài thành một cảnh mở dần.}