烟花来咯~ 使用socketIO和canvas实现云烟花

1,321 阅读6分钟

我正在参加「码上掘金迎新年」编程比赛,详情请看:码上掘金迎新年

前言

不久前学习了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);

运行一下,连接成功 image.png

通信事件

接下来我们整理一下程序的通信事件

  1. 用户填写用户名后向服务端发送用户名,服务端接收并广播给所有用户
  2. 用户点燃烟花时向服务服务端发送消息,服务端接收并广播给其他用户
  3. 客户端接收服务端广播的消息

发送与监听事件

socket.io在发送与监听上给我们提供了emit与on这两个事件

客户端向服务端发送消息

//Client
socket.emit("send", "biu");

//Server
io.on("connection", (socket) => {
  socket.on("send", (e) => {
    console.log(e);
  });
});

send~1.gif

服务端向客户端发送消息

//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~~~");
  });
});

ret~1.gif

但是,以上代码只能实现客户端与服务端的通信,并不能将信息发送给其他客户端。

广播事件

socket.io在广播上给我们提供了io.emit与socket.broadcast.emit这两个事件。

两者的区别在于io.emit是给所有连接的客户端发送消息

image.png

socket.broadcast.emit是给除发送者外的所有连接的客户端发送消息

image.png

//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)
});

all.gif

示例代码(node.js部分注释在代码底部)

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章进行了解。

lAHPJxDj1tS2FAXNAQ7NAeA_480_270.gif

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)生成一个粒子。

当我们运行以上代码时,会发现这个情况,一般可以通过清空画布、然后重绘图形,从而达到动画的效果。很明显缺少了清空画布这一操作。

粒子.gif

我们可以实现一个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进行了颜色值的叠加。 image.png

image.png d 关于globalCompositeOperation更多的知识与案例,可以去《HTML5 Canvas开发详解》第12章进行了解。

hsla()与rgba()

问题又得到了解决,但是,新的问题又来了。当把代码运行完之后,可以发现粒子轨迹上残留了一条淡淡划痕(这里为了显示明显,我将body的背景颜色改为了#eee)

image.png lAHPKG0OUei1fGHNARjNAZA_400_280.gif

原因又要回到上面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循环生成多个粒子信息在进行绘制,这样烟花的效果实现了。

canvas烟花

总结

第一次写文章,真难写,越写越乱,很多东西讲不清楚,其中也参考了不少大佬的文章(那些漂亮的排版怎么搞的🤤),还是有些细节没弄明白。有没有大佬带带🤤。有些功能没实现完,薅了阿晴的卷轴,小姨的爆竹灯笼,结果都来得及没登场,还残留了一些bug...

image.png image.png image.png

小插曲

昨天早上改代码的时候发现一个大哥对着我的接口疯狂输出😧

image.png

image.png

然后我把服务器停了一小会。我中午又去看了一下🤤

image.png

image.png

image.png

大哥真无聊😥

image.png