koenbok / Framer

Framer - Design Everything
https://framer.com
MIT License
5.83k stars 477 forks source link

Proposal: New Animation & States API #378

Open nvh opened 8 years ago

nvh commented 8 years ago

We're changing the way animations are defined and how they interact with states. There are a couple of reasons for this:

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:

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:

jordandobson commented 8 years ago

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 notifications@github.com 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 .

mrrocks commented 8 years ago

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"
nvh commented 8 years ago

What about using animateTo and switchTo for both, states and properties?

I like the Idea, but how would animateToNextState fit into it?

nvh commented 8 years ago

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

mrrocks commented 8 years ago

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"
mrrocks commented 8 years ago

Quick question, do we really need the key "options"?

layer.animateTo
    x: 10
    options:
        curve: "ease"

#VS

layer.animateTo
    x: 10
    curve: "ease"
nvh commented 8 years ago

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

tehfailsafe commented 8 years ago

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
jordandobson commented 8 years ago

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.

nvh commented 8 years ago

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?

nvh commented 8 years ago

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.

koenbok commented 8 years ago

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%

IanBellomy commented 8 years ago

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
nvh commented 8 years ago

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.

nvh commented 8 years ago

I've updated the proposal today, and changed a few things:

Would love some extra feedback on this!

IanBellomy commented 8 years ago

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)?

nvh commented 8 years ago

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.

bensleveritt commented 7 years ago

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.

IanBellomy commented 7 years ago

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:

screen shot 2016-07-19 at 2 11 48 pm

We could include a third dimension for pressed-ness:

screen shot 2016-07-19 at 2 15 54 pm

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:

  1. There is more logic to write.
  2. It's more difficult to isolate state related properties from each other.
  3. 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, and selectedInactiveHidden. And if these states modify a new property like borderRadius, we'll need to add the borderRadius 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()
jordandobson commented 7 years ago

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.

nvh commented 7 years ago

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"]
koenbok commented 7 years ago

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.

IanBellomy commented 7 years ago

@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.

nvh commented 7 years ago

I've moved the Deterministic states discussion to a separate issue

uxdiogenes commented 7 years ago

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?

nvh commented 7 years ago

I've updated the proposal after some internal discussions with the team:

nvh commented 7 years ago

@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.

uxdiogenes commented 7 years ago

I see your point! States are usually set up all at once up front.

koenbok commented 7 years ago

Reminder to self: after playing for a few days with this, layer.animateToNextState() starts to feel a bit out of place to me.

koenbok commented 7 years ago

@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
nvh commented 7 years ago

I've implemented layer.options, but think that is too broad and want to change it to layer.animationOptions. We don't have any properties on the states object anymore

koenbok commented 7 years ago

I see that, but we're trying to move away from animationOptions to options in many places, plus it's a magic property on states and in the .animate function options now, so I'd prefer layer.states.options.

nvh commented 7 years ago

This has just been merged into master. The changes are fully backwards compatible, so every project should keep working if you choose 'Update Framer Library'