Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animation API refactor #420

Merged
merged 21 commits into from
Sep 21, 2016
Merged

Animation API refactor #420

merged 21 commits into from
Sep 21, 2016

Conversation

koenbok
Copy link
Owner

@koenbok koenbok commented Sep 17, 2016

Update: this now lives here: https://github.com/koenbok/Framer/wiki/New-Animation-API

Continuing from #378

API Overview

This is a current relevant overview of how the API will be from now on. For more information about what changed, see below.

layer.animate(properties: object | state: string, options={})

Returns: Animation object
  • properties or state: either an object with property values you want to animate to, or an existing state name.
  • options: options for the animation (see animationOptions).

This creates a new animation for a layer based on a state or new property values and starts it immediately. If the properties have an options key, it is used as options for the animation.

If there is currently an animation on the layer, it gets stopped if it is animating the same properties as the new animation. Simply put, no two animations can be ran on width at the same time, but you can have two separate animations, one running on width and the other on height.

Preferred Examples

Simple examples for animating of properties.

# Properties animation
layer.animate
    x: 200

# Properties spring animation
layer.animate
    x: 200
    options:
        curve: "spring"

Simple examples for animating with states.

layer.states.test =
    x: 200

# State animation
layer.animate "test"

# State animation with options
layer.animate "test",
    curve: "spring"

layer.animateStop()

Stop all animations for this layer.

layer.animationOptions

Animation objects manage animations that target a layer and properties. An animation will tween between a start and end value, with a curve. The start value is determined when the animation starts, and the end value is defined by properties. If the start values equal to end values, they won't animate.

  • curve — A string, set to ease by default. (Optional)
  • curveOptions — An object with the options of the set curve. (Optional)
  • time — A number, the duration in seconds. (Optional)
  • delay — A number, the delay of the animation. (Optional)
  • repeat — A number, the amount of times it repeats. (Optional)
  • colorModel — A string, the model to animate colors in. (Optional)
  • instant — A boolean, don't animate. (Optional)

layer.states (object)

The states object on a layer holds all the different states for a layer. They can be used to animate to, switch to, or cycled through. States are a great way to organize different visual properties of layers.

The properties of a state can be anything on a layer like x, y, etc. Properties that cannot be animated like html or visible can still be used but will not be animated. They will get set at the end of a transition.

States can have a special animationOptions key to hold animation options like curve. They will be used whenever layers get animated to that state.

There are two special states: current and previous. They refer to the current and last state that a layer is in. Additionally, the have a name key so you can check the current state name with layer.states.current.name.

Examples

# Set a single state
layer.states.stateA =
    x: 100

# Animate to the state  
layer.animate("stateA")

# Go to the state without animation
layer.stateSwitch("stateA")

Here is a common example where we set multiple states and cycle through them.

# Set multiple states at once
layer.states =
    stateA:
        x: 100
    stateB:
        x: 200

# On a click, go back and forth between states.
layer.onTap -> layer.stateCycle(["stateA", "stateB"])

layer.statesCycle(states: array<string>?, options={})

Changes overview

Animation

Change a property.

layer = new Layer


## After

layer.animate
  x: 100

## Before

layer.animate
  properties:
    x: 100

Change a property with options.

## 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 at once.

## 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"

Animate to state with options.

## Before

layer.states.add
  stateE:
    x: 200

layer.states.switch "stateE",
  curve: "ease-in"

## After

layer.states.stateE =
    x: 200

layer.animate "stateE",
    curve: "ease-in"

States can now also include animation options in the state itself. This is handy for declaring how to animate to a state as well. This has the exact same result as above.

layer.states.stateE =
    x: 200
    animationOptions:
        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.stateSwitch "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
     animationOptions:
         instant: true

Cycling through state

Next has been renamed to cycling, because next was pretty ambigious depending on the context.

## Before

layer.states.next()
layer.states.next("stateB","stateC")

## After

layer.stateCycle()
layer.stateCycle(["stateB", "stateC"])  # Preferred
layer.stateCycle("stateB", "stateC")    # Also valid

