UNPKG

material-motion

Version:

Makes it easy to add rich, interactive motion to your application.

117 lines 5.3 kB
/** @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