Daniel Gast
Daniel Gast

Reputation: 195

Canvas createPattern() and fill() with an offset

I'm using the canvas to draw simple rectangles and fill them with a flooring pattern (PNG). But if I utilize a "Camera" script to handle transform offsets for the HTML5 canvas, the rectangle shape moves appropriately, but the fill pattern seems to always draw from a fixed point on the screen (I'm assuming the top left). Is there a way to "nail down" the fill pattern so it always lines up with the rectangle, no matter the canvas transform, or a way to add an offset on fill() that can be calculated elsewhere in the code? I could just use drawImage() of course, but drawing the rectangles is more versatile for my purposes.

        sampleDrawFunction(fillTexture, x1, y1, x2, y2) {
        // This is oversimplified, but best I can do with a ~10k lines of code
        x1 -= Camera.posX;
        x2 -= Camera.posX;
        y1 = -1 * y1 - Camera.posY;
        y2 = -1 * y2 - Camera.posY;

        // Coordinates need to be adjusted for where the camera is positioned

        var img = new Image();
        img.src = fillTexture;
        var pattern = this.ctx.createPattern(img, "repeat");
        this.ctx.fillStyle = pattern;

        // Translate canvas's coordinate pattern to match what the camera sees
        this.ctx.save();
        this.ctx.translate(Camera.posX - Camera.topLeftX, Camera.posY - Camera.topLeftY);
        this.ctx.fillStyle = pattern;
        this.ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
        this.ctx.restore();
}

Thank you.

Upvotes: 3

Views: 1587

Answers (1)

Kaiido
Kaiido

Reputation: 136638

Canvas fills (CanvasPatterns and CanvasGradients) are always relative to the context's transformation matrix, so they do indeed default at top left corner of the canvas, and don't really care about where the path using them will be be.

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = begin; //!\ ALWAYS WAIT FOR YOUR IMAGE TO LOAD BEFORE DOING ANYTHING WITH IT!
img.crossOrigin = "anonymous";
img.src = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png";

function begin() {
  const rect_size = 20;
  ctx.fillStyle = ctx.createPattern( img, 'no-repeat' );
  // drawing a checkerboard of several rects shows that the pattern doesn't move
  for ( let y = 0; y < canvas.height; y += rect_size ) {
    for ( let x = (y / rect_size % 2) ? rect_size : 0 ; x < canvas.width; x += rect_size * 2 ) {
      ctx.fillRect( x, y, rect_size, rect_size );
    }
  }

}
<canvas id="canvas" width="500" height="500"></canvas>

Now, because they are relative to the context transform matrix, this means we can also move them by changing that transformation matrix:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = begin; //!\ ALWAYS WAIT FOR YOUR IMAGE TO LOAD BEFORE DOING ANYTHING WITH IT!
img.crossOrigin = "anonymous";
img.src = "https://upload.wikimedia.org/wikipedia/commons/f/f7/Cool_bunny_sprite.png";


function begin() {
  const rect_size = 244;
  const max_speed = 5;
  let x_speed = Math.random() * max_speed;
  let y_speed = Math.random() * max_speed;

  ctx.fillStyle = ctx.createPattern( img, 'repeat' );
  
  let x = 0;
  let y = 0;
  requestAnimationFrame( anim );
  
  function anim( now ) {

    clear();

    x = (x + x_speed);
    y = (y + y_speed);
    
    if( x > canvas.width || x < -rect_size ) {
      x_speed = Math.random() * max_speed * -Math.sign( x_speed );
      x = Math.min( Math.max( x, -rect_size ), canvas.width );
    }
    if( y > canvas.height || y < -rect_size ) {
      y_speed = Math.random() * max_speed * -Math.sign( y_speed )
      y = Math.min( Math.max( y, -rect_size ), canvas.height );
    }

    // we change the transformation matrix of our context
    ctx.setTransform( 1, 0, 0, 1, x, y );
    // we thus always draw at coords 0,0
    ctx.fillRect( 0, 0, rect_size, rect_size );
    ctx.strokeRect( 0, 0, rect_size, rect_size );

    requestAnimationFrame( anim );

  }
  function clear() {
    // since we changed the tranform matrix we need to reset it to identity
    ctx.setTransform( 1, 0, 0, 1, 0, 0 );
    ctx.clearRect( 0, 0, canvas.width, canvas.height );

  }
}
<canvas id="canvas" width="300" height="300"></canvas>

We can even detach the path declaration from the filling by changing the transformation matrix after we declared the sub-path and of course by replacing the shorthand fillRect() with beginPath(); rect(); fill()

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = begin; //!\ ALWAYS WAIT FOR YOUR IMAGE TO LOAD BEFORE DOING ANYTHING WITH IT!
img.crossOrigin = "anonymous";
img.src = "https://upload.wikimedia.org/wikipedia/commons/f/f7/Cool_bunny_sprite.png";


function begin() {
  const rect_size = 244;
  const max_speed = 5;
  let x_speed = Math.random() * max_speed;
  let y_speed = Math.random() * max_speed;

  ctx.fillStyle = ctx.createPattern( img, 'repeat' );
  
  let x = 0;
  let y = 0;
  requestAnimationFrame( anim );
  
  function anim( now ) {

    clear();

    x = (x + x_speed);
    y = (y + y_speed);
    
    if( x > canvas.width || x < -rect_size ) {
      x_speed = Math.random() * max_speed * -Math.sign( x_speed );
      x = Math.min( Math.max( x, -rect_size ), canvas.width );
    }
    if( y > canvas.height || y < -rect_size ) {
      y_speed = Math.random() * max_speed * -Math.sign( y_speed )
      y = Math.min( Math.max( y, -rect_size ), canvas.height );
    }

    // we declare the sub-path first, with identity matrix applied
    ctx.beginPath();
    ctx.rect( 50, 50, rect_size, rect_size );
    // we change the transformation matrix of our context
    ctx.setTransform( 1, 0, 0, 1, x, y );
    // and now we fill
    ctx.fill();
    ctx.stroke();

    requestAnimationFrame( anim );

  }
  function clear() {
    // since we changed the tranform matrix we need to reset it to identity
    ctx.setTransform( 1, 0, 0, 1, 0, 0 );
    ctx.clearRect( 0, 0, canvas.width, canvas.height );

  }
}
<canvas id="canvas" width="300" height="300"></canvas>

But in your case, it sounds that transforming the whole drawing is the easiest and most idiomatic way to follow. Not too sure why you are modifying your x and y coordinates in relation to the camera. Generally, if we use a camera object it's for the objects in the scene don't have to care about it, and stay relative to the world.

Upvotes: 5

Related Questions