Created
December 17, 2015 02:29
-
-
Save yuka2py/14a2e96aac167590617b to your computer and use it in GitHub Desktop.
Rippleエフェクトだけを実現する React コンポーネント。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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})`; | |
}; | |
} | |
既知の不具合やら治したいところ(どなかた修正案あったら教えてください!)
- IE だけ1度目のクリックで onMouseDown が発生しない。ダブルクリックすると発生する。意味がわからないので、放置しています。
- this.cancelMouseEvent がイケてない。どなたか他の回避方法あったらご教示ください。
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
良いのがなかったのでアチコチ参考にしつつ書いた。
タッチデバイス、マウスデバイス両対応。
Rippleエフェクトをつけたいコンテナの中に置くだけ。
親コンテナには position: relative だけ必要。