-
-
Notifications
You must be signed in to change notification settings - Fork 110
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Text example * Text layout * Clean-up * More clean-up * More clean-up * Immutable methods * new_with_scaler * Example clean-up * Move text generation to three-d * Documentation + import * Add posibility to move camera * Improved text example * Fix text example on web * Handle line breaks * Improved text example * White background * Don't expose swash * Size as input to generator * Avoid exposing swash * Font index as input parameter + error handling
- Loading branch information
Showing
8 changed files
with
276 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "text" | ||
version = "0.1.0" | ||
authors = ["Asger Nyman Christiansen <[email protected]>"] | ||
edition = "2021" | ||
|
||
[lib] | ||
crate-type = ["cdylib"] | ||
|
||
[dependencies] | ||
three-d = { path = "../../", features = ["text"] } | ||
|
||
[target.'cfg(target_arch = "wasm32")'.dependencies] | ||
log = "0.4" | ||
wasm-bindgen = "0.2" | ||
wasm-bindgen-futures = "0.4" | ||
console_error_panic_hook = "0.1" | ||
console_log = "1" |
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
#![allow(special_module_name)] | ||
mod main; | ||
|
||
// Entry point for wasm | ||
#[cfg(target_arch = "wasm32")] | ||
use wasm_bindgen::prelude::*; | ||
|
||
#[cfg(target_arch = "wasm32")] | ||
#[wasm_bindgen(start)] | ||
pub fn start() -> Result<(), JsValue> { | ||
console_log::init_with_level(log::Level::Debug).unwrap(); | ||
|
||
use log::info; | ||
info!("Logging works!"); | ||
|
||
std::panic::set_hook(Box::new(console_error_panic_hook::hook)); | ||
main::main(); | ||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
use three_d::*; | ||
|
||
pub fn main() { | ||
let window = Window::new(WindowSettings { | ||
title: "Text!".to_string(), | ||
max_size: Some((1280, 720)), | ||
..Default::default() | ||
}) | ||
.unwrap(); | ||
|
||
let context = window.gl(); | ||
|
||
let mut camera = Camera::new_orthographic( | ||
window.viewport(), | ||
vec3(window.viewport().width as f32 * 0.5, 0.0, 2.0), | ||
vec3(window.viewport().width as f32 * 0.5, 0.0, 0.0), | ||
vec3(0.0, 1.0, 0.0), | ||
window.viewport().height as f32, | ||
0.1, | ||
10.0, | ||
); | ||
|
||
let text_generator = TextGenerator::new(include_bytes!("font0.ttf"), 0, 30.0).unwrap(); | ||
let text_mesh0 = text_generator.generate("Hello, World!"); | ||
let text_mesh1 = text_generator.generate("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Vivamus rutrum, augue vitae interdum dapibus, risus velit interdum dui, sit amet condimentum wisi sem vel odio. Nam lorem. Sed et leo sed est vehicula suscipit. Nunc volutpat, sapien non laoreet cursus, ipsum ipsum varius velit, sit amet lacinia nulla enim quis erat. Curabitur sagittis. Donec quis nulla et wisi molestie consequat. Nulla vel neque. Proin dignissim volutpat leo. | ||
Suspendisse ac libero sit amet leo bibendum aliquam. Pellentesque nisl. Etiam sed sem et purus convallis mattis. Sed fringilla eros id risus. | ||
Aliquam fermentum mattis lectus. Nunc luctus. Integer accumsan pede quis risus. Vestibulum et ante. | ||
Morbi dolor. In nisl. Curabitur malesuada. | ||
Morbi tincidunt semper tortor. Maecenas hendrerit. Vivamus fermentum ante ut wisi. Nunc mattis. Praesent nunc. Suspendisse potenti. Morbi sapien. | ||
Quisque sapien libero, ornare eget, tincidunt semper, convallis vel, sem. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; "); | ||
|
||
let text_generator = TextGenerator::new(include_bytes!("font1.ttf"), 0, 100.0).unwrap(); | ||
let text_mesh2 = text_generator.generate("Hi!\nHow are you?"); | ||
|
||
// Create models | ||
let mut text0 = Gm::new( | ||
Mesh::new(&context, &text_mesh0), | ||
ColorMaterial { | ||
color: Srgba::RED, | ||
..Default::default() | ||
}, | ||
); | ||
text0.set_transformation(Mat4::from_scale(10.0)); | ||
|
||
let mut text1 = Gm::new( | ||
Mesh::new(&context, &text_mesh1), | ||
ColorMaterial { | ||
color: Srgba::BLACK, | ||
..Default::default() | ||
}, | ||
); | ||
text1.set_transformation(Mat4::from_translation(vec3(50.0, 500.0, 0.0))); | ||
|
||
let mut text2 = Gm::new( | ||
Mesh::new(&context, &text_mesh2), | ||
ColorMaterial { | ||
color: Srgba::BLACK, | ||
..Default::default() | ||
}, | ||
); | ||
text2.set_transformation(Mat4::from_translation(vec3(1000.0, -200.0, 0.0))); | ||
|
||
// Render loop | ||
window.render_loop(move |frame_input| { | ||
camera.set_viewport(frame_input.viewport); | ||
|
||
for event in frame_input.events.iter() { | ||
match *event { | ||
Event::MouseMotion { delta, button, .. } => { | ||
if button == Some(MouseButton::Left) { | ||
let speed = 1.3; | ||
let right = camera.right_direction(); | ||
let up = right.cross(camera.view_direction()); | ||
let delta = -right * speed * delta.0 + up * speed * delta.1; | ||
camera.translate(&delta); | ||
} | ||
} | ||
_ => {} | ||
} | ||
} | ||
|
||
frame_input | ||
.screen() | ||
.clear(ClearState::color_and_depth(1.0, 1.0, 1.0, 1.0, 1.0)) | ||
.render(&camera, &[&text0, &text1, &text2], &[]); | ||
FrameOutput::default() | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
use crate::*; | ||
use lyon::math::Point; | ||
use lyon::path::Path; | ||
use lyon::tessellation::*; | ||
use std::collections::HashMap; | ||
use swash::zeno::{Command, PathData}; | ||
use swash::{scale::ScaleContext, shape::ShapeContext, FontRef, GlyphId}; | ||
|
||
/// | ||
/// A utility struct for generating a [CpuMesh] from a text string with a given font. | ||
/// | ||
pub struct TextGenerator<'a> { | ||
map: HashMap<GlyphId, CpuMesh>, | ||
font: FontRef<'a>, | ||
line_height: f32, | ||
size: f32, | ||
} | ||
|
||
impl<'a> TextGenerator<'a> { | ||
/// | ||
/// Creates a new TextGenerator with the given font and size in pixels per em. | ||
/// The index indicates the specific font in a font collection. Set to 0 if unsure. | ||
/// | ||
pub fn new(font_bytes: &'a [u8], font_index: u32, size: f32) -> Result<Self, RendererError> { | ||
let font = FontRef::from_index(font_bytes, font_index as usize) | ||
.ok_or(RendererError::MissingFont(font_index))?; | ||
let mut context = ScaleContext::new(); | ||
let mut scaler = context.builder(font).size(size).build(); | ||
let mut map = HashMap::new(); | ||
let mut line_height: f32 = 0.0; | ||
font.charmap().enumerate(|_, id| { | ||
if let Some(outline) = scaler.scale_outline(id) { | ||
let mut builder = Path::builder(); | ||
for command in outline.path().commands() { | ||
match command { | ||
Command::MoveTo(p) => { | ||
builder.begin(Point::new(p.x, p.y)); | ||
} | ||
Command::LineTo(p) => { | ||
builder.line_to(Point::new(p.x, p.y)); | ||
} | ||
Command::CurveTo(p1, p2, p3) => { | ||
builder.cubic_bezier_to( | ||
Point::new(p1.x, p1.y), | ||
Point::new(p2.x, p2.y), | ||
Point::new(p3.x, p3.y), | ||
); | ||
} | ||
Command::QuadTo(p1, p2) => { | ||
builder.quadratic_bezier_to( | ||
Point::new(p1.x, p1.y), | ||
Point::new(p2.x, p2.y), | ||
); | ||
} | ||
Command::Close => builder.close(), | ||
} | ||
} | ||
let path = builder.build(); | ||
|
||
let mut tessellator = FillTessellator::new(); | ||
let mut geometry: VertexBuffers<Vec3, u32> = VertexBuffers::new(); | ||
let options = FillOptions::default(); | ||
if tessellator | ||
.tessellate_path( | ||
&path, | ||
&options, | ||
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| { | ||
vec3(vertex.position().x, vertex.position().y, 0.0) | ||
}), | ||
) | ||
.is_ok() | ||
{ | ||
let mesh = CpuMesh { | ||
positions: Positions::F32(geometry.vertices), | ||
indices: Indices::U32(geometry.indices), | ||
..Default::default() | ||
}; | ||
line_height = line_height.max(mesh.compute_aabb().size().y); | ||
map.insert(id, mesh); | ||
} | ||
} | ||
}); | ||
Ok(Self { | ||
map, | ||
font, | ||
line_height, | ||
size, | ||
}) | ||
} | ||
|
||
/// | ||
/// Generates a [CpuMesh] from the given text string. | ||
/// | ||
pub fn generate(&self, text: &str) -> CpuMesh { | ||
let mut shape_context = ShapeContext::new(); | ||
let mut shaper = shape_context.builder(self.font).size(self.size).build(); | ||
let mut positions = Vec::new(); | ||
let mut indices = Vec::new(); | ||
let mut y = 0.0; | ||
let mut x = 0.0; | ||
|
||
shaper.add_str(text); | ||
shaper.shape_with(|cluster| { | ||
let t = text.get(cluster.source.to_range()); | ||
if matches!(t, Some("\n")) { | ||
// Move to the next line | ||
y -= self.line_height * 1.2; // Add 20% extra space between lines | ||
x = 0.0; | ||
} | ||
for glyph in cluster.glyphs { | ||
let mesh = self.map.get(&glyph.id).unwrap(); | ||
|
||
let index_offset = positions.len() as u32; | ||
let Indices::U32(mesh_indices) = &mesh.indices else { | ||
unreachable!() | ||
}; | ||
indices.extend(mesh_indices.iter().map(|i| i + index_offset)); | ||
|
||
let position = vec3(x + glyph.x, y + glyph.y, 0.0); | ||
let Positions::F32(mesh_positions) = &mesh.positions else { | ||
unreachable!() | ||
}; | ||
positions.extend(mesh_positions.iter().map(|p| p + position)); | ||
} | ||
x += cluster.advance(); | ||
}); | ||
|
||
CpuMesh { | ||
positions: Positions::F32(positions), | ||
indices: Indices::U32(indices), | ||
..Default::default() | ||
} | ||
} | ||
} |