Skip to content

Commit

Permalink
replace global variables usage with AnimationController
Browse files Browse the repository at this point in the history
Now each component has a pointer to an AnimationController. By default,
it is inherited from its parent (Container or Alignment).
This commit also makes animations more performant by making animation
updates happen on window frames instead of as often as possible, this
also reduces the load on the CPU.
  • Loading branch information
zenith391 committed Oct 29, 2024
1 parent 4de0b92 commit ddfa942
Show file tree
Hide file tree
Showing 14 changed files with 135 additions and 60 deletions.
2 changes: 1 addition & 1 deletion examples/7gui/counter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,6 @@ pub fn main() !void {
window.show();

// Count to 100 in 2000ms
count.animate(capy.Easings.InOut, 100, 2000);
count.animate(window.animation_controller, capy.Easings.InOut, 100, 2000);
capy.runEventLoop();
}
3 changes: 2 additions & 1 deletion examples/colors.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ pub fn animateRandomColor(button_: *anyopaque) !void {
const root = button.getRoot().?.as(capy.Container);
const rect = root.getChild("background-rectangle").?.as(capy.Rect);
const randomColor = capy.Color{ .red = random.int(u8), .green = random.int(u8), .blue = random.int(u8) };
rect.color.animate(capy.Easings.InOut, randomColor, 1000);
const animator = button.getAnimationController();
rect.color.animate(animator, capy.Easings.InOut, randomColor, 1000);
}

pub fn main() !void {
Expand Down
5 changes: 3 additions & 2 deletions examples/demo.zig
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,14 @@ fn moveButton(button_: *anyopaque) !void {
const button = @as(*capy.Button, @ptrCast(@alignCast(button_)));
const parent = button.getParent().?.as(capy.Alignment);

const animator = button.getAnimationController();
const alignX = &parent.x;
// Ensure the current animation is done before starting another
if (!alignX.hasAnimation()) {
if (alignX.get() == 0) { // if on the left
alignX.animate(capy.Easings.InOut, 1, 1000);
alignX.animate(animator, capy.Easings.InOut, 1, 1000);
} else {
alignX.animate(capy.Easings.InOut, 0, 1000);
alignX.animate(animator, capy.Easings.InOut, 0, 1000);
}
}
}
5 changes: 3 additions & 2 deletions examples/fade.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ var opacity = capy.Atom(f32).of(1.0);
// TODO: switch back to *capy.Button_Impl when ziglang/zig#12325 is fixed
fn startAnimation(button_: *anyopaque) !void {
const button = @as(*capy.Button, @ptrCast(@alignCast(button_)));
const animator = button.getAnimationController();

// Ensure the current animation is done before starting another
if (!opacity.hasAnimation()) {
if (opacity.get() == 0) { // if hidden
// Show the label in 1000ms
opacity.animate(capy.Easings.In, 1, 1000);
opacity.animate(animator, capy.Easings.In, 1, 1000);
button.setLabel("Hide");
} else {
// Hide the label in 1000ms
opacity.animate(capy.Easings.Out, 0, 1000);
opacity.animate(animator, capy.Easings.Out, 0, 1000);
button.setLabel("Show");
}
}
Expand Down
4 changes: 2 additions & 2 deletions examples/osm-viewer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,8 @@ pub const MapViewer = struct {
self.peer = try capy.backend.Canvas.create();
try self.setupEvents();
}
try self.centerX.implicitlyAnimate(&self.targetCenterX, capy.Easings.InOut, 50);
try self.centerY.implicitlyAnimate(&self.targetCenterY, capy.Easings.InOut, 50);
try self.centerX.implicitlyAnimate(&self.targetCenterX, self.getAnimationController(), capy.Easings.InOut, 50);
try self.centerY.implicitlyAnimate(&self.targetCenterY, self.getAnimationController(), capy.Easings.InOut, 50);

_ = try self.centerX.addChangeListener(.{ .function = onCenterChange, .userdata = self });
_ = try self.centerY.addChangeListener(.{ .function = onCenterChange, .userdata = self });
Expand Down
42 changes: 35 additions & 7 deletions src/AnimationController.zig
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub fn init(allocator: std.mem.Allocator, on_frame: *EventSource) !*AnimationCon
});
try listener.enabled.dependOn(.{&controller.animated_atoms.length}, &struct {
fn callback(length: usize) bool {
return length > 0;
return length >= 0;
}
}.callback);
controller.listener = listener;
Expand All @@ -39,12 +39,40 @@ pub fn init(allocator: std.mem.Allocator, on_frame: *EventSource) !*AnimationCon

