Skip to content

Instantly share code, notes, and snippets.

@henrik
Last active October 14, 2015 09:34
Show Gist options
  • Save henrik/e2b4a6a89d2f6925955c to your computer and use it in GitHub Desktop.
Save henrik/e2b4a6a89d2f6925955c to your computer and use it in GitHub Desktop.
Simplistic CoffeeScript tweener for use e.g. with React. There's also https://github.com/chenglou/react-tween-state but that's more complex and seems not maintained at the time of writing.
tweener = new Tweener
# If the onChange callback changes the state of a React component, pretty things will happen.
tweener.tween
from: 1
to: 100
ms: 1000
steps: 25
onChange: (value) -> console.log("value is now #{value}")
# If you need to stop it before it's done.
tweener.clear()
# Transitions an integer to another integer in steps. This is "tweening" ("inbetweening").
class @Tweener
tween: ({from, to, ms, steps, onChange}) ->
# If a new tween is started in the middle of a previous one, stop the first one.
@clear()
# Must ceil. If we "round", it could be 0, causing an endless loop.
perFrame = Math.ceil((to - from) / steps)
currentValue = from
tweenFrame = =>
if currentValue >= to
@clear()
else
nextValue = currentValue + perFrame
nextValue = Math.min(nextValue, to)
onChange(nextValue)
currentValue = nextValue
@interval = setInterval(tweenFrame, ms/steps)
# This used to be called "stop" but then we had a "this" mixup and called "window.stop()", causing major pain :/
clear: ->
clearInterval(@interval)
describe "Tweener", ->
# http://jasmine.github.io/2.0/introduction.html#section-Mocking_the_JavaScript_Timeout_Functions
beforeEach -> jasmine.clock().install()
afterEach -> jasmine.clock().uninstall()
describe ".tween", ->
it "transitions an integer", ->
tweener = new Tweener
spy = jasmine.createSpy("callback")
tweener.tween
from: 1
to: 100
ms: 1000
steps: 10
onChange: spy
expect(spy).not.toHaveBeenCalled()
jasmine.clock().tick(100)
expectLatestCallToBeWith(spy, 11)
jasmine.clock().tick(100)
expectLatestCallToBeWith(spy, 21)
# …
jasmine.clock().tick(700)
expectLatestCallToBeWith(spy, 91)
jasmine.clock().tick(100)
expectLatestCallToBeWith(spy, 100)
it "cancels an ongoing tween if a new one is started", ->
tweener = new Tweener
spy1 = jasmine.createSpy("callback 1")
spy2 = jasmine.createSpy("callback 2")
tweener.tween
from: 1
to: 100
ms: 1000
steps: 10
onChange: spy1
jasmine.clock().tick(100)
expectLatestCallToBeWith(spy1, 11)
tweener.tween
from: 500
to: 600
ms: 1000
steps: 10
onChange: spy2
jasmine.clock().tick(100)
# Value should be unchanged for this one.
expectLatestCallToBeWith(spy1, 11)
# But this one starts tweening.
expectLatestCallToBeWith(spy2, 510)
it "doesn't ever exceed the 'to' value", ->
tweener = new Tweener
spy = jasmine.createSpy("callback")
# Regression.
# Going from 1 to 10 in 5 integer steps naively could mean 1, 3, 5, 7, 9, 11. We don't want to overshoot.
tweener.tween
from: 1
to: 10
ms: 1000
steps: 5
onChange: spy
jasmine.clock().tick(1000)
expectLatestCallToBeWith(spy, 10)
# Regression.
# We had a bug where small increments were rounded down to 0 and caused endless looping…
it "handles small steps correctly", ->
tweener = new Tweener
spy = jasmine.createSpy("callback")
tweener.tween
from: 1
to: 10
ms: 1000
steps: 1000
onChange: spy
jasmine.clock().tick(1000)
expectLatestCallToBeWith(spy, 10)
describe ".clear", ->
it "stops the transition", ->
tweener = new Tweener
spy = jasmine.createSpy("callback")
tweener.tween
from: 1
to: 100
ms: 1000
steps: 10
onChange: spy
jasmine.clock().tick(100)
expectLatestCallToBeWith(spy, 11)
tweener.clear()
# Unchanged!
jasmine.clock().tick(100)
expectLatestCallToBeWith(spy, 11)
it "silently does nothing if there's no ongoing transition", ->
# We expect this not to cause any errors.
tweener = new Tweener
tweener.clear()
# No warnings about "no expectations", please, Jasmine.
expect(true).toBeTruthy()
# Helpers
expectLatestCallToBeWith = (spy, expected) =>
expect(spy.calls.mostRecent().args[0]).toBe(expected)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment