Touying: Interrupted Animations

Hey there, thanks a lot for the awesome Touying library \(^-^)/

Question:
A pattern I see myself repeating is an animation that is interrupted by another slide.

Example:
A(1/3) → A(2/3) → B(1/2) → B(2/2) → A(3/3)

The solution I am currently using basically parameterizes the content, so I can call the function twice, once passing something like uncover("2-") and once passing just the identity function.

However, I have also found touying-recall and waypoints and was wondering whether there was any way to achieve this using a combination of the two. This would require having a possibility to “hide”/“defer” the occurence of A(3/3)

Thanks and let me know if you need more info or you have a completely different idea :typstguy_talking:

Hi first of all thanks for the really interesting question.

Touying rn builds the set of slides you could recall as it traverses the tree thus we cannot recall something before it has been inserted into the tree. And we have no easy way to achieve an invisible slide that can be recalled. perhaps that would be a nice feature.
Furthermore we cannot defer the execution of a subslide really, you can only set a limit on the maximum subslide count by passing in repeat to the slide function. And the subslides always start at 1.

I think the easiest solution rn would be to write a function that parses slide content similar to how touying does and just emits what you want.

I wrote up a very simplified version here:

#import "@preview/touying:0.7.4": *
#import themes.simple: *

#show: simple-theme.with(
  config-info(
    title: "Split Your Slides"
  )
)
/// Allows you to relatively simply split your slides. See the example below.
/// Works by parsing the metadata of the animation method and changing the subslides based on your range and emitting it into a repeat fixed slide.
///
/// - s (int): the start of the range you want to show 
/// - e (int): the last index of the range you want to show
/// - head (content): a heading to be used for the slide
/// - bodies (content): multiple bodies containing pure `only` or `uncover` functions. 
/// -> content
#let split-slide(s,e,head:[== My Heading],..bodies) = {
  assert(s<=e, message:"s (="+str(s)+") must be less equal than e (="+str(e)+").")
  let parse-body(body) = {
    let animation-fns = ("uncover": uncover, "only": only)

    let d = (:)
    let m = utils.trim(body.children).at(0)
    d.insert("fn", animation-fns.at(repr(m.value.fn)))
    d.insert("args-cover-fn", m.value.args.at("cover-fn"))
    d.insert("args-subslide", m.value.args.at(0))
    d.insert("args-body", m.value.args.at(1))
    return d
  }
  let body-dicts = ()
  for body in bodies.pos() {
    body-dicts.push(parse-body(body))
  }

  let transform-subslides(body-dict, s, e) = {
    let transform-int-subslide(s,r) = {
      calc.max(1, r - s + 1)
    }
    let new-subslide = none
    if type(body-dict.args-subslide) == array {
      new-subslide = body-dict.args-subslide.map(transform-int-subslide.with(s))
    } else if type(body-dict.args-subslide) == int {
      new-subslide = transform-int-subslide(s,body-dict.args-subslide)
    } else if type(body-dict.args-subslide) == str {
      //split into numbers and "-" array
      let compute-new-range(subslide) = {
        //a-b -> (a, "-", b)
        let splitted = subslide.matches(regex("(\\d*)(-)?(\\d*)")).at(0).captures 
        let this-s = if splitted.at(0)=="" {1} else {int(splitted.at(0))}
        let this-e = if splitted.at(2)=="" {1000} else {int(splitted.at(2))}
        //compute intersection of both intervals s..e, this-s..this-e.
        let union-s = calc.max(s, this-s)
        let union-e = calc.min(e, this-e)
        let union = union-e - union-s
        //compute if visible
        let first = if union >= 0 {transform-int-subslide(s, union-s)} else {1000}
        (first, splitted.at(1), first+union).map(str).join("")
      }
      new-subslide = compute-new-range(body-dict.args-subslide)
    }
    (body-dict.fn)(cover-fn: body-dict.args-cover-fn, new-subslide, body-dict.args-body)
  }

  let combined-body = ()
  for body-dict in body-dicts {
    combined-body.push(transform-subslides(body-dict, s,e))
  }

  slide(repeat:e - s + 1)[
    #place(hide(head))
    #combined-body.sum()
  ]
}


#let slide-a(s,e) = split-slide(
  s, e,
  head:[== Slide A]
)[
  #uncover("1-2")[First Content, ]
][
  #uncover("2-")[Second Content, ]
][
  #uncover("3-")[Third Content]
]


#slide-a(1,2)

== Slide B
#slide[
Number 1,
#pause 
Number 2 
]

#slide-a(3,3)

You can ofc make this much better but at some point you are just repeating what touying internally does.
Perhaps create an issue and we can think about this. The easiest solution would likely be that we allow passing in some argument into a slide function that tells touying to not emit any of the content but save it in the recaller map still.

This gave me reason to revisit the soon to be usable touying-render function and allow it to emit multiple subslides at once. From touying 0.8.0 onwards you will thus be able to do:

#import "@preview/touying:0.7.4": *
#import themes.simple: *

#show: simple-theme.with(
  config-info(
    title: "Split Your Slides"
  )
)
#slide-a-content = [
  First Content
  #pause
  Second Content
  #pause
  Third Content
]

== Slide A (part 1)
#touying-render(slide-a-content, subslides: "-2")

== Slide B
Number 1,
#pause 
Number 2 

== Slide A (part 2)
#touying-render(slide-a-content, subslides: 3)

Ofc waypoints etc will also be supported with their range. It does also work for tables and even graphics you bind via touying-reduce.

Thank you for the quick and kind response!
The preview looks really awesome, can’t wait to test it. Its becoming harder and harder not to force all PowerPoint users around me to use Typst xD