使用svg画一个可以动的时钟

发表于:2023-07-08 18:02技术,svg热度:53喜欢:0

原理

  1. 预先定一个svg,当做模板,设置可替换插槽
  2. 服务端读取svg文件,当做字符串
  3. 获取当前时间,转换成时钟的初始角度,以及终止角度,根据时分秒针不同,动画的持续时间也不同

模板核心代码

<line id="hourHand" x1="50" y1="50" x2="50" y2="24" hourHandPlace>
      <animateTransform xlink:href="#hourHand" attributeName="transform" attributeType="XML" type="rotate"
        from="hourHandTransFrom" to="hourHandTransTo" dur="43200s" repeatCount="indefinite" />
    </line>
    <line id="minHand" x1="50" y1="50" x2="50" y2="20" minHandPlace>
      <animateTransform xlink:href="#minHand" attributeName="transform" attributeType="XML" type="rotate"
        from="minHandTransFrom" to="minHandTransTo" dur="3600s" repeatCount="indefinite" />
    </line>
    <line id="secHand" x1="50" y1="50" x2="50" y2="16" secHandPlace>
      <animateTransform xlink:href="#secHand" attributeName="transform" attributeType="XML" type="rotate"
        from="secHandTransFrom" to="secHandTransTo" dur="60s" repeatCount="indefinite" />
    </line>

其中 hourHandPlace hourHandTransFrom hourHandTransTo 是字符串模板,分别表示时钟时针的tansform变换初始角度、
动画初始值,动画的结束值
通过服务端模板替换后,转换为

<line id="hourHand" x1="50" y1="50" x2="50" y2="24" transform="rotate(188,50,50)">
      <animateTransform xlink:href="#hourHand" attributeName="transform" attributeType="XML" type="rotate"
        from="188 50 50" to="548 50 50" dur="43200s" repeatCount="indefinite" />
    </line>

分钟和秒钟同理

服务端核心代码

    let svg = readFileSync(resolve('public', 'clock.svg'), 'utf-8');
    const now = new Date(); //当前时间
    const min = now.getMinutes(); //分钟
    const sec = now.getSeconds(); //秒
    const hour = (now.getHours() % 12) + min / 60; //转换成可以在时钟上显示的时间

    const hourAngle = hour * 30; //每30°表示一个小时
    const minAngle = min * 6; //每6°表示一分钟
    const secAngle = sec * 6; //每6°表示一秒

    const setTransform = (str, angle) => {
      const attr = `transform="rotate(${angle},50,50)"`;
      svg = svg.replace(str, attr);
    };
    [
      ['hourHandPlace', hourAngle],
      ['minHandPlace', minAngle],
      ['secHandPlace', secAngle],
    ].forEach((ele) => {
      setTransform(ele[0], ele[1]);
    });

    [
      ['hourHandTrans', hourAngle],
      ['minHandTrans', minAngle],
      ['secHandTrans', secAngle],
    ].forEach((ele: [string, number]) => {
      const from = ele[1] + ' 50 50';
      const to = ele[1] + 360 + ' 50 50';
      const fromKey = ele[0] + 'From';
      const toKey = ele[0] + 'To';
      svg = svg.replace(fromKey, from);
      svg = svg.replace(toKey, to);
    });
    return svg;

完整代码

模板代码

