diff --git a/README.md b/README.md index fd4e849..24aa119 100644 --- a/README.md +++ b/README.md @@ -21,31 +21,44 @@ This is a template for creating slides in [Typst](https://typst.app/). #slide(title: "A boring static slide")[ Some boring static text. + + #lorem(20) ] #slide[ A fancy dynamic slide without a title. - More text. - #only(2)[This appears later!] + #uncover("2-")[This appears later!] ] -``` -As you can see, creating slides is as simple as using the `#slide` function. -(You do not need to care about stuff like `#new-section` or `#only` in the -beginning.) +#slide(theme-variant: "wake up")[ + Focus! +] + +#new-section("Conclusion") + +#slide(title: "Take home message")[ + Read the book! + + Try it out! + + Create themes! +] +``` This code produces these PDF pages: ![title slide](assets/simple.png) -That's all to get you started! +As you can see, creating slides is as simple as using the `#slide` function. +You can also use different +[themes](https://andreaskroepelin.github.io/typst-slides/book/theme-gallery/index.html) +(contributions welcome if you happen to +[create your own](https://andreaskroepelin.github.io/typst-slides/book/themes.html#create-your-own-theme)!) + +For dynamic content, the template also provides [a convenient API for complex +overlays](https://andreaskroepelin.github.io/typst-slides/book/dynamic.html). + For more details, visit the [book](https://andreaskroepelin.github.io/typst-slides/book)! -There are also different -[themes](https://andreaskroepelin.github.io/typst-slides/book/theme-gallery/index.html) -to choose from! -If you like, you can even -[create your own](https://andreaskroepelin.github.io/typst-slides/book/themes.html#create-your-own-theme). - **⚠ This template is in active development. While I try to make sure that the `main`-branch always is in a usable state, there are no compatibility guarantees!** diff --git a/assets/Makefile b/assets/Makefile new file mode 100644 index 0000000..ba08ce9 --- /dev/null +++ b/assets/Makefile @@ -0,0 +1,4 @@ +simple: + pdftoppm ../examples/simple.pdf ./simple -png + montage ./simple-* -geometry +50+50 -background LightGray ./simple.png + rm ./simple-* diff --git a/assets/first-slide.png b/assets/first-slide.png deleted file mode 100644 index 401eb58..0000000 Binary files a/assets/first-slide.png and /dev/null differ diff --git a/assets/simple.png b/assets/simple.png index dd95b79..c500840 100644 Binary files a/assets/simple.png and b/assets/simple.png differ diff --git a/assets/title-slide.png b/assets/title-slide.png deleted file mode 100644 index 62eed44..0000000 Binary files a/assets/title-slide.png and /dev/null differ diff --git a/book/src/dynamic.md b/book/src/dynamic.md index 49d0d67..a2da1ba 100644 --- a/book/src/dynamic.md +++ b/book/src/dynamic.md @@ -1,40 +1,298 @@ # Dynamic slides -This template offers basic support for dynamic slides with changing content. -That means you can show or hide parts of a slide at different points in time. -To restrict the visiblity of content to certain "subslides", use one of the -following function: - -- `#only(2)[content]`: content is only visible on 2nd subslide -- `#until(3)[content]`: content is only visible on 1st, 2nd, and 3rd subslide -- `#beginning(2)[content]`: content is visible from the 2nd subslide onwards -- `#one-by-one(start: 2)[one][two][three]`: uncover `one` on the 2nd slide, then - `two` on the 3rd slide, and then `three` on the 4th slide. You can specify an - arbitrary number of contents. The `start` argument is optional. - `#one-by-one[x][y]` will uncover `x` on the first subslide. - -Let's see this in action: -```typ -#slide[ - always visible - #only(1)[only visible on the first subslide] - #beginning(2)[only visible on the second subslide] + +The PDF format does not (trivially) allow to include animations, as one would be +used to from, say, Powerpoint. +The solution PDF-based presentation slides use is to create multiple PDF pages +for one slide, each with slightly different content. +This enables us to have some basic dynamic elements on our slides. + +In this book, we will use the term _logical slide_ for a section of content that +was created by one call to `#slide`, and _subslide_ for a resulting PDF page. +Each logical side can have an arbitrary amount of subslides and every subslide +is part of exactly one logical slide. +Note that the same page number is displayed for all subslides of a logical slide. + + +In the LaTeX beamer package, the functionalities described on this page are +called "overlays". + +## Reserve space or not? +When you want to specify that a certain piece of content should be displayed one +some subslides but not on others, the first question should be what should happen +on the subslides it is _not_ displayed on. +You could either want +- that it is completely not existing there, or +- that it is invisible but it still occupies the space it would need otherwise + (see [the docs of the `#hide` function](https://typst.app/docs/reference/layout/hide/)) + +The two different behaviours can be achieved using either `#only` or `#uncover`, +respectively. +The intuition behind it is that, in one case, content is _only_ existing on some +slides, and, in the other case, it is merely _covered_ when not displayed. + +## General syntax for `#only` and `#uncover` +Both functions are used in the same way. +They each take two positional arguments, the first is a description of the +subslides the content is supposed to be shown on, the second is the content itself. +Note that Typst provides some syntactic sugar for trailing content arguments, +namely putting the content block _behind_ the function call. + +You could therefore write: +```typ +#only(2)[Some content to display only on subslide 2] +#uncover(3)[Some content to uncover only on subslide 3] +``` + +In this example, we specified only a single subslide index, resulting in content +that is shown on that exact subslide and at no other one. +Let's explore more complex rules next: + +## Complex display rules +There are multiple options to define more complex display rules. + +### Array +The simplest extension of the single-number case is to use an array. +For example +```typ +#uncover((1, 2, 4))[...] +``` +will uncover its content on the first, second and fourth subslide. +The array elements can actually themselves be any kind of rule that is explained +on this page. + +### Interval +You can also provied a (bounded or half-bounded) interval in the form of a +dictionary with a `beginning` and/or an `until` key: +```typ +#only((beginning: 1, until: 5)[Content displayed on subslides 1, 2, 3, 4, and 5] +#only((beginning: 2))[Content displayed on subslide 2 and every following one] +#only((until: 3))[Content displayed on subslides 1, 2, and 3] +#only((:))[Content that is always displayed] +``` +In the last case, you would not need to use `#only` anyways, obviously. + +### Convenient syntax as strings +In principle, you can specify every rule using numbers, arrays, and intervals. +However, consider having to write +```typ +#uncover(((until: 2), 4, (beginning: 6, until: 8), (beginning: 10)))[...] +``` +That's only fun the first time. +Therefore, we provide a convenient alternative. +You can equivalently write: +```typ +#uncover("-2, 4, 6-8, 10-")[...] +``` +Much better, right? +The spaces are optional, so just use them if you find it more readable. + +Unless you you are creating those function calls programmaticly, it is a good +recommendation to use the single-number syntax (`#only(1)[...]`) if that +suffices and the string syntax for any more complex use case. + +## Higher level helper functions +With `#only` and `#uncover` you can come a long way but there are some reoccurring +situations for which helper functions are provided. + +### `#one-by-one` and `#line-by-line` +Consider some code like the following: +```typ +#uncover("1-")[first ] +#uncover("2-")[second ] +#uncover("3-")[third] +``` +The goal here is to uncover parts of the slide one by one, so that an increasing +amount of content is shown. +A shorter but equivalent way would be to write +```typ +#one-by-one[first ][second ][third] +``` + +And what about this? +```typ +#uncover("3-")[first ] +#uncover("4-")[second ] +#uncover("5-")[third] +``` +Now, we still want to uncover certain elements one after the other but starting +on subslide 3. +We can use the optional `start` argument of `#one-by-one` for that: +```typ +#one-by-one(start: 3)[first ][second ][third] +``` + +`#one-by-one` is especially useful for arbitrary contents that you want to display +in that manner. +Often, you just want to do that with very simple elements, however. +A very frequent use case are bullet lists. +Instead of +```typ +#one-by-one[ + - first +][ + - second +][ + - third +] +``` +you can also write +```typ +#line-by-line[ + - first + - second + - third ] ``` +The content provided as an argument to `#line-by-line` is parsed as a `sequence` +by Typst with one element per line (hence the name of this function). +We then simply iterate over that `sequence` as if it were given to `#one-by-one`. -The number of subslides (number of PDF pages produced) one logical slide is -converted to depends on the highest subslide index you specified content to -appear or disappear. +Note that there also is an optional `start` argument for `#line-by-line`, which +works just the same as for `#one-by-one`. + +### `pause` as an alternative to `#one-by-one` +There is yet another way to solve the same problem as `#one-by-one`. +If you have used the LaTeX beamer package before, you might be familiar with the +`\pause` command. +It makes everything after it on that slide appear on the next subslide. + +Remember that the concept of "do something with everything after it" is covered +by the `#show: ...` mechanism in Typst. +We exploit that to use the `pause` function in the following way. +```typ +Show this first. +#show: pause(2) +Show this later. +#show: pause(3) +Show this even later. +#show: pause(4) +That took aaaages! +``` +This would be equivalent to: +```typ +#one-by-one[ + Show this first. +][ + Show this later. +][ + Show this even later. +][ + That took aaaages! +] +``` +It is obvious that `pause` only brings an advantage over `#one-by-one` when you +want to distribute a lot of code onto different subslides. + +**Hint:** +You might be annoyed by having to manually number the pauses as in the code +above. +You can diminish that issue a bit by using a counter variable: +```typ +Show this first. +#let pc = 1 // `pc` for pause counter +#{ pc += 1 } #show: pause(pc) +Show this later. +#{ pc += 1 } #show: pause(pc) +Show this even later. +#{ pc += 1 } #show: pause(pc) +That took aaaages! +``` +This has the advantage that every `pause` line looks identical and you can move +them around arbitrarily. +In later versions of this template, there could be a nicer solution to this +issue, hopefully. + +### `#alternatives` to substitute content +The so far discussed helpers `#one-by-one`, `#line-by-line`, and `pause` all +build upon `#uncover`. +There is an analogon to `#one-by-one` that is based on `#only`, namely +`#alternatives`. +You can use it to show some content on one subslide, then substitute it by +something else, then by something else, etc. + +Consider this example: +```typ +#only(1)[Ann] #only(2)[Bob] #only(3)[Christopher] +likes +#only(1)[chocolate] #only(2)[strawberry] #only(3)[vanilla] +ice cream. +``` +Here, we want to display three different sentences with the same structure: +Some person likes some sort of ice cream. +Using `#only`, the positioning of `likes` and `ice cream` moves around in the +produced slide because, for example, `Ann` takes much less space than +`Christopher`. +This somewhat disturbs the perception of the constant structure of the sentence +and that only the names and kinds of ice cream change. + +To avoid such movement and only subsitute certain parts of content, you can use +the `#alternatives` function. +With it, our example becomes: +```typ +#alternatives[Ann][Bob][Christopher] +likes +#alternatives[chocolate][strawberry][vanilla] +ice cream. +``` + +`#alternatives` will put enough empty space around, for example, `Ann` such that +it usese the same amount of space as `Christopher`. +In a sense, it is like a mix of `#only` and `#uncover` with some reserving of +space. + +By default, all elements that enter an `#alternatives` command are aligned at +the bottom left corner. +This might not always be the desired or most pleasant way to position it, so you +can provide an optional `position` argument to `#alternatives` that takes an +[`alignment` or `2d alignment`](https://typst.app/docs/reference/layout/align/#parameters--alignment). +For example: +```typ +We know that +#alternatives(position: center + horizon)[$pi$][$sqrt(2)^2 + 1/3$] +is +#alternative[irrational][rational]. +``` +makes the mathematical terms look better positioned. + +Similar to `#one-by-one` and `#line-by-line`, `#alternatives` also has an optional +`start` argument that works just the same as for the other two. -Note that the same page number is displayed for all subslides of a logical slide. ## Cover mode -You can decide if you want covered content to be completely hidden (the default) -or if you want it to be visible but muted (printed in a light gray). -Switch between the two modes by using +Covered content (using `#uncover`, `#one-by-one`, `#line-by-line`, or `pause`) +is completely invisible, by default. +You can decide to make it visible but less prominent using the optional `mode` +argument to each of those functions. +The `mode` argument takes two different values: `"invisible"` (the default) and +`"transparent"`. +(This terminology is taken from LaTeX beamer as well.) +With `mode: "transparent"`, text is printed in a light gray. + +Use it as follows: +```typ +#uncover("3-5", mode: "transparent")[...] +#one-by-one(start: 2, mode: "transparent")[...][...] +#line-by-line(mode: "transparent")[ + ... + ... +] +#show: pause(4, mode: "transparent") +``` + +**Warning!** +The transparent mode really only wraps the covered content in a ```typ -#cover-mode-hide // covered content is hidden completely -#cover-mode-mute // covered content is visible but muted +#text(fill: gray.lighten(50%)[...] ``` +so it has only limited control over the actual display. +Especially +- text that defines its own color (e.g. syntax highlighting), +- visualisations, +- images + +will not be affected by that. +This make the transparent mode only somewhat useful today. + ## Internal number of repetitions **TL;DR:** For slides with more than ten subslides, you need to set the `max-repetitions` diff --git a/examples/demo.typ b/examples/demo.typ new file mode 100644 index 0000000..8a479e2 --- /dev/null +++ b/examples/demo.typ @@ -0,0 +1,304 @@ +#import "../slides.typ": * + +#show: slides.with( + authors: "Andreas Kröpelin", + short-authors: "A. Kröpelin", + title: [`typst-slides`: Easily creating slides in Typst ], + subtitle: "An overview over all the features", + short-title: "Slides template demo", + date: "April 2023" +) + +#show link: set text(blue) + +#new-section("Introduction") + +#slide(title: "About this presentation")[ + This presentation is supposed to briefly showcase what you can do with this + template. + + For a full documentation, read the + #link("https://andreaskroepelin.github.io/typst-slides/book/")[online book]. +] + +#slide(title: "A title")[ + Let's explore what we have here. + + On the top of this slide, you can see the slide title. + + We used the `title` argument of the `#slide` function for that: + ```typ + #slide(title: "First slide")[ + ... + ] + ``` +] + +#slide[ + Titles are not mandatory, this slide doesn't have one. + + But did you notice that the current section name is displayed above that + top line? + + We defined it using + #raw("#new-section(\"Introduction\")", lang: "typst", block: false). + + This helps our audience with not getting lost after a microsleep. +] + +#slide(title: "The bottom of the slide")[ + Now, look down! + + There we have some general info for the audience about what talk they are + actually attending right now. + + You can also see the slide number there. +] + +#new-section("Dynamic content") + +#slide(title: [A dynamic slide with `pause`s])[ + Sometimes we don't want to display everything at once. + #let pc = 1 + #{ pc += 1 } #show: pause(pc) + + That's what the `pause` function is there for! + Use it as + ```typ + #show: pause(n) + ``` + #{ pc += 1 } #show: pause(pc) + + It makes everything after it appear at the `n`-th subslide. + + #text(.6em)[(Also note that the slide number does not change while we are here.)] +] + +#slide(title: "Fine-grained control")[ + When `#pause` does not suffice, you can use more advanced commands to show + or hide content. + + These are your options: + - `#uncover` + - `#only` + - `#alternatives` + - `#one-by-one` + - `#line-by-line` + + Let's explore them in more detail! +] + +#let example = block.with( + width: 100%, + inset: .5em, + fill: aqua.lighten(80%), + radius: .5em +) + +#slide(title: [`#uncover`: Reserving space])[ + With `#uncover`, content still occupies space, even when it is not displayed. + + For example, #uncover(2)[these words] are only visible on the second "subslide". + + In `()` behind `#uncover`, you specify _when_ to show the content, and in + `[]` you then say _what_ to show: + #example[ + ```typ + #uncover(3)[Only visible on the third "subslide"] + ``` + #uncover(3)[Only visible on the third "subslide"] + ] +] + +#slide(title: "Complex display rules")[ + So far, we only used single subslide indices to define when to show something. + + We can also use arrays of numbers... + #example[ + #set text(size: .8em) + ```typ + #uncover((1, 3, 4))[Visible on subslides 1, 3, and 4] + ``` + #uncover((1, 3, 4))[Visible on subslides 1, 3, and 4] + ] + + ...or a dictionary with `beginning` and/or `until` keys: + #example[ + #set text(size: .8em) + ```typ + #uncover((beginning: 2, until: 4))[Visible on subslides 2, 3, and 4] + ``` + #uncover((beginning: 2, until: 4))[Visible on subslides 2, 3, and 4] + ] +] + +#slide(title: "Convenient rules as strings")[ + As as short hand option, you can also specify rules as strings in a special + syntax. + + Comma separated, you can use rules of the form + #table( + columns: (auto, auto), + column-gutter: 1em, + stroke: none, + align: (x, y) => (right, left).at(x), + [`1-3`], [from subslide 1 to 3 (inclusive)], + [`-4`], [all the time until subslide 4 (inclusive)], + [`2-`], [from subslide 2 onwards], + [`3`], [only on subslide 3], + ) + #example[ + #set text(.8em) + ```typ + #uncover("-2, 4-6, 8-")[Visible on subslides 1, 2, 4, 5, 6, and from 8 onwards] + ``` + #uncover("-2, 4-6, 8-")[Visible on subslides 1, 2, 4, 5, 6, and from 8 onwards] + ] +] + +#slide(title: [`#only`: Reserving no space])[ + Everything that works with `#uncover` also works with `#only`. + + However, content is completely gone when it is not displayed. + + For example, #only(2)[#text(red)[see how]] the rest of this sentence moves. + + Again, you can use complex string rules, if you want. + #example[ + ```typ + #only("2-4, 6")[Visible on subslides 2, 3, 4, and 6] + ``` + #only("2-4, 6")[Visible on subslides 2, 3, 4, and 6] + ] +] + +#slide(title: [`#alternatives`: Substituting content])[ + You might be tempted to try + #example[ + #set text(.8em) + ```typ + #only(1)[Ann] #only(2)[Bob] #only(3)[Christopher] likes #only(1)[chocolate] #only(2)[strawberry] #only(3)[vanilla] ice cream. + ``` + #only(1)[Ann] #only(2)[Bob] #only(3)[Christopher] + likes + #only(1)[chocolate] #only(2)[strawberry] #only(3)[vanilla] + ice cream. + ] + + But it is hard to see what piece of text actually changes because everything + moves around. + Better: + #example[ + #set text(.8em) + ```typ + #alternatives[Ann][Bob][Christopher] likes #alternatives[chocolate][strawberry][vanilla] ice cream. + ``` + #alternatives[Ann][Bob][Christopher] likes #alternatives[chocolate][strawberry][vanilla] ice cream. + ] +] + +#slide(title: [`#one-by-one`: An alternative for `#pause`])[ + `#alternatives` is to `#only` what `#one-by-one` is to `#uncover`. + + `#one-by-one` behaves similar to using `#pause` but you can additionally + state when uncovering should start. + #example[ + #set text(.8em) + ```typ + #one-by-one(start: 2)[one ][by ][one] + ``` + #one-by-one(start: 2)[one ][by ][one] + ] + + `start` can also be omitted, then it starts with the first subside: + #example[ + #set text(.8em) + ```typ + #one-by-one[one ][by ][one] + ``` + #one-by-one[one ][by ][one] + ] +] + +#slide(title: [`#line-by-line`: syntactic sugar for `#one-by-one`])[ + Sometimes it is convenient to write the different contents to uncover one + at a time in subsequent lines. + + This comes in especially handy for bullet lists, enumerations, and term lists. + #example[ + #set text(.8em) + #grid( + columns: (1fr, 1fr), + gutter: 1em, + ```typ + #line-by-line(start: 2)[ + - first + - second + - third + ] + ```, + line-by-line(start: 2)[ + - first + - second + - third + ] + ) + ] + + `start` is again optional and defaults to `1`. +] + +#slide(title: "Different ways of covering content")[ + When content is covered, it is completely invisible by default. + + However, you can also just display it in light gray by using the + `mode` argument with the value `"transparent"`: + #let pc = 1 + #{ pc += 1 } #show: pause(pc, mode: "transparent") + + Covered content is then displayed differently. + #{ pc += 1 } #show: pause(pc, mode: "transparent") + + Every `uncover`-based function has an optional `mode` argument: + - `#show: pause(...)` + - `#uncover(...)[...]` + - `#one-by-one(...)[...][...]` + - `#line-by-line(...)[...][...]` +] + +#new-section("Themes") + +#slide(title: "How a slide looks...")[ + ... is defined by the _theme_ of the presentation. + + This demo uses the default theme. + + Because of it, the title slide and the decoration on each slide (with + section name, short title, slide number etc.) look the way they do. + + Themes can also provide variants, for example ... +] + +#slide(theme-variant: "wake up")[ + ... this one! + + It's very minimalist and helps the audience focus on an important point. +] + +#slide(title: "Your own theme?")[ + If you want to create your own design for slides, you can define custom + themes! + + #link("https://andreaskroepelin.github.io/typst-slides/book/themes.html#create-your-own-theme")[The book] + explains how to do so. +] + +#new-section("Conclusion") + +#slide(title: "That's it!")[ + Hopefully you now have some kind of idea what you can do with this template. + + Consider giving it + #link("https://github.com/andreasKroepelin/typst-slides")[a GitHub star #text(font: "OpenMoji")[#emoji.star]] + or open an issue if you run into bugs or have feature requests. +] \ No newline at end of file diff --git a/examples/doc.typ b/examples/doc.typ deleted file mode 100644 index 23fb734..0000000 --- a/examples/doc.typ +++ /dev/null @@ -1,48 +0,0 @@ -#import "../slides.typ": * - -#set text( - font: "DejaVu Sans", -) - -#show: slides.with( - authors: ("John Doe", "Jane Doe"), - short-authors: "J+J Doe", - title: "Demonstration of a new Typst template for slides", - short-title: "Slides template demo", - date: "March 2023" -) - -#new-section("Introduction") - -#slide(title: "First slide")[ - #lorem(10) -] - -#slide[ - Second slide, without a title - #lorem(10) -] - -#new-section("Multislides") - -#slide(title: "A multislide")[ - Note how the page number does not increase while we are here. - - #only(2)[ - $ integral exp(-(mu - x)^2 / (2 sigma^2) ) dif x $ - ] - - #until(3)[Hurry reading this, it disappears after subslide 3!] - - #beginning(4)[Huh, pretty empty here #sym.dots.h] -] - -#slide(title: "A multislide with list items appearing one by one")[ - - #grid( - columns: (1fr, 1fr), - gutter: 1em, - one-by-one[- abc][- def][- ghi], - one-by-one(start: 2)[1. jkl][2. mno][3. pqr], - ) -] diff --git a/examples/gauss.typ b/examples/gauss.typ index 6898b98..29ce971 100644 --- a/examples/gauss.typ +++ b/examples/gauss.typ @@ -41,10 +41,12 @@ #one-by-one[ *base case:* Let $n = 1$. Then $sum_(i=1)^1 i = (1 dot.c 2)/2 = 1$ #emoji.checkmark.heavy - #v(1em) + // #v(1em) + ][ *ind. hypothesis:* Let $sum_(i=1)^k i = k(k+1)/2$ for some $k >= 1$. - #v(1em) + // #v(1em) + ][ *ind. step:* Show that $sum_(i=1)^(k+1) i = ((k+1)(k+2))/2$ $ diff --git a/examples/simple.typ b/examples/simple.typ index a0980e4..74cf03e 100644 --- a/examples/simple.typ +++ b/examples/simple.typ @@ -13,10 +13,25 @@ #slide(title: "A boring static slide")[ Some boring static text. + + #lorem(20) ] #slide[ A fancy dynamic slide without a title. - More text. - #only(2)[This appears later!] + #uncover("2-")[This appears later!] +] + +#slide(theme-variant: "wake up")[ + Focus! +] + +#new-section("Conclusion") + +#slide(title: "Take home message")[ + Read the book! + + Try it out! + + Create themes! ] diff --git a/slides.typ b/slides.typ index b747a88..f9da8fc 100644 --- a/slides.typ +++ b/slides.typ @@ -1,23 +1,216 @@ +// ============================== +// ======== GLOBAL STATE ======== +// ============================== + #let section = state("section", none) #let subslide = counter("subslide") #let logical-slide = counter("logical-slide") #let repetitions = counter("repetitions") -#let cover-mode = state("cover-mode", "hide") #let global-theme = state("global-theme", none) -#let cover-mode-hide = cover-mode.update("hide") -#let cover-mode-mute = cover-mode.update("mute") #let new-section(name) = section.update(name) -// avoid "#set" interferences -#let full-box(obj) = { - box( - width: 100%, height: auto, baseline: 0%, fill: none, - stroke: none, radius: 0%, inset: 0%, outset: 0%, - obj - ) +// ================================= +// ======== DYNAMIC CONTENT ======== +// ================================= + +#let _slides-cover(mode, body) = { + if mode == "invisible" { + hide(body) + } else if mode == "transparent" { + text(gray.lighten(50%), body) + } else { + panic("Illegal cover mode: " + mode) + } +} + +#let _parse-subslide-indices(s) = { + let parts = s.split(",").map(p => p.trim()) + let parse-part(part) = { + let match-until = part.match(regex("^-([[:digit:]]+)$")) + let match-beginning = part.match(regex("^([[:digit:]]+)-$")) + let match-range = part.match(regex("^([[:digit:]]+)-([[:digit:]]+)$")) + let match-single = part.match(regex("^([[:digit:]]+)$")) + if match-until != none { + let parsed = int(match-until.captures.first()) + // assert(parsed > 0, "parsed idx is non-positive") + ( until: parsed ) + } else if match-beginning != none { + let parsed = int(match-beginning.captures.first()) + // assert(parsed > 0, "parsed idx is non-positive") + ( beginning: parsed ) + } else if match-range != none { + let parsed-first = int(match-range.captures.first()) + let parsed-last = int(match-range.captures.last()) + // assert(parsed-first > 0, "parsed idx is non-positive") + // assert(parsed-last > 0, "parsed idx is non-positive") + ( beginning: parsed-first, until: parsed-last ) + } else if match-single != none { + let parsed = int(match-single.captures.first()) + // assert(parsed > 0, "parsed idx is non-positive") + parsed + } else { + panic("failed to parse visible slide idx:" + part) + } + } + parts.map(parse-part) +} + +#let _check-visible(idx, visible-subslides) = { + if type(visible-subslides) == "integer" { + idx == visible-subslides + } else if type(visible-subslides) == "array" { + visible-subslides.any(s => _check-visible(idx, s)) + } else if type(visible-subslides) == "string" { + let parts = _parse-subslide-indices(visible-subslides) + _check-visible(idx, parts) + } else if type(visible-subslides) == "dictionary" { + let lower-okay = if "beginning" in visible-subslides { + visible-subslides.beginning <= idx + } else { + true + } + + let upper-okay = if "until" in visible-subslides { + visible-subslides.until >= idx + } else { + true + } + + lower-okay and upper-okay + } else { + panic("you may only provide a single integer, an array of integers, or a string") + } +} + +#let _last-required-subslide(visible-subslides) = { + if type(visible-subslides) == "integer" { + visible-subslides + } else if type(visible-subslides) == "array" { + calc.max(..visible-subslides.map(s => _last-required-subslide(s))) + } else if type(visible-subslides) == "string" { + let parts = _parse-subslide-indices(visible-subslides) + _last-required-subslide(parts) + } else if type(visible-subslides) == "dictionary" { + let last = 0 + if "beginning" in visible-subslides { + last = calc.max(last, visible-subslides.beginning) + } + if "until" in visible-subslides { + last = calc.max(last, visible-subslides.until) + } + last + } else { + panic("you may only provide a single integer, an array of integers, or a string") + } +} + +#let _conditional-display(visible-subslides, reserve-space, mode, body) = { + repetitions.update(rep => calc.max(rep, _last-required-subslide(visible-subslides))) + locate( loc => { + if _check-visible(subslide.at(loc).first(), visible-subslides) { + body + } else if reserve-space { + _slides-cover(mode, body) + } + }) +} + +#let uncover(visible-subslides, mode: "invisible", body) = { + _conditional-display(visible-subslides, true, mode, body) +} + +#let only(visible-subslides, body) = { + _conditional-display(visible-subslides, false, "doesn't even matter", body) +} + +#let one-by-one(start: 1, mode: "invisible", ..children) = { + repetitions.update(rep => calc.max(rep, start + children.pos().len() - 1)) + for (idx, child) in children.pos().enumerate() { + uncover((beginning: start + idx), mode: mode, child) + } +} + +#let alternatives(start: 1, position: bottom + left, ..children) = { + repetitions.update(rep => calc.max(rep, start + children.pos().len() - 1)) + style(styles => { + let sizes = children.pos().map(c => measure(c, styles)) + let max-width = calc.max(..sizes.map(sz => sz.width)) + let max-height = calc.max(..sizes.map(sz => sz.height)) + for (idx, child) in children.pos().enumerate() { + only(start + idx, box( + width: max-width, + height: max-height, + align(position, child) + )) + } + }) +} + +#let line-by-line(start: 1, mode: "invisible", body) = { + let items = if repr(body.func()) == "sequence" { + body.children + } else { + ( body, ) + } + + let idx = start + for item in items { + if repr(item.func()) != "space" { + uncover((beginning: idx), mode: mode, item) + idx += 1 + } else { + item + } + } +} + +#let pause(beginning, mode: "invisible") = body => { + uncover((beginning: beginning), mode: mode, body) +} + + +// ================================ +// ======== SLIDE CREATION ======== +// ================================ + +#let slide( + max-repetitions: 10, + theme-variant: "default", + override-theme: none, + ..kwargs, + body +) = { + pagebreak(weak: true) + logical-slide.step() + locate( loc => { + subslide.update(1) + repetitions.update(1) + + let slide-content = global-theme.at(loc).variants.at(theme-variant) + if override-theme != none { + slide-content = override-theme + } + let slide-info = kwargs.named() + + for _ in range(max-repetitions) { + locate( loc-inner => { + let curr-subslide = subslide.at(loc-inner).first() + if curr-subslide <= repetitions.at(loc-inner).first() { + if curr-subslide > 1 { pagebreak(weak: true) } + + slide-content(slide-info, body) + } + }) + subslide.step() + } + }) } +// =============================== +// ======== DEFAULT THEME ======== +// =============================== + #let slides-default-theme(color: teal) = data => { let title-slide = { align(center + horizon)[ @@ -104,91 +297,9 @@ ) } -#let slide( - max-repetitions: 10, - theme-variant: "default", - override-theme: none, - ..kwargs, - body -) = { - pagebreak(weak: true) - logical-slide.step() - locate( loc => { - subslide.update(1) - repetitions.update(1) - - let slide-content = global-theme.at(loc).variants.at(theme-variant) - if override-theme != none { - slide-content = override-theme - } - let slide-info = kwargs.named() - - for _ in range(max-repetitions) { - locate( loc-inner => { - let curr-subslide = subslide.at(loc-inner).first() - if curr-subslide <= repetitions.at(loc-inner).first() { - if curr-subslide > 1 { pagebreak(weak: true) } - slide-content(slide-info, body) - } - }) - subslide.step() - } - }) -} - -#let slides-custom-hide(body) = { - locate( loc => { - let mode = cover-mode.at(loc) - // wrap in box to avoid hiding issues with list, equation and other types - if mode == "hide" { - hide(full-box(body)) - } else if mode == "mute" { - text(gray.lighten(50%), full-box(body)) - } else { - panic("Illegal `cover-mode`: " + mode) - } - }) -} - -#let only(visible-slide-number, body) = { - repetitions.update(rep => calc.max(rep, visible-slide-number)) - locate( loc => { - if subslide.at(loc).first() == visible-slide-number { - full-box(body) - } else { - slides-custom-hide(body) - } - }) -} - -#let beginning(first-visible-slide-number, body) = { - repetitions.update(rep => calc.max(rep, first-visible-slide-number)) - locate( loc => { - if subslide.at(loc).first() >= first-visible-slide-number { - full-box(body) - } else { - slides-custom-hide(body) - } - }) -} - -#let until(last-visible-slide-number, body) = { - repetitions.update(rep => calc.max(rep, last-visible-slide-number)) - locate( loc => { - if subslide.at(loc).first() <= last-visible-slide-number { - full-box(body) - } else { - slides-custom-hide(body) - } - }) -} - -#let one-by-one(start: 1, ..children) = { - repetitions.update(rep => calc.max(rep, start + children.pos().len() - 1)) - for (idx, child) in children.pos().enumerate() { - beginning(start + idx, child) - } -} +// =================================== +// ======== TEMPLATE FUNCTION ======== +// =================================== #let slides( title: none,