Skip to content

Commit

Permalink
[added] toPolar operator
Browse files Browse the repository at this point in the history
Summary:
This is useful for building marking menus.  Like `distanceFrom`, it accepts an `origin$` argument, which allows us to ensure it can only be called on `Observable<Point2D>`.  (Another solution would have been `point$.subtractedBy(origin$).toPoint()`, but without the argument, we can't constrain `T`).

I inlined the Pythagorean theorem rather than returning `combineLatest({ distance: this.distanceFrom(origin$), angle: … })` to avoid emitting separately for each of `distance` and `angle`.

Part of #231

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

Tags: #material_motion

Differential Revision: http://codereview.cc/D3444
  • Loading branch information
appsforartists committed Jun 1, 2018
1 parent ea01c11 commit c751378
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 5 deletions.
70 changes: 70 additions & 0 deletions packages/core/src/operators/__tests__/toPolar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/** @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 { expect, use as useInChai } from 'chai';
import * as sinonChai from 'sinon-chai';
useInChai(sinonChai);

import {
beforeEach,
describe,
it,
} from 'mocha-sugar-free';

import {
stub,
} from 'sinon';

import {
MemorylessIndefiniteSubject,
MotionSubject,
} from '../../observables/';

describe('motionObservable.toPolar',
() => {
let subject;
let argSubject;
let listener;

beforeEach(
() => {
subject = new MotionSubject();
argSubject = new MemorylessIndefiniteSubject();
listener = stub();
}
);

it('should emit the correct distance',
() => {
subject.toPolar({ origin$: { x: 10, y: 10 } }).pluck('distance').subscribe(listener);

subject.next({ x: 3, y: 10 });

expect(listener).to.have.been.calledWith(7);
}
);

it('should emit the correct angle',
() => {
subject.toPolar({ origin$: { x: 10, y: 10 } }).pluck('angle').subscribe(listener);

subject.next({ x: 10, y: 0 });

expect(listener).to.have.been.calledWith(Math.PI / -2);
}
);
}
);
10 changes: 9 additions & 1 deletion packages/core/src/operators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ import {
withTimestamp,
} from './timestamp';

import {
MotionPolarizable,
withToPolar,
} from './toPolar';

export interface ObservableWithMotionOperators<T> extends
ObservableWithFoundationalMotionOperators<T>,
MotionAddable<T>,
Expand All @@ -138,6 +143,7 @@ export interface ObservableWithMotionOperators<T> extends
MotionMergeable<T>,
MotionMultipliable<T>,
MotionPluckable<T>,
MotionPolarizable<T>,
MotionRewritable<T>,
MotionRewriteRangeable,
MotionRewriteToable,
Expand Down Expand Up @@ -170,8 +176,9 @@ export function withMotionOperators<T, S extends Constructor<Observable<T>>>(sup
const result18 = withIsAnyOf<T, typeof result17>(result17);
const result19 = withAppendUnit<T, typeof result18>(result18);
const result20 = withInverted<T, typeof result19>(result19);
const result21 = withToPolar<T, typeof result20>(result20);

return result20;
return result21;
}

export * from './addedBy';
Expand All @@ -195,3 +202,4 @@ export * from './subtractedBy';
export * from './threshold';
export * from './thresholdRange';
export * from './timestamp';
export * from './toPolar';
78 changes: 78 additions & 0 deletions packages/core/src/operators/toPolar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/** @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 {
combineLatest,
} from '../combineLatest';

import {
Constructor,
Dict,
MotionReactiveMappable,
Observable,
ObservableWithMotionOperators,
Point2D,
PolarCoords,
} from '../types';

import {
isDefined,
} from '../typeGuards';

export type ToPolarOrigin<T> = (T & Point2D) | (Observable<T & Point2D>);

export type ToPolarArgs<T> = {
origin$: ToPolarOrigin<T>,
};

export interface MotionPolarizable<T> {
toPolar(kwargs: ToPolarArgs<T>): ObservableWithMotionOperators<PolarCoords>;
toPolar(origin$: ToPolarOrigin<T>): ObservableWithMotionOperators<PolarCoords>;
}

export function withToPolar<T, S extends Constructor<MotionReactiveMappable<T>>>(superclass: S): S & Constructor<MotionPolarizable<T>> {
return class extends superclass implements MotionPolarizable<T> {
/**
* Converts a stream of `Point2D`s to a stream of `{ distance, angle }`,
* where `distance` is in pixels and `angle` is in radians.
*/
toPolar(kwargs: ToPolarArgs<T>): ObservableWithMotionOperators<PolarCoords>;
toPolar(origin$: ToPolarOrigin<T>): ObservableWithMotionOperators<PolarCoords>;
toPolar({ origin$ }: ToPolarArgs<T> & ToPolarOrigin<T>): ObservableWithMotionOperators<PolarCoords> {
if (!isDefined(origin$)) {
origin$ = arguments[0] as ToPolarOrigin<T>;
}

return this._reactiveMap({
transform({ upstream, origin }: Dict<T & Point2D>) {
const deltaX = upstream.x - origin.x;
const deltaY = upstream.y - origin.y;

return {
// Inlining Pythagorean theorem instead of using `combineLatest` +
// `distanceFrom`, both for simplicity and to avoid emitting twice
// for each upstream emission.
distance: Math.sqrt(deltaX ** 2 + deltaY ** 2),
angle: Math.atan2(deltaY, deltaX),
};
},
inputs: {
origin: origin$,
},
});
}
};
}
14 changes: 10 additions & 4 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ export {
} from 'indefinite-observable';

export {
MotionAddable,
MotionAppendUnitable,
MotionClampable,
MotionDebounceable,
MotionDeduplicable,
MotionDivisible,
MotionFilterable,
MotionFlattenable,
MotionInvertible,
Expand All @@ -52,22 +54,21 @@ export {
MotionMemorable,
MotionMergeable,
MotionMulticastable,
MotionMultipliable,
MotionNextOperable,
MotionDivisible,
MotionAddable,
MotionPluckable,
MotionPolarizable,
MotionReactiveMappable,
MotionReactiveNextOperable,
MotionReadable,
MotionRewritable,
MotionRewriteRangeable,
MotionRewriteToable,
MotionMultipliable,
MotionSeedable,
MotionSubtractable,
MotionTappable,
MotionThresholdable,
MotionThresholdRangeable,
MotionThresholdable,
MotionTimestampable,
MotionWindowable,
ObservableWithFoundationalMotionOperators,
Expand Down Expand Up @@ -98,6 +99,11 @@ export type Point2D = {
y: number,
};

export type PolarCoords = {
angle: number,
distance: number,
};

export interface Spring<T> {
readonly destination$: ObservableWithMotionOperators<T>;
destination: T;
Expand Down

0 comments on commit c751378

Please sign in to comment.