Compare commits
5 Commits
8097ff725b
...
e317e21cfe
| Author | SHA1 | Date | |
|---|---|---|---|
| e317e21cfe | |||
| 94dc7fef0e | |||
| fff22349de | |||
| 782ccae6e2 | |||
| 796ffa5b2d |
291
src/app.zig
291
src/app.zig
@ -3,18 +3,21 @@ const log = std.log.scoped(.app);
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Math = @import("math");
|
||||
const Vec2 = Math.Vec2;
|
||||
|
||||
const STBImage = @import("stb_image");
|
||||
const STBTrueType = @import("stb_truetype");
|
||||
const Tiled = @import("tiled");
|
||||
|
||||
const Platform = @import("platform/root.zig");
|
||||
const Gfx = Platform.Gfx;
|
||||
const Vec2 = Platform.Vec2;
|
||||
const Audio = Platform.Audio;
|
||||
const ImGUI = Platform.ImGUI;
|
||||
const Rect = Platform.Rect;
|
||||
const Color = Platform.Color;
|
||||
|
||||
const CanvasScaler = @import("./canvas_scaler.zig");
|
||||
const Font = @import("./font.zig");
|
||||
const Tilesheet = @import("./tilesheet.zig");
|
||||
|
||||
const App = @This();
|
||||
|
||||
@ -41,21 +44,22 @@ const PlayerSprites = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const Tile = struct {
|
||||
ground_sprite: ?Gfx.Sprite.Id,
|
||||
|
||||
entity: Entity.Id,
|
||||
|
||||
const empty = Tile{
|
||||
.ground_sprite = null,
|
||||
.entity = .nil
|
||||
};
|
||||
};
|
||||
|
||||
const Tilemap = struct {
|
||||
tiles: []Tile,
|
||||
width: usize,
|
||||
height: usize,
|
||||
|
||||
const Tile = struct {
|
||||
ground_sprite: ?Gfx.Sprite.Id,
|
||||
wall_sprite: ?Gfx.Sprite.Id,
|
||||
|
||||
const empty = Tile{
|
||||
.ground_sprite = null,
|
||||
.wall_sprite = null
|
||||
};
|
||||
};
|
||||
|
||||
pub fn init(gpa: Allocator, width: usize, height: usize) !Tilemap {
|
||||
const tiles = try gpa.alloc(Tile, width * height);
|
||||
@memset(tiles, .empty);
|
||||
@ -81,8 +85,9 @@ const Tilemap = struct {
|
||||
const Entity = struct {
|
||||
type: Type,
|
||||
|
||||
pos_x: i32 = 0,
|
||||
pos_y: i32 = 0,
|
||||
pos_x: i32 = -1,
|
||||
pos_y: i32 = -1,
|
||||
visual_pos: Vec2 = .init(0, 0),
|
||||
sprite: ?Gfx.Sprite.Id = null,
|
||||
|
||||
player_direction: Direction = .down,
|
||||
@ -90,17 +95,83 @@ const Entity = struct {
|
||||
|
||||
const Type = enum {
|
||||
player,
|
||||
box
|
||||
box,
|
||||
wall
|
||||
};
|
||||
|
||||
const SlotMap = Platform.SlotMapType(u8, u16, Entity);
|
||||
const SlotMap = Platform.SlotMapType(u16, u8, Entity);
|
||||
const Id = SlotMap.Id;
|
||||
};
|
||||
|
||||
roboto_font: Gfx.Font.Id,
|
||||
tilemap: Tilemap,
|
||||
entities: Entity.SlotMap,
|
||||
const World = struct {
|
||||
tilemap: Tilemap,
|
||||
entities: Entity.SlotMap,
|
||||
|
||||
fn spawnPlayer(self: *World) !Entity.Id {
|
||||
return try self.entities.insert(Entity{
|
||||
.type = .player,
|
||||
});
|
||||
}
|
||||
|
||||
fn spawnBox(self: *World, sprite: Gfx.Sprite.Id) !Entity.Id {
|
||||
return try self.entities.insert(Entity{
|
||||
.type = .box,
|
||||
.sprite = sprite
|
||||
});
|
||||
}
|
||||
|
||||
fn spawnWall(self: *World, sprite: Gfx.Sprite.Id) !Entity.Id {
|
||||
return try self.entities.insert(Entity{
|
||||
.type = .wall,
|
||||
.sprite = sprite
|
||||
});
|
||||
}
|
||||
|
||||
fn getTile(self: *World, x: i32, y: i32) ?*Tile {
|
||||
const width: i32 = @intCast(self.tilemap.width);
|
||||
const height: i32 = @intCast(self.tilemap.height);
|
||||
if (0 <= x and x < width and 0 <= y and y < height) {
|
||||
return &self.tilemap.tiles[@intCast(x + y * width)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn moveEntity(self: *World, id: Entity.Id, x: i32, y: i32) bool {
|
||||
const e = self.entities.get(id) orelse return false;
|
||||
const destination_tile = self.getTile(x, y) orelse return false;
|
||||
|
||||
if (self.entities.exists(destination_tile.entity)) {
|
||||
// Cna't overwrite entity
|
||||
return false;
|
||||
}
|
||||
destination_tile.entity = id;
|
||||
|
||||
if (self.getTile(e.pos_x, e.pos_y)) |tile| {
|
||||
tile.entity = .nil;
|
||||
}
|
||||
|
||||
e.pos_x = x;
|
||||
e.pos_y = y;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn movePlayer(self: *World, id: Entity.Id, dir_x: i32, dir_y: i32) bool {
|
||||
const e = self.entities.get(id) orelse return false;
|
||||
|
||||
const next_x = e.pos_x+dir_x;
|
||||
const next_y = e.pos_y+dir_y;
|
||||
if (!self.moveEntity(id, next_x, next_y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
world: World,
|
||||
|
||||
roboto_font: Font,
|
||||
tilesheet: Tilesheet,
|
||||
|
||||
player_sprites: PlayerSprites,
|
||||
@ -111,58 +182,11 @@ footstep_sound: Audio.SoundId = .nil,
|
||||
|
||||
sfx_bus: Audio.BusId,
|
||||
|
||||
const Tilesheet = struct {
|
||||
image: Gfx.ImageData,
|
||||
sprites: []Gfx.Sprite.Id,
|
||||
tile_size: Vec2,
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
pub fn init(gpa: Allocator, image: Gfx.ImageData, tile_width: u32, tile_height: u32) !Tilesheet {
|
||||
const width = image.width / tile_width;
|
||||
const height = image.height / tile_height;
|
||||
const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height);
|
||||
errdefer gpa.free(sprites);
|
||||
@memset(sprites, .nil);
|
||||
|
||||
return Tilesheet{
|
||||
.sprites = sprites,
|
||||
.image = image,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.tile_size = .initFromInt(u32, tile_width, tile_height)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Tilesheet) void {
|
||||
for (self.sprites) |sprite| {
|
||||
Gfx.deinitSprite(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: Tilesheet, x: usize, y: usize) Gfx.Sprite.Id {
|
||||
if (x >= self.width or y >= self.height) {
|
||||
log.warn("Attempt to get tile which is out of bounds", .{});
|
||||
return .nil;
|
||||
}
|
||||
|
||||
const sprite_index = y * self.width + x;
|
||||
if (self.sprites[sprite_index] == Gfx.Sprite.Id.nil) {
|
||||
const sprite = Gfx.initSprite(.{ .padding = 1 });
|
||||
Gfx.setSprite(sprite, .{
|
||||
.image = self.image,
|
||||
.rect = Rect.init(@floatFromInt(x), @floatFromInt(y), 1, 1).multiply(self.tile_size)
|
||||
});
|
||||
self.sprites[sprite_index] = sprite;
|
||||
}
|
||||
|
||||
return self.sprites[sprite_index];
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
||||
const robot_font = Gfx.initFont();
|
||||
Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf"));
|
||||
const roboto_font = try Font.init(plt.gpa, .{
|
||||
.spritesheet = Gfx.initSpritesheet(.{ .format = .r8 }),
|
||||
.ttf_data = plt.assets.readFile("roboto-font/Roboto-Regular.ttf")
|
||||
});
|
||||
|
||||
const tilesheet_png = try STBImage.load(plt.assets.readFile("sokoban/sokoban_tilesheet.png"));
|
||||
const tilesheet = try Tilesheet.init(
|
||||
@ -172,7 +196,7 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
||||
.height = tilesheet_png.height,
|
||||
.pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels }
|
||||
},
|
||||
64, 64,
|
||||
.init(64, 64)
|
||||
);
|
||||
|
||||
var player_sprites: PlayerSprites = undefined;
|
||||
@ -206,17 +230,18 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
||||
|
||||
self.* = App{
|
||||
.player_sprites = player_sprites,
|
||||
.roboto_font = robot_font,
|
||||
.roboto_font = roboto_font,
|
||||
.animation_timer = 0,
|
||||
.tilesheet = tilesheet,
|
||||
.footstep_buffer = footstep_sound,
|
||||
.sfx_bus = Audio.addBus(.{ .label = "sfx" }),
|
||||
.tilemap = undefined,
|
||||
.entities = try .initCapacity(plt.gpa, 256)
|
||||
.world = .{
|
||||
.tilemap = undefined,
|
||||
.entities = try .initCapacity(plt.gpa, 256)
|
||||
}
|
||||
};
|
||||
|
||||
var player_pos_x: i32 = 0;
|
||||
var player_pos_y: i32 = 0;
|
||||
var player_pos: Vec2 = .zero;
|
||||
{
|
||||
var scratch = std.heap.ArenaAllocator.init(plt.gpa);
|
||||
defer scratch.deinit();
|
||||
@ -234,7 +259,7 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
||||
|
||||
const bounds = tilemap.getTileBounds();
|
||||
|
||||
self.tilemap = try Tilemap.init(plt.gpa, bounds.width(), bounds.height());
|
||||
self.world.tilemap = try Tilemap.init(plt.gpa, bounds.width(), bounds.height());
|
||||
|
||||
const ground = tilemap.getLayer("Ground").?;
|
||||
const walls = tilemap.getLayer("Walls").?;
|
||||
@ -243,7 +268,7 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
||||
const x = bounds.min_x + ox;
|
||||
const y = bounds.min_y + oy;
|
||||
|
||||
const tile = self.tilemap.get(ox, oy);
|
||||
const tile = self.world.tilemap.get(ox, oy);
|
||||
|
||||
if (tilemap.getTile(ground, tilesets, x, y)) |tiled_tile| {
|
||||
const pos = tiled_tile.getPosition();
|
||||
@ -252,17 +277,18 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
||||
|
||||
if (tilemap.getTile(walls, tilesets, x, y)) |tiled_tile| {
|
||||
const pos = tiled_tile.getPosition();
|
||||
tile.wall_sprite = tilesheet.get(pos.x, pos.y);
|
||||
const wall_id = try self.world.spawnWall(tilesheet.get(pos.x, pos.y));
|
||||
_ = self.world.moveEntity(wall_id, @intCast(ox), @intCast(oy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const markers = &tilemap.getLayer("Markers").?.variant.object;
|
||||
const spawn = markers.getByName("spawn").?;
|
||||
player_pos_x = @divFloor(@as(i32, @intFromFloat(spawn.shape.point.x)), @as(i32, @intCast(tilemap.tile_width)));
|
||||
player_pos_x -= @intCast(bounds.min_x);
|
||||
player_pos_y = @divFloor(@as(i32, @intFromFloat(spawn.shape.point.y)),@as(i32, @intCast(tilemap.tile_height)));
|
||||
player_pos_y -= @intCast(bounds.min_y);
|
||||
player_pos.x = @divFloor(spawn.shape.point.x, @as(f32, @floatFromInt(tilemap.tile_width)));
|
||||
player_pos.x -= @floatFromInt(bounds.min_x);
|
||||
player_pos.y = @divFloor(spawn.shape.point.y, @as(f32, @floatFromInt(tilemap.tile_height)));
|
||||
player_pos.y -= @floatFromInt(bounds.min_y);
|
||||
|
||||
const boxes = tilemap.getLayer("Boxes").?;
|
||||
for (0..bounds.height()) |oy| {
|
||||
@ -272,35 +298,19 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
||||
|
||||
if (tilemap.getTile(boxes, tilesets, x, y)) |tiled_tile| {
|
||||
const pos = tiled_tile.getPosition();
|
||||
_ = try self.spawnBox(@intCast(ox), @intCast(oy), tilesheet.get(pos.x, pos.y));
|
||||
const box_id = try self.world.spawnBox(tilesheet.get(pos.x, pos.y));
|
||||
_ = self.world.moveEntity(box_id, @intCast(ox), @intCast(oy));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const player_id = try self.spawnPlayer();
|
||||
const player = self.entities.getAssumeExists(player_id);
|
||||
player.pos_x = player_pos_x;
|
||||
player.pos_y = player_pos_y;
|
||||
const player_id = try self.world.spawnPlayer();
|
||||
_ = self.world.moveEntity(player_id, @intFromFloat(player_pos.x), @intFromFloat(player_pos.y));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn spawnPlayer(self: *App) !Entity.Id {
|
||||
return try self.entities.insert(Entity{
|
||||
.type = .player,
|
||||
});
|
||||
}
|
||||
|
||||
fn spawnBox(self: *App, x: i32, y: i32, sprite: Gfx.Sprite.Id) !Entity.Id {
|
||||
return try self.entities.insert(Entity{
|
||||
.type = .box,
|
||||
.pos_x = x,
|
||||
.pos_y = y,
|
||||
.sprite = sprite
|
||||
});
|
||||
}
|
||||
|
||||
pub fn frame(self: *App, plt: Platform.Frame) !void {
|
||||
const input = plt.input;
|
||||
|
||||
@ -322,44 +332,48 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
|
||||
dir_y = 0;
|
||||
}
|
||||
|
||||
Gfx.setClearColor(.rgb(40, 40, 40));
|
||||
const clear_color = Color.rgb(40, 40, 40);
|
||||
Gfx.setClearColor(clear_color);
|
||||
|
||||
const tile_size = Vec2.init(48, 48);
|
||||
for (0..self.tilemap.height) |y| {
|
||||
for (0..self.tilemap.width) |x| {
|
||||
const tile = self.tilemap.get(x, y);
|
||||
var canvas = CanvasScaler{
|
||||
.size = Vec2.initFromInt(usize, self.world.tilemap.width, self.world.tilemap.height).multiply(tile_size)
|
||||
};
|
||||
canvas.push();
|
||||
defer canvas.pop(clear_color);
|
||||
|
||||
for (0..self.world.tilemap.height) |y| {
|
||||
for (0..self.world.tilemap.width) |x| {
|
||||
const tile = self.world.tilemap.get(x, y);
|
||||
|
||||
if (tile.ground_sprite) |sprite| {
|
||||
Gfx.drawSprite(sprite, Vec2.initFromInt(usize, x, y).multiply(tile_size), tile_size, .white);
|
||||
}
|
||||
|
||||
if (tile.wall_sprite) |sprite| {
|
||||
Gfx.drawSprite(sprite, Vec2.initFromInt(usize, x, y).multiply(tile_size), tile_size, .white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var entity_iter = self.entities.iterator();
|
||||
var entity_iter = self.world.entities.iterator();
|
||||
while (entity_iter.next()) |entity_id| {
|
||||
const entity = self.entities.getAssumeExists(entity_id);
|
||||
const entity = self.world.entities.getAssumeExists(entity_id);
|
||||
if (entity.type == .player) {
|
||||
entity.pos_x += dir_x;
|
||||
entity.pos_y += dir_y;
|
||||
if ((dir_x != 0 or dir_y != 0) and self.world.movePlayer(entity_id, dir_x, dir_y)) {
|
||||
var new_direction = entity.player_direction;
|
||||
if (dir_x < 0) {
|
||||
new_direction = .left;
|
||||
} else if (dir_x > 0) {
|
||||
new_direction = .right;
|
||||
} else if (dir_y > 0) {
|
||||
new_direction = .down;
|
||||
} else if (dir_y < 0) {
|
||||
new_direction = .up;
|
||||
}
|
||||
|
||||
var new_direction = entity.player_direction;
|
||||
if (dir_x < 0) {
|
||||
new_direction = .left;
|
||||
} else if (dir_x > 0) {
|
||||
new_direction = .right;
|
||||
} else if (dir_y > 0) {
|
||||
new_direction = .down;
|
||||
} else if (dir_y < 0) {
|
||||
new_direction = .up;
|
||||
}
|
||||
if (entity.player_direction != new_direction) {
|
||||
entity.player_frame_index = 0;
|
||||
entity.player_direction = new_direction;
|
||||
}
|
||||
|
||||
if (entity.player_direction != new_direction) {
|
||||
entity.player_frame_index = 0;
|
||||
entity.player_direction = new_direction;
|
||||
self.footstep_sound = Audio.play(.{ .buffer = self.footstep_buffer, .bus = self.sfx_bus });
|
||||
}
|
||||
|
||||
const frames = self.player_sprites.getDirection(entity.player_direction);
|
||||
@ -376,27 +390,32 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
|
||||
entity.player_frame_index = 0;
|
||||
}
|
||||
|
||||
if (dir_x != 0 or dir_y != 0) {
|
||||
self.footstep_sound = Audio.play(.{ .buffer = self.footstep_buffer, .bus = self.sfx_bus });
|
||||
}
|
||||
|
||||
entity.sprite = frames[entity.player_frame_index];
|
||||
}
|
||||
|
||||
entity.visual_pos = .initFromInt(i32, entity.pos_x, entity.pos_y);
|
||||
|
||||
if (entity.sprite) |sprite| {
|
||||
Gfx.drawSprite(
|
||||
sprite,
|
||||
Vec2.initFromInt(i32, entity.pos_x, entity.pos_y).multiply(tile_size),
|
||||
entity.visual_pos.multiply(tile_size),
|
||||
tile_size,
|
||||
.white
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// self.roboto_font.drawText(.{
|
||||
// .text = try std.fmt.allocPrint(plt.frame, "Hello {:.0}!", .{plt.time()}),
|
||||
// .pos = .init(0, 0),
|
||||
// .height = 32,
|
||||
// });
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, plt: Platform.Deinit) void {
|
||||
self.tilemap.deinit(plt.gpa);
|
||||
self.entities.slots.deinit(plt.gpa);
|
||||
self.roboto_font.deinit(plt.gpa);
|
||||
Gfx.deinitSpritesheet(self.roboto_font.spritesheet);
|
||||
|
||||
self.world.tilemap.deinit(plt.gpa);
|
||||
self.world.entities.slots.deinit(plt.gpa);
|
||||
}
|
||||
|
||||
71
src/canvas_scaler.zig
Normal file
71
src/canvas_scaler.zig
Normal file
@ -0,0 +1,71 @@
|
||||
const Math = @import("math");
|
||||
const Vec2 = Math.Vec2;
|
||||
|
||||
const Platform = @import("platform/root.zig");
|
||||
const Gfx = Platform.Gfx;
|
||||
const Color = Platform.Color;
|
||||
|
||||
const Canvas = @This();
|
||||
|
||||
size: Vec2,
|
||||
|
||||
fn getScale(self: Canvas) f32 {
|
||||
const window_size = Gfx.getWindowSize();
|
||||
const canvas_size = self.size;
|
||||
|
||||
const scale_x = window_size.x / canvas_size.x;
|
||||
const scale_y = window_size.y / canvas_size.y;
|
||||
return @min(scale_x, scale_y);
|
||||
}
|
||||
|
||||
fn getScaledCanvas(self: Canvas) Vec2 {
|
||||
return self.size.multiplyScalar(self.getScale());
|
||||
}
|
||||
|
||||
pub fn push(self: Canvas) void {
|
||||
Gfx.transformPush();
|
||||
|
||||
const scale = self.getScale();
|
||||
const window_size = Gfx.getWindowSize();
|
||||
const scaled_canvas = self.size.multiplyScalar(scale);
|
||||
|
||||
Gfx.transformTranslate(.init(
|
||||
(window_size.x - scaled_canvas.x) / 2,
|
||||
(window_size.y - scaled_canvas.y) / 2,
|
||||
));
|
||||
|
||||
Gfx.transformScale(.init(scale, scale));
|
||||
}
|
||||
|
||||
pub fn pop(self: Canvas, color: Color) void {
|
||||
Gfx.transformPop();
|
||||
|
||||
const scale = self.getScale();
|
||||
const window_size = Gfx.getWindowSize();
|
||||
const scaled_canvas = self.size.multiplyScalar(scale);
|
||||
if (window_size.x > scaled_canvas.x) {
|
||||
const margin_x = (window_size.x - scaled_canvas.x) / 2;
|
||||
Gfx.drawRectangle(
|
||||
.init(0, 0),
|
||||
.init(margin_x, window_size.y),
|
||||
color
|
||||
);
|
||||
Gfx.drawRectangle(
|
||||
.init(window_size.x-margin_x, 0),
|
||||
.init(margin_x, window_size.y),
|
||||
color
|
||||
);
|
||||
} else {
|
||||
const margin_y = (window_size.y - scaled_canvas.y) / 2;
|
||||
Gfx.drawRectangle(
|
||||
.init(0, 0),
|
||||
.init(window_size.x, margin_y),
|
||||
color
|
||||
);
|
||||
Gfx.drawRectangle(
|
||||
.init(0, window_size.y-margin_y),
|
||||
.init(window_size.x, margin_y),
|
||||
color
|
||||
);
|
||||
}
|
||||
}
|
||||
844
src/font.zig
Normal file
844
src/font.zig
Normal file
@ -0,0 +1,844 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.font);
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Io = std.Io;
|
||||
|
||||
const Platform = @import("platform/root.zig");
|
||||
const Gfx = Platform.Gfx;
|
||||
const SlotMapType = Platform.SlotMapType;
|
||||
const Vec2 = Platform.Vec2;
|
||||
const Rect = Platform.Rect;
|
||||
const Color = Platform.Color;
|
||||
|
||||
const STBTrueType = @import("stb_truetype");
|
||||
|
||||
const Font = @This();
|
||||
|
||||
spritesheet: Gfx.Spritesheet.Id,
|
||||
stb: STBTrueType.Font,
|
||||
|
||||
cache: GlyphCache,
|
||||
|
||||
const Options = struct {
|
||||
spritesheet: Gfx.Spritesheet.Id,
|
||||
ttf_data: [:0]const u8,
|
||||
glyph_slots: u32 = 256
|
||||
};
|
||||
|
||||
pub fn init(gpa: Allocator, opts: Options) !Font {
|
||||
const offset = STBTrueType.Font.getFontOffsetForIndex(opts.ttf_data, 0)
|
||||
orelse return error.MissingFont0;
|
||||
|
||||
const stb_font = try STBTrueType.Font.init(opts.ttf_data, offset);
|
||||
|
||||
var cache = try GlyphCache.init(gpa, opts.glyph_slots);
|
||||
errdefer cache.deinit(gpa);
|
||||
|
||||
return Font{
|
||||
.spritesheet = opts.spritesheet,
|
||||
.stb = stb_font,
|
||||
.cache = cache,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Font, gpa: Allocator) void {
|
||||
self.cache.deinit(gpa);
|
||||
}
|
||||
|
||||
pub fn getGlyphIndex(font: *Font, codepoint: u21) ?Glyph.Index {
|
||||
if (font.stb.findGlyphIndex(codepoint)) |index| {
|
||||
return index;
|
||||
} else {
|
||||
// Fallback
|
||||
return font.getGlyphIndex('?');
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getGlyph(font: *Font, index: Glyph.Index, scale_x: f32, scale_y: f32) ?*Font.Glyph {
|
||||
const glyph_key = Font.Glyph.Key{
|
||||
.index = index,
|
||||
.scale_x = .fromF32(scale_x),
|
||||
.scale_y = .fromF32(scale_y)
|
||||
};
|
||||
|
||||
var iog_result = font.cache.insertOrGet(glyph_key);
|
||||
if (iog_result == null and font.cache.lru_tail != GlyphSlot.nil_id) {
|
||||
const id = font.cache.lru_tail;
|
||||
if (font.cache.get(id).last_used_frame != Gfx.getFrameIndex()) {
|
||||
font.cache.remove(id);
|
||||
iog_result = font.cache.insertOrGet(glyph_key);
|
||||
log.debug("Evicted least recently used glyph", .{});
|
||||
}
|
||||
}
|
||||
|
||||
if (iog_result) |result| {
|
||||
const glyph_id = result.id;
|
||||
const glyph = font.cache.get(glyph_id);
|
||||
|
||||
if (!result.found_existing) {
|
||||
glyph.box = font.stb.getGlyphBitmapBox(
|
||||
glyph_key.index,
|
||||
glyph_key.scale_x.toF32(),
|
||||
glyph_key.scale_y.toF32()
|
||||
);
|
||||
|
||||
const box_width: u32 = @intCast(glyph.box.x1 - glyph.box.x0);
|
||||
const box_height: u32 = @intCast(glyph.box.y1 - glyph.box.y0);
|
||||
if (box_width > 0 and box_height > 0) {
|
||||
const sprite = Gfx.initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 });
|
||||
|
||||
assert(box_width < Font.Glyph.max_width);
|
||||
assert(box_height < Font.Glyph.max_height);
|
||||
|
||||
var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined;
|
||||
const bitmap = Gfx.ImageData{
|
||||
.pixels = .{ .r8 = (&bitmap_buffer).ptr },
|
||||
.width = box_width,
|
||||
.height = box_height
|
||||
};
|
||||
font.stb.makeGlyphBitmap(
|
||||
bitmap.pixels.?.r8,
|
||||
box_width, box_height,
|
||||
box_width,
|
||||
glyph_key.scale_x.toF32(),
|
||||
glyph_key.scale_y.toF32(),
|
||||
glyph_key.index
|
||||
);
|
||||
Gfx.setSprite(sprite, .{ .image = bitmap });
|
||||
|
||||
glyph.sprite = sprite;
|
||||
}
|
||||
}
|
||||
|
||||
glyph.last_used_frame = Gfx.getFrameIndex();
|
||||
return glyph;
|
||||
} else {
|
||||
log.warn("Failed to render glyph, because cache is full", .{});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getAscent(font: *Font, height: f32) f32 {
|
||||
const vmetrics = font.stb.getFontVMetrics();
|
||||
const scale = font.stb.scaleForPixelHeight(height);
|
||||
return @as(f32, @floatFromInt(vmetrics.ascent)) * scale;
|
||||
}
|
||||
|
||||
pub fn measureText(font: *Font, height: f32, text: []const u8) Rect {
|
||||
var bounds: Rect = .init(0, 0, 0, 0);
|
||||
var first_glyph = false;
|
||||
|
||||
var layout = TextRunLayout.init(font, height);
|
||||
var iter = std.unicode.Utf8Iterator{
|
||||
.bytes = text,
|
||||
.i = 0
|
||||
};
|
||||
while (iter.nextCodepoint()) |codepoint| {
|
||||
const glyph_index = font.getGlyphIndex(codepoint) orelse continue;
|
||||
const glyph_layout = layout.next(glyph_index) orelse continue;
|
||||
|
||||
if (first_glyph) {
|
||||
bounds = glyph_layout.rect;
|
||||
first_glyph = false;
|
||||
} else {
|
||||
bounds = bounds.expand(glyph_layout.rect);
|
||||
}
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
pub const DrawTextOptions = struct {
|
||||
text: []const u8,
|
||||
pos: Vec2,
|
||||
height: f32,
|
||||
color: Gfx.Color = .white,
|
||||
alignment: Vec2 = .init(0, 0)
|
||||
};
|
||||
|
||||
pub fn drawText(font: *Font, opts: DrawTextOptions) void {
|
||||
var layout = Font.TextRunLayout.init(font, opts.height);
|
||||
|
||||
var pos = opts.pos;
|
||||
pos.y += font.getAscent(opts.height);
|
||||
|
||||
if (!opts.alignment.eql(.zero)) {
|
||||
const bounds = font.measureText(opts.height, opts.text);
|
||||
pos = pos.sub(bounds.size.multiply(opts.alignment));
|
||||
}
|
||||
|
||||
var iter = std.unicode.Utf8Iterator{
|
||||
.bytes = opts.text,
|
||||
.i = 0
|
||||
};
|
||||
while (iter.nextCodepoint()) |codepoint| {
|
||||
const glyph_index = font.getGlyphIndex(codepoint) orelse continue;
|
||||
const glyph_layout = layout.next(glyph_index) orelse continue;
|
||||
if (glyph_layout.sprite == Gfx.Sprite.Id.nil) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var glyph_pos = glyph_layout.rect.pos.add(pos);
|
||||
|
||||
// Round to nearest pixel to avoid artifacts
|
||||
glyph_pos.x = @round(glyph_pos.x);
|
||||
glyph_pos.y = @round(glyph_pos.y);
|
||||
|
||||
Gfx.drawSprite(
|
||||
glyph_layout.sprite,
|
||||
glyph_pos,
|
||||
glyph_layout.rect.size,
|
||||
opts.color
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const GlyphCache = struct {
|
||||
glyphs: []GlyphSlot,
|
||||
glyphs_hashmap: []GlyphSlot.Id,
|
||||
glyphs_used: std.bit_set.DynamicBitSetUnmanaged,
|
||||
lru_head: GlyphSlot.Id,
|
||||
lru_tail: GlyphSlot.Id,
|
||||
free_head: GlyphSlot.Id,
|
||||
|
||||
fn init(gpa: Allocator, capacity: u32) !GlyphCache {
|
||||
assert(capacity > 0);
|
||||
|
||||
const glyphs = try gpa.alloc(GlyphSlot, capacity);
|
||||
errdefer gpa.free(glyphs);
|
||||
|
||||
const glyphs_hashmap = try gpa.alloc(GlyphSlot.Id, 2*capacity);
|
||||
errdefer gpa.free(glyphs);
|
||||
|
||||
var glyphs_used = try std.bit_set.DynamicBitSetUnmanaged.initEmpty(gpa, capacity);
|
||||
errdefer glyphs_used.deinit(gpa);
|
||||
|
||||
var self: GlyphCache = .{
|
||||
.glyphs = glyphs,
|
||||
.glyphs_hashmap = glyphs_hashmap,
|
||||
.glyphs_used = glyphs_used,
|
||||
.lru_head = GlyphSlot.nil_id,
|
||||
.lru_tail = GlyphSlot.nil_id,
|
||||
.free_head = undefined
|
||||
};
|
||||
@memset(self.glyphs_hashmap, GlyphSlot.nil_id);
|
||||
@memset(self.glyphs, .{
|
||||
.glyph = undefined,
|
||||
.lru_prev = GlyphSlot.nil_id,
|
||||
.lru_next = GlyphSlot.nil_id,
|
||||
.next_free = GlyphSlot.nil_id,
|
||||
});
|
||||
|
||||
assert(self.glyphs_hashmap.len > self.glyphs.len);
|
||||
|
||||
self.free_head = 1;
|
||||
for (1.., self.glyphs[1..(self.glyphs.len-1)]) |i, *glyph| {
|
||||
glyph.next_free = @intCast(i + 1);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
fn deinit(self: *GlyphCache, gpa: Allocator) void {
|
||||
for (self.glyphs_hashmap) |glyph_id| {
|
||||
if (glyph_id == GlyphSlot.nil_id) {
|
||||
continue;
|
||||
}
|
||||
const slot = self.glyphs[glyph_id];
|
||||
slot.glyph.deinit();
|
||||
}
|
||||
|
||||
gpa.free(self.glyphs);
|
||||
gpa.free(self.glyphs_hashmap);
|
||||
self.glyphs_used.deinit(gpa);
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
fn get(self: *GlyphCache, id: GlyphSlot.Id) *Glyph {
|
||||
return &self.glyphs[id].glyph;
|
||||
}
|
||||
|
||||
fn appendToLRUHead(self: *GlyphCache, id: GlyphSlot.Id) void {
|
||||
if (self.lru_head != GlyphSlot.nil_id) {
|
||||
self.glyphs[self.lru_head].lru_prev = id;
|
||||
self.glyphs[id].lru_next = self.lru_head;
|
||||
} else {
|
||||
assert(self.lru_tail == GlyphSlot.nil_id);
|
||||
self.lru_tail = id;
|
||||
}
|
||||
|
||||
self.glyphs[id].lru_prev = GlyphSlot.nil_id;
|
||||
self.lru_head = id;
|
||||
}
|
||||
|
||||
fn removeFromLRU(self: *GlyphCache, id: GlyphSlot.Id) void {
|
||||
assert(id != GlyphSlot.nil_id);
|
||||
|
||||
const lru_prev = self.glyphs[id].lru_prev;
|
||||
const lru_next = self.glyphs[id].lru_next;
|
||||
|
||||
if (lru_prev != GlyphSlot.nil_id) {
|
||||
self.glyphs[lru_prev].lru_next = lru_next;
|
||||
}
|
||||
|
||||
if (lru_next != GlyphSlot.nil_id) {
|
||||
self.glyphs[lru_next].lru_prev = lru_prev;
|
||||
}
|
||||
|
||||
if (id == self.lru_head) {
|
||||
self.lru_head = lru_next;
|
||||
}
|
||||
if (id == self.lru_tail) {
|
||||
self.lru_tail = lru_prev;
|
||||
}
|
||||
}
|
||||
|
||||
fn popFreeId(self: *GlyphCache) ?GlyphSlot.Id {
|
||||
if (self.free_head != GlyphSlot.nil_id) {
|
||||
const result = self.free_head;
|
||||
self.free_head = self.glyphs[result].next_free;
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn pushFreeId(self: *GlyphCache, id: GlyphSlot.Id) void {
|
||||
assert(id != GlyphSlot.nil_id);
|
||||
self.glyphs[id].next_free = self.free_head;
|
||||
self.free_head = id;
|
||||
}
|
||||
|
||||
pub const InsertOrGetResult = struct {
|
||||
id: GlyphSlot.Id,
|
||||
found_existing: bool,
|
||||
};
|
||||
|
||||
fn insertOrGet(self: *GlyphCache, key: Glyph.Key) ?InsertOrGetResult {
|
||||
for (0..self.glyphs_hashmap.len) |offset| {
|
||||
const i = (key.toInt() + offset) % self.glyphs_hashmap.len;
|
||||
const hashmap_id = self.glyphs_hashmap[i];
|
||||
if (hashmap_id == GlyphSlot.nil_id) {
|
||||
const id = self.popFreeId() orelse return null;
|
||||
self.glyphs_hashmap[i] = id;
|
||||
|
||||
self.glyphs[id].glyph = .init(key);
|
||||
self.glyphs[id].lru_next = GlyphSlot.nil_id;
|
||||
self.glyphs[id].lru_prev = GlyphSlot.nil_id;
|
||||
self.appendToLRUHead(id);
|
||||
return InsertOrGetResult{
|
||||
.id = id,
|
||||
.found_existing = false
|
||||
};
|
||||
} else {
|
||||
const glyph = &self.glyphs[hashmap_id].glyph;
|
||||
if (glyph.key == key) {
|
||||
// An entry for this key already exists
|
||||
self.removeFromLRU(hashmap_id);
|
||||
self.appendToLRUHead(hashmap_id);
|
||||
return InsertOrGetResult{
|
||||
.id = hashmap_id,
|
||||
.found_existing = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn removeFromHashmap(self: *GlyphCache, id: GlyphSlot.Id) void {
|
||||
var bucket = self.glyphs[id].glyph.key.toInt() % self.glyphs_hashmap.len;
|
||||
for (0..self.glyphs_hashmap.len) |_| {
|
||||
assert(self.glyphs_hashmap[bucket] != GlyphSlot.nil_id);
|
||||
if (self.glyphs_hashmap[bucket] == id) {
|
||||
break;
|
||||
}
|
||||
|
||||
bucket = (bucket + 1) % self.glyphs_hashmap.len;
|
||||
}
|
||||
|
||||
assert(self.glyphs_hashmap[bucket] == id);
|
||||
|
||||
var empty = bucket;
|
||||
var current = (bucket + 1) % self.glyphs_hashmap.len;
|
||||
|
||||
while (self.glyphs_hashmap[current] != GlyphSlot.nil_id) {
|
||||
const current_glyph_id = self.glyphs_hashmap[current];
|
||||
const home = self.glyphs[current_glyph_id].glyph.key.toInt() % self.glyphs_hashmap.len;
|
||||
|
||||
if (self.hashmapDistance(home, current) > self.hashmapDistance(home, empty)) {
|
||||
self.glyphs_hashmap[empty] = self.glyphs_hashmap[current];
|
||||
empty = current;
|
||||
}
|
||||
|
||||
current = (current + 1) % self.glyphs_hashmap.len;
|
||||
}
|
||||
|
||||
assert(self.glyphs_hashmap[empty] != GlyphSlot.nil_id);
|
||||
self.glyphs_hashmap[empty] = GlyphSlot.nil_id;
|
||||
}
|
||||
|
||||
fn hashmapDistance(self: GlyphCache, from: usize, to: usize) usize {
|
||||
if (to >= from) {
|
||||
return to - from;
|
||||
} else {
|
||||
return self.glyphs_hashmap.len - from + to;
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(self: *GlyphCache, id: GlyphSlot.Id) void {
|
||||
assert(id != GlyphSlot.nil_id);
|
||||
self.pushFreeId(id);
|
||||
self.removeFromHashmap(id);
|
||||
self.removeFromLRU(id);
|
||||
|
||||
const glyph = self.glyphs[id].glyph;
|
||||
glyph.deinit();
|
||||
}
|
||||
|
||||
fn getLRUItems(self: *GlyphCache, gpa: Allocator) ![]GlyphSlot.Id {
|
||||
var lru: std.ArrayList(GlyphSlot.Id) = .empty;
|
||||
defer lru.deinit(gpa);
|
||||
|
||||
var lru_iter = self.lru_head;
|
||||
while (lru_iter != GlyphSlot.nil_id) {
|
||||
try lru.append(gpa, lru_iter);
|
||||
lru_iter = self.glyphs[lru_iter].lru_next;
|
||||
}
|
||||
|
||||
return try lru.toOwnedSlice(gpa);
|
||||
}
|
||||
|
||||
test {
|
||||
const gpa = std.testing.allocator;
|
||||
var cache = try GlyphCache.init(gpa, 256);
|
||||
defer cache.deinit(gpa);
|
||||
|
||||
const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
|
||||
const id1 = cache.insert(key).?;
|
||||
const id2 = cache.insert(key).?;
|
||||
try std.testing.expectEqual(id1, id2);
|
||||
}
|
||||
|
||||
test {
|
||||
const gpa = std.testing.allocator;
|
||||
var cache = try GlyphCache.init(gpa, 256);
|
||||
defer cache.deinit(gpa);
|
||||
|
||||
const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
|
||||
const id1 = cache.insert(key).?;
|
||||
cache.remove(id1);
|
||||
const id2 = cache.insert(key).?;
|
||||
try std.testing.expectEqual(id1, id2);
|
||||
}
|
||||
|
||||
test {
|
||||
const gpa = std.testing.allocator;
|
||||
var cache = try GlyphCache.init(gpa, 256);
|
||||
defer cache.deinit(gpa);
|
||||
|
||||
const id1 = cache.insert(Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
|
||||
const id2 = cache.insert(Glyph.Key{ .index = 11, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
|
||||
try std.testing.expect(id1 != id2);
|
||||
}
|
||||
|
||||
test {
|
||||
const gpa = std.testing.allocator;
|
||||
const limit = 256;
|
||||
var cache = try GlyphCache.init(gpa, limit);
|
||||
defer cache.deinit(gpa);
|
||||
|
||||
for (0..limit-1) |i| {
|
||||
const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) });
|
||||
try std.testing.expect(id != null);
|
||||
}
|
||||
const id = cache.insert(Glyph.Key{ .index = limit, .scale_x = .fromF32(1), .scale_y = .fromF32(1) });
|
||||
try std.testing.expect(id == null);
|
||||
}
|
||||
|
||||
test {
|
||||
const gpa = std.testing.allocator;
|
||||
const limit = 256;
|
||||
var cache = try GlyphCache.init(gpa, limit);
|
||||
defer cache.deinit(gpa);
|
||||
|
||||
for (0..10) |_| {
|
||||
var ids: std.ArrayList(GlyphSlot.Id) = .empty;
|
||||
defer ids.deinit(gpa);
|
||||
|
||||
for (0..(limit-1)) |i| {
|
||||
const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) });
|
||||
try std.testing.expect(id != null);
|
||||
try ids.append(gpa, id.?);
|
||||
}
|
||||
assert(cache.free_head == GlyphSlot.nil_id);
|
||||
|
||||
const lru = try cache.getLRUItems(gpa);
|
||||
defer gpa.free(lru);
|
||||
|
||||
const expected_lru = try gpa.dupe(GlyphSlot.Id, ids.items);
|
||||
defer gpa.free(expected_lru);
|
||||
std.mem.reverse(GlyphSlot.Id, expected_lru);
|
||||
|
||||
try std.testing.expectEqualSlices(GlyphSlot.Id, expected_lru, lru);
|
||||
|
||||
for (ids.items) |id| {
|
||||
cache.remove(id);
|
||||
}
|
||||
assert(cache.lru_head == GlyphSlot.nil_id);
|
||||
assert(cache.lru_tail == GlyphSlot.nil_id);
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
const gpa = std.testing.allocator;
|
||||
var cache = try GlyphCache.init(gpa, 256);
|
||||
defer cache.deinit(gpa);
|
||||
|
||||
const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
|
||||
const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
|
||||
const id3 = cache.insert(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
|
||||
|
||||
const lru = try cache.getLRUItems(gpa);
|
||||
defer gpa.free(lru);
|
||||
|
||||
try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru);
|
||||
}
|
||||
|
||||
test {
|
||||
const gpa = std.testing.allocator;
|
||||
var cache = try GlyphCache.init(gpa, 256);
|
||||
defer cache.deinit(gpa);
|
||||
|
||||
const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
|
||||
const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
|
||||
const id3 = cache.insert(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
|
||||
|
||||
const lru = try cache.getLRUItems(gpa);
|
||||
defer gpa.free(lru);
|
||||
|
||||
try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru);
|
||||
}
|
||||
|
||||
test {
|
||||
const gpa = std.testing.allocator;
|
||||
var cache = try GlyphCache.init(gpa, 256);
|
||||
defer cache.deinit(gpa);
|
||||
|
||||
const id = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
|
||||
try std.testing.expect(cache.lru_head != GlyphSlot.nil_id);
|
||||
try std.testing.expect(cache.lru_tail != GlyphSlot.nil_id);
|
||||
cache.remove(id);
|
||||
try std.testing.expect(cache.lru_head == GlyphSlot.nil_id);
|
||||
try std.testing.expect(cache.lru_tail == GlyphSlot.nil_id);
|
||||
}
|
||||
|
||||
test {
|
||||
const gpa = std.testing.allocator;
|
||||
var cache = try GlyphCache.init(gpa, 256);
|
||||
defer cache.deinit(gpa);
|
||||
|
||||
const key1 = Glyph.Key{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
|
||||
const id1 = cache.insert(key1).?;
|
||||
const key2 = Glyph.Key{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
|
||||
const id2 = cache.insert(key2).?;
|
||||
const key3 = Glyph.Key{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
|
||||
const id3 = cache.insert(key3).?;
|
||||
|
||||
_ = cache.lookup(key2);
|
||||
|
||||
{
|
||||
const lru = try cache.getLRUItems(gpa);
|
||||
defer gpa.free(lru);
|
||||
try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id2, id3, id1 }, lru);
|
||||
}
|
||||
|
||||
_ = cache.lookup(key1);
|
||||
|
||||
{
|
||||
const lru = try cache.getLRUItems(gpa);
|
||||
defer gpa.free(lru);
|
||||
try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id1, id2, id3 }, lru);
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
const gpa = std.testing.allocator;
|
||||
var cache = try GlyphCache.init(gpa, 256);
|
||||
defer cache.deinit(gpa);
|
||||
|
||||
const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
|
||||
const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
|
||||
const id3 = cache.insert(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
|
||||
|
||||
{
|
||||
const lru = try cache.getLRUItems(gpa);
|
||||
defer gpa.free(lru);
|
||||
try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru);
|
||||
}
|
||||
|
||||
_ = cache.remove(id2);
|
||||
|
||||
{
|
||||
const lru = try cache.getLRUItems(gpa);
|
||||
defer gpa.free(lru);
|
||||
try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id1 }, lru);
|
||||
}
|
||||
|
||||
_ = cache.remove(id3);
|
||||
|
||||
{
|
||||
const lru = try cache.getLRUItems(gpa);
|
||||
defer gpa.free(lru);
|
||||
try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id1 }, lru);
|
||||
}
|
||||
|
||||
_ = cache.remove(id1);
|
||||
|
||||
{
|
||||
const lru = try cache.getLRUItems(gpa);
|
||||
defer gpa.free(lru);
|
||||
try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ }, lru);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const GlyphSlot = struct {
|
||||
glyph: Glyph,
|
||||
|
||||
lru_next: GlyphSlot.Id,
|
||||
lru_prev: GlyphSlot.Id,
|
||||
|
||||
next_free: GlyphSlot.Id,
|
||||
|
||||
const Id = u16;
|
||||
const nil_id: GlyphSlot.Id = 0;
|
||||
|
||||
comptime {
|
||||
assert(@bitSizeOf(Id) % 8 == 0);
|
||||
}
|
||||
};
|
||||
|
||||
const Glyph = struct {
|
||||
key: Key,
|
||||
|
||||
box: STBTrueType.Box,
|
||||
sprite: Gfx.Sprite.Id,
|
||||
last_used_frame: u32,
|
||||
|
||||
const max_width: u32 = 512;
|
||||
const max_height: u32 = 512;
|
||||
|
||||
const Index = u32;
|
||||
|
||||
const Key = packed struct(u64) {
|
||||
index: Index,
|
||||
scale_x: q4d12,
|
||||
scale_y: q4d12,
|
||||
|
||||
pub fn toInt(self: Key) u64 {
|
||||
return @bitCast(self);
|
||||
}
|
||||
|
||||
// 4 integer bits, 12 fractional bits
|
||||
const q4d12 = enum(u16) {
|
||||
_,
|
||||
|
||||
pub fn toF32(integer: q4d12) f32 {
|
||||
return @as(f32, @floatFromInt(@intFromEnum(integer))) / (1 << 12);
|
||||
}
|
||||
|
||||
pub fn fromF32(float: f32) q4d12 {
|
||||
return @enumFromInt(@as(u16, @intFromFloat(float * (1 << 12))));
|
||||
}
|
||||
|
||||
pub fn format(self: q4d12, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
try writer.print("q12d4{{ {} }}", .{self.toF32()});
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
try std.testing.expectEqual(q4d12.fromF32(10.0625).toF32(), 10.0625);
|
||||
}
|
||||
};
|
||||
|
||||
fn init(key: Key) Glyph {
|
||||
return Glyph{
|
||||
.key = key,
|
||||
.box = .{
|
||||
.x0 = 0,
|
||||
.y0 = 0,
|
||||
.x1 = 0,
|
||||
.y1 = 0,
|
||||
},
|
||||
.sprite = .nil,
|
||||
.last_used_frame = 0
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: Glyph) void {
|
||||
Gfx.deinitSprite(self.sprite);
|
||||
}
|
||||
};
|
||||
|
||||
pub const TextRunLayout = struct {
|
||||
font: *Font,
|
||||
scale_x: f32,
|
||||
scale_y: f32,
|
||||
|
||||
prev_glyph: ?Font.Glyph.Index,
|
||||
pen_pos: Vec2,
|
||||
|
||||
const GlyphRect = struct {
|
||||
rect: Rect,
|
||||
sprite: Gfx.Sprite.Id,
|
||||
};
|
||||
|
||||
pub fn init(font: *Font, height: f32) TextRunLayout {
|
||||
const scale = font.stb.scaleForPixelHeight(height);
|
||||
|
||||
return TextRunLayout{
|
||||
.font = font,
|
||||
.scale_x = scale,
|
||||
.scale_y = scale,
|
||||
.prev_glyph = null,
|
||||
.pen_pos = .init(0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn next(self: *TextRunLayout, glyph_index: Font.Glyph.Index) ?GlyphRect {
|
||||
const glyph = self.font.getGlyph(glyph_index, self.scale_x, self.scale_y) orelse return null;
|
||||
|
||||
if (self.prev_glyph) |prev_glyph| {
|
||||
const kerning = self.font.stb.getGlyphKernAdvance(prev_glyph, glyph_index);
|
||||
self.pen_pos.x += @as(f32, @floatFromInt(kerning)) * self.scale_x;
|
||||
}
|
||||
|
||||
const hmetrics = self.font.stb.getGlyphHMetrics(glyph_index);
|
||||
const glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0);
|
||||
|
||||
var draw_pos = self.pen_pos;
|
||||
draw_pos.x -= @floatFromInt(glyph.box.x0);
|
||||
draw_pos.x += @as(f32, @floatFromInt(hmetrics.left_side_bearing)) * self.scale_x;
|
||||
draw_pos.y += @floatFromInt(glyph.box.y0);
|
||||
const draw_rect = Rect{
|
||||
.pos = draw_pos,
|
||||
.size = glyph_size
|
||||
};
|
||||
|
||||
self.pen_pos.x += @as(f32, @floatFromInt(hmetrics.advance_width)) * self.scale_x;
|
||||
self.prev_glyph = glyph_index;
|
||||
|
||||
return GlyphRect{
|
||||
.rect = draw_rect,
|
||||
.sprite = glyph.sprite
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const TextBuilder = struct {
|
||||
gpa: std.mem.Allocator,
|
||||
glyphs: std.ArrayList(TextBuilder.Glyph),
|
||||
|
||||
color: Color,
|
||||
baseline: f32,
|
||||
layout: ?Font.TextRunLayout,
|
||||
|
||||
bounds: Rect,
|
||||
|
||||
const Glyph = struct {
|
||||
sprite: Gfx.Sprite.Id,
|
||||
rect: Rect,
|
||||
color: Color
|
||||
};
|
||||
|
||||
pub fn init(gpa: std.mem.Allocator) TextBuilder {
|
||||
return TextBuilder{
|
||||
.gpa = gpa,
|
||||
.glyphs = .empty,
|
||||
.color = .white,
|
||||
.baseline = 0,
|
||||
.layout = null,
|
||||
.bounds = .init(0, 0, 0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn font(self: *TextBuilder, new_font: *Font, height: f32) void {
|
||||
var new_layout = Font.TextRunLayout.init(new_font, height);
|
||||
if (self.layout) |layout| {
|
||||
if (new_layout.font == layout.font and
|
||||
new_layout.scale_x == layout.scale_x and
|
||||
new_layout.scale_y == layout.scale_y
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const font_baseline = new_font.getAscent(height);
|
||||
if (font_baseline > self.baseline) {
|
||||
// Fixup previously drawn glyph to match the new larger baseline
|
||||
const offset_y = font_baseline - self.baseline;
|
||||
for (self.glyphs.items) |*glyph| {
|
||||
glyph.rect.pos.y += offset_y;
|
||||
}
|
||||
self.bounds.pos.y += offset_y;
|
||||
self.baseline = font_baseline;
|
||||
}
|
||||
|
||||
if (self.layout) |layout| {
|
||||
new_layout.pen_pos = layout.pen_pos;
|
||||
if (layout.font == new_layout.font) {
|
||||
new_layout.prev_glyph = layout.prev_glyph;
|
||||
}
|
||||
}
|
||||
|
||||
self.layout = new_layout;
|
||||
}
|
||||
|
||||
pub fn putCodepoint(self: *TextBuilder, codepoint: u21) !void {
|
||||
if (self.layout == null) {
|
||||
return;
|
||||
}
|
||||
const layout = &self.layout.?;
|
||||
|
||||
const glyph_index = layout.font.getGlyphIndex(codepoint) orelse return;
|
||||
const glyph_layout = layout.next(glyph_index) orelse return;
|
||||
|
||||
var rect = glyph_layout.rect;
|
||||
rect.pos.y += self.baseline;
|
||||
|
||||
try self.glyphs.append(self.gpa, TextBuilder.Glyph{
|
||||
.sprite = glyph_layout.sprite,
|
||||
.rect = rect,
|
||||
.color = self.color
|
||||
});
|
||||
|
||||
if (self.bounds.size.x > 0 and self.bounds.size.y > 0) {
|
||||
self.bounds = self.bounds.expand(rect);
|
||||
} else {
|
||||
self.bounds = rect;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn put(self: *TextBuilder, text: []const u8) !void {
|
||||
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
|
||||
while (iter.nextCodepoint()) |codepoint| {
|
||||
try self.putCodepoint(codepoint);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(self: *TextBuilder, pos: Vec2) void {
|
||||
for (self.glyphs.items) |glyph| {
|
||||
Gfx.drawSprite(glyph.sprite, glyph.rect.pos.add(pos), glyph.rect.size, glyph.color);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn offset(self: *TextBuilder, vec2: Vec2) void {
|
||||
if (self.layout) |*layout| {
|
||||
layout.pen_pos = layout.pen_pos.add(vec2);
|
||||
}
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@ -10,12 +10,11 @@ out vec4 color;
|
||||
out vec4 uv;
|
||||
|
||||
layout(binding = 0) uniform vs_params {
|
||||
mat4 view;
|
||||
mat4 projection;
|
||||
mat4 mvp;
|
||||
};
|
||||
|
||||
void main() {
|
||||
gl_Position = projection * view * vec4(position, 0, 1);
|
||||
gl_Position = mvp * vec4(position, 0, 1);
|
||||
color = color0;
|
||||
uv = vec4(texcoord0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
@ -87,7 +87,11 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
|
||||
};
|
||||
|
||||
pub fn clearRetainingCapacity(self: *Self) void {
|
||||
self.* = .initBuffer(self.slots.items);
|
||||
var slots = self.slots;
|
||||
slots.clearRetainingCapacity();
|
||||
|
||||
self.* = .empty;
|
||||
self.slots = slots;
|
||||
}
|
||||
|
||||
fn insertHole(self: *Self, index: Index) void {
|
||||
@ -245,16 +249,16 @@ test "insert & remove" {
|
||||
const expect = std.testing.expect;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var map: TestMap = .empty;
|
||||
var map: TestMap = try .initCapacity(gpa, 32);
|
||||
defer map.deinit(gpa);
|
||||
|
||||
const id1 = try map.insertUndefined(gpa);
|
||||
const id1 = try map.insertUndefined();
|
||||
try expect(map.exists(id1));
|
||||
try expect(map.remove(id1));
|
||||
try expect(!map.exists(id1));
|
||||
try expect(!map.remove(id1));
|
||||
|
||||
const id2 = try map.insertUndefined(gpa);
|
||||
const id2 = try map.insertUndefined();
|
||||
try expect(map.exists(id2));
|
||||
try expect(!map.exists(id1));
|
||||
}
|
||||
@ -263,18 +267,18 @@ test "generation wrap around" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var map: TestMap = .empty;
|
||||
var map: TestMap = try .initCapacity(gpa, 32);
|
||||
defer map.deinit(gpa);
|
||||
|
||||
// Grow array list so that at least 1 slot exists
|
||||
const id1 = try map.insertUndefined(gpa);
|
||||
const id1 = try map.insertUndefined();
|
||||
map.removeAssumeExists(id1);
|
||||
|
||||
// Artificially increase generation count
|
||||
map.slots.items[id1.index].generation = std.math.maxInt(@FieldType(TestMap.Id, "generation"));
|
||||
|
||||
// Check if generation wraps around
|
||||
const id2 = try map.insertUndefined(gpa);
|
||||
const id2 = try map.insertUndefined();
|
||||
map.removeAssumeExists(id2);
|
||||
try expectEqual(id1.index, id2.index);
|
||||
try expectEqual(0, map.slots.items[id1.index].generation);
|
||||
@ -284,13 +288,13 @@ test "iterator" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var map: TestMap = .empty;
|
||||
var map: TestMap = try .initCapacity(gpa, 32);
|
||||
defer map.deinit(gpa);
|
||||
|
||||
// Create array which has a hole
|
||||
const id1 = try map.insertUndefined(gpa);
|
||||
const id2 = try map.insertUndefined(gpa);
|
||||
const id3 = try map.insertUndefined(gpa);
|
||||
const id1 = try map.insertUndefined();
|
||||
const id2 = try map.insertUndefined();
|
||||
const id3 = try map.insertUndefined();
|
||||
|
||||
map.removeAssumeExists(id2);
|
||||
|
||||
@ -305,14 +309,14 @@ test "clear retaining capacity" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var map: TestMap = .empty;
|
||||
var map: TestMap = try .initCapacity(gpa, 32);
|
||||
defer map.deinit(gpa);
|
||||
|
||||
const id1 = try map.insertUndefined(gpa);
|
||||
const id1 = try map.insertUndefined();
|
||||
try expect(map.exists(id1));
|
||||
map.clearRetainingCapacity();
|
||||
|
||||
const id2 = try map.insertUndefined(gpa);
|
||||
const id2 = try map.insertUndefined();
|
||||
try expect(map.exists(id2));
|
||||
|
||||
try expectEqual(id1, id2);
|
||||
|
||||
57
src/tilesheet.zig
Normal file
57
src/tilesheet.zig
Normal file
@ -0,0 +1,57 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.app);
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Platform = @import("platform/root.zig");
|
||||
const Gfx = Platform.Gfx;
|
||||
const Vec2 = Platform.Vec2;
|
||||
const Rect = Platform.Rect;
|
||||
|
||||
const Tilesheet = @This();
|
||||
|
||||
image: Gfx.ImageData,
|
||||
sprites: []Gfx.Sprite.Id,
|
||||
tile_size: Vec2,
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
pub fn init(gpa: Allocator, image: Gfx.ImageData, tile_size: Vec2) !Tilesheet {
|
||||
const width = @divFloor(image.width, @as(u32, @intFromFloat(tile_size.x)));
|
||||
const height = @divFloor(image.height, @as(u32, @intFromFloat(tile_size.y)));
|
||||
const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height);
|
||||
errdefer gpa.free(sprites);
|
||||
@memset(sprites, .nil);
|
||||
|
||||
return Tilesheet{
|
||||
.sprites = sprites,
|
||||
.image = image,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.tile_size = tile_size
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Tilesheet) void {
|
||||
for (self.sprites) |sprite| {
|
||||
Gfx.deinitSprite(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: Tilesheet, x: usize, y: usize) Gfx.Sprite.Id {
|
||||
if (x >= self.width or y >= self.height) {
|
||||
log.warn("Attempt to get tile which is out of bounds", .{});
|
||||
return .nil;
|
||||
}
|
||||
|
||||
const sprite_index = y * self.width + x;
|
||||
if (self.sprites[sprite_index] == Gfx.Sprite.Id.nil) {
|
||||
const sprite = Gfx.initSprite(.{ .padding = 1 });
|
||||
Gfx.setSprite(sprite, .{
|
||||
.image = self.image,
|
||||
.rect = Rect.init(@floatFromInt(x), @floatFromInt(y), 1, 1).multiply(self.tile_size)
|
||||
});
|
||||
self.sprites[sprite_index] = sprite;
|
||||
}
|
||||
|
||||
return self.sprites[sprite_index];
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user