Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

style: use requestAnimationFrame for auto pan #71

Merged
merged 9 commits into from
Feb 1, 2018
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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed this to let as I was modifying this variable at some point in development, but now there is no reason for this change. Although it's no big deal, I should be using const.


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);
Copy link
Contributor Author

@auroranil auroranil Feb 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I was passing props around for scaleFactorMin and scaleFactorMax values, but now they are being passed through the value argument.

I forgot to remove these props arguments in zoom and stopZooming functions. That should not be there.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point about this change is that I'm not sure that set scaleFactorMin and scaleFactorMax in the value object is the right way to handle this two props. These are two statics props and probably they don't change in the component lifecycle. Talking about refactor I think that the previous way to pass them was better.

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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs the auto pan loop when component mounts, whether or not the user is actually auto panning. We should refactor the code so that auto pan loop only runs when the user hovers on one or more of the auto pan regions.

I have not thoroughly check the performance metrics, so we should consider optimising the code if the performance drop is too large.

Copy link
Owner

@chrvadala chrvadala Feb 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see... to handle this we can use the mouseEnter and mouseLeave callbacks

onMouseEnter={ event => {
if (detectTouch()) return;
let nextValue = onMouseEnterOrLeave(event, this.ViewerDOM, this.getTool(), this.getValue(), this.props);
if (this.getValue() !== nextValue) this.setValue(nextValue);
}}
onMouseLeave={ event => {
let nextValue = onMouseEnterOrLeave(event, this.ViewerDOM, this.getTool(), this.getValue(), this.props);
if (this.getValue() !== nextValue) this.setValue(nextValue);
}}
but I think that it isn't really necessary. I suppose that usually there's at most one running instance of it (2, 3 in some complex situations). It should not cause a performance drop.

}

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