diff --git a/packages/vx-demo/components/gallery.js b/packages/vx-demo/components/gallery.js index 71e71aa3d..c5936085d 100644 --- a/packages/vx-demo/components/gallery.js +++ b/packages/vx-demo/components/gallery.js @@ -42,6 +42,7 @@ import LinkTypes from './tiles/linkTypes'; import Threshold from './tiles/threshold'; import Chord from './tiles/chord'; import Polygons from './tiles/polygons'; +import ZoomI from './tiles/zoom-i'; const items = [ '#242424', @@ -903,7 +904,23 @@ export default class Gallery extends React.Component { -
+ + +
+
+ + {({ width, height }) => } + +
+
+
Zoom I
+
+
{''}
+
+
+
+ +
diff --git a/packages/vx-demo/components/tiles/zoom-i.js b/packages/vx-demo/components/tiles/zoom-i.js new file mode 100644 index 000000000..79cd00866 --- /dev/null +++ b/packages/vx-demo/components/tiles/zoom-i.js @@ -0,0 +1,218 @@ +import React from 'react'; +import { Zoom } from '@vx/zoom'; +import { localPoint } from '@vx/event'; +import { RectClipPath } from '@vx/clip-path'; +import { genPhyllotaxis } from '@vx/mock-data'; +import { scaleLinear } from '@vx/scale'; +import { interpolateRainbow } from 'd3-scale-chromatic'; + +const bg = '#0a0a0a'; +const points = [...new Array(1000)]; + +const colorScale = scaleLinear({ range: [0, 1], domain: [0, 1000] }); +const sizeScale = scaleLinear({ domain: [0, 600], range: [0.5, 8] }); + +const initialTransform = { + scaleX: 1.27, + scaleY: 1.27, + translateX: -211.62, + translateY: 162.59, + skewX: 0, + skewY: 0 +}; + +export default class ZoomI extends React.Component { + constructor(props) { + super(props); + this.state = { showMiniMap: true }; + this.toggleMiniMap = this.toggleMiniMap.bind(this); + } + + toggleMiniMap() { + this.setState(prevState => { + return { + showMiniMap: !prevState.showMiniMap + }; + }); + } + + render() { + const { width, height } = this.props; + const { showMiniMap } = this.state; + + const gen = genPhyllotaxis({ radius: 10, width, height }); + const phyllotaxis = points.map((d, i) => gen(i)); + + return ( + + + {zoom => { + return ( +
+ + + + + {phyllotaxis.map((point, i) => { + return ( + + 500 ? sizeScale(1000 - i) : sizeScale(i)} + fill={interpolateRainbow(colorScale(i))} + /> + + ); + })} + + { + if (!zoom.isDragging) return; + zoom.dragEnd(); + }} + onDoubleClick={event => { + const point = localPoint(event); + zoom.scale({ scaleX: 1.1, scaleY: 1.1, point }); + }} + /> + {showMiniMap && ( + + + {phyllotaxis.map((d, i) => { + const { x, y } = d; + return ( + + 500 ? sizeScale(1000 - i) : sizeScale(i)} + fill={interpolateRainbow(colorScale(i))} + /> + + ); + })} + + + )} + +
+ + + + + +
+
+ +
+
+ ); + }} +
+
+ Based on Mike Bostock's{' '} + Pan & Zoom III +
+ +
+ ); + } +} diff --git a/packages/vx-demo/package.json b/packages/vx-demo/package.json index dbb23a045..c5e68d4e8 100644 --- a/packages/vx-demo/package.json +++ b/packages/vx-demo/package.json @@ -47,6 +47,7 @@ "@vx/threshold": "0.0.184", "@vx/tooltip": "0.0.184", "@vx/voronoi": "0.0.183", + "@vx/zoom": "0.0.182", "classnames": "^2.2.5", "d3-array": "^1.1.1", "d3-collection": "^1.0.4", @@ -54,6 +55,7 @@ "d3-hierarchy": "^1.1.4", "d3-interpolate": "^1.1.5", "d3-scale": "^1.0.6", + "d3-scale-chromatic": "^1.3.3", "d3-shape": "^1.0.6", "d3-time-format": "^2.0.5", "next": "^4.1.3", diff --git a/packages/vx-demo/pages/zoom-i.js b/packages/vx-demo/pages/zoom-i.js new file mode 100644 index 000000000..05ff9ccfc --- /dev/null +++ b/packages/vx-demo/pages/zoom-i.js @@ -0,0 +1,176 @@ +import React from 'react'; +import Show from '../components/show'; +import ZoomI from '../components/tiles/zoom-i'; + +export default () => { + return ( + + {`import React from 'react'; +import { Zoom } from '@vx/zoom'; +import { localPoint } from '@vx/event'; +import { RectClipPath } from '@vx/clip-path'; +import { genPhyllotaxis } from '@vx/mock-data'; +import { scaleLinear } from '@vx/scale'; +import { interpolateRainbow } from 'd3-scale-chromatic'; + +const bg = '#0a0a0a'; +const points = [...new Array(1000)]; + +const colorScale = scaleLinear({ range: [0, 1], domain: [0, 1000] }); +const sizeScale = scaleLinear({ domain: [0, 600], range: [0.5, 8] }); + +const initialTransform = { + scaleX: 1.27, + scaleY: 1.27, + translateX: -211.62, + translateY: 162.59, + skewX: 0, + skewY: 0 +}; + +class ZoomDemo extends React.Component { + constructor(props) { + super(props); + this.state = { showMiniMap: true }; + this.toggleMiniMap = this.toggleMiniMap.bind(this); + } + + toggleMiniMap() { + this.setState(prevState => { + return { + showMiniMap: !prevState.showMiniMap + }; + }); + } + + render() { + const { width, height } = this.props; + const { showMiniMap } = this.state; + + const gen = genPhyllotaxis({ radius: 10, width, height }); + const phyllotaxis = points.map((d, i) => gen(i)); + + return ( + + {zoom => { + return ( +
+ + + + + {phyllotaxis.map((point, i) => { + return ( + + 500 ? sizeScale(1000 - i) : sizeScale(i)} + fill={interpolateRainbow(colorScale(i))} + /> + + ); + })} + + { + if (!zoom.isDragging) return; + zoom.dragEnd(); + }} + onDoubleClick={event => { + const point = localPoint(event); + zoom.scale({ scaleX: 1.1, scaleY: 1.1, point }); + }} + /> + {showMiniMap && ( + + + {phyllotaxis.map((d, i) => { + const { x, y } = d; + return ( + + 500 ? sizeScale(1000 - i) : sizeScale(i)} + fill={interpolateRainbow(colorScale(i))} + /> + + ); + })} + + + )} + +
+ + + + + +
+
+ +
+
+ ); + }} +
+ ); + } +}`} +
+ ); +}; diff --git a/packages/vx-demo/static/docs/vx-zoom.html b/packages/vx-demo/static/docs/vx-zoom.html index feeb05e9f..463b640ea 100644 --- a/packages/vx-demo/static/docs/vx-zoom.html +++ b/packages/vx-demo/static/docs/vx-zoom.html @@ -33,9 +33,63 @@
-

