qmd-lab / closeread

https://closeread.dev
MIT License
130 stars 5 forks source link

transitions from revealjs #13

Closed andrewpbray closed 5 months ago

andrewpbray commented 1 year ago

There seem to be two very useful transitions implemented in revealjs that could be useful in the cr format: auto-animate and code highlights.

  1. Auto Animate: Docs / Source
  2. Step-by-step code highlights: Docs / Source

In an ideal world, we could add a revealjs dependency, then in our source / filter add whatever classes are needed to "fool" the revealjs code into thinking that scrolling an object into the viewer is analogous to making a given slide (.section) visible. But that seems like it'd be too good to be true? Alternatively, we could harvest the revealjs source for parts, modify them to our needs, then add it to our existing scrollama dependency. To add to the mix, code highlighting (sans transitions) is

As we wade into this thicket of the revealjs library and the implementation in Quarto, it might be useful to loop in a dev like Christophe to get his advice.

Thoughts?

jimjam-slam commented 1 year ago

Yeah, I think it makes sense to reach out! My implementation of the sticky stack is already based on r-stack, so I see a lot of the parallels there. I'm starting to think (just outlined in https://github.com/quarto-lab/close-read/pull/11#issuecomment-1668704230) that our API might need an adjustment or two anyway, so I think it makes sense to look at our scope broadly and evaluate right now anyway 😄

andrewpbray commented 1 year ago

Hmmm... continuing the line of thinking around the parallels with revealjs...

Similarities between cr and reveal

r-stack is used when to position .fragments on top of one another. The docs describe fragments...

The default fragment style is to start out invisible and fade in. This style can be changed by appending a different class to the fragment.

So the .fragment class is very similar to our .cr-sticky class that's inside a r-stack layout. With our current transition setup, our "fragments", too, start out invisible and fade in. Animation between fragments is organized by building an index of fragments, as we do with cr-sticky objects (this is used, too, for code-highlighting). That's all to say, I wonder how much we'd buy by just appending the fragment class inside our filter.

Differences between cr and reveal

Trigger- or object- based transitions

There are some interesting differences too, though. Unlike revealjs, cr has the choice of specifying the type of transition either on the sticky object / fragment or on the step / trigger. Right now we're aiming at putting it on the trigger. That makes some sense to me since transitions will often effect both the next and the last sticky object (.fade-out-and-fade-in). It would also allow for a more concise version of auto-animate; you'd only need to add it to the trigger, not to both objects that are getting animated between. You could also argue that the syntax is more explicit on the nature of the action: if we're triggering something, what is it? That line of reasoning, though, would suggest triggers of the form {.cr-step from="histogram-plot" to="scatterplot" transition="fade-out-and-fade-in"}. If we're requiring id labels, then, on every object, then we could combine labeling with the flagging of sticky content: setting cr-id: histogram-plot would both enable a targeted transition and flag the object for the sticky stack (looking for cr-id instead of cr: sticky).

If it were on the object, it'd probably make the most sense for there to be the transitions that would effect only that object. E.g. if it were on a plot, we'd add cr-transition-in: fade and cr-transition-out: fade or just cr-transition: fade-in-and-fade-out but where the latter refers to just that plot instead of that plot and one of its neighbors. FWIW, this object-based transition is used in revealjs' code highlighting in #| code-line-numbers: "|6|9", which is understandable given they don't have a trigger that they can append that line-number info to. If we adopted object-based transitions generally, we could use their exact syntax. If we adopted trigger-based transitions generally, we could either use their syntax, with each transition triggered by a vanilla {.cr-step}, or implement it our own trigger-based syntax.

Ok, so here's a pitch: we could a use trigger-based syntax that features multiple actions encoded by the class of the trigger, then object ids and any additional parameters as attributes. Some examples:

A. Crossfading from one image to another {.cr-transition from="img1" to "img2" by="fade-out-and-fade-in"}

B. Auto-animate {.cr-transition from="list1" to="list2" by="auto-animate"} or {.cr-auto-animate from="list1" to="list2"}

