--- /dev/null
+/*
+
+Bitmap Fonts
+
+Simple bitmap font renderer. Splits an image of all font characters a set of glyphs
+that can be used to render individual characters / text.
+
+When creating a bitmap font, you need to provide the font texture, and the order of the
+glyphs in the texture in a string. You can use non-ASCII characters in the string using
+'\xAA' as a character.
+
+The font data provided is an RGBA byte array, with interlaced components.
+In other words the data should look like:
+
+ R G B A R G B A R G B A ...
+
+The top left (0-th) pixel is taken to be the separator color. It is used to determine the
+outline of every glyph. Glyphs are read from the top left, left to right, top to bottom.
+The separator color must be present on the entire boundaries of the glyph.
+
+*/
+
+
+
+
+
+package bitmap_font
+use package core
+
+Bitmap_Font :: struct {
+ font_texture : Bitmap_Font_Texture;
+
+ em: f32; // Width of 'M'
+
+ glyphs : map.Map(u32, Glyph);
+
+ Glyph :: struct {
+ x0, y0, x1, y1: f32;
+ w, h: f32;
+ }
+
+ get_glyph :: (use bmp: ^Bitmap_Font, char: u8) -> ^Glyph {
+ return map.get_ptr(^glyphs, ~~char);
+ }
+}
+
+Bitmap_Font_Texture :: struct {
+ Components_Per_Pixel :: 4; // NOTE: RGBA only
+
+ data : [] u8;
+ width, height : u32;
+
+ // NOTE: Assumes pixels are laid out in RGBA format.
+ get_pixel :: (use texture: ^Bitmap_Font_Texture, x: u32, y: u32) -> u32 {
+ return (cast(^u32) data.data)[y * width + x];
+
+ // SPEED: All of these values could be read simultaneously by treating the data
+ // array as a slice of u32, and then not multiplying by Components_Per_Pixel.
+ //
+ // return (cast(u32) data[(y * width + x) * Components_Per_Pixel + 0] << 24)
+ // | (cast(u32) data[(y * width + x) * Components_Per_Pixel + 1] << 16)
+ // | (cast(u32) data[(y * width + x) * Components_Per_Pixel + 2] << 8)
+ // | (cast(u32) data[(y * width + x) * Components_Per_Pixel + 3] << 0);
+ }
+}
+
+
+// NOTE: Takes the raw RGBA font data. Therefore, font_data.count is expected to
+// be a multiple of 4 in length, with interlaced red, green, blue and alpha components.
+// The font data can be load from another source using this method, which is why it
+// is preferred.
+bitmap_font_create :: (bft: Bitmap_Font_Texture, charset: str) -> Bitmap_Font {
+ assert(bft.data.count <= bft.width * bft.height * Bitmap_Font_Texture.Components_Per_Pixel, "Bad font size.");
+
+ bmp: Bitmap_Font;
+ bmp.font_texture = bft;
+ map.init(^bmp.glyphs, .{0,0,0,0,0,0});
+
+ success := bitmap_font_prepare_glyphs(^bmp, charset);
+ assert(success, "Failed to load glyphs out of font.");
+
+ return bmp;
+}
+
+bitmap_font_prepare_glyphs :: (use bmp: ^Bitmap_Font, charset: str) -> bool {
+ separator_color := font_texture->get_pixel(0, 0);
+
+ g_x0, g_y0: u32;
+ g_x0, g_y0 = 0, 0;
+
+ for char: charset {
+ if g_y0 >= font_texture.height do return false;
+
+ // These will need to be converted to floating point when they are inserted into the glyph,
+ // but it is easier to think about them as actual pixel coordinates when parsing the glyphs.
+ x0, y0, x1, y1: u32;
+ x0, y0 = g_x0 + 1, g_y0 + 1;
+ x1, y1 = x0, y0;
+
+ while font_texture->get_pixel(x1, y0) != separator_color do x1 += 1;
+ while font_texture->get_pixel(x0, y1) != separator_color do y1 += 1;
+
+ map.put(^glyphs, ~~char, .{
+ x0 = cast(f32) x0 / ~~font_texture.width,
+ y0 = cast(f32) y0 / ~~font_texture.height,
+ x1 = cast(f32) x1 / ~~font_texture.width,
+ y1 = cast(f32) y1 / ~~font_texture.height,
+ w = cast(f32) (x1 - x0) / ~~font_texture.width,
+ h = cast(f32) (y1 - y0) / ~~font_texture.height,
+ });
+
+ g_x0 = x1 + 1;
+
+ // CLEANUP: This is gross because Onyx does not support short circuit boolean operators,
+ // and because g_x0 being greater than font_texture.width would potentially overflow in
+ // get_pixel, they need to be checked in two different steps instead of with '||'.
+ reset := g_x0 >= font_texture.width;
+ if !reset do reset = font_texture->get_pixel(g_x0, g_y0) != separator_color;
+ if reset {
+ g_x0 = 0;
+ g_y0 = y1 + 1;
+ }
+ }
+
+ M_glyph := map.get_ptr(^glyphs, #char "M");
+ if M_glyph != null {
+ em = M_glyph.w * ~~font_texture.width;
+ } else {
+ // ROBUSTNESS: If there is no 'M' in the character set, then just use 16. It will probably be very wrong.
+ em = 16;
+ }
+
+ return true;
+}
#private_file events :: package js_events
#private_file gl :: package gl
+#private_file bitmap_font :: package bitmap_font
+
BAR_HEIGHT :: 32.0f
particle_board: ParticleBoard;
world_texture : gl.GLTexture
world_texture_data : [] u8
+font_texture : gl.GLTexture
+font : bitmap_font.Bitmap_Font
+
window_width := 0
window_height := 0
gfx.immediate_renderer_init();
+ init_font();
+
+ gl.enable(gl.BLEND);
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+
particle_board = create_board(world_width, world_height);
}
+init_font :: () {
+ font_data := #file_contents "src/res/font_2.data";
+
+ bft := bitmap_font.Bitmap_Font_Texture.{
+ data = font_data,
+ width = 256,
+ height = 256,
+ };
+
+ font = bitmap_font.bitmap_font_create(bft, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 \xff:");
+
+ font_texture = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, font_texture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 256, 0, gl.RGBA, gl.UNSIGNED_BYTE, font_data);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.bindTexture(gl.TEXTURE_2D, -1);
+}
+
poll_events :: () {
for event: events.consume() do switch event.kind {
case .Resize {
}
}
-update :: () {
+fps := 0;
+fps_timer := 1.0f;
+frames := 0
+update :: (dt: f32) {
+ fps_timer -= dt;
+ frames += 1;
+ if fps_timer <= 0 {
+ fps = frames;
+ frames = 0;
+ fps_timer = 1.0f;
+ }
+
if mouse_down {
spawn_particles(~~mouse_x, ~~mouse_y, 20, selected_particle);
}
gl.bindTexture(gl.TEXTURE_2D, -1);
+ fps_buffer : [16] u8;
+ fps_string := conv.str_format("FPS: %i", ~~fps_buffer, fps);
+ draw_text(fps_string, 0, BAR_HEIGHT);
+
draw_selection_bar :: () {
gfx.rect(.{ 0, 0 }, .{ ~~window_width, BAR_HEIGHT }, color=.{.2,.2,.2});
}
}
+draw_text :: (text: str, x: f32, y: f32, size := 32.0f, color := gfx.Color4.{1,1,1}) {
+
+ gl.bindTexture(gl.TEXTURE_2D, font_texture);
+ gfx.set_texture(0);
+
+ for char: text {
+ glyph := font->get_glyph(char);
+
+ if glyph == null {
+ glyph = font->get_glyph(255);
+ assert(glyph != null, "NO NULL GLYPH");
+ }
+
+ gfx.textured_rect(
+ .{ x, y },
+ .{ glyph.w * size * font.em, glyph.h * size * font.em },
+ .{ glyph.x0, glyph.y0 },
+ .{ glyph.x1 - glyph.x0, glyph.y1 - glyph.y0 },
+ color = color);
+
+ x += glyph.w * size * font.em;
+ }
+
+ gfx.flush();
+ gl.bindTexture(gl.TEXTURE_2D, -1);
+}
+
+
+last_time := 0;
#export "loop" () {
- poll_events();
+ time_now :: () -> i32 #foreign "game" "time_now" ---
+
+ if last_time == 0 do last_time = time_now();
- update();
+ now := time_now();
+ dt := cast(f32) (now - last_time) / 1000.0f;
+ last_time = now;
+
+ poll_events();
+ update(dt);
draw();
}