canvas入门笔记| 炫酷星空图星轨运动设计思路

2,561 阅读3分钟

最近在学习canvas,掌握了理论知识后,以星空图为例实践下。示例样式

1、画背景布

<canvas>是HTML的一个标签,可以直接设置widthheight

html:

<canvas id="myCanvas">
Your browser does not support HTML5 Canvas.//添加文本,用于不支持canvas的浏览器
</canvas>

在js中,也可以直接通过document.createElement('canvas')创建<canvas>元素,并通过appendChild('canvas')插入。

引用并且初始化背景布 js:

var canvas = document.getElementById('myCanvas');//引用canvas元素
var ctx= canvas.getContext('2d');//获取2D环境
canvas.width=window.innerWidth;
canvas.height=window.innerHeight;//使canvas画布的大小为整个屏幕的大小

2、画星星

画星星我们需要另外的canvas元素,而且这个元素应该是动态创建的

var canvas2=document.createElement('canvas');//创建另外的canvas元素
var ctx2=canvas2.getContext('2d');

绘制圆形

星星的绘制基础图形就是圆形,需要用到arc()

  • 角度和弧度 在css的旋转中,用到的是角度(deg),而canvas中用到的是弧度(rad)
rad = (π / 180) * deg //2π即为一周的弧度
deg = (rad * 180) / π

在js中πMath.PI

  • 绘制圆和圆弧
arc(x, y, radius, startRad, endRad, [anticlockwise]);

其中:
(x,y):绘制的圆/圆弧的圆心坐标;
radius:半径
startRad:起始弧度;
endRad:终止弧度;
anticlockwise:绘制方向,true逆时针,false逆时针(默认);

image.png 绘制圆形时,startRad=0,endRad=2π

ctx2.arc(50, 50, 50, 0, Math.PI * 2);//画一个圆(x,y,半径,起始弧度,结束弧度)

渐变

示例中的星空是有渐变效果的,canvas中的渐变分为两种,线性渐变径向渐变

  • 线性渐变
ctx.createLinearGradient(x1,y1,x2,y2);//(x1,y1)渐变起始点,(x2,y2)渐变终点

x1=x2,y1≠y2垂直的线性渐变;
x1≠x2,y1=y2水平的线性渐变;
x1≠x2,y1≠y2角度的线性渐变;

  • 径向渐变
ctx.createRadialGradient(x1,y1,r1,x2,y2,r2);//起始圆:圆心(x1,y1) 半径r1;结束圆:圆心(x2,y2) 半径r2

  • 添加渐变颜色
gradient.addColorStop(position,color)

position:指定渐变中颜色所在的相对位置(0~1);
color:指定渐变中的颜色

  • 填充渐变
ctx.fillStyle=gradient;

画一个渐变颜色的星星

var gradient=ctx2.createRadialGradient(50,50,0,50,50,50);
gradient.addColorStop(0,'#fff');
gradient.addColorStop(0.1, 'hsl(217, 61%, 33%)');
gradient.addColorStop(0.25, 'hsl(217, 64%, 6%)');
gradient.addColorStop(1, 'transparent');
ctx2.fillStyle = gradient;
ctx2.beginPath();
ctx2.arc(50, 50, 50, 0, Math.PI * 2);//画一个圆(x,y,半径,起始弧度,结束弧度)
ctx2.fill();
  • 渲染到画布上
ctx.drawImage(canvas2, 200,200, 50, 50);//在canvas绘制canvas2(img,x,y,w,h)

此时你会得到一颗星星

image.png

  • 很多颗星星 将渲染放在循环中,位置由随机数生成,你会得到很多颗星星
var starNum=1000;//星星数量
function drawStar(){
    for(let i=0;i<starNum;i++){
        let starX=Math.random()*w;
	let starY=Math.random()*h;//随机生成星星的位置
	ctx.drawImage(canvas2, starX,starY, 50, 50);//在canvas绘制canvas2(img,x,y,w,h)
    }
}
drawStar();

image.png

星星的随机性

而不难发现示例中的星空,为了真实的效果,四周的星星大一些,中间的星星小一些

image.png

因此我们的随机数里除了位置随机,星星的wh也应当遵从一定的规律 分析:我们看到星星沿着星轨运动可以看作是沿着一个一个的同心圆运动

image.png 如上图,我们找到屏幕的视觉中心,越靠近里面的运动越慢,星星直径越小,越靠近外面的,直径越大,运动越快,当然为了真实一点,用随机数来取。

