Selkie.git | lib/Selkie/App/Internal/ | Animation.rakumod
=begin pod
=head1 NAME
Selkie::App::Internal::Animation - internal tween-group and animation-refcount role for Selkie::App
=head1 DESCRIPTION
Implementation detail composed by C<Selkie::App>. Use
C<Selkie::App.tweens>, C<Selkie::App.begin-animation>,
C<Selkie::App.end-animation>, and C<Selkie::App.animating> from
application code, and L<Selkie::Tween> for the animation primitives
themselves.
Two pieces of state live here:
=item The B<animation refcount> — an C<atomicint> that the render loop reads once per frame to decide whether to bypass the idle ladder. Held separately from the tween group so that non-interpolated animation (a spinner while a job runs) can pin the loop too.
=item The B<lazy tween group> — created on first use of C<tweens>, at which point exactly one C<'selkie-tweens'> frame callback is registered. Apps that never animate never allocate the group and never pay for the callback; apps that do pay one empty-array check per frame while idle.
The refcount is atomic because C<begin-animation> / C<end-animation>
are public API and a store handler running on a worker thread is an
entirely plausible caller. The tween group is not thread-safe and is
only ever touched from the app thread's frame callback.
=end pod
unit role Selkie::App::Internal::Animation;
use Selkie::Tween;
# > 0 means "the render loop must stay at the hot budget". Owned by
# this role, read once per frame by RenderLoop via App.animating.
has atomicint $!animation-refs;
has TweenGroup $!tween-group;
method !begin-animation(--> Int) {
($!animation-refs⚛++) + 1;
}
method !end-animation(--> Int) {
# Clamp at zero without ever letting a concurrent begin/end pair
# observe a negative count: re-read and retry rather than
# decrementing first and correcting afterwards.
loop {
my int $current = ⚛$!animation-refs;
return 0 if $current <= 0;
return $current - 1
if cas($!animation-refs, $current, $current - 1) == $current;
}
}
method !animating(--> Bool) {
⚛$!animation-refs > 0;
}
method !tween-group(--> TweenGroup) {
$!tween-group //= do {
my $group = TweenGroup.new(
on-active => { self!begin-animation },
on-idle => { self!end-animation },
);
# One callback for the whole app, registered once. Ticking an
# empty group is an array-length check, so the cost of having
# ever animated is negligible once everything has settled.
self.on-frame({ $group.tick }, name => 'selkie-tweens');
$group;
};
}