Skip to content

Commit

Permalink
[added] springSource
Browse files Browse the repository at this point in the history
Summary: Closes #116

Test Plan: Tests included for basic functionality.  Will have to devise tests for threshold, friction, tension, and initialVelocity.

Reviewers: featherless, markwei, #material_motion, O2 Material Motion, O3 Material JavaScript platform reviewers

Reviewed By: featherless, markwei, #material_motion, O2 Material Motion

Subscribers: markwei, featherless

Tags: #material_motion

Differential Revision: http://codereview.cc/D2473
  • Loading branch information
appsforartists committed Jan 6, 2017
1 parent dac9464 commit a0aea21
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 4 deletions.
2 changes: 2 additions & 0 deletions packages/springs-adaptor-rebound/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"build": "yarn run clean; $( yarn bin )/tsc"
},
"dependencies": {
"material-motion-streams": "0.0.0",
"rebound": "^0.0.13",
"tslib": "^1.2.0"
},
"devDependencies": {
Expand Down
100 changes: 98 additions & 2 deletions packages/springs-adaptor-rebound/src/__tests__/springSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,19 @@ import {
stub,
} from 'sinon';

import springSource from '../springSource';
import {
SimulationLooper,
} from 'rebound';

import {
State,
constantProperty,
} from 'material-motion-streams';

import {
_springSystem,
springSource,
} from '../springSource';

declare function require(name: string);

Expand All @@ -35,15 +47,99 @@ require('chai').use(
require('sinon-chai')
);


describe('springSource',
() => {
let listener;

beforeEach(
() => {
_springSystem.setLooper(new SimulationLooper());
listener = stub();
}
);

it('transitions from initialValue to destination',
() => {
springSource({
initialValue: constantProperty(2),
destination: constantProperty(3),
}).subscribe(listener);

expect(listener.firstCall).to.have.been.calledWith(2);
expect(listener.lastCall).to.have.been.calledWith(3);
}
);

it('starts at rest',
() => {
let firstAtRestTime;
let firstNextTime;

springSource({
initialValue: constantProperty(0),
destination: constantProperty(0),
}).subscribe({
next(value) {},
state: listener
});

expect(listener).to.have.been.calledOnce;
expect(listener).to.have.been.calledWith(State.AT_REST);
}
);

it('',
it('becomes active before dispatching new values',
() => {
let state;
let tested;

const spring = springSource({
initialValue: constantProperty(0),
destination: constantProperty(1),
});

const subscription = spring.subscribe({
next(value) {
if (value !== 0 && !tested) {
expect(state).to.equal(State.ACTIVE);
tested = true;
}
},

state(value) {
state = value;
}
});

expect(tested).to.equal(true);
}
);

it('comes to rest upon completion',
() => {
let next;
let tested;

const spring = springSource({
initialValue: constantProperty(0),
destination: constantProperty(1),
});

const subscription = spring.subscribe({
next(value) {
next = value;
},

state(value) {
if (next === 1 && !tested) {
expect(value).to.equal(State.AT_REST);
tested = true;
}
}
});

expect(tested).to.equal(true);
}
);
}
Expand Down
124 changes: 123 additions & 1 deletion packages/springs-adaptor-rebound/src/springSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,128 @@
* under the License.
*/

export default function springSource() {
import {
MotionObservable,
MotionObserver,
SpringArgs,
State,
constantProperty,
} from 'material-motion-streams';

import {
Listener,
SpringSystem,
} from 'rebound';

// Exported so we can switch out the timing loop in unit tests
export let _springSystem = new SpringSystem();

export type NumericDict = {
[key: string]: number,
};

// perhaps signature should be
//
// springSource(kwargs:{ initialValue: T, destination: T } & Partial<SpringArgs<T>>)
//
// and then destructured in the body:
//
// const { initialValue, … } = { ...springDefaults, ...kwargs }

/**
* Creates a spring and returns a stream of its interpolated values. The
* default spring has a tension of 342 and friction of 30.
*
* Currently only accepts numeric values for initialValue and destination, but
* will eventually accept a dictionary of string:number. Each key:value pair in
* the dictionary will be represented by an independent spring. If any of the
* springs emits a value, the latest values from each should be emitted, e.g.
* {x: 10, y: 43 }.
*
* Currently accepts ReadableProperty values for each argument. Will eventually
* support ReactiveProperty arguments, at which point the spring will begin
* emitting new values whenever the destination changes.
*/
export function springSource<T extends number | NumericDict>({
initialValue,
destination,
// Set defaults for things that are consistent across springs types
//
// This might need to move into `connect` when these become reactive
// properties e.g.:
//
// tension: property.startWith(defaultTension).read()
initialVelocity,
threshold = constantProperty(Number.EPSILON),
tension = constantProperty(342),
friction = constantProperty(30),
}: SpringArgs<T>) {
const firstInitialValue = initialValue.read();

if (isNumber(firstInitialValue)) {
// TypeScript doesn't seem to infer that if firstInitialValue is a number,
// then T must be a number, so we cast the args here.
return numericSpringSource({
initialValue,
destination,
initialVelocity,
threshold,
tension,
friction,
} as SpringArgs<number>);
} else {
throw new Error("springSource only supports numbers.");
}
}
export default springSource;

function numericSpringSource({
destination,
initialValue,
initialVelocity = constantProperty(0),
threshold,
tension,
friction,
}: SpringArgs<number>) {
return new MotionObservable(
(observer: MotionObserver<number>) => {
const spring = _springSystem.createSpringWithConfig({
tension: tension.read(),
friction: friction.read(),
});

const listener: Listener = {
onSpringUpdate() {
observer.next(spring.getCurrentValue());
},

onSpringActivate() {
observer.state(State.ACTIVE);
},

onSpringAtRest() {
observer.state(State.AT_REST);
},
};

observer.state(State.AT_REST);
spring.addListener(listener);

// Whenever the spring is subscribed to, it pulls its values from its
// parameters
spring.setCurrentValue(initialValue.read());
spring.setVelocity(initialVelocity.read());
spring.setEndValue(destination.read());
spring.setRestSpeedThreshold(threshold.read());

return function disconnect() {
spring.removeListener(listener);
spring.setAtRest();
};
}
);
}

function isNumber(value: any): value is number {
return typeof value === 'number';
}
3 changes: 3 additions & 0 deletions packages/streams/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
*/

export * from './types';
export * from './properties';

export * from './MotionObservable';
export { default as MotionObservable } from './MotionObservable';

export * from './MotionRuntime';
export { default as MotionRuntime } from './MotionRuntime';
25 changes: 25 additions & 0 deletions packages/streams/src/properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/** @license
* Copyright 2016 - present The Material Motion Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

import {
ScopedReadable,
} from './types';

export function constantProperty<T>(value: T): ScopedReadable<T> {
return {
read: () => value,
};
};
2 changes: 1 addition & 1 deletion packages/streams/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export type SpringArgs<T> = {
destination: ScopedReadable<T>,
initialValue: ScopedReadable<T>,
initialVelocity: ScopedReadable<T>,
threshold: ScopedReadable<T>,
threshold: ScopedReadable<number>,
friction: ScopedReadable<number>,
tension: ScopedReadable<number>,
};

0 comments on commit a0aea21

Please sign in to comment.