# With options
layer.stateCycle ["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.default - The state the layer had upon creation.
  • 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.previous.name and layer.states.current.name. The name key only gets added in the context of these special states, so the special state objects are not the same as the actual state object. They are completely equal, with the name key added.

Notice the absence of layer.states.next, this functionality will be provided by layer.stateCycle() 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 default and current.

layer.states = 
    left:
        x: Align.left
    right:
        x: Align.right

layer.stateCycle()

print layer.states.current.name # "left"
print layer.stateNames # ["default", "left", "right"]

The states on a layer is pretty much a normal object. So you can use different ways to loop through the states.

layer.states = 
    left:
        x: Align.left
    right:
        x: Align.right

# All thsese print ["default", "left", "right"]
print layer.stateNames # ["default", "left", "right"]
print Object.keys(layer.states)
print _.keys(layer.states)
print (k for k of layer.states)

# But beware of this one, it is empty: []
print (k for k in layer.states)

Older stuff:

Some refactoring for the new animation api

  • Move state switching responsibility back to the LayerStateMachine.
  • Clean up animation events; AnimationStart, AnimationStop and AnimationEnd.
  • Deprecate StateWillSwitch and StateDidSwitch in favor of StateSwitchStart, StateSwitchStop and StateSwitchEnd.
  • Refactored LayerStates to use prototype properties, so we don't make them in constructor.

Changes

  • You cannot override the deprecated names anymore (like .add) to use as a state name, because they would not show up in state names.
  • Remove the initial state and keep default as is.

Worries

  • Ideally Object.keys(layer.states) should be the same as for k in layer.states but that does not seem to be possible.
  • Maybe we need to change layer.states.current back from the name to the actual properties and add a name if you return them so you can do layer.states.current.name. This would lose comparison by reference, though.
  • There is a (double) circular dependency between LayerLayerStatesLayerStateMachine and back. Not the end of the world, but I'm not sure if the GC is happy about this.

See: https://github.com/motif/company/issues/2633, https://github.com/motif/company/issues/2635

@koenbok
Copy link
Owner Author

koenbok commented Sep 18, 2016

https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/keys

The Object.keys() method returns an array of a given object's own enumerable properties, in the same order as that provided by a for...in loop (the difference being that a for-in loop enumerates properties in the prototype chain as well).

Given this, I think we could actually make Object.keys and for...in work like expected.

@koenbok
Copy link
Owner Author

koenbok commented Sep 18, 2016

So, this begs the question; what should this do?

layer = new Layer

print "layer.stateNames", layer.stateNames
print "Object.keys", Object.keys(layer.states)
print "for..in", (k for k in layer.states)

The current output is:

» "layer.stateNames", ["default"]
» "Object.keys", []
» "for..in", []

To me, it seems like:

  • All three always need to be equal. [update: impossible, see below]
  • I'd prefer to lean towards ["initial"] and hide default, but maybe not when we use deprecated methods (so we don't break compatibility).

@koenbok
Copy link
Owner Author

koenbok commented Sep 18, 2016

All right, so we can't get for..in to behave exactly like Object.keys() which is sad. See the full lookup table of how they work here. But for..of works well, which is closest to what we can get.

I also have default replaced with initial except when you are using deprecated methods. The only breaking exception is when you listen to StateDidSwitch, the previous/current will always be initial, never default.

- We now always use initial, unless you are using deprecated methods.
- .stateNames, Object.keys(layer.states) and for...of all work the same (except for for...in)
- The initial state is managed by LayerStates again.
@nvh
Copy link
Collaborator

nvh commented Sep 18, 2016

@koenbok about default/initial: have you seen: https://github.com/motif/company/issues/2624

@koenbok
Copy link
Owner Author

koenbok commented Sep 18, 2016

Yeah :-/ let's chat about it tomorrow.

@koenbok
Copy link
Owner Author

koenbok commented Sep 19, 2016

Ok, re: https://github.com/motif/company/issues/2624 we decided to rip out the initial state for now and keep default the way it is.

Copy link
Collaborator

@nvh nvh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think all's good, have some minor suggestions and questions. If you have some general remarks about this refactor, I'd also like to hear them!

# Mix of current and old api
if arguments.length is 2
layer = args[0]
if args[1].properties
Copy link
Collaborator

@nvh nvh Sep 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we check for null with
if args[1].properties?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

properties = args[1].properties
else
properties = args[1]
options = args[1].options if args[1].options
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here args[i].options?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

delete options.properties
delete options.options

# print "Animation", layer, properties, options
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

@@ -64,6 +96,9 @@ class exports.Animation extends BaseClass
@_originalState = @_currentState()
@_repeatCounter = @options.repeat

@define "layer",
get: -> @_layer
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use later setting of layers in the animation preview of autocode. Are you ok with just using private api there?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah let's do that for now

@@ -195,6 +195,7 @@ class TouchEmulator extends BaseClass
curve: "ease-out"

