Elliot Nelson
Elliot Nelson

Reputation: 11557

HTML5 Canvas Sweep Gradient

It looks like the HTML5 canvas does not support a "sweep gradient" - a gradient where the color stops rotate around the center, rather than emanating from the center.

Is there any way to simulate a sweep gradient on a canvas? I suppose I could do something similar with lots of little linear gradients, but at that point I'm basically rendering the gradient myself.

Example of sweep gradient

Upvotes: 0

Views: 1079

Answers (1)

Kaiido
Kaiido

Reputation: 137006

Indeed there is no built-in for such a thing.

Not sure what you had in mind with these "lots of little linear gradients", but you actually just need a single one, the size of your circle's circumference, and only to get the correct colors to use.

What you'll need a lot though are lines, since we'll draw these around the center point using the solid colors we had in the linearGradient.

So to render this, you just move to the center point, then draw a line using a solid color from the linear gradient, then rotate and repeat.

To get all the colors of a linearGradient, you just need to draw it and map it's ImageData to CSS colors.

The hard part though is that to be able to have an object that behaves like a CanvasGradient, we need to be able to set it as a fillStyle or strokeStyle. This is possible by returning a CanvasPattern. An other difficulty is that gradients are virtually infinitely big. A non-repeating Pattern is not.
I didn't found a good solution to overcome this problem, but as a workaround, we can use the size of the target canvas as a limit.

Here is a rough implementation:

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>

But beware, all this is quite computationally heavy, so be sure to use it sporadically and to cache your resulting Gradients/Patterns.

Upvotes: 1

Related Questions