mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-14 09:39:15 +00:00
Pinch-zoom: scale around given origin. (#139)
Also setting a min scale.
This commit is contained in:
@@ -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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user