概要
ホバーすると飛沫が飛んで垂れるボタンを実装します。
gooey effectについては、以下を参考してください。
スタイリングには、emotion/css(CSS in JS)を使用しています。
実装
以下のGIFは、gooey effectを外したときの描画です。
これにgooey effectを足すことで、滑らかな飛沫を表現しています。
.tsx
import React, { useRef, useState, VFC } from 'react';
import { VscGithub } from 'react-icons/vsc';
import { css, cx } from '@emotion/css';
export const GooeyEffectButton: VFC = () => {
const splashCount = 10 // 飛沫の数
const splashSizeRange = [20, 30] // 飛沫の大きさの範囲(px)
const splashRange = [-80, 80] // 飛び散る範囲(px)
const dripRange = [0, 100] // 飛沫の垂れ具合(px)
const [hover, setHover] = useState(false)
const splashParamsRef = useRef<{ size: number; mx: number; my: number; drip: number }[]>(
[...Array(splashCount)].map(() => {
return {
size: 25,
mx: 0,
my: 0,
drip: 0
}
})
)
/** 指定範囲のランダムな整数を返す */
const getRandomInt = (min: number, max: number) => {
const ratio = max - min + 1 // contain max val
return Math.floor(Math.random() * ratio) + min
}
const mouseEnterHandler = () => {
splashParamsRef.current = splashParamsRef.current.map(() => {
return {
size: getRandomInt(splashSizeRange[0], splashSizeRange[1]),
mx: getRandomInt(splashRange[0], splashRange[1]),
my: getRandomInt(splashRange[0], splashRange[1]),
drip: getRandomInt(dripRange[0], dripRange[1])
}
})
setHover(true)
}
return (
<div className={styles.container}>
{/* filter */}
<svg style={{ width: 0, height: 0 }}>
<filter id="gooey">
<feGaussianBlur in="SourceGraphic" stdDeviation="10" />
<feColorMatrix
values="
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 20 -5
"
/>
</filter>
</svg>
{/* contents */}
<div className={styles.outer}>
<div className={styles.splashContainer}>
<div className={styles.circle}>
{splashParamsRef.current.map((p, i) => (
<div
key={i}
className={cx(styles.splash(p.size), {
[styles.hover(p.mx, p.my, p.drip)]: hover
})}>
{/* さらに遠くに飛沫を飛ばす */}
<div
className={cx(styles.splash(p.size * 0.9), {
[styles.hover(p.mx * 1.1, p.my * 1.1, p.drip)]: hover
})}
/>
</div>
))}
</div>
</div>
<VscGithub
className={styles.icon}
onMouseEnter={mouseEnterHandler}
onMouseLeave={() => setHover(false)}
/>
</div>
</div>
)
}
const templates = {
flex: css`
display: flex;
justify-content: center;
align-items: center;
`
}
const styles = {
container: css`
position: relative;
width: 100vw;
height: 100vh;
${templates.flex}
`,
outer: css`
position: absolute;
width: 500px;
height: 500px;
${templates.flex}
border: 1px solid #404040;
border-radius: 50%;
`,
splashContainer: css`
position: relative;
width: 100%;
height: 100%;
${templates.flex}
border-radius: 50%;
overflow: hidden;
filter: url(#gooey);
`,
circle: css`
position: relative;
width: 50px;
height: 50px;
${templates.flex}
background-color: white;
border-radius: 50%;
box-shadow: 0 0 30px white;
`,
splash: (size: number) => css`
position: absolute;
width: ${size}px;
height: ${size}px;
${templates.flex}
background-color: white;
border-radius: 50%;
box-shadow: 0 0 30px white;
transform: translate(0px, 0px);
transition: transform 0.5s;
&::before {
content: '';
position: absolute;
width: ${size * 0.8}px;
height: ${size * 0.8}px;
border-radius: 50%;
background-color: white;
transform: translate(0px, 0px);
transition: transform 2s;
}
&::after {
content: '';
position: absolute;
width: ${size * 0.4}px;
height: ${0}px;
background-color: white;
transform: translate(0px, 0px);
transition: 2s;
}
`,
hover: (mx: number, my: number, drip: number) => css`
transform: translate(${mx}px, ${my}px);
&::before {
transform: translate(0px, ${drip}px);
}
&::after {
height: ${drip}px;
transform: translate(0px, ${drip / 2}px);
}
`,
icon: css`
position: absolute;
width: 70px;
height: 70px;
font-size: 2rem;
border-radius: 50%;
cursor: pointer;
color: #1e1e1e;
`
}
- 飛沫のサイズ、飛ぶ範囲、垂れ具合はすべてランダムに決めています。
GitHubアイコンをホバーしたタイミングで、ランダムな値を決めて、hover状態をtrueにしています。
.tsx
/** 指定範囲のランダムな整数を返す */
const getRandomInt = (min: number, max: number) => {
const ratio = max - min + 1 // contain max val
return Math.floor(Math.random() * ratio) + min
}
const mouseEnterHandler = () => {
splashParamsRef.current = splashParamsRef.current.map(() => {
return {
size: getRandomInt(splashSizeRange[0], splashSizeRange[1]),
mx: getRandomInt(splashRange[0], splashRange[1]),
my: getRandomInt(splashRange[0], splashRange[1]),
drip: getRandomInt(dripRange[0], dripRange[1])
}
})
setHover(true)
}
- 飛沫は2段階にわけて飛ばしています。
.tsx
<div
key={i}
className={cx(styles.splash(p.size), {
[styles.hover(p.mx, p.my, drip)]: hover
})}>
{/* さらに遠くに飛沫を飛ばす */}
<div
className={cx(styles.splash(p.size * 0.9), {[styles.hover(p.mx * 1.1, p.my * 1.1, drip)]: hover})}
/>
</div>