1. 程式人生 > >文藝碼農~Canvas手把手教你如何繪制一輛跑車

文藝碼農~Canvas手把手教你如何繪制一輛跑車

直接 nim 建築 ogr 一條直線 asp 規律 說明 發現

技術分享圖片

  • 作者:首席填坑官?蘇南
  • 來源:@IT·平頭哥聯盟
  • 交流:912594095,公眾號:honeyBadger8;本文原創,著作權歸作者所有,轉載請註明原鏈接及出處。

前言

  靈感來源於前些天撿到錢了,就想著是時候給自己買輛車了,工作這麽多年了應該對自己好一點,在網上搜索了一下看到這個車型。其實幾年前是買過一輛的,但是不到一個月就被人偷了,傷心了好久。這次一定鎖好,上三把鎖保證小偷再也偷不走了,於是我拿著錢去買了些益力多,跟同事分享了,心情還是比較愉悅的。—— @IT·平頭哥聯盟,我是首席填坑官?蘇南(South·Su) ^_^~

  但想來作為一名程序(嗯,還是個菜鳥,專業首席填坑官哦),車基本是用不上的啦,為啥?因為有改不完的bug,記得剛畢業那時候最大的夢想是:“撩個妹子 攜手仗劍天涯,懲奸除惡、劫富濟貧,快意人生~”,無奈一入IT深似海,從此BUG改不完啊。所以還是多學習吧,這不就學著畫了個車滿足一下自己的心裏安慰,在這裏把大家一起分享一下,唉,有點扯偏了~,大家先來看一下最終的效果圖吧!

技術分享圖片
  

過程解析:

  效果已經看了到,有沒有感覺很牛B??其實也就一般般啦~,接下來就讓我帶大家一起分解一下它的實現過程吧
  canvas中文名中:畫布,它就跟我們在紙上畫畫一樣,畫某樣東西之前,我們要先學會構思、拆解你要畫的東西,就跟汽車、手機等東西一樣,一個成品都是由很多零件組成的,當你拆解開來,一點點完成再組裝的,就會變的容易的多。

  • 繪制地平線
    • 首先我們基於畫布的高度取一定的比例,在底部畫一條線;
    • 從觀察動畫,它還有幾個點,這個是用於視差滾動的時候,來欺騙我們的眼睛的,直接一條線肯定再怎麽動也沒有用,點的移動可以形成一個動畫的效果;
    • 再加一點修飾,幾個點移動有點太單調了,大家可以想像一下,當你騎車的時候,車的速度
      與周圍的事物、建築、人產生一個交差,那種感覺是很刺激的,那麽我們也來加一點東西,讓動畫看起來更豐富一些,我選擇了 三條線,線本身有個漸變過渡的效果,比純色要靈動些動畫看起來更逼真,而且初始它是不在畫布範圍內的,這個點要註意一下;
    • 下面的兩張圖,第二張是生成gif工具裏截出來的,它就是動畫的分解,其實所謂的動畫,也是由一張張靜態圖組成,然後快速過渡,讓視覺形成了視差,最後欺騙了大腦,我看見動畫了……
    • 知識點lineTostrokeStylestrokerestore等,這裏不一一講解了,如有不了解可自行查看 w3school API

技術分享圖片

