feat: non-ttf text rendering using original sprite text, remove black bg from assets
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
assets/unpacked/text/!.png
Normal file
|
After Width: | Height: | Size: 100 B |
BIN
assets/unpacked/text/-.png
Normal file
|
After Width: | Height: | Size: 86 B |
BIN
assets/unpacked/text/0.png
Normal file
|
After Width: | Height: | Size: 119 B |
BIN
assets/unpacked/text/1.png
Normal file
|
After Width: | Height: | Size: 103 B |
BIN
assets/unpacked/text/2.png
Normal file
|
After Width: | Height: | Size: 112 B |
BIN
assets/unpacked/text/3.png
Normal file
|
After Width: | Height: | Size: 109 B |
BIN
assets/unpacked/text/4.png
Normal file
|
After Width: | Height: | Size: 115 B |
BIN
assets/unpacked/text/5.png
Normal file
|
After Width: | Height: | Size: 114 B |
BIN
assets/unpacked/text/6.png
Normal file
|
After Width: | Height: | Size: 109 B |
BIN
assets/unpacked/text/7.png
Normal file
|
After Width: | Height: | Size: 105 B |
BIN
assets/unpacked/text/8.png
Normal file
|
After Width: | Height: | Size: 116 B |
BIN
assets/unpacked/text/9.png
Normal file
|
After Width: | Height: | Size: 111 B |
BIN
assets/unpacked/text/A.png
Normal file
|
After Width: | Height: | Size: 109 B |
BIN
assets/unpacked/text/B.png
Normal file
|
After Width: | Height: | Size: 103 B |
BIN
assets/unpacked/text/C.png
Normal file
|
After Width: | Height: | Size: 113 B |
BIN
assets/unpacked/text/D.png
Normal file
|
After Width: | Height: | Size: 119 B |
BIN
assets/unpacked/text/E.png
Normal file
|
After Width: | Height: | Size: 102 B |
BIN
assets/unpacked/text/F.png
Normal file
|
After Width: | Height: | Size: 105 B |
BIN
assets/unpacked/text/G.png
Normal file
|
After Width: | Height: | Size: 110 B |
BIN
assets/unpacked/text/H.png
Normal file
|
After Width: | Height: | Size: 105 B |
BIN
assets/unpacked/text/I.png
Normal file
|
After Width: | Height: | Size: 95 B |
BIN
assets/unpacked/text/J.png
Normal file
|
After Width: | Height: | Size: 105 B |
BIN
assets/unpacked/text/K.png
Normal file
|
After Width: | Height: | Size: 111 B |
BIN
assets/unpacked/text/L.png
Normal file
|
After Width: | Height: | Size: 93 B |
BIN
assets/unpacked/text/M.png
Normal file
|
After Width: | Height: | Size: 117 B |
BIN
assets/unpacked/text/N.png
Normal file
|
After Width: | Height: | Size: 118 B |
BIN
assets/unpacked/text/O.png
Normal file
|
After Width: | Height: | Size: 103 B |
BIN
assets/unpacked/text/P.png
Normal file
|
After Width: | Height: | Size: 109 B |
BIN
assets/unpacked/text/Q.png
Normal file
|
After Width: | Height: | Size: 118 B |
BIN
assets/unpacked/text/R.png
Normal file
|
After Width: | Height: | Size: 113 B |
BIN
assets/unpacked/text/S.png
Normal file
|
After Width: | Height: | Size: 114 B |
BIN
assets/unpacked/text/T.png
Normal file
|
After Width: | Height: | Size: 90 B |
BIN
assets/unpacked/text/U.png
Normal file
|
After Width: | Height: | Size: 101 B |
BIN
assets/unpacked/text/V.png
Normal file
|
After Width: | Height: | Size: 119 B |
BIN
assets/unpacked/text/W.png
Normal file
|
After Width: | Height: | Size: 114 B |
BIN
assets/unpacked/text/X.png
Normal file
|
After Width: | Height: | Size: 126 B |
BIN
assets/unpacked/text/Y.png
Normal file
|
After Width: | Height: | Size: 107 B |
BIN
assets/unpacked/text/Z.png
Normal file
|
After Width: | Height: | Size: 107 B |
BIN
assets/unpacked/text/_copyright.png
Normal file
|
After Width: | Height: | Size: 119 B |
BIN
assets/unpacked/text/_double_quote.png
Normal file
|
After Width: | Height: | Size: 91 B |
BIN
assets/unpacked/text/_forward_slash.png
Normal file
|
After Width: | Height: | Size: 77 B |
BIN
assets/unpacked/text/blank.png
Normal file
|
After Width: | Height: | Size: 72 B |
26
src/game.rs
@@ -29,6 +29,7 @@ use crate::map::Map;
|
||||
use crate::texture::animated::AnimatedTexture;
|
||||
use crate::texture::blinking::BlinkingTexture;
|
||||
use crate::texture::sprite::{AtlasMapper, AtlasTile, SpriteAtlas};
|
||||
use crate::texture::text::TextTexture;
|
||||
use crate::texture::{get_atlas_tile, sprite};
|
||||
|
||||
/// The main game state.
|
||||
@@ -52,6 +53,7 @@ pub struct Game {
|
||||
atlas: Rc<SpriteAtlas>,
|
||||
font: Font<'static, 'static>,
|
||||
map_texture: AtlasTile,
|
||||
text_texture: TextTexture,
|
||||
|
||||
// Audio
|
||||
pub audio: Audio,
|
||||
@@ -67,11 +69,10 @@ impl Game {
|
||||
let map = Rc::new(RefCell::new(Map::new(RAW_BOARD)));
|
||||
let atlas_bytes = get_asset_bytes(Asset::Atlas).expect("Failed to load asset");
|
||||
let atlas_texture = unsafe {
|
||||
sprite::texture_to_static(
|
||||
texture_creator
|
||||
.load_texture_bytes(&atlas_bytes)
|
||||
.expect("Could not load atlas texture from asset API"),
|
||||
)
|
||||
let texture = texture_creator
|
||||
.load_texture_bytes(&atlas_bytes)
|
||||
.expect("Could not load atlas texture from asset API");
|
||||
sprite::texture_to_static(texture)
|
||||
};
|
||||
let atlas_json = get_asset_bytes(Asset::AtlasJson).expect("Failed to load asset");
|
||||
let atlas_mapper: AtlasMapper = serde_json::from_slice(&atlas_json).expect("Could not parse atlas JSON");
|
||||
@@ -102,6 +103,7 @@ impl Game {
|
||||
.load_font_from_rwops(font_rwops, 24)
|
||||
.expect("Could not load font from asset API")
|
||||
};
|
||||
let text_texture = TextTexture::new(Rc::clone(&atlas), 1.0);
|
||||
let audio = Audio::new();
|
||||
Game {
|
||||
pacman,
|
||||
@@ -113,6 +115,7 @@ impl Game {
|
||||
atlas,
|
||||
font,
|
||||
map_texture,
|
||||
text_texture,
|
||||
audio,
|
||||
fps_1s: 0.0,
|
||||
fps_10s: 0.0,
|
||||
@@ -321,6 +324,7 @@ impl Game {
|
||||
let lives_offset = 3;
|
||||
let score_offset = 7 - (score_text.len() as i32);
|
||||
let gap_offset = 6;
|
||||
self.text_texture.set_scale(2.0);
|
||||
self.render_text_on(
|
||||
canvas,
|
||||
&*texture_creator,
|
||||
@@ -355,14 +359,8 @@ impl Game {
|
||||
position: IVec2,
|
||||
color: Color,
|
||||
) {
|
||||
let surface = self.font.render(text).blended(color).expect("Could not render text surface");
|
||||
let texture = texture_creator
|
||||
.create_texture_from_surface(&surface)
|
||||
.expect("Could not create texture from surface");
|
||||
let query = texture.query();
|
||||
let dst_rect = sdl2::rect::Rect::new(position.x, position.y, query.width, query.height);
|
||||
canvas
|
||||
.copy(&texture, None, Some(dst_rect))
|
||||
.expect("Could not render text texture");
|
||||
self.text_texture
|
||||
.render(canvas, text, glam::UVec2::new(position.x as u32, position.y as u32))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod animated;
|
||||
pub mod blinking;
|
||||
pub mod directional;
|
||||
pub mod sprite;
|
||||
pub mod text;
|
||||
|
||||
pub fn get_atlas_tile(atlas: &Rc<SpriteAtlas>, name: &str) -> AtlasTile {
|
||||
SpriteAtlas::get_tile(atlas, name).unwrap_or_else(|| panic!("Could not find tile {}", name))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use glam::U16Vec2;
|
||||
use sdl2::rect::Rect;
|
||||
use sdl2::render::{Texture, WindowCanvas};
|
||||
use sdl2::render::{Canvas, RenderTarget, Texture};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
@@ -27,7 +27,7 @@ pub struct AtlasTile {
|
||||
}
|
||||
|
||||
impl AtlasTile {
|
||||
pub fn render(&self, canvas: &mut WindowCanvas, dest: Rect) -> Result<()> {
|
||||
pub fn render<C: RenderTarget>(&self, canvas: &mut Canvas<C>, dest: Rect) -> Result<()> {
|
||||
let src = Rect::new(self.pos.x as i32, self.pos.y as i32, self.size.x as u32, self.size.y as u32);
|
||||
canvas.copy(&self.atlas.texture, src, dest).map_err(anyhow::Error::msg)?;
|
||||
Ok(())
|
||||
@@ -54,6 +54,10 @@ impl SpriteAtlas {
|
||||
size: U16Vec2::new(frame.width, frame.height),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn texture(&self) -> &Texture<'static> {
|
||||
&self.texture
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn texture_to_static<'a>(texture: Texture<'a>) -> Texture<'static> {
|
||||
|
||||
155
src/texture/text.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
//! This module provides text rendering using the texture atlas.
|
||||
//!
|
||||
//! The TextTexture system renders text from the atlas using character mapping.
|
||||
//! It supports a subset of characters with special handling for characters that
|
||||
//! can't be used in filenames.
|
||||
//!
|
||||
//! # Example Usage
|
||||
//!
|
||||
//! ```rust
|
||||
//! use crate::texture::text::TextTexture;
|
||||
//! use std::rc::Rc;
|
||||
//!
|
||||
//! // Create a text texture with 1.0 scale (8x8 pixels per character)
|
||||
//! let mut text_renderer = TextTexture::new(atlas.clone(), 1.0);
|
||||
//!
|
||||
//! // Render text at position (100, 50)
|
||||
//! text_renderer.render(canvas, "PAC-MAN", glam::UVec2::new(100, 50))?;
|
||||
//!
|
||||
//! // Change scale for larger text
|
||||
//! text_renderer.set_scale(2.0);
|
||||
//! text_renderer.render(canvas, "SCORE: 1000", glam::UVec2::new(50, 100))?;
|
||||
//!
|
||||
//! // Calculate text width for positioning
|
||||
//! let width = text_renderer.text_width("GAME OVER");
|
||||
//! let height = text_renderer.text_height();
|
||||
//! ```
|
||||
//!
|
||||
//! # Supported Characters
|
||||
//!
|
||||
//! - Letters: A-Z, a-z
|
||||
//! - Numbers: 0-9
|
||||
//! - Common symbols: ! ? . , : ; - _ ( ) [ ] { } < > = + * / \ | & @ # $ % ^ ~ ` ' "
|
||||
//! - Space character
|
||||
//!
|
||||
//! # Character Mapping
|
||||
//!
|
||||
//! Most characters use their literal name (e.g., "A.png", "1.png").
|
||||
//! Special characters use alternative names:
|
||||
//! - `"` → "text/_double_quote.png"
|
||||
//! - `'` → "text/_single_quote.png"
|
||||
//! - `\` → "text/\\backslash.png"
|
||||
//! - ` ` (space) → "text/space.png"
|
||||
//!
|
||||
//! # Memory Optimization
|
||||
//!
|
||||
//! The system caches character tiles in a HashMap to avoid repeated
|
||||
//! atlas lookups. Only tiles for used characters are stored in memory.
|
||||
|
||||
use anyhow::Result;
|
||||
use glam::UVec2;
|
||||
use sdl2::rect::Rect;
|
||||
use sdl2::render::{Canvas, RenderTarget};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
||||
|
||||
/// A text texture that renders characters from the atlas.
|
||||
pub struct TextTexture {
|
||||
atlas: Rc<SpriteAtlas>,
|
||||
char_map: HashMap<char, AtlasTile>,
|
||||
scale: f32,
|
||||
}
|
||||
|
||||
impl TextTexture {
|
||||
/// Creates a new text texture with the given atlas and scale.
|
||||
pub fn new(atlas: Rc<SpriteAtlas>, scale: f32) -> Self {
|
||||
Self {
|
||||
atlas,
|
||||
char_map: HashMap::new(),
|
||||
scale,
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a character to its atlas tile, handling special characters.
|
||||
fn get_char_tile(&mut self, c: char) -> Option<AtlasTile> {
|
||||
if let Some(tile) = self.char_map.get(&c) {
|
||||
return Some(tile.clone());
|
||||
}
|
||||
|
||||
let tile_name = self.char_to_tile_name(c)?;
|
||||
let tile = SpriteAtlas::get_tile(&self.atlas, &tile_name)?;
|
||||
self.char_map.insert(c, tile.clone());
|
||||
Some(tile)
|
||||
}
|
||||
|
||||
/// Converts a character to its tile name in the atlas.
|
||||
fn char_to_tile_name(&self, c: char) -> Option<String> {
|
||||
let name = match c {
|
||||
// Letters A-Z
|
||||
'A'..='Z' => format!("text/{}.png", c),
|
||||
// Numbers 0-9
|
||||
'0'..='9' => format!("text/{}.png", c),
|
||||
// Special characters
|
||||
'!' => "text/!.png".to_string(),
|
||||
'-' => "text/-.png".to_string(),
|
||||
'"' => "text/_double_quote.png".to_string(),
|
||||
'/' => "text/_forward_slash.png".to_string(),
|
||||
// Skip spaces for now - they don't have a tile
|
||||
' ' => return None,
|
||||
|
||||
// Unsupported character
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(name)
|
||||
}
|
||||
|
||||
/// Renders a string of text at the given position.
|
||||
pub fn render<C: RenderTarget>(&mut self, canvas: &mut Canvas<C>, text: &str, position: UVec2) -> Result<()> {
|
||||
let mut x_offset = 0;
|
||||
let char_width = (8.0 * self.scale) as u32;
|
||||
let char_height = (8.0 * self.scale) as u32;
|
||||
|
||||
for c in text.chars() {
|
||||
if let Some(tile) = self.get_char_tile(c) {
|
||||
let dest = sdl2::rect::Rect::new((position.x + x_offset) as i32, position.y as i32, char_width, char_height);
|
||||
tile.render(canvas, dest)?;
|
||||
}
|
||||
// Always advance x_offset for all characters (including spaces)
|
||||
x_offset += char_width;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the scale for text rendering.
|
||||
pub fn set_scale(&mut self, scale: f32) {
|
||||
self.scale = scale;
|
||||
}
|
||||
|
||||
/// Gets the current scale.
|
||||
pub fn scale(&self) -> f32 {
|
||||
self.scale
|
||||
}
|
||||
|
||||
/// Calculates the width of a string in pixels at the current scale.
|
||||
pub fn text_width(&mut self, text: &str) -> u32 {
|
||||
let char_width = (8.0 * self.scale) as u32;
|
||||
let mut width = 0;
|
||||
|
||||
for c in text.chars() {
|
||||
if self.char_to_tile_name(c).is_some() {
|
||||
width += char_width;
|
||||
}
|
||||
}
|
||||
|
||||
width
|
||||
}
|
||||
|
||||
/// Calculates the height of text in pixels at the current scale.
|
||||
pub fn text_height(&self) -> u32 {
|
||||
(8.0 * self.scale) as u32
|
||||
}
|
||||
}
|
||||