186 lines
6.8 KiB
Markdown
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. |