目录

闲聊

看下效果

先贴下代码吧

大概说一下流程

下面让我来详细说一下

1、初始化基础属性

2、添加鼠标移动事件并实时更新鼠标坐标

3、通过随机数生成粒子的坐标和横纵轴速度

4、渲染粒子并将粒子对象保存在数组中

5、调用requestAnimationFrame启动动画,使粒子移动起来

6、通过横纵坐标和速度计算粒子位置

7、计算与鼠标距离进行坐标的修正

8、计算与鼠标距离并进行连线

9、计算粒子直接的距离并进行连线

10、添加鼠标点击事件并调用粒子的散开事件

11、通过与鼠标的距离和相对位置进行计算来重新给粒子添加速度

12、监听页面大小变化来初始化画布

总结


闲聊

一年前觉得别人写的贼酷贼神奇的canvas粒子动画背景,一年后自己写了一个更nb的,hahahaha!

好吧,其实也没啥难的,前后大概花了俩小时,只不过是最近才正儿八经学了一下canvas,写个东西来练练手。

言归正传,这个粒子背景的粒子移动和粒子直接的连线以及和鼠标的连线都很简单,两个难点在于鼠标跟随和点击散开,下面的介绍中我将重点说一下这两个功能点。

看下效果

没有鼠标?截图给隐藏掉了,位置就不用我说了吧。

gif好糊啊= =

先贴下代码吧

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Starry</title>
</head>
<body><div style="position: fixed;top: 0;left:0;bottom: 0;right: 0;z-index: 0"><canvas id="canvas" style="background-color: rgb(50,64,87);"></canvas></div><script type="text/javascript">const canvas = document.getElementById('canvas')const ctx = canvas.getContext('2d')let width = window.innerWidthlet height = window.innerHeightlet dotsNum = 80 // 点的数量let radius = 1 // 圆的半径,连接线宽度的一半let fillStyle = 'rgba(255,255,255,0.5)' // 点的颜色let lineWidth = radius * 2let connection = 120 // 连线最大距离let followLength = 80 // 鼠标跟随距离let dots = []let animationFrame = nulllet mouseX = nulllet mouseY = nullfunction addCanvasSize () { // 改变画布尺寸width = window.innerWidthheight = window.innerHeightcanvas.width = widthcanvas.height = heightctx.clearRect(0, 0, width, height)dots = []if (animationFrame) window.cancelAnimationFrame(animationFrame)initDots(dotsNum)moveDots()}function mouseMove (e) {mouseX = e.clientXmouseY = e.clientY}function mouseOut (e) {mouseX = nullmouseY = null}function mouseClick () {for (const dot of dots) dot.elastic()}class Dot {constructor(x, y) {this.x = xthis.y = ythis.speedX = Math.random() * 2 - 1this.speedY = Math.random() * 2 - 1this.follow = false}draw () {ctx.beginPath()ctx.arc(this.x, this.y, radius, 0, 2 * Math.PI)ctx.fill()ctx.closePath()}move () {if (this.x >= width || this.x <= 0) this.speedX = -this.speedXif (this.y >= height || this.y <= 0) this.speedY = -this.speedYthis.x += this.speedXthis.y += this.speedYif (this.speedX >= 1) this.speedX--if (this.speedX <= -1) this.speedX++if (this.speedY >= 1) this.speedY--if (this.speedY <= -1) this.speedY++this.correct()this.connectMouse()this.draw()}correct () { // 根据鼠标的位置修正if (!mouseX || !mouseY) returnlet lengthX = mouseX - this.xlet lengthY = mouseY - this.yconst distance = Math.sqrt(lengthX ** 2 + lengthY ** 2)if (distance <= followLength) this.follow = trueelse if (this.follow === true && distance > followLength && distance <= followLength + 8) {let proportion = followLength / distancelengthX *= proportionlengthY *= proportionthis.x = mouseX - lengthXthis.y = mouseY - lengthY} else this.follow = false}connectMouse () { // 点与鼠标连线if (mouseX && mouseY) {let lengthX = mouseX - this.xlet lengthY = mouseY - this.yconst distance = Math.sqrt(lengthX ** 2 + lengthY ** 2)if (distance <= connection) {opacity = (1 - distance / connection) * 0.5ctx.strokeStyle = `rgba(255,255,255,${opacity})`ctx.beginPath()ctx.moveTo(this.x, this.y)ctx.lineTo(mouseX, mouseY);ctx.stroke();ctx.closePath()}}}elastic () { // 鼠标点击后的弹射let lengthX = mouseX - this.xlet lengthY = mouseY - this.yconst distance = Math.sqrt(lengthX ** 2 + lengthY ** 2)if (distance >= connection) returnconst rate = 1 - distance / connection // 距离越小此值约接近1this.speedX = 40 * rate * -lengthX / distancethis.speedY = 40 * rate * -lengthY / distance}}function initDots (num) { // 初始化粒子ctx.fillStyle = fillStylectx.lineWidth = lineWidthfor (let i = 0; i < num; i++) {const x = Math.floor(Math.random() * width)const y = Math.floor(Math.random() * height)const dot = new Dot(x, y)dot.draw()dots.push(dot)}}function moveDots () { // 移动并建立点与点之间的连接线ctx.clearRect(0, 0, width, height)for (const dot of dots) {dot.move()}for (let i = 0; i < dots.length; i++) {for (let j = i; j < dots.length; j++) {const distance = Math.sqrt((dots[i].x - dots[j].x) ** 2 + (dots[i].y - dots[j].y) ** 2)if (distance <= connection) {opacity = (1 - distance / connection) * 0.5ctx.strokeStyle = `rgba(255,255,255,${opacity})`ctx.beginPath()ctx.moveTo(dots[i].x, dots[i].y)ctx.lineTo(dots[j].x, dots[j].y);ctx.stroke();ctx.closePath()}}}animationFrame = window.requestAnimationFrame(moveDots)}addCanvasSize()initDots(dotsNum)moveDots()document.onmousemove = mouseMovedocument.onmouseout = mouseOutdocument.onclick = mouseClickwindow.onresize = addCanvasSize
</script>
</body>
</html>

