手把手教你畫圓錐漸變

愛程式設計的李先森發表於2019-04-28

前言

昨天公司培訓canvas相關內容,然後培訓完還留下一道homework,覺得挺有意思的,特來與大家分享分享。大家可以先不看我的實現,自己嘗試試試,還是可以學到不少知識的。

題目內容

image.png

初看題目內容好像挺簡單的,不就是個漸變嘛,看我的,翻翻萬能的mdn查查canvas漸變api,

CanvasGradient 介面表示描述漸變的不透明物件。通過 CanvasRenderingContext2D.createLinearGradient() 或 CanvasRenderingContext2D.createRadialGradient() 的返回值得到. developer.mozilla.org/zh-CN/docs/…

好像哪不對,這兩個漸變api只有線性漸變(LinearGradient)和圓形漸變(RadialGradient);而題目的意思是繪製一個扇形漸變,從0到360度的一個按照角度漸變的一個圓。然後我就問我們設計的小夥伴,怎麼畫這種圓錐漸變,畢竟工具畫圖和程式碼畫圖思路還是一樣的,只不過過程不一樣。然而現實是,ps自帶角度漸變。

image.png

what?好吧,只能自己分析了。

分析題目

首先拋開漸變不談,我們把顏色分成幾塊,每塊一種顏色是不是就是我們熟悉的餅圖。

image.png

那麼我們運用微分的思想,把圓分成更多份的扇形,每種扇形一個顏色是不是就能實現題目的效果呢?我們來試試。

漸變色的實現

