Animation library
Overview
All animations generated by the Prysm exporter are controlled through the Prysm animation library. It is responsible for the execution of complex animations, frame scripts, timeline control, etc. The library was created so that the runtime of documents can be as close as possible to the Flash runtime. Therefore, there are very strict rules regarding how and when elements are updated.
The library works with timelines, animation states, and property tracks.
- A property track defines an animation for a certain property, much like CSS keyframes.
- An animation state is a container for multiple property tracks.
- A timeline is a container for animation states and other timelines. It also has a current time and is responsible for updating all of its animation states at any given time.
Animation is then achieved by constantly updating timelines to change the values of animation states.
Initialization
The library attaches an object named animationSystem
to the window.prysm
namespace. The object has an animationData
key that configures the animations of objects.
Associating animation data to DOM nodes is done through their data-prysm-id
attribute. When a node is connected to the DOM, it checks whether there is an entry for its data-prysm-id
in the object. If there is, the element is assigned a prysm instance that contains an Animation State and/or a Timeline depending on its configuration. Elements that don't have a data-prysm-id
are ignored by the library.
window.prysm.animationSystem.animationData = {
"prysm_0": {
timeline: {
duration: 1000,
labels: {
"aa": {
"Layer_2": 0
}
},
playbackRate: -1,
frameCatchUpCount: 5,
isPaused: false
}
},
"prysm_0_0": {
timeline: {
duration: 41.667,
playbackRate: 1,
frameCatchUpCount: 2,
isPaused: true
},
animationState: {
showTime: 0,
hideTime: 1000,
"550x400": [
new window.prysm.ASPropertyTrack(window.prysm.ASPropertyType.LEFT, [
new window.prysm.ASPropertyStop(0,
new window.prysm.ASValueUnit(0, window.prysm.ASUnitType.VH),
false
),
new window.prysm.ASPropertyStop(958.333,
new window.prysm.ASValueUnit(112.5, window.prysm.ASUnitType.VH),
false
)
]),
new window.prysm.ASPropertyTrack(window.prysm.ASPropertyType.TOP, [
new window.prysm.ASPropertyStop(75,
new window.prysm.ASValueUnit(0, window.prysm.ASUnitType.VH),
false
)
]),
new window.prysm.ASPropertyTrack(window.prysm.ASPropertyType.Z_INDEX, [
new window.prysm.ASPropertyStop(0,
1,
false
)
]),
new window.prysm.ASPropertyTrack(window.prysm.ASPropertyType.TRANSFORM_ORIGIN, [
new window.prysm.ASPropertyStop(0,
[
new window.prysm.ASValueUnit(6.125, window.prysm.ASUnitType.VH),
new window.prysm.ASValueUnit(6.125, window.prysm.ASUnitType.VH)
],
false
)
])
]
}
},
"prysm_0_0_0": {
animationState: {
showTime: 0,
hideTime: 41.667,
}
}
};
Updating animations
When the start
method of the library is called it starts updating animations whenever possible. This is done through a request animation frame loop, that executes JavaScript before the screen can be updated.
To have correctly synchronized animations, it is important to have all elements registered before starting the animation system. This is why the animation system starts on engine.whenReady
that in turn waits for the window.load
event.
It is important to note that animation states aren't standalone. Instead, they are animated through timelines.
When updating animations, the system notifies all root timelines that they have to update. They notify all of their animation states that in turn update the properties of their associated DOM nodes. Timelines also notify their nested timelines, hence the entire document is updated.
A timeline spawns and despawns nested timelines and animation states. The play state of a timeline determines whether it will update or not - A paused timeline will not advance its animation states until played.
Note: In the scenario where there is a Movie Clip(timeline B) nested in the timeline(A) and A is stopped while B is playing, the timeline B will continue animating.
As the state of animations is connected to the DOM nodes duplicate nodes created via data binding automatically receive an animation.
Updating animations specifics
The main building blocks of the library are the AnimationSystem
, Timeline
, AnimationState
, and PropertyTrack
classes.
- A
PropertyTrack
is a sequence of animated or static values for a property. The property track defines how values are interpolated and can return the value of the property for any given time. Properties are animated according to the CSS specification. - An
AnimationState
represents an element in a timeline. It has a reference to a DOM node, the show time and the hide time of the node, and animations in the form of property tracks. The animation state is responsible for updating the styles of the DOM node. - A
Timeline
contains animation states, labels, and frame scripts. It is responsible for advancing all elements at the same time, executing frame scripts, and seeking time on timeline control methods. - The
AnimationSystem
contains the top-level timelines and is responsible for updating them when possible. Timelines in turn update their nested timelines, hence the whole document is updated each time that the animation system is updated.
Documents start hidden by default (via display: none
).
When the animation library is started it queues a request animation frame loop, makes the document visible, and spawns all timelines and animation states that have to be visible. Timelines with a positive playback rate are spawned at time 0
, while timelines with a negative playback rate are spawned at time duration - 0.1
.
Each timeline knows its last update time. The time between the previous and the current request animation call is the delta. The animation system executes all frame scripts that are between these two points and reacts accordingly.
If there are no frame scripts, the library just updates all animation states to the new update time which is (lastUpdate + delta) % duration
. When the new time exceeds the timeline duration, the timeline loops.
If there are frame scripts, the system updates the timeline states to the time of the script and then executes it. If there are multiple frame scripts for a frame, they are all executed. When the scripts are executed, the system checks whether the timeline has to seek to a different time, pause or change its playback rate. This forces the following rules.
- Methods are called from top to bottom (animate order).
- The last
self.play()
orself.stop()
call takes precedence. - The last
self.gotoAndPlay()
,self.gotoAndStop()
orself.playFromTo()
call takes precedence. - If a
self.gotoAndPlay()
,self.gotoAndStop()
orself.playFromTo()
is called,self.play()
andself.stop()
calls are ignored.
When a frame script is called one of the following things can happen.
- The timeline has to seek somewhere.
- The timeline has to stop.
- The timeline changes its playback rate.
Timelines seek immediately and that doesn't waste any delta, which can lead to infinite loops e.g. a frame script at frame 1 seeks to frame 5 and at frame 5 a frame script seeks to frame 1. This behavior is the same in Flash.
If a timeline has to stop, the update doesn't continue.
If the timeline changes direction, the remaining time for the update is spent in the opposite direction.
If there is a play from to that ends before the last update or the method to be called, it is iterated and the delta is adjusted.
The actions that are done after a frame script are the following.
- While the timeline has to seek somewhere, seek it.
- This updates the states to the new (seek) time and calls all frame scripts at that time.
- If the timeline is stopped, stop updating.
- If there is a play from to that ends at the time of the frame script, iterate it.
- Internally this performs the same actions, as play from to implies a seek.
- Otherwise, continue.
This means that the precedence of actions is the following.
- Seek -
self.gotoAndStop()
,self.gotoAndPlay()
,self.playFromTo()
. - Stop -
self.stop()
- End play from to
If a frame script seeks the current position of the timeline the frame scripts at that position are not called. This is because they have already been executed and follow the implementation of Flash.
Triggering animations on an event
The abovementioned has an interesting effect when we want to create an animation that is stopped by default and triggered on an event.
To create an animation that is stopped by default you write a self.stop()
frame script on the first frame.
To trigger it you can create a click event on the symbol wrapper that plays the animation from the first frame.
Since frame scripts on the same frame are not executed when the timeline is sought to the same time the animation will start, however, clicking it a second time will execute them and stop the animation again.
To avoid that you can create a dummy first frame that has the self.stop()
frame script and the first state of the animated content. That way the element will be rendered correctly. Since clicking on the element seeks its timeline to the START
label that is on the second frame, the stop script is not executed when the element is clicked again and the animation is retriggered on multiple clicks. What is more, since timelines loop when they end by default the self.stop()
script will be executed when the animation ends. This way the animation is correct when clicked once.
Frames catch up
The speed with which a timeline is updated depends on the number of frame scripts in it. If the delta is big and there are a lot of frame scripts, the update can be quite slow. Therefore, the framesCatchUp
parameter of timelines is implemented. It specifies how many frames will be executed at maximum. Frame scripts and actions that have happened before the framesCatchUp
time are ignored.
When the delta is greater than the framesCatchUp * fps
, the timeline is sought to lastUpdate + delta - framesCatchUp * fps
and the algorithm continues.
Frame scripts specifics
Frame scripts are written through the Actions panel of Adobe Animate. You can write random JavaScript in it and it will be executed.
- The
this
in frame scripts is bound to the movie clip DOM node. - The first parameter of the frame script is a
self
variable that is the timeline object instance. - The timeline object contains labels, seeking methods, etc.
- The movie clip DOM node is an instance of a JavaScript class that can be controlled via a custom class definition.
Custom class definitions
You can provide a custom implementation of a movie clip class through the custom class definition UI. When you write something in the input field of the custom class definition a boilerplate class is generated.
class boilerplate extends window.prysm.MovieClip {
connectedCallback() {
super.connectedCallback();
}
disconnectedCallback() {
super.disconnectedCallback();
}
attributeChangedCallback(name, oldValue, newValue) {
super.attributeChangedCallback(name, oldValue, newValue);
}
}
It is important to note that the class implementation must extend the window.prysm.MovieClip
class and it must call the super
methods from the boilerplate to correctly work with the animation system.
Timeline control methods
- The
play
method resumes playing a timeline. - The
stop
method stops playing a timeline. - The
gotoAndStop
method seeks the timeline to a different frame, executes all frame scripts in that frame, and stops the timeline. - The
gotoAndPlay
method seeks the timeline to a different frame, executes all frame scripts in that frame, and resumes playing the timeline. - The
playFromTo
method seeks the timeline to a different frame and remembers a frame where the timeline has to stop. - The
playFromTo(start, end)
,gotoAndStop(frame)
,gotoAndStart(frame)
methods accept a number, treated as a frame index or an object. The object must contain aname
key that specifies a label name and can contain alayerName
key that specifies the layer of the label in the source document. - The
playFromTo
method has an iteration count parameter.-1
is infinite,0
is an error. When the iteration count is>1
the timeline will return to the start time when the end time is reached. - The
playFromTo
method returns a Promise that resolves when the to time is reachediterationCount
times. If theplayFromTo
is cleared the Promise remains pending. - All timeline control methods clear the previous
playFromTo
. - The
playbackRate
property of timelines is used to change its direction. TheplaybackRate
can be a float number. Greater absolute values force the timeline to update faster e.g aplaybackRate
of 2 will make a timeline long 1000ms to be iterated twice for 1000ms.
Freezing states and timelines
Sometimes you want to hide an element or a subtree of elements from the screen. Using the hideDomNode
method will not stop the animation library from showing it again and advancing its nested timelines. To achieve the desired effect you have to freeze
the element first. Frozen animation states are not changed by parent timelines. Therefore, the following code will stop the element from being animated and hide it.
const element = document.querySelector(".myElement");
element.prysmInstance.animationState.freeze();
element.style.display = "none";
If the element is a MovieClip its prysm instance will have both an animation state and a timeline. When you freeze its state the animation of the movie clip, defined in the timeline where it is instantiated, will not be advanced. That implies that that movie clip element won't be hidden/shown by its parent timeline and its visual appearance won't be updated. However, its timeline will still be updated and its nested animations will advance.
To hide a subtree of elements, you can recursively freeze all nested states in the timeline and stop all timelines, but that is slow. For that, we implemented a freeze
method on timelines as well. When a timeline is frozen it will be ignored by its parent timeline, hence all nested animations will not advance.
The following code freezes a movie clip instance and hides it. This stops the animation system from advancing it and provides a performance benefit for hidden elements.
const element = document.querySelector(".myElement");
element.prysmInstance.animationState.freeze();
element.prysmInstance.timeline.freeze();
element.style.display = "none";
Note: Frozen elements will still rescale when the screen is resized to correctly support responsive UI.
It is important to note that freezing takes precedence higher in the DOM tree. So if you freeze a timeline it will freeze everything nested within it. Freezing/Unfreezing an animation state nested inside it won't have an effect while the parent is frozen. When the parent is unfrozen, elements that are explicitly frozen within it will remain frozen.
When a timeline or animation state is unfrozen it will be synchronized with the parent timeline and will continue from where it left off.
Initialization specifics
The window.prysm.animationSystem.animationData
collection is a map from data-prysm-id
to animation data objects.
window.prysm.animationSystem.animationData = {
"prysm_0": {},
"prysm_0_0": {},
"prysm_0_0_0": {},
};
An animation data object can contain a timeline
and an animationState
key.
window.prysm.animationSystem.animationData = {
"prysm_0": {
timeline: {},
animationState: {}
}
};
The timeline
entry can contain the following keys.
duration
- the duration of the timeline MANDATORYframeCatchUpCount
- the frame catch up countlabels
- a labels map used by timeline control methodsisPaused
- whether the timeline starts pausedplaybackRate
- the initial playback rate of the timeline
window.prysm.animationSystem.animationData = {
"prysm_0": {
timeline: {
duration: 1000,
labels: {
"aa": {
"Layer_2": 0
}
},
playbackRate: -1,
frameCatchUpCount: 5,
isPaused: false
}
}
};
The animationState
contains the following keys.
showTime
is the time when the element is shown.hideTime
is the time when the element is hidden.
The other keys in the animationState
are treated as resolution keys. The resolution keys contain the animation for a certain screen resolution.
window.prysm.animationSystem.animationData = {
"prysm_0_0": {
animationState: {
showTime: 0,
hideTime: 1000,
"550x400": {}
}
}
};
A screen resolution key can contain the following keys.
top
left
right
bottom
width
height
transformOrigin
transform
filter
opacity
clipPath
zIndex
mixBlendMode
maskImage
maskPosition
maskSize
Each of the abovementioned keys defines a property track.
window.prysm.animationSystem.animationData = {
"prysm_0_0": {
animationState: {
showTime: 0,
hideTime: 1000,
"550x400": [
new window.prysm.ASPropertyTrack(window.prysm.ASPropertyType.LEFT, [
new window.prysm.ASPropertyStop(0,
new window.prysm.ASValueUnit(0, window.prysm.ASUnitType.VH),
false
),
new window.prysm.ASPropertyStop(41.667,
new window.prysm.ASValueUnit(4.887, window.prysm.ASUnitType.VH),
true
),
new window.prysm.ASPropertyStop(83.333,
new window.prysm.ASValueUnit(9.775, window.prysm.ASUnitType.VH),
true
),
new window.prysm.ASPropertyStop(125,
new window.prysm.ASValueUnit(14.662, window.prysm.ASUnitType.VH),
true
),
//...
new window.prysm.ASPropertyStop(958.333,
new window.prysm.ASValueUnit(112.5, window.prysm.ASUnitType.VH),
true
),
])
]
}
}
};
The first ASPropertyStop
argument is the time
, the second is the value and the third specifies whether the system will interpolate values between the previous one and the current one.
Nested animations
Nested animations are updated in the following scenario.
- Each time that a timeline is sought, its nested timelines are spawned/despawned based on the new time.
- When a frame script is called, nested timelines are ticked with the passed delta from the last update to the frame script time.
- When a timeline loops, nested timelines are ticked.
- When the timeline is updated to the last time of the tick, nested timelines are updated.
Timeline control in the UI
For each timeline, you can change its playbackRate
and framesCatchUp
through the UI.
These options are available for the scene or symbol.
You can modify them from the Scene
tab, Scene timeline control
accordion, and from the Symbol
tab, Basic properties
accordion.
Changing them for the symbol will affect the symbol's timeline animations.
Changing them for the scene will affect scenes' timeline animations.
Callbacks
The animation system runs a loop that advances each frame. It exposes callbacks for crucial events, so that logic can be executed based on them. For example, the WILL_START
callback is executed before the system starts working.
Callbacks are executed each time that the event happens and have to be explicitly removed. You can set a callback that executes only once via the following code.
const callback = (foo) => {
// do stuff
window.prysm.animationSystem.removeCallback(callbackType, callback);
};
window.prysm.animationSystem.addCallback(callbackType, callback);
Changes in 2021.4.0
Initialization optimization
Firstly, the whenRegistered
method is removed in 2020.4.0 as all connectedCallbacks
(when nodes are registered in the animation system) are called before the Window: load
event. This makes it possible to skip creating all promises in the library and still properly synchronize initialization. Since the engine.whenReady
event waits for the Window: load
event, the animation system is started after engine.whenReady
. After the load event, connectedCallbacks
are called immediately after a node is appended to the DOM, so any JavaScript that requires the element to be registered can be executed immediately after the element is added.
Furthermore, there was a lot of duplication in the initialization state of the animation system. We mitigated that, by making it possible to refer to a different animation state. This way there won't be many copies of the same state when the same symbol is used multiple times.
"prysm_0_1_0": "prysm_0_0_0",
"prysm_0_1_0_0": "prysm_0_0_0_0",
What is more, the animation system accepted string values that required parsers to transform them into internal types that can be animated. We changed the input of the library to the internal types directly. This skips the parsing steps and makes loading times faster. Since many documents are already created with the previous version of the library we created a CLPrysmAnimationSystemBackwardsCompatability.js
script that provides backward compatibility. The goal is to deprecate the old input format, but in the meantime, you can just include the script as an external file when needed.
Last but not least, the animation system works with requestAnimationFrame(timestamp)
. To know how much time has passed since the last update the timestamp of that method is used. In the previous version, we queued a requestAnimationFrame
on start and the first update was an initial setup step. The next step was the first update, so there was 1 frame delay. With the current version, we added a requestAnimationFrame
that keeps track of timestamps while the animation system is idle. This way, when the animation system is started we immediately execute the initial step and the next update already has a delta and there isn't a 1 frame delay.
Data binding
There wasn't a well-defined place where data binding should have been created/synchronized in the previous version to correctly work with the animation system. We exposed the addCallback
method to the animation system and the window.prysm.ASCallbackType
options for it. The window.prysm.ASCallbackType.WILL_START
is called before the animation system starts. This is the place, where data binding code should be executed. When that method is invoked, all elements will be registered in the animation system, so structural data binding will be synchronized with the animation system. If the document is exported through the Exporter, engine.whenReady
is guaranteed to have been called.
window.prysm.animationSystem.addCallback(window.prysm.ASCallbackType.WILL_START, function() {
engine.createJSModel("model", myModel);
engine.synchronizeModels();
});
You can remove the callbacks through the removeCallback
method. If you don't remove them they will be called every time before the animation system starts.
Error handling
When an element fails to register in the animation system it will call all callbacks associated with window.prysm.ASCallbackType.REGISTRATION_ERROR
, so you can add your error handling code to that callback.
Changes in 2022.3.0
Note: Using 2022.3.0 requires a specific version of Coherent Prysm. Check the compatibility chart for reference.
The animation library received a performance optimization overhaul. It makes the library ~3.5 times faster for certain UIs. To achieve this the files generated from the Exporter are changed, hence to take advantage of the new version you have to regenerate all of your FLA files.
We also renamed the tick
methods to advance
to match the rest of the company's products. What is more, we made the methods a bit more descriptive Timeline.advanceTimeline
instead of Timeline.tick
to make it easier to inspect the library in the Performance
tab of the Inspector.
Firstly, all properties that the library animates are set through custom Cohtml setters that don't require strings. This reduces the generation of strings in JavaScript just for the sake of parsing them in C++ and yields a performance gain.
- The
left
,right
,top
,bottom
,width
, andheight
properties are set through unit-specific setters likeleftVH
,topPERCENT
, etc. - The
opacity
andzIndex
setters accept units. - The
transformOrigin
,transform
,clipPath
,filter
,maskPosition
, andmaskSize
properties are set throughFloat32Array
setters. The array contains the serialized property value. - The
mixBlendMode
andmaskImage
setters still use strings.
The abovementioned changes greatly reduce the generation of temporary strings that in turn reduce the garbage collection procs and that leads to fewer spikes and better performance. To reduce the GC procs even further we implemented variable caches and reuse the same memory to interpolate property values.
Secondly, we simplified the accepted values for the transform
, filter
, and clipPath
properties to suit the generated content from Prysm rather than being fully web compliant. This change simplified algorithms that covered scenarios that weren't ever used and yields a performance gain.
- The
transform
property accepts the same number of matchingMATRIX
andTRANSLATE
functions. - The
filter
property accepts a different number of matchingCOH_AXIS_BLUR
,DROP_SHADOW
, andCOH_COLOR_MATRIX
functions. - e.g
COH_AXIS_BLUR
DROP_SHADOW
->COH_AXIS_BLUR
is accepted, butCOH_AXIS_BLUR
DROP_SHADOW
->DROP_SHADOW
isn't asCOH_AXIS_BLUR
andDROP_SHADOW
don't match - The
clipPath
property accepts paths with the same number of matchingZ
,M
,L
, andQ
segments.
Thirdly, we stopped the library from setting the same style value to the same element over and over again.
- We realized that setting a style to an element is an expensive operation but the library sets values on each advance. For example, when animating a movie clip transform we set both the transform and the transform-origin of the clip in each frame. This is rather inefficient, so we made the library cache the last value set and set a new value only if it is different from the previously set one.
Note: This implies that if the style value is changed from outside the animation library, for example from user code, the animation library won't change the value.
Changes in 2023.1.1
- Added a
window.prysm.ASCallbackType.ADVANCED
callback that is called every time the animation system has finished advancing. It can be used as a place to synchronize data binding models after lazy components are shown for the first time e.g. you show a component that uses data binding on a key event for the first time. - Added a
Export lazy animation states
option. More information can be found here. - Stopped the
framesCatchUp
option from having an effect in the runtime as it causes state loss and unexpected looping animations.
Changes in 2023.4.0
- Moved the initialization of the animation library to the
window.load
event instead ofengine.whenReady
as it now waits forDOMContentLoaded
which is too early for initialization. - Changed the prysm scene element to be the
body
to be able to support surface partitioning.