Skip to content

check passes when an overflow: hidden text mask cuts descenders in the resting state #5231

Description

@pixagentur

Describe the bug

The common line-mask reveal (text rises inside an overflow: hidden wrapper) leaves the wrapper as tall as the line box. With a tight line-height (display type is usually set at 0.9 to 1.1), the descenders of g/p/y/j and the umlaut or accent marks above capitals sit outside the line box, so the mask cuts them after the animation has finished, for the rest of the shot. hyperframes check reports nothing at the settled samples.

Two things combine:

  1. Since fix(layout): flag text a clipping card cuts off #4577 the clipping-card text check uses line-height bounds "to avoid treating the font's full ascent/descent as clipped display text". That avoids false positives, but it also means real descender ink outside the line box is never compared with the clipping ancestor.
  2. The sanctioned waiver for parked content is data-layout-allow-overflow on the mask (hyperframes-animation rules and blueprints recommend it for exactly this pattern). It is all-or-nothing over the timeline, so it also waives the resting state. In our real case the mask carried the waiver, check passed, three agent self-reviews of contact sheets missed it, and the client spotted the cut "g" in a screenshot.

Minimal reproduction

npx hyperframes init repro --non-interactive --example blank

Then replace the #title rule, the <h1> and the example tween in repro/index.html:

<style>
  .mask { overflow: hidden; line-height: 1; }
  #title { line-height: 1; white-space: nowrap; color: #f4f4f5; font-size: 200px; font-weight: 600; letter-spacing: -0.03em; }
</style>
...
<div id="name-mask" class="mask clip" data-start="0" data-duration="10" data-track-index="0"><h1 id="title">Betreuung</h1></div>
...
<script>
  const tl = gsap.timeline({ paused: true });
  tl.from("#title", { yPercent: 100, duration: 0.8, ease: "power3.out" }, 0.2);
  window.__timelines["main"] = tl;
  tl.seek(0);
</script>

(No data-layout-allow-overflow here, so this is the strictest case.)

Steps to reproduce

  1. cd repro && npx hyperframes check --json --at 2,5,9
  2. npx hyperframes snapshot --at 5 --no-end --zoom "#name-mask"

Expected behavior

At t = 2, 5, 9 the reveal has long finished. The tail of the "g" is cut by #name-mask, so check should report a clipped-text finding (e.g. text_box_overflow / clipped_text against #name-mask) at least at warning severity.

Actual behavior

check returns "ok": true with errorCount 0, warningCount 0, infoCount 0 for layout. The zoomed snapshot clearly shows the "g" descender cut off flat at the mask edge. During the entrance (t ≈ 0.56) check emits only info-level text_box_overflow/container_overflow, which is the expected transient.

A Range-rect probe (document.createRange().selectNodeContents(textNode).getBoundingClientRect(), i.e. font ascent..descent) against every ancestor with overflow != visible flags it at all settled times: glyph box 422..657 px vs mask 440..640 px. Padding the mask (padding-block: 0.2em; margin-block: -0.2em) makes the probe pass at settled times and only fail mid-entrance, which is correct.

Environment

✓ Version          0.8.141 (latest)
✓ Node.js          v24.18.0 (darwin arm64)
✓ FFmpeg           ffmpeg 6.0
✓ Chrome           bundled chrome-headless-shell 152.0.7977.30

Additional context

Suggested fixes, any of which would have caught it:

  1. Check: at samples where the subject is not moving relative to its clipping ancestor (settled), compare font ascent/descent (or measured ink bounds, which fix(layout): flag text a clipping card cuts off #4577 already computes for parked lines) with the clipping ancestor and report a cut. Keep the line-height exemption for moving/parked content only.
  2. Waiver scope: let data-layout-allow-overflow be limited to motion, e.g. data-layout-allow-overflow="transit", so the resting state is still audited; or document that the waiver hides resting clipping.
  3. Skills: in hyperframes-animation/techniques.md §12 (Clip-Path Reveal Masks) and the mask-reveal-up guidance, state that a text mask needs block padding of about 0.2em (descenders, umlauts, accents) with a compensating negative margin, or must release the clip (overflow: visible / clip-path: none) once the entrance settles.

Duplicate search (issues and PRs, open and closed): descender, descenders, glyph, "text clipping", "clipped text overflow", "mask reveal", "overflow hidden text", "ink bounds", text_occluded, "clip text check". Related but not covering this: #4577 (merged, see above), #2767 (closed, scrollHeight > clientHeight truncation, which does not fire because the line box fits), #3923 (closed, content_overlap trimming), #5207 (open, gradient text masks).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions