material-motion
Version:
Makes it easy to add rich, interactive motion to your application.
244 lines • 9.28 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 { Axis, State, } from '../enums';
import { anyOf, not, when, } from '../aggregators';
import { getVelocity$, } from '../getVelocity$';
import { createProperty, } from '../observables/';
import { subscribe, } from '../subscribe';
export class Tossable {
constructor({ draggable, spring }) {
this.state$ = createProperty({
initialValue: State.AT_REST,
});
/**
* This is the point from which all other resistance calculations are
* measured.
*/
this.resistanceOrigin$ = createProperty({
initialValue: { x: 0, y: 0 },
});
/**
* This is the distance from the origin that an item can be freely dragged
* without encountering resistance.
*/
this.radiusUntilResistance$ = createProperty({
initialValue: 0,
});
/**
* For carousels or swipeable lists, this is the width of one item.
*
* To apply resistance, the calculation needs to determine the amount of
* progress through a drag. `resistanceBasis` is the denominator in this
* calculation. For instance, if a drag is 20px beyond `radiusUntilResistance`
* and `resistanceBasis` is 50, the drag progress used by the resistance
* calculation is 40%.
*
* Note: a drag cannot move farther than `resistanceBasis` beyond
* `radiusUntilResistance`.
*/
this.resistanceBasis$ = createProperty({
initialValue: 0,
});
/**
* This value determines how far beyond `radiusUntilResistance` a drag is
* limited to.
*
* It works in conjunction with `resistanceBasis`. If `resistanceBasis` is 50
* and `resistanceFactor` is 5, the drag is limited to 10px (basis / factor)
* beyond `radiusUntilResistance`.
*/
this.resistanceFactor$ = createProperty({
initialValue: 0,
});
this.location$ = createProperty({
initialValue: { x: 0, y: 0 },
});
this.draggable = draggable;
this.spring = spring;
const dragIsAtRest$ = draggable.state$.rewrite({
mapping: {
[State.AT_REST]: true,
[State.ACTIVE]: false,
}
}).dedupe();
const whenDragIsAtRest$ = when(dragIsAtRest$);
const whenDragIsActive$ = when(not(dragIsAtRest$));
// This block needs to come before the one that sets spring enabled to
// ensure the spring initializes with the correct values; otherwise, it will
// start from 0
subscribe({
sink: spring.initialValue$,
source: this.location$._debounce({ pulse$: whenDragIsAtRest$ }),
});
const locationOnDown$ = this.location$._debounce({ pulse$: whenDragIsActive$ });
this.draggedLocation$ = draggable.value$.addedBy({
value$: locationOnDown$,
onlyEmitWithUpstream: true
})._reactiveMap({
transform: ({ upstream: location, resistanceOrigin, radiusUntilResistance, resistanceBasis, resistanceFactor, }) => {
if (!resistanceFactor) {
return location;
}
// We apply resistance radially, leading to all the trig below. In most
// cases, the draggable element will be axis locked, which means there's
// room to short circuit the logic here with simpler solutions when we
// know either x or y is constant.
const locationFromOrigin = {
x: location.x - resistanceOrigin.x,
y: location.y - resistanceOrigin.y,
};
const overflowRadius = Math.sqrt(Math.pow(locationFromOrigin.x, 2) + Math.pow(locationFromOrigin.y, 2)) - radiusUntilResistance;
const resistanceProgress = Math.max(0, Math.min(1, overflowRadius / resistanceBasis));
if (overflowRadius < 0) {
return location;
}
const radiusWithResistance = resistanceBasis / resistanceFactor * Math.sin(resistanceProgress * Math.PI / 2) + radiusUntilResistance;
const angle = Math.atan2(locationFromOrigin.y, locationFromOrigin.x);
return {
x: resistanceOrigin.x + radiusWithResistance * Math.cos(angle),
y: resistanceOrigin.y + radiusWithResistance * Math.sin(angle),
};
},
inputs: {
resistanceOrigin: this.resistanceOrigin$,
radiusUntilResistance: this.radiusUntilResistance$,
resistanceBasis: this.resistanceBasis$,
resistanceFactor: this.resistanceFactor$,
},
onlyEmitWithUpstream: true,
});
this.velocity$ = getVelocity$({
// Since drag starts at rest, whenDragIsAtRest$ emits immediately. Thus,
// we start with { 0, 0 } to ensure velocity doesn't emit undefined.
value$: this.draggedLocation$.startWith({ x: 0, y: 0 }),
pulse$: whenDragIsAtRest$,
});
subscribe({
sink: spring.initialVelocity$,
source: this.velocity$,
});
subscribe({
sink: spring.enabled$,
source: dragIsAtRest$,
});
subscribe({
sink: this.state$,
source: anyOf([
spring.state$.isAnyOf([State.ACTIVE]),
draggable.state$.isAnyOf([State.ACTIVE]),
]).rewrite({
mapping: {
true: State.ACTIVE,
false: State.AT_REST,
},
}).dedupe(),
});
subscribe({
sink: this.location$,
source: spring.enabled$.rewrite({
mapping: {
true: spring.value$,
false: this.draggedLocation$,
},
emitOnKeyChange: false,
})._debounce(),
});
this.styleStreams = {
translate$: this.location$,
willChange$: this.state$.rewrite({
mapping: {
[State.ACTIVE]: 'transform',
[State.AT_REST]: '',
},
}),
};
}
get state() {
return this.state$.read();
}
get resistanceOrigin() {
return this.resistanceOrigin$.read();
}
set resistanceOrigin(value) {
this.resistanceOrigin$.write(value);
}
get radiusUntilResistance() {
return this.radiusUntilResistance$.read();
}
set radiusUntilResistance(value) {
this.radiusUntilResistance$.write(value);
}
get resistanceBasis() {
return this.resistanceBasis$.read();
}
set resistanceBasis(value) {
this.resistanceBasis$.write(value);
}
get resistanceFactor() {
return this.resistanceFactor$.read();
}
set resistanceFactor(value) {
this.resistanceFactor$.write(value);
}
}
export default Tossable;
export function applyLinearResistanceToTossable({ tossable, min$, max$, axis$, basis$, factor$, }) {
subscribe({
sink: tossable.resistanceBasis$,
source: basis$,
});
subscribe({
sink: tossable.resistanceFactor$,
source: factor$,
});
subscribe({
sink: tossable.radiusUntilResistance$,
source: min$._reactiveMap({
transform: ({ upstream: min, max }) => Math.abs(max - min) / 2,
inputs: {
max: max$,
}
}),
});
subscribe({
sink: tossable.resistanceOrigin$,
source: axis$._reactiveMap({
transform: ({ upstream: axis, min, max }) => {
const linearCenter = min + (max - min) / 2;
if (axis === Axis.X) {
return {
x: linearCenter,
y: 0,
};
}
else if (axis === Axis.Y) {
return {
x: 0,
y: linearCenter,
};
}
else {
console.warn(`Cannot apply linear resistance if axis isn't locked`);
}
},
inputs: {
min: min$,
max: max$,
},
}),
});
}
//# sourceMappingURL=Tossable.js.map