整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

用HTML5的canvas来画一个梦幻星空,来学习一下吧

随着HTML5的火热,越来越多的人投入到HTML5开发中了,canvas作为HTML5中比较重要的一个元素,在很多官网的主页面中被使用到。今天我们一起来看看如何使用canvas画出一个梦幻的星空背景,还会有流星运动。

本文的代码已经放到Github上了,感兴趣的可以自取,Github地址如下。

https://github.com/zhouxiongking/article-pages/blob/master/articles/starry/starry.html

HTML5

实现效果

首先我们来看看通过canvas实现的星空效果图,如下所示。

效果图

代码实现

接下来我们看看这个效果是如何通过代码一步步实现的。

首先来看看页面上的HTML代码,只有一个Div元素。

HTML代码

Javascript代码

首先我们需要定义一些常量,比如画布的宽和高,星星数量,流星个数。在这个星空中流星其实是星星的一个,只是添加了动态效果。

页面初始化

然后是设定一个定时器,通过一段随机时间生成一个流星的索引号。

流星索引号

紧接着来看看生成一个星星的方法,该方法返回一个星星的各项参数,包括x,y轴坐标,透明度,x,y轴偏移量。

生成星星的参数

然后是最重要的render方法,通过该方法可以将星星渲染至画布上,我们将这个方法拆开看,首先是对流星的绘制,流星索引号通过上面metor方法获得。

画流星

然后是对于星星各项参数的处理,比如有的星星生成的点坐标超出了屏幕宽高,有的透明度是负数,都要将其处理成正常参数。

各项参数判断

最后是在画布中进行绘制。

画布绘制

至此,这个画面效果的讲解完毕,如果代码正确的话,就可以看到文中出现的效果图。

结束语

今天这篇文章主要是借助HTML5中的canvas画出了一个梦幻星空的效果,你学会了吗?

天躺在床上刷抖音的时候,看见了一个马克笔随便画星空的视频,很有意思。


先看效果:


开始需求分析:

1、渐变色的背景

2、画一颗树和一些草

3、水面的倒影

4、随便画点星星

5、画一颗流星


1、渐变色的背景

先确定200*500的区域,使用css3的线性渐变属性,依次深蓝、浅蓝、紫色、粉色、黄色画出一个渐变色的背景。

为了使背景更真实,使用同样的颜色顺序,在不同的角度,加上一些模糊和透明。再画一遍重叠起来。

再重叠一层黑色,使画布更暗一些。

