revealStyleFor function
- GptMarkdownAnimation effect,
- double progress,
- Color color, {
- Color? accent,
- double? fontSize,
The style an in-flight character is drawn with.
progress is 0 the moment the character is revealed and 1 when its
entrance is over. color is the colour it will settle into.
The result is a delta, applied over whatever the span already carries, so
an effect only names what it changes and inherits the rest. That is also
what makes GptMarkdownAnimation.blurIn safe: foreground and color
cannot both be set on one style, and TextStyle.merge drops the inherited
colour when the delta brings a paint.
Returns null when the character should be drawn exactly as it will settle, which the renderer takes as licence to skip styling it at all.
accent is the colour GptMarkdownAnimation.wave's crest lands in; the
widget passes the theme's ColorScheme.primary. Without one the crest is
derived from color, which on the near-grey text colours of most themes
is a grey — close to indistinguishable from GptMarkdownAnimation.fade.
fontSize is the text's laid-out size, which
GptMarkdownAnimation.blurIn scales its blur by, so the effect reads the
same at any size. Without one the blur is a fixed 3 logical pixels.
Implementation
TextStyle? revealStyleFor(
GptMarkdownAnimation effect,
double progress,
Color color, {
Color? accent,
double? fontSize,
}) {
if (progress >= 1) {
return null;
}
final t = progress.clamp(0.0, 1.0);
switch (effect) {
case GptMarkdownAnimation.none:
case GptMarkdownAnimation.typewriter:
return null;
case GptMarkdownAnimation.fade:
return TextStyle(color: color.withValues(alpha: color.a * t));
case GptMarkdownAnimation.blurIn:
// Opacity runs ahead of the blur. The other way round — blur strongest
// while the character is still nearly transparent — the blur was over
// before anyone could see it, and the effect read as a plain fade: by
// half opacity it was under a pixel. Here a word is two-thirds opaque
// while still clearly soft, then sharpens through the rest of its
// entrance, finishing as its opacity long since has.
final opacity = Curves.easeOut.transform((t * 1.6).clamp(0.0, 1.0));
final soft = 1 - t;
final peak = fontSize == null
? _blurSigma
: (fontSize * _blurPerEm).clamp(_minPeakSigma, _maxPeakSigma);
final sigma = peak * soft * soft;
final paint = Paint()..color = color.withValues(alpha: color.a * opacity);
if (sigma > _minSigma) {
paint.maskFilter = ui.MaskFilter.blur(ui.BlurStyle.normal, sigma);
}
// `foreground` and `color` are mutually exclusive in a TextStyle, and
// the base may carry a colour, so it is cleared here.
return TextStyle(color: null, foreground: paint);
case GptMarkdownAnimation.wave:
// Full opacity almost immediately — the motion readers see is the
// colour crest travelling, not letters materialising.
if (accent != null) {
final opacity = (t * 4).clamp(0.0, 1.0);
// Eased in, so a letter holds the accent for a moment before it
// settles: the crest is a band a few words long, not a flicker.
final crest =
Color.lerp(accent, color, Curves.easeIn.transform(t)) ?? color;
return TextStyle(color: crest.withValues(alpha: color.a * opacity));
}
final opacity = (t * 3).clamp(0.0, 1.0);
final crest = Color.lerp(_waveCrest(color), color, t) ?? color;
return TextStyle(color: crest.withValues(alpha: color.a * opacity));
}
}