Skip to content

Instantly share code, notes, and snippets.

@avelican
Last active February 2, 2024 01:32
Show Gist options
  • Save avelican/2f1023aa0195806de3eaa37c3f637321 to your computer and use it in GitHub Desktop.
Save avelican/2f1023aa0195806de3eaa37c3f637321 to your computer and use it in GitHub Desktop.
sprite font renderer
<!doctype html>
<html lang="en">
<head>
<title>sprite font</title>
<style>
body{
background-color: gray;
margin: 0;
padding: 0;
}
canvas{
background-color: white;
/*border: 1px solid black;*/
width: 2048px;
height: 1024px;
image-rendering: pixelated;
} </style>
</head>
<body>
<canvas width=512 height=256></canvas>
<img src="font.png" style="display:block;">
<script>
const font = document.querySelector('img');
font.onload = init;
const canvas = document.querySelector('canvas');
const context = canvas.getContext("2d");
const RENDER_SCALE = 4;
const FONT_BITMAP_WIDTH = 128;
const FONT_BITMAP_HEIGHT = 48;
const CHARS_PER_FONT_ROW = 16;
const CHARS_PER_FONT_COL = 6;
const CHAR_COUNT = CHARS_PER_FONT_ROW * CHARS_PER_FONT_COL;
const CHAR_WIDTH = FONT_BITMAP_WIDTH / CHARS_PER_FONT_ROW;
const CHAR_HEIGHT = FONT_BITMAP_HEIGHT / CHARS_PER_FONT_COL;
const TARGET_START_X = CHAR_WIDTH; //
let TARGET_END_X = canvas.width - CHAR_WIDTH;
// TARGET_END_X *= 1.5; // debug
const TARGET_START_Y = CHAR_HEIGHT;
const MAX_LINE_WIDTH = TARGET_END_X - TARGET_START_X;
// const CHARS_PER_SCREEN_LINE = MAX_LINE_WIDTH / CHAR_WIDTH;
const LINE_HEIGHT = CHAR_HEIGHT * 1.5;
const LETTER_SPACING = 1;
let text = '';
text = "Sphynx of Black Quartz, Judge my Vow!";
text += "\n";
// text += " ";
text += "The quick brown fox jumps over the lazy dog.";
text += "\n";
// text += " ";
text += "Lorem ipsum dolor sit amet.";
text += '\n\n';
text += " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
// text += " !\"#$%&'()*+,-";
// text += "()*+,-./0123456789";
text = `Sneed's Feed & Seed (Formerly Chuck's) is a joke and innuendo from the animated television series The Simpsons. The questionable validity of the punchline and confusing nature of the joke's structure has turned it into a tool for trolling and memes referencing the original quip, which uses the scene from the show as an exploitable in photoshops and other edits. After spreading to 4chan, "Sneedposting" became a widespread shitposting format to reference Sneed's Feed & Seed.
The original scene comes from the fifth episode of season 11 of The Simpsons titled "E I E I (ANNOYED GRUNT)," which aired on November 7th, 1999.[2] In the scene, Homer parks outside the titular farming supply store "Sneed's Feed & Seed" (underneath in parenthesis reads "Formerly Chuck's") before being berated by two men.`;
text = `Suffering, or pain in a broad sense,[1] may be an experience of unpleasantness or aversion, possibly associated with the perception of harm or threat of harm in an individual.[2] Suffering is the basic element that makes up the negative valence of affective phenomena. The opposite of suffering is pleasure or happiness.
Suffering is often categorized as physical[3] or mental.[4] It may come in all degrees of intensity, from mild to intolerable. Factors of duration and frequency of occurrence usually compound that of intensity. Attitudes toward suffering may vary widely, in the sufferer or other people, according to how much it is regarded as avoidable or unavoidable, useful or useless, deserved or undeserved.
Suffering occurs in the lives of sentient beings in numerous manners, often dramatically. As a result, many fields of human activity are concerned with some aspects of suffering. These aspects may include the nature of suffering, its processes, its origin and causes, its meaning and significance, its related personal, social, and cultural behaviors,[5] its remedies, management, and uses.
The word suffering is sometimes used in the narrow sense of physical pain, but more often it refers to psychological pain, or more often yet it refers to pain in the broad sense, i.e. to any unpleasant feeling, emotion or sensation. The word pain usually refers to physical pain, but it is also a common synonym of suffering. The words pain and suffering are often used both together in different ways. For instance, they may be used as interchangeable synonyms. Or they may be used in 'contradistinction' to one another, as in "pain is physical, suffering is mental", or "pain is inevitable, suffering is optional". Or they may be used to define each other, as in "pain is physical suffering", or "suffering is severe physical or mental pain".
Qualifiers, such as physical, mental, emotional, and psychological, are often used to refer to certain types of pain or suffering. In particular, mental pain (or suffering) may be used in relationship with physical pain (or suffering) for distinguishing between two wide categories of pain or suffering. A first caveat concerning such a distinction is that it uses physical pain in a sense that normally includes not only the 'typical sensory experience of physical pain' but also other unpleasant bodily experiences including air hunger, hunger, vestibular suffering, nausea, sleep deprivation, and itching. A second caveat is that the terms physical or mental should not be taken too literally: physical pain or suffering, as a matter of fact, happens through conscious minds and involves emotional aspects, while mental pain or suffering happens through physical brains and, being an emotion, involves important physiological aspects.
The word unpleasantness, which some people use as a synonym of suffering or pain in the broad sense, may refer to the basic affective dimension of pain (its suffering aspect), usually in contrast with the sensory dimension, as for instance in this sentence: "Pain-unpleasantness is often, though not always, closely linked to both the intensity and unique qualities of the painful sensation."[6] Other current words that have a definition with some similarity to suffering include distress, unhappiness, misery, affliction, woe, ill, discomfort, displeasure, disagreeableness.`;
// text = `The quick brown
// fox jumps over the
// lazy dog.`;
// text = `Sneed's Feed & Seed (Formerly Chuck's) is a joke and innuendo from the animated television series The Simpsons. The questionable validity of the punchline`;
function init() {
console.log('init');
initFont();
// note: initFont() CANNOT run after renderText... thanks to some web security silliness
// well, it works if there's no CORS, but I can't be arsed to set up a server
// also not sure why the hell an image file is considered CORS when they're both on file://
// questions I don't want to know the answer to...
render();
}
function render(){
context.clearRect(0,0,canvas.width,canvas.height);
renderText(text);
renderMargin();
}
document.onmousemove = mouseMove;
const round = Math.round;
function mouseMove(e) {
// console.log(e);
// let lastX = Mouse.x;
// let lastY = Mouse.y;
const rect = e.target.getBoundingClientRect();
const elementX = e.clientX - rect.left;
const elementY = e.clientY - rect.top;
const Mouse_x = round(elementX / RENDER_SCALE);
const Mouse_y = round(elementY / RENDER_SCALE);
TARGET_END_X = Mouse_x;
render();
// if (Mouse.down)
// world_drag(lastX, lastY, Mouse.x, Mouse.y);
}
let char_widths = [];
let char_startx = [];
function initFont() {
console.log('initFont');
// detects char widths
// px
let x = 0;
let y = 0;
// mashallah it will be erased
context.drawImage(font, 0, 0);
// iterate over each character
for (let i = 0; i < CHAR_COUNT; i++) {
const x_index = i % CHARS_PER_FONT_ROW;
const y_index = Math.floor(i / CHARS_PER_FONT_ROW);
let x_start = -1;
let x_end = -1;
// iterate over each pixel in the character
for (let x = x_index*CHAR_WIDTH; x<(x_index+1)*CHAR_WIDTH; x++) {
for (let y = y_index*CHAR_HEIGHT; y<(y_index+1)*CHAR_HEIGHT; y++) {
const letter = String.fromCharCode(i + 32);
// console.log(letter);
const pixel = context.getImageData(x, y, 1, 1).data;
if(pixel[3] > 0) { // check alpha
if (x_start == -1) {
x_start = x;
}
x_end = x; // gets constantly updated until the last time we see an opaque pixel
}
}
}
let width = x_end - x_start;
width += 1; // 1px wide chars have same start and end index, which would give 0
if(x_start == -1) {
// empty character
x_start = x_index * CHAR_WIDTH; // reset start_x
width = CHAR_WIDTH / 2; // NOTE: adjust space char width here
}
char_startx[i] = x_start;
char_widths[i] = width;
// console.log(char_startx[i], char_widths[i]);
}
context.clearRect(0,0,canvas.width,canvas.height);
}
function measureTextWidth(text) {
let width = 0;
for(let i = 0; i < text.length; i++) {
const ascii = text.charCodeAt(i);
const sprite_index = ascii - 32;
const ch_width = char_widths[sprite_index];
width += ch_width
width += LETTER_SPACING;
}
return width;
}
function renderTextCaps(text) {
text = text.toUpperCase();
renderText(text);
}
// let debug_y = 0;
function renderText(text) {
debug_y = TARGET_START_Y;
console.log('renderText');
// console.log(text);
{ // word-wrap
let _text = text.split(''); // workaround for frigging javascript
let cur_line_width = 0;
let lastSpaceIndex = -1;
for (let i = 0; i < text.length; i++) {
if (text[i] == '\n') {
cur_line_width = 0;
continue;
}
// console.log(text[i]);
const letter = text[i]; // debug
if (text[i] == ' ')
lastSpaceIndex = i;
const ascii = text.charCodeAt(i);
const sprite_index = ascii - 32;
const ch_width = char_widths[sprite_index];
cur_line_width += ch_width
cur_line_width += LETTER_SPACING;
const x_end = TARGET_START_X + cur_line_width;
// if (cur_line_width >= TARGET_END_X) { // TODO: ? should this be >= or >
if (x_end >= TARGET_END_X) { // TODO: ? should this be >= or >
// {
// console.log(letter, cur_line_width, TARGET_END_X);
// let x = x_end;
// let y = 0;
// let w = CHAR_WIDTH;
// let h = CHAR_HEIGHT;
// debugRect(x,y,4,4)
// }
// calculate current line width
const chunk = text.substring(lastSpaceIndex, i);
// console.log(chunk)
cur_line_width = measureTextWidth(chunk);
_text[lastSpaceIndex] = '\n'; // workaround
}
}
text = _text.join(''); // end workaround
}
// console.log(text);
// let cur_line_width = 0;
let target_x = TARGET_START_X;
let target_y = TARGET_START_Y;
for (let i = 0; i < text.length; i++) {
const letter = text[i];
// console.log(letter);
if (letter == '\n') {
// console.log('NEWLINE DETECTED. Reset target_x; increment target_y');
target_x = TARGET_START_X;
target_y += LINE_HEIGHT;
continue;
}
const ascii = text.charCodeAt(i);
const sprite_index = ascii - 32;
// console.log(letter, ascii, sprite_index);
// note: this could be optimized, modulo is expensive
// but we probably won't have much text
const x_index = sprite_index % CHARS_PER_FONT_ROW;
const y_index = Math.floor(sprite_index / CHARS_PER_FONT_ROW);
// console.log(x_index, y_index)
const x_coord = x_index * CHAR_WIDTH;
let y_coord = y_index * CHAR_HEIGHT;
const width = char_widths[sprite_index];
const startx = char_startx[sprite_index];
context.drawImage(font, startx, y_coord, width, CHAR_HEIGHT, target_x, target_y, width, CHAR_HEIGHT);
target_x += width;
target_x += LETTER_SPACING;
// target_y += 20; // debug
// cur_line_width += width;
// cur_line_width += LETTER_SPACING;
// this should rarely run, since word-wrap above handles most cases.
// this runs if there was a word with no spaces, longer than the screen
// if (target_x >= TARGET_END_X) {
// target_x = TARGET_START_X;
// target_y += LINE_HEIGHT;
// }
}
}
function renderMargin() {
const x = TARGET_END_X;
const y = 0;
const width = 1;
const height = canvas.height;
context.fillStyle = 'red';
context.fillRect(x, y, width, height);
}
function debugPixel(x,y){
context.fillStyle = 'magenta';
context.fillRect(x, y, 1, 1);
}
function debugRect(x,y,w,h) {
context.fillStyle = 'magenta';
// context.fillRect(x, y, w, h);
context.fillRect(x, debug_y, w, h);
debug_y += LINE_HEIGHT;
}
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<title>sprite font</title>
<style>
body{
background-color: gray;
}
canvas{
background-color: white;
border: 1px solid black;
width: 1024px;
height: 1024px;
image-rendering: pixelated;
} </style>
</head>
<body>
<canvas width=256 height=256></canvas>
<img src="font.png" style="display:block;">
<script>
const font = document.querySelector('img');
font.onload = renderText;
const canvas = document.querySelector('canvas');
const context = canvas.getContext("2d");
const FONT_BITMAP_WIDTH = 128;
const FONT_BITMAP_HEIGHT = 48;
const CHARS_PER_FONT_ROW = 16;
const CHARS_PER_FONT_COL = 6;
const CHAR_WIDTH = FONT_BITMAP_WIDTH / CHARS_PER_FONT_ROW;
const CHAR_HEIGHT = FONT_BITMAP_HEIGHT / CHARS_PER_FONT_COL;
const TARGET_START_X = CHAR_WIDTH; //
const TARGET_END_X = canvas.width - CHAR_WIDTH;
const TARGET_START_Y = CHAR_HEIGHT;
const MAX_LINE_WIDTH = TARGET_END_X - TARGET_START_X;
const CHARS_PER_SCREEN_LINE = MAX_LINE_WIDTH / CHAR_WIDTH;
const LINE_HEIGHT = CHAR_HEIGHT * 1.5;
let text;
text = "Sphynx of Black Quartz, Judge my Vow!";
text += "\n";
// text += " ";
text += "The quick brown fox jumps over the lazy dog.";
text += "\n";
// text += " ";
text += "Lorem ipsum dolor sit amet.";
text += '\n\n';
text += " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
function renderText() {
let target_x = TARGET_START_X;
let target_y = TARGET_START_Y;
let chars_line = 0;
for (let i = 0; i < text.length; i++) {
const letter = text[i];
if (letter == '\n') {
chars_line = 0;
target_x = TARGET_START_X;
target_y += LINE_HEIGHT;
continue;
}
const ascii = text.charCodeAt(i);
const img_index = ascii - 32;
console.log(letter, ascii, img_index);
// note: this could be optimized, modulo is expensive
// but we probably won't have much text
const x_index = img_index % CHARS_PER_FONT_ROW;
const y_index = Math.floor(img_index / CHARS_PER_FONT_ROW);
console.log(x_index, y_index)
const x_coord = x_index * CHAR_WIDTH;
const y_coord = y_index * CHAR_HEIGHT;
context.drawImage(font, x_coord, y_coord, CHAR_WIDTH, CHAR_HEIGHT, target_x, target_y, CHAR_WIDTH, CHAR_HEIGHT);
target_x += CHAR_WIDTH;
chars_line++;
if (chars_line == CHARS_PER_SCREEN_LINE) {
chars_line = 0;
target_x = TARGET_START_X;
target_y += LINE_HEIGHT;
}
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment