typstage

0.2.0

Animated HTML talks and a PDF handout from one source.

Etwas vorführen statt behaupten#

Ziel dieses Kapitels: eine Folie, auf der etwas geschieht, das Typst selbst nicht bewegen kann – eine Konstruktion, ein Video, eine gezeichnete Bewegung.

Ein Applet neben den Stichpunkten#

geogebra() bringt GeoGebra-Applets auf die Folie. Der übliche Aufbau: links die Konstruktion, rechts die Stichpunkte, darunter die Befehle.

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

#show: presentation.with(theme: themes.lesson)

== Parametervariation

#side-by-side(
  geogebra(width: 100%, height: 330pt),
  stagger[
    - Ausgangslage: $a = 1$, die Normalparabel
    - $a = "2,5"$ -- gestreckt, schmaler und steiler
    - $a = "0,35"$ -- gestaucht, breiter und flacher
    - $a = -"1,5"$ -- gespiegelt an der $x$-Achse
  ],
)

#ggb-view(x: (-3.6, 4.6), y: (-3.4, 4.6), grid: false)
#ggb-run("a=1", "f(x)=a*x^2")
#ggb-style("f", color: accent, thickness: 6)
#ggb-tween("a", at: 2, to: 2.5, duration: 950)
#ggb-tween("a", at: 3, to: 0.35, duration: 950)
#ggb-tween("a", at: 4, to: -1.5, duration: 1300)

ggb-run baut die Konstruktion auf, ggb-style gibt ihr die Farben der Folie, und jedes ggb-tween lässt einen Wert auf seinem Schritt an den neuen Wert laufen statt zu springen – auf denselben Schritten wie die Stichpunkte daneben. Kein Befehl nennt dabei das Applet: steht nur eines auf der Folie, finden die Befehle es von selbst. Erst zwei brauchen Namen und ein target:.

Tipp
Ein Tween gehört auf Schritt 2 oder später. Beim Betreten einer Folie spielt die Laufzeitumgebung alle Aufträge bis zum aktuellen Schritt sofort nach – ein Tween auf Schritt 1 käme deshalb nie als Bewegung an.
Hinweis
Das Applet lädt zur Laufzeit von geogebra.org nach: ohne Netz bleibt der Rahmen leer. Alles Weitere zu geogebra und den ggb-Befehlen steht im Kapitel GeoGebra.

Was auf dem Papier an dieser Stelle steht#

In der PDF bliebe an dieser Stelle ein leerer Kasten. geogebra und embed nehmen deshalb zwei Angaben für die gedruckte Ausgabe: fallback tritt an die Stelle des Rahmens – eine CeTZ-Zeichnung, ein Bild, eine Tabelle –, link setzt darunter eine im PDF anklickbare Adresse. Ohne fallback bleibt im Handout ein graues Rechteck mit der Beschriftung aus label.

#embed(
  url: "https://www.geogebra.org/calculator",
  width: 100%, height: 240pt,
  link: "https://www.geogebra.org/calculator",
  label: [GeoGebra-Applet],
)

Ein eigenes Dokument einbetten#

Hinweis

Ist das eigene Dokument selbst ein Typst-Dokument, braucht es davon nichts. Gib seinem Inhalt einen Namen und hol ihn dir:

// karte.typ -- übersetzt weiterhin für sich, mit eigener Seite
#let karte = [ ... ]
#set page(width: 16cm, height: 8.2cm)
#karte

// vortrag.typ
#import "karte.typ": karte
== Die Karte
#karte

Das set page bleibt zurück, die Folie behält ihre Geometrie. Was ankommt, ist Inhalt des Decks: dieselben Schriften, in jeder Größe scharf, im PDF dabei, und Schritt für Schritt aufdeckbar. Unter typst watch (siehe „Während du schreibst„) baut ein Speichern in karte.typ das Deck neu und bringt es auf demselben Schritt zurück. Ein Rahmen kann davon nichts.

YouTube-Videos lassen sich mit embed(url: "https://www.youtube.com/embed/VIDEO_ID") oder der Domain www.youtube-nocookie.com einbinden. Play/Pause, Zeitleiste und j/k/l im Presenter steuern die Bühne; die Vorschau folgt stumm. Die externe YouTube-API lädt erst beim ersten sichtbaren YouTube-Embed. Internet und ein über HTTP(S) geöffnetes Deck sind erforderlich; file:// kann Fehler 153 verursachen. Blockiert der Browser Autoplay, einmal Play auf der Bühne anklicken.

embed setzt beliebige Web-Inhalte in einen abgeschotteten Rahmen: url lädt eine Seite, html bettet ein eigenes Dokument als Text ein. Der Rahmen wird in Folieneinheiten vermessen und zeigt so in jedem Fenster denselben Ausschnitt.

#embed(
  html: "<div style=\"height:100%;display:grid;place-items:center\">"
      + "<canvas id=\"c\" width=\"320\" height=\"200\"></canvas></div>"
      + "<script>/* zeichnet in c */</script>",
  width: 100%, height: 220pt,
  fallback: align(center + horizon, [eine laufende Zeichenfläche]),
)

Ein Dokument aus html bekommt den Grundstil des Vortrags vorangestellt: es füllt seinen Rahmen, ist durchsichtig und trägt die laufende Schrift. Der eigene Stil gewinnt darüber; style: false schaltet den Grundstil ab.

Im gezoomten Rahmen ist ein CSS-Pixel genau ein Punkt der Folie. Wer den Inhalt in em bemaßt, dessen Dokument wächst mit den Folien mit; wer 15px schreibt, hat fest 15 Punkte neben einer 19-Punkt-Folienschrift stehen.

Achtung
height: 100% greift nur, weil der Grundstil html und body eine Höhe gibt. Mit style: false ist der Rahmen nur so hoch wie sein Inhalt, und justify-content: center zentriert im Nichts.

Soll das Dokument den Schritten der Folie folgen, bekommt es einen Namen. bridge-job legt einen Auftrag an diesen Namen ab, den der Browser beim Erreichen des Schritts in den Rahmen zustellt:

#embed(html: "…", bridge: <applet>, width: 100%, height: 240pt)
#bridge-job(<applet>, (befehl: "setze", wert: 3), at: 2)

payload ist ein Wörterbuch und wird ungelesen durchgereicht; was darin steht, ist Sache des Dokuments auf der anderen Seite. Darauf setzen die ggb-Befehle auf, und jedes Begleitpaket kann es genauso tun.

Achtung

Das Dokument muss sich anmelden. An einen Rahmen, der sich nie gemeldet hat, wird nichts zugestellt, und zwar wortlos. Beide Felder werden gebraucht: alles ohne typstage: 1 wird verworfen, bevor ready angesehen wird.

parent.postMessage({ typstage: 1, ready: 1 }, "*");
Achtung
Beim Zurückblättern und beim Betreten einer Folie wird der ganze Lauf von vorn wiederholt. Aufträge müssen deshalb wiederholbar sein: „setze 𝑎 auf 2,5“ ist gut, „erhöhe 𝑎 um 1“ nicht.

Audio und Medienausschnitte#

audio nimmt eine lokale Datei neben dem HTML oder eine direkte HTTP(S)-Audio-URL. Die Wiedergabe startet standardmäßig manuell. Audio, Video und YouTube nehmen start und end als Sekundenpositionen in der Quelle; loop: true wiederholt nur diesen Ausschnitt.

#audio("musik.mp3", start: 30, end: 75, loop: true)
#video("film.mp4", start: 30, end: 75, loop: true)
#embed(url: "https://www.youtube.com/embed/M7lc1UVf-VE",
       start: 30, end: 75, loop: true)

