juliangarnier / anime

JavaScript animation engine
https://animejs.com
MIT License
50.42k stars 3.68k forks source link

(PR) Timeline Markers / Labels #810

Closed imgdiegoleal closed 2 years ago

imgdiegoleal commented 2 years ago

Is your feature request related to a problem? Please describe.

I would like to add labelled offsets (aka markers) to anime.timeline(), which would solve the following scenarios:

  1. the cognitive overhead of orchestrating complex animations involving many components,
  2. not being able to ensure animations are added in a particular order, which would merit using absolute offsets - this becomes unwieldy very quickly in client code,
    • one has to rely on using useRef + useEffect + useLayoutEffect in React to build up a timeline, reference elements that will be handled imperatively, and play once all elements are present - all of this happens asynchronously, which again, would require using absolute offsets.

Extra benefits from this PR

  1. create markers based on/relative to other markers.
  2. tweak animation offsets (2nd argument to timeline.add), relative to an existing marker.

Describe the solution you'd like

anime
  .timeline()
  .addMarker({ name: 'myMarker', offset: 2000 })
  .addMarker({ name: 'anotherMarker', offset: '-=1000', referenceMarker: 'myMarker' )
  .add(props, null, 'myMarker')
  .add(props, '+=1000', 'myMarker')
  .add(props, null, 'anotherMarker')

Describe alternatives you've considered

Calculating and using absolute offsets in client code then feeding it into anime. This becomes incredibly unwieldy, resulting in quite a lot of boilerplate being introduced.

Additional context

PR: https://github.com/juliangarnier/anime/pull/809 Working example: https://github.com/diegolealco/anime-test/blob/main/index.jsx

I am happy to move the working example to the examples folder.

imgdiegoleal commented 2 years ago

I am closing this issue.

I decided to setup and publish a fork as I need these changes quickly, plus some other improvements and bug fixes that are on the way.

different55 commented 2 years ago

Not sure I understand the alternative you considered, is it this?

var myMarker = 2000;
var anotherMarker = myMarker-1000;
anime
    .timeline()
    .add(props, myMarker)
    .add(props, myMarker+1000)
    .add(props, anotherMarker);

It doesn't seem any less wieldy to me, and it seems a bit less confusing and a lot more expressive than the offset & referenceMarker syntax. Looking at where similar syntax is used, none of the reasons for it existing in those places (mostly either obnoxious-to-reference stuff or nearly-self-referential stuff) seems to apply to markers, so it'd be nice to keep markers as JS variables rather than completely eating them and losing something in the process. But I think a marker system definitely is a good idea to bind those variables to the timeline they belong to. And it'd help inject more semantics into animations, it's easy to get lost in it all.

Not gonna lie though, I'm not sure how to bind them to the timeline without making them almost as obnoxious to type out. Some scattered thoughts, maybe just keep it the same way animations currently work, applying their offset relative to the most-recently-added thing.

anime.timeline()
    .addMarker({name: "myMarker", offset: 2000})
    .addMarker({name: "anotherMarker", offset: "-=1000"})
    // Could just use the name of a marker as the offset
    .add(props, "myMarker")

But trying to change as little as possible we still lose just about everything that's nice about having accessible variables. Maybe instead we break the chain if a reference to the timeline is needed?

var tl = anime.timeline();
tl
    .addMarker("myMarker", {offset: 2000})
    .addMarker("anotherMarker", {offset: tl.marker("myMarker")-1000})
    // Probably would keep += syntax around, and also keep it relative to the last marker added.
    .addMarker("evenMoreMarker", {offset: "+=500"})
    // Can just reference the marker name straight,
    .add(props, "myMarker")
    // Or use the longer form for complex changes like aiming perfectly in between two markers.
    .add(props, (tl.marker("myMarker") + tl.marker("anotherMarker")) / 2);

Alternatively, maybe we could do offsets completely differently. That long marker expression would pretty weird stapled onto the end of a long list of props, so what about doing away with that argument entirely? Insertion position seems to be a function of the timeline anyway, so we could make that relationship more explicit by splitting the offset off into its own function.

tl
    // Starts after last animation
    .add(props)

    // Starts 500ms after last animation
    // Equivalent to .add(props, "+=500")
    // Needs a better name, maybe something related to the start time, the in-point, a cursor, I'm not sure.
    .timeShift("+=500")
    .add(props)

    // Also works with markers. Starts at 2000ms.
    .timeShift("myMarker")
    .add(props)

    // Or combine them both. Starts at 2500ms:
    .timeShift("myMarker").timeShift("+=500")
    .add(props)

    // Absolute values are still here, and the lengthy syntax could still work for the things that need it.
    .timeShift(500)
    .timeShift((tl.marker("myMarker")+tl.marker("anotherMarker))/2);
different55 commented 2 years ago

Now that I think about it, you could also add markers mid-timeline. For example, adding a marker to help sync multiple animations to one end point. Could get screwy with spring easing but it seems like everything gets screwy with spring easing.

var timeline = anime.timeline();
timeline
    .add(props) // Fancy intro animation
    .addMarker("introEnd") // Offset is whenever the last animation ends
    .add(props)
    .timeShift("introEnd")
    .add(props);
imgdiegoleal commented 2 years ago

Good points all around.

You're also right about it being obnoxious to reference. On the fork I made, labels are held in timeline.labels, you should be able to the following:

// ps I renamed addMarker to label
timeline.label({ name: 'bar', reference: timeline.labels.foo, offset: '-=1000' })
// or 
timeline.label('bar', timeline.labels.foo, '-=1000' )

// then
timeline.add({...}, 'bar')
// or
timeline.add({...}, timeline.labels.bar)

the alternative (as you mentioned) would be to just use constants:

const FOO_OFFSET = 1000
const BAR_OFFSET = FOO_OFFSET + 500
const BAZ_OFFSET = FOO_OFFSET - 50

timeline.add({...}, BAZ_OFFSET)

which is probably better, except:

  1. you need to put these constants in a single place and import them whenever an animation is to be added to a timeline from within a component (react specific and if you have decided that the component is responsible for adding its own animations to the timeline); on the other hand - using the above implementation/feature, if you have access to the instance then you have access to the labels.
  2. I believe there is some value (semantically) for these offsets to be bound to a timeline instance, not to mention that constants can get lost among other variables depending on code cleanliness
  3. you would lose use of anime's ability to compute relative values because all constants would probably end up being absolute offsets, unless you like pain and start mixing absolute and relative offsets in those constants.
  4. the fact the library does not prescribe a way to deal with this, there is a chance people would just not do it and did it in a way that is difficult to manage.
imgdiegoleal commented 2 years ago

I'll sit down to think about it. Good stuff.