canvas绘制太阳系

原文地址:canvas绘制太阳系
学习canvas有一段时间了,顺便写个小项目练手,该项目用到的知识点包括:

  1. ES6面向对象
  2. 基本的三角函数
  3. canvas部分有:坐标变换,渐变,混合模式,线条和图形的绘制。

实际效果: solar system

solar system

场景

首先建立场景类,主要用来组织管理对象,统一更新和绘制对象。这里用到了ES6的类语法,构造函数建立对象列表属性planets,绘制背景方法drawBG,使用requestAnimationFrame反复执行的动画方法animate

绘制背景使用到了径向渐变:createRadialGradient(x1,y1,r1,x2,y2,r2); 该渐变主要用于创建两个圆相交过渡效果,如果前后两个圆心相同(x1==x2 && y1==y2),则会构造同心圆样式的渐变。 这样我们就以太阳为中心的黄色调渐变到黑色,最后用fillRect填充整个背景。

//场景
class Stage {
    constructor(){
        this.planets=[];
    }
    init(ctx){
        ctx.translate(W/2,H/2);//坐标重置为中间
        this.animate(ctx);
    }
    //绘制背景
    drawBG(ctx){
        ctx.save();
        ctx.globalCompositeOperation = "source-over";
        var gradient=ctx.createRadialGradient(0,0,0,0,0,600);
        gradient.addColorStop(0,'rgba(3,12,13,0.1)');
        gradient.addColorStop(1,'rgba(0,0,0,1');
        ctx.fillStyle=gradient;
        // ctx.fillStyle='rgba(0,0,0,0.9)';
        ctx.fillRect(-W/2,-H/2,W,H);
        ctx.restore();
    }
    //执行动画
    animate(ctx){
        var that=this,
            startTime=new Date();
        (function run(){
            that.drawBG(ctx);
            that.planets.forEach(item=>{
                item.update(startTime);
                item.draw(ctx);
            });
            requestAnimationFrame(run);
        }());
    }
}

星球

然后建立星球基类,除构造函数,还有更新位置角度的方法Update,对象绘制方法draw。之后所有的星球,都会初始化该类或者继承该类建立对应星球。

行星绕太阳做圆周运动,这个可以用三角函数根据角度和半径求出x,y,但还有更加方便的方法,那就是使用canvas提供的坐标旋转方法rotate,以360度为一个周期。

/**
 * 星球基类
 */
class Planet{
    /**
     * @param  {Number} x         x坐标
     * @param  {Number} y         y坐标
     * @param  {Number} r         半径
     * @param  {Number} duration  周期(秒)
     * @param  {Object} fillStyle 
     * @param  {Object} blurStyle 
     */
    constructor(x,y,r,duration,fillStyle,blurStyle){
        this.x=x;
        this.y=y;
        this.r=r;
        this.duration=duration;
        this.angle=0;
        this.fillStyle=fillStyle;
        this.blurStyle=blurStyle;
    }
    update(startTime){
        this.angle=Tween.linear(new Date()-startTime,0,Math.PI*2,this.duration*1000);
    }
    draw(ctx){
        ctx.save();
        ctx.rotate(this.angle);
        // ctx.translate(this.x,this.y);
        drawCircle(this.x,this.blurStyle.color);
        ctx.beginPath();
        // ctx.globalCompositeOperation = "lighter";
        ctx.fillStyle=this.fillStyle;
        ctx.shadowColor=this.blurStyle.color;
        ctx.shadowBlur=this.blurStyle.blur;             
        // ctx.arc(0,0,this.r,Math.PI*2,false);
        ctx.arc(this.x,this.y,this.r,0,Math.PI*2,false);
        ctx.fill();
        ctx.restore();
    }
};

太阳

开始建立第一个对象-太阳,继承上面的星球基类Planet,重写draw方法

/**
 * 太阳
 */
