
# 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

```text
[ 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í:

```html
<!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.