Skip to content
Document DB 13 - MCP Server, REST API, Field Level Encryption, Transactional Outbox, & More!SHOW ME!!

Offscreen Export

Shiny.Maui.Controls.Keyframe.Export renders a KeyframeScene to discrete frames, offscreen and deterministically, and encodes them to an animated GIF with no external dependency.

Terminal window
dotnet add package Shiny.Maui.Controls.Keyframe.Export
using Shiny.Maui.Controls.Keyframe.Export;
var exporter = new FrameExporter(scene);
var options = new ExportOptions { Fps = 25, Scale = 2.0 };
GifEncoder.EncodeToFile("out.gif", exporter.Frames(options), options.Fps);

Nothing here touches a display link or a UI thread. The animation is sampled at exact frame times through IAnimationNode.Evaluate — possible only because evaluation is a pure function of time. The same property that makes scrubbing exact makes export byte-identical across runs.

Frame times are computed as index / fps in ticks, never accumulated, so a long export cannot drift. They deliberately avoid TimeSpan.FromSeconds, which rounds to whole milliseconds — coarser than a frame at 60fps, and enough to quantise every frame time.

Property Default Notes
Fps 30 Frames per second. Must be positive. See the caveat below before trusting 30.
Duration scene’s own Overrides how much of the animation is exported. Required for an infinite animation, which has no natural end.
Size scene’s DesignSize Output size in pixels.
Scale 1.0 Multiplies the output size — render at 2× or 3×.
Background scene’s Background Painted behind every frame. null leaves the frame transparent.
Member Notes
FrameExporter(scene, renderer?) The renderer defaults to SkiaFrameRenderer.
Frames(options?, cancellationToken) Renders each frame in turn, lazily — a long export never holds more than one frame’s pixels in memory.
FrameAt(progress, options?) Renders a single frame at a normalised position. Handy for a poster image.

An ExportedFrame carries Index, Time, Progress, Pixels (premultiplied RGBA, row major, four bytes per pixel), Width, and Height.

The exporter includes the closing frame: a one-second export at 30fps renders 31 frames, ending exactly on the final pose rather than one frame short of it.

// A poster frame at the animation's midpoint
var frame = exporter.FrameAt(0.5, new ExportOptions { Scale = 3.0 });
Member Notes
GifEncoder.Encode(stream, frames, fps, options?, ct) Writes to a stream, which is left open.
GifEncoder.EncodeToFile(path, frames, fps, options?, ct) Convenience wrapper.
GifOptions.MaxColors Palette size per frame, 2–256.
GifOptions.LoopCount How many times to repeat. Zero — the default — loops forever.

Frames are enumerated lazily all the way through, so encoding a long animation streams rather than buffering.

IFrameRenderer is a single method:

byte[] Render(int width, int height, Color? background, Action<ICanvas> draw);

SkiaFrameRenderer is only the default — it’s there because Skia runs headless on Windows, macOS, and Linux, which is what makes export usable from CI. Implement the interface yourself if you’d rather not take the Skia dependency, and pass it to the FrameExporter constructor.

  • MP4 / video export — out of scope; it needs a real codec. Pipe FrameExporter.Frames() to ffmpeg’s stdin instead.
  • APNG and WebP — not implemented.
  • A Lottie parser — nothing reads Lottie JSON today, though KeyframeScene is the scene graph one would need.