大概说一下流程

  1. 初始化基础属性
  2. 添加鼠标移动事件并实时更新鼠标坐标
  3. 通过随机数生成粒子的坐标和横纵轴速度
  4. 渲染粒子并将粒子对象保存在数组中
  5. 调用requestAnimationFrame启动动画,使粒子移动起来
  6. 通过横纵坐标和速度计算粒子位置
  7. 计算与鼠标距离进行坐标的修正
  8. 计算与鼠标距离并进行连线
  9. 计算粒子直接的距离并进行连线
  10. 添加鼠标点击事件并调用粒子的散开事件
  11. 通过与鼠标的距离和相对位置进行计算来重新给粒子添加速度
  12. 监听页面大小变化来初始化画布

下面让我来详细说一下

1、初始化基础属性

在这里初始化一些基础属性,粒子大小啊、颜色啊、数量啊叭啦叭啦的,先过过眼,下面都会用到。

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
let width = window.innerWidth
let height = window.innerHeightlet dotsNum = 80 // 点的数量
let radius = 1 // 圆的半径,连接线宽度的一半
let fillStyle = 'rgba(255,255,255,0.5)' // 点的颜色
let lineWidth = radius * 2
let connection = 120 // 连线最大距离
let followLength = 80 // 鼠标跟随距离let dots = [] // 粒子集合
let animationFrame = null
let mouseX = null
let mouseY = null

2、添加鼠标移动事件并实时更新鼠标坐标

在这里实时更新全局的鼠标坐标值,为下面与鼠标连线的处理做准备。

function mouseMove (e) {mouseX = e.clientXmouseY = e.clientY
}function mouseOut (e) {mouseX = nullmouseY = null
}document.onmousemove = mouseMove
document.onmouseout = mouseOut

3、通过随机数生成粒子的坐标和横纵轴速度

这一步比较简单,在构造函数里通过随机数对粒子的横纵坐标和速度进行初始化,这里注意,速度是有正负值的,我在这把速度限制在-1到1之间,另外有个follow属性非常重要,在接下来我会讲到。

class Dot {constructor(x, y) {this.x = xthis.y = ythis.speedX = Math.random() * 2 - 1this.speedY = Math.random() * 2 - 1this.follow = false}
}

4、渲染粒子并将粒子对象保存在数组中

这一步循环生成粒子对象并调用粒子对象的draw方法进行渲染,然后把粒子存入dots中以备后面使用。到这步完成,页面上就已经可以出现好多粒子了。

function initDots (num) { // 初始化粒子ctx.fillStyle = fillStylectx.lineWidth = lineWidthfor (let i = 0; i < num; i++) {const x = Math.floor(Math.random() * width)const y = Math.floor(Math.random() * height)const dot = new Dot(x, y)dot.draw()dots.push(dot)}
}draw () { // class Dotctx.beginPath()ctx.arc(this.x, this.y, radius, 0, 2 * Math.PI)ctx.fill()ctx.closePath()
}

5、调用requestAnimationFrame启动动画,使粒子移动起来

清空画布并调用粒子的move方法重新计算位置,这里使用window.requestAnimationFrame来请求动画帧,这样实现的动画要比setIntervel效果要更好,更自然,不了解的小伙伴可以自行百度一下。

function moveDots () { // 移动并建立点与点之间的连接线ctx.clearRect(0, 0, width, height)for (const dot of dots) {dot.move()}animationFrame = window.requestAnimationFrame(moveDots)
}

6、通过横纵坐标和速度计算粒子位置

这一步,主要对粒子进行碰撞检测,当检测到粒子贴近窗口边缘时,需要把碰撞所对应的速度分量改为其相反值,然后重新得出粒子的横纵坐标;在这个方法下还有四行处理,目的是对速度绝对值大于1的分量进行减速,这个是为后面鼠标点击散开而做的处理,后面我会说到。在这些处理都结束之后,会调用一个位置修正和与鼠标连线的处理,这两个我后面会说,都完成后,掉用draw重新绘制粒子。

move () { // class Dotif (this.x >= width || this.x <= 0) this.speedX = -this.speedXif (this.y >= height || this.y <= 0) this.speedY = -this.speedYthis.x += this.speedXthis.y += this.speedYif (this.speedX >= 1) this.speedX--if (this.speedX <= -1) this.speedX++if (this.speedY >= 1) this.speedY--if (this.speedY <= -1) this.speedY++this.correct()this.connectMouse()this.draw()
}

7、计算与鼠标距离进行坐标的修正

这一步就是我上面说的难点之一,鼠标跟随。下面我先尽可能通俗易懂的说一下我的实现思想。

我会设置一个牵引半径,这个牵引半径以鼠标为圆心会形成一个圆形的牵引区域,这个圆形区域内任何试图逃出的粒子都会被鼠标牵引住,无法逃出这个牵引半径。

那么,重点就在如何修正逃出粒子的坐标。在这里,我虚拟了一个区域,我称之为修正区域,这个修正区域是在牵引区域外面加了一圈,就像游泳圈一样,中间是牵引区域,外面的那一圈是修正区域。这个修正区域的作用,就是在发现有粒子从牵引区域逃逸到修正区域时,修正粒子的坐标到牵引区域的最外侧,这样就实现了鼠标对粒子的牵引。

这里要注意的是,一定是从牵引区域移动到修正区域的粒子才会被牵引,从外部区域进入修正区域的粒子是不能触发牵引的,要不路过修正区域的粒子就会被吸进来(好像也挺有意思0.0)。

看到这可能有人会想,这样的话粒子一出去就会被修正回来,会不会永远都逃不出这个牵引半径呢?答案是不会,因为我们知道通过requestAnimationFrame请求的动画最快为60帧,也就是说,只要你鼠标移动的速度够快,在下一帧到来的时候原本在牵引区域的粒子没有经过修正区域直接在修正区域和牵引区域以外,那么这个粒子就不再会被鼠标牵引。

大体的逻辑就是这样,下面将一下实现。

