material-motion
Version:
Makes it easy to add rich, interactive motion to your application.
117 lines • 5.3 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 { MotionObservable, } from './observables/proxies';
import { createPlucker, } from './operators/pluck';
import { isPoint2D, } from './typeGuards';
// These constants are exported for testing.
export const MAXIMUM_AGE = 250;
export const MAXIMUM_INCOMING_DISPATCHES = 5;
const pluckTimestamp = createPlucker('timestamp');
const pluckValue = createPlucker('value');
const pluckX = createPlucker('value.x');
const pluckY = createPlucker('value.y');
/**
* Computes the velocity of an incoming stream and emits the result. Velocity's
* denominator is in milliseconds; if the incoming stream is measured in pixels,
* the resulting stream will be in pixels / millisecond.
*
* Velocity is computed by watching the trailing 250ms of up to 5 emissions and
* measuring the distance between the longest pair of events moving in the
* current direction. This approach is more resiliant to anomolous data than a
* simple (nextPosition - prevPosition) / (nextTime - prevTime).
*
* If `pulse$` is supplied, `velocity(pulse$)` will only emit values when
* `pulse$` emits a value. This is useful for ensuring that velocity is only
* calculated when it will be used.
*/
export function getVelocity$({ value$, pulse$,
// These values are borrowed from Matthew Bolohan's drag implementation (which
// presumes the units are "px / ms").
maximumVelocity = 5, defaultVelocity = 1, }) {
return new MotionObservable((observer) => {
let records = [];
const trailingSubscription = value$.timestamp()._slidingWindow({ size: MAXIMUM_INCOMING_DISPATCHES }).subscribe(nextRecords => records = nextRecords);
const pulseSubscription = pulse$.timestamp().subscribe(({ timestamp: now }) => {
const recentRecords = records.filter(({ timestamp }) => now - timestamp < MAXIMUM_AGE);
if (records.length && isPoint2D(records[0].value)) {
observer.next({
x: calculateVelocity({ records: recentRecords, pluckValue: pluckX, pluckTimestamp, maximumVelocity, defaultVelocity }),
y: calculateVelocity({ records: recentRecords, pluckValue: pluckY, pluckTimestamp, maximumVelocity, defaultVelocity }),
});
}
else {
observer.next(calculateVelocity({ records: recentRecords, pluckValue, pluckTimestamp, maximumVelocity, defaultVelocity }));
}
});
return () => {
trailingSubscription.unsubscribe();
pulseSubscription.unsubscribe();
};
});
}
;
/**
* Computes velocity using the oldest value that's moving in the current
* direction.
*
* It walks backwards over an array of records and makes sure that each segment
* is going in the same direction (incrementing or decrementing) as the last
* segment. When it detects a change in direction (or has checked the whole
* array), it returns the velocity between the newest value and the oldest
* moving in the same direction.
*/
function calculateVelocity({ records, pluckValue, pluckTimestamp, maximumVelocity, defaultVelocity }) {
if (records.length < 2) {
return 0;
}
let velocity = 0;
// Backing over the array pairwise, `next` is the newer value and `prev` is
// the older value. `last` is always the final value.
const lastIndex = records.length - 1;
const lastValue = pluckValue(records[lastIndex]);
const lastTimestamp = pluckTimestamp(records[lastIndex]);
let prevValue;
let prevTimestamp;
let nextValue = lastValue;
let nextTimestamp = lastTimestamp;
for (let i = lastIndex - 1; i >= 0; i--) {
prevValue = pluckValue(records[i]);
prevTimestamp = pluckTimestamp(records[i]);
const averageVelocity = (lastValue - prevValue) / (lastTimestamp - prevTimestamp);
const pairwiseVelocity = (nextValue - prevValue) / (nextTimestamp - prevTimestamp);
if (velocity !== 0 && pairwiseVelocity > 0 !== velocity > 0) {
break;
}
else {
velocity = averageVelocity;
}
nextValue = prevValue;
nextTimestamp = prevTimestamp;
}
if (Math.abs(velocity) > maximumVelocity) {
// If there isn't enough data to trust our accuracy (perhaps the main thread
// was blocked), use the default velocity.
const direction = velocity / Math.abs(velocity);
if (records.length < 3) {
velocity = direction * defaultVelocity;
}
else {
velocity = direction * maximumVelocity;
}
}
return velocity;
}
//# sourceMappingURL=getVelocity$.js.map