When avatar/header are GIF, generate static versions (#1428)
* When avatar/header are GIF, generate static versions. Account API returns "avatar"/"avatar_static", "header"/"header_static" Static version is the same as original for other cases Web UI de-animates avatars in toots, lists of users Fix #441, fix #596, prerequisite for #1064 * Fix JS test * Add rake task to generate static avatars/headers from GIF ones, add test
This commit is contained in:
@ -65,7 +65,7 @@ const Account = React.createClass({
|
||||
<div className='account'>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={36} /></div>
|
||||
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
|
@ -1,103 +1,18 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
// From: http://stackoverflow.com/a/18320662
|
||||
const resample = (canvas, width, height, resize_canvas) => {
|
||||
let width_source = canvas.width;
|
||||
let height_source = canvas.height;
|
||||
width = Math.round(width);
|
||||
height = Math.round(height);
|
||||
|
||||
let ratio_w = width_source / width;
|
||||
let ratio_h = height_source / height;
|
||||
let ratio_w_half = Math.ceil(ratio_w / 2);
|
||||
let ratio_h_half = Math.ceil(ratio_h / 2);
|
||||
|
||||
let ctx = canvas.getContext("2d");
|
||||
let img = ctx.getImageData(0, 0, width_source, height_source);
|
||||
let img2 = ctx.createImageData(width, height);
|
||||
let data = img.data;
|
||||
let data2 = img2.data;
|
||||
|
||||
for (let j = 0; j < height; j++) {
|
||||
for (let i = 0; i < width; i++) {
|
||||
let x2 = (i + j * width) * 4;
|
||||
let weight = 0;
|
||||
let weights = 0;
|
||||
let weights_alpha = 0;
|
||||
let gx_r = 0;
|
||||
let gx_g = 0;
|
||||
let gx_b = 0;
|
||||
let gx_a = 0;
|
||||
let center_y = (j + 0.5) * ratio_h;
|
||||
let yy_start = Math.floor(j * ratio_h);
|
||||
let yy_stop = Math.ceil((j + 1) * ratio_h);
|
||||
|
||||
for (let yy = yy_start; yy < yy_stop; yy++) {
|
||||
let dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half;
|
||||
let center_x = (i + 0.5) * ratio_w;
|
||||
let w0 = dy * dy; //pre-calc part of w
|
||||
let xx_start = Math.floor(i * ratio_w);
|
||||
let xx_stop = Math.ceil((i + 1) * ratio_w);
|
||||
|
||||
for (let xx = xx_start; xx < xx_stop; xx++) {
|
||||
let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half;
|
||||
let w = Math.sqrt(w0 + dx * dx);
|
||||
|
||||
if (w >= 1) {
|
||||
// pixel too far
|
||||
continue;
|
||||
}
|
||||
|
||||
// hermite filter
|
||||
weight = 2 * w * w * w - 3 * w * w + 1;
|
||||
let pos_x = 4 * (xx + yy * width_source);
|
||||
|
||||
// alpha
|
||||
gx_a += weight * data[pos_x + 3];
|
||||
weights_alpha += weight;
|
||||
|
||||
// colors
|
||||
if (data[pos_x + 3] < 255)
|
||||
weight = weight * data[pos_x + 3] / 250;
|
||||
|
||||
gx_r += weight * data[pos_x];
|
||||
gx_g += weight * data[pos_x + 1];
|
||||
gx_b += weight * data[pos_x + 2];
|
||||
weights += weight;
|
||||
}
|
||||
}
|
||||
|
||||
data2[x2] = gx_r / weights;
|
||||
data2[x2 + 1] = gx_g / weights;
|
||||
data2[x2 + 2] = gx_b / weights;
|
||||
data2[x2 + 3] = gx_a / weights_alpha;
|
||||
}
|
||||
}
|
||||
|
||||
// clear and resize canvas
|
||||
if (resize_canvas === true) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
} else {
|
||||
ctx.clearRect(0, 0, width_source, height_source);
|
||||
}
|
||||
|
||||
// draw
|
||||
ctx.putImageData(img2, 0, 0);
|
||||
};
|
||||
|
||||
const Avatar = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
src: React.PropTypes.string.isRequired,
|
||||
staticSrc: React.PropTypes.string,
|
||||
size: React.PropTypes.number.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
animated: React.PropTypes.bool
|
||||
animate: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps () {
|
||||
return {
|
||||
animated: true
|
||||
animate: false
|
||||
};
|
||||
},
|
||||
|
||||
@ -117,38 +32,30 @@ const Avatar = React.createClass({
|
||||
this.setState({ hovering: false });
|
||||
},
|
||||
|
||||
handleLoad () {
|
||||
this.canvas.width = this.image.naturalWidth;
|
||||
this.canvas.height = this.image.naturalHeight;
|
||||
this.canvas.getContext('2d').drawImage(this.image, 0, 0);
|
||||
|
||||
resample(this.canvas, this.props.size * window.devicePixelRatio, this.props.size * window.devicePixelRatio, true);
|
||||
},
|
||||
|
||||
setImageRef (c) {
|
||||
this.image = c;
|
||||
},
|
||||
|
||||
setCanvasRef (c) {
|
||||
this.canvas = c;
|
||||
},
|
||||
|
||||
render () {
|
||||
const { src, size, staticSrc, animate } = this.props;
|
||||
const { hovering } = this.state;
|
||||
|
||||
if (this.props.animated) {
|
||||
return (
|
||||
<div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}>
|
||||
<img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ borderRadius: '4px' }} />
|
||||
</div>
|
||||
);
|
||||
const style = {
|
||||
...this.props.style,
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
backgroundSize: `${size}px ${size}px`
|
||||
};
|
||||
|
||||
if (hovering || animate) {
|
||||
style.backgroundImage = `url(${src})`;
|
||||
} else {
|
||||
style.backgroundImage = `url(${staticSrc})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}>
|
||||
<img ref={this.setImageRef} onLoad={this.handleLoad} src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ position: 'absolute', top: '0', left: '0', opacity: hovering ? '1' : '0', borderRadius: '4px' }} />
|
||||
<canvas ref={this.setCanvasRef} style={{ borderRadius: '4px', width: this.props.size, height: this.props.size, opacity: hovering ? '0' : '1' }} />
|
||||
</div>
|
||||
<div
|
||||
className='avatar'
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -90,7 +90,7 @@ const Status = React.createClass({
|
||||
|
||||
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}>
|
||||
<div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
|
||||
<Avatar src={status.getIn(['account', 'avatar'])} size={48} />
|
||||
<Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
|
||||
</div>
|
||||
|
||||
<DisplayName account={status.get('account')} />
|
||||
|
@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
const AutosuggestAccount = ({ account }) => (
|
||||
<div style={{ overflow: 'hidden' }} className='autosuggest-account'>
|
||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div>
|
||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={18} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
);
|
||||
|
@ -17,7 +17,7 @@ const NavigationBar = React.createClass({
|
||||
render () {
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink>
|
||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink>
|
||||
|
||||
<div style={{ flex: '1 1 auto', marginLeft: '8px' }}>
|
||||
<strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong>
|
||||
|
@ -50,7 +50,7 @@ const ReplyIndicator = React.createClass({
|
||||
<div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
|
||||
|
||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
|
||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} /></div>
|
||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
</div>
|
||||
|
@ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
|
||||
<div>
|
||||
<div style={outerStyle}>
|
||||
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
|
||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={48} /></div>
|
||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
|
@ -54,7 +54,7 @@ const DetailedStatus = React.createClass({
|
||||
return (
|
||||
<div style={{ padding: '14px 10px' }} className='detailed-status'>
|
||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
|
||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} size={48} /></div>
|
||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div>
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
@import 'variables';
|
||||
|
||||
.app-body{
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
}
|
||||
|
||||
.button {
|
||||
@ -165,6 +165,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 4px;
|
||||
background: transparent no-repeat;
|
||||
background-position: 50%;
|
||||
background-clip: padding-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lightbox .icon-button {
|
||||
color: $color1;
|
||||
}
|
||||
|
Reference in New Issue
Block a user