canvas繪制太陽系


原文地址:canvas繪制太陽系
學習canvas有一段時間了,順便寫個小項目練手,該項目用到的知識點包括:

  1. ES6面向對象
  2. 基本的三角函數
  3. canvas部分有:坐標變換,漸變,混合模式,線條和圖形的繪制。

實際效果: solar system(推薦在chrome或safari下運行)

場景

首先建立場景類,主要用來組織管理對象,統一更新和繪制對象。這里用到了ES6的類語法,構造函數建立對象列表屬性planets,繪制背景方法drawBG,使用requestAnimationFrame反復執行的動畫方法animate

繪制背景使用到了徑向漸變:createRadialGradient(x1,y1,r1,x2,y2,r2); 該漸變主要用於創建兩個圓相交過渡效果,如果前后兩個圓心相同(x1x2 && y1y2),則會構造同心圓樣式的漸變。 這樣我們就以太陽為中心的黃色調漸變到黑色,最后用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);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM