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.

Tới giờ mọi thứ chuyển động như khối cứng. 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. 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.

Chuyển giữa Idle, Wave, Walk, và tích “show joints” để thấy các điểm xoay.

Mở demo đầy đủ:

Một khớp là một nhóm được dịch và xoay

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. Đặ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>

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ó. Đó là forward kinematics, nền tảng của animation xương.

Điểm xoay là gốc của nhóm

Đây là điểm tinh tế của senior. rotate(θ) xoay quanh (0,0) của hệ tọa độ hiện tại. 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. Thứ tự quan trọng: translate rồi rotate.

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.

Điều khiển rig: góc theo thời gian

Animation giờ chỉ là “mỗi khớp ở góc nào tại thời điểm t?” Chuyển động chu kỳ = sóng sin. 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})`);

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

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”.

Vì sao dùng JS thay vì SMIL/CSS ở đây

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ế). 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. Cho một cử chỉ cố định, độc lập, animateTransform của SMIL trên mỗi khớp cũng được.

Lời cảnh báo của sư phụ

  • Translate trước rotate. Đảo thứ tự là chi quay quanh điểm sai — lỗi rig số 1.
  • Vẽ mỗi chi từ gốc của chính nó.
  • Animate góc, không phải đầu mút.
  • Cho một tư thế tĩnh khi prefers-reduced-motion.

Thực hành

  1. Thêm cử chỉ tay thứ hai: một cú reo hai tay.
  2. Tinh chỉnh dáng đi: đổi tần số chân và độ gập gối tới khi nhìn tự nhiên.
  3. Thêm nhún đầu: lồng đầu trong nhóm nhỏ và xoay nhẹ theo pha bước.

Phần tiếp theo

Nhân vật của bạn tự animate theo đồng hồ riêng. Tiếp theo ta trao đồng hồ cho người đọc. Ở 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.