<div class="div0"></div>
.bg {
position: relative;
background: #ccc;
}
.yukidaruma {
position: absolute;
background: #fff;
}
type VFC = React.VFC
type CSSProperties = React.CSSProperties
const {useEffect, useMemo, useRef, useState} = React
const {render} = ReactDOM
/** ゆきだるまの動きを決めるスタイル。 */
type YukidarumaStyle = {
head: CSSProperties;
body: CSSProperties;
}
/** 一周にかかるフレーム数。 */
const numFrames = 88
/** 背景の幅。 */
const bgWidth = 200
/** 背景の高さ。 */
const bgHeight = 200
/** フレームごとのゆきだるまの動きを生成する。 */
const convertToYukidarumaMotions = (
width: number,
height: number,
radius: number
): CSSProperties => {
console.log('useMemoによって、ここは一度しか実行されない')
return [...Array(numFrames)].map((_item, frame) => {
/** ゆきだるまが頭の上で描いている円が何radまで描けているか。 */
const rad = Math.PI * 2 * frame / numFrames
/** あたまのX座標。 */
const hx = radius * Math.sin(rad)
/** あたまのY座標。 */
const hy = radius * (1 - Math.cos(rad))
/** あたまとからだの楕円の高さ。 */
const partHeight = width * (height - hy) / height
/** からだのX座標。 */
const bx = 0
/** からだのY座標。 */
const by = height - partHeight
/** ゆきだるまの左上端からのオフセットX距離。 */
const ox = (bgWidth - width) / 2
/** ゆきだるまの左上端からのオフセットY距離。 */
const oy = (bgHeight - height) / 2
return {
head: {
width: `${width}px`,
height: `${partHeight}px`,
transform: `translate3d(${hx + ox}px,${hy + oy}px,0)`,
borderRadius: `${width / 2}px / ${partHeight / 2}px`,
},
body: {
width: `${width}px`,
height: `${partHeight}px`,
transform: `translate3d(${bx + ox}px,${by + oy}px,0)`,
borderRadius: `${width / 2}px / ${partHeight / 2}px`,
},
}
})
}
/** おどるゆきだるま */
const App: VFC<{}> = () => {
/**
* requestAnimationFrameのたび加算されるフレーム数。
* numFrames に達するとリセットされる。
*/
const [frame, setFrame] = useState<number>(0)
/** ゆきだるまの横幅。 */
const [width, setWidth] = useState<number>(100)
/** ゆきだるまの、一番縦に伸びているときの身長。 */
const [height, setHeight] = useState<number>(150)
/** ゆきだるまが頭の上で描いている円の直径。 */
const [radius, setRadius] = useState<number>(10)
/** requestAnimationFrameの戻り値の参照。 */
const animationRef = useRef<number | undefined>(undefined)
useEffect(() => {
const animate = () => {
setFrame((prevFrame) =>
prevFrame + 1 >= numFrames ? 0 : prevFrame + 1
)
animationRef.current = requestAnimationFrame(animate)
}
animationRef.current = requestAnimationFrame(animate)
return () => {
cancelAnimationFrame(animationRef.current)
}
}, [])
/** 背景のスタイル。 */
const bgStyle = {width: `${bgWidth}px`, height: `${bgHeight}px`}
/** ゆきだるまの動きのデータ。 */
const yukidarumaMotions =
useMemo(
() => convertToYukidarumaMotions(width, height, radius),
[width, height, radius]
)
return (<>
<div
className="bg"
style={bgStyle}
>
<div
className="yukidaruma"
style={yukidarumaMotions[frame].head}
/>
<div
className="yukidaruma"
style={yukidarumaMotions[frame].body}
/>
</div>
</>)
}
render(
<App />,
document.querySelector('.div0')
)
View Compiled
This Pen doesn't use any external CSS resources.