jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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 khi prefers-reduced-motion.}

Practice {Thực hành}

  1. 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.}
  2. 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.}
  3. 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()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.}