使用 javascript(形状变形)创建 svg 路径
Creating svg paths with javascript(shape morphing)
所以我有这个 class 用于形状变形:
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');
}
}
});
}());
有人可以解释一下这段代码吗?我真的不明白路径是如何使用时间创建的,这些点是如何放置的,我该如何修改 it.What 的范围?为什么 delayPointsArray 使用三角函数?
基本上是这部分我没听懂:
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))));
}
}
}
为什么要用时间?这样做的目的是什么:
points[i] = ease.cubicInOut(Math.min(Math.max(time - this.delayPointsArray[i], 0) / this.duration, 1)) * 100
如果你看看 updatePath()
是如何被调用的,它是这样的:
this.updatePath(Date.now() - (this.timeStart + this.delayPerPath * i))
因此传入的 time
值是当前时间与我们正在处理的路径的开始时间之间的差异。
那么您感兴趣的代码行在做什么?
points[i] = ease.cubicInOut(Math.min(Math.max(time - this.delayPointsArray[i], 0) / this.duration, 1)) * 100
我要忽略 delayPointsArray
。它根据角度稍微修改开始时间。没有看到完整的演示,我不确定原因。
这行代码的目的是计算当前路径的动画距离。结果是从0到100的坐标值的形式。
它在那一行代码中做了很多事情。那么让我们分解一下各个步骤。
首先,我们将经过的 time
限制为最小值 0。
Math.max(time, 0)
换句话说,动画开始时间之前的任何内容都变为零。
然后我们除以动画的持续时间。
Math.max(time, 0) / duration
这将产生一个从 0(代表动画开始)到 1(代表动画结束)的值。但是,如果经过的时间在动画结束之后,该值也可能大于 1。因此下一步。
现在将此值限制为最大值 1。
Math.min( Math.max(time, 0) / duration, 1)
我们现在有一个 >= 0 且 <= 1 的值,它描述了动画过程中路径应该位于的位置。 0 如果我们应该在动画开始位置。 1 如果我们应该在动画结束位置。如果动画正在进行,则介于两者之间。
但是这个值是严格线性的,对应于时间的进程。通常线性运动不是你想要的。这是不自然的。物体在开始移动时加速,在停止时减速。这就是 easeInOut()
函数要做的事情。如果您不熟悉缓动曲线,请看下图。
来源:Google: The Basics of Easing
所以我们传入一个从0..1(横轴)开始的线性时间值。它将return一个考虑了加速和减速的修改值。
最后一步乘以100,转换为最终坐标值(0..100)。
希望这对您有所帮助。
所以我有这个 class 用于形状变形:
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');
}
}
});
}());
有人可以解释一下这段代码吗?我真的不明白路径是如何使用时间创建的,这些点是如何放置的,我该如何修改 it.What 的范围?为什么 delayPointsArray 使用三角函数?
基本上是这部分我没听懂:
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))));
}
}
}
为什么要用时间?这样做的目的是什么:
points[i] = ease.cubicInOut(Math.min(Math.max(time - this.delayPointsArray[i], 0) / this.duration, 1)) * 100
如果你看看 updatePath()
是如何被调用的,它是这样的:
this.updatePath(Date.now() - (this.timeStart + this.delayPerPath * i))
因此传入的 time
值是当前时间与我们正在处理的路径的开始时间之间的差异。
那么您感兴趣的代码行在做什么?
points[i] = ease.cubicInOut(Math.min(Math.max(time - this.delayPointsArray[i], 0) / this.duration, 1)) * 100
我要忽略 delayPointsArray
。它根据角度稍微修改开始时间。没有看到完整的演示,我不确定原因。
这行代码的目的是计算当前路径的动画距离。结果是从0到100的坐标值的形式。
它在那一行代码中做了很多事情。那么让我们分解一下各个步骤。
首先,我们将经过的
time
限制为最小值 0。Math.max(time, 0)
换句话说,动画开始时间之前的任何内容都变为零。
然后我们除以动画的持续时间。
Math.max(time, 0) / duration
这将产生一个从 0(代表动画开始)到 1(代表动画结束)的值。但是,如果经过的时间在动画结束之后,该值也可能大于 1。因此下一步。
现在将此值限制为最大值 1。
Math.min( Math.max(time, 0) / duration, 1)
我们现在有一个 >= 0 且 <= 1 的值,它描述了动画过程中路径应该位于的位置。 0 如果我们应该在动画开始位置。 1 如果我们应该在动画结束位置。如果动画正在进行,则介于两者之间。
但是这个值是严格线性的,对应于时间的进程。通常线性运动不是你想要的。这是不自然的。物体在开始移动时加速,在停止时减速。这就是
easeInOut()
函数要做的事情。如果您不熟悉缓动曲线,请看下图。来源:Google: The Basics of Easing
所以我们传入一个从0..1(横轴)开始的线性时间值。它将return一个考虑了加速和减速的修改值。
最后一步乘以100,转换为最终坐标值(0..100)。
希望这对您有所帮助。