Skip to content

Commit

Permalink
changes zoom and scale source of calculations
Browse files Browse the repository at this point in the history
  • Loading branch information
dplumlee committed Jan 14, 2020
1 parent 387da98 commit e8f24ba
Show file tree
Hide file tree
Showing 11 changed files with 82 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@

import { Vector2 } from '../../types';

interface UserScaled {
readonly type: 'userScaled';
interface UserSetZoomLevel {
readonly type: 'userSetZoomLevel';
/**
* A vector who's `x` and `y` component will be the new scaling factors for the projection.
* A number whose value is always between 0 and 1 and will be the new scaling factor for the projection.
*/
readonly payload: Vector2;
readonly payload: number;
}

interface UserZoomed {
readonly type: 'userZoomed';
/**
* A value to zoom in by. Should be a fraction of `1`. For a `'wheel'` event when `event.deltaMode` is `'pixel'`, pass `event.deltaY / -renderHeight` where `renderHeight` is the height of the Resolver element in pixels.
* A value to zoom in by. Should be a fraction of `1`. For a `'wheel'` event when `event.deltaMode` is `'pixel'`,
* pass `event.deltaY / -renderHeight` where `renderHeight` is the height of the Resolver element in pixels.
*/
payload: number;
}
Expand Down Expand Up @@ -65,7 +66,7 @@ interface UserMovedPointer {
}

export type CameraAction =
| UserScaled
| UserSetZoomLevel
| UserSetRasterSize
| UserSetPositionOfCamera
| UserStartedPanning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CameraState } from '../../types';
import { cameraReducer } from './reducer';
import { inverseProjectionMatrix } from './selectors';
import { applyMatrix3 } from '../../lib/vector2';
import { scaleToZoom } from './scale_to_zoom';

