Skip to content

Add an animated-image element for GIF / APNG / animated WebP - #3

Open
shanerbaner82 wants to merge 1 commit into
mainfrom
feat/animated-image
Open

Add an animated-image element for GIF / APNG / animated WebP#3
shanerbaner82 wants to merge 1 commit into
mainfrom
feat/animated-image

Conversation

@shanerbaner82

Copy link
Copy Markdown
Contributor

Why

Core's image element renders only the first frame of an animated file — SwiftUI's AsyncImage and Coil's default decoders both stop there.

The apparent workaround, pointing video-player at an MP4, does not survive a list. Playback surfaces are scarce:

  • iOSMediaPlayerManager is a singleton holding one player/source. A second video-player with a different source hits teardown(), destroying the AVPlayer the first surface is still displaying. The dead surfaces then render MediaPlayerVideoRenderer's Color.black fill, so extra GIFs show as solid black rectangles.
  • Android — each element gets its own VideoView, but concurrent hardware decoders run out after a handful; later ones quietly stop animating.

Found while building a GIF picker into a timeline: past ~4 posted GIFs, previews froze on Android and went black on iOS.

What

<animated-image> decodes to ordinary bitmaps, so it allocates no playback session and a feed can show dozens.

iOS CGAnimateImageDataWithBlock (ImageIO) feeding frames to a UIImageView layer — no new dependency
Android Coil AnimatedImageDecoder (API 28+), falling back to GifDecoder at the plugin's API 26 floor
Web a plain <img>, which browsers animate natively
<animated-image :src="$gif['url']" :fit="2" alt="{{ $gif['title'] }}" class="w-full h-48 rounded-2xl"/>

Attributes: src, fit (same mapping as image), alt, autoplay, loop. loop is iOS-only — Android honours the loop count baked into the file. Adds io.coil-kt.coil3:coil-gif to the Android dependencies, since core ships coil-compose but not the animated decoder.

Guidance in the README: use video-player for video with audio and transport controls, animated-image for looping imagery, and always prefer the latter when several animations share a screen.

Also: web renderers for both components

The web target has no rendering for a plugin's element types unless the plugin supplies one, and an unknown leaf renders as nothing. video-player was therefore silently invisible in the browser. MediaPlayerServiceProvider now registers HTML renderers for both — <video playsinline> and <img> — guarded by class_exists so it is a no-op without mobile-web.

Tests

37 pass. The existing registers the video_player component test asserted toHaveCount(1) on the manifest, so it now looks components up by type instead of index. Added coverage for the new component, the Coil dependency, and a check that every manifest component ships renderer files for both platforms.

Verification status

Verified end to end on the web target (element renders, GIFs load and animate, many coexist) and through component tests. The Swift and Kotlin renderers are not yet exercised on a simulator — they need a full rebuild since native code only compiles at build time. Worth a device pass before merge.

Drive-by: README @press@tap in the overlay example.

🤖 Generated with Claude Code

Core's `image` element renders only the first frame of an animated file:
SwiftUI's `AsyncImage` and Coil's default decoders both stop there. The
apparent workaround — pointing `video-player` at an MP4 — does not survive a
list, because playback surfaces are scarce. iOS shares a single AVPlayer, so
`preparePlayer` tears down the previous one and every earlier surface falls
back to the renderer's black fill; Android gives each element its own
VideoView but runs out of hardware decoders after a handful.

`animated-image` decodes to ordinary bitmaps instead, so a feed can show
dozens. iOS drives ImageIO's `CGAnimateImageDataWithBlock` into a UIImageView
layer, adding no dependency; Android registers Coil's `AnimatedImageDecoder`
(API 28+), falling back to `GifDecoder` at the plugin's API 26 floor.

Also register web renderers for both components. The web target has no
rendering for a plugin's element types unless the plugin supplies one, and an
unknown leaf renders as nothing — so `video-player` was silently invisible in
the browser. Both now have HTML equivalents (`<video>` and `<img>`).

Drive-by: README `@press` -> `@tap` in the overlay example.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant