typstage

0.2.0

Animated HTML presentations from a single Typst file, and a PDF handout from the same source. Typst typesets, the browser moves: magic-move morphing, step-by-step reveals, slide transitions, media and a speaker view in the same file.

GeoGebra#

GeoGebra builds the construction, the slides supply the dramaturgy. A job can sit on every step: set values, show or hide objects, change colours, move the viewport, start a motion.

Quick start#

geogebra() puts an applet on the slide. The commands that drive it stand in the same slide body and produce no output of their own.

#import "@preview/typstage:0.2.0": *

#presentation(
  slide([Remote controlled], {
    geogebra(app: "classic", perspective: "G", height: 240pt,
             link: "https://www.geogebra.org/calculator")
    ggb-run("a=1", "f(x)=a*x^2")
    ggb-set((a: 3), at: 2)
  }),
)

The parabola is there from the start; on step 2 a becomes 3.

Every command takes at, the step selector known from anim; the default is "1-". The applet frame has no step of its own, so bullet points beside it still start on step one.

Note
The applet lives in the HTML export only; for the PDF see On paper.

Which applet is meant#

With one applet on the slide the commands find it themselves. Two applets need names, and the commands then need target — a string or a label:

#geogebra(<left>, height: 200pt)
#geogebra(<right>, height: 200pt)
#ggb-run("A=(0,0)", target: <left>)
#ggb-run("B=(1,1)", target: "right")

With no applet, or with more than one and no target, nothing is guessed. The build stops and names what it found:

error: panicked with: typstage: 2 applets on this slide
(left, right) — say which one is meant, e.g. target: "left".

Building the construction#

ggb-run hands GeoGebra commands to evalCommand, one at a time. The order counts: whatever is needed has to exist first.

#ggb-run(at: "1-",
         "k: x^2+y^2=4", "t=Slider(0,6.283,0.01)",
         "P=(2cos(t),2sin(t))", "s=Segment((0,0),P)")
Careful
GeoGebra’s scripting commands — SetColor, SetValue, SetVisibleInView and their relatives — are not accepted by evalCommand and come to nothing inside ggb-run. Use ggb-set, ggb-style, ggb-show and ggb-hide instead. Rejected commands land in the browser’s console.

Entering a slide and paging back reset the applet and repeat the run, so commands have to be repeatable. Fix the colour on "1-" for the same reason: on a rebuild GeoGebra would otherwise hand out the next colour of its palette.

#ggb-run("a=1", "f(x)=a*x^2", at: "1-")
#ggb-style("f", at: "1-", color: dark, thickness: 3)
Note
A .ggb file cannot be embedded: Typst has no way to inline binary data into the HTML. Build the construction with ggb-run, or load it from GeoGebra through material: geogebra(material: "abc123xy").

Values, appearance, viewport#

ggb-set takes a dictionary of object name and value, ggb-show and ggb-hide any number of object names. Build everything at the start and reveal it when its turn comes:

#ggb-hide("P", "s", "t", at: "1-")
#ggb-show("P", "s", at: 2)
#ggb-set((a: 3), at: 2)
#ggb-set((a: -2, b: 0.5), at: 3)

Appearance#

ggb-style takes the object names and the settings to change. What is not named stays as it is.

SettingEffect
colorcolour, as a Typst colour and not a GeoGebra one
thicknessline weight
line-styleline style as a number (solid, dashed, dotted …)
fillingfill, 0 to 1
point-sizepoint size
tracetrace on or off
labellabel visible or not
label-modekind of label as a number (name, value, caption …)
fixedheld against being moved
captiona caption of your own
layerlayer, that is, what lies in front of what
positionplace as (x, y)

color takes a Typst colour, so the construction carries the colours of the slides instead of GeoGebra’s palette.

#ggb-style("P", at: 2, color: accent, point-size: 6)
#ggb-style("s", at: 2, color: dark, thickness: 3)
#ggb-style("d", at: 3, color: accent, filling: 0.18, thickness: 4)
Careful
position counts in coordinates of the plane, except for a slider made with Slider: that one sits at an absolute place on the screen and counts in pixels. Two sliders both written as (-3.9, 2.2) land in the same corner.

Viewport#

ggb-view sets the visible range as well as the grid and the axes. x and y take effect only together; each is a pair of smallest and largest value.

#ggb-view(at: 2, x: (-3, 3), y: (-3, 3), grid: false)
#ggb-view(at: 3, axes: false)
Careful
ggb-view sets x and y separately, so a range that does not match the shape of the box stretches one axis and a circle becomes an ellipse. Where the geometry carries the argument, give the box a fixed size and match the ranges to its proportions.

Without ggb-view the visible range follows from width and height.

Motion#

Two ways to set something moving, and they do different things.

ggb-animate starts GeoGebra’s own animation: back and forth without end until the slide is left. trace switches on the trace of the named objects, speed sets the pace, playing: false stops it.

#ggb-animate("t", at: 3, speed: 1.2, trace: ("P",))

ggb-tween moves a value once from A to B and stops. Everything that depends on it follows along — a segment whose endpoint travels, an arc whose angle grows — and that is how a construction draws itself. from gives the starting value, duration the time in milliseconds, easing the shape.

