CLI tool
ianthehenry/bauble avatar
ianthehenry/bauble

Bauble: writing signed distance fields in Janet and compiling them to GLSL

a playground for making 3D art with lisp and math

599 stars14 forksJanetMIT

At a glance

What is it?
Bauble is a playground for composing signed distance functions in Janet, compiling them to GLSL and rendering them in WebGL, with a browser studio and a CLI that can export high resolution images, non-square renders, raw shader source and 3D meshes. The project calls itself a toy, and the tooling reflects a single author working in Lisp and Nix.
Who is it for?
Adopt Bauble if you want to experiment with signed distance field modelling in a Lisp rather than in GLSL, and you are happy to treat the tool as a research playground rather than a production library. Do not adopt it as a dependency for a shipping application, because the project describes itself as a toy, publishes no releases, and the readme documents a single contributor's own workflow.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 151 days ago.
What is it written in?
Mainly Janet, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

SDFs in a Lisp, compiled to a shader

Bauble is a toy for composing signed distance functions in a high-level language, Janet, compiling them to GLSL, and rendering them via WebGL.

That is the whole idea in one sentence, and it is a good one to understand, because signed distance field modelling normally means writing GLSL by hand and iterating in a shader editor. Bauble moves the authoring into Janet, where you write ordinary functions, and treats the GLSL as an output artefact.

The consequences are the usual ones from moving an expression language up a level. A distance function is a function of a point, and in a Lisp that is a function of a point. Composition of shapes becomes ordinary function composition. The compilation step is what turns it back into something a GPU can run, and it is the part that is not described in the readme, so the interesting question of exactly what subset of Janet is supported and how the generated GLSL looks is something you learn by reading the source.

The project is reachable at bauble.studio, and the readme points to a video introduction in which the author models an infinite number of hot air balloons live. The readme is careful to note that Bauble has changed quite a bit since that video, so it does not translate one to one, but that it should give a good idea of the gist. There is also a linked Twitter account for more example videos of the development.

The repository is small and top level, with src for the Janet side, studio for the web UI, tests, a build directory, and the unusual choice of redo and a shell.nix alongside. The language of the project is Janet, the licence is MIT, and the last push was on 2026-05-25 with no GitHub releases published.

Dependencies, and the Janet version that actually matters

The dependency list is short and split by which half of the project you are working on.

For the CLI the only requirement is Janet itself. For the web UI there is emscripten, which compiles the Janet runtime to WebAssembly, redo, which is the build tool, and yarn for the JavaScript dependencies. For the tests there is pngcrush, marked as tests only.

Then there is a version constraint that the readme explains carefully, and it is the kind of detail that saves an afternoon. Bauble requires at least Janet 1.36.0, the first release with integer literal syntax. It may work with newer versions of Janet, assuming the image format is compatible, but the recommended path is to update the version of Janet that Bauble includes, at build/janet/janet-version, to match your local version.

That is a real coupling. The project vendors a Janet version, and the image format is the interface between the tool and the runtime, so a Janet upgrade is not a matter of reinstalling a newer binary. The practical rule is to keep your local Janet at the version Bauble bundles, and to update the bundled one deliberately rather than drifting.

The emscripten dependency is worth naming separately, because it explains the architecture. The browser studio is not a separate implementation of the language in JavaScript, it is the same Janet code compiled to WebAssembly, running in the browser with WebGL doing the rendering. The Janet and the GLSL are the same pipeline in both the CLI and the studio, which is why the studio can show you the shader source the CLI would produce.

The web UI also depends on a separate project, codemirror-lang-janet, which provides the Janet grammar for the CodeMirror editor in the studio. The readme documents the local development loop for it: clone that repository, run yalc publish from its root, then run yalc link codemirror-lang-janet in the Bauble repository to see your changes locally.

Building it, and the redo build system

The build is described as a set of shell commands, and the split between the Janet side and the studio side is the same split as the dependency list.

To build the CLI, the first thing is installing the Janet dependencies, which is done with the Janet package manager:

code
$ (cd src && jpm -l deps)

To build the web UI you install the JavaScript dependencies as well, first at the root and then inside the studio directory:

code
$ yarn
$ (cd studio && yarn)

The actual build commands follow. The CLI is built with jpm -l build, the web UI with redo, and a minified optimised build is produced by setting BUILD_MODE=prod:

code
$ (cd src && jpm -l build)
code
$ BUILD_MODE=prod redo

And the CLI is then run from its build directory:

code
$ src/build/bauble help

redo is the build tool that deserves a note. It is a build system in the Red language that runs a list of commands, and this project uses it with a file called all.do at the repository root, alongside a clean.do. The all.do naming convention is how redo works, and it means the build is a program you read rather than a configuration file you edit, which is a fit for a project written by someone who thinks in Lisp.

The shell.nix at the root suggests the environment is described with Nix, which is the other unusual choice here. Together, redo and Nix say that this is a project where the toolchain is scripted in a Lisp rather than configured in YAML, and that is a reasonable choice for a single maintainer who wants the whole environment reproducible.

