JS饼状图扇区颜色渐变与立体效果实现

作者:窗边听雨

时间:2026-11-03

浏览:535

深入讲解如何使用原生JavaScript在HTML5 Canvas中实现具有颜色渐变与3D立体效果的饼状图,涵盖仿射透视投影、空间侧壁挤出、画家算法深度排序、径向高光与曲面法向阴影渐变等核心技术与完整工程代码。

文章目录
HTML5 Canvas 图形学算法 3D 可视化 阅读时长约 8 分钟 代码无外部依赖
JS饼状图扇区颜色渐变与立体效果实现
实现具有颜色渐变与立体效果的 JavaScript 饼状图,核心在于两套图形学转换:一是利用倾斜仿射变换与挤出几何算法构建具备厚度与正确遮挡关系的 3D 柱体扇区;二是依据光照模型为扇区顶面与外侧壁分别匹配角度渐变、径向高光和法向明暗过渡。本文将基于原生 HTML5 Canvas 深入剖析 3D 扇区的几何推导、深度排序(画家算法)、侧壁可见性判定、光影渐变着色,并提供完整的工程化实现方案与边界优化技巧。

一、立体饼图与色彩渐变的核心机理

在二维平面中,标准的饼状图由多个共享圆心的平面圆形扇区(Sector)构成。要让扁平的扇区呈现出逼真的立体感与材质光泽,不能仅靠单一的浅色或阴影滤镜,而需要从几何空间、遮挡消隐和光学着色三个维度进行数学建模。

1.1 仿射投影压缩与等轴测透视

在无需引入臃肿 WebGL 渲染管线的前提下,最经典且高效的 3D 俯仰透视方式是通过 2D 仿射变换压缩 Y 轴坐标。通过在 Canvas 绘图上下文中应用 ctx.scale(1, ky)(其中压缩比 ky 通常设定在 0.5 到 0.6 之间),原本半径为 R 的圆形扇区被压平为半长轴为 R、半短轴为 R × ky 的椭圆扇区。这种变换在平面视网膜上形成了近似等轴测的俯视视差。

1.2 空间厚度挤出与实体曲面构成

仅仅压扁圆面只能得到平面椭圆,必须赋予其空间纵深厚度。在几何学中,厚度是通过沿 Z 轴(在 2D 投影平面中表现为垂直向下沿 Y 轴正方向)将椭圆顶面平移距离 H,形成实体圆柱段。一个完整的 3D 扇区由三类面组合封闭:

  • 顶面椭圆扇区:由圆心、起止角度边界线及上部椭圆弧线构成的闭合受光面。
  • 外圆弧柱面侧壁:连接顶面外椭圆弧与平移 H 后的底面椭圆弧所构成的曲面封闭带。
  • 径向剖切面侧壁:扇区起始边与终止边沿厚度方向拉伸形成的两个垂直平面四边形。
JS饼状图扇区颜色渐变与立体效果实现
图 1 说明:展示了 3D 饼状图扇区从标准二维圆弧、仿射透视压缩、纵向空间挤出侧壁到最终光影渐变着色的四阶段几何演进过程。

二、渲染管线与绘制执行步骤

由于 3D 饼图存在空间前后层级,若直接按数据原始顺序绘制,位于前方的扇区侧壁会被后方扇区覆盖,产生严重的“穿模”缺陷。因此,整个渲染流水线必须遵循严格的排序与分层执行步骤。

2.1 步骤一:数据归一化与扇区角度量化

首先遍历数据集合,累加所有数据项的数值总和 total。随后按各项占比乘以 2π,依次推导出每个扇区的起始弧度 startAngle、终止弧度 endAngle,以及代表扇区中轴方向的平分角 midAngle = (startAngle + endAngle) / 2。

2.2 步骤二:画家算法深度排序(Depth Sorting)

