In every tutorial that I could find for how to rotate a sprite image on a canvas the canvas itself is rotated before applying sprite to it:
function drawSprite(sprite, x, y, deg)
{
const width = sprite.width / 2,
height = sprite.height / 2;
x = x + width;
y = y + height;
//clear main canvas
mainCtx.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
// move origin to the coordinates of the center where sprite will be drawn
mainCtx.translate(x, y);
// rotate canvas
mainCtx.rotate(deg);
// draw sprite
mainCtx.drawImage(sprite, -width, -height);
// restore previous rotation and origin
mainCtx.rotate(-deg);
mainCtx.translate(-x, -y);
}
//never mind the rest
const mainCtx = mainCanvas.getContext("2d"),
sprite = (() =>
{
const canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d"),
width = canvas.width = ctx.width = 100,
height = canvas.height = ctx.height = 50;
ctx.font = '20px arial';
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "lightgreen";
ctx.fillRect(0, 0, width, height);
ctx.strokeRect(0, 0, width, height);
ctx.strokeText("my sprite", width/2, height/2);
return canvas;
})();
let r = 0;
const d = Math.sqrt(sprite.width *sprite.width + sprite.height*sprite.height),
w = mainCanvas.width = mainCtx.width = 400,
h = mainCanvas.height = mainCtx.height = 200;
mainCtx.fillStyle = "pink";
setInterval(() =>
{
const deg = r++*Math.PI/180;
let x = ((w-d)/2) + (Math.sin(deg)*((w-d)/2)),
y = ((h-d)/1.2) + (Math.cos(deg)*((h-d)/2));
drawSprite(sprite, x, y, deg);
}, 10);
<canvas id="mainCanvas"></canvas>
To me this is counterintuitive, why can't we rotate sprite itself before drawing it on main canvas? Why doesn't this work?
function drawSprite(sprite, x, y, deg)
{
const spriteCtx = sprite.getContext("2d");
//clear main canvas
mainCtx.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
// rotate sprite
spriteCtx.rotate(deg);
// draw sprite
mainCtx.drawImage(sprite, x, y);
}
//never mind the rest
const mainCtx = mainCanvas.getContext("2d"),
sprite = (() =>
{
const canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d"),
width = canvas.width = ctx.width = 100,
height = canvas.height = ctx.height = 50;
ctx.font = '20px arial';
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "lightgreen";
ctx.fillRect(0, 0, width, height);
ctx.strokeRect(0, 0, width, height);
ctx.strokeText("my sprite", width/2, height/2);
return canvas;
})();
let r = 0;
const d = Math.sqrt(sprite.width *sprite.width + sprite.height*sprite.height),
w = mainCanvas.width = mainCtx.width = 400,
h = mainCanvas.height = mainCtx.height = 200;
mainCtx.fillStyle = "pink";
setInterval(() =>
{
const deg = r++*Math.PI/180;
let x = ((w-d)/2) + (Math.sin(deg)*((w-d)/2)),
y = ((h-d)/1.2) + (Math.cos(deg)*((h-d)/2));
drawSprite(sprite, x, y, deg);
}, 10);
<canvas id="mainCanvas"></canvas>
Wouldn't it be faster rotate a 100x100 sprite vs 10000x10000 main canvas?
Because the drawImage
function takes only x(s),y(s) coordinate and width(s),/height(s).
I.e it only ever draws a straight rectangle, there is no way to make it draw anything skewed.
So you have to rotate the context's Current Transformation Matrix (CTM), which is not the canvas, so that the drawing is transformed.
Note that drawing a bitmap as a rectangle is a very basic model for drawing APIs.
As for the speed, once again you don't rotate the canvas, only the CTM and this only affects the future drawings and costs almost nothing anyway.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.font = "30px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.translate(150, 75);
const txtArr = [ "first", "second", "third", "fourth" ];
const colors = [ "red", "blue", "green", "orange" ];
for (let i = 0; i < txtArr.length; i++) {
ctx.fillStyle = colors[i];
ctx.fillText(txtArr[i], 0, 0);
// This doesn't rotate the previous drawings
ctx.rotate(Math.PI / txtArr.length);
}
<canvas></canvas>
So, yes, you could have your own drawImage(source, matrix)
which would be something like
this.save();
this.setTransform(matrix);
this.drawImage(source, 0, 0);
this.restore();
But as you can see this means actually more operations per draw call, and thus performing two drawings on the same CTM would actually cost more than setting the CTM only once.