Skip to content

v0.0.11

Choose a tag to compare

@johnhenry johnhenry released this 04 Jul 08:07

0.0.11

Added

  • 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.rateFunctionFactories before 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).