Skip to content

Javis.jl (Julia)

A frame-function animation layer on top of Luxor.jl's Cairo-backed 2D vector drawing — you build a Video, attach Objects that each run a Luxor drawing function over a frame range, apply Actions eased by Animations.jl, and render the result to a gif or mp4.

FieldValue
LanguageJulia
LicenseMIT — "MIT License … Copyright (c) 2020 Ole Kröger <o.kroeger@opensourc.es> and contributors" (LICENSE)
RepositoryJuliaAnimators/Javis.jl
Documentationjuliaanimators.github.io/Javis.jl
CategoryFrame-function 2D animation layer over Luxor/Cairo — "just an animation layer on top of it" (tutorial 1)
First release2020 (LICENSE copyright 2020); latest release v0.9.0 (May 26 2022); reviewed Jul 11 2026
Drawing layer (Luxor)Luxor.jlCairo.jl → the Cairo graphics library — a CPU vector rasterizer
Luxor version pinLuxor = "2.12, 3" in Project.toml — will not resolve against today's Luxor v4
Video outputgif (via FFMPEG.jl palettegen/paletteuse) or mp4 (via VideoIO.jl libx264, crf=23); default framerate 30
MaintenanceDormant. Last release May 2022; last commit to master Aug 2022 (see the warning below)

WARNING

Javis is effectively dormant. Its most recent registered release is v0.9.0 (May 26 2022) and the last commit to master is dated August 2022 — roughly four years stale as of this review (July 2026). Its Project.toml pins julia = "1.5" and Luxor = "2.12, 3", so it does not resolve against the currently-maintained Luxor v4.x. The drawing layer it sits on, Luxor.jl, is by contrast actively maintained (v4.5.0, April 9 2026). Treat Javis as a well-documented but frozen design study, and Luxor as the live substrate underneath it.


Overview

What it solves

Javis is a Julia package for scripting mathematical animations and visualizations. Its own tagline is deliberately modest — "Javis makes generating simple animations a breeze!" (README.md, where the name expands to "Julia Animations and Visualizations") — and the first tutorial states the architecture plainly:

"Javis.jl is an abstraction on top of powerful graphics tools to make animations and visualizations easy to create. It is built on top of the fantastic Julia drawing packages, Luxor.jl and Cairo.jl."tutorial 1

The division of labour is the whole point: Luxor does the drawing, Javis does the timing. Javis contributes the Video canvas, the frame-range object model, the Action/easing timeline, and the encode step; every actual mark on the canvas is a Luxor call (circle, line, poly, sethue, background). The tutorial is explicit about this seam:

"In general you can use all Luxor functions inside Javis. Javis is just an animation layer on top of it."tutorial 1

For this survey Javis is the exemplar of a frame-function drawing engine: it has no retained Mobject scene graph of vector geometry the way Manim does. Each frame is produced by re-running every active object's drawing function from scratch, so an "object" is a frame-scoped Luxor script, not a persistent VMobject whose control points are interpolated.

Design philosophy

Javis calls its authoring model the Object–Action paradigm:

"We use Object-Action paradigm for creating visualizations."README.md § Design Philosophy

The project mission frames the scope, and is candid about what Javis is not:

"Javis.jl is a tool focused on providing an easy to use interface for making animations and developing visualizations quickly - while having fun! 😃"mission

"Javis is not a plotting library.Javis focuses on freedom for the user. We approach Javis in the same way an artist approaches an empty canvas. We provide the basic tools but it is up to the user to create most of the functionality they wish to see."mission

The lineage is stated outright — "Our project mission was inspired by the mission, philosophy, and interface of projects such as manim, Fedora, Zotero…" (mission) — and the acknowledgements thank "Grant Sanderson of 3blue1brown … for inspiring us to create something like this in Julia" and "Cormullion the inventor of Luxor.jl" (README.md). So Javis is a Manim-inspired idea re-expressed on a Cairo scripting stack, not a re-implementation of Manim's retained-geometry engine.


How it works

A Javis program is four moves: declare a Video, register Objects over frame ranges, attach Actions, then render. Everything drawn inside an object is Luxor.

The Video canvas. Video(width, height) allocates the pixel canvas and becomes the implicit target of every subsequent object — "Defines the video canvas for an animation" (Video.jl):