在俯视透视模型中,Y 坐标越小(即处于上侧)的元素在视觉空间中距离观察者越远,Y 坐标越大(处于下侧)的元素距离观察者越近。 依据图形学中的“画家算法”(Painter's Algorithm),必须先绘制远景、再绘制近景。每个扇区中轴点在单位圆上的垂直分量为 Math.sin(midAngle)。我们据此构建排序权重,将数值从小到大(从远端到近端)排序,确定绝对绘制次序。

2.3 步骤三:侧壁可见性区间判定与裁剪

在外圆弧侧壁的渲染中,背对观察者的外弧柱面(位于椭圆上半圆,即弧度区间 [π, 2π])会被扇区自身的顶面完全遮挡,无需浪费算力绘制;只有面向观察者(位于下半圆 [0, π])的外圆弧侧壁才真实可见。 因此,在绘制外弧侧壁前,必须将扇区的角度区间 [startAngle, endAngle] 与可见区间 [0, π] 进行布尔交集求交,精确裁剪出可见弧段后再执行柱面挤出。

JS饼状图扇区颜色渐变与立体效果实现
图 2 说明:展示了 3D 渐变饼图从输入数据归一化、画家算法深度排序、侧壁可见性判断分支,到侧壁曲面生成与顶面光影叠合的完整执行流程。

三、扇区渐变着色与曲面光影算法实现

纯色的立体模型往往显得机械呆板,真实世界中的曲面材质具有随观察角度与入射光线变化的渐变特性。在 Canvas 2D 环境下,通过组合使用线性渐变(createLinearGradient)与径向渐变(createRadialGradient),可以模拟出逼真的兰伯特漫反射与高光效果。

3.1 顶面拟真光照:径向高光反射

扇区顶面是主要的视觉承载面。我们使用偏心径向渐变来模拟斜上方平行光源对平面的照射。以扇区几何中心为基准,向假想光源方向(如左上方 -45° 方向)偏移微小距离构建高光焦点 (xf, yf)。渐变色标从高光区的提亮色(明度提升 20%~25%),平滑过渡到基准色,并在接近外边缘处微降明度(降低 15%~20%),从而赋予顶面微妙的弧面张力感。

3.2 外弧侧壁法向漫反射与连续明暗过渡

外圆柱侧壁是连续弯曲的曲面。外弧上每一点的法向量为 N(θ) = (cos θ, sin θ)。当光源从左侧照射时,左侧侧壁的法向量与光线夹角较小,接受较多光照;右侧侧壁则背光。 在实现上,无需逐像素计算光照方程,而是沿着可见外弧的起点坐标 (x1, y1) 到终点坐标 (x2, y2) 建立线性渐变。线性渐变内部根据起止角度与光源矢量的内积,计算并设置对应的深浅色标,极高效率地实现柱面圆滑明暗转折。

JS饼状图扇区颜色渐变与立体效果实现
图 3 说明:详细解析了顶面扇区偏心径向高光反射模型(左)与外弧柱面法向漫反射线性衰减着色模型(右)的数学映射几何。

四、完整原生 JavaScript 引擎实现与实战示例

下面提供一套可以直接在现代浏览器中运行的原生 JavaScript 实现方案。代码封装为自包含的 render3DPieChart 函数,内部处理了画布设备像素比自适应、深度排序、可见性区间切割、空间挤出侧壁绘制与复合色彩渐变。

/**
 * 3D 渐变饼状图渲染引擎 (原生 Canvas 2D 实现)
 * @param {HTMLCanvasElement} canvas - 目标画布节点
 * @param {Array} data - 数据项数组 [{ label, value, color }, ...]
 * @param {Object} options - 配置项
 */
function render3DPieChart(canvas, data, options = {}) {
  const {
    cx = canvas.width / 2,
    cy = canvas.height / 2,
    radius = 160,
    height = 36,           // 立体挤出厚度
    scaleY = 0.55,         // 俯视透视压缩比例
    explodedIndex = 0,     // 默认悬浮高亮扇区索引
    explodeDistance = 20   // 悬浮扇区位移距离
  } = options;

  const ctx = canvas.getContext('2d');
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 1. 数据归一化与弧度计算
  const total = data.reduce((sum, item) => sum + item.value, 0);
  let currentAngle = -Math.PI / 2; // 从 12 点钟方向开始

  const sectors = data.map((item, index) => {
    const sliceAngle = (item.value / total) * Math.PI * 2;
    const startAngle = currentAngle;
    const endAngle = currentAngle + sliceAngle;
    const midAngle = (startAngle + endAngle) / 2;
    currentAngle = endAngle;

    return {
      ...item,
      index,
      startAngle,
      endAngle,
      midAngle,
      isExploded: index === explodedIndex
    };
  });

  // 2. 画家算法深度排序:中轴 Y 轴分量越小越靠后,先绘制
  const sortedSectors = [...sectors].sort((a, b) => {
    return Math.sin(a.midAngle) - Math.sin(b.midAngle);
  });

  // 辅助颜色明暗调节函数 (十六进制色值调整)
  function adjustColor(hex, factor) {
    let num = parseInt(hex.replace('#', ''), 16);
    let r = Math.min(255, Math.max(0, Math.round(((num >> 16) & 0xff) * factor)));
    let g = Math.min(255, Math.max(0, Math.round(((num >> 8) & 0xff) * factor)));
    let b = Math.min(255, Math.max(0, Math.round((num & 0xff) * factor)));
    return `rgb(${r},${g},${b})`;
  }

  // 3. 逐层遍历渲染各扇区
  sortedSectors.forEach(sector => {
    ctx.save();

    // 计算悬浮偏移位移向量 (Δx, Δy)
    let offsetX = 0;
    let offsetY = 0;
    if (sector.isExploded) {
      offsetX = Math.cos(sector.midAngle) * explodeDistance;
      offsetY = Math.sin(sector.midAngle) * explodeDistance * scaleY;
    }

    const sectorCx = cx + offsetX;
    const sectorCy = cy + offsetY;

    // --- A. 绘制可见外圆弧柱面侧壁 ---
    // 归一化角到 [0, 2π] 区间用于判定下半圆 [0, π]
    const normStart = (sector.startAngle % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2);
    const normEnd = (sector.endAngle % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2);

    // 针对跨越可见区间的角度做外弧采样绘制 (步进逼近法或分段曲面)
    const arcStep = 0.05;
    let a1 = sector.startAngle;
    let a2 = sector.endAngle;

    for (let a = a1; a < a2; a += arcStep) {
      const subStart = a;
      const subEnd = Math.min(a + arcStep, a2);
      const subMid = (subStart + subEnd) / 2;
      const normSubMid = (subMid % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2);

      // 仅当下半圆可见区间 (0 到 π) 时绘制外侧壁
      if (normSubMid >= 0 && normSubMid <= Math.PI) {
        const x1 = sectorCx + Math.cos(subStart) * radius;
        const y1 = sectorCy + Math.sin(subStart) * radius * scaleY;
        const x2 = sectorCx + Math.cos(subEnd) * radius;
        const y2 = sectorCy + Math.sin(subEnd) * radius * scaleY;

        // 根据曲面法线计算受光衰减系数 (左上方光源)
        const lightIntensity = 0.45 + 0.5 * Math.max(0, -Math.cos(subMid) * 0.7 + Math.sin(subMid) * 0.3);
        const wallColor = adjustColor(sector.color, lightIntensity);

        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.lineTo(x2, y2 + height);
        ctx.lineTo(x1, y1 + height);
        ctx.closePath();
        ctx.fillStyle = wallColor;
        ctx.fill();
      }
    }

    // --- B. 绘制径向剖切面侧壁 (如果切面法向朝向观察者) ---
    [sector.startAngle, sector.endAngle].forEach(edgeAngle => {
      const normEdge = (edgeAngle % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2);
      // 判定径向边是否朝向视点
      if (Math.sin(edgeAngle) > -0.2) {
        const edgeX = sectorCx + Math.cos(edgeAngle) * radius;
        const edgeY = sectorCy + Math.sin(edgeAngle) * radius * scaleY;

        ctx.beginPath();
        ctx.moveTo(sectorCx, sectorCy);
        ctx.lineTo(edgeX, edgeY);
        ctx.lineTo(edgeX, edgeY + height);
        ctx.lineTo(sectorCx, sectorCy + height);
        ctx.closePath();
        ctx.fillStyle = adjustColor(sector.color, 0.65);
        ctx.fill();
      }
    });

    // --- C. 绘制顶面椭圆扇区与高光渐变 ---
    ctx.save();
    ctx.translate(sectorCx, sectorCy);
    ctx.scale(1, scaleY);

    // 建立偏心高光径向渐变
    const focusDist = radius * 0.25;
    const fx = Math.cos(sector.midAngle - 0.4) * focusDist;
    const fy = Math.sin(sector.midAngle - 0.4) * focusDist;
    const topGrad = ctx.createRadialGradient(fx, fy, 5, 0, 0, radius);
    topGrad.addColorStop(0, adjustColor(sector.color, 1.25)); // 中心高光提亮
    topGrad.addColorStop(0.7, sector.color);                 // 基准主色
    topGrad.addColorStop(1, adjustColor(sector.color, 0.8));  // 边缘环境渐暗

    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.arc(0, 0, radius, sector.startAngle, sector.endAngle);
    ctx.closePath();
    ctx.fillStyle = topGrad;
    ctx.fill();

    // 顶面半透明高光边缘描边
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.35)';
    ctx.lineWidth = 1;
    ctx.stroke();

    ctx.restore();
    ctx.restore();
  });
}
代码使用说明:调用 render3DPieChart(canvas, data, options) 时,仅需传入普通的 Canvas DOM 节点及标准配色数组(如 #345E82、#AD8060 等)。函数内部自动完成仿射缩放与光照色标计算,无需引入任何第三方 3D 引擎。

五、工程边界防护与性能优化

在实际生产环境中落地 3D 饼状图时,往往会遇到高分屏模糊、扇区接缝白边(Seam Artifacts)以及极端数值渲染等工程边界问题。针对这些典型隐患,需要采取针对性的防护措施。

隐患类型 具体表现 产生根因 工程防范措施
接缝发虚与白边 相邻扇区或侧壁与顶面连接处出现微小背景贯穿缝隙 抗锯齿(Subpixel Anti-aliasing)插值导致的透明像素泄漏 在计算侧壁与扇区弧度步长时增加 0.005 rad 的微弱重叠冗余
Retina 屏发虚模糊 在高 DPR 移动设备上饼图轮廓毛糙不清晰 Canvas 逻辑像素尺寸未对齐物理像素 将 canvas.width/height 乘以 devicePixelRatio 并通过 CSS 限制显示宽高
全量 100% 扇区退化 单项数据占比 100% 时侧壁断裂或无法闭合 起始弧度与终止弧度相等或差值为 2π 导致多边形退化 单独分支特判:直接按完整圆柱侧壁与完整椭圆顶面连续绘制
悬浮位移层级穿透 悬浮扇区移出后遮挡关系错乱 固定深度排序未重新根据位移后的局部 Y 坐标重算 排序键加入悬浮向量在 Y 轴的投影偏移权重 offsetY
JS饼状图扇区颜色渐变与立体效果实现
图 4 说明:系统性总结了 3D 渐变饼图技术全景,呈现了从输入数据解析、几何投影变换、光影渲染管线到边界防护与交互响应的完整架构。

六、实现总结

实现高质量的 JavaScript 3D 渐变饼状图,本质是将几何拓扑剖分与轻量光学模型在二维画布上做精确投影。通过将视角转化为仿射压缩矩阵、借助画家算法解决空间遮挡、使用下半圆区间裁剪剔除无效背向侧壁,并结合径向高光与切向线性渐变,我们以极低的代码体积和极佳的渲染性能,在原生 Canvas 环境中构建出了具有专业工业质感的可视化图表。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章