#ggb-run("t_1=0", "s=Segment(A,(4*t_1,0))", at: "1-")
#ggb-tween("t_1", at: 2, to: 1, duration: 700)
Careful

ggb-tween needs a step number, not a range: at: 2, not at: "2-". Otherwise the build stops with “ggb-tween() needs a step number”.

A tween on step 1 never arrives as motion: on entering a slide the runtime replays the run up to the current step at once, and tweens jump to their target value. Step 1 builds up, drawing starts at step 2.

From the next step on the value sits on its target, so paging back shows the finished drawing instead of the motion again.

On paper#

There is no applet in the PDF. A labelled placeholder keeps the size of the frame, and link puts the way to the live applet beneath it.

#geogebra(height: 90pt, link: "https://www.geogebra.org/calculator")

Better is a drawing of your own. fallback takes any content: an image, a table, above all a drawing with CeTZ.

#geogebra(height: 120pt, link: "https://www.geogebra.org/calculator",
  fallback: cetz.canvas(length: 0.8cm, {
    import cetz.draw: *
    line((-2.6, 0), (2.6, 0), stroke: luma(70%))
    line((0, -0.4), (0, 2.6), stroke: luma(70%))
    line(..range(0, 45).map(i => (-2.2 + i * 0.1, 0.5 * calc.pow(-2.2 + i * 0.1, 2))),
         stroke: dark + 1.6pt)
  }))
Tip
Where the applet runs through several states, the better stand-in is the whole run as a row of pictures, not a photograph of one step.

Both take effect in the PDF only.

How the applet looks#

seamless: true, the default, takes the frame off the applet and puts its drawing area in the colour of the slide. It then looks like part of the slide rather than a window inside a window. background sets that colour.

#geogebra(height: 240pt, background: rgb("#f4f1ea"))
#geogebra(height: 240pt, seamless: false)   // with GeoGebra's own frame

background: auto, the default, takes the paper of the theme in force, so an applet on a dark theme comes up dark.

Careful
The viewport cannot be dragged by hand, and that is the default: whoever reaches beside the point during a talk would otherwise push the whole plane away. pan: true gives dragging and zooming back; points and sliders can be dragged either way.

font-size counts in points of the slide, like width and height, so the applet’s font grows with the slide instead of staying physically the same size on a projector. The default is 17, one above GeoGebra’s 16.

Careful
GeoGebra snaps the font size to steps, so neighbouring values often come out at the same height.
#geogebra(height: 240pt, font-size: 22)      // larger axis numbers
#geogebra(height: 240pt, pan: true)          // viewport by hand

grid and axes follow GeoGebra’s own default while they are auto and force one or the other otherwise. perspective: "G" shows the graphics view alone, app chooses the GeoGebra app (default "classic"), language the interface language, and animation-button shows GeoGebra’s play button.

Size#

width and height count in the measurements of the slide, not in screen pixels, and width: 100% is the usual case. What keeps every window showing the same crop is the visible range: it is set from the box the first time the applet appears, and after that ggb-view decides.

Tip
Two applets side by side sit best in a grid, each with width: 100% and a height of its own.

From the speaker view#

The speaker window runs a copy of every applet. m switches its pointer from the pen to the embedded frame; the applet in front of you is then the live one, and the copy on the canvas follows what you do to it.

Only what a hand has touched travels: a dragged point, a slider, the panned view. Creating, deleting or renaming sends the whole construction. An animation running on both sides sends nothing.

Careful
A step change resets both copies and replays the jobs of the slide. A change made by hand lives as long as the step does. Where a position is meant to stay, it belongs in the deck with ggb-set.
Tip
Pin down whatever is not meant to move: ggb-style("A", "B", fixed: true) nails the points that merely span a construction. Otherwise a hand in the talk easily takes the wrong one — with Thales, the diameter instead of the point on the half circle, and the whole arc travels with it.

Point(k) is a point on the path that a hand can take; Point(k, 0.3) is pinned to that parameter and cannot be dragged at all. Where it should start is said with position:. examples/geogebra-sprecher.typ is a deck built around exactly this: Thales with a point that walks along the half circle and leaves its trace, and a parabola with two sliders.

The keyboard#

Click the applet and it holds the focus; every key then lands inside it. The keys the talk uses are handed back out of the frame — see “A frame that has the focus”. Without a toolbar and without an algebra input, no key changes the construction anyway.

Whose applet this is#

This package does not ship GeoGebra. The browser fetches what runs in the frame from codebase, https://www.geogebra.org/apps/ by default. Three things follow:

  1. Without a network the frame stays empty. Whoever presents offline puts GeoGebra’s files beside the deck and points codebase at them.
  2. The applet stands under GeoGebra’s terms, not under this package’s MIT licence, which covers the Typst and runtime code here. For commercial use, read GeoGebra’s.
  3. The viewer’s browser talks to geogebra.org. Where that is unwanted — a firewall, a data protection requirement — codebase sends it elsewhere.
Note
On paper none of this is left: the PDF fetches nothing.