0.16.0: animated GIF / APNG export + transparent canvas

- New frame-by-frame export pipeline in src/lib/frameExport.ts: loads
  scene images, walks rangeIn..rangeOut at scene.fps, drawSceneFrame
  to an offscreen canvas, awaits a per-frame callback, yields every
  4 frames to keep progress responsive, honours AbortSignal.
- Animated GIF via gifenc (per-frame quantize/palette, ~5 KB lib).
- Animated PNG via UPNG.js + pako (lossless RGBA, alpha-aware, ~30 KB).
- drawSceneFrame now clearRects first and skips the background fill
  when scene.canvas.background === 'transparent' — prerequisite for
  the alpha to reach the APNG output.
- DocumentDrawer: new checkerboard toggle next to the colour picker
  flips the stage between its last-picked colour and transparent;
  disabled swatch + 'Transparent' hex label make the state obvious.
- ExportDialog: three new option rows — Animated GIF, Animated PNG,
  and an APNG preview page that bundles the rendered APNG into a
  standalone HTML with a checkerboard backdrop + dark/light toggles
  for verifying transparency at a glance.
- Tests: renderFrames frame count + abort, GIF header magic, APNG
  acTL presence, transparent round-trip (decoded alpha == 0 on
  un-painted pixels), preview HTML structure, and the two UI tests
  covering ExportDialog's new rows and the progress-view transition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-24 10:21:30 +02:00
1 parent 827d80bfd0
commit fcfd21428d
12 files changed
+956 -14

No files matched your search

+24
View File
@@ -395,6 +395,8 @@ export const en = {
width: 'Width',
height: 'Height',
background: 'Background',
backgroundTransparent: 'Transparent',
backgroundTransparentTooltip: 'Transparent background (APNG exports keep alpha)',
duration: 'Duration (s)',
fps: 'FPS',
timelineHint: 'Timeline length sets the ruler scale. Playback stops at the last keyframe.',
@@ -417,11 +419,21 @@ export const en = {
webmBadge: 'Browser · Realtime',
webmDesc: 'No server — recorded in real time',
webmNotSupported: 'Browser does not support MediaRecorder',
gifBadge: 'Browser · Frame-by-frame',
gifDesc: '256-colour palette, broadest compatibility',
apngBadge: 'Browser · Lossless',
apngDesc: 'Full RGBA with transparency — .png file',
apngPreviewTitle: 'APNG preview page',
apngPreviewBadge: 'HTML · Checkerboard',
apngPreviewDesc: 'Standalone HTML with a checkerboard backdrop so you can verify transparency',
cancel: 'Cancel',
failed: 'Export failed',
back: 'Back',
labelWebm: 'Rendering WebM (browser, realtime)…',
labelMp4: 'Rendering MP4 (server)…',
labelGif: 'Rendering GIF (browser, frame-by-frame)…',
labelApng: 'Rendering APNG (browser, frame-by-frame)…',
labelApngPreview: 'Rendering APNG + preview page…',
skipHidden: 'Skip hidden tracks',
skipHiddenDesc: 'Drop layers toggled off in the editor from every format below.',
skipHiddenNone: 'No editor-hidden tracks in the scene.',
@@ -1174,6 +1186,8 @@ export const de: DeepPartial<Messages> = {
width: 'Breite',
height: 'Höhe',
background: 'Hintergrund',
backgroundTransparent: 'Transparent',
backgroundTransparentTooltip: 'Transparenter Hintergrund (beim APNG-Export bleibt Alpha erhalten)',
duration: 'Dauer (s)',
fps: 'FPS',
timelineHint: 'Die Timeline-Länge bestimmt die Skala im Lineal. Die Wiedergabe stoppt am Ende.',
@@ -1195,11 +1209,21 @@ export const de: DeepPartial<Messages> = {
webmBadge: 'Browser · Echtzeit',
webmDesc: 'Ohne Server: direkt im Browser aufgenommen.',
webmNotSupported: 'Dieser Browser unterstützt MediaRecorder nicht.',
gifBadge: 'Browser · Frame-für-Frame',
gifDesc: '256-Farben-Palette, maximale Kompatibilität.',
apngBadge: 'Browser · Verlustfrei',
apngDesc: 'Volles RGBA mit Transparenz — .png-Datei.',
apngPreviewTitle: 'APNG-Vorschauseite',
apngPreviewBadge: 'HTML · Schachbrett',
apngPreviewDesc: 'Eigenständige HTML-Datei mit Schachbrett-Hintergrund zum Transparenz-Check.',
cancel: 'Abbrechen',
failed: 'Export fehlgeschlagen',
back: 'Zurück',
labelWebm: 'WebM wird gerendert (Browser, Echtzeit)…',
labelMp4: 'MP4 wird gerendert (Server)…',
labelGif: 'GIF wird gerendert (Browser, frame-für-frame)…',
labelApng: 'APNG wird gerendert (Browser, frame-für-frame)…',
labelApngPreview: 'APNG + Vorschau werden gerendert…',
skipHidden: 'Versteckte Tracks überspringen',
skipHiddenDesc: 'Lässt editor-versteckte Ebenen in allen Exportformaten weg.',
skipHiddenNone: 'In dieser Szene gibt es keine editor-versteckten Tracks.',