初衷
情人节快到了,本人一直在外地,没能回家,和女友也无法dating,于是想给她一点特别的小惊喜。不知怎么想的,写个H5吧,女友在这方面见识得不多,应该会喜欢吧。
调了几个元素:雪花(最后并没有具象成雪花状,用了白色半透明圆点来替代)、文字粒子化。
这个创意很早以前就有了,13年HTML5刚风行的时候,就有google的大神写了一个粒子化的神作,后来也不断有人模仿。
原理
利用canvas 的
imageData
这个对象,我们可以取到当前canvas 里面具有rgba信息的点。有着这个,我们就可以操控已有的粒子去填充那些点,这样我们不但可以粒子化文字,图片、符号等等,只要是能画上canvas 的,都可以用这种方法粒子化。
实现思路
首先我们初始化一堆粒子,并让它们在canvas里面以不同的速度不同的角度运动。 变量定义:
data: function () {
return {
context: "HAPPY Valentine's DAY", // 文字内容
can: null, // canvas
cxt: null, // canvas context
width: window.innerWidth, // 屏幕宽度
height: window.innerHeight, // 屏幕高度
particles: [], //在背景中运动的粒子
particlesToBeMoved: [], //被选中去填充文字的粒子
points: [], //imageData中选出来的电
particleNum: 1500, //可用的粒子数,可以自定义
intervalId: 0, // setinterval id,方便取消
power: 1 // 倍率
}
},
复制代码
功能函数:
1、initBackground
是用来初始化particles
给每个粒子都随机化赋予了坐标、弧度、透明度等等,这里的moveDirec 是它之后运动的方向 2、draw
函数是用来绘制particles
中的粒子 3、moveRestParticles
实现按照moveDirect进行移动的功能。我们还做了判断,如果遇到墙壁,则反弹。
initBackground: function () {
this.can = document.getElementById('canvas')
this.cxt = this .can.getContext('2d')
for (let i = 0; i < this.particleNum; i++) {
this.particles.push({
position: {x: this.getRandom(this.width), y: this.getRandom(this.height)},
moveDirec: {x: Math.random() > 0.5 ? Math.random() * 5 : -Math.random() * 5, y: Math.random() > 0.5 ? Math.random() * 5 : -Math.random() * 5},
size: this.getRandom(1) * 0.5,
opacity: this.getRandom(1)
})
}
},
draw: function () {
this.cxt.clearRect(0, 0, this.can.width, this.can.height)
this.particles.concat(this.particlesToBeMoved).forEach(p => {
this.cxt.fillStyle = 'rgba(255, 255, 255, ' + p.opacity + ')'
this.cxt.beginPath()
this.cxt.arc(p.position.x, p.position.y, p.size * 10, 0, 2 * Math.PI, true)
this.cxt.fill()
})
},
moveRestParticles: function () {
this.particles.forEach(p => {
p.position.x += p.moveDirec.x
p.position.y += p.moveDirec.y
if (p.position.x > this.width || p.position.x <= 0) {
p.moveDirec.x = -p.moveDirec.x
}
if (p.position.y > this.height || p.position.y <= 0) {
p.moveDirec.y = -p.moveDirec.y
}
})
this.draw()
}
复制代码
我们只要用 setInterval(this.moveRestParticles, 10)
就能让粒子动起来了。
第一步就完成了,下面要完成文字像素提取,并且移动粒子到像素点。
makeWords
是将文字显示在canvas 上 getPoint
是获取刚在画在canvas 上的文字的像素信息,也就是开头提到的 imageData
我们把符合条件的点(point) 放在数组里。这里的筛选方法是不完全的,也就是,我们并没有把全部点都筛选出来,而是筛选了一部分,因为如果全部筛选的话,会使得粒子数量庞大,在视觉上显得是很凌乱。如果你想的话,可以吧 x+=4 y+=4
修改成 x+=1 y+=1
makeWords: function (word, cxt) {
cxt.font = '80px Adele bold'
cxt.fillStyle = 'rgba(100, 100, 100, 256)'
cxt.fillText(word, this.can.width / 10, this.can.height / 4)
cxt.save()
},
getPoint: function (can) {
let cxt = can.getContext('2d')
let imgData = cxt.getImageData(0, 0, can.width, can.height)
let points = []
for (let x = 0; x < imgData.width; x += 4) {
for (let y = 0; y < imgData.height; y += 4) {
let i = (y * imgData.width + x) * 4
if (imgData.data[i] <= 128 && imgData.data[i + 1] <= 128 && imgData.data[i + 2] <= 128 && imgData.data[i + 3] >= 128) {
let point = new Point(x - 3, y - 3, cxt)
points.push(point)
}
}
}
return points
}
复制代码
showWords
是入口函数,用了Promise
是方便后面控制多个语句的显示。
makeAnitation
是移动像素点,我们先打乱了粒子,让画面看上去更自然。后面的移动函数就很简单了,一看就能懂。
showWords: function () {
return new Promise((resolve, reject) => {
this.makeWords(this.context, this.cxt)
this.points = this.getPoint(this.can)
this.cxt.clearRect(0, 0, this.can.width, this.can.height)
if (this.points.length > this.particleNum) {
console.log(this.points.length)
console.log('超出数量')
resolve()
} else {
this.makeAnimation()
setTimeout(resolve, 10000)
}
})
},
makeAnimation: function () {
this.shuffleParticle()
this.getParticlesToBeMoved()
this.judgeTheDirect()
this.intervalId = setInterval(this.moveParticles, 10)
},
clearAnimation: function () {
clearInterval(this.intervalId)
this.particles.push(...this.particlesToBeMoved)
this.particlesToBeMoved = []
this.intervalId = setInterval(this.moveRestParticles, 10)
},
shuffleParticle: function () {
this.particles = _.shuffle(this.particles)
},
getParticlesToBeMoved: function () {
for (let i = 0; i < this.points.length; i++) {
this.particlesToBeMoved.push(this.particles.shift())
}
},
judgeTheDirect: function () {
for (let i = 0; i < this.points.length; i++) {
if (this.points[i].x - this.particlesToBeMoved[i].position.x < 0) {
this.points[i].dx = -this.points[i].dx
}
if (this.points[i].y - this.particlesToBeMoved[i].position.y < 0) {
this.points[i].dy = -this.points[i].dy
}
if (this.points[i].size - this.particlesToBeMoved[i].size < 0) {
this.points[i].ds = -this.points[i].ds
}
if (this.points[i].opacity - this.particlesToBeMoved[i].opacity < 0) {
this.points[i].do = -this.points[i].do
}
}
},
moveParticles: function () {
for (let i = 0; i < this.points.length; i++) {
if (Math.abs(this.points[i].x - this.particlesToBeMoved[i].position.x) <= Math.abs(this.points[i].dx) || this.particlesToBeMoved[i].position.x < 0) {
this.particlesToBeMoved[i].position.x = this.points[i].x
} else {
this.particlesToBeMoved[i].position.x += this.points[i].dx
}
if (Math.abs(this.points[i].y - this.particlesToBeMoved[i].position.y) <= Math.abs(this.points[i].dy) || this.particlesToBeMoved[i].position.y < 0) {
this.particlesToBeMoved[i].position.y = this.points[i].y
} else {
this.particlesToBeMoved[i].position.y += this.points[i].dy
}
if (Math.abs(this.points[i].size - this.particlesToBeMoved[i].size) <= Math.abs(this.points[i].ds) || this.particlesToBeMoved[i].size < 0) {
this.particlesToBeMoved[i].size = this.points[i].size
} else {
this.particlesToBeMoved[i].size += this.points[i].ds
}
if (Math.abs(this.points[i].opacity - this.particlesToBeMoved[i].opacity) <= Math.abs(this.points.do) || this.particlesToBeMoved[i].opacity < 0) {
this.particlesToBeMoved[i].opacity = this.points[i].opacity
} else {
this.particlesToBeMoved[i].opacity += this.points[i].do
}
}
this.moveRestParticles()
// this.draw()
},
复制代码
最后放一下效果图:
完整代码详见github