Skip to content

Releases: johnhenry/ecmanim

v0.0.13

Choose a tag to compare

@johnhenry johnhenry released this 04 Jul 17:37

Added

  • sampleSceneAt(sceneOrConstruct, targetTime, config?) — replays a Scene's construct() to a target time for scrubbing/embedding without a full render.
  • glyphsFromDomSvg() — builds MathTex glyphs from CDN MathJax's real-DOM tex2svg() output, so vector MathTex now renders correctly in-browser.
  • examples/e2e-feature-tour.ts — an end-to-end scene exercising the 0.0.7-0.0.12 feature surface through the real public package, which surfaced 4 real bugs (see Fixed).
  • examples/compositor/ — a browser-based scene-compositing/animation editor: layers/canvas/inspector/timeline, keyframe snapping, an independent play-range/loop marker, undo/redo, and ecmanim-source export.
  • examples/studio-demo/ — a runnable Studio dev-server example showing schema/props, camera stops, and property-keyframe tracks.

Fixed

  • FlexGroup's flexGrow/flexShrink now actually resize the child (#23).
  • Circumscribe/Flash/FocusOn no longer render skewed under a 3D camera when targeting an already-fixed-in-frame mobject (#21).
  • HarfBuzz-shaped glyphs no longer render upside down.
  • SVGMobject's <linearGradient> fill no longer silently drops when combined with a clip-path.
  • setTextShapingBackend() and the rest of the HarfBuzz text-shaping API are now exported from the public barrels.
  • attachKeyframeTimelineEditor's drag no longer loses tracking when the pointer leaves the canvas mid-drag.

v0.0.12

Choose a tag to compare

@johnhenry johnhenry released this 04 Jul 08:26

0.0.12

Fixed

  • startStudio() was hardcoded to bind only to 127.0.0.1, with no way to
    configure it
    — unreachable from any device other than the one running
    the dev server, which reads exactly like a firewall problem (connection
    refused, not filtered) if you're viewing from another machine on the LAN
    or over a remote/SSH-tunneled session. Added a host option (still
    defaults to 127.0.0.1 — this dev server has no auth); passing
    host: "0.0.0.0" binds wide-open. StudioHandle gained urls: string[],
    every address the server is actually reachable at (loopback + every
    discovered LAN address when wildcard-bound), since a literal
    http://0.0.0.0:PORT/ URL isn't reliably browsable as-is.

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 `Ax...
Read more

ecmanim 0.0.10

Choose a tag to compare

@johnhenry johnhenry released this 04 Jul 00:49

Fixed

  • Node: Text/estimateTextSize() now auto-load a system font lazily on first use, instead of silently falling back to the raster/CHAR_ASPECT estimate until an explicit loadVectorFont() call (issue #16).

ecmanim 0.0.9

Choose a tag to compare

@johnhenry johnhenry released this 03 Jul 06:37

Fixed

  • loadVectorFont/resolveFontPath (Node) are now exported from ecmanim/ecmanim/node, not just the internal renderer/fonts-node.ts module (issue #14, PR #15). Without a public way to force the vector-glyph font path render() uses internally, Text/estimateTextSize() measured before the first render() call in a process silently used the raster/CHAR_ASPECT estimate instead, which could disagree with the real glyph metrics render() ends up using by ~10% — enough to turn a correctly-gated "measure before you render" layout check into clipped output.

See CHANGELOG.md for full details.

ecmanim 0.0.8

Choose a tag to compare

@johnhenry johnhenry released this 03 Jul 06:25

Changed

  • skills/ecmanim-practical-authoring: removed the "Confirmed library
    gotchas" section documenting issues #3, #5, and #7 — all three are fixed
    and published as of 0.0.7, so those write-ups and workarounds were stale.
    Replaced with a short "Reporting a new bug" pointer to
    assets/bug_report_template.md.

See CHANGELOG.md for full details.

ecmanim 0.0.7

Choose a tag to compare

@johnhenry johnhenry released this 03 Jul 06:16

Fixed

  • FadeIn (and other single-mobject Animation subclasses) silently duck-typed an extra positional Mobject argument as config, corrupting the first mobject instead of raising an error. Fixes #7. The Animation base constructor now throws a TypeError when config is mobject-shaped; multiple mobjects must be wrapped in a Group.
  • Mobject.color was a dead field for rendering purposes — raw assignment never synced VMobject's strokeColor/fillColor, which the renderer actually reads. Fixes #5. color is now a getter/setter backed by _color, forwarding to setColor().
  • CLI render/plan crashed with TypeError: Class constructor ... cannot be invoked without 'new' for scene files that imported Scene through a different specifier than the CLI itself. Fixes #3 (also closed #4 as a duplicate). Replaced instanceof Scene checks with a duck-typed isSceneLike().

See CHANGELOG.md for full details.

ecmanim 0.0.5

Choose a tag to compare

@johnhenry johnhenry released this 03 Jul 00:13

Fixed

  • Axes/NumberPlane's c2p() didn't track transforms applied after construction (shift(), moveTo(), nesting inside a shifted parent, etc.), reported as #2. Fixed at the root: NumberLine.numberToPoint()/pointToNumber() now derive from the axis line's current rendered geometry rather than frozen construction-time scalars, and Axes.coordsToPoint()/pointToCoords() were simplified to mirror upstream Python manim's CoordinateSystem.coords_to_point.

See CHANGELOG.md for full details.

ecmanim 0.0.4

Choose a tag to compare

@johnhenry johnhenry released this 02 Jul 23:00

Fixed

  • Axes/NumberPlane placed points at the wrong x-coordinate for any xRange not centered on zero (e.g. [0, 70]) — fixes #1. See CHANGELOG for the full root-cause writeup.

ecmanim 0.0.3

Choose a tag to compare

@johnhenry johnhenry released this 02 Jul 21:46

Added

  • A hierarchical skills/ folder (root ecmanim skill + 9 domain skills: timeline, captions/audio, voiceover, presentation, interchange, physics, authoring-pipeline, studio, render-cli) for LLM coding agents. Plain reference material — nothing auto-activates on install.

Fixed

  • skills/ was never actually published to npm (present in git, missing from package.json's files). Now included.
  • skills/README.md documents npx skills-npm as the standard opt-in way to symlink these skills into an agent's skills directory.