Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> JavaScript

Tạo JS Radial Gradient với Matrix trong HTML Canvas


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ệ scale
  • rotate(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ới
  • setTransform(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
  );
}