【SVG】用可缩放矢量图形绘制一个仪表盘

1,407 阅读4分钟

可缩放矢量图形,即SVG,是W3C XML的分支语言之一,用于标记可缩放的矢量图形。目前SVG在Firefox、Opera、Webkit浏览器、IE等浏览器中已经部分实现。

整体效果如下图所示:

动态.gif

一、创建一个SVG,并设置宽420px、高316px。

 <svg
     width="420px"
     height="316px"
     version="1.1"
     xmlns="http://www.w3.org/2000/svg"
  >
  </svg>

注意:用transform="translate(210, 158)"将svg中所有元素的中心点设为数学坐标轴原点,以便于后面进行坐标点计算。

二、圆

使用circle元素绘制一个圆形。
r 圆的半径
cx 圆心的x位置
cy 圆心的y位置

首先画一个圆心坐标为(0, 0),圆半径为r = 124的正圆。
使用fill="#F2FDFF"设置填充色。

<circle fill="#F2FDFF" cx="0" cy="0" r="124" transform="translate(210, 158)"/>

圆.png

三、分值进度条

弧形命令A rx ry x-axis-rotation large-arc-flag sweep-flag x y是创建SVG曲线的命令。
前两个参数rx ry:分别是x轴半径和y轴半径;
参数x-axis-rotation:弧形的x轴旋转角度;
参数large-arc-flag:表示弧线角度(0表示小角度弧,1表示大角度弧);
参数sweep-flag:表示弧线的方向(0表示逆时针,1表示顺时针);
最后两个参数是弧形的终点。

使用弧形命令画进度条关键之处在于计算出弧形的终点位置:
设得分为m,总分max = 100,则角度Q = 180 - m * 180 / max
设分值进度条端点的横坐标值为x,纵坐标值为y,则可知:
y/x=tanQ
x*x + y*y= r*r = 124*124
由上可分别求得x,y的值:

// 定义计算坐标值函数 Q角度 r半径
function calculateXy(Q, r) {
  if (Q === 0) {
    return { x: r, y: 0 };
  }
  if (Q === 180) {
    return { x: -r, y: 0 };
  }
  let tanQ = Math.tan((2 * Math.PI * Q) / 360), // 倾斜角度的正切值
    y = Math.abs(Math.sqrt(1 / (tanQ * tanQ + 1)) * r * tanQ), // y始终大于0,所以取绝对值。
    x = y / tanQ;
  return { x, y };
}

假设得分m=30,通过计算即可得:
Q = 126
tanQ = -1.3763819204711738
y = 100.31810730249349
x = -72.88537128426665

/* <defs>标签在svg中用来存放样式段落;*/
/*<linearGradient>为可通过id属性被其它元素引用的线性渐变色节点*/
<defs>
    <linearGradient id="strokeGradient">
        <stop offset="0%" stop-color="#8FEAE9" />
        <stop offset="100%" stop-color="#26A7DE" />
    </linearGradient>
</defs>

/* web规范中,往上偏离元素中心点,top值<0,所以弧形曲线的终点坐标为(x, -y)*/
/* M-124 0表示弧形曲线的起点坐标 */
/* stroke-width="18"设置曲线的宽度 */
<path
    :d="`M-124 0 A 124 124, 0, 0, 1, ${x} ${-y}`"
    stroke-width="18"
    stroke="url(#strokeGradient)"
    stroke-linecap="round"
    fill="transparent"
    transform="translate(210, 158)"
/>

分值进度条.png

四、分值进度条的端点

<circle stroke="#00D3CA" fill="#fff" :cx="x" :cy="-y" r="10" transform="translate(210, 158)"/>

分值进度端点.png

五、表盘刻度

刻度线就是在两个点之间画直线。首先是“Move to”命令,M,前面出现过,它需要两个参数,分别是需要移动到的点的x轴和y轴的坐标。画直线常用的是“Line to”命令,LL需要两个参数,分别是一个点的x轴和y轴坐标,L命令将会在当前位置和新位置(L前面画笔所在的点)之间画一条线段。

共计要画25个刻度线,从右到左第n个刻度线的倾斜角度为n*(180°/24)

// 计算路径
let path = "";
for (let i = 0; i < 25; i++) {
  let deg = i * (180 / 24),
    // 刻度线外端:弧形半径为100
    x1 = calculateXy(deg, 100).x,
    y1 = calculateXy(deg, 100).y,
    // 刻度线内端:长线弧形半径为88,长线弧形半径为80
    r = i % 4 === 0 ? 80 : 88,
    x2 = calculateXy(deg, r).x,
    y2 = calculateXy(deg, r).y;
  path += `M${x1} ${-y1} L${x2} ${-y2} `;
}
this.path = path;
<path transform="translate(210, 158)" :d="path" stroke="#98E1DE" />

刻度.png

具体数值如下图:
刻度线.png

六、表盘中心的不规则三角形

用三个二次贝塞尔曲线Q画三角形。Q x1 y1, x y (or q dx1 dy1, dx dy)需要两组参数,控制点坐标和终点坐标。

