import sharp from "sharp" // Character to vestaboard code mapping const charToCode: Record = { " ": 0, A: 1, B: 2, C: 3, D: 4, E: 5, F: 6, G: 7, H: 8, I: 9, J: 10, K: 11, L: 12, M: 13, N: 14, O: 15, P: 16, Q: 17, R: 18, S: 19, T: 20, U: 21, V: 22, W: 23, X: 24, Y: 25, Z: 26, "1": 27, "2": 28, "3": 29, "4": 30, "5": 31, "6": 32, "7": 33, "8": 34, "9": 35, "0": 36, "!": 37, "@": 38, "#": 39, "$": 40, "(": 41, ")": 42, "-": 44, "+": 46, "&": 47, "=": 48, ";": 49, ":": 50, "'": 52, '"': 53, "%": 54, ",": 55, ".": 56, "/": 59, "?": 60, "°": 62, "🟥": 63, "🟧": 64, "🟨": 65, "🟩": 66, "🟦": 67, "🟪": 68, "⬜": 69, "⬛": 70, } // Code to display info for rendering const codeToDisplay: Record = { 0: { bg: "#1a1a1a", fg: "#ffffff" }, // space (empty black tile) 63: { bg: "#e63946", fg: "#ffffff" }, // red 64: { bg: "#f4a261", fg: "#1a1a1a" }, // orange 65: { bg: "#e9c46a", fg: "#1a1a1a" }, // yellow 66: { bg: "#2a9d8f", fg: "#ffffff" }, // green 67: { bg: "#0077b6", fg: "#ffffff" }, // blue 68: { bg: "#9b5de5", fg: "#ffffff" }, // purple 69: { bg: "#ffffff", fg: "#1a1a1a" }, // white 70: { bg: "#1a1a1a", fg: "#ffffff" }, // black } // Code to character (for letters/numbers/symbols) const codeToChar: Record = {} for (const [char, code] of Object.entries(charToCode)) { if (code >= 1 && code <= 62) { codeToChar[code] = char } } const TILE_SIZE = 24 const GAP = 2 const COLS = 22 const ROWS = 6 // Parse emoji grid string to number array export const parseEmojiGrid = (grid: string): number[][] => { const lines = grid.trim().split("\n") const result: number[][] = [] for (const line of lines) { const row: number[] = [] const chars = [...line] // Handle multi-byte emoji correctly for (const char of chars) { const code = charToCode[char.toUpperCase()] if (code !== undefined) { row.push(code) } } // Pad or trim to 22 columns while (row.length < COLS) row.push(0) if (row.length > COLS) row.length = COLS result.push(row) } // Pad or trim to 6 rows while (result.length < ROWS) result.push(Array(COLS).fill(0)) if (result.length > ROWS) result.length = ROWS return result } // Convert number grid to emoji string export const gridToEmoji = (grid: number[][]): string => { const emojiMap: Record = { 0: " ", 63: "🟥", 64: "🟧", 65: "🟨", 66: "🟩", 67: "🟦", 68: "🟪", 69: "⬜", 70: "⬛", } return grid .map((row) => row .map((code) => { if (emojiMap[code] !== undefined) return emojiMap[code] if (codeToChar[code]) return codeToChar[code] return " " }) .join("") ) .join("\n") } // Render grid to SVG string const gridToSvg = (grid: number[][]): string => { const width = COLS * TILE_SIZE + (COLS - 1) * GAP const height = ROWS * TILE_SIZE + (ROWS - 1) * GAP let svg = `` svg += `` // background for (let row = 0; row < ROWS; row++) { for (let col = 0; col < COLS; col++) { const code = grid[row]![col]! const x = col * (TILE_SIZE + GAP) const y = row * (TILE_SIZE + GAP) // Determine tile appearance let bg = "#1a1a1a" let fg = "#ffffff" let char: string | undefined const display = codeToDisplay[code] if (display) { bg = display.bg fg = display.fg } else if (codeToChar[code]) { char = codeToChar[code] bg = "#1a1a1a" fg = "#ffffff" } // Draw tile svg += `` // Draw character if present if (char) { const fontSize = 14 const textX = x + TILE_SIZE / 2 const textY = y + TILE_SIZE / 2 + fontSize * 0.35 // Escape XML special characters const escaped = char.replace(/&/g, '&').replace(//g, '>') svg += `${escaped}` } } } svg += "" return svg } // Render emoji grid string to PNG buffer export const renderToPng = async (emojiGrid: string): Promise => { const grid = parseEmojiGrid(emojiGrid) const svg = gridToSvg(grid) return sharp(Buffer.from(svg)).png().toBuffer() } // Render number grid to PNG buffer export const renderGridToPng = async (grid: number[][]): Promise => { const svg = gridToSvg(grid) return sharp(Buffer.from(svg)).png().toBuffer() }