Skip to content

Instantly share code, notes, and snippets.

@yuka2py
Created December 17, 2015 02:29
Show Gist options
  • Save yuka2py/14a2e96aac167590617b to your computer and use it in GitHub Desktop.
Save yuka2py/14a2e96aac167590617b to your computer and use it in GitHub Desktop.
Rippleエフェクトだけを実現する React コンポーネント。
import React from 'react';
import assign from 'object-assign';
export default class Ripple extends React.Component
{
constructor(props) {
super(props);
this.canvas = null;
this.context = null;
this.waves = [];
this.devicePixelRatio = window.devicePixelRatio || 1;
this.maxRadius = 0.0;
this.alphaColorBuilder = null;
this.inAnimation = false;
this.isActive = false;
this.stateChangeAt = 0;
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleMouseOut = this.handleMouseOut.bind(this);
this.handleMouseOver = this.handleMouseOver.bind(this);
this.animate = this.animate.bind(this);
}
componentDidMount() {
super.componentDidMount();
this.setup();
}
componentDidUpdate() {
super.componentDidUpdate();
this.setup();
}
componentWillUnmount() {
super.componentWillUnmount();
this.canvas = null;
this.context = null;
this.handleTouchStart = null;
this.handleTouchEnd = null;
this.handleMouseDown = null;
this.handleMouseUp = null;
this.handleMouseOut = null;
this.handleMouseOver = null;
this.animate = null;
}
animate() {
this.inAnimation = true;
let animDuration = this.props.animDuration;
let fillMaxAlpha = this.props.fillMaxAlpha;
let waveMaxAlpha = this.props.waveMaxAlpha;
let now = new Date().getTime();
if (!this.context) {
return;
}
//canvasクリア
let ctx = this.context;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//背景色(アクティブなら濃くなり、非アクティブで薄くなって消える)
let isActive = this.isActive;
let progress = Math.min(1.0, (now - this.stateChangeAt) / animDuration);
let fillAlpha = (isActive ? progress : (1 - progress)) * fillMaxAlpha;
ctx.fillStyle = this.alphaColorBuilder(fillAlpha);
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//波紋(アクティブ状態に関係なく、波は広がり、最後は薄くなって消える)
let activeWaves = [];
this.waves.forEach(wave => {
//波が発生してからの経過時間
let delta = now - wave.createAt;
//進行度。1が波が一番ハッキリするポイント。1以上で消えていき、2で消える。
let progress = delta / animDuration;
//波の半径。時間の経過とともに広がっていく
let radius = progress * this.maxRadius;
//波の不透過度。アクティブなうちは不透明に、非アクティブなものは透明に向かう
let alpha = progress <= 1
? Math.min(waveMaxAlpha, progress * waveMaxAlpha)
: Math.min(waveMaxAlpha, (2 - progress) * waveMaxAlpha);
//完了していない波を描画
if (2 >= progress) {
activeWaves.push(wave);
ctx.beginPath();
ctx.arc(wave.pos.x, wave.pos.y, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = this.alphaColorBuilder(alpha);
ctx.fill();
}
});
//完了していない波のみ残す
this.waves = activeWaves;
//アニメーションの終了または継続
if (0 === this.waves.length && !isActive && fillAlpha == 0) {
this.inAnimation = false;
} else {
setTimeout(this.animate, 0);
}
}
activate(point = null) {
this.isActive = true;
this.stateChangeAt = new Date().getTime();
//ポイントがあれば、波を作成
if (point) {
let canvas = this.canvas;
let rect = canvas.getBoundingClientRect();
let x = point.x - rect.left;
let y = point.y - rect.top;
if (!isNaN(x) && !isNaN(y)) {
this.waves.push({
pos: { x, y },
createAt: new Date().getTime(),
isCompleted: false
});
}
}
//アニメーション中でなければ、アニメーションを開始
if (!this.inAnimation) {
this.animate();
}
}
deactivate() {
this.isActive = false;
this.stateChangeAt = new Date().getTime();
}
setup() {
this.canvas = this.refs.canvas;
this.context = this.canvas.getContext('2d');
this.color = window.getComputedStyle(this.canvas).color;
this.alphaColorBuilder = createAlphaColorBuilder(this.color);
this.maxRadius = (this.canvas.clientWidth + this.canvas.clientHeight) / 2;
this.canvas.setAttribute('width', this.canvas.clientWidth * this.devicePixelRatio + 'px');
this.canvas.setAttribute('height', this.canvas.clientHeight * this.devicePixelRatio + 'px');
this.context.scale(this.devicePixelRatio, this.devicePixelRatio);
}
handleMouseOver() {
if (this.cancelMouseEvent) return;
this.activate();
}
handleMouseOut() {
if (this.cancelMouseEvent) return;
this.deactivate();
}
handleMouseDown(e) {
if (this.cancelMouseEvent) return;
this.activate({
x: e.clientX,
y: e.clientY
});
}
handleMouseUp() {
if (this.cancelMouseEvent) return;
this.deactivate();
}
handleTouchStart(e) {
if (e.changedTouches && e.changedTouches[0]) {
this.activate({
x: e.changedTouches[0].clientX,
y: e.changedTouches[0].clientY
});
}
}
handleTouchEnd(e) {
this.deactivate(e);
//開発注:
//iOS(タッチデバイス?)において、touchEndイベント時にマウスイベントが発生するものがあった。
//e.preventDefault() すると、クリックイベントも発火されなくなるため、
//ここでは、touchEndイベント後の0.5秒間のマウスイベントをキャンセルして回避した。
this.cancelMouseEvent = true;
setTimeout(() => {
this.cancelMouseEvent = false;
}, 500);
}
render() {
let { className, style, ...props } = this.props;
style = assign({
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
width: '100%',
height: '100%'
}, style);
let classNames = ['paper-ripple'];
if (className) {
classNames.push(className);
}
return <canvas
ref="canvas"
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
onTouchStart={this.handleTouchStart}
onTouchEnd={this.handleTouchEnd}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
className={classNames.join(' ')}
style={style}
{...props}
/>
}
}
Ripple.defaultProps = {
animDuration: 250,
fillMaxAlpha: 0.15,
waveMaxAlpha: 0.25,
canvasStyle: {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
width: '100%',
height: '100%'
}
};
function createAlphaColorBuilder(color) {
let matches = color.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
let [, r, g, b] = matches ? matches : [0, 255, 255, 255];
return function(alpha) {
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
}
@yuka2py
Copy link
Author

yuka2py commented Dec 17, 2015

良いのがなかったのでアチコチ参考にしつつ書いた。
タッチデバイス、マウスデバイス両対応。
Rippleエフェクトをつけたいコンテナの中に置くだけ。
親コンテナには position: relative だけ必要。

@yuka2py
Copy link
Author

yuka2py commented Dec 17, 2015

既知の不具合やら治したいところ(どなかた修正案あったら教えてください!)

  • IE だけ1度目のクリックで onMouseDown が発生しない。ダブルクリックすると発生する。意味がわからないので、放置しています。
  • this.cancelMouseEvent がイケてない。どなたか他の回避方法あったらご教示ください。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment