如何滚动以沿着调整 SVG 大小的路径将 SVG 移动到窗口中心?

问题描述 投票:0回答:1

我按照这篇文章如何调整 SVG 大小 进行操作,并在调整 SVG 大小时成功地使红色方块始终位于路径上。然而,出现了一个新问题:向下滚动时,红色方块不会跟随位于中线(窗口中心)的位置移动。 我该如何解决它?

      let svg = document.querySelector(".svg-path");
      let mPath = document.getElementById("Path_440");
      let strokePath = document.getElementById("theFill");
      let pathIcon = document.getElementById("pathIcon");

      document.addEventListener("DOMContentLoaded", function () {
        // auto adjust viewBox
        let svgRect = svg.getBBox();
        let width = svgRect.width;
        let height = svgRect.height;
        svg.setAttribute("viewBox", `0 0 ${width} ${height}`);

        // update offset path
        defineNewOffsetPath();
      });

      function defineNewOffsetPath() {
        // retrieve the current scale from SVG transformation matrix
        let matrix = svg.getCTM();
        let scale = matrix.a;

        // parse path data
        let d = mPath.getAttribute("d");
        let pathData = parsePathData(d);

        //scale pathdata
        pathData = scalePathData(pathData, scale);

        // apply scaled pathdata as stringified d attribute value
        d = pathDataToD(pathData);

        pathIcon.style.offsetPath = `path('${d}')`;
      }

      // recalculate offset path on resize
      window.addEventListener("resize", (e) => {
        defineNewOffsetPath();
        matrix = svg.getScreenCTM();
        updateLengthLookup();
      });

      // just for illustration
      resizeObserver();
      function resizeObserver() {
        defineNewOffsetPath();
      }
      new ResizeObserver(resizeObserver).observe(scrollDiv);
      /**
       * scale path data proportional
       */
      function scalePathData(pathData, scale = 1) {
        let pathDataScaled = [];
        pathData.forEach((com, i) => {
          let { type, values } = com;
          let comT = {
            type: type,
            values: [],
          };

          switch (type.toLowerCase()) {
            // line to shorthands
            case "h":
              comT.values = [values[0] * scale]; // horizontal - x-only
              break;
            case "v":
              comT.values = [values[0] * scale]; // vertical - x-only
              break;

            // arcto
            case "a":
              comT.values = [
                values[0] * scale, // rx: scale
                values[1] * scale, // ry: scale
                values[2], // x-axis-rotation: keep it
                values[3], // largeArc: dito
                values[4], // sweep: dito
                values[5] * scale, // final x: scale
                values[6] * scale, // final y: scale
              ];
              break;

            /**
             * Other point based commands: L, C, S, Q, T
             * scale all values
             */
            default:
              if (values.length) {
                comT.values = values.map((val, i) => {
                  return val * scale;
                });
              }
          }
          pathDataScaled.push(comT);
        });
        return pathDataScaled;
      }

      /**
       * parse stringified path data used in d attribute
       * to an array of computable command data
       */
      function parsePathData(d) {
        d = d
          // remove new lines, tabs an comma with whitespace
          .replace(/[\n\r\t|,]/g, " ")
          // pre trim left and right whitespace
          .trim()
          // add space before minus sign
          .replace(/(\d)-/g, "$1 -")
          // decompose multiple adjacent decimal delimiters like 0.5.5.5 => 0.5 0.5 0.5
          .replace(/(\.)(?=(\d+\.\d+)+)(\d+)/g, "$1$3 ");

        let pathData = [];
        let cmdRegEx = /([mlcqazvhst])([^mlcqazvhst]*)/gi;
        let commands = d.match(cmdRegEx);

        // valid command value lengths
        let comLengths = {
          m: 2,
          a: 7,
          c: 6,
          h: 1,
          l: 2,
          q: 4,
          s: 4,
          t: 2,
          v: 1,
          z: 0,
        };
        commands.forEach((com) => {
          let type = com.substring(0, 1);
          let typeRel = type.toLowerCase();
          let isRel = type === typeRel;
          let chunkSize = comLengths[typeRel];

          // split values to array
          let values = com
            .substring(1, com.length)
            .trim()
            .split(" ")
            .filter(Boolean);

          /**
           * A - Arc commands
           * large arc and sweep flags
           * are boolean and can be concatenated like
           * 11 or 01
           * or be concatenated with the final on path points like
           * 1110 10 => 1 1 10 10
           */
          if (typeRel === "a" && values.length != comLengths.a) {
            let n = 0,
              arcValues = [];
            for (let i = 0; i < values.length; i++) {
              let value = values[i];

              // reset counter
              if (n >= chunkSize) {
                n = 0;
              }
              // if 3. or 4. parameter longer than 1
              if ((n === 3 || n === 4) && value.length > 1) {
                let largeArc = n === 3 ? value.substring(0, 1) : "";
                let sweep =
                  n === 3 ? value.substring(1, 2) : value.substring(0, 1);
                let finalX = n === 3 ? value.substring(2) : value.substring(1);
                let comN = [largeArc, sweep, finalX].filter(Boolean);
                arcValues.push(comN);
                n += comN.length;
              } else {
                // regular
                arcValues.push(value);
                n++;
              }
            }
            values = arcValues.flat().filter(Boolean);
          }

          // string  to number
          values = values.map(Number);

          // if string contains repeated shorthand commands - split them
          let hasMultiple = values.length > chunkSize;
          let chunk = hasMultiple ? values.slice(0, chunkSize) : values;
          let comChunks = [
            {
              type: type,
              values: chunk,
            },
          ];

          // has implicit or repeated commands – split into chunks
          if (hasMultiple) {
            let typeImplicit = typeRel === "m" ? (isRel ? "l" : "L") : type;
            for (let i = chunkSize; i < values.length; i += chunkSize) {
              let chunk = values.slice(i, i + chunkSize);
              comChunks.push({
                type: typeImplicit,
                values: chunk,
              });
            }
          }
          comChunks.forEach((com) => {
            pathData.push(com);
          });
        });

        /**
         * first M is always absolute/uppercase -
         * unless it adds relative linetos
         * (facilitates d concatenating)
         */
        pathData[0].type = "M";
        return pathData;
      }

      /**
       * serialize pathData array to
       * d attribute string
       */
      function pathDataToD(pathData, decimals = 3) {
        let d = ``;
        pathData.forEach((com) => {
          d += `${com.type}${com.values
            .map((val) => {
              return +val.toFixed(decimals);
            })
            .join(" ")}`;
        });
        return d;
      }
 
      // **Code about scroll below**


      // steps for pathlength lookup
      let precision = 1000;

      // get transform matrix to translate svg units to screen coordinates
      let matrix = svg.getScreenCTM();

      function getLengthLookup(path, precision = 100) {
        //create pathlength lookup
        let pathLength = path.getTotalLength();
        let lengthLookup = {
          yArr: [],
          lengthArr: [],
          pathLength: pathLength,
        };

        // sample point to calculate Y at pathLengths
        let step = Math.floor(pathLength / precision);

        for (let l = 0; l < pathLength; l += step) {
          let pt = SVGToScreen(matrix, path.getPointAtLength(l));
          let y = pt.y;
          lengthLookup.yArr.push(y);
          lengthLookup.lengthArr.push(l);
        }
        return lengthLookup;
      }

      const lengthLookup = getLengthLookup(mPath, precision);
      const { lengthArr, yArr, pathLength } = lengthLookup;
      const maxHeight =
        document.documentElement.scrollHeight - window.innerHeight;
  

      window.addEventListener("scroll", (e) => {
        scrollPathicon();
      });
      function scrollPathicon() {
        let scrollPosMid = getViewportMiddleY();
        midline.style.top = scrollPosMid + "px";

        // get y pos length
        let found = false;

        for (let i = 0; i < yArr.length && !found; i++) {
          // find next largest y in lookup
          let y = yArr[i];
          if (y >= scrollPosMid) {
            let length = lengthArr[i];

            // adjust length via interpolated approximation
            let yPrev = yArr[i - 1] ? yArr[i - 1] : yArr[i];
            let lengthPrev = lengthArr[i - 1] ? lengthArr[i - 1] : length;
            let ratioL = (1 / lengthArr[i]) * lengthPrev;
            let ratioY = (1 / y) * scrollPosMid;
            let ratio = Math.max(ratioL, ratioY);

            let dashLength = lengthArr[i] * ratio;

            // calculate offsetDistance
            let offsetDist = (100 / pathLength) * dashLength;
            pathIcon.style.offsetDistance = offsetDist + "%";

            // change dasharray
            strokePath.setAttribute(
              "stroke-dasharray",
              `${dashLength} ${pathLength}`
            );

            // stop loop
            found = true;
          }
        }
      }

      /**
       * Get the absolute center/middle y-coordinate
       * of the current scroll viewport
       */
      function getViewportMiddleY() {
        const viewportHeight = window.innerHeight;
        const scrollY = window.scrollY || window.pageYOffset;
        const element = document.documentElement;
        const elementOffsetTop = element.offsetTop;
        const middleY = scrollY + viewportHeight / 2 + elementOffsetTop;
        return middleY;
      }

      function SVGToScreen(matrix, pt) {
        let p = new DOMPoint(pt.x, pt.y);
        p = p.matrixTransform(matrix);
        return p;
      }
       html {
        margin: 0;
        padding: 0;
      }

      .svg-path {
        overflow: visible;
        width: 100%;
      }

      #pathIcon {
        position: absolute;
        inset: 0;
        width: 5vw;
        height: 5vw;
        offset-rotate: 0deg;
        offset-distance: 10%;
      }

      #scrollDiv {
        overflow: auto;
        margin: 10px;
        padding-bottom: 10vw;
      }
      #midline {
        display: block;
        position: absolute;
        width: 100%;
        height: 1px;
        border-top: 1px solid orange;
      }
 <div id="scrollDiv" style="position: relative">
      <svg class="svg-path" viewBox="0 0 0 0" fill="none">
        <defs>
          <path
            id="Path_440"
            d="M1293 2 s-16 74.47-96 91.5c-91.45 19.47-308.67-2.43-424.5 7-227 0-469.89 25.44-493.5 195-11 79-13.89 124.33-11 207.5s1.9 142.65 37 238c41.77 113.46 465 97.5 789.5 91 271.5 10.5 581.52-40 671.5 179 37.18 90.5 0 446.5 0 482.5 0 67.5-70 120-148 134-153 0-429.89 5-614.5 5-271 0-633.97-26.81-691.5 85-40.39 78.5-36 202.5-36 264.5 1.12 92.28-3.45 162.17 36 276 27.86 80.39 409.15 66.5 669 66.5 316 0 696.34-17 758.5 79 54.07 83.5 33.23 212.68 33.23 361.5 0 128-2 232.5-120.73 267-39.33 11.43-415 0-759 0-487.5 0-891-2-891-2"
            stroke="#020878"
            stroke-width="2"
            stroke-miterlimit="10"
            stroke-linecap="round"
            stroke-dasharray="20 20"
          />
        </defs>
        <use
          class="stroke"
          href="#Path_440"
          stroke="#ccc"
          stroke-width="10"
          stroke-dasharray="20 10"
        ></use>
        <use
          class="stroke"
          id="theFill"
          href="#Path_440"
          stroke-dasharray="925.988 9259.88"
          stroke-width="10"
          stroke="#4cacff"
        ></use>
      </svg>
      <svg id="pathIcon" fill="none">
        <rect width="100" height="100" fill="red" fill-opacity="0.5" />
      </svg>
      <div id="midline"></div>
    </div>

