diff --git a/build-utilities/src/lib.rs b/build-utilities/src/lib.rs index acf766cd3d..9bc5f7f456 100644 --- a/build-utilities/src/lib.rs +++ b/build-utilities/src/lib.rs @@ -3,32 +3,34 @@ use std::{fs, path}; use std::io::ErrorKind; -/// Download the release package from github -/// -/// The project_url should be a project's main page on github. -pub fn github_download( - project_url : &str, - version : &str, - filename : &str, - destination_dir : &path::Path -) { - let url = format!( - "{project}/releases/download/{version}/{filename}", - project = project_url, - version = version, - filename = filename - ); +/// A structure describing a concrete release package on github. +pub struct GithubRelease> { + pub project_url : Str, + pub version : Str, + pub filename : Str, +} - let destination_file = path::Path::new(destination_dir) - .join(filename); +impl> GithubRelease { + /// Download the release package from github + /// + /// The project_url should be a project's main page on github. + pub fn download(&self, destination_dir:&path::Path) { + let url = format!( + "{project}/releases/download/{version}/{filename}", + project = self.project_url.as_ref(), + version = self.version.as_ref(), + filename = self.filename.as_ref()); + let destination_dir_str = destination_dir.to_str().unwrap(); + let destination_file = destination_dir.join(self.filename.as_ref()); - let rm_error = fs::remove_file(&destination_file).err(); - let fatal_rm_error = - rm_error.filter(|err| err.kind() != ErrorKind::NotFound); - fatal_rm_error.unwrap_none(); + Self::remove_old_file(&destination_file); + download_lp::download(url.as_str(),destination_dir_str).unwrap(); + } - download_lp::download( - url.as_str(), - destination_dir.to_str().unwrap() - ).unwrap(); + fn remove_old_file(file:&path::Path) { + let result = fs::remove_file(&file); + let error = result.err(); + let fatal_error = error.filter(|err| err.kind() != ErrorKind::NotFound); + fatal_error.unwrap_none(); + } } diff --git a/examples/03-text/.gitignore b/examples/03-text/.gitignore index f06235c460..16acd49d7f 100644 --- a/examples/03-text/.gitignore +++ b/examples/03-text/.gitignore @@ -1,2 +1,3 @@ node_modules dist +package-lock.json diff --git a/lib/core/Cargo.toml b/lib/core/Cargo.toml index 35364b554e..2f3f67ce0f 100644 --- a/lib/core/Cargo.toml +++ b/lib/core/Cargo.toml @@ -26,7 +26,7 @@ failure = { version = "0.1.5" } derive_more = { version = "0.15.0" } shrinkwraprs = { version = "0.2.1" } itertools = { version = "0.8" } -nalgebra = { version = "0.18.1" } +nalgebra = { version = "0.19.0" } bit_field = { version = "0.10.0" } paste = { version = "0.1.6" } enum_dispatch = { version = "= 0.1.3" } # https://gitlab.com/antonok/enum_dispatch/issues/10 @@ -34,7 +34,6 @@ typenum = { version = "1.11.2" } rustc-hash = { version = "1.0.1" } console_error_panic_hook = { version = "0.1.6" } num_enum = { version = "0.4.2" } -smallvec = { version = "1.0.0" } [dependencies.web-sys] version = "0.3.4" diff --git a/lib/core/embedded-fonts/build.rs b/lib/core/embedded-fonts/build.rs index 16f0f99676..329949f320 100644 --- a/lib/core/embedded-fonts/build.rs +++ b/lib/core/embedded-fonts/build.rs @@ -11,19 +11,17 @@ pub struct FillMapRsFile { } impl FillMapRsFile { - fn create>(path : P) -> io::Result { + fn create>(path:P) -> io::Result { let mut file = fs::File::create(path)?; writeln!(file, "{{")?; Ok(FillMapRsFile{ file }) } - fn add_font_inserting_line(&mut self, font_name : &str, font_file : &str) - -> io::Result<()> { - writeln!( - self.file, - " fonts_by_name.insert(\"{}\", include_bytes!(\"{}\"));", - font_name, - font_file + fn add_font_inserting_line(&mut self, font_name:&str, font_file:&str) -> io::Result<()> { + writeln!(self.file, + " font_data_by_name.insert(\"{font_name}\", include_bytes!(\"{font_file}\"));", + font_name = font_name, + font_file = font_file ) } @@ -37,38 +35,34 @@ impl FillMapRsFile { // ==================== mod deja_vu { - use std::path; - use basegl_build_utilities::github_download; use crate::FillMapRsFile; - pub const PACKAGE_NAME : &str = "dejavu-fonts-ttf-2.37.zip"; - pub const PACKAGE_VERSION : &str = "version_2_37"; - pub const PROJECT_URL : &str = - "https://github.com/dejavu-fonts/dejavu-fonts/"; + use std::path; + use basegl_build_utilities::GithubRelease; + + pub const PACKAGE : GithubRelease<&str> = GithubRelease { + project_url : "https://github.com/dejavu-fonts/dejavu-fonts/", + version : "version_2_37", + filename : "dejavu-fonts-ttf-2.37.zip" + }; - pub const PACKAGE_FONTS_PREFIX: &str = "dejavu-fonts-ttf-2.37/ttf"; + pub const PACKAGE_FONTS_PREFIX : &str = "dejavu-fonts-ttf-2.37/ttf"; - pub fn font_file_from_font_name(font_name : &str) -> String { + pub fn font_file_from_font_name(font_name:&str) -> String { return format!("{}.ttf", font_name); } - pub fn extract_font(package_path : &path::Path, font_name : &str) { - let font_file = font_file_from_font_name(font_name); - let font_package_path = format!("{}/{}", - PACKAGE_FONTS_PREFIX, - font_file - ); - - let mut archive = zip::ZipArchive::new( - std::fs::File::open(package_path).unwrap() - ).unwrap(); - let mut input = archive.by_name( - font_package_path.as_str() - ).unwrap(); - let mut output = std::fs::File::create( - package_path.parent().unwrap().join(font_file) - ).unwrap(); - std::io::copy(&mut input, &mut output).unwrap(); + pub fn extract_font(package_path:&path::Path, font_name:&str) { + let font_file = font_file_from_font_name(font_name); + let font_in_package_path = format!("{}/{}",PACKAGE_FONTS_PREFIX,font_file); + let package_dir = package_path.parent().unwrap(); + let output_path = package_dir.join(font_file); + + let archive_file = std::fs::File::open(package_path).unwrap(); + let mut archive = zip::ZipArchive::new(archive_file).unwrap(); + let mut input_stream = archive.by_name(font_in_package_path.as_str()).unwrap(); + let mut output_stream = std::fs::File::create(output_path).unwrap(); + std::io::copy(&mut input_stream, &mut output_stream).unwrap(); } pub const FONTS_TO_EXTRACT : &[&str] = &[ @@ -79,8 +73,7 @@ mod deja_vu { "DejaVuSansMono-Oblique", "DejaVuSansCondensed", "DejaVuSerif", - "DejaVuSerifCondensed", - ]; + "DejaVuSerifCondensed" ]; pub fn extract_all_fonts(package_path : &path::Path) { for font_name in FONTS_TO_EXTRACT { @@ -89,35 +82,27 @@ mod deja_vu { } pub fn download_and_extract_all_fonts(out_dir : &path::Path) { - github_download( - PROJECT_URL, - PACKAGE_VERSION, - PACKAGE_NAME, - &out_dir - ); - - let package_path = out_dir.join(PACKAGE_NAME); + let package_path = out_dir.join(PACKAGE.filename); + + PACKAGE.download(&out_dir); extract_all_fonts(package_path.as_path()); } - pub fn add_entries_to_fill_map_rs(file : &mut FillMapRsFile) { + pub fn add_entries_to_fill_map_rs(file:&mut FillMapRsFile) { for font_name in FONTS_TO_EXTRACT { let font_file = font_file_from_font_name(font_name); - file.add_font_inserting_line( - font_name, - font_file.as_str() - ).unwrap(); + + file.add_font_inserting_line(font_name,font_file.as_str()).unwrap(); } } } fn main() { - let out = env::var("OUT_DIR").unwrap(); - let out_dir = path::Path::new(&out); - let fill_map_rs_path = out_dir.join("fill_map.rs"); + let out = env::var("OUT_DIR").unwrap(); + let out_dir = path::Path::new(&out); + let fill_map_rs_path = out_dir.join("fill_map.rs"); - let mut fill_map_rs_file = - FillMapRsFile::create(fill_map_rs_path).unwrap(); + let mut fill_map_rs_file = FillMapRsFile::create(fill_map_rs_path).unwrap(); deja_vu::download_and_extract_all_fonts(out_dir); deja_vu::add_entries_to_fill_map_rs(&mut fill_map_rs_file); diff --git a/lib/core/embedded-fonts/src/lib.rs b/lib/core/embedded-fonts/src/lib.rs index 361fa8f076..dcd8529485 100644 --- a/lib/core/embedded-fonts/src/lib.rs +++ b/lib/core/embedded-fonts/src/lib.rs @@ -7,7 +7,7 @@ use basegl_prelude::*; /// /// For list of embedded fonts, see FONTS_TO_EXTRACT constant in `build.rs` pub struct EmbeddedFonts { - pub font_data_by_name: HashMap<&'static str, &'static [u8]> + pub font_data_by_name: HashMap<&'static str,&'static [u8]> } impl EmbeddedFonts { @@ -16,23 +16,21 @@ impl EmbeddedFonts { /// For list of embedded fonts, see `FONTS_TO_EXTRACT` constant in /// `build.rs` pub fn create_and_fill() -> EmbeddedFonts { - let mut fonts_by_name : HashMap<&'static str, &'static [u8]> - = HashMap::new(); + let mut font_data_by_name = HashMap::<&'static str,&'static [u8]>::new(); include!(concat!(env!("OUT_DIR"), "/fill_map.rs")); - EmbeddedFonts { - font_data_by_name: fonts_by_name - } + EmbeddedFonts{font_data_by_name} } } #[cfg(test)] mod test { - use crate::EmbeddedFonts; + use crate::*; #[test] fn loading_embedded_fonts() { - let fonts = EmbeddedFonts::create_and_fill(); + let fonts = EmbeddedFonts::create_and_fill(); let example_font = fonts.font_data_by_name.get("DejaVuSans").unwrap(); + assert_eq!(0x00, example_font[0]); assert_eq!(0x01, example_font[1]); assert_eq!(0x1d, example_font[example_font.len()-1]); diff --git a/lib/core/msdf-sys/Cargo.toml b/lib/core/msdf-sys/Cargo.toml index cae4f95639..59f13131f3 100644 --- a/lib/core/msdf-sys/Cargo.toml +++ b/lib/core/msdf-sys/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] wasm-bindgen = "0.2.53" js-sys = "0.3.30" -vector2d = "2.2.0" +nalgebra = "0.19.0" basegl-prelude = { version = "0.1.0", path="../../prelude" } [dev-dependencies] diff --git a/lib/core/msdf-sys/build.rs b/lib/core/msdf-sys/build.rs index 0c1e2b9fd0..674a0be888 100644 --- a/lib/core/msdf-sys/build.rs +++ b/lib/core/msdf-sys/build.rs @@ -1,21 +1,20 @@ -use basegl_build_utilities::github_download; mod msdfgen_wasm { - use crate::github_download; - use std::{path, fs}; + use basegl_build_utilities::GithubRelease; + + use std::{path,fs}; use std::io::Write; - pub const VERSION : &str = "v1.0.1"; - pub const FILENAME : &str = "msdfgen_wasm.js"; - pub const PROJECT_URL : &str = "https://github.com/luna/msdfgen-wasm"; + pub const PACKAGE : GithubRelease<&str> = GithubRelease { + project_url : "https://github.com/luna/msdfgen-wasm", + version : "v1.1", + filename : "msdfgen_wasm.js" + }; + + pub const FILENAME : &str = PACKAGE.filename; pub fn download() { - github_download( - PROJECT_URL, - VERSION, - FILENAME, - path::Path::new(".") // Note [Downloading to src dir] - ) + PACKAGE.download(path::Path::new(".")) // Note [Downloading to src dir] } /* Note [Downloading to src dir] @@ -30,8 +29,10 @@ mod msdfgen_wasm { * remember to remove msdfgen_wasm.js entry from .gitignore */ - const PATCH_LINE : &str = "; export { ccall, getValue, \ - _msdfgen_maxMSDFSize, _msdfgen_generateMSDF, _msdfgen_freeFont, \ + const PATCH_LINE : &str = "; export { ccall, getValue, _msdfgen_getKerning,\ + _msdfgen_generateAutoframedMSDF, _msdfgen_result_getMSDFData,\ + _msdfgen_result_getAdvance, _msdfgen_result_getTranslation,\ + _msdfgen_result_getScale, _msdfgen_freeResult, _msdfgen_freeFont,\ addInitializationCb, isInitialized }"; /// Patches downloaded msdfgen_wasm.js file @@ -40,7 +41,11 @@ mod msdfgen_wasm { /// be explicitly exported. Examples works without this line perfectly. pub fn patch_for_wasm_bindgen_test() { let path = path::Path::new(FILENAME); - let mut file = fs::OpenOptions::new().append(true).open(path).unwrap(); + + let mut open_options = fs::OpenOptions::new(); + open_options.append(true); + + let mut file = open_options.open(path).unwrap(); file.write(PATCH_LINE.as_bytes()).unwrap(); } } diff --git a/lib/core/msdf-sys/src/emscripten_data.rs b/lib/core/msdf-sys/src/emscripten_data.rs new file mode 100644 index 0000000000..99f14c460e --- /dev/null +++ b/lib/core/msdf-sys/src/emscripten_data.rs @@ -0,0 +1,110 @@ +use wasm_bindgen::JsValue; +use crate::internal::emscripten_get_value_from_memory; +use crate::prelude::*; + +// ================================ +// === EmscriptenRepresentation === +// ================================ + +/// Trait of type having its representation in emscripten API +/// +/// The _emscirpten API_ is a set of functions that are put to library by emscripten SDK, the +/// especially useful is a function reading value from given address in `msdfgen` library memory +/// (we cannot do it directly, because each wasm module have separate address space) +pub trait EmscriptenRepresentation : Sized { + const EMSCRIPTEN_SIZE_IN_BYTES : usize; + const EMSCRIPTEN_TYPE_NAME : &'static str; + + /// Convert from JsValue returned from emscripten API + fn from_js_value(js_value : JsValue) -> Option; + + /// Read value from address in `msdfgen` library memory + fn read_from_emscripten_memory(address : usize) -> Option { + let js_value = emscripten_get_value_from_memory(address,Self::EMSCRIPTEN_TYPE_NAME); + Self::from_js_value(js_value) + } +} + +impl EmscriptenRepresentation for f32 { + const EMSCRIPTEN_SIZE_IN_BYTES : usize = 4; + const EMSCRIPTEN_TYPE_NAME : &'static str = "float"; + + fn from_js_value(js_value: JsValue) -> Option { + js_value.as_f64().map(|f| f as f32) + } +} + +impl EmscriptenRepresentation for f64 { + const EMSCRIPTEN_SIZE_IN_BYTES : usize = 8; + const EMSCRIPTEN_TYPE_NAME : &'static str = "double"; + + fn from_js_value(js_value: JsValue) -> Option { + js_value.as_f64() + } +} + +// ======================= +// === ArrayMemoryView === +// ======================= + +/// View of array in `msdfgen` library memory +pub struct ArrayMemoryView { + begin_address : usize, + end_address : usize, + type_marker : std::marker::PhantomData +} + +/// Iterator over values in `msdfgen` library memory +/// +/// It cannot outlives view from which was created, because one might expect, that data may be freed +/// by library once view is destroyed +pub struct ArrayMemoryViewIterator<'a, F : EmscriptenRepresentation> { + next_read_address : usize, + end_address : usize, + view_lifetime : std::marker::PhantomData<&'a ArrayMemoryView> +} + +impl ArrayMemoryView { + + /// Create view from first element's address and array size + pub fn new(address : usize, size : usize) -> ArrayMemoryView { + let size_in_bytes = size * F::EMSCRIPTEN_SIZE_IN_BYTES; + ArrayMemoryView { + begin_address : address, + end_address : address + size_in_bytes, + type_marker : std::marker::PhantomData + } + } + + /// Create an empty view + pub fn empty() -> ArrayMemoryView { + ArrayMemoryView { + begin_address : 0, + end_address : 0, + type_marker : std::marker::PhantomData + } + } + + /// Iterator over elements + pub fn iter(&self) -> ArrayMemoryViewIterator { + ArrayMemoryViewIterator { + next_read_address : self.begin_address, + end_address : self.end_address, + view_lifetime : std::marker::PhantomData + } + } +} + +impl<'a, F : EmscriptenRepresentation> +Iterator for ArrayMemoryViewIterator<'a, F> { + type Item = F; + + fn next(&mut self) -> Option { + let has_element = self.next_read_address < self.end_address; + has_element.and_option_from(|| { + let current_value = F::read_from_emscripten_memory(self.next_read_address).unwrap(); + self.next_read_address += F::EMSCRIPTEN_SIZE_IN_BYTES; + Some(current_value) + }) + } +} diff --git a/lib/core/msdf-sys/src/internal.rs b/lib/core/msdf-sys/src/internal.rs index c6727ecfd7..42897cc3a7 100644 --- a/lib/core/msdf-sys/src/internal.rs +++ b/lib/core/msdf-sys/src/internal.rs @@ -1,113 +1,61 @@ use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; -use crate::prelude::*; #[wasm_bindgen(module = "/msdfgen_wasm.js")] extern { #[wasm_bindgen(js_name="addInitializationCb")] - pub fn on_emscripten_runtime_initialized(callback : JsValue) - -> js_sys::Promise; + pub fn on_emscripten_runtime_initialized(callback:JsValue) -> js_sys::Promise; #[wasm_bindgen(js_name="isInitialized")] pub fn is_emscripten_runtime_initialized() -> bool; #[wasm_bindgen(js_name="ccall")] - pub fn emscripten_call_function( - name : &str, - return_type : &str, - types : js_sys::Array, - values : js_sys::Array + pub fn emscripten_call_function + ( name : &str + , return_type : &str + , types : js_sys::Array + , values : js_sys::Array ) -> JsValue; #[wasm_bindgen(js_name="getValue")] - pub fn emscripten_get_value_from_memory( - address: usize, - a_type: &str + pub fn emscripten_get_value_from_memory(address:usize, a_type:&str) -> JsValue; + + #[wasm_bindgen(js_name="_msdfgen_getKerning")] + pub fn msdfgen_get_kerning(font_handle:JsValue, left_unicode:u32, right_unicode:u32) -> f64; + + #[wasm_bindgen(js_name="_msdfgen_generateAutoframedMSDF")] + pub fn msdfgen_generate_msdf + ( width : usize + , height : usize + , font_handle : JsValue + , unicode : u32 + , edge_coloring_angle_threshold : f64 + , range : f64 + , max_scale : f64 + , edge_threshold : f64 + , overlap_support : bool ) -> JsValue; - #[wasm_bindgen(js_name="_msdfgen_maxMSDFSize")] - pub fn msdfgen_max_msdf_size() -> usize; + #[wasm_bindgen(js_name="_msdfgen_result_getMSDFData")] + pub fn msdfgen_result_get_msdf_data(result_handle:JsValue) -> usize; - #[wasm_bindgen(js_name="_msdfgen_generateMSDF")] - pub fn msdfgen_generate_msdf( - width : usize, - height : usize, - font_handle : JsValue, - unicode : u32, - edge_coloring_angle_threshold : f64, - range : f64, - scale_x : f64, - scale_y : f64, - translate_x : f64, - translate_y : f64, - edge_threshold : f64, - overlap_support : bool - ) -> usize; + #[wasm_bindgen(js_name="_msdfgen_result_getAdvance")] + pub fn msdfgen_result_get_advance(result_handle:JsValue) -> f64; - #[wasm_bindgen(js_name="_msdfgen_freeFont")] - pub fn msdfgen_free_font(font_handle: JsValue); -} + #[wasm_bindgen(js_name="_msdfgen_result_getTranslation")] + pub fn msdfgen_result_get_translation(result_handle:JsValue) -> usize; -pub mod emscripten_data_types { - pub const FLOAT_SIZE_IN_BYTES : usize = 4; + #[wasm_bindgen(js_name="_msdfgen_result_getScale")] + pub fn msdfgen_result_get_scale(result_handle:JsValue) -> usize; - pub const ARRAY : &str = "array"; - pub const NUMBER : &str = "number"; - pub const FLOAT : &str = "float"; -} + #[wasm_bindgen(js_name="_msdfgen_freeResult")] + pub fn msdfgen_free_result(result_handle:JsValue); -// ========================== -// === F32ArrayMemoryView === -// ========================== - -pub struct F32ArrayMemoryView { - begin_address : usize, - end_address : usize -} - -pub struct F32ArrayMemoryViewIterator { - next_read_address : usize, - end_address : usize -} - -impl F32ArrayMemoryView { - pub fn new(address : usize, size : usize) -> F32ArrayMemoryView { - let size_in_bytes = - size * emscripten_data_types::FLOAT_SIZE_IN_BYTES; - F32ArrayMemoryView { - begin_address : address, - end_address : address + size_in_bytes - } - } - - pub fn iter(&self) -> F32ArrayMemoryViewIterator { - F32ArrayMemoryViewIterator { - next_read_address : self.begin_address, - end_address : self.end_address - } - } + #[wasm_bindgen(js_name="_msdfgen_freeFont")] + pub fn msdfgen_free_font(font_handle:JsValue); } -impl IntoIterator for F32ArrayMemoryView { - type Item = f32; - type IntoIter = F32ArrayMemoryViewIterator; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } +pub mod ccall_types { + pub const ARRAY : &str = "array"; + pub const NUMBER : &str = "number"; } - -impl Iterator for F32ArrayMemoryViewIterator { - type Item = f32; - - fn next(&mut self) -> Option { - let has_element = self.next_read_address < self.end_address; - has_element.and_option_from(|| { - let ret_val = emscripten_get_value_from_memory( - self.next_read_address, - emscripten_data_types::FLOAT); - self.next_read_address += emscripten_data_types::FLOAT_SIZE_IN_BYTES; - Some(ret_val.as_f64().unwrap() as f32) - }) - } -} \ No newline at end of file diff --git a/lib/core/msdf-sys/src/lib.rs b/lib/core/msdf-sys/src/lib.rs index e619844047..117ae700c3 100644 --- a/lib/core/msdf-sys/src/lib.rs +++ b/lib/core/msdf-sys/src/lib.rs @@ -1,20 +1,14 @@ mod internal; +pub mod emscripten_data; pub mod test_utils; + pub use basegl_prelude as prelude; +use internal::*; -use internal::{ - on_emscripten_runtime_initialized, - is_emscripten_runtime_initialized, - emscripten_call_function, - msdfgen_generate_msdf, - msdfgen_free_font, - emscripten_data_types, - F32ArrayMemoryView, -}; +use emscripten_data::ArrayMemoryView; use js_sys::Uint8Array; use wasm_bindgen::JsValue; use wasm_bindgen::prelude::Closure; -pub use vector2d::Vector2D; // ====================== // === Initialization === @@ -24,7 +18,7 @@ pub use vector2d::Vector2D; /// /// The callback passed as argument will be called once the msdfgen libirary /// will be initialized. -pub fn run_once_initialized(callback : F) +pub fn run_once_initialized(callback:F) where F : 'static + FnOnce() { if is_emscripten_runtime_initialized() { callback() @@ -47,22 +41,33 @@ impl Font { /// /// Loads font from a any format which freetype library can handle. /// See [https://www.freetype.org/freetype2/docs/index.html] for reference. - pub fn load_from_memory(data: &[u8]) -> Self { - let param_types = js_sys::Array::of2( - &JsValue::from_str(emscripten_data_types::ARRAY), - &JsValue::from_str(emscripten_data_types::NUMBER) - ); - let params = js_sys::Array::of2( - &JsValue::from(Uint8Array::from(data)), - &JsValue::from_f64(data.len() as f64) - ); - let handle = emscripten_call_function( - "msdfgen_loadFontMemory", - emscripten_data_types::NUMBER, - param_types, - params); + pub fn load_from_memory(data:&[u8]) -> Self { + let array_type_js = JsValue::from_str(ccall_types::ARRAY); + let number_type_js = JsValue::from_str(ccall_types::NUMBER); + let data_js_array = Uint8Array::from(data); + let data_js = JsValue::from(data_js_array); + let data_size_js = JsValue::from_f64(data.len() as f64); + + let function_name = "msdfgen_loadFontMemory"; + let return_type = ccall_types::NUMBER; + let param_types = js_sys::Array::of2(&array_type_js,&number_type_js); + let params = js_sys::Array::of2(&data_js,&data_size_js); + + let handle = emscripten_call_function(function_name,return_type,param_types,params); Font { handle } } + + pub fn retrieve_kerning(&self, left:char, right:char) -> f64 { + let left_unicode = left as u32; + let right_unicode = right as u32; + msdfgen_get_kerning(self.handle.clone(),left_unicode,right_unicode) + } + + pub fn mock_font() -> Font { + Font { + handle : JsValue::from_f64(0.0) + } + } } impl Drop for Font { @@ -79,64 +84,102 @@ impl Drop for Font { /// /// The structure gathering MSDF generation parameters meant to be same for all /// rendered glyphs -pub struct MSDFParameters { +pub struct MsdfParameters { pub width : usize, pub height : usize, pub edge_coloring_angle_threshold : f64, pub range : f64, + pub max_scale : f64, pub edge_threshold : f64, pub overlap_support : bool } -pub const MAX_MSDF_SIZE : usize = 64; -pub const MSDF_CHANNELS_COUNT : usize = 3; - -///// Generate Mutlichannel Signed Distance Field (MSDF) for one glyph -///// -///// For more information about MSDF see [https://github.com/Chlumsky/msdfgen]. -pub fn generate_msdf>( - output : &mut Output, - font : &Font, - unicode : u32, - params : &MSDFParameters, - scale : Vector2D, - translate : Vector2D, -) { - assert!(params.width <= MAX_MSDF_SIZE); - assert!(params.height <= MAX_MSDF_SIZE); - - let output_size = params.width * params.height * MSDF_CHANNELS_COUNT; - let output_address = msdfgen_generate_msdf( - params.width, - params.height, - font.handle.clone(), - unicode, - params.edge_coloring_angle_threshold, - params.range, - scale.x, - scale.y, - translate.x, - translate.y, - params.edge_threshold, - params.overlap_support - ); - let view = F32ArrayMemoryView::new(output_address, output_size); - - output.extend(view); // Note [Output variable] +pub struct MultichannelSignedDistanceField { + handle : JsValue, + pub advance : f64, + pub translation : nalgebra::Vector2, + pub scale : nalgebra::Vector2, + pub data : ArrayMemoryView +} + +impl MultichannelSignedDistanceField { + pub const CHANNELS_COUNT : usize = 3; + + /// Generate Mutlichannel Signed Distance Field (MSDF) for one glyph + /// + /// For more information about MSDF see + /// [https://github.com/Chlumsky/msdfgen]. + pub fn generate(font:&Font, unicode:u32, params:&MsdfParameters) + -> MultichannelSignedDistanceField { + let handle = msdfgen_generate_msdf + ( params.width + , params.height + , font.handle.clone() + , unicode + , params.edge_coloring_angle_threshold + , params.range,params.max_scale + , params.edge_threshold + , params.overlap_support + ); + let advance = msdfgen_result_get_advance(handle.clone()); + let translation = Self::translation(&handle); + let scale = Self::scale(&handle); + let data_adress = msdfgen_result_get_msdf_data(handle.clone()); + let data_size = params.width * params.height * Self::CHANNELS_COUNT; + let data = ArrayMemoryView::new(data_adress,data_size); + MultichannelSignedDistanceField{handle,advance,translation,scale,data} + } + + const DIMENSIONS: usize = 2; + + fn translation(handle : &JsValue) -> nalgebra::Vector2 { + let address = msdfgen_result_get_translation(handle.clone()); + let view = ArrayMemoryView::new(address,Self::DIMENSIONS); + let mut iter = view.iter(); + let translate_x = iter.next().unwrap(); + let translate_y = iter.next().unwrap(); + nalgebra::Vector2::new(translate_x,translate_y) + } + + fn scale(handle : &JsValue) -> nalgebra::Vector2 { + let address = msdfgen_result_get_scale(handle.clone()); + let view = ArrayMemoryView::new(address,Self::DIMENSIONS); + let mut iter = view.iter(); + let scale_x = iter.next().unwrap(); + let scale_y = iter.next().unwrap(); + nalgebra::Vector2::new(scale_x, scale_y) + } + + pub fn mock_results() -> MultichannelSignedDistanceField { + MultichannelSignedDistanceField { + handle : JsValue::from_f64(0.0), + advance : 0.0, + translation : nalgebra::Vector2::new(0.0, 0.0), + scale : nalgebra::Vector2::new(1.0, 1.0), + data : ArrayMemoryView::empty() + } + } +} + +impl Drop for MultichannelSignedDistanceField { + fn drop(&mut self) { + msdfgen_free_result(self.handle.clone()); + } } + // ============= // === Tests === // ============= #[cfg(test)] mod tests { - use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - use internal::msdfgen_max_msdf_size; use crate::*; + use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; use basegl_core_embedded_fonts::EmbeddedFonts; use std::future::Future; use test_utils::TestAfterInit; + use nalgebra::Vector2; wasm_bindgen_test_configure!(run_in_browser); @@ -148,29 +191,29 @@ mod tests { let font = Font::load_from_memory( font_base.font_data_by_name.get("DejaVuSansMono-Bold").unwrap() ); - let params = MSDFParameters { - width: 32, - height: 32, - edge_coloring_angle_threshold: 3.0, - range: 2.0, - edge_threshold: 1.001, - overlap_support: true + let params = MsdfParameters { + width : 32, + height : 32, + edge_coloring_angle_threshold : 3.0, + range : 2.0, + max_scale : 2.0, + edge_threshold : 1.001, + overlap_support : true }; // when - let mut msdf = Vec::::new(); - generate_msdf( - &mut msdf, + let msdf = MultichannelSignedDistanceField::generate( &font, 'A' as u32, ¶ms, - Vector2D { x: 1.0, y: 1.0 }, - Vector2D { x: 0.0, y: 0.0 } ); // then - // Note [asserts] - assert_eq!(0.42730755, msdf[0]); - assert_eq!(0.75, msdf[10]); - assert_eq!(-9.759168, msdf[msdf.len()-1]); + let data : Vec = msdf.data.iter().collect(); + assert_eq!(-0.9408906 , data[0]); // Note [asserts] + assert_eq!(0.2 , data[10]); + assert_eq!(-4.3035655 , data[data.len()-1]); + assert_eq!(Vector2::new(3.03125, 1.0), msdf.translation); + assert_eq!(Vector2::new(1.25, 1.25) , msdf.scale); + assert_eq!(19.265625 , msdf.advance); }) } @@ -179,11 +222,4 @@ mod tests { * we're checking rust - js interface only, so there is no need to check * all values */ - - #[wasm_bindgen_test(async)] - fn msdf_data_limits() -> impl Future { - TestAfterInit::schedule(|| { - assert!(MAX_MSDF_SIZE <= msdfgen_max_msdf_size()); - }) - } } diff --git a/lib/core/msdf-sys/src/test_utils.rs b/lib/core/msdf-sys/src/test_utils.rs index 8419d669ee..4da7e3c4db 100644 --- a/lib/core/msdf-sys/src/test_utils.rs +++ b/lib/core/msdf-sys/src/test_utils.rs @@ -4,23 +4,21 @@ use std::future::Future; use crate::{ is_emscripten_runtime_initialized, run_once_initialized }; /// The future for running test after initialization -pub struct TestAfterInit { +pub struct TestAfterInit { test : F } -impl TestAfterInit { - pub fn schedule(test : F) -> TestAfterInit { - TestAfterInit { test } +impl TestAfterInit { + pub fn schedule(test:F) -> TestAfterInit { + TestAfterInit{test} } } -impl Future for TestAfterInit { +impl Future for TestAfterInit { type Output = (); - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) - -> Poll { - + fn poll(self:Pin<&mut Self>, cx:&mut Context<'_>) -> Poll { if is_emscripten_runtime_initialized() { (self.test)(); Poll::Ready(()) diff --git a/lib/core/src/data/container.rs b/lib/core/src/data/container.rs index fea9d21128..3dd6d63f67 100644 --- a/lib/core/src/data/container.rs +++ b/lib/core/src/data/container.rs @@ -11,3 +11,73 @@ pub trait Add { } pub type AddResult = >::Result; + +// ======================= +// === CachingIterator === +// ======================= + +/// Iterator wrapper caching the last retrieved value +/// +/// The item type is `(Option, T)` where the second tuple element is +/// a current value and first element is a previous one `None` on the first +/// iteration. +pub struct CachingIterator> { + last : Option, + iter : It +} + +impl> Iterator for CachingIterator { + type Item = (Option, T); + + fn next(&mut self) -> Option { + self.iter.next().map(|value| { + let new_last = Some(value.clone()); + let old_last = std::mem::replace(&mut self.last, new_last); + (old_last, value) + }) + } +} + +/// A trait for wrapping in caching iterator +/// +/// It is implemented for each iterator over cloneable items. +pub trait IntoCachingIterator { + type Item : Clone; + type Iter : Iterator; + + fn cache_last_value(self) -> CachingIterator; +} + +impl> IntoCachingIterator for It { + type Item = T; + type Iter = Self; + + fn cache_last_value(self) -> CachingIterator { + CachingIterator { + last : None, + iter : self + } + } +} + +#[cfg(test)] +mod tests { + use crate::data::container::IntoCachingIterator; + + #[test] + fn caching_iterator_on_empty() { + let data = Vec::::new(); + let result = data.iter().cache_last_value().next(); + assert_eq!(None, result); + } + + #[test] + fn caching_iterator() { + let data = vec![2, 3, 5]; + let mut caching_iterator = data.iter().cloned().cache_last_value(); + assert_eq!(Some((None ,2)), caching_iterator.next()); + assert_eq!(Some((Some(2),3)), caching_iterator.next()); + assert_eq!(Some((Some(3),5)), caching_iterator.next()); + assert_eq!(None , caching_iterator.next()); + } +} diff --git a/lib/core/src/data/opt_vec.rs b/lib/core/src/data/opt_vec.rs index 72745063a8..dfbd445238 100644 --- a/lib/core/src/data/opt_vec.rs +++ b/lib/core/src/data/opt_vec.rs @@ -1,5 +1,4 @@ use crate::prelude::*; -use smallvec::SmallVec; use std::iter::FilterMap; use std::slice; diff --git a/lib/core/src/lib.rs b/lib/core/src/lib.rs index 053622eecd..579b61575f 100644 --- a/lib/core/src/lib.rs +++ b/lib/core/src/lib.rs @@ -94,14 +94,28 @@ mod example_01 { // ================== mod example_03 { + use wasm_bindgen::prelude::*; + use crate::utils; - use crate::display::world::{World, WorkspaceID, Workspace, Add}; + use crate::display::world::{World,Workspace,Add}; use crate::text::font::FontRenderInfo; use crate::Color; + use crate::dirty::traits::SharedSetter1; use basegl_core_embedded_fonts::EmbeddedFonts; use itertools::iproduct; - use wasm_bindgen::prelude::*; + + const FONT_NAMES : &[&str] = & + [ "DejaVuSans" + , "DejaVuSansMono" + , "DejaVuSansMono-Bold" + , "DejaVuSansMono-Oblique" + , "DejaVuSansCondensed" + , "DejaVuSerif" + , "DejaVuSerifCondensed" + ]; + + const SIZES : &[f64] = &[0.016, 0.024, 0.032, 0.048, 0.064]; #[wasm_bindgen] #[allow(dead_code)] @@ -109,29 +123,19 @@ mod example_03 { utils::set_panic_hook(); basegl_core_msdf_sys::run_once_initialized(|| { let mut world_ref = World::new(); - let workspace_id : WorkspaceID = world_ref.add(Workspace::build("canvas")); - - let world = &mut world_ref.borrow_mut(); - let workspace = &mut world[workspace_id]; - - let font_base = EmbeddedFonts::create_and_fill(); - let font_names = [ - "DejaVuSans", - "DejaVuSansMono", - "DejaVuSansMono-Bold", - "DejaVuSansMono-Oblique", - "DejaVuSansCondensed", - "DejaVuSerif", - "DejaVuSerifCondensed", - ]; - let mut fonts : Box<[FontRenderInfo]> = font_names.iter().map( - |name| FontRenderInfo::from_embedded(&font_base, name) - ).collect(); - let sizes = [0.024, 0.032, 0.048, 0.064]; - - for (i, (font, size)) in - iproduct!(0..fonts.len(), sizes.iter()).enumerate() { + let workspace_id = world_ref.add(Workspace::build("canvas")); + let world = &mut world_ref.borrow_mut(); + let workspace = &mut world[workspace_id]; + let font_base = EmbeddedFonts::create_and_fill(); + let font_creator = |name:&&'static str| FontRenderInfo::from_embedded(&font_base,name); + let fonts_iter = FONT_NAMES.iter().map(font_creator); + let mut fonts = fonts_iter.collect::>(); + + let all_cases = iproduct!(0..fonts.len(), SIZES.iter()); + + for (i, (font, size)) in all_cases.enumerate() { + let line_position = nalgebra::Vector2::new(-0.95, 0.9 - 0.064*(i as f64)); let text_compnent = crate::text::TextComponentBuilder { text : "To be, or not to be, that is the question: \ Whether 'tis nobler in the mind to suffer \ @@ -139,12 +143,10 @@ mod example_03 { Or to take arms against a sea of troubles \ And by opposing end them." .to_string(), - font : &mut fonts[font], - x : -0.95, - y : 0.9 - 0.064*(i as f32), - size : *size, - color : Color {r: 1.0, g: 1.0, b: 1.0, a: 1.0}, - background_color : Color {r: 0.0, g: 0.0, b: 0.0, a: 1.0} + font : &mut fonts[font], + position : line_position, + size : *size, + color : Color {r: 1.0, g: 1.0, b: 1.0, a: 1.0}, }.build(workspace); workspace.text_components.push(text_compnent); } @@ -153,8 +155,6 @@ mod example_03 { } } -//////////////////////////////////////////////// -//////////////////////////////////////////////// // ================= // === Utilities === diff --git a/lib/core/src/text.rs b/lib/core/src/text.rs new file mode 100644 index 0000000000..64d881ce74 --- /dev/null +++ b/lib/core/src/text.rs @@ -0,0 +1,238 @@ +pub mod font; +pub mod glyph_render; +pub mod msdf; + +use crate::prelude::*; + +use crate::Color; +use crate::display::world::Workspace; +use crate::text::glyph_render::{GylphSquareVerticesBuilder, GlyphSquareTextureCoordinatesBuilder}; +use crate::text::msdf::MsdfTexture; + +use font::FontRenderInfo; +use basegl_backend_webgl::{Context,compile_shader,link_program,Program,Shader}; +use js_sys::Float32Array; +use nalgebra::{Vector2,Similarity2,Transform2}; +use web_sys::{WebGlRenderingContext,WebGlBuffer,WebGlTexture}; + +pub struct TextComponentBuilder<'a> { + pub text : String, + pub font : &'a mut FontRenderInfo, + pub position : Vector2, + pub size : f64, + pub color : Color, +} + +#[derive(Debug)] +pub struct TextComponent { + gl_context : WebGlRenderingContext, + gl_program : Program, + gl_vertex_buffer : WebGlBuffer, + gl_texture_coordinates_buffer : WebGlBuffer, + gl_msdf_texture : WebGlTexture, + buffers_size : usize, +} + +impl<'a> TextComponentBuilder<'a> { + pub fn build(mut self, workspace : &Workspace) -> TextComponent { + self.load_all_chars(); + let gl_context = workspace.context.clone(); + let gl_program = self.create_program(&gl_context); + let gl_vertex_buffer = self.create_vertex_bufffer(&gl_context); + let gl_tex_coord_buffer = self.create_texture_coordinates_buffer(&gl_context); + let gl_msdf_texture = self.create_msdf_texture(&gl_context); + let glyph_vertices_count = glyph_render::GLYPH_SQUARE_VERTICES_BASE_LAYOUT.len(); + let buffers_size = self.text.len() * glyph_vertices_count; + self.setup_uniforms(&gl_context, &gl_program); + TextComponent { + gl_context, + gl_program, + gl_vertex_buffer, + gl_texture_coordinates_buffer : gl_tex_coord_buffer, + gl_msdf_texture, + buffers_size + } + } + + fn load_all_chars(&mut self) { + for ch in self.text.chars() { + self.font.get_glyph_info(ch); + } + } + + fn create_program(&self, gl_context:&Context) -> Program { + gl_context.get_extension("OES_standard_derivatives").unwrap().unwrap(); + let vert_shader = self.create_vertex_shader(gl_context); + let frag_shader = self.create_fragment_shader(gl_context); + link_program(&gl_context, &vert_shader, &frag_shader).unwrap() + } + + fn create_vertex_shader(&self, gl_context:&Context) -> Shader { + let body = include_str!("text/msdf_vert.glsl"); + let shader_type = WebGlRenderingContext::VERTEX_SHADER; + + compile_shader(gl_context,shader_type,body).unwrap() + } + + fn create_fragment_shader(&self, gl_context:&Context) -> Shader { + let body = include_str!("text/msdf_frag.glsl"); + let shader_type = WebGlRenderingContext::FRAGMENT_SHADER; + + compile_shader(gl_context,shader_type,body).unwrap() + } + + fn create_buffer(gl_context:&Context, vertices:&[f32]) -> WebGlBuffer { + let target = WebGlRenderingContext::ARRAY_BUFFER; + + let buffer = gl_context.create_buffer().unwrap(); + gl_context.bind_buffer(target,Some(&buffer)); + Self::set_bound_buffer_data(gl_context,target,vertices); + buffer + } + + fn set_bound_buffer_data(gl_context:&Context, target:u32, data:&[f32]) { + let usage = WebGlRenderingContext::STATIC_DRAW; + unsafe { // Note [unsafe buffer_data] + let float_32_array = Float32Array::view(&data); + gl_context.buffer_data_with_array_buffer_view(target,&float_32_array,usage); + } + } + + /* Note [unsafe buffer_data] + * + * The Float32Array::view is safe as long there are no allocations done + * until it is destroyed. This way of creating buffers were taken from + * wasm-bindgen examples + * (https://rustwasm.github.io/wasm-bindgen/examples/webgl.html) + */ + + fn create_vertex_bufffer(&mut self, gl_context:&Context) -> WebGlBuffer { + let to_window = self.to_window_transform(); + let font = &mut self.font; + + let mut vertices_builder = GylphSquareVerticesBuilder::new(font,to_window); + let char_to_vertices = |ch| vertices_builder.build_for_next_glyph(ch); + let grouped_vertices = self.text.chars().map(char_to_vertices); + let vertices = grouped_vertices.flatten(); + let buffer_data = vertices.map(|f| f as f32).collect::>(); + Self::create_buffer(gl_context,buffer_data.as_ref()) + } + + fn to_window_transform(&self) -> Transform2 { + const ROTATION : f64 = 0.0; + let similarity = Similarity2::new(self.position,ROTATION,self.size); + nalgebra::convert(similarity) + } + + fn create_texture_coordinates_buffer(&mut self, gl_context:&Context) -> WebGlBuffer { + let font = &mut self.font; + + let mut texture_coordinates_builder = GlyphSquareTextureCoordinatesBuilder::new(font); + let char_to_texture_coordinates = |ch| texture_coordinates_builder.build_for_next_glyph(ch); + let grouped_texture_coordinates = self.text.chars().map(char_to_texture_coordinates); + let texture_coordinates = grouped_texture_coordinates.flatten(); + let converted_data = texture_coordinates.map(|f| f as f32); + let buffer_data = converted_data.collect::>(); + Self::create_buffer(gl_context,buffer_data.as_ref()) + } + + fn create_msdf_texture(&self, gl_ctx:&Context) + -> WebGlTexture { + let msdf_texture = gl_ctx.create_texture().unwrap(); + let target = Context::TEXTURE_2D; + let wrap = Context::CLAMP_TO_EDGE as i32; + let min_filter = Context::LINEAR as i32; + let width = MsdfTexture::WIDTH as i32; + let height = self.font.msdf_texture.rows() as i32; + let border = 0; + let tex_level = 0; + let format = Context::RGB; + let internal_fmt = Context::RGB as i32; + let tex_type = Context::UNSIGNED_BYTE; + let data = Some(self.font.msdf_texture.data.as_slice()); + + gl_ctx.bind_texture(target,Some(&msdf_texture)); + gl_ctx.tex_parameteri(target,Context::TEXTURE_WRAP_S,wrap); + gl_ctx.tex_parameteri(target,Context::TEXTURE_WRAP_T,wrap); + gl_ctx.tex_parameteri(target,Context::TEXTURE_MIN_FILTER,min_filter); + let tex_image_result = + gl_ctx.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array + ( target + , tex_level + , internal_fmt + , width + , height + , border + , format + , tex_type + , data + ); + tex_image_result.unwrap(); + msdf_texture + } + + fn setup_uniforms(&self, gl_context:&Context, gl_program:&Program) { + let color = &self.color; + let range = FontRenderInfo::MSDF_PARAMS.range as f32; + let msdf_width = MsdfTexture::WIDTH as f32; + let msdf_height = self.font.msdf_texture.rows() as f32; + let color_loc = gl_context.get_uniform_location(gl_program,"color"); + let range_loc = gl_context.get_uniform_location(gl_program,"range"); + let msdf_loc = gl_context.get_uniform_location(gl_program,"msdf"); + let msdf_size_loc = gl_context.get_uniform_location(gl_program,"msdfSize"); + + gl_context.use_program(Some(gl_program)); + gl_context.uniform4f(color_loc.as_ref(),color.r,color.g,color.b,color.a); + gl_context.uniform1f(range_loc.as_ref(),range); + gl_context.uniform1i(msdf_loc.as_ref(),0); + gl_context.uniform2f(msdf_size_loc.as_ref(),msdf_width,msdf_height); + } +} + +impl TextComponent { + + pub fn display(&self) { + let gl_context = &self.gl_context; + + gl_context.use_program(Some(&self.gl_program)); + self.bind_buffer_to_attribute("position",&self.gl_vertex_buffer); + self.bind_buffer_to_attribute("texCoord",&self.gl_texture_coordinates_buffer); + self.setup_blending(); + gl_context.bind_texture(Context::TEXTURE_2D, Some(&self.gl_msdf_texture)); + gl_context.draw_arrays(WebGlRenderingContext::TRIANGLES,0,self.buffers_size as i32); + } + + fn bind_buffer_to_attribute(&self, attribute_name:&str, buffer:&WebGlBuffer) { + let gl_context = &self.gl_context; + let gl_program = &self.gl_program; + let location = gl_context.get_attrib_location(gl_program,attribute_name) as u32; + let target = WebGlRenderingContext::ARRAY_BUFFER; + let item_size = 2; + let item_type = WebGlRenderingContext::FLOAT; + let normalized = false; + let stride = 0; + let offset = 0; + + gl_context.enable_vertex_attrib_array(location); + gl_context.bind_buffer(target,Some(buffer)); + gl_context.vertex_attrib_pointer_with_i32 + ( location + , item_size + , item_type + , normalized + , stride + , offset + ); + } + + fn setup_blending(&self) { + let gl_context = &self.gl_context; + let rgb_source = Context::SRC_ALPHA; + let alpha_source = Context::ZERO; + let rgb_destination = Context::ONE_MINUS_SRC_ALPHA; + let alhpa_destination = Context::ONE; + + gl_context.enable(Context::BLEND); + gl_context.blend_func_separate(rgb_source,rgb_destination,alpha_source,alhpa_destination); + } +} diff --git a/lib/core/src/text/font.rs b/lib/core/src/text/font.rs index f240db8366..dbe3917b3c 100644 --- a/lib/core/src/text/font.rs +++ b/lib/core/src/text/font.rs @@ -1,135 +1,171 @@ use crate::prelude::*; +use crate::text::msdf:: +{MsdfTexture,convert_msdf_transformation,x_distance_from_msdf_value}; + use basegl_core_msdf_sys as msdf_sys; use basegl_core_embedded_fonts::EmbeddedFonts; +use msdf_sys::{MsdfParameters,MultichannelSignedDistanceField}; +use std::collections::hash_map::Entry::{Occupied,Vacant}; -// ==================== -// === MSDF Texture === -// ==================== +// ======================== +// === Font render info === +// ======================== -/// Texture with msdf for all loaded glyphs of font +/// Data used for rendering a single glyph /// -/// This structure keeps texture data in 8-bit-per-channel RGB format, which -/// is ready to be passed to webgl texImage2D. The texture contains MSDFs for -/// all loaded glyphs, organized in vertical column. +/// Each distance and transformation values are expressed in normalized coordinates, where +/// (0.0, 0.0) is initial pen position for an character, and `y` = 1.0 is _ascender_. /// -/// It implements Extend trait making possible to pass this structure -/// as an output argument for `basegl_core_msdf_sys::generate_msdf` function -pub struct MsdfTexture { - pub data : Vec -} - -impl MsdfTexture { - pub const WIDTH : usize = 32; - - /// Number of rows in texture - pub fn rows(&self) -> usize { - self.data.len()/(msdf_sys::MSDF_CHANNELS_COUNT*Self::WIDTH) - } - - fn convert_cell_from_f32(value : f32) -> u8 { - nalgebra::clamp(value*255.0, 0.0, 255.0) as u8 - } -} - -impl Extend for MsdfTexture { - /// Extends texture with new MSDF data in f32 format - fn extend>(&mut self, iter: T) { - self.data.extend( - iter.into_iter().map(Self::convert_cell_from_f32) - ); - } -} - -// ================= -// === Char info === -// ================= - -/// A single character data used for rendering +/// `from_base_layout` transforms the _base square_ for a character, such the glyph will be rendered +/// correctly with assigned MSDF texture. The _base square_ corners are (0.0, 0.0), (1.0, 1.0). +/// See also `glyph_render::GLYPH_SQUARE_VERTICES_BASE_LAYOUT`. /// -/// For now it has only information which fragment of `MsdfTexture` keeps MSDF -/// of this character -pub struct CharRenderInfo { +/// For explanation of various font-rendering terms, see +/// [freetype documentation](https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-1) +pub struct GlyphRenderInfo { pub msdf_texture_rows : std::ops::Range, + pub from_base_layout : nalgebra::Projective2, + pub advance : f64 } /// A single font data used for rendering /// -/// The data for individual characters are load on demand +/// The data for individual characters and kerning are load on demand. +/// +/// Each distance and transformation values are expressed in normalized coordinates, where `y` = 0.0 +/// is _baseline_ and `y` = 1.0 is _ascender_. For explanation of various font-rendering terms, see +/// [freetype documentation](https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-1) pub struct FontRenderInfo { - pub name : String, - pub msdf_sys_handle : msdf_sys::Font, - pub msdf_texture : MsdfTexture, - chars : HashMap + pub name : String, + pub msdf_sys_font : msdf_sys::Font, + pub msdf_texture : MsdfTexture, + glyphs : HashMap, + kerning : HashMap<(char,char),f64> } impl FontRenderInfo { - pub const MSDF_PARAMS : msdf_sys::MSDFParameters = - msdf_sys::MSDFParameters { + pub const MAX_MSDF_SHRINK_FACTOR : f64 = 4.; // Note [Picked MSDF parameters] + pub const MAX_MSDF_GLYPH_SCALE : f64 = 2.; // Note [Picked MSDF parameters] + + pub const MSDF_PARAMS : MsdfParameters = MsdfParameters { width : MsdfTexture::WIDTH, - height : MsdfTexture::WIDTH, - edge_coloring_angle_threshold : 3.0, - range : 8.0, - edge_threshold : 1.001, - overlap_support : true + height : MsdfTexture::ONE_GLYPH_HEIGHT, + edge_coloring_angle_threshold : 3.0, // Note [Picked MSDF parameters] + range : Self::MAX_MSDF_SHRINK_FACTOR * Self::MAX_MSDF_GLYPH_SCALE, + max_scale : Self::MAX_MSDF_GLYPH_SCALE, + edge_threshold : 1.001, // Note [Picked MSDF parameters] + overlap_support : true // Note [Picked MSDF parameters] }; - /// Create render info for font data in memory - pub fn new( - name : String, - font_data : &[u8], - ) -> FontRenderInfo { + /* Note [Picked MSDF parameters] + * + * The range was picked such way, that we avoid fitting range in one rendered pixel. + * Otherwise the antialiasing won't work. I assumed some maximum `shrink factor` (how many + * times rendered square will be smaller than MSDF size), and pick an arbitrary maximum glyph + * scale up. + * + * The rest of parameters are the defaults taken from msdfgen library + */ + + /// Create render info based on font data in memory + pub fn new(name:String, font_data:&[u8]) -> FontRenderInfo { FontRenderInfo { name, - msdf_sys_handle : msdf_sys::Font::load_from_memory(font_data), - msdf_texture : MsdfTexture { data : Vec::new() }, - chars : HashMap::new() + msdf_sys_font : msdf_sys::Font::load_from_memory(font_data), + msdf_texture : MsdfTexture { data : Vec::new() }, + glyphs : HashMap::new(), + kerning : HashMap::new() } } /// Create render info for one of embedded fonts - pub fn from_embedded( - base : &EmbeddedFonts, - name : &'static str - ) -> FontRenderInfo { + pub fn from_embedded(base:&EmbeddedFonts, name:&'static str) + -> FontRenderInfo { let font_data = base.font_data_by_name.get(name).unwrap(); - crate::text::font::FontRenderInfo::new( - name.to_string(), font_data - ) + FontRenderInfo::new(name.to_string(),font_data) } /// Load char render info - pub fn load_char(&mut self, ch : char) { - let msdf_texture_rows_begin = self.msdf_texture.rows(); - msdf_sys::generate_msdf( - &mut self.msdf_texture, - &self.msdf_sys_handle, - ch as u32, - &FontRenderInfo::MSDF_PARAMS, - // TODO [AO] should be soon loaded from font info - msdf_sys::Vector2D{ x : 1.0, y : 1.0 }, - // TODO [AO] should be soon loaded from font info - msdf_sys::Vector2D{ x : 2.0, y : 2.25 } - ); - let msdf_texture_rows_end = self.msdf_texture.rows(); - let char_info = CharRenderInfo { - msdf_texture_rows : msdf_texture_rows_begin..msdf_texture_rows_end + pub fn load_char(&mut self, ch:char) { + let handle = &self.msdf_sys_font; + let unicode = ch as u32; + let params = Self::MSDF_PARAMS; + let msdf_height = MsdfTexture::ONE_GLYPH_HEIGHT; + let msdf_tex_rows_begin = self.msdf_texture.rows(); + let msdf_tex_rows_end = msdf_tex_rows_begin + msdf_height; + + let msdf = MultichannelSignedDistanceField::generate(handle,unicode,¶ms); + let msdf_transformation = convert_msdf_transformation(&msdf); + let advance = x_distance_from_msdf_value(msdf.advance); + let glyph_info = GlyphRenderInfo { + msdf_texture_rows : msdf_tex_rows_begin..msdf_tex_rows_end, + from_base_layout : msdf_transformation.inverse(), + advance }; - self.chars.insert(ch, char_info); + self.msdf_texture.extend(msdf.data.iter()); + self.glyphs.insert(ch, glyph_info); } - /// Get or create render info for one character - pub fn get_or_create_char_info(&mut self, ch : char) -> &CharRenderInfo { - if !self.chars.contains_key(&ch) { + /// Get render info for one character, generating one if not found + pub fn get_glyph_info(&mut self, ch:char) -> &GlyphRenderInfo { + if !self.glyphs.contains_key(&ch) { self.load_char(ch); } - self.chars.get(&ch).unwrap() + self.glyphs.get(&ch).unwrap() + } + + /// Get kerning between two characters + pub fn get_kerning(&mut self, left : char, right : char) -> f64 { + match self.kerning.entry((left,right)) { + Occupied(entry) => *entry.get(), + Vacant(entry) => { + let msdf_val = self.msdf_sys_font.retrieve_kerning(left, right); + let normalized = x_distance_from_msdf_value(msdf_val); + *entry.insert(normalized) + } + } + } + + #[cfg(test)] + pub fn mock_font(name : String) -> FontRenderInfo { + FontRenderInfo { + name, + msdf_sys_font : msdf_sys::Font::mock_font(), + msdf_texture : MsdfTexture { data : Vec::new() }, + glyphs : HashMap::new(), + kerning : HashMap::new() + } + } + + #[cfg(test)] + pub fn mock_char_info(&mut self, ch : char) -> &mut GlyphRenderInfo { + let msdf_height = MsdfTexture::ONE_GLYPH_HEIGHT; + let msdf_texture_rows_begin = self.msdf_texture.rows(); + let msdf_texture_rows_end = msdf_texture_rows_begin + msdf_height; + let data_size = MsdfTexture::ONE_GLYPH_SIZE; + let msdf_data = (0..data_size).map(|_| 0.12345); + + let char_info = GlyphRenderInfo { + msdf_texture_rows : (msdf_texture_rows_begin..msdf_texture_rows_end), + from_base_layout : nalgebra::Transform::identity(), + advance : 0.0 + }; + self.msdf_texture.extend(msdf_data); + self.glyphs.insert(ch, char_info); + self.glyphs.get_mut(&ch).unwrap() + } + + #[cfg(test)] + pub fn mock_kerning_info(&mut self, l : char, r : char, value : f64) { + self.kerning.insert((l, r),value); } } #[cfg(test)] mod tests { - use crate::text::font::{MsdfTexture, FontRenderInfo}; + use super::*; + use crate::text::msdf::MsdfTexture; use basegl_core_msdf_sys as msdf_sys; use basegl_core_embedded_fonts::EmbeddedFonts; use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; @@ -146,18 +182,6 @@ mod tests { ) } - #[test] - fn extending_msdf_texture() { - let mut texture = MsdfTexture { - data : Vec::new() - }; - let msdf_values: &[f32] = &[-0.5, 0.0, 0.25, 0.5, 0.75, 1.0, 1.25]; - texture.extend(msdf_values[..4].iter().cloned()); - texture.extend(msdf_values[4..].iter().cloned()); - - assert_eq!([0, 0, 63, 127, 191, 255, 255], texture.data.as_slice()); - } - wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test(async)] @@ -167,7 +191,7 @@ mod tests { assert_eq!(TEST_FONT_NAME, font_render_info.name); assert_eq!(0, font_render_info.msdf_texture.data.len()); - assert_eq!(0, font_render_info.chars.len()); + assert_eq!(0, font_render_info.glyphs.len()); }) } @@ -179,21 +203,24 @@ mod tests { font_render_info.load_char('A'); font_render_info.load_char('B'); - let expected_texture_size = MsdfTexture::WIDTH * MsdfTexture::WIDTH - * msdf_sys::MSDF_CHANNELS_COUNT * 2; + let chars = 2; + let tex_width = MsdfTexture::WIDTH; + let tex_height = MsdfTexture::ONE_GLYPH_HEIGHT * chars; + let channels = MultichannelSignedDistanceField::CHANNELS_COUNT; + let tex_size = tex_width * tex_height * channels; - assert_eq!(MsdfTexture::WIDTH * 2, - font_render_info.msdf_texture.rows()); - assert_eq!(expected_texture_size, - font_render_info.msdf_texture.data.len()); - assert_eq!(2, font_render_info.chars.len()); + assert_eq!(tex_height , font_render_info.msdf_texture.rows()); + assert_eq!(tex_size , font_render_info.msdf_texture.data.len()); + assert_eq!(chars , font_render_info.glyphs.len()); - let first_char = font_render_info.chars.get(&'A').unwrap(); - let second_char = font_render_info.chars.get(&'B').unwrap(); + let first_char = font_render_info.glyphs.get(&'A').unwrap(); + let second_char = font_render_info.glyphs.get(&'B').unwrap(); - assert_eq!(0..MsdfTexture::WIDTH, first_char.msdf_texture_rows); - assert_eq!(MsdfTexture::WIDTH..2 * MsdfTexture::WIDTH, - second_char.msdf_texture_rows); + let first_range = 0..MsdfTexture::ONE_GLYPH_HEIGHT; + let second_range = MsdfTexture::ONE_GLYPH_HEIGHT..tex_height; + + assert_eq!(first_range , first_char.msdf_texture_rows); + assert_eq!(second_range , second_char.msdf_texture_rows); }) } @@ -203,16 +230,16 @@ mod tests { let mut font_render_info = create_test_font_render_info(); { - let char_info = font_render_info.get_or_create_char_info('A'); + let char_info = font_render_info.get_glyph_info('A'); assert_eq!(0..MsdfTexture::WIDTH, char_info.msdf_texture_rows); } - assert_eq!(1, font_render_info.chars.len()); + assert_eq!(1, font_render_info.glyphs.len()); { - let char_info = font_render_info.get_or_create_char_info('A'); + let char_info = font_render_info.get_glyph_info('A'); assert_eq!(0..MsdfTexture::WIDTH, char_info.msdf_texture_rows); } - assert_eq!(1, font_render_info.chars.len()); + assert_eq!(1, font_render_info.glyphs.len()); }) } -} \ No newline at end of file +} diff --git a/lib/core/src/text/glyph_render.rs b/lib/core/src/text/glyph_render.rs new file mode 100644 index 0000000000..658423c93a --- /dev/null +++ b/lib/core/src/text/glyph_render.rs @@ -0,0 +1,284 @@ +use crate::prelude::*; + +use crate::text::font::FontRenderInfo; +use crate::text::msdf::MsdfTexture; + +use nalgebra::{Point2,Transform2,Translation2,Affine2,Matrix3,Scalar}; + +// ============================ +// === Base vertices layout === +// ============================ + +pub const GLYPH_SQUARE_VERTICES_BASE_LAYOUT: &[(f64, f64)] = & + [ (0.0, 0.0) + , (0.0, 1.0) + , (1.0, 0.0) + , (1.0, 0.0) + , (0.0, 1.0) + , (1.0, 1.0) + ]; + +fn point_to_iterable(p:Point2) -> SmallVec<[T;2]> { + p.iter().cloned().collect() +} + +// ================================== +// === GylphSquareVerticesBuilder === +// ================================== + +const GLYPH_SQUARE_VERTICES_SIZE : usize = GLYPH_SQUARE_VERTICES_BASE_LAYOUT.len() * 2; + +pub type GlyphSquareVertices = SmallVec<[f64;GLYPH_SQUARE_VERTICES_SIZE]>; + +/// Builder for glyph vertices +/// +/// Once created, each `build_for_next_glyph` gives vertices of the next glyph's square +pub struct GylphSquareVerticesBuilder<'a> { + pub previous_char : Option, + pub font : &'a mut FontRenderInfo, + pub pen_position : Point2, + pub to_window : Transform2, +} + +impl<'a> GylphSquareVerticesBuilder<'a> { + + /// New GylphSquareVerticesBuilder + /// + /// The newly created builder start to place glyphs with pen located at `to_window` * (0.0, 0.0) + pub fn new(font:&'a mut FontRenderInfo, to_window:Transform2) + -> GylphSquareVerticesBuilder<'a> { + GylphSquareVerticesBuilder { + previous_char : None, + font, + pen_position : Point2::new(0.0, 0.0), + to_window + } + } + + /// Compute vertices for one glyph and move pen for next position + pub fn build_for_next_glyph(&mut self, ch:char) -> GlyphSquareVertices { + let apply_kerning = self.translation_by_kerning_value(ch); + let to_pen_position = self.translation_by_pen_position(); + let glyph_info = self.font.get_glyph_info(ch); + let glyph_specific_transform = &glyph_info.from_base_layout; + let to_window = &self.to_window; + let advance_pen = Translation2::new(glyph_info.advance, 0.0); + let pen_transformation = apply_kerning * advance_pen; + + self.pen_position = pen_transformation * self.pen_position; + self.previous_char = Some(ch); + let plain_base = GLYPH_SQUARE_VERTICES_BASE_LAYOUT.iter(); + let base = plain_base .map(|(x, y)| Point2::new(*x, *y)); + let glyph_fixed = base .map(|p| glyph_specific_transform * p); + let moved_to_pen_position = glyph_fixed .map(|p| to_pen_position * p); + let kerning_applied = moved_to_pen_position.map(|p| apply_kerning * p); + let mapped_to_window = kerning_applied .map(|p| to_window * p); + mapped_to_window.map(point_to_iterable).flatten().collect() + } + + fn translation_by_kerning_value(&mut self, ch:char) -> Translation2 { + let prev_char = self.previous_char; + let opt_value = prev_char.map(|lc| self.font.get_kerning(lc, ch)); + let value = opt_value.unwrap_or(0.0); + Translation2::new(value, 0.0) + } + + fn translation_by_pen_position(&self) -> Translation2{ + Translation2::new(self.pen_position.x, self.pen_position.y) + } +} + +// ================================= +// === TextureCoordinatesBuilder === +// ================================= + +const GLYPH_SQUARE_TEXTURE_COORDINATES_SIZE : usize = GLYPH_SQUARE_VERTICES_BASE_LAYOUT.len() * 2; + +pub type GlyphTextureCoordinates = SmallVec<[f64;GLYPH_SQUARE_TEXTURE_COORDINATES_SIZE]>; + +/// Builder for glyph MSDF texture coordinates +pub struct GlyphSquareTextureCoordinatesBuilder<'a> { + pub font : &'a mut FontRenderInfo +} + +impl<'a> GlyphSquareTextureCoordinatesBuilder<'a> { + /// Create new builder using given font + pub fn new(font:&'a mut FontRenderInfo) -> GlyphSquareTextureCoordinatesBuilder<'a> { + GlyphSquareTextureCoordinatesBuilder {font} + } + + /// Compute texture coordinates for `ch` + pub fn build_for_next_glyph(&mut self, ch:char) -> GlyphTextureCoordinates { + let border_align = self.align_borders_to_msdf_cell_center_transform(); + let to_proper_fragment = self.glyph_texture_fragment_transform(ch); + + let plain_base = GLYPH_SQUARE_VERTICES_BASE_LAYOUT.iter(); + let base = plain_base .map(|(x,y)| Point2::new(*x, *y)); + let aligned_to_border = base .map(|p| border_align * p); + let transformed = aligned_to_border.map(|p| to_proper_fragment * p); + transformed.map(point_to_iterable).flatten().collect() + } + + /// Transformation aligning borders to MSDF cell center + /// + /// Each cell in MSFD contains a distance measured from its center, therefore the borders of + /// glyph's square should be matched with center of MSDF cells to read distance properly + /// + /// The transformation's input should be a point in _single MSDF space_, where (0.0, 0.0) is + /// the bottom-left corner of MSDF, and (1.0, 1.0) is the top-right corner. + pub fn align_borders_to_msdf_cell_center_transform(&self) -> Affine2 { + let columns = MsdfTexture::WIDTH as f64; + let rows = MsdfTexture::ONE_GLYPH_HEIGHT as f64; + let column_size = 1.0 / columns; + let row_size = 1.0 / rows; + + let translation_x = column_size / 2.0; + let translation_y = row_size / 2.0; + let scale_x = 1.0 - column_size; + let scale_y = 1.0 - row_size; + let matrix = Matrix3::new + ( scale_x, 0.0 , translation_x + , 0.0 , scale_y, translation_y + , 0.0 , 0.0 , 1.0 + ); + Affine2::from_matrix_unchecked(matrix) + } + + /// Transformation MSDF texture fragment associated with given glyph + /// + /// The MSDF texture contains MSDFs for many glyphs. The returned transform maps the point in + /// a _single MSDF space_ to actual texture space. In other words, a (0.0, 0.0) point will be + /// mapped to bottom-left corner of `ch` texture fragment, and a (1.0, 1.0) will be mapped to + /// upper-right corner. + pub fn glyph_texture_fragment_transform(&mut self, ch:char) -> Affine2 { + let one_glyph_rows = MsdfTexture::ONE_GLYPH_HEIGHT as f64; + let all_rows = self.font.msdf_texture.rows() as f64; + + let fraction = one_glyph_rows / all_rows; + let glyph_info = self.font.get_glyph_info(ch); + let offset = glyph_info.msdf_texture_rows.start as f64 / all_rows; + let matrix = nalgebra::Matrix3::new + ( 1.0, 0.0 , 0.0 + , 0.0, fraction, offset + , 0.0, 0.0 , 1.0 + ); + Affine2::from_matrix_unchecked(matrix) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + use crate::text::font::GlyphRenderInfo; + + use basegl_core_msdf_sys::test_utils::TestAfterInit; + use std::future::Future; + use wasm_bindgen_test::wasm_bindgen_test; + + #[wasm_bindgen_test(async)] + fn build_vertices_for_glyph_square() -> impl Future { + TestAfterInit::schedule(|| { + let mut font = FontRenderInfo::mock_font("Test font".to_string()); + mock_a_glyph_info(&mut font); + mock_w_glyph_info(&mut font); + font.mock_kerning_info('A', 'W', -0.16); + let to_window_transformation = { + let to_window_mtx = nalgebra::Matrix3::new + ( 0.1, 0.0, -1.0 + , 0.0, 0.1, -0.5 + , 0.0, 0.0, 1.0 + ); + nalgebra::Transform2::from_matrix_unchecked(to_window_mtx) + }; + + let mut builder = GylphSquareVerticesBuilder::new(&mut font,to_window_transformation); + let a_vertices = builder.build_for_next_glyph('A'); + assert_eq!(Some('A'), builder.previous_char); + assert_eq!(0.56 , builder.pen_position.x); + assert_eq!(0.0 , builder.pen_position.y); + let w_vertices = builder.build_for_next_glyph('W'); + assert_eq!(Some('W'), builder.previous_char); + assert_eq!(1.1 , builder.pen_position.x); + assert_eq!(0.0 , builder.pen_position.y); + + let expected_a_vertices = & + [ -0.99 , -0.48 + , -0.99 , -0.4 + , -0.94 , -0.48 + , -0.94 , -0.48 + , -0.99 , -0.4 + , -0.94 , -0.4 + ]; + let expected_w_vertices = & + [ -0.95 , -0.48 + , -0.95 , -0.39 + , -0.89 , -0.48 + , -0.89 , -0.48 + , -0.95 , -0.39 + , -0.89 , -0.39 + ]; + + assert_eq!(expected_a_vertices, a_vertices.as_ref()); + assert_eq!(expected_w_vertices, w_vertices.as_ref()); + }) + } + + #[wasm_bindgen_test(async)] + fn build_texture_coordinates_for_glyph_square() -> impl Future { + TestAfterInit::schedule(|| { + let mut font = FontRenderInfo::mock_font("Test font".to_string()); + font.mock_char_info('A'); + font.mock_char_info('W'); + + let mut builder = GlyphSquareTextureCoordinatesBuilder::new(&mut font); + let a_texture_coordinates = builder.build_for_next_glyph('A'); + let w_texture_coordinates = builder.build_for_next_glyph('W'); + + let expected_a_coordinates = & + [ 1./64. , 1./128. + , 1./64. , 63./128. + , 63./64. , 1./128. + , 63./64. , 1./128. + , 1./64. , 63./128. + , 63./64. , 63./128. + ]; + let expected_w_coordinates = & + [ 1./64. , 65./128. + , 1./64. , 127./128. + , 63./64. , 65./128. + , 63./64. , 65./128. + , 1./64. , 127./128. + , 63./64. , 127./128. + ]; + + assert_eq!(expected_a_coordinates, a_texture_coordinates.as_ref()); + assert_eq!(expected_w_coordinates, w_texture_coordinates.as_ref()); + }) + } + + fn mock_a_glyph_info(font:&mut FontRenderInfo) -> &mut GlyphRenderInfo { + let a_info = font.mock_char_info('A'); + a_info.advance = 0.56; + let trans_mtx = nalgebra::Matrix3::new + ( 0.5, 0.0, 0.1 + , 0.0, 0.8, 0.2 + , 0.0, 0.0, 1.0 + ); + a_info.from_base_layout = nalgebra::Projective2::from_matrix_unchecked(trans_mtx); + a_info + } + + fn mock_w_glyph_info(font:&mut FontRenderInfo) -> &mut GlyphRenderInfo { + let a_info = font.mock_char_info('W'); + a_info.advance = 0.7; + let trans_mtx = nalgebra::Matrix3::new + ( 0.6, 0.0, 0.1 + , 0.0, 0.9, 0.2 + , 0.0, 0.0, 1.0 + ); + a_info.from_base_layout = nalgebra::Projective2::from_matrix_unchecked(trans_mtx); + a_info + } +} diff --git a/lib/core/src/text/mod.rs b/lib/core/src/text/mod.rs deleted file mode 100644 index 04cbb68cf8..0000000000 --- a/lib/core/src/text/mod.rs +++ /dev/null @@ -1,267 +0,0 @@ -use basegl_backend_webgl::{Context, compile_shader, link_program, Program, }; - -use web_sys::{WebGlRenderingContext, WebGlBuffer, WebGlTexture}; -use crate::prelude::*; -use crate::Color; - -pub mod font; - -use font::FontRenderInfo; -use crate::text::font::MsdfTexture; -use js_sys::Float32Array; -use crate::display::world::Workspace; - -pub struct TextComponentBuilder<'a> { - pub text : String, - pub font : &'a mut FontRenderInfo, - pub x : f32, - pub y : f32, - pub size : f32, - pub color : Color, - pub background_color : Color, -} - -#[derive(Debug)] -pub struct TextComponent { - gl_context : WebGlRenderingContext, - gl_program : Program, - gl_vertex_buf : WebGlBuffer, - gl_tex_coord_buf : WebGlBuffer, - gl_msdf_texture : WebGlTexture, - buffers_size : usize, -} - -impl<'a> TextComponentBuilder<'a> { - pub fn build(mut self, workspace : &Workspace) -> TextComponent { - let gl_context = workspace.context.clone(); - let gl_program = self.create_program(&gl_context); - let gl_vertex_buf = self.create_vertex_buf(&gl_context); - let gl_tex_coord_buf = self.create_tex_coord_buf(&gl_context); - let gl_msdf_texture = - self.create_msdf_texture(&gl_context, &gl_program); - - self.setup_uniforms(&gl_context, &gl_program); - - TextComponent { - gl_context, - gl_program, - gl_vertex_buf, - gl_tex_coord_buf, - gl_msdf_texture, - buffers_size: self.text.len() - } - } - - fn create_program(&self, gl_context : &Context) -> Program { - gl_context.get_extension("OES_standard_derivatives") - .unwrap().unwrap(); - - let vert_shader = compile_shader( - &gl_context, - WebGlRenderingContext::VERTEX_SHADER, - include_str!("msdf_vert.glsl") - ).unwrap(); - - let frag_shader = compile_shader( - &gl_context, - WebGlRenderingContext::FRAGMENT_SHADER, - include_str!("msdf_frag.glsl") - ).unwrap(); - - link_program(&gl_context, &vert_shader, &frag_shader).unwrap() - } - - fn create_buffer( - gl_context : &Context, - vertices : &[f32] - ) -> WebGlBuffer { - let buffer = gl_context.create_buffer().unwrap(); - gl_context.bind_buffer( - WebGlRenderingContext::ARRAY_BUFFER, - Some(&buffer) - ); - - unsafe { // Note [unsafe buffer_data] - let float_32_array = Float32Array::view(&vertices); - gl_context.buffer_data_with_array_buffer_view( - WebGlRenderingContext::ARRAY_BUFFER, - &float_32_array, - WebGlRenderingContext::STATIC_DRAW, - ); - } - - buffer - } - - /* Note [unsafe buffer_data] - * - * The Float32Array::view is safe as long there are no allocations done - * until it is destroyed. This way of creating buffers were taken from - * wasm-bindgen examples - * (https://rustwasm.github.io/wasm-bindgen/examples/webgl.html) - */ - - fn create_vertex_buf(&self, gl_context : &Context) -> WebGlBuffer { - let y_max = self.y + self.size; - let x_step = self.size; - let vertices = (0..self.text.len()).map(|i| { - let ix = self.x + (i as f32) * x_step; - let ix_max = ix + x_step; - vec![ix, self.y, ix, y_max, ix_max, self.y, ix_max, - self.y, ix, y_max, ix_max, y_max] // Note [The ugly code] - }).flatten().collect::>(); - - Self::create_buffer(gl_context, vertices.as_ref()) - } - - /* Note [The ugly code] - * - * I have already refactored this function on branch - * wip/ao/letter-alignment. Please be patient - */ - - fn create_tex_coord_buf(&mut self, gl_context : &Context) -> WebGlBuffer { - let font = &mut self.font; - for ch in self.text.chars() { - font.get_or_create_char_info(ch); - } - let vertices = self.text.chars().map(|c| { - let msdf_rows = font.msdf_texture.rows() as f32; - let info = font.get_or_create_char_info(c); - let y_min = info.msdf_texture_rows.start as f32 / msdf_rows; - let y_max = info.msdf_texture_rows.end as f32 / msdf_rows; - vec![0.0, y_min, 0.0, y_max, 1.0, y_min, - 1.0, y_min, 0.0, y_max, 1.0, y_max] - }).flatten().collect::>(); - - Self::create_buffer(gl_context, vertices.as_ref()) - } - - fn create_msdf_texture(&self, gl_context : &Context, gl_program : &Program) - -> WebGlTexture { - - let msdf_texture = gl_context.create_texture().unwrap(); - gl_context.bind_texture(Context::TEXTURE_2D, Some(&msdf_texture)); - - gl_context.tex_parameteri( - Context::TEXTURE_2D, - Context::TEXTURE_WRAP_S, - Context::CLAMP_TO_EDGE as i32 - ); - gl_context.tex_parameteri( - Context::TEXTURE_2D, - Context::TEXTURE_WRAP_T, - Context::CLAMP_TO_EDGE as i32 - ); - gl_context.tex_parameteri( - Context::TEXTURE_2D, - Context::TEXTURE_MIN_FILTER, - Context::LINEAR as i32 - ); - - gl_context.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( - Context::TEXTURE_2D, - 0, - Context::RGB as i32, - MsdfTexture::WIDTH as i32, - self.font.msdf_texture.rows() as i32, - 0, - Context::RGB, - Context::UNSIGNED_BYTE, - Some(self.font.msdf_texture.data.as_slice()) - ).unwrap(); - - let msdf_loc = gl_context.get_uniform_location(gl_program, "msdf"); - let msdf_size_loc = - gl_context.get_uniform_location(gl_program, "msdfSize"); - - gl_context.use_program(Some(gl_program)); - gl_context.uniform1i(msdf_loc.as_ref(), 0); - gl_context.uniform2f( - msdf_size_loc.as_ref(), - MsdfTexture::WIDTH as f32, - self.font.msdf_texture.rows() as f32 - ); - - msdf_texture - } - - fn setup_uniforms(&self, gl_context : &Context, gl_program : &Program) { - let bg_color_loc = - gl_context.get_uniform_location(gl_program, "bgColor"); - let fg_color_loc = - gl_context.get_uniform_location(gl_program, "fgColor"); - let px_range_loc = - gl_context.get_uniform_location(gl_program, "pxRange"); - - gl_context.use_program(Some(gl_program)); - gl_context.uniform4f( - bg_color_loc.as_ref(), - self.background_color.r, - self.background_color.g, - self.background_color.b, - self.background_color.a - ); - gl_context.uniform4f( - fg_color_loc.as_ref(), - self.color.r, - self.color.g, - self.color.b, - self.color.a, - ); - gl_context.uniform1f( - px_range_loc.as_ref(), - FontRenderInfo::MSDF_PARAMS.range as f32 - ); - } -} - -impl TextComponent { - - pub fn display(&self) { - let gl = &self.gl_context; - let program = &self.gl_program; - - gl.use_program(Some(&self.gl_program)); - - let position_location = gl.get_attrib_location(program, "position"); - gl.enable_vertex_attrib_array(position_location as u32); - gl.bind_buffer( - WebGlRenderingContext::ARRAY_BUFFER, - Some(&self.gl_vertex_buf) - ); - gl.vertex_attrib_pointer_with_i32( - position_location as u32, - 2, - WebGlRenderingContext::FLOAT, - false, - 0, - 0 - ); - - gl.bind_texture(Context::TEXTURE_2D, Some(&self.gl_msdf_texture)); - - let tex_coord_location = gl.get_attrib_location(program, "texCoord"); - assert!(tex_coord_location >= 0); - gl.enable_vertex_attrib_array(tex_coord_location as u32); - gl.bind_buffer( - WebGlRenderingContext::ARRAY_BUFFER, - Some(&self.gl_tex_coord_buf) - ); - gl.vertex_attrib_pointer_with_i32( - tex_coord_location as u32, - 2, - WebGlRenderingContext::FLOAT, - false, - 0, - 0 - ); - - - gl.draw_arrays( - WebGlRenderingContext::TRIANGLES, - 0, - (self.buffers_size*6) as i32, - ); - } -} diff --git a/lib/core/src/text/msdf.rs b/lib/core/src/text/msdf.rs new file mode 100644 index 0000000000..b4cacb0f80 --- /dev/null +++ b/lib/core/src/text/msdf.rs @@ -0,0 +1,142 @@ +use basegl_core_msdf_sys as msdf_sys; +use msdf_sys::MultichannelSignedDistanceField; +use nalgebra::clamp; + +// ==================== +// === MSDF Texture === +// ==================== + +/// Texture with msdf for all loaded glyphs of font +/// +/// This structure keeps texture data in 8-bit-per-channel RGB format, which +/// is ready to be passed to webgl texImage2D. The texture contains MSDFs for +/// all loaded glyphs, organized in vertical column. +pub struct MsdfTexture { + pub data : Vec +} + +impl MsdfTexture { + pub const CHANNELS_COUNT : usize = MultichannelSignedDistanceField::CHANNELS_COUNT; + pub const WIDTH : usize = 32; + pub const ROW_SIZE : usize = Self::CHANNELS_COUNT * Self::WIDTH; + pub const ONE_GLYPH_HEIGHT : usize = 32; + pub const ONE_GLYPH_SIZE : usize = Self::ROW_SIZE * Self::ONE_GLYPH_HEIGHT; + + /// Number of rows in texture + pub fn rows(&self) -> usize { + self.data.len() / Self::ROW_SIZE + } + + fn convert_cell_from_f32(value : f32) -> u8 { + const UNSIGNED_BYTE_MIN : f32 = 0.0; + const UNSIGNED_BYTE_MAX : f32 = 255.0; + + let scaled_to_byte = value * UNSIGNED_BYTE_MAX; + let clamped_to_byte = clamp(scaled_to_byte,UNSIGNED_BYTE_MIN,UNSIGNED_BYTE_MAX); + clamped_to_byte as u8 + } +} + +impl Extend for MsdfTexture { + /// Extends texture with new MSDF data in f32 format + fn extend>(&mut self, iter:T) { + let f32_iterator = iter.into_iter(); + let converted_iterator = f32_iterator.map(Self::convert_cell_from_f32); + self.data.extend(converted_iterator); + } +} + +// ================================== +// === msdf-sys values converting === +// ================================== + +/// Converts x dimension distance obtained from msdf-sys to vertex-space values +/// +/// The values obtained from `msdf-sys` are expressed in MSDF cells. This +/// function convert them to normalized coordinates, where +/// (0.0, 0.0) is initial pen position for an character, and `y` = 1.0 is +/// _ascender_. +pub fn x_distance_from_msdf_value(msdf_value:f64) -> f64 { + msdf_value / MsdfTexture::WIDTH as f64 +} + +/// Converts y dimension distance obtained from msdf-sys to vertex-space values +/// +/// The values obtained from `msdf-sys` are expressed in MSDF cells. This +/// function convert them to normalized coordinates, where +/// (0.0, 0.0) is initial pen position for an character, and `y` = 1.0 is +/// _ascender_. +pub fn y_distance_from_msdf_value(msdf_value:f64) -> f64 { + msdf_value / MsdfTexture::ONE_GLYPH_HEIGHT as f64 +} + +/// Converts transformation obtained from msdf-sys to vertex-space values +/// +/// This function get the transformation obtained from `msdf_sys` which is +/// expressed in MSDF units, and convert it to normalized coordinates, where +/// (0.0, 0.0) is initial pen position for an character, and `y` = 1.0 is +/// _ascender_. +pub fn convert_msdf_transformation(msdf:&MultichannelSignedDistanceField) +-> nalgebra::Projective2 { + let translate_converted_x = x_distance_from_msdf_value(msdf.translation.x); + let translate_converted_y = y_distance_from_msdf_value(msdf.translation.y); + let translate_scaled_x = translate_converted_x * msdf.scale.x; + let translate_scaled_y = translate_converted_y * msdf.scale.y; + let msdf_transformation_matrix = nalgebra::Matrix3::new( + msdf.scale.x, 0.0, translate_scaled_x, + 0.0, msdf.scale.y, translate_scaled_y, + 0.0, 0.0, 1.0 + ); + nalgebra::Projective2::from_matrix_unchecked(msdf_transformation_matrix) +} + +#[cfg(test)] +mod test { + use super::*; + + use basegl_core_msdf_sys::test_utils::TestAfterInit; + use nalgebra::Vector2; + use std::future::Future; + use wasm_bindgen_test::wasm_bindgen_test; + + #[test] + fn extending_msdf_texture() { + let mut texture = MsdfTexture{data : Vec::new()}; + let msdf_values: &[f32] = &[-0.5, 0.0, 0.25, 0.5, 0.75, 1.0, 1.25]; + texture.extend(msdf_values[..4].iter().cloned()); + texture.extend(msdf_values[4..].iter().cloned()); + + assert_eq!([0, 0, 63, 127, 191, 255, 255], texture.data.as_slice()); + } + + #[test] + fn x_dimension_converting() { + assert_eq!(1.0/8.0, x_distance_from_msdf_value(4.0)); + assert_eq!(1.0/2.0, x_distance_from_msdf_value(16.0)); + } + + #[test] + fn y_dimension_converting() { + assert_eq!(1.0/8.0, y_distance_from_msdf_value(4.0)); + assert_eq!(1.0/2.0, y_distance_from_msdf_value(16.0)); + } + + #[wasm_bindgen_test(async)] + fn msdf_transformation_converting() -> impl Future { + TestAfterInit::schedule(|| { + let mut msdf = MultichannelSignedDistanceField::mock_results(); + msdf.scale = Vector2::new(2.0, 3.0); + msdf.translation = Vector2::new(16.0, 4.0); + + let converted = convert_msdf_transformation(&msdf); + + let expected_mtx = nalgebra::Matrix3::new( + 2.0, 0.0, 1.0, + 0.0, 3.0, 3.0/8.0, + 0.0, 0.0, 1.0 + ); + + assert_eq!(expected_mtx, *converted.matrix()); + }) + } +} diff --git a/lib/core/src/text/msdf_frag.glsl b/lib/core/src/text/msdf_frag.glsl index 6b8c3e1259..90f2d53ef9 100644 --- a/lib/core/src/text/msdf_frag.glsl +++ b/lib/core/src/text/msdf_frag.glsl @@ -1,28 +1,37 @@ #extension GL_OES_standard_derivatives : enable varying highp vec2 vTexCoord; -varying highp vec2 msdfCoord; -uniform sampler2D msdf; -uniform highp vec2 msdfSize; -uniform highp float pxRange; -uniform highp vec4 bgColor; -uniform highp vec4 fgColor; +uniform sampler2D msdf; +// Number of MSDF cells in row and column +uniform highp vec2 msdfSize; +// Range parameter of msdf generation - the distance between 0.0 and 1.0 values +// expressed in MSDF cells +uniform highp float range; +uniform highp vec4 color; -highp float median(highp float r, highp float g, highp float b) { - return max(min(r, g), min(max(r, g), b)); +highp float median(highp vec3 v) { + return max(min(v.x, v.y), min(max(v.x, v.y), v.z)); } void main() { - highp vec2 msdfUnitTex = pxRange/msdfSize; - highp vec2 msdfUnitPx = msdfUnitTex/fwidth(vTexCoord); - highp vec3 smple = texture2D(msdf, vTexCoord).rgb; - highp float sigDist = median(smple.r, smple.g, smple.b) - 0.5; + highp vec2 msdfUnitTex = range/msdfSize; + highp vec2 msdfUnitPx = msdfUnitTex/fwidth(vTexCoord); + highp float avgMsdfUnitPx = (msdfUnitPx.x + msdfUnitPx.y) / 2.0; + // Note [dpiDilate] + highp float dpiDilate = avgMsdfUnitPx < range*0.49 ? 1.0 : 0.0; - highp float dpiDilate = (msdfUnitPx.x + msdfUnitPx.y) < pxRange*0.9 ? 1.0 : 0.0; + highp vec3 msdfSample = texture2D(msdf, vTexCoord).rgb; + highp float sigDist = median(msdfSample) - 0.5; - highp float sigDistPx = sigDist*((msdfUnitPx.x + msdfUnitPx.y)/2.0); - highp float opacity = clamp(sigDistPx + 0.5 + dpiDilate*0.1, 0.0, 1.0); - gl_FragColor = mix(bgColor, fgColor, opacity); -// gl_FragColor = vec4(sigDist+0.5, dpiDilate, 0.0, 1.0); + highp float sigDistPx = sigDist * avgMsdfUnitPx; + highp float opacity = 0.5 + sigDistPx + dpiDilate*0.1; + gl_FragColor = vec4(color.xyz, color.w * clamp(opacity, 0.0, 1.0)); } + +/* Note [dpiDilate] + * + * This is 1.0 on low dpi and 0.0 otherwise. We use this parameter to fatten + * somewhat font on low resolutions. The thershold and exact value of this + * fattening was picked by trial an error, searching for best rendering effect. + */ diff --git a/lib/core/src/text/msdf_vert.glsl b/lib/core/src/text/msdf_vert.glsl index 913a2f7452..17a178d096 100644 --- a/lib/core/src/text/msdf_vert.glsl +++ b/lib/core/src/text/msdf_vert.glsl @@ -6,4 +6,4 @@ varying vec2 vTexCoord; void main() { vTexCoord = texCoord; gl_Position = vec4(position, 0.0, 1.0); -} \ No newline at end of file +} diff --git a/lib/prelude/Cargo.toml b/lib/prelude/Cargo.toml index 90713eacca..24d8f304e3 100644 --- a/lib/prelude/Cargo.toml +++ b/lib/prelude/Cargo.toml @@ -14,4 +14,5 @@ itertools = "0.8" derivative = "1.0.3" num = "0.2.0" boolinator = "2.4.0" -paste = "0.1" \ No newline at end of file +paste = "0.1" +smallvec = "1.0.0" diff --git a/lib/prelude/src/lib.rs b/lib/prelude/src/lib.rs index 06cd2caa27..33dd243e5e 100644 --- a/lib/prelude/src/lib.rs +++ b/lib/prelude/src/lib.rs @@ -15,6 +15,7 @@ pub use itertools::Itertools; pub use num::Num; pub use paste; pub use shrinkwraprs::Shrinkwrap; +pub use smallvec::SmallVec; pub use std::cell::Ref; pub use std::cell::RefMut; pub use std::cell::RefCell; diff --git a/lib/system/web/js/resize_observer.js b/lib/system/web/js/resize_observer.js index 568820393c..b739200952 100644 --- a/lib/system/web/js/resize_observer.js +++ b/lib/system/web/js/resize_observer.js @@ -57,6 +57,7 @@ let resizeObserverPool = new Pool((...args) => new ResizeObserver(...args)) export function resize_observe(target, f) { let id = resizeObserverPool.reserve(resize_observer_update(f)) resizeObserverPool[id].observe(target) + return id } export function resize_unobserve(id) {