@vx/zoom

+

@vx/Zoom

+

+ +

+

Installation

npm install --save @vx/zoom
+

Components

+ +

API

+

<Zoom />

+ + + +

# Zoom.children<func> required

+

# Zoom.constrain<func>

+

By default constrain() will only constrain scale values. To change +constraints you can pass in your own constrain function as a prop.

+

For example, if you wanted to constrain your view to within [[0, 0], [width, height]]:

+
function constrain(transformMatrix, prevTransformMatrix) {
+  const min = applyMatrixToPoint(transformMatrix, { x: 0, y: 0 });
+  const max = applyMatrixToPoint(transformMatrix, { x: width, y: height });
+  if (max.x < width || max.y < height) {
+    return prevTransformMatrix;
+  }
+  if (min.x > 0 || min.y > 0) {
+    return prevTransformMatrix;
+  }
+  return transformMatrix;
+}
 
+

@param {matrix} transformMatrix +@param {matrix} prevTransformMatrix +@returns {martix}

+

# Zoom.height<number> required

+

# Zoom.scaleXMax<number>
DefaultInfinity

+

# Zoom.scaleXMin<number>
Default0

+

# Zoom.scaleYMax<number>
DefaultInfinity

+

# Zoom.scaleYMin<number>
Default0

+

# Zoom.transformMatrix<shape[object Object]>
Default{ + scaleX: 1, + scaleY: 1, + translateX: 0, + translateY: 0, + skewX: 0, + skewY: 0 +}

