From 8ebdaeab69ad02ec3d87bc45e9b9a6b714b0f565 Mon Sep 17 00:00:00 2001 From: Callum Law Date: Tue, 16 Jun 2026 10:56:00 +1200 Subject: [PATCH] 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. --- Libraries/LibWeb/CMakeLists.txt | 1 + .../CSS/StyleValues/ImageStyleValue.cpp | 118 ++---------------- .../LibWeb/CSS/StyleValues/ImageStyleValue.h | 18 +-- Libraries/LibWeb/DOM/Document.cpp | 16 --- Libraries/LibWeb/DOM/Document.h | 2 - .../HTML/AnimatedBitmapDecodedImageData.cpp | 97 +++++++++++--- .../HTML/AnimatedBitmapDecodedImageData.h | 35 ++++-- .../LibWeb/HTML/AnimatedDecodedImageData.cpp | 84 +++++++++++++ .../LibWeb/HTML/AnimatedDecodedImageData.h | 38 ++++++ .../LibWeb/HTML/BitmapDecodedImageData.cpp | 4 +- .../LibWeb/HTML/BitmapDecodedImageData.h | 9 +- Libraries/LibWeb/HTML/DecodedImageData.cpp | 2 + Libraries/LibWeb/HTML/DecodedImageData.h | 14 +-- Libraries/LibWeb/HTML/HTMLImageElement.cpp | 94 +------------- Libraries/LibWeb/HTML/HTMLImageElement.h | 11 -- Libraries/LibWeb/HTML/HTMLInputElement.h | 1 - Libraries/LibWeb/HTML/HTMLObjectElement.h | 1 - .../LibWeb/HTML/SharedResourceRequest.cpp | 1 + Libraries/LibWeb/Internals/Internals.cpp | 37 +++++- Libraries/LibWeb/Internals/Internals.h | 2 +- Libraries/LibWeb/Internals/Internals.idl | 2 +- Libraries/LibWeb/Layout/ImageProvider.cpp | 2 +- Libraries/LibWeb/Layout/ImageProvider.h | 2 - Libraries/LibWeb/Layout/TreeBuilder.cpp | 7 -- Libraries/LibWeb/Painting/ImagePaintable.cpp | 2 +- Libraries/LibWeb/SVG/SVGDecodedImageData.cpp | 6 +- Libraries/LibWeb/SVG/SVGDecodedImageData.h | 9 +- Libraries/LibWeb/SVG/SVGImageElement.cpp | 34 ----- Libraries/LibWeb/SVG/SVGImageElement.h | 7 -- ...te-consumer-uses-first-consumer-timing.png | Bin 0 -> 320 bytes ...e-consumer-uses-first-consumer-timing.html | 44 +++++++ ...-background-image-repaints-on-advance.html | 21 +--- ...imated-mask-image-repaints-on-advance.html | 19 +-- Tests/LibWeb/Text/data/loop-count-2.gif | Bin 0 -> 69 bytes ...ckground-image-finite-loop-stops-timer.txt | 1 + ...und-image-pauses-for-inactive-document.txt | 2 + ...background-image-shared-resource-count.txt | 2 + ...d-image-timer-count-is-document-scoped.txt | 4 +- ...ge-timer-doesnt-stop-after-base-change.txt | 2 + ...t-stop-after-full-layout-tree-teardown.txt | 3 + ...snt-stop-after-layout-node-replacement.txt | 3 + ...nd-image-timer-doesnt-stop-when-hidden.txt | 2 + ...nd-image-timer-stops-after-base-change.txt | 2 - ...-stops-after-full-layout-tree-teardown.txt | 3 - ...er-stops-after-layout-node-replacement.txt | 3 - ...ckground-image-timer-stops-when-hidden.txt | 2 - ...nt-image-timer-doesnt-stop-when-hidden.txt | 2 + ...-content-image-timer-stops-when-hidden.txt | 2 - .../animated-img-restart-shared-resource.txt | 5 + .../inline-relative-css-image-base-url.txt | 4 +- ...kground-image-finite-loop-stops-timer.html | 31 +++++ ...nd-image-pauses-for-inactive-document.html | 52 ++++++++ ...ackground-image-shared-resource-count.html | 24 ++++ ...-image-timer-count-is-document-scoped.html | 15 ++- ...-timer-doesnt-stop-after-base-change.html} | 12 +- ...stop-after-full-layout-tree-teardown.html} | 8 +- ...t-stop-after-layout-node-replacement.html} | 8 +- ...-image-timer-doesnt-stop-when-hidden.html} | 6 +- ...-image-timer-doesnt-stop-when-hidden.html} | 6 +- .../animated-img-restart-shared-resource.html | 28 +++++ .../inline-relative-css-image-base-url.html | 14 ++- Tests/LibWeb/Text/input/include.js | 15 +++ 62 files changed, 568 insertions(+), 433 deletions(-) create mode 100644 Libraries/LibWeb/HTML/AnimatedDecodedImageData.cpp create mode 100644 Libraries/LibWeb/HTML/AnimatedDecodedImageData.h create mode 100644 Tests/LibWeb/Screenshot/expected/animated-background-image-late-consumer-uses-first-consumer-timing.png create mode 100644 Tests/LibWeb/Screenshot/input/animated-background-image-late-consumer-uses-first-consumer-timing.html create mode 100644 Tests/LibWeb/Text/data/loop-count-2.gif create mode 100644 Tests/LibWeb/Text/expected/css/animated-background-image-finite-loop-stops-timer.txt create mode 100644 Tests/LibWeb/Text/expected/css/animated-background-image-pauses-for-inactive-document.txt create mode 100644 Tests/LibWeb/Text/expected/css/animated-background-image-shared-resource-count.txt create mode 100644 Tests/LibWeb/Text/expected/css/animated-background-image-timer-doesnt-stop-after-base-change.txt create mode 100644 Tests/LibWeb/Text/expected/css/animated-background-image-timer-doesnt-stop-after-full-layout-tree-teardown.txt create mode 100644 Tests/LibWeb/Text/expected/css/animated-background-image-timer-doesnt-stop-after-layout-node-replacement.txt create mode 100644 Tests/LibWeb/Text/expected/css/animated-background-image-timer-doesnt-stop-when-hidden.txt delete mode 100644 Tests/LibWeb/Text/expected/css/animated-background-image-timer-stops-after-base-change.txt delete mode 100644 Tests/LibWeb/Text/expected/css/animated-background-image-timer-stops-after-full-layout-tree-teardown.txt delete mode 100644 Tests/LibWeb/Text/expected/css/animated-background-image-timer-stops-after-layout-node-replacement.txt delete mode 100644 Tests/LibWeb/Text/expected/css/animated-background-image-timer-stops-when-hidden.txt create mode 100644 Tests/LibWeb/Text/expected/css/animated-generated-content-image-timer-doesnt-stop-when-hidden.txt delete mode 100644 Tests/LibWeb/Text/expected/css/animated-generated-content-image-timer-stops-when-hidden.txt create mode 100644 Tests/LibWeb/Text/expected/css/animated-img-restart-shared-resource.txt create mode 100644 Tests/LibWeb/Text/input/css/animated-background-image-finite-loop-stops-timer.html create mode 100644 Tests/LibWeb/Text/input/css/animated-background-image-pauses-for-inactive-document.html create mode 100644 Tests/LibWeb/Text/input/css/animated-background-image-shared-resource-count.html rename Tests/LibWeb/Text/input/css/{animated-background-image-timer-stops-after-base-change.html => animated-background-image-timer-doesnt-stop-after-base-change.html} (58%) rename Tests/LibWeb/Text/input/css/{animated-background-image-timer-stops-after-full-layout-tree-teardown.html => animated-background-image-timer-doesnt-stop-after-full-layout-tree-teardown.html} (61%) rename Tests/LibWeb/Text/input/css/{animated-background-image-timer-stops-after-layout-node-replacement.html => animated-background-image-timer-doesnt-stop-after-layout-node-replacement.html} (60%) rename Tests/LibWeb/Text/input/css/{animated-background-image-timer-stops-when-hidden.html => animated-background-image-timer-doesnt-stop-when-hidden.html} (64%) rename Tests/LibWeb/Text/input/css/{animated-generated-content-image-timer-stops-when-hidden.html => animated-generated-content-image-timer-doesnt-stop-when-hidden.html} (62%) create mode 100644 Tests/LibWeb/Text/input/css/animated-img-restart-shared-resource.html diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 9c46daeed8..243b8b067f 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -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 diff --git a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp index 0cf59d2185..eaac46b87b 100644 --- a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp @@ -8,7 +8,6 @@ */ #include -#include #include #include #include @@ -22,7 +21,6 @@ #include #include #include -#include namespace Web::CSS { @@ -34,7 +32,7 @@ ImageStyleValueResource::ImageStyleValueResource(GC::Refcss_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 ImageStyleValueResource::decoded_image_data() const @@ -74,15 +67,9 @@ GC::Ptr 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::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 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(); - image_data->paint(context, current_frame_index, dest_int_rect, image_rendering); + image_data->paint(context, dest_int_rect, image_rendering); } Optional 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()); - + return image_data->current_frame(dest_rect.size().to_type()); 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 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 diff --git a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h index 7d22e17fca..8273235061 100644 --- a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h +++ b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h @@ -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 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 m_resource_request; - GC::Ptr m_timer; HashTable 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 create(URL const&, Optional<::URL::URL> style_resource_base_url); static ValueComparingNonnullRefPtr 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 color_if_single_pixel_bitmap(DOM::Document const&) const override; Optional current_frame(DOM::Document const&, DevicePixelRect const& dest_rect = {}) const; - size_t current_frame_index(DOM::Document const&) const; GC::Ptr image_data(DOM::Document const&) const; diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 1223c86bfe..66edb4e983 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -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; diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index ff3371ef5c..314fe1b639 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -831,8 +831,6 @@ public: CSS::ImageStyleValueResource const* css_image_resource(URL::URL const&) const; CSS::ImageStyleValueResource& create_css_image_resource(GC::Ref); 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 entry); diff --git a/Libraries/LibWeb/HTML/AnimatedBitmapDecodedImageData.cpp b/Libraries/LibWeb/HTML/AnimatedBitmapDecodedImageData.cpp index 16fcfe45e0..c1076fd893 100644 --- a/Libraries/LibWeb/HTML/AnimatedBitmapDecodedImageData.cpp +++ b/Libraries/LibWeb/HTML/AnimatedBitmapDecodedImageData.cpp @@ -9,11 +9,14 @@ #include #include #include +#include #include +#include #include #include #include #include +#include namespace Web::HTML { @@ -55,6 +58,7 @@ void AnimatedBitmapDecodedImageData::deliver_frames_for_session(i64 session_id, GC::Ref AnimatedBitmapDecodedImageData::create( JS::Realm& realm, + DOM::Document& document, i64 session_id, u32 frame_count, u32 loop_count, @@ -63,8 +67,11 @@ GC::Ref AnimatedBitmapDecodedImageData::create( Vector durations, Vector> initial_bitmaps) { + auto animation_timer = Platform::Timer::create(realm.heap()); + auto document_observer = realm.create(realm, document); + auto data = realm.create( - 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 durations) - : m_session_id(session_id) + Vector durations, + GC::Ref animation_timer, + GC::Ref 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 AnimatedBitmapDecodedImageData::default_frame(G return frame(0, size); } +Optional 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 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 #include #include +#include #include 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 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 default_frame(Gfx::IntSize = {}) const override; - virtual Optional 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 current_frame(Gfx::IntSize = {}) const override; virtual Optional intrinsic_width() const override; virtual Optional intrinsic_height() const override; virtual Optional 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>, u32 start_frame_index); @@ -77,10 +74,22 @@ private: u32 loop_count, Gfx::IntSize, Gfx::ColorSpace, - Vector durations); + Vector durations, + GC::Ref, + GC::Ref); 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 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 m_animation_timer; }; } diff --git a/Libraries/LibWeb/HTML/AnimatedDecodedImageData.cpp b/Libraries/LibWeb/HTML/AnimatedDecodedImageData.cpp new file mode 100644 index 0000000000..568aec3240 --- /dev/null +++ b/Libraries/LibWeb/HTML/AnimatedDecodedImageData.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Callum Law + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "AnimatedDecodedImageData.h" +#include +#include + +namespace Web::HTML { + +void AnimatedDecodedImageData::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_document_observer); +} + +AnimatedDecodedImageData::AnimatedDecodedImageData(GC::Ref 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(); +} + +} diff --git a/Libraries/LibWeb/HTML/AnimatedDecodedImageData.h b/Libraries/LibWeb/HTML/AnimatedDecodedImageData.h new file mode 100644 index 0000000000..76f485cb7c --- /dev/null +++ b/Libraries/LibWeb/HTML/AnimatedDecodedImageData.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Callum Law + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +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); + + 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 m_document_observer; + bool m_should_start_animation_on_client_registration { true }; +}; + +} diff --git a/Libraries/LibWeb/HTML/BitmapDecodedImageData.cpp b/Libraries/LibWeb/HTML/BitmapDecodedImageData.cpp index 46694ec05f..b74f226a28 100644 --- a/Libraries/LibWeb/HTML/BitmapDecodedImageData.cpp +++ b/Libraries/LibWeb/HTML/BitmapDecodedImageData.cpp @@ -34,7 +34,7 @@ size_t BitmapDecodedImageData::external_memory_size() const return m_frame.bitmap().data_size(); } -Optional BitmapDecodedImageData::frame(size_t, Gfx::IntSize) const +Optional BitmapDecodedImageData::current_frame(Gfx::IntSize) const { return m_frame; } @@ -59,7 +59,7 @@ Optional 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()); diff --git a/Libraries/LibWeb/HTML/BitmapDecodedImageData.h b/Libraries/LibWeb/HTML/BitmapDecodedImageData.h index 751eeb5ccf..1300d5aec2 100644 --- a/Libraries/LibWeb/HTML/BitmapDecodedImageData.h +++ b/Libraries/LibWeb/HTML/BitmapDecodedImageData.h @@ -21,18 +21,13 @@ public: virtual ~BitmapDecodedImageData() override; virtual Optional default_frame(Gfx::IntSize = {}) const override; - virtual Optional 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 current_frame(Gfx::IntSize = {}) const override; virtual Optional intrinsic_width() const override; virtual Optional intrinsic_height() const override; virtual Optional 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); diff --git a/Libraries/LibWeb/HTML/DecodedImageData.cpp b/Libraries/LibWeb/HTML/DecodedImageData.cpp index de7f44e13b..2236268925 100644 --- a/Libraries/LibWeb/HTML/DecodedImageData.cpp +++ b/Libraries/LibWeb/HTML/DecodedImageData.cpp @@ -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() diff --git a/Libraries/LibWeb/HTML/DecodedImageData.h b/Libraries/LibWeb/HTML/DecodedImageData.h index a4cdbbde2e..b6ee56ab9a 100644 --- a/Libraries/LibWeb/HTML/DecodedImageData.h +++ b/Libraries/LibWeb/HTML/DecodedImageData.h @@ -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 default_frame(Gfx::IntSize = {}) const = 0; - virtual Optional frame(size_t frame_index, Gfx::IntSize = {}) const = 0; - virtual int frame_duration(size_t frame_index) const = 0; + virtual Optional 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 intrinsic_width() const = 0; virtual Optional 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 m_clients; diff --git a/Libraries/LibWeb/HTML/HTMLImageElement.cpp b/Libraries/LibWeb/HTML/HTMLImageElement.cpp index 86a45b2d4f..7cc67a1baf 100644 --- a/Libraries/LibWeb/HTML/HTMLImageElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLImageElement.cpp @@ -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(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 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: diff --git a/Libraries/LibWeb/HTML/HTMLImageElement.h b/Libraries/LibWeb/HTML/HTMLImageElement.h index 792917dca2..fc3aa464f0 100644 --- a/Libraries/LibWeb/HTML/HTMLImageElement.h +++ b/Libraries/LibWeb/HTML/HTMLImageElement.h @@ -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 m_animation_timer; - size_t m_current_frame_index { 0 }; - size_t m_loops_completed { 0 }; - bool m_animation_paused_by_visibility { false }; - Optional m_load_event_delayer; GC::Ptr m_document_observer; diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.h b/Libraries/LibWeb/HTML/HTMLInputElement.h index 679cb9f867..dd36cfffc0 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -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 decoded_image_data() const override { return image_data(); } virtual void initialize(JS::Realm&) override; diff --git a/Libraries/LibWeb/HTML/HTMLObjectElement.h b/Libraries/LibWeb/HTML/HTMLObjectElement.h index 786bc81856..c141031a89 100644 --- a/Libraries/LibWeb/HTML/HTMLObjectElement.h +++ b/Libraries/LibWeb/HTML/HTMLObjectElement.h @@ -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 decoded_image_data() const override { return image_data(); } GC::Ptr image_data() const; diff --git a/Libraries/LibWeb/HTML/SharedResourceRequest.cpp b/Libraries/LibWeb/HTML/SharedResourceRequest.cpp index ebe8e09aec..d0a63875a3 100644 --- a/Libraries/LibWeb/HTML/SharedResourceRequest.cpp +++ b/Libraries/LibWeb/HTML/SharedResourceRequest.cpp @@ -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, diff --git a/Libraries/LibWeb/Internals/Internals.cpp b/Libraries/LibWeb/Internals/Internals.cpp index 329817742b..8835d57400 100644 --- a/Libraries/LibWeb/Internals/Internals.cpp +++ b/Libraries/LibWeb/Internals/Internals.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -35,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +56,7 @@ #include #include #include +#include #include 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 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(*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(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 { diff --git a/Libraries/LibWeb/Internals/Internals.h b/Libraries/LibWeb/Internals/Internals.h index ac8ddbb747..d1dd29d6f0 100644 --- a/Libraries/LibWeb/Internals/Internals.h +++ b/Libraries/LibWeb/Internals/Internals.h @@ -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 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); diff --git a/Libraries/LibWeb/Internals/Internals.idl b/Libraries/LibWeb/Internals/Internals.idl index f5b3b2e26a..d021a4c7c2 100644 --- a/Libraries/LibWeb/Internals/Internals.idl +++ b/Libraries/LibWeb/Internals/Internals.idl @@ -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); diff --git a/Libraries/LibWeb/Layout/ImageProvider.cpp b/Libraries/LibWeb/Layout/ImageProvider.cpp index f190f3afe2..3cf6b7269a 100644 --- a/Libraries/LibWeb/Layout/ImageProvider.cpp +++ b/Libraries/LibWeb/Layout/ImageProvider.cpp @@ -50,7 +50,7 @@ Optional ImageProvider::intrinsic_size() const Optional ImageProvider::current_image_frame(Optional size) const { if (auto const& data = decoded_image_data()) - return data->frame(current_frame_index(), size.value_or(intrinsic_size().value_or({}).to_type())); + return data->current_frame(size.value_or(intrinsic_size().value_or({}).to_type())); return {}; } diff --git a/Libraries/LibWeb/Layout/ImageProvider.h b/Libraries/LibWeb/Layout/ImageProvider.h index c985591f65..f1a59537d4 100644 --- a/Libraries/LibWeb/Layout/ImageProvider.h +++ b/Libraries/LibWeb/Layout/ImageProvider.h @@ -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 decoded_image_data() const = 0; Optional intrinsic_width() const; diff --git a/Libraries/LibWeb/Layout/TreeBuilder.cpp b/Libraries/LibWeb/Layout/TreeBuilder.cpp index d17fdb414d..db77d9816b 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.cpp +++ b/Libraries/LibWeb/Layout/TreeBuilder.cpp @@ -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 decoded_image_data() const override { if (auto document = this->document()) diff --git a/Libraries/LibWeb/Painting/ImagePaintable.cpp b/Libraries/LibWeb/Painting/ImagePaintable.cpp index 43875b50b0..0b2a559ce1 100644 --- a/Libraries/LibWeb/Painting/ImagePaintable.cpp +++ b/Libraries/LibWeb/Painting/ImagePaintable.cpp @@ -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(); } diff --git a/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp b/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp index a9da29e147..7a4d34297a 100644 --- a/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp +++ b/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp @@ -221,7 +221,7 @@ RefPtr SVGDecodedImageData::render_to_surface(Gfx::IntSize return surface; } -Optional SVGDecodedImageData::frame(size_t, Gfx::IntSize size) const +Optional SVGDecodedImageData::current_frame(Gfx::IntSize size) const { if (size.is_empty()) return {}; @@ -243,7 +243,7 @@ Optional 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 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()) diff --git a/Libraries/LibWeb/SVG/SVGDecodedImageData.h b/Libraries/LibWeb/SVG/SVGDecodedImageData.h index deb9e334e4..c077191eff 100644 --- a/Libraries/LibWeb/SVG/SVGDecodedImageData.h +++ b/Libraries/LibWeb/SVG/SVGDecodedImageData.h @@ -25,24 +25,19 @@ public: virtual ~SVGDecodedImageData() override; virtual Optional default_frame(Gfx::IntSize = {}) const override; - virtual Optional frame(size_t frame_index, Gfx::IntSize) const override; + virtual Optional current_frame(Gfx::IntSize = {}) const override; virtual Optional intrinsic_width() const override; virtual Optional intrinsic_height() const override; virtual Optional 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, GC::Ref, GC::Ref, GC::Ref); diff --git a/Libraries/LibWeb/SVG/SVGImageElement.cpp b/Libraries/LibWeb/SVG/SVGImageElement.cpp index 20569dc229..efa65704a2 100644 --- a/Libraries/LibWeb/SVG/SVGImageElement.cpp +++ b/Libraries/LibWeb/SVG/SVGImageElement.cpp @@ -5,7 +5,6 @@ */ #include "SVGImageElement.h" -#include #include #include #include @@ -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 SVGImageElement::create_layout_node(CSS::ComputedProperties return make_ref_counted(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 SVGImageElement::decoded_image_data() const { if (!m_resource_request) diff --git a/Libraries/LibWeb/SVG/SVGImageElement.h b/Libraries/LibWeb/SVG/SVGImageElement.h index 63ad5b031d..824e9c1fad 100644 --- a/Libraries/LibWeb/SVG/SVGImageElement.h +++ b/Libraries/LibWeb/SVG/SVGImageElement.h @@ -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 decoded_image_data() const override; protected: @@ -57,17 +56,11 @@ private: virtual RefPtr create_layout_node(CSS::ComputedProperties const&) override; virtual void decoded_image_data_did_update() override { set_needs_repaint(); } - void animate(); - GC::Ptr m_x; GC::Ptr m_y; GC::Ptr m_width; GC::Ptr m_height; - RefPtr m_animation_timer; - size_t m_current_frame_index { 0 }; - size_t m_loops_completed { 0 }; - Optional m_href; GC::Ptr m_resource_request; diff --git a/Tests/LibWeb/Screenshot/expected/animated-background-image-late-consumer-uses-first-consumer-timing.png b/Tests/LibWeb/Screenshot/expected/animated-background-image-late-consumer-uses-first-consumer-timing.png new file mode 100644 index 0000000000000000000000000000000000000000..689c22f986ed4646144fcd2442f0c0bc02d1aa3f GIT binary patch literal 320 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2WU528t}$Az%!o*aCb)Tp9i|{Qv(ye{Vz} zP~@wpi(^Q|t+!`4@-is!uxz~F@OQayl*U8>- + + +
+
+
+ + + diff --git a/Tests/LibWeb/Screenshot/input/animated-background-image-repaints-on-advance.html b/Tests/LibWeb/Screenshot/input/animated-background-image-repaints-on-advance.html index 7d48fcfa7b..6ea09301db 100644 --- a/Tests/LibWeb/Screenshot/input/animated-background-image-repaints-on-advance.html +++ b/Tests/LibWeb/Screenshot/input/animated-background-image-repaints-on-advance.html @@ -13,27 +13,16 @@ }
+ + +
+ diff --git a/Tests/LibWeb/Text/input/css/animated-background-image-pauses-for-inactive-document.html b/Tests/LibWeb/Text/input/css/animated-background-image-pauses-for-inactive-document.html new file mode 100644 index 0000000000..dd7210ae56 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/animated-background-image-pauses-for-inactive-document.html @@ -0,0 +1,52 @@ + + + diff --git a/Tests/LibWeb/Text/input/css/animated-background-image-shared-resource-count.html b/Tests/LibWeb/Text/input/css/animated-background-image-shared-resource-count.html new file mode 100644 index 0000000000..85abdb7e42 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/animated-background-image-shared-resource-count.html @@ -0,0 +1,24 @@ + + + +
+
+ diff --git a/Tests/LibWeb/Text/input/css/animated-background-image-timer-count-is-document-scoped.html b/Tests/LibWeb/Text/input/css/animated-background-image-timer-count-is-document-scoped.html index 28969f02cf..06fcb8f30e 100644 --- a/Tests/LibWeb/Text/input/css/animated-background-image-timer-count-is-document-scoped.html +++ b/Tests/LibWeb/Text/input/css/animated-background-image-timer-count-is-document-scoped.html @@ -1,6 +1,8 @@ diff --git a/Tests/LibWeb/Text/input/css/animated-background-image-timer-stops-after-full-layout-tree-teardown.html b/Tests/LibWeb/Text/input/css/animated-background-image-timer-doesnt-stop-after-full-layout-tree-teardown.html similarity index 61% rename from Tests/LibWeb/Text/input/css/animated-background-image-timer-stops-after-full-layout-tree-teardown.html rename to Tests/LibWeb/Text/input/css/animated-background-image-timer-doesnt-stop-after-full-layout-tree-teardown.html index fc132b0d7a..0577c54a84 100644 --- a/Tests/LibWeb/Text/input/css/animated-background-image-timer-stops-after-full-layout-tree-teardown.html +++ b/Tests/LibWeb/Text/input/css/animated-background-image-timer-doesnt-stop-after-full-layout-tree-teardown.html @@ -10,19 +10,21 @@
Hello diff --git a/Tests/LibWeb/Text/input/css/animated-background-image-timer-stops-after-layout-node-replacement.html b/Tests/LibWeb/Text/input/css/animated-background-image-timer-doesnt-stop-after-layout-node-replacement.html similarity index 60% rename from Tests/LibWeb/Text/input/css/animated-background-image-timer-stops-after-layout-node-replacement.html rename to Tests/LibWeb/Text/input/css/animated-background-image-timer-doesnt-stop-after-layout-node-replacement.html index 002372f98c..be973f6ac6 100644 --- a/Tests/LibWeb/Text/input/css/animated-background-image-timer-stops-after-layout-node-replacement.html +++ b/Tests/LibWeb/Text/input/css/animated-background-image-timer-doesnt-stop-after-layout-node-replacement.html @@ -9,19 +9,21 @@
diff --git a/Tests/LibWeb/Text/input/css/animated-background-image-timer-stops-when-hidden.html b/Tests/LibWeb/Text/input/css/animated-background-image-timer-doesnt-stop-when-hidden.html similarity index 64% rename from Tests/LibWeb/Text/input/css/animated-background-image-timer-stops-when-hidden.html rename to Tests/LibWeb/Text/input/css/animated-background-image-timer-doesnt-stop-when-hidden.html index b14afd0679..fa338c6c67 100644 --- a/Tests/LibWeb/Text/input/css/animated-background-image-timer-stops-when-hidden.html +++ b/Tests/LibWeb/Text/input/css/animated-background-image-timer-doesnt-stop-when-hidden.html @@ -9,16 +9,18 @@
diff --git a/Tests/LibWeb/Text/input/css/animated-generated-content-image-timer-stops-when-hidden.html b/Tests/LibWeb/Text/input/css/animated-generated-content-image-timer-doesnt-stop-when-hidden.html similarity index 62% rename from Tests/LibWeb/Text/input/css/animated-generated-content-image-timer-stops-when-hidden.html rename to Tests/LibWeb/Text/input/css/animated-generated-content-image-timer-doesnt-stop-when-hidden.html index 07192312a0..d71f52ea4f 100644 --- a/Tests/LibWeb/Text/input/css/animated-generated-content-image-timer-stops-when-hidden.html +++ b/Tests/LibWeb/Text/input/css/animated-generated-content-image-timer-doesnt-stop-when-hidden.html @@ -7,16 +7,18 @@
diff --git a/Tests/LibWeb/Text/input/css/animated-img-restart-shared-resource.html b/Tests/LibWeb/Text/input/css/animated-img-restart-shared-resource.html new file mode 100644 index 0000000000..0730fd893d --- /dev/null +++ b/Tests/LibWeb/Text/input/css/animated-img-restart-shared-resource.html @@ -0,0 +1,28 @@ + + + + + diff --git a/Tests/LibWeb/Text/input/css/inline-relative-css-image-base-url.html b/Tests/LibWeb/Text/input/css/inline-relative-css-image-base-url.html index 9f100d9cd9..71aa817b10 100644 --- a/Tests/LibWeb/Text/input/css/inline-relative-css-image-base-url.html +++ b/Tests/LibWeb/Text/input/css/inline-relative-css-image-base-url.html @@ -9,11 +9,13 @@ " >