Pinch-zoom: scale around given origin. (#139)

Also setting a min scale.
This commit is contained in:
Jake Archibald
2018-09-05 09:39:26 +01:00
committed by GitHub
parent 485ba174e3
commit 700b1f15cd
2 changed files with 95 additions and 26 deletions

View File

@@ -6,7 +6,14 @@ interface Point {
clientY: number; clientY: number;
} }
interface ApplyChangeOpts { interface ChangeOptions {
/**
* Fire a 'change' event if values are different to current values
*/
allowChangeEvent?: boolean;
}
interface ApplyChangeOpts extends ChangeOptions {
panX?: number; panX?: number;
panY?: number; panY?: number;
scaleDiff?: number; scaleDiff?: number;
@@ -14,14 +21,21 @@ interface ApplyChangeOpts {
originY?: number; originY?: number;
} }
interface SetTransformOpts { interface SetTransformOpts extends ChangeOptions {
scale?: number; scale?: number;
x?: number; x?: number;
y?: number; y?: number;
/** }
* Fire a 'change' event if values are different to current values
*/ type ScaleRelativeToValues = 'container' | 'content';
allowChangeEvent?: boolean;
export interface ScaleToOpts extends ChangeOptions {
/** Transform origin. Can be a number, or string percent, eg "50%" */
originX?: number | string;
/** Transform origin. Can be a number, or string percent, eg "50%" */
originY?: number | string;
/** Should the transform origin be relative to the container, or content? */
relativeTo?: ScaleRelativeToValues;
} }
function getDistance(a: Point, b?: Point): number { function getDistance(a: Point, b?: Point): number {
@@ -38,6 +52,15 @@ function getMidpoint(a: Point, b?: Point): Point {
}; };
} }
function getAbsoluteValue(value: string | number, max: number): number {
if (typeof value === 'number') return value;
if (value.trimRight().endsWith('%')) {
return max * parseFloat(value) / 100;
}
return parseFloat(value);
}
// I'd rather use DOMMatrix/DOMPoint here, but the browser support isn't good enough. // I'd rather use DOMMatrix/DOMPoint here, but the browser support isn't good enough.
// Given that, better to use something everything supports. // Given that, better to use something everything supports.
let cachedSvg: SVGSVGElement; let cachedSvg: SVGSVGElement;
@@ -54,6 +77,8 @@ function createPoint(): SVGPoint {
return getSVG().createSVGPoint(); return getSVG().createSVGPoint();
} }
const MIN_SCALE = 0.01;
export default class PinchZoom extends HTMLElement { export default class PinchZoom extends HTMLElement {
// The element that we'll transform. // The element that we'll transform.
// Ideally this would be shadow DOM, but we don't have the browser // Ideally this would be shadow DOM, but we don't have the browser
@@ -103,6 +128,45 @@ export default class PinchZoom extends HTMLElement {
return this._transform.a; return this._transform.a;
} }
/**
* Change the scale, adjusting x/y by a given transform origin.
*/
scaleTo(scale: number, opts: ScaleToOpts = {}) {
let {
originX = 0,
originY = 0,
} = opts;
const {
relativeTo = 'content',
allowChangeEvent = false,
} = opts;
const relativeToEl = (relativeTo === 'content' ? this._positioningEl : this);
// No content element? Fall back to just setting scale
if (!relativeToEl) {
this.setTransform({ scale, allowChangeEvent });
return;
}
const rect = relativeToEl.getBoundingClientRect();
originX = getAbsoluteValue(originX, rect.width);
originY = getAbsoluteValue(originY, rect.height);
if (relativeTo === 'content') {
originX += this.x;
originY += this.y;
}
this._applyChange({
allowChangeEvent,
originX,
originY,
scaleDiff: scale / this.scale,
});
}
/** /**
* Update the stage with a given scale/x/y. * Update the stage with a given scale/x/y.
*/ */
@@ -175,6 +239,9 @@ export default class PinchZoom extends HTMLElement {
* Update transform values without checking bounds. This is only called in setTransform. * Update transform values without checking bounds. This is only called in setTransform.
*/ */
_updateTransform(scale: number, x: number, y: number, allowChangeEvent: boolean) { _updateTransform(scale: number, x: number, y: number, allowChangeEvent: boolean) {
// Avoid scaling to zero
if (scale < MIN_SCALE) return;
// Return if there's no change // Return if there's no change
if ( if (
scale === this.scale && scale === this.scale &&
@@ -217,7 +284,7 @@ export default class PinchZoom extends HTMLElement {
} }
// Do a bounds check // Do a bounds check
this.setTransform(); this.setTransform({ allowChangeEvent: true });
} }
private _onWheel(event: WheelEvent) { private _onWheel(event: WheelEvent) {
@@ -240,6 +307,7 @@ export default class PinchZoom extends HTMLElement {
scaleDiff, scaleDiff,
originX: event.clientX - thisRect.left, originX: event.clientX - thisRect.left,
originY: event.clientY - thisRect.top, originY: event.clientY - thisRect.top,
allowChangeEvent: true,
}); });
} }
@@ -264,6 +332,7 @@ export default class PinchZoom extends HTMLElement {
originX, originY, scaleDiff, originX, originY, scaleDiff,
panX: newMidpoint.clientX - prevMidpoint.clientX, panX: newMidpoint.clientX - prevMidpoint.clientX,
panY: newMidpoint.clientY - prevMidpoint.clientY, panY: newMidpoint.clientY - prevMidpoint.clientY,
allowChangeEvent: true,
}); });
} }
@@ -273,6 +342,7 @@ export default class PinchZoom extends HTMLElement {
panX = 0, panY = 0, panX = 0, panY = 0,
originX = 0, originY = 0, originX = 0, originY = 0,
scaleDiff = 1, scaleDiff = 1,
allowChangeEvent = false,
} = opts; } = opts;
const matrix = createMatrix() const matrix = createMatrix()
@@ -287,10 +357,10 @@ export default class PinchZoom extends HTMLElement {
// Convert the transform into basic translate & scale. // Convert the transform into basic translate & scale.
this.setTransform({ this.setTransform({
allowChangeEvent,
scale: matrix.a, scale: matrix.a,
x: matrix.e, x: matrix.e,
y: matrix.f, y: matrix.f,
allowChangeEvent: true,
}); });
} }
} }

