Home Reference Source

src/index.js

/**
 * Utils to extrude the tiles in a tileset by 1px.
 *
 * TODO:
 *  - Allow for customizable extrusion amount
 *  - Repacking large images?
 *  - Web app
 */

const Jimp = require("jimp");
const { copyPixels, copyPixelToRect } = require("./copy-pixels");

/**
 * Accepts an image path and returns a Promise that resolves to a Buffer containing the extruded
 * tileset image.
 * @param {integer} tileWidth - tile width in pixels.
 * @param {integer} tileHeight - tile height in pixels.
 * @param {string} inputPath - the path to the tileset you want to extrude.
 * @param {object} [options] - optional settings.
 * @param {string} [options.mime=Jimp.AUTO] - the mime type that should be used for the buffer.
 * Defaults to Jimp.AUTO which tries to use the image's original mime type, and if not available,
 * uses png. Supported mime options: "image/png", "image/jpeg", "image/bmp".
 * @param {integer} [options.margin=0] - number of pixels between tiles and the edge of the tileset
 * image.
 * @param {integer} [options.spacing=0] - number of pixels between neighboring tiles.
 * @param {integer} [options.extrusion=1] - number of pixels to extrude the tiles.
 * @param {number} [options.color=0xffffff00] - color to use for the background color, which only
 * matters if there is margin or spacing. This is passed directly to jimp which takes RGBA hex or a
 * CSS color string, e.g. '#FF0000'. This defaults to transparent white.
 * @returns {Promise<Buffer>} - A promise that resolves to an image buffer, or rejects with an
 * error.
 */
async function extrudeTilesetToBuffer(
  tileWidth,
  tileHeight,
  inputPath,
  { mime = Jimp.AUTO, margin, spacing, color } = {}
) {
  const options = { margin, spacing, color };
  const extrudedImage = await extrudeTilesetToJimp(tileWidth, tileHeight, inputPath, options).catch(
    (err) => {
      console.error("Error extruding tileset: ", err);
      throw err;
    }
  );
  const buffer = await extrudedImage.getBufferAsync(mime).catch((err) => {
    console.error("Buffer could not be created from tileset.");
    throw err;
  });
  return buffer;
}

/**
 * Accepts an image path and saves out an extruded version of the tileset to `outputPath`. It
 * returns a Promise that resolves when the file has finished saving.
 * @param {integer} tileWidth - tile width in pixels.
 * @param {integer} tileHeight - tile height in pixels.
 * @param {string} inputPath - the path to the tileset you want to extrude.
 * @param {string} outputPath - the path to output the extruded tileset image.
 * @param {object} [options] - optional settings.
 * @param {integer} [options.margin=0] - number of pixels between tiles and the edge of the tileset
 * image.
 * @param {integer} [options.spacing=0] - number of pixels between neighboring tiles.
 * @param {integer} [options.extrusion=1] - number of pixels to extrude the tiles.
 * @param {number} [options.color=0xffffff00] - color to use for the background color, which only
 * matters if there is margin or spacing. This is passed directly to jimp which takes RGBA hex or a
 * CSS color string, e.g. '#FF0000'. This defaults to transparent white.
 * @returns {Promise} - A promise that resolves when finished saving, or rejects with an error.
 */
async function extrudeTilesetToImage(tileWidth, tileHeight, inputPath, outputPath, options) {
  const extrudedImage = await extrudeTilesetToJimp(tileWidth, tileHeight, inputPath, options).catch(
    (err) => {
      console.error("Error extruding tileset: ", err);
      throw err;
    }
  );
  await extrudedImage.writeAsync(outputPath).catch((err) => {
    console.error(`Tileset image could not be saved to: ${outputPath}`);
    throw err;
  });
}

/**
 * Accepts an image path and returns a Jimp image object containing the extruded image. This is
 * exposed for advanced image processing purposes. For more common uses, see extrudeTilesetToImage
 * or extrudeTilesetToBuffer. It returns a Promise that resolves when it is finished extruding the
 * image.
 * @param {integer} tileWidth - tile width in pixels.
 * @param {integer} tileHeight - tile height in pixels.
 * @param {string} inputPath - the path to the tileset you want to extrude.
 * @param {object} [options] - optional settings.
 * @param {integer} [options.margin=0] - number of pixels between tiles and the edge of the tileset
 * image.
 * @param {integer} [options.spacing=0] - number of pixels between neighboring tiles.
 * @param {integer} [options.extrusion=1] - number of pixels to extrude the tiles.
 * @param {number} [options.color=0xffffff00] - color to use for the background color, which only
 * matters if there is margin or spacing. This is passed directly to jimp which takes RGBA hex or a
 * CSS color string, e.g. '#FF0000'. This defaults to transparent white.
 * @returns {Promise<Image>} - A promise that resolves to a Jimp image object, or rejects with an
 * error.
 */
