摘要
本文主要介绍一种WEB形式的烟花(fireworks)效果(图1所示),该效果基于Canvas实现,巧妙地运用了canvas绘图的特性,并加入了物理力作用的模拟,使整体效果非常绚丽、逼真。本文从本质上介绍了其实现原理,便于其他可视化爱好者能快速上手。本文从视觉渲染和运动轨迹模拟两个方面详细描述了该效果的实现原理及细节。
在线效果和代码在这里,可点击链接到Codepen查看。
图1 - Canvas烟花效果截图
引言
“东风夜放花千树。更吹落、星如雨。”——青玉案·元夕。烟花的耀眼只是一瞬,但人们追求美的心却是永恒。本文所要介绍的效果,是我所见过的最美丽、最符合人的精神图像的作品。
网上还有一种类似于火星迸发的线条很细的模拟烟花效果,我觉得比本文这一款差多了。如果作为烟花爆竹出售,我感觉它们是一块钱和十块钱效果的差别,这是一个不恰当的比喻。
烟花效果的逼真在于其颜色的多彩变幻和运行轨迹的合理性。从现象上看,一道光柱逐渐上升,到达一定高度时停止,同时出现多条弧形的流星效果并逐渐下降。随着烟花绽放的时间增加,其颜色逐渐暗淡,最终消逝不见。从本质上来看,它是一系列头部颜色最亮、尾部越来越暗,且轨迹越来淡的线条。从外表上来看,它爆炸前是一条直线,爆炸后变成多条曲线。
本文所描述的实现方式巧妙地利用了Canvas绘图的技巧,通过合理的重绘和清除策略,完美地模拟了烟花绽放。
正文
一、颜色渲染
虽然其实质是线条的渲染,但对于烟花来讲,我们最直观的感觉是有一个实物飞上天,然后有很多小的实物因爆炸而散落下降。因此我们可在画布上根据轨迹不断绘制小圆点,圆点因连续运动而形成线条。再通过不断重绘画布,用带有一定透明度的背景色来渲染画布整个区域,并且保证新绘制的和原来绘制的图形都存在,即可达到轨迹颜色逐渐暗淡的效果。
为了清晰描述这一过程,此处把该部分功能独立出来,我们单独来看看这个线条效果是怎么来的。
如图2所示,为了方便查看,我们把线条放大,节奏放慢。可点击链接到Codepen查看源码。
图2 - 线条渲染
In loop update
context.fillStyle = "rgba(0, 0, 0, 0.05)";
context.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
draw on canvas
this.render = function(){
if(this.end)return;
var c = context;
c.save();
c.globalCompositeOperation = 'lighter';
var x = this.pos.x, y = this.pos.y, r = this.size;
var gradient = c.createRadialGradient(x, y, 0.1, x, y, r);
gradient.addColorStop(0.1, "rgba(255, 255, 255 ," + 1 + ")");
gradient.addColorStop(1, "rgba(0, 0, 0, " + 1 + ")");
c.fillStyle = gradient;
c.beginPath();
c.arc(x, y, r, 0, Math.PI * 2, true);
c.closePath();
c.fill();
c.restore();
};
如代码标注部分(行2、10、13、19)所示,有四点主要内容:画圆、颜色径向渐变、绘图方式、重绘。
1、画圆。 x, y代表运行轨迹的当前坐标。控制运动的代码部分只需要负责计算并更新x和y的值。渲染的代码只负责在该坐标点画圆并填充。
2、颜色径向渐变。圆点的填充颜色,其中 createRadialGradient() 函数的第1、2个参数与第4、5个参数分别表示渐变开始和渐变结束的圆心坐标。在这里设置为相同的值,使颜色过渡效果更自然真实。
3、绘图方式。Canvas的 globalCompositeOperation 属性决定下一次绘图时如何将一个源(新的)图像绘制到目标(已有)的图像上。此处设置为 'lighter' 使同时显示源图像和目标图像,否则会影响到循环中的画布重绘。
4、重绘。使用 fillRect() 函数对整个画布进行清除,同时设置 fillStyle 属性为带一定不透明度的背景色。如此每一次清除画布将使原有的图形变得暗淡,随着时间流逝,而最终消失在夜幕背景下,或被最新的烟花轨迹覆盖。
以上几点是此次烟花渲染的核心之处,它巧妙运用了Canvas绘图的特点,完美地用动画仿真了烟花的绽放。
二、轨迹模拟
烟花轨迹模拟的核心是模拟物体在物理力作用下的运行状态。在此我们主要考虑方向、速度、重力的影响。烟花在爆炸时向四面八方散开,体现在2D画布上就是0~360度随机方向。我们假想在爆炸的一瞬间冲击力最大,速度最大,然后受阻力影响速度逐渐减小。此外我们需要考虑重力的影响,使其最终大致呈自由落体运动。
为了纯粹演示运动轨迹,我们把该部分逻辑独立出来,用d3.js模拟整个爆炸过程。
如图3所示,可点击链接到Codepen查看源码。
图3 - 轨迹模拟
图4 - 轨迹瞬间
接下来我们直接看核心源码:
创建模型列表
function create(){
初始化
circles = [];
for (var i = 0; i < count; i++) {
var particle = new Circle(objs[i], pos[0], pos[1]);
var angle = Math.random() * Math.PI * 2;
var speed = Math.cos(Math.random() * Math.PI / 2) * 15;
particle.vel.x = Math.cos(angle) * speed;
particle.vel.y = Math.sin(angle) * speed;
particle.size = 10;
particle.gravity = 0.2;
particle.resistance = 0.92;
particle.shrink = Math.random() * 0.05 + 0.93;
circles.push(particle);
}
}
In Model Class
模型参数更新
this.update = function(){
this.vel.x *= this.resistance;
this.vel.y *= this.resistance;
// gravity down
this.vel.y += this.gravity;
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
// shrink
this.size *= this.shrink;
this.move();
};
实体位置移动
this.move = function(){
this.object.attr('cx', this.pos.x)
.attr('cy', this.pos.y)
.attr('r', this.size)
;
}
我们抽象出一个物体模型,它的主要属性是pos(当前位置)、vel(方向参数)、resistance(阻力作用参数)、gravity(重力系数),主要方法是update() 和 move()。可在Codepen查看完整源码。
以上代码中, create() 是一个初始化所有爆炸物的方法,总数量count设置为80左右的随机数,objs是用d3.js添加的svg上的元素集合(在此我们使用圆形circle),模型类的每一个实例的‘object‘属性对应一个画布上的元素,在此根据分层思想把update()和move()分开,前者负责逻辑,后者负责效果。
在create里初始化了模型的一系列参数,其中重要的是移动方向,我们逐条来分析:
var angle = Math.random() * Math.PI * 2; 不难理解,就是角度取 0 ~ 360 度的随机一个方向。取值在0 ~ 6.28之间。
var speed = Math.cos(Math.random() * Math.PI / 2) * 15; 其中括号内的取值在 0 ~ π/2 之间,我们知道在此区间上cos函数的值为正,值域在0 ~ 1之间,故speed的取值在0 ~ 15之间。
particle.vel.x = Math.cos(angle) * speed; 区间0 ~ 2π上,cos函数的取值是-1 ~ 1之间,正负概率均等,参考图5。
particle.vel.y = Math.sin(angle) * speed; 区间0 ~ 2π上,sin函数的取值是-1 ~ 1之间,正负概率均等。
因此,方向偏移变量vel分布在二维坐标轴的四个象限,各爆炸物的初始坐标离爆炸点的横纵距离随机分布在0 ~ 15之间。
图5 - 三角函数曲线
在update()函数里,修改方向偏移变量,使力度逐渐衰减,并加入重力影响,然后修改物体的当前位置坐标,以及逐渐减小物体在视觉范围内的尺寸。
this.vel.x *= this.resistance; resistance 的赋值是0.92,故偏移量每次以92%的比例衰减。因此在效果图中我们看到,物体完全自由落体前移动速度有越来越慢的缓冲效果,类似于cubicOut缓冲。
this.vel.y *= this.resistance;
this.vel.y += this.gravity; gravity是重力系数,所以运动时需要每次沿y轴下方适当偏移。当vel变量的衰减殆尽时,只受重力作用影响,就呈直线下降状态。(此处物理专业的朋友禁止较真!)
this.pos.x += this.vel.x; 物体位置信息更新。
this.pos.y += this.vel.y;
this.size *= this.shrink; 物体尺寸大小衰减。
this.move(); 视图层移动。
在这个简单的轨迹模拟程序里,我们直接使用 setInterval() 函数来实现动画刷新,也可以使用 requestAnimationFrame() 函数显得更正式些。
至此,我们的物体物理运动轨迹模拟完成,如图3所示,尽管没有什么渲染的成分,看起来依然有烟花绽放的感觉。
总结
本文介绍了一种基于Canvas的烟花效果实现方式,该方式巧妙地利用了Canvas渲染的特性,其动画受浏览器性能的影响较小,并加入物理力作用效果,使动画整体看起来很逼真,效果很绚丽。本文详细介绍了其原理及实现细节,有助于其他有相似需求的开发者能迅速使用并改进,实现更完美的作品。
其它
在此声明,本文所述的方法非本人原创,也没有找到原作者,在此鸣谢!
基于以上内容的介绍,有兴趣的开发者可以使用d3js框架实现整套的完整效果。
若您发现本文所述有失偏驳之处,或有待改进之处,或您有其它想法、意见及建议,请在评论区留言,谢谢!