Skip to content

Instantly share code, notes, and snippets.



Last active Sep 27, 2019
What would you like to do?
Making a big image zoomable

Making a big image zoomable

When you have a giant image and you want to make it easy to pan and zoom without downloading the whole 50MB image into someone's browser, a nice workaround is to cut that image into tiles at different zoom levels and view it as it were a map. An example where I've used this technique is The "Snowpiercer" Scenario.

One way to cut your big image into the requisite tiles is with

Alternatively, this Node script will do the cutting after you install node-canvas and mkdirp:

const fs = require("fs"),
  path = require("path"),
  mkdirp = require("mkdirp"),
  { createCanvas, loadImage } = require("canvas");

const minZoom = 0,
  maxZoom = 6,
  tileDirectory = "tiles";

const tile = createCanvas(256, 256).getContext("2d");

loadImage("original-image.png").then(function(img) {
  // Center the image in a square
  const size = Math.max(img.width, img.height);
  const centered = createCanvas(size, size);
      Math.round((size - img.width) / 2),
      Math.round((size - img.height) / 2)

  // Make each zoom level
  for (let z = minZoom; z <= maxZoom; z++) {
    const dim = 256 * Math.pow(2, z);
    const numTiles = dim / 256;
    const rescaled = createCanvas(dim, dim);
    rescaled.getContext("2d").drawImage(centered, 0, 0, dim, dim);

    // Render each tile
    for (let x = 0; x < numTiles; x++) {
      const dir = path.join(tileDirectory, z.toString(), x.toString());
      for (let y = 0; y < numTiles; y++) {
        console.warn(z, x, y);
        tile.clearRect(0, 0, 256, 256);
        tile.drawImage(rescaled, x * 256, y * 256, 256, 256, 0, 0, 256, 256);
        fs.writeFileSync(path.join(dir, y + ".png"), tile.canvas.toBuffer());

This will create a tiles directory will all of your tiles in the file structure that something like Leaflet expects.

Now, you can wrap the image in a viewer like this:

<!DOCTYPE html>
<meta charset="utf-8" />
  body {
    margin: 0;
    padding: 0;
    background-color: #fff;

  #map {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;

  <div id="map"></div>
    var map ="map").setView([0, 0], 2);
    L.tileLayer("tiles/{z}/{x}/{y}.png", {
      minZoom: 0,
      maxZoom: 6,
      noWrap: true

    map.setMaxBounds([[-90, -180], [90, 180]]);


  • You could speed this up by adding some concurrency
  • You could also skip tiles that are completely outside the original image
  • You could generate 512x512 retina tiles
  • You could use fs.mkdirSync with recursive: true instead of mkdirp on recent versions of Node.

This comment has been minimized.

Copy link

@cpietsch cpietsch commented May 15, 2019

this is golden! very simple and clean. before I used for that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment