Framer
Framer copied to clipboard
Proposal: New Animation & States API
We're changing the way animations are defined and how they interact with states. There are a couple of reasons for this:
- There are two ways to animate a layer now: With
layer.states.switch
andlayer.animate
, we would like to combine them - The current way of animating a layer directly needs the
properties:
keyword, which is confusing for beginners - It's not obvious how to specify animation options for state switches
This proposal tries to provide a solution for the above problems. please provide us with your feedback in the comments!
Basic animation of a layer
Just change a property
layer = new Layer
## Before
layer.animate
properties:
x: 100
## After
layer.animate
x: 100
Change the animation timing
## Before
layer.animate
properties:
x: 100
time: 0.5
## After
layer.animate
x: 100
options:
time: 0.5
Change the animation curve
## Before
layer.animate
properties:
x: 100
curve: "spring(250, 50, 0)"
## After
layer.animate
x: 100
options:
curve: "spring(250, 50, 0)"
States
Add a single state
## Before
layer.states.add
stateA:
x: 100
## After
layer.states.stateA =
x: 100
Define multiple states
## Before
layer.states.add
stateB:
x: 200
stateC:
x: 400
## After
layer.states =
stateB:
x: 200
stateC:
x: 400
Notice the subtle difference between calling a function and setting a property. This means that where previously it was possible to add multiple states multiple times, in the new API we will override the existing states when calling layer.states = ...
again. However, this is really unlikely and one could still achieve this by doing:
layer.states =
stateA:
x: 100
stateB:
x: 200
layer.states = _.extend layer.states,
stateC:
x: 300
stateD:
x: 400
Animate to state
## Before
layer.states.switch "stateA"
## After
layer.animate "stateA"
Add animation options to a state change
The options for an animation can be provided in a state as well
## Before
layer.states.add
stateE:
x: 200
layer.states.switch "stateE",
curve: "ease-in"
## After
layer.states.stateE =
x: 200
options:
curve: "ease-in"
layer.animate "stateE"
Instantly switch to a state
Switching instantly will become an option of the animation
## Before
layer.states.switchInstant "stateB"
## After
layer.switchInstant "stateB"
## Which will be a shorthand for:
layer.animate "stateB",
instant: true
This means it can also be defined directly in a state itself:
layer.states =
stateA:
x: 100
options:
instant: true
Move to the next state
## Before
layer.states.next()
layer.states.next("stateB","stateC")
## After
layer.animateToNextState()
layer.animateToNextState ["stateB","stateC"] # Preferred
layer.animateToNextState "stateB","stateC" # Also valid
layer.animateToNextState ["stateB","stateC"],
time: 0.5
Notice how we use an array of states names here, to support animation options as second argument
Special states
There are three special states that will be set automatically and can't be overridden:
-
layer.states.initial
- The state the layer had upon creation. This will contain the properties that are provided to the constructor, not ones that were set in a layer stage (i.e. withlayer.height = 100
) -
layer.states.previous
- The previous state the layer was in -
layer.states.current
- The current state the layer is in
These states contain the actual values and not (as is the case with layer.states.current
now) the state string. The name of the previous an current states will still be available through layer.states.previousName
and layer.states.currentName
.
Notice the absence of layer.states.next
, this functionality will be provided by layer.animateToNextState()
as described above.
Listing all the states
We will add a new property layer.stateNames
that lists all the names of states currently defined on a layer. This list will contain layer.states.initial
, but won't contain previous
and current
.
layer.states =
left:
x: Align.left
right:
x: Align.right
layer.animateToNextState()
print layer.states.currentName # "left"
print layer.stateNames # ["initial", "left", "right"]
~~Determinism~~
Out of this proposal, moved to #384 ~~Is the previous state API the order state changes occurred influenced the resulting position of the layer. Consider the following example:~~
background = new BackgroundLayer
layer = new Layer
layer.states.add
right:
x: Align.right
bottom:
y: Align.bottom
left:
x: Align.left
top:
y: Align.top
background.onClick ->
layer.states.next()
~~(Also available here: http://share.framerjs.com/ff6vwjpn0wet/)~~
~~Because not all states define all properties, you can end up with some in-between state (try clicking quickly in the example). Therefor we would like to make states deterministic. That is: every state defines every property. If you omit a property during the definition of the state, we use the initial state as the default value for that property.~~
~~Examples of this can be found here: http://share.framerjs.com/kthdx2kmp00m/
The red layer shows deterministic states, but all derived from the initial states, this layer will always end up in one of the corners, but never reach the bottom right corner
The blue layer has deterministic states, and defines the x
and y
for every state, resulting in the intended behaviour.~~
Details
The formal API of layer.animate
will be:
layer.animate(properties, options)
However, by adding the options key to the properties object, you can take a shortcut that looks nicer in coffeescript:
layer.animate
x: 100
options:
curve: "spring"
delay: 1
The same will work for states, so animation options can be specified directly in the state:
layer.states.stateD =
y: 100
options:
delay: 1
time: 0.25
layer.animate "stateD"
## But could also be used like this:
layer.animate layer.states.stateD
## When options are provided, they override the options of the state
layer.animate "stateD",
delay: 0 # Delay will be 0 and not 1
It used to be possible to set layer.states.animationOptions
to change the animation option of every state change. This will be broadened to set the animationOptions for all animations on a layer:
## Before
layer.states.animationOptions =
curve: "spring"
## After
layer.animationOptions =
curve: "spring"
Pitfalls
Common pitfalls we expect with the new approach, so we should have excellent documentation and (if possible) error messages for them:
- Setting one of the preserved state names:
initial
,previous
, orcurrent
- Checking
layer.states.current
expecting a string
This looks great.
How are CSS changes handled when state is set to initial?
Are they all reset and clobbered any user added styles or are they preserved? On Thu, Jul 14, 2016 at 4:12 AM Niels van Hoorn [email protected] wrote:
We're changing the way animations are defined and how they interact with states. There are a couple of reasons for this:
- There are two ways to animate a layer now: With layer.states.switch and layer.animate , we would like to combine them
- The current way of animating a layer directly needs the properties: keyword, which is confusing for beginners
- It's not obvious how to specify animation options for state switches
This proposal tries to provide a solution for the above problems. please provide us with your feedback in the comments! Basic animation of a layer Just change a property
layer = new Layer
Before
layer.animate properties: x: 100
After
layer.animateTo x: 100
Change the animation timing
Before
layer.animate properties: x: 100 time: 0.5
After
layer.animateTo x: 100 options: time: 0.5
Change the animation curve
Before
layer.animate properties: x: 100 curve: "spring(250, 50, 0)"
After
layer.animateTo x: 100 options: curve: spring(250, 50, 0) # Notice that the quotes are gone
States Add a single state
Before
layer.states.add stateA: x: 100
After
layer.states.stateA = x: 100
Add multiple states
Before
layer.states.add stateB: x: 200 stateC: x: 400
After
layer.states = stateB: x: 200 stateC: x: 400
Animate to state
Before
layer.states.switch "stateA"
After
layer.animateToState "stateA"
Add animation options to a state change
Before
layer.states.add stateE: x: 200
layer.states.switch "stateE", curve: "ease-in"
After
layer.states.stateE = x: 200 options: curve: "ease-in"
layer.states.animateTo "stateE"
Instantly switch to a state
Before
layer.states.switchInstant "stateB"
After
layer.switchToState "stateB"
Move to the next state
Before
layer.states.next() layer.states.next("stateB","stateC")
After
layer.animateToNextState() layer.animateToNextState("stateB","stateC")
Special states
There are three special states will be set automatically and can't be overridden:
- layer.states.initial - The state the layer had upon creation. This will contain the properties that are provided to the constructor, not ones that were set in a layer stage (i.e. with layer.height = 100)
- layer.states.previous - The previous state the layer was in
- layer.states.current - The current state the layer is in
These states contain the actual values and not (as is the case with layer.states.current now) the state string. The name of the previous an current states will still be available through layer.states.previousName and layer.states.currentName Details
The formal API of layer.animateTo and layer.animateToState will be:
layer.animateTo(properties,options)
However, by adding the options key to the properties object, you can take a shortcut that looks nicer in coffeescript:
layer.animateTo x: 100 options: curve: "spring" delay: 1
The same will work for animateToState, so animation options can be specified directly in the state:
layer.states.stateD = y: 100 options: delay: 1 time: 0.25
layer.animateToState "stateD"
But could also be used like this:
layer.animateTo layer.states.stateD
When options are provided, they override the options of the state
layer.animateToState "stateD", delay: 0 # Delay will be 0 and not 1
Pitfalls
Common pitfalls we expect with the new approach, so we should have excellent documentation and (if possible) error messages for them:
- Setting one of the preserved state names: initial, previous, or current
- Using animate instead of animateTo or animateToState
- Checking layer.states.current expecting a string
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/koenbok/Framer/issues/378, or mute the thread https://github.com/notifications/unsubscribe/AACbYhTwrdhBGpkPNDEsX4wAZ998_1yDks5qVf1jgaJpZM4JMOxj .
What about using animateTo and switchTo for both, states and properties?
layer.animateTo x: 10
layer.animateTo "state"
And maybe even switchTo
layer.switchTo x: 10
layer.switchTo "state"
What about using animateTo and switchTo for both, states and properties?
I like the Idea, but how would animateToNextState
fit into it?
How are CSS changes handled when state is set to initial?
Initial should be the the state that the layer is in after the constructor, so setting a property after the constructor won't change the initial state, and setting the state to initial should bring it back to the state after it was just constructed
I like the Idea, but how would animateToNextState fit into it?
What about something like this?
layer.animateTo layer.states.next
layer.animateTo layer.states.previous
#or reserving the strings "next" and "previous"
layer.animateTo "next"
layer.animateTo "previous"
Quick question, do we really need the key "options"?
layer.animateTo
x: 10
options:
curve: "ease"
#VS
layer.animateTo
x: 10
curve: "ease"
Quick question, do we really need the key "options"?
Yes we do. Not only can the options potentially clash with properties on the layer, we also need a hook to support Auto-Code for animation options in the future
What about an animiateFrom? Something I abused thoroughly in gsap. It's not quite as needed in framer since we have states, but it could be useful for setting up animate-in transitions when importing from sketch.
layer.originalX = layer.x
layer.x = 1000
layer.animateTo
x: this.originalX
#vs
layer.animateFrom
x: 1000
Initial should be the the state that the layer is in after the constructor, so setting a property after the constructor won't change the initial state, and setting the state to initial should bring it back to the state after it was just constructed
So is it possible to return to the initial or default state without blowing away my font settings I've set in the style property?
That's horrible for people that need to add in support for things you don't support that you just blow all CSS custom settings away.
So is it possible to return to the initial or default state without blowing away my font settings I've set in the style property?
That's horrible for people that need to add in support for things you don't support that you just blow all CSS custom settings away.
Ah, now I understand what you meant. I think we should only save and set properties Framer knows about, and leave custom set styles alone. Do you agree @koenbok?
What about an animiateFrom? Something I abused thoroughly in gsap. It's not quite as needed in framer since we have states, but it could be useful for setting up animate-in transitions when importing from sketch.
I think we'll rather focus on people using states then put another hammer in their toolbox and have them looking for nails. Especially since we want Auto-Code to generate one way to do things.
Ah, now I understand what you meant. I think we should only save and set properties Framer knows about, and leave custom set styles alone. Do you agree @koenbok?
100%
All of this sounds pretty good.
Will changing state property values after the fact impact the state changes when they happen? So we can do things like override the initial values created during layer construction:
assets = Framer.Importer.Load("...")
assets.myLayer.states.initial.opacity = 0
Will changing state property values after the fact impact the state changes when they happen?
For added states: definitely, for the initial state my first reaction is: no, because it is fixed, but have to think about it some more.
I've updated the proposal today, and changed a few things:
- Using
animateTo
for properties and state switches - Changed
animateToNextState
toanimateToNext
- Changed how we switch instantly to a state
- Clarified the difference between
layer.states.add
andlayer.states = ...
- Add a section about listing of state names
- Add a section about determinism
Would love some extra feedback on this!
I'm curious, from the team's point of view and based on their experiences working with designers/developers, to what ends is the state machine most commonly used for (or intended for)?
I'm curious, from the team's point of view and based on their experiences working with designers/developers, to what ends is the state machine most commonly used for (or intended for)?
Mostly for defining, well, states of a prototype. If a prototype has a certain navigation, usually the positions the layers can be in are defined as states, so they can be easily switched to.
This seems like a positive direction. I've been (probably erroneously) using States to manage simple animations, and getting annoyed when the HTML changes, or the layer returns to it's original location.
I see now I should have been using Animate instead, but since the States aren't quite deterministic, I assumed any un-declared properties wouldn't revert to default.
I think the behavior shown in the non-deterministic states example is preferable and that the meaning of the code is entirely intelligible. The state 'bottom', for example, is a state where the block is on the bottom edge. If the desired result is for the block to be in the bottom-right corner, the state data should include both an x
and y
value. Making the state system entirely deterministic may be too constraining and introduce other complexities.
Framer's state system is one-dimensional, but many interactions (or state spaces) are easier to describe using two or more dimensions. For example, a simple toggle button could have selected-ness states and active-ness states:

We could include a third dimension for pressed-ness:

If states aren't completely deterministic, we could create this button by using a mix of states and custom triggered animations:
btn = new Layer
backgroundColor: "clear"
borderColor: "white"
borderWidth: 10
btn.states.add
selected:
backgroundColor: "white"
borderWidth: 0
btn.onMouseDown ->
@states.next()
btn.onMouseOver ->
@animate
properties:
scale:1.2
btn.onMouseOut ->
@animate
properties:
scale:1
btn.deactivate = ->
@ignoreEvents = true
@animate
properties:
opacity:0.5
btn.activate = ->
@ignoreEvents = false
@animate
properties:
opacity:1
If states are entirely deterministic (as described above) we'll need to flatten the state space, make property value combinations more explicit, and handle the logic manually:
btn = new Layer
backgroundColor: "clear"
borderColor: "white"
borderWidth: 10
btn.states.add
selected:
backgroundColor: "white"
scale:1
selectedInactive:
backgroundColor: "white"
opacity: 0.5
scale:1
defaultInactive:
backgroundColor: "clear"
opacity: 0.5
scale:1
selectedPressed:
backgroundColor: "white"
opacity: 1
scale:1.2
defaultPressed:
backgroundColor: "white"
opacity: 1
scale:1.2
btn.onMouseDown ->
if @states.current == "default"
@states.switch "defaultPressed"
else if @states.current == "selected"
@states.switch "selectedPressed"
btn.onMouseUp ->
print @states.current
if @states.current == "selectedPressed"
@states.switch "default"
else if @states.current == "defaultPressed"
@states.switch "selected"
btn.disable = ->
@ignoreEvents = true
if @states.current == "selected"
@states.switch "selectedInactive"
else if @states.current == "default"
@states.switch "defaultInactive"
btn.enable = ->
@ignoreEvents = false
if @states.current == "selectedInactive"
@states.switch "selected"
else if @states.current == "defaultInactive"
@states.switch "default"
Flatting things out like this strikes me as less preferable because:
- There is more logic to write.
- It's more difficult to isolate state related properties from each other.
- It's more work to add more dimensions. For example, if we want to add a hidden vs. revealed states, we'll need to add states and logic for
defaultHidden
,defaultInactiveHidden
,selectedHidden
, andselectedInactiveHidden
. And if these states modify a new property likeborderRadius
, we'll need to add theborderRadius
values for other states that already exist.
Of course, we could 'fake' multi-dimensional states using nested structures:
container = new Layer
backgroundColor: "clear"
borderColor: "white"
width:100
height:100
borderWidth: 10
asset = new Layer
width: 75
height: 75
midX:40
midY:40
backgroundColor: "clear"
asset.parent = container
asset.states.add
selected:
backgroundColor: "white"
container.states.add
inactive:
opacity: 0.5
container.onMouseDown ->
asset.states.next()
container.disable = ->
@ignoreEvents = true
@states.switch "inactive"
container.enable = ->
@ignoreEvents = false
@states.switch "default"
This approach is similar to a Flash-era technique where complex widgets and behaviors were created by nested MovieClips / animations.
A catch of this approach is that it requires pre-planning around the structures you'll need. (In my experience, this is a little tricky for novices/students.)
It's also a little messy if you work with imported structures and would like to make the code more generalizable...
imported.btnContainer.onMouseDown ->
@getChildrenWithName("asset").states.next()
If a nested approach to building things is desirable, I wonder if it'd be worth looking at states with child state data. e.g.
container = new Layer
asset = new Layer
asset.parent = container
container.asset = asset
container.states.add
hovered:
asset:
scale:1
This is getting to be similar to child-selectors in CSS of course. For better or worse.
It could lead to a really handy way to slop together behaviors if the importer included an (admittedly risky) option to create properties on layers that matched their child names.
psd = Framer.Importer(...)
psd.btn.states.add
hovered:
childLayer:
roation:45
psd.btn.onMouseOver -> @states.next()
psd.btn.onMouseOut -> @states.next()
It could be nice to take and existing state and clone it while adjusting a few properties. That way you can have bottom state and also have bottom right you could clone bottom by providing an existing state name and then followed by an object with new or adjusted properties.
Thanks for the great writeup @IanBellomy! I really dislike the idea of exploding state spaces because of flattening. I'm now wondering if it would be possible to have deterministic states, but have something to switch to multiple states at once, where if you switch to [bottom
,left
] all the properties defined in left
(probably just x
) would override the properties in bottom
(where x
would be inherited from the default state). It would add a bunch of complexity though, so I'm not really sure it's an improvement.
An easy way to combine or clone states, as @jordandobson suggests, would be really simple in the new model, because states are basically just regular objects with properties, so you could do something like:
layer.states.bottomLeft = _.merge(layer.states.bottom, layer.states.left)
And we could see if we could provide shortcuts for this, maybe something like:
layer.states.bottomLeft = ["bottom","left"]
This some great feedback @IanBellomy. I think we might decouple deterministic states from this update and rethink them a bit.
To give some context on what we were also planning with them too: we are thinking about how to capture/replay your project state. If we ever want to build features like leaving comments or feedback we need some parts of Framer to be more deterministic.
@nvh , I agree with your thoughts on assigning multiple states. It sounds interesting, but potentially complex—like the beginning of a cascading style system...
@koenbok , thanks for the peek behind the curtain. That makes sense.
I've moved the Deterministic states discussion to a separate issue
This seems pretty solid. One thing I think would be handy would be the ability to provide an animation options object to layer.animateTo "state"
as another argument for one-off overrides of the animation to get to a state.
Also, calling using _extend
to avoid overwriting the states
property seems pretty advanced for a beginner, and a little unwieldy. What if you kept states.add
as a shortcut to ...states = _.extend layer.states, someObj
?
I've updated the proposal after some internal discussions with the team:
-
animateTo
has been changed back toanimate
- added
switchInstant
as a function on layer to be a shorthand forlayer.animate "stateA", instant: true
- renamed
animateToNext
toanimateToNextState
@uxdiogenes Thanks for the feedback!
Your first suggestion is already part of the proposal, but because it's not the preferred way, it's all the way on the bottom under 'Details'
I see adding multiple states multiple times as a very thin use-case. What would be a good example? We're still debating the best way to handle this, so suggestions how to improve are more than welcome!
As the layer.states
will be as close to a plain javascript object as possible, I dislike adding functions to it.
I see your point! States are usually set up all at once up front.
Reminder to self: after playing for a few days with this, layer.animateToNextState()
starts to feel a bit out of place to me.
@nvh I thought we decided to implement the old animationOptions
like this, but I can't find it.
Old
layer = new Layer
layer.states.animationOptions =
time: 10
New
layer = new Layer
layer.states.options =
time: 10