hideTouchCursor: ->
return unless @touchPointLayer.opacity > 0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this in this PR?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:-/

#Support the old properties syntax
if properties.properties?
# console.warn "Using Layer.animate with 'properties' key is deprecated: please provide properties directly and use the 'options' key to provide animation options instead"
# print "layer.animate", properties, options
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Print comment can be removed


# Support the old properties syntax, we add all properties top level and
# move the options into an options property.
if properties.hasOwnProperty("properties")
Copy link
Collaborator

@nvh nvh Sep 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we check hasOwnProperty here instead of doing properties.properties?

# With the new api we treat the properties as animatable properties, and use
# the special options keyword for animation options.
if properties.hasOwnProperty("options")
options = _.defaults(properties.options, options)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think specifying options in the function should have precedence over specifying them in a options key

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep.

get: -> @_stateMachine.states
get: ->
@_states ?= new LayerStates(@)
return @_states
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really dislike this behaviour of capturing the state inside of a getter, leads to unexpected behaviour. Can't we add it to the creation / addition of states?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I'd like that, but:

  1. There is no hook to see if a state gets created (because it's just an object key).
  2. This guarantees backwards compat

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with 2), we could fix 1) if we ever move to initial, but am I correct that in order to drop this, we would also need to drop the backwards compatibility of default?

for name, state of states
@_stateMachine.states[name] = state
@states.machine.reset()
_.extend(@states, states)
Copy link
Collaborator

@nvh nvh Sep 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could capture the current state of the layer here, we should however be aware of the setting of states individually...

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, because this should work too:

layer = new Layer
layer.states.test = {x: 500}
layer.animate "default"


it "should have an extra state", ->
layer = new Layer
layer.states.test = {x: 100}
testStates(layer, ["initial", "test"])
testStates(layer, ["default", "test"])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use initialStateName here as well

Copy link
Collaborator

@nvh nvh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏸

Copy link
Collaborator

@nvh nvh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, some minor remarks, but noting really blocking

return @animateToState(properties, options)
# Support options as an object
options = options.options if options.options?
return @states.machine.switchTo(properties, options)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nicer to put properties in a new variable stateName here to make it more clear what is happening

states = []
states = _.flatten(args) if args.length
@animate(@states.machine.next(states), options)

stateSwitch: (stateName, options={}) ->
@animate(stateName, _.defaults({}, options, {instant:true}))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to support animated: true?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meh.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well ok.

@@ -21,7 +21,7 @@ class exports.LayerStateMachine extends BaseClass
get: -> @_currentName

@define "previousName",
get: -> _.last(@_previousNames)
get: -> _.last(@_previousNames) or "default"
Copy link
Collaborator

@nvh nvh Sep 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? (I don't understand the case where _.last(@_previousNames) is null)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to always return something here so you don't have to do a null check (if you just created a layer and ask for layer.states.previous.x.

@@ -15,6 +15,9 @@ deprecatedWarning = (name, suggestion) ->
message += ", use '#{suggestion}' instead." if suggestion?
console.warn message

namedState = (state, name) ->
return _.extend({}, state, {name: name})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we want to enforce the named state by reversing state and {name: name}

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

@@ -3,6 +3,9 @@ assert = require "assert"

initialStateName = "default"

cleanState = (state) ->
return _.pickBy(state, (value, key) -> key isnt "name")
Copy link
Collaborator

@nvh nvh Sep 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could have a better name then cleanState, maybe removeKeysFromState(state,keys=["name"])?

@@ -158,24 +185,24 @@ describe "LayerStates", ->
# it "should be a no-op to change to the current state", ->
# layer = new Layer
# layer.states.stateA = {x: 100}
# layer.switchInstant "stateA"
# layer.stateSwitch "stateA"
# animation = layer.animate "stateA", time: 0.05
# assert.equal(animation, null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this test disabled?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the behaviour changed. We actually do always return an animation now so that the events get called and you can skip a null check.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. Then it can be removed instead of commented out

@koenbok
Copy link
Owner Author

koenbok commented Sep 20, 2016

Review @nvh?

Copy link
Collaborator

@nvh nvh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@koenbok koenbok changed the title Animation api refactor Animation API refactor Sep 21, 2016
@koenbok
Copy link
Owner Author

koenbok commented Sep 21, 2016

Ok here we go.

@koenbok koenbok merged commit 99f0882 into master Sep 21, 2016
@nvh nvh deleted the animation-api-refactor branch January 17, 2018 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants