Reputation: 195
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
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