Last active
October 14, 2015 09:34
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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