material-motion
Version:
Makes it easy to add rich, interactive motion to your application.
214 lines • 9.01 kB
JavaScript
/** @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 { Spring as WobbleSpring, } from 'wobble';
import { createProperty, } from '../observables/createProperty';
import { MotionObservable, } from '../observables/MotionObservable';
import { State, } from '../enums';
export const DEFAULT_STIFFNESS = 342;
export const DEFAULT_DAMPING = 30;
export const DEFAULT_THRESHOLD = .001;
// Springs are a core primitive in Material Motion, yet, the implementations we
// have are all from 3rd party libraries. Thus, the common
// getters/setters/properties live here, and each implementation can extend it
// to implement its own `value$`.
export class NumericSpring {
constructor() {
this.destination$ = createProperty({
initialValue: 0,
});
this.initialValue$ = createProperty({
initialValue: 0,
});
this.initialVelocity$ = createProperty({
initialValue: 0,
});
this.stiffness$ = createProperty({
initialValue: DEFAULT_STIFFNESS,
});
this.damping$ = createProperty({
initialValue: DEFAULT_DAMPING,
});
this.threshold$ = createProperty({
initialValue: DEFAULT_THRESHOLD,
});
this.enabled$ = createProperty({
initialValue: true,
});
this.state$ = createProperty({
initialValue: State.AT_REST,
});
this.value$ = new MotionObservable((observer) => {
const spring = new WobbleSpring();
const updateSpringConfig = spring.updateConfig.bind(spring);
let initialized = false;
let initialValueChangedWhileDisabled = false;
let initialVelocityChangedWhileDisabled = false;
this.state$.write(State.AT_REST);
spring.onStart(() => {
this.state$.write(State.ACTIVE);
});
spring.onUpdate(() => {
observer.next(spring.currentValue);
});
spring.onStop(() => {
this.state$.write(State.AT_REST);
});
// During initialization, the configuration values will be set in the
// order they are subscribed to; hence,
const subscriptions = [
// properties that configure the spring
this.stiffness$._map(transformToValueWithKey('stiffness')).subscribe(updateSpringConfig),
this.damping$._map(transformToValueWithKey('damping')).subscribe(updateSpringConfig),
this.threshold$._map(transformToValueWithKey('restDisplacementThreshold')).subscribe(updateSpringConfig),
// properties that initialize the spring
this.initialVelocity$.subscribe((initialVelocity) => {
if (this.enabled$.read()) {
updateSpringConfig({ initialVelocity });
}
else {
initialVelocityChangedWhileDisabled = true;
}
}),
this.initialValue$.subscribe((initialValue) => {
// We need to figure out what the right interplay is between
// `initialValue` and `value$`. It's probably more convenient for
// an author if changes to `initialValue` are passed through to
// `value$`. This means you can do something like
// `spring.value$.subscribe(location$)` and trust `location$` is
// always up-to-date. However, it's unclear how that would affect
// `state$`. Should `state$` go at_rest -> active -> at_rest every
// time something touches `initialValue`? Should `value$` be able
// to emit `initialValue$` without touching `value$`? Should we
// require authors to worry about connecting the two?
//
// Punting on this for now and forcing authors to deal with it.
if (this.enabled$.read()) {
updateSpringConfig({ fromValue: initialValue });
observer.next(initialValue);
}
else {
initialValueChangedWhileDisabled = true;
}
}),
// properties that can start/stop the spring
this.enabled$.subscribe((enabled) => {
if (initialized) {
if (enabled) {
// For simple cases, the spring can manage its own state;
// however, we provide property APIs (like initialValue$) for
// situations where it should be managed externally.
//
// The ability to manage this state externally shouldn't require
// every author to do so; hence, we use dirty-checking to only
// initialize the spring's values if the author has used these
// features.
//
// There's probably a more elegant way to do this (queueing up
// changes as they come through and then applying the most
// recent one when enabled is set), but this is the simplest
// quick solution.
const springConfig = {
toValue: this.destination$.read(),
};
if (initialValueChangedWhileDisabled) {
const initialValue = this.initialValue$.read();
springConfig.fromValue = initialValue;
observer.next(initialValue);
}
if (initialVelocityChangedWhileDisabled) {
springConfig.initialVelocity = this.initialVelocity$.read();
}
updateSpringConfig(springConfig);
spring.start();
initialValueChangedWhileDisabled = false;
initialVelocityChangedWhileDisabled = false;
}
else {
spring.stop();
}
}
}),
this.destination$.subscribe((destination) => {
if (this.enabled$.read()) {
updateSpringConfig({ toValue: destination });
spring.start();
}
;
}),
];
initialized = true;
return function disconnect() {
subscriptions.forEach(subscription => subscription.unsubscribe());
};
})._remember();
}
get destination() {
return this.destination$.read();
}
set destination(value) {
this.destination$.write(value);
}
get initialValue() {
return this.initialValue$.read();
}
set initialValue(value) {
this.initialValue$.write(value);
}
get initialVelocity() {
return this.initialVelocity$.read();
}
set initialVelocity(value) {
this.initialVelocity$.write(value);
}
get stiffness() {
return this.stiffness$.read();
}
set stiffness(value) {
this.stiffness$.write(value);
}
get damping() {
return this.damping$.read();
}
set damping(value) {
this.damping$.write(value);
}
get threshold() {
return this.threshold$.read();
}
set threshold(value) {
this.threshold$.write(value);
}
get enabled() {
return this.enabled$.read();
}
set enabled(value) {
this.enabled$.write(value);
}
get state() {
return this.state$.read();
}
}
export default NumericSpring;
function transformToValueWithKey(key) {
return {
transform(value) {
return {
[key]: value,
};
},
};
}
//# sourceMappingURL=NumericSpring.js.map