我认为计算SVG在视口中的长度和offsetDistance是错误的。我尝试在调整大小事件中的 DefineNewOffsetPath 之后计算 updateLengthLookup,但它不起作用。我不知道如何解决它。

javascript html animation svg scroll
1个回答
0
投票

存在很多计算问题以及过时和未定义的变量(已在评论中提到)。

matrix
中的
defineNewOffsetPath()
应分配为
svg.getScreenCTM()
,并且如果该值为
null
,则函数必须停止。为了简化计算,可以从
body
#scrollDiv
中删除边距和填充。

每次窗口大小发生变化时,

updateLengthLookup()
都应该更新 svg 点查找。最好防止事件处理程序中过多的重新计算。来自
y
SVGToScreen
值也应考虑文档的
scrollTop

scrollPathicon()
svg / div 中,必须在查找循环之前计算比率。一旦找到目标位置,
lengthArr[i]
即可用于
offsetDist
计算,无需
ratio
调整。

let svg = document.querySelector(".svg-path");
let mPath = document.getElementById("Path_440");
let strokePath = document.getElementById("theFill");
let pathIcon = document.getElementById("pathIcon");

document.addEventListener("DOMContentLoaded", function() {
  // auto adjust viewBox
  let svgRect = svg.getBBox();
  let width = svgRect.width;
  let height = svgRect.height;
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);

  // update offset path
  defineNewOffsetPath();

  updateLengthLookup();
  scrollPathicon();
});

