This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
1. Linear Interpolation (lerp)
Linear Interpolation (nội suy tuyến tính) là nền tảng của mọi animation. Công thức
cực kỳ đơn giản nhưng vô cùng mạnh mẽ: cho 2 giá trị a và b, tìm giá trị
trung gian tại vị trí t (0 đến 1).
// Công thức lerp: a + (b - a) * t
function lerp(a, b, t) {
return a + (b - a) * t;
}
// t = 0 → trả về a (điểm bắt đầu)
// t = 1 → trả về b (điểm kết thúc)
// t = 0.5 → trả về trung điểm
console.log(lerp(0, 100, 0)); // 0
console.log(lerp(0, 100, 0.25)); // 25
console.log(lerp(0, 100, 0.5)); // 50
console.log(lerp(0, 100, 1)); // 100
// Lerp cho vector 2D
function lerpVec2(a, b, t) {
return {
x: lerp(a.x, b.x, t),
y: lerp(a.y, b.y, t)
};
}
// Lerp cho màu sắc (RGB)
function lerpColor(colorA, colorB, t) {
return {
r: Math.round(lerp(colorA.r, colorB.r, t)),
g: Math.round(lerp(colorA.g, colorB.g, t)),
b: Math.round(lerp(colorA.b, colorB.b, t))
};
}
Ứng dụng thực tế: di chuyển mượt mà từ A đến B bằng cách tăng dần t theo thời gian.
// Animation di chuyển mượt từ A đến B
const start = { x: 50, y: 300 };
const end = { x: 700, y: 100 };
const duration = 2; // 2 giây
let elapsed = 0;
function update(dt) {
elapsed += dt;
const t = Math.min(elapsed / duration, 1); // clamp 0-1
const pos = lerpVec2(start, end, t);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(pos.x, pos.y, 20, 0, Math.PI * 2);
ctx.fillStyle = '#f59e0b';
ctx.fill();
if (t >= 1) {
console.log('Animation hoàn thành!');
}
}
// Smooth follow (lerp mỗi frame — không cần t tích lũy)
// Rất phổ biến cho camera follow, smooth cursor
function smoothFollow(current, target, smoothing, dt) {
// smoothing: 0.1 = rất mượt, 0.5 = nhanh
return lerp(current, target, 1 - Math.pow(1 - smoothing, dt * 60));
}
// Ví dụ: camera mượt theo player
camera.x = smoothFollow(camera.x, player.x, 0.1, dt);
camera.y = smoothFollow(camera.y, player.y, 0.1, dt);
2. Easing Functions (Robert Penner)
Linear lerp di chuyển đều — trông cứng và không tự nhiên. Easing functions biến đổi
giá trị t (0→1) thành đường cong, tạo cảm giác tăng tốc, giảm tốc, hoặc nảy. Robert
Penner là người đầu tiên hệ thống hóa các công thức này. Tham khảo trực quan tại
easings.net. Demo bên dưới vẽ
đường cong easing và đồng thời cho một chấm chạy theo timing đó để bạn cảm nhận chuyển động:
const Easing = {
// === Quad (bậc 2) — nhẹ nhàng ===
easeInQuad: t => t * t,
easeOutQuad: t => t * (2 - t),
easeInOutQuad: t => t < 0.5
? 2 * t * t
: -1 + (4 - 2 * t) * t,
// === Cubic (bậc 3) — mạnh hơn Quad ===
easeInCubic: t => t * t * t,
easeOutCubic: t => (--t) * t * t + 1,
easeInOutCubic: t => t < 0.5
? 4 * t * t * t
: (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
// === Quart (bậc 4) ===
easeInQuart: t => t * t * t * t,
easeOutQuart: t => 1 - (--t) * t * t * t,
// === Quint (bậc 5) ===
easeInQuint: t => t * t * t * t * t,
easeOutQuint: t => 1 + (--t) * t * t * t * t,
// === Sine — rất mượt, tự nhiên ===
easeInSine: t => 1 - Math.cos(t * Math.PI / 2),
easeOutSine: t => Math.sin(t * Math.PI / 2),
easeInOutSine: t => -(Math.cos(Math.PI * t) - 1) / 2,
// === Expo — rất mạnh, gần như dừng rồi bắn ra ===
easeInExpo: t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1)),
easeOutExpo: t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
// === Back — đi ngược trước khi tiến (overshoot) ===
easeInBack: t => {
const s = 1.70158; // overshoot amount
return t * t * ((s + 1) * t - s);
},
easeOutBack: t => {
const s = 1.70158;
t -= 1;
return t * t * ((s + 1) * t + s) + 1;
},
// === Elastic — nảy như dây thun ===
easeInElastic: t => {
if (t === 0 || t === 1) return t;
return -Math.pow(2, 10 * (t - 1)) *
Math.sin((t - 1.1) * 5 * Math.PI);
},
easeOutElastic: t => {
if (t === 0 || t === 1) return t;
return Math.pow(2, -10 * t) *
Math.sin((t - 0.1) * 5 * Math.PI) + 1;
},
// === Bounce — nảy như quả bóng rơi ===
easeOutBounce: t => {
if (t < 1 / 2.75) {
return 7.5625 * t * t;
} else if (t < 2 / 2.75) {
t -= 1.5 / 2.75;
return 7.5625 * t * t + 0.75;
} else if (t < 2.5 / 2.75) {
t -= 2.25 / 2.75;
return 7.5625 * t * t + 0.9375;
} else {
t -= 2.625 / 2.75;
return 7.5625 * t * t + 0.984375;
}
},
easeInBounce: t => 1 - Easing.easeOutBounce(1 - t)
};
Sử dụng easing rất đơn giản — thay vì dùng t trực tiếp trong lerp, ta cho
t đi qua easing function trước:
// Không easing (linear) — cứng nhắc
const linearPos = lerp(startX, endX, t);
// Có easing — mượt mà, tự nhiên
const easedT = Easing.easeOutCubic(t);
const easedPos = lerp(startX, endX, easedT);
// So sánh trực quan: vẽ nhiều ball với easing khác nhau
const easings = [
{ name: 'linear', fn: t => t },
{ name: 'easeInQuad', fn: Easing.easeInQuad },
{ name: 'easeOutQuad', fn: Easing.easeOutQuad },
{ name: 'easeInOutCubic', fn: Easing.easeInOutCubic },
{ name: 'easeOutElastic', fn: Easing.easeOutElastic },
{ name: 'easeOutBounce', fn: Easing.easeOutBounce }
];
function drawComparison(t) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
easings.forEach((easing, i) => {
const y = 50 + i * 60;
const easedT = easing.fn(t);
const x = lerp(50, canvas.width - 50, easedT);
// Label
ctx.fillStyle = '#94a3b8';
ctx.font = '12px monospace';
ctx.fillText(easing.name, 10, y + 5);
// Ball
ctx.beginPath();
ctx.arc(x, y, 12, 0, Math.PI * 2);
ctx.fillStyle = '#06b6d4';
ctx.fill();
});
}
3. Tween Engine tự viết
Một Tween (in-between) là object quản lý animation từ giá trị A đến B trong khoảng thời gian nhất định, với easing tùy chọn. Tween engine quản lý danh sách các tween đang chạy và update chúng mỗi frame. Hãy click vào bất kỳ đâu trên demo bên dưới — ô vuông sẽ tween mượt mà tới vị trí đó với easing bạn chọn, để lại vệt mờ:
💡 Click vào canvas để đặt đích đến cho ô vuông.
class Tween {
constructor(target, properties, options = {}) {
this.target = target;
this.properties = {};
this.duration = options.duration || 1; // giây
this.easing = options.easing || (t => t); // linear mặc định
this.delay = options.delay || 0;
this.onUpdate = options.onUpdate || null;
this.onComplete = options.onComplete || null;
this.elapsed = 0;
this.started = false;
this.completed = false;
// Lưu giá trị from/to cho mỗi property
for (const [key, toValue] of Object.entries(properties)) {
this.properties[key] = {
from: target[key],
to: toValue
};
}
}
update(deltaTime) {
if (this.completed) return;
// Delay
if (this.delay > 0) {
this.delay -= deltaTime;
return;
}
if (!this.started) {
this.started = true;
// Cập nhật "from" tại thời điểm bắt đầu thực
for (const [key, prop] of Object.entries(this.properties)) {
prop.from = this.target[key];
}
}
this.elapsed += deltaTime;
const rawT = Math.min(this.elapsed / this.duration, 1);
const easedT = this.easing(rawT);
// Cập nhật tất cả properties
for (const [key, prop] of Object.entries(this.properties)) {
this.target[key] = prop.from + (prop.to - prop.from) * easedT;
}
if (this.onUpdate) this.onUpdate(easedT);
if (rawT >= 1) {
this.completed = true;
if (this.onComplete) this.onComplete();
}
}
}
// Tween Manager
class TweenManager {
constructor() {
this.tweens = [];
}
add(target, properties, options) {
const tween = new Tween(target, properties, options);
this.tweens.push(tween);
return tween;
}
update(deltaTime) {
for (let i = this.tweens.length - 1; i >= 0; i--) {
this.tweens[i].update(deltaTime);
if (this.tweens[i].completed) {
this.tweens.splice(i, 1);
}
}
}
clear() {
this.tweens = [];
}
}
// === Sử dụng ===
const tweenMgr = new TweenManager();
const box = { x: 50, y: 200, width: 40, height: 40, opacity: 0 };
// Fade in + di chuyển
tweenMgr.add(box, { x: 400, y: 100, opacity: 1 }, {
duration: 1.5,
easing: Easing.easeOutCubic,
onComplete: () => {
// Chain: sau khi xong, tween tiếp
tweenMgr.add(box, { x: 700, y: 300 }, {
duration: 1,
easing: Easing.easeInOutQuad
});
}
});
// Trong game loop:
function update(dt) {
tweenMgr.update(dt);
}
function draw() {
ctx.globalAlpha = box.opacity;
ctx.fillStyle = '#8b5cf6';
ctx.fillRect(box.x, box.y, box.width, box.height);
ctx.globalAlpha = 1;
}
4. Bezier Curves cho Animation
Cubic Bezier là đường cong được xác định bởi 4 điểm điều khiển (P0, P1, P2, P3). CSS
transition-timing-function: cubic-bezier(x1,y1,x2,y2) chính là dạng này. Trên Canvas, ta
dùng Bezier cho cả animation timing và path animation.
// Quadratic Bezier: 3 điểm điều khiển
function quadraticBezier(p0, p1, p2, t) {
const mt = 1 - t;
return {
x: mt * mt * p0.x + 2 * mt * t * p1.x + t * t * p2.x,
y: mt * mt * p0.y + 2 * mt * t * p1.y + t * t * p2.y
};
}
// Cubic Bezier: 4 điểm điều khiển
function cubicBezier(p0, p1, p2, p3, t) {
const mt = 1 - t;
const mt2 = mt * mt;
const t2 = t * t;
return {
x: mt2 * mt * p0.x + 3 * mt2 * t * p1.x +
3 * mt * t2 * p2.x + t2 * t * p3.x,
y: mt2 * mt * p0.y + 3 * mt2 * t * p1.y +
3 * mt * t2 * p2.y + t2 * t * p3.y
};
}
// Path animation: di chuyển object dọc theo đường Bezier
const pathPoints = {
p0: { x: 50, y: 350 },
p1: { x: 200, y: 50 }, // điểm điều khiển 1
p2: { x: 500, y: 50 }, // điểm điều khiển 2
p3: { x: 700, y: 350 }
};
let pathT = 0;
function animatePath(dt) {
pathT += dt * 0.3; // tốc độ di chuyển
if (pathT > 1) pathT = 0; // loop
const pos = cubicBezier(
pathPoints.p0, pathPoints.p1,
pathPoints.p2, pathPoints.p3, pathT
);
// Vẽ đường path
ctx.beginPath();
ctx.moveTo(pathPoints.p0.x, pathPoints.p0.y);
ctx.bezierCurveTo(
pathPoints.p1.x, pathPoints.p1.y,
pathPoints.p2.x, pathPoints.p2.y,
pathPoints.p3.x, pathPoints.p3.y
);
ctx.strokeStyle = 'rgba(148, 163, 184, 0.3)';
ctx.lineWidth = 2;
ctx.stroke();
// Vẽ object
ctx.beginPath();
ctx.arc(pos.x, pos.y, 12, 0, Math.PI * 2);
ctx.fillStyle = '#f59e0b';
ctx.fill();
}
Để dùng Cubic Bezier như CSS cubic-bezier(x1,y1,x2,y2) cho easing timing, ta cần giải
ngược: cho t thời gian, tìm y (progress).
// CSS cubic-bezier() tương đương — dùng Newton-Raphson
function cubicBezierEasing(x1, y1, x2, y2) {
// Tính x(t) từ cubic bezier (P0=0,0 P3=1,1)
function sampleCurveX(t) {
return ((1 - 3 * x2 + 3 * x1) * t +
(3 * x2 - 6 * x1)) * t + 3 * x1 * t;
}
function sampleCurveY(t) {
return ((1 - 3 * y2 + 3 * y1) * t +
(3 * y2 - 6 * y1)) * t + 3 * y1 * t;
}
function sampleCurveDerivX(t) {
return (3 * (1 - 3 * x2 + 3 * x1)) * t * t +
(2 * (3 * x2 - 6 * x1)) * t + 3 * x1;
}
// Newton-Raphson: tìm t sao cho x(t) = input
function solveCurveX(x) {
let t = x;
for (let i = 0; i < 8; i++) {
const residue = sampleCurveX(t) - x;
if (Math.abs(residue) < 1e-7) return t;
const d = sampleCurveDerivX(t);
if (Math.abs(d) < 1e-7) break;
t -= residue / d;
}
return t;
}
// Trả về easing function: input t(0-1) → output y(0-1)
return function(t) {
return sampleCurveY(solveCurveX(t));
};
}
// CSS tương đương:
const easeCSS = cubicBezierEasing(0.25, 0.1, 0.25, 1.0); // ease
const easeInOutCSS = cubicBezierEasing(0.42, 0, 0.58, 1.0); // ease-in-out
const customCSS = cubicBezierEasing(0.68, -0.55, 0.27, 1.55); // custom overshoot
5. Spring Animation (Hooke's Law)
Spring (lò xo) animation tạo chuyển động tự nhiên nhất vì dựa trên vật lý thực. Thay vì định nghĩa
duration và easing cố định, spring animation được điều khiển bởi
stiffness (độ cứng), damping (ma sát), và
mass (khối lượng). Kết quả: overshoot tự nhiên, settle mượt mà.
// Hooke's Law: F = -kx - dv
// k = stiffness (độ cứng lò xo)
// d = damping (hệ số ma sát/giảm chấn)
// x = displacement (khoảng cách từ vị trí cân bằng)
// v = velocity (vận tốc hiện tại)
class Spring {
constructor(options = {}) {
this.stiffness = options.stiffness || 180; // k: lò xo cứng
this.damping = options.damping || 12; // d: giảm chấn
this.mass = options.mass || 1;
this.velocity = 0;
this.current = options.from || 0;
this.target = options.to || 1;
this.settled = false;
this.restThreshold = 0.001;
}
setTarget(value) {
this.target = value;
this.settled = false;
}
update(deltaTime) {
if (this.settled) return this.current;
// Lực lò xo: F = -k * (current - target)
const displacement = this.current - this.target;
const springForce = -this.stiffness * displacement;
// Lực giảm chấn: F = -d * velocity
const dampingForce = -this.damping * this.velocity;
// Tổng lực → gia tốc: a = F/m
const acceleration = (springForce + dampingForce) / this.mass;
// Cập nhật velocity và position
this.velocity += acceleration * deltaTime;
this.current += this.velocity * deltaTime;
// Kiểm tra đã ổn định chưa
if (
Math.abs(this.velocity) < this.restThreshold &&
Math.abs(displacement) < this.restThreshold
) {
this.current = this.target;
this.velocity = 0;
this.settled = true;
}
return this.current;
}
}
// === Sử dụng ===
const springX = new Spring({ stiffness: 200, damping: 15, from: 50, to: 600 });
const springY = new Spring({ stiffness: 120, damping: 10, from: 300, to: 100 });
// Presets phổ biến:
const presets = {
gentle: { stiffness: 120, damping: 14 },
wobbly: { stiffness: 180, damping: 8 }, // nhiều overshoot
stiff: { stiffness: 300, damping: 20 }, // nhanh, ít overshoot
slow: { stiffness: 60, damping: 12 }, // chậm, mượt
molasses:{ stiffness: 80, damping: 30 } // rất nặng nề
};
function update(dt) {
const x = springX.update(dt);
const y = springY.update(dt);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(x, y, 20, 0, Math.PI * 2);
ctx.fillStyle = '#ec4899';
ctx.fill();
}
// Click để đổi target → spring tự animate
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
springX.setTarget(e.clientX - rect.left);
springY.setTarget(e.clientY - rect.top);
});
So sánh spring với easing: easing có duration cố định và kết thúc chính xác tại thời điểm định trước. Spring không có duration — nó kết thúc khi năng lượng tiêu hao hết. Overshoot là tự nhiên, không cần công thức đặc biệt. Thư viện tham khảo: react-spring, Framer Motion, Anime.js.
// Spring cho nhiều thuộc tính cùng lúc
class SpringAnimator {
constructor(target, config = {}) {
this.target = target;
this.springs = {};
this.config = { stiffness: 170, damping: 26, ...config };
}
to(properties) {
for (const [key, value] of Object.entries(properties)) {
if (!this.springs[key]) {
this.springs[key] = new Spring({
...this.config,
from: this.target[key],
to: value
});
} else {
this.springs[key].setTarget(value);
}
}
}
update(dt) {
let allSettled = true;
for (const [key, spring] of Object.entries(this.springs)) {
this.target[key] = spring.update(dt);
if (!spring.settled) allSettled = false;
}
return allSettled;
}
}
// Ví dụ: animate scale + opacity khi hover
const card = { x: 200, y: 150, scale: 1, opacity: 0.8 };
const cardSpring = new SpringAnimator(card, { stiffness: 200, damping: 18 });
// Hover in
cardSpring.to({ scale: 1.1, opacity: 1 });
// Hover out
cardSpring.to({ scale: 1, opacity: 0.8 });
6. Câu hỏi trắc nghiệm ôn tập
Trắc nghiệm 1: Linear Interpolation
lerp(100, 300, 0.75) trả về giá trị nào?
Trắc nghiệm 2: Easing Functions
Easing function nào tạo hiệu ứng "bắt đầu chậm, kết thúc nhanh" (tăng tốc dần)?
Trắc nghiệm 3: Spring Animation
Trong spring animation, thông số nào kiểm soát mức độ overshoot (vượt quá đích)?
Tải file code thực hành minh họa bài học
File tổng hợp các easing functions, tween engine, bezier curves và spring animation:
Tải về canvas_easing.js
Comments
Bình luận