Merge pull request #897 from lightpanda-io/animate

Add Element.animate and Animation
This commit is contained in:
Karl Seguin
2025-07-15 23:09:08 +08:00
committed by GitHub
5 changed files with 177 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
// Copyright (C) 2023-2025s Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// 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/>.
const std = @import("std");
const Page = @import("../page.zig").Page;
const JsObject = @import("../env.zig").JsObject;
const Promise = @import("../env.zig").Promise;
const PromiseResolver = @import("../env.zig").PromiseResolver;
const Animation = @This();
effect: ?JsObject,
timeline: ?JsObject,
ready_resolver: ?PromiseResolver,
finished_resolver: ?PromiseResolver,
pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !Animation {
return .{
.effect = if (effect) |eo| try eo.persist() else null,
.timeline = if (timeline) |to| try to.persist() else null,
.ready_resolver = null,
.finished_resolver = null,
};
}
pub fn get_playState(self: *const Animation) []const u8 {
_ = self;
return "finished";
}
pub fn get_pending(self: *const Animation) bool {
_ = self;
return false;
}
pub fn get_finished(self: *Animation, page: *Page) !Promise {
if (self.finished_resolver == null) {
const resolver = page.main_context.createPromiseResolver();
try resolver.resolve(self);
self.finished_resolver = resolver;
}
return self.finished_resolver.?.promise();
}
pub fn get_ready(self: *Animation, page: *Page) !Promise {
// never resolved, because we're always "finished"
if (self.ready_resolver == null) {
const resolver = page.main_context.createPromiseResolver();
self.ready_resolver = resolver;
}
return self.ready_resolver.?.promise();
}
pub fn get_effect(self: *const Animation) ?JsObject {
return self.effect;
}
pub fn set_effect(self: *Animation, effect: JsObject) !void {
self.effect = try effect.persist();
}
pub fn get_timeline(self: *const Animation) ?JsObject {
return self.timeline;
}
pub fn set_timeline(self: *Animation, timeline: JsObject) !void {
self.timeline = try timeline.persist();
}
pub fn _play(self: *const Animation) void {
_ = self;
}
pub fn _pause(self: *const Animation) void {
_ = self;
}
pub fn _cancel(self: *const Animation) void {
_ = self;
}
pub fn _finish(self: *const Animation) void {
_ = self;
}
pub fn _reverse(self: *const Animation) void {
_ = self;
}
const testing = @import("../../testing.zig");
test "Browser.Animation" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
defer runner.deinit();
try runner.testCases(&.{
.{ "let a1 = document.createElement('div').animate(null, null)", null },
.{ "a1.playState", "finished" },
.{ "let cb = [];", null },
.{ "a1.ready.then(() => { cb.push('ready') })", null },
.{
\\ a1.finished.then((x) => {
\\ cb.push('finished');
\\ cb.push(x == a1);
\\ })
,
null,
},
.{ "cb", "finished,true" },
}, .{});
}

View File

@@ -52,4 +52,5 @@ pub const Interfaces = .{
@import("performance.zig").Interfaces, @import("performance.zig").Interfaces,
PerformanceObserver, PerformanceObserver,
@import("range.zig").Interfaces, @import("range.zig").Interfaces,
@import("Animation.zig"),
}; };

View File

@@ -32,6 +32,9 @@ const NodeList = @import("nodelist.zig").NodeList;
const HTMLElem = @import("../html/elements.zig"); const HTMLElem = @import("../html/elements.zig");
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot; const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
const Animation = @import("Animation.zig");
const JsObject = @import("../env.zig").JsObject;
pub const Union = @import("../html/elements.zig").Union; pub const Union = @import("../html/elements.zig").Union;
// WEB IDL https://dom.spec.whatwg.org/#element // WEB IDL https://dom.spec.whatwg.org/#element
@@ -499,6 +502,12 @@ pub const Element = struct {
} }
return sr; return sr;
} }
pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation {
_ = self;
_ = opts;
return Animation.constructor(effect, null);
}
}; };
// Tests // Tests

View File

@@ -41,5 +41,8 @@ const WebApis = struct {
pub const JsThis = Env.JsThis; pub const JsThis = Env.JsThis;
pub const JsObject = Env.JsObject; pub const JsObject = Env.JsObject;
pub const Function = Env.Function; pub const Function = Env.Function;
pub const Promise = Env.Promise;
pub const PromiseResolver = Env.PromiseResolver;
pub const Env = js.Env(*Page, WebApis); pub const Env = js.Env(*Page, WebApis);
pub const Global = @import("html/window.zig").Window; pub const Global = @import("html/window.zig").Window;

View File

@@ -1164,6 +1164,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
}; };
} }
pub fn createPromiseResolver(self: *JsContext) PromiseResolver {
return .{
.js_context = self,
.resolver = v8.PromiseResolver.init(self.v8_context),
};
}
// Probing is part of trying to map a JS value to a Zig union. There's // Probing is part of trying to map a JS value to a Zig union. There's
// a lot of ambiguity in this process, in part because some JS values // a lot of ambiguity in this process, in part because some JS values
// can almost always be coerced. For example, anything can be coerced // can almost always be coerced. For example, anything can be coerced
@@ -1871,6 +1878,32 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
}; };
} }
pub const PromiseResolver = struct {
js_context: *JsContext,
resolver: v8.PromiseResolver,
pub fn promise(self: PromiseResolver) Promise {
return .{
.promise = self.resolver.getPromise(),
};
}
pub fn resolve(self: PromiseResolver, value: anytype) !void {
const js_context = self.js_context;
const js_value = try js_context.zigValueToJs(value);
// resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.resolve(js_context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToResolvePromise;
}
}
};
pub const Promise = struct {
promise: v8.Promise,
};
pub const Inspector = struct { pub const Inspector = struct {
isolate: v8.Isolate, isolate: v8.Isolate,
inner: *v8.Inspector, inner: *v8.Inspector,
@@ -2475,6 +2508,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
return value.js_obj.toValue(); return value.js_obj.toValue();
} }
if (T == Promise) {
// we're returning a v8.Promise
return value.promise.toObject().toValue();
}
if (@hasDecl(T, "_EXCEPTION_ID_KLUDGE")) { if (@hasDecl(T, "_EXCEPTION_ID_KLUDGE")) {
return isolate.throwException(value.inner); return isolate.throwException(value.inner);
} }