From 8f2c91568c7ab552a87d02813e6b02be65f8707f Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Tue, 27 Jun 2017 20:43:53 +0900 Subject: [PATCH] Maintain aspect ratio for preview image (#3966) --- .../features/ui/components/image_loader.js | 122 ++++++++++++++---- app/javascript/styles/components.scss | 30 +++-- 2 files changed, 116 insertions(+), 36 deletions(-) diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js index 5c3879970..52c3a898b 100644 --- a/app/javascript/mastodon/features/ui/components/image_loader.js +++ b/app/javascript/mastodon/features/ui/components/image_loader.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; export default class ImageLoader extends React.PureComponent { @@ -20,46 +21,121 @@ export default class ImageLoader extends React.PureComponent { error: false, } - componentWillMount() { - this._loadImage(this.props.src); + removers = []; + + get canvasContext() { + if (!this.canvas) { + return null; + } + this._canvasContext = this._canvasContext || this.canvas.getContext('2d'); + return this._canvasContext; } - componentWillReceiveProps(props) { - this._loadImage(props.src); + componentDidMount () { + this.loadImage(this.props); } - _loadImage(src) { + componentWillReceiveProps (nextProps) { + if (this.props.src !== nextProps.src) { + this.loadImage(nextProps); + } + } + + loadImage (props) { + this.removeEventListeners(); + this.setState({ loading: true, error: false }); + Promise.all([ + this.loadPreviewCanvas(props), + this.loadOriginalImage(props), + ]) + .then(() => { + this.setState({ loading: false, error: false }); + this.clearPreviewCanvas(); + }) + .catch(() => this.setState({ loading: false, error: true })); + } + + loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => { const image = new Image(); + const removeEventListeners = () => { + image.removeEventListener('error', handleError); + image.removeEventListener('load', handleLoad); + }; + const handleError = () => { + removeEventListeners(); + reject(); + }; + const handleLoad = () => { + removeEventListeners(); + this.canvasContext.drawImage(image, 0, 0, width, height); + resolve(); + }; + image.addEventListener('error', handleError); + image.addEventListener('load', handleLoad); + image.src = previewSrc; + this.removers.push(removeEventListeners); + }) - image.onerror = () => this.setState({ loading: false, error: true }); - image.onload = () => this.setState({ loading: false, error: false }); - - image.src = src; - - this.setState({ loading: true }); + clearPreviewCanvas () { + const { width, height } = this.canvas; + this.canvasContext.clearRect(0, 0, width, height); } - render() { - const { alt, src, previewSrc, width, height } = this.props; + loadOriginalImage = ({ src }) => new Promise((resolve, reject) => { + const image = new Image(); + const removeEventListeners = () => { + image.removeEventListener('error', handleError); + image.removeEventListener('load', handleLoad); + }; + const handleError = () => { + removeEventListeners(); + reject(); + }; + const handleLoad = () => { + removeEventListeners(); + resolve(); + }; + image.addEventListener('error', handleError); + image.addEventListener('load', handleLoad); + image.src = src; + this.removers.push(removeEventListeners); + }); + + removeEventListeners () { + this.removers.forEach(listeners => listeners()); + this.removers = []; + } + + setCanvasRef = c => { + this.canvas = c; + } + + render () { + const { alt, src, width, height } = this.props; const { loading } = this.state; + const className = classNames('image-loader', { + 'image-loader--loading': loading, + }); + return ( -
- {alt} + - {loading && + {!loading && ( - } + )}
); } diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index bb9723f5a..91ebd91fd 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1099,20 +1099,22 @@ .image-loader { position: relative; -} -.image-loader__preview-img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - filter: blur(2px); -} + &.image-loader--loading { + .image-loader__preview-canvas { + filter: blur(2px); + } + } -.media-modal img.image-loader__preview-img { - width: 100%; - height: 100%; + .image-loader__img { + position: absolute; + top: 0; + left: 0; + right: 0; + width: 100%; + height: 100%; + background-image: none; + } } .navigation-bar { @@ -2933,6 +2935,7 @@ button.icon-button.active i.fa-retweet { position: relative; img, + canvas, video { max-width: 80vw; max-height: 80vh; @@ -2940,7 +2943,8 @@ button.icon-button.active i.fa-retweet { height: auto; } - img { + img, + canvas { display: block; background: url('../images/void.png') repeat; }