最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to animate element along svg path on scroll? - Stack Overflow

programmeradmin2浏览0评论

I have an issue: I have to change element position&angle on scroll event. Like my webpage is a long road with background - and I need to animate car movements along this path during the scroll.

I good explanation is here: / - a perfect solution, that meets my requirements. But Unfortunately it's extremely outdated.

Maybe somebody has an up to date solution-library? Or code-ideas, how to animate element via svg-path with page scrolling?

Also i tried .html - but it's not something that I need, because my path is difficult. Not just a couple of circles.

I have an issue: I have to change element position&angle on scroll event. Like my webpage is a long road with background - and I need to animate car movements along this path during the scroll.

I good explanation is here: http://prinzhorn.github.io/skrollr-path/ - a perfect solution, that meets my requirements. But Unfortunately it's extremely outdated.

Maybe somebody has an up to date solution-library? Or code-ideas, how to animate element via svg-path with page scrolling?

Also i tried http://scrollmagic.io/examples/expert/bezier_path_animation.html - but it's not something that I need, because my path is difficult. Not just a couple of circles.

Share Improve this question asked Jun 18, 2018 at 13:18 byCoderbyCoder 9,18427 gold badges120 silver badges259 bronze badges 2
  • What does "outdated" mean? If it works, what does it matter if it is four years old? – Paul LeBeau Commented Jun 18, 2018 at 13:57
  • @PaulLeBeau it doesn't work with android 4.4, MacOS etc. It makes a huge problem. – byCoder Commented Jun 18, 2018 at 13:58
Add a ment  | 

2 Answers 2

Reset to default 11

Here is some vanilla Javascript that moves a "car" along a path according to how much the page has scrolled.

It should work in all (most) browsers. The part that you may need to tweak is how we get the page height (document.documentElement.scrollHeight). You may need to use different methods depending on the browsers you want to support.

function positionCar()
{
  var  scrollY = window.scrollY || window.pageYOffset;
  var  maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
  var  path = document.getElementById("path1");
  // Calculate distance along the path the car should be for the current scroll amount
  var  pathLen = path.getTotalLength();
  var  dist = pathLen * scrollY / maxScrollY;
  var  pos = path.getPointAtLength(dist);
  // Calculate position a little ahead of the car (or behind if we are at the end), so we can calculate car angle
  if (dist + 1 <= pathLen) {
    var  posAhead = path.getPointAtLength(dist + 1);
    var  angle = Math.atan2(posAhead.y - pos.y, posAhead.x - pos.x);
  } else {
    var  posBehind = path.getPointAtLength(dist - 1);
    var  angle = Math.atan2(pos.y - posBehind.y, pos.x - posBehind.x);
  }
  // Position the car at "pos" totated by "angle"
  var  car = document.getElementById("car");
  car.setAttribute("transform", "translate(" + pos.x + "," + pos.y + ") rotate(" + rad2deg(angle) + ")");
}

function rad2deg(rad) {
  return 180 * rad / Math.PI;
}

// Reposition car whenever there is a scroll event
window.addEventListener("scroll", positionCar);

// Position the car initially
positionCar();
body {
  min-height: 3000px;
}

svg {
  position: fixed;
}
<svg width="500" height="500"
     viewBox="0 0 672.474 933.78125">
  <g transform="translate(-54.340447,-64.21875)" id="layer1">
   <path d="m 60.609153,64.432994 c 0,0 -34.345187,72.730986 64.649767,101.015256 98.99494,28.28427 321.2285,-62.62946 321.2285,-62.62946 0,0 131.31984,-52.527932 181.82746,16.16244 50.50763,68.69037 82.04198,196.41856 44.44671,284.86302 -30.25843,71.18422 -74.75128,129.29952 -189.90867,133.34013 -115.15739,4.04061 -72.73099,-153.54318 -72.73099,-153.54318 0,0 42.42641,-129.29953 135.36044,-119.198 92.93404,10.10152 -14.14213,-129.29953 -141.42135,-94.95434 -127.27922,34.34518 -183.84777,80.8122 -206.07112,121.2183 -22.22336,40.40611 -42.06243,226.23742 -26.26397,305.06607 8.77013,43.75982 58.20627,196.1403 171.72594,270.72088 73.8225,48.50019 181.82745,2.02031 181.82745,2.02031 0,0 94.95434,-12.12183 78.7919,-155.56349 -16.16244,-143.44166 -111.68403,-138.77778 -139.9683,-138.77778 -28.28427,0 83.39976,-156.18677 83.39976,-156.18677 0,0 127.27922,-189.90867 107.07617,16.16245 C 634.3758,640.21994 864.69058,888.71747 591.94939,941.2454 319.2082,993.77334 -16.162441,539.20469 153.54319,997.81395"
    id="path1"
    style="fill:none;stroke:#ff0000;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>


    <path id="car" d="M-15,-10 L15,0 L -15,10 z" fill="yellow" stroke="red" stroke-width="7.06"/>
  </g>