View File

@@ -1,5 +1,5 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import PinchZoom from './custom-els/PinchZoom'; import PinchZoom, { ScaleToOpts } from './custom-els/PinchZoom';
import './custom-els/PinchZoom'; import './custom-els/PinchZoom';
import './custom-els/TwoUp'; import './custom-els/TwoUp';
import * as style from './style.scss'; import * as style from './style.scss';
@@ -19,6 +19,13 @@ interface State {
altBackground: boolean; altBackground: boolean;
} }
const scaleToOpts: ScaleToOpts = {
originX: '50%',
originY: '50%',
relativeTo: 'container',
allowChangeEvent: true,
};
export default class Output extends Component<Props, State> { export default class Output extends Component<Props, State> {
state: State = { state: State = {
scale: 1, scale: 1,
@@ -48,14 +55,6 @@ export default class Output extends Component<Props, State> {
if (prevProps.rightImg !== this.props.rightImg && this.canvasRight) { if (prevProps.rightImg !== this.props.rightImg && this.canvasRight) {
drawBitmapToCanvas(this.canvasRight, this.props.rightImg); drawBitmapToCanvas(this.canvasRight, this.props.rightImg);
} }
const { scale } = this.state;
if (scale !== prevState.scale && this.pinchZoomLeft && this.pinchZoomRight) {
// @TODO it would be nice if PinchZoom exposed a variant of setTransform() that
// preserved translation. It currently only does this for mouse wheel events.
this.pinchZoomLeft.setTransform({ scale });
this.pinchZoomRight.setTransform({ scale });
}
} }
shouldComponentUpdate(nextProps: Props, nextState: State) { shouldComponentUpdate(nextProps: Props, nextState: State) {
@@ -71,16 +70,16 @@ export default class Output extends Component<Props, State> {
@bind @bind
zoomIn() { zoomIn() {
this.setState({ if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
scale: Math.min(this.state.scale * 1.25, 100),
}); this.pinchZoomLeft.scaleTo(this.state.scale * 1.25, scaleToOpts);
} }
@bind @bind
zoomOut() { zoomOut() {
this.setState({ if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
scale: Math.max(this.state.scale / 1.25, 0.0001),
}); this.pinchZoomLeft.scaleTo(this.state.scale / 1.25, scaleToOpts);
} }
@bind @bind
@@ -100,9 +99,9 @@ export default class Output extends Component<Props, State> {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const percent = parseFloat(target.value); const percent = parseFloat(target.value);
if (isNaN(percent)) return; if (isNaN(percent)) return;
this.setState({ if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
scale: percent / 100,
}); this.pinchZoomLeft.scaleTo(percent / 100, scaleToOpts);
} }
@bind @bind