Files

186 lines
6.8 KiB
Markdown

# 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.