Skip to content

fix(gif): compose sub-rectangle frames so the animation actually plays - #195

Merged
LeadcodeDev merged 1 commit into
mainfrom
fix/gif-frame-composition
Aug 12, 2026
Merged

fix(gif): compose sub-rectangle frames so the animation actually plays#195
LeadcodeDev merged 1 commit into
mainfrom
fix/gif-frame-composition

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Closes #185.

What

A gif component showed its first frame and then nothing. No error, no warning — the scenario validated and the render succeeded.

ffmpeg -f lavfi -i "testsrc2=size=64x64:rate=12:duration=2" anim.gif   # 24 frames
rustmotion render -f gif.json -o f.png --frame 0    # 16140 non-black pixels
rustmotion render -f gif.json -o f.png --frame 5    #     0 non-black pixels

Cause

Every frame after the first is usually a sub-rectangle holding only the pixels that changed. The decoder stored frame.buffer alongside the canvas dimensions, so the buffer was shorter than width * height * 4. raster_from_data returned None, the if let Some(img) arm was skipped, and nothing was drawn or said. The first frame is full-canvas in most encoders — which is why it was the only one that ever appeared.

How

decode_composed_frames composes each frame onto a persistent canvas at (left, top):

  • alpha 0 means "leave what is underneath" — GIF transparency is an index, not a channel, and that is the whole point of the sub-rectangle encoding;
  • disposal is honoured: Background clears the frame's rect, Previous restores the canvas captured before compositing, Keep/Any leave it;
  • the rect is clamped to the canvas, so a malformed GIF cannot panic the blit.

Even at the right size, drawing an optimised frame on its own would have shown a fragment against an empty background — the composition is the fix, not just the sizing.

Two smaller corrections came with it: the three silent returns now report once per source (the treatment #181 gave the audio analysis), and AlphaType is Unpremul because the decoder emits straight RGBA.

After

frame  0 -> 16140 non-black   fingerprint 2db960
frame  5 -> 16128 non-black   fingerprint a26a0c
frame 10 -> 16104 non-black   fingerprint ae6838
frame 20 -> 16056 non-black   fingerprint ab2845

Tests

gif had none. Three added, on a fixture written with the gif encoder — frame 0 full canvas, frame 1 a 2x2 sub-rectangle with Keep:

  • both frames are drawable, full-canvas, and differ; the kept half stays red and the patched half turns blue
  • a missing file returns None and claims its warn-once slot — silence was the bug, so the test asserts the reporting, not just the return
  • a rect running past the canvas is clamped

cargo test --workspace green, cargo fmt --check and cargo clippy --all-targets -- -D warnings clean.

A gif component showed its first frame and then nothing — no error, no
warning, scenario valid, render successful.

Every frame after the first is usually a sub-rectangle holding only the pixels
that changed. The decoder stored frame.buffer alongside the *canvas*
dimensions, so the buffer was shorter than width * height * 4;
raster_from_data returned None and the `if let Some(img)` arm was skipped. The
first frame is full-canvas in most encoders, which is why it was the only one
that ever appeared.

Compose each frame onto a persistent canvas at (left, top), honouring alpha 0
as "leave what is underneath" and the disposal method (Background clears the
frame's rect, Previous restores the canvas, Keep/Any leave it). Even at the
right size, drawing an optimised frame alone would have shown a fragment on an
empty background.

The three silent exits now report once per source, the way #181 made the audio
analysis report. AlphaType is Unpremul: the decoder emits straight RGBA.
@LeadcodeDev
LeadcodeDev merged commit 844213b into main Aug 12, 2026
3 checks passed
@LeadcodeDev
LeadcodeDev deleted the fix/gif-frame-composition branch August 12, 2026 15:25
@LeadcodeDev LeadcodeDev added the bug Something isn't working label Aug 12, 2026
@LeadcodeDev LeadcodeDev self-assigned this Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gif: only the first frame renders, the rest silently draw nothing

1 participant