mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Merge pull request #1596 from lightpanda-io/animation-improve
Improve Animation support: async update from idle => running => finished
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user