<svg id="clock" viewBox="0 0 100 100" width="500" height="500" role="img" xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink">

  <style>
    #clock {
      stroke: black;
      stroke-linecap: round;
      fill: #eef;
    }

    #face {
      stroke-width: 3px;
    }

    #ticks {
      stroke-width: 2;
    }

    #hourHand {
      stroke-width: 5px;
    }

    #minHand {
      stroke-width: 3px;
    }

    #secHand {
      stroke-width: 1px;
    }

    #numbers {
      font-family: sans-serif;
      font-size: 7pt;
      font-weight: bold;
      text-anchor: middle;
      stroke: none;
      fill: black;
    }
  </style>
  <defs>
    <filter id=" shadow" x="-50%" y="-50%" width="200%" height="200%">
      <feGaussianBlur in="sourceAlpha" stDeviation="1" result="blur" />
      <feOffset in="blur" dx="1" dy="1" result="shadow" />
      <feMerge>
        <feMergeMode in="SourceGraphic" />
        <feMergeMode in="shadow" />
      </feMerge>
    </filter>
  </defs>
  <circle id="face" cx="50" cy="50" r="45" />
  <g id="ticks">
    <line x1="50" y1="5.000" x2="50.00" y2="10.00" />
    <line x1="72.50" y1="11.03" x2="70.00" y2="15.36" />
    <line x1="88.97" y1="27.50" x2="84.64" y2="30.00" />
    <line x1="95.00" y1="50.00" x2="90.00" y2="50.00" />
    <line x1="88.97" y1="72.50" x2="84.64" y2="70.00" />
    <line x1="72.50" y1="88.97" x2="70.00" y2="84.64" />
    <line x1="50.00" y1="95.00" x2="50.00" y2="90.00" />
    <line x1="27.50" y1="88.97" x2="30.00" y2="84.64" />
    <line x1="11.03" y1="72.50" x2="15.36" y2="70.00" />
    <line x1="5.000" y1="50.00" x2="10.00" y2="50.00" />
    <line x1="11.03" y1="27.50" x2="15.36" y2="30.00" />
    <line x1="27.50" y1="11.03" x2="30.00" y2="15.36" />
  </g>
  <g id="numbers">
    <text x="50" y="18">12</text><text x="85" y="53">3</text>
    <text x="50" y="88">6</text><text x="15" y="53">9</text>
  </g>
  <g id="hands">
    <line id="hourHand" x1="50" y1="50" x2="50" y2="24" hourHandPlace>
      <animateTransform xlink:href="#hourHand" attributeName="transform" attributeType="XML" type="rotate"
        from="hourHandTransFrom" to="hourHandTransTo" dur="43200s" repeatCount="indefinite" />
    </line>
    <line id="minHand" x1="50" y1="50" x2="50" y2="20" minHandPlace>
      <animateTransform xlink:href="#minHand" attributeName="transform" attributeType="XML" type="rotate"
        from="minHandTransFrom" to="minHandTransTo" dur="3600s" repeatCount="indefinite" />
    </line>
    <line id="secHand" x1="50" y1="50" x2="50" y2="16" secHandPlace>
      <animateTransform xlink:href="#secHand" attributeName="transform" attributeType="XML" type="rotate"
        from="secHandTransFrom" to="secHandTransTo" dur="60s" repeatCount="indefinite" />
    </line>
  </g>
</svg>

服务端代码

export class ClockController {
  @Get('clock.svg')
  @Header('Content-Type', 'image/svg+xml')
  svgClock() {
    //获取表示时钟和分针的svg元素
    let svg = readFileSync(resolve('public', 'clock.svg'), 'utf-8');
    const now = new Date(); //当前时间
    const min = now.getMinutes(); //分钟
    const sec = now.getSeconds(); //秒
    const hour = (now.getHours() % 12) + min / 60; //转换成可以在时钟上显示的时间

    const hourAngle = hour * 30; //每30°表示一个小时
    const minAngle = min * 6; //每6°表示一分钟
    const secAngle = sec * 6; //每6°表示一秒

    const setTransform = (str, angle) => {
      const attr = `transform="rotate(${angle},50,50)"`;
      svg = svg.replace(str, attr);
    };
    [
      ['hourHandPlace', hourAngle],
      ['minHandPlace', minAngle],
      ['secHandPlace', secAngle],
    ].forEach((ele) => {
      setTransform(ele[0], ele[1]);
    });

    [
      ['hourHandTrans', hourAngle],
      ['minHandTrans', minAngle],
      ['secHandTrans', secAngle],
    ].forEach((ele: [string, number]) => {
      const from = ele[1] + ' 50 50';
      const to = ele[1] + 360 + ' 50 50';
      const fromKey = ele[0] + 'From';
      const toKey = ele[0] + 'To';
      svg = svg.replace(fromKey, from);
      svg = svg.replace(toKey, to);
    });
    return svg;
  }
}

注意事项

  1. 作为接口,返回的contentType需要设置成image/svg+xml
  2. 某些网站会将外链的svg缓存下来,这个时候服务端的代码就失效了,时钟的开始时间即是添加缓存的时间