diff --git a/packages/vx-zoom/package.json b/packages/vx-zoom/package.json index 73ee3fca7..f2cfb4f6a 100644 --- a/packages/vx-zoom/package.json +++ b/packages/vx-zoom/package.json @@ -5,6 +5,7 @@ "sideEffects": false, "main": "lib/index.js", "module": "esm/index.js", + "types": "lib/index.d.ts", "files": [ "lib", "esm" @@ -36,6 +37,7 @@ "react": "^15.0.0-0 || ^16.0.0-0" }, "dependencies": { + "@types/react": "*", "@vx/event": "0.0.192", "prop-types": "^15.6.2" } diff --git a/packages/vx-zoom/src/Zoom.jsx b/packages/vx-zoom/src/Zoom.tsx similarity index 59% rename from packages/vx-zoom/src/Zoom.jsx rename to packages/vx-zoom/src/Zoom.tsx index c8749e40d..50e78c56d 100644 --- a/packages/vx-zoom/src/Zoom.jsx +++ b/packages/vx-zoom/src/Zoom.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { localPoint } from '@vx/event'; import { composeMatrices, @@ -10,36 +9,113 @@ import { identityMatrix, scaleMatrix, } from './util/matrix'; +import { TransformMatrix, Point, Translate, Scale, ScaleSignature, ProvidedZoom } from './types'; -class Zoom extends React.Component { - constructor(props) { - super(props); +export type ZoomProps = { + /** Width of the zoom container. */ + width: number; + /** Height of the zoom container. */ + height: number; + /** + * ```js + * wheelDelta(event) + * ``` + * + * 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?: (event: React.WheelEvent | WheelEvent) => Scale; + /** Minimum x scale value for transform. */ + scaleXMin?: number; + /** Maximum x scale value for transform. */ + scaleXMax?: number; + /** Minimum y scale value for transform. */ + scaleYMin?: number; + /** Maximum y scale value for transform. */ + scaleYMax?: 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?: (transform: TransformMatrix, prevTransform: TransformMatrix) => TransformMatrix; + /** Initial transform matrix to apply. */ + transformMatrix?: TransformMatrix; + /** + * By default passive is `false`. This will wrap children in a
and add an active wheel + * event listener (handleWheel). `handleWheel()` will call `event.preventDefault()` before other + * execution. This prevents an outer parent from scrolling when the mouse wheel is used to zoom. + * + * When passive is `true` it is required to add `` to handle + * wheel events. **Note:** By default you do not need to add ``. + * This is only necessary when ``. + */ + passive?: boolean; + /** style object to apply to zoom div container. */ + style?: React.CSSProperties; + /** className to apply to zoom div container. */ + className?: string; + children: (zoom: ProvidedZoom & ZoomState) => React.ReactNode; +}; - this.state = { - initialTransformMatrix: props.transformMatrix, - transformMatrix: props.transformMatrix, - isDragging: false, - }; +type ZoomState = { + initialTransformMatrix: TransformMatrix; + transformMatrix: TransformMatrix; + isDragging: boolean; +}; - 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); - } +class Zoom extends React.Component { + static defaultProps = { + passive: false, + scaleXMin: 0, + scaleXMax: Infinity, + scaleYMin: 0, + scaleYMax: Infinity, + transformMatrix: { + scaleX: 1, + scaleY: 1, + translateX: 0, + translateY: 0, + skewX: 0, + skewY: 0, + }, + wheelDelta: (event: React.WheelEvent | WheelEvent) => { + return -event.deltaY > 0 ? { scaleX: 1.1, scaleY: 1.1 } : { scaleX: 0.9, scaleY: 0.9 }; + }, + style: undefined, + className: undefined, + }; + + containerRef: HTMLDivElement | null = null; + + startPoint: Point | undefined = undefined; + + startTranslate: Translate | undefined = undefined; + + state = { + initialTransformMatrix: this.props.transformMatrix!, + transformMatrix: this.props.transformMatrix!, + isDragging: false, + }; componentDidMount() { const { passive } = this.props; @@ -51,26 +127,26 @@ class Zoom extends React.Component { componentWillUnmount() { const { passive } = this.props; if (this.containerRef && !passive) { - this.containerRef.removeEventListener('wheel', this.handleWheel, { passive: false }); + this.containerRef.removeEventListener('wheel', this.handleWheel); } } - applyToPoint({ x, y }) { + applyToPoint = ({ x, y }: Point) => { const { transformMatrix } = this.state; return applyMatrixToPoint(transformMatrix, { x, y }); - } + }; - applyInverseToPoint({ x, y }) { + applyInverseToPoint = ({ x, y }: Point) => { const { transformMatrix } = this.state; return applyInverseMatrixToPoint(transformMatrix, { x, y }); - } + }; - reset() { + reset = () => { const { initialTransformMatrix } = this.state; this.setTransformMatrix(initialTransformMatrix); - } + }; - scale({ scaleX, scaleY: maybeScaleY, point }) { + scale = ({ scaleX, scaleY: maybeScaleY, point }: ScaleSignature) => { const scaleY = maybeScaleY || scaleX; const { transformMatrix } = this.state; const { width, height } = this.props; @@ -83,21 +159,21 @@ class Zoom extends React.Component { translateMatrix(-translate.x, -translate.y), ); this.setTransformMatrix(nextMatrix); - } + }; - translate({ translateX, translateY }) { + translate = ({ translateX, translateY }: Translate) => { const { transformMatrix } = this.state; const nextMatrix = composeMatrices(transformMatrix, translateMatrix(translateX, translateY)); this.setTransformMatrix(nextMatrix); - } + }; - translateTo({ x, y }) { + translateTo = ({ x, y }: Point) => { const { transformMatrix } = this.state; const point = applyInverseMatrixToPoint(transformMatrix, { x, y }); this.setTranslate({ translateX: point.x, translateY: point.y }); - } + }; - setTranslate({ translateX, translateY }) { + setTranslate = ({ translateX, translateY }: Translate) => { const { transformMatrix } = this.state; const nextMatrix = { ...transformMatrix, @@ -105,76 +181,77 @@ class Zoom extends React.Component { translateY, }; this.setTransformMatrix(nextMatrix); - } + }; - setTransformMatrix(transformMatrix) { - this.setState(prevState => { - return { transformMatrix: this.constrain(transformMatrix, prevState.transformMatrix) }; - }); - } + setTransformMatrix = (transformMatrix: TransformMatrix) => { + this.setState(prevState => ({ + transformMatrix: this.constrain(transformMatrix, prevState.transformMatrix), + })); + }; - invert() { + invert = () => { return inverseMatrix(this.state.transformMatrix); - } + }; - toStringInvert() { + 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 } = this.props; - const { scaleX, scaleY } = transformMatrix; - const shouldConstrainScaleX = scaleX > scaleXMax || scaleX < scaleXMin; - const shouldConstrainScaleY = scaleY > scaleYMax || scaleY < scaleYMin; + constrain = + this.props.constrain || + ((transformMatrix: TransformMatrix, prevTransformMatrix: TransformMatrix) => { + const { scaleXMin, scaleXMax, scaleYMin, scaleYMax } = 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; - } + if (shouldConstrainScaleX || shouldConstrainScaleY) { + return prevTransformMatrix; + } + return transformMatrix; + }); - dragStart(event) { + dragStart = (event: React.MouseEvent) => { const { transformMatrix } = this.state; const { translateX, translateY } = transformMatrix; - this.startPoint = localPoint(event); + this.startPoint = localPoint(event) || undefined; this.startTranslate = { translateX, translateY }; this.setState({ isDragging: true }); - } + }; - dragMove(event) { - if (!this.state.isDragging) return; + dragMove = (event: React.MouseEvent) => { + if (!this.state.isDragging || !this.startPoint || !this.startTranslate) return; const currentPoint = localPoint(event); - const dx = -(this.startPoint.x - currentPoint.x); - const dy = -(this.startPoint.y - currentPoint.y); + const dx = currentPoint ? -(this.startPoint.x - currentPoint.x) : -this.startPoint.x; + const dy = currentPoint ? -(this.startPoint.y - currentPoint.y) : -this.startPoint.y; this.setTranslate({ translateX: this.startTranslate.translateX + dx, translateY: this.startTranslate.translateY + dy, }); - } + }; - dragEnd(/** event */) { + dragEnd = () => { this.startPoint = undefined; this.startTranslate = undefined; this.setState({ isDragging: false }); - } + }; - handleWheel(event) { - const { passive } = this.props; + handleWheel = (event: React.WheelEvent | WheelEvent) => { + const { passive, wheelDelta } = this.props; if (!passive) event.preventDefault(); - const { wheelDelta } = this.props; - const point = localPoint(event); - const { scaleX, scaleY } = wheelDelta(event); + const point = localPoint(event) || undefined; + const { scaleX, scaleY } = wheelDelta!(event); this.scale({ scaleX, scaleY, point }); - } + }; - toString() { + toString = () => { const { transformMatrix } = this.state; const { translateX, translateY, scaleX, scaleY, skewX, skewY } = transformMatrix; return `matrix(${scaleX}, ${skewY}, ${skewX}, ${scaleY}, ${translateX}, ${translateY})`; - } + }; - center() { + center = () => { const { width, height } = this.props; const center = { x: width / 2, y: height / 2 }; const inverseCentroid = this.applyInverseToPoint(center); @@ -182,20 +259,19 @@ class Zoom extends React.Component { translateX: inverseCentroid.x - center.x, translateY: inverseCentroid.y - center.y, }); - } + }; - clear() { + clear = () => { this.setTransformMatrix(identityMatrix()); - } + }; render() { const { passive, children, style, className } = this.props; - const zoom = { + const zoom: ProvidedZoom & ZoomState = { ...this.state, center: this.center, clear: this.clear, scale: this.scale, - scaleTo: this.scaleTo, translate: this.translate, translateTo: this.translateTo, setTranslate: this.setTranslate, @@ -228,89 +304,4 @@ class Zoom extends React.Component { } } -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, - }), - /** - * By default passive is `false`. This will wrap children in a
and add an active wheel - * event listener (handleWheel). `handleWheel()` will call `event.preventDefault()` before other - * execution. This prevents an outer parent from scrolling when the mouse wheel is used to zoom. - * - * When passive is `true` it is required to add `` to handle - * wheel events. **Note:** By default you do not need to add ``. - * This is only necessary when ``. - */ - passive: PropTypes.bool, - style: PropTypes.object, - className: PropTypes.string, -}; - -Zoom.defaultProps = { - passive: false, - 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 }; - }, - style: undefined, - className: undefined, -}; - export default Zoom; diff --git a/packages/vx-zoom/src/index.js b/packages/vx-zoom/src/index.ts similarity index 100% rename from packages/vx-zoom/src/index.js rename to packages/vx-zoom/src/index.ts diff --git a/packages/vx-zoom/src/types.ts b/packages/vx-zoom/src/types.ts new file mode 100644 index 000000000..35c0455e4 --- /dev/null +++ b/packages/vx-zoom/src/types.ts @@ -0,0 +1,69 @@ +export interface TransformMatrix { + scaleX: number; + scaleY: number; + translateX: number; + translateY: number; + skewX: number; + skewY: number; +} + +export interface Point { + x: number; + y: number; +} + +export type Translate = Pick; + +export type Scale = Pick; + +export interface ScaleSignature { + scaleX: TransformMatrix['scaleX']; + scaleY?: TransformMatrix['scaleY']; + point?: Point; +} + +export interface ProvidedZoom { + /** Sets translateX/Y to the center defined by width and height. */ + center: () => void; + /** Sets the transform matrix to the identity matrix. */ + clear: () => void; + /** Applies the specified scaleX + optional scaleY transform relative to the specified point (or center of canvas if unspecified). */ + scale: (scale: ScaleSignature) => void; + /** Multiplies the current transform matrix by the specified translation. */ + translate: (translate: Translate) => void; + /** Translates to a specific x,y point. */ + translateTo: (point: Point) => void; + /** Sets the translation of the current transform matrix to the specified translation. */ + setTranslate: (translate: Translate) => void; + /** + * Sets the transform matrix to the specified matrix, constraining the transform + * scale by default (or applying props.constrain if provided). + */ + setTransformMatrix: (matrix: TransformMatrix) => void; + /** Resets the transform to the initial transform specified by props. */ + reset: () => void; + /** Callback for a wheel event, updating scale based on props.wheelDelta, relative to the mouse position. */ + handleWheel: (event: React.WheelEvent | WheelEvent) => void; + /** Callback for dragEnd, sets isDragging to false. */ + dragEnd: () => void; + /** Callback for dragMove, results in a scale transform. */ + dragMove: (event: React.MouseEvent) => void; + /** Callback for dragStart, sets isDragging to true. */ + dragStart: (event: React.MouseEvent) => void; + /** + * Returns a string representation of the matrix transform: + * matrix(${scaleX}, ${skewY}, ${skewX}, ${scaleY}, ${translateX}, ${translateY}) + */ + toString: () => string; + /** Returns the inverse of the current transform matrix. */ + invert: () => TransformMatrix; + /** + * Returns the string representation of the inverse of the current transform matrix: + * matrix(${scaleX}, ${skewY}, ${skewX}, ${scaleY}, ${translateX}, ${translateY}) + */ + toStringInvert: () => string; + /** Applies the current transform matrix to the specified point. */ + applyToPoint: ({ x, y }: Point) => Point; + /** Applies the inverse of the current transform matrix to the specified point. */ + applyInverseToPoint: ({ x, y }: Point) => Point; +} diff --git a/packages/vx-zoom/src/util/matrix.js b/packages/vx-zoom/src/util/matrix.ts similarity index 69% rename from packages/vx-zoom/src/util/matrix.js rename to packages/vx-zoom/src/util/matrix.ts index cd31dd72c..966c61423 100644 --- a/packages/vx-zoom/src/util/matrix.js +++ b/packages/vx-zoom/src/util/matrix.ts @@ -1,5 +1,6 @@ -/* eslint-disable no-case-declarations */ -export function identityMatrix() { +import { TransformMatrix, Point } from '../types'; + +export function identityMatrix(): TransformMatrix { return { scaleX: 1, scaleY: 1, @@ -17,7 +18,7 @@ export function createMatrix({ translateY = 0, skewX = 0, skewY = 0, -}) { +}: Partial): TransformMatrix { return { scaleX, scaleY, @@ -28,7 +29,14 @@ export function createMatrix({ }; } -export function inverseMatrix({ scaleX, scaleY, translateX, translateY, skewX, skewY }) { +export function inverseMatrix({ + scaleX, + scaleY, + translateX, + translateY, + skewX, + skewY, +}: TransformMatrix) { const denominator = scaleX * scaleY - skewY * skewX; return { scaleX: scaleY / denominator, @@ -40,27 +48,33 @@ export function inverseMatrix({ scaleX, scaleY, translateX, translateY, skewX, s }; } -export function applyMatrixToPoint(matrix, { x, y }) { +export function applyMatrixToPoint(matrix: TransformMatrix, { x, y }: Point) { 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 }) { +export function applyInverseMatrixToPoint(matrix: TransformMatrix, { x, y }: Point) { return applyMatrixToPoint(inverseMatrix(matrix), { x, y }); } -export function scaleMatrix(scaleX, maybeScaleY = undefined) { +export function scaleMatrix( + scaleX: TransformMatrix['scaleX'], + maybeScaleY: TransformMatrix['scaleY'] | undefined = undefined, +) { const scaleY = maybeScaleY || scaleX; return createMatrix({ scaleX, scaleY }); } -export function translateMatrix(translateX, translateY) { +export function translateMatrix( + translateX: TransformMatrix['translateX'], + translateY: TransformMatrix['translateY'], +) { return createMatrix({ translateX, translateY }); } -export function multiplyMatrices(matrix1, matrix2) { +export function multiplyMatrices(matrix1: TransformMatrix, matrix2: TransformMatrix) { return { scaleX: matrix1.scaleX * matrix2.scaleX + matrix1.skewX * matrix2.skewY, scaleY: matrix1.skewY * matrix2.skewX + matrix1.scaleY * matrix2.scaleY, @@ -73,7 +87,7 @@ export function multiplyMatrices(matrix1, matrix2) { }; } -export function composeMatrices(...matrices) { +export function composeMatrices(...matrices: TransformMatrix[]): TransformMatrix { switch (matrices.length) { case 0: throw new Error('composeMatrices() requires arguments: was called with no args'); @@ -81,9 +95,10 @@ export function composeMatrices(...matrices) { return matrices[0]; case 2: return multiplyMatrices(matrices[0], matrices[1]); - default: + default: { const [matrix1, matrix2, ...restMatrices] = matrices; const matrix = multiplyMatrices(matrix1, matrix2); return composeMatrices(matrix, ...restMatrices); + } } } diff --git a/packages/vx-zoom/test/Zoom.test.jsx b/packages/vx-zoom/test/Zoom.test.tsx similarity index 100% rename from packages/vx-zoom/test/Zoom.test.jsx rename to packages/vx-zoom/test/Zoom.test.tsx