参考了很多大佬的算法,得出如下结果:

  • 首先取到屏幕中心点距离,即最外层圆(最大轨道)的半径
    function maxOrbit(x, y) {
        var max = Math.max(x, y),
            diameter=Math.round(Math.sqrt(x*x+y*y))
        return diameter / 2;//矩形对角线的一半
    }
    
    //在[min,max]随机取一个整数(包括min,max)
   function random(min, max) {
        if (arguments.length < 2) {
            max = min;
            min = 0;
        }//如果只传了一个min,则max取min,min取0

        if (min > max) {
            var hold = max;
            max = min;
            min = hold;
        }//min>max,互换

        return Math.floor(Math.random() * (max - min + 1)) + min;
    }
  • 为了使其是以一个同心圆散开,因此定位x/y是以屏幕w/h的一半+/-一个在0到最大圆半径之间的随机数的值。可以借助三角函数sin()/cos(),其周期是-1~1之间
var orbitRadius = random(maxOrbit(w, h));//[0,最大轨道半径]之间取随机数,这个随机数作为基础的随机变量
var orbitX = w / 2;//屏幕宽的一半
var orbitY = h / 2;//屏幕高的一半
function drawStar(){
    for(let i=0;i<starNum;i++){
        var starX = Math.sin(i) * orbitRadius + orbitX,//sin和cos可能只是想要个-1~1之间的随机数
    starY = Math.cos(i) * orbitRadius + this.orbitY,//orbitRadius越大,越靠近边缘,w和h越有可能大,因此大的星星都在四周(也存在小的)
	ctx.drawImage(canvas2, starX,starY, 50, 50);//在canvas绘制canvas2(img,x,y,w,h)
    }
}
drawStar();

此时可以得到一个随机的圆环 image.png

  • 星星的大小,星星的w/h也改为随机数
var radius = random(60, this.orbitRadius) / 12;//至于这个变量依据什么,我只能说:借鉴
function drawStar(){
    for(let i=0;i<starNum;i++){
        var starX = Math.sin(i) * orbitRadius + orbitX,//sin和cos只是想要个-1~1之间的随机数
    starY = Math.cos(i) * orbitRadius + this.orbitY,//orbitRadius越大,越靠近边缘,w和h越有可能大,因此大的星星都在四周(因为取随机数,也存在小的)
	ctx.drawImage(canvas2, starX,starY, radius, radius);//在canvas绘制canvas2(img,x,y,w,h)
    }
}
drawStar();

此时我们得到一个近大远小的圆环 image.png

image.png

  • 重复调用drawStar,利用window.requestAnimationFrame()画出多层
var orbitX = w / 2;//屏幕宽的一半
var orbitY = h / 2;//屏幕高的一半
var orbitRadius,radius;
function drawStar(){
    orbitRadius = random(maxOrbit(w, h));//[0,最大轨道半径]之间取随机数
    radius=random(60, this.orbitRadius) / 12;//
    for(let i=0;i<starNum;i++){
        var starX = Math.sin(i) * orbitRadius + orbitX,//sin和cos可能只是想要个-1~1之间的随机数
            starY = Math.cos(i) * orbitRadius + orbitY;//orbitRadius越大,越靠近边缘,w和h越有可能大,因此大的星星都在四周(也存在小的)
	 ctx.drawImage(canvas2, starX,starY, radius, radius);//在canvas绘制canvas2(img,x,y,w,h)
    }
    window.requestAnimationFrame(drawStar);
}
drawStar();

此时,虽然已经达到了想要的效果,还是不像星空,因为,太规律了,因此我们需要加一些随机因素 image.png

  • 随机因素,就在于,sin()cos()里的数字我们取的是循环i,这样它就会规律地出现位置,我们把i换位[0,starNum]中的一个随机数
var orbitX = w / 2;//屏幕宽的一半
var orbitY = h / 2;//屏幕高的一半
var orbitRadius,radius,timePassed;
function drawStar(){
    orbitRadius = random(maxOrbit(w, h));//[0,最大轨道半径]之间取随机数
    radius=random(60, this.orbitRadius) / 12;
    starCount = random(0, starNum)
    for(let i=0;i<starNum;i++){
        var starX = Math.sin(starCount) * orbitRadius + orbitX,//sin和cos可能只是想要个-1~1之间的随机数
            starY = Math.cos(starCount) * orbitRadius + orbitY;//orbitRadius越大,越靠近边缘,w和h越有可能大,因此大的星星都在四周(也存在小的)
	 ctx.drawImage(canvas2, starX,starY, radius, radius);//在canvas绘制canvas2(img,x,y,w,h)
    }
    window.requestAnimationFrame(drawStar);
}
drawStar();

你会得到一个闪烁的星空图(是在不断绘制新的星星) 屏幕录制2021-12-03 16.34.41.gif

星空动起来

原理:以上的设计,每次调用window.requestAnimationFrame()实际只画了一颗星星,而想让其动起来,每次是画了1000颗星星,调用window.requestAnimationFrame()后,重新画1000颗星星,以达到旋转的效果

  • 改进,我们将drawStar()封装到原型,而只在animate调用时使用window.requestAnimationFrame()

