我正在参加「码上掘金迎新年」编程比赛,详情请看:码上掘金迎新年
前言
不久前学习了socket.io的文章,正好参加这次活动实现一个云烟花的程序。
socket.io部分实现
建立通信
socket.io部分并不复杂,我们首先构建scoket服务端与客户端的基础代码
//Client
import { io } from "socket.io-client";
const socket = io("127.0.0.1:3000");
socket.on("connect", () => {
// ...
});
//Server
const { createServer } = require("http");
const { Server } = require("socket.io");
const httpServer = createServer();
const io = new Server(httpServer, { cors:{origin: "*" }});
io.on("connection", (socket) => {
// ...
});
httpServer.listen(3000);
运行一下,连接成功
通信事件
接下来我们整理一下程序的通信事件
- 用户填写用户名后向服务端发送用户名,服务端接收并广播给所有用户
- 用户点燃烟花时向服务服务端发送消息,服务端接收并广播给其他用户
- 客户端接收服务端广播的消息
发送与监听事件
socket.io在发送与监听上给我们提供了emit与on这两个事件
客户端向服务端发送消息
//Client
socket.emit("send", "biu");
//Server
io.on("connection", (socket) => {
socket.on("send", (e) => {
console.log(e);
});
});
服务端向客户端发送消息
//Clinet
socket.on("ret", (e) => {
console.log(e)
})
//Server
//这里直接写在send中,服务端收到消息后触发ret
io.on("connection", (socket) => {
socket.on("send", (e) => {
console.log(e);
socket.emit("ret", "biu~~~");
});
});
但是,以上代码只能实现客户端与服务端的通信,并不能将信息发送给其他客户端。
广播事件
socket.io在广播上给我们提供了io.emit与socket.broadcast.emit这两个事件。
两者的区别在于io.emit是给所有连接的客户端发送消息
socket.broadcast.emit是给除发送者外的所有连接的客户端发送消息
//Clinet
socket.on("fire", (e) => {
console.log(e);
});
socket.on("enter", (e) => {
console.log(e);
});
socket.emit("fire", "未知文命向你丢了块二向箔");
socket.emit("enter", "给岁月以文明而不是给文明以岁月");
//Server
socket.on("fire", (e) => {
//给除发送者外的所有连接的客户端发送消息
socket.broadcast.emit('fire', e)
});
socket.on("enter", (e) => {
//给所有连接的客户端发送消息
io.emit('enter', e)
});
canvas烟花部分实现
canvas烟花部分也很容易,我们分部拆解一下,这里代码做了一些化简,可以结合后面提供的完整代码进行分析。
一个粒子的生与死
createFireworks生成了一个粒子的一些基本信息,角度、弧度、速率、半径范围、粒子大小、透明度等。
var particles = [];
function createFireworks(sx, sy) {
let p = {};
let angle = Math.floor(Math.random() * 360);
p.radians = angle * Math.PI / 180;
p.x = sx;
p.y = sy;
p.speed = 5;
p.radius = 5;
p.size = 5;
p.alpha = 1;
particles.push(p);
}
生成信息后我们再使用drawFireworks进行绘制,这里实现了粒子的受重力影响,主要通过vy与radius控制,同时进行透明度alpha的递减,再对粒子进行移除。关于物理动画详细的知识大家可以去微信读书搜索《HTML5 Canvas开发详解》第14章进行了解。
function drawFireworks() {
//清空重绘
clearCanvas();
for (let i = 0; i < particles.length; i++) {
let p = particles[i];
let vx = Math.cos(p.radians) * p.radius;
let vy = Math.sin(p.radians) * p.radius + 1;
p.x += vx;
p.y += vy;
p.radius *= 1 - p.speed / 100;
p.alpha -= 0.005;
if (p.alpha <= 0) {
particles.splice(i, 1);
continue;
}
context.beginPath();
context.arc(p.x, p.y, p.size, 0, Math.PI * 2, false);
context.closePath();
context.fillStyle = 'rgba(255,0,0,'+p.alpha+')';
context.fill();
}
}
这个时候粒子还是不会动的,接下来加入tick进行重绘。请记住requestAnimationFrame,后面来会展开来说。
function tick() {
drawFireworks();
requestAnimationFrame(tick);
}
tick();
最后再加上createFireworks(200,200)生成一个粒子。
当我们运行以上代码时,会发现这个情况,一般可以通过清空画布、然后重绘图形,从而达到动画的效果。很明显缺少了清空画布这一操作。
我们可以实现一个clearCanvas
function clearCanvas() {
context.fillStyle = '#ffffff';
context.fillRect(0, 0, canvas.width, canvas.height);
}
先在程序开始时清空画布,再在tick中drawFireworks前一行或者drawFireworks内第一行进行调用,目前看样子是符合我们的预期。
但是需要注意的是这里设置的颜色为白色,在云烟花中我们需要一个透明的画布,如果改为以下代码context.fillStyle = '#ffffff00';会发现clearCanvas失效了,因为正常情况下,浏览器会按照图形绘制的顺序来依次显示每个图形,后面绘制的会覆盖前面绘制的,遵循“后来者居上”原则。显然我们无法通过一张透明的图去进行覆盖,解决这个问题可以用globalCompositeOperation属性中的copy。copy的作用为只显示新图,旧图作透明处理。
function clearCanvas() {
context.fillStyle = '#ffffff00';
context.globalCompositeOperation = 'copy';
context.fillRect(0, 0, canvas.width, canvas.height);
}
但是,最后并不使用这种方案,因为在粒子拖尾的实现中把这个问题顺便解决了。
粒子的拖尾
前面介绍globalCompositeOperation属性,它也是实现拖尾的主力。 将之前所有和clearCanvas有关的代码全部干掉,将tick改为以下代码,发现实现拖尾的同时一并将画布进行覆盖重绘了。
function tick() {
context.globalCompositeOperation = 'destination-out';
context.fillStyle = 'rgba(0,0,0,' + 10 / 100 + ')';
context.fillRect(0, 0, canvas.width, canvas.height);
context.globalCompositeOperation = 'lighter';
drawFireworks();
requestAnimationFrame(tick);
}
以下是globalCompositeOperation的说明,单个粒子中效果不太明显,大家可以去看完整代码,烟花在运行的初期中心特别的亮就是lighter进行了颜色值的叠加。
d
关于globalCompositeOperation更多的知识与案例,可以去《HTML5 Canvas开发详解》第12章进行了解。
hsla()与rgba()
问题又得到了解决,但是,新的问题又来了。当把代码运行完之后,可以发现粒子轨迹上残留了一条淡淡划痕(这里为了显示明显,我将body的背景颜色改为了#eee)
原因又要回到上面tick中的context.fillStyle,这个属性与拖尾息息相关,大家可以修改值进行对比,值越大划痕越浅直到消失,但是拖尾消失了,值越小划痕越重,拖尾越长。鱼和熊掌怎么兼得,这时就需要考虑新的要素,比如颜色的值域,颜色越浅越亮,划痕越淡直到消失。在云烟花中我们通过随机生成烟花颜色,用rgba实现略微复杂,需要考虑色域的范围,但是我们通过hsla就可以很方便的实现。
这里先介绍一下hsla()
hsla() 函数使用色相、饱和度、亮度、透明度来定义颜色。
- 色相(H) 是色彩的基本属性,就是平常所说的颜色名称,如红色、黄色等。
- 饱和度(S) 是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取 0-100% 的数值。
- 亮度(L) 取 0-100%,增加亮度,颜色会向白色变化;减少亮度,颜色会向黑色变化。
- 透明度(A) 取值 0~1 之间, 代表透明度。
我们将drawFireworks中的rgba改为以下方式,问题就得到了解决。
context.fillStyle = 'hsla(' + 0 + ', 100%,' + 51 + '%, ' + p.alpha + ')';
在将一个粒子的效果全部完成后,我们只需要在createFireworks写一个for循环生成多个粒子信息在进行绘制,这样烟花的效果实现了。
总结
第一次写文章,真难写,越写越乱,很多东西讲不清楚,其中也参考了不少大佬的文章(那些漂亮的排版怎么搞的🤤),还是有些细节没弄明白。有没有大佬带带🤤。有些功能没实现完,薅了阿晴的卷轴,小姨的爆竹灯笼,结果都来得及没登场,还残留了一些bug...
小插曲
昨天早上改代码的时候发现一个大哥对着我的接口疯狂输出😧
然后我把服务器停了一小会。我中午又去看了一下🤤
大哥真无聊😥