css - 更改 svg 描边动画的起点

标签 css animation svg stroke-dasharray

我有 svg 笔画动画 https://codesandbox.io/s/magical-hill-r92ong

但是是从右下位置开始的,可以从中上开始吗like on screenshot (red dot)

我尝试将描边-dashoffset设置为负值,这有助于设置起点,但描边动画不会结束

最佳答案

Shift M (起点)

移动起点实际上并不太复杂 - 前提是您使用绝对命令并且您的路径不包含任何简写命令(更多详细信息如下):

上部中心命令将是第 16n 个或第 17n 个命令:

/**
* 1st chunk - becomes 2nd 
* M will be replaced by 
* last C command end coordinates in this chunk 
*/
M 169.605 152.315
C 154.541 166.243 136.811 179.567 116.416 192.29
C 115.335 193.043 114.099 193.683 112.708 194.21
C 111.318 194.737 110.082 195 109 195
C 107.996 195 106.798 194.737 105.408 194.21
C 103.94 193.683 102.665 193.043 101.584 192.29
C 81.189 179.567 63.459 166.243 48.395 152.315
C 33.33 138.313 21.665 123.972 13.399 109.292
C 5.133 94.612 1 79.744 1 64.688
C 1 55.353 2.545 46.809 5.635 39.055
C 8.648 31.301 12.936 24.563 18.498 18.842
C 23.983 13.12 30.395 8.716 37.734 5.63
C 44.996 2.543 52.914 1 61.489 1
C 72.073 1 81.421 3.635 89.532 8.905
C 96.443 13.394 102.177 22.924 106.732 31.093
C 107.743 32.834 110.257 32.834    111.268 31.093

/**
* 2nd chunk - new M: 111.268 31.093 = previous C command end point
*/
C 115.823 22.924 121.557 13.394 128.468 8.905
C 136.579 3.635 145.927 1 156.511 1
C 165.086 1 173.004 2.543 180.266 5.63
C 187.605 8.716 194.017 13.12 199.502 18.842
C 205.064 24.563 209.352 31.301 212.365 39.055
C 215.455 46.809 217 55.353 217 64.688
C 217 79.744 212.867 94.612 204.601 109.292
C 196.335 123.972 184.67 138.313 169.605 152.315

/** append to final path data */
Z

重新排序的路径

M 111.268 31.093

C 115.823 22.924 121.557 13.394 128.468 8.905
C 136.579 3.635 145.927 1 156.511 1
C 165.086 1 173.004 2.543 180.266 5.63
C 187.605 8.716 194.017 13.12 199.502 18.842
C 205.064 24.563 209.352 31.301 212.365 39.055
C 215.455 46.809 217 55.353 217 64.688
C 217 79.744 212.867 94.612 204.601 109.292
C 196.335 123.972 184.67 138.313 169.605 152.315

C 154.541 166.243 136.811 179.567 116.416 192.29
C 115.335 193.043 114.099 193.683 112.708 194.21
C 111.318 194.737 110.082 195 109 195
C 107.996 195 106.798 194.737 105.408 194.21
C 103.94 193.683 102.665 193.043 101.584 192.29
C 81.189 179.567 63.459 166.243 48.395 152.315
C 33.33 138.313 21.665 123.972 13.399 109.292
C 5.133 94.612 1 79.744 1 64.688
C 1 55.353 2.545 46.809 5.635 39.055
C 8.648 31.301 12.936 24.563 18.498 18.842
C 23.983 13.12 30.395 8.716 37.734 5.63
C 44.996 2.543 52.914 1 61.489 1
C 72.073 1 81.421 3.635 89.532 8.905
C 96.443 13.394 102.177 22.924 106.732 31.093
C 107.743 32.834 110.257 32.834 111.268 31.093

Z

但是,如果您的路径包含相对(小写命令)或简写命令,例如 H,则此方法将不起作用。 , V (水平/垂直线),S (三次曲线),T (二次贝塞尔曲线)。

JS方法1:使用 getPathData() 移动起点(填充)

