Merge pull request #1596 from lightpanda-io/animation-improve

Improve Animation support: async update from idle => running => finished
This commit is contained in:
Karl Seguin
2026-02-19 18:01:17 +08:00
committed by GitHub
2 changed files with 208 additions and 13 deletions

View File

@@ -3,13 +3,67 @@
<script id=animation> <script id=animation>
let a1 = document.createElement('div').animate(null, null); let a1 = document.createElement('div').animate(null, null);
testing.expectEqual('finished', a1.playState); testing.expectEqual('idle', a1.playState);
let cb = []; let cb = [];
a1.ready.then(() => { cb.push('ready') });
a1.finished.then((x) => { a1.finished.then((x) => {
cb.push('finished'); cb.push(a1.playState);
cb.push(x == a1); cb.push(x == a1);
}); });
testing.eventually(() => testing.expectEqual(['finished', true], cb)); a1.ready.then(() => {
cb.push(a1.playState);
a1.play();
cb.push(a1.playState);
});
testing.eventually(() => testing.expectEqual(['idle', 'running', 'finished', true], cb));
</script>
<script id=startTime>
let a2 = document.createElement('div').animate(null, null);
// startTime defaults to null
testing.expectEqual(null, a2.startTime);
// startTime is settable
a2.startTime = 42.5;
testing.expectEqual(42.5, a2.startTime);
// startTime can be reset to null
a2.startTime = null;
testing.expectEqual(null, a2.startTime);
</script>
<script id=onfinish>
let a3 = document.createElement('div').animate(null, null);
// onfinish defaults to null
testing.expectEqual(null, a3.onfinish);
let calls = [];
// onfinish callback should be scheduled and called asynchronously
a3.onfinish = function() { calls.push('finish'); };
a3.play();
testing.eventually(() => testing.expectEqual(['finish'], calls));
</script>
<script id=pause>
let a4 = document.createElement('div').animate(null, null);
let cb4 = [];
a4.finished.then((x) => { cb4.push(a4.playState) });
a4.ready.then(() => {
a4.play();
cb4.push(a4.playState)
a4.pause();
cb4.push(a4.playState)
});
testing.eventually(() => testing.expectEqual(['running', 'paused'], cb4));
</script>
<script id=finish>
let a5 = document.createElement('div').animate(null, null);
testing.expectEqual('idle', a5.playState);
let cb5 = [];
a5.finished.then((x) => { cb5.push(a5.playState) });
a5.ready.then(() => {
cb5.push(a5.playState);
a5.play();
});
testing.eventually(() => testing.expectEqual(['idle', 'finished'], cb5));
</script> </script>

View File

@@ -16,40 +16,118 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../../log.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Allocator = std.mem.Allocator;
const Animation = @This(); const Animation = @This();
const PlayState = enum {
idle,
running,
paused,
finished,
};
_page: *Page,
_arena: Allocator,
_effect: ?js.Object.Global = null, _effect: ?js.Object.Global = null,
_timeline: ?js.Object.Global = null, _timeline: ?js.Object.Global = null,
_ready_resolver: ?js.PromiseResolver.Global = null, _ready_resolver: ?js.PromiseResolver.Global = null,
_finished_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 { 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 deinit(self: *Animation, _: bool) void {
pub fn pause(_: *Animation) void {} self._page.releaseArena(self._arena);
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 .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 { pub fn getFinished(self: *Animation, page: *Page) !js.Promise {
if (self._finished_resolver == null) { if (self._finished_resolver == null) {
const resolver = page.js.local.?.createPromiseResolver(); const resolver = page.js.local.?.createPromiseResolver();
resolver.resolve("Animation.getFinished", self);
self._finished_resolver = try resolver.persist(); self._finished_resolver = try resolver.persist();
return resolver.promise(); return resolver.promise();
} }
return page.js.toLocal(self._finished_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 { pub fn getReady(self: *Animation, page: *Page) !js.Promise {
// never resolved, because we're always "finished"
if (self._ready_resolver == null) { if (self._ready_resolver == null) {
const resolver = page.js.local.?.createPromiseResolver(); const resolver = page.js.local.?.createPromiseResolver();
resolver.resolve("Animation.getReady", self);
self._ready_resolver = try resolver.persist(); self._ready_resolver = try resolver.persist();
return resolver.promise(); return resolver.promise();
} }
@@ -72,6 +150,65 @@ pub fn setTimeline(self: *Animation, timeline: ?js.Object.Global) !void {
self._timeline = timeline; 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 JsApi = struct {
pub const bridge = js.Bridge(Animation); pub const bridge = js.Bridge(Animation);
@@ -79,6 +216,8 @@ pub const JsApi = struct {
pub const name = "Animation"; pub const name = "Animation";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); pub const play = bridge.function(Animation.play, .{});
@@ -86,12 +225,14 @@ pub const JsApi = struct {
pub const cancel = bridge.function(Animation.cancel, .{}); pub const cancel = bridge.function(Animation.cancel, .{});
pub const finish = bridge.function(Animation.finish, .{}); pub const finish = bridge.function(Animation.finish, .{});
pub const reverse = bridge.function(Animation.reverse, .{}); 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 pending = bridge.property(false, .{ .template = false });
pub const finished = bridge.accessor(Animation.getFinished, null, .{}); pub const finished = bridge.accessor(Animation.getFinished, null, .{});
pub const ready = bridge.accessor(Animation.getReady, null, .{}); pub const ready = bridge.accessor(Animation.getReady, null, .{});
pub const effect = bridge.accessor(Animation.getEffect, Animation.setEffect, .{}); pub const effect = bridge.accessor(Animation.getEffect, Animation.setEffect, .{});
pub const timeline = bridge.accessor(Animation.getTimeline, Animation.setTimeline, .{}); 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"); const testing = @import("../../../testing.zig");