julia
using Javis
myvideo = Video(500, 500)   # width × height in pixels; sets CURRENT_VIDEO

Object = a frame range + a Luxor drawing function. An Object is "what is drawn in a defined frame range"; its function "gets called with the arguments video, object, frame". The body is ordinary Luxor:

julia
function ground(args...)
    background("white")   # Luxor: fill the canvas
    sethue("black")       # Luxor: set the pen colour
end

Background(1:70, ground)                              # applied to all later objects
red_ball = Object(1:70, (args...) -> object(O, "red"), Point(100, 0))

Background is a special object that "specif[ies] that the ground function is applied to all objects afterwards" (tutorial 1); O is Luxor's shorthand for the origin Point(0, 0), which Luxor places at the centre of the canvas with the y-axis pointing down.

Action/act! animate the object over its frames. An Action "gives an Object … the opportunity to move, change color or much more"; its function takes video, object, action, rel_frame, and its frames are relative to the parent object (1:10 = the object's first ten frames, not the video's). act! attaches actions:

julia
obj = Object((args...) -> circle(O, 50, :fill))
act!(obj, Action(1:20, appear(:fade)))          # fade in over the object's frames 1–20
act!(obj, Action(21:50, Translation(50, 50)))   # then translate
act!(obj, Action(81:100, disappear(:fade)))     # then fade out

Predefined actions include appear/disappear, translate/anim_translate, rotate/rotate_around/anim_rotate_around, scale/anim_scale, follow_path, and morph_to.

Easing via Animations.jl. The Action's optional animation argument is an easing function or a full Animations.jl Animation; "The default is linear()" and "Possible simple easing functions is sineio()" (Action.jl). Either supply a keyframed Animation, or pass an easing per action:

julia
using Javis, Animations
# keyframed animation: times 0→1, values, per-segment easings
circle_anim = Animation([0.0, 0.3, 0.6, 1.0],
                        [O, Point(150, 0), Point(150, 150), O],
                        [sineio(), polyin(5), expin(8)])
act!(obj, Action(1:150, circle_anim, translate()))
# …or the simpler per-action easing form:
act!(obj, Action(1:50,  sineio(), anim_translate(150, 0)))
act!(obj, Action(51:100, polyin(2), anim_translate(0, 150)))

Frame specifications. Frames are a UnitRange (1:70), the symbols :same (reuse the previous element's frames) or :all, an RFrames ("define frames in a relative fashion"RFrames(10) = the next ten frames after the previous object), or, inside an Action, a GFrames ("define frames in a global fashion" — a video-absolute range translated into each object's local timeline). A Frames value "stores the actual computed frames and the user input … The frames are computed in render".

render produces the file. render(video; pathname="out.gif") "Renders all previously defined Object drawings to the user-defined Video as a gif or mp4" (render):

julia
render(myvideo; pathname="tutorial_1.gif")   # framerate defaults to 30

How Luxor works (the drawing substrate)

Because Javis is inseparable from it, the drawing layer deserves its own note. Luxor.jl is "a Julia package for drawing simple static 2D vector graphics" whose "aim … is to provide an easy to use 'scripting-like' interface to Cairo.jl" (luxorcairo.md):

"The focus of Luxor is on simplicity and ease of use: it should be easier to use than plain Cairo.jl, with shorter names, fewer underscores, default contexts, and simplified functions."index.md

It is procedural and CPU-vector: "Luxor is thoroughly procedural and static: your code issues a sequence of simple graphics 'commands' until you've completed a drawing, then the results are saved into a PDF, PNG, SVG, or EPS file" (index.md). A drawing function issues commands like background, sethue, setline, circle, line, poly, rect, text — the exact vocabulary a Javis object body uses. Luxor even points back at Javis for motion: "If you want to build complex or elaborate animations, use Javis.jl and Makie" (index.md).

The rest of this page walks the survey's eight axes against this Javis-over-Luxor machinery.


Object & scene model

Javis is best described as retained object list, immediate-mode drawing. The Video keeps a list (video.objects) of every Object you register, so in that weak sense the object set is retained. But an Object holds a function, not geometry: its field is "func::Function: The drawing function which draws something on the canvas" (Object.jl). There is no persistent tree of vector paths — no Mobject family, no submobject list of VMobject control points. Each frame, render calls every active object's func(video, object, frame), and the function redraws from scratch with Luxor. That is the immediate-mode half of the survey's retained-vs-immediate axis, wrapped in a retained registry.

