Files
SonicForgeStudio/md/27_ZOOM_TECH.md

6.8 KiB

Giải pháp Virtual Viewport Rendering cho Waveform Zoom

Phương pháp này sử dụng kỹ thuật Virtual Viewport Rendering (Rendering theo vùng nhìn).

Cơ chế hoạt động:

  1. Thanh cuộn ảo (Virtual Scrollbar): Duy trì một thẻ div ẩn (hoặc gán chiều rộng cho container) bằng chiều rộng lý thuyết của toàn bộ file audio khi zoom. Nhưng Canvas thực tế thì luôn cố định chiều rộng bằng khung nhìn (Viewport).
  2. Xử lý phần ẩn: Các phần ngoài khung nhìn sẽ không được vẽ/render lên Canvas. Dữ liệu âm thanh gốc (Audio Buffer / Array Data) vẫn nằm nguyên trong bộ nhớ (RAM/JS Array), không bị ảnh hưởng.
  3. Khi Zoom Out: Tính toán lại khoảng thời gian [startTime, endTime] rộng hơn, lấy mảng dữ liệu sample tương ứng trong khoảng đó và vẽ đè lại lên Canvas.

1. Kiến trúc tổng quan

[ Toàn bộ Audio Buffer trong Memory: 0s ----------------------> 180s ]
                                         |     Khung nhìn     |
                                         v   (Canvas Fixed)   v
                                    [startTime ------------> endTime]


2. Mã nguồn triển khai (Pure HTML5 & JS)

Đoạn code bên dưới minh họa cơ chế zoom chính xác tại vị trí con trỏ chuột mà không sợ quá tải Canvas hay nhảy vị trí:

<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <style>
    #viewport {
      width: 800px;         /* Chiều rộng khung nhìn cố định */
      height: 150px;
      overflow-x: auto;     /* Hiện thanh cuộn */
      position: relative;
      background: #1e1e1e;
    }

    /* Container giả lập chiều rộng thực tế để tạo thanh cuộn */
    #virtual-content {
      height: 1px;          
      pointer-events: none;
    }

    /* Canvas cố định vị trí luôn đè theo khung nhìn */
    #waveform-canvas {
      position: sticky;
      left: 0;
      top: 0;
      width: 800px;
      height: 150px;
      display: block;
    }
  </style>
</head>
<body>

<div id="viewport">
  <div id="virtual-content"></div>
  <canvas id="waveform-canvas" width="800" height="150"></canvas>
</div>

<script>
// --- GIẢ LẬP DỮ LIỆU AUDIO (Audio Buffer / Sample Data) ---
const AUDIO_DURATION = 60; // Audio dài 60 giây
const SAMPLE_RATE = 100;   // 100 samples/giây
const audioSamples = new Float32Array(AUDIO_DURATION * SAMPLE_RATE);

// Tạo sóng âm giả lập
for (let i = 0; i < audioSamples.length; i++) {
  audioSamples[i] = Math.sin(i * 0.05) * 0.8; 
}

// --- KHAI BÁO BIẾN TRẠNG THÁI ---
const viewport = document.getElementById('viewport');
const virtualContent = document.getElementById('virtual-content');
const canvas = document.getElementById('waveform-canvas');
const ctx = canvas.getContext('2d');

const VIEWPORT_WIDTH = 800;
const VIEWPORT_HEIGHT = 150;

let pixelsPerSecond = 100; // Mức zoom ban đầu (100px = 1s)

