LibWeb: Use saveLayer compositing for background-clip: text

Instead of rendering text glyphs into a separate mask surface and using
clipShader, paint the backgrounds first and then composite the text
glyphs via saveLayer with SkBlendMode::kDstIn. Skia's saveLayer
automatically sizes its backing at device resolution including CSS
transforms, so no manual scale computation is needed.

Fixes pixelation when zooming in on clipped backgrounds on e.g. the
title of https://modern-css.com/.
This commit is contained in:
Jelle Raaijmakers 2026-02-17 12:59:11 +01:00 committed by Alexander Kalenik
parent 7c9b3c08fa
commit f2e6f70fbb
5 changed files with 53 additions and 10 deletions

View file

@ -87,20 +87,23 @@ void paint_background(DisplayListRecordingContext& context, PaintableBox const&
auto paint_into_isolated_group = any_of(resolved_background.layers, [](auto const& layer) {
return layer.blend_mode != CSS::MixBlendMode::Normal;
});
if (paint_into_isolated_group) {
display_list_recorder.save_layer();
}
bool is_root_element = paintable_box.layout_node().is_root_element();
bool needs_text_clip = resolved_background.needs_text_clip && !is_root_element;
RefPtr<DisplayList> text_clip_display_list;
Gfx::IntRect text_clip_rect;
if (needs_text_clip) {
text_clip_display_list = compute_text_clip_paths(context, paintable_box, resolved_background.background_rect.location());
text_clip_rect = context.rounded_device_rect(resolved_background.background_rect).to_type<int>();
display_list_recorder.save();
auto display_list = compute_text_clip_paths(context, paintable_box, resolved_background.background_rect.location());
auto rect = context.rounded_device_rect(resolved_background.background_rect);
display_list_recorder.add_mask(move(display_list), rect.to_type<int>(), Gfx::MaskKind::Alpha);
display_list_recorder.add_clip_rect(text_clip_rect);
display_list_recorder.save_layer();
}
if (paint_into_isolated_group)
display_list_recorder.save_layer();
BackgroundBox border_box {
resolved_background.background_rect,
border_radii
@ -323,10 +326,14 @@ void paint_background(DisplayListRecordingContext& context, PaintableBox const&
}
}
if (needs_text_clip) {
if (paint_into_isolated_group)
display_list_recorder.restore();
if (needs_text_clip) {
display_list_recorder.apply_effects(1.0f, Gfx::CompositingAndBlendingOperator::DestinationIn);
display_list_recorder.paint_nested_display_list(move(text_clip_display_list), text_clip_rect);
display_list_recorder.restore();
display_list_recorder.restore();
}
if (paint_into_isolated_group) {
display_list_recorder.restore();
}
}

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<style>
body { margin: 0; }
.scaled {
transform: scale(3);
transform-origin: left top;
}
.text {
font-size: 20px;
font-weight: 800;
color: red;
}
</style>
<div class="scaled">
<div class="text">Abcdefg</div>
</div>

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<meta name="fuzzy" content="maxDifference=0-17;totalPixels=0-1097" />
<link rel="match" href="../expected/background-clip-text-in-scaled-ancestor-ref.html" />
<style>
body { margin: 0; }
.scaled {
transform: scale(3);
transform-origin: left top;
}
.text {
font-size: 20px;
font-weight: 800;
color: transparent;
background: red;
background-clip: text;
}
</style>
<div class="scaled">
<div class="text">Abcdefg</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 246 KiB

View file

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="match" href="../expected/css-background-clip-text-ref.html" />
<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-27676">
<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-27725">
<style>
html {