function defineNewOffsetPath() {
  // retrieve the current scale from SVG transformation matrix
  let matrix = svg.getScreenCTM();
  if (!matrix) {
    return;
  }
  let scale = matrix.a;

  // parse path data
  let d = mPath.getAttribute("d");
  let pathData = parsePathData(d);

  //scale pathdata
  pathData = scalePathData(pathData, scale);

  // apply scaled pathdata as stringified d attribute value
  d = pathDataToD(pathData);

  pathIcon.style.offsetPath = `path('${d}')`;
}

// recalculate offset path on resize
window.addEventListener("resize", (e) => {
  defineNewOffsetPath();
  updateLengthLookup();
  scrollPathicon();
});

// just for illustration
resizeObserver();

function resizeObserver() {
  defineNewOffsetPath();
}
new ResizeObserver(resizeObserver).observe(scrollDiv);
/**
 * scale path data proportional
 */
function scalePathData(pathData, scale = 1) {
  let pathDataScaled = [];
  pathData.forEach((com, i) => {
    let {
      type,
      values
    } = com;
    let comT = {
      type: type,
      values: [],
    };

    switch (type.toLowerCase()) {
      // line to shorthands
      case "h":
        comT.values = [values[0] * scale]; // horizontal - x-only
        break;
      case "v":
        comT.values = [values[0] * scale]; // vertical - x-only
        break;

        // arcto
      case "a":
        comT.values = [
          values[0] * scale, // rx: scale
          values[1] * scale, // ry: scale
          values[2], // x-axis-rotation: keep it
          values[3], // largeArc: dito
          values[4], // sweep: dito
          values[5] * scale, // final x: scale
          values[6] * scale, // final y: scale
        ];
        break;

        /**
         * Other point based commands: L, C, S, Q, T
         * scale all values
         */
      default:
        if (values.length) {
          comT.values = values.map((val, i) => {
            return val * scale;
          });
        }
    }
    pathDataScaled.push(comT);
  });
  return pathDataScaled;
}

