Add TOML font loading support and refactor font handling
This commit is contained in:
@@ -34,6 +34,9 @@ dependencies {
|
|||||||
testImplementation("io.avaje:avaje-inject-test:${avajeInjectVersion}")
|
testImplementation("io.avaje:avaje-inject-test:${avajeInjectVersion}")
|
||||||
testAnnotationProcessor("io.avaje:avaje-inject-generator:${avajeInjectVersion}")
|
testAnnotationProcessor("io.avaje:avaje-inject-generator:${avajeInjectVersion}")
|
||||||
|
|
||||||
|
// Parsing TOML files
|
||||||
|
implementation("tools.jackson.dataformat:jackson-dataformat-toml:3.0.2")
|
||||||
|
|
||||||
// LibGDX
|
// LibGDX
|
||||||
api("com.badlogicgames.gdx:gdx:$gdxVersion")
|
api("com.badlogicgames.gdx:gdx:$gdxVersion")
|
||||||
api("com.badlogicgames.gdx:gdx-backend-lwjgl3:$gdxVersion")
|
api("com.badlogicgames.gdx:gdx-backend-lwjgl3:$gdxVersion")
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ public class DiceOS extends ApplicationAdapter {
|
|||||||
vmViewport = new ScreenViewport();
|
vmViewport = new ScreenViewport();
|
||||||
vmViewport.update(display.getWidth(), display.getHeight(), true);
|
vmViewport.update(display.getWidth(), display.getHeight(), true);
|
||||||
|
|
||||||
|
|
||||||
for (var task : startupTasks) {
|
for (var task : startupTasks) {
|
||||||
task.onStartup();
|
task.onStartup();
|
||||||
}
|
}
|
||||||
@@ -83,22 +82,21 @@ public class DiceOS extends ApplicationAdapter {
|
|||||||
ScreenUtils.clear(Color.GREEN);
|
ScreenUtils.clear(Color.GREEN);
|
||||||
|
|
||||||
// Render background
|
// Render background
|
||||||
display.draw(clouds, 0, 0);
|
gc.draw(clouds, 0, 0);
|
||||||
|
|
||||||
windowService.paint(gc);
|
windowService.paint(gc);
|
||||||
|
|
||||||
// Render borders
|
// Render borders
|
||||||
var param = new DisplayService.Parameters();
|
var param = new GraphicsContext.Parameters();
|
||||||
display.draw(border, 0, 0, param);
|
gc.draw(border, 0, 0, param);
|
||||||
param.flipX = true;
|
param.flipX = true;
|
||||||
display.draw(border, display.getWidth() - 8, 0, param);
|
gc.draw(border, display.getWidth() - 8, 0, param);
|
||||||
param.flipY = true;
|
param.flipY = true;
|
||||||
display.draw(border, display.getWidth() - 8, display.getHeight() - 8, param);
|
gc.draw(border, display.getWidth() - 8, display.getHeight() - 8, param);
|
||||||
param.flipX = false;
|
param.flipX = false;
|
||||||
display.draw(border, 0, display.getHeight() - 8, param);
|
gc.draw(border, 0, display.getHeight() - 8, param);
|
||||||
|
|
||||||
font.setColor(1, 1, 1, 0.5f);
|
font.draw(display.getBatch(), "This is DiceOS", 6, display.getHeight() - 12);
|
||||||
font.draw(display.getBatch(), "DiceOS", 50, display.getHeight() - 40);
|
|
||||||
|
|
||||||
// Finish rendering to screen buffer
|
// Finish rendering to screen buffer
|
||||||
display.getBatch().end();
|
display.getBatch().end();
|
||||||
|
|||||||
@@ -41,22 +41,4 @@ public class DisplayService implements OnStartup {
|
|||||||
public int getHeight() {
|
public int getHeight() {
|
||||||
return 360;
|
return 360;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void draw(Texture texture, int x, int y) {
|
|
||||||
draw(texture, x, y, Parameters.DEFAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void draw(Texture texture, int x, int y, Parameters params) {
|
|
||||||
batch.draw(texture,
|
|
||||||
x, getHeight() - y - texture.getHeight(), texture.getWidth(), texture.getHeight(),
|
|
||||||
0, 0, texture.getWidth(), texture.getHeight(),
|
|
||||||
params.flipX, params.flipY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Parameters {
|
|
||||||
public final static Parameters DEFAULT = new Parameters();
|
|
||||||
|
|
||||||
public boolean flipX = false;
|
|
||||||
public boolean flipY = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
package be.seeseemelk.diceos.system;
|
package be.seeseemelk.diceos.system;
|
||||||
|
|
||||||
import be.seeseemelk.diceos.system.utils.Map2D;
|
import be.seeseemelk.diceos.system.font.FontLoader;
|
||||||
|
import be.seeseemelk.diceos.system.font.FontMetadata;
|
||||||
|
import be.seeseemelk.diceos.system.gfx.NinePatchLoader;
|
||||||
import com.badlogic.gdx.Gdx;
|
import com.badlogic.gdx.Gdx;
|
||||||
import com.badlogic.gdx.graphics.Pixmap;
|
import com.badlogic.gdx.graphics.Pixmap;
|
||||||
import com.badlogic.gdx.graphics.Texture;
|
import com.badlogic.gdx.graphics.Texture;
|
||||||
import com.badlogic.gdx.graphics.g2d.BitmapFont;
|
import com.badlogic.gdx.graphics.g2d.BitmapFont;
|
||||||
import com.badlogic.gdx.graphics.g2d.TextureRegion;
|
import com.badlogic.gdx.graphics.g2d.NinePatch;
|
||||||
import io.avaje.inject.Component;
|
import io.avaje.inject.Component;
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.dataformat.toml.TomlFactory;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -20,7 +24,9 @@ import java.util.Map;
|
|||||||
public class ResourceLoader {
|
public class ResourceLoader {
|
||||||
private final Map<String, Pixmap> pixmaps = new HashMap<>();
|
private final Map<String, Pixmap> pixmaps = new HashMap<>();
|
||||||
private final Map<String, Texture> textures = new HashMap<>();
|
private final Map<String, Texture> textures = new HashMap<>();
|
||||||
|
private final Map<String, NinePatch> ninePatches = new HashMap<>();
|
||||||
private final Map<String, BitmapFont> fonts = new HashMap<>();
|
private final Map<String, BitmapFont> fonts = new HashMap<>();
|
||||||
|
private final ObjectMapper mapper = new ObjectMapper(new TomlFactory());
|
||||||
|
|
||||||
public Pixmap loadPixmap(String path) {
|
public Pixmap loadPixmap(String path) {
|
||||||
return pixmaps.computeIfAbsent(path, p -> new Pixmap(Gdx.files.internal(p)));
|
return pixmaps.computeIfAbsent(path, p -> new Pixmap(Gdx.files.internal(p)));
|
||||||
@@ -30,6 +36,14 @@ public class ResourceLoader {
|
|||||||
return textures.computeIfAbsent(path, p -> new Texture(Gdx.files.internal(p)));
|
return textures.computeIfAbsent(path, p -> new Texture(Gdx.files.internal(p)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NinePatch loadNinePatch(String path) {
|
||||||
|
return ninePatches.computeIfAbsent(path, p -> {
|
||||||
|
var pixmap = loadPixmap(p);
|
||||||
|
var loader = new NinePatchLoader(pixmap);
|
||||||
|
return loader.getNinePatch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public BitmapFont loadFont(String path) {
|
public BitmapFont loadFont(String path) {
|
||||||
return fonts.computeIfAbsent(path, this::loadBitmapfont);
|
return fonts.computeIfAbsent(path, this::loadBitmapfont);
|
||||||
}
|
}
|
||||||
@@ -37,186 +51,9 @@ public class ResourceLoader {
|
|||||||
private BitmapFont loadBitmapfont(String path) {
|
private BitmapFont loadBitmapfont(String path) {
|
||||||
log.info("Loading bitmap font from {}", path);
|
log.info("Loading bitmap font from {}", path);
|
||||||
|
|
||||||
@Cleanup("dispose") var pixmap = loadPixmap(path + ".png");
|
var metadataString = Gdx.files.internal(path + ".toml").readString();
|
||||||
var texture = new Texture(pixmap);
|
var metadata = mapper.readValue(metadataString, FontMetadata.class);
|
||||||
var region = new TextureRegion(texture);
|
@Cleanup("dispose") var pixmap = loadPixmap(metadata.getGeneral().getSource());
|
||||||
|
return FontLoader.loadBitmapfont(pixmap, metadata);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ public class WindowService implements OnStartup {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStartup() {
|
public void onStartup() {
|
||||||
var texture = resourceLoader.loadTexture("system/menubar.png");
|
menubar = resourceLoader.loadNinePatch("system/menubar.png");
|
||||||
menubar = new NinePatch(texture, 9, 9, 15, 1);
|
|
||||||
|
|
||||||
systemMenubar = new Menubar()
|
systemMenubar = new Menubar()
|
||||||
.addItem("Dice")
|
.addItem("Dice")
|
||||||
@@ -25,6 +24,6 @@ public class WindowService implements OnStartup {
|
|||||||
|
|
||||||
void paint(GraphicsContext gc) {
|
void paint(GraphicsContext gc) {
|
||||||
// Render menubar
|
// Render menubar
|
||||||
menubar.draw(gc.getBatch(), 0, gc.getHeight() - 16, gc.getWidth(), 16);
|
gc.draw(menubar, 0, 0, gc.getWidth(), 13);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
215
src/main/java/be/seeseemelk/diceos/system/font/FontLoader.java
Normal file
215
src/main/java/be/seeseemelk/diceos/system/font/FontLoader.java
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
package be.seeseemelk.diceos.system.font;
|
||||||
|
|
||||||
|
import be.seeseemelk.diceos.system.utils.Map2D;
|
||||||
|
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 lombok.experimental.UtilityClass;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads bitmap fonts from a TOML file and a PNG file containing timing information.
|
||||||
|
* <p>
|
||||||
|
* This class should not be used directly. Instead, use the {@link be.seeseemelk.diceos.system.ResourceLoader}.
|
||||||
|
*/
|
||||||
|
@UtilityClass
|
||||||
|
@Slf4j
|
||||||
|
public class FontLoader {
|
||||||
|
/**
|
||||||
|
* Loads a bitmap font from a pixmap and metadata.
|
||||||
|
* @param pixmap The pixmap containing the font texture and timing information.
|
||||||
|
* @param metadata The metadata in TOML format.
|
||||||
|
* @return The loaded bitmap font.
|
||||||
|
*/
|
||||||
|
public static BitmapFont loadBitmapfont(Pixmap pixmap, FontMetadata metadata) {
|
||||||
|
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 = metadata.getGeneral().getOrder().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 + metadata.getPadding().getHorizontal();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add space character
|
||||||
|
var spaceGlyph = new BitmapFont.Glyph();
|
||||||
|
spaceGlyph.id = ' ';
|
||||||
|
spaceGlyph.width = 0;
|
||||||
|
spaceGlyph.height = 0;
|
||||||
|
spaceGlyph.xadvance = metadata.getPadding().getSpace();
|
||||||
|
data.setGlyph(spaceGlyph.id, spaceGlyph);
|
||||||
|
|
||||||
|
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 static 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 static 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package be.seeseemelk.diceos.system.font;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class FontMetadata {
|
||||||
|
private GeneralSection general;
|
||||||
|
private PaddingSection padding;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package be.seeseemelk.diceos.system.font;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class GeneralSection {
|
||||||
|
/**
|
||||||
|
* The image file containing the font glyphs.
|
||||||
|
*/
|
||||||
|
private String source;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The order of the glyphs in the image file.
|
||||||
|
*/
|
||||||
|
private String order;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package be.seeseemelk.diceos.system.font;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class PaddingSection {
|
||||||
|
/**
|
||||||
|
* The number of pixels to pad between characters.
|
||||||
|
*/
|
||||||
|
private int horizontal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The width of the space character.
|
||||||
|
*/
|
||||||
|
private int space;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package be.seeseemelk.diceos.system.gfx;
|
package be.seeseemelk.diceos.system.gfx;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.Texture;
|
||||||
import com.badlogic.gdx.graphics.g2d.Batch;
|
import com.badlogic.gdx.graphics.g2d.Batch;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.NinePatch;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@@ -13,4 +15,26 @@ public class GraphicsContext {
|
|||||||
private final Batch batch;
|
private final Batch batch;
|
||||||
private final int height;
|
private final int height;
|
||||||
private final int width;
|
private final int width;
|
||||||
|
|
||||||
|
public void draw(NinePatch image, int x, int y, int width, int height) {
|
||||||
|
image.draw(batch, x, getHeight() - height - y, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void draw(Texture texture, int x, int y) {
|
||||||
|
draw(texture, x, y, Parameters.DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void draw(Texture texture, int x, int y, Parameters params) {
|
||||||
|
batch.draw(texture,
|
||||||
|
x, getHeight() - y - texture.getHeight(), texture.getWidth(), texture.getHeight(),
|
||||||
|
0, 0, texture.getWidth(), texture.getHeight(),
|
||||||
|
params.flipX, params.flipY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Parameters {
|
||||||
|
public final static Parameters DEFAULT = new Parameters();
|
||||||
|
|
||||||
|
public boolean flipX = false;
|
||||||
|
public boolean flipY = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package be.seeseemelk.diceos.system.gfx;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.Pixmap;
|
||||||
|
import com.badlogic.gdx.graphics.Texture;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.NinePatch;
|
||||||
|
import com.badlogic.gdx.graphics.g2d.TextureRegion;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a 9-patch image from a pixmap.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public class NinePatchLoader {
|
||||||
|
private final Pixmap pixmap;
|
||||||
|
private int xStart = 0;
|
||||||
|
private int xEnd = 0;
|
||||||
|
private int yStart = 0;
|
||||||
|
private int yEnd = 0;
|
||||||
|
private NinePatch ninePatch;
|
||||||
|
|
||||||
|
public NinePatchLoader(Pixmap pixmap) {
|
||||||
|
this.pixmap = pixmap;
|
||||||
|
parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parse() {
|
||||||
|
// Top border
|
||||||
|
for (int x = 1; x < pixmap.getWidth() - 1; x++) {
|
||||||
|
int pixel = pixmap.getPixel(x, 0);
|
||||||
|
var isBlack = pixel == 0x000000ff;
|
||||||
|
if (isBlack) {
|
||||||
|
if (xStart == 0) {
|
||||||
|
xStart = x - 1;
|
||||||
|
}
|
||||||
|
xEnd = x - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left border
|
||||||
|
for (int y = 1; y < pixmap.getHeight() - 1; y++) {
|
||||||
|
int pixel = pixmap.getPixel(0, y);
|
||||||
|
var isBlack = pixel == 0x000000ff;
|
||||||
|
if (isBlack) {
|
||||||
|
if (yStart == 0) {
|
||||||
|
yStart = y - 1;
|
||||||
|
}
|
||||||
|
yEnd = y - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var texture = new Texture(pixmap);
|
||||||
|
var region = new TextureRegion(texture,
|
||||||
|
//1, 1,
|
||||||
|
1, 1,
|
||||||
|
pixmap.getWidth() - 1, pixmap.getHeight() - 1
|
||||||
|
);
|
||||||
|
ninePatch = new NinePatch(region,
|
||||||
|
xStart, pixmap.getWidth() - xEnd - 2,
|
||||||
|
yStart, pixmap.getHeight() - yEnd - 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
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
|
|
||||||
10
src/main/resources/system/font.toml
Normal file
10
src/main/resources/system/font.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[general]
|
||||||
|
source = "system/font.png"
|
||||||
|
order = """
|
||||||
|
aAbBcCdDeEfFgGhHiIjJkKlLmMnN
|
||||||
|
oOpPqQrRsStTuUvVwWxXyYzZ
|
||||||
|
"""
|
||||||
|
|
||||||
|
[padding]
|
||||||
|
horizontal = 1
|
||||||
|
space = 2
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
aAbBcCdDeEfFgGhHiIjJkKlLmMnN
|
|
||||||
oOpPqQrRsStTuUvVwWxXyYzZ
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 257 B |
Reference in New Issue
Block a user