技術分享圖片

  horizon(){
      /**
      * 輪子的底部,也稱地平線:
      1.清除畫布
      2.畫一條直線,且高度6px
      本文@IT·平頭哥聯盟-首席填坑官?蘇南分享,非商業轉載請註明原鏈接及出處
       */

      this.wheelPos = [];
      this.ctx.save();//首席填坑官?蘇南的專欄 交流:912594095、公眾號:honeyBadger8
      this.ctx.clearRect(0, 0, this.canvasW, this.canvasH);

      let horizonX = 0,horizonY = this.canvasH-100;
      this.ctx.beginPath();
      this.ctx.strokeStyle = this.color;
      this.ctx.lineWidth=6;
      this.ctx.moveTo(horizonX,horizonY);
      this.ctx.lineTo(this.canvasW,horizonY);
      this.ctx.closePath();
      this.ctx.stroke();

      Array.from({length:5}).map((k,v)=>{
          let dotProportion = (this.canvasW*0.49)*v-this.oneCent;
          this.wheelPos.push({x:dotProportion,y:horizonY-this.wheelRadius});
          let startX = dotProportion-(this.animateNum*2); //用於動畫滾動移動
          this.ctx.beginPath();
          this.ctx.strokeStyle = "#f9f8ef";
          this.ctx.lineWidth=6;
          this.ctx.moveTo(startX,horizonY);
          this.ctx.lineTo(startX+5,horizonY);
          this.ctx.closePath();
          this.ctx.stroke();
      });
      this.ctx.restore();
      this.shuttle();
      // this.wheel();
  }
  shuttle(){
      /**
      * 畫幾根橫線,有點視差,感覺騎車在飛速穿梭的感覺:
      本文@IT·平頭哥聯盟-首席填坑官?蘇南分享,非商業轉載請註明原鏈接及出處
       */
      let shuttleX = this.canvasW+100,
              shuttleY = this.canvasH/6;
      let shuttleW = shuttleX+100;
      [0,40,0].map((k,v)=>{
          let random = Math.random()+2;
          let x = shuttleX+k-(this.animateNum*(2.2*random));
          let y = shuttleY+v*24;
          let w = shuttleW+k-(this.animateNum*(2.2*random));
          let grd=this.ctx.createLinearGradient(x,y,w,y);
          grd.addColorStop(0,"#30212c");
          grd.addColorStop(1,"#fff");
          this.ctx.beginPath();
          this.ctx.lineCap="round";
          this.ctx.strokeStyle = grd;
          this.ctx.lineWidth=3;//首席填坑官?蘇南的專欄 交流:912594095、公眾號:honeyBadger8
          this.ctx.moveTo(x,y);
          this.ctx.lineTo(w,y);
          this.ctx.stroke();
          this.ctx.closePath();

      });

  }
  • 繪制車輪

    • 接下來我們來畫車的兩個輪子,輪子的位置在哪裏呢?我也是觀察了有一會才發現的,其實剛才的地平線,兩點的位置,就是車輪的中心點;
    • 所以在剛才繪制點的時候,就記錄了5個點的坐標,這樣就省去了一次計算,中間有兩次是我們需要的
    • 知識點arcfill
      技術分享圖片
  console.log(this.wheelPos);
  this.wheelPos = this.wheelPos.slice(1,3); //這裏取1-3
  console.log(this.wheelPos);
  this.wheelPos.map((wheelItem,v)=>{
    let wheelItemX = wheelItem.x, 
    wheelItemY= wheelItem.y-this.wheelBorder/1.5;

    //外胎
    this.ctx.beginPath();
    this.ctx.lineWidth=this.wheelBorder;
    this.ctx.fillStyle = "#f5f5f0";
    this.ctx.strokeStyle = this.color;
    this.ctx.arc(wheelItemX,wheelItemY,this.wheelRadius,0,Math.PI*2,false);
    this.ctx.closePath();
    this.ctx.stroke();
    this.ctx.fill();

    //最後兩輪胎中心點圓軸承
    this.axisDot(wheelItemX,wheelItemY);
    this.ctx.restore();

  });
  this.ctx.restore();
  • 同理,上面畫好了兩個圓,但車輪肯定有軸承,前後輪我做了些汪樣的處理,後輪是實心的加了個填充;
  • 前輪是畫了一點斷點的圓,用於動畫的轉動,
  • 在外輪的半徑上進行縮小一定比較,畫內圈,這裏我取了外圈的.94,作為內圓的半徑,
  • 還加了兩個半圓的描邊修飾,讓動畫跑起來的時候,車輪有動起來的感覺,半圓 Math.PI 就是一個180,(Math.PI * degrees) / 180; degrees 就是我們想要繪制的起始/結束角度;
  • 從下圖可以看出,圓的填充用了 放射性漸變,createRadialGradient-創建放射狀/環形的漸變(用在畫布內容上)
    技術分享圖片