async function extrudeTilesetToJimp(
  tileWidth,
  tileHeight,
  inputPath,
  { margin = 0, spacing = 0, color = 0xffffff00, extrusion = 1 } = {}
) {
  const image = await Jimp.read(inputPath).catch((err) => {
    console.error(`Tileset image could not be loaded from: ${inputPath}`);
    throw err;
  });

  const { width, height } = image.bitmap;

  // Solve for "cols" & "rows" to get the formulae used here:
  //  width = 2 * margin + (cols - 1) * spacing + cols * tileWidth
  //  height = 2 * margin + (rows - 1) * spacing + rows * tileHeight
  const cols = (width - 2 * margin + spacing) / (tileWidth + spacing);
  const rows = (height - 2 * margin + spacing) / (tileHeight + spacing);

  if (!Number.isInteger(cols) || !Number.isInteger(rows)) {
    throw new Error(
      "Non-integer number of rows or cols found. The image doesn't match the specified parameters. Double check your margin, spacing, tileWidth and tileHeight."
    );
  }

  // Same calculation but in reverse & inflating the tile size by the extrusion amount
  const newWidth = 2 * margin + (cols - 1) * spacing + cols * (tileWidth + 2 * extrusion);
  const newHeight = 2 * margin + (rows - 1) * spacing + rows * (tileHeight + 2 * extrusion);

  const extrudedImage = await new Jimp(newWidth, newHeight, color);

  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      let srcX = margin + col * (tileWidth + spacing); // x of tile top left
      let srcY = margin + row * (tileHeight + spacing); // y of tile top left
      let destX = margin + col * (tileWidth + spacing + 2 * extrusion); // x of the extruded tile top left
      let destY = margin + row * (tileHeight + spacing + 2 * extrusion); // y of the extruded tile top left
      const tw = tileWidth;
      const th = tileHeight;

      // Copy the tile.
      copyPixels(image, srcX, srcY, tw, th, extrudedImage, destX + extrusion, destY + extrusion);

      for (let i = 0; i < extrusion; i++) {
        // Extrude the top row.
        copyPixels(image, srcX, srcY, tw, 1, extrudedImage, destX + extrusion, destY + i);

        // Extrude the bottom row.
        copyPixels(
          image,
          srcX,
          srcY + th - 1,
          tw,
          1,
          extrudedImage,
          destX + extrusion,
          destY + extrusion + th + (extrusion - i - 1)
        );

        // Extrude left column.
        copyPixels(image, srcX, srcY, 1, th, extrudedImage, destX + i, destY + extrusion);

        // Extrude the right column.
        copyPixels(
          image,
          srcX + tw - 1,
          srcY,
          1,
          th,
          extrudedImage,
          destX + extrusion + tw + (extrusion - i - 1),
          destY + extrusion
        );
      }

      // Extrude the top left corner.
      copyPixelToRect(image, srcX, srcY, extrudedImage, destX, destY, extrusion, extrusion);

      // Extrude the top right corner.
      copyPixelToRect(
        image,
        srcX + tw - 1,
        srcY,
        extrudedImage,
        destX + extrusion + tw,
        destY,
        extrusion,
        extrusion
      );

      // Extrude the bottom left corner.
      copyPixelToRect(
        image,
        srcX,
        srcY + th - 1,
        extrudedImage,
        destX,
        destY + extrusion + th,
        extrusion,
        extrusion
      );

      // Extrude the bottom right corner.
      copyPixelToRect(
        image,
        srcX + tw - 1,
        srcY + th - 1,
        extrudedImage,
        destX + extrusion + tw,
        destY + extrusion + th,
        extrusion,
        extrusion
      );
    }
  }

  return extrudedImage;
}

module.exports = {
  extrudeTilesetToBuffer,
  extrudeTilesetToImage,
  extrudeTilesetToJimp,
};