class Sun extends Planet{
    draw(ctx){
        ctx.save();
        ctx.beginPath();
        ctx.globalCompositeOperation = "source-over";
        ctx.fillStyle=this.fillStyle;
        ctx.shadowColor=this.blurStyle.color;
        ctx.shadowBlur=this.blurStyle.blur;             
        ctx.arc(this.x,this.y,this.r,0,Math.PI*2,false);
        ctx.fill();
        ctx.restore();  
    }
}

土星

土星有美丽的土星环,所以也继承出一个单独的类,重写draw方法,其中土星环比较麻烦,建立了很多颜色节点的径向渐变。

/**
 * 土星
 */
class Saturn extends Planet{
    draw(ctx){
        ctx.save();
        ctx.rotate(this.angle);
        drawCircle(this.x,this.blurStyle.color);

        ctx.beginPath();
        ctx.fillStyle=this.fillStyle;           
        ctx.arc(this.x,this.y,this.r,Math.PI*2,false);
        ctx.fill();

        //土星光环
        ctx.globalCompositeOperation = "source-over";
        var gradient=ctx.createRadialGradient(this.x,this.y,0,this.x,this.y,this.r+25);
        var startStop=(this.r+3)/(this.r+24);
        gradient.addColorStop(startStop,'#282421');
        gradient.addColorStop(startStop+0.06,'#282421');
        gradient.addColorStop(startStop+0.1,'#7e7966');
        gradient.addColorStop(startStop+0.18,'#706756');
        gradient.addColorStop(startStop+0.24,'#7e7966');
        gradient.addColorStop(startStop+0.25,'#282421');
        gradient.addColorStop(startStop+0.26,'#282421');
        gradient.addColorStop(startStop+0.27,'#807766');
        gradient.addColorStop(1,'#595345');
        ctx.fillStyle=gradient;
        ctx.beginPath();
        ctx.arc(this.x,this.y,this.r+24,0,Math.PI*2,true);
        ctx.arc(this.x,this.y,this.r+3,0,Math.PI*2,false);
        ctx.fill();
        ctx.restore();  
    }
}

建立星球

接着开始初始化星球对象,包括太阳和八大行星,然后所有的星球颜色都使用了径向渐变,这样更加的美观。这里给出太阳,水星,土星的例子,其他的行星如此类推。

// 初始化场景类
var stage=new Stage();

// sun
var sunStyle=ctx.createRadialGradient(0,0,0,0,0,60);
sunStyle.addColorStop(0,'white');
sunStyle.addColorStop(0.5,'white');
sunStyle.addColorStop(0.8,'#ffca1e');
sunStyle.addColorStop(1,'#b4421d');
var sun=new Sun(0,0,60,0,sunStyle,{color:'#b4421d',blur:300});
stage.planets.push(sun);

// mercury
var mercuryStyle=ctx.createRadialGradient(100,0,0,100,0,9);
mercuryStyle.addColorStop(0,'#75705a');
mercuryStyle.addColorStop(1,'#464646');
var mercury=new Planet(100,0,9,8.77,mercuryStyle,{color:'#464646'});
stage.planets.push(mercury);


//saturn 
var saturnStyle=ctx.createRadialGradient(500,0,0,500,0,26);
saturnStyle.addColorStop(0,'#f2e558');
saturnStyle.addColorStop(1,'#4c4a3b');
var saturn =new Saturn(500,0,26,1075.995,saturnStyle,{color:'#4c4a3b'});
stage.planets.push(saturn);

小行星带

当然还有火星和木星之间的小行星带,同理继承星球基类,这里用到了图像混合模式globalCompositeOperation,使用xor可以和背景对比度没那么突兀。当然还有其他属性值,比如source-over, lighter等。这里我们随机生成了300个对象,一样填充进场景类的planets属性统一更新绘制。

/**
 * 小行星
 */
class Asteroid extends Planet{
    draw(ctx){
        ctx.save();
        ctx.rotate(this.angle);
        ctx.beginPath();
        ctx.globalCompositeOperation = "xor";
        ctx.fillStyle=this.fillStyle;           
        ctx.arc(this.x,this.y,this.r,0,Math.PI*2,false);
        ctx.fill();
        ctx.restore();  
    }
}

function createAsteroids(){
    var asteroid=null,
        x=300,y=0, r=2,rd=300,
        angle=0, d=283, 
        color='#fff';
    for(var i=0;i<400;i++){
        rd=Random(300,320);
        angle=Random(0,Math.PI*2*1000)/1000;
        x=Math.round(Math.cos(angle)*rd);
        y=Math.round(Math.sin(angle)*rd);
        r=Random(1,3);
        d=Random(28.3,511);
        color=getAsteroidColor();
        // console.log(angle,color);
        asteroid = new Asteroid(x,y,r,d,color,{color:color,blur:1});
        stage.planets.push(asteroid);
    }
}

彗星

基本快完成了,但我们除此之外,可以再添加做椭圆运动的彗星,这样更加酷。一样随机生成20个彗星填充进场景类统一更新绘制。

/**
 * 彗星
 */
class Comet {
    constructor(cx,cy,a,b,r,angle,color,duration){
        this.a=a;
        this.b=b;
        this.r=r;
        this.cx=cx;
        this.cy=cy;
        this.x=0;
        this.y=0;
        this.color=color;
        this.angle=angle;
        this.duration=duration;
    }
    update(startTime){
        var t=Tween.linear(new Date()-startTime,0,Math.PI*2,this.duration*1000);
        this.x=this.cx+this.a*Math.cos(this.angle+t);
        this.y=this.cy+this.b*Math.sin(this.angle+t);
    }
    draw(){
        ctx.save();
        ctx.rotate(this.angle);
        //画运动轨迹
        ctx.lineWidth=0.5;
        ctx.strokeStyle='rgba(15,69,116,0.2)';
        Shape.ellipse(ctx,this.cx,this.cy,this.a,this.b);

        //画球
        ctx.beginPath();
        // ctx.globalCompositeOperation = "lighter";
        ctx.globalCompositeOperation = "source-atop";
        ctx.shadowColor=this.color;
        ctx.shadowBlur=1;
        ctx.fillStyle=this.color;
        ctx.arc(this.x,this.y,this.r,0,Math.PI*2,false);
        ctx.fill();
        //画尾迹
        ctx.restore();
    }
}

function createComets(){
    var l=180,
        a=800,b=300,
        cx=a-l,cy=0,
        r=3,duration=30,angle=0,color='#fff',
        comet = null;
    for(var i=0;i<20;i++){
        l=Random(120,350)
        a=Random(600,1000);
        b=a/Random(1,3);
        cx=a-l;
        r=Random(2,4);
        angle=Random(0,Math.PI*2*1000)/1000;
        color=getCometColor();
        duration=Random(20,100);
        stage.planets.push(new Comet(cx,cy,a,b,r,angle,color,duration));
    }
}

运动轨迹

最后的细节,就是标识出行星圆周运动的轨道,当然最简单的是按运动半径画个圆。但我们用线性渐变添加好看的尾迹,这样效果更好

function drawCircle(r,color){
    var hsl=Color.hexToHsl(color);
    ctx.lineWidth=1;
    // ctx.strokeStyle='rgba(176,184,203,0.3)';
    // ctx.arc(0,0,this.x,Math.PI*2,false);
    // ctx.stroke();
    var gradient=ctx.createLinearGradient(-r,0,r,0);
    gradient.addColorStop(0,'hsla('+hsl[0]+','+hsl[1]+'%,0%,.3)');
    gradient.addColorStop(0.6,'hsla('+hsl[0]+','+hsl[1]+'%,50%,.9)');
    gradient.addColorStop(1,'hsla('+hsl[0]+','+hsl[1]+'%,80%,1)');
    ctx.strokeStyle=gradient;
        // ctx.shadowColor=color;
        // ctx.shadowBlur=4;    
    ctx.beginPath();
    ctx.arc(0,0,r,0,Math.PI,true);
    ctx.stroke();
}

最后

所有的部分都已经完成,我们只需要启动场景类即可

createAsteroids();
createComets();
stage.init(ctx);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容