zestyrx
zestyrx

Reputation: 89

Applying a linear gradient to a CanvasPattern (Canvas API)

I have a simple pattern that repeats and a gradient that extends beyond the dimensions of the pattern. It is possible to apply the linear gradient to the the rendered pattern as it repeats?

I tried the following, but it doesn't actually paint the gradient as I would expect:

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');

const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, '#ff0000');
gradient.addColorStop(1, '#00ff00');

const patternCanvas = document.createElement('canvas');
const patternContext = patternCanvas.getContext('2d');

patternCanvas.height = 10;
patternCanvas.width = 10;
patternContext.fillStyle = gradient; // this doesn't work as expected
patternContext.arc(5, 5, 2.5, 0, Math.PI * 2);
patternContext.fill();

const pattern = context.createPattern(patternCanvas, 'repeat');

context.fillStyle = pattern;
context.fillRect(0, 0, canvas.width, canvas.height);
<canvas id = 'canvas'>

Upvotes: 1

Views: 465

Answers (1)

Kaiido
Kaiido

Reputation: 136698

The CanvasPattern only holds a bitmap, i.e pixels. You can not make it change dynamically other than transforming it (as in moving, scaling, rotating).

So your idea of applying the same pattern shape, but with a gradient fill is not doable with a CanvasPattern only.

However, it's still quite simple to achieve, in two steps:

Every time you'll want to draw this gradient pattern, you'll first fill the pattern as a solid color, then change the composite operation to source-in, and then apply your gradient.

const ctx = createCanvasContext2d( 500, 500 );
document.body.append( ctx.canvas );
const pat = createPattern();
const grad = ctx.createLinearGradient( 0, 0, 500, 500);
grad.addColorStop( 0, 'red' );
grad.addColorStop( 0.5, 'yellow' );
grad.addColorStop( 1, 'blue' );

ctx.arc( 250, 250, 250, 0, Math.PI*2 );
// first the pattern
ctx.fillStyle = pat;
ctx.fill();
// change the composite operation
ctx.globalCompositeOperation = 'source-in';
// now draw the gradient
ctx.fillStyle = grad;
ctx.fill();
// reset to default
ctx.globalCompositeOperation = 'source-over';

function createPattern() {
  // we create a small canvas context just for the pattern
  const pattern_ctx = createCanvasContext2d( 20, 20 );
  // simply a black circle
  pattern_ctx.arc( 10, 10, 5, Math.PI*2, 0 );
  pattern_ctx.fill();
  return pattern_ctx.createPattern( pattern_ctx.canvas, 'repeat' );
}

function createCanvasContext2d( width=300, height=width||150 ) {
  const canvas = document.createElement( 'canvas' );
  canvas.width = width;
  canvas.height = height;
  return canvas.getContext( '2d' );
}

However the downside of this approach is that this operation requires you have a clear context, because everything that is not transparent when the gradient is drawn will get filled, and everything that is not in the filled area will get removed:

const ctx = createCanvasContext2d( 500, 500 );
document.body.append( ctx.canvas );
const pat = createPattern();
const grad = ctx.createLinearGradient( 0, 0, 500, 500);
grad.addColorStop( 0, 'red' );
grad.addColorStop( 0.5, 'yellow' );
grad.addColorStop( 1, 'blue' );

// draw a green rect both in and out of our future gradient pattern
ctx.fillStyle = 'green';
ctx.fillRect(0,0,150,150);

ctx.arc( 250, 250, 250, 0, Math.PI*2 );
// first the pattern
ctx.fillStyle = pat;
ctx.fill();
// change the composite operation
ctx.globalCompositeOperation = 'source-in';
// now draw the gradient
ctx.fillStyle = grad;
ctx.fill();
// reset to default
ctx.globalCompositeOperation = 'source-over';

function createPattern() {
  // we create a small canvas context just for the pattern
  const pattern_ctx = createCanvasContext2d( 20, 20 );
  // simply a black circle
  pattern_ctx.arc( 10, 10, 5, Math.PI*2, 0 );
  pattern_ctx.fill();
  return pattern_ctx.createPattern( pattern_ctx.canvas, 'repeat' );
}

function createCanvasContext2d( width=300, height=width||150 ) {
  const canvas = document.createElement( 'canvas' );
  canvas.width = width;
  canvas.height = height;
  return canvas.getContext( '2d' );
}

To avoid this, you can keep a second context that you'll use only to make such compositing operations, and that you'll be able to draw on the main context using drawImage.

const ctx = createCanvasContext2d( 500, 500 );
document.body.append( ctx.canvas );

const compositing_ctx = createCanvasContext2d( 500, 500 );

const pat = createPattern();
const grad = ctx.createLinearGradient( 0, 0, 500, 500);
grad.addColorStop( 0, 'red' );
grad.addColorStop( 0.5, 'yellow' );
grad.addColorStop( 1, 'blue' );

// draw a green rect both in and out of our future gradient pattern
ctx.fillStyle = 'green';
ctx.fillRect(0,0,150,150);

// make the compositing on the off-screen context
compositing_ctx.arc( 250, 250, 250, 0, Math.PI*2 );
compositing_ctx.fillStyle = pat;
compositing_ctx.fill();
compositing_ctx.globalCompositeOperation = 'source-in';
compositing_ctx.fillStyle = grad;
compositing_ctx.fill();
compositing_ctx.globalCompositeOperation = 'source-over';
// draw to main
ctx.drawImage( compositing_ctx.canvas, 0, 0 );

// And a small red one?
compositing_ctx.clearRect( 0, 0, 500, 500 );
compositing_ctx.beginPath();
compositing_ctx.rect( 350, 0, 150, 150 );
compositing_ctx.fillStyle = pat;
compositing_ctx.fill();
compositing_ctx.globalCompositeOperation = 'source-in';
compositing_ctx.fillStyle = "red";
compositing_ctx.fill();
compositing_ctx.globalCompositeOperation = 'source-over';
// draw to main
ctx.drawImage( compositing_ctx.canvas, 0, 0 );


function createPattern() {
  // we create a small canvas context just for the pattern
  const pattern_ctx = createCanvasContext2d( 20, 20 );
  // simply a black circle
  pattern_ctx.arc( 10, 10, 5, Math.PI*2, 0 );
  pattern_ctx.fill();
  return pattern_ctx.createPattern( pattern_ctx.canvas, 'repeat' );
}

function createCanvasContext2d( width=300, height=width||150 ) {
  const canvas = document.createElement( 'canvas' );
  canvas.width = width;
  canvas.height = height;
  return canvas.getContext( '2d' );
}

Upvotes: 1

Related Questions