Wavy decoration should continue across element boundaries

#41287729 Filed 2016-11-23 New / P4

Problem

A text-decoration: wavy underline restarts its wave phase at every inline fragment boundary. Nesting an element like <em> or <strong> inside a decorated element visibly breaks the wave into segments, instead of drawing one continuous wave across the whole decorated text.

Blink paints text decorations per text fragment and anchored the wavy tile pattern at each fragment's own origin. Unless the fragment width happens to be a multiple of the wavelength, the phase jumps at the boundary.

Cases

The Rendered tab is your browser rendering the case live: on an unpatched Chrome the wave breaks at each nested element; on a patched build it matches Expected. The Expected tab is a capture from a patched build; Diff highlights the changed pixels (magenta) against the unpatched rendering.

Case 1: link with nested <em> (original repro)

live browser rendering; a phase jump at "nested em" is the failure
Expected: continuous wavy underline across the em boundary
Changed pixels along the underline, starting at the em boundary

Case 2: mixed <strong>/<em> run

THISisAtest.
live browser rendering; four phase jumps on an unpatched build
Expected: one continuous wave under THISisAtest.
Changed pixels along the underline

Case 3: multiple text nodes via spans

multiple separate text nodes
live browser rendering; spellcheck-style markup made of separate text nodes
Expected: one continuous green wave
Changed pixels along the underline

Before / after

Captures from chrome --headless=new at device scale factor 1, 2x nearest-neighbor crops around the underline.

Case 1

before wave restarts at the em boundary
Before: wavy underline breaks at the nested em
after continuous wave
After: continuous wavy underline

Case 2

before a seam at every element boundary
Before: wavy underline with seams
after continuous wave
After: continuous wavy underline

Case 3

before wave restarts at each text node
Before: wavy underline breaks between text nodes
after continuous wave
After: continuous wavy underline

Cause and fix

DecorationLinePainter::PaintWavyTextDecoration() paints the wave as a repeating tile translated to the fragment's own paint origin, so every fragment starts at phase zero. The CSS decorating box infrastructure (issue 1008951) already propagates the box that owns the decoration to each fragment; it was used for the vertical offset and thickness, but not for the pattern phase.

The fix computes each fragment's inline offset from the start of its decorating box (TextDecorationInfo::InlineOffsetFromDecoratingBox()) and shifts the wavy tile pattern by that amount modulo the wavelength, anchoring the wave at the decorating box instead of the fragment. Fragments of the same decorated run therefore share one continuous wave. Behind the default-on WavyDecorationContinuousPhase runtime feature; --disable-features=WavyDecorationContinuousPhase restores the old per-fragment phase as a kill switch.

<div style="text-decoration: wavy red underline;">THIS<strong>is</strong>A<em>test</em>.</div>

Tests

Unit test: InlinePaintContextTest.WavyDecorationPhaseContinuesAcrossFragments (third_party/blink/renderer/core/paint/inline_paint_context_test.cc).

WPT reftest: css/css-text-decor/text-decoration-wavy-phase-across-elements-001.html (fails on unpatched builds, passes with the fix).

Links

Issue 41287729 Standalone case 1 Standalone case 2 Standalone case 3