From ef313b91b0e77f7c3ce247badbc8b5f42b886170 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 04:54:24 -0400 Subject: [PATCH] Text: transparent text fills are faded once, not twice A run's solid fill with a:alpha was resolved into the glyph colour's alpha and also kept as the run's fill opacity, and the renderer multiplied both: text at 60 % transparency drew at 16 % opacity instead of 40 %. The colour now carries the transparency alone. PowerPoint's export of "Transparent 60% text" (red) is a light red that now matches. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/text/src/lib.rs | 5 +++-- crates/text/src/tests.rs | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 48a466f..e4e189c 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -54,7 +54,7 @@ pub struct GlyphRun { pub para: usize, pub chars: (usize, usize), pub link: bool, - /// Alpha of the fill (0–1). + /// Opacity of the fill on top of `color`'s own alpha (0 for no fill). pub alpha: f64, pub gradient: Option, /// Shadow and glow behind the glyphs (`a:rPr/a:effectLst`). @@ -269,7 +269,8 @@ fn make_style(ctx: &Ctx, r: &RunProps, scale: f64, prompt: Option) -> Styl let fake_italic = italic && !face.italic; let size = (r.size.unwrap_or(18.0) * scale).clamp(0.5, 4000.0); let (mut color, alpha, gradient) = match &r.fill { - Some(Fill::Solid { color }) => (ctx.color(color, None), color.alpha(), None), + // The resolved colour carries the fill's transparency (`a:alpha`): it is applied once. + Some(Fill::Solid { color }) => (ctx.color(color, None), 1.0, None), Some(Fill::Gradient(g)) => { // Colours resolved here (theme, colour map), so the renderer needs no colour scheme. let mut g = g.clone(); diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 12d80dc..58c6c7d 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -871,3 +871,17 @@ fn capitals_keep_characters_whose_capital_is_a_sequence() { assert_eq!(r.glyphs[1].0, face.glyph_for('ß'), "ß is not drawn as S"); } } + +#[test] +fn transparent_text_is_faded_once() { + let (p, mut sh) = setup("Faded", 2000.0, 300.0); + let run = &mut sh.text.as_mut().unwrap().paragraphs[0].runs[0]; + run.props.fill = Some(deckcraft_model::Fill::solid( + deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::rgb(255, 0, 0)).with(deckcraft_color::ColorTransform::Alpha(40000)), + )); + let l = lay(&p, &sh, 2000.0, 300.0); + let r = l.runs.iter().find(|r| !r.cells.is_empty()).unwrap(); + // 60 % transparency: 40 % opaque in all, not 40 % of 40 %. + let opacity = r.color.a as f64 / 255.0 * r.alpha; + assert!((opacity - 0.4).abs() < 0.01, "{opacity}"); +}