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

SVG viewbox support (2.0) #150

Merged
merged 18 commits into from
Sep 17, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/features/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
* Obtain default value
* @returns {Object}
*/
export function getDefaultValue(viewerWidth, viewerHeight, SVGWidth, SVGHeight, scaleFactorMin, scaleFactorMax) {
export function getDefaultValue(viewerWidth, viewerHeight, SVGViewBoxX, SVGViewBoxY, SVGWidth, SVGHeight, scaleFactorMin, scaleFactorMax) {
return set({}, {
...identity(),
version: 2,
Expand All @@ -23,6 +23,8 @@ export function getDefaultValue(viewerWidth, viewerHeight, SVGWidth, SVGHeight,
prePinchMode: null,
viewerWidth,
viewerHeight,
SVGViewBoxX,
SVGViewBoxY,
SVGWidth,
SVGHeight,
scaleFactorMin,
Expand Down Expand Up @@ -112,12 +114,14 @@ export function setViewerSize(value, viewerWidth, viewerHeight) {
/**
*
* @param value
* @param SVGViewBoxX
* @param SVGViewBoxY
* @param SVGWidth
* @param SVGHeight
* @returns {Object}
*/
export function setSVGSize(value, SVGWidth, SVGHeight) {
return set(value, {SVGWidth, SVGHeight});
export function setSVGViewBox(value, SVGViewBoxX, SVGViewBoxY, SVGWidth, SVGHeight) {
return set(value, {SVGViewBoxX, SVGViewBoxY, SVGWidth, SVGHeight});
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/features/pan.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export function pan(value, SVGDeltaX, SVGDeltaY, panLimit = undefined) {
// apply pan limits
if (panLimit) {
let [{x: x1, y: y1}, {x: x2, y: y2}] = applyToPoints(matrix, [
{x: panLimit, y: panLimit},
{x: value.SVGWidth - panLimit, y: value.SVGHeight - panLimit}
{x: value.SVGViewBoxX + panLimit, y: value.SVGViewBoxY + panLimit},
{x: value.SVGViewBoxX + value.SVGWidth - panLimit, y: value.SVGViewBoxY + value.SVGHeight - panLimit}
]);

//x limit
Expand Down
42 changes: 28 additions & 14 deletions src/features/zoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,35 +96,49 @@ export function fitSelection(value, selectionSVGPointX, selectionSVGPointY, sele
}

export function fitToViewer(value, SVGAlignX=ALIGN_LEFT, SVGAlignY=ALIGN_TOP) {
let {viewerWidth, viewerHeight, SVGWidth, SVGHeight} = value;
let {viewerWidth, viewerHeight, SVGViewBoxX, SVGViewBoxY, SVGWidth, SVGHeight} = value;

let scaleX = viewerWidth / SVGWidth;
let scaleY = viewerHeight / SVGHeight;
let scaleLevel = Math.min(scaleX, scaleY);

const scaleMatrix = scale(scaleLevel, scaleLevel);
let translationMatrix = translate(0, 0);

let translateX = -SVGViewBoxX * scaleX;
let translateY = -SVGViewBoxY * scaleY;

// after fitting, SVG and the viewer will match in width (1) or in height (2)
if (scaleX < scaleY) {
//(1) match in width, meaning scaled SVGHeight <= viewerHeight
let remainderY = viewerHeight - scaleX * SVGHeight;

if (SVGAlignY === ALIGN_CENTER)
translationMatrix = translate(0, Math.round(remainderY / 2));
if (SVGAlignY === ALIGN_BOTTOM)
translationMatrix = translate(0, remainderY);
}
else {
switch(SVGAlignY) {
case ALIGN_TOP:
translateY = -SVGViewBoxY * scaleLevel;
break;
case ALIGN_CENTER:
translateY = Math.round(remainderY / 2) - SVGViewBoxY * scaleLevel;
break;
case ALIGN_BOTTOM:
translateY = remainderY - SVGViewBoxY * scaleLevel;
break;
}
} else {
//(2) match in height, meaning scaled SVGWidth <= viewerWidth
let remainderX = viewerWidth - scaleY * SVGWidth;

if (SVGAlignX === ALIGN_CENTER)
translationMatrix = translate(Math.round(remainderX / 2), 0);
if (SVGAlignX === ALIGN_RIGHT)
translationMatrix = translate(remainderX, 0);
switch(SVGAlignX) {
case ALIGN_LEFT:
translateX = -SVGViewBoxX * scaleLevel;
break;
case ALIGN_CENTER:
translateX = Math.round(remainderX / 2) - SVGViewBoxX * scaleLevel;
break;
case ALIGN_RIGHT:
translateX = remainderX - SVGViewBoxX * scaleLevel;
break;
}
}

const translationMatrix = translate(translateX, translateY);
const matrix = transform(
translationMatrix, //2
scaleMatrix //1
Expand Down
12 changes: 7 additions & 5 deletions src/ui-miniature/miniature-mask.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ import RandomUID from "../utils/RandomUID";

const prefixID = 'react-svg-pan-zoom_miniature'

function MiniatureMask({SVGWidth, SVGHeight, x1, y1, x2, y2, zoomToFit, _uid}) {
let maskID = `${prefixID}_mask_${_uid}`
function MiniatureMask({SVGViewBoxX, SVGViewBoxY, SVGWidth, SVGHeight, x1, y1, x2, y2, zoomToFit, _uid}) {
const maskID = `${prefixID}_mask_${_uid}`

return (
<g>
<defs>
<mask id={maskID}>
<rect x="0" y="0" width={SVGWidth} height={SVGHeight} fill="#ffffff"/>
<rect x={SVGViewBoxX} y={SVGViewBoxY} width={SVGWidth} height={SVGHeight} fill="#ffffff"/>
<rect x={x1} y={y1} width={x2 - x1} height={y2 - y1}/>
</mask>
</defs>

<rect
x="0"
y="0"
x={SVGViewBoxX}
y={SVGViewBoxY}
width={SVGWidth}
height={SVGHeight}
style={{
Expand All @@ -34,6 +34,8 @@ function MiniatureMask({SVGWidth, SVGHeight, x1, y1, x2, y2, zoomToFit, _uid}) {
MiniatureMask.propTypes = {
SVGWidth: PropTypes.number.isRequired,
SVGHeight: PropTypes.number.isRequired,
SVGViewBoxX: PropTypes.number.isRequired,
SVGViewBoxY: PropTypes.number.isRequired,
x1: PropTypes.number.isRequired,
y1: PropTypes.number.isRequired,
x2: PropTypes.number.isRequired,
Expand Down
16 changes: 9 additions & 7 deletions src/ui-miniature/miniature.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const {min, max} = Math;
export default function Miniature(props) {

let {value, onChangeValue, children, position, background, SVGBackground, width: miniatureWidth, height: miniatureHeight} = props;
let {SVGWidth, SVGHeight, viewerWidth, viewerHeight} = value;
let {SVGViewBoxX, SVGViewBoxY, SVGWidth, SVGHeight, viewerWidth, viewerHeight} = value;

let ratio = SVGHeight / SVGWidth;

Expand Down Expand Up @@ -45,8 +45,8 @@ export default function Miniature(props) {
};

let centerTranslation = ratio >= 1
? `translate(${(miniatureWidth - (SVGWidth * zoomToFit)) / 2}, 0)`
: `translate(0, ${(miniatureHeight - (SVGHeight * zoomToFit)) / 2})`;
? `translate(${(miniatureWidth - (SVGWidth * zoomToFit)) / 2 - SVGViewBoxX * zoomToFit}, ${ - SVGViewBoxY * zoomToFit})`
: `translate(${ - SVGViewBoxX * zoomToFit}, ${(miniatureHeight - (SVGHeight * zoomToFit)) / 2 - SVGViewBoxY * zoomToFit})`;

return (
<div role="navigation" style={style}>
Expand All @@ -59,16 +59,18 @@ export default function Miniature(props) {

<rect
fill={SVGBackground}
x={0}
y={0}
width={value.SVGWidth}
height={value.SVGHeight}/>
x={SVGViewBoxX}
y={SVGViewBoxY}
width={SVGWidth}
height={SVGHeight}/>

{children}

<MiniatureMask
SVGWidth={SVGWidth}
SVGHeight={SVGHeight}
SVGViewBoxX={SVGViewBoxX}
SVGViewBoxY={SVGViewBoxY}
x1={x1}
y1={y1}
x2={x2}
Expand Down
58 changes: 41 additions & 17 deletions src/viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
isValueValid,
reset,
setPointOnViewerCenter,
setSVGSize,
setSVGViewBox,
setViewerSize,
setZoomLevels
} from './features/common';
Expand Down Expand Up @@ -63,14 +63,22 @@ export default class ReactSVGPanZoom extends React.Component {

constructor(props, context) {
const {value, width: viewerWidth, height: viewerHeight, scaleFactorMin, scaleFactorMax, children} = props;
const {width: SVGWidth, height: SVGHeight} = children.props;
const {withViewBox: SVGViewBox} = children.props;
let defaultValue;
if (SVGViewBox) {
const [SVGViewBoxX, SVGViewBoxY, SVGWidth, SVGHeight] = SVGViewBox.split(' ').map(parseFloat);
defaultValue = getDefaultValue(viewerWidth, viewerHeight, SVGViewBoxX, SVGViewBoxY, SVGWidth, SVGHeight, scaleFactorMin, scaleFactorMax)
} else {
const {width: SVGWidth, height: SVGHeight} = children.props;
defaultValue = getDefaultValue(viewerWidth, viewerHeight, 0, 0, SVGWidth, SVGHeight, scaleFactorMin, scaleFactorMax)
}

super(props, context);
this.ViewerDOM = null;
this.state = {
pointerX: null,
pointerY: null,
defaultValue: getDefaultValue(viewerWidth, viewerHeight, SVGWidth, SVGHeight, scaleFactorMin, scaleFactorMax)
defaultValue
}
this.autoPanLoop = this.autoPanLoop.bind(this);

Expand All @@ -91,6 +99,26 @@ export default class ReactSVGPanZoom extends React.Component {
printMigrationTipsRelatedToProps(props)
}

// This block checks the size of the SVG
const {withViewBox: SVGViewBox} = props.children.props;
if (SVGViewBox) {
// if the withViewBox prop is specified
const [x, y, width, height] = SVGViewBox.split(' ').map(parseFloat);

if(value.SVGViewBoxX !== x || value.SVGViewBoxY !== y || value.SVGWidth !== width || value.SVGHeight !== height) {
nextValue = setSVGViewBox(nextValue, x, y, width, height);
needUpdate = true;
}
} else {
// if the width and height props are specified
const {width: SVGWidth, height: SVGHeight} = props.children.props;
if (value.SVGWidth !== SVGWidth || value.SVGHeight !== SVGHeight) {
nextValue = setSVGViewBox(nextValue, 0, 0, SVGWidth, SVGHeight);
needUpdate = true;
}
}

// This block checks the size of the viewer
if (
prevProps.width !== props.width ||
prevProps.height !== props.height
Expand All @@ -99,16 +127,7 @@ export default class ReactSVGPanZoom extends React.Component {
needUpdate = true;
}

let {width: SVGWidth, height: SVGHeight} = props.children.props;
let {width: prevSVGWidth, height: prevSVGHeight} = prevProps.children.props;
if (
prevSVGWidth !== SVGWidth ||
prevSVGHeight !== SVGHeight
) {
nextValue = setSVGSize(nextValue, SVGWidth, SVGHeight);
needUpdate = true;
}

// This blocks checks the scale factors
if (
prevProps.scaleFactorMin !== props.scaleFactorMin ||
prevProps.scaleFactorMax !== props.scaleFactorMax
Expand Down Expand Up @@ -361,8 +380,8 @@ export default class ReactSVGPanZoom extends React.Component {
<rect
fill={this.props.SVGBackground}
style={this.props.SVGStyle}
x={0}
y={0}
x={value.SVGViewBoxX || 0}
y={value.SVGViewBoxY || 0}
width={value.SVGWidth}
height={value.SVGHeight}/>
<g>
Expand Down Expand Up @@ -444,6 +463,8 @@ ReactSVGPanZoom.propTypes = {
f: PropTypes.number.isRequired,
viewerWidth: PropTypes.number.isRequired,
viewerHeight: PropTypes.number.isRequired,
SVGViewBoxX: PropTypes.number.isRequired,
SVGViewBoxY: PropTypes.number.isRequired,
SVGWidth: PropTypes.number.isRequired,
SVGHeight: PropTypes.number.isRequired,
startX: PropTypes.number,
Expand Down Expand Up @@ -591,8 +612,11 @@ ReactSVGPanZoom.propTypes = {
' `' + types.join('`, `') + '`.'
);
}
if (!prop.props.hasOwnProperty('width') || !prop.props.hasOwnProperty('height')) {
return new Error('SVG should have props `width` and `height`');
if (
(!prop.props.hasOwnProperty('width') || !prop.props.hasOwnProperty('height')) &&
(!prop.props.hasOwnProperty('withViewBox'))
) {
return new Error('SVG should have props `width` and `height` or `withViewBox`');
}

}
Expand Down
55 changes: 55 additions & 0 deletions storybook/stories/ViewboxStory.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, {StrictMode} from 'react';
import {action} from '@storybook/addon-actions';
import {noArgsDecorator, viewerMouseEventDecorator} from './actions-decorator';

import {UncontrolledReactSVGPanZoom} from '../../src/index';
import {boolean} from "@storybook/addon-knobs";

export default class ViewboxStory extends React.Component {
constructor(props) {
super(props)
this.Viewer = null;
}

componentDidMount() {
this.Viewer.fitToViewer();
}

render() {
return (
<StrictMode>

<UncontrolledReactSVGPanZoom
width={400} height={400}

ref={Viewer => this.Viewer = Viewer}

onClick={viewerMouseEventDecorator('onClick')}

onChangeValue={noArgsDecorator('onChangeValue')}
onChangeTool={action('onChangeTool')}

detectAutoPan={boolean('detectAutoPan', false)}
detectWheel={boolean('detectWheel', false)}
detectPinchGesture={boolean('detectPinchGesture', false)}
>

<svg
width={100} height={100}
withViewBox="10 10 80 80"
>

<rect x="20" y="20" width="60" height="60" fill="yellow"/>
<circle cx="20" cy="20" r="4" fill="red"/>
<circle cx="80" cy="80" r="4" fill="red"/>

<circle cx="0" cy="0" r="4" fill="blue"/>
<circle cx="100" cy="100" r="4" fill="blue"/>

<circle cx="50" cy="50" r="2" fill="black"/>
</svg>
</UncontrolledReactSVGPanZoom>
</StrictMode>
)
}
}
2 changes: 2 additions & 0 deletions storybook/stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import AutosizerViewer from './AutosizerViewer'
import DifferentSizesStory from './DifferentSizesStory';
import RuntimeResizeStory from "./RuntimeResizeStory";
import UncontrolledViewerStory from "./UncontrolledViewerStory";
import ViewboxStory from './ViewboxStory'

storiesOf('React SVG Pan Zoom', module)
.addDecorator(withKnobs)
Expand All @@ -20,5 +21,6 @@ storiesOf('React SVG Pan Zoom', module)
.add('Autosizer viewer', () => <AutosizerViewer />)
.add('Different Sizes', () => <DifferentSizesStory />)
.add('Runtime Resize', () => <RuntimeResizeStory />)
.add('Viewbox prop', () => <ViewboxStory />)