ai_wave_animations

ineelakandan.in · Source · Issues

The gallery app: fourteen styles reacting to one shared controller

The example app: every style driven by one WaveController — switch activity, drag the level, hit pause.

Assistant-style wave animations for Flutter — the kind of motion you see in Gemini, Claude, Perplexity and ChatGPT voice UIs.

Fourteen ready-made looks, every knob exposed, one optional controller to drive them all. Pure CustomPaint, no plugins, no native code: the same widget runs on Android, iOS, web, Windows, macOS and Linux.

Assistant flavours

Widget Look Best for
GeminiWave Layered multi-hue sine ribbons with a bloom Hero banners, thinking state
ClaudeWave Slow warm overlapping surfaces Calm idle, backgrounds
PerplexityWave Rounded travelling equaliser bars Live microphone level
ChatGptWave Breathing orb with a rippling silhouette Voice mode, compact avatars
GeminiWave ClaudeWave
GeminiWave ClaudeWave
PerplexityWave ChatGptWave
PerplexityWave ChatGptWave

Frequency visualisers

Ten more looks, each built around a different idea of what a "frequency" is — rings per second, cycles around a circle, twists across the width, revolutions of a radar sweep.

Widget Look Frequency means
PulseRingWave Rings expanding from a pulsing core Rings emitted per second
RadialBarsWave Circular equaliser around an open centre Cycles around the circle
OscilloscopeWave AM trace on a graticule with phosphor ghosts Carrier cycles across width
SpiralWave Wave-modulated vortex arms Cycles along one arm
LiquidWave Gooey metaball blob with a wet highlight Attractor orbit rate
HelixWave Rotating double helix with depth-sorted strands Twists across the width
ParticleWave Sparks riding a wave field, each with a trail Field cycles across width
GridWave Synthwave terrain rushing at the viewer Terrain cycles across width
AuroraWave Light curtains drifting past each other Curtain folds across width
SonarWave Radar sweep with contacts that flare and fade Revolutions per second
PulseRingWave RadialBarsWave SpiralWave
PulseRingWave RadialBarsWave SpiralWave
LiquidWave SonarWave GridWave
LiquidWave SonarWave GridWave
OscilloscopeWave HelixWave ParticleWave
OscilloscopeWave HelixWave ParticleWave
AuroraWave
AuroraWave

Still frames; regenerate them with flutter test tool/render_previews.dart (and flutter test tool/render_play_buttons.dart --update-goldens for the button strip).

Install

dependencies:
  ai_wave_animations: ^0.2.0
import 'package:ai_wave_animations/ai_wave_animations.dart';

Quick start

GeminiWave(height: 96)

That is the whole thing. Everything below is optional.

Driving the animation

Three ways, from simplest to most dynamic.

1. A static state. Amplitude and speed follow the conversation phase:

ClaudeWave(activity: WaveActivity.thinking)
WaveActivity Intensity Speed Reacts to level
idle 0.22 0.45× no
listening 0.70 0.85× yes
thinking 0.85 1.35× no
speaking 1.00 1.00× yes

2. A fixed level. Useful for previews and tests:

PerplexityWave(activity: WaveActivity.listening, level: 0.8)

3. A controller. Feed it live audio and switch states at runtime. One controller can drive any number of widgets in sync:

final controller = WaveController(activity: WaveActivity.listening);

micStream.listen((rms) => controller.level = rms); // 0.0 .. 1.0

PerplexityWave(controller: controller);
ChatGptWave(controller: controller);

// later
controller.setActivity(WaveActivity.speaking);
controller.pause();

level is clamped, NaN-guarded and exponentially smoothed inside the widget, so raw noisy microphone data is safe to push straight in at any rate.

Switching looks at runtime

AiWave(
  style: AiWaveStyle.perplexity, // any of the fourteen
  height: 72,
  controller: controller,
)

AiWave exposes only the parameters every style shares; anything left null keeps that style's own default. Use the dedicated widget when you need its style-specific knobs.

Inside a play button

WavePlayButton wraps any style in a round, tappable button with a play glyph that morphs into a pause glyph — and stops the wave with it.

WavePlayButton

WavePlayButton(
  style: AiWaveStyle.radialBars,
  size: 132,
  controller: controller, // optional: tap toggles controller.isAnimating
)
Parameter Default Meaning
style AiWaveStyle.radialBars Which wave fills the button
size 128.0 Diameter in logical pixels
controller null Tap toggles it; the glyph follows it
isPlaying null Drives the state from outside instead
initiallyPlaying true Starting state when the button owns it
onPressed null Called on tap, after the state flips
onPlayingChanged null Called with the new state on every tap
iconColor colors.first Glyph colour
iconSize 0.3 Glyph size as a fraction of size
borderWidth 0.0 Rim width; 0 hides it
wavePadding zero Inset between the rim and the wave
morphDuration 320ms Play → pause morph length
pressScale 0.93 How far it shrinks while held

The circular styles (radialBars, pulseRing, sonar, spiral, liquid) are the ones designed for this; the rest work too, just cropped to the circle.

Parameters

Shared by every widget

Parameter Type Default Meaning
width / height double? per style null stretches to the parent constraint
colors List<Color> brand palette Any length; sampled or swept as the style requires
speed double 1.0 Time multiplier; 0 freezes
paused bool false Stops the ticker, keeps the last frame
controller WaveController? null External driver; overrides activity and level
activity WaveActivity? idle Conversation phase
level double? null Static amplitude drive, 0..1
backgroundColor Color? null Solid backdrop
backgroundGradient Gradient? null Gradient backdrop; wins over backgroundColor
borderRadius BorderRadiusGeometry? null Rounds the backdrop
padding EdgeInsetsGeometry zero Inset between box and wave
clipBehavior Clip per style Applied when a backdrop or radius is set
respectReducedMotion bool true Freezes to a still frame when the OS asks
semanticLabel String? 'Animated wave' null hides it from screen readers
child Widget? null Stacked on top of the wave

GeminiWave

Parameter Default Meaning
waveCount 4 Stacked ribbons
amplitude 0.55 Peak as a fraction of the available half-height
frequency 1.4 Sine cycles across the width
strokeWidth 3.0 Front ribbon width; back layers get thinner
phaseShift 0.12 Phase offset per layer, in turns
taper 1.0 Edge-pin exponent; 0 disables the taper
glow 0.45 Bloom strength 0..1; 0 skips the blurred pass
sweepSpeed 0.12 Gradient scroll, turns per second (negative reverses)
harmonics 3 Sine harmonics summed per ribbon; 1 is a pure sine
turbulence 0.06 Organic noise 0..1
minOpacity 0.35 Opacity of the furthest back ribbon
mirror true Flips every other layer for a woven look
verticalAlignment 0.0 -1 top, 0 centre, 1 bottom
strokeCap StrokeCap.round Ribbon end cap
blendMode BlendMode.srcOver BlendMode.plus gives a neon look on dark backgrounds
resolution 3.0 Logical pixels between samples; lower is smoother, costlier

ClaudeWave

Parameter Default Meaning
waveCount 3 Stacked surfaces
amplitude 0.12 Crest height as a fraction of the widget height
frequency 1.1 Sine cycles across the width
waterLevel 0.55 Fill height from the bottom, 0..1
style ClaudeWaveStyle.fill fill, stroke or fillAndStroke
strokeWidth 2.0 Crest line width
phaseShift 0.18 Phase offset per surface, in turns
layerOffset 0.045 Vertical stagger per surface, fraction of height
minOpacity 0.25 Back surface opacity
maxOpacity 0.9 Front surface opacity
fadeToBottom true Fades each fill downwards instead of a flat colour
alternateDirection true Sends every other surface the other way
turbulence 0.05 Organic noise 0..1
taper 0.0 0 full width, 1 pinned to both edges
resolution 3.0 Logical pixels between samples

PerplexityWave

Parameter Default Meaning
barCount 24 Number of bars
barWidth null Fixed width; null divides evenly
barSpacing 5.0 Gap between bars
barRadius null Corner radius; null gives pills
minBarHeight 0.08 Shortest bar, fraction of height
maxBarHeight 1.0 Tallest bar, fraction of height
frequency 1.6 Wave cycles across the row
alignment PerplexityBarAlignment.center center, bottom or top
gradientAcrossBars true One scrolling gradient vs per-bar colours
sweepSpeed 0.1 Gradient scroll, turns per second
turbulence 0.12 Per-bar noise 0..1
taperEdges true Shrinks bars near both ends
glow 0.0 Bloom strength 0..1
opacity 1.0 Overall opacity multiplier

ChatGptWave

Parameter Default Meaning
radiusFactor 0.72 Radius as a fraction of half the shortest side
amplitude 0.10 Silhouette ripple depth, fraction of the radius
lobes 3 Bumps around the circumference
layers 3 Concentric blobs
rotationSpeed 0.25 Ripple drift, turns per second (negative flips)
breathing 0.05 Depth of the slow scale pulse
turbulence 0.18 Organic noise 0..1
glow 0.5 Bloom strength 0..1
ringWidth 0.0 Halo ring width; 0 hides it
ringColor null Defaults to colors.first
ringGap 10.0 Distance from the orb edge to the ring
opacity 1.0 Overall opacity multiplier
segments 96 Points around the circumference
center Alignment.center Where the orb sits in the box