getPathData()setPathData()方法基于w3c working draft SVG 路径规范提供了标准化的解析方式 <path> d属性到命令数组以及通过 svgelement.setPathData(pathData) 再次应用操纵的数据– 所以它是“有点官方”(作为 pathSegList() 的继承者/替代品)

仍然(2023)不受主要浏览器的原生支持,您可以使用 Jarek Foksa's polyfill

let pathData = path.getPathData({normalize:true});

inputShift.setAttribute('max', pathData.length-1);

inputShift.addEventListener("input", (e) => {
  let off = +e.currentTarget.value;
  if(off>=pathData.length-1){
    off=0;
    inputShift.value=off;
  }else if(off==0 ){
    off=pathData.length-1;
    inputShift.value=off;
  }
  let pathDataShift = roundPathData(shiftSvgStartingPoint(pathData, off), 3);
  
  path.setPathData(pathDataShift);
  svgOut.value = path.getAttribute("d");
});


/**
 * shift starting point
 */
function shiftSvgStartingPoint(pathData, offset) {
  let pathDataL = pathData.length;
  let newStartIndex = 0;
  if (offset == 0) {
    return pathData;
  }
  
  //exclude Z/z (closepath) command if present
  let lastCommand = pathData[pathDataL - 1]["type"];
  let trimRight = lastCommand.toLowerCase() == "z" ? 1 : 0;
 

  // M start offset
  newStartIndex =
    offset + 1 < pathData.length - 1
      ? offset + 1
      : pathData.length - 1 - trimRight;

  // slice array to reorder
  let pathDataStart = pathData.slice(newStartIndex);
  let pathDataEnd = pathData.slice(0, newStartIndex);

  // remove original M
  pathDataEnd.shift();
  let pathDataEndL = pathDataEnd.length;

  let pathDataEndLastValues = pathDataEnd[pathDataEndL - 1]["values"];
  let pathDataEndLastXY = [
    pathDataEndLastValues[pathDataEndLastValues.length - 2],
    pathDataEndLastValues[pathDataEndLastValues.length - 1]
  ];

  //remove z(close path) from original pathdata array
  if (trimRight) {
    pathDataStart.pop();
    pathDataEnd.push({
      type: "Z",
      values: []
    });
  }
  // prepend new M command and concatenate array chunks
  pathData = [
    {
      type: "M",
      values: pathDataEndLastXY
    }
  ]
    .concat(pathDataStart)
    .concat(pathDataEnd);

  return pathData;
}

// just rounding to prevent awful floating point values
function roundPathData(pathData, decimals = -1) {
    pathData.forEach((com, c) => {
        if (decimals >= 0) {
            com.values.forEach((val, v) => {
                pathData[c].values[v] = +val.toFixed(decimals);
            });
        }
    });
    return pathData;
}
svg{
  width:20em;
  overflow:visible;
}

