Preserve authored alpha in shaped text
This commit is contained in:
1 parent
9455d18439
commit
53c0e64cfe
2 files changed
+129
-3
No files matched your search
@@ -1845,7 +1845,11 @@ impl TextEngine {
|
|||||||
let glyph_color = glyph.color_opt.unwrap_or(default);
|
let glyph_color = glyph.color_opt.unwrap_or(default);
|
||||||
let fill_index = metadata_fill(glyph.metadata);
|
let fill_index = metadata_fill(glyph.metadata);
|
||||||
let mut draw_pixel = |x, y, color: Color| {
|
let mut draw_pixel = |x, y, color: Color| {
|
||||||
let a = color.a() as u32;
|
// cosmic-text's mask rasterizer replaces the authored
|
||||||
|
// alpha with glyph coverage (see its `blend base alpha?`
|
||||||
|
// TODO). Reapply the span alpha here so transparent and
|
||||||
|
// translucent CSS text do not become opaque at paint.
|
||||||
|
let a = color.a() as u32 * glyph_color.a() as u32 / 255;
|
||||||
if a == 0 {
|
if a == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1924,8 +1928,12 @@ impl TextEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stroke the underline segments (opaque; text is already drawn above).
|
// Stroke underline segments with the same authored alpha as their
|
||||||
|
// glyphs. Transparent text decorations are transparent too.
|
||||||
for (x0, x1, y, thick, col) in underlines {
|
for (x0, x1, y, thick, col) in underlines {
|
||||||
|
if col[3] == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let t = thick.max(1.0).round() as i32;
|
let t = thick.max(1.0).round() as i32;
|
||||||
for dt in 0..t {
|
for dt in 0..t {
|
||||||
let py = oy as i32 + y as i32 + dt;
|
let py = oy as i32 + y as i32 + dt;
|
||||||
@@ -1947,7 +1955,23 @@ impl TextEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let i = (py * pw + px) as usize;
|
let i = (py * pw + px) as usize;
|
||||||
pixels[i] = tiny_skia::PremultipliedColorU8::from_rgba(col[0], col[1], col[2], 255).unwrap_or(pixels[i]);
|
let dst = pixels[i];
|
||||||
|
let sa = col[3] as u32;
|
||||||
|
let inv = 255 - sa;
|
||||||
|
let out_a = sa + dst.alpha() as u32 * inv / 255;
|
||||||
|
let out_r = col[0] as u32 * sa / 255
|
||||||
|
+ dst.red() as u32 * inv / 255;
|
||||||
|
let out_g = col[1] as u32 * sa / 255
|
||||||
|
+ dst.green() as u32 * inv / 255;
|
||||||
|
let out_b = col[2] as u32 * sa / 255
|
||||||
|
+ dst.blue() as u32 * inv / 255;
|
||||||
|
pixels[i] = tiny_skia::PremultipliedColorU8::from_rgba(
|
||||||
|
out_r as u8,
|
||||||
|
out_g as u8,
|
||||||
|
out_b as u8,
|
||||||
|
out_a as u8,
|
||||||
|
)
|
||||||
|
.unwrap_or(dst);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2590,6 +2614,44 @@ mod tests {
|
|||||||
(geometry, ink, cached_weights)
|
(geometry, ink, cached_weights)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shaped_text_preserves_authored_alpha_through_mask_rasterization() {
|
||||||
|
let render_alpha = |alpha: u8, underline: bool| {
|
||||||
|
let tree = obscura_dom::parse_html("<p id='copy'>Alpha</p>");
|
||||||
|
let copy = tree.get_element_by_id("copy").unwrap();
|
||||||
|
let mut style = LayoutStyle::default();
|
||||||
|
style.display = Display::Block;
|
||||||
|
style.font_size = Some(32.0);
|
||||||
|
style.line_height = Some(crate::LineHeight::Px(40.0));
|
||||||
|
style.color = Some([0, 0, 0, alpha]);
|
||||||
|
style.underline = Some(underline);
|
||||||
|
let styles = HashMap::from([(copy, style)]);
|
||||||
|
let mut engine = TextEngine::new();
|
||||||
|
let item = engine.try_build(&tree, copy, &styles).unwrap();
|
||||||
|
engine.finalize(item, (0.0, 0.0), 160.0, None);
|
||||||
|
let mut pixmap = tiny_skia::Pixmap::new(160, 48).unwrap();
|
||||||
|
engine.paint_item(item, &mut pixmap, (0.0, 0.0));
|
||||||
|
pixmap
|
||||||
|
.pixels()
|
||||||
|
.iter()
|
||||||
|
.map(|pixel| u64::from(pixel.alpha()))
|
||||||
|
.sum::<u64>()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
render_alpha(0, true),
|
||||||
|
0,
|
||||||
|
"transparent glyphs and decorations must not paint"
|
||||||
|
);
|
||||||
|
let half = render_alpha(128, false);
|
||||||
|
let opaque = render_alpha(255, false);
|
||||||
|
let ratio = half * 100 / opaque;
|
||||||
|
assert!(
|
||||||
|
(48..=52).contains(&ratio),
|
||||||
|
"50% CSS alpha must retain half the opaque coverage: half={half}, opaque={opaque}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn variable_wght_changes_shape_and_true_outline() {
|
fn variable_wght_changes_shape_and_true_outline() {
|
||||||
let (regular_geometry, regular_ink, regular_cache) = render_variable_weight(400);
|
let (regular_geometry, regular_ink, regular_cache) = render_variable_weight(400);
|
||||||
|
|||||||
@@ -5667,6 +5667,70 @@ mod tests {
|
|||||||
assert_eq!(red_pixels, 0, "::before must not bleed through ::after");
|
assert_eq!(red_pixels, 0, "::before must not bleed through ::after");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn angular_absolute_primary_button_paints_one_generated_label() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<html><head><style>
|
||||||
|
:root {
|
||||||
|
--orange-red:#f00; --vivid-pink:#f0f; --electric-violet:#70f;
|
||||||
|
--page-bg-radial-gradient:radial-gradient(circle,#fff 0%,#fff 100%);
|
||||||
|
--page-background:#fff; --primary-contrast:#111;
|
||||||
|
}
|
||||||
|
html, body { margin:0 }
|
||||||
|
.section { position:relative; width:500px; height:300px }
|
||||||
|
.content button { position:absolute; bottom:48px }
|
||||||
|
.docs-primary-btn {
|
||||||
|
cursor:pointer; border:none; outline:none; position:relative;
|
||||||
|
border-radius:4px; padding:12px 24px; width:max-content;
|
||||||
|
color:transparent; font-size:14px; font-weight:600;
|
||||||
|
background:linear-gradient(90deg,var(--orange-red) 0%,
|
||||||
|
var(--vivid-pink) 50%,var(--electric-violet) 100%);
|
||||||
|
}
|
||||||
|
.docs-primary-btn::before {
|
||||||
|
content:attr(text); position:absolute; inset:1px;
|
||||||
|
background:var(--page-bg-radial-gradient); border-radius:3px;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
color:var(--primary-contrast);
|
||||||
|
}
|
||||||
|
.docs-primary-btn::after {
|
||||||
|
content:attr(text); position:absolute; inset:1px;
|
||||||
|
background:var(--page-background); border-radius:3px;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
color:var(--primary-contrast);
|
||||||
|
}
|
||||||
|
</style></head><body>
|
||||||
|
<section class="section"><div class="content">
|
||||||
|
<button id="cta" class="docs-primary-btn" text="Learn more">Learn more</button>
|
||||||
|
</div></section>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
let laid = crate::layout_dom(&tree, (500.0, 300.0));
|
||||||
|
let cta = tree.get_element_by_id("cta").unwrap();
|
||||||
|
let rect = laid.rects[&cta];
|
||||||
|
let style = &laid.styles[&cta];
|
||||||
|
assert_eq!(style.color, Some([0, 0, 0, 0]));
|
||||||
|
let pixmap = paint_dom(&tree, (500.0, 300.0), None).expect("pixmap");
|
||||||
|
let rows = (rect.y.floor() as u32..(rect.y + rect.height).ceil() as u32)
|
||||||
|
.map(|y| {
|
||||||
|
(rect.x.floor() as u32..(rect.x + rect.width).ceil() as u32)
|
||||||
|
.filter(|&x| {
|
||||||
|
let pixel = pixmap.pixel(x, y).unwrap();
|
||||||
|
pixel.red() < 100 && pixel.green() < 100 && pixel.blue() < 100
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let ink_rows = rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(row, &ink)| (ink > 0).then_some(row))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert!(
|
||||||
|
ink_rows.last().unwrap() - ink_rows.first().unwrap() <= 14,
|
||||||
|
"only the generated label may paint; transparent host text leaked across rows {rows:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn native_select_paints_only_the_selected_label_and_arrow() {
|
fn native_select_paints_only_the_selected_label_and_arrow() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
Reference in new issue
Block a user