PulseRingWave

Parameter Default Meaning
ringCount 4 Rings in flight at once
frequency 1.0 Rings emitted per second (2.0 is 120 bpm)
coreRadius 0.28 Pulsing core radius, fraction of the half-size
startRadius 0.18 Radius a ring is born at
strokeWidth 2.5 Width of a fresh ring; rings thin as they travel
wobble 0.05 How far rings deform from a circle
lobes 3 Lobes in that deformation
easing 0.75 >1 shoots out and coasts, <1 accelerates
glow 0.5 Bloom strength 0..1
turbulence 0.08 Organic noise 0..1
center Alignment.center Where rings are emitted from
segments 72 Samples per ring when wobble > 0

RadialBarsWave

Parameter Default Meaning
barCount 56 Bars around the full circle
innerRadius 0.52 Radius the bars stand on; the centre stays free
barLength 1.0 Length of a peaking bar
minBarLength 0.12 Length of a resting bar
barWidth 3.0 Bar thickness
frequency 3.0 Cycles around the circle; integers meet cleanly
harmonics 3 Sines summed per bar; 1 is pure
rotationSpeed 0.04 Ring rotation, turns per second
mirror true Folds the pattern like a real analyser
startAngle -π/2 Angle of the first bar (12 o'clock)
ringWidth 1.0 Ring the bars stand on; 0 hides it
glow 0.4 Bloom strength 0..1
turbulence 0.1 Organic noise 0..1

OscilloscopeWave

Parameter Default Meaning
frequency 3.5 Carrier cycles across the width
amplitude 0.7 Peak height, fraction of the half-height
modulation 0.55 AM depth; 0 is a plain carrier
modulationFrequency 0.75 Envelope cycles across the width
strokeWidth 2.0 Live trace width
persistence 4 Phosphor ghosts behind the trace
persistenceGap 0.055 Seconds of wave time between ghosts
gridDivisions 10 Vertical graticule divisions; 0 hides it
gridOpacity 0.18 Graticule opacity
beamDotRadius 2.5 Dot riding the beam; 0 hides it
taper 0.0 0 lets the trace run off both edges
glow 0.5 Bloom strength 0..1
resolution 2.5 Logical pixels between samples

SpiralWave

Parameter Default Meaning
armCount 3 Arms spaced evenly around the circle
turns 2.2 Revolutions from centre to edge
frequency 5.0 Wave cycles along one arm
amplitude 0.06 Radial wave depth, fraction of the half-size
rotationSpeed 0.09 Vortex spin, turns per second
waveSpeed 1.4 How fast the wave travels outwards
strokeWidth 5.0 Width at the centre
tipWidth 0.6 Width at the tip
innerRadius 0.08 Radius an arm starts at
turbulence 0.08 Organic noise 0..1
glow 0.45 Bloom strength 0..1
blendMode BlendMode.srcOver plus for neon on dark
segments 160 Segments per arm

LiquidWave

Parameter Default Meaning
blobCount 5 Attractors deforming the surface
radiusFactor 0.62 Resting radius, fraction of the half-size
frequency 0.16 Attractor orbit rate, turns per second
pull 0.34 How hard an attractor pushes the surface out
spread 0.07 Bump width in turns: small gives distinct fingers
orbit 0.5 Rate at which attractor strength breathes
highlight 0.35 Specular highlight strength; 0 hides it
rimWidth 0.0 Rim light around the silhouette
turbulence 0.04 Organic noise 0..1
glow 0.5 Bloom strength 0..1
segments 128 Samples around the silhouette

HelixWave

Parameter Default Meaning
strandCount 2 2 is the classic double helix; 3+ braids
frequency 2.0 Full twists across the width
amplitude 0.62 Peak height, fraction of the half-height
strokeWidth 3.5 Strand width at its nearest point
rungCount 24 Rungs joining the strands; 0 hides them
rungWidth 1.6 Rung stroke width
rungOpacity 0.55 Opacity of a fully open rung
nodeRadius 2.6 Dots where rungs meet a strand; 0 hides them
depthContrast 0.6 How much dimmer and thinner the far side gets
taper 0.0 0 lets the helix run off both edges
glow 0.35 Bloom strength 0..1

ParticleWave

Parameter Default Meaning
particleCount 70 Sparks alive at once
frequency 1.6 Field cycles across the width
amplitude 0.5 Peak height, fraction of the half-height
flowSpeed 1.0 How fast sparks drift across
spread 0.18 Vertical scatter around the wave
particleRadius 2.2 Radius of an average spark
trailLength 0.35 Trail length as a fraction of the width
trailWidth 0.8 Trail thickness relative to the radius
turbulence 0.12 Organic noise 0..1
taper 0.6 Also fades sparks in and out at the edges
glow 0.55 Bloom strength 0..1
blendMode BlendMode.srcOver plus makes overlaps burn
seed 7 Re-rolls the particle layout

GridWave

Parameter Default Meaning
rows 18 Horizontal rows in flight
columns 16 Rails converging on the vanishing point
horizon 0.42 Horizon height, 0 at the top
perspective 2.6 Depth curve; higher bunches rows at the horizon
frequency 1.5 Terrain cycles across the width
amplitude 0.16 Terrain height, fraction of the ground area
scrollSpeed 0.12 Depth units per second towards the viewer
lineWidth 1.2 Width at the horizon; near rows get thicker
horizonGlow 0.5 Light band on the horizon; 0 hides it
glow 0.45 Bloom strength 0..1
blendMode BlendMode.srcOver plus for the neon look
resolution 6.0 Logical pixels between samples along a row

AuroraWave

Parameter Default Meaning
bandCount 4 Curtains stacked front to back
frequency 0.9 Curtain folds across the width
amplitude 0.35 Fold depth, fraction of the height
bandHeight 0.55 How far a curtain rises from its hem
drift 0.35 How fast the folds travel sideways
turbulence 0.28 Raggedness of the hem 0..1
softness 0.45 Edge blur; 0 gives hard-edged ribbons
baseline 0.45 Vertical anchor, 0 at the top
opacity 0.85 Below 1 keeps additive stacking from blowing out
blendMode BlendMode.plus Overlaps brighten

SonarWave

Parameter Default Meaning
frequency 0.4 Sweep rate in revolutions per second
blips 6 contacts SonarBlip(angle, distance, size, drift)
ringCount 4 Range rings
ringWidth 1.0 Ring and crosshair stroke width
sweepWidth 2.0 Sweep arm width; 0 hides it
trailTurns 0.3 Afterglow wedge length, in turns
crosshair true Draws the horizontal and vertical crosshair
gridOpacity 0.35 Rings and crosshair opacity; 0 hides them
blipRadius 3.0 Radius of a resting contact
afterglow 0.28 Wedge brightness; 0 hides it
glow 0.45 Bloom strength 0..1
center Alignment.center Where the scope sits in the box

Palettes

WavePalette ships the brand defaults, but colors accepts any list:

GeminiWave(colors: const [Color(0xFF00E5FF), Color(0xFF7C4DFF)]);
ClaudeWave(colors: Theme.of(context).colorScheme.primary.let(...));

Available: WavePalette.gemini, geminiClassic, claude, claudePaper, claudeInk, perplexity, perplexityInk, chatGpt, chatGptGreen, chatGptInk, pulse, spectrum, oscilloscope, scopeInk, spiral, helix, particles, liquid, grid, gridInk, aurora, auroraInk, sonar.

Performance notes

  • Nothing repaints while paused is true, while a controller is paused, or while the OS requests reduced motion — the ticker is stopped, not throttled.
  • glow is the only expensive parameter (it adds a blurred pass). Set it to 0 on low-end devices and on web if you see frame drops.
  • resolution trades smoothness for cost on the path-based styles. 3.0 is a good default; 6.0 roughly halves the sample count.
  • segments around 96 is plenty for the closed-shape styles (ChatGptWave, LiquidWave, PulseRingWave); higher values are rarely distinguishable.
  • particleCount, barCount, rows and persistence are the count knobs on the visualisers; halving one halves that style's draw calls.

Accessibility

Every widget carries a semanticLabel (pass null to hide it) and honours MediaQuery.disableAnimations, rendering a single still frame instead of animating. Set respectReducedMotion: false to opt out.

Building your own style

WaveCanvas is exported. It owns the ticker, level smoothing, activity envelope, sizing, backdrop and accessibility, and hands your painter a WaveFrame each tick:

WaveCanvas(
  height: 100,
  controller: controller,
  painterBuilder: (context, frame) => MyPainter(frame: frame),
);

WaveMath (sinTurns, edgeTaper, smoothTowards, smoothPath, smoothClosedPath) and ValueNoise are exported too.

Example app

cd example
flutter run           # or: -d chrome, -d windows, -d macos, -d linux

The gallery has a page per assistant style with every parameter on a live slider, a Visualisers page with all ten new looks and the play buttons, a shared activity bar and a simulated microphone feed.

Author

Built and maintained by Neelakandan.

License

MIT — see LICENSE. © Neelakandan.

Libraries

ai_wave_animations
Assistant-style wave animations for Flutter.