#path {
    marker-start: url(#markerStart);
    marker-mid: url(#markerRound);
    stroke-width: 0.33%;
}

textarea{
  display:block;
  width:100%;
  min-height:30em;
}
<p><label>Shift starting point <input type="range" id="inputShift" steps="1" min="0" max="100" value="0"></label></p>

<svg id="svgPrev" viewBox="1 1 216 194">
  <path id="path" d="M169.6 152.3c-15.1 13.9-32.8 27.3-53.2 40c-1.1 0.7-2.3 1.4-3.7 1.9s-2.6 0.8-3.7 0.8s-2.2-0.3-3.6-0.8s-2.7-1.2-3.8-1.9c-20.4-12.7-38.1-26.1-53.2-40s-26.7-28.3-35-43s-12.4-29.6-12.4-44.6c0-9.3 1.5-17.9 4.6-25.6s7.3-14.5 12.9-20.3s11.9-10.1 19.2-13.2s15.2-4.6 23.8-4.6c10.6 0 19.9 2.6 28 7.9c6.9 4.5 12.7 14 17.2 22.2c1 1.7 3.6 1.7 4.6 0c4.5-8.2 10.3-17.7 17.2-22.2c8.1-5.3 17.4-7.9 28-7.9c8.6 0 16.5 1.5 23.8 4.6s13.7 7.5 19.2 13.2s9.9 12.5 12.9 20.3s4.6 16.3 4.6 25.6c0 15-4.1 29.9-12.4 44.6s-19.9 29-35 43z"></path>
</svg>
<h3>Output</h3>
<textarea id="svgOut" ></textarea>

<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
  <defs>
    <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
      <circle cx="5" cy="5" r="10" fill="green"></circle>

      <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <circle cx="5" cy="5" r="2.5" fill="red"></circle>
      </marker>
  </defs>
</svg>

<script src="https://cdn.jsdelivr.net/npm/<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="1b6b7a6f73367f7a6f7a366b7477627d7277775b2a352b352f" rel="noreferrer noopener nofollow">[email protected]</a>/path-data-polyfill.min.js"></script>

它是如何工作的

  • 解析<path> d属性到命令数组
  • 通过 getPathData({normalize:true}) 将它们转换为绝对坐标
  • 此选项还可以转换简写形式,如 v , h , s和二次命令到三次 q , t以及 arcto 命令 a !所以这是一个相当“激进/有损”的转换。
  • 基本上只是将 pathData 拆分为数组 block 并进行排序(对前面的新 M 命令进行上述更改)

JS方法2:保留Q , A命令(也基于 getPathdata() )

在这种情况下,您需要更高级的标准化。

  • 转换为所有绝对命令坐标
  • 将速记命令标准化为其对应的速记命令,例如 s => c , t => q , v , h => l

let pathData = pathDataToLonghands(path.getPathData());
inputShift.setAttribute('max', pathData.length - 1);

inputShift.addEventListener("input", (e) => {
  let off = +e.currentTarget.value;
  if (off >= pathData.length - 1) {
    off = 0;
    inputShift.value = off;
  } else if (off == 0) {
    off = pathData.length - 1;
    inputShift.value = off;
  }
  let pathDataShift = shiftSvgStartingPoint(pathData, off);
  pathDataShift = roundPathData(pathDataShift, 3)
  path.setPathData(pathDataShift);
  svgOut.value = path.getAttribute("d");
});


/**
 * shift starting point
 */
function shiftSvgStartingPoint(pathData, offset) {
  let pathDataL = pathData.length;
  let newStartIndex = 0;
  if (offset == 0) {
    return pathData;
  }

  //exclude Z/z (closepath) command if present
  let lastCommand = pathData[pathDataL - 1]["type"];
  let trimRight = lastCommand.toLowerCase() == "z" ? 1 : 0;


  // M start offset
  newStartIndex =
    offset + 1 < pathData.length - 1 ?
    offset + 1 :
    pathData.length - 1 - trimRight;

  // slice array to reorder
  let pathDataStart = pathData.slice(newStartIndex);
  let pathDataEnd = pathData.slice(0, newStartIndex);

  // remove original M
  pathDataEnd.shift();
  let pathDataEndL = pathDataEnd.length;

  let pathDataEndLastValues = pathDataEnd[pathDataEndL - 1]["values"];
  let pathDataEndLastXY = [
    pathDataEndLastValues[pathDataEndLastValues.length - 2],
    pathDataEndLastValues[pathDataEndLastValues.length - 1]
  ];

  //remove z(close path) from original pathdata array
  if (trimRight) {
    pathDataStart.pop();
    pathDataEnd.push({
      type: "Z",
      values: []
    });
  }
  // prepend new M command and concatenate array chunks
  pathData = [{
      type: "M",
      values: pathDataEndLastXY
    }]
    .concat(pathDataStart)
    .concat(pathDataEnd);

  return pathData;
}

/**
 * decompose/convert shorthands to "longhand" commands:
 * H, V, S, T => L, L, C, Q
 * reversed method: pathDataToShorthands()
 */
function pathDataToLonghands(pathData) {
  pathData = pathDataToAbsolute(pathData);
  let pathDataLonghand = [];
  let comPrev = {
    type: "M",
    values: pathData[0].values
  };
  pathDataLonghand.push(comPrev);

  for (let i = 1; i < pathData.length; i++) {
    let com = pathData[i];
    let type = com.type;
    let values = com.values;
    let valuesL = values.length;
    let valuesPrev = comPrev.values;
    let valuesPrevL = valuesPrev.length;
    let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
    let cp1X, cp1Y, cpN1X, cpN1Y, cpN2X, cpN2Y, cp2X, cp2Y;
    let [prevX, prevY] = [
      valuesPrev[valuesPrevL - 2],
      valuesPrev[valuesPrevL - 1]
    ];
    switch (type) {
      case "H":
        comPrev = {
          type: "L",
          values: [values[0], prevY]
        };
        break;
      case "V":
        comPrev = {
          type: "L",
          values: [prevX, values[0]]
        };
        break;
      case "T":
        [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
        [prevX, prevY] = [
          valuesPrev[valuesPrevL - 2],
          valuesPrev[valuesPrevL - 1]
        ];
        // new control point
        cpN1X = prevX + (prevX - cp1X);
        cpN1Y = prevY + (prevY - cp1Y);
        comPrev = {
          type: "Q",
          values: [cpN1X, cpN1Y, x, y]
        };
        break;
      case "S":
        [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
        [cp2X, cp2Y] =
        valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
        [prevX, prevY] = [
          valuesPrev[valuesPrevL - 2],
          valuesPrev[valuesPrevL - 1]
        ];
        // new control points
        cpN1X = 2 * prevX - cp2X;
        cpN1Y = 2 * prevY - cp2Y;
        cpN2X = values[0];
        cpN2Y = values[1];
        comPrev = {
          type: "C",
          values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
        };

        break;
      default:
        comPrev = {
          type: type,
          values: values
        };
    }
    pathDataLonghand.push(comPrev);
  }
  return pathDataLonghand;
}


/**
 * This is just a port of Dmitry Baranovskiy's 
 * pathToRelative/Absolute methods used in snap.svg
 * https://github.com/adobe-webplatform/Snap.svg/
 */
function pathDataToAbsolute(pathData, decimals = -1) {
  let M = pathData[0].values;
  let x = M[0],
    y = M[1],
    mx = x,
    my = y;
  // loop through commands
  for (let i = 1; i < pathData.length; i++) {
    let cmd = pathData[i];
    let type = cmd.type;
    let typeAbs = type.toUpperCase();
    let values = cmd.values;

    if (type != typeAbs) {
      type = typeAbs;
      cmd.type = type;
      // check current command types
      switch (typeAbs) {
        case "A":
          values[5] = +(values[5] + x);
          values[6] = +(values[6] + y);
          break;

        case "V":
          values[0] = +(values[0] + y);
          break;

        case "H":
          values[0] = +(values[0] + x);
          break;

        case "M":
          mx = +values[0] + x;
          my = +values[1] + y;

        default:
          // other commands
          if (values.length) {
            for (let v = 0; v < values.length; v++) {
              // even value indices are y coordinates
              values[v] = values[v] + (v % 2 ? y : x);
            }
          }
      }
    }
    // is already absolute
    let vLen = values.length;
    switch (type) {
      case "Z":
        x = +mx;
        y = +my;
        break;
      case "H":
        x = values[0];
        break;
      case "V":
        y = values[0];
        break;
      case "M":
        mx = values[vLen - 2];
        my = values[vLen - 1];

      default:
        x = values[vLen - 2];
        y = values[vLen - 1];
    }

  }
  // round coordinates
  if (decimals >= 0) {
    pathData = roundPathData(pathData, decimals);
  }
  return pathData;
}

// just rounding to prevent awful floating point values
function roundPathData(pathData, decimals = -1) {
  pathData.forEach((com, c) => {
    if (decimals >= 0) {
      com.values.forEach((val, v) => {
        pathData[c].values[v] = +val.toFixed(decimals);
      });
    }
  });
  return pathData;
}
svg {
  width: 20em;
  overflow: visible;
}

#path {
  marker-start: url(#markerStart);
  marker-mid: url(#markerRound);
  stroke-width: 0.33%;
}

textarea {
  display: block;
  width: 100%;
  min-height: 30em;
}
<p><label>Shift starting point <input type="range" id="inputShift" steps="1" min="0" max="100" value="0"></label></p>

<svg id="svgPrev" viewBox="1 1 216 194">
  <path id="path" d="
                M 50 0         
                Q 36.4 0 24.8 6.8        
                t -18 18         
                t -6.8 25.2         
                C 0 63.8 5.6 76.3 14.65 85.35         
                s 21.55 14.65 35.35 14.65         
                A 50 50 0 0 0100 50         
                h -12.5         
                v -25         
                H 50         
                V 0        
                z "></path>
</svg>
<h3>Output</h3>
<textarea id="svgOut"></textarea>

<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
  <defs>
    <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
      <circle cx="5" cy="5" r="5" fill="green"></circle>

      <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <circle cx="5" cy="5" r="2.5" fill="red"></circle>
      </marker>
  </defs>
</svg>

<script src="https://cdn.jsdelivr.net/npm/<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="a7d7c6d3cf8ac3c6d3c68ad7c8cbdec1cecbcbe79689978993" rel="noreferrer noopener nofollow">[email protected]</a>/path-data-polyfill.min.js"></script>

您可以尝试我的codepen示例 path direction and starting point sanitizer

替代方案:笔划-dashoffset

此外,您还可以使用stroke-dashoffset如此处所述:
"How to change start point of svg line animation"

body {
  font-family: sans-serif;
  margin:1em;
}

svg {
  overflow: visible;
  height:75vmin;
  width:auto;
}

svg path {
  stroke-dashoffset: 260;
  animation: anim 3s ease-in-out forwards infinite;
}

@keyframes anim {
  0% {
    stroke-dasharray: 0 672;
  }
  100% {
    stroke-dasharray: 672 0;
  }
}
<svg width="218" height="196" viewBox="0 0 218 196" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M169.605 152.315C154.541 166.243 136.811 179.567 116.416 192.29C115.335 193.043 114.099 193.683 112.708 194.21C111.318 194.737 110.082 195 109 195C107.996 195 106.798 194.737 105.408 194.21C103.94 193.683 102.665 193.043 101.584 192.29C81.1888 179.567 63.4592 166.243 48.3948 152.315C33.3304 138.313 21.6652 123.972 13.3991 109.292C5.13304 94.6123 1 79.7443 1 64.688C1 55.3531 2.54506 46.8087 5.63519 39.0547C8.64807 31.3007 12.9356 24.5631 18.4979 18.8417C23.9828 13.1203 30.3948 8.71634 37.7339 5.6298C44.9957 2.54327 52.9142 1 61.4893 1C72.073 1 81.4206 3.63485 89.5322 8.90454C96.4432 13.3943 102.177 22.9236 106.732 31.0933C107.743 32.8343 110.257 32.8343 111.268 31.0933C115.823 22.9236 121.557 13.3943 128.468 8.90454C136.579 3.63485 145.927 1 156.511 1C165.086 1 173.004 2.54327 180.266 5.6298C187.605 8.71634 194.017 13.1203 199.502 18.8417C205.064 24.5631 209.352 31.3007 212.365 39.0547C215.455 46.8087 217 55.3531 217 64.688C217 79.7443 212.867 94.6123 204.601 109.292C196.335 123.972 184.67 138.313 169.605 152.315Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"></path>
    </svg>

关于css - 更改 svg 描边动画的起点,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/74729070/

相关文章:

javascript - 获取浏览器视口(viewport)的宽度和高度,无论内容大小如何,并且不调整到 100%?

javascript - 使用 Jquery 和 servlet 检索图像不显示图像

android - `alpha` 动画期间的奇怪阴影行为

javascript - jquery 动画和 css 动画之间的滞后

svg - 应用蒙版后奇怪的 svg 路径裁剪

iPhone - 在矢量应用程序上创建 Quartz 路径?

html - 图像蒙版不适用于 CSS - SVG

javascript - 使用变换比例时如何使图像在 div 中居中

html tailwindcss 给 li 标签添加边框

python - 无法使用ffmpeg保存matplotlib动画