From 20f9a2107de35a52fe21584e31f99989160356da Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Fri, 19 Dec 2025 11:41:37 +0100 Subject: [PATCH] Add bitmap font loading functionality and implement Map2D for 2D key-value mapping --- .../be/seeseemelk/diceos/system/DiceOS.java | 8 + .../diceos/system/ResourceLoader.java | 197 ++++++++++++++++++ .../seeseemelk/diceos/system/utils/Map2D.java | 44 ++++ src/main/resources/system/dice.png | Bin 240 -> 274 bytes src/main/resources/system/dice2.png | Bin 0 -> 231 bytes src/main/resources/system/font.fnt | 4 + src/main/resources/system/font.png | Bin 0 -> 875 bytes src/main/resources/system/font.txt | 2 + 8 files changed, 255 insertions(+) create mode 100644 src/main/java/be/seeseemelk/diceos/system/utils/Map2D.java create mode 100644 src/main/resources/system/dice2.png create mode 100644 src/main/resources/system/font.fnt create mode 100644 src/main/resources/system/font.png create mode 100644 src/main/resources/system/font.txt diff --git a/src/main/java/be/seeseemelk/diceos/system/DiceOS.java b/src/main/java/be/seeseemelk/diceos/system/DiceOS.java index 245d2d7..403f97f 100644 --- a/src/main/java/be/seeseemelk/diceos/system/DiceOS.java +++ b/src/main/java/be/seeseemelk/diceos/system/DiceOS.java @@ -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(); diff --git a/src/main/java/be/seeseemelk/diceos/system/ResourceLoader.java b/src/main/java/be/seeseemelk/diceos/system/ResourceLoader.java index 6dd4548..9a257f8 100644 --- a/src/main/java/be/seeseemelk/diceos/system/ResourceLoader.java +++ b/src/main/java/be/seeseemelk/diceos/system/ResourceLoader.java @@ -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 pixmaps = new HashMap<>(); private final Map textures = new HashMap<>(); + private final Map 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(); + 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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; + } + } } diff --git a/src/main/java/be/seeseemelk/diceos/system/utils/Map2D.java b/src/main/java/be/seeseemelk/diceos/system/utils/Map2D.java new file mode 100644 index 0000000..b5ab781 --- /dev/null +++ b/src/main/java/be/seeseemelk/diceos/system/utils/Map2D.java @@ -0,0 +1,44 @@ +package be.seeseemelk.diceos.system.utils; + +import lombok.EqualsAndHashCode; + +import java.util.HashMap; +import java.util.Map; + +public class Map2D { + private final Map 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); + } +} diff --git a/src/main/resources/system/dice.png b/src/main/resources/system/dice.png index e4bdd3b0fe236ed40254930b86b0433c1084d576..2aab7a24d6d7d9f32cb819d29390db7518e2aa50 100644 GIT binary patch delta 246 zcmVw5riNNeK;24Sb+yC*am3B8#jyKW?$66PzW>Z z0m24|uFN=wd|vWGqR^g%cSlLeWX+a1$hz!R?rcM%!9c<5vL})Wq})>z%#?3F?mr^( ztpBJ?0C4Eo@bSSZADki}lDSU-suUi3K!2f;sR<&3RDMsBz;EW>@z4?Hwg=*STeFG7CFyw|#72=UG*b*5s^gV$zlk~5 w1tB`tYycq5fw)Xf*1pWUvhR@Y)_U;&0#`t=f3*lcRsaA107*qoM6N<$g2H}wfdBvi delta 212 zcmbQl^nr1LNG(&67Iy3=9k`>5jgR3=A9lx&I`x0{IHb9znhg3{`3j3=J&| z48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!CBxDS$hu^F$&LX_|@NTFU5AHgz>27%tvQj3>g^mE(nSL=+tKe8p+`4>gTe~ HDWM4fj*maF diff --git a/src/main/resources/system/dice2.png b/src/main/resources/system/dice2.png new file mode 100644 index 0000000000000000000000000000000000000000..69354525a6c4bb80aec28443e4990e5cd085f6c0 GIT binary patch literal 231 zcmVPx&B}qg7kK59vjkZw2V|WYy1^3h8T|4TJOEL z<2c#~i{`a?yjSzB(D|EsuXwMF57|D5WLC-d-b4N0QM$4504Ral1-||z>KphNZ-@V? zzRde%c|Cq?Q+QP$R9@8w^}RhC>~}?8)}eG%^o%rDrxDET<<($~bg1@Jz3kmp8!V6` zf1)vo>{kL6b<2r_@TdIft@_{r_7i`!_p#GVRtL zRnZsCLUjrBXk(j|Q9ZrcY?ASV(N{11%Ca8$6!C|#!SEWM&q&V%#hBG_Svr94NAAnE zg)?o`_oK^^9c{489CeVFX;r<?P^_d72k~i zSvgj>mXSFj$rOI?(5u)`PE1rg)jH>FeY9GDXS@{|)n0UP6aqh{?ylEWI_>0-WW3oM z(J^?h4#wz|XSQvH|9B#1okHTt80t!8)6s$K$w;O+jWi{zS+=5?8QJw{p1h)8`OWky z*+!AC+EVD1$rJgKLbW_~&IsiO5#wdBZXrdtwK6hKz^c`aeT(2Xz$#ewN}05^xjO2Z znIgm9C6eaQvObJ9R_R>{_95Z*-`J+`MEa`1Sh_so$XHth8Gj6?&S#{JbbKBSTTbGB zkL_^~UVvk^MBkX-mW=05l(9WKWBZ*M+zVzl)@oG&^&=hiP9>0vaMfUSud{I<-ePRt zc$4e4%CfD;4e0^ss%zv^{M~>002ovPDHLkV1ht( By@db( literal 0 HcmV?d00001 diff --git a/src/main/resources/system/font.txt b/src/main/resources/system/font.txt new file mode 100644 index 0000000..d1daced --- /dev/null +++ b/src/main/resources/system/font.txt @@ -0,0 +1,2 @@ +aAbBcCdDeEfFgGhHiIjJkKlLmMnN +oOpPqQrRsStTuUvVwWxXyYzZ