+

# Zoom.wheelDelta<func>

+
 wheelDelta(event.deltaY)
+
+

A function that returns {scaleX,scaleY} factors to scale the matrix by. +Scale factors greater than 1 will increase (zoom in), less than 1 will descrease (zoom out).
Defaultevent => { + return -event.deltaY > 0 ? { scaleX: 1.1, scaleY: 1.1 } : { scaleX: 0.9, scaleY: 0.9 }; +}

+

# Zoom.width<number> required

+
diff --git a/packages/vx-mock-data/src/generators/genPhyllotaxis.js b/packages/vx-mock-data/src/generators/genPhyllotaxis.js new file mode 100644 index 000000000..a6537a491 --- /dev/null +++ b/packages/vx-mock-data/src/generators/genPhyllotaxis.js @@ -0,0 +1,11 @@ +export default function genPhyllotaxis({ radius, width, height }) { + const theta = Math.PI * (3 - Math.sqrt(5)); + return i => { + const r = radius * Math.sqrt(i); + const a = theta * i; + return { + x: width / 2 + r * Math.cos(a), + y: height / 2 + r * Math.sin(a) + }; + }; +} diff --git a/packages/vx-mock-data/src/index.js b/packages/vx-mock-data/src/index.js index aeff68674..f7e951011 100644 --- a/packages/vx-mock-data/src/index.js +++ b/packages/vx-mock-data/src/index.js @@ -2,6 +2,7 @@ export { default as genDateValue } from './generators/genDateValue'; export { default as genRandomNormalPoints } from './generators/genRandomNormalPoints'; export { default as genBin } from './generators/genBin'; export { default as genBins } from './generators/genBins'; +export { default as genPhyllotaxis } from './generators/genPhyllotaxis'; export { default as genStats } from './generators/genStats'; export { default as appleStock } from './mocks/appleStock'; export { default as letterFrequency } from './mocks/letterFrequency'; diff --git a/packages/vx-mock-data/test/genPhyllotaxis.test.js b/packages/vx-mock-data/test/genPhyllotaxis.test.js new file mode 100644 index 000000000..0bfb32081 --- /dev/null +++ b/packages/vx-mock-data/test/genPhyllotaxis.test.js @@ -0,0 +1,28 @@ +import { genPhyllotaxis } from '../src'; + +describe('generators/genPhyllotaxis', () => { + test('it should be defined', () => { + expect(genPhyllotaxis).toBeDefined(); + }); + + test('it should return a function', () => { + const pointFn = genPhyllotaxis({ + radius: 10, + width: 200, + height: 200 + }); + expect(typeof pointFn).toEqual('function'); + }); + + test('it should return a point [x, y] when calling the returned function', () => { + const pointFn = genPhyllotaxis({ + radius: 10, + width: 200, + height: 200 + }); + const point = pointFn(3); + const expected = { x: 110, y: 113 }; + expect(Math.floor(point.x)).toEqual(expected.x); + expect(Math.floor(point.y)).toEqual(expected.y); + }); +}); diff --git a/packages/vx-zoom/Readme.md b/packages/vx-zoom/Readme.md index 4911266be..9e9ba7ade 100644 --- a/packages/vx-zoom/Readme.md +++ b/packages/vx-zoom/Readme.md @@ -1,5 +1,86 @@ -# @vx/zoom +# @vx/Zoom + + + + + + +## Installation ``` npm install --save @vx/zoom -``` \ No newline at end of file +``` + + +## Components + + + + - [Zoom](#zoom-) + +## API + + + +

<Zoom />