.bg-color {
  background-image: linear-gradient(170deg, #000093 13%, #9f35ff, #ff8000 70%, #f9f900 );
}
.bg-color2 {
  background-image: linear-gradient( 180deg, #000093 13%, #9f35ff, #ff8000 80%, #f9f900 );
  opacity: 0.3;
  filter: blur(6px);
}
.bg-color3 {
  background: rgba(0,0,0,.2);
}


2、画一棵树和草

使用html来画一棵树的话,需要很多个节点,性能和效果都很差。这里使用canvas来画树。

画树的教程,公众号出过好几次了,这里就不在重写了。

基本原理就是,从一个点向一个方向画一条直线。从终点开始,重新这个流程。期间可以修改一个角度画出一分支。

草就更加简单。随便在底部画一些杂乱的直线。

// 画一棵树
function drawTree(x, y, deg, step, type) {
  var deg1 = step % 2 == 0 ? 0.1 : -0.1;
  var x1 = x + Math.cos(deg + deg1) * (step + 5) * 0.9;
  var y1 = y + Math.sin(deg + deg1) * (step - 1) * 0.9;
  ctx.beginPath();
  ctx.lineWidth = step / 3;
  ctx.moveTo(x, y);
  ctx.lineTo(x1, y1);
  ctx.stroke();
  if (step > 12) {
    ctx.arc(x, y, step / 6, 0, Math.PI * 2);
    ctx.fill();
  }
  if (step < 3 && Math.random() > 0.7) {
    var r = 2 + Math.random() * 2;
    ctx.arc(x1 + Math.random() * 3, y1 + Math.random() * 3, r, r, Math.PI + r);
    ctx.fill();
  }
  step--;
  if (step > 0) {
    drawTree(x1, y1, deg, step, type);
    if (step % 2 == 1 && step < 17)
      drawTree(x1, y1, deg + 0.2, Math.round(step / 1.13));
    if (step % 2 == 0 && step < 17)
      drawTree(x1, y1, deg - 0.2, Math.round(step / 1.13) );
  }
}


3、水面的倒影

最简单的做法,就是使用canvas.toDataUrl 拿到canvas的图片数据。在底部放一个反转的图片就可以。

我这里希望水面的倒影可以动起来。

新建一个canvas,使用ctx.getImageData拿到我们画好的树的像素点数据。

使用正弦给像素的x轴做一些偏移,得到一个新的数据。put到倒影的canvas上。

在使用requestAnimationFrame,做出一个流畅的左右摆动的倒影动画。

最后,在原数据基础上,增加一些杂色,使得倒影有一些黑白的横线,模拟水波的高亮。

var startWave = 0 // 水波起始位置
// 倒影增加水波纹效果
function wave(star){
  var newImgData = ctxShadow.createImageData(200,150)
  var pos = 0
  var source = 0
  startWave += 0.2
  start = startWave
  for(var y = 0 ; y < CANVAS_HEIGHT ; y ++) {
    start += 0.5
    for(var x = 0 ; x < CANVAS_WIDTH ; x ++) {
      pos = (y * CANVAS_WIDTH + x) * 4
      source = (y * CANVAS_WIDTH + x + Math.round(Math.sin(start)* 1.5)) * 4
      newImgData.data[pos + 0] = imgData.data[source + 0];
      newImgData.data[pos + 1] = imgData.data[source + 1];
      newImgData.data[pos + 2] = imgData.data[source + 2];
      newImgData.data[pos + 3] = imgData.data[source + 3];
    }
  }
  ctxShadow.putImageData(newImgData,0,0)
  requestAnimationFrame(wave)
}


4、画星空

这个简单,就不再写代码了,就随意写一些白色的div,随机插入背景上。


其实到这一步,已经基本上完成了。


5、加一些流星

要画流星,需要画出一个渐渐变淡变窄的白线。

这里偷了个懒,在视觉效果上,一个渐渐变淡的白线,人眼看到,就感觉渐渐变窄。

这里使用白色加透明渐变,画出一个流星的轮廓。加入从右到左动画效果。

再加入一个外包的div,做一下旋转和缩放。


效果完成!!!!


具体效果,建议查看原文。

代码仓库地址:

https://github.com/shb190802/html5

演示地址:

http://suohb.com/demo/win/starrySky.html

不是还蛮酷的呢?利用周末时间我们来学习并实现一下,本文我们就来一点一点分析怎么实现它!


分析


首先我们看看这个效果具体有哪些要点。首先,这么炫酷的效果肯定是要用到 Canvas 了,每个星星可以看作为一个粒子,因此,整个效果实际上就是粒子系统了。此外,我们可以发现每个粒子之间是相互连接的,只不过离的近的粒子之间的连线较粗且透明度较低,而离的远的则相反。


开始 Coding


HTML 部分


这部分我就简单放了一个 标签,设置样式使其填充全屏。


<canvas height="620" width="1360" id="canvas" style="position: absolute; height: 100%;"/>


然后为了让所有元素没有间距和内部,我还加了一条全局样式:


* {

margin: 0;

padding: 0;

}


JavaScript 部分


下面我们来写核心的代码。首先我们要得到那个 canvas 并得到绘制上下文:


var canvasEl = document.getElementById('canvas');

var ctx = canvasEl.getContext('2d');

var mousePos = [0, 0];


紧接着我们声明两个变量,分别用于存储“星星”和边:


var nodes = [];

var edges = [];


下一步,我们做些准备工作,就是让画布在窗口大小发生变化时重新绘制,并且调整自身分辨率:


window.onresize = function () {

canvasEl.width = document.body.clientWidth;

canvasEl.height = canvasEl.clientHeight;

if (nodes.length == 0) {

constructNodes();

}

render();

};

window.onresize(); // trigger the event manually.


我们在第一次修改大小后构建了所有节点,这里就要用到下一个函数(constructNodes)了


这个函数中我们随机创建几个点,我们用字典对象的方式存储这些点的各个信息:


function constructNodes() {

for (var i = 0; i < 100; i++) {

var node = {

drivenByMouse: i == 0,

x: Math.random() * canvasEl.width,

y: Math.random() * canvasEl.height,

vx: Math.random() * 1 - 0.5,

vy: Math.random() * 1 - 0.5,

radius: Math.random() > 0.9 ? 3 + Math.random() * 3 : 1 + Math.random() * 3

};

nodes.push(node);

}

nodes.forEach(function (e) {

nodes.forEach(function (e2) {

if (e == e2) {

return;

}

var edge = {

from: e,

to: e2

}

addEdge(edge);

});

});

}


为了实现后面一个更炫酷的效果,我给第一个点加了一个 drivenByMouse 属性,这个点的位置不会被粒子系统管理,也不会绘制出来,但是它会与其他点连线,这样就实现了鼠标跟随的效果了。


这里稍微解释一下 radius 属性的取值,我希望让绝大部分点都是小半径的,而极少数的点半径比较大,所以我这里用了一点小 tricky,就是用概率控制点的半径取值,不断调整这个概率阈值就能获取期待的半径随机分布。


点都构建完毕了,就要构建点与点之间的连线了,我们用到双重遍历,把两个点捆绑成一组,放到 edges 数组中。注意这里我用了另外一个函数来完成这件事,而没有直接用 edges.push() ,为什么?


假设我们之前连接了 A、B两点,也就是外侧循环是A,内侧循环是B,那么在下一次循环中,外侧为B,内侧为A,是不是也会创建一条边呢?而实际上,这两个边除了方向不一样以外是完全一样的,这完全没有必要而且占用资源。因此我们在 addEdge 函数中进行一个判断:


function addEdge(edge) {

var ignore = false;

edges.forEach(function (e) {

if (e.from == edge.from & e.to == edge.to) {

ignore = true;

}

if (e.to == edge.from & e.from == edge.to) {

ignore = true;

}

});

if (!ignore) {

edges.push(edge);

}

}


至此,我们的准备工作就完毕了,下面我们要让点动起来:


function step() {

nodes.forEach(function (e) {

if (e.drivenByMouse) {

return;

}

e.x += e.vx;

e.y += e.vy;

function clamp(min, max, value) {

if (value > max) {

return max;

} else if (value < min) {

return min;

} else {

return value;

}

}

if (e.x <= 0 || e.x >= canvasEl.width) {

e.vx *= -1;

e.x = clamp(0, canvasEl.width, e.x)

}

if (e.y <= 0 || e.y >= canvasEl.height) {

e.vy *= -1;

e.y = clamp(0, canvasEl.height, e.y)

}

});

adjustNodeDrivenByMouse();

render();

window.requestAnimationFrame(step);

}

function adjustNodeDrivenByMouse() {

nodes[0].x += (mousePos[0] - nodes[0].x) / easingFactor;

nodes[0].y += (mousePos[1] - nodes[0].y) / easingFactor;

}


看到这么一大段代码不要害怕,其实做的事情很简单。这是粒子系统的核心,就是遍历粒子,并且更新其状态。更新的公式就是


v = v + a

s = s + v


a是加速度,v是速度,s是位移。由于我们这里不涉及加速度,所以就不写了。然后我们需要作一个边缘的碰撞检测,不然我们的“星星”都无拘无束地一点点飞~走~了~。边缘碰撞后的处理方式就是让速度矢量反转,这样粒子就会“掉头”回来。


还记得我们需要做的鼠标跟随吗?也在这处理,我们让第一个点的位置一点一点移动到鼠标的位置,下面这个公式很有意思,可以轻松实现缓动:


x = x + (t - x) / factor


其中 factor 是缓动因子,t 是最终位置,x 是当前位置。至于这个公式的解释还有个交互大神 Bret Victor 在他的演讲中提到过,视频做的非常好,有条(ti)件(zi)大家一定要看看: Bret Victor – Stop Drawing Dead Fish


好了,回到主题。我们在上面的函数中处理完了一帧中的数据,我们要让整个粒子系统连续地运转起来就需要一个timer了,但是十分不提倡大家使用 setInterval,而是尽可能使用 requestAnimationFrame,它能保证你的帧率锁定在


剩下的就是绘制啦:


function render() {

ctx.fillStyle = backgroundColor;

ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);

edges.forEach(function (e) {

var l = lengthOfEdge(e);

var threshold = canvasEl.width / 8;

if (l > threshold) {

return;

}

ctx.strokeStyle = edgeColor;

ctx.lineWidth = (1.0 - l / threshold) * 2.5;

ctx.globalAlpha = 1.0 - l / threshold;

ctx.beginPath();

ctx.moveTo(e.from.x, e.from.y);

ctx.lineTo(e.to.x, e.to.y);

ctx.stroke();

});

ctx.globalAlpha = 1.0;

nodes.forEach(function (e) {

if (e.drivenByMouse) {

return;

}

ctx.fillStyle = nodeColor;

ctx.beginPath();

ctx.arc(e.x, e.y, e.radius, 0, 2 * Math.PI);

ctx.fill();

});

}


常规的 Canvas 绘图操作,注意 beginPath 一定要调用,不然你的线就全部穿在一起了… 需要说明的是,在绘制边的时候,我们先要计算两点距离,然后根据一个阈值来判断是否要绘制这条边,这样我们才能实现距离远的点之间连线不可见的效果。


到这里,我们的整个效果就完成了。如果不明白大家也可以去GitHub项目: CyandevToys / ParticleWeb去看完整的源码。Have fun!!

源自:http://www.jianshu.com/p/f5c0f9c4bc39

声明:文章著作权归作者所有,如有侵权,请联系小编删除。