[turn back...]

silent hill 1 font exploration

here’s a demo i created to explore silent hill 1’s font. it’s not perfect but i hope you enjoy!

/* constants */
const CHAR_WIDTH = 24;
const CHAR_HEIGHT = 24;
const GRID_SIZE = 10;
const CHARMAP = `!"#$%&'()*+,—./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\`]^_'abcdefghijklmnopqrstuvwxyz{|}‾©~\"`;
const MARGIN_X = 30;
const TEXT_MARGIN_Y = 5;

/* state */
let currentString = "";
let gridDrawn = false;

/* objects */
const characterImages = [];
let spriteMap;
let textbox;

function preload() {
  spriteMap = loadImage(window.SH1_SPRITE_PATH || "0.png");
}

const write = (string, drawBoundingBox = true) => {
  let cursorX = 0;
  let cursorY = 0;
  Array.from(string).forEach((character) => {
    // wrap text
    if (cursorX + CHAR_WIDTH > width) {
      cursorX = 0;
      cursorY += CHAR_HEIGHT;
    }

    // handle spaces or invalid characters
    const index = CHARMAP.indexOf(character);
    if (character === " " || index < 0) {
      cursorX += CHAR_WIDTH;
      return;
    }

    const sprite = characterImages[index];
    image(sprite, cursorX, cursorY);

    if (drawBoundingBox) {
      push();
      if (window.SH1_FONT_BBOX_COLOR) {
        stroke(window.SH1_FONT_BBOX_COLOR);
      } else {
        stroke(0, 255, 0, 255);
      }
      rect(cursorX, cursorY, sprite.width, sprite.height);
      pop();
    }

    cursorX += sprite.width;
  });
};

const loadCharacters = () => {
  spriteMap.loadPixels();
  for (let i = 0; i < GRID_SIZE; i++) {
    for (let j = 0; j < GRID_SIZE; j++) {
      // get bounding box of character
      let startX = j * CHAR_WIDTH;
      let startY = i * CHAR_HEIGHT;
      let [minX, minY] = [startX + CHAR_WIDTH, startY];
      let [maxX, maxY] = [startX, startY];

      for (let k = 0; k < CHAR_WIDTH * CHAR_HEIGHT; k++) {
        const x = startX + (k % CHAR_WIDTH);
        const y = startY + Math.floor(k / CHAR_WIDTH);
        const index = x + y * spriteMap.width;
        if (spriteMap.pixels[index * 4 + 3] !== 0) {
          // nonzero alpha
          maxX = Math.max(maxX, x);
          maxY = Math.max(maxY, y);
          minX = Math.min(minX, x);
        }
      }

      characterImages.push(
        spriteMap.get(minX, minY, maxX - minX + 1, maxY - minY + 1)
      );
    }
  }
};

const drawGrid = () => {
  // create grid
  translate(CHAR_WIDTH * (GRID_SIZE - 1) + MARGIN_X, 0);
  for (let k = 0; k < GRID_SIZE * GRID_SIZE; k++) {
    translate(CHAR_WIDTH, 0);
    if (k % GRID_SIZE === 0) {
      translate(-CHAR_WIDTH * GRID_SIZE, CHAR_HEIGHT);
    }
    rect(0, 0, CHAR_WIDTH, CHAR_HEIGHT);
    write(CHARMAP.substring(k, k + 1));
  }
  translate(-CHAR_WIDTH * (GRID_SIZE - 1), CHAR_HEIGHT + TEXT_MARGIN_Y);

  // text
  write("silent hill", false);
  translate(0, CHAR_HEIGHT);
  write("type anything", false);
};

function setup() {
  loadCharacters();

  canvas = createCanvas(512, 330);
  background(0);
  noFill();
  stroke(255);

  textbox = createInput();
  textbox.attribute("placeholder", "type something here...");

  if (window.SH1_FONT_CANVAS_PARENT) {
    const element = window.SH1_FONT_CANVAS_PARENT;
    canvas.parent(element);
    textbox.parent(element);
  }
}

function draw() {
  currentString = textbox.value();
  if (currentString) {
    // write the text
    gridDrawn = false;
    background(0);
    write(currentString, false);
  } else if (!gridDrawn) {
    // visualize the sprite map
    gridDrawn = true;
    background(0);
    drawGrid();
  }
}