13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【React】飛沫が飛んで垂れるボタンを実装する

Last updated at Posted at 2021-10-01

概要

ホバーすると飛沫が飛んで垂れるボタンを実装します。

output(video-cutter-js.com) (4).gif

gooey effectについては、以下を参考してください。

スタイリングには、emotion/css(CSS in JS)を使用しています。

実装

以下のGIFは、gooey effectを外したときの描画です。
これにgooey effectを足すことで、滑らかな飛沫を表現しています。

output(video-cutter-js.com) (5).gif

.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>
13
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?