Animations-Demo — 1-sekündiges Einblenden eines Bildes
Demonstriert die Erstellung einer einfachen Einblenden-Animation, indem die Helligkeit eines Quellbildes iterativ angepasst und die Einzelbilder zu einem Animationscontainer hinzugefügt werden.
INPUT
OUTPUT
JavaScript
// Animation demo — 1-second fade-in of an image
// anim_fade_in.js
//!INPUT: INPUT
//!OUTPUT: OUTPUT
//!PARAM: FRAMES:integer=20,min=1,max=200
//!PARAM: DELAY_MS:integer=50,min=10,max=5000
//!PARAM: QUALITY:integer=90,min=50,max=100
//!PARAM: MAX_EDGE:integer=800,min=64,max=4096
// Why QUALITY=90 instead of the usual lossy WebP default (85): a
// fade-in spends a lot of its time in the near-black range where
// every frame differs from its neighbour by ~1/(FRAMES-1) brightness
// step. At q=85 the encoder doesn't have enough bits per macroblock
// to represent those tiny deltas cleanly and visible block artifacts
// appear in the dark frames. Crank to 90 and the headroom is back
// for the cost of ~10–15 % larger file. Drop to 80 to see what the
// artifacts look like; bump to 100 for fully lossless.
// Engine.animation(w, h, { loop }) creates a multi-frame container.
// loop=0 means infinite playback (APNG-spec). Use loop=1 to play
// the fade-in once and then stop on the final frame.
//
// Each frame is an ImageHandle whose pixels get COPIED into the
// animation. The handle stays usable, so we clone the source per
// frame, brighten it to the desired opacity, push it in, and free
// the clone. The original source stays alive throughout.
// Auf MAX_EDGE verkleinern, bevor irgendetwas animiert wird. Eine
// Animation kostet ihre Bildflaeche mal Anzahl der Bilder: dieselbe
// Vorlage in 2048 x 2048 ergibt ueber 20 Bilder rund 5 MB, in 800 x
// 800 sind es gut 0,8 MB — bei gleicher Aussage, denn angesehen wird
// so etwas in einer Galerie oder auf einer Seite, nicht in
// Originalgroesse. Wer die volle Aufloesung braucht, setzt MAX_EDGE
// hoch; die Rechnung bleibt dieselbe.
const full = Engine.loadImage(INPUT);
const scale = Math.min(1, MAX_EDGE / Math.max(full.width, full.height));
const src = scale < 1
? full.clone().resize(Math.round(full.width * scale), Math.round(full.height * scale))
: full;
// Beim Verkleinern ist src ein Klon; dann wird die Vorlage nicht mehr
// gebraucht. Ohne das bliebe ein 2048er-Bild bis zum Skriptende liegen.
if (src !== full) full.free();
const anim = Engine.animation(src.width, src.height, { loop: 0 });
const p = Engine.progress("Building fade-in", { total: FRAMES });
for (let i = 0; i < FRAMES; i++) {
const opacity = i / (FRAMES - 1);
const frame = src.clone().brightness(opacity);
anim.addFrame(frame, { delay: DELAY_MS });
frame.free();
p.step(1, `frame ${i + 1}/${FRAMES} opacity=${opacity.toFixed(2)}`);
}
p.done(`✓ ${FRAMES} frames`);
// Hold the final fully-bright frame for 5 s so the loop has a clean
// rest beat — without this the animation flickers as it restarts.
anim.addFrame(src.clone(), { delay: 5000 });
// saveAPNG would also work; saveWebP lossy keeps the gallery asset
// small (~85 % smaller than APNG) at visually identical quality on
// non-dark content. See the QUALITY param note at the top of this
// file for why we default to 90 here.
const enc = Engine.progress("Encoding WebP");
anim.saveWebP(OUTPUT, { lossy: QUALITY < 100, quality: QUALITY });
enc.done("✓ saved");
anim.free();
src.free();
// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0