A 2d Viewport for Canvas

While I was tweaking the pathfinder from last time, the view started to look a bit cramped – the canvas had to show all the available information, regardless of scale or size. This got me thinking about viewports.

Viewports are a common concept in user interfaces; they’re just so common, we don’t always think about them. Any time you’re using a window with a scroll bar, the visible area is your viewport, and the scroll bar lets you move around your available information.

It’s a bit like the passenger window of a car. You see a section of the world through it; as the car moves, the view will change, but your position in relation to the window does not.

In terms of coding, viewports are quite straightforward. You need a position, which tells you where your car window is in the world, a size, which determines how much of the world you can see through it, and some way of mapping coordinates from your world to your screen.

The sample for this post can be found here. I’ve also made a slightly nicer example using a tile map, which you can see here – there’s still a timing issue with the tile loading, so if you just see a grey screen after a few seconds, you might need to hit refresh a couple of times.

The tile images used in this post and the example are from a free tile set from the Lost Garden.

Projections

The way you map positions from your world to your screen is called a projection. In this post, we’ll be dealing with a simple two dimensional world, so we don’t have to deal with perspective; since we don’t need to simulate depth, we just need to worry about where things are.

In this case, finding what goes where on the screen is a question of offsetting the position of an object in the world by the position of your viewport; that is

x of object on screen = x of object in world – x of viewport
y of object on screen = y of object in world – y of viewport

global

In the example above, our world is a 10×10 grid, while our viewport is a 3×3 grid located at world position 2,2. To convert our world position to a screen position, we subtract the coordinates of the viewport – 2,2 becomes 0,0; 3,2 becomes 1,0, and so on. This gives us

view

Now, while the above gives us screen coordinates, but the scale of those coordinates might be a bit off. In the example above, the world uses a simple unit of measure, which works fine, but each world unit represents 40px on screen. If we want to draw that on a screen, we need to multiply the coordinate values by a scale to get the final pixel coordinates and dimensions.

Taking the same example again:

x of object on screen = (x of object in world – x of viewport) * scale
y of object on screen = (y of object in world – y of viewport) * scale

So 2, 2 becomes 0, 0; 3, 2 becomes 40, 0; 3, 3 becomes 40,40; this gives us the exact coordinates we need to put the view on the canvas.

In CoffeeScript, this is simply:

toScreenCoordinates: (point) ->
    { x: (point.x - @x) * @scale , y: (point.y - @y) * @scale }

Where point is the point we want to map, and @ points to the viewport.

Using a scale, we can render the same map in a number of different viewports, giving us a mini map of sorts. This can be useful for any number of practical applications, such as image editing, where you need to keep an eye on the whole picture while playing around with a detail.

minimap

In the picture above, we’re using two viewports – a primary one and a minimap. The primary view has a scale of 5 – that gives 5 px for each world unit. The minimap uses a scale of 0.2 – 1px for every 5 world units. Since each viewport is represented by a set of world coordinates, we can even draw the outline of the primary viewport in the minimap to show the position you’re looking at. You could even, playing around with the numbers, flip it around so as to have a high level map in the main view, and zoomed detail in the secondary view.

For the examples, I’m rendering the secondary view in the same canvas, but there’s no particular reason for this. You could just as easily render it in a separate canvas. I’m also using the same rendering function for both, but again, you could provide separate ones as you prefer.

Mapping back

Once you have an object on screen, your users will probably try to interact with it in some manner. They’re funny that way, users. Now, normally you can figure out where they clicked on screen and just used back, but since the view is a projection of the actual object, you’re going to need to map it back to see what in the world got their attention. Luckily, it’s enough to just reverse the formula we used earlier to get a world coordinate from a screen coordinate:

x of object in world = (x of object on screen / scale) + x of viewport
y of object in world = (y of object on screen / scale) + y of viewport

or, in CoffeeScript:

toWorldCoordinates: (point) ->
    { x: (point.x / @scale) + @x, y: (point.y / @scale) + @y }

Taking the earlier example, if the user clicks on the canvas at 40,80, and assuming viewport is at 2,2 and the scale is 40, we get:

x: (40 / 40) + 2: 3
y: (80 / 40) + 2: 4

or 3, 4; 1 unit to the right and 2 units below the corner of the viewport. In the examples, you can see it used in the click event for the minimaps.

And that’s pretty much it for this viewport. I’d like to revisit it at some point and add support for isometric projections. In the meantime, the pathfinder still needs plenty of tweaking, and that loading issue with the tile map loading will need some attention.

This is not your granddaddy’s JavaScript. Find out more with The Little Book of JavaScript!

One Reply to “A 2d Viewport for Canvas”

Comments are closed.