I'm having trouble with a movable canvas that adjusts as the 'player' moves around the map. As drawing 600 tiles, 60 times a second is very inefficient, I switched over to using translate3d
and only draw
once the player crossed a full tile -- but it keeps glitching and not moving around smooth. How would I achieve this properly?
const ctx = canvas.getContext('2d');
canvas.height = 200;
canvas.width = 600;
const tileSize = canvas.height/6;
const MAIN = {position:{x: 120, y: 120}};
const canvasRefresh = {x: 0, y: 20};
document.body.onmousemove = e => MAIN.position = {x: e.clientX, y: e.clientY};
const tiles = {x: 20, y: 20}
function update(){
moveMap();
requestAnimationFrame(update);
}
function drawMap(){
for(var i = 0; i < tiles.x; i++){
for(var j = 0; j < tiles.y; j++){
ctx.fillStyle = ['black', 'green','orange'][Math.floor((i+j+canvasRefresh.x1+canvasRefresh.y1)%3)];
ctx.fillRect(tileSize * i, tileSize * j, tileSize, tileSize);
}
}
}
function moveMap(){
const sector = {
x: Math.round(-MAIN.position.x % tileSize),
y: Math.round(-MAIN.position.y % tileSize)
};
const x2 = Math.floor(MAIN.position.x/tileSize);
const y2 = Math.floor(MAIN.position.y/tileSize);
if(canvasRefresh.x1 != x2 || canvasRefresh.y1 != y2){
canvasRefresh.x1 = x2;
canvasRefresh.y1 = y2;
requestAnimationFrame(drawMap);
}
$('#canvas').css({
transform: "translate3d(" + sector.x + "px, " + sector.y + "px, 0)"
});
}
update();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas id=canvas></canvas>
There are a few things going on:
drawMap
instead of using requestAnimationFrame
As ggorlen mentioned in the comments, using requestAnimationFrame
multiple times in an update cycle is an unusual practice. When you use requestAnimationFrame
, you're calling the function on the next frame update, meaning there will be a frame where the map isn't redrawn, causing a slight flicker. Instead, if you invoke it immediately, it'll redraw the map for that frame. Also, it's a good idea to consolidate all your painting and updating to one invocation of requestAnimationFrame
, since it makes it clearer what order things are updated.
So you should change requestAnimationFrame(drawMap);
to drawMap();
Modulo arithmetic (i.e. the %
operator) generally works with integers. In the case where you have MAIN.position.x % tileSize
, it glitches out every so often because tileSize
isn't an integer (200 / 6). To find remainders using non-integer numbers, we can use a custom function:
function remainder(a, b) {
return a - Math.floor(a / b) * b;
}
and replace instances of modulo arithmetic with our new function (e.g. changing MAIN.position.x % tileSize
to remainder(MAIN.position.x, tileSize)
)
Math.round
vs Math.floor
Finally, you probably want to use Math.floor
instead of Math.round
, because Math.round
returns 0, both for ranges between (-1, 0) and (0, 1), while Math.floor
returns -1, and 0.
You may want to using a containing div and corresponding css to hide the edges of the canvas that are being redrawn:
In the HTML:
<div class="container">
<canvas id=canvas></canvas>
</div>
In the CSS:
.container {
width: 560px;
height: 160px;
overflow: hidden;
}
All together it looks like this:
const ctx = canvas.getContext('2d');
canvas.height = 200;
canvas.width = 600;
const tileSize = canvas.height/6;
const MAIN = {position:{x: 120, y: 120}};
const canvasRefresh = {x: 0, y: 20};
document.body.onmousemove = e => MAIN.position = {x: e.clientX, y: e.clientY};
const tiles = {x: 20, y: 20}
function update(){
moveMap();
requestAnimationFrame(update);
}
function drawMap(){
for(var i = 0; i < tiles.x; i++){
for(var j = 0; j < tiles.y; j++){
ctx.fillStyle = ['black', 'green','orange'][Math.floor((i+j+canvasRefresh.x1+canvasRefresh.y1)%3)];
ctx.fillRect(tileSize * i, tileSize * j, tileSize, tileSize);
}
}
}
function remainder(a, b) {
return a - Math.floor(a / b) * b;
}
function moveMap(){
const sector = {
x: Math.floor(-remainder(MAIN.position.x, tileSize)),
y: Math.floor(-remainder(MAIN.position.y, tileSize))
};
const x2 = Math.floor(MAIN.position.x/tileSize);
const y2 = Math.floor(MAIN.position.y/tileSize);
if(canvasRefresh.x1 != x2 || canvasRefresh.y1 != y2){
canvasRefresh.x1 = x2;
canvasRefresh.y1 = y2;
drawMap();
}
$('#canvas').css({
transform: "translate3d(" + sector.x + "px, " + sector.y + "px, 0)"
});
}
update();
.container {
width: 560px;
height: 160px;
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="container">
<canvas id=canvas></canvas>
</div>