【React】半透明なアナログ時計を実装する

<!-- =========================================================== -->
# 概要
<!-- =========================================================== -->
**上下にゆっくり動く半透明なアナログ時計**を実装します。

![output(video-cutter-js.com) (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/bd1ba662-6781-d4e7-1b97-21c63e1f0b62.gif)

:::note info
本記事は、以下の動画を<strong>React</strong>で実装したものです。
:::

https://www.youtube.com/watch?v=Li2apfUzKeE

:::note info
スタイリングには、<strong>emotion/css</strong>（CSS in JS）を使用しています。
:::

https://emotion.sh/docs/@emotion/css

https://qiita.com/nemutas/items/dac73aa645f27cde92cd

<!-- =========================================================== -->
# 実装
<!-- =========================================================== -->
```.tsx
import React, { useEffect, useRef, VFC } from 'react';
import { css, keyframes } from '@emotion/css';

export const GlassmorphismAnalogClock: VFC = () => {
	const hRef = useRef<HTMLDivElement>(null)
	const minRef = useRef<HTMLDivElement>(null)
	const secRef = useRef<HTMLDivElement>(null)

	useEffect(() => {
		const intervalID = setInterval(() => {
			const deg = 6
			let day = new Date()
			let hh = day.getHours() * 30
			let mm = day.getMinutes() * deg
			let ss = day.getSeconds() * deg

			hRef.current!.style.transform = `rotateZ(${hh + mm / 12}deg)`
			minRef.current!.style.transform = `rotateZ(${mm}deg)`
			secRef.current!.style.transform = `rotateZ(${ss}deg)`
		})

		return () => clearInterval(intervalID)
	}, [])

	return (
		<div className={styles.screen}>
			<div className={styles.container}>
				<div className={styles.box}>
					<div className={styles.clock}>
						{/* hour hand */}
						<div className={styles.handContainer}>
							<div ref={hRef} className={styles.hour}></div>
						</div>
						{/* minutes hand */}
						<div className={styles.handContainer}>
							<div ref={minRef} className={styles.minutes}></div>
						</div>
						{/* seconds hand */}
						<div className={styles.handContainer}>
							<div ref={secRef} className={styles.seconds}></div>
						</div>
					</div>
				</div>
			</div>
		</div>
	)
}

const animations = {
	animate: keyframes`
    0%, 100% {
      transform: translateY(10px);
    }
    50% {
      transform: translateY(-10px);
    }
  `
}

const templates = {
	flex: css`
		display: flex;
		justify-content: center;
		align-items: center;
	`,
	hand: css`
		display: flex;
		justify-content: center;
		position: absolute;
		border-radius: 50%;
	`
}

const styles = {
	screen: css`
		position: relative;
		width: 100%;
		height: 100%;
		${templates.flex}

		&::before {
			content: '';
			position: absolute;
			top: 0;
			left: 0;
			width: 100%;
			height: 100%;
			background: linear-gradient(45deg, #e91e63 50%, #ffc107 50%);
		}
		&::after {
			content: '';
			position: absolute;
			top: -20px;
			left: 0;
			width: 100%;
			height: 100%;
			background: linear-gradient(160deg, #03a9f4 50%, transparent 50%);
			animation: ${animations.animate} 5s ease-in-out infinite;
		}
	`,
	container: css`
		position: relative;

		&::before {
			content: '';
			position: absolute;
			bottom: -150px;
			width: 100%;
			height: 60px;
			background: radial-gradient(rgba(0, 0, 0, 0.2), transparent, transparent);
			border-radius: 50%;
		}
	`,
	box: css`
		position: relative;
		z-index: 1;
		width: 400px;
		height: 400px;
		backdrop-filter: blur(25px);
		border-radius: 50%;
		border: 1px solid rgba(255, 255, 255, 0.5);
		animation: ${animations.animate} 5s ease-in-out infinite;
		animation-delay: -2.5s;
	`,
	clock: css`
		position: absolute;
		top: 10px;
		bottom: 10px;
		left: 10px;
		right: 10px;
		${templates.flex}
		background: radial-gradient(transparent, rgba(255, 255, 255, 0.2)), url('/assets/clock.png');
		background-size: cover;
		border-radius: 50%;
		backdrop-filter: blur(25px);
		border: 1px solid rgba(255, 255, 255, 0.5);
		border-bottom: none;
		border-right: none;
		box-shadow: -10px -10px 20px rgba(255, 255, 255, 0.1), 10px 10px 20px rgba(0, 0, 0, 0.1),
			0px 40px 50px rgba(0, 0, 0, 0.2);

		&::before {
			content: '';
			position: absolute;
			width: 15px;
			height: 15px;
			background-color: #fff;
			border-radius: 50%;
			z-index: 1000;
		}
	`,
	handContainer: css`
		position: absolute;
		${templates.flex}
	`,
	hour: css`
		width: 160px;
		height: 160px;
		${templates.hand}

		&::before {
			content: '';
			position: absolute;
			width: 8px;
			height: 80px;
			background-color: #ff105e;
			z-index: 11;
			border-radius: 6px;
		}
	`,
	minutes: css`
		width: 190px;
		height: 190px;
		${templates.hand}

		&::before {
			content: '';
			position: absolute;
			width: 4px;
			height: 90px;
			background-color: #fff;
			z-index: 12;
			border-radius: 6px;
		}
	`,
	seconds: css`
		width: 230px;
		height: 230px;
		${templates.hand}

		&::before {
			content: '';
			position: absolute;
			width: 2px;
			height: 150px;
			background-color: #fff;
			z-index: 12;
			border-radius: 6px;
		}
	`
}
```

* `background`に**linear-gradient**を使用して、斜めの領域（`#03a9f4`・`#e91e63`・`#ffc107`）を作成しています。（個人的にここが一番参考になりました）

```.tsx
screen: css`
	・・・

	&::before {
		・・・
		background: linear-gradient(45deg, #e91e63 50%, #ffc107 50%);
	}
	&::after {
		・・・
		background: linear-gradient(160deg, #03a9f4 50%, transparent 50%);
	}
`
```

* **setInterval**の第2引数には、`delay`を取ることができ、指定した間隔でcallback関数が実行されます。
`delay`を省略した場合、setIntervalを呼び出せるタイミングで、callback関数が実行されるようです。

https://developer.mozilla.org/ja/docs/Web/API/setInterval

* 時針（分針、秒針）は、以下のようなイメージでパーツを作成して、灰色の領域（実際は透明）を回転させています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/7d042765-614a-424a-00b3-1809315e48b5.png" width="500">

```.tsx
hour: css`
	position: absolute;
	width: 160px;
	height: 160px;
	display: flex;
	justify-content: center;
	border-radius: 50%;

	&::before {
		content: '';
		position: absolute;
		width: 8px;
		height: 80px;
		background-color: #ff105e;
		z-index: 11;
		border-radius: 6px;
	}
`
```