For a quick local look, the readme suggests serving the built site with alive-server, which is listed in the JavaScript devDependencies along with postcss, postcss-cli, postcss-minify and autoprefixer:

code
$ node_modules/.bin/alive-server public

The web UI's JavaScript side is small by design, since the interesting logic is in Janet and the studio is mostly an editor around it.

The CLI is the only path to some of the output

The CLI section of the readme is short, and it contains the sentence that tells you where the tool's limits are.

Currently the CLI is the only way to export high-resolution images, render images with non-square aspect ratios, view raw GLSL shader source, and export 3D meshes.

That is a significant limitation, and it is the kind of thing that changes what you would use the tool for. A browser playground that can only show you a square preview is a sketchpad, whereas a CLI that can render a 4000 pixel wide image, render a non-square aspect ratio properly, dump the generated GLSL and export a mesh is a tool you could use for actual asset production.

So the studio and the CLI are not two front ends to the same thing. The studio is where you iterate, and the CLI is where you produce. The readme lists the CLI invocation in two forms, and the distinction is about whether you have built the project:

code
$ cd src
$ jpm -l deps
$ jpm -l build
$ build/bauble

And then the shorter path, running it straight from the interpreter once the dependencies are installed:

code
$ cd src
$ jpm -l janet cli/init.janet

The second form runs the Janet file directly with the interpreter rather than through the compiled binary, which is the faster loop while you are writing code.

The export of 3D meshes is the capability that is hardest to get elsewhere, because a signed distance field is a description of a shape rather than a mesh of triangles, and converting between the two is exactly the kind of thing that is tedious to write. Having it in the CLI means you can model in the language and then take the result somewhere else.

Tests, snapshots and a warning about the output

The testing setup has two layers, and the second one tells you how the project treats visual output.

The first is Judge unit tests, run from the src directory:

code
$ (cd src && judge)

The second is snapshot tests, which need separate dependencies installed once:

code
# you only have to do this once
$ (cd tests && jpm -l deps)

and then run through the Janet test suite:

code
$ (cd tests && jpm -l janet suite.janet)

Snapshot tests will write a file called tests/summary.html, and the readme's assessment of it is blunt: it is not a very good file.

That single line is the clearest signal about how the project is run. Snapshot testing for a graphics tool means rendering an image and comparing it against a stored reference, and the result is an HTML page of image diffs. Calling it not very good is the author telling you that the workflow is functional rather than pleasant, and that you should expect to look at the images rather than trust a green tick.

Before committing snapshot changes, the readme asks you to run:

code
$ (cd tests && jpm -l janet gc.janet)

which deletes old snapshots and compresses the new ones, and it depends on pngcrush, which is why that tool appears in the dependencies as tests only.

The judge framework is a separate project by the same author, and the Janet package manager commands with the -l flag are how the project wires up local path dependencies, which is the Janet equivalent of a workspace link. The whole test story assumes you are working inside the Janet ecosystem, and the judge dependency is not a package you would find in a general JavaScript project.

Editorial conclusion

Adopt Bauble if you want to experiment with signed distance field modelling in a Lisp rather than in GLSL, and you are happy to treat the tool as a research playground rather than a production library. Do not adopt it as a dependency for a shipping application, because the project describes itself as a toy, publishes no releases, and the readme documents a single contributor's own workflow. Before you commit, install Janet 1.36.0 or newer, read the note about the bundled Janet version, and try the web studio at bauble.studio first, since the browser path needs no build at all and tells you quickly whether the language fits how you think about shape.

Frequently asked questions

What is Bauble?

Bauble is a toy for composing signed distance functions in a high-level language, Janet, compiling them to GLSL and rendering them via WebGL. You can try it at bauble.studio, and the readme links a video introduction in which the author models an infinite number of hot air balloons.

What do I need to install to build Bauble?

Janet for the CLI, at least version 1.36.0 which was the first with integer literal syntax. The web UI additionally needs emscripten, redo and yarn, and the tests need pngcrush. The readme advises updating the Janet version Bauble includes at build/janet/janet-version to match your local version if you want to upgrade.

How do I build the Bauble CLI and the web UI?

Install the Janet dependencies with jpm -l deps in the src directory, then build the CLI with jpm -l build. The web UI is built with redo, or with BUILD_MODE=prod redo for a minified optimised build, after running yarn at the root and in the studio directory.

What can the Bauble CLI do that the browser studio cannot?

The readme states that the CLI is currently the only way to export high-resolution images, render images with non-square aspect ratios, view raw GLSL shader source, and export 3D meshes. The studio is for iterating, the CLI is for producing output.

How does Bauble test itself?

With Judge unit tests run from the src directory, and snapshot tests that render images and compare them against stored references, run through the Janet suite in the tests directory. Snapshot tests write a tests/summary.html, which the readme says is not a very good file, and a gc.janet script deletes old snapshots and compresses new ones using pngcrush.

Official sources

  1. ianthehenry/bauble on GitHub
  2. Issues
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ianthehenry-bauble.svg)](https://hysenlabs.com/projects/ianthehenry-bauble)

Was this useful?