Add bitmap font loading functionality and implement Map2D for 2D key-value mapping

This commit is contained in:
2025-12-19 11:41:37 +01:00
parent 4b00036369
commit 20f9a2107d
8 changed files with 255 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ import be.seeseemelk.diceos.system.gfx.GraphicsContext;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.ScreenUtils;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
@@ -32,6 +33,8 @@ public class DiceOS extends ApplicationAdapter {
private int offsetY = 0;
private GraphicsContext gc;
private BitmapFont font;
@Override
public void create() {
log.info("DiceOS starting...");
@@ -47,6 +50,8 @@ public class DiceOS extends ApplicationAdapter {
task.onStartup();
}
font = resourceLoader.loadFont("system/font");
log.info("DiceOS started!");
}
@@ -92,6 +97,9 @@ public class DiceOS extends ApplicationAdapter {
param.flipX = false;
display.draw(border, 0, display.getHeight() - 8, param);
font.setColor(1, 1, 1, 0.5f);
font.draw(display.getBatch(), "DiceOS", 50, display.getHeight() - 40);
// Finish rendering to screen buffer
display.getBatch().end();
display.getScreenBuffer().end();

View File

@@ -1,19 +1,26 @@
package be.seeseemelk.diceos.system;
import be.seeseemelk.diceos.system.utils.Map2D;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import io.avaje.inject.Component;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
@Component
public class ResourceLoader {
private final Map<String, Pixmap> pixmaps = new HashMap<>();
private final Map<String, Texture> textures = new HashMap<>();
private final Map<String, BitmapFont> fonts = new HashMap<>();
public Pixmap loadPixmap(String path) {
return pixmaps.computeIfAbsent(path, p -> new Pixmap(Gdx.files.internal(p)));
@@ -22,4 +29,194 @@ public class ResourceLoader {
public Texture loadTexture(String path) {
return textures.computeIfAbsent(path, p -> new Texture(Gdx.files.internal(p)));
}
public BitmapFont loadFont(String path) {
return fonts.computeIfAbsent(path, this::loadBitmapfont);
}
private BitmapFont loadBitmapfont(String path) {
log.info("Loading bitmap font from {}", path);
@Cleanup("dispose") var pixmap = loadPixmap(path + ".png");
var texture = new Texture(pixmap);
var region = new TextureRegion(texture);
var data = new BitmapFont.BitmapFontData();
var glyphMap = new Map2D<Integer, Integer, BitmapFont.Glyph>();
var glyphLines = Gdx.files.internal(path + ".txt").readString().split("\n");
for (var y = 0; y < glyphLines.length; y++) {
var line = glyphLines[y].toCharArray();
for (var x = 0; x < line.length; x++) {
var glyph = new BitmapFont.Glyph();
glyph.id = line[x];
glyphMap.put(x, y, glyph);
data.setGlyph(glyph.id, glyph);
}
}
// Read the vertical timing lines to find the horizontal ones and read glyph heights
VerticalTiming[] glyphHeights = readGlyphHeights(pixmap, glyphLines.length);
// Read the horizontal timing lines to read glyph widths
HorizontalTiming[][] glyphWidths = new HorizontalTiming[glyphLines.length][];
for (var y = 0; y < glyphHeights.length; y++) {
glyphWidths[y] = readHorizontalTimings(pixmap, glyphLines[y].length(), glyphHeights[y].timingLine);
}
// Assign glyph sizes
for (var y = 0; y < glyphLines.length; y++) {
for (var x = 0; x < glyphLines[y].length(); x++) {
var glyph = glyphMap.get(x, y);
var widthTiming = glyphWidths[y][x];
var heightTiming = glyphHeights[y];
glyph.width = widthTiming.getGlyphWidth();
glyph.height = heightTiming.getGlyphHeight();
glyph.srcX = widthTiming.glyphStart;
glyph.srcY = heightTiming.glyphStart;
glyph.xoffset = 0;
glyph.yoffset = 0;
glyph.xadvance = glyph.width;
glyph.u = (float)widthTiming.glyphStart / pixmap.getWidth();
glyph.u2 = (float)widthTiming.glyphEnd / pixmap.getWidth();
glyph.v = (float)heightTiming.glyphStart / pixmap.getHeight();
glyph.v2 = (float)heightTiming.glyphEnd / pixmap.getHeight();
}
}
return new BitmapFont(data, region, true);
}
/**
* Reads the vertical timing line from the pixmap.
* <p>
* The timing line is a 1 pixel wide line on the left side of the pixmap that indicate the positions of the
* horizontal timing lines, and the heights of the glyphs.
* <p>
* A white pixel on the timing line indicates the line is unused, a black indicates either a timing line, or the
* height of a glyph.
* <p>
* It is expected that the line ends with a white pixel.
*
* @param pixmap The pixmap containing the timing line.
* @param glyphCountY The number of glyphs in the Y direction.
* @return An array of glyph heights.
*/
private VerticalTiming[] readGlyphHeights(Pixmap pixmap, int glyphCountY) {
log.info("Reading vertical timings for {} glyphs", glyphCountY);
enum State {
SEARCHING_FOR_LINE,
WAITING_FOR_GLYPH_HEIGHTS,
READING_GLYPH_HEIGHTS
}
State state = State.SEARCHING_FOR_LINE;
VerticalTiming[] glyphHeights = new VerticalTiming[glyphCountY];
VerticalTiming currentTiming = null;
int currentGlyphLine = 0;
int y = 0;
while (y < pixmap.getHeight()) {
boolean isBlack = pixmap.getPixel(0, y) == 0x000000ff;
switch (state) {
case SEARCHING_FOR_LINE -> {
if (isBlack) {
currentTiming = new VerticalTiming();
glyphHeights[currentGlyphLine++] = currentTiming;
currentTiming.timingLine = y;
state = State.WAITING_FOR_GLYPH_HEIGHTS;
}
}
case WAITING_FOR_GLYPH_HEIGHTS -> {
if (isBlack) {
currentTiming.glyphStart = y;
state = State.READING_GLYPH_HEIGHTS;
}
}
case READING_GLYPH_HEIGHTS -> {
if (!isBlack) {
currentTiming.glyphEnd = y;
state = State.SEARCHING_FOR_LINE;
log.info("Found glyph height: {} (from {} to {})", currentTiming.getGlyphHeight(), currentTiming.glyphStart, currentTiming.glyphEnd);
}
}
}
y++;
}
return glyphHeights;
}
private static class VerticalTiming {
/// The Y position of the horizontal timing line.
int timingLine;
/// The start Y position of the glyph line.
int glyphStart;
/// The end Y position of the glyph line.
int glyphEnd;
public int getGlyphHeight() {
return glyphEnd - glyphStart;
}
}
/**
* Reads the horizontal timing lines from the pixmap.
* <p>
* The timing lines are 1 pixel high lines and can occur at any Y position in the pixmap.
* The Y position is indicated through the vertical timing.
* <p>
* A white pixel on the timing line indicates the line is unused, a line of black pixels indicates the width of a glyph.
* @param pixmap The pixmap containing the timing lines.
* @param glyphCountX The number of glyphs in the X direction.
* @param timingLineY The Y position of the timing line to read.
* @return An array of glyph widths.
*/
private HorizontalTiming[] readHorizontalTimings(Pixmap pixmap, int glyphCountX, int timingLineY) {
log.info("Reading horizontal timings for {} glyphs at y={}", glyphCountX, timingLineY);
enum State {
WAITING_FOR_GLYPH_WIDTHS,
READING_GLYPH_WIDTHS
}
State state = State.WAITING_FOR_GLYPH_WIDTHS;
HorizontalTiming[] glyphWidths = new HorizontalTiming[glyphCountX];
HorizontalTiming currentTiming = null;
int currentGlyphIndex = 0;
int x = 1;
while (x < pixmap.getWidth()) {
boolean isBlack = pixmap.getPixel(x, timingLineY) == 0x000000ff;
switch (state) {
case WAITING_FOR_GLYPH_WIDTHS -> {
if (isBlack) {
currentTiming = new HorizontalTiming();
glyphWidths[currentGlyphIndex++] = currentTiming;
currentTiming.glyphStart = x;
state = State.READING_GLYPH_WIDTHS;
}
}
case READING_GLYPH_WIDTHS -> {
if (!isBlack) {
currentTiming.glyphEnd = x;
state = State.WAITING_FOR_GLYPH_WIDTHS;
log.info("Found glyph width: {} (from {} to {})", currentTiming.getGlyphWidth(), currentTiming.glyphStart, currentTiming.glyphEnd);
}
}
}
x++;
}
return glyphWidths;
}
private static class HorizontalTiming {
/// The X position of the vertical timing line.
int glyphStart;
/// The end X position of the vertical timing line.
int glyphEnd;
public int getGlyphWidth() {
return glyphEnd - glyphStart;
}
}
}

View File

@@ -0,0 +1,44 @@
package be.seeseemelk.diceos.system.utils;
import lombok.EqualsAndHashCode;
import java.util.HashMap;
import java.util.Map;
public class Map2D<K1, K2, V> {
private final Map<Position, V> map = new HashMap<>();
@EqualsAndHashCode
private class Position{
K1 k1;
K2 k2;
}
public void put(K1 key1, K2 key2, V value) {
Position position = new Position();
position.k1 = key1;
position.k2 = key2;
map.put(position, value);
}
public V get(K1 key1, K2 key2) {
Position position = new Position();
position.k1 = key1;
position.k2 = key2;
return map.get(position);
}
public boolean containsKey(K1 key1, K2 key2) {
Position position = new Position();
position.k1 = key1;
position.k2 = key2;
return map.containsKey(position);
}
public void remove(K1 key1, K2 key2) {
Position position = new Position();
position.k1 = key1;
position.k2 = key2;
map.remove(position);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

View File

@@ -0,0 +1,4 @@
info face="Dice Font" size=8 bold=0 italic=0 charset="" unicode=0 stretchH=100 smooth=0 aa=0 padding=1,1,1,1 spacing=1,1
common lineHeight=8 base=7 scaleW=160 scaleH=28 pages=1 packed=0 alphaChnl=0
page id=0 file="font.png"
char id=65 x=5 y=0 width=60 height=7 xoffset=5 yoffset=5 xadvance=6 page=0 chnl=15

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

View File

@@ -0,0 +1,2 @@
aAbBcCdDeEfFgGhHiIjJkKlLmMnN
oOpPqQrRsStTuUvVwWxXyYzZ