Skip to content

Commit

Permalink
style: use requestAnimationFrame for auto pan (#71)
Browse files Browse the repository at this point in the history
* fix: NaN bug caused by not specifying default prop value

This occurs when scrolling with the mouse wheel.

* style: use requestAnimationFrame for auto pan

Delta values have been reduced so that the speed of the auto pan is roughly the same.

Using requestAnimationFrame provides smoother auto panning than using setInterval with a large interval.

* feat: set minimum and maximum zoom levels

* fix: documentation regarding with zoom level props

* fix: better zoom level limit support (mobile included)

* fix: forgot hard zoom limit for mobile devices
  • Loading branch information
auroranil authored and chrvadala committed Feb 1, 2018
1 parent 1555998 commit 7c2b47b
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 31 deletions.
6 changes: 4 additions & 2 deletions docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
| preventPanOutside | `true` | Boolean | User can't move the image outside the viewer |
| scaleFactor | `1.1` | Number | How much scale in or out (%) |
| scaleFactorOnWheel| `1.06` | Number | how much scale in or out on mouse wheel (requires `detectWheel` to be enabled) (%) |
| scaleFactorMax | - | Number | maximum amount of scale a user can zoom in to
| scaleFactorMin | - | Number | minimum amount of scale a user can zoom out of
| miniaturePosition | `left` | one of `none`, `right`, `left` | Miniature position |
| miniatureBackground | `#616264`| String | background of the miniature |
| miniatureWidth | `100` | Number | Miniature width (px) |
Expand All @@ -45,8 +47,8 @@
## Methods
|Method|Description|
|-----|------|
| `pan( SVGDeltaX, SVGDeltaY )` | Apply a pan |
| `zoom(SVGPointX, SVGPointY, scaleFactor)` | Zoom in or out the SVG |
| `pan(SVGDeltaX, SVGDeltaY)` | Apply a pan |
| `zoom(SVGPointX, SVGPointY, scaleFactor)` | Zoom in or out the SVG |
| `fitSelection(selectionSVGPointX, selectionSVGPointY, selectionWidth, selectionHeight)`| Fit an SVG area to viewer |
| `fitToViewer()` | Fit all SVG to Viewer |
| `setPointOnViewerCenter(SVGPointX, SVGPointY, zoomLevel)`| Set a point on Viewer center |
Expand Down
11 changes: 11 additions & 0 deletions src/features/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ export function setSVGSize(value, SVGWidth, SVGHeight) {
return set(value, {SVGWidth, SVGHeight});
}

/**
*
* @param value
* @param scaleFactorMin
* @param scaleFactorMax
* @returns {Object}
*/
export function setZoomLevels(value, scaleFactorMin, scaleFactorMax) {
return set(value, {scaleFactorMin, scaleFactorMax});
}

/**
*
* @param value
Expand Down
10 changes: 8 additions & 2 deletions src/features/interactions-touch.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from '../constants';
import {resetMode, getSVGPoint, set} from './common';
import {onMouseDown, onMouseMove, onMouseUp} from './interactions';
import {isZoomLevelGoingOutOfBounds, limitZoomLevel} from './zoom';

function hasPinchPointDistance(value) {
return typeof value.pinchPointDistance === 'number';
Expand All @@ -19,7 +20,12 @@ function onMultiTouch(event, ViewerDOM, tool, value, props) {
const pinchPointDistance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
const previousPointDistance = hasPinchPointDistance(value) ? value.pinchPointDistance : pinchPointDistance;
const svgPoint = getSVGPoint(value, (x1 + x2) / 2, (y1 + y2) / 2);
const distanceFactor = pinchPointDistance/previousPointDistance;
let distanceFactor = pinchPointDistance/previousPointDistance;

if (isZoomLevelGoingOutOfBounds(value, distanceFactor)) {
// Do not change translation and scale of value
return value;
}

if (event.cancelable) {
event.preventDefault();
Expand All @@ -34,7 +40,7 @@ function onMultiTouch(event, ViewerDOM, tool, value, props) {

return set(value, set({
mode: MODE_ZOOMING,
...matrix,
...limitZoomLevel(value, matrix),
startX: null,
startY: null,
endX: null,
Expand Down
12 changes: 6 additions & 6 deletions src/features/interactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function onMouseDown(event, ViewerDOM, tool, value, props, coords = null)
switch (tool) {
case TOOL_ZOOM_OUT:
let SVGPoint = getSVGPoint(value, x, y);
nextValue = zoom(value, SVGPoint.x, SVGPoint.y, 1 / props.scaleFactor);
nextValue = zoom(value, SVGPoint.x, SVGPoint.y, 1 / props.scaleFactor, props);
break;

case TOOL_ZOOM_IN:
Expand Down Expand Up @@ -65,7 +65,7 @@ export function onMouseMove(event, ViewerDOM, tool, value, props, coords = null)
switch (tool) {
case TOOL_ZOOM_IN:
if (value.mode === MODE_ZOOMING)
nextValue = forceExit ? stopZooming(value, x, y, props.scaleFactor) : updateZooming(value, x, y);
nextValue = forceExit ? stopZooming(value, x, y, props.scaleFactor, props) : updateZooming(value, x, y);
break;

case TOOL_AUTO:
Expand Down Expand Up @@ -97,12 +97,12 @@ export function onMouseUp(event, ViewerDOM, tool, value, props, coords = null) {
switch (tool) {
case TOOL_ZOOM_OUT:
if (value.mode === MODE_ZOOMING)
nextValue = stopZooming(value, x, y, 1 / props.scaleFactor);
nextValue = stopZooming(value, x, y, 1 / props.scaleFactor, props);
break;

case TOOL_ZOOM_IN:
if (value.mode === MODE_ZOOMING)
nextValue = stopZooming(value, x, y, props.scaleFactor);
nextValue = stopZooming(value, x, y, props.scaleFactor, props);
break;

case TOOL_AUTO:
Expand Down Expand Up @@ -138,7 +138,7 @@ export function onDoubleClick(event, ViewerDOM, tool, value, props, coords = nul
let modifierKeysReducer = (current, modifierKey) => current || event.getModifierState(modifierKey);
let modifierKeyActive = props.modifierKeys.reduce(modifierKeysReducer, false);
let scaleFactor = modifierKeyActive ? 1 / props.scaleFactor : props.scaleFactor;
nextValue = zoom(value, SVGPoint.x, SVGPoint.y, scaleFactor);
nextValue = zoom(value, SVGPoint.x, SVGPoint.y, scaleFactor, props);
}
break;

Expand Down Expand Up @@ -166,7 +166,7 @@ export function onWheel(event, ViewerDOM, tool, value, props, coords = null) {
let scaleFactor = mapRange(delta, -1, 1, props.scaleFactorOnWheel, 1 / props.scaleFactorOnWheel);

let SVGPoint = getSVGPoint(value, x, y);
let nextValue = zoom(value, SVGPoint.x, SVGPoint.y, scaleFactor);
let nextValue = zoom(value, SVGPoint.x, SVGPoint.y, scaleFactor, props);

event.preventDefault();
return nextValue;
Expand Down
8 changes: 4 additions & 4 deletions src/features/pan.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,10 @@ export function autoPanIfNeeded(value, viewerX, viewerY) {
let deltaX = 0;
let deltaY = 0;

if (viewerY <= 20) deltaY = 20;
if (value.viewerWidth - viewerX <= 20) deltaX = -20;
if (value.viewerHeight - viewerY <= 20) deltaY = -20;
if (viewerX <= 20) deltaX = 20;
if (viewerY <= 20) deltaY = 2;
if (value.viewerWidth - viewerX <= 20) deltaX = -2;
if (value.viewerHeight - viewerY <= 20) deltaY = -2;
if (viewerX <= 20) deltaX = 2;

deltaX = deltaX / value.d;
deltaY = deltaY / value.d;
Expand Down
60 changes: 53 additions & 7 deletions src/features/zoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,53 @@ import {MODE_IDLE, MODE_ZOOMING} from '../constants';
import {set, getSVGPoint} from './common';
import calculateBox from '../utils/calculateBox';

function lessThanScaleFactorMin (value, scaleFactor) {
return value.scaleFactorMin && (value.d * (scaleFactor)) <= value.scaleFactorMin;
}

function moreThanScaleFactorMax (value, scaleFactor) {
return value.scaleFactorMax && (value.d * scaleFactor) >= value.scaleFactorMax;
}

export function isZoomLevelGoingOutOfBounds(value, scaleFactor) {
return lessThanScaleFactorMin(value, scaleFactor) && scaleFactor < 1 || moreThanScaleFactorMax(value, scaleFactor) && scaleFactor > 1;
}

export function limitZoomLevel(value, matrix) {
let scaleLevel = matrix.a;

if(value.scaleFactorMin != null) {
// limit minimum zoom
scaleLevel = Math.max(scaleLevel, value.scaleFactorMin);
}

if(value.scaleFactorMax != null) {
// limit maximum zoom
scaleLevel = Math.min(scaleLevel, value.scaleFactorMax);
}

return set(matrix, {
a: scaleLevel,
d: scaleLevel
});
}

export function zoom(value, SVGPointX, SVGPointY, scaleFactor) {
if (isZoomLevelGoingOutOfBounds(value, scaleFactor)) {
// Do not change translation and scale of value
return value;
}

let matrix = transform(
const matrix = transform(
fromObject(value),
translate(SVGPointX, SVGPointY),
scale(scaleFactor, scaleFactor),
translate(-SVGPointX, -SVGPointY)
)
);

return set(value, {
mode: MODE_IDLE,
...matrix,
...limitZoomLevel(value, matrix),
startX: null,
startY: null,
endX: null,
Expand All @@ -31,14 +66,25 @@ export function fitSelection(value, selectionSVGPointX, selectionSVGPointY, sele

let scaleLevel = Math.min(scaleX, scaleY);

let matrix = transform(
const matrix = transform(
scale(scaleLevel, scaleLevel), //2
translate(-selectionSVGPointX, -selectionSVGPointY) //1
);

if(isZoomLevelGoingOutOfBounds(value, scaleLevel / value.d)) {
// Do not allow scale and translation
return set(value, {
mode: MODE_IDLE,
startX: null,
startY: null,
endX: null,
endY: null
});
}

return set(value, {
mode: MODE_IDLE,
...matrix,
...limitZoomLevel(value, matrix),
startX: null,
startY: null,
endX: null,
Expand Down Expand Up @@ -75,7 +121,7 @@ export function updateZooming(value, viewerX, viewerY) {
});
}

export function stopZooming(value, viewerX, viewerY, scaleFactor) {
export function stopZooming(value, viewerX, viewerY, scaleFactor, props) {
let {startX, startY, endX, endY} = value;

let start = getSVGPoint(value, startX, startY);
Expand All @@ -86,6 +132,6 @@ export function stopZooming(value, viewerX, viewerY, scaleFactor) {
return fitSelection(value, box.x, box.y, box.width, box.height);
} else {
let SVGPoint = getSVGPoint(value, viewerX, viewerY);
return zoom(value, SVGPoint.x, SVGPoint.y, scaleFactor);
return zoom(value, SVGPoint.x, SVGPoint.y, scaleFactor, props);
}
}
40 changes: 30 additions & 10 deletions src/viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import eventFactory from './events/event-factory';

//features
import {pan} from './features/pan';
import {getDefaultValue, setViewerSize, setSVGSize, setPointOnViewerCenter, reset} from './features/common';
import {getDefaultValue, setViewerSize, setSVGSize, setPointOnViewerCenter, reset, setZoomLevels} from './features/common';
import {
onMouseDown,
onMouseMove,
Expand Down Expand Up @@ -54,6 +54,8 @@ export default class ReactSVGPanZoom extends React.Component {
tool: tool ? tool : TOOL_NONE
};
this.ViewerDOM = null;

this.autoPanLoop = this.autoPanLoop.bind(this);
}

componentWillReceiveProps(nextProps) {
Expand All @@ -72,6 +74,11 @@ export default class ReactSVGPanZoom extends React.Component {
needUpdate = true;
}

if (value.scaleFactorMin !== nextProps.scaleFactorMin || value.scaleFactorMax !== nextProps.scaleFactorMax) {
nextValue = setZoomLevels(nextValue, nextProps.scaleFactorMin, nextProps.scaleFactorMax);
needUpdate = true;
}

if (needUpdate) {
this.setValue(nextValue);
}
Expand Down Expand Up @@ -181,23 +188,30 @@ export default class ReactSVGPanZoom extends React.Component {
onEventHandler(eventFactory(event, value, ViewerDOM));
}

autoPanLoop() {
let coords = {x: this.state.viewerX, y: this.state.viewerY};
let nextValue = onInterval(null, this.ViewerDOM, this.getTool(), this.getValue(), this.props, coords);

if (this.getValue() !== nextValue) {
this.setValue(nextValue);
}

if(this.autoPanIsRunning) {
requestAnimationFrame(this.autoPanLoop);
}
}


componentDidMount() {
let {props, state} = this;
if (props.onChangeValue) props.onChangeValue(state.value);

this.autoPanTimer = setInterval(() => {
let coords = {x: this.state.viewerX, y: this.state.viewerY};
let nextValue = onInterval(null, this.ViewerDOM, this.getTool(), this.getValue(), this.props, coords);

if (this.getValue() !== nextValue) {
this.setValue(nextValue);
}
}, 200);
this.autoPanIsRunning = true;
requestAnimationFrame(this.autoPanLoop);
}

componentWillUnmount() {
clearTimeout(this.autoPanTimer);
this.autoPanIsRunning = false;
}

render() {
Expand Down Expand Up @@ -465,6 +479,12 @@ ReactSVGPanZoom.propTypes = {
//how much scale in or out on mouse wheel (requires detectWheel enabled)
scaleFactorOnWheel: PropTypes.number,

// maximum amount of scale a user can zoom in to
scaleFactorMax: PropTypes.number,

// minimum amount of a scale a user can zoom out of
scaleFactorMin: PropTypes.number,

//current active tool (TOOL_NONE, TOOL_PAN, TOOL_ZOOM_IN, TOOL_ZOOM_OUT)
tool: PropTypes.oneOf([TOOL_AUTO, TOOL_NONE, TOOL_PAN, TOOL_ZOOM_IN, TOOL_ZOOM_OUT]),

Expand Down
3 changes: 3 additions & 0 deletions storybook/stories/ViewerStory.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ export default class MainStory extends Component {
onTouchStart={viewerTouchEventDecorator('onTouchStart')}
onTouchMove={noArgsDecorator('onTouchMove')}
onTouchEnd={viewerTouchEventDecorator('onTouchEnd')}

scaleFactorMin={number('scaleFactorMin', undefined)}
scaleFactorMax={number('scaleFactorMax', undefined)}
>

<svg width={1440} height={1440}>
Expand Down

0 comments on commit 7c2b47b

Please sign in to comment.