You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 02:24:57