LibWeb: Transfer animation ownership to AnimatedBitmapDecodedImageData
Previously animation ownership was a messy split between `AnimatedBitmapDecodedImageData` and the consumers (i.e. `ImageStyleValueResource`, `HTMLImageElement`, and `SVGImageElement`) with `AnimatedBitmapDecodedImageData` owning the frames and a current frame index, and the consumers owning the rest of the state (e.g. loop count, timers to drive the animation forward, their own current index). This had a couple of main issues: - While `AnimatedDecodedImageData` partially synchronized animations by dropping unexpected advancement notifications, this didn't apply to other animation state which meant, for instance, that a later started consumer could drive the animation of an earlier one past the max loop count (albeit without invalidating the earlier consumer). - Multiple consumers didn't share frame timings, meaning animations could be up to a full frame out of sync visually. - Animations were paused depending on whether there were any consumers, this is different to the behavior in other browsers (where they continue regardless of whether there are any consumers). - It was an overgeneralization of how animations need to work - only `AnimatedBitmapDecodedImageData` works with an indexed frame model, with animated SVGs (although not yet implemented) relying on their internal event loop to be driven forward. Given the above the new approach implemented in this commit is: - The API for `DecodedImageData` is animation system agnostic, only exposing `default_frame`, `current_frame`, and `restart_animation` methods not reliant on providing a specific frame index. - `AnimatedBitmapDecodedImageData` owns its own timer, loop count, etc. The animation starts when the first consumer registers and ends when the document is hidden or becomes inactive (or completes in the case of finite animations). - Consumers are invalidated by `AnimatedBitmapDecodedImageData` when required. Tests have been added for: - Animations being paused when the document becomes inactive and restarted when it becomes active again. - Frame timings being synchronized across consumers. - Restarts triggered by `HTMLImageElement` applying to all consumers. - Processing ending once a non-infinite animation plays to completion. The tests to ensure animations are cancelled when consumers are removed (e.g. `animated-background-image-timer-stops-when-hidden.html`) have been updated to assert the inverse since animation state is now per resource not per consumer.
This commit is contained in:
parent
27381e9b00
commit
8ebdaeab69
62 changed files with 568 additions and 433 deletions
|
|
@ -688,6 +688,7 @@ set(SOURCES
|
|||
HTML/SessionHistoryTraversalQueue.cpp
|
||||
HTML/SharedResourceRequest.cpp
|
||||
HTML/AnimatedBitmapDecodedImageData.cpp
|
||||
HTML/AnimatedDecodedImageData.cpp
|
||||
HTML/SharedWorker.cpp
|
||||
HTML/SharedWorkerGlobalScope.cpp
|
||||
HTML/SourceSet.cpp
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
#include <AK/AnyOf.h>
|
||||
#include <LibGC/Function.h>
|
||||
#include <LibGC/Weak.h>
|
||||
#include <LibGfx/DecodedImageFrame.h>
|
||||
#include <LibWeb/CSS/CSSStyleSheet.h>
|
||||
|
|
@ -22,7 +21,6 @@
|
|||
#include <LibWeb/HTML/SharedResourceRequest.h>
|
||||
#include <LibWeb/Painting/DisplayListRecorder.h>
|
||||
#include <LibWeb/Painting/DisplayListRecordingContext.h>
|
||||
#include <LibWeb/Platform/Timer.h>
|
||||
|
||||
namespace Web::CSS {
|
||||
|
||||
|
|
@ -34,7 +32,7 @@ ImageStyleValueResource::ImageStyleValueResource(GC::Ref<HTML::SharedResourceReq
|
|||
// FIXME: Can we directly access the resource (i.e. this) here instead of looking it up in the document?
|
||||
if (auto document = weak_document.ptr()) {
|
||||
if (auto* resource = document->css_image_resource(url))
|
||||
resource->on_decoded_image_data_loaded(*document);
|
||||
resource->on_decoded_image_data_loaded();
|
||||
}
|
||||
},
|
||||
nullptr);
|
||||
|
|
@ -42,7 +40,6 @@ ImageStyleValueResource::ImageStyleValueResource(GC::Ref<HTML::SharedResourceReq
|
|||
|
||||
ImageStyleValueResource::~ImageStyleValueResource()
|
||||
{
|
||||
stop_animation_timer();
|
||||
VERIFY(m_image_style_values.is_empty());
|
||||
unregister_with_decoded_image_data_if_needed();
|
||||
}
|
||||
|
|
@ -50,23 +47,19 @@ ImageStyleValueResource::~ImageStyleValueResource()
|
|||
void ImageStyleValueResource::visit_edges(JS::Cell::Visitor& visitor)
|
||||
{
|
||||
visitor.visit(m_resource_request);
|
||||
visitor.visit(m_timer);
|
||||
}
|
||||
|
||||
void ImageStyleValueResource::register_image_style_value(DOM::Document& document, ImageStyleValue const& image_style_value)
|
||||
void ImageStyleValueResource::register_image_style_value(ImageStyleValue const& image_style_value)
|
||||
{
|
||||
m_image_style_values.set(&image_style_value);
|
||||
start_animation_timer_if_needed(document);
|
||||
register_with_decoded_image_data_if_needed();
|
||||
}
|
||||
|
||||
void ImageStyleValueResource::unregister_image_style_value(ImageStyleValue const& image_style_value)
|
||||
{
|
||||
m_image_style_values.remove(&image_style_value);
|
||||
if (m_image_style_values.is_empty()) {
|
||||
stop_animation_timer();
|
||||
if (m_image_style_values.is_empty())
|
||||
unregister_with_decoded_image_data_if_needed();
|
||||
}
|
||||
}
|
||||
|
||||
GC::Ptr<HTML::DecodedImageData> ImageStyleValueResource::decoded_image_data() const
|
||||
|
|
@ -74,15 +67,9 @@ GC::Ptr<HTML::DecodedImageData> ImageStyleValueResource::decoded_image_data() co
|
|||
return m_resource_request->image_data();
|
||||
}
|
||||
|
||||
bool ImageStyleValueResource::has_active_animation_timer() const
|
||||
{
|
||||
return m_timer && m_timer->is_active();
|
||||
}
|
||||
|
||||
void ImageStyleValueResource::on_decoded_image_data_loaded(DOM::Document& document)
|
||||
void ImageStyleValueResource::on_decoded_image_data_loaded()
|
||||
{
|
||||
notify_image_style_values_did_update();
|
||||
start_animation_timer_if_needed(document);
|
||||
if (!m_image_style_values.is_empty())
|
||||
register_with_decoded_image_data_if_needed();
|
||||
}
|
||||
|
|
@ -93,79 +80,6 @@ void ImageStyleValueResource::notify_image_style_values_did_update()
|
|||
image_style_value->notify_clients_did_update();
|
||||
}
|
||||
|
||||
void ImageStyleValueResource::start_animation_timer_if_needed(DOM::Document& document)
|
||||
{
|
||||
if (m_image_style_values.is_empty() || !is_animatable())
|
||||
return;
|
||||
|
||||
if (m_timer && m_timer->is_active())
|
||||
return;
|
||||
|
||||
if (!m_timer) {
|
||||
auto timer = Platform::Timer::create(document.heap());
|
||||
m_timer = timer;
|
||||
timer->on_timeout = GC::create_function(document.heap(), [weak_document = GC::Weak(document), url = m_resource_request->url()] {
|
||||
if (auto document = weak_document.ptr())
|
||||
document->animate_css_image_resource(url);
|
||||
});
|
||||
}
|
||||
|
||||
m_timer->set_interval(current_frame_duration());
|
||||
m_timer->start();
|
||||
}
|
||||
|
||||
void ImageStyleValueResource::stop_animation_timer()
|
||||
{
|
||||
if (m_timer && m_timer->is_active())
|
||||
m_timer->stop();
|
||||
}
|
||||
|
||||
bool ImageStyleValueResource::is_animatable() const
|
||||
{
|
||||
auto image_data = this->decoded_image_data();
|
||||
if (!image_data || !image_data->is_animated() || image_data->frame_count() <= 1)
|
||||
return false;
|
||||
|
||||
return !animation_has_completed();
|
||||
}
|
||||
|
||||
bool ImageStyleValueResource::animation_has_completed() const
|
||||
{
|
||||
auto image_data = this->decoded_image_data();
|
||||
return image_data && image_data->loop_count() > 0 && m_loops_completed == image_data->loop_count();
|
||||
}
|
||||
|
||||
int ImageStyleValueResource::current_frame_duration() const
|
||||
{
|
||||
auto image_data = this->decoded_image_data();
|
||||
if (!image_data)
|
||||
return 0;
|
||||
|
||||
return image_data->frame_duration(m_current_frame_index);
|
||||
}
|
||||
|
||||
void ImageStyleValueResource::animate(DOM::Document&)
|
||||
{
|
||||
auto image_data = m_resource_request->image_data();
|
||||
if (!image_data)
|
||||
return;
|
||||
|
||||
m_current_frame_index = (m_current_frame_index + 1) % image_data->frame_count();
|
||||
m_current_frame_index = image_data->notify_frame_advanced(m_current_frame_index);
|
||||
auto current_frame_duration = image_data->frame_duration(m_current_frame_index);
|
||||
|
||||
if (m_timer && current_frame_duration != m_timer->interval())
|
||||
m_timer->restart(current_frame_duration);
|
||||
|
||||
if (m_current_frame_index == image_data->frame_count() - 1) {
|
||||
++m_loops_completed;
|
||||
if (animation_has_completed())
|
||||
stop_animation_timer();
|
||||
}
|
||||
|
||||
notify_image_style_values_did_update();
|
||||
}
|
||||
|
||||
ValueComparingNonnullRefPtr<ImageStyleValue const> ImageStyleValue::create(URL const& url)
|
||||
{
|
||||
return adopt_ref(*new (nothrow) ImageStyleValue(url));
|
||||
|
|
@ -190,11 +104,6 @@ ImageStyleValue::ImageStyleValue(URL const& url, Optional<::URL::URL> style_reso
|
|||
|
||||
ImageStyleValue::~ImageStyleValue() = default;
|
||||
|
||||
u64 ImageStyleValue::active_animation_timer_count(DOM::Document const& document)
|
||||
{
|
||||
return document.active_css_image_animation_timer_count();
|
||||
}
|
||||
|
||||
GC::Ptr<HTML::SharedResourceRequest> ImageStyleValue::fetch_image(DOM::Document& document) const
|
||||
{
|
||||
RuleOrDeclaration rule_or_declaration {
|
||||
|
|
@ -256,30 +165,17 @@ void ImageStyleValue::paint(DisplayListRecordingContext& context, DOM::Document
|
|||
if (!image_data)
|
||||
return;
|
||||
|
||||
auto current_frame_index = this->current_frame_index(document);
|
||||
auto dest_int_rect = dest_rect.to_type<int>();
|
||||
image_data->paint(context, current_frame_index, dest_int_rect, image_rendering);
|
||||
image_data->paint(context, dest_int_rect, image_rendering);
|
||||
}
|
||||
|
||||
Optional<Gfx::DecodedImageFrame> ImageStyleValue::current_frame(DOM::Document const& document, DevicePixelRect const& dest_rect) const
|
||||
{
|
||||
if (auto image_data = this->image_data(document))
|
||||
return image_data->frame(current_frame_index(document), dest_rect.size().to_type<int>());
|
||||
|
||||
return image_data->current_frame(dest_rect.size().to_type<int>());
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t ImageStyleValue::current_frame_index(DOM::Document const& document) const
|
||||
{
|
||||
auto resolved_url = this->resolved_url(document);
|
||||
if (!resolved_url.has_value())
|
||||
return 0;
|
||||
|
||||
if (auto const* resource = document.css_image_resource(*resolved_url))
|
||||
return resource->current_frame_index();
|
||||
return 0;
|
||||
}
|
||||
|
||||
GC::Ptr<HTML::DecodedImageData> ImageStyleValue::image_data(DOM::Document const& document) const
|
||||
{
|
||||
auto resolved_url = this->resolved_url(document);
|
||||
|
|
@ -397,7 +293,7 @@ void ImageStyleValue::register_client(Client& client) const
|
|||
resource = document->create_css_image_resource(*resource_request);
|
||||
}
|
||||
|
||||
resource->register_image_style_value(*document, *this);
|
||||
resource->register_image_style_value(*this);
|
||||
}
|
||||
|
||||
void ImageStyleValue::unregister_client(Client& client) const
|
||||
|
|
|
|||
|
|
@ -32,32 +32,20 @@ public:
|
|||
|
||||
void visit_edges(JS::Cell::Visitor&);
|
||||
|
||||
void register_image_style_value(DOM::Document&, ImageStyleValue const&);
|
||||
void register_image_style_value(ImageStyleValue const&);
|
||||
void unregister_image_style_value(ImageStyleValue const&);
|
||||
bool can_be_removed() const { return m_image_style_values.is_empty(); }
|
||||
|
||||
[[nodiscard]] virtual GC::Ptr<HTML::DecodedImageData> decoded_image_data() const override;
|
||||
[[nodiscard]] size_t current_frame_index() const { return m_current_frame_index; }
|
||||
[[nodiscard]] bool has_active_animation_timer() const;
|
||||
|
||||
void animate(DOM::Document&);
|
||||
|
||||
private:
|
||||
virtual void decoded_image_data_did_update() override { notify_image_style_values_did_update(); }
|
||||
|
||||
void on_decoded_image_data_loaded(DOM::Document&);
|
||||
void on_decoded_image_data_loaded();
|
||||
void notify_image_style_values_did_update();
|
||||
void start_animation_timer_if_needed(DOM::Document&);
|
||||
void stop_animation_timer();
|
||||
bool is_animatable() const;
|
||||
bool animation_has_completed() const;
|
||||
int current_frame_duration() const;
|
||||
|
||||
GC::Ref<HTML::SharedResourceRequest> m_resource_request;
|
||||
GC::Ptr<Platform::Timer> m_timer;
|
||||
HashTable<ImageStyleValue const*> m_image_style_values;
|
||||
size_t m_current_frame_index { 0 };
|
||||
size_t m_loops_completed { 0 };
|
||||
};
|
||||
|
||||
class ImageStyleValue final
|
||||
|
|
@ -88,7 +76,6 @@ public:
|
|||
static ValueComparingNonnullRefPtr<ImageStyleValue const> create(URL const&, Optional<::URL::URL> style_resource_base_url);
|
||||
static ValueComparingNonnullRefPtr<ImageStyleValue const> create(::URL::URL const&);
|
||||
virtual ~ImageStyleValue() override;
|
||||
static u64 active_animation_timer_count(DOM::Document const&);
|
||||
|
||||
virtual void serialize(StringBuilder&, SerializationMode) const override;
|
||||
virtual bool equals(StyleValue const& other) const override;
|
||||
|
|
@ -106,7 +93,6 @@ public:
|
|||
|
||||
virtual Optional<Gfx::Color> color_if_single_pixel_bitmap(DOM::Document const&) const override;
|
||||
Optional<Gfx::DecodedImageFrame> current_frame(DOM::Document const&, DevicePixelRect const& dest_rect = {}) const;
|
||||
size_t current_frame_index(DOM::Document const&) const;
|
||||
|
||||
GC::Ptr<HTML::DecodedImageData> image_data(DOM::Document const&) const;
|
||||
|
||||
|
|
|
|||
|
|
@ -6798,22 +6798,6 @@ void Document::remove_css_image_resource_if_unused(URL::URL const& url)
|
|||
m_css_image_resources.remove(it);
|
||||
}
|
||||
|
||||
void Document::animate_css_image_resource(URL::URL const& url)
|
||||
{
|
||||
if (auto* resource = css_image_resource(url))
|
||||
resource->animate(*this);
|
||||
}
|
||||
|
||||
u64 Document::active_css_image_animation_timer_count() const
|
||||
{
|
||||
u64 count = 0;
|
||||
for (auto const& it : m_css_image_resources) {
|
||||
if (it.value->has_active_animation_timer())
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void Document::prune_image_resource_caches()
|
||||
{
|
||||
static constexpr size_t decoded_image_resource_cache_limit = 8 * MiB;
|
||||
|
|
|
|||
|
|
@ -831,8 +831,6 @@ public:
|
|||
CSS::ImageStyleValueResource const* css_image_resource(URL::URL const&) const;
|
||||
CSS::ImageStyleValueResource& create_css_image_resource(GC::Ref<HTML::SharedResourceRequest>);
|
||||
void remove_css_image_resource_if_unused(URL::URL const&);
|
||||
void animate_css_image_resource(URL::URL const&);
|
||||
u64 active_css_image_animation_timer_count() const;
|
||||
void prune_image_resource_caches();
|
||||
|
||||
void restore_the_history_object_state(NonnullRefPtr<HTML::SessionHistoryEntry> entry);
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@
|
|||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibJS/Runtime/ExternalMemory.h>
|
||||
#include <LibJS/Runtime/Realm.h>
|
||||
#include <LibJS/Runtime/VM.h>
|
||||
#include <LibWeb/CSS/ComputedValues.h>
|
||||
#include <LibWeb/DOM/DocumentObserver.h>
|
||||
#include <LibWeb/HTML/AnimatedBitmapDecodedImageData.h>
|
||||
#include <LibWeb/Painting/DisplayListRecorder.h>
|
||||
#include <LibWeb/Painting/DisplayListRecordingContext.h>
|
||||
#include <LibWeb/Platform/ImageCodecPlugin.h>
|
||||
#include <LibWeb/Platform/Timer.h>
|
||||
|
||||
namespace Web::HTML {
|
||||
|
||||
|
|
@ -55,6 +58,7 @@ void AnimatedBitmapDecodedImageData::deliver_frames_for_session(i64 session_id,
|
|||
|
||||
GC::Ref<AnimatedBitmapDecodedImageData> AnimatedBitmapDecodedImageData::create(
|
||||
JS::Realm& realm,
|
||||
DOM::Document& document,
|
||||
i64 session_id,
|
||||
u32 frame_count,
|
||||
u32 loop_count,
|
||||
|
|
@ -63,8 +67,11 @@ GC::Ref<AnimatedBitmapDecodedImageData> AnimatedBitmapDecodedImageData::create(
|
|||
Vector<u32> durations,
|
||||
Vector<NonnullRefPtr<Gfx::Bitmap>> initial_bitmaps)
|
||||
{
|
||||
auto animation_timer = Platform::Timer::create(realm.heap());
|
||||
auto document_observer = realm.create<DOM::DocumentObserver>(realm, document);
|
||||
|
||||
auto data = realm.create<AnimatedBitmapDecodedImageData>(
|
||||
session_id, frame_count, loop_count, size, move(color_space), move(durations));
|
||||
session_id, frame_count, loop_count, size, move(color_space), move(durations), animation_timer, document_observer);
|
||||
|
||||
// Place initial bitmaps into the buffer pool.
|
||||
for (u32 i = 0; i < initial_bitmaps.size(); ++i) {
|
||||
|
|
@ -89,18 +96,32 @@ AnimatedBitmapDecodedImageData::AnimatedBitmapDecodedImageData(
|
|||
u32 loop_count,
|
||||
Gfx::IntSize size,
|
||||
Gfx::ColorSpace color_space,
|
||||
Vector<u32> durations)
|
||||
: m_session_id(session_id)
|
||||
Vector<u32> durations,
|
||||
GC::Ref<Platform::Timer> animation_timer,
|
||||
GC::Ref<DOM::DocumentObserver> document_observer)
|
||||
: AnimatedDecodedImageData(document_observer)
|
||||
, m_session_id(session_id)
|
||||
, m_frame_count(frame_count)
|
||||
, m_loop_count(loop_count)
|
||||
, m_size(size)
|
||||
, m_color_space(move(color_space))
|
||||
, m_durations(move(durations))
|
||||
, m_animation_timer(animation_timer)
|
||||
{
|
||||
m_animation_timer->on_timeout = GC::create_function(vm().heap(), [weak_this = GC::Weak { *this }] {
|
||||
if (auto self = weak_this.ptr())
|
||||
self->advance_animation();
|
||||
});
|
||||
}
|
||||
|
||||
AnimatedBitmapDecodedImageData::~AnimatedBitmapDecodedImageData() = default;
|
||||
|
||||
void AnimatedBitmapDecodedImageData::visit_edges(Cell::Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_animation_timer);
|
||||
}
|
||||
|
||||
size_t AnimatedBitmapDecodedImageData::external_memory_size() const
|
||||
{
|
||||
size_t size = JS::vector_external_memory_size(m_durations);
|
||||
|
|
@ -114,10 +135,56 @@ size_t AnimatedBitmapDecodedImageData::external_memory_size() const
|
|||
void AnimatedBitmapDecodedImageData::finalize()
|
||||
{
|
||||
Base::finalize();
|
||||
m_animation_timer->stop();
|
||||
session_registry().remove(m_session_id);
|
||||
Platform::ImageCodecPlugin::the().stop_animation_decode(m_session_id);
|
||||
}
|
||||
|
||||
bool AnimatedBitmapDecodedImageData::animation_has_completed() const
|
||||
{
|
||||
return m_loop_count > 0 && m_loops_completed == m_loop_count;
|
||||
}
|
||||
|
||||
void AnimatedBitmapDecodedImageData::reset_animation()
|
||||
{
|
||||
m_current_frame_index = 0;
|
||||
m_loops_completed = 0;
|
||||
maybe_request_more_frames(m_current_frame_index);
|
||||
notify_clients_did_update();
|
||||
}
|
||||
|
||||
void AnimatedBitmapDecodedImageData::start_animation()
|
||||
{
|
||||
// NB: We should only ever start the animation when the first client is registered, or when animation restarts, both
|
||||
// of which should guarantee that we are at the beginning of the animation.
|
||||
VERIFY(m_current_frame_index == 0 && m_loops_completed == 0);
|
||||
|
||||
m_animation_timer->start(frame_duration(0));
|
||||
}
|
||||
|
||||
void AnimatedBitmapDecodedImageData::stop_animation()
|
||||
{
|
||||
m_animation_timer->stop();
|
||||
}
|
||||
|
||||
void AnimatedBitmapDecodedImageData::advance_animation()
|
||||
{
|
||||
m_current_frame_index = (m_current_frame_index + 1) % m_frame_count;
|
||||
maybe_request_more_frames(m_current_frame_index);
|
||||
|
||||
auto current_frame_duration = frame_duration(m_current_frame_index);
|
||||
if (current_frame_duration != m_animation_timer->interval())
|
||||
m_animation_timer->restart(current_frame_duration);
|
||||
|
||||
if (m_current_frame_index == m_frame_count - 1) {
|
||||
++m_loops_completed;
|
||||
if (animation_has_completed())
|
||||
stop_animation();
|
||||
}
|
||||
|
||||
notify_clients_did_update();
|
||||
}
|
||||
|
||||
AnimatedBitmapDecodedImageData::BufferSlot const* AnimatedBitmapDecodedImageData::find_slot(u32 frame_index) const
|
||||
{
|
||||
for (auto const& slot : m_buffer_slots) {
|
||||
|
|
@ -159,6 +226,11 @@ Optional<Gfx::DecodedImageFrame> AnimatedBitmapDecodedImageData::default_frame(G
|
|||
return frame(0, size);
|
||||
}
|
||||
|
||||
Optional<Gfx::DecodedImageFrame> AnimatedBitmapDecodedImageData::current_frame(Gfx::IntSize size) const
|
||||
{
|
||||
return frame(m_current_frame_index, size);
|
||||
}
|
||||
|
||||
int AnimatedBitmapDecodedImageData::frame_duration(size_t frame_index) const
|
||||
{
|
||||
if (frame_index >= m_durations.size())
|
||||
|
|
@ -181,9 +253,9 @@ Optional<CSSPixelFraction> AnimatedBitmapDecodedImageData::intrinsic_aspect_rati
|
|||
return CSSPixels(m_size.width()) / CSSPixels(m_size.height());
|
||||
}
|
||||
|
||||
void AnimatedBitmapDecodedImageData::paint(DisplayListRecordingContext& context, size_t frame_index, Gfx::IntRect dst_rect, CSS::ImageRendering image_rendering) const
|
||||
void AnimatedBitmapDecodedImageData::paint(DisplayListRecordingContext& context, Gfx::IntRect dst_rect, CSS::ImageRendering image_rendering) const
|
||||
{
|
||||
auto decoded_frame = frame(frame_index);
|
||||
auto decoded_frame = current_frame();
|
||||
if (!decoded_frame.has_value())
|
||||
return;
|
||||
|
||||
|
|
@ -212,23 +284,14 @@ void AnimatedBitmapDecodedImageData::receive_frames(Vector<NonnullRefPtr<Gfx::Bi
|
|||
}
|
||||
}
|
||||
|
||||
size_t AnimatedBitmapDecodedImageData::notify_frame_advanced(size_t caller_frame_index)
|
||||
{
|
||||
// We own the frame progression. Only advance when a caller reports
|
||||
// the expected next frame (this deduplicates multiple callers per tick).
|
||||
size_t expected_next = (m_current_frame_index + 1) % m_frame_count;
|
||||
if (caller_frame_index == expected_next) {
|
||||
m_current_frame_index = expected_next;
|
||||
maybe_request_more_frames(m_current_frame_index);
|
||||
}
|
||||
return m_current_frame_index;
|
||||
}
|
||||
|
||||
void AnimatedBitmapDecodedImageData::maybe_request_more_frames(size_t current_frame_index)
|
||||
{
|
||||
if (m_request_in_flight)
|
||||
return;
|
||||
|
||||
// TODO: Once all `ImageProvider`s are `DecodedImageData::Client`s we can defer loading new frames if we have no
|
||||
// clients
|
||||
|
||||
// Count how many frames ahead of current are in the pool.
|
||||
u32 frames_ahead = 0;
|
||||
for (u32 offset = 1; offset <= BUFFER_POOL_SIZE; ++offset) {
|
||||
|
|
|
|||
|
|
@ -12,19 +12,22 @@
|
|||
#include <LibGfx/ColorSpace.h>
|
||||
#include <LibGfx/DecodedImageFrame.h>
|
||||
#include <LibGfx/Forward.h>
|
||||
#include <LibWeb/HTML/AnimatedDecodedImageData.h>
|
||||
#include <LibWeb/HTML/DecodedImageData.h>
|
||||
|
||||
namespace Web::HTML {
|
||||
|
||||
class AnimatedBitmapDecodedImageData final : public DecodedImageData {
|
||||
GC_CELL(AnimatedBitmapDecodedImageData, DecodedImageData);
|
||||
class AnimatedBitmapDecodedImageData final : public AnimatedDecodedImageData {
|
||||
GC_CELL(AnimatedBitmapDecodedImageData, AnimatedDecodedImageData);
|
||||
GC_DECLARE_ALLOCATOR(AnimatedBitmapDecodedImageData);
|
||||
friend class Web::Internals::Internals;
|
||||
|
||||
public:
|
||||
static constexpr bool OVERRIDES_FINALIZE = true;
|
||||
|
||||
static GC::Ref<AnimatedBitmapDecodedImageData> create(
|
||||
JS::Realm&,
|
||||
DOM::Document&,
|
||||
i64 session_id,
|
||||
u32 frame_count,
|
||||
u32 loop_count,
|
||||
|
|
@ -35,22 +38,16 @@ public:
|
|||
|
||||
virtual ~AnimatedBitmapDecodedImageData() override;
|
||||
virtual void finalize() override;
|
||||
virtual void visit_edges(Cell::Visitor&) override;
|
||||
|
||||
virtual Optional<Gfx::DecodedImageFrame> default_frame(Gfx::IntSize = {}) const override;
|
||||
virtual Optional<Gfx::DecodedImageFrame> frame(size_t frame_index, Gfx::IntSize = {}) const override;
|
||||
virtual int frame_duration(size_t frame_index) const override;
|
||||
|
||||
virtual size_t frame_count() const override { return m_frame_count; }
|
||||
virtual size_t loop_count() const override { return m_loop_count; }
|
||||
virtual bool is_animated() const override { return true; }
|
||||
virtual Optional<Gfx::DecodedImageFrame> current_frame(Gfx::IntSize = {}) const override;
|
||||
|
||||
virtual Optional<CSSPixels> intrinsic_width() const override;
|
||||
virtual Optional<CSSPixels> intrinsic_height() const override;
|
||||
virtual Optional<CSSPixelFraction> intrinsic_aspect_ratio() const override;
|
||||
|
||||
virtual void paint(DisplayListRecordingContext&, size_t frame_index, Gfx::IntRect dst_rect, CSS::ImageRendering) const override;
|
||||
|
||||
virtual size_t notify_frame_advanced(size_t caller_frame_index) override;
|
||||
virtual void paint(DisplayListRecordingContext&, Gfx::IntRect dst_rect, CSS::ImageRendering) const override;
|
||||
|
||||
void receive_frames(Vector<NonnullRefPtr<Gfx::Bitmap>>, u32 start_frame_index);
|
||||
|
||||
|
|
@ -77,10 +74,22 @@ private:
|
|||
u32 loop_count,
|
||||
Gfx::IntSize,
|
||||
Gfx::ColorSpace,
|
||||
Vector<u32> durations);
|
||||
Vector<u32> durations,
|
||||
GC::Ref<Platform::Timer>,
|
||||
GC::Ref<DOM::DocumentObserver>);
|
||||
|
||||
virtual size_t external_memory_size() const override;
|
||||
|
||||
virtual void reset_animation() override;
|
||||
virtual void start_animation() override;
|
||||
virtual void stop_animation() override;
|
||||
|
||||
void advance_animation();
|
||||
bool animation_has_completed() const;
|
||||
|
||||
int frame_duration(size_t frame_index) const;
|
||||
Optional<Gfx::DecodedImageFrame> frame(size_t frame_index, Gfx::IntSize = {}) const;
|
||||
|
||||
BufferSlot const* find_slot(u32 frame_index) const;
|
||||
BufferSlot& evict_oldest_slot();
|
||||
void maybe_request_more_frames(size_t current_frame_index);
|
||||
|
|
@ -98,6 +107,8 @@ private:
|
|||
bool m_request_in_flight { false };
|
||||
u32 m_current_frame_index { 0 };
|
||||
u32 m_last_requested_start_frame { 0 };
|
||||
u32 m_loops_completed { 0 };
|
||||
GC::Ref<Platform::Timer> m_animation_timer;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
84
Libraries/LibWeb/HTML/AnimatedDecodedImageData.cpp
Normal file
84
Libraries/LibWeb/HTML/AnimatedDecodedImageData.cpp
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Callum Law <callumlaw1709@outlook.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "AnimatedDecodedImageData.h"
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/DocumentObserver.h>
|
||||
|
||||
namespace Web::HTML {
|
||||
|
||||
void AnimatedDecodedImageData::visit_edges(Cell::Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_document_observer);
|
||||
}
|
||||
|
||||
AnimatedDecodedImageData::AnimatedDecodedImageData(GC::Ref<DOM::DocumentObserver> document_observer)
|
||||
: m_document_observer(document_observer)
|
||||
{
|
||||
auto weak_this = GC::Weak { *this };
|
||||
|
||||
// OPTIMIZATION: To avoid CPU churn in background tabs we cancel the animation when the document is inactive or
|
||||
// hidden. Other browsers disagree on what should happen when the document becomes active again,
|
||||
// Blink continues the animation from where it would be if it had been running the whole time, and
|
||||
// Gecko restarts the animation. For now we restart the animation since that's simpler.
|
||||
m_document_observer->set_document_became_inactive([weak_this] {
|
||||
if (auto self = weak_this.ptr())
|
||||
self->stop_animation();
|
||||
});
|
||||
|
||||
m_document_observer->set_document_became_active([weak_this] {
|
||||
if (auto self = weak_this.ptr()) {
|
||||
if (!self->has_clients())
|
||||
self->m_should_start_animation_on_client_registration = true;
|
||||
|
||||
self->restart_animation();
|
||||
}
|
||||
});
|
||||
|
||||
m_document_observer->set_document_visibility_state_observer([weak_this](HTML::VisibilityState visibility_state) {
|
||||
if (auto self = weak_this.ptr()) {
|
||||
switch (visibility_state) {
|
||||
case HTML::VisibilityState::Hidden:
|
||||
self->stop_animation();
|
||||
break;
|
||||
case HTML::VisibilityState::Visible:
|
||||
if (!self->has_clients())
|
||||
self->m_should_start_animation_on_client_registration = true;
|
||||
self->restart_animation();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void AnimatedDecodedImageData::restart_animation()
|
||||
{
|
||||
stop_animation();
|
||||
reset_animation();
|
||||
start_animation_if_needed();
|
||||
}
|
||||
|
||||
void AnimatedDecodedImageData::on_client_registered()
|
||||
{
|
||||
if (!m_should_start_animation_on_client_registration)
|
||||
return;
|
||||
|
||||
m_should_start_animation_on_client_registration = false;
|
||||
|
||||
start_animation_if_needed();
|
||||
}
|
||||
|
||||
void AnimatedDecodedImageData::start_animation_if_needed()
|
||||
{
|
||||
// NB: Animations should start when the first client is registered while the document is active and visible.
|
||||
if (!has_clients() || !m_document_observer->document()->is_fully_active() || m_document_observer->document()->hidden())
|
||||
return;
|
||||
|
||||
start_animation();
|
||||
}
|
||||
|
||||
}
|
||||
38
Libraries/LibWeb/HTML/AnimatedDecodedImageData.h
Normal file
38
Libraries/LibWeb/HTML/AnimatedDecodedImageData.h
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Callum Law <callumlaw1709@outlook.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/HTML/DecodedImageData.h>
|
||||
|
||||
namespace Web::HTML {
|
||||
|
||||
class AnimatedDecodedImageData : public DecodedImageData {
|
||||
GC_CELL(AnimatedDecodedImageData, DecodedImageData);
|
||||
GC_DECLARE_ALLOCATOR(AnimatedDecodedImageData);
|
||||
|
||||
public:
|
||||
virtual void visit_edges(Cell::Visitor&) override;
|
||||
|
||||
virtual void restart_animation() override;
|
||||
|
||||
protected:
|
||||
AnimatedDecodedImageData(GC::Ref<DOM::DocumentObserver>);
|
||||
|
||||
virtual void start_animation() = 0;
|
||||
virtual void reset_animation() = 0;
|
||||
virtual void stop_animation() = 0;
|
||||
|
||||
virtual void on_client_registered() override;
|
||||
|
||||
private:
|
||||
void start_animation_if_needed();
|
||||
|
||||
GC::Ref<DOM::DocumentObserver> m_document_observer;
|
||||
bool m_should_start_animation_on_client_registration { true };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ size_t BitmapDecodedImageData::external_memory_size() const
|
|||
return m_frame.bitmap().data_size();
|
||||
}
|
||||
|
||||
Optional<Gfx::DecodedImageFrame> BitmapDecodedImageData::frame(size_t, Gfx::IntSize) const
|
||||
Optional<Gfx::DecodedImageFrame> BitmapDecodedImageData::current_frame(Gfx::IntSize) const
|
||||
{
|
||||
return m_frame;
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ Optional<CSSPixelFraction> BitmapDecodedImageData::intrinsic_aspect_ratio() cons
|
|||
return CSSPixels(m_frame.width()) / CSSPixels(m_frame.height());
|
||||
}
|
||||
|
||||
void BitmapDecodedImageData::paint(DisplayListRecordingContext& context, size_t, Gfx::IntRect dst_rect, CSS::ImageRendering image_rendering) const
|
||||
void BitmapDecodedImageData::paint(DisplayListRecordingContext& context, Gfx::IntRect dst_rect, CSS::ImageRendering image_rendering) const
|
||||
{
|
||||
auto scaling_mode = CSS::to_gfx_scaling_mode(image_rendering, m_frame.size(), dst_rect.size());
|
||||
|
||||
|
|
|
|||
|
|
@ -21,18 +21,13 @@ public:
|
|||
virtual ~BitmapDecodedImageData() override;
|
||||
|
||||
virtual Optional<Gfx::DecodedImageFrame> default_frame(Gfx::IntSize = {}) const override;
|
||||
virtual Optional<Gfx::DecodedImageFrame> frame(size_t frame_index, Gfx::IntSize = {}) const override;
|
||||
|
||||
virtual int frame_duration(size_t) const override { return 0; }
|
||||
virtual size_t frame_count() const override { return 1; }
|
||||
virtual size_t loop_count() const override { return 0; }
|
||||
virtual bool is_animated() const override { return false; }
|
||||
virtual Optional<Gfx::DecodedImageFrame> current_frame(Gfx::IntSize = {}) const override;
|
||||
|
||||
virtual Optional<CSSPixels> intrinsic_width() const override;
|
||||
virtual Optional<CSSPixels> intrinsic_height() const override;
|
||||
virtual Optional<CSSPixelFraction> intrinsic_aspect_ratio() const override;
|
||||
|
||||
virtual void paint(DisplayListRecordingContext&, size_t frame_index, Gfx::IntRect dst_rect, CSS::ImageRendering) const override;
|
||||
virtual void paint(DisplayListRecordingContext&, Gfx::IntRect dst_rect, CSS::ImageRendering) const override;
|
||||
|
||||
private:
|
||||
BitmapDecodedImageData(Gfx::DecodedImageFrame&& frame);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ void DecodedImageData::Client::register_with_decoded_image_data_if_needed()
|
|||
return;
|
||||
|
||||
image_data->m_clients.set(this);
|
||||
|
||||
image_data->on_client_registered();
|
||||
}
|
||||
|
||||
void DecodedImageData::Client::unregister_with_decoded_image_data_if_needed()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ namespace Web::HTML {
|
|||
// https://html.spec.whatwg.org/multipage/images.html#img-req-data
|
||||
class DecodedImageData : public JS::Cell {
|
||||
GC_CELL(DecodedImageData, JS::Cell);
|
||||
friend class Web::Internals::Internals;
|
||||
|
||||
public:
|
||||
class Client {
|
||||
|
|
@ -37,17 +38,12 @@ public:
|
|||
[[nodiscard]] bool is_cors_cross_origin() const { return m_is_cors_cross_origin; }
|
||||
void set_is_cors_cross_origin(bool value) { m_is_cors_cross_origin = value; }
|
||||
|
||||
virtual void paint([[maybe_unused]] DisplayListRecordingContext&, [[maybe_unused]] size_t frame_index, [[maybe_unused]] Gfx::IntRect dst_rect, CSS::ImageRendering) const = 0;
|
||||
virtual void paint([[maybe_unused]] DisplayListRecordingContext&, [[maybe_unused]] Gfx::IntRect dst_rect, CSS::ImageRendering) const = 0;
|
||||
|
||||
virtual Optional<Gfx::DecodedImageFrame> default_frame(Gfx::IntSize = {}) const = 0;
|
||||
virtual Optional<Gfx::DecodedImageFrame> frame(size_t frame_index, Gfx::IntSize = {}) const = 0;
|
||||
virtual int frame_duration(size_t frame_index) const = 0;
|
||||
virtual Optional<Gfx::DecodedImageFrame> current_frame(Gfx::IntSize = {}) const = 0;
|
||||
|
||||
virtual size_t frame_count() const = 0;
|
||||
virtual size_t loop_count() const = 0;
|
||||
virtual bool is_animated() const = 0;
|
||||
|
||||
virtual size_t notify_frame_advanced(size_t frame_index) { return frame_index; }
|
||||
virtual void restart_animation() { }
|
||||
|
||||
virtual Optional<CSSPixels> intrinsic_width() const = 0;
|
||||
virtual Optional<CSSPixels> intrinsic_height() const = 0;
|
||||
|
|
@ -57,6 +53,8 @@ protected:
|
|||
DecodedImageData();
|
||||
|
||||
void notify_clients_did_update();
|
||||
bool has_clients() const { return !m_clients.is_empty(); }
|
||||
virtual void on_client_registered() { }
|
||||
|
||||
private:
|
||||
HashTable<Client*> m_clients;
|
||||
|
|
|
|||
|
|
@ -148,9 +148,6 @@ GC_DEFINE_ALLOCATOR(HTMLImageElement);
|
|||
HTMLImageElement::HTMLImageElement(DOM::Document& document, DOM::QualifiedName qualified_name)
|
||||
: HTMLElement(document, move(qualified_name))
|
||||
{
|
||||
m_animation_timer = Core::Timer::create();
|
||||
m_animation_timer->on_timeout = [this] { animate(); };
|
||||
|
||||
document.register_viewport_client(*this);
|
||||
}
|
||||
|
||||
|
|
@ -171,25 +168,12 @@ void HTMLImageElement::initialize(JS::Realm& realm)
|
|||
m_current_request = ImageRequest::create(realm, document().page());
|
||||
|
||||
// AD-HOC: Create a DocumentObserver eagerly to handle document lifecycle changes.
|
||||
// The document_became_inactive callback handles the navigation case by clearing the
|
||||
// load event delayer and stopping the animation timer.
|
||||
// The document_became_inactive callback handles the navigation case by clearing the load event delayer.
|
||||
// A document_became_active callback is set lazily by update_the_image_data() when
|
||||
// needed to restart image loading after the document becomes active again.
|
||||
m_document_observer = realm.create<DOM::DocumentObserver>(realm, document());
|
||||
m_document_observer->set_document_became_inactive([this]() {
|
||||
m_load_event_delayer.clear();
|
||||
m_animation_timer->stop();
|
||||
m_animation_paused_by_visibility = false;
|
||||
});
|
||||
m_document_observer->set_document_visibility_state_observer([this](HTML::VisibilityState visibility_state) {
|
||||
if (visibility_state == HTML::VisibilityState::Hidden) {
|
||||
m_animation_paused_by_visibility = m_animation_timer->is_active();
|
||||
m_animation_timer->stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_animation_paused_by_visibility)
|
||||
start_animation_timer_if_visible();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -206,6 +190,8 @@ void HTMLImageElement::adopted_from(DOM::Document& old_document)
|
|||
|
||||
if (m_load_event_delayer.has_value())
|
||||
m_load_event_delayer.emplace(document());
|
||||
|
||||
// FIXME: The current and pending requests may still be pointing at the old document's SharedResourceRequests.
|
||||
}
|
||||
|
||||
void HTMLImageElement::visit_edges(Cell::Visitor& visitor)
|
||||
|
|
@ -699,7 +685,6 @@ void HTMLImageElement::update_the_image_data_impl(bool restart_animations, bool
|
|||
m_current_request = ImageRequest::create(document().realm(), document().page());
|
||||
m_current_request->set_image_data(entry->image_data);
|
||||
m_current_request->set_state(ImageRequest::State::CompletelyAvailable);
|
||||
m_current_frame_index = 0;
|
||||
register_with_decoded_image_data_if_needed();
|
||||
|
||||
// 5. Prepare the current request for presentation given the img element.
|
||||
|
|
@ -985,13 +970,6 @@ void HTMLImageElement::add_callbacks_to_image_request(GC::Ref<ImageRequest> imag
|
|||
if (!maybe_omit_events || previous_url != url_string)
|
||||
dispatch_event(DOM::Event::create(realm(), HTML::EventNames::load));
|
||||
|
||||
m_current_frame_index = 0;
|
||||
m_animation_timer->stop();
|
||||
if (image_data->is_animated() && image_data->frame_count() > 1) {
|
||||
m_animation_timer->set_interval(image_data->frame_duration(0));
|
||||
start_animation_timer_if_visible();
|
||||
}
|
||||
|
||||
m_load_event_delayer.clear();
|
||||
}));
|
||||
},
|
||||
|
|
@ -1230,38 +1208,8 @@ void HTMLImageElement::handle_failed_fetch()
|
|||
// https://html.spec.whatwg.org/multipage/rendering.html#restart-the-animation
|
||||
void HTMLImageElement::restart_the_animation()
|
||||
{
|
||||
m_current_frame_index = 0;
|
||||
|
||||
if (current_request_has_running_animation()) {
|
||||
start_animation_timer_if_visible();
|
||||
} else {
|
||||
m_animation_timer->stop();
|
||||
m_animation_paused_by_visibility = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool HTMLImageElement::current_request_has_running_animation() const
|
||||
{
|
||||
auto image_data = m_current_request->image_data();
|
||||
return image_data && image_data->is_animated() && image_data->frame_count() > 1;
|
||||
}
|
||||
|
||||
void HTMLImageElement::start_animation_timer_if_visible()
|
||||
{
|
||||
if (!current_request_has_running_animation()) {
|
||||
m_animation_timer->stop();
|
||||
m_animation_paused_by_visibility = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (document().visibility_state_value() == VisibilityState::Hidden) {
|
||||
m_animation_timer->stop();
|
||||
m_animation_paused_by_visibility = true;
|
||||
return;
|
||||
}
|
||||
|
||||
m_animation_paused_by_visibility = false;
|
||||
m_animation_timer->start();
|
||||
if (auto image_data = m_current_request->image_data())
|
||||
image_data->restart_animation();
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/images.html#update-the-source-set
|
||||
|
|
@ -1424,38 +1372,6 @@ void HTMLImageElement::set_source_set(SourceSet source_set)
|
|||
m_source_set = move(source_set);
|
||||
}
|
||||
|
||||
void HTMLImageElement::animate()
|
||||
{
|
||||
if (document().visibility_state_value() == VisibilityState::Hidden) {
|
||||
m_animation_timer->stop();
|
||||
m_animation_paused_by_visibility = true;
|
||||
return;
|
||||
}
|
||||
|
||||
auto image_data = m_current_request->image_data();
|
||||
if (!image_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_current_frame_index = (m_current_frame_index + 1) % image_data->frame_count();
|
||||
m_current_frame_index = image_data->notify_frame_advanced(m_current_frame_index);
|
||||
auto current_frame_duration = image_data->frame_duration(m_current_frame_index);
|
||||
|
||||
if (current_frame_duration != m_animation_timer->interval()) {
|
||||
m_animation_timer->restart(current_frame_duration);
|
||||
}
|
||||
|
||||
if (m_current_frame_index == image_data->frame_count() - 1) {
|
||||
++m_loops_completed;
|
||||
if (m_loops_completed > 0 && m_loops_completed == image_data->loop_count()) {
|
||||
m_animation_timer->stop();
|
||||
m_animation_paused_by_visibility = false;
|
||||
}
|
||||
}
|
||||
|
||||
set_needs_repaint();
|
||||
}
|
||||
|
||||
bool HTMLImageElement::allows_auto_sizes() const
|
||||
{
|
||||
// An img element allows auto-sizes if:
|
||||
|
|
|
|||
|
|
@ -98,8 +98,6 @@ public:
|
|||
ImageRequest& current_request() { return *m_current_request; }
|
||||
ImageRequest const& current_request() const { return *m_current_request; }
|
||||
|
||||
virtual size_t current_frame_index() const override { return m_current_frame_index; }
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/images.html#upgrade-the-pending-request-to-the-current-request
|
||||
void upgrade_pending_request_to_current_request();
|
||||
|
||||
|
|
@ -140,15 +138,6 @@ private:
|
|||
|
||||
virtual void decoded_image_data_did_update() override { set_needs_repaint(); }
|
||||
|
||||
bool current_request_has_running_animation() const;
|
||||
void start_animation_timer_if_visible();
|
||||
void animate();
|
||||
|
||||
RefPtr<Core::Timer> m_animation_timer;
|
||||
size_t m_current_frame_index { 0 };
|
||||
size_t m_loops_completed { 0 };
|
||||
bool m_animation_paused_by_visibility { false };
|
||||
|
||||
Optional<DOM::DocumentLoadEventDelayer> m_load_event_delayer;
|
||||
|
||||
GC::Ptr<DOM::DocumentObserver> m_document_observer;
|
||||
|
|
|
|||
|
|
@ -287,7 +287,6 @@ private:
|
|||
virtual bool supports_dimension_attributes() const override { return type_state() == TypeAttributeState::ImageButton; }
|
||||
|
||||
// ^Layout::ImageProvider
|
||||
virtual size_t current_frame_index() const override { return 0; }
|
||||
virtual GC::Ptr<HTML::DecodedImageData> decoded_image_data() const override { return image_data(); }
|
||||
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ private:
|
|||
virtual i32 default_tab_index_value() const override;
|
||||
|
||||
// ^Layout::ImageProvider
|
||||
virtual size_t current_frame_index() const override { return 0; }
|
||||
virtual GC::Ptr<DecodedImageData> decoded_image_data() const override { return image_data(); }
|
||||
|
||||
GC::Ptr<DecodedImageData> image_data() const;
|
||||
|
|
|
|||
|
|
@ -221,6 +221,7 @@ void SharedResourceRequest::handle_successful_fetch(URL::URL const& url_string,
|
|||
|
||||
strong_this->m_image_data = AnimatedBitmapDecodedImageData::create(
|
||||
strong_this->m_document->realm(),
|
||||
*strong_this->m_document,
|
||||
result.session_id,
|
||||
result.frame_count,
|
||||
result.loop_count,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@
|
|||
#include <LibWeb/Bindings/MainThreadVM.h>
|
||||
#include <LibWeb/CSS/CSSStyleSheet.h>
|
||||
#include <LibWeb/CSS/PreferredColorScheme.h>
|
||||
#include <LibWeb/CSS/StyleValues/ImageStyleValue.h>
|
||||
#include <LibWeb/Compositor/AsyncScrollTree.h>
|
||||
#include <LibWeb/Compositor/AsyncScrollingState.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
|
|
@ -35,6 +34,7 @@
|
|||
#include <LibWeb/Dump.h>
|
||||
#include <LibWeb/Fetch/Fetching/Fetching.h>
|
||||
#include <LibWeb/Geometry/DOMRect.h>
|
||||
#include <LibWeb/HTML/AnimatedBitmapDecodedImageData.h>
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/HTML/EventLoop/EventLoop.h>
|
||||
#include <LibWeb/HTML/EventLoop/TaskQueue.h>
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
#include <LibWeb/HTML/Navigable.h>
|
||||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||||
#include <LibWeb/HTML/SessionHistoryEntry.h>
|
||||
#include <LibWeb/HTML/SharedResourceRequest.h>
|
||||
#include <LibWeb/HTML/TraversableNavigable.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/Internals/InternalGamepad.h>
|
||||
|
|
@ -55,6 +56,7 @@
|
|||
#include <LibWeb/Painting/DisplayListResourceStorage.h>
|
||||
#include <LibWeb/Painting/PaintableBox.h>
|
||||
#include <LibWeb/Painting/ViewportPaintable.h>
|
||||
#include <LibWeb/WebIDL/ExceptionOr.h>
|
||||
#include <LibWeb/WebIDL/Promise.h>
|
||||
|
||||
namespace Web::Internals {
|
||||
|
|
@ -856,9 +858,38 @@ bool Internals::style_sheet_may_have_has_selectors(CSS::CSSStyleSheet& style_she
|
|||
return style_sheet.selector_insights().has_has_selectors;
|
||||
}
|
||||
|
||||
WebIDL::UnsignedLongLong Internals::active_image_style_value_animation_count()
|
||||
WebIDL::ExceptionOr<JS::Object*> Internals::image_animation_state_for_url(String const& url)
|
||||
{
|
||||
return CSS::ImageStyleValue::active_animation_timer_count(window().associated_document());
|
||||
auto& document = window().associated_document();
|
||||
auto parsed_url = document.encoding_parse_url(url);
|
||||
if (!parsed_url.has_value())
|
||||
return WebIDL::SimpleException { .type = WebIDL::SimpleExceptionType::TypeError, .message = MUST(String::formatted("Invalid URL: '{}'", url)) };
|
||||
|
||||
auto it = document.shared_resource_requests().find(*parsed_url);
|
||||
if (it == document.shared_resource_requests().end())
|
||||
return WebIDL::SimpleException { .type = WebIDL::SimpleExceptionType::TypeError, .message = MUST(String::formatted("URL doesn't have any associated shared resource requests: '{}'", url)) };
|
||||
|
||||
auto image_data = it->value->image_data();
|
||||
|
||||
if (!image_data)
|
||||
return WebIDL::SimpleException { .type = WebIDL::SimpleExceptionType::TypeError, .message = MUST(String::formatted("URL's shared resource request doesn't have any associated image data: '{}'", url)) };
|
||||
|
||||
auto const* animated_bitmap_data = as_if<HTML::AnimatedBitmapDecodedImageData>(*image_data);
|
||||
|
||||
if (!animated_bitmap_data)
|
||||
return WebIDL::SimpleException { .type = WebIDL::SimpleExceptionType::TypeError, .message = MUST(String::formatted("URL's associated image is not an animated bitmap: '{}'", url)) };
|
||||
|
||||
auto object = JS::Object::create(realm(), nullptr);
|
||||
|
||||
object->define_direct_property("timerActive"_utf16_fly_string, JS::Value(animated_bitmap_data->m_animation_timer->is_active()), JS::default_attributes);
|
||||
object->define_direct_property("sessionID"_utf16_fly_string, JS::Value(static_cast<double>(animated_bitmap_data->m_session_id)), JS::default_attributes);
|
||||
object->define_direct_property("frameIndex"_utf16_fly_string, JS::Value(animated_bitmap_data->m_current_frame_index), JS::default_attributes);
|
||||
object->define_direct_property("frameCount"_utf16_fly_string, JS::Value(animated_bitmap_data->m_frame_count), JS::default_attributes);
|
||||
object->define_direct_property("loopsCompleted"_utf16_fly_string, JS::Value(animated_bitmap_data->m_loops_completed), JS::default_attributes);
|
||||
object->define_direct_property("loopCount"_utf16_fly_string, JS::Value(animated_bitmap_data->m_loop_count), JS::default_attributes);
|
||||
object->define_direct_property("clientCount"_utf16_fly_string, JS::Value(image_data->m_clients.size()), JS::default_attributes);
|
||||
|
||||
return object.ptr();
|
||||
}
|
||||
|
||||
struct AsyncScrollingStateSnapshot {
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ public:
|
|||
void set_preferred_color_scheme(StringView color_scheme);
|
||||
String canvas_color_scheme();
|
||||
bool style_sheet_may_have_has_selectors(CSS::CSSStyleSheet&);
|
||||
WebIDL::UnsignedLongLong active_image_style_value_animation_count();
|
||||
WebIDL::ExceptionOr<JS::Object*> image_animation_state_for_url(String const& url);
|
||||
JS::Object* async_scrolling_state();
|
||||
bool async_scrolling_state_blocks_wheel_event_at(double x, double y);
|
||||
bool async_scrolling_state_can_wheel_scroll_at(double x, double y, double delta_x, double delta_y, bool force_stale_wheel_event_regions);
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ interface Internals {
|
|||
DOMString canvasColorScheme();
|
||||
// Returns the selector-insight cache state for stylesheet invalidation tests.
|
||||
boolean styleSheetMayHaveHasSelectors(CSSStyleSheet sheet);
|
||||
unsigned long long activeImageStyleValueAnimationCount();
|
||||
object imageAnimationStateForURL(USVString url);
|
||||
|
||||
object asyncScrollingState();
|
||||
boolean asyncScrollingStateBlocksWheelEventAt(double x, double y);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ Optional<CSSPixelSize> ImageProvider::intrinsic_size() const
|
|||
Optional<Gfx::DecodedImageFrame> ImageProvider::current_image_frame(Optional<Gfx::IntSize> size) const
|
||||
{
|
||||
if (auto const& data = decoded_image_data())
|
||||
return data->frame(current_frame_index(), size.value_or(intrinsic_size().value_or({}).to_type<int>()));
|
||||
return data->current_frame(size.value_or(intrinsic_size().value_or({}).to_type<int>()));
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ public:
|
|||
|
||||
bool is_image_available() const { return decoded_image_data() != nullptr; }
|
||||
|
||||
virtual size_t current_frame_index() const = 0;
|
||||
|
||||
virtual GC::Ptr<HTML::DecodedImageData> decoded_image_data() const = 0;
|
||||
|
||||
Optional<CSSPixels> intrinsic_width() const;
|
||||
|
|
|
|||
|
|
@ -219,13 +219,6 @@ public:
|
|||
m_layout_node = layout_node;
|
||||
}
|
||||
|
||||
virtual size_t current_frame_index() const override
|
||||
{
|
||||
if (auto document = this->document())
|
||||
return m_image->current_frame_index(*document);
|
||||
return 0;
|
||||
}
|
||||
|
||||
virtual GC::Ptr<HTML::DecodedImageData> decoded_image_data() const override
|
||||
{
|
||||
if (auto document = this->document())
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ void ImagePaintable::paint(DisplayListRecordingContext& context, PaintPhase phas
|
|||
context.display_list_recorder().save();
|
||||
context.display_list_recorder().add_clip_rect(image_int_rect_device_pixels);
|
||||
}
|
||||
decoded_image_data->paint(context, m_image_provider.current_frame_index(), draw_rect, computed_values().image_rendering());
|
||||
decoded_image_data->paint(context, draw_rect, computed_values().image_rendering());
|
||||
if (draw_rect_needs_clip)
|
||||
context.display_list_recorder().restore();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ RefPtr<Gfx::PaintingSurface> SVGDecodedImageData::render_to_surface(Gfx::IntSize
|
|||
return surface;
|
||||
}
|
||||
|
||||
Optional<Gfx::DecodedImageFrame> SVGDecodedImageData::frame(size_t, Gfx::IntSize size) const
|
||||
Optional<Gfx::DecodedImageFrame> SVGDecodedImageData::current_frame(Gfx::IntSize size) const
|
||||
{
|
||||
if (size.is_empty())
|
||||
return {};
|
||||
|
|
@ -243,7 +243,7 @@ Optional<Gfx::DecodedImageFrame> SVGDecodedImageData::default_frame(Gfx::IntSize
|
|||
{
|
||||
// FIXME: Implement this properly once we support animated SVGs, potentially by creating a temporary internal
|
||||
// document which has animations disabled.
|
||||
return frame(0, size);
|
||||
return current_frame(size);
|
||||
}
|
||||
|
||||
Optional<CSSPixels> SVGDecodedImageData::intrinsic_width() const
|
||||
|
|
@ -300,7 +300,7 @@ void SVGDecodedImageData::SVGPageClient::visit_edges(Visitor& visitor)
|
|||
visitor.visit(m_svg_page);
|
||||
}
|
||||
|
||||
void SVGDecodedImageData::paint(DisplayListRecordingContext& context, size_t, Gfx::IntRect dst_rect, CSS::ImageRendering) const
|
||||
void SVGDecodedImageData::paint(DisplayListRecordingContext& context, Gfx::IntRect dst_rect, CSS::ImageRendering) const
|
||||
{
|
||||
auto display_list = record_display_list(dst_rect.size(), context.display_list_recorder().resource_storage());
|
||||
if (!display_list.has_value())
|
||||
|
|
|
|||
|
|
@ -25,24 +25,19 @@ public:
|
|||
virtual ~SVGDecodedImageData() override;
|
||||
|
||||
virtual Optional<Gfx::DecodedImageFrame> default_frame(Gfx::IntSize = {}) const override;
|
||||
virtual Optional<Gfx::DecodedImageFrame> frame(size_t frame_index, Gfx::IntSize) const override;
|
||||
virtual Optional<Gfx::DecodedImageFrame> current_frame(Gfx::IntSize = {}) const override;
|
||||
|
||||
virtual Optional<CSSPixels> intrinsic_width() const override;
|
||||
virtual Optional<CSSPixels> intrinsic_height() const override;
|
||||
virtual Optional<CSSPixelFraction> intrinsic_aspect_ratio() const override;
|
||||
|
||||
// FIXME: Support SVG animations. :^)
|
||||
virtual int frame_duration(size_t) const override { return 0; }
|
||||
virtual size_t frame_count() const override { return 1; }
|
||||
virtual size_t loop_count() const override { return 0; }
|
||||
virtual bool is_animated() const override { return false; }
|
||||
|
||||
DOM::Document const& svg_document() const { return *m_document; }
|
||||
|
||||
virtual void visit_edges(Cell::Visitor& visitor) override;
|
||||
virtual size_t external_memory_size() const override;
|
||||
|
||||
virtual void paint(DisplayListRecordingContext&, size_t frame_index, Gfx::IntRect dst_rect, CSS::ImageRendering) const override;
|
||||
virtual void paint(DisplayListRecordingContext&, Gfx::IntRect dst_rect, CSS::ImageRendering) const override;
|
||||
|
||||
private:
|
||||
SVGDecodedImageData(GC::Ref<Page>, GC::Ref<SVGPageClient>, GC::Ref<DOM::Document>, GC::Ref<SVG::SVGSVGElement>);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
#include "SVGImageElement.h"
|
||||
#include <LibCore/Timer.h>
|
||||
#include <LibGC/Heap.h>
|
||||
#include <LibGfx/DecodedImageFrame.h>
|
||||
#include <LibWeb/Bindings/SVGImageElement.h>
|
||||
|
|
@ -26,8 +25,6 @@ GC_DEFINE_ALLOCATOR(SVGImageElement);
|
|||
SVGImageElement::SVGImageElement(DOM::Document& document, DOM::QualifiedName qualified_name)
|
||||
: SVGGraphicsElement(document, move(qualified_name))
|
||||
{
|
||||
m_animation_timer = Core::Timer::create();
|
||||
m_animation_timer->on_timeout = [this] { animate(); };
|
||||
}
|
||||
|
||||
SVGImageElement::~SVGImageElement() = default;
|
||||
|
|
@ -196,12 +193,6 @@ void SVGImageElement::fetch_the_document(URL::URL const& url)
|
|||
m_resource_request->add_callbacks(
|
||||
[this, resource_request = GC::Root { m_resource_request }] {
|
||||
m_load_event_delayer.clear();
|
||||
auto image_data = resource_request->image_data();
|
||||
if (image_data->is_animated() && image_data->frame_count() > 1) {
|
||||
m_current_frame_index = 0;
|
||||
m_animation_timer->set_interval(image_data->frame_duration(0));
|
||||
m_animation_timer->start();
|
||||
}
|
||||
register_with_decoded_image_data_if_needed();
|
||||
set_needs_style_update(true);
|
||||
set_needs_layout_update(DOM::SetNeedsLayoutReason::SVGImageElementFetchTheDocument);
|
||||
|
|
@ -226,31 +217,6 @@ RefPtr<Layout::Node> SVGImageElement::create_layout_node(CSS::ComputedProperties
|
|||
return make_ref_counted<Layout::SVGImageBox>(document(), *this, style);
|
||||
}
|
||||
|
||||
void SVGImageElement::animate()
|
||||
{
|
||||
auto image_data = m_resource_request->image_data();
|
||||
if (!image_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_current_frame_index = (m_current_frame_index + 1) % image_data->frame_count();
|
||||
auto current_frame_duration = image_data->frame_duration(m_current_frame_index);
|
||||
|
||||
if (current_frame_duration != m_animation_timer->interval()) {
|
||||
m_animation_timer->restart(current_frame_duration);
|
||||
}
|
||||
|
||||
if (m_current_frame_index == image_data->frame_count() - 1) {
|
||||
++m_loops_completed;
|
||||
if (m_loops_completed > 0 && m_loops_completed == image_data->loop_count()) {
|
||||
m_animation_timer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
if (paintable())
|
||||
paintable()->set_needs_repaint();
|
||||
}
|
||||
|
||||
GC::Ptr<HTML::DecodedImageData> SVGImageElement::decoded_image_data() const
|
||||
{
|
||||
if (!m_resource_request)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ public:
|
|||
Gfx::FloatRect bounding_box() const;
|
||||
|
||||
// ^Layout::ImageProvider
|
||||
virtual size_t current_frame_index() const override { return m_current_frame_index; }
|
||||
virtual GC::Ptr<HTML::DecodedImageData> decoded_image_data() const override;
|
||||
|
||||
protected:
|
||||
|
|
@ -57,17 +56,11 @@ private:
|
|||
virtual RefPtr<Layout::Node> create_layout_node(CSS::ComputedProperties const&) override;
|
||||
virtual void decoded_image_data_did_update() override { set_needs_repaint(); }
|
||||
|
||||
void animate();
|
||||
|
||||
GC::Ptr<SVG::SVGAnimatedLength> m_x;
|
||||
GC::Ptr<SVG::SVGAnimatedLength> m_y;
|
||||
GC::Ptr<SVG::SVGAnimatedLength> m_width;
|
||||
GC::Ptr<SVG::SVGAnimatedLength> m_height;
|
||||
|
||||
RefPtr<Core::Timer> m_animation_timer;
|
||||
size_t m_current_frame_index { 0 };
|
||||
size_t m_loops_completed { 0 };
|
||||
|
||||
Optional<URL::URL> m_href;
|
||||
|
||||
GC::Ptr<HTML::SharedResourceRequest> m_resource_request;
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 320 B |
|
|
@ -0,0 +1,44 @@
|
|||
<!doctype html>
|
||||
<html class="reftest-wait">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#container {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.target {
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
background: url("../data/red-then-green.gif") no-repeat;
|
||||
}
|
||||
</style>
|
||||
<div id="container">
|
||||
<div class="target"></div>
|
||||
</div>
|
||||
<script src="../../Text/input/include.js"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
const imageURL = "../data/red-then-green.gif";
|
||||
|
||||
// Wait for the animation to start.
|
||||
await waitForImageAnimationState(imageURL, state => state.timerActive);
|
||||
|
||||
// Wait for half of the first frame's duration
|
||||
await timeout(250);
|
||||
|
||||
// Add a new consumer. It's frame timings should be aligned with the first consumer.
|
||||
const lateTarget = document.createElement("div");
|
||||
lateTarget.className = "target";
|
||||
container.append(lateTarget);
|
||||
|
||||
// Wait for the animation to advance to the second frame. Both consumers should be showing the second frame
|
||||
await waitForImageAnimationState(imageURL, state => state.frameIndex > 0);
|
||||
|
||||
document.documentElement.classList.remove("reftest-wait");
|
||||
})();
|
||||
</script>
|
||||
</html>
|
||||
|
|
@ -13,27 +13,16 @@
|
|||
}
|
||||
</style>
|
||||
<div id="target"></div>
|
||||
<script src="../../Text/input/include.js"></script>
|
||||
<script>
|
||||
const waitForAnimationToStart = () =>
|
||||
new Promise(resolve => {
|
||||
const wait = () => {
|
||||
if (internals.activeImageStyleValueAnimationCount() > 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(wait);
|
||||
};
|
||||
|
||||
wait();
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const imageURL = "../data/red-then-green.gif";
|
||||
|
||||
// Wait for the animation to start
|
||||
await waitForAnimationToStart();
|
||||
await waitForImageAnimationState(imageURL, state => state.timerActive);
|
||||
|
||||
// Wait until the next frame
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
await waitForImageAnimationState(imageURL, state => state.frameIndex > 0);
|
||||
|
||||
// Take a screenshot - the image should have been invalidated and repainted.
|
||||
document.documentElement.classList.remove("reftest-wait");
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
}
|
||||
</style>
|
||||
<div id="target"></div>
|
||||
<script src="../../Text/input/include.js"></script>
|
||||
<script>
|
||||
const gifURL = "../data/animated-mask-image.gif";
|
||||
|
||||
|
|
@ -22,26 +23,12 @@
|
|||
.getElementById("target")
|
||||
.style.setProperty("mask-image", `linear-gradient(transparent, transparent), url("${gifURL}")`);
|
||||
|
||||
const waitForAnimationToStart = () =>
|
||||
new Promise(resolve => {
|
||||
const wait = () => {
|
||||
if (internals.activeImageStyleValueAnimationCount() > 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(wait);
|
||||
};
|
||||
|
||||
wait();
|
||||
});
|
||||
|
||||
(async () => {
|
||||
// Wait for the animation to start
|
||||
await waitForAnimationToStart();
|
||||
await waitForImageAnimationState(gifURL, state => state.timerActive);
|
||||
|
||||
// Wait until the next frame
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
await waitForImageAnimationState(gifURL, state => state.frameIndex > 0);
|
||||
|
||||
// Take a screenshot - the mask image should have been invalidated and repainted.
|
||||
document.documentElement.classList.remove("reftest-wait");
|
||||
|
|
|
|||
BIN
Tests/LibWeb/Text/data/loop-count-2.gif
Normal file
BIN
Tests/LibWeb/Text/data/loop-count-2.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 B |
|
|
@ -0,0 +1 @@
|
|||
PASS
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
timer stopped after iframe removal: true
|
||||
animation restarted after iframe reattach: true
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
timer active for shared animation: true
|
||||
client count: 1
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
top document active animations: 0
|
||||
iframe document active animations: 1
|
||||
top document has animation state: false
|
||||
iframe document timer active: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
timer active before base change: true
|
||||
timer active after removal: true
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
timer active before teardown: true
|
||||
timer active after teardown: true
|
||||
timer active after hiding: true
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
timer active before replacement: true
|
||||
timer active after replacement: true
|
||||
timer active after hiding: true
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
timer active before hiding: true
|
||||
timer active after hiding: true
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
active animations before base change: 1
|
||||
active animations after removal: 0
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
active animations before teardown: 1
|
||||
active animations after teardown: 1
|
||||
active animations after hiding: 0
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
active animations before replacement: 1
|
||||
active animations after replacement: 1
|
||||
active animations after hiding: 0
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
active animations before hiding: 1
|
||||
active animations after hiding: 0
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
timer active before hiding: true
|
||||
timer active after hiding: true
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
active animations before hiding: 1
|
||||
active animations after hiding: 0
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
client count before restart: 2
|
||||
frame advanced before restart: true
|
||||
session unchanged after restart: true
|
||||
animation restarted after src reset: true
|
||||
client count after restart: 2
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
Computed URL before base change: url("../wpt-import/images/anim-gr.gif")
|
||||
Computed URL after base change: url("../wpt-import/images/anim-gr.gif")
|
||||
active animations before base change: 1
|
||||
active animations after base change: 1
|
||||
timer active before base change: true
|
||||
timer active after base change: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<!doctype html>
|
||||
<script src="../include.js"></script>
|
||||
<style>
|
||||
#target {
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
background-image: url("../../data/loop-count-2.gif");
|
||||
}
|
||||
</style>
|
||||
<div id="target"></div>
|
||||
<script>
|
||||
const imageURL = "../../data/loop-count-2.gif";
|
||||
|
||||
promiseTest(async () => {
|
||||
console.log("A");
|
||||
|
||||
// Wait for the animation to start.
|
||||
await waitForImageAnimationState(imageURL, state => state.timerActive);
|
||||
|
||||
console.log("B");
|
||||
|
||||
// Wait for the animation to complete.
|
||||
await timeout(2500);
|
||||
|
||||
console.log("C");
|
||||
|
||||
println(internals.imageAnimationStateForURL(imageURL).timerActive ? "FAIL" : "PASS");
|
||||
|
||||
console.log("D");
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<!doctype html>
|
||||
<script src="../include.js"></script>
|
||||
<script>
|
||||
const imageURL = "../wpt-import/images/anim-gr.gif";
|
||||
|
||||
async function loadIframe(iframe) {
|
||||
const loaded = new Promise(resolve => iframe.addEventListener("load", resolve, { once: true }));
|
||||
document.body.append(iframe);
|
||||
await loaded;
|
||||
iframe.contentWindow.internals.dumpLayoutTree(iframe.contentDocument);
|
||||
}
|
||||
|
||||
promiseTest(async () => {
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.srcdoc = `
|
||||
<!DOCTYPE html>
|
||||
<style>
|
||||
#target {
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
background-image: url("../wpt-import/images/anim-gr.gif");
|
||||
}
|
||||
</style>
|
||||
<div id="target"></div>
|
||||
`;
|
||||
|
||||
await loadIframe(iframe);
|
||||
|
||||
const iframeWindow = iframe.contentWindow;
|
||||
|
||||
// Wait the animation to progress to the second frame
|
||||
await waitForImageAnimationState(imageURL, state => state.timerActive && state.frameIndex > 0, iframeWindow);
|
||||
|
||||
// Make the iframe document inactive - this should stop the animation.
|
||||
iframe.remove();
|
||||
|
||||
// Wait a tick for the iframe to become inactive
|
||||
await timeout(0);
|
||||
|
||||
println(
|
||||
`timer stopped after iframe removal: ${!iframeWindow.internals.imageAnimationStateForURL(imageURL).timerActive}`
|
||||
);
|
||||
|
||||
// Make the iframe document active again - this should reset the animation.
|
||||
await loadIframe(iframe);
|
||||
|
||||
const stateAfterReattach = iframe.contentWindow.internals.imageAnimationStateForURL(imageURL);
|
||||
println(
|
||||
`animation restarted after iframe reattach: ${stateAfterReattach.timerActive && stateAfterReattach.frameIndex === 0 && stateAfterReattach.loopsCompleted === 0}`
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<!doctype html>
|
||||
<script src="../include.js"></script>
|
||||
<style>
|
||||
.target {
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
background-image: url("../../data/loop-count-2.gif");
|
||||
}
|
||||
</style>
|
||||
<div class="target"></div>
|
||||
<div class="target"></div>
|
||||
<script>
|
||||
const imageURL = "../../data/loop-count-2.gif";
|
||||
|
||||
promiseTest(async () => {
|
||||
await new Promise(resolve => window.addEventListener("load", resolve));
|
||||
|
||||
internals.dumpLayoutTree(document);
|
||||
|
||||
const state = internals.imageAnimationStateForURL(imageURL);
|
||||
println(`timer active for shared animation: ${state.timerActive}`);
|
||||
println(`client count: ${state.clientCount}`);
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="../include.js"></script>
|
||||
<script>
|
||||
const imageURL = "../wpt-import/images/anim-gr.gif";
|
||||
|
||||
asyncTest(async done => {
|
||||
const iframe = document.createElement("iframe");
|
||||
const iframeLoaded = new Promise(resolve => iframe.addEventListener("load", resolve, { once: true }));
|
||||
|
|
@ -10,7 +12,7 @@
|
|||
#target {
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
background-image: url("../wpt-import/images/anim-gr.gif");
|
||||
background-image: url("${imageURL}");
|
||||
}
|
||||
</style>
|
||||
<div id="target"></div>
|
||||
|
|
@ -20,8 +22,15 @@
|
|||
|
||||
internals.dumpLayoutTree(iframe.contentDocument);
|
||||
|
||||
println(`top document active animations: ${internals.activeImageStyleValueAnimationCount()}`);
|
||||
println(`iframe document active animations: ${iframe.contentWindow.internals.activeImageStyleValueAnimationCount()}`);
|
||||
let topDocumentHasAnimationState = true;
|
||||
try {
|
||||
internals.imageAnimationStateForURL(imageURL);
|
||||
} catch {
|
||||
topDocumentHasAnimationState = false;
|
||||
}
|
||||
|
||||
println(`top document has animation state: ${topDocumentHasAnimationState}`);
|
||||
println(`iframe document timer active: ${iframe.contentWindow.internals.imageAnimationStateForURL(imageURL).timerActive}`);
|
||||
|
||||
done();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,14 +9,15 @@
|
|||
"
|
||||
></div>
|
||||
<script>
|
||||
const imageURL = new URL("../wpt-import/images/anim-gr.gif", document.baseURI).href;
|
||||
|
||||
asyncTest(async done => {
|
||||
await new Promise(resolve => window.addEventListener("load", resolve));
|
||||
|
||||
internals.dumpLayoutTree(document);
|
||||
const activeAnimationsBeforeBaseChange =
|
||||
internals.activeImageStyleValueAnimationCount();
|
||||
const timerActiveBeforeBaseChange = internals.imageAnimationStateForURL(imageURL).timerActive;
|
||||
println(
|
||||
`active animations before base change: ${activeAnimationsBeforeBaseChange}`
|
||||
`timer active before base change: ${timerActiveBeforeBaseChange}`
|
||||
);
|
||||
|
||||
const base = document.createElement("base");
|
||||
|
|
@ -25,9 +26,8 @@
|
|||
|
||||
target.remove();
|
||||
internals.dumpLayoutTree(document);
|
||||
const activeAnimationsAfterRemoval =
|
||||
internals.activeImageStyleValueAnimationCount();
|
||||
println(`active animations after removal: ${activeAnimationsAfterRemoval}`);
|
||||
const timerActiveAfterRemoval = internals.imageAnimationStateForURL(imageURL).timerActive;
|
||||
println(`timer active after removal: ${timerActiveAfterRemoval}`);
|
||||
done();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -10,19 +10,21 @@
|
|||
<div id="target"></div>
|
||||
<dialog id="dialog">Hello</dialog>
|
||||
<script>
|
||||
const imageURL = "../wpt-import/images/anim-gr.gif";
|
||||
|
||||
asyncTest(async done => {
|
||||
await new Promise(resolve => window.addEventListener("load", resolve));
|
||||
|
||||
internals.dumpLayoutTree(document);
|
||||
println(`active animations before teardown: ${internals.activeImageStyleValueAnimationCount()}`);
|
||||
println(`timer active before teardown: ${internals.imageAnimationStateForURL(imageURL).timerActive}`);
|
||||
|
||||
dialog.showModal();
|
||||
internals.dumpLayoutTree(document);
|
||||
println(`active animations after teardown: ${internals.activeImageStyleValueAnimationCount()}`);
|
||||
println(`timer active after teardown: ${internals.imageAnimationStateForURL(imageURL).timerActive}`);
|
||||
|
||||
target.style.display = "none";
|
||||
internals.dumpLayoutTree(document);
|
||||
println(`active animations after hiding: ${internals.activeImageStyleValueAnimationCount()}`);
|
||||
println(`timer active after hiding: ${internals.imageAnimationStateForURL(imageURL).timerActive}`);
|
||||
done();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -9,19 +9,21 @@
|
|||
</style>
|
||||
<div id="target"></div>
|
||||
<script>
|
||||
const imageURL = "../wpt-import/images/anim-gr.gif";
|
||||
|
||||
asyncTest(async done => {
|
||||
await new Promise(resolve => window.addEventListener("load", resolve));
|
||||
|
||||
internals.dumpLayoutTree(document);
|
||||
println(`active animations before replacement: ${internals.activeImageStyleValueAnimationCount()}`);
|
||||
println(`timer active before replacement: ${internals.imageAnimationStateForURL(imageURL).timerActive}`);
|
||||
|
||||
target.style.display = "inline";
|
||||
internals.dumpLayoutTree(document);
|
||||
println(`active animations after replacement: ${internals.activeImageStyleValueAnimationCount()}`);
|
||||
println(`timer active after replacement: ${internals.imageAnimationStateForURL(imageURL).timerActive}`);
|
||||
|
||||
target.style.display = "none";
|
||||
internals.dumpLayoutTree(document);
|
||||
println(`active animations after hiding: ${internals.activeImageStyleValueAnimationCount()}`);
|
||||
println(`timer active after hiding: ${internals.imageAnimationStateForURL(imageURL).timerActive}`);
|
||||
done();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -9,16 +9,18 @@
|
|||
</style>
|
||||
<div id="target"></div>
|
||||
<script>
|
||||
const imageURL = "../wpt-import/images/anim-gr.gif";
|
||||
|
||||
asyncTest(async done => {
|
||||
await new Promise(resolve => window.addEventListener("load", resolve));
|
||||
|
||||
internals.dumpLayoutTree(document);
|
||||
println(`active animations before hiding: ${internals.activeImageStyleValueAnimationCount()}`);
|
||||
println(`timer active before hiding: ${internals.imageAnimationStateForURL(imageURL).timerActive}`);
|
||||
|
||||
target.style.display = "none";
|
||||
internals.dumpLayoutTree(document);
|
||||
|
||||
println(`active animations after hiding: ${internals.activeImageStyleValueAnimationCount()}`);
|
||||
println(`timer active after hiding: ${internals.imageAnimationStateForURL(imageURL).timerActive}`);
|
||||
done();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -7,16 +7,18 @@
|
|||
</style>
|
||||
<div id="target"></div>
|
||||
<script>
|
||||
const imageURL = "../wpt-import/images/anim-gr.gif";
|
||||
|
||||
asyncTest(async done => {
|
||||
await new Promise(resolve => window.addEventListener("load", resolve));
|
||||
|
||||
internals.dumpLayoutTree(document);
|
||||
println(`active animations before hiding: ${internals.activeImageStyleValueAnimationCount()}`);
|
||||
println(`timer active before hiding: ${internals.imageAnimationStateForURL(imageURL).timerActive}`);
|
||||
|
||||
target.style.display = "none";
|
||||
internals.dumpLayoutTree(document);
|
||||
|
||||
println(`active animations after hiding: ${internals.activeImageStyleValueAnimationCount()}`);
|
||||
println(`timer active after hiding: ${internals.imageAnimationStateForURL(imageURL).timerActive}`);
|
||||
done();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<!doctype html>
|
||||
<script src="../include.js"></script>
|
||||
<img id="first" src="../../data/loop-count-2.gif" />
|
||||
<img id="second" src="../../data/loop-count-2.gif" />
|
||||
<script>
|
||||
const imageURL = "../../data/loop-count-2.gif";
|
||||
|
||||
promiseTest(async () => {
|
||||
const stateBeforeRestart = await waitForImageAnimationState(imageURL, state => state.frameIndex > 0);
|
||||
|
||||
println(`client count before restart: ${stateBeforeRestart.clientCount}`);
|
||||
println(`frame advanced before restart: ${stateBeforeRestart.frameIndex > 0}`);
|
||||
|
||||
first.removeAttribute("src");
|
||||
await timeout(0);
|
||||
|
||||
const reloaded = new Promise(resolve => first.addEventListener("load", resolve, { once: true }));
|
||||
first.src = imageURL;
|
||||
await reloaded;
|
||||
|
||||
const stateAfterRestart = internals.imageAnimationStateForURL(imageURL);
|
||||
println(`session unchanged after restart: ${stateAfterRestart.sessionID === stateBeforeRestart.sessionID}`);
|
||||
println(
|
||||
`animation restarted after src reset: ${stateAfterRestart.frameIndex === 0 && stateAfterRestart.loopsCompleted === 0 && stateAfterRestart.timerActive}`
|
||||
);
|
||||
println(`client count after restart: ${stateAfterRestart.clientCount}`);
|
||||
});
|
||||
</script>
|
||||
|
|
@ -9,11 +9,13 @@
|
|||
"
|
||||
></div>
|
||||
<script>
|
||||
const imageURL = new URL("../wpt-import/images/anim-gr.gif", document.location).href;
|
||||
|
||||
asyncTest(done => {
|
||||
window.addEventListener("load", () => {
|
||||
internals.dumpLayoutTree(document);
|
||||
const activeAnimationsBeforeBaseChange =
|
||||
internals.activeImageStyleValueAnimationCount();
|
||||
const timerActiveBeforeBaseChange =
|
||||
internals.imageAnimationStateForURL(imageURL).timerActive;
|
||||
|
||||
const beforeBaseChange = getComputedStyle(target).backgroundImage;
|
||||
const base = document.createElement("base");
|
||||
|
|
@ -22,18 +24,18 @@
|
|||
const afterBaseChange = getComputedStyle(target).backgroundImage;
|
||||
|
||||
internals.dumpLayoutTree(document);
|
||||
const activeAnimationsAfterBaseChange =
|
||||
internals.activeImageStyleValueAnimationCount();
|
||||
const timerActiveAfterBaseChange =
|
||||
internals.imageAnimationStateForURL(imageURL).timerActive;
|
||||
|
||||
println(
|
||||
`Computed URL before base change: ${beforeBaseChange}`
|
||||
);
|
||||
println(`Computed URL after base change: ${afterBaseChange}`);
|
||||
println(
|
||||
`active animations before base change: ${activeAnimationsBeforeBaseChange}`
|
||||
`timer active before base change: ${timerActiveBeforeBaseChange}`
|
||||
);
|
||||
println(
|
||||
`active animations after base change: ${activeAnimationsAfterBaseChange}`
|
||||
`timer active after base change: ${timerActiveAfterBaseChange}`
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -52,6 +52,21 @@ function timeout(ms) {
|
|||
return promise;
|
||||
}
|
||||
|
||||
async function waitForImageAnimationState(url, predicate, targetWindow = window) {
|
||||
return new Promise(async resolve => {
|
||||
while (true) {
|
||||
try {
|
||||
const state = targetWindow.internals.imageAnimationStateForURL(url);
|
||||
if (predicate(state)) return resolve(state);
|
||||
} catch {
|
||||
// The image hasn't loaded yet.
|
||||
}
|
||||
|
||||
await animationFrame();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const __testErrorHandlerController = new AbortController();
|
||||
window.addEventListener(
|
||||
"error",
|
||||
|
|
|
|||
Loading…
Reference in a new issue