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

Pinch zoom features #139

Merged
merged 1 commit into from
Sep 5, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
86 changes: 78 additions & 8 deletions src/components/Output/custom-els/PinchZoom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,36 @@ interface Point {
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;
panY?: number;
scaleDiff?: number;
originX?: number;
originY?: number;
}

interface SetTransformOpts {
interface SetTransformOpts extends ChangeOptions {
scale?: number;
x?: number;
y?: number;
/**
* Fire a 'change' event if values are different to current values
*/
allowChangeEvent?: boolean;
}

type ScaleRelativeToValues = 'container' | 'content';

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 {
Expand All @@ -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.
// Given that, better to use something everything supports.
let cachedSvg: SVGSVGElement;
Expand All @@ -54,6 +77,8 @@ function createPoint(): SVGPoint {
return getSVG().createSVGPoint();
}

const MIN_SCALE = 0.01;

export default class PinchZoom extends HTMLElement {
// The element that we'll transform.
// Ideally this would be shadow DOM, but we don't have the browser
Expand Down Expand Up @@ -103,6 +128,45 @@ export default class PinchZoom extends HTMLElement {
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;
Copy link
Collaborator Author

@jakearchibald jakearchibald Aug 16, 2018

Choose a reason for hiding this comment

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

Are these good defaults? Maybe the settings we use in squoosh should be the defaults?


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.
*/
Expand Down Expand Up @@ -175,6 +239,9 @@ export default class PinchZoom extends HTMLElement {
* Update transform values without checking bounds. This is only called in setTransform.
*/
_updateTransform(scale: number, x: number, y: number, allowChangeEvent: boolean) {
// Avoid scaling to zero
if (scale < MIN_SCALE) return;

// Return if there's no change
if (
scale === this.scale &&
Expand Down Expand Up @@ -217,7 +284,7 @@ export default class PinchZoom extends HTMLElement {
}

// Do a bounds check
this.setTransform();
this.setTransform({ allowChangeEvent: true });
}

private _onWheel(event: WheelEvent) {
Expand All @@ -240,6 +307,7 @@ export default class PinchZoom extends HTMLElement {
scaleDiff,
originX: event.clientX - thisRect.left,
originY: event.clientY - thisRect.top,
allowChangeEvent: true,
});
}

Expand All @@ -264,6 +332,7 @@ export default class PinchZoom extends HTMLElement {
originX, originY, scaleDiff,
panX: newMidpoint.clientX - prevMidpoint.clientX,
panY: newMidpoint.clientY - prevMidpoint.clientY,
allowChangeEvent: true,
});
}

Expand All @@ -273,6 +342,7 @@ export default class PinchZoom extends HTMLElement {
panX = 0, panY = 0,
originX = 0, originY = 0,
scaleDiff = 1,
allowChangeEvent = false,
} = opts;

const matrix = createMatrix()
Expand All @@ -287,10 +357,10 @@ export default class PinchZoom extends HTMLElement {

// Convert the transform into basic translate & scale.
this.setTransform({
allowChangeEvent,
scale: matrix.a,
x: matrix.e,
y: matrix.f,
allowChangeEvent: true,
});
}
}
Expand Down
35 changes: 17 additions & 18 deletions src/components/Output/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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/TwoUp';
import * as style from './style.scss';
Expand All @@ -19,6 +19,13 @@ interface State {
altBackground: boolean;
}

const scaleToOpts: ScaleToOpts = {
originX: '50%',
originY: '50%',
relativeTo: 'container',
allowChangeEvent: true,
};

export default class Output extends Component<Props, State> {
state: State = {
scale: 1,
Expand Down Expand Up @@ -48,14 +55,6 @@ export default class Output extends Component<Props, State> {
if (prevProps.rightImg !== this.props.rightImg && this.canvasRight) {
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) {
Expand All @@ -71,16 +70,16 @@ export default class Output extends Component<Props, State> {

@bind
zoomIn() {
this.setState({
scale: Math.min(this.state.scale * 1.25, 100),
});
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');

this.pinchZoomLeft.scaleTo(this.state.scale * 1.25, scaleToOpts);
}

@bind
zoomOut() {
this.setState({
scale: Math.max(this.state.scale / 1.25, 0.0001),
});
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');

this.pinchZoomLeft.scaleTo(this.state.scale / 1.25, scaleToOpts);
}

@bind
Expand All @@ -100,9 +99,9 @@ export default class Output extends Component<Props, State> {
const target = event.target as HTMLInputElement;
const percent = parseFloat(target.value);
if (isNaN(percent)) return;
this.setState({
scale: percent / 100,
});
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');

this.pinchZoomLeft.scaleTo(percent / 100, scaleToOpts);
}

@bind
Expand Down