根據我們分析的思路,首先我們先從顏色等份開始做起,顏色常見的表示有四種十六進位制顏色值(#000000),RGBA,HSL和HSV。

  • HSL:H(hue)色相,S(saturation)飽和度,以及L(lightness)亮度
  • HSV:H(hue)色相,S(saturation)飽和度,以及V(value)色調
  • RGBA:Red(紅色)Green(綠色)Blue(藍色)和Alpha的色彩空間

色相(Hue):取值範圍是從0°到360°正上方為0°的話,0度為R(紅)色,120度為G(綠)色,240度為B(藍)色

image.png

因此其實這個題目我認為用這個顏色值是最好的,算出來的漸變比較好看,不過這裡我使用的是RGBA。感興趣的小夥伴可以嘗試用HSV寫個漸變演算法,用過角度變換。
亮度(lightness):最下面是0%也最暗,最上面是100%,最亮
image.png

飽和度(saturation):和亮度一樣也是通過百分比表示的。
image.png

這些作為補充知識,這裡我是使用的RGBA顏色。竟然顏色需要等分,那麼我把顏色轉換成RGBA,然後等分RGB三種顏色,每一份取三種顏色的差值的

/**
     *
     * @param startColor 指定起始顏色
     * @param endColor   指定結束顏色
     * @param step       劃分漸變色區域數量
     * @returns {Array}  返回漸變色陣列
     */
let gradientColor = function(startColor, endColor, step) {
  let startRGB = this.colorRgb(startColor); //轉換為rgb陣列模式
  let startR = startRGB[0];
  let startG = startRGB[1];
  let startB = startRGB[2];

  let endRGB = this.colorRgb(endColor);
  let endR = endRGB[0];
  let endG = endRGB[1];
  let endB = endRGB[2];

  let sR = (endR - startR) / step; //總差值
  let sG = (endG - startG) / step;
  let sB = (endB - startB) / step;

  let colorArr = [];
  for (let i = 0; i < step; i++) {
    //計算每一步的hex值
    let hex = this.colorHex('rgb(' + parseInt((sR * i + startR)) + ',' + parseInt((sG * i + startG)) + ',' +
                            parseInt((sB * i + startB)) + ')');
    colorArr.push(hex);
  }
  return colorArr;
};
複製程式碼

我們把相應的十六進位制顏色轉換成RGB然後根據起始顏色和末顏色,計算出差值,即每份的顏色值。得出的陣列就是梯度顏色的陣列。相應的顏色裝換函式如下:

 // 將hex表示方式轉換為rgb表示方式(這裡返回rgb陣列模式)
gradientColor.prototype.colorRgb = function(sColor) {
  let reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
  sColor = sColor.toLowerCase();
  if (sColor && reg.test(sColor)) {
    if (sColor.length === 4) {
      let sColorNew = "#";
      for (let i = 1; i < 4; i += 1) {
        sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));
      }
      sColor = sColorNew;
    }
    //處理六位的顏色值
    let sColorChange = [];
    for (let i = 1; i < 7; i += 2) {
      sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2)));
    }
    return sColorChange;
  } else {
    return sColor;
  }
};
// 將rgb表示方式轉換為hex表示方式
gradientColor.prototype.colorHex = function(rgb) {
  let _this = rgb;
  let reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
  if (/^(rgb|RGB)/.test(_this)) {
    let aColor = _this.replace(/(?:\(|\)|rgb|RGB)*/g, "").split(",");
    let strHex = "#";
    for (let i = 0; i < aColor.length; i++) {
      let hex = Number(aColor[i]).toString(16);
      hex = hex < 10 ? 0 + '' + hex : hex; // 保證每個rgb的值為2位
      if (hex === "0") {
        hex += hex;
      }
      strHex += hex;
    }
    if (strHex.length !== 7) {
      strHex = _this;
    }
    return strHex;
  } else if (reg.test(_this)) {
    let aNum = _this.replace(/#/, "").split("");
    if (aNum.length === 6) {
      return _this;
    } else if (aNum.length === 3) {
      let numHex = "#";
      for (let i = 0; i < aNum.length; i += 1) {
        numHex += (aNum[i] + aNum[i]);
      }
      return numHex;
    }
  } else {
    return _this;
  }
};
複製程式碼

更多顏色轉換方法可以參考張鑫旭大大的文章www.zhangxinxu.com/wordpress/2…
然後我們可以直接這樣呼叫:

let color_list = new gradientColor("#706caa", "#f2f2b0", 360);
console.log(color_list);
複製程式碼

這樣控制檯我們就能看到我們計算出的漸變顏色陣列了

image.png

繪製圓

心急的小夥伴可能想畫圓還不簡單分分鐘畫一個圓

context.beginPath();
context.arc(150, 75, 50, 0, Math.PI * 2);
context.stroke();
複製程式碼

但是如果這麼畫圓,怎麼填充漸變色呢,想想前面的餅圖,我們把餅圖分成更多份,分成360份呢?是不是就相當於有很多線段,起始點一樣,長度一樣,圍繞起始點排列成一個圓!看到這裡聰明的你應該就想到該怎麼做了吧。是的,用畫線的方式,來畫圓,可能你覺得不可思議,moveTo和lineTo怎麼可能畫圓呢?下面我們就來分析如何畫一個圓。
大家還記得圓的極座標方程嗎,我給大家回顧回顧;
圓的極座標公式:ρ²=x²+y²,x=ρcosθ,y=ρsinθ  tanθ=y/x,(x不為0)
下面的動圖顯示的很詳細,圓上任意一點與圓心的線段都是可以通過極座標表示出來的,並且如果我們每畫一根線都儲存下面,畫滿一圈後不就是一個填充圓嗎。

手把手教你畫圓錐漸變

通過上面的分析,我們來寫程式碼

var center = [200, 200];		//圓的中心
var r = 100;		//	圓的半徑
ctx.moveTo(center[0] + r, center[1]);			//先把起始點移到圓上
for (var i = 0; i < 360; i++) {
  var ii = i * Math.PI / 180;					//角度轉弧度
  ctx.lineWidth = 2;
  var x = r + r * Math.cos(ii);			//圓上任一點的橫座標
  var y = r - r * Math.sin(ii);			//圓上任一點縱座標
  ctx.lineTo(x, y);
}
ctx.stroke();
複製程式碼

這樣我們就能看到最後的結果了;

image.png

完美,我們通過moveTo和lineTo畫出了一個圓,細心的小夥伴應該看到,右邊有一點缺失,沒連上,那是應為我們把圓分為360份但是,最後一份應該與第一份相連,也就是closePath,因此我們迴圈多加一次,361次就可以閉合了;
image.png

我們現在知道畫圓了,那麼同理我們把起始點移動到圓形,並且把圓心和圓上每一份的點連起來,不就是一個實心圓了嗎。

var center = [200, 200];		//圓的中心
var r = 100;		//	圓的半徑

for (var i = 0; i < 360; i++) {
  var ii = i * Math.PI / 180;					//角度轉弧度
  ctx.save();
  ctx.beginPath();
  ctx.lineWidth = 2;
  ctx.moveTo(center[0], center[1]);				//移動路徑到圓心
  var x = r + r * Math.cos(ii);			//圓上任一點的橫座標
  var y = r - r * Math.sin(ii);			//圓上任一點縱座標
  ctx.lineTo(x, y);
  ctx.closePath();
  ctx.stroke();
}

複製程式碼

這樣我們就得到一個實心圓,一個由360根線組成的圓

image.png

那麼對應的,我們把每根線的顏色也由前面我們計算的漸變色來對應上,程式碼也很簡單;

var center = [200, 200];		//圓的中心
var r = 100;		//	圓的半徑

for (var i = 0; i < 360; i++) {
  var ii = i * Math.PI / 180;					//角度轉弧度
  ctx.save();
  ctx.beginPath();
  ctx.lineWidth = 2;
  ctx.strokeStyle = color_list[i];
  ctx.moveTo(center[0], center[1]);				//移動路徑到圓心
  var x = r + r * Math.cos(ii);			//圓上任一點的橫座標
  var y = r - r * Math.sin(ii);			//圓上任一點縱座標
  ctx.lineTo(x, y);
  ctx.closePath();
  ctx.stroke();
}
複製程式碼

image.png

完整程式碼請看這裡canvas圓錐漸變
很完美,一切都按我們設想的一樣。如果讀者有更好的方法,可以給我留言,一起學習交流交流。
本著只是做個題目,但是發現很有意思,後續我會封裝一下,做成一個漸變庫,支援各種漸變。

參考資料

相關文章