offsetAt method
Displacement at elapsed time since the most recent shake call.
Exactly Offset.zero before any shake has ever been triggered, and
again once elapsed reaches _decay (clamped, not asymptotic) — a
live-driven ScreenShake uses this to know when it can stop ticking.
No direction given to shake (the default): primary uses sin,
secondary uses cos at a different phase, mapped straight onto x/y —
this traces a symmetric ellipse, not a straight diagonal line, and is
byte-for-byte the same formula this method always used (IDEA-60 never
changes this path).
With a direction: the SAME primary oscillation now runs fully
ALONG that (unit) vector, and secondary runs perpendicular to it —
scaled down by _perpendicularFactor — so the shake's peak
displacement is visibly biased along the collision direction instead
of tracing a shape-agnostic ellipse.
Implementation
Offset offsetAt(Duration elapsed) {
if (elapsed <= Duration.zero) elapsed = Duration.zero;
if (_decay <= Duration.zero || elapsed >= _decay) return Offset.zero;
final t = elapsed.inMicroseconds / 1e6;
final decaySeconds = _decay.inMicroseconds / 1e6;
// ponytail: naive exponential envelope tuned so it's ~e^-4 (~1.8% of
// intensity) by the end of `decay`, not a physically modeled impulse
// response — good enough for a juice effect, revisit if a designer
// wants a different "feel" curve.
final envelope = math.exp(-4 * t / decaySeconds);
final primary =
_intensity * envelope * math.sin(2 * math.pi * _frequency * t);
final secondary =
_intensity * envelope * math.cos(2 * math.pi * _frequency * 1.3 * t);
final dir = _direction;
if (dir == null) return Offset(primary, secondary);
final perpendicular = Offset(-dir.dy, dir.dx);
return dir * primary + perpendicular * (secondary * _perpendicularFactor);
}