技術分享圖片

技術分享圖片

  context.createRadialGradient(x0,y0,r0,x1,y1,r1);
  + createRadialGradient API 說明:
    x0 = 漸變的開始圓的 x 坐標
    y0 = 漸變的開始圓的 y 坐標
    r0 = 開始圓的半徑
    x1 = 漸變的結束圓的 x 坐標
    y1 = 漸變的結束圓的 y 坐標
    r1 = 結束圓的半徑

    詳細使用請看下面代碼的實例

  let scaleMultiple = this.wheelRadius*.94;
  let speed1 = this.animateNum*2; //外圈半圓速度
  let speed2 = this.animateNum*3; //內小圈半圓速度
  //後輪
  if(v === 0){

    //內圓
    this.ctx.beginPath();
    let circleGrd=this.ctx.createRadialGradient(wheelItemX,wheelItemY,18,wheelItemX,wheelItemY,scaleMultiple);
      circleGrd.addColorStop(0,"#584a51");
      circleGrd.addColorStop(1,"#11090d");
    this.ctx.fillStyle = circleGrd;
    this.ctx.arc(wheelItemX,wheelItemY,scaleMultiple,0,Math.PI*2,false);
    this.ctx.fill();
    this.ctx.closePath();

    //兩個半圓線

    [
      {lineW:2,radius:scaleMultiple*.6,sAngle:getRads(-135+speed1) , eAngle:getRads(110+speed1)},
      {lineW:1.2,radius:scaleMultiple*.45,sAngle:getRads(45+speed2) , eAngle:getRads(-50+speed2)}
    ].map((k,v)=>{
      this.ctx.beginPath();
      this.ctx.lineCap="round";
      this.ctx.strokeStyle ="#fff";
      this.ctx.lineWidth=k.lineW;
      this.ctx.arc(wheelItemX,wheelItemY,k.radius,k.sAngle,k.eAngle,true);
      this.ctx.stroke();
      this.ctx.closePath();

    });
    this.ctx.restore();

  }
  • 接下來我們就拿前輪開刀
  • 前輪也是畫了幾個半圓,大概就是以某個角度為起點,然後分別畫幾個半圓,整體是一個半徑,中間有斷開,如: eAngle = [0,135,270], sAngle = [-45,0,180];就能畫出如下圖的圓:

技術分享圖片

技術分享圖片

  • 具體實現請看下面代碼
  //兩個圓,再縮小一圈,畫線圓
  Array.from({length:3}).map((k,v)=>{
    let prevIndex = v-1 <= 0 ? 0 : v-1;
    let eAngle = v*135, sAngle = -45+(prevIndex*45)+v*90;
    let radius = scaleMultiple*.75;
    let _color_ = "#120008";
    this.ctx.beginPath();
    this.ctx.lineCap="round";
    this.ctx.strokeStyle = _color_;
    this.ctx.lineWidth=3.5;
    this.ctx.arc(wheelItemX,wheelItemY,radius,getRads(sAngle+speed1),getRads(eAngle+speed1),false);
    this.ctx.stroke();
    this.ctx.closePath();

    if(v<2){
      //再縮小一圈
      let eAngleSmaller = 15+ v*210, sAngleSmaller = -30+v*90;
      let radiusSmaller = scaleMultiple*.45;
      this.ctx.beginPath();
      this.ctx.lineCap="round";
      this.ctx.strokeStyle = _color_;
      this.ctx.lineWidth=3;
      this.ctx.arc(wheelItemX,wheelItemY,radiusSmaller,getRads(sAngleSmaller+speed2),getRads(eAngleSmaller+speed2),false);
      this.ctx.stroke();
      this.ctx.closePath();
    }
    this.ctx.restore();
  });
  • 繪制車身車架

    • 車架,應該也是本次分享中較大的難點之一,剛開始我也是這麽認為的,但認真冷靜、冷靜、靜靜之後分析也還好,
    • 最開始是用了最笨的辦法,lineTOmoveTo、一根一根線的畫,畫到一半時發現畫兩個三角或者一個菱形即可,然後再把幾根主軸重新畫一下,於是兩種方法都嘗試了一下,
    • 先說三角的吧,配合下面畫的一個圖講解一下,
    • 找到圓盤的中心點,介於後輪半徑之上;
    • 分析車架的結構,我們可以看為是一個菱形,也可以看著是兩個三角形,這裏以三角為例,菱形可以看 carBracket2方法;
    • 首先算出三角形的起點、再算出三角形的角度、高度,請看下面示圖;
    • 最後在後輪的中心點蓋上一個圓點 用於遮擋三角的部分
    • 菱形 就要簡單些的,但看起來逼格沒有這麽高端,就是用lineTo點對點的劃線,
    • 以上就是車架的繪制過程,其實感覺菱形是是要簡單、代碼量也少些的,有興趣的同學可以自己嘗試一下,大家可以看下面的主要代碼,新手上路,如果有更好的方式,歡迎老司機指點:

