diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 58fde12096..56eb45edb9 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2268,14 +2268,16 @@ impl OutlinePen for GlyphOutlinePen<'_> { } /// Draws each glyph of `glyph_run` into a `BezPath` (with the run's position and faux-italic `tilt_tan` baked in) -/// and calls `emit` for each non-empty glyph. Zero-geometry glyphs advance by `space_extra` for justified spacing. -fn draw_glyph_run_to_bezpaths(glyph_run: &parley::GlyphRun<'_, ()>, x_offset: f32, space_extra: f32, tilt_tan: f64, mut emit: impl FnMut(&BezPath)) { +/// and calls `emit` with the path and the run's RGBA brush color for each non-empty glyph. +/// Zero-geometry glyphs advance by `space_extra` for justified spacing. +fn draw_glyph_run_to_bezpaths(glyph_run: &parley::GlyphRun<'_, text_nodes::RgbaColor>, x_offset: f32, space_extra: f32, tilt_tan: f64, mut emit: impl FnMut(&BezPath, [u8; 4])) { let mut run_x = glyph_run.offset() + x_offset; let run_y = glyph_run.baseline(); let run = glyph_run.run(); let font = run.font(); let font_size_pts = run.font_size(); let normalized_coords: Vec = run.normalized_coords().iter().map(|c| NormalizedCoord::from_bits(*c)).collect(); + let run_color = glyph_run.style().brush.0; let Ok(font_ref) = SkrifaFontRef::from_index(font.data.as_ref(), font.index) else { return }; let outlines = font_ref.outline_glyphs(); @@ -2293,7 +2295,7 @@ fn draw_glyph_run_to_bezpaths(glyph_run: &parley::GlyphRun<'_, ()>, x_offset: f3 let path = &mut bez_path; let mut pen = GlyphOutlinePen { path, ox, oy, tilt_tan }; if outline.draw(settings, &mut pen).is_ok() && !bez_path.elements().is_empty() { - emit(&bez_path); + emit(&bez_path, run_color); } else if space_extra != 0. && glyph.advance > 0. { run_x += space_extra; } @@ -2428,15 +2430,15 @@ impl Render for List { align, }; - let mut glyph_paths: Vec = Vec::new(); + let mut glyph_paths: Vec<(String, [u8; 4])> = Vec::new(); text_nodes::TextContext::with_thread_local(|ctx| { let Some(layout) = ctx.layout_text(text, &font, typesetting) else { return }; let tilt_tan = letter_tilt.to_radians().tan(); text_nodes::for_each_styled_glyph_run(&layout, text, typesetting, |glyph_run, x_offset, space_extra| { - draw_glyph_run_to_bezpaths(glyph_run, x_offset, space_extra, tilt_tan, |bez_path| { - glyph_paths.push(bez_path.to_svg()); + draw_glyph_run_to_bezpaths(glyph_run, x_offset, space_extra, tilt_tan, |bez_path, color| { + glyph_paths.push((bez_path.to_svg(), color)); }); }); }); @@ -2461,7 +2463,7 @@ impl Render for List { } }, |render| { - for path_d in glyph_paths { + for (path_d, color) in glyph_paths { render.leaf_tag("path", |attributes| { attributes.push("d", path_d); if let RenderMode::Outline = render_params.render_mode { @@ -2469,8 +2471,12 @@ impl Render for List { attributes.push("stroke", "black"); attributes.push("stroke-width", "1"); } else { - attributes.push("fill", "black"); + let [r, g, b, a] = color; + attributes.push("fill", format!("#{r:02x}{g:02x}{b:02x}")); attributes.push("fill-rule", "nonzero"); + if a < 255 { + attributes.push("fill-opacity", format!("{:.4}", a as f32 / 255.)); + } } }); } @@ -2531,12 +2537,14 @@ impl Render for List { let tilt_tan = letter_tilt.to_radians().tan(); text_nodes::for_each_styled_glyph_run(&layout, text, typesetting, |glyph_run, x_offset, space_extra| { - draw_glyph_run_to_bezpaths(glyph_run, x_offset, space_extra, tilt_tan, |bez_path| { + draw_glyph_run_to_bezpaths(glyph_run, x_offset, space_extra, tilt_tan, |bez_path, color| { if let RenderMode::Outline = render_params.render_mode { let (outline_stroke, outline_color) = get_outline_styles(render_params); scene.stroke(&outline_stroke, affine, outline_color, None, bez_path); } else { - scene.fill(peniko::Fill::NonZero, affine, peniko::Color::BLACK, None, bez_path); + let [r, g, b, a] = color; + let vello_color = peniko::color::AlphaColor::::new([r as f32 / 255., g as f32 / 255., b as f32 / 255., a as f32 / 255.]); + scene.fill(peniko::Fill::NonZero, affine, vello_color, None, bez_path); } }); }); diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index 742e12cb04..35931c4312 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -19,7 +19,7 @@ use unicode_segmentation::UnicodeSegmentation; pub use core_types as gcore; pub use fallback::FALLBACK_FONT_RESOURCE; pub use font::*; -pub use text_context::{TextContext, for_each_styled_glyph_run}; +pub use text_context::{RgbaColor, TextContext, for_each_styled_glyph_run}; pub use to_path::*; pub use vector_types; diff --git a/node-graph/nodes/text/src/path_builder.rs b/node-graph/nodes/text/src/path_builder.rs index 740d120a0e..1fe50ee0ca 100644 --- a/node-graph/nodes/text/src/path_builder.rs +++ b/node-graph/nodes/text/src/path_builder.rs @@ -105,7 +105,7 @@ impl PathBuilder { has_geometry } - pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, letter_tilt: f64, per_glyph_items: bool, x_offset: f32, space_extra: f32) { + pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, crate::text_context::RgbaColor>, letter_tilt: f64, per_glyph_items: bool, x_offset: f32, space_extra: f32) { let mut run_x = glyph_run.offset() + x_offset; let run_y = glyph_run.baseline(); diff --git a/node-graph/nodes/text/src/text_context.rs b/node-graph/nodes/text/src/text_context.rs index c582035357..74c38d1f3d 100644 --- a/node-graph/nodes/text/src/text_context.rs +++ b/node-graph/nodes/text/src/text_context.rs @@ -9,6 +9,13 @@ use parley::{AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, Lin use std::collections::HashMap; use vector_types::Vector; +/// Minimal RGBA brush type (channels: `[r, g, b, a]`, each 0–255) that satisfies +/// `parley::style::Brush` via its blanket impl for `Clone + PartialEq + Default + Debug`. +/// A newtype over `[u8; 4]` is required because Rust's orphan rule forbids +/// `impl ForeignTrait for [ForeignType; N]` in an external crate. +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] +pub struct RgbaColor(pub [u8; 4]); + thread_local! { static THREAD_TEXT: RefCell = RefCell::new(TextContext::default()); } @@ -16,7 +23,7 @@ thread_local! { /// Iterates the glyph runs of a laid-out text in reading order, computing each line's last-line alignment correction /// (`x_offset` and per-space `space_extra`) and skipping runs clipped by `max_height`. Shared by the vector shaper and the /// SVG/Vello text renderers so the alignment logic lives in one place. -pub fn for_each_styled_glyph_run(layout: &Layout<()>, text: &str, typesetting: TypesettingConfig, mut visit: impl FnMut(&GlyphRun<'_, ()>, f32, f32)) { +pub fn for_each_styled_glyph_run(layout: &Layout, text: &str, typesetting: TypesettingConfig, mut visit: impl FnMut(&GlyphRun<'_, RgbaColor>, f32, f32)) { let alignment_width = typesetting.max_width.map(|w| w as f32).unwrap_or_else(|| layout.full_width()); let last_line_correction = typesetting.align.last_line_correction(); @@ -71,7 +78,7 @@ pub fn for_each_styled_glyph_run(layout: &Layout<()>, text: &str, typesetting: T #[derive(Default)] pub struct TextContext { font_context: FontContext, - layout_context: LayoutContext<()>, + layout_context: LayoutContext, font_info_cache: HashMap, } @@ -106,12 +113,14 @@ impl TextContext { } /// Create a text layout from the given font resource and typesetting configuration. - pub fn layout_text(&mut self, text: &str, font: &Resource, typesetting: TypesettingConfig) -> Option> { + pub fn layout_text(&mut self, text: &str, font: &Resource, typesetting: TypesettingConfig) -> Option> { let (font_family, font_info) = self.get_font_info(font)?; const DISPLAY_SCALE: f32 = 1.; let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, DISPLAY_SCALE, false); + builder.push_default(StyleProperty::Brush(RgbaColor([0, 0, 0, 255]))); + builder.push_default(StyleProperty::FontSize(typesetting.font_size as f32)); builder.push_default(StyleProperty::LetterSpacing(typesetting.letter_spacing as f32)); builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(std::borrow::Cow::Owned( @@ -122,7 +131,21 @@ impl TextContext { builder.push_default(StyleProperty::FontWidth(font_info.width())); builder.push_default(LineHeight::FontSizeRelative(typesetting.line_height_ratio as f32)); - let mut layout: Layout<()> = builder.build(text); + // DEMO: alternate colors and double the font size for each subsequent word to prove style spans work. + const STYLE_A: RgbaColor = RgbaColor([0, 0, 0, 255]); + const STYLE_B: RgbaColor = RgbaColor([220, 30, 30, 255]); + let base_font_size = typesetting.font_size as f32; + let mut byte_cursor = 0usize; + for (word_index, word) in text.split_inclusive(|c: char| c.is_whitespace()).enumerate() { + let end = byte_cursor + word.len(); + let is_even = word_index % 2 == 0; + let font_size = base_font_size * 2.0_f32.powi(word_index as i32); + builder.push(StyleProperty::Brush(if is_even { STYLE_A } else { STYLE_B }), byte_cursor..end); + builder.push(StyleProperty::FontSize(font_size), byte_cursor..end); + byte_cursor = end; + } + + let mut layout: Layout = builder.build(text); layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32)); layout.align(typesetting.align.into(), AlignmentOptions::default());