Table of Contents

Animation

A raster holds one or more frames, all the size of the raster and the same pixel type. GIF and APNG files load with every frame composited to full size, as a browser shows them, so each frame is a complete picture you can edit on its own.

using Raster<Rgba8> anim = Raster.Load<Rgba8>("pan.gif");
Console.WriteLine($"{anim.FrameCount} frames, plays {anim.Metadata.LoopCount} times (0 = forever)");

anim.Frames.RemoveAt(0);                                            // frames are removed,
anim.Frames.Add(anim.Frames[0]).Metadata.Delay = TimeSpan.FromMilliseconds(500); // copied in,
anim.Frames.Move(anim.FrameCount - 1, 0);                           // and moved
anim.Apply(ops => ops.Fit(64, 64), Ct);                            // operations run on every frame

anim.Save("pan-small.gif");                                         // or .png for an APNG
using Raster<Rgba8> still = anim.CopyFrame(0);                      // one frame as its own raster
  • Frames.Add and Frames.Insert copy the frame you pass, from this raster or another of the same size.
  • FrameMetadata.Delay is how long a frame shows; RasterMetadata.LoopCount is how many times the animation plays, 0 for forever.
  • Every operation in a chain runs on every frame. DrawImage draws its image's first frame onto each one.

Reading part of an animation

// Only the first frame, decoding nothing after it: a fast preview of an animation.
using Raster first = Raster.Load("pan.gif", options: new ReaderOptions { Frames = ..1 });

ReaderOptions.Frames takes a range: ..1 for the first frame, ^1.. for the last (which has to read every frame to find it), 2..5 for three in the middle. Raster.Probe counts the frames without decoding them.

Writing animations

Saving a raster with more than one frame as GIF, PNG, WebP or JPEG XL writes an animation. The writers work out each frame's stored rectangle, disposal and blend from the pixels, so an edited animation stays small: only what changed between frames is stored.

GIF holds at most 256 colors per frame and has on-or-off transparency; APNG keeps full color and partial alpha. The GIF and PNG and APNG pages have each writer's options.

Frame by frame

Raster<TPixel>.Frames is a FrameList<TPixel>: index, add, insert, move and remove frames. Each frame's FrameMetadata holds its delay, how it is disposed (FrameDisposal) and blended (FrameBlend) when written as APNG, and the area that changed.

using Raster<Rgba8> clip = Raster.Load<Rgba8>("pan.gif");
FrameList<Rgba8> frames = clip.Frames;

// Play the clip forward and then backward: append the middle frames in reverse.
for (int i = frames.Count - 2; i > 0; i--)
{
    Frame<Rgba8> copy = frames.Add(frames[i]);
    copy.Metadata.Delay = TimeSpan.FromMilliseconds(80);
    copy.Metadata.Disposal = FrameDisposal.None;
    copy.Metadata.Blend = FrameBlend.Source;
}

frames.RemoveAt(0);
clip.Save("pan-bounce.gif");
using Raster<Rgba8> clip = Raster.Load<Rgba8>("pan.gif");
TimeSpan length = TimeSpan.Zero;
foreach (Frame<Rgba8> frame in clip.Frames)
{
    FrameMetadata timing = frame.Metadata;
    length += timing.Delay;
    Console.WriteLine($"{timing.Delay.TotalMilliseconds} ms, {timing.Disposal}, {timing.Blend}, changed area {timing.Bounds}");
}

Animation tools

Six steps work on the whole animation rather than on each frame. They run in Apply, Derive and Transcode (which then decodes the whole animation first) and mix with per-frame steps, but not inside Within, since a scope is an area of every frame. Frames in memory are already full pictures of what is on screen, so Egami needs no Coalesce, and the writers find each frame's changed area themselves, so it needs no Optimize.

// Shrink a screen recording: merge frames that barely change, drop frames no viewer shows, then resample to 25 fps.
recording.Apply(ops => ops.MergeDuplicates(tolerance: 0.01f).DropZeroDelay().Retime(25), Ct);
recording.Save("recording.gif");
  • MergeDuplicates(tolerance) removes each frame equal to the one before it and adds its delay to the frame that stays, as ImageMagick's -layers RemoveDups; what is on screen at every moment is unchanged.
  • DropZeroDelay() removes frames with no delay, which viewers never show, as -layers RemoveZero (nothing, when every delay is zero).
  • Retime(fps) samples fixed ticks, each showing the frame visible when it starts, as ffmpeg's fps filter, then merges neighbours showing the same frame. It never adds frames and keeps the total duration; a rate at or above the animation's own changes nothing. Browsers show GIF delays under 20 ms as 100 ms, so 50 fps is the useful ceiling for GIF.
// Twice as fast, then backwards: a boomerang clip is the clip followed by its reverse.
using Raster fast = clip.Derive(ops => ops.ChangeSpeed(2), Ct);
using Raster backwards = fast.Derive(ops => ops.Reverse(), Ct);

ChangeSpeed(factor) divides every delay, rounded to whole milliseconds; Reverse() plays the frames backwards with their delays.

One animation on another

// A looping sticker on an animated background: the sticker keeps its own speed and loops for the background's whole length.
background.Apply(ops => ops.DrawAnimation(sticker, new Point(10, 10), new AnimationOverlayOptions { Repeat = 0, Opacity = 0.9f }), Ct);
background.Save("with-sticker.gif");

DrawAnimation cuts both timelines at every frame boundary of either (boundaries closer than 10 ms snap together, since GIF counts in hundredths), and each piece shows the base frame and the overlay frame of that moment, drawn as DrawImage draws with AnimationOverlayOptions.Blend and Opacity, so an overlay at another speed keeps its speed. Repeat plays the overlay that many times (0 loops it for the base's whole length) from Start. A still base lasts as long as the overlay plays and takes its loop count. AnimationTiming.ByIndex pairs base frame i with overlay frame i instead, as ImageMagick's -layers Composite does. The number of frames the result will have is checked against MaxFrames and MaxTotalBytes before anything is drawn.

Recipes

Short answers to common tasks in this area. Each one compiles and runs in Egami's test run.

Make an animated GIF from frames

// Start from one picture and add frames: each Add copies the frame you pass.
using Raster<Rgba8> animation = Raster.Load<Rgba8>("duck.png");
animation.Apply(ops => ops.Fit(120, 120));
using Raster<Rgba8> next = animation.Derive(ops => ops.HueRotate(120));
using Raster<Rgba8> last = animation.Derive(ops => ops.HueRotate(240));
animation.Frames.Add(next.Frames[0]);
animation.Frames.Add(last.Frames[0]);
foreach (Frame<Rgba8> frame in animation.Frames)
{
    frame.Metadata.Delay = TimeSpan.FromMilliseconds(400);
}

animation.Metadata.LoopCount = 0;   // forever
animation.Save("hues.gif");

Run it

samples/Animation is a console app that does what this guide shows on the bundled images: dotnet run --project samples/Animation writes its results to ./output.