TinyVolt
Your markdown editor for STEM.
Nova is a markdown editor focused on STEM. Powered by a declarative graphics DSL, native autodiff and reactivity, it turns a static markdown into an interactive canvas - including LaTeX strings and even the markdown text itself.
Editor: A VS Code-style editor where one can write and preview content locally.
Platform: The invisible scaffold that handles publishing, hosting and serving. The same article lives in two places:
Code in a ```pygeomatic block runs without the reader seeing it. If you want the reader to set something off themselves, give those lines a name with with group("name"):, then write {label}(ref:name) in your text. That becomes a link. The reader clicks it, and those lines run. For a single command you don't need a group: put the command inside the link itself: {set the scale}(scale = gm.scalar(1)). Everything else is normal markdown, math included.
# Drawing a walk ```pygeomatic origin = gm.p0 a = gm.point(3, 0) walk = gm.line(origin, a) gm.hide(walk) with group("walk-x"): gm.highlight(walk) gm.show(walk) ``` Reach the point by {moving a distance}(ref:walk-x) of $3$ units. Or reset it inline: {set scale to 1}(scale = gm.scalar(1)).
Give a formula an id (a %id: line inside the $$…$$ block), reach it with gm.tex("id"), then attach a store node. Whatever drives the node reflows the formula. Two modes:
bind - show a node's value in a slottex.family.slot (e.g. int.upper, frac.num) and call .bind(node).$$ %id:energy \int_{a}^{b} x^2 \, dx $$ ```pygeomatic b = gm.scalar(3, out="b") energy = gm.tex("energy") # matches %id:energy energy.int.upper.bind(b) # upper limit now shows b ``` Change it: {b = 5}(b = gm.scalar(5))
highlight - paint matrix cells by position$$ %id:M \begin{pmatrix} a & b & c \\ d & e & f \\ g & h & i \end{pmatrix} $$ ```pygeomatic r = gm.scalar(0, out="r") M = gm.tex("M") M.highlight(M.rows() == r, color="pink") # row r M.triu().highlight(color="blue") # upper triangle ``` Move it: {row 1}(r = gm.scalar(1)) · {row 2}(r = gm.scalar(2))
M[3:, 4:]. Regions: M.diag() / M.triu() / M.tril().cols - rows > 0, combine with & / |, gate with .scale(node).A slider is just a number the reader can change. Make one with gm.ui.slider, then put it in a sentence using an f-string. You need gm.md(…) to write that sentence, because a ```pygeomatic block produces no text on its own, and an f-string only works inside python.
Moving the slider doesn't re-run your code. It changes the number, and anything built from that number updates on its own: the drawing, and any formula you bound to it above.
```pygeomatic r = gm.ui.slider(1, 5, step=0.5, value=3, label="radius") c = gm.circle(gm.p0, r) # r is an ordinary Scalar gm.md(f"Drag to resize: {r}") # the slider appears here ```
r is an ordinary number - exactly what gm.scalar(3) would have given you. So r * 2, gm.circle(gm.p0, r) and .bind(r) all work as usual.
gm.ui.slider(start, stop, step=, value=, label=, show_value=) → a number, for sliding through a range.gm.ui.number(start=, stop=, value=, step=, label=) → a number, for typing an exact one. Start and stop are optional.gm.ui.checkbox(value, label=) → true or false.gm.ui.dropdown(options, value=, label=) → the chosen option; text options give text, number options give a number.gm.ui.radio(options, value=, label=) → the same as a dropdown, but every option is on screen.gm.ui.text(value, label=, placeholder=) → whatever the reader types.Give each control its own name. {r} looks up the control by its name, so two controls sharing one name would be ambiguous.
with when(…) hides some text until a condition is true. The condition is checked as the reader moves things, so ticking a box or dragging a slider makes text appear and disappear. There is nothing to click.
```pygeomatic show = gm.ui.checkbox(False, label="Show the proof") gm.md(f"{show}") with when(show): # a Bool node on its own gm.md("Because $ab=ba$, the map commutes.") with when(gm.cond.ge(r, 4)): # or a comparison gm.md("**Large:** the radius is 4 or more.") ```
gm.cond.ge / gt / le / lt / eq / ne. Text can only use eq and ne.&, | and ~, or gm.cond.all_(…) and gm.cond.any_(…).Write gm.cond.ge(r, 4), not r >= 4. In geomatic, r >= 4 already means something else - it builds a new value. The two are kept apart on purpose.
when block. Links run in the order they appear on the page, and hiding one doesn't take it out of that order - so the reader gets stuck waiting to click something they can't see. Text, math and controls are all fine.with gm.ui.onclick(node): attaches pygeomatic commands to a shape drawn in the canvas. The reader clicks it on the canvas and those commands run.
```pygeomatic label = gm.annotate_text_box("where does it land?", 2, 3) with gm.ui.onclick(label): # runs when the reader clicks the box p = gm.point(2, 3) far = gm.gt(gm.distance(p, gm.p0), 1) with when(far): gm.md("The point landed outside the unit circle.") ```
One can define a new node or redefine / overwrite an existing one.
gm.ui.slider and friends). The control has to exist before the reader touches anything.group(…), or gm.md(…). Prose is written once, when the article is built; gate it with when instead.Handlers live with the published article, not with the commands. Copying a command list into the editor carries the drawing but not its clicks.
Everything above waits for a click. with gm.onpageload(): doesn't: those commands run the moment the page loads, before the reader touches anything. Use it for the picture you want the article to open on, and for controls - a slider is on screen straight away, so the number behind it has to be there too.
```pygeomatic with gm.onpageload(): # runs before any link r = gm.ui.slider(1, 5, step=0.5, value=3, label="radius") gm.circle(gm.p0, r) gm.md(f"Drag to resize: {r}") ```
The rest of the article can build on what the block made - here, later commands can move or recolour that circle. A handler works the other way round, because its commands haven't run yet.
```pygeomatic block, like everything else.Start over brings it back. Clearing the canvas - the Start over button, or a \clear of your own - rebuilds the block first, so the reader lands back on the page as they first found it.
Put a with gm.ui.onclick(…) inside the block and the shape is on the canvas, waiting to be clicked, before the reader does anything. The shape is built at load; what the click does still waits for the click.
```pygeomatic with gm.onpageload(): box = gm.annotate_text_box("what happens here?", 2, 3) with gm.ui.onclick(box): # still waits for the click p = gm.point(2, 3) ```