fn update(ptr: ?*anyopaque) void {
const self: *AnimationController = @ptrCast(@alignCast(ptr.?));
var iterator = self.animated_atoms.iterate();
defer iterator.deinit();

while (iterator.next()) |item| {
if (item.fnPtr(item.userdata) == true) {
// TODO: remove.
// List of atoms that are no longer animated and that need to be removed from the list
var toRemove = std.BoundedArray(usize, 64).init(0) catch unreachable;
{
var iterator = self.animated_atoms.iterate();
defer iterator.deinit();
{
var i: usize = 0;
while (iterator.next()) |item| : (i += 1) {
if (item.fnPtr(item.userdata) == false) { // animation ended
toRemove.append(i) catch |err| switch (err) {
error.Overflow => {}, // It can be removed on the next call to animateAtoms()
};
}
}
}

// The following code is part of the same block as swapRemove relies on the caller locking
// the mutex
{
// The index list is ordered in increasing index order
const indexList = toRemove.constSlice();
// So we iterate it backward in order to avoid indices being invalidated
if (indexList.len > 0) {
var i: usize = indexList.len - 1;
while (i >= 0) {
_ = self.animated_atoms.swapRemove(indexList[i]);
if (i == 0) {
break;
} else {
i -= 1;
}
}
}
}
}
}
Expand All @@ -63,4 +91,4 @@ var null_animation_controller_instance = AnimationController{
/// This animation controller is never triggered. It is used by components that don't have a proper
/// animation controller.
/// It cannot be deinitialized.
pub var null_animation_controller = &null_animation_controller_instance;
pub const null_animation_controller = &null_animation_controller_instance;
10 changes: 0 additions & 10 deletions src/capy.zig
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,6 @@ pub fn init() !void {

Monitors.init();

var listener = eventStep.listen(.{ .callback = animateAtoms }) catch unreachable;
// The listener is enabled only if there is at least 1 atom currently being animated
listener.enabled.dependOn(.{&@import("data.zig")._animatedAtomsLength}, &struct {
fn a(num: usize) bool {
return num >= 1;
}
}.a) catch unreachable;

var timerListener = eventStep.listen(.{ .callback = @import("timer.zig").handleTimersTick }) catch unreachable;
// The listener is enabled only if there is at least 1 timer is running
timerListener.enabled.dependOn(.{&@import("timer.zig").runningTimers.length}, &struct {
Expand All @@ -130,8 +122,6 @@ pub fn deinit() void {
isCapyInitialized = false;
Monitors.deinit();

@import("data.zig")._animatedAtoms.deinit();
@import("data.zig")._animatedAtomsLength.deinit();
@import("timer.zig").runningTimers.deinit();

eventStep.deinitAllListeners();
Expand Down
12 changes: 12 additions & 0 deletions src/components/Alignment.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const internal = @import("../internal.zig");
const Size = @import("../data.zig").Size;
const Atom = @import("../data.zig").Atom;
const Widget = @import("../widget.zig").Widget;
const AnimationController = @import("../AnimationController.zig");

/// `Alignment` is a component used to align the enclosed component within the space
/// it's been given.
Expand Down Expand Up @@ -99,13 +100,24 @@ pub const Alignment = struct {
peer.move(widgetPeer, x, y);
peer.resize(widgetPeer, finalSize.width, finalSize.height);
}

_ = self.widget_data.atoms.animation_controller.addChangeListener(.{
.function = onAnimationControllerChange,
.userdata = self,
}) catch {};
onAnimationControllerChange(self.widget_data.atoms.animation_controller.get(), self);
}
}

pub fn getPreferredSize(self: *Alignment, available: Size) Size {
return self.child.get().getPreferredSize(available);
}

fn onAnimationControllerChange(new_value: *AnimationController, userdata: ?*anyopaque) void {
const self: *Alignment = @ptrCast(@alignCast(userdata));
self.child.get().animation_controller.set(new_value);
}

pub fn cloneImpl(self: *Alignment) !*Alignment {
_ = self;
// const widget_clone = try self.child.get().clone();
Expand Down
18 changes: 14 additions & 4 deletions src/containers.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const scratch_allocator = @import("internal.zig").scratch_allocator;
const lasting_allocator = @import("internal.zig").lasting_allocator;
const Size = @import("data.zig").Size;
const Rectangle = @import("data.zig").Rectangle;
const AnimationController = @import("AnimationController.zig");
const capy = @import("capy.zig");

const isErrorUnion = @import("internal.zig").isErrorUnion;
Expand Down Expand Up @@ -385,12 +386,13 @@ pub const Container = struct {

pub fn show(self: *Container) !void {
if (self.peer == null) {
_ = try self.widget_data.atoms.animation_controller.addChangeListener(.{ .function = onAnimationControllerChange, .userdata = self });
// Trigger the onAnimationControllerChange function now so that the current animation
// controller propagates to children
onAnimationControllerChange(self.widget_data.atoms.animation_controller.get(), self);

var peer = try backend.Container.create();
for (self.children.items) |widget| {
if (self.expand) {
widget.container_expanded = true;
}
widget.parent = self.asWidget();
try widget.show();
peer.add(widget.peer.?);
}
Expand Down Expand Up @@ -490,6 +492,7 @@ pub const Container = struct {
}

genericWidget.parent = self.asWidget();
genericWidget.animation_controller.set(self.widget_data.atoms.animation_controller.get());
genericWidget.ref();
try self.children.append(genericWidget);

Expand Down Expand Up @@ -538,6 +541,13 @@ pub const Container = struct {
self.children.deinit();
}

fn onAnimationControllerChange(newValue: *AnimationController, userdata: ?*anyopaque) void {
const self: *Container = @ptrCast(@alignCast(userdata));
for (self.children.items) |child| {
child.animation_controller.set(newValue);
}
}

pub fn cloneImpl(self: *Container) !*Container {
_ = self;
// var children = std.ArrayList(Widget).init(lasting_allocator);
Expand Down
61 changes: 34 additions & 27 deletions src/data.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const Container_Impl = @import("containers.zig").Container_Impl;
const internal = @import("internal.zig");
const lasting_allocator = internal.lasting_allocator;
const trait = @import("trait.zig");
const AnimationController = @import("AnimationController.zig");

/// Linear interpolation between floats a and b with factor t.
fn lerpFloat(a: anytype, b: @TypeOf(a), t: f64) @TypeOf(a) {
Expand Down Expand Up @@ -118,14 +119,6 @@ test isListAtom {
try std.testing.expect(!isListAtom(Rectangle));
}

// TODO: use ListAtom when it's done
pub var _animatedAtoms = std.ArrayList(struct {
fnPtr: *const fn (data: *anyopaque) bool,
userdata: *anyopaque,
}).init(lasting_allocator);
pub var _animatedAtomsLength = Atom(usize).of(0);
pub var _animatedAtomsMutex = std.Thread.Mutex{};

fn isAnimatableType(comptime T: type) bool {
if (trait.isNumber(T) or (comptime trait.isContainer(T) and @hasDecl(T, "lerp"))) {
return true;
Expand Down Expand Up @@ -208,6 +201,11 @@ pub fn Atom(comptime T: type) type {

fn computeChecksum(value: T) u8 {
const Crc = std.hash.crc.Crc8Wcdma;

// comptime causes a lot of problems with hashing, so we just set the checksum to
// 0, it's only used to detect potentially changed states, so it is not a problem.
if (@inComptime()) return 0;

return switch (@typeInfo(T).pointer.size) {
.One => Crc.hash(std.mem.asBytes(value)),
.Many, .C, .Slice => Crc.hash(std.mem.sliceAsBytes(value)),
Expand Down Expand Up @@ -258,7 +256,7 @@ pub fn Atom(comptime T: type) type {

/// Makes the atom follow the value of the given atom, but with an animation added.
/// Note that the animated atom is automatically destroyed when the original atom is destroyed.
pub fn implicitlyAnimate(self: *Self, original: *Self, easing: Easing, duration: u64) !void {
pub fn implicitlyAnimate(self: *Self, original: *Self, controller: *AnimationController, easing: Easing, duration: u64) !void {
self.set(original.get());
const AnimationParameters = struct {
easing: Easing,
Expand All @@ -267,6 +265,7 @@ pub fn Atom(comptime T: type) type {
original_ptr: *Self,
is_deinit: bool = false,
change_listener_id: usize,
controller: *AnimationController,
};

const userdata = try internal.lasting_allocator.create(AnimationParameters);
Expand All @@ -276,12 +275,13 @@ pub fn Atom(comptime T: type) type {
.self_ptr = self,
.original_ptr = original,
.change_listener_id = undefined,
.controller = controller,
};

const animate_fn = struct {
fn a(new_value: T, uncast: ?*anyopaque) void {
const ptr: *AnimationParameters = @ptrCast(@alignCast(uncast));
ptr.self_ptr.animate(ptr.easing, new_value, ptr.duration);
ptr.self_ptr.animate(ptr.controller, ptr.easing, new_value, ptr.duration);
}
}.a;

Expand Down Expand Up @@ -349,9 +349,9 @@ pub fn Atom(comptime T: type) type {

/// Starts an animation on the atom, from the current value to the `target` value. The
/// animation will last `duration` milliseconds.
pub fn animate(self: *Self, anim: *const fn (f64) f64, target: T, duration: u64) void {
pub fn animate(self: *Self, controller: *AnimationController, anim: *const fn (f64) f64, target: T, duration: u64) void {
if (comptime !isAnimatable) {
@compileError("animate() called on data that is not animable");
@compileError("animate() called on data that is not animatable");
}
const currentValue = self.get();
self.value = .{ .Animated = Animation(T){
Expand All @@ -362,19 +362,22 @@ pub fn Atom(comptime T: type) type {
.animFn = anim,
} };

var contains = false;
_animatedAtomsMutex.lock();
defer _animatedAtomsMutex.unlock();

for (_animatedAtoms.items) |item| {
if (@as(*anyopaque, @ptrCast(self)) == item.userdata) {
contains = true;
break;
const is_already_animated = blk: {
var iterator = controller.animated_atoms.iterate();
defer iterator.deinit();
while (iterator.next()) |item| {
if (@as(*anyopaque, @ptrCast(self)) == item.userdata) {
break :blk true;
}
}
}
if (!contains) {
_animatedAtoms.append(.{ .fnPtr = @as(*const fn (*anyopaque) bool, @ptrCast(&Self.update)), .userdata = self }) catch {};
_animatedAtomsLength.set(_animatedAtoms.items.len);
break :blk false;
};

if (!is_already_animated) {
controller.animated_atoms.append(.{
.fnPtr = @as(*const fn (*anyopaque) bool, @ptrCast(&Self.update)),
.userdata = self,
}) catch {};
}
}

Expand Down Expand Up @@ -1238,10 +1241,14 @@ test "animated atom" {
defer original.deinit();

{
var animated = try Atom(i32).withImplicitAnimation(&original, Easings.Linear, 5000);
const animation_controller = AnimationController.null_animation_controller;
var animated = try Atom(i32).withImplicitAnimation(
animation_controller,
&original,
Easings.Linear,
5000,
);
defer animated.deinit();
defer _animatedAtoms.clearAndFree();
defer _animatedAtomsLength.set(0);

original.set(1000);
try std.testing.expect(animated.hasAnimation());
Expand Down
Loading

0 comments on commit ddfa942

Please sign in to comment.