結論 :使用moveTo把畫布坐標從O移動到A點 x/y,lineToA開始畫到B結束,再從BC點,閉合,即一個三角完成
技術分享圖片

技術分享圖片

技術分享圖片

技術分享圖片


//方法二:三角形
  …………此處省略N行代碼
  [
  {
    moveX:triangleX1,
    moveY:triangleY1,
    lineX1:coordinateX,
    lineY1:triangleH1,
    lineX2:discX,
    lineY2:discY,
  },
  {
    moveX:triangleX2+15,
    moveY:triangleY2,
    lineX1:triangleX1,
    lineY1:triangleY1,
    lineX2:discX,
    lineY2:triangleH2,
  },
  ].map((k,v)=>{
    this.ctx.beginPath();
    this.ctx.moveTo(k.moveX,k.moveY); //把坐標移動到A點,從A開始
    this.ctx.strokeStyle = this.gearColor;
    this.ctx.lineWidth=coordinateW;
    this.ctx.lineTo(k.lineX1,k.lineY1);//從A開始,畫到B點結束
    this.ctx.lineTo(k.lineX2,k.lineY2); //再從B到C點,閉合
    this.ctx.closePath();
    this.ctx.stroke();
    this.ctx.restore();
  });
  ……

//方法一:菱形

  …………此處省略N行代碼
  this.ctx.beginPath();
  this.ctx.strokeStyle = this.gearColor;
  this.ctx.lineWidth=coordinateW;
  this.ctx.moveTo(polygon1X,polygon1Y);
  this.ctx.lineTo(coordinateX,height);
  this.ctx.lineTo(discX,discY); 
  this.ctx.lineTo(polygon2X,polygon1Y+5);
  this.ctx.lineTo(polygon2X-5,polygon1Y);
  this.ctx.lineTo(polygon1X,polygon1Y);
  this.ctx.closePath();
  this.ctx.stroke();
  ……

技術分享圖片

  • 繪制車的豪華寶坐、扶手
    • 坐位一開始是比較懵逼的,不知道如何下手,圓也不圓、方也不方,後面又去復習一下canvas的API,發現了quadraticCurveTo能滿足這個需求,—— 二次貝塞爾曲線
    • 畫完之後,思考了很久,也沒有發現什麽技巧,或者規律,可能數學學的不好,沒辦法只能這樣慢慢描了
    • 扶手也是一樣的,開始嘗試quadraticCurveTo,半天也沒畫成功,後面嘗試去找了它鄰居bezierCurveTo,—— 三次貝塞爾曲線
    • 提示:三次貝塞爾曲線需要三個點。前兩個點是用於三次貝塞爾計算中的控制點,第三個點是曲線的結束點。曲線的開始點是當前路徑中最後一個點
    • 知識點quadraticCurveTobezierCurveTocreateLinearGradient

