みかづきブログ・カスタム

基本的にはちょちょいのほいです。

Reactで等速で往復運動するコンポーネントをつくる 👾

単純な往復運動であればsin関数を使って実装しますが、これだと等速にはなりません。
いや、数学の得意な人であれば、どうにかして等速にできるのかもしれないですが、今回は単純に区間を区切った式で等速往復を実装します。

DEMO

※ クリックすると動き出します

ソースコード(抜粋)

function Invader({
  count,
  initX
}) {
  const maxDistance = 400; // 折り返しを行う距離
  const speed = 1;
  const size = 40;
  const [x, setX] = useState(0);

  useEffect(() => {
    setX(initX);
  }, []);

  useEffect(() => {
    const totalX = initX + count * speed;
    const level = 0 | totalX / maxDistance; // 折り返した数
    const toRight = Boolean(level % 2); // 往路(to Left)か復路(to Right)か判定
    let x = totalX % maxDistance;

    if (toRight) { // 復路の場合は位置を反転
      x = maxDistance - x;
    }

    setX(x);
  }, [count]);

  return (
    <div style={{
      left: `${ x }px`,
      position: 'absolute',
      fontSize: `${size}px`
    }}>👾</div>
  );
}

愚直に実装しました。
往路と復路で条件分岐して、位置を反転させています。

次は動きをカクカクさせてみましょう。

DEMO

※ クリックすると動き出します

ソースコード(抜粋)

function Invader({
  count,
  initX
}) {
  const maxDistance = 400;
  const speed = 1;
  const size = 40;
  const [x, setX] = useState(0);

  useEffect(() => {
    setX(initX);
  }, []);

  useEffect(() => {
    const totalX = initX + count * speed;
    const level = 0 | totalX / maxDistance;
    const toRight = Boolean(level % 2);
    let x = (0 | totalX % maxDistance / size) * size; // 一度小数点以下を切り捨てることで段階的に変化させる

    if (toRight) { // 復路の場合は位置を反転
      x = maxDistance - x;
    }

    setX(x);
  }, [count]);

  return (
    <div style={{
      left: `${ x }px`,
      position: 'absolute',
      fontSize: `${size}px`
    }}>👾</div>
  );
}

修正した箇所はコメントを入れた1箇所のみです。
小数点以下を切り捨てることでカクカク動くようになりました。
次は、端まで来た時に一歩前進させてみます。

DEMO

※ クリックすると動き出します

ソースコード(抜粋)

function Invader({
  count,
  initX,
  initY
}) {
  const maxDistance = 400;
  const speed = 1;
  const size = 40;
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  useEffect(() => {
    setX(initX);
    setY(initY);
  }, []);

  useEffect(() => {
    const totalX = initX + count * speed;
    const level = 0 | totalX / maxDistance;
    const toRight = Boolean(level % 2);
    let x = (0 | totalX % maxDistance / size) * size;
    const y = level * size; // 折り返した数だけ前進させる

    if (toRight) {
      x = maxDistance - x;
    }

    setX(x);
    setY(y);
  }, [count]);

  return (
    <div style={{
      left: `${ x }px`,
      top: `${ y }px`,
      position: 'absolute',
      fontSize: `${size}px`
    }}>👾</div>
  );
}

前進するようになりました。
しかし、前進する時は斜め方向に動いているように見えます。
なので、等速で動かすことをやめて、端に来たらX方向の移動を一時停止してみます。

DEMO


ソースコード(抜粋)

function Invader({
  count,
  initX,
  initY
}) {
  const maxDistance = 400;
  const speed = 1;
  const size = 40;
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  useEffect(() => {
    setX(initX);
    setY(initY);
  }, []);

  useEffect(() => {
    const totalX = initX + count * speed;
    const level = 0 | totalX / (maxDistance + size); // 一時停止分を考慮
    const toRight = Boolean(level % 2);
    let x = (0 | Math.min(maxDistance, totalX % (maxDistance + size)) / size) * size;  // 一時停止分を考慮
    const y = level * size;

    if (toRight) {
      x = maxDistance - x;
    }

    setX(x);
    setY(y);
  }, [count]);

  return (
    <div style={{
      left: `${ x }px`,
      top: `${ y }px`,
      position: 'absolute',
      fontSize: `${size}px`
    }}>👾</div>
  );
}

折り返すまでの距離を若干伸ばしつつ、Xの限界値は伸ばした距離を考慮しないことで、両端での一時停止を実現しました。

今回は以上です。