完整代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style type="text/css">
    body {
        background:#060e1b;
        overflow: hidden;
    }
    </style>
</head>

<body>
    <canvas id="canvas"></canvas>
    <script type="text/javascript">
    var canvas = document.getElementById('canvas'),
        ctx = canvas.getContext('2d'),
        w = canvas.width = window.innerWidth,
        h = canvas.height = window.innerHeight,
        hue = 217,
        stars = [],
        count = 0,
        maxStars = 1400;

    var canvas2 = document.createElement('canvas'),
        ctx2 = canvas2.getContext('2d');
    canvas2.width = 100;
    canvas2.height = 100;
    var half = canvas2.width / 2,
        gradient2 = ctx2.createRadialGradient(half, half, 0, half, half, half);
    gradient2.addColorStop(0.025, '#fff');
    gradient2.addColorStop(0.1, 'hsl(' + hue + ', 61%, 33%)');
    gradient2.addColorStop(0.25, 'hsl(' + hue + ', 64%, 6%)');
    gradient2.addColorStop(1, 'transparent');

    ctx2.fillStyle = gradient2;
    ctx2.beginPath();
    ctx2.arc(half, half, half, 0, Math.PI * 2);//画一个圆(x,y,半径,起始弧度,结束弧度)
    ctx2.fill();

    function random(min, max) {
        if (arguments.length < 2) {
            max = min;
            min = 0;
        }//如果只传了一个min,则max取min,min取0

        if (min > max) {
            var hold = max;
            max = min;
            min = hold;
        }//min>max,互换

        return Math.floor(Math.random() * (max - min + 1)) + min;//在[min,max]随机取一个整数(包括min,max)
    }

    function maxOrbit(x, y) {
        var max = Math.max(x, y),
            // diameter = Math.round(Math.sqrt(max * max + max * max));//Math.round取整
            diameter=Math.round(Math.sqrt(x*x+y*y))
        return diameter / 2;//矩形对角线的一半
    }

    var Star = function() {

        this.orbitRadius = random(maxOrbit(w, h));
        this.radius = random(60, this.orbitRadius) / 12;
        this.orbitX = w / 2;
        this.orbitY = h / 2;
        this.timePassed = random(0, maxStars);
        this.speed = random(this.orbitRadius) / 50000;
        this.alpha = random(2, 10) / 10;//0.2-1之间的透明度

        count++;
        stars[count] = this;
    }

    Star.prototype.draw = function(i) {
        var x = Math.sin(this.timePassed) * this.orbitRadius + this.orbitX,//sin和cos可能只是想要个-1~1之间的随机数
            y = Math.cos(this.timePassed) * this.orbitRadius + this.orbitY,//orbitRadius越大,越靠近边缘,w和h越有可能大,因此大的星星都在四周(也存在小的)
            twinkle = random(10);

       // 增加一些闪烁的效果
        if (twinkle === 1 && this.alpha > 0) {
            this.alpha -= 0.05;
        } else if (twinkle === 2 && this.alpha < 1) {
            this.alpha += 0.05;
        }
   
        ctx.globalAlpha = this.alpha;//设置图像的透明度
        ctx.drawImage(canvas2, x - this.radius / 2, y - this.radius / 2, this.radius, this.radius);//在canvas绘制canvas2(img,x,y,w,h)
        this.timePassed += this.speed;//转起来
    }

    for (var i = 0; i < maxStars; i++) {
        new Star();
    }

    function animation() {
        ctx.globalCompositeOperation = 'source-over';//图像合成,源图形不透明地方显示源图形,其余显示目标图形
        ctx.globalAlpha = 0.8;//图像透明度
        ctx.fillStyle = 'hsla(' + hue + ', 64%, 6%, 1)';
        ctx.fillRect(0, 0, w, h)

        ctx.globalCompositeOperation = 'lighter';//显示源图像+目标图像
        for (var i = 1, l = stars.length; i < l; i++) {
            stars[i].draw(i);
        };
        window.requestAnimationFrame(animation);
    }

    animation();
    </script>
</body>

</html>

canvas图像合成

合成是指如何精细控制画布上对象的透明度和分层效果。在默认情况之下,如果在Canvas之中将某个物体(源)绘制在另一个物体(目标)之上,那么浏览器就会简单地把源特体的图像叠放在目标物体图像上面。

  • globalAlpha:设置图像的透明度。默认值为1,表示完全不透明,这个值必须设置在图像绘制之前。
  • globalCompositeOperation:该属性的值在globalAlpha以及所有变换都生效后控制当前canvas位图中绘制图形。(共有26个值)可参考此链接

其中以上代码中: source-over:(默认值)源图形覆盖目标图形,源图形不透明的地方显示源图形,其余显示目标图形 image.png lighter:两图形中重叠部分作加色处理

image.png

最终效果:

屏幕录制2021-12-02 16.17.28.gif