简体   繁体   中英

Creating svg paths with javascript(shape morphing)

So I have this class which is used for shape morphing:

class ShapeOverlays {
  constructor(elm) {
    this.elm = elm;
    this.path = elm.querySelectorAll('path');
    this.numPoints = 18;
    this.duration = 600;
    this.delayPointsArray = [];
    this.delayPointsMax = 300;
    this.delayPerPath = 100;
    this.timeStart = Date.now();
    this.isOpened = false;
    this.isAnimating = false;
  }
    toggle() {
    this.isAnimating = true;
    const range = 4 * Math.random() + 6;
    for (var i = 0; i < this.numPoints; i++) {
      const radian = i / (this.numPoints - 1) * Math.PI;
      this.delayPointsArray[i] = (Math.sin(-radian) + Math.sin(-radian * range) + 2) / 4 * this.delayPointsMax;
    }
    if (this.isOpened === false) {
      this.open();
    } else {
      this.close();
    }
  }
  open() {
    this.isOpened = true;
    this.elm.classList.add('is-opened');
    this.timeStart = Date.now();
    this.renderLoop();
  }
  close() {
    this.isOpened = false;
    this.elm.classList.remove('is-opened');
    this.timeStart = Date.now();
    this.renderLoop();
  }
  updatePath(time) {
    const points = [];
    for (var i = 0; i < this.numPoints + 1; i++) {
      points[i] = ease.cubicInOut(Math.min(Math.max(time - this.delayPointsArray[i], 0) / this.duration, 1)) * 100
    }

    let str = '';
    str += (this.isOpened) ? `M 0 0 V ${points[0]} ` : `M 0 ${points[0]} `;
    for (var i = 0; i < this.numPoints - 1; i++) {
      const p = (i + 1) / (this.numPoints - 1) * 100;
      const cp = p - (1 / (this.numPoints - 1) * 100) / 2;
      str += `C ${cp} ${points[i]} ${cp} ${points[i + 1]} ${p} ${points[i + 1]} `;
    }
    str += (this.isOpened) ? `V 0 H 0` : `V 100 H 0`;
    return str;
  }
  render() {
    if (this.isOpened) {
      for (var i = 0; i < this.path.length; i++) {
        this.path[i].setAttribute('d', this.updatePath(Date.now() - (this.timeStart + this.delayPerPath * i)));
      }
    } else {
      for (var i = 0; i < this.path.length; i++) {
        this.path[i].setAttribute('d', this.updatePath(Date.now() - (this.timeStart + this.delayPerPath * (this.path.length - i - 1))));
      }
    }
  }
  renderLoop() {
    this.render();
    if (Date.now() - this.timeStart < this.duration + this.delayPerPath * (this.path.length - 1) + this.delayPointsMax) {
      requestAnimationFrame(() => {
        this.renderLoop();
      });
    }
    else {
      this.isAnimating = false;
    }
  }
}

(function() {
  const elmHamburger = document.querySelector('.hamburger');
  const gNavItems = document.querySelectorAll('.global-menu__item');
  const elmOverlay = document.querySelector('.shape-overlays');
  const overlay = new ShapeOverlays(elmOverlay);

  elmHamburger.addEventListener('click', () => {
    if (overlay.isAnimating) {
      return false;
    }
    overlay.toggle();
    if (overlay.isOpened === true) {
      elmHamburger.classList.add('is-opened-navi');
      for (var i = 0; i < gNavItems.length; i++) {
        gNavItems[i].classList.add('is-opened');
      }
    } else {
      elmHamburger.classList.remove('is-opened-navi');
      for (var i = 0; i < gNavItems.length; i++) {
        gNavItems[i].classList.remove('is-opened');
      }
    }
  });
}());

Can some one please explain this code? I don't really get how the paths are created using time,how the points are placed and how could I modify it.What is range used for? Why are trigonometral functions used for the delayPointsArray?

Basically it's this part that I don't get:

updatePath(time) {
        const points = [];
        for (var i = 0; i < this.numPoints + 1; i++) {
          points[i] = ease.cubicInOut(Math.min(Math.max(time - this.delayPointsArray[i], 0) / this.duration, 1)) * 100
        }

        let str = '';
        str += (this.isOpened) ? `M 0 0 V ${points[0]} ` : `M 0 ${points[0]} `;
        for (var i = 0; i < this.numPoints - 1; i++) {
          const p = (i + 1) / (this.numPoints - 1) * 100;
          const cp = p - (1 / (this.numPoints - 1) * 100) / 2;
          str += `C ${cp} ${points[i]} ${cp} ${points[i + 1]} ${p} ${points[i + 1]} `;
        }
        str += (this.isOpened) ? `V 0 H 0` : `V 100 H 0`;
        return str;
      }
      render() {
        if (this.isOpened) {
          for (var i = 0; i < this.path.length; i++) {
            this.path[i].setAttribute('d', this.updatePath(Date.now() - (this.timeStart + this.delayPerPath * i)));
          }
        } else {
          for (var i = 0; i < this.path.length; i++) {
            this.path[i].setAttribute('d', this.updatePath(Date.now() - (this.timeStart + this.delayPerPath * (this.path.length - i - 1))));
          }
        }
      }

Why is time being used? What is the purpose of this:

points[i] = ease.cubicInOut(Math.min(Math.max(time - this.delayPointsArray[i], 0) / this.duration, 1)) * 100

If you look at how updatePath() is being called, it's like this:

this.updatePath(Date.now() - (this.timeStart + this.delayPerPath * i))

So the time value passed in is the difference between the current time, and the start time of the path we are working with.

So what then is the line of code you are interested in, doing?

points[i] = ease.cubicInOut(Math.min(Math.max(time - this.delayPointsArray[i], 0) / this.duration, 1)) * 100

I'm going to ignore delayPointsArray . It is modifying the start time slightly based on angle. Without seeing the full demo, I'm not sure of the reason for that.

The purpose of this line of code is to calculate how far through the current path's animation we are. The result is in the form of a coordinate value from 0 to 100.

It's doing a lot in that one line of code. So let's break down the individual steps.

  1. Firstly, we are clamping the elapsed time to minimum of 0.

     Math.max(time, 0) 

    In other words, anything before the animation start time becomes zero.

  2. Then we divide by the animation's duration.

     Math.max(time, 0) / duration 

    This will result in a value from 0, representing the start of the animation, to 1, representing the end of the animation. However, the value might also be greater than 1 if the elapsed time is after the end of the animation. Hence the next step.

  3. Now clamp this value to a maximum of 1.

     Math.min( Math.max(time, 0) / duration, 1) 

    We now have a value >= 0 and <= 1 whichdescribes where in the course of the animation, the path is supposed to be. 0 if we should be at the animations start position. 1 if we should be at the animations end position. And somewhere in between if the animation is in progress.

  4. However this value is strictly linear, corresponding with the progression of time. And usually linear movement is not what you want. It is unnatural. Objects accelarate when the start moving and decelerate when the come to a stop. That will be what the easeInOut() function will be doing. If you are not familiar with easing curves, take a look at the diagram below.

    缓入缓出时序曲线

    Source: Google: The Basics of Easing

    So we pass in a linear time value from 0..1 (horizontal axis). It will return a modified value that takes into account acceleration and deceleration.

  5. The final step is to multiply by 100, to convert to a final coordinate value (0..100).

Hope this helps.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM