前言
俗话说:过了腊八就是年。腊八节过完,新春已经踩着风火轮追来了!你期待今年的新春吗?
除夕、春节,是一年间最重要的几个节日,也是难得阖家大团圆的美妙之日,回忆起过往的新春佳节,小包就不由思绪万千,有快乐,有开心,有向往,有苦涩,春节的时光短暂却又永恒,深深篆刻在小包的记忆之海。
不知道你记忆中的新春会是什么样子那?接下来首先以几段关键词动画,来领略一下小包的新春回忆吧。
欢迎大佬们在评论区中评论,分享你的新春记忆关键词或关键词动画,我们一起来体悟或者猜测你的新春回忆,点赞+评论最多的掘友小包会送他掘金周边一份。
工具
在分享案例玩法和实现之前,先提供几个工具,方便大家定制自己的新春回忆。
- 表情文字网站: 可以去网站中选取你想要的表情文字 (链接提供人: 春佬)
- 录屏工具ScreenToGif: 大家可以借助录屏工具录制自己难忘的新春回忆。工具使用起来比较简单粗暴。
关于录屏这里,小包其实本来预想是实现生成动图功能,找了好多方案,实现效果都比较差,后面小包会继续推进这方面的研究,如果有进展会继续更新后续研究。
玩法
体验地址: 定制你的专属新春回忆
玩法非常简单,大致就这几点注意事项:
- 关键词输入在下划线位置
- 多个关键词可通过空格或者中英文逗号分隔
- 支持中英文、表情文字
- 文字尽量不要太长,五字以内
- 输入关键词序列后,回车键开启动画
下面先来看看小包的春节回忆吧。
童年
童年时光已经离小包有些遥远了,那个年代物质生活不算丰富,精神生活比较匮乏,但架不住小包当时单纯啊,翻山越岭,爬山跨海,上天入地,给小包留下了无数刻骨铭心的回忆,青山、近邻、小河、伙伴,简单的生活创造无尽的美好。
除夕夜
那时代的年也是年味十足,除夕夜小包一家三口,吃着水饺,看着春晚,热情似火,初一零时,月夜小桥,爆竹声声,红包鼓鼓。关键词: 👨👩👦,🥟,📺,👏,🕛,🌉,🧨,🧧
春节
春节的日子就更充实了,一家三口,六点起床,大红灯笼,七点拜年,走家串巷,些许人群,糖果花生,快乐逍遥。演化成关键词:👨👩👦,🕕,🏮,🕖,🤝,👨👨👦👦,🍬,🥜,🥰
青春
后续部分的表情关键词就不做解读了,大家可以猜一下,发在评论里面,最贴近的会有掘金周边送出
关键词: 👨👨👧👦,🥟,📺,🕛,广场,🧨,🎇,🙏
疫情
说实话,随着年龄的增长,物质生活和精神生活都迅速发展,社会的浮躁气息愈来愈浓,年味也越来越淡,但疫情那年,会成为小包永恒的记忆,那年年味有可能更淡了,但情味却浓上加浓,没有什么困难可以战胜中华民族。
关键词: 👨👨👧👦,📺,🦠,🛌,🥼,🦸🏻,🏆,致敬
代码实现
粒子效果其实小包已经实现过多次,所以简单部分小包就不做详细讲解,核心讲解动画切换部分。
创建粒子类
粒子主要包含粒子随机位置、粒子目标位置、粒子颜色和粒子半径。本项目中粒子半径统一设置为2。
class Particle { constructor({ x = 0, y = 0, tx = 0, ty = 0, radius = 2, color = "#F00000" }) { // 当前坐标为随机生成坐标 this.x = particle.x; this.y = y; // 目标点坐标(副画布原有粒子坐标) this.tx = tx; this.ty = ty; this.radius = 2; this.color = color; } // 粒子绘制 draw(ctx) { ctx.save(); ctx.translate(this.x, this.y); ctx.fillStyle = this.color; ctx.beginPath(); // 绘制圆形 ctx.arc(0, 0, this.radius, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.restore(); return this; } } 复制代码
提取像素信息
创建副画布及绘制文字
将要生成粒子的文字渲染到副画布上,副画布是虚拟画布,只做提取文字像素的载体使用,不会渲染到页面中。
const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const viewWidth = window.innerWidth * 0.5; const viewHeight = window.innerHeight * 0.5; canvas.width = viewWidth; canvas.height = viewHeight; // 预留处理图片的接口 if (typeof target === "string") { // 绘制文字 // 保证长文字的在PC端及移动端的完整展示 // 处理的有几分粗糙 ctx.font = `${ target.length < 3 ? textWidth : (textWidth * 3) / (1 + target.length) }px bold`; // 设置文字的基本样式 ctx.fillStyle = colorList[rand(0, colorList.length)]; ctx.textBaseline = "middle"; ctx.textAlign = "center"; ctx.fillText(target, viewWidth / 2, viewHeight / 2); } 复制代码
获取像素点数据
重点介绍一下getImageData方法
- 语法
ctx.getImageData(sx, sy, sw, sh);
- 参数
sx, sy
: 要提取图像的左上角x y
坐标sw, sh
: 要提取图像的宽高
- 返回值
返回ImageData
对象,该对象拷贝了指定图像区域的像素。对于图像中每个像素点,都分别存放RGBA
四方面的信息,所有的像素数据以一维数组形式存放在data
属性中(每个像素点由rgba
四个值组成,因此每次需要乘以 4 才能跳到下一个像素点)
const { data, width, height } = ctx.getImageData(0, 0, viewWidth, viewHeight); 复制代码
由上图可见,imageData
中包含四个属性,上面我们使用了 width height data
,data
中存放了巨量的像素点数据,以我们这个案例为例子,第一个关键词就包含了 737280
个数字,因此我们需要根据一定的算法来进行像素筛选。
提取绘制粒子的像素点
这里解释一下 interval
的作用以及为什么可以实现对像素点数量的控制?
getImageData
提取画布的全部像素点,如上图 data
所示,一共提取了 737280 / 4 = 184320
个像素点,如果不经筛选全部绘制成半径为 2
的粒子,一方面粒子会发生重叠;另一方面如果生成 18万 个粒子,为了保证粒子运动的流畅性,使用 requestAnimationFrame
更新粒子位置,如此巨大数量的粒子频繁渲染对电脑的渲染性能要求极高,就比如小包的电脑,就完全无法运行,卡顿感爆棚。
因此我们需要寻找一个办法,即能适配各种各样的显示屏,又能要渲染恰到好处的粒子数,那我们应该怎么处理 getImageData
返回数据那?
大佬们想出一个很好的办法,画布的 width, height
通过 getImageData
同步获得,因此我们可以把画布看作 width * height
个宽高都为 1
的网格构成。我们在这个网格中取数据,interval
为选取间隔,每隔 interval
选取一个像素点,
interval
值越大,间隔越大,所以每一行中选取的像素点就越少。(如果 interval
选取太大,选区的像素点就越少,文字有可能会发生缺失,因此我们要选取合适的 interval
)
接下来就是如何定位到这个像素点?getImageData
返回的数据是按行来读取像素点,每个像素点使用四个数组位来存放。如果我们以宽高为基准,那么纵坐标 y * 画布宽度 + 横坐标 x得出像素点位置,然后乘以4计算出在 ImageData
数组的索引位置。横坐标 x 和纵坐标 y 即是粒子的目标位置。
// 获取目标对象的像素点,interval 控制像素点数量,值越大返回的像素点越少 const pixeles = []; // 遍历像素数据,用interval减少取到的像素数据 for (let x = 0; x < width; x += interval) { for (let y = 0; y < height; y += interval) { const pos = (y * width + x) * 4; // 只提取 rgba 中透明度大于0.5的像素,即aplha > 128 if (data[pos + 3] > 128) { pixeles.push({ x, y, rgba: [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]], }); } } } return pixeles; } 复制代码
绘制粒子
根据合适 interval
选取的像素点对应创建粒子,粒子的目标位置和颜色由像素点提供,当前位置通过随机数生成。
function createParticles({ text, radius, interval }) { const pixeles = getWordPxInfo(text, interval); return pixeles.map((particle) => { return new Particle({ x: Math.random() * (50 + window.innerWidth * 0.5) - 50, y: Math.random() * (50 + window.innerHeight * 0.5) - 50, tx: particle.x, ty: particle.y, radius: particle.raduis, color: particle.rgba, }); }); } 复制代码
生成的粒子如下图分布:
文字切换
每次更新粒子位置后需要去判断是否所有的粒子到达目标,如果所有粒子都完成运动,则进入下一个文字动画。因此文字切换的难点在于如何检测所有的粒子是否完成运动?
每个粒子上有 finished
属性,默认值为 false
。规定粒子当前位置距离目标位置小于 0.1
代表当前粒子运动结束,当前粒子运动结束后修改其 finished
值为 true
;如果大于 0.1
,粒子继续运动。(粒子运动采取的缓动系数为0.09)
由于每个粒子上都具备 finished
属性,我们可以通过 filter
过滤出所有 finished
属性为 true
的粒子,如果过滤出粒子数量等于总粒子数量,当前文字动画结束,切换下一个文字。
// 参数分别是粒子数组及下一个文字动画回调 function drawFrame(particles, finished) { // 开启粒子渲染动画 const timer = window.requestAnimationFrame(() => { drawFrame(particles, finished); }); ctx.clearRect(0, 0, canvas.width, canvas.height); // 缓动系数设置为0.09 const easing = 0.09; const finishedParticles = particles.filter((particle) => { // 当前坐标和目标点之间的距离 const dx = particle.tx - particle.x; const dy = particle.ty - particle.y; // 粒子移动速度 let vx = dx * easing; let vy = dy * easing; // 判断当前粒子是否完成动画 if (Math.abs(dx) < 0.1 && Math.abs(dy) < 0.1) { // 完成动画位置不会在改变,并修改 finished 为 true particle.finished = true; particle.x = particle.tx; particle.y = particle.ty; } else { // 未完成动画继续更新粒子位置 particle.x += vx; particle.y += vy; } // 绘制粒子新位置 particle.draw(ctx); // 判断返回完成运动的的粒子数 return particle.finished; }); if (finishedParticles.length === particles.length) { // 全部粒子运动结束,结束当前动画 window.cancelAnimationFrame(timer); // 开启下一轮文字回调 finished && finished(); } return particles; } 复制代码
代码实现到这里,就可以实现当前文字的动画效果及动画效果结束判断,我们只需要在当前动画效果结束后,执行 finished
回调即可实现切换。
动画切换函数 loop
loop
函数设计为了给 drawFrame
提供下一个文字的渲染。代码逻辑比较简单,不在多言。
function loop(words, i = 0) { return drawFrame( // 生成粒子 createParticles({ text: words[i], radius: 2, interval: 5 }), // finish函数部分=》下一个文字 () => { i++; // hack一下空文字 if (i < words.length && words[i].length > 0) { loop(words, i); } } ); } 复制代码
虎年送大礼
说句掏心窝子的话,自从十月份在掘金开始写文,刚迈入 2022
年,小包就成功登临 LV4
,小包对自己的认知还是特别清晰的,小包需要快速进步才能配称得上优秀作者。
但真的十分感谢大佬们的支持,在掘金这边相识了很多新的朋友,希望朋友的友谊能地久天长。另外还要感谢掘金社区的可爱运营们,负责,接地气,让小包这个大龄写手重新拥有热情,重拾初心,希望新的一年可以和掘金一起成长,一起进步。
这半年来,零零散散薅了社区不少羊毛,有些送给了朋友,送完朋友还剩一些,就送给掘金的最熟悉的陌生人吧,希望未来的日子能越来越熟悉。
- 小包预留俩组关键词,猜测最贴切的掘友,小包会送上掘金周边一份
- 分享你的新春回忆(录制动图或者关键词都可),收获到点赞和评论之后最多的掘友,小包同样也会送上掘金周边一份
- 别的奖项暂时还没想出,后面如果想出在动态添加上
源码仓库
源码地址: 定制你的专属新春回忆
体验地址:定制你的专属新春回忆
如果感觉有帮助的话,别忘了给小包点个 ⭐ 。