mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-23 20:23:32 +01:00
Add limitation of image's max zoom rate (max to the original size of image) (#15094)
* limit image max scale rate to fit the actual image size * replace with MIN_SCALE * fix behavior on touch screen
This commit is contained in:
parent
ab00998503
commit
f720af6b72
1 changed files with 51 additions and 18 deletions
|
@ -113,7 +113,8 @@ class ZoomableImage extends React.PureComponent {
|
||||||
state = {
|
state = {
|
||||||
scale: MIN_SCALE,
|
scale: MIN_SCALE,
|
||||||
zoomMatrix: {
|
zoomMatrix: {
|
||||||
type: null, // 'full-width' 'full-height'
|
type: null, // 'width' 'height'
|
||||||
|
fullScreen: null, // bool
|
||||||
rate: null, // full screen scale rate
|
rate: null, // full screen scale rate
|
||||||
clientWidth: null,
|
clientWidth: null,
|
||||||
clientHeight: null,
|
clientHeight: null,
|
||||||
|
@ -122,12 +123,15 @@ class ZoomableImage extends React.PureComponent {
|
||||||
clientHeightFixed: null,
|
clientHeightFixed: null,
|
||||||
scrollTop: null,
|
scrollTop: null,
|
||||||
scrollLeft: null,
|
scrollLeft: null,
|
||||||
|
translateX: null,
|
||||||
|
translateY: null,
|
||||||
},
|
},
|
||||||
zoomState: 'expand', // 'expand' 'compress'
|
zoomState: 'expand', // 'expand' 'compress'
|
||||||
navigationHidden: false,
|
navigationHidden: false,
|
||||||
dragPosition: { top: 0, left: 0, x: 0, y: 0 },
|
dragPosition: { top: 0, left: 0, x: 0, y: 0 },
|
||||||
dragged: false,
|
dragged: false,
|
||||||
lockScroll: { x: 0, y: 0 },
|
lockScroll: { x: 0, y: 0 },
|
||||||
|
lockTranslate: { x: 0, y: 0 },
|
||||||
}
|
}
|
||||||
|
|
||||||
removers = [];
|
removers = [];
|
||||||
|
@ -168,18 +172,24 @@ class ZoomableImage extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate () {
|
componentDidUpdate () {
|
||||||
|
this.setState({ zoomState: this.state.scale >= this.state.zoomMatrix.rate ? 'compress' : 'expand' });
|
||||||
|
|
||||||
|
if (this.state.scale === MIN_SCALE) {
|
||||||
|
this.container.style.removeProperty('cursor');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UNSAFE_componentWillReceiveProps () {
|
||||||
|
// reset when slide to next image
|
||||||
if (this.props.zoomButtonHidden) {
|
if (this.props.zoomButtonHidden) {
|
||||||
this.setState({ scale: MIN_SCALE }, () => {
|
this.setState({
|
||||||
|
scale: MIN_SCALE,
|
||||||
|
lockTranslate: { x: 0, y: 0 },
|
||||||
|
}, () => {
|
||||||
this.container.scrollLeft = 0;
|
this.container.scrollLeft = 0;
|
||||||
this.container.scrollTop = 0;
|
this.container.scrollTop = 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ zoomState: this.state.scale >= this.state.zoomMatrix.rate ? 'compress' : 'expand' });
|
|
||||||
|
|
||||||
if (this.state.scale === 1) {
|
|
||||||
this.container.style.removeProperty('cursor');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEventListeners () {
|
removeEventListeners () {
|
||||||
|
@ -192,7 +202,7 @@ class ZoomableImage extends React.PureComponent {
|
||||||
|
|
||||||
const event = normalizeWheel(e);
|
const event = normalizeWheel(e);
|
||||||
|
|
||||||
if (this.state.zoomMatrix.type === 'full-width') {
|
if (this.state.zoomMatrix.type === 'width') {
|
||||||
// full width, scroll vertical
|
// full width, scroll vertical
|
||||||
this.container.scrollTop = Math.max(this.container.scrollTop + event.pixelY, this.state.lockScroll.y);
|
this.container.scrollTop = Math.max(this.container.scrollTop + event.pixelY, this.state.lockScroll.y);
|
||||||
} else {
|
} else {
|
||||||
|
@ -268,7 +278,7 @@ class ZoomableImage extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
zoom(nextScale, midpoint) {
|
zoom(nextScale, midpoint) {
|
||||||
const { scale } = this.state;
|
const { scale, zoomMatrix } = this.state;
|
||||||
const { scrollLeft, scrollTop } = this.container;
|
const { scrollLeft, scrollTop } = this.container;
|
||||||
|
|
||||||
// math memo:
|
// math memo:
|
||||||
|
@ -283,6 +293,15 @@ class ZoomableImage extends React.PureComponent {
|
||||||
this.setState({ scale: nextScale }, () => {
|
this.setState({ scale: nextScale }, () => {
|
||||||
this.container.scrollLeft = nextScrollLeft;
|
this.container.scrollLeft = nextScrollLeft;
|
||||||
this.container.scrollTop = nextScrollTop;
|
this.container.scrollTop = nextScrollTop;
|
||||||
|
// reset the translateX/Y constantly
|
||||||
|
if (nextScale < zoomMatrix.rate) {
|
||||||
|
this.setState({
|
||||||
|
lockTranslate: {
|
||||||
|
x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)),
|
||||||
|
y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,14 +326,18 @@ class ZoomableImage extends React.PureComponent {
|
||||||
const { offsetWidth, offsetHeight } = this.image;
|
const { offsetWidth, offsetHeight } = this.image;
|
||||||
const clientHeightFixed = clientHeight - NAV_BAR_HEIGHT;
|
const clientHeightFixed = clientHeight - NAV_BAR_HEIGHT;
|
||||||
|
|
||||||
const type = width/height < clientWidth / clientHeightFixed ? 'full-width' : 'full-height';
|
const type = width / height < clientWidth / clientHeightFixed ? 'width' : 'height';
|
||||||
const rate = type === 'full-width' ? clientWidth / offsetWidth : clientHeightFixed / offsetHeight;
|
const fullScreen = type === 'width' ? width > clientWidth : height > clientHeightFixed;
|
||||||
const scrollTop = type === 'full-width' ? (clientHeight - offsetHeight) / 2 - NAV_BAR_HEIGHT : (clientHeightFixed - offsetHeight) / 2;
|
const rate = type === 'width' ? Math.min(clientWidth, width) / offsetWidth : Math.min(clientHeightFixed, height) / offsetHeight;
|
||||||
|
const scrollTop = type === 'width' ? (clientHeight - offsetHeight) / 2 - NAV_BAR_HEIGHT : (clientHeightFixed - offsetHeight) / 2;
|
||||||
const scrollLeft = (clientWidth - offsetWidth) / 2;
|
const scrollLeft = (clientWidth - offsetWidth) / 2;
|
||||||
|
const translateX = type === 'width' ? (width - offsetWidth) / (2 * rate) : 0;
|
||||||
|
const translateY = type === 'height' ? (height - offsetHeight) / (2 * rate) : 0;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
zoomMatrix: {
|
zoomMatrix: {
|
||||||
type: type,
|
type: type,
|
||||||
|
fullScreen: fullScreen,
|
||||||
rate: rate,
|
rate: rate,
|
||||||
clientWidth: clientWidth,
|
clientWidth: clientWidth,
|
||||||
clientHeight: clientHeight,
|
clientHeight: clientHeight,
|
||||||
|
@ -323,6 +346,8 @@ class ZoomableImage extends React.PureComponent {
|
||||||
clientHeightFixed: clientHeightFixed,
|
clientHeightFixed: clientHeightFixed,
|
||||||
scrollTop: scrollTop,
|
scrollTop: scrollTop,
|
||||||
scrollLeft: scrollLeft,
|
scrollLeft: scrollLeft,
|
||||||
|
translateX: translateX,
|
||||||
|
translateY: translateY,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -340,6 +365,10 @@ class ZoomableImage extends React.PureComponent {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
|
lockTranslate: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
}, () => {
|
}, () => {
|
||||||
this.container.scrollLeft = 0;
|
this.container.scrollLeft = 0;
|
||||||
this.container.scrollTop = 0;
|
this.container.scrollTop = 0;
|
||||||
|
@ -351,6 +380,10 @@ class ZoomableImage extends React.PureComponent {
|
||||||
x: zoomMatrix.scrollLeft,
|
x: zoomMatrix.scrollLeft,
|
||||||
y: zoomMatrix.scrollTop,
|
y: zoomMatrix.scrollTop,
|
||||||
},
|
},
|
||||||
|
lockTranslate: {
|
||||||
|
x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX,
|
||||||
|
y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY,
|
||||||
|
},
|
||||||
}, () => {
|
}, () => {
|
||||||
this.container.scrollLeft = zoomMatrix.scrollLeft;
|
this.container.scrollLeft = zoomMatrix.scrollLeft;
|
||||||
this.container.scrollTop = zoomMatrix.scrollTop;
|
this.container.scrollTop = zoomMatrix.scrollTop;
|
||||||
|
@ -371,15 +404,15 @@ class ZoomableImage extends React.PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { alt, src, width, height, intl } = this.props;
|
const { alt, src, width, height, intl } = this.props;
|
||||||
const { scale } = this.state;
|
const { scale, lockTranslate } = this.state;
|
||||||
const overflow = scale === 1 ? 'hidden' : 'scroll';
|
const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll';
|
||||||
const zoomButtonSshouldHide = !this.state.navigationHidden && !this.props.zoomButtonHidden ? '' : 'media-modal__zoom-button--hidden';
|
const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : '';
|
||||||
const zoomButtonTitle = this.state.zoomState === 'compress' ? intl.formatMessage(messages.compress) : intl.formatMessage(messages.expand);
|
const zoomButtonTitle = this.state.zoomState === 'compress' ? intl.formatMessage(messages.compress) : intl.formatMessage(messages.expand);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={`media-modal__zoom-button ${zoomButtonSshouldHide}`}
|
className={`media-modal__zoom-button ${zoomButtonShouldHide}`}
|
||||||
title={zoomButtonTitle}
|
title={zoomButtonTitle}
|
||||||
icon={this.state.zoomState}
|
icon={this.state.zoomState}
|
||||||
onClick={this.handleZoomClick}
|
onClick={this.handleZoomClick}
|
||||||
|
@ -402,7 +435,7 @@ class ZoomableImage extends React.PureComponent {
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
style={{
|
style={{
|
||||||
transform: `scale(${scale})`,
|
transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`,
|
||||||
transformOrigin: '0 0',
|
transformOrigin: '0 0',
|
||||||
}}
|
}}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
|
|
Loading…
Reference in a new issue