Radial Gradient cơ bản
ctx.createRadialGradient(x0, y0, r0, x1, y1, r1) tạo gradient từ.circle (x0,y0,r0) đến circle (x1,y1,r1).
Transformation Matrix trong Canvas
Canvas 2D Context có ma trận biến đổi (transformation matrix) điều khiển cách vẽ. Các method:
translate(x, y): Dịch chuyển gốc tọa độscale(x, y): Tỷ lệ scalerotate(angle): Xoay (radian)transform(a, b, c, d, e, f): Nhân ma trận hiện tại với ma trận mớisetTransform(a, b, c, d, e, f): Reset về identity rồi apply ma trận mới
Ma trận 2D (3x3, homogeneous coordinates):
[ a c e ] [ scaleX skewY translateX ] [ b d f ] = [ skewX scaleY translateY ] [ 0 0 1 ] [ 0 0 1 ]
Ví dụ: Radial Gradient với Matrix Transform
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Radial Gradient with Matrix</title>
<style>
body { display: flex; justify-content: center; padding: 2rem; background: #f5f5f5; }
.container { display: flex; gap: 2rem; flex-wrap: wrap; }
.panel { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
canvas { border: 1px solid #ddd; border-radius: 4px; background: #fafafa; }
label { display: block; margin: 0.5rem 0 0.25rem; font-weight: 500; font-size: 0.875rem; }
input[type="range"] { width: 200px; }
.value { font-family: monospace; color: #3498db; }
</style>
</head>
<body>
<div class="container">
<canvas id="canvas" width="400" height="400"></canvas>
<div class="panel" style="min-width: 250px;">
<h3>Điều khiển Gradient</h3>
<label>Inner Circle X: <span class="value" id="ixVal">200</span></label>
<input type="range" id="innerX" min="0" max="400" value="200" oninput="updateGradient()">
<label>Inner Circle Y: <span class="value" id="iyVal">200</span></label>
<input type="range" id="innerY" min="0" max="400" value="200" oninput="updateGradient()">
<label>Inner Radius: <span class="value" id="irVal">0</span></label>
<input type="range" id="innerR" min="0" max="200" value="0" oninput="updateGradient()">
<label>Outer Circle X: <span class="value" id="oxVal">200</span></label>
<input type="range" id="outerX" min="0" max="400" value="200" oninput="updateGradient()">
<label>Outer Circle Y: <span class="value" id="oyVal">200</span></label>
<input type="range" id="outerY" min="0" max="400" value="200" oninput="updateGradient()">
<label>Outer Radius: <span class="value" id="orVal">200</span></label>
<input type="range" id="outerR" min="0" max="300" value="200" oninput="updateGradient()">
<hr style="margin: 1rem 0">
<label>Scale X: <span class="value" id="sxVal">1</span></label>
<input type="range" id="scaleX" min="0.1" max="3" step="0.1" value="1" oninput="updateGradient()">
<label>Scale Y: <span class="value" id="syVal">1</span></label>
<input type="range" id="scaleY" min="0.1" max="3" step="0.1" value="1" oninput="updateGradient()">
<label>Rotate (deg): <span class="value" id="rotVal">0</span></label>
<input type="range" id="rotate" min="0" max="360" value="0" oninput="updateGradient()">
<label>Translate X: <span class="value" id="txVal">0</span></label>
<input type="range" id="transX" min="-200" max="200" value="0" oninput="updateGradient()">
<label>Translate Y: <span class="value" id="tyVal">0</span></label>
<input type="range" id="transY" min="-200" max="200" value="0" oninput="updateGradient()">
<hr style="margin: 1rem 0">
<label>Color Stops</label>
<div style="display: flex; gap: 0.5rem; margin-bottom: 0.5rem;">
<input type="color" id="color1" value="#ff0000" oninput="updateGradient()">
<input type="range" id="stop1" min="0" max="1" step="0.01" value="0" oninput="updateGradient()">
</div>
<div style="display: flex; gap: 0.5rem;">
<input type="color" id="color2" value="#00ff00" oninput="updateGradient()">
<input type="range" id="stop2" min="0" max="1" step="0.01" value="1" oninput="updateGradient()">
</div>
</div>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function updateGradient() {
// Read controls
const ix = parseFloat(document.getElementById('innerX').value);
const iy = parseFloat(document.getElementById('innerY').value);
const ir = parseFloat(document.getElementById('innerR').value);
const ox = parseFloat(document.getElementById('outerX').value);
const oy = parseFloat(document.getElementById('outerY').value);
const or = parseFloat(document.getElementById('outerR').value);
const sx = parseFloat(document.getElementById('scaleX').value);
const sy = parseFloat(document.getElementById('scaleY').value);
const rot = parseFloat(document.getElementById('rotate').value) * Math.PI / 180;
const tx = parseFloat(document.getElementById('transX').value);
const ty = parseFloat(document.getElementById('transY').value);
const c1 = document.getElementById('color1').value;
const s1 = parseFloat(document.getElementById('stop1').value);
const c2 = document.getElementById('color2').value;
const s2 = parseFloat(document.getElementById('stop2').value);
// Update labels
['ix','iy','ir','ox','oy','or','sx','sy','rot','tx','ty'].forEach(id => {
document.getElementById(id + 'Val').textContent = document.getElementById(id).value;
});
// Clear
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Save state
ctx.save();
// Apply transformation matrix
// Order: Translate → Rotate → Scale (được apply ngược: scale → rotate → translate)
ctx.translate(canvas.width/2 + tx, canvas.height/2 + ty); // Move origin to center + offset
ctx.rotate(rot);
ctx.scale(sx, sy);
ctx.translate(-canvas.width/2, -canvas.height/2); // Move origin back
// Create radial gradient IN TRANSFORMED SPACE
// Gradient coordinates are in the CURRENT transformation space
const gradient = ctx.createRadialGradient(ix, iy, ir, ox, oy, or);
gradient.addColorStop(s1, c1);
gradient.addColorStop(s2, c2);
// Draw shape filled with gradient
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Restore
ctx.restore();
// Draw coordinate axes (untransformed)
drawAxes();
}
function drawAxes() {
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
ctx.strokeStyle = 'rgba(0,0,0,0.2)';
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
// Center lines
ctx.beginPath();
ctx.moveTo(canvas.width/2, 0);
ctx.lineTo(canvas.width/2, canvas.height);
ctx.moveTo(0, canvas.height/2);
ctx.lineTo(canvas.width, canvas.height/2);
ctx.stroke();
ctx.setLineDash([]);
ctx.font = '10px monospace';
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.fillText('(0,0)', 5, 15);
ctx.fillText(`(${canvas.width},${canvas.height})`, canvas.width - 80, canvas.height - 5);
ctx.restore();
}
// Initial
updateGradient();
</script>
</body>
</html>
Giải thích luồng biến đổi (Transformation Pipeline)
// Khi vẽ với gradient đã transform: // 1. Tạo gradient tại toạ độ "world" (chưa transform) const gradient = ctx.createRadialGradient(ix, iy, ir, ox, oy, or); // 2. Apply transform lên context ctx.translate(tx, ty); // T3: Dịch cuối cùng ctx.rotate(rot); // T2: Xoay ctx.scale(sx, sy); // T1: Scale đầu tiên // 3. Vẽ hình - hình này sẽ bị transform ctx.fillRect(0, 0, w, h); // Vẽ trong không gian đã transform // Kết quả: Gradient được vẽ trong không gian đã transform // Tức là gradient cũng bị scale, rotate, translate theo // Đây là điểm khác biệt quan trọng: Gradient THEO context transform
Pattern thường dùng: Gradient cố định, Shape transform
Nếu muốn gradient KHÔNG bị transform (cố định trên màn hình), tạo gradient SAU khi reset transform:
// Pattern: Shape transform, Gradient fixed on screen ctx.save(); ctx.translate(cx, cy); ctx.rotate(angle); ctx.scale(sx, sy); // Vẽ shape VỚI gradient cố định // Cách 1: Tạo gradient ở không gian màn hình (reset transform tạm) ctx.setTransform(1, 0, 0, 1, 0, 0); const gradient = ctx.createRadialGradient(screenCx, screenCy, 0, screenCx, screenCy, radius); gradient.addColorStop(0, 'red'); gradient.addColorStop(1, 'blue'); // Restore shape transform ctx.translate(cx, cy); ctx.rotate(angle); ctx.scale(sx, sy); ctx.fillStyle = gradient; ctx.fillRect(-w/2, -h/2, w, h); ctx.restore();
Matrix Math: setTransform() trực tiếp
// Tự tính ma trận thay vì chuỗi translate/rotate/scale
// a, b, c, d, e, f
// a = scaleX * cos(rotation)
// b = scaleX * sin(rotation)
// c = scaleY * -sin(rotation) (skewY)
// d = scaleY * cos(rotation) (scaleY)
// e = translateX
// f = translateY
function setTransformFromProps(ctx, { x, y, scaleX, scaleY, rotation }) {
const cos = Math.cos(rotation);
const sin = Math.sin(rotation);
ctx.setTransform(
scaleX * cos, // a
scaleX * sin, // b
-scaleY * sin, // c (skewY)
scaleY * cos, // d
x, // e
y // f
);
}