Skip to content

Commit

Permalink
more comments
Browse files Browse the repository at this point in the history
  • Loading branch information
oatkiller committed Jan 7, 2020
1 parent 65c88cd commit 0a2e8da
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,38 @@ function initialState(): CameraState {
};
}

/**
* 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 = 3;

export const cameraReducer: Reducer<CameraState, ResolverAction> = (
state = initialState(),
action
) => {
if (action.type === 'userScaled') {
/**
* 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, 0.1, 3), clamp(deltaY, 0.1, 3)],
scaling: [
clamp(deltaX, minimumScale, maximumScale),
clamp(deltaY, minimumScale, maximumScale),
],
};
} 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, 0.1, 3);
const newScaleY = clamp(state.scaling[1] + action.payload, 0.1, 3);
const newScaleX = clamp(state.scaling[0] + action.payload, minimumScale, maximumScale);
const newScaleY = clamp(state.scaling[1] + action.payload, minimumScale, maximumScale);

const stateWithNewScaling: CameraState = {
...state,
Expand Down Expand Up @@ -71,11 +87,17 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = (
return stateWithNewScaling;
}
} else if (action.type === 'userSetPositionOfCamera') {
/**
* Handle the case where the position of the camera is explicitly set, for example by a 'back to center' feature.
*/
return {
...state,
translationNotCountingCurrentPanning: action.payload,
};
} else if (action.type === 'userStartedPanning') {
/**
* When the user begins panning with a mousedown event we mark the starting position for later comparisons.
*/
return {
...state,
panning: {
Expand All @@ -84,6 +106,9 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = (
},
};
} else if (action.type === 'userStoppedPanning') {
/**
* When the user stops panning (by letting up on the mouse) we calculate the new translation of the camera.
*/
if (userIsPanning(state)) {
return {
...state,
Expand All @@ -94,6 +119,10 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = (
return state;
}
} else if (action.type === 'userSetRasterSize') {
/**
* Handle resizes of the Resolver component. We need to know the size in order to convert between screen
* and world coordinates.
*/
return {
...state,
rasterSize: action.payload,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
translationTransformation,
} from '../../lib/transformation';

interface ClippingPlane {
interface ClippingPlanes {
renderWidth: number;
renderHeight: number;
clippingPlaneRight: number;
Expand All @@ -23,16 +23,22 @@ interface ClippingPlane {
clippingPlaneBottom: number;
}

/**
* The viewable area in the Resolver map, in world coordinates.
*/
export function viewableBoundingBox(state: CameraState): AABB {
const { renderWidth, renderHeight } = clippingPlane(state);
const { renderWidth, renderHeight } = clippingPlanes(state);
const matrix = inverseProjectionMatrix(state);
return {
minimum: applyMatrix3([0, renderHeight], matrix),
maximum: applyMatrix3([renderWidth, 0], matrix),
};
}

function clippingPlane(state: CameraState): ClippingPlane {
/**
* The 2D clipping planes used for the orthographic projection. See https://en.wikipedia.org/wiki/Orthographic_projection
*/
function clippingPlanes(state: CameraState): ClippingPlanes {
const renderWidth = state.rasterSize[0];
const renderHeight = state.rasterSize[1];
const clippingPlaneRight = renderWidth / 2 / state.scaling[0];
Expand All @@ -49,7 +55,8 @@ function clippingPlane(state: CameraState): ClippingPlane {
}

/**
* https://en.wikipedia.org/wiki/Orthographic_projection
* A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates.
* See https://en.wikipedia.org/wiki/Orthographic_projection
*/
export const projectionMatrix: (state: CameraState) => Matrix3 = state => {
const {
Expand All @@ -59,7 +66,7 @@ export const projectionMatrix: (state: CameraState) => Matrix3 = state => {
clippingPlaneTop,
clippingPlaneLeft,
clippingPlaneBottom,
} = clippingPlane(state);
} = clippingPlanes(state);

return multiply(
// 5. convert from 0->2 to 0->rasterWidth (or height)
Expand All @@ -86,6 +93,18 @@ export const projectionMatrix: (state: CameraState) => Matrix3 = state => {
);
};

/**
* The camera has a translation value (not counting any current panning.) This is initialized to (0, 0) and
* updating any time panning ends.
*
* When the user is panning, we keep the initial position of the pointer and the current position of the
* pointer. The difference between these values equals the panning vector.
*
* When the user is panning, the translation of the camera is found by adding the panning vector to the
* translationNotCountingCurrentPanning.
*
* We could update the translation as the user moved the mouse but floating point drift (round-off error) could occur.
*/
export function translation(state: CameraState): Vector2 {
if (state.panning) {
return add(
Expand All @@ -101,6 +120,10 @@ export function translation(state: CameraState): Vector2 {
}
}

/**
* A matrix that when applied to a Vector2 converts it from screen coordinates to world coordinates.
* See https://en.wikipedia.org/wiki/Orthographic_projection
*/
export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state => {
const {
renderWidth,
Expand All @@ -109,7 +132,7 @@ export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state =>
clippingPlaneTop,
clippingPlaneLeft,
clippingPlaneBottom,
} = clippingPlane(state);
} = clippingPlanes(state);

const [translationX, translationY] = translation(state);

Expand Down Expand Up @@ -145,6 +168,12 @@ 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;

/**
* Whether or not the user is current panning the map.
*/
export const userIsPanning = (state: CameraState): boolean => state.panning !== undefined;
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ 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);
}

/**
* Used to assert that two Vector2s are close to each other (accounting for round-off errors.)
*/
export function expectVectorsToBeClose(first: Vector2, second: Vector2): void {
expect(first[0]).toBeCloseTo(second[0]);
expect(first[1]).toBeCloseTo(second[1]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,45 @@
import * as cameraSelectors from './camera/selectors';
import { ResolverState } from '../types';

/**
* A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates.
* See https://en.wikipedia.org/wiki/Orthographic_projection
*/
export const projectionMatrix = composeSelectors(
cameraStateSelector,
cameraSelectors.projectionMatrix
);

/**
* A matrix that when applied to a Vector2 converts it from screen coordinates to world coordinates.
* See https://en.wikipedia.org/wiki/Orthographic_projection
*/
export const inverseProjectionMatrix = composeSelectors(
cameraStateSelector,
cameraSelectors.inverseProjectionMatrix
);

/**
* The scale by which world values are scaled when rendered.
*/
export const scale = composeSelectors(cameraStateSelector, cameraSelectors.scale);

/**
* Whether or not the user is current panning the map.
*/
export const userIsPanning = composeSelectors(cameraStateSelector, cameraSelectors.userIsPanning);

/**
* Returns the camera state from within ResolverState
*/
function cameraStateSelector(state: ResolverState) {
return state.camera;
}

/**
* Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a
* concern-specific selector. `selector` should return the concern-specific state.
*/
function composeSelectors<OuterState, InnerState, ReturnValue>(
selector: (state: OuterState) => InnerState,
secondSelector: (state: InnerState) => ReturnValue
Expand Down

0 comments on commit 0a2e8da

Please sign in to comment.