You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Page-transition playback resume (ecmanim/browser): enablePageTransitionResume(playerEl, opts?) carries a <manim-player>'s
playback position across a full page navigation — saves { time } to sessionStorage on pagehide, restores it via seekTime() once the new
page's player fires "ready". savePlaybackPosition()/ restorePlaybackPosition() are the underlying pure functions if you want
to wire your own lifecycle hooks. Opt-in { viewTransition: true }
additionally does a View Transitions snapshot handoff (canvases don't
participate in the browser's DOM-snapshot mechanism directly, so this
captures the outgoing frame into a plain <img> tagged with a shared view-transition-name, and tags the incoming canvas with the same name).
FlexGroup (src/mobject/flex_group.ts): opt-in real Flexbox layout
via Yoga (Meta/React's portable WASM
Flexbox engine), a new optionalDependency (yoga-layout) mirroring @napi-rs/canvas/three/harfbuzzjs's graceful-degrade pattern. direction/justifyContent/alignItems/gap at the container level; flexGrow/flexShrink/flexBasis/margin per child via setChildFlex(). await group.layout() builds a fresh Yoga node tree
from the group's current children and repositions them — necessarily
async (Yoga's WASM must load first), documented prominently as the one
sharp edge in docs/flex-group.md. Fully additive: mobjects outside a FlexGroup are unaffected, and a child can still pin its own size.
WebGL raster-text batching (ThreeRenderer): raster Text
(RasterText) mobjects now render as ONE shared texture atlas + ONE
merged quad mesh instead of one THREE.Sprite (own CanvasTexture) per
mobject — converts N draw calls into 1. New src/renderer/text_atlas.ts's buildTextAtlas() does simple shelf-packing (sort tallest-first, pack
into rows). Scoped to a 2D-orthographic camera, where a flat quad is
visually identical to a billboarded sprite (the camera always looks
straight down -Z); a genuine 3D/perspective camera keeps the original
per-sprite path so real per-mobject billboarding still works. Falls back
to the per-sprite path gracefully wherever no synchronous canvas/document
backend is available (e.g. headless Node), same as the pre-existing
per-sprite code already did.
Mobject.cacheStatic() + CanvasRenderer static-subtree render cache:
an opt-in marker that, on an unchanged frame (content-based fingerprint
of geometry/style AND camera state — NOT reference equality, since interpolate() mutates points element-by-element while keeping the
same outer array reference), blits a small cached offscreen bitmap
instead of re-walking the mobject's bezier path. Screen-space, MVP-scoped:
invalidated on any camera-state change, so it mainly helps static-camera
scenes with many unchanging elements (dense axis labels, background
grids), not continuous camera motion. Requires a synchronous
offscreen-canvas backend (OffscreenCanvas or a detached <canvas>
element) — gracefully no-ops (draws directly, same as always) under
Node/no-DOM, where only an async @napi-rs/canvas import is available.
Property-keyframe Studio timeline: Scene.track(keyframes) (mirrors addSound()'s ergonomic) creates a PlayableKeyframeTrack
(src/reactive/keyframes.ts) — Cluster 2's KeyframeTrack plus
absolute-time tick(dt)/seek(t), kept in exact agreement so authoring
playback and a Studio scrub can never drift apart. bindTrack(mobject, prop, track) wires a track's value onto a mobject property via the
ordinary updater mechanism — zero Scene/render changes needed for
playback correctness. scene.keyframeTracks mirrors the existing sections/sounds array pattern. New computeKeyframeMarkers()/ renderKeyframeTimeline()/attachKeyframeTimelineEditor()
(src/studio/timeline.ts) draw a draggable per-track keyframe strip;
dragging updates a keyframe's time (keeping keyframes sorted), and a
debounced onCommit hook is meant to call item 7's parameter-only
re-render primitive (player.rerender()) to rebake frames, since Player.frames[] are frozen bitmaps that a drag alone can't affect.
Rendered props panel (startStudio({ props: true })): draws one
control per schemaToControls() descriptor, pre-filled from the schema's
own defaults. Edits are debounced (80ms) and re-render via <manim-player>.rerender(props) → Player.record(scene, { props })
(parameter-only re-render, no re-import()), validated through schema.safeParse() first. A real file-save reload still does a full load() + panel reset; the two triggers are kept structurally separate
(a rerender-triggered "ready" event carries the same schema object, so it
doesn't reset the panel).
Waveform visualization (startStudio({ waveform: true })): draws a
bar-chart waveform strip below the live preview for each of the scene's addSound()-scheduled sounds, positioned on the shared timeline via src/studio/timeline.ts's new computeWaveformBars()/renderWaveform().
Reuses the existing getAudioData()/getWaveformPortion() audio
primitives (both Node ffmpeg and browser AudioContext backends) — no new
audio decoding. Opt-in; off by default.
Parameter-only re-render primitive: runConstruct(sceneOrConstruct, scene, props?) and Player.record(sceneOrConstruct, { props? }) thread props through to a Scene subclass's own config.props or a bare
construct function's 2nd argument — both additive/opt-in. This still
re-runs construct() and re-records every frame; it doesn't itself avoid
that cost.
Player step navigation: steps()/stepContaining()/seekToStep()/ nextStep()/prevStep(), mirroring the existing section-navigation
methods but reading scene.playRecords (finer-grained, independent of
section boundaries). <manim-player>'s presenter keydown handler now has
two tiers: plain Right/Left step; Shift+Right/Left (or PageDown/PageUp)
jump whole sections.
Scene.nextSection() gains an optional notes parameter (and SceneSection.notes) for presenter-mode speaker notes.
Player.drawFrameTo(ctx, frameIndex, opts?): draws an arbitrary
recorded frame to an arbitrary ctx/position/size — "nearly free" since
frames are already rasterized bitmaps. seek() now uses this internally;
it's also the primitive behind section-overview thumbnails.
src/studio/timeline.ts: shared time/frame↔pixel mapping
(timeToPixel/pixelToTime/frameToPixel/pixelToFrame) plus computeSectionThumbnails()/renderSectionOverview() (a jump-to-section
overview strip) and computeStepMarkers(). Each render function has a
DOM-free "compute layout" half, independently unit-testable.
MovingCameraScene.defineCameraStop(name, stop) / goToCameraStop(name, config?): named camera viewpoints
(center/width/height/zoom), sugar over camera.frame.animate.moveTo()/ setWidth()/setHeight(), applied as a single composed animation (not one
per field, which would otherwise race to overwrite the frame mobject's
points each tick). zoom scales the frame's own width/height — documented
as a distinct concept from the interactive camera's camera.zoom
multiplier.
copyMemberwiseStyle(dest, src, extraExclude?) (src/mobject/copy_style.ts):
a shared denylist-based memberwise style copy, extracted from Mobject.become(). Now also used by alwaysRedraw() and reactive()'s
rebuild step, in place of their own independently-hardcoded allowlists —
any current or future custom field on a Mobject subclass now redraws
correctly through all three paths, not just the fields each one happened
to enumerate.
KeyframeTrack<T> / PlayKeyframeTrack / animateSignal()
(src/animation/keyframe_track.ts): a unified keyframe-track primitive
that, unlike every other easing tool here, keeps its structured, mutable
keyframe list around for introspection/editing (a Studio scrub UI can
splice keyframes directly; valueAt(t) reflects it immediately).
Per-keyframe ease (a RateFunc or a string resolved via running())
eases the transition arriving at that keyframe. Default interpolation
handles number/number[] via V.lerp; options.interpolate is the
escape hatch for other types (e.g. Color.lerp for a color track). PlayKeyframeTrack is an Animation for scene.play()-driven use
(explicit config.runTime wins over the track's own duration, same
precedence as transitions.ts's springTiming()); .valueAt(t) is also
usable directly inside a plain addUpdater. animateSignal(signal, track)
points a PlayKeyframeTrack at a signal's setter, giving "a signal driven
by a keyframe timeline" with no separate mechanism.
SceneRenderer interface + renderFrame() (src/renderer/scene_renderer.ts): CanvasRenderer, ThreeRenderer, and SVGRenderer each gain an additive renderFrame(mobjects) method that purely delegates to their existing,
differently-named public method (renderScene/render/renderToString
respectively). Those existing methods are unchanged and remain the
primary API (used across 15+ call sites) — renderFrame() is a shared,
uniform entry point for code that wants to treat any backend
interchangeably, not a replacement or rename.
reprojectCurve(domainSamples | curve, targetSystem, options?)
(src/mobject/coordinate_reprojection.ts): rebuilds a curve sampled in
domain (coordinate) space against a different coordinate system (e.g. an Axes-plotted curve reprojected onto a PolarPlane), reusing the same setPointsAsCorners construction Axes.plot() uses so fidelity matches a
curve plotted directly against the target. targetSystem is typed
structurally ({ coordsToPoint(a, b) }), so Axes/PolarPlane/ ComplexPlane all work as either source or target. Axes.plot() now
stamps a hidden _domainSamples tag on its result so reprojectCurve(curve, targetSystem) can read the samples back
automatically instead of requiring the caller to re-supply them.
SpringParams.velocity0: the analytic spring (src/animation/spring.ts)
now accepts a nonzero initial velocity (default 0, byte-identical to the
prior zero-initial-velocity formula in every damping regime). Enables
"fling and decelerate" momentum — spring a value back toward itself
(from === to) seeded with a release velocity, instead of the usual
"seek a fixed target from rest".
Studio drag momentum (src/studio/interactive.ts): attachInteractiveCamera(..., { momentum: true }) continues panning (2D)
or orbiting (3D) after a drag release, decelerating via a spring seeded
with the release velocity (from a short ring buffer of recent pointer
samples). Opt-in via momentum, tunable via momentumConfig; a fresh
drag cancels any in-flight momentum. now/scheduleFrame/cancelFrame
are injectable (default Date.now/requestAnimationFrame) for
deterministic testing.
Repeat (src/animation/repeat.ts): a standalone Animation wrapper
adding count/yoyo/repeatDelay to any leaf Animation, AnimationGroup,
or built Timeline, without reaching into their internals. yoyo mirrors
odd-indexed cycles; repeatDelay holds the previous cycle's end value
between cycles. Infinite repeat is out of scope (no infinite-time concept
in this render model); count: Infinity throws.
Stagger value-transform helpers (src/animation/stagger.ts): cycle(values) (index-safe modulo cycling) and staggerRange(from, to)
(linear distribution by index), usable with LaggedStartMap's widened (mobject, index, total) factory signature (previously (mobject) only —
backward compatible, existing single-arg factories are unaffected).
Scene.autoAnimateToNextSection(name, buildNext, config?): an opt-in
Reveal.js Auto-Animate-style section transition. Snapshots the scene, lets buildNext() mutate this.mobjects into the next section's state (moves,
additions, removals), then plays a TransformMatchingAuto between the two
states instead of a hard cut — landing on the true original mobjects
afterward so identity is preserved for later code. Strictly opt-in; plain nextSection() is unaffected.
VMobject.alignPointsWith() now searches for the best cyclic subpath
rotation between the two shapes' subpath orders (by total centroid-to-
centroid distance) before aligning, so a compound shape whose subpaths were
authored/traversed in a different order (but represent the same elements)
still matches subpath-for-subpath by position. Capped at 32 subpaths
(falls back to identity order above that); zero-cost no-op for the
dominant single-subpath case.
Parameterized back/elastic easings: easeInBackFactory/easeOutBackFactory/ easeInOutBackFactory(overshoot?) and easeInElasticFactory/easeOutElasticFactory/ easeInOutElasticFactory(amplitude?, period?) (GSAP's back.out(2)/ elastic.out(1, 0.3) ergonomic), byte-identical to the existing plain
exports at default args. Registered as "backIn"/"backOut"/"backInOut" and "elasticIn"/"elasticOut"/"elasticInOut" rate-function factories, so running("backOut:2") / running("elasticOut:1,0.3") resolve them by name.
Unified rate-function registry: running() now checks registry.rateFunctions/registry.rateFunctionFactoriesbefore the
built-in RATE_FUNCTIONS map, so a plugin can override a built-in name by
registering under the same key. Added colon-parameterized name parsing
("name:arg1,arg2") dispatching to a registered factory. "spring"
(an fps=60 convenience default; use springRate(config, scene.fps) directly
for frame-accurate springs) and a "bezier:x1,y1,x2,y2" factory
(wrapping Easing.bezier) are now registered built-ins.
TransitionConfig.timing: a TimingPreset (linearTiming(rateFunc?) / springTiming(config?, durationInFrames?)) supplying crossFade/slide/ wipe's shared rateFunc and, optionally, a suggested runTime — explicit config.runTime always wins over a preset's computed duration. springTiming() measures its own natural settle time via measureSpring()
unless durationInFrames is given explicitly.
Style-preset registration API: registry.stylePresets + registerStylePreset(name, preset), checked by resolveStyle() alongside
the built-in STYLE_PRESETS map — the same plugin-registry pattern already
used for colors/rate-functions/mobjects.
Word-wrap for Text: a new width config option greedily wraps long
lines to fit, using real glyph-advance measurement when a vector font is
loaded or the CHAR_ASPECT estimate otherwise. estimateTextSize() gained
a matching opts.width parameter.
Optional HarfBuzz text-shaping backend (setTextShapingBackend("harfbuzz"),
opt-in, default remains "opentype"): real GSUB/GPOS shaping via the harfbuzzjs WASM build — actual ligatures, combining-mark composition, and
correct kerning/positioning — instead of the previous naive per-character charToGlyph loop. disableLigatures (previously a dead config field) now
has real effect when this backend is active. Falls back to the "opentype"
backend transparently if HarfBuzz can't load or a font has no raw bytes
available to build an hb.Font from.
Code.diffTo(other): morphs one Code snapshot's tokens into another's
via TransformMatchingAuto (the Reveal.js Auto-Animate / animated-code-diff
idea), disambiguating repeated identical tokens on one line via a seeded matchId.
SVG <linearGradient> fill and rect/circle <clipPath> support in SVGMobject, plus matching gradient-export support in the SVG renderer.
Fixed
linearTiming/springTiming/registerStylePreset are now actually
exported from the public ecmanim package — implemented earlier in this
release but never wired into src/index.ts's barrel, so they were
unreachable from import { ... } from "ecmanim" despite being documented
as part of this release. Caught during a pre-release documentation audit
(every code sample in the docs above was executed against the real
package to confirm it, which is how these two were found).
Scene.play(animation, config) no longer requires an undocumented
internal _playConfig: true marker on the config object (GitHub issue #19). Previously, a bare trailing config object like { runTime: 0.5 }
— exactly the natural way to call play(), and how every one of this
codebase's OWN call sites did it, just always remembering the marker —
was silently treated as an animation instead of config if that marker
was missing, crashing with "a.begin is not a function" (or, depending
on what else was in the call, misbehaving in ways that looked like
corrupted opacity/positioning on a large VGroup/vector-field FadeIn). play() now also recognizes config structurally: a trailing plain
object with neither .begin (Animation-shaped) nor _isAnimateBuilder
can only ever have been config, since anything else in that shape was
already guaranteed to crash — so this is strictly safer and fully
backward compatible with the existing marker.
alwaysRedraw() was missing "radius" from its own hardcoded 7-field
allowlist (reactive()'s separate 9-field allowlist had it) — a ValueTracker-driven alwaysRedraw(() => new Circle({ radius: r() }))
could silently stop reflecting radius changes depending on which of the
two rebuild paths built it. Both now use copyMemberwiseStyle().
Mobject.become() no longer copies updatingSuspended from its
source — previously it could silently un-suspend a mid-animation
mobject.
SVGMobject no longer renders <defs>/<clipPath>/<linearGradient>
contents as visible shapes — these definition-only containers were
previously not excluded from the render walk.
Kerning: vectorized_text.ts's glyph-advance loop now calls
opentype.js's font.getKerningValue(), previously available but unused.
Grapheme-cluster-aware glyph iteration: combining-mark sequences (e.g. "e" + U+0301) and multi-codepoint emoji now build as a single glyph
slot instead of silently dropping the combining mark's own outline.
Text.ts and vectorized_text.ts's previously-independent, near-identical
glyph-building loops are now one shared implementation
(src/mobject/text_shaping.ts).