/**
 * parse stringified path data used in d attribute
 * to an array of computable command data
 */
function parsePathData(d) {
  d = d
    // remove new lines, tabs an comma with whitespace
    .replace(/[\n\r\t|,]/g, " ")
    // pre trim left and right whitespace
    .trim()
    // add space before minus sign
    .replace(/(\d)-/g, "$1 -")
    // decompose multiple adjacent decimal delimiters like 0.5.5.5 => 0.5 0.5 0.5
    .replace(/(\.)(?=(\d+\.\d+)+)(\d+)/g, "$1$3 ");

  let pathData = [];
  let cmdRegEx = /([mlcqazvhst])([^mlcqazvhst]*)/gi;
  let commands = d.match(cmdRegEx);

  // valid command value lengths
  let comLengths = {
    m: 2,
    a: 7,
    c: 6,
    h: 1,
    l: 2,
    q: 4,
    s: 4,
    t: 2,
    v: 1,
    z: 0,
  };
  commands.forEach((com) => {
    let type = com.substring(0, 1);
    let typeRel = type.toLowerCase();
    let isRel = type === typeRel;
    let chunkSize = comLengths[typeRel];

    // split values to array
    let values = com
      .substring(1, com.length)
      .trim()
      .split(" ")
      .filter(Boolean);

    /**
     * A - Arc commands
     * large arc and sweep flags
     * are boolean and can be concatenated like
     * 11 or 01
     * or be concatenated with the final on path points like
     * 1110 10 => 1 1 10 10
     */
    if (typeRel === "a" && values.length != comLengths.a) {
      let n = 0,
        arcValues = [];
      for (let i = 0; i < values.length; i++) {
        let value = values[i];

        // reset counter
        if (n >= chunkSize) {
          n = 0;
        }
        // if 3. or 4. parameter longer than 1
        if ((n === 3 || n === 4) && value.length > 1) {
          let largeArc = n === 3 ? value.substring(0, 1) : "";
          let sweep =
            n === 3 ? value.substring(1, 2) : value.substring(0, 1);
          let finalX = n === 3 ? value.substring(2) : value.substring(1);
          let comN = [largeArc, sweep, finalX].filter(Boolean);
          arcValues.push(comN);
          n += comN.length;
        } else {
          // regular
          arcValues.push(value);
          n++;
        }
      }
      values = arcValues.flat().filter(Boolean);
    }

    // string  to number
    values = values.map(Number);

    // if string contains repeated shorthand commands - split them
    let hasMultiple = values.length > chunkSize;
    let chunk = hasMultiple ? values.slice(0, chunkSize) : values;
    let comChunks = [{
      type: type,
      values: chunk,
    }, ];

    // has implicit or repeated commands – split into chunks
    if (hasMultiple) {
      let typeImplicit = typeRel === "m" ? (isRel ? "l" : "L") : type;
      for (let i = chunkSize; i < values.length; i += chunkSize) {
        let chunk = values.slice(i, i + chunkSize);
        comChunks.push({
          type: typeImplicit,
          values: chunk,
        });
      }
    }
    comChunks.forEach((com) => {
      pathData.push(com);
    });
  });

  /**
   * first M is always absolute/uppercase -
   * unless it adds relative linetos
   * (facilitates d concatenating)
   */
  pathData[0].type = "M";
  return pathData;
}

