HTML5 Canvas 扫描渐变

HTML5 Canvas Sweep Gradient

HTML5 canvas 似乎不支持 "sweep gradient" - 颜色停止围绕中心旋转而不是从中心发出的渐变。

有什么方法可以在 canvas 上模拟扫描梯度吗?我想我可以用很多小的线性渐变做类似的事情,但那时我基本上是自己渲染渐变。

确实没有内置这样的东西。

不确定您对这些“很多小线性渐变”的想法,但实际上您只需要一个,即圆周的大小,并且仅得到正确的颜色来使用。

你最需要的是线条,因为我们将使用 linearGradient 中的纯色围绕中心点绘制这些线条。

所以要渲染它,您只需移动到中心点,然后使用线性渐变的纯色画一条线,然后旋转并重复。

要获得 linearGradient 的所有颜色,您只需绘制它并将其 ImageData 映射到 CSS 颜色。

虽然困难的部分是能够拥有一个行为类似于 CanvasGradient 的对象,我们需要能够将其设置为 fillStyle 或 strokeStyle。 这可以通过返回 CanvasPattern 来实现。另一个困难是梯度实际上是无限大的。非重复模式不是。
我没有找到解决这个问题的好方法,但作为解决方法,我们可以使用目标的大小 canvas 作为限制。

这是一个粗略的实现:

class SweepGrad {
  constructor(ctx, x, y) {
    this.x = x;
    this.y = y;
    this.target = ctx;
    this.colorStops = [];
  }
  addColorStop(offset, color) {
    this.colorStops.push({offset, color});
  }
  render() {
    // get the current size of the target context
    const w = this.target.canvas.width;
    const h = this.target.canvas.width;
    const x = this.x;
    const y = this.y;
    // get the max length our lines can be
    const maxDist = Math.ceil(Math.max(
      Math.hypot(x, y),
      Math.hypot(x - w, y),
      Math.hypot(x - w, y - h),
      Math.hypot(x, y - h)
    ));
    // the circumference of our maxDist circle
    // this will determine the number of lines we will draw
    // (we double it to avoid some antialiasing artifacts at the edges)
    const circ = maxDist*Math.PI*2 *2;
  
    // create a copy of the target canvas
    const canvas = this.target.canvas.cloneNode();
    const ctx = canvas.getContext('2d');

    // generate the linear gradient used to get all our colors
    const linearGrad = ctx.createLinearGradient(0, 0, circ, 0);
    this.colorStops.forEach(stop => 
     linearGrad.addColorStop(stop.offset, stop.color)
    );
    const colors = getLinearGradientColors(linearGrad, circ);
    // draw our gradient
    ctx.setTransform(1,0,0,1,x,y);

    for(let i = 0; i<colors.length; i++) {
      ctx.beginPath();
      ctx.moveTo(0,0);
      ctx.lineTo(maxDist, 0);
      ctx.strokeStyle = colors[i];
      ctx.stroke();
      ctx.rotate((Math.PI*2)/colors.length);
    }
    // return a Pattern so we can use it as fillStyle or strokeStyle
    return ctx.createPattern(canvas, 'no-repeat');
  }

}
// returns an array of CSS colors from a linear gradient
function getLinearGradientColors(grad, length) {
  const canvas = Object.assign(document.createElement('canvas'), {width: length, height: 10});
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = grad;
  ctx.fillRect(0,0,length, 10);
  return ctx.getImageData(0,0,length,1).data
    .reduce((out, channel, i) => {
      const px_index = Math.floor(i/4);
      const px_slot = out[px_index] || (out[px_index] = []);
      px_slot.push(channel);
      if(px_slot.length === 4) {
         px_slot[3] /= 255;
         out[px_index] = `rgba(${px_slot.join()})`;
      }
      return out;
    }, []);
}

// How to use
const ctx = canvas.getContext('2d');

const redblue = new SweepGrad(ctx, 70, 70);
redblue.addColorStop(0, 'red');
redblue.addColorStop(1, 'blue');
// remeber to call 'render()' to get the Pattern back
// maybe a Proxy could handle that for us?
ctx.fillStyle = redblue.render();
ctx.beginPath();
ctx.arc(70,70,50,Math.PI*2,0);
ctx.fill();

const yellowgreenred = new SweepGrad(ctx, 290, 80);
yellowgreenred.addColorStop(0, 'yellow');
yellowgreenred.addColorStop(0.5, 'green');
yellowgreenred.addColorStop(1, 'red');
ctx.fillStyle = yellowgreenred.render();
ctx.fillRect(220,10,140,140);

// just like with gradients, 
// we need to translate the context so it follows our drawing
ctx.setTransform(1,0,0,1,-220,-10);
ctx.lineWidth = 10;
ctx.strokeStyle = ctx.fillStyle;
ctx.stroke(); // stroke the circle
canvas{border:1px solid}
<canvas id="canvas" width="380" height="160"></canvas>

但要注意,所有这些计算量都很大,所以一定要偶尔使用它并缓存你的结果 Gradients/Patterns.