22//
33// SPDX-License-Identifier: MIT
44
5- // Export a reduction as an animated GIF. Each frame of the block animation is rendered to an SVG string,
6- // rasterized on an offscreen canvas, and encoded by a caller-provided GIF encoder. The encoder is passed
7- // in rather than imported, so this package stays dependency-free: only a consumer that wants GIF export
8- // installs one (gifenc) and hands it over. The frames are the same morph the live view plays, over a fixed
9- // view box (the union of every state's bounds) so the animation does not jump or rescale.
5+ // Export a reduction as SVG animation frames or a browser GIF. The browser path rasterizes each SVG on an
6+ // offscreen canvas and uses a caller-provided GIF encoder. The frames use a fixed view box over every
7+ // state, so the animation does not jump or rescale.
108
119import type { Atom } from "@metta-ts/hyperon" ;
1210import { DEFAULT_TRACE_MS } from "../anim" ;
11+ import { encodeBrowserSvgAnimation , type SvgAnimation } from "../svg-gif" ;
1312import type { BlockSettings } from "./settings" ;
1413import { placeProgram , type BlockBox } from "./layout" ;
1514import { boxesToPrims , interpolate , ease , type Prim } from "./animate" ;
@@ -129,14 +128,12 @@ export function frameSvg(
129128 return parts . join ( "" ) ;
130129}
131130
132- /** Encode a sequence of reduction states into an animated GIF, morphing between them. Each state is a
133- * frontier (one or more terms), so a nondeterministic step shows every branch. */
134- export async function reductionGif (
131+ /** Build the nested-block SVG frames for a reduction with explicit block settings. */
132+ export function blockReductionSvgsWithSettings (
135133 states : readonly Atom [ ] [ ] ,
136134 s : BlockSettings ,
137- lib : GifEncoderLib ,
138135 opts : GifOptions = { } ,
139- ) : Promise < Blob > {
136+ ) : SvgAnimation {
140137 if ( states . length === 0 ) throw new Error ( "no reduction states to export" ) ;
141138 const width = opts . width ?? 720 ;
142139 const holdMs = opts . holdMs ?? 260 ;
@@ -146,38 +143,30 @@ export async function reductionGif(
146143 let vb = boundsOf ( placeProgram ( states [ 0 ] ! , s ) , s ) ;
147144 for ( const frontier of states ) vb = union ( vb , boundsOf ( placeProgram ( frontier , s ) , s ) ) ;
148145 const height = Math . max ( 1 , Math . round ( ( width * vb . h ) / vb . w ) ) ;
149-
150- const canvas = document . createElement ( "canvas" ) ;
151- canvas . width = width ;
152- canvas . height = height ;
153- const ctx = canvas . getContext ( "2d" ) ;
154- if ( ctx === null ) throw new Error ( "no 2d canvas context for GIF export" ) ;
155-
156- const gif = lib . GIFEncoder ( ) ;
157- const addFrame = async ( prims : readonly Prim [ ] , delay : number ) : Promise < void > => {
158- const svg = frameSvg ( prims , vb , width , height , s . canvas ) ;
159- const img = new Image ( ) ;
160- img . src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent ( svg ) ;
161- await img . decode ( ) ;
162- ctx . fillStyle = s . canvas ;
163- ctx . fillRect ( 0 , 0 , width , height ) ;
164- ctx . drawImage ( img , 0 , 0 , width , height ) ;
165- const { data } = ctx . getImageData ( 0 , 0 , width , height ) ;
166- const palette = lib . quantize ( data , 256 ) ;
167- const index = lib . applyPalette ( data , palette ) ;
168- gif . writeFrame ( index , width , height , { palette, delay } ) ;
169- } ;
170-
146+ const background = opts . background ?? s . canvas ;
147+ const frames : SvgAnimation [ "frames" ] = [ ] ;
171148 const n = states . length ;
172149 const perStep = framesPerStepFor ( opts , n - 1 ) ;
173-
174- await addFrame ( framePrims [ 0 ] ! , holdMs ) ;
150+ frames . push ( { svg : frameSvg ( framePrims [ 0 ] ! , vb , width , height , background ) , delay : holdMs } ) ;
175151 for ( let i = 1 ; i < n ; i ++ ) {
176152 for ( let k = 1 ; k <= perStep ; k ++ ) {
177153 const prims = interpolate ( framePrims [ i - 1 ] ! , framePrims [ i ] ! , ease ( k / perStep ) ) ;
178- await addFrame ( prims , k === perStep ? holdMs : stepMs ) ;
154+ frames . push ( {
155+ svg : frameSvg ( prims , vb , width , height , background ) ,
156+ delay : k === perStep ? holdMs : stepMs ,
157+ } ) ;
179158 }
180159 }
181- gif . finish ( ) ;
182- return new Blob ( [ gif . bytes ( ) as BlobPart ] , { type : "image/gif" } ) ;
160+ return { frames, width, height, background } ;
161+ }
162+
163+ /** Encode a sequence of reduction states into an animated GIF, morphing between them. Each state is a
164+ * frontier (one or more terms), so a nondeterministic step shows every branch. */
165+ export async function reductionGif (
166+ states : readonly Atom [ ] [ ] ,
167+ s : BlockSettings ,
168+ lib : GifEncoderLib ,
169+ opts : GifOptions = { } ,
170+ ) : Promise < Blob > {
171+ return encodeBrowserSvgAnimation ( blockReductionSvgsWithSettings ( states , s , opts ) , lib , 256 ) ;
183172}
0 commit comments