SVG画图的一个小技巧

383 阅读2分钟

背景介绍

在日常开发中我们不可避免的要使用svg画图来解决如进图条的问题,最近在开发中遇到了一个仪表盘如下图所示:

Snipaste_2023-08-15_15-48-15.png

看到这张图的时候我的第一想法是使用echarts的仪表盘来解决,但很遗憾echarts的仪表盘貌似并没有让指针从中间开始转动的配置,而且开始和结束的时候两个小点也不好处理,所以我们就得用SVG画图来解决。

开始实现

画好背景与画布

首先我们问ui小姐姐要来一张图片作为背景,然后将画布定位在背景图圆弧四周

<div className={styles.panelContain}>
    <div className={styles.svgContainer}>
    </div>
 </div>
.panelContain {
    width: 10.125rem;
    height: 5.4375rem;
    padding: 0.75rem 1rem 0.3125rem 0.375rem;
    background-image: url("@/assets/resource-aggregation/pannel-bg.webp");
    background-repeat: no-repeat;
    background-size: 100% 100%;
  }
  .svgContainer {
    position: relative; /* 设置相对定位,使内部的绝对定位生效 */
    width: 100%; /* 让容器占满父元素的宽度 */
    height: 0; /* 设置高度为0,由内容决定 */
    padding-bottom: 100%; /* 设置 padding-bottom 为百分比,与宽度相同 */
  }

效果如图:

image.png

画个圆吧

背景好了,我们也该让圆弧展示出来了,这里我们使用SVG中的circle来实现

<div className={styles.panelContain}>
        <div className={styles.svgContainer}>
          <svg
            width="100%"
            height="100%"
            xmlns="http://www.w3.org/2000/svg"
            preserveAspectRatio="xMidYMid meet"
            className={styles.svgContent}
          >
            <defs>
              <linearGradient id="circleGradient" cx="50%" cy="50%" r="50%">
                <stop offset={`0`} stopColor="#28C8FF" />
                <stop offset="100%" stopColor="#2758FF" />
              </linearGradient>
            </defs>
            <circle
              cx="50%"
              cy="50%"
              r={4.0625 * fontSize}
              stroke="url(#circleGradient)"
              strokeWidth="0.5rem"
              fill="none"
              strokeLinecap="round"
            />
          </svg>
        </div>
      </div>

image.png

园变半圆

这里我们需要用到strokeDasharraystrokeDashoffset两个属性,一个表示圆环的长度,一个表示圆环的偏移量,我们我们将circle修改一下

<circle
  cx="50%"
  cy="50%"
  r={4.0625 * fontSize}
  stroke="url(#circleGradient)"
  strokeWidth="0.5rem"
  fill="none"
  strokeDasharray={2 * Math.PI * 4.0625 * fontSize}
  strokeDashoffset={-2 * Math.PI * 4.0625 * fontSize * 0.5}
  strokeLinecap="round"
  ref={pathRef}
/>

image.png

我们发现变成了半圆

进度不从0开始

以图中百分之七十五至百分之百为例:

  1. 这里我们需要先算出开始位置到结束位置的长度
  2. 算出这段长度在整个园中的比例,用0.5减去这个比例在加上0.5,则得到圆环需要偏移的量
  3. strokeDashoffset后面的0.5替换为第2步得到的偏移量

image.png

加上开头结尾小圆点

开头结尾的小圆点,同样使用circle在0的位置画图,然后旋转即可 完整代码如下

      const [radio, setRadio] = useState(1);
      const [rotate, setRotate] = useState(0);
      const [minRotate, setMinRotate] = useState(0);
      useEffect(() => {
        // 这个值是整数的话则逆时针转,负数顺时针转,
        // 设置值,0%-120%为0度到162度,大于120%统一设置为170度
        // 模拟数据75%-100%
        const baseRadio = 162 / 120;
        const minNum = baseRadio * 75;
        const maxNum = baseRadio * 100;
        setRadio(0.5 + (0.5 - (maxNum - minNum) / 360));
        setRotate(180 - maxNum);
        setMinRotate(180 - minNum);
      }, []);
      <div className={styles.panelContain}>
        <div className={styles.svgContainer}>
          <svg
            width="100%"
            height="100%"
            xmlns="http://www.w3.org/2000/svg"
            preserveAspectRatio="xMidYMid meet"
            className={styles.svgContent}
            ref={svgRef}
            style={{ transform: `rotate(-${rotate}deg)` }}
          >
            <defs>
              <linearGradient id="circleGradient" cx="50%" cy="50%" r="50%">
                <stop offset={`${radio * 100}%`} stopColor="#28C8FF" />
                <stop offset="100%" stopColor="#2758FF" />
              </linearGradient>
            </defs>
            <circle
              cx="50%"
              cy="50%"
              r={4.0625 * fontSize}
              stroke="url(#circleGradient)"
              strokeWidth="0.5rem"
              fill="none"
              strokeDasharray={2 * Math.PI * 4.0625 * fontSize}
              strokeDashoffset={-2 * Math.PI * 4.0625 * fontSize * radio}
              strokeLinecap="round"
              ref={pathRef}
            />
          </svg>
          <svg
            width="100%"
            height="100%"
            style={{ transform: `rotate(-${minRotate}deg)` }}
            xmlns="http://www.w3.org/2000/svg"
            preserveAspectRatio="xMidYMid meet"
            className={styles.svgContent}
            ref={svgRef}
          >
            <circle
              cx={`calc(100% - ${(5 * fontSize) / 16}px)`}
              cy="50%"
              r={0.2 * fontSize}
              fill="#fff"
            />
          </svg>
          <svg
            width="100%"
            height="100%"
            style={{ transform: `rotate(-${rotate}deg)` }}
            xmlns="http://www.w3.org/2000/svg"
            preserveAspectRatio="xMidYMid meet"
            className={styles.svgContent}
            ref={svgRef}
          >
            <circle
              cx={`calc(100% - ${(5 * fontSize) / 16}px)`}
              cy="50%"
              r={0.2 * fontSize}
              fill="#fff"
            />
          </svg>
        </div>
      </div>
.panelContain {
    width: 10.125rem;
    height: 5.4375rem;
    padding: 0.75rem 1rem 0.3125rem 0.375rem;
    background-image: url("@/assets/resource-aggregation/pannel-bg.webp");
    background-repeat: no-repeat;
    background-size: 100% 100%;
  }
  .svgContainer {
    position: relative; /* 设置相对定位,使内部的绝对定位生效 */
    width: 100%; /* 让容器占满父元素的宽度 */
    height: 0; /* 设置高度为0,由内容决定 */
    padding-bottom: 100%; /* 设置 padding-bottom 为百分比,与宽度相同 */
  }

  .svgContent {
    position: absolute; /* 设置绝对定位,使 SVG 元素填充容器 */
    width: 100%; /* 使 SVG 元素宽度占满容器 */
    height: 100%; /* 使 SVG 元素高度占满容器 */
  }

image.png