手把手教你画圆锥渐变

3,025 阅读6分钟

前言

昨天公司培训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圆锥渐变
很完美,一切都按我们设想的一样。如果读者有更好的方法,可以给我留言,一起学习交流交流。
本着只是做个题目,但是发现很有意思,后续我会封装一下,做成一个渐变库,支持各种渐变。

参考资料