技術分享圖片
技術分享圖片
技術分享圖片

  //坐位
  this.ctx.restore();
  let seatX = (discX-85),seatY=discY-140;
  let curve1Cpx = [seatX-5,seatY+30,seatX+75,seatY+8];
  let curve2Cpx =[seatX+85,seatY-5,seatX,seatY]; 
  this.ctx.beginPath();
  // this.ctx.fillStyle = this.gearColor;
  let grd=this.ctx.createLinearGradient(seatX,seatY,seatX+10,seatY+60); //漸變的角度 
  grd.addColorStop(0,"#712450");
  grd.addColorStop(1,"#11090d");
  this.ctx.fillStyle = grd;
  this.ctx.moveTo(seatX,seatY);
  this.ctx.quadraticCurveTo(...curve1Cpx);
  this.ctx.quadraticCurveTo(...curve2Cpx);
  this.ctx.fill();

  //車前軸上的手柄
  let steeringX = lever1X-20,steeringY = lever1Y-45;
  let steeringStep1 = [steeringX+40,steeringY-10,steeringX+40,steeringY-10,steeringX+35,steeringY+15]
  let steeringStep2 = [steeringX+30,steeringY+25,steeringX+25,steeringY+23,steeringX+18,steeringY+23]
  this.ctx.beginPath();
  this.ctx.lineCap="round";
  this.ctx.strokeStyle = "#712450";
  this.ctx.lineWidth=coordinateW;
  this.ctx.moveTo(steeringX,steeringY); //40 60;
  this.ctx.bezierCurveTo(...steeringStep1);
  this.ctx.bezierCurveTo(...steeringStep2);
  this.ctx.stroke();
  this.ctx.closePath();
  • 繪制車的發動機、腳踏板
    • 到了這裏,也快接近本文的尾聲了,接下來要講的是是車輛中最重要的部分,車中間齒×××,一輛車沒有它,你做的再好也是白搭了;
    • 前面多次講到齒輪的中心點,包括兩個三角都是以它的中心計算的三角角度,知道了位置那就容易了,一樣的先畫幾個圓,每個按一定的比例縮小;
    • 然後外圍再畫一圈鋸齒,這樣齒輪大概就畫好了,齒輪的技巧在於以圓盤為中心點,畫一圈線,它跟時鐘的刻度原理是一樣的;
    • 腳踏板,這個好理解,就是用lineTo畫兩跟線,其中一根進行一個90度的旋轉就ok了,但重點是它在動畫過程中的一個過程呢,我的分析過程是這樣:
    • 豎著的這根軸是,以圓盤齒輪的中點為基點 N* (Math.PI / 180)轉動;
    • 橫著的這根軸,也就是腳踏板,它是以豎著的軸底部為Y軸中心點,以自身寬度的二分之一為X軸為中心點,同樣以 N* (Math.PI / 180)rotate角度旋轉。
    • 說了這麽多,我們來看幾張動態圖吧,順便貼上代碼:

技術分享圖片