C. Code highlighting {.cr-highlight lines="6-8" in="histogram-1"}

D. Poem highlighting {.cr-highlight lines="6-8" in="poem-1"}

E. Math highlighting {.cr-highlight lines="6-8" in="proof-1"}

E. Zooming (a la the nytimes close-read on img or text) {.cr-zoom x=".4" y=".8 in="img1" zoom-level="200%"}

Note that all of these use-cases could be made object-based. For example, E would look like zoom="|.4,.8|.1,.1|" zoom-level="200%" if we wanted to start with the image, then pan to two different zoom spots, then pan back out to the image.

One last consideration: in time we might want to add parameters to the trigger describing how that particular narrative block works. The only one I can really think of is to add a custom offset like {.cr-step offset="0.25"}.

I've gone back and forth about 17 times about whether I like trigger-or object-based transitions =). Right now trigger-based has a slight leg up because it might make it a bit easier to reason about how a cr doc will animate in html when reading the source. In the object-based mode it might get puzzling to scan back and forth between the objects and the triggers and keep count of where things should be at.

Stacked fragments vs arranged fragments

Another interesting difference: if the analogy of sticky objects as fragments, we're currently only using the r-stack layout instead of it being the exception (see #11 convo and revealjs docs). The question would be, then, how we'd want to allow an exception where multiple objects/fragments will appear (after transition) in their nature html location next to other objects. Seems like a post v1 sort of thing to me maybe?

jimjam-slam commented 1 year ago

That's really helpful! Do you have some free time tomorrow (say 8 AM Wed Melbourne/3 PM Tues Berkeley) to talk it out over the phone? I feel like this is an area that would benefit from a chat!

jimjam-slam commented 1 year ago

Okay! I'm having a crack at a refactor in #15 with the following assumptions:

I'll start with the transition effect refactor, and then we can try to implement some of these focus effects!

jimjam-slam commented 1 year ago

I'm quickly realising that this design is a bit less robust to potentially skipping scroll events if you jump around the page too fast. My first pass (commit https://github.com/quarto-lab/close-read/pull/15/commits/3eae88fa6e3341789a6dc8864d77cae6849bcc14 in #15) is to apply a transition effect by reading from the activated step (when you scroll down and an element enters), and to undo that effect if a step goes out when scrolling up (by just applying the transition in reverse).

But this depends on only moving sequentially through the steps: if you skip around, it's going to get stuffed up.

Since we can get a list of all the steps by querying the document, it is possible to reconstruct state if something goes wrong. The questions is whether we need to go all the way back to the star tand reapply everything every time there's a step, or whether we just need to go back to the last transition. It might not matter from a performance perspective, but I'd have to try it and see.

The other approach is to simply reset everything back to being not active on every step, and then decide what to activate. That's probably simpler, but it does mean that:

  1. if you skip to a step without a transition effect (ie. a focus-only step), nothing appears at all. So you have to traverse upward to the most recent step with a transition effect to work out what to turn on. That seems doable.
  2. You can't have progressive fade in effects (eg. "fade in 1, fade in 2, fade in 3, fade them all over" unless you adjust cr-from and cr-to to potentially accept comma separated lists, and then you do them by going (for each step):
    • cr-to=firstelement
    • cr-to=firstelement,secondelement
    • cr-to=firstelement,secondelement,thirdelement
    • cr-from=firstelement,secondelement,thirdelement cr-to=somethingelse

Although it's a bit more verbose for the user, I think this second approach makes more sense in terms of what's feasible to code performatively. Otherwise I think you'd have to reconstruct the entire scrollytelling from the top to the present to determine state. What do you think, @andrewpbray?

jimjam-slam commented 1 year ago

I haven't tested transitioning several elements in or out simultaneously yet with cr-from or cr-to, but it does seem like the equivalent behaviour is now working with attributes as of https://github.com/quarto-lab/close-read/pull/15/commits/b223a98907847b067f78bed38c6d2e3fe5f9c444 in #15!

I need to look more into strategies for when Scrollama doesn't fire events because you're scrolling too fast.