/**
 * serialize pathData array to
 * d attribute string
 */
function pathDataToD(pathData, decimals = 3) {
  let d = ``;
  pathData.forEach((com) => {
    d += `${com.type}${com.values
      .map((val) => {
        return +val.toFixed(decimals);
      })
      .join(" ")}`;
  });
  return d;
}

// **Code about scroll below**


function getLengthLookup(path, precision = 100) {
  //create pathlength lookup
  const pathLength = path.getTotalLength();
  const lengthLookup = {
    yArr: [],
    lengthArr: [],
    pathLength: pathLength,
  };

  // sample point to calculate Y at pathLengths
  const step = Math.floor(pathLength / precision);
  const matrix = svg.getScreenCTM();
  const scrollTop = document.documentElement.scrollTop;

  for (let l = 0; l < pathLength; l += step) {
    let pt = SVGToScreen(matrix, path.getPointAtLength(l));
    let y = pt.y + scrollTop;
    lengthLookup.yArr.push(y);
    lengthLookup.lengthArr.push(l);
  }
  return lengthLookup;
}

let lengthLookup = {};
let lengthLookupRuns = false;

function updateLengthLookup() {
  if (lengthLookupRuns) {
    return;
  }
  // steps for pathlength lookup
  let precision = 1000;
  lengthLookupRuns = true;
  lengthLookup = getLengthLookup(mPath, precision);
  lengthLookupRuns = false;
}

