'Centering a Canvas no-repeat pattern fill

I'm trying to replicate the animated icon that mapbox has here https://docs.mapbox.com/mapbox-gl-js/example/add-image-animated/

enter image description here

Except I want to have an image for the inner circle.

enter image description here

So far I can get fairly close by using a create pattern as above. But the image is repeating instead of being centered in the inner circle.

How can I have my image cropped and centered in the inner circle ?

    // Copy pasta from mapbox example ... 
    // Draw the outer circle.
    context.clearRect(0, 0, this.width, this.height)
    context.beginPath()
    context.arc(this.width / 2, this.height / 2, outerRadius, 0, Math.PI * 2)
    
    context.fillStyle = `hsl(46deg 85% 67% / ${1 - t})`
    context.fill()

    // Draw the inner circle.
    context.beginPath()
    context.arc(this.width / 2, this.height / 2, radius, 0, Math.PI * 2)
    
    // I've added this 
    const pattern = context.createPattern(image, 'repeat')
    context.fillStyle = pattern

    // I've tried this but the image isn't cropped and isn't centered. 
    // context.drawImage(image, this.width / 2, this.height / 2, 150, 150)

    context.strokeStyle = 'white'
    context.lineWidth = 2 + 4 * (1 - t)
    context.fill()
    context.stroke()


Solution 1:[1]

Actually, I think you were almost there.

Since you don't want it to repeat, first set the pattern to not repeat:

const pattern = ctx.createPattern(img, 'no-repeat');

The pattern fill calculation starts in the upper left hand corner (0, 0), so that alone won't do it. We'll have to transform the pattern as well:

pattern.setTransform(
    new DOMMatrix(
    [
        // No rotation, 1-1 scale
        1, 0, 0, 1,
        // Translate to center, offset by half-image
        canvas.width / 2 - img.width / 2, 
        canvas.height / 2 - img.height / 2
    ])
);

Docs:


Here's a demo of how to use it. I couldn't get their CodePen demo to work, so I had to base it off if that and improvise a bit:

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const size = 300;

const img = new Image();
img.src = 'https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle/canvas_fillstyle.png';
img.onload = function() {
  const pattern = ctx.createPattern(img, 'no-repeat');
  const radius = (size / 2) * 0.3;
    const outerRadius = (size / 2) * 0.7 + radius;
  
  ctx.fillStyle = `hsl(46deg 85% 67% / ${0})`;
  ctx.arc(canvas.width / 2, canvas.height / 2, outerRadius, 0, Math.PI * 2);
  ctx.fill();

  ctx.beginPath();
  ctx.arc(canvas.width / 2, canvas.height / 2, radius, 0, Math.PI * 2);

  pattern.setTransform(new DOMMatrix([1, 0, 0, 1, canvas.width / 2 - img.width / 2, canvas.height / 2 - img.height / 2]));
  ctx.fillStyle = pattern;

  ctx.strokeStyle = 'white';
  ctx.lineWidth = 2;
  ctx.fill();
  ctx.stroke();
};
canvas
{
  border: 1px solid black;
  background-color: gray;
}
<canvas width="400" height="400"></canvas>
<div>For reference, here's the original image:</div>
<img src="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle/canvas_fillstyle.png" />

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 General Grievance