From 1461d029db5d227d6c4514096c24925e35c7b1e1 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 19 Feb 2026 09:41:24 +0100 Subject: [PATCH 1/5] Improve Animation support: async update from idle => running => finished --- src/browser/tests/animation/animation.html | 62 ++++++++- src/browser/webapi/animation/Animation.zig | 152 +++++++++++++++++++-- 2 files changed, 201 insertions(+), 13 deletions(-) diff --git a/src/browser/tests/animation/animation.html b/src/browser/tests/animation/animation.html index 27e562a0..97bfe077 100644 --- a/src/browser/tests/animation/animation.html +++ b/src/browser/tests/animation/animation.html @@ -3,13 +3,67 @@ + + + + + + + + diff --git a/src/browser/webapi/animation/Animation.zig b/src/browser/webapi/animation/Animation.zig index 2b6855ac..a9443d90 100644 --- a/src/browser/webapi/animation/Animation.zig +++ b/src/browser/webapi/animation/Animation.zig @@ -16,40 +16,113 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); +const log = @import("../../../log.zig"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); +const Allocator = std.mem.Allocator; + const Animation = @This(); +const PlayState = enum { + idle, + running, + paused, + finished, +}; + +_page: *Page, +_arena: Allocator, + _effect: ?js.Object.Global = null, _timeline: ?js.Object.Global = null, _ready_resolver: ?js.PromiseResolver.Global = null, _finished_resolver: ?js.PromiseResolver.Global = null, +_startTime: ?f64 = null, +_onFinish: ?js.Function.Temp = null, +_playState: PlayState = .idle, +// Fake the animation by passing the states: +// .idle => .running once play() is called. +// .running => .finished after 10ms when update() is callback. +// +// TODO add support for effect and timeline pub fn init(page: *Page) !*Animation { - return page._factory.create(Animation{}); + const arena = try page.getArena(.{ .debug = "Animation" }); + errdefer page.releaseArena(arena); + + const self = try page._factory.create(Animation{ + ._page = page, + ._arena = arena, + }); + + return self; } -pub fn play(_: *Animation) void {} -pub fn pause(_: *Animation) void {} -pub fn cancel(_: *Animation) void {} -pub fn finish(_: *Animation) void {} -pub fn reverse(_: *Animation) void {} +pub fn play(self: *Animation, page: *Page) !void { + if (self._playState == .running) { + return; + } + + // transition to running. + self._playState = .running; + + // Schedule the transition from .idle => .running in 10ms. + page.js.strongRef(self); + try page.js.scheduler.add( + self, + Animation.update, + 10, + .{ .name = "animation.update" }, + ); +} + +pub fn pause(self: *Animation) void { + self._playState = .paused; +} + +pub fn cancel(_: *Animation) void { + log.warn(.not_implemented, "Animation.cancel", .{}); +} + +pub fn finish(self: *Animation, page: *Page) void { + if (self._playState == .finished) { + return; + } + + self._playState = .finished; + + // resolve finished + if (self._finished_resolver) |resolver| { + page.js.local.?.toLocal(resolver).resolve("Animation.getFinished", self); + } + // call onfinish + if (self._onFinish) |func| { + page.js.local.?.toLocal(func).call(void, .{}) catch |err| { + log.warn(.js, "Animation._onFinish", .{ .err = err }); + }; + } +} + +pub fn reverse(_: *Animation) void { + log.warn(.not_implemented, "Animation.reverse", .{}); +} pub fn getFinished(self: *Animation, page: *Page) !js.Promise { if (self._finished_resolver == null) { const resolver = page.js.local.?.createPromiseResolver(); - resolver.resolve("Animation.getFinished", self); self._finished_resolver = try resolver.persist(); return resolver.promise(); } return page.js.toLocal(self._finished_resolver).?.promise(); } +// The ready promise is immediately resolved. pub fn getReady(self: *Animation, page: *Page) !js.Promise { - // never resolved, because we're always "finished" if (self._ready_resolver == null) { const resolver = page.js.local.?.createPromiseResolver(); + resolver.resolve("Animation.getReady", self); self._ready_resolver = try resolver.persist(); return resolver.promise(); } @@ -72,6 +145,63 @@ pub fn setTimeline(self: *Animation, timeline: ?js.Object.Global) !void { self._timeline = timeline; } +pub fn getStartTime(self: *const Animation) ?f64 { + return self._startTime; +} + +pub fn setStartTime(self: *Animation, value: ?f64, page: *Page) !void { + self._startTime = value; + return self.play(page); +} + +pub fn getOnFinish(self: *const Animation) ?js.Function.Temp { + return self._onFinish; +} + +pub fn deinit(self: *Animation, _: bool) void { + self._page.releaseArena(self._arena); +} + +// callback function transitionning from a state to another +fn update(ctx: *anyopaque) anyerror!?u32 { + const self: *Animation = @ptrCast(@alignCast(ctx)); + + switch (self._playState) { + .running => { + // transition to finished. + self._playState = .finished; + + var ls: js.Local.Scope = undefined; + self._page.js.localScope(&ls); + defer ls.deinit(); + + // resolve finished + if (self._finished_resolver) |resolver| { + ls.toLocal(resolver).resolve("Animation.getFinished", self); + } + // call onfinish + if (self._onFinish) |func| { + ls.toLocal(func).call(void, .{}) catch |err| { + log.warn(.js, "Animation._onFinish", .{ .err = err }); + }; + } + }, + .idle, .paused, .finished => {}, + } + + // No future change scheduled, set the object weak for garbage collection. + self._page.js.weakRef(self); + return null; +} + +pub fn setOnFinish(self: *Animation, cb: ?js.Function.Temp) !void { + self._onFinish = cb; +} + +pub fn playState(self: *const Animation) []const u8 { + return @tagName(self._playState); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Animation); @@ -79,6 +209,8 @@ pub const JsApi = struct { pub const name = "Animation"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const weak = true; + pub const finalizer = bridge.finalizer(Animation.deinit); }; pub const play = bridge.function(Animation.play, .{}); @@ -86,12 +218,14 @@ pub const JsApi = struct { pub const cancel = bridge.function(Animation.cancel, .{}); pub const finish = bridge.function(Animation.finish, .{}); pub const reverse = bridge.function(Animation.reverse, .{}); - pub const playState = bridge.property("finished", .{ .template = false }); + pub const playState = bridge.accessor(Animation.playState, null, .{}); pub const pending = bridge.property(false, .{ .template = false }); pub const finished = bridge.accessor(Animation.getFinished, null, .{}); pub const ready = bridge.accessor(Animation.getReady, null, .{}); pub const effect = bridge.accessor(Animation.getEffect, Animation.setEffect, .{}); pub const timeline = bridge.accessor(Animation.getTimeline, Animation.setTimeline, .{}); + pub const startTime = bridge.accessor(Animation.getStartTime, Animation.setStartTime, .{}); + pub const onfinish = bridge.accessor(Animation.getOnFinish, Animation.setOnFinish, .{}); }; const testing = @import("../../../testing.zig"); From 5248b9fc6f52ba6d0bd4dc9984929ed2d67bcd11 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 19 Feb 2026 10:35:46 +0100 Subject: [PATCH 2/5] Update src/browser/webapi/animation/Animation.zig Co-authored-by: Karl Seguin --- src/browser/webapi/animation/Animation.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/webapi/animation/Animation.zig b/src/browser/webapi/animation/Animation.zig index a9443d90..04df7e90 100644 --- a/src/browser/webapi/animation/Animation.zig +++ b/src/browser/webapi/animation/Animation.zig @@ -163,7 +163,7 @@ pub fn deinit(self: *Animation, _: bool) void { } // callback function transitionning from a state to another -fn update(ctx: *anyopaque) anyerror!?u32 { +fn update(ctx: *anyopaque) !?u32 { const self: *Animation = @ptrCast(@alignCast(ctx)); switch (self._playState) { From 9939797792943964f6ca058e1b63c2d22076ec42 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 19 Feb 2026 10:36:29 +0100 Subject: [PATCH 3/5] fix comment --- src/browser/webapi/animation/Animation.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/webapi/animation/Animation.zig b/src/browser/webapi/animation/Animation.zig index 04df7e90..7c2e8839 100644 --- a/src/browser/webapi/animation/Animation.zig +++ b/src/browser/webapi/animation/Animation.zig @@ -68,7 +68,7 @@ pub fn play(self: *Animation, page: *Page) !void { // transition to running. self._playState = .running; - // Schedule the transition from .idle => .running in 10ms. + // Schedule the transition from .running => .finished in 10ms. page.js.strongRef(self); try page.js.scheduler.add( self, From d75f5f92313641a77e533f8254ab4e14aced86dc Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 19 Feb 2026 10:41:07 +0100 Subject: [PATCH 4/5] don't play animation when startTime is set to null --- src/browser/webapi/animation/Animation.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/browser/webapi/animation/Animation.zig b/src/browser/webapi/animation/Animation.zig index 7c2e8839..4daaa8d5 100644 --- a/src/browser/webapi/animation/Animation.zig +++ b/src/browser/webapi/animation/Animation.zig @@ -151,6 +151,12 @@ pub fn getStartTime(self: *const Animation) ?f64 { pub fn setStartTime(self: *Animation, value: ?f64, page: *Page) !void { self._startTime = value; + + // if the startTime is null, don't play the animation. + if (value == null) { + return; + } + return self.play(page); } From e15b8145b11637535c54da31f9084ea7f905eba8 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 19 Feb 2026 10:50:12 +0100 Subject: [PATCH 5/5] create Animation in the pool arena --- src/browser/webapi/animation/Animation.zig | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/browser/webapi/animation/Animation.zig b/src/browser/webapi/animation/Animation.zig index 4daaa8d5..0ebe396b 100644 --- a/src/browser/webapi/animation/Animation.zig +++ b/src/browser/webapi/animation/Animation.zig @@ -52,14 +52,19 @@ pub fn init(page: *Page) !*Animation { const arena = try page.getArena(.{ .debug = "Animation" }); errdefer page.releaseArena(arena); - const self = try page._factory.create(Animation{ + const self = try arena.create(Animation); + self.* = .{ ._page = page, ._arena = arena, - }); + }; return self; } +pub fn deinit(self: *Animation, _: bool) void { + self._page.releaseArena(self._arena); +} + pub fn play(self: *Animation, page: *Page) !void { if (self._playState == .running) { return; @@ -164,10 +169,6 @@ pub fn getOnFinish(self: *const Animation) ?js.Function.Temp { return self._onFinish; } -pub fn deinit(self: *Animation, _: bool) void { - self._page.releaseArena(self._arena); -} - // callback function transitionning from a state to another fn update(ctx: *anyopaque) !?u32 { const self: *Animation = @ptrCast(@alignCast(ctx));