最近很巧的了解了svg矢量动画的制作,并上手试了一下,用d3和Snap结合可以很好地做出一些很不错的简单动画效果
效果图
首先我们先了解一些基本概念
1.为什么不能直接在网页端绘制图案?
大家都知道前端一个很重要的概念叫‘元素’,我们的很多操作本质上就是在操作dom元素,即文档里面的某个html元素,但是html没有单纯的形状元素(比如圆形、三角形等等)但是我们可以通过div去构造,比如我们设置宽高相等就构建了一个正方形,设置样式圆角50%就构建了一个圆形,但这些都是一些旁门左道,我们想要画复杂图案,有两个思路;
(1)栅格图案 :说人话就是一张图片,栅格化图案都是由一个个像素点构成,但是栅格图案因为是像素构成的无法随意改变形状,更不要说动画了
可以把这些小方格看出像素
(2)矢量图案:用点与线直接构成的图案,因为是用代码定义出来的,所以可扩展性和可变性很强,但是复杂度不高
矢量图是由线段和点组成的
如果我们想做前端的动画的话,那肯定是用矢量图案好一点,现在的很多自定义图案的组件库(比如d3,Snap,Canvas等等都是基于矢量图案的)
html也有矢量图的标签<svg>
2.d3是啥
官方文档D3js: Data-Driven Documents
简而言之是一个直接操纵dom文档的js库,但是在本篇中的主角并不是他,我们只是用d3创建一个可控的<svg>元素
3.Snap是啥
官方文档Snap.svg - Home
简而言之是一个svg矢量图的库,我们可以用一种很直观的方式创建一些简单的矢量图案,更重要的是snap自带动画api
废话不多说我们直接实操,这里以破碎动画效果为例,点击鼠标后,在当前位置出现一些很细小的矢量图案,图案向着四周发射,
那么思路如下:
1.用d3创建一个svg元素,当做画布
2.用snap包裹画布
3.用snap在某一个点创建几个随机大小的矩形
4.使这些矩形执行动画,位移到随机坐标(随机坐标在一定范围内,防止破碎太远)
5.使这些矩形执行动画,透明度慢慢变成0(逐渐隐藏)
6.移除这些矩形
有了思路我们好办很多,首先
首先我们在我们的页面创建一个dom元素
这个是一个空的div,不占位置,随便写即可,但是要定义id来方便我们之后找到
之后我们通过d3定义一个svg对象
var svg = d3.select("#container").append("svg")
.attr("width", 1500)
.attr("height", 3000)
.attr("id", 'svg');
我们选择了刚刚id为container的dom对象,增加了一个svg对象,长1500像素,宽3000像素,id为svg
打开调试工具我们可以看到container这个dom下面新增了一个我们刚刚定义的svg
在这里,d3的使命就结束了,d3已经为我们创建了一个1500*3000的画布
之后我们初始化一个snap对象s
//初始化一个snap对象
s = Snap('#svg');
这一行就是把我们id为svg的svg对象包装成snap对象(原理见下图)
那么对于我们创建的s,我们可以通过snap命令去创建一些snap矢量图案
我们通过rect命令,在某一点[centerPointX,centerPointY]创建大小随机的7个矩形
//首先在中心点附近随机生成7个大小各异的矩形
var rect1 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
var rect2 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
var rect3 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
var rect4 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
var rect5 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
var rect6 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
var rect7 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
因为snap对象必须是实例,这里用了遍历的写法,也方便大家学习
centerPointX和centerPointY可以你自己定义,只要不超过我们的画布就行,是根据画布像素来的,比如[100,200]
之后对每一个对象填充黑色
rect1.attr({
fill: "rgba(0,0,0,1)"
});
rect2.attr({
fill: "rgba(0,0,0,1)"
});
rect3.attr({
fill: "rgba(0,0,0,1)"
});
rect4.attr({
fill: "rgba(0,0,0,1)"
});
rect5.attr({
fill: "rgba(0,0,0,1)"
});
rect6.attr({
fill: "rgba(0,0,0,1)"
});
rect7.attr({
fill: "rgba(0,0,0,1)"
});
.attr是snap的一个方法,可以为一个snap对象修改属性
这个时候我们的7个黑色矩形只是停留在坐标,我们需要让每个矩形执行一个动画,动画的变化是变化坐标到25像素外,50像素内的某个随机范围
rect1.animate({ x: Math.random() > 0.5 ? (centerPointX + 25 + Math.round(Math.random() * 24)) : (centerPointX - 25 - Math.round(Math.random() * 24)), y: Math.random() > 0.5 ? (centerPointY + 25 + Math.round(Math.random() * 24)) : (centerPointY - 25 - Math.round(Math.random() * 24)) }, 100, mina.easeinout, function () {})
//剩下6个都和上面一样
这里用到了许多Math.random()方法来生成随机数,主要是为了使矩形的新xy坐标在50*50和25*25的回字形范围内生成,产生一种‘破碎的’的效果
动画效果还不错,但是感觉有点生硬了,我们在回调里面再加一个动画,动画的变化是把不透明度慢慢变成0
rect1.animate({ x: Math.random() > 0.5 ? (centerPointX + 25 + Math.round(Math.random() * 24)) : (centerPointX - 25 - Math.round(Math.random() * 24)), y: Math.random() > 0.5 ? (centerPointY + 25 + Math.round(Math.random() * 24)) : (centerPointY - 25 - Math.round(Math.random() * 24)) }, 100, mina.easeinout, function () {
//逐渐消失
rect1.animate({ "opacity": 0 }, 800 + Math.round(Math.random() * 400), mina.easeinout, function () {})
})
//剩下6个都和上面一样
最后,我们再在不透明度变成0的动画回调里面,在300毫秒后把我们的矩形给删除
rect1.animate({ x: Math.random() > 0.5 ? (centerPointX + 25 + Math.round(Math.random() * 24)) : (centerPointX - 25 - Math.round(Math.random() * 24)), y: Math.random() > 0.5 ? (centerPointY + 25 + Math.round(Math.random() * 24)) : (centerPointY - 25 - Math.round(Math.random() * 24)) }, 100, mina.easeinout, function () {
//逐渐消失
rect1.animate({ "opacity": 0 }, 800 + Math.round(Math.random() * 400), mina.easeinout, function () {
//销毁元素
setTimeout(() => {
rect1.remove()
}, 200 + Math.round(Math.random() * 200))
})
})
//剩下6个都和上面一样
现在破碎效果就做好了,我们甚至可以可以通过指定坐标的加减,来使得我们的破碎效果是指定某个方向
这就是通过d3+Snap制作简单前端动画的步骤,了解原理后,我们可以制作一些更复杂的动画
下面附上源码
//破碎动画
export const render = () => {
//定义动画的坐标
let centerPointX = 100
let centerPointY = 200
//首先在中心点附近随机生成7个大小各异的矩形
var rect1 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
var rect2 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
var rect3 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
var rect4 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
var rect5 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
var rect6 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
var rect7 = s.rect(centerPointX, centerPointY, 1 + Math.round(Math.random() * 4), 1 + Math.round(Math.random() * 4), 0, 0);
//对于每个都填充黑色
rect1.attr({
fill: "rgba(0,0,0,1)"
});
rect2.attr({
fill: "rgba(0,0,0,1)"
});
rect3.attr({
fill: "rgba(0,0,0,1)"
});
rect4.attr({
fill: "rgba(0,0,0,1)"
});
rect5.attr({
fill: "rgba(0,0,0,1)"
});
rect6.attr({
fill: "rgba(0,0,0,1)"
});
rect7.attr({
fill: "rgba(0,0,0,1)"
});
//之后向四周发散,最后淡化消失
//xSpray和ySpray使所有矩形的喷射方向趋于一致
let xSpray = Math.random()
let ySpray = Math.random()
//每一个矩形都执行动画
rect1.animate({ x: xSpray > 0.5 ? (centerPointX + 25 + Math.round(Math.random() * 24)) : (centerPointX - 25 - Math.round(Math.random() * 24)), y: ySpray > 0.5 ? (centerPointY + 25 + Math.round(Math.random() * 24)) : (centerPointY - 25 - Math.round(Math.random() * 24)) }, 100, mina.easeinout, function () {
//逐渐消失
rect1.animate({ "opacity": 0 }, 800 + Math.round(Math.random() * 400), mina.easeinout, function () {
//销毁元素
setTimeout(() => {
rect1.remove()
}, 200 + Math.round(Math.random() * 200))
})
})
rect2.animate({ x: xSpray > 0.5 ? (centerPointX + 25 + Math.round(Math.random() * 24)) : (centerPointX - 25 - Math.round(Math.random() * 24)), y: ySpray > 0.5 ? (centerPointY + 25 + Math.round(Math.random() * 24)) : (centerPointY - 25 - Math.round(Math.random() * 24)) }, 100, mina.easeinout, function () {
//逐渐消失
rect2.animate({ "opacity": 0 }, 800 + Math.round(Math.random() * 400), mina.easeinout, function () {
//销毁元素
setTimeout(() => {
rect2.remove()
}, 200 + Math.round(Math.random() * 200))
})
})
rect3.animate({ x: xSpray > 0.5 ? (centerPointX + 25 + Math.round(Math.random() * 24)) : (centerPointX - 25 - Math.round(Math.random() * 24)), y: ySpray > 0.5 ? (centerPointY + 25 + Math.round(Math.random() * 24)) : (centerPointY - 25 - Math.round(Math.random() * 24)) }, 100, mina.easeinout, function () {
//逐渐消失
rect3.animate({ "opacity": 0 }, 800 + Math.round(Math.random() * 400), mina.easeinout, function () {
//销毁元素
setTimeout(() => {
rect3.remove()
}, 200 + Math.round(Math.random() * 200))
})
})
rect4.animate({ x: xSpray > 0.5 ? (centerPointX + 25 + Math.round(Math.random() * 24)) : (centerPointX - 25 - Math.round(Math.random() * 24)), y: ySpray > 0.5 ? (centerPointY + 25 + Math.round(Math.random() * 24)) : (centerPointY - 25 - Math.round(Math.random() * 24)) }, 100, mina.easeinout, function () {
//逐渐消失
rect4.animate({ "opacity": 0 }, 800 + Math.round(Math.random() * 400), mina.easeinout, function () {
//销毁元素
setTimeout(() => {
rect4.remove()
}, 200 + Math.round(Math.random() * 200))
})
})
rect5.animate({ x: xSpray > 0.5 ? (centerPointX + 25 + Math.round(Math.random() * 24)) : (centerPointX - 25 - Math.round(Math.random() * 24)), y: ySpray > 0.5 ? (centerPointY + 25 + Math.round(Math.random() * 24)) : (centerPointY - 25 - Math.round(Math.random() * 24)) }, 100, mina.easeinout, function () {
//逐渐消失
rect5.animate({ "opacity": 0 }, 800 + Math.round(Math.random() * 400), mina.easeinout, function () {
//销毁元素
setTimeout(() => {
rect5.remove()
}, 200 + Math.round(Math.random() * 200))
})
})
rect6.animate({ x: xSpray > 0.5 ? (centerPointX + 25 + Math.round(Math.random() * 24)) : (centerPointX - 25 - Math.round(Math.random() * 24)), y: ySpray > 0.5 ? (centerPointY + 25 + Math.round(Math.random() * 24)) : (centerPointY - 25 - Math.round(Math.random() * 24)) }, 100, mina.easeinout, function () {
//逐渐消失
rect6.animate({ "opacity": 0 }, 800 + Math.round(Math.random() * 400), mina.easeinout, function () {
//销毁元素
setTimeout(() => {
rect6.remove()
}, 200 + Math.round(Math.random() * 200))
})
})
rect7.animate({ x: xSpray > 0.5 ? (centerPointX + 25 + Math.round(Math.random() * 24)) : (centerPointX - 25 - Math.round(Math.random() * 24)), y: ySpray > 0.5 ? (centerPointY + 25 + Math.round(Math.random() * 24)) : (centerPointY - 25 - Math.round(Math.random() * 24)) }, 100, mina.easeinout, function () {
//逐渐消失
rect7.animate({ "opacity": 0 }, 800 + Math.round(Math.random() * 400), mina.easeinout, function () {
//销毁元素
setTimeout(() => {
rect7.remove()
}, 200 + Math.round(Math.random() * 200))
})
})
}
附录 SVG Attribute reference - SVG: Scalable Vector Graphics | MDN 这个网站可以查询svg的属性