fix: git commit node modules
This commit is contained in:
+186
@@ -0,0 +1,186 @@
|
||||
|
||||
# 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.
|
||||
Reference in New Issue
Block a user