diff --git a/Cargo.toml b/Cargo.toml index ec42aca9e8..bcae2fefde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [features] default = ["headless", "wgpu", "winit"] -headless = ["asset", "core", "derive", "diagnostic", "gltf", "input", "pbr", "render", "serialization", "transform", "ui", "window"] +headless = ["asset", "core", "derive", "diagnostic", "gltf", "input", "pbr", "render", "serialization", "text", "transform", "ui", "window"] asset = ["bevy_asset"] core = ["bevy_core"] derive = ["bevy_derive"] @@ -16,6 +16,7 @@ input = ["bevy_input"] pbr = ["bevy_pbr"] render = ["bevy_render"] serialization = ["bevy_serialization"] +text = ["bevy_text"] transform = ["bevy_transform"] ui = ["bevy_ui"] window = ["bevy_window"] @@ -42,6 +43,7 @@ bevy_pbr = { path = "crates/bevy_pbr", optional = true } bevy_render = { path = "crates/bevy_render", optional = true } bevy_serialization = { path = "crates/bevy_serialization", optional = true } bevy_transform = { path = "crates/bevy_transform", optional = true } +bevy_text = { path = "crates/bevy_text", optional = true } bevy_ui = { path = "crates/bevy_ui", optional = true } bevy_window = { path = "crates/bevy_window", optional = true } bevy_wgpu = { path = "crates/bevy_wgpu", optional = true } @@ -149,6 +151,10 @@ path = "examples/shader/shader_custom_material.rs" name = "shader_defs" path = "examples/shader/shader_defs.rs" +[[example]] +name = "text" +path = "examples/ui/text.rs" + [[example]] name = "ui" path = "examples/ui/ui.rs" diff --git a/assets/fonts/FiraMono-Medium.ttf b/assets/fonts/FiraMono-Medium.ttf new file mode 100755 index 0000000000..1e95ced4c4 Binary files /dev/null and b/assets/fonts/FiraMono-Medium.ttf differ diff --git a/assets/fonts/FiraSans-Bold.ttf b/assets/fonts/FiraSans-Bold.ttf new file mode 100755 index 0000000000..95e1660240 Binary files /dev/null and b/assets/fonts/FiraSans-Bold.ttf differ diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 32a68a3d27..8d59e93442 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -4,10 +4,6 @@ pub use handle::*; use bevy_core::bytes::GetBytes; use std::collections::HashMap; -pub trait Asset { - fn load(descriptor: D) -> Self; -} - pub struct AssetStorage { assets: HashMap, names: HashMap>, diff --git a/crates/bevy_render/src/texture/texture.rs b/crates/bevy_render/src/texture/texture.rs index 8e9234271c..182cbee0f7 100644 --- a/crates/bevy_render/src/texture/texture.rs +++ b/crates/bevy_render/src/texture/texture.rs @@ -1,5 +1,5 @@ use crate::shader::ShaderDefSuffixProvider; -use bevy_asset::{Asset, Handle}; +use bevy_asset::Handle; use std::fs::File; pub const TEXTURE_ASSET_INDEX: usize = 0; @@ -19,10 +19,8 @@ impl Texture { pub fn aspect(&self) -> f32 { self.height as f32 / self.width as f32 } -} -impl Asset for Texture { - fn load(descriptor: TextureType) -> Self { + pub fn load(descriptor: TextureType) -> Self { let (data, width, height) = match descriptor { TextureType::Data(data, width, height) => (data.clone(), width, height), TextureType::Png(path) => { diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml new file mode 100644 index 0000000000..48bf3534b8 --- /dev/null +++ b/crates/bevy_text/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bevy_text" +version = "0.1.0" +authors = ["Carter Anderson "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bevy_render = { path = "../bevy_render" } +skribo = "0.1.0" +font-kit = "0.6" +pathfinder_geometry = "0.5" \ No newline at end of file diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs new file mode 100644 index 0000000000..4b8cd968b9 --- /dev/null +++ b/crates/bevy_text/src/font.rs @@ -0,0 +1,27 @@ +use crate::render::render_text; +use bevy_render::{texture::Texture, Color}; +use font_kit::{error::FontLoadingError, metrics::Metrics}; +use skribo::{FontCollection, FontFamily}; +use std::sync::Arc; + +pub struct Font { + pub collection: FontCollection, + pub metrics: Metrics, +} + +impl Font { + pub fn try_from_bytes(font_data: Vec) -> Result { + let font = font_kit::font::Font::from_bytes(Arc::new(font_data), 0)?; + let metrics = font.metrics(); + let mut collection = FontCollection::new(); + collection.add_family(FontFamily::new_from_font(font)); + Ok(Font { + collection, + metrics, + }) + } + + pub fn render_text(&self, text: &str, color: Color, width: usize, height: usize) -> Texture { + render_text(self, text, color, width, height) + } +} diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs new file mode 100644 index 0000000000..6b305a4028 --- /dev/null +++ b/crates/bevy_text/src/lib.rs @@ -0,0 +1,4 @@ +mod render; +mod font; + +pub use font::*; diff --git a/crates/bevy_text/src/render.rs b/crates/bevy_text/src/render.rs new file mode 100644 index 0000000000..1859492354 --- /dev/null +++ b/crates/bevy_text/src/render.rs @@ -0,0 +1,141 @@ +use crate::Font; +use bevy_render::{ + texture::{Texture, TextureType}, + Color, +}; +use font_kit::{ + canvas::{Canvas, Format, RasterizationOptions}, + hinting::HintingOptions, +}; +use pathfinder_geometry::transform2d::Transform2F; +use skribo::{LayoutSession, TextStyle}; +use std::ops::Range; + +struct TextSurface { + width: usize, + height: usize, + pixels: Vec, +} + +fn composite(a: u8, b: u8) -> u8 { + let y = ((255 - a) as u16) * ((255 - b) as u16); + let y = (y + (y >> 8) + 0x80) >> 8; // fast approx to round(y / 255) + 255 - (y as u8) +} + +impl TextSurface { + fn new(width: usize, height: usize) -> TextSurface { + let pixels = vec![0; width * height]; + TextSurface { + width, + height, + pixels, + } + } + + fn paint_from_canvas(&mut self, canvas: &Canvas, x: i32, y: i32) { + let (cw, ch) = (canvas.size.x(), canvas.size.y()); + let (w, h) = (self.width as i32, self.height as i32); + let y = y - ch; + let xmin = 0.max(-x); + let xmax = cw.min(w - x); + let ymin = 0.max(-y); + let ymax = ch.min(h - y); + for yy in ymin..(ymax.max(ymin)) { + for xx in xmin..(xmax.max(xmin)) { + let pix = canvas.pixels[(cw * yy + xx) as usize]; + let dst_ix = ((y + yy) * w + x + xx) as usize; + self.pixels[dst_ix] = composite(self.pixels[dst_ix], pix); + } + } + } + + fn paint_layout_session>( + &mut self, + layout: &mut LayoutSession, + x: i32, + y: i32, + size: f32, + range: Range, + ) { + for run in layout.iter_substr(range) { + let font = run.font(); + for glyph in run.glyphs() { + let glyph_id = glyph.glyph_id; + let glyph_x = (glyph.offset.x() as i32) + x; + let glyph_y = (glyph.offset.y() as i32) + y; + let bounds = font + .font + .raster_bounds( + glyph_id, + size, + Transform2F::default(), + HintingOptions::None, + RasterizationOptions::GrayscaleAa, + ) + .unwrap(); + if bounds.width() > 0 && bounds.height() > 0 { + let origin_adj = bounds.origin().to_f32(); + let neg_origin = -origin_adj; + let mut canvas = Canvas::new(bounds.size(), Format::A8); + font.font + .rasterize_glyph( + &mut canvas, + glyph_id, + size, + Transform2F::from_translation(neg_origin), + HintingOptions::None, + RasterizationOptions::GrayscaleAa, + ) + .unwrap(); + self.paint_from_canvas( + &canvas, + glyph_x + bounds.origin_x(), + glyph_y - bounds.origin_y(), + ); + } + } + } + } +} + +pub fn render_text(font: &Font, text: &str, color: Color, width: usize, height: usize) -> Texture { + let mut surface = TextSurface::new(width, height); + let style = TextStyle { + size: height as f32, + }; + let offset = style.size * (font.metrics.ascent - font.metrics.cap_height) + / font.metrics.units_per_em as f32; + + let mut layout = LayoutSession::create(&text, &style, &font.collection); + surface.paint_layout_session( + &mut layout, + 0, + style.size as i32 - offset as i32, + style.size, + 0..text.len(), + ); + let color_u8 = [ + (color.r * 255.0) as u8, + (color.g * 255.0) as u8, + (color.b * 255.0) as u8, + ]; + + Texture::load(TextureType::Data( + surface + .pixels + .iter() + .map(|p| { + vec![ + color_u8[0], + color_u8[1], + color_u8[2], + (color.a * *p as f32) as u8, + ] + }) + .flatten() + .collect::>(), + surface.width, + surface.height, + )) +} diff --git a/examples/ui/text.rs b/examples/ui/text.rs new file mode 100644 index 0000000000..a8f57c2d2c --- /dev/null +++ b/examples/ui/text.rs @@ -0,0 +1,41 @@ +use bevy::prelude::*; +use std::{fs::File, io::Read}; + +fn main() { + App::build() + .add_default_plugins() + .add_startup_system(setup) + .run(); +} + +fn setup(world: &mut World, resources: &mut Resources) { + let mut texture_storage = resources.get_mut::>().unwrap(); + let font_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/fonts/FiraSans-Bold.ttf" + ); + let mut font_file = File::open(&font_path).unwrap(); + let mut buffer = Vec::new(); + font_file.read_to_end(&mut buffer).unwrap(); + let font = Font::try_from_bytes(buffer).unwrap(); + + let texture = font.render_text("Hello from Bevy!", Color::rgba(0.9, 0.9, 0.9, 1.0), 500, 60); + let half_width = texture.width as f32 / 2.0; + let half_height = texture.height as f32 / 2.0; + let texture_handle = texture_storage.add(texture); + let mut color_materials = resources.get_mut::>().unwrap(); + world + .build() + // 2d camera + .add_entity(Camera2dEntity::default()) + // texture + .add_entity(UiEntity { + node: Node::new( + math::vec2(0.0, 0.0), + Anchors::CENTER, + Margins::new(-half_width, half_width, -half_height, half_height), + ), + material: color_materials.add(ColorMaterial::texture(texture_handle)), + ..Default::default() + }); +} diff --git a/src/lib.rs b/src/lib.rs index 4b0fef87d7..fbd1bd7ea8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,6 +63,8 @@ pub use bevy_input as input; pub use bevy_pbr as pbr; #[cfg(feature = "render")] pub use bevy_render as render; +#[cfg(feature = "text")] +pub use bevy_text as text; #[cfg(feature = "serialization")] pub use bevy_serialization as serialization; #[cfg(feature = "transform")] diff --git a/src/prelude.rs b/src/prelude.rs index 51d2387237..b3b4c8da4b 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,5 +1,5 @@ #[cfg(feature = "asset")] -pub use crate::asset::{Asset, AssetStorage, Handle}; +pub use crate::asset::{AssetStorage, Handle}; #[cfg(feature = "core")] pub use crate::core::{ time::Time, @@ -29,6 +29,8 @@ pub use crate::render::{ texture::{Texture, TextureType}, ActiveCamera, ActiveCamera2d, Camera, CameraType, Color, ColorSource, Renderable, }; +#[cfg(feature = "text")] +pub use crate::text::Font; #[cfg(feature = "transform")] pub use crate::transform::prelude::*; #[cfg(feature = "ui")]