首先,排除鼠标不在页面里的情况,我在之前的鼠标移出页面的时候加了一个方法,会把鼠标的横纵坐标值都设为null,这里做下判断return一下就行了。

接下来就要计算粒子与鼠标的相对距离,先计算横纵坐标的相对距离,再用其纵坐标的相对距离计算出距离鼠标的位置。

计算出鼠标与粒子的相对距离后,就需要对其进行判断,这里分三种情况:

  1. 在牵引区域
  2. 在外部区域
  3. 在修正区域

1、在牵引区域:将粒子的follow属性设为true,说明这个粒子现在处在牵引区域,这个属性是为了对粒子是否从牵引区域进入的修正区域做判断用的。

2、在外部区域:将粒子的follow属性设为false,作用同上。

3、在修正区域:首先判断粒子的follow属性,若为false,则说明粒子是从外部区域进入的,则不做处理;我在这把游泳圈的宽度设置为8,就是之前讲的,鼠标很小的速度就能使粒子脱离牵引。当粒子的follow属性为true也就是说需要进行修正时,通过粒子与鼠标的相对距离与牵引半径的比值,乘粒子与鼠标横纵坐标的相对距离,得到修正后的粒子坐标与鼠标位置的横纵偏移量,然后通过鼠标的横纵坐标减去刚刚得到的偏移量,就得到了修正后粒子的横纵坐标。

对上面的第三种情况打个比方,就好像一个矩形,矩形的长和宽就是鼠标和粒子的横纵相对距离,对角线就是粒子与鼠标的相对距离,然后我们需要把这个矩形等比例缩小,缩小到对角线距离等于牵引距离。简单画了个图:

correct () { // 根据鼠标的位置修正 class dotif (!mouseX || !mouseY) returnlet lengthX = mouseX - this.xlet lengthY = mouseY - this.yconst distance = Math.sqrt(lengthX ** 2 + lengthY ** 2)if (distance <= followLength) this.follow = trueelse if (this.follow === true && distance > followLength && distance <= followLength + 8) {let proportion = followLength / distancelengthX *= proportionlengthY *= proportionthis.x = mouseX - lengthXthis.y = mouseY - lengthY} else this.follow = false
}

8、计算与鼠标距离并进行连线

这一步就是通过粒子的坐标和鼠标的坐标计算出相对距离,判断其如果小于连线距离,那么就与鼠标直接绘制连线,值得一提的是,这条连接线的透明度是随着粒子与鼠标的距离改变而改变的,距离越大,越趋近于透明,透明度最大为0.5,具体计算很简单,我就不说了,大家直接看代码吧。

if (mouseX && mouseY) {let lengthX = mouseX - this.xlet lengthY = mouseY - this.yconst distance = Math.sqrt(lengthX ** 2 + lengthY ** 2)if (distance <= connection) {opacity = (1 - distance / connection) * 0.5ctx.strokeStyle = `rgba(255,255,255,${opacity})`ctx.beginPath()ctx.moveTo(this.x, this.y)ctx.lineTo(mouseX, mouseY);ctx.stroke();ctx.closePath()}
}

9、计算粒子直接的距离并进行连线

这一步和上一步同理,只不过是变成了判断两个粒子之间的距离进行连线,在这里用了一个双重for循环,值得注意的是里面的哪层for循环的起始值不是0,要不每条连接线会绘制两次,就不对了。和上一步一样,粒子之间的连接线也是随着距离变化透明度变化的。

for (let i = 0; i < dots.length; i++) {for (let j = i; j < dots.length; j++) {const distance = Math.sqrt((dots[i].x - dots[j].x) ** 2 + (dots[i].y - dots[j].y) ** 2)if (distance <= connection) {opacity = (1 - distance / connection) * 0.5ctx.strokeStyle = `rgba(255,255,255,${opacity})`ctx.beginPath()ctx.moveTo(dots[i].x, dots[i].y)ctx.lineTo(dots[j].x, dots[j].y);ctx.stroke();ctx.closePath()}}
}

10、添加鼠标点击事件并调用粒子的散开事件

在这一步添加一个全局的点击事件,这个事件会调用所有粒子的elastic方法,并判断是否执行散开动作。

