Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Dynamic max and min zoom in the new ImageView #5916

Merged
merged 20 commits into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from 11 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
3 changes: 1 addition & 2 deletions res/css/views/elements/_ImageView.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ limitations under the License.

.mx_ImageView_image {
pointer-events: all;
max-width: 95%;
max-height: 95%;
flex-shrink: 0;
}

.mx_ImageView_panel {
Expand Down
132 changes: 76 additions & 56 deletions src/components/views/elements/ImageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {normalizeWheelEvent} from "../../../utils/Mouse";

const MIN_ZOOM = 100;
const MAX_ZOOM = 300;
// Max scale to keep gaps around the image
const MAX_SCALE = 0.95;
// This is used for the buttons
const ZOOM_STEP = 10;
const ZOOM_STEP = 0.10;
// This is used for mouse wheel events
const ZOOM_COEFFICIENT = 0.5;
const ZOOM_COEFFICIENT = 0.0025;
// If we have moved only this much we can zoom
const ZOOM_DISTANCE = 10;

const IMAGE_WRAPPER_CLASS = "mx_ImageView_image_wrapper";

interface IProps {
src: string, // the source of the image being displayed
Expand All @@ -62,8 +62,10 @@ interface IProps {
}

interface IState {
rotation: number,
zoom: number,
minZoom: number,
maxZoom: number,
rotation: number,
translationX: number,
translationY: number,
moving: boolean,
Expand All @@ -75,8 +77,10 @@ export default class ImageView extends React.Component<IProps, IState> {
constructor(props) {
super(props);
this.state = {
zoom: 0,
minZoom: MAX_SCALE,
maxZoom: MAX_SCALE,
rotation: 0,
zoom: MIN_ZOOM,
translationX: 0,
translationY: 0,
moving: false,
Expand All @@ -99,43 +103,82 @@ export default class ImageView extends React.Component<IProps, IState> {
// We have to use addEventListener() because the listener
// needs to be passive in order to work with Chromium
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
window.addEventListener("resize", this.calculateZoom);
this.calculateZoom();
}

componentWillUnmount() {
this.focusLock.current.removeEventListener('wheel', this.onWheel);
}

private onKeyDown = (ev: KeyboardEvent) => {
if (ev.key === Key.ESCAPE) {
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
}
};

private onWheel = (ev: WheelEvent) => {
ev.stopPropagation();
ev.preventDefault();
private calculateZoom = () => {
// TODO: What if we don't have width and height props?
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved

const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0];

const zoomX = imageWrapper.clientWidth / this.props.width;
const zoomY = imageWrapper.clientHeight / this.props.height;
// We set minZoom to the min of the zoomX and zoomY to avoid overflow in
// any direction. We also multiply by MAX_SCALE to get a gap around the
// image by default
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
// If minZoom is bigger or equal to 1, it means we scaling the image up
// to fit the viewport and therefore we want to disable zooming, so we
// set the maxZoom to be the same as the minZoom. Otherwise, we are
// scaling the image down - we want the user to be allowed to zoom to
// 100%
const maxZoom = minZoom >= 1 ? minZoom : 1;

if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom});
this.setState({
minZoom: minZoom,
maxZoom: maxZoom,
});
}

const {deltaY} = normalizeWheelEvent(ev);
const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT);
private zoom(delta: number) {
const newZoom = this.state.zoom + delta;

if (newZoom <= MIN_ZOOM) {
if (newZoom <= this.state.minZoom) {
this.setState({
zoom: MIN_ZOOM,
zoom: this.state.minZoom,
translationX: 0,
translationY: 0,
});
return;
}
if (newZoom >= MAX_ZOOM) {
this.setState({zoom: MAX_ZOOM});
if (newZoom >= this.state.maxZoom) {
this.setState({zoom: this.state.maxZoom});
return;
}

this.setState({
zoom: newZoom,
});
}

private onWheel = (ev: WheelEvent) => {
ev.stopPropagation();
ev.preventDefault();

const {deltaY} = normalizeWheelEvent(ev);
this.zoom(-(deltaY * ZOOM_COEFFICIENT));
};

private onZoomInClick = () => {
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
this.zoom(ZOOM_STEP);
};

private onZoomOutClick = () => {
this.zoom(-ZOOM_STEP);
};

private onKeyDown = (ev: KeyboardEvent) => {
if (ev.key === Key.ESCAPE) {
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
}
};

private onRotateCounterClockwiseClick = () => {
Expand All @@ -150,31 +193,6 @@ export default class ImageView extends React.Component<IProps, IState> {
this.setState({ rotation: rotationDegrees });
};

private onZoomInClick = () => {
if (this.state.zoom >= MAX_ZOOM) {
this.setState({zoom: MAX_ZOOM});
return;
}

this.setState({
zoom: this.state.zoom + ZOOM_STEP,
});
};

private onZoomOutClick = () => {
if (this.state.zoom <= MIN_ZOOM) {
this.setState({
zoom: MIN_ZOOM,
translationX: 0,
translationY: 0,
});
return;
}
this.setState({
zoom: this.state.zoom - ZOOM_STEP,
});
};

private onDownloadClick = () => {
const a = document.createElement("a");
a.href = this.props.src;
Expand Down Expand Up @@ -217,8 +235,8 @@ export default class ImageView extends React.Component<IProps, IState> {
if (ev.button !== 0) return;

// Zoom in if we are completely zoomed out
if (this.state.zoom === MIN_ZOOM) {
this.setState({zoom: MAX_ZOOM});
if (this.state.zoom === this.state.minZoom) {
this.setState({zoom: this.state.maxZoom});
return;
}

Expand Down Expand Up @@ -251,7 +269,7 @@ export default class ImageView extends React.Component<IProps, IState> {
Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE
) {
this.setState({
zoom: MIN_ZOOM,
zoom: this.state.minZoom,
translationX: 0,
translationY: 0,
});
Expand Down Expand Up @@ -290,13 +308,15 @@ export default class ImageView extends React.Component<IProps, IState> {
let cursor;
if (this.state.moving) {
cursor= "grabbing";
} else if (this.state.zoom === MIN_ZOOM) {
} else if (this.state.maxZoom === this.state.minZoom) {
cursor = "pointer";
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
} else if (this.state.zoom === this.state.minZoom) {
cursor = "zoom-in";
} else {
cursor = "zoom-out";
}
const rotationDegrees = this.state.rotation + "deg";
const zoomPercentage = this.state.zoom/100;
const zoom = this.state.zoom;
const translatePixelsX = this.state.translationX + "px";
const translatePixelsY = this.state.translationY + "px";
// The order of the values is important!
Expand All @@ -308,7 +328,7 @@ export default class ImageView extends React.Component<IProps, IState> {
transition: this.state.moving ? null : "transform 200ms ease 0s",
transform: `translateX(${translatePixelsX})
translateY(${translatePixelsY})
scale(${zoomPercentage})
scale(${zoom})
rotate(${rotationDegrees})`,
};

Expand Down Expand Up @@ -427,7 +447,7 @@ export default class ImageView extends React.Component<IProps, IState> {
{this.renderContextMenu()}
</div>
</div>
<div className="mx_ImageView_image_wrapper">
<div className={IMAGE_WRAPPER_CLASS}>
<img
src={this.props.src}
title={this.props.name}
Expand Down