6.8 KiB
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:
- 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). - 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. - 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ù zoom10\times, 100\timeshay1000\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,
pixelsPerSecondgiảm xuống. HàmrenderVisibleWaveform()tự động mở rộng khoảng[startTime, endTime](ví dụ từ 2 giây thành 30 giây). MảngaudioSamplesgố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.