技術分享圖片


  discGear(coordinateX,coordinateY,coordinateW){
    //車中間齒××× disc
    let discX = coordinateX,discY = coordinateY;
    let discRadius = this.wheelRadius*.36;//車輪的3.6;

    let discDotX = discX+discRadius+8,discDotY = discRadius/.98;
    this.ctx.restore();
    this.ctx.save();
    this.ctx.translate(discX,discY);
    // this.ctx.rotate(-(Math.PI/2));

    Array.from({length:30}).map((v,index)=>{
      let radian = (Math.PI / 15) ;
      this.ctx.beginPath();
      this.ctx.lineCap="round";
      this.ctx.strokeStyle = this.color;
      this.ctx.rotate(radian);
      this.ctx.lineWidth=3;
      this.ctx.moveTo(0,discDotY);
      this.ctx.lineTo(1.5,discDotY);
      // ctx.arc(discDotX,discDotY,6,0,Math.PI*2,false);
      this.ctx.closePath();
      this.ctx.stroke();

    });
    this.pedal(discX,discY,discRadius);
    this.pedal(discX,discY,discRadius,1);

    this.ctx.restore();
  }
  pedal(coordinateX,coordinateY,discRadius,turnAngle=0){

    //腳踏板,分兩次初始化,一次在中心齒輪繪制之前,一次在之後,
   //本文由@IT·平頭哥聯盟-首席填坑官?蘇南分享

    let pedalX = coordinateX, pedalY = coordinateY - discRadius*.7;
    let pedalW = 6,
        pedalH =  discRadius*1.9;
    let radian = (this.animateNum)*(Math.PI / 180) ;
    let radianHor = (this.animateNum)*(Math.PI / 180) ;
    let turnAngleNum = 1;
    let moveY = 28;
    if(turnAngle !== 0){
      this.ctx.rotate(-180*(Math.PI/180));
      turnAngleNum = (Math.PI/180);
    };
    this.ctx.beginPath();
    this.ctx.rotate(radian*turnAngleNum);
    this.ctx.lineCap="round";
    this.ctx.strokeStyle = this.gearColor;
    this.ctx.lineWidth=pedalW;
    this.ctx.moveTo(-1,moveY);
    this.ctx.lineTo(0,pedalH);
    this.ctx.closePath();
    this.ctx.stroke();

    this.ctx.save();
    let pedalHorW = pedalH/1.5,pedalHorH=pedalW;
    this.ctx.translate(0,pedalH);
    this.ctx.beginPath();
    this.ctx.rotate(-radianHor);

    this.ctx.lineCap="round";//首席填坑官?蘇南的專欄 交流:912594095、公眾號:honeyBadger8
    this.ctx.fillStyle = "#fff";
    this.ctx.strokeStyle = this.gearColor;
    this.ctx.lineWidth =2;
    this.ctx.roundRect(-pedalHorW/2,-2,pedalHorW,pedalHorH,5);
    this.ctx.closePath();
    this.ctx.fill();
    this.ctx.stroke();

    this.ctx.restore();
  }
  • 繪制車的鏈條
    • 鏈條用的是 bezierCurveTo ,cp1x,cp1y,cp2x,cp2y,x,y等參數畫出來的,具體看下面代碼吧,其實就是兩個半橢圓的拼接……
      技術分享圖片
  //鏈條

  let chainW = ( coordinateX+discRadius - this.wheelPos[0].x) / 2;
  let chainX = this.wheelPos[0].x +chainW-5 ;
  let chainY = coordinateY;
  this.ctx.save();
  this.ctx.translate(chainX,chainY+4.8);
  this.ctx.rotate(-2*(Math.PI/180));
  let r = chainW+chainW*.06,h = discRadius/2;
  this.ctx.beginPath();
  this.ctx.moveTo(-r, -1);
  this.ctx.lineWidth=3;
  this.ctx.strokeStyle = "#1e0c1a";
  this.ctx.bezierCurveTo(-r,h*1.5,r,h*4,r,0);
  this.ctx.bezierCurveTo(r,-h*4,-r,-h*1.5,-r,0);
  this.ctx.closePath();
  this.ctx.stroke();
  this.ctx.restore();

尾聲

  以上就是今天@IT·平頭哥聯盟-首席填坑官?蘇南給你帶來的分享,整個車的繪制過程,感覺車架部分應該還有更好的做法,如果您有更好的建議及想法,歡迎斧正,最後送上完整的示例圖!
  文章源碼獲取-> blog-resource
  想直接在線預覽

技術分享圖片

PS:如果您覺得文章不錯,想獲取更多前端內容,那就請關註下方的 公眾號,有驚喜哦。

技術分享圖片

更多文章:

  • webpack4配置詳解之慢嚼細咽
  • 如何給localStorage設置一個過期時間?
  • easy-mock 最好的備胎沒有之一
  • immutability因React官方出鏡之使用總結分享!
  • 面試踩過的坑,都在這裏了~
  • 你應該做的前端性能優化之總結大全!
  • 如何給localStorage設置一個過期時間?
  • 動畫一點點 - 如何用CSS3畫出懂你的3D魔方?

作者:蘇南 - 首席填坑官

鏈接:https://segmentfault.com/a/1190000016969897

交流:912594095、公眾號:honeyBadger8

本文原創,著作權歸作者所有。商業轉載請聯系@IT·平頭哥聯盟獲得授權,非商業轉載請註明原鏈接及出處。

文藝碼農~Canvas手把手教你如何繪制一輛跑車