Practice III. Pixel typography

— JavaScript & Haskell version

2021-07-11

This is a comparative study, similar to a game I made in that I implement the same behavior in two different languages for practice and comparison.

Excluding the data structure for the font, instead present in the GitHub repo, I’ve tried to emphasize similarities:

JavaScript

const charHeight = 8;
const pixelSize = 3;
const lineHeight = charHeight * pixelSize + 10;
const magenta = 'magenta';
const black = 'black';
const widthBetweenCharacters = pixelSize;
const filledPoint = 'O';
const canvas = document.querySelector('#canvas');
const context = canvas.getContext('2d');

const setPixel = (x, y, strokeColor) => {
  context.fillStyle = strokeColor;
  context.fillRect(x, y, pixelSize, pixelSize);
};
    
const setPixel = require("./draw.js");

const trimLn = (ln) => ln.trim();

const isPointFilled = (pnt) => pnt === filledPoint;

const drawPntOfCh = (x, y, color, chIndex) => (pixel, pixelX) => {
  if (isPointFilled(pixel)) {
    setPixel(x + pixelX * pixelSize, y + chIndex * pixelSize, color);
  }
};

const drawPntsOfCh = (x, y, color) => (ch, chIndex) =>
  ch.split("").forEach(drawPntOfCh(x, y, color, chIndex));

const getCharWidth = (ch) => ch.split("\n")[2].trim().length;

const sumLnWidth = (acc, ch) =>
  acc + getCharWidth(font[ch]) * pixelSize + widthBetweenCharacters;

const getRelativeX = (ln) => ln.split("").reduce(sumLnWidth, 0);

const drawCharacter = (ln, x, y, color) => (ch, chX) =>
  font[ch]
    .split("\n")
    .map(trimLn)
    .forEach(
      drawPntsOfCh(
        x + getRelativeX(ln.slice(0, chX)),
        y,
        ch in font ? color : magenta
      )
    );

const drawLine = (x, y, color) => (ln, lnY = 0) =>
  ln.trim().length > 0 &&
  ln.split("").forEach(drawCharacter(ln, x, y + lnY * lineHeight, color));

const drawText = (text, color, x, y) =>
  text.split("\n").length === 1
    ? drawLine(x, y, color)(text.toUpperCase())
    : text.toUpperCase().split("\n").forEach(drawLine(x, y, color));

drawText(example, black, 50, 48);    

Haskell

import           Graphics.Gloss
import qualified Data.Map       as Map
import           Data.Maybe
import           Font -- data structure for the font
import           Graphics.Gloss.Interface.Pure.Game
import           Text

type Position = (Int, Int)

type ChFontData = [(Int, [(Int, Char)])]

window ∷ Display
window = InWindow "Pixly Typography" (1024, 768) (0, 0)

main ∷ IO ()
main = display window white picture
  where
    picture = pictures $ printText (0, 0) "Hello, Pixly!"
    
printText ∷ Position → [Char] → [Picture]
printText pos text =
  concatMap (\(i, ch) → drawChar ch pos (take i text)) $
  zip [0 .. length text] text

pixelSize ∷ Int
pixelSize = 3

background ∷ Color
background = white

forgroundColor ∷ Color
forgroundColor = black

widthBetweenChs ∷ Int
widthBetweenChs = pixelSize

formatLine ∷ [a] → [(Int, a)]
formatLine lns = zip [0 .. length lns] lns

formatCh ∷ [[Char]] → ChFontData
formatCh = formatLine . map formatLine

getLinesOfCh ∷ Char → ChFontData
getLinesOfCh =
  formatCh .
  reverse . filter (/= "") . (lines . fromMaybe []) . flip Map.lookup font

getCharWidth ∷ Char → Int
getCharWidth = length . snd . head . getLinesOfCh

isPntFull ∷ Char → Bool
isPntFull ch = ch == 'O'

setPixel ∷ Color → (Float, Float) → Picture
setPixel rectColor (x, y) =
  translate x y $
  color rectColor $
  rectangleSolid (fromIntegral pixelSize) (fromIntegral pixelSize)

drawChar ∷ Char → Position → [Char] → [Picture]
drawChar ch pos text =
  concatMap (\fontData → drawPtnLn fontData pos text) $ getLinesOfCh ch

sumLns ∷ Int → Char → Int
sumLns acc x = acc + getCharWidth x * pixelSize + widthBetweenChs

lineLen ∷ [Char] → Int
lineLen = foldl sumLns 0

drawPtnLn ∷ (Int, [(Int, Char)]) → Position → [Char] → [Picture]
drawPtnLn (y2, ln) pos textSoFar =
  map (\(x2, ch) → drawLnPart (x2, y2) ch pos (lineLen textSoFar)) ln

drawLnPart ∷ Position → Char → Position → Int → Picture
drawLnPart (x2, y2) ch (x1, y1) currLen =
  drawPtn
    (x1 * pixelSize + currLen + x2 * pixelSize, y1 * pixelSize + y2 * pixelSize)
    ch

drawPtn ∷ Position → Char → Picture
drawPtn (x, y) ch =
  setPixel
    (if isPntFull ch
       then forgroundColor
       else background)
    ( fromIntegral x - ((1024 / 2) - (fromIntegral pixelSize / 2))
    , fromIntegral y - ((768 / 2) - (fromIntegral pixelSize / 2)))

About | Archive