What a Javis "object" does carry is a small state record — start_pos::Union{Object,Point} (its origin, "It gets translated to this point"), a current_setting::ObjectSetting, an actions vector, and a result::Vector (whatever the drawing function returned). The result is how one object reads another's live state: pos(object) (short for get_position) reads a moving object's current point so a later object can draw a trail or a connector to it (tutorial 1 threads pos(red_ball) into a path!/connector function). This is Javis's answer to Manim's updater/ValueTracker reactivity — a pull of the previous frame's returned value, not a dependency graph.

Grouping is provided by layers (@JLayer), a sub-canvas of objects that an Action transforms as a unit — "an action is applied to a layer as a whole and not on the objects inside it" (act!). There is no deeper scene-graph nesting or parent/child dirty-flag propagation; the model is intentionally flat.


Animation & timing model

This is Javis's defining axis. Its execution model is an imperative construction phase followed by a frame-function sampling phase: Object, Background, Action, and act! calls build up video.objects; then render iterates the frame indices and, for each, replays the active objects and their actions. Contrast Manim, whose self.play(...) steps a play-loop that interpolates the control points of a retained VMobject; Javis instead re-executes draw functions and lets each Action mutate the Luxor coordinate system (a translate/rotate/scale) or the object's drawing parameters for that frame.

  • Frames are relative and composable by construction. An action's frames are "defined in a relative fashion so 1:10 means the first ten frames of the object and not the first ten frames of the Video" (Action.jl). Sequencing is expressed by adjacent ranges (1:20, 21:50, 81:100) or relatively with RFrames; this is Javis's flavour of animation composition — a hand-laid timeline of ranges rather than an AnimationGroup/LaggedStart combinator algebra.

  • Interpolation and easing come from Animations.jl. The Action's anim::Animation reshapes the relative time parameter before the action runs — the survey's rate-function/easing step. get_interpolation(action, rel_frame) evaluates the eased value that _translate/_rotate/_scale then apply. Javis ships the raw easing names of Animations.jl (linear, sineio, polyin, expin, …) rather than Manim's named smooth/rush_into set; the underlying idea (an eased [0,1]→[0,1] remap before the base lerp) is identical, and is what the rate-functions.d probe tabulates.

  • morph_to is Javis's Transform. It tweens one shape into another and faces the same point-count alignment problem Manim's Transform does — but solved over Luxor polygons (Vector{Point}), not Bézier control nets. match_num_points "points are added to the polygon with less points until both polygons have the same number of points" (morphs.jl); add_points! distributes the new nodes by arc-length (polydistances); compute_shortest_morphing_dist "Rotates from_poly internally to check which mapping produces the smallest morphing distance" (a cyclic-offset search for a natural point correspondence); and reorder_match uses the Hungarian algorithm (Hungarian.jl) to pair up multiple sub-shapes. It is explicitly limited:

    "Currently morphing is quite simple and only works for basic shapes. It especially does not work with functions which produce more than one polygon or which produce filled polygons."morph_to

    Because Javis flattens to polylines and equalises by inserting points along arc length (not by de Casteljau curve subdivision on a shared Bézier basis), its morph is a coarser cousin of the alignment the bezier-eval.d probe demonstrates.


Rendering backend & rasterization

Javis has exactly one backend: Luxor over Cairo — a CPU-vector rasterizer, with no GPU path at all. This is the opposite pole from ManimGL or GLMakie, and it makes Javis' output deterministic by construction (the CPU-vector oracle the survey prizes). Every frame is rasterized by Cairo: Javis draws each object into a Cairo surface via Luxor, then reads the surface back as an image matrix (get_javis_frameMatrix{RGB{N0f8}}) for encoding.

  • Anti-aliasing is Cairo's analytic coverage AA on vector edges — inherited wholesale from the drawing layer, not something Javis configures. The frame-capture.d probe's 1px analytic disc-edge coverage stands in for this CPU path.
  • Color model / gamma is delegated: Luxor takes colours through Colors.jl (sethue("red"), RGB/HSV/named CSS colours) and Cairo does the compositing. Javis adds no colour-space policy of its own; it lerps colour channels through Animations.jl where an action animates a colour, and leaves blending/premultiplication to Cairo.
  • Resolution is the Video's pixel width/height; render also exposes a rescale_factor to downscale frames "for faster rendering" (render).

