Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NormalizedCoord> = 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();
Expand All @@ -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;
}
Expand Down Expand Up @@ -2428,15 +2430,15 @@ impl Render for List<String> {
align,
};

let mut glyph_paths: Vec<String> = 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));
});
});
});
Expand All @@ -2461,16 +2463,20 @@ impl Render for List<String> {
}
},
|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 {
attributes.push("fill", "none");
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.));
}
}
});
}
Expand Down Expand Up @@ -2531,12 +2537,14 @@ impl Render for List<String> {
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::<peniko::color::Srgb>::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);
}
});
});
Expand Down
2 changes: 1 addition & 1 deletion node-graph/nodes/text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion node-graph/nodes/text/src/path_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
31 changes: 27 additions & 4 deletions node-graph/nodes/text/src/text_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ 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<TextContext> = RefCell::new(TextContext::default());
}

/// 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<RgbaColor>, 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();

Expand Down Expand Up @@ -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<RgbaColor>,
font_info_cache: HashMap<ResourceHash, (FamilyId, FontInfo)>,
}

Expand Down Expand Up @@ -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<Layout<()>> {
pub fn layout_text(&mut self, text: &str, font: &Resource, typesetting: TypesettingConfig) -> Option<Layout<RgbaColor>> {
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(
Expand All @@ -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<RgbaColor> = builder.build(text);

layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32));
layout.align(typesetting.align.into(), AlignmentOptions::default());
Expand Down