// --- HÀM 1: CHỈ VẼ PHẦN HIỂN THỊ TRONG KHUNG NHÌN ---
function renderVisibleWaveform() {
  // 1. Cập nhật độ dài ảo cho thanh cuộn
  const totalWidth = AUDIO_DURATION * pixelsPerSecond;
  virtualContent.style.width = `${totalWidth}px`;

  // 2. Xác định khoảng thời gian đang nằm trong khung nhìn (Viewport)
  const scrollLeft = viewport.scrollLeft;
  const startTime = scrollLeft / pixelsPerSecond;
  const endTime = (scrollLeft + VIEWPORT_WIDTH) / pixelsPerSecond;

  // 3. Xóa Canvas cũ
  ctx.clearRect(0, 0, VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
  ctx.fillStyle = '#00ffcc';

  // 4. Lấy các sample âm thanh tương ứng trong khoảng [startTime, endTime]
  const startSampleIndex = Math.floor(startTime * SAMPLE_RATE);
  const endSampleIndex = Math.ceil(endTime * SAMPLE_RATE);

  // 5. Vẽ đúng các sample này lên Canvas (Vẽ từ x = 0 đến VIEWPORT_WIDTH)
  const middleY = VIEWPORT_HEIGHT / 2;
  
  for (let i = startSampleIndex; i < endSampleIndex; i++) {
    if (i < 0 || i >= audioSamples.length) continue;

    // Thời gian của sample này
    const sampleTime = i / SAMPLE_RATE;
    
    // Tọa độ X trên Canvas cố định (đã trừ đi scrollLeft)
    const x = (sampleTime * pixelsPerSecond) - scrollLeft;
    
    // Chiều cao cột sóng âm
    const amplitude = audioSamples[i] * (VIEWPORT_HEIGHT / 2);

    ctx.fillRect(x, middleY - amplitude / 2, 2, amplitude);
  }
}

// --- HÀM 2: LẮNG NGHE SỰ KIỆN CUỘN VÀ ZOOM ---

// Khi người dùng kéo thanh cuộn
viewport.addEventListener('scroll', () => {
  renderVisibleWaveform();
});

// Khi người dùng lăn chuột để ZOOM tại điểm con trỏ
viewport.addEventListener('wheel', (e) => {
  e.preventDefault();

  // Tọa độ chuột trong khung nhìn Viewport
  const rect = viewport.getBoundingClientRect();
  const mouseX = e.clientX - rect.left;

  // Tính thời điểm (giây) ngay bên dưới con trỏ chuột
  const currentScrollLeft = viewport.scrollLeft;
  const timeAtMouse = (currentScrollLeft + mouseX) / pixelsPerSecond;

  // Hệ số Zoom (Phóng to / Thu nhỏ tùy ý)
  const zoomFactor = e.deltaY < 0 ? 1.15 : 1 / 1.15;
  
  // Giới hạn zoom out tối thiểu (vừa vặn khung nhìn) và zoom in tối đa
  const minPxPerSec = VIEWPORT_WIDTH / AUDIO_DURATION;
  const maxPxPerSec = 50000; // Có thể zoom sâu mà không sợ vỡ DOM
  
  const newPixelsPerSecond = Math.max(minPxPerSec, Math.min(maxPxPerSec, pixelsPerSecond * zoomFactor));

  if (newPixelsPerSecond === pixelsPerSecond) return;

  // Cập nhật mức zoom mới
  pixelsPerSecond = newPixelsPerSecond;

  // Cập nhật lại vị trí thanh cuộn sao cho điểm timeAtMouse vẫn nằm đúng ở mouseX
  viewport.scrollLeft = (timeAtMouse * pixelsPerSecond) - mouseX;

  // Vẽ lại
  renderVisibleWaveform();
}, { passive: false });

// Khởi tạo lần đầu
renderVisibleWaveform();
</script>

</body>
</html>


3. Các điểm quan trọng giúp giải quyết bài toán

  • Thẻ Canvas cố định (position: sticky): Dù zoom 10\times, 100\times hay 1000\times, chiều rộng Canvas không thay đổi (luôn là 800px). Điều này giúp tránh hoàn toàn việc vượt giới hạn chiều rộng của trình duyệt (max canvas width limit).
  • Khôi phục dữ liệu khi Zoom Out: Khi zoom out, pixelsPerSecond giảm xuống. Hàm renderVisibleWaveform() tự động mở rộng khoảng [startTime, endTime] (ví dụ từ 2 giây thành 30 giây). Mảng audioSamples gốc trong RAM vẫn giữ nguyên, hàm vẽ chỉ cần duyệt mảng rộng hơn và vẽ lại lên Canvas.
  • Tối ưu hiệu năng: Hệ thống chỉ tốn tài nguyên GPU/CPU để render đúng số lượng cột sóng xuất hiện trong khung nhìn thay vì vẽ toàn bộ file audio dài.