<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

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js
  3. https://unpkg.com/@types/react@17.0.37/index.d.ts
  4. https://unpkg.com/@types/react-dom@17.0.11/index.d.ts