+ + + +# *Zoom*.**children**<func> `required` + +# *Zoom*.**constrain**<func> + +By default constrain() will only constrain scale values. To change +constraints you can pass in your own constrain function as a prop. + +For example, if you wanted to constrain your view to within [[0, 0], [width, height]]: + +```js +function constrain(transformMatrix, prevTransformMatrix) { + const min = applyMatrixToPoint(transformMatrix, { x: 0, y: 0 }); + const max = applyMatrixToPoint(transformMatrix, { x: width, y: height }); + if (max.x < width || max.y < height) { + return prevTransformMatrix; + } + if (min.x > 0 || min.y > 0) { + return prevTransformMatrix; + } + return transformMatrix; +} +``` + +@param {matrix} transformMatrix +@param {matrix} prevTransformMatrix +@returns {martix} + +# *Zoom*.**height**<number> `required` + +# *Zoom*.**scaleXMax**<number>
DefaultInfinity
+ +# *Zoom*.**scaleXMin**<number>
Default0
+ +# *Zoom*.**scaleYMax**<number>
DefaultInfinity
+ +# *Zoom*.**scaleYMin**<number>
Default0
+ +# *Zoom*.**transformMatrix**<shape[object Object]>
Default{ + scaleX: 1, + scaleY: 1, + translateX: 0, + translateY: 0, + skewX: 0, + skewY: 0 +}
+ +# *Zoom*.**wheelDelta**<func> + +```js + wheelDelta(event.deltaY) +``` + +A function that returns {scaleX,scaleY} factors to scale the matrix by. +Scale factors greater than 1 will increase (zoom in), less than 1 will descrease (zoom out).
Defaultevent => { + return -event.deltaY > 0 ? { scaleX: 1.1, scaleY: 1.1 } : { scaleX: 0.9, scaleY: 0.9 }; +}
+ +# *Zoom*.**width**<number> `required` diff --git a/packages/vx-zoom/docs/api.md b/packages/vx-zoom/docs/api.md new file mode 100644 index 000000000..2283deac2 --- /dev/null +++ b/packages/vx-zoom/docs/api.md @@ -0,0 +1,62 @@ +

<Zoom />

+ + + +# *Zoom*.**children**<func> `required` + +# *Zoom*.**constrain**<func> + +By default constrain() will only constrain scale values. To change +constraints you can pass in your own constrain function as a prop. + +For example, if you wanted to constrain your view to within [[0, 0], [width, height]]: + +```js +function constrain(transformMatrix, prevTransformMatrix) { + const min = applyMatrixToPoint(transformMatrix, { x: 0, y: 0 }); + const max = applyMatrixToPoint(transformMatrix, { x: width, y: height }); + if (max.x < width || max.y < height) { + return prevTransformMatrix; + } + if (min.x > 0 || min.y > 0) { + return prevTransformMatrix; + } + return transformMatrix; +} +``` + +@param {matrix} transformMatrix +@param {matrix} prevTransformMatrix +@returns {martix} + +# *Zoom*.**height**<number> `required` + +# *Zoom*.**scaleXMax**<number>
DefaultInfinity
+ +# *Zoom*.**scaleXMin**<number>
Default0
+ +# *Zoom*.**scaleYMax**<number>
DefaultInfinity
+ +# *Zoom*.**scaleYMin**<number>
Default0
+ +# *Zoom*.**transformMatrix**<shape[object Object]>
Default{ + scaleX: 1, + scaleY: 1, + translateX: 0, + translateY: 0, + skewX: 0, + skewY: 0 +}
+ +# *Zoom*.**wheelDelta**<func> + +```js + wheelDelta(event.deltaY) +``` + +A function that returns {scaleX,scaleY} factors to scale the matrix by. +Scale factors greater than 1 will increase (zoom in), less than 1 will descrease (zoom out).
Defaultevent => { + return -event.deltaY > 0 ? { scaleX: 1.1, scaleY: 1.1 } : { scaleX: 0.9, scaleY: 0.9 }; +}
+ +# *Zoom*.**width**<number> `required` diff --git a/packages/vx-zoom/docs/description.md b/packages/vx-zoom/docs/description.md new file mode 100644 index 000000000..2880ad8e0 --- /dev/null +++ b/packages/vx-zoom/docs/description.md @@ -0,0 +1,5 @@ +# @vx/Zoom + + + + diff --git a/packages/vx-zoom/docs/docs.md b/packages/vx-zoom/docs/docs.md new file mode 100644 index 000000000..9e9ba7ade --- /dev/null +++ b/packages/vx-zoom/docs/docs.md @@ -0,0 +1,86 @@ +# @vx/Zoom + + + + + + +## Installation + +``` +npm install --save @vx/zoom +``` + + +## Components + + + + - [Zoom](#zoom-) + +## API + + + +

<Zoom />

+ + + +# *Zoom*.**children**<func> `required` + +# *Zoom*.**constrain**<func> + +By default constrain() will only constrain scale values. To change +constraints you can pass in your own constrain function as a prop. + +For example, if you wanted to constrain your view to within [[0, 0], [width, height]]: + +```js +function constrain(transformMatrix, prevTransformMatrix) { + const min = applyMatrixToPoint(transformMatrix, { x: 0, y: 0 }); + const max = applyMatrixToPoint(transformMatrix, { x: width, y: height }); + if (max.x < width || max.y < height) { + return prevTransformMatrix; + } + if (min.x > 0 || min.y > 0) { + return prevTransformMatrix; + } + return transformMatrix; +} +``` + +@param {matrix} transformMatrix +@param {matrix} prevTransformMatrix +@returns {martix} + +# *Zoom*.**height**<number> `required` + +# *Zoom*.**scaleXMax**<number>
DefaultInfinity
+ +# *Zoom*.**scaleXMin**<number>
Default0
+ +# *Zoom*.**scaleYMax**<number>
DefaultInfinity
+ +# *Zoom*.**scaleYMin**<number>
Default0
+ +# *Zoom*.**transformMatrix**<shape[object Object]>
Default{ + scaleX: 1, + scaleY: 1, + translateX: 0, + translateY: 0, + skewX: 0, + skewY: 0 +}
+ +# *Zoom*.**wheelDelta**<func> + +```js + wheelDelta(event.deltaY) +``` + +A function that returns {scaleX,scaleY} factors to scale the matrix by. +Scale factors greater than 1 will increase (zoom in), less than 1 will descrease (zoom out).
Defaultevent => { + return -event.deltaY > 0 ? { scaleX: 1.1, scaleY: 1.1 } : { scaleX: 0.9, scaleY: 0.9 }; +}
+ +# *Zoom*.**width**<number> `required` diff --git a/packages/vx-zoom/docs/install.md b/packages/vx-zoom/docs/install.md new file mode 100644 index 000000000..4993b2bf7 --- /dev/null +++ b/packages/vx-zoom/docs/install.md @@ -0,0 +1,5 @@ +## Installation + +``` +npm install --save @vx/zoom +``` diff --git a/packages/vx-zoom/package.json b/packages/vx-zoom/package.json index ac373dc8e..6d6341e01 100644 --- a/packages/vx-zoom/package.json +++ b/packages/vx-zoom/package.json @@ -8,25 +8,19 @@ "scripts": { "build": "npm run build:babel && npm run build:dist", "build:dist": "rm -rf dist && mkdir dist && rollup -c", - "build:babel": "rm -rf build && mkdir build && babel src --out-dir build --ignore node_modules/ --presets @babel/preset-react,@babel/preset-env", + "build:babel": + "rm -rf build && mkdir build && babel src --out-dir build --ignore node_modules/ --presets @babel/preset-react,@babel/preset-env", "prepublish": "npm run build", - "test": "jest" + "test": "jest", + "docs": + "cd ./docs && ../../../node_modules/.bin/react-docgen --exclude ../src/index.js,../src/util ../src/ | ../../../scripts/buildDocs.sh" }, - "files": [ - "dist", - "build" - ], + "files": ["dist", "build"], "repository": { "type": "git", "url": "git+https://github.com/hshoff/vx.git" }, - "keywords": [ - "vx", - "react", - "d3", - "visualizations", - "charts" - ], + "keywords": ["vx", "react", "d3", "visualizations", "charts"], "author": "@hshoff", "license": "MIT", "bugs": { @@ -75,10 +69,14 @@ "rollup-plugin-replace": "^2.0.0", "rollup-plugin-uglify": "^4.0.0" }, + "peerDependencies": { + "react": "^15.0.0-0 || ^16.0.0-0" + }, "jest": { - "setupFiles": [ - "raf/polyfill", - "/test/enzyme-setup.js" - ] + "setupFiles": ["raf/polyfill", "/test/enzyme-setup.js"] + }, + "dependencies": { + "@vx/event": "0.0.182", + "prop-types": "^15.6.2" } } diff --git a/packages/vx-zoom/src/Zoom.js b/packages/vx-zoom/src/Zoom.js index 60b22e643..fbc63dc8c 100644 --- a/packages/vx-zoom/src/Zoom.js +++ b/packages/vx-zoom/src/Zoom.js @@ -1 +1,273 @@ -export default class Zoom {} +import React from 'react'; +import PropTypes from 'prop-types'; +import { localPoint } from '@vx/event'; +import { + composeMatrices, + inverseMatrix, + applyMatrixToPoint, + applyInverseMatrixToPoint, + translateMatrix, + identityMatrix, + scaleMatrix +} from './util/matrix'; + +class Zoom extends React.Component { + constructor(props) { + super(props); + + this.state = { + initialTransformMatrix: props.transformMatrix, + transformMatrix: props.transformMatrix, + isDragging: false + }; + + this.toString = this.toString.bind(this); + this.clear = this.clear.bind(this); + this.center = this.center.bind(this); + this.handleWheel = this.handleWheel.bind(this); + this.dragStart = this.dragStart.bind(this); + this.dragMove = this.dragMove.bind(this); + this.dragEnd = this.dragEnd.bind(this); + this.reset = this.reset.bind(this); + this.constrain = props.constrain ? props.constrain.bind(this) : this.constrain.bind(this); + this.scale = this.scale.bind(this); + this.translate = this.translate.bind(this); + this.translateTo = this.translateTo.bind(this); + this.setTranslate = this.setTranslate.bind(this); + this.setTransformMatrix = this.setTransformMatrix.bind(this); + this.invert = this.invert.bind(this); + this.applyToPoint = this.applyToPoint.bind(this); + this.applyInverseToPoint = this.applyInverseToPoint.bind(this); + this.toStringInvert = this.toStringInvert.bind(this); + } + + applyToPoint({ x, y }) { + const { transformMatrix } = this.state; + return applyMatrixToPoint(transformMatrix, { x, y }); + } + + applyInverseToPoint({ x, y }) { + const { transformMatrix } = this.state; + return applyInverseMatrixToPoint(transformMatrix, { x, y }); + } + + reset() { + const { initialTransformMatrix } = this.state; + this.setTransformMatrix(initialTransformMatrix); + } + + scale({ scaleX, scaleY, point }) { + if (!scaleY) scaleY = scaleX; + const { transformMatrix } = this.state; + const { width, height } = this.props; + point = point || { x: width / 2, y: height / 2 }; + const translate = applyInverseMatrixToPoint(transformMatrix, point); + const nextMatrix = composeMatrices( + transformMatrix, + translateMatrix(translate.x, translate.y), + scaleMatrix(scaleX, scaleY), + translateMatrix(-translate.x, -translate.y) + ); + this.setTransformMatrix(nextMatrix); + } + + translate({ translateX, translateY }) { + const { transformMatrix } = this.state; + const nextMatrix = composeMatrices(transformMatrix, translateMatrix(translateX, translateY)); + this.setTransformMatrix(nextMatrix); + } + + translateTo({ x, y }) { + const { transformMatrix } = this.state; + const point = applyInverseMatrixToPoint(transformMatrix, { x, y }); + this.setTranslate({ translateX: point.x, translateY: point.y }); + } + + setTranslate({ translateX, translateY }) { + const { transformMatrix } = this.state; + const nextMatrix = { + ...transformMatrix, + translateX, + translateY + }; + this.setTransformMatrix(nextMatrix); + } + + setTransformMatrix(transformMatrix) { + this.setState(prevState => { + return { transformMatrix: this.constrain(transformMatrix, prevState.transformMatrix) }; + }); + } + + invert() { + return inverseMatrix(this.state.transformMatrix); + } + + toStringInvert() { + const { translateX, translateY, scaleX, scaleY, skewX, skewY } = this.invert(); + return `matrix(${scaleX}, ${skewY}, ${skewX}, ${scaleY}, ${translateX}, ${translateY})`; + } + + constrain(transformMatrix, prevTransformMatrix) { + const { scaleXMin, scaleXMax, scaleYMin, scaleYMax, constrain } = this.props; + const { scaleX, scaleY } = transformMatrix; + const shouldConstrainScaleX = scaleX > scaleXMax || scaleX < scaleXMin; + const shouldConstrainScaleY = scaleY > scaleYMax || scaleY < scaleYMin; + + if (shouldConstrainScaleX || shouldConstrainScaleY) { + return prevTransformMatrix; + } + return transformMatrix; + } + + dragStart(event) { + const { transformMatrix } = this.state; + const { translateX, translateY } = transformMatrix; + this.startPoint = localPoint(event); + this.startTranslate = { translateX, translateY }; + this.setState({ isDragging: true }); + } + + dragMove(event) { + if (!this.state.isDragging) return; + const currentPoint = localPoint(event); + const dx = -(this.startPoint.x - currentPoint.x); + const dy = -(this.startPoint.y - currentPoint.y); + this.setTranslate({ + translateX: this.startTranslate.translateX + dx, + translateY: this.startTranslate.translateY + dy + }); + } + + dragEnd(event) { + this.startPoint = undefined; + this.startTranslate = undefined; + this.setState({ isDragging: false }); + } + + handleWheel(event) { + event.preventDefault(); + const { wheelDelta } = this.props; + const point = localPoint(event); + const { scaleX, scaleY } = wheelDelta(event); + this.scale({ scaleX, scaleY, point }); + } + + toString() { + const { transformMatrix } = this.state; + const { translateX, translateY, scaleX, scaleY, skewX, skewY } = transformMatrix; + return `matrix(${scaleX}, ${skewY}, ${skewX}, ${scaleY}, ${translateX}, ${translateY})`; + } + + center() { + const { width, height } = this.props; + const center = { x: width / 2, y: height / 2 }; + const inverseCentroid = this.applyInverseToPoint(center); + this.translate({ + translateX: inverseCentroid.x - center.x, + translateY: inverseCentroid.y - center.y + }); + } + + clear() { + this.setTransformMatrix(identityMatrix()); + } + + render() { + const { children } = this.props; + const zoom = { + ...this.state, + center: this.center, + clear: this.clear, + scale: this.scale, + scaleTo: this.scaleTo, + translate: this.translate, + translateTo: this.translateTo, + setTranslate: this.setTranslate, + setTransformMatrix: this.setTransformMatrix, + reset: this.reset, + handleWheel: this.handleWheel, + dragEnd: this.dragEnd, + dragMove: this.dragMove, + dragStart: this.dragStart, + toString: this.toString, + invert: this.invert, + toStringInvert: this.toStringInvert, + applyToPoint: this.applyToPoint, + applyInverseToPoint: this.applyInverseToPoint + }; + return children(zoom); + } +} + +Zoom.propTypes = { + children: PropTypes.func.isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + /** + * ```js + * wheelDelta(event.deltaY) + * ``` + * + * A function that returns {scaleX,scaleY} factors to scale the matrix by. + * Scale factors greater than 1 will increase (zoom in), less than 1 will descrease (zoom out). + */ + wheelDelta: PropTypes.func, + scaleXMin: PropTypes.number, + scaleXMax: PropTypes.number, + scaleYMin: PropTypes.number, + scaleYMax: PropTypes.number, + /** + * By default constrain() will only constrain scale values. To change + * constraints you can pass in your own constrain function as a prop. + * + * For example, if you wanted to constrain your view to within [[0, 0], [width, height]]: + * + * ```js + * function constrain(transformMatrix, prevTransformMatrix) { + * const min = applyMatrixToPoint(transformMatrix, { x: 0, y: 0 }); + * const max = applyMatrixToPoint(transformMatrix, { x: width, y: height }); + * if (max.x < width || max.y < height) { + * return prevTransformMatrix; + * } + * if (min.x > 0 || min.y > 0) { + * return prevTransformMatrix; + * } + * return transformMatrix; + * } + * ``` + * + * @param {matrix} transformMatrix + * @param {matrix} prevTransformMatrix + * @returns {martix} + */ + constrain: PropTypes.func, + transformMatrix: PropTypes.shape({ + scaleX: PropTypes.number, + scaleY: PropTypes.number, + translateX: PropTypes.number, + translateY: PropTypes.number, + skewX: PropTypes.number, + skewY: PropTypes.number + }) +}; + +Zoom.defaultProps = { + scaleXMin: 0, + scaleXMax: Infinity, + scaleYMin: 0, + scaleYMax: Infinity, + transformMatrix: { + scaleX: 1, + scaleY: 1, + translateX: 0, + translateY: 0, + skewX: 0, + skewY: 0 + }, + wheelDelta: event => { + return -event.deltaY > 0 ? { scaleX: 1.1, scaleY: 1.1 } : { scaleX: 0.9, scaleY: 0.9 }; + } +}; + +export default Zoom; diff --git a/packages/vx-zoom/src/index.js b/packages/vx-zoom/src/index.js index ae5b24e9c..57fad4d99 100644 --- a/packages/vx-zoom/src/index.js +++ b/packages/vx-zoom/src/index.js @@ -1 +1,12 @@ export { default as Zoom } from './Zoom'; +export { + identityMatrix, + createMatrix, + inverseMatrix, + applyMatrixToPoint, + applyInverseMatrixToPoint, + scaleMatrix, + translateMatrix, + multiplyMatrices, + composeMatrices +} from './util/matrix'; diff --git a/packages/vx-zoom/src/util/matrix.js b/packages/vx-zoom/src/util/matrix.js new file mode 100644 index 000000000..de99ba614 --- /dev/null +++ b/packages/vx-zoom/src/util/matrix.js @@ -0,0 +1,90 @@ +/* eslint-disable no-trailing-spaces */ +/* eslint-disable no-case-declarations */ +export function identityMatrix() { + return { + scaleX: 1, + scaleY: 1, + translateX: 0, + translateY: 0, + skewX: 0, + skewY: 0 + }; +} + +export function createMatrix({ + scaleX = 1, + scaleY = 1, + translateX = 0, + translateY = 0, + skewX = 0, + skewY = 0 +}) { + return { + scaleX, + scaleY, + translateX, + translateY, + skewX, + skewY + }; +} + +export function inverseMatrix({ scaleX, scaleY, translateX, translateY, skewX, skewY }) { + const denominator = scaleX * scaleY - skewY * skewX; + return { + scaleX: scaleY / denominator, + scaleY: scaleX / denominator, + translateX: (scaleY * translateX - skewX * translateY) / -denominator, + translateY: (skewY * translateX - scaleX * translateY) / denominator, + skewX: skewX / -denominator, + skewY: skewY / -denominator + }; +} + +export function applyMatrixToPoint(matrix, { x, y }) { + return { + x: matrix.scaleX * x + matrix.skewX * y + matrix.translateX, + y: matrix.skewY * x + matrix.scaleY * y + matrix.translateY + }; +} + +export function applyInverseMatrixToPoint(matrix, { x, y }) { + return applyMatrixToPoint(inverseMatrix(matrix), { x, y }); +} + +export function scaleMatrix(scaleX, scaleY = undefined) { + if (!scaleY) scaleY = scaleX; + return createMatrix({ scaleX, scaleY }); +} + +export function translateMatrix(translateX, translateY) { + return createMatrix({ translateX, translateY }); +} + +export function multiplyMatrices(matrix1, matrix2) { + return { + scaleX: matrix1.scaleX * matrix2.scaleX + matrix1.skewX * matrix2.skewY, + scaleY: matrix1.skewY * matrix2.skewX + matrix1.scaleY * matrix2.scaleY, + translateX: + matrix1.scaleX * matrix2.translateX + matrix1.skewX * matrix2.translateY + matrix1.translateX, + translateY: + matrix1.skewY * matrix2.translateX + matrix1.scaleY * matrix2.translateY + matrix1.translateY, + skewX: matrix1.scaleX * matrix2.skewX + matrix1.skewX * matrix2.scaleY, + skewY: matrix1.skewY * matrix2.scaleX + matrix1.scaleY * matrix2.skewY + }; +} + +export function composeMatrices(...matrices) { + switch (matrices.length) { + case 0: + throw new Error('composeMatrices() requires arguments: was called with no args'); + case 1: + return matrices[0]; + case 2: + return multiplyMatrices(matrices[0], matrices[1]); + default: + const [matrix1, matrix2, ...restMatrices] = matrices; + const matrix = multiplyMatrices(matrix1, matrix2); + return composeMatrices(matrix, ...restMatrices); + } +} diff --git a/packages/vx-zoom/test/Zoom.test.js b/packages/vx-zoom/test/Zoom.test.js index 9a968f0a6..ee6fbb521 100644 --- a/packages/vx-zoom/test/Zoom.test.js +++ b/packages/vx-zoom/test/Zoom.test.js @@ -1,7 +1,13 @@ -import { Zoom } from '../src'; +import { Zoom, inverseMatrix } from '../src'; describe('Zoom', () => { test('it should be defined', () => { expect(Zoom).toBeDefined(); }); }); + +describe('inverseMatrix', () => { + test('it should be defined', () => { + expect(inverseMatrix).toBeDefined(); + }); +});