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..0ebe396b 100644 --- a/src/browser/webapi/animation/Animation.zig +++ b/src/browser/webapi/animation/Animation.zig @@ -16,40 +16,118 @@ // 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 arena.create(Animation); + self.* = .{ + ._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 deinit(self: *Animation, _: bool) void { + self._page.releaseArena(self._arena); +} + +pub fn play(self: *Animation, page: *Page) !void { + if (self._playState == .running) { + return; + } + + // transition to running. + self._playState = .running; + + // Schedule the transition from .running => .finished 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 +150,65 @@ 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; + + // if the startTime is null, don't play the animation. + if (value == null) { + return; + } + + return self.play(page); +} + +pub fn getOnFinish(self: *const Animation) ?js.Function.Temp { + return self._onFinish; +} + +// callback function transitionning from a state to another +fn update(ctx: *anyopaque) !?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 +216,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 +225,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");