function mouseClick () {for (const dot of dots) dot.elastic()
}document.onclick = mouseClick

11、通过与鼠标的距离和相对位置进行计算来重新给粒子添加速度

这是第二个难点,点击散开的具体实现。

简单来说,点击散开就是在鼠标点击的时候,判断粒子与鼠标的相对距离,如果小于某个阈(yu四声,这我老是念错= =)值,就会被以鼠标相反的方向弹开,而且距离越小弹开的速度越快。还记得在之前计算粒子位置的move方法那里,我写的对速度绝对值大于1的判断么,这就用到了。当我弹开粒子时,粒子速度可能很快,所以要对粒子进行减速处理,这就是那四行代码的作用。

下面来说实现。

首先算出粒子与鼠标的横纵相对距离以及相对距离,然后判断其相对距离,当小于连接距离时,重新计算粒子的速度。

可以看到,我在下面通过1减去粒子与鼠标的相对距离与连接距离的比值,计算出了一个rate参数,鼠标距离鱼粒子距离越近,这个值约接近于1,然后用这个值去乘40(这个40是我设置的粒子弹开的速度上限),就得到了粒子的速度。

然后我们需要拿这个速度去计算粒子的横纵方向的分速度,我们用粒子的速度分别去乘负的粒子与鼠标的横纵相对距离与粒子与鼠标的相对距离的比值,就得到了粒子的横纵方向的分速度,这个比值就是所谓的正弦和余弦。

另外,为什么是负的,因为粒子需要向鼠标点击相反的反向弹开,所以在这里要取下反。

到这,这个粒子动画背景就基本算是完成了。

elastic () { // 鼠标点击后的弹射let lengthX = mouseX - this.xlet lengthY = mouseY - this.yconst distance = Math.sqrt(lengthX ** 2 + lengthY ** 2)if (distance >= connection) returnconst rate = 1 - distance / connection // 距离越小此值约接近1this.speedX = 40 * rate * -lengthX / distancethis.speedY = 40 * rate * -lengthY / distance
}// 上面粒子减速的处理
if (this.speedX >= 1) this.speedX--
if (this.speedX <= -1) this.speedX++
if (this.speedY >= 1) this.speedY--
if (this.speedY <= -1) this.speedY++

12、监听页面大小变化来初始化画布

最后,这是一个优化,因为用户改变浏览器窗口大小的时候,使画布可以根据改变后的大小重新加载,具体操作大家直接看代码吧,写博客的时候才想起来这个地方应该加个防抖的,懒得改了,有心的小伙伴们自己加一下吧哈哈。

function addCanvasSize () { // 改变画布尺寸width = window.innerWidthheight = window.innerHeightcanvas.width = widthcanvas.height = heightctx.clearRect(0, 0, width, height)dots = []if (animationFrame) window.cancelAnimationFrame(animationFrame)initDots(dotsNum)moveDots()
}window.onresize = addCanvasSize

总结

在实现的时候,我并没有借鉴任何资料,这种经过独立思考实现的小玩意还是非常令人高兴的,有空我会把它封装成js包,以方便在任何需要的地方使用。

练手项目,难免有一些不足,也欢迎大佬们提出宝贵意见,大家一起交流进步。

学无止境!!

