如何用JavaScript Canvas绘制类Rekordbox的3频段实心波形
复刻Rekordbox 3频段实心波形的纯JavaScript方案
1. 实现平滑包络波形的改造方案
数据预处理:简化冗余细节
通过降采样+滑动窗口极值过滤减少数据点,保留核心包络特征,避免细节过载:
function simplifyWaveformData(rawData, windowSize) { const simplified = []; for (let i = 0; i < rawData.length; i += windowSize) { const window = rawData.slice(i, Math.min(i + windowSize, rawData.length)); const min = Math.min(...window.map(p => p.min)); const max = Math.max(...window.map(p => p.max)); simplified.push({ min, max }); } return simplified; }
路径平滑:消除边缘锯齿
放弃直线段绘制,改用二次贝塞尔曲线连接数据点,生成自然平滑的包络路径:
function drawSmoothFilledPath(ctx, data, canvasWidth, baseY, scale) { ctx.beginPath(); // 绘制上边缘 ctx.moveTo(0, baseY - data[0].max * scale); for (let i = 1; i < data.length; i++) { const prev = data[i-1]; const curr = data[i]; const segmentWidth = canvasWidth / data.length; const prevX = (i-1) * segmentWidth; const currX = i * segmentWidth; const midX = (prevX + currX) / 2; const midY = baseY - ((prev.max + curr.max) / 2) * scale; ctx.quadraticCurveTo(prevX, baseY - prev.max * scale, midX, midY); ctx.quadraticCurveTo(currX, baseY - curr.max * scale, midX, midY); } // 绘制下边缘并闭合路径 ctx.lineTo(canvasWidth, baseY - data[data.length-1].min * scale); for (let i = data.length-2; i >= 0; i--) { const prev = data[i+1]; const curr = data[i]; const segmentWidth = canvasWidth / data.length; const prevX = (i+1) * segmentWidth; const currX = i * segmentWidth; const midX = (prevX + currX) / 2; const midY = baseY - ((prev.min + curr.min) / 2) * scale; ctx.quadraticCurveTo(prevX, baseY - prev.min * scale, midX, midY); ctx.quadraticCurveTo(currX, baseY - curr.min * scale, midX, midY); } ctx.closePath(); ctx.fill(); }
Canvas基础抗锯齿优化
开启内置平滑并优化线条属性:
ctx.imageSmoothingEnabled = true; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
2. 多频段差异化处理策略
降采样窗口差异化
根据频段特性调整窗口大小,平衡细节与简洁度:
- 低频:使用大窗口(16-32个样本),宏观包络足够体现低频能量变化
- 中频:中等窗口(8-16个样本),保留适度细节
- 高频:小窗口(2-8个样本),保留细碎的高频特征
// 示例调用 const simplifiedLow = simplifyWaveformData(rawLowData, 32); const simplifiedMid = simplifyWaveformData(rawMidData, 12); const simplifiedHigh = simplifyWaveformData(rawHighData, 4);
平滑强度差异化
对低频应用多次滑动平均增强平滑效果,高频则弱化或关闭平滑:
function applyMovingAverage(data, iterations) { let smoothed = [...data]; for (let iter = 0; iter < iterations; iter++) { const temp = []; for (let i = 1; i < smoothed.length - 1; i++) { temp.push({ min: (smoothed[i-1].min + smoothed[i].min + smoothed[i+1].min) / 3, max: (smoothed[i-1].max + smoothed[i].max + smoothed[i+1].max) / 3 }); } smoothed = temp; } return smoothed; } // 差异化平滑 const smoothedLow = applyMovingAverage(simplifiedLow, 3); const smoothedMid = applyMovingAverage(simplifiedMid, 1); const smoothedHigh = simplifiedHigh; // 高频不做额外平滑
缩放比例差异化
根据频段能量调整垂直缩放比例,避免高频波形过矮:
const scaleConfig = { low: 0.35, mid: 0.55, high: 0.75 }; const scaledLow = smoothedLow.map(p => ({ min: p.min * scaleConfig.low, max: p.max * scaleConfig.low })); const scaledMid = smoothedMid.map(p => ({ min: p.min * scaleConfig.mid, max: p.max * scaleConfig.mid })); const scaledHigh = smoothedHigh.map(p => ({ min: p.min * scaleConfig.high, max: p.max * scaleConfig.high }));
3. 匹配Rekordbox视觉风格的技巧
配色与透明度设置
参考Rekordbox经典配色,通过透明度区分层级:
- 低频:橙色不透明填充,强视觉基底
- 中频:绿色半透明(0.7-0.8),叠加于低频之上
- 高频:蓝色半透明(0.5-0.6),叠加于最上层
const canvas = document.getElementById('waveform'); const ctx = canvas.getContext('2d'); const height = canvas.height; // 绘制顺序:低频→中频→高频,上层叠加下层 ctx.fillStyle = 'rgba(255, 110, 0, 1)'; // 低频 drawSmoothFilledPath(ctx, scaledLow, canvas.width, height * 0.9, 1); ctx.fillStyle = 'rgba(0, 210, 100, 0.75)'; // 中频 drawSmoothFilledPath(ctx, scaledMid, canvas.width, height * 0.6, 1); ctx.fillStyle = 'rgba(0, 160, 255, 0.6)'; // 高频 drawSmoothFilledPath(ctx, scaledHigh, canvas.width, height * 0.3, 1);
轮廓增强辨识度
给每个频段添加细纯色轮廓,强化波形边界:
function drawPathOutline(ctx, data, canvasWidth, baseY, scale, strokeColor) { ctx.strokeStyle = strokeColor; ctx.lineWidth = 1; // 绘制上边缘轮廓 ctx.beginPath(); ctx.moveTo(0, baseY - data[0].max * scale); for (let i = 1; i < data.length; i++) { const prev = data[i-1]; const curr = data[i]; const segmentWidth = canvasWidth / data.length; const prevX = (i-1) * segmentWidth; const currX = i * segmentWidth; const midX = (prevX + currX) / 2; const midY = baseY - ((prev.max + curr.max) / 2) * scale; ctx.quadraticCurveTo(prevX, baseY - prev.max * scale, midX, midY); } ctx.stroke(); // 绘制下边缘轮廓 ctx.beginPath(); ctx.moveTo(canvasWidth, baseY - data[data.length-1].min * scale); for (let i = data.length-2; i >= 0; i--) { const prev = data[i+1]; const curr = data[i]; const segmentWidth = canvasWidth / data.length; const prevX = (i+1) * segmentWidth; const currX = i * segmentWidth; const midX = (prevX + currX) / 2; const midY = baseY - ((prev.min + curr.min) / 2) * scale; ctx.quadraticCurveTo(prevX, baseY - prev.min * scale, midX, midY); } ctx.stroke(); } // 绘制轮廓示例 drawPathOutline(ctx, scaledLow, canvas.width, height * 0.9, 1, '#ff6a00'); drawPathOutline(ctx, scaledMid, canvas.width, height * 0.6, 1, '#00d264'); drawPathOutline(ctx, scaledHigh, canvas.width, height * 0.3, 1, '#00a0ff');
垂直分层布局
将三个频段在Canvas垂直方向上划分独立区域,避免互相干扰,符合Rekordbox的视觉分层逻辑。
内容的提问来源于stack exchange,提问作者Gabriel
相关产品推荐
相关产品推荐