</svg>

I adapted Paul LeBau's answer for my purposes in TypeScript and figured I'd leave it here in case it's helpful for anyone.

NOTE: It's broken as hell when running the snippet in Stack Overflow. No clue why when it functions perfectly in VSCode. First thing I'd remended is full screening the snippet if you can't see the SVG. Then actually try copy/pasting this into your own project before making any claims about there being an issue with the posted code.

Notable additions from Paul's:

  1. A clickToScroll function allowing you to click anywhere on the svg path and have it scroll accordingly (does not work at all in the SO snippet window)
  2. The car (rider) will face towards the direction it's moving

Here's the Typescript version separately as SO doesn't support TS :(

interface PathRider {
    ride: () => void;
    clickToScroll: (e: MouseEvent) => void;
    onClick: (e: MouseEvent, callback: (pt: DOMPoint) => void) => void;
};
    
const usePathRider = (
    rider: SVGPathElement,
    path: SVGPathElement,
    rideOnInit = true
): PathRider => {
    const maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
    const pathLen = path.getTotalLength();
    
    //====================
    /* Helper Functions */
    
    const radToDeg = (rad: number) => (180 * rad) / Math.PI;
    
    const distance = (a: DOMPoint, b: DOMPoint) =>
        Math.sqrt(Math.pow(a.y - b.y, 2) + Math.pow(a.x - b.x, 2));
    
    //=========================
    /* Click-based Functions */
    
    const step = 0.5; // how granularly it should check for the point on the path closest to where the user clicks. the lower the value the less performant the operation is
    let currLen = -step;
    const pointArr: DOMPoint[] = [];
    while ((currLen += step) <= pathLen)
        pointArr.push(path.getPointAtLength(currLen));

    const onClick = (e: MouseEvent, callback: (pt: DOMPoint) => void) => {
        let pt = new DOMPoint(e.clientX, e.clientY);
        callback(pt.matrixTransform(path.getScreenCTM().inverse()));
    };
    
    const getLengthAtPoint = (pt: DOMPoint) => {
        let bestGuessIdx = 0;
        let bestGuessDist = Number.MAX_VALUE;
        let guessDist: number;
    
        pointArr.forEach((point, idx) => {
            if ((guessDist = distance(pt, point)) < bestGuessDist) {
                bestGuessDist = guessDist;
                bestGuessIdx = idx;
            }
        });
    
        return bestGuessIdx * step;
    };
    
    const getScrollPosFromLength = (len: number) => (len * maxScrollY) / pathLen;
    
    const clickToScroll = (e: MouseEvent) => {
        onClick(e, (point) => {
            const lengthAtPoint = getLengthAtPoint(point);
            const scrollPos = getScrollPosFromLength(lengthAtPoint);
    
            window.scrollTo({
                top: scrollPos,
                behavior: 'smooth',
            });
        });
    };

    //==========================
    /* Scroll-based functions */
    
    let lastDist: number; // for determining direction
    const ride = () => {
        const scrollY = window.scrollY || window.pageYOffset;
        const dist = (pathLen * scrollY) / maxScrollY;
        const pos = path.getPointAtLength(dist);
        let angle: number;
    
        // calculate position a little ahead of the rider (or behind if we are at the end),
        //  so we can calculate the rider angle
        const dir = lastDist < dist; // true=right
        if (dir ? dist + 1 <= pathLen : dist - 1 >= 0) {
            const nextPos = path.getPointAtLength(dist + (dir ? 1 : -1));
            angle = Math.atan2(nextPos.y - pos.y, nextPos.x - pos.x);
        } else {
            const nextPos = path.getPointAtLength(dist + (dir ? -1 : 1));
            angle = Math.atan2(pos.y - nextPos.y, pos.x - nextPos.x);
        }
    
        lastDist = dist;
    
        rider.setAttribute(
            'transform',
            `translate(${pos.x}, ${pos.y}) rotate(${radToDeg(angle)})`
        );
    };
    
    if (rideOnInit) ride();
    
    return {
        ride,
        clickToScroll,
        onClick,
    };
};

Snippets for running in SO

const usePathRider = (
  rider,
  path,
  rideOnInit = true
) => {
  const maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
  const pathLen = path.getTotalLength();
  const step = 0.5;

  let currLen = -step;
  const pointArr = [];
  while ((currLen += step) <= pathLen)
    pointArr.push(path.getPointAtLength(currLen));

  //====================
  /* Helper Functions */

  const radToDeg = (rad) => (180 * rad) / Math.PI;

  const distance = (a, b) =>
    Math.sqrt(Math.pow(a.y - b.y, 2) + Math.pow(a.x - b.x, 2));

  //============
  /* Closures */

  const onClick = (e, callback) => {
    let pt = new DOMPoint(e.clientX, e.clientY);
    callback(pt.matrixTransform(path.getScreenCTM().inverse()));
  };

  const getLengthAtPoint = (pt) => {
    let bestGuessIdx = 0;
    let bestGuessDist = Number.MAX_VALUE;
    let guessDist;

    pointArr.forEach((point, idx) => {
      if ((guessDist = distance(pt, point)) < bestGuessDist) {
        bestGuessDist = guessDist;
        bestGuessIdx = idx;
      }
    });

    return bestGuessIdx * step;
  };

  const getScrollPosFromLength = (len) => (len * maxScrollY) / pathLen;

  const clickToScroll = (e) => {
    onClick(e, (point) => {
      const lengthAtPoint = getLengthAtPoint(point);
      const scrollPos = getScrollPosFromLength(lengthAtPoint);

      window.scrollTo({
        top: scrollPos,
        behavior: 'smooth',
      });
    });
  };

  let lastDist;
  const ride = () => {
    const scrollY = window.scrollY || window.pageYOffset;
    const dist = (pathLen * scrollY) / maxScrollY;
    const pos = path.getPointAtLength(dist);
    let angle;

    // calculate position a little ahead of the rider (or behind if we are at the end),
    //  so we can calculate the rider angle
    const dir = lastDist < dist; // true=right
    if (dir ? dist + 1 <= pathLen : dist - 1 >= 0) {
      const nextPos = path.getPointAtLength(dist + (dir ? 1 : -1));
      angle = Math.atan2(nextPos.y - pos.y, nextPos.x - pos.x);
    } else {
      const nextPos = path.getPointAtLength(dist + (dir ? -1 : 1));
      angle = Math.atan2(pos.y - nextPos.y, pos.x - nextPos.x);
    }

    lastDist = dist;

    rider.setAttribute(
      'transform',
      `translate(${pos.x}, ${pos.y}) rotate(${radToDeg(angle)})`
    );
  };

  if (rideOnInit) ride();

  return {
    ride,
    clickToScroll,
    onClick,
  };
};


/* VANILLA JS USAGE */

const svgRider = document.getElementById('rider');
const svgPath = document.getElementById('path');

const pathRider = usePathRider(svgRider, svgPath);

// Reposition car whenever there is a scroll event
window.addEventListener("scroll", pathRider.ride);
body {
  min-height: 3000px;
}

svg {
  position: fixed;
}
<svg onclick="pathRider.clickToScroll" xmlns="http://www.w3/2000/svg" xmlns:xlink="http://www.w3/1999/xlink" viewBox="0 0 300 285" shape-rendering="geometricPrecision" text-rendering="geometricPrecision"><path
            id="path"
            d="M22.061031,163.581791c.750375-2.251126,2.251125-16.508254,26.263131-20.26013s20.26013-8.254128,42.02101,2.251125q21.76088,10.505253-12.006004,18.009005-33.016508.750374,0-18.009005t52.526263,4.427213q6.003001,19.584792,19.134567,14.332166t16.133067-14.332166q32.266133-12.081041,45.772886,0t47.273637,0"
            transform="translate(.000002 0.000001)"
            fill="none"
            stroke="#3f5787"
            stroke-width="0.6"
            stroke-dasharray="3"
        />
        <path
            id="rider"
            d="M-2,-2 L3,0 L -2,2 z"
            stroke="red"
            stroke-width="0.6"
        /></svg
    >

发布评论

评论列表(0)

  1. 暂无评论