前端canvas粒子动画背景(带鼠标跟随和点击散开)相关推荐

  1. html粒子动画效果设置为背景,canvas粒子动画背景的实现示例

    效果 :) 不带连线效果: 带连线的效果: 教程 要实现这样的效果其实很简单,大概分为这么几个步骤: 创建canvas 首先需要在需要展示粒子背景的父元素中创建一个canvas标签, 指定width和 ...

  2. fastadmin后台login登录页面增加canvas粒子动画背景

    地址演示地址登录 <!DOCTYPE html> <html> <head><link rel="stylesheet" media=&q ...

  3. canvas粒子动画

    2019独角兽企业重金招聘Python工程师标准>>> 周末在家玩了一下canvas粒子动画,先看看效果,用的图是我们微众银行的干爹'qq',先看看效果 ##1.获取图片信息 ### ...

  4. 看你骨骼惊奇,这里有一套 Canvas 粒子动画方案了解一下?

    导语:在日常的开发过程中,我们会常常会用到canvas来制作一些动画特效,其中有一个动画种类,需要我们生成一定数量,形状类似且行为基本一致的粒子,通过这些粒子的运动,来展现动画效果,比如:下雨,闪烁的 ...

  5. html图像粒子转换动画,html5 canvas粒子动画生成图片特效

    特效描述:html5 canvas 粒子动画生成 图片特效.html5基于canvas粒子化图片 利用easying.js公式计算粒子动态轨迹方程, x轴方向和y轴方向的公式均可以选择 可以设置粒子运 ...

  6. html++鼠标跟随动画,css3动画过渡实现鼠标跟随导航效果

    本篇文章主要介绍了css3动画过渡实现鼠标跟随导航效果,分享给大家,具体如下: 鼠标跟随导航效果 效果知识点:html/css布局思维, div+css讲解,css3动画,盒子模型, 浮动与定位,鼠标 ...

  7. 打造高大上的Canvas粒子动画

    首先来看下我们准备要做的粒子动画效果是怎么样的~ 是这样: 或者是这样: 甚至是这样: 很酷炫! 那如何去实现类似上面的粒子动画甚至根据自己的喜好去做更多其他轨迹的动画呢~请看下面详细的讲解. 技术选 ...

  8. canvas瀑布动画背景js特效

    下载地址使用canvas绘制的瀑布动画背景特效,可以当做网页背景. dd:

  9. JavaScript动画特效——Canvas粒子动画

    这次,将制作飘雪的效果,如果用CSS动画样式来做的话,可以想象,那是有多么难.所以就用JavaScript来制作. Canvas官方教程,很详细:http://canvas.migong.org/ca ...

最新文章

  1. java 自定义异常 未回滚_抛出自定义异常,spring AOP事务不回滚的解决方案
  2. python中文注释缩进_Python入门学习之注释、行与缩进
  3. 本期最新 9 篇论文,帮你完美解决「读什么」的问题 | PaperDaily #19
  4. 如何覆盖上一次commit_第一次漂冰雪就上难度 “小白”如何在冰雪中漂明白中置后驱...
  5. 201128阶段二MVC框架模式、FFmpeg
  6. Nginx安装手册(摘自入云龙老师教案,亲测可用)
  7. [导入]从函数RND的使用想到的!
  8. java班长竞选投票_竞选班长采取投票式,引家长不满,班主任:您说该怎么选?...
  9. 使用vue脚手架进行模块化开发
  10. Linux 内核参数:meminfo
  11. php gd绘制图片,PHP-用GD绘制图形
  12. 名片 - 名片设计的比例
  13. 业务流程图绘制方法经验谈(下篇)
  14. 机械臂编程_建立自己的机械臂-编程
  15. java hh24miss_Java编程时间格式与数据库中时间格式转化
  16. java 1029: 三角形判定
  17. i html设置为不倾斜,css如何不让字体倾斜?
  18. 于晓津外头游荡了半天
  19. 西游记中孙悟空大闹天宫时天庭各路神将是真的对孙悟空放水吗?
  20. 安装elasticsearch踩过的坑,教训写在这里,一起共同努力,希望大家先通读一下再按着步骤操作,看过就知道哪些坑可以掠过

热门文章

  1. 单片机CPLD/FPGA开发综合实验,QY-DPJ05
  2. 美国洛杉矶某曾经知名品牌一型号单反数码相机辐射整改案例
  3. Java 在数据库中生成一年的假日与工作日信息
  4. 南京廖华计算机二级考试答案,计算机二级Msoffice演示文稿试题库答案(解题步骤)...
  5. 万得与网易游戏平台开发一面
  6. 电脑优化及精品软件分享
  7. 「github资料」40个Python可视化图表案例(附零基础学习资料)篇幅较长,建议收藏
  8. 关于inpho5、inpho8软件文件中像点坐标系的说明
  9. 商业世界的革新,数字化转型的“顶层设计”
  10. sno什么意思mysql_sno是什么意思