There is no WGLMakie-style browser target and no signed-distance-field glyph path — the reproducibility/quality trade-off the survey draws between CPU and GPU backends is resolved entirely on the CPU side here.


Typesetting & text

Text has two routes, both ultimately Luxor/Cairo.

  • Plain text via Luxor. Luxor offers "two ways to draw text … the so-called 'toy' API or the 'pro' API" (text.md). The Toy API (text, fontface, fontsize) is Cairo's toy text interface; the Pro API (setfont, settext) adds alignment, rotation, and "the presence of any pseudo-Pango-flavored markup" (text.md) — i.e. Pango/HarfBuzz shaping through Cairo. A Javis object simply calls these inside its drawing function. Rendering glyphs as animatable vector outlines (Manim's glyph-outline extraction) is not Javis's default path — text is drawn by Cairo like any other mark.

  • LaTeX via MathJax → SVG → Luxor paths. Javis renders LaTeXStrings (L"9\frac{3}{4}", from LaTeXStrings.jl) through its own latex() function. Crucially, it does not shell out to a TeX distribution and dvisvgm the way both Manim forks do; it uses MathJax's Node CLI to produce SVG, which svg2luxor then parses into Luxor paths. The dependency is explicit:

    "This only works if tex2svg is installed. It can be installed using the following command … npm install -g mathjax-node-cli"latex.jl

    So Javis's math pipeline is a variant of the survey's LaTeX-to-SVG route — MathJax instead of latex+dvisvgm, but the same "typeset to SVG, then ingest the SVG paths as vector geometry" shape, disk-independent of a full TeX install (only Node + mathjax-node-cli).

NOTE

