2010-06-29

Canvas Transforms and Origin Re-Mapping

The nodehockey client receives a set of coordinates from the server in units of meters. These physical coordinates need to be translated into pixel values on the canvas.

There are a few obstacles that need to be overcome. The first, is that there is no direct mapping of pixels to meters. My original solution was to take each coordinate and multiply it by a scaling factor, which was the height of the canvas in pixels divided by the height of the physical table in meters. To translate back when sending coordinates from the client to the server, I divided the canvas coordinates by the same scaling factor.

The second issue is that the canvas element places its origin at the top-left of the element, with positive y-values going down towards the bottom. The physical model uses the standard Cartesian scale, with the origin at the bottom-left and positive y-values going up. The original solution to that was to subtract the scaled y-coordinate from the canvas height for rendering, and do the opposite for sending mouse position back.

It turns out, canvas has a built-in method for dealing with these types of problems. Instead of manually scaling each coordinate, you can set a scaling factor on the canvas drawing context, and it will do the scaling for you:
var tableHeight = physical.height;
var canvasHeight = 400;
var scaleFactor = canvasHeight / tableHeight;

context.scale(scaleFactor, -scaleFactor);

The scale() method takes 2 arguments. The first is the multiplier on the x-axis, the second is on the y-axis. They are independent. The above code also reverses the canvas's y-axis, so that y-values move bottom to top.

There is still an issue with this solution. Since y-values are being translated into the negative range, they are drawn from the top of the canvas element upwards, hiding them from view. To solve this, a further transformation must be applied:
context.translate(0, -tableHeight);
The translate() method shifts all coordinates on the canvas by the amounts specified by the first (x-value) and second (y-value) arguments. In this case, it shifts all y-values down by the height of the physical table, which will be scaled to the height of the canvas element. Effectively, this moves to origin of the canvas to the bottom-left, meaning a straight translation of physical coordinates to pixel coordinates.

Translating from canvas pixels back to physical meters still requires a manual transformation:
function scaleToPhysical(coords) {
    scaled = {
        x : coords.x / scaleFactor,
        y : (canvasHeight - coords.y) / scaleFactor,
        r : coords.r / scaleFactor
    }
    return scaled;
}
Anyone know of a better way to do this?

Another note: the scaling applies to all line styles as well as coordinates. Therefore, it's helpful to know the width of an actual pixel in scaled terms. The following code will find the "physical" width of a single pixel:
var singlePixel = 1 / scaleFactor;

No comments:

Post a Comment