Skip to content

TextComponent glyphs jump up by one ascent when an OpacityEffect starts #4014

Description

@spydon

Description

The first time an OpacityEffect (or any HasPaint change) updates a TextComponent, the rendered glyphs teleport upward by one font ascent and stay there. Combined with a movement effect this reads as a sudden vertical jump in the middle of an otherwise smooth animation.

Related to #4013 but a distinct defect: that one is about shadow alpha, this one is about glyph positioning.

Cause

TextComponent.updateBounds translates the freshly formatted element down by its ascent:

@internal
void updateBounds() {
  _updateElement();
  final measurements = _textElement.metrics;
  _textElement.translate(0, measurements.ascent);
  size.setValues(measurements.width, measurements.height);
}

but TextComponent.onChanged, which runs on every paint mutation, rebuilds the element without that translation:

@override
void onChanged() {
  _textRenderer = _textRenderer.copyWithPaint(paint) as T;
  _updateElement();
}

So the element created before the first paint change is ascent-translated, and every element created after a paint change is not, shifting the drawn text up by metrics.ascent for the rest of the component's life.

Steps to reproduce

final text = TextComponent(
  text: '+1 kr',
  textRenderer: TextPaint(style: const TextStyle(fontSize: 24)),
)
  ..add(MoveByEffect(Vector2(0, -60), EffectController(duration: 0.8)))
  ..add(OpacityEffect.fadeOut(EffectController(duration: 0.5, startDelay: 0.3)));

The text rises smoothly for 0.3 seconds, then jumps up by roughly the ascent when the fade begins.

Expected behavior

Paint changes only affect paint; the glyphs stay where they are.

Suggested fix

Have onChanged apply the same ascent translation as updateBounds (or call a shared helper), for example:

@override
void onChanged() {
  _textRenderer = _textRenderer.copyWithPaint(paint) as T;
  _updateElement();
  _textElement.translate(0, _textElement.metrics.ascent);
}

Observed on the perf/component-set-backing branch; the implementation is the same on main.

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