Development · Sardinia Drive
How We Built the Graphics Engine for Sardinia Drive
How a road defined along a route becomes a perspective scene in Canvas 2D.
A road in two coordinates
In the alpha engine, z is distance along the stage and x is lateral offset. Each bend has a start, an end and a change in heading. If t is progress from 0 to 1 through the bend, its contribution to heading follows 3t² − 2t³. The slope of that function is zero at both ends, so curvature builds and fades smoothly.
Sampling the road gives its centre position, heading and curvature. The driving model reads curvature four units ahead, while the camera takes the tangent at the player’s position. The same bend at Lago Cuga shapes both the picture and the sideways pull you feel at the wheel.
From distance to pixels
The renderer measures the sampled point’s lateral offset from the road tangent at the player’s position. In the formula, baseHeading is the local lateral slope, multiplied by the distance ahead. It also subtracts 87% of the vehicle’s lateral offset, so the camera follows it partway as it steers. It then converts those offsets to pixels by multiplying by focal length divided by depth.
depth = roadPointZ - playerZ
scale = focalLength / depth
offset = roadCenter - baseCenter - baseHeading * depth
screenX = canvasWidth / 2 + (offset - 0.87 * playerX) * scale
screenY = horizonY + cameraHeight * scale
Take a 900 × 600 pixel Canvas buffer during a race. The horizon is at y = 243, the focal length is 396 and the camera height is 2.05 units. For a cross-section 7.8 units wide, the projection gives these results.
| Depth | Width in pixels | Canvas row |
|---|---|---|
| 15 units | 206 | 297 |
| 30 units | 103 | 270 |
Doubling the distance halves both the width and the vertical gap from the horizon. The expanding scale in the foreground is one of the visual cues for speed.
Building the road out of quadrilaterals
The renderer steps through up to 225 units ahead of the player in increments of 3. It projects the near and far cross-sections of each strip and connects their four corners. The same pass draws the asphalt, edges and centre dashes at different lateral offsets. Far strips are drawn first, and the nearest strip stops 1.35 units from the camera, where dividing by depth would make the scale grow too large.
The shore of Lago Cuga works the same way. The lake is widest at the centre and follows a parabolic profile down to zero 170 units away. The renderer computes a width for each end of the strip. The edge of the closed lane at the roadworks likewise starts from a lateral offset and bends with the asphalt.
How sprites overlap
After drawing the road, the engine collects roadside objects, traffic and the player’s vehicle in one list sorted by z, farthest first. A nearer bus can cover an olive tree drawn earlier. The sort uses one anchor point per sprite. When large silhouettes cross, the resulting overlap is approximate.
For the player, the engine calculates the depth at which the rear axle lands at 82% of the screen height. It puts the axle in the same list and applies the same depth offset to traffic. The player vehicles have three 320 × 320 pixel poses in a WebP atlas. Steering and slide state select the pose, then the renderer adds rotation and a shadow.
Horizon, framing and pixel cost
The Capo Caccia backdrop is split at the waterline. The renderer aligns both pieces to the same horizon as the road and scrolls the water highlights at different speeds. On a portrait screen, the arrival shot widens as the focal length drops to 77% of its racing value. The clock and progress readout are HTML elements above the canvas and stay in place.
The canvas buffer uses the device’s pixel ratio up to a cap of 1.5. On a 3× phone the buffer contains roughly a quarter as many pixels as it would at native resolution. The browser scales the result to the visible area. The trade-off limits the number of pixels the engine needs to redraw.