ladybird/Libraries/LibWeb/SVG/SVGDecodedImageData.h
Callum Law 8ebdaeab69 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.
2026-06-18 10:44:25 +02:00

113 lines
4.6 KiB
C++

/*
* Copyright (c) 2023, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <LibGfx/DecodedImageFrame.h>
#include <LibWeb/HTML/DecodedImageData.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/DisplayList.h>
#include <LibWeb/Painting/DisplayListResourceStorage.h>
namespace Web::SVG {
class SVGDecodedImageData final : public HTML::DecodedImageData {
GC_CELL(SVGDecodedImageData, HTML::DecodedImageData);
GC_DECLARE_ALLOCATOR(SVGDecodedImageData);
public:
class SVGPageClient;
static ErrorOr<GC::Ref<SVGDecodedImageData>> create(JS::Realm&, GC::Ref<Page>, URL::URL const&, ReadonlyBytes encoded_svg);
virtual ~SVGDecodedImageData() override;
virtual Optional<Gfx::DecodedImageFrame> default_frame(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. :^)
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&, Gfx::IntRect dst_rect, CSS::ImageRendering) const override;
private:
SVGDecodedImageData(GC::Ref<Page>, GC::Ref<SVGPageClient>, GC::Ref<DOM::Document>, GC::Ref<SVG::SVGSVGElement>);
RefPtr<Gfx::PaintingSurface> render_to_surface(Gfx::IntSize) const;
Optional<Painting::DisplayListResource> record_display_list(Gfx::IntSize, Painting::DisplayListResourceStorage&) const;
void prune_cached_display_list_resources() const;
// FIXME: Remove this once everything is using surfaces instead.
mutable HashMap<Gfx::IntSize, Gfx::DecodedImageFrame> m_cached_rendered_frames;
mutable HashMap<Gfx::IntSize, NonnullRefPtr<Gfx::PaintingSurface>> m_cached_rendered_surfaces;
struct CachedDisplayList {
NonnullRefPtr<Painting::DisplayList> display_list;
Painting::AccumulatedVisualContextTree visual_context_tree;
};
mutable HashMap<Gfx::IntSize, CachedDisplayList> m_cached_display_lists;
GC::Ref<Page> m_page;
GC::Ref<SVGPageClient> m_page_client;
GC::Ref<DOM::Document> m_document;
GC::Ref<SVG::SVGSVGElement> m_root_element;
};
class SVGDecodedImageData::SVGPageClient final : public PageClient {
GC_CELL(SVGDecodedImageData::SVGPageClient, PageClient);
GC_DECLARE_ALLOCATOR(SVGDecodedImageData::SVGPageClient);
public:
static GC::Ref<SVGPageClient> create(JS::VM& vm, Page& page)
{
return vm.heap().allocate<SVGPageClient>(page);
}
virtual ~SVGPageClient() override = default;
GC::Ref<Page> m_host_page;
GC::Ptr<Page> m_svg_page;
virtual u64 id() const override { VERIFY_NOT_REACHED(); }
virtual Page& page() override { return *m_svg_page; }
virtual Page const& page() const override { return *m_svg_page; }
virtual bool is_connection_open() const override { return false; }
virtual Gfx::Palette palette() const override { return m_host_page->client().palette(); }
virtual DevicePixelRect screen_rect() const override { return {}; }
virtual double zoom_level() const override { return 1.0; }
virtual double device_pixel_ratio() const override { return 1.0; }
virtual double device_pixels_per_css_pixel() const override { return 1.0; }
virtual CSS::PreferredColorScheme preferred_color_scheme() const override { return m_host_page->client().preferred_color_scheme(); }
virtual CSS::PreferredContrast preferred_contrast() const override { return m_host_page->client().preferred_contrast(); }
virtual CSS::PreferredMotion preferred_motion() const override { return m_host_page->client().preferred_motion(); }
virtual size_t screen_count() const override { return 1; }
virtual void request_file(FileRequest) override { }
virtual Queue<QueuedInputEvent>& input_event_queue() override { VERIFY_NOT_REACHED(); }
virtual void report_finished_handling_input_event([[maybe_unused]] u64 page_id, [[maybe_unused]] EventResult event_was_handled) override { }
virtual void request_frame() override { }
virtual bool is_headless() const override { return m_host_page->client().is_headless(); }
private:
explicit SVGPageClient(Page& host_page)
: m_host_page(host_page)
{
}
virtual bool is_svg_page_client() const override { return true; }
virtual void visit_edges(Visitor&) override;
};
}