Die Sprecheransicht hat Wiedergabetasten und eine ziehbare Zeitleiste. k startet oder pausiert, j und l springen zehn Sekunden innerhalb des Ausschnitts. Shift+L wechselt Hell/Dunkel. YouTube braucht HTTP(S) und Internetzugang. Seine Bedienleiste ist standardmäßig verborgen; ?controls=1 in der URL zeigt sie wieder. Titel, Branding und Videoqualität bestimmt weiterhin YouTube.

Die Klassenuhr kann bei null einen optionalen Signalton auf der Bühne spielen:

#show: presentation.with(room: (
  clock: (step: 5, sound: "gong.mp3"),
))

sound nimmt eine Datei oder direkte URL; none lässt die Uhr stumm. Die Audiofreigabe des Browsers gilt weiterhin. speaker-view: (shortcuts: false) verbirgt die Tastaturhilfe beim Start. h oder ? schaltet sie um; verborgen nimmt sie keinen Platz ein.

Video#

video legt ein echtes HTML5-Video über die Folie: beim Betreten der Folie läuft es an, beim Verlassen hält es an.

#video("wellen.mp4", width: 100%, height: 240pt, poster: image("welle.png"))

autoplay und muted sind an, loop und controls aus – Browser lassen ein Video von sich aus nur stumm anlaufen. radius rundet die Ecken, at und enter sagen wie bei jedem Element, ab welchem Schritt es da ist. Auf Papier steht das poster; ohne poster bleibt im Handout ein leeres Rechteck.

Ein Video, das zur Glocke endet#

Vor der Stunde läuft ein Musikvideo, und es soll in dem Augenblick aufhören, in dem die Stunde beginnt. ends-at sagt nicht, wann es anfängt, sondern wann es zu Ende sein soll:

#video("intro.mp4", width: 100%, height: 100%, muted: false, ends-at: "08:15")

Beim Betreten der Folie liest die Laufzeit die Länge des Videos und setzt es so weit hinein, dass sein letztes Bild auf diese Minute fällt. Wer den Raum um 08:11 aufschließt, bekommt die letzten vier Minuten; wer um 08:07 kommt, die letzten acht.

Gerechnet wird mit der Uhr des Raums, nicht mit der des Vortrags. Wer die Stunde von der ersten auf die dritte verlegt, ändert mit room: (bell: …) eine Zeile statt einer je Video:

#show: presentation.with(room: (bell: "09:50"))

Ein video(ends-at: auto) nimmt die Zeit dann von dort.

Achtung
Ein Video mit Ton läuft ohne Zutun nicht an – Browser lassen das nur stumm zu. Ein Klick oder ein Tastendruck im Fenster gibt es frei, und danach läuft auch das nächste.

Daumenkino#

Bei flipbook zeichnet Typst jedes Einzelbild: render bekommt t von 0.0 bis 1.0 und gibt dazu das Bild – auch aus CeTZ, Fletcher oder einer Formel. Die Bilder liegen als SVG in der Datei und bleiben in jeder Größe scharf.

#flipbook(
  t => box(width: 100%, height: 100%,
    place(left + horizon, dx: t * 88%, circle(radius: 9pt, fill: accent))),
  frames: 24, fps: 20, width: 100%, height: 46pt,
)

frames ist die Zahl der Einzelbilder (Vorgabe 24), fps das Tempo beim Abspielen (Vorgabe 30). loop ist an; pingpong läuft statt dessen vor und zurück und geht dem loop vor. Ist beides aus, bleibt das letzte Bild stehen.

Die Uhr beginnt, wenn das Daumenkino zu sehen ist, nicht wenn seine Folie kommt: ein flipbook(at: "3-", loop: false) liegt auf den ersten beiden Schritten still und fängt beim Aufdecken bei null an.

Auf Papier steht ein einziges Bild: render(0.0), oder was still an seine Stelle setzt. Steht das System des Zuschauers auf „Bewegung reduzieren“, läuft das Daumenkino gar nicht erst los.

Achtung
Jedes Einzelbild wird wirklich gesetzt: 24 Bilder heißen 24 Layouts und 24 SVG-Bäume in der Datei. Bei aufwendigen Zeichnungen wächst beides schnell.