describe('inverseProjectionMatrix', () => {
let store: Store<CameraState, CameraAction>;
Expand Down Expand Up @@ -59,7 +60,7 @@ describe('inverseProjectionMatrix', () => {
});
describe('when the user has zoomed to 0.5', () => {
beforeEach(() => {
const action: CameraAction = { type: 'userScaled', payload: [0.5, 0.5] };
const action: CameraAction = { type: 'userSetZoomLevel', payload: scaleToZoom(0.5) };
store.dispatch(action);
});
it('should convert 150, 100 (center) to 0, 0 (center) in world space', () => {
Expand Down Expand Up @@ -89,7 +90,7 @@ describe('inverseProjectionMatrix', () => {
describe('when the user has scaled to 2', () => {
// the viewport will only cover half, or 150x100 instead of 300x200
beforeEach(() => {
const action: CameraAction = { type: 'userScaled', payload: [2, 2] };
const action: CameraAction = { type: 'userSetZoomLevel', payload: scaleToZoom(2) };
store.dispatch(action);
});
// we expect the viewport to be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { Store, createStore } from 'redux';
import { CameraAction } from './action';
import { CameraState } from '../../types';
import { cameraReducer } from './reducer';
import { projectionMatrix } from './selectors';
import { projectionMatrix, scale } from './selectors';
import { applyMatrix3 } from '../../lib/vector2';
import { scaleToZoom } from './scale_to_zoom';

describe('projectionMatrix', () => {
let store: Store<CameraState, CameraAction>;
Expand Down Expand Up @@ -56,7 +57,7 @@ describe('projectionMatrix', () => {
});
describe('when the user has zoomed to 0.5', () => {
beforeEach(() => {
const action: CameraAction = { type: 'userScaled', payload: [0.5, 0.5] };
const action: CameraAction = { type: 'userSetZoomLevel', payload: scaleToZoom(0.5) };
store.dispatch(action);
});
it('should convert 0, 0 (center) in world space to 150, 100 (center)', () => {
Expand Down Expand Up @@ -92,7 +93,7 @@ describe('projectionMatrix', () => {
describe('when the user has scaled to 2', () => {
// the viewport will only cover half, or 150x100 instead of 300x200
beforeEach(() => {
const action: CameraAction = { type: 'userScaled', payload: [2, 2] };
const action: CameraAction = { type: 'userSetZoomLevel', payload: scaleToZoom(2) };
store.dispatch(action);
});
// we expect the viewport to be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,34 @@ import { userIsPanning, translation, projectionMatrix, inverseProjectionMatrix }
import { clamp } from '../../lib/math';

import { CameraState, ResolverAction } from '../../types';
import { scaleToZoom } from './scale_to_zoom';

function initialState(): CameraState {
return {
scaling: [1, 1] as const,
scalingFactor: scaleToZoom(1), // Defaulted to halfway zoomed
rasterSize: [0, 0] as const,
translationNotCountingCurrentPanning: [0, 0] as const,
latestFocusedWorldCoordinates: null,
};
}

/**
* The minimum allowed value for the camera scale. This is the least scale that we will ever render something at.
*/
const minimumScale = 0.1;

/**
* The maximum allowed value for the camera scale. This is greatest scale that we will ever render something at.
*/
const maximumScale = 6;

export const cameraReducer: Reducer<CameraState, ResolverAction> = (
state = initialState(),
action
) => {
if (action.type === 'userScaled') {
if (action.type === 'userSetZoomLevel') {
/**
* Handle the scale being explicitly set, for example by a 'reset zoom' feature, or by a range slider with exact scale values
*/
const [deltaX, deltaY] = action.payload;

return {
...state,
scaling: [
clamp(deltaX, minimumScale, maximumScale),
clamp(deltaY, minimumScale, maximumScale),
],
scalingFactor: clamp(action.payload, 0, 1),
};
} else if (action.type === 'userZoomed') {
/**
* When the user zooms we change the scale. Limit the change in scale so that we aren't liable for supporting crazy values (e.g. infinity or negative scale.)
*/
const newScaleX = clamp(state.scaling[0] + action.payload, minimumScale, maximumScale);
const newScaleY = clamp(state.scaling[1] + action.payload, minimumScale, maximumScale);

const stateWithNewScaling: CameraState = {
...state,
scaling: [newScaleX, newScaleY],
scalingFactor: clamp(state.scalingFactor + action.payload, 0, 1),
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { maximum, minimum } from './scaling_constants';

/**
* Calculates the zoom factor (between 0 and 1) for a given scale value.
*/
export const scaleToZoom = (scale: number): number => {
const delta = maximum - minimum;
return (scale - minimum) / delta;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* The minimum allowed value for the camera scale. This is the least scale that we will ever render something at.
*/
export const minimum = 0.1;

/**
* The maximum allowed value for the camera scale. This is greatest scale that we will ever render something at.
*/
export const maximum = 6;
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
orthographicProjection,
translationTransformation,
} from '../../lib/transformation';
import { maximum, minimum } from './scaling_constants';

interface ClippingPlanes {
renderWidth: number;
Expand Down Expand Up @@ -43,8 +44,8 @@ export function viewableBoundingBox(state: CameraState): AABB {
function clippingPlanes(state: CameraState): ClippingPlanes {
const renderWidth = state.rasterSize[0];
const renderHeight = state.rasterSize[1];
const clippingPlaneRight = renderWidth / 2 / state.scaling[0];
const clippingPlaneTop = renderHeight / 2 / state.scaling[1];
const clippingPlaneRight = renderWidth / 2 / scale(state)[0];
const clippingPlaneTop = renderHeight / 2 / scale(state)[1];

return {
renderWidth,
Expand Down Expand Up @@ -112,9 +113,9 @@ export function translation(state: CameraState): Vector2 {
return add(
state.translationNotCountingCurrentPanning,
divide(subtract(state.panning.currentOffset, state.panning.origin), [
state.scaling[0],
scale(state)[0],
// Invert `y` since the `.panning` vectors are in screen coordinates and therefore have backwards `y`
-state.scaling[1],
-scale(state)[1],
])
);
} else {
Expand Down Expand Up @@ -175,7 +176,11 @@ export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state =>
/**
* The scale by which world values are scaled when rendered.
*/
export const scale = (state: CameraState): Vector2 => state.scaling;
export const scale = (state: CameraState): Vector2 => {
const delta = maximum - minimum;
const value = state.scalingFactor * delta + minimum;
return [value, value];
};

/**
* Whether or not the user is current panning the map.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { Store } from 'redux';
import { CameraAction } from './action';
import { CameraState, Vector2 } from '../../types';

type CameraStore = Store<CameraState, CameraAction>;

/**
* Dispatches a 'userScaled' action.
*/
export function userScaled(store: CameraStore, scalingValue: [number, number]): void {
const action: CameraAction = { type: 'userScaled', payload: scalingValue };
store.dispatch(action);
}
import { Vector2 } from '../../types';

/**
* Used to assert that two Vector2s are close to each other (accounting for round-off errors.)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { cameraReducer } from './reducer';
import { createStore, Store } from 'redux';
import { CameraState, AABB } from '../../types';
import { viewableBoundingBox, inverseProjectionMatrix } from './selectors';
import { userScaled, expectVectorsToBeClose } from './test_helpers';
import { expectVectorsToBeClose } from './test_helpers';
import { scaleToZoom } from './scale_to_zoom';
import { applyMatrix3 } from '../../lib/vector2';

describe('zooming', () => {
Expand Down Expand Up @@ -43,7 +44,8 @@ describe('zooming', () => {
);
describe('when the user has scaled in to 2x', () => {
beforeEach(() => {
userScaled(store, [2, 2]);
const action: CameraAction = { type: 'userSetZoomLevel', payload: scaleToZoom(2) };
store.dispatch(action);
});
it(
...cameraShouldBeBoundBy({
Expand All @@ -52,20 +54,29 @@ describe('zooming', () => {
})
);
});
describe('when the user zooms in by 1 zoom unit', () => {
describe('when the user zooms in by 0.1 scaling factor', () => {
beforeEach(() => {
const action: CameraAction = {
type: 'userZoomed',
payload: 1,
};
store.dispatch(action);
});
it(
...cameraShouldBeBoundBy({
minimum: [-75, -50],
maximum: [75, 50],
})
);
it('should zoom in a little bit', () => {
const actual = viewableBoundingBox(store.getState());
expect(actual).toMatchInlineSnapshot(`
Object {
"maximum": Array [
25.000000000000007,
16.666666666666668,
],
"minimum": Array [
-25,
-16.666666666666668,
],
}
`);
});
});
it('the raster position 200, 50 should map to the world position 50, 50', () => {
expectVectorsToBeClose(applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())), [
Expand Down Expand Up @@ -126,7 +137,8 @@ describe('zooming', () => {
});
describe('when the user scales to 2x', () => {
beforeEach(() => {
userScaled(store, [2, 2]);
const action: CameraAction = { type: 'userSetZoomLevel', payload: scaleToZoom(2) };
store.dispatch(action);
});
it('should be centered on 100, 0', () => {
const worldCenterPoint = applyMatrix3(
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/endpoint/public/embeddables/resolver/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ export interface CameraState {
readonly panning?: PanningState;

/**
* Scales the coordinate system, used for zooming.
* Scales the coordinate system, used for zooming. Should always be between 0 and 1
*/
readonly scaling: Vector2;
readonly scalingFactor: number;

/**
* The size (in pixels) of the Resolver component.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ const Resolver = styled(

const handleWheel = useCallback(
(event: WheelEvent) => {
// we use elementBoundingClientRect to interpret pixel deltas as a fraction of the element's height
if (
elementBoundingClientRect !== null &&
event.ctrlKey &&
Expand All @@ -105,7 +104,9 @@ const Resolver = styled(
event.preventDefault();
dispatch({
type: 'userZoomed',
payload: (-2 * event.deltaY) / elementBoundingClientRect.height,
// we use elementBoundingClientRect to interpret pixel deltas as a fraction of the element's height
// when pinch-zooming in on a mac, deltaY is a negative number but we want the payload to be positive
payload: event.deltaY / -elementBoundingClientRect.height,
});
}
},
Expand Down

0 comments on commit e8f24ba

Please sign in to comment.