Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rotated shape is moving on y-axis when resizing shape width

I'm trying to resize a rotated shape on canvas. My problem is that when I call the rendering function, the shape starts "drifting" depending on the shape angle. How can I prevent this?

I've made a simplified fiddle demonstrating the problem, when the canvas is clicked, the shape is grown and for some reason it drifts upwards.

Here's the fiddle: https://jsfiddle.net/x5gxo1p7/

<style>
    canvas {
        position: absolute;
        box-sizing: border-box;
        border: 1px solid red;
    }
</style>
<body>
<div>
    <canvas id="canvas"></canvas>
</div>
</body>

<script type="text/javascript">
    var canvas = document.getElementById('canvas');
    canvas.width = 300;
    canvas.height= 150;
    var ctx = canvas.getContext('2d');
    var counter = 0;

    var shape = {
        top: 120,
        left: 120,
        width: 120,
        height: 60,
        rotation: Math.PI / 180 * 15
    };



    function draw() {
        var h2 = shape.height / 2;
        var w2 = shape.width / 2;

        var x = w2;
        var y = h2;


        ctx.save();
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.translate(75,37.5)
        ctx.translate(x, y);

        ctx.rotate(Math.PI / 180 * 15);
        ctx.translate(-x, -y);

        ctx.fillStyle = '#000';
        ctx.fillRect(0, 0, shape.width, shape.height);
        ctx.restore();
}


canvas.addEventListener('click', function() {

    shape.width = shape.width + 15;
    window.requestAnimationFrame(draw.bind(this));
});

window.requestAnimationFrame(draw.bind(this));
</script>

In the "real" code the shape is resized when the resize-handle is clicked and moved but I think this example demonstrates the problem sufficiently.

EDIT: updated fiddle to clarify the issue:

https://jsfiddle.net/x5gxo1p7/9/

like image 504
Jomppe Avatar asked Aug 08 '16 11:08

Jomppe


1 Answers

Always use local coordinates to define shapes.

When rendering content that is intended to be transformed the content should be in its own (local) coordinate system. Think of a image. the top left pixel is always at 0,0 on the image no matter where you render it. The pixels are at their local coordinates, when rendered they are moved to the (world) canvas coordinates via the current transformation.

So if you make your shape with coordinates set to its local, making the rotation point at its local origin (0,0) the display coordinates are stored separately as world coordinates

var shape = {
    top: -30,   // local coordinates with rotation origin 
    left: -60,  // at 0,0
    width: 120,
    height: 60,
    world : {
         x : canvas.width / 2,
         y : canvas.height / 2,
         rot : Math.PI / 12,   // 15deg clockwise
    }
};

Now you don't have to mess about with translating forward and back... blah blah total pain.

Just

ctx.save();
ctx.translate(shape.world.x,shape.world.y);
ctx.rotate(shape.world.rot);
ctx.fillRect(shape.left, shape.top, shape.width, shape.height)
ctx.restore();

or event quicker and eliminating the need to use save and restore

ctx.setTransform(1,0,0,1,shape.world.x,shape.world.y);
ctx.rotate(shape.world.rot);
ctx.fillRect(shape.left, shape.top, shape.width, shape.height);

The local shape origin (0,0) is where the transformation places the translation.

This greatly simplifies a lot of the work that has to be done

var canvas = document.getElementById('canvas');
canvas.width = 300;
canvas.height= 150;
var ctx = canvas.getContext('2d');
ctx.fillStyle = "black";
ctx.strokeStyle = "red";
ctx.lineWidth = 2;  


var shape = {
    top: -30,   // local coordinates with rotation origin 
    left: -60,  // at 0,0
    width: 120,
    height: 60,
    world : {
         x : canvas.width / 2,
         y : canvas.height / 2,
         rot : Math.PI / 12,   // 15deg clockwise
     }
};

function draw() {
    ctx.setTransform(1,0,0,1,0,0); // to clear use default transform
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // you were scaling the shape, that can be done via a transform
    // once you have moved the shape to the world coordinates.
    ctx.setTransform(1,0,0,1,shape.world.x,shape.world.y);
    ctx.rotate(shape.world.rot);
 
    // after the transformations have moved the local to the world
    // you can ignore the canvas coordinates and work within the objects
    // local. In this case showing the unscaled box
    ctx.strokeRect(shape.left, shape.top, shape.width, shape.height);
    // and a line above the box
    ctx.strokeRect(shape.left, shape.top - 5, shape.width, 1);
 
    ctx.scale(0.5,0.5); // the scaling you were doing
    ctx.fillRect(shape.left, shape.top, shape.width, shape.height);
}

canvas.addEventListener('click', function() {
    shape.width += 15;
    shape.left -= 15 / 2;
    shape.world.rot += Math.PI / 45; // rotate to illustrate location 
                                     // of local origin
    var distToMove = 15/2;
    shape.world.x += Math.cos(shape.world.rot) * distToMove;
    shape.world.y += Math.sin(shape.world.rot) * distToMove;
    draw();
});
// no need to use requestAnimationFrame (RAF) if you are not animation 
// but its not wrong. Nor do you need to bind this (in this case
// this = window) to the callback RAF does not bind a context
// to the callback 
/*window.requestAnimationFrame(draw.bind(this));*/
requestAnimationFrame(draw); // functionaly identical
// or just
/*draw()*/ //will work
 body { font-family : Arial,"Helvetica Neue",Helvetica,sans-serif; font-size : 12px; color : #242729;} /* SO font currently being used */

canvas { border: 1px solid red; }
<canvas id="canvas"></canvas>
<p>Click to grow "and rotate" (I add that to illustrate the local origin)</p>
<p>I have added a red box and a line above the box, showing how using the local coordinates to define a shape makes it a lot easier to then manipulate that shape when rendering "see code comments".</p>
like image 62
Blindman67 Avatar answered Sep 28 '22 09:09

Blindman67