The modern Luxor (v4) gained native LaTeX/Typst typesetting via weak-dep extensions (MathTeXEngine.jl + LaTeXStrings.jl, and Typstry for Typst, per Luxor's Project.toml). Javis cannot use them: it is pinned to Luxor 2.12, 3, predating those extensions, so its LaTeX story remains the MathJax/tex2svg path above.


Output & encoding

render performs the survey's frame-capture-and-readback then codec/muxing/pixel-format steps, choosing the pipeline by file extension.

  • Readback. For each frame, Javis composites the objects into a Cairo surface and reads it back as a Matrix{RGB{N0f8}} image (the frame-capture.d probe models this render→framebuffer step).

  • gif (default): each frame is written as a zero-padded PNG into a temp directory, then FFMPEG.jl builds the gif in two passes — a palettegen pass to derive an optimal palette, then a paletteuse pass — straight from the source (render):

    julia
    ffmpeg_exe(`-loglevel $(ffmpeg_loglevel) -i $(tempdirectory)/%10d.png -vf
                palettegen $(tempdirectory)/palette.png`)
    ffmpeg_exe(`-loglevel $(ffmpeg_loglevel) -framerate $framerate -i $(tempdirectory)/%10d.png
                -i $(tempdirectory)/palette.png -lavfi paletteuse -y $pathname`)
  • mp4: frames stream to VideoIO.jl via open_video_out(pathname, frame_image, framerate=…, encoder_options=(crf=23, preset="medium")) and write(video_io, frame_image) — an in-process libav H.264 (libx264) encode (render). Any other extension errors: "Currently, only gif and mp4 creation is supported."

  • framerate defaults to 30; render also exposes postprocess_frame and postprocess_frames_flow hooks (e.g. postprocess_frames_flow=reverse to reverse a clip) and a streamconfig for experimental livestreaming.

So Javis straddles both integration styles the survey names: an ffmpeg subprocess for gif and in-process libav (through VideoIO) for mp4.


Interactivity, preview & authoring

Authoring is a plain Julia script (or REPL/notebook), and preview is a first-class feature rather than an afterthought. render(video; liveview=true) opens an interactive frame viewer — a Gtk/GtkReactive window (javis_viewer.jl, deps Gtk, GtkReactive, Interact) with a slider to scrub frames while developing. Inside notebooks the same call returns the frames for Jupyter and Pluto viewers (the render body special-cases IJulia and PlutoRunner), so an author can preview interactively and then flip to a file render from one script.

This is a genuinely different authoring loop from a play-loop engine: because each frame is an independent function of its index, liveview can jump to any frame directly (no need to replay a timeline up to it) — an ergonomic dividend of the frame-function execution model. What Javis does not offer is a live, in-window scene-graph editor or GPU real-time playback; preview is a scrubber over CPU-rendered frames.


Extensibility & API surface

The API surface is deliberately thin because Luxor is the extension point.

  • Pass-through to Luxor. "you can use all Luxor functions inside Javis" (tutorial 1); Javis re-exports the Luxor names it needs, so custom drawing is just writing a Julia function of (video, object, frame) that calls Luxor. (Javis even warns against using Luxor alongside it, to avoid method-ambiguity from the re-exports.)
  • Shorthand objects. Convenience constructors wrap common Luxor shapes as ready-made objects — JCircle, JBox, JLine, JRect, JEllipse, JPoly, JStar — and JShape lets you register a custom shorthand.
  • Custom actions. An Action's function is an arbitrary (video, object, action, rel_frame) closure, so new animation behaviours are ordinary closures over get_interpolation; change animates arbitrary drawing keywords, and follow_path drives an object along a point list.
  • Layers. @JLayer groups objects into a transformable, cacheable sub-canvas.

The trade-off is that the animation vocabulary (appear, disappear, translate, rotate, scale, morph_to, follow_path) is small and fixed; richness comes from composing Luxor drawing with Animations.jl easings, not from a deep built-in animation library.


Determinism, caching & performance

  • Deterministic frame sampling. Strong. The sole backend is Cairo CPU-vector rendering, which is bit-reproducible for a fixed Cairo build — Javis inherits the survey's CPU-oracle reproducibility for free, and a frame is a pure function of its index, so re-rendering yields identical output. The frame-capture.d probe's FNV-1a checksum illustrates the determinism a Cairo render provides.
  • Content-hash caching — N/A. This is a real finding: Javis has no partial-movie / per-play() render cache of the kind Manim community relies on. render unconditionally re-draws every frame from scratch (a @showprogress … for frame in frames loop that recomputes each object's drawing), writes each to a temp PNG (gif) or streams it to the encoder (mp4), and there is no input-hash short-circuit for unchanged segments. The caching lever that makes Manim's iterate-render loop fast has no analog here.
  • Performance. Rendering cost is linear in frames × drawing-complexity and CPU-bound in Cairo; there is no GPU acceleration and no incremental recompute between frames. rescale_factor (render smaller, upscale) is the main built-in speed knob, plus Julia's usual first-call ("time-to-first-plot") compilation cost. For long or heavy animations this is the weakest axis — every frame pays full price, every run.

Strengths

  • Clean separation of concerns. Luxor draws, Javis times — "just an animation layer on top of it" (tutorial 1) — so the full power of a mature 2D vector library is available inside every frame with no wrapper API to learn.
  • Deterministic CPU-vector output. One Cairo backend means bit-reproducible frames and a pure frame-of-index model — ideal for regression-testable video.
  • Animations.jl easing is expressive. Keyframed Animations with per-segment easings (sineio, polyin, expin, …) give fine-grained, composable motion control.
  • Preview is first-class. liveview, Jupyter, and Pluto viewers scrub any frame directly — a dividend of the frame-function model.
  • Both encode styles. gif via an ffmpeg palette pipeline and mp4 via in-process VideoIO H.264, from one render call.
  • Well-documented. Eight tutorials, a mission statement, and a worked LaTeX path make it an unusually legible design study.

Weaknesses

  • Dormant. No release since May 2022, no commits since Aug 2022; pinned to julia 1.5 and Luxor 2.12/3, so it will not co-install with today's Luxor v4.
  • No render cache. Every frame re-renders on every run; no content-hash partial-movie reuse.
  • CPU-only, no GPU. No real-time GPU playback or WebGL target; performance is Cairo-bound.
  • Morphing is limited. "only works for basic shapes … does not work with functions which produce more than one polygon or which produce filled polygons" (morphs.jl); polygon-arc-length alignment, not Bézier subdivision.
  • No retained vector scene graph. Objects are frame-scoped draw functions, not a persistent Mobject/VMobject tree — cross-object relations are read back through pos()/result, not a dependency graph.
  • LaTeX needs an external Node toolchain (mathjax-node-cli/tex2svg), and can't use modern Luxor's native math extensions because of the version pin.

Key design decisions and trade-offs

DecisionRationaleTrade-off
Be an animation layer over Luxor, not a drawing engineReuse a mature 2D vector library; "just an animation layer on top of it"Inherits Luxor's model wholesale — CPU-only, no retained vector geometry
Objects are frame-scoped draw functions, not retained geometryAny Luxor code works per frame; frames are pure functions of their indexNo Transform-of-control-points; cross-object state pulled via pos()/result
Frame-relative Actions eased by Animations.jlTimeline = adjacent ranges + external easing library; fine-grained motionNo AnimationGroup/LaggedStart combinators; timing is hand-laid ranges
morph_to over Luxor polygons (arc-length point matching)Simple, dependency-light shape tween using existing polygon toolsBasic shapes only; no filled/multi-polygon morph; coarser than Bézier subdivision
Single Cairo CPU-vector backendDeterministic, bit-reproducible frames; the CPU oracleNo GPU/WebGL, no real-time playback; render cost is fully CPU-bound
LaTeX via MathJax tex2svg → SVG → Luxor pathsAvoids a full TeX install; only Node's mathjax-node-cli neededExternal Node dependency; can't use modern Luxor's native MathTeXEngine/Typst
No per-frame content-hash cacheSimplicity; every frame is independently reproducibleEvery render re-draws every frame — slow iterate loop on long/heavy animations
First-class liveview/Jupyter/Pluto previewFrame-of-index model lets any frame be viewed directly, no timeline replayPreview is a CPU-frame scrubber, not a live GPU scene editor

Sources

  • JuliaAnimators/Javis.jl README.md — tagline ("makes generating simple animations a breeze!"), "Object-Action paradigm", acknowledgements crediting Luxor/Cairo and 3blue1brown, shorthand/viewer contributions.
  • LICENSE"MIT License … Copyright (c) 2020 Ole Kröger …".
  • Project.toml — deps (Animations, Cairo, Luxor, FFMPEG, VideoIO, LaTeXStrings, Hungarian, Gtk/GtkReactive), version = "0.8.0", compat julia = "1.5", Luxor = "2.12, 3".
  • project mission — scope statement, "not a plotting library", inspiration from "manim, Fedora, Zotero…".
  • tutorial 1"abstraction on top of … Luxor.jl and Cairo.jl", "just an animation layer on top of it", the full Video/Background/Object/act!/ render worked example, origin-at-centre note.
  • src/structs/Video.jl · Object.jl · Action.jl · Frames.jl · RFrames.jl · GFrames.jl — the Video/Object/Action/act! data model and frame-spec docstrings.
  • src/Javis.jl render — gif (palettegen/paletteuse) vs mp4 (open_video_out, crf=23) pipelines, framerate 30, postprocess/liveview hooks.
  • src/action_animations.jlappear/disappear/translate/ rotate/scale/follow_path, the Animations.jl Animation/sineio/polyin/ expin easing examples, get_interpolation.
  • src/morphs.jlmorph_to limitations, match_num_points / add_points! / compute_shortest_morphing_dist / reorder_match alignment.
  • src/latex.jllatex(LaTeXString, …), "only works if tex2svg is installed", mathjax-node-cli.
  • Luxor: JuliaGraphics/Luxor.jl · LICENSE.md (MIT "Expat", "Copyright (c) 2017-2022: cormullion and contributors") · index.md ("drawing simple static 2D vector graphics", procedural/static, PDF/PNG/SVG/EPS) · luxorcairo.md ("scripting-like interface to Cairo.jl") · text.md (Toy vs Pro text APIs, pseudo-Pango markup) · Project.toml (v4.6, julia = "1.9", MathTeXEngine/LaTeXStrings/Typstry extensions).
  • Animations.jl · Animations.jl docs — the easing / keyframe-animation library Javis's Action uses.
  • LaTeXStrings.jl · MathTeXEngine.jl · Cairo.jl · Cairo graphics library · FFMPEG.jl · VideoIO.jl — the referenced dependencies.