window.addEventListener("scroll", (e) => {
  scrollPathicon();
});

function scrollPathicon() {
  if (lengthLookupRuns) {
    return;
  }
  const {
    lengthArr,
    yArr,
    pathLength
  } = lengthLookup;
  const scrollPosMid = getViewportMiddleY();
  const ratio = yArr[yArr.length - 1] / document.documentElement.offsetHeight;
  const scrollPosScaledToY = scrollPosMid * ratio;
  midline.style.top = scrollPosMid + "px";

  for (let i = 0; i < yArr.length; i++) {
    // find next largest y in lookup
    let y = yArr[i];
    if (y >= scrollPosScaledToY) {

      let dashLength = lengthArr[i];

      let offsetDist = (100 / pathLength) * dashLength;
      pathIcon.style.offsetDistance = offsetDist + "%";

      // change dasharray
      strokePath.setAttribute(
        "stroke-dasharray",
        `${dashLength} ${pathLength}`
      );

      // stop loop
      break;
    }
  }
}

/**
 * Get the absolute center/middle y-coordinate
 * of the current scroll viewport
 */
function getViewportMiddleY() {
  const viewportHeight = window.innerHeight;
  const scrollY = window.scrollY || window.pageYOffset;
  const element = document.documentElement;
  const elementOffsetTop = element.offsetTop;
  const middleY = scrollY + viewportHeight / 2 + elementOffsetTop;
  return middleY;
}

function SVGToScreen(matrix, pt) {
  let p = new DOMPoint(pt.x, pt.y);
  p = p.matrixTransform(matrix);
  return p;
}
html,
body {
  margin: 0;
  padding: 0;
}

.svg-path {
  overflow: visible;
  width: 100%;
}

#pathIcon {
  position: absolute;
  inset: 0;
  width: 5vw;
  height: 5vw;
  offset-rotate: 0deg;
  offset-distance: 10%;
}

#scrollDiv {
  overflow: auto;
}

#midline {
  display: block;
  position: absolute;
  width: 100%;
  height: 1px;
  border-top: 1px solid orange;
}
<div id="scrollDiv" style="position: relative">
  <svg class="svg-path" viewBox="0 0 0 0" fill="none">
        <defs>
          <path
            id="Path_440"
            d="M1293 2 s-16 74.47-96 91.5c-91.45 19.47-308.67-2.43-424.5 7-227 0-469.89 25.44-493.5 195-11 79-13.89 124.33-11 207.5s1.9 142.65 37 238c41.77 113.46 465 97.5 789.5 91 271.5 10.5 581.52-40 671.5 179 37.18 90.5 0 446.5 0 482.5 0 67.5-70 120-148 134-153 0-429.89 5-614.5 5-271 0-633.97-26.81-691.5 85-40.39 78.5-36 202.5-36 264.5 1.12 92.28-3.45 162.17 36 276 27.86 80.39 409.15 66.5 669 66.5 316 0 696.34-17 758.5 79 54.07 83.5 33.23 212.68 33.23 361.5 0 128-2 232.5-120.73 267-39.33 11.43-415 0-759 0-487.5 0-891-2-891-2"
            stroke="#020878"
            stroke-width="2"
            stroke-miterlimit="10"
            stroke-linecap="round"
            stroke-dasharray="20 20"
          />
        </defs>
        <use
          class="stroke"
          href="#Path_440"
          stroke="#ccc"
          stroke-width="10"
          stroke-dasharray="20 10"
        ></use>
        <use
          class="stroke"
          id="theFill"
          href="#Path_440"
          stroke-dasharray="925.988 9259.88"
          stroke-width="10"
          stroke="#4cacff"
        ></use>
      </svg>
  <svg id="pathIcon" fill="none">
        <rect width="100" height="100" fill="red" fill-opacity="0.5" />
      </svg>
  <div id="midline"></div>
</div>

© www.soinside.com 2019 - 2024. All rights reserved.