Search code examples
javascripthtmlhtml5-canvas

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

  • 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" />