// n值用来调节三角形大小
<path
  transform="translate(210, 158)"
  :d="`M${n*-32.5} ${n*-17.03} Q${n*0} ${n*-64.12} ${n*32.5} ${n*-17.03} Q${n*59} ${n*34.06} ${n*0} ${n*34.06}  Q${n*-59} ${n*34.06} ${n*-32.5} ${n*-17.03}`"
  fill="rgba(0,211,202,0.23)"
/>

三角形1.png

// n值用来调节三角形大小
<path
  transform="translate(210, 158)"
  :d="`M${-n*-32.5} ${-n*-17.03} Q${-n*0} ${-n*-64.12} ${-n*32.5} ${-n*-17.03} Q${-n*59} ${-n*34.06} ${-n*0} ${-n*34.06}  Q${-n*-59} ${-n*34.06} ${-n*-32.5} ${-n*-17.03}`"
  fill="rgba(0,147,211,0.19)"
/>

三角形2.png

七、添加分数

 <text
      x="0"
      y="10"
      font-size="28"
      text-anchor="middle"
      fill="white"
      transform="translate(210, 158)"
    >
      {{m}}
 </text>

静态.png

八、动画效果

// 完整代码
<svg
    width="420px"
    height="316px"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
  >
    <defs>
      <linearGradient id="strokeGradient">
        <stop offset="0%" stop-color="#8FEAE9" />
        <stop offset="100%" stop-color="#26A7DE" />
      </linearGradient>
    </defs>
    <circle
      transform="translate(210, 158)"
      fill="#F2FDFF"
      cx="0"
      cy="0"
      r="124"
    />
    <path
      transform="translate(210, 158)"
      :d="`M-124 0 A 124 124, 0, 0, 1, ${x} ${-y}`"
      stroke-width="18"
      stroke="url(#strokeGradient)"
      stroke-linecap="round"
      fill="transparent"
    />
    <circle
      transform="translate(210, 158)"
      stroke="#00D3CA"
      fill="#fff"
      :cx="x"
      :cy="-y"
      r="10"
    />
    <path transform="translate(210, 158)" :d="path" stroke="#98E1DE" />
    <path
      transform="translate(210, 158)"
      :d="`M${n*-32.5} ${n*-17.03} Q${n*0} ${n*-64.12} ${n*32.5} ${n*-17.03} Q${n*59} ${n*34.06} ${n*0} ${n*34.06}  Q${n*-59} ${n*34.06} ${n*-32.5} ${n*-17.03}`"
      fill="rgba(0,211,202,0.23)"
    />
    <path
      transform="translate(210, 158)"
      :d="`M${-n*-32.5} ${-n*-17.03} Q${-n*0} ${-n*-64.12} ${-n*32.5} ${-n*-17.03} Q${-n*59} ${-n*34.06} ${-n*0} ${-n*34.06}  Q${-n*-59} ${-n*34.06} ${-n*-32.5} ${-n*-17.03}`"
      fill="rgba(0,147,211,0.19)"
    />
    <text
      x="0"
      y="10"
      transform="translate(210, 158)"
      :font-size="('' + totalScore).indexOf('-9000') > -1 ? 20 : 28"
      text-anchor="middle"
      fill="white"
    >
      {{m}}
    </text>
</svg>

export default {
  data() {
    return {
      n: 1.5, // 不规则三角形放大比例
      m: 30, // 得分
      path: "", // 表盘刻度线路径
      x: -124, // 得分进度条起点x
      y: 0 // 得分进度条起点y
    };
  },
  created() {
    let max = 100, // 总分
      r = 124, // 半径
      angle = 180 - (this.m * 180) / max, // 角度
      Q = 180; // 初始角度
      
    // 添加动效
    this.timer = window.setInterval((_) => {
      if (Q < angle) {
        window.clearInterval(this.timer);
      }
      Q -= 1;
      this.x = calculateXy(Q, r).x;
      this.y = calculateXy(Q, r).y;
    }, 10);

    let path = "";
    for (let i = 0; i < 25; i++) {
      let deg = i * (180 / 24),
        // 刻度线外端:弧形半径为100
        x1 = calculateXy(deg, 100).x,
        y1 = calculateXy(deg, 100).y,
        // 刻度线内端:长线弧形半径为88,长线弧形半径为80
        r = i % 4 === 0 ? 80 : 88,
        x2 = calculateXy(deg, r).x,
        y2 = calculateXy(deg, r).y;
      path += `M${x1} ${-y1} L${x2} ${-y2} `;
    }
    this.path = path;

    function calculateXy(Q, r) {
      if (Q === 0) {
        return { x: r, y: 0 };
      }
      if (Q === 180) {
        return { x: -r, y: 0 };
      }
      let tanQ = Math.tan((2 * Math.PI * Q) / 360), // 倾斜角度的正切值
        y = Math.abs(Math.sqrt(1 / (tanQ * tanQ + 1)) * r * tanQ), // y始终大于0,所以取绝对值。
        x = y / tanQ;
      return { x, y };
    }
  }
};

SVG教程 developer.mozilla.org/zh-CN/docs/…