71478


# 弧を描いて指定位置で止まりたい

  1. ゆるやかに減速しながら直進する物体を
  2. 境界線を超えたら一気に減速しつつ
  3. 入射角に見合った弧を描きながら境界線まで戻ってきて
  4. ちょうど境界線で止まる（動きが止まったときの位置がちょうど境界線上になる）

ようにしたいです

1と2まではできました（まだGoogle Chromeでしか確認していませんが）

3と4はどうすればいいですか？

よりかんたんなコードでそれっぽい動きをするならじっさいの物理（？）の公式じゃなくてもぜんぜんかまいません

## 1と2まではできたコード

ブラウザのコンソールに貼り付けて確認してください

[code]

    const MASATSU = .92
    const HAYASA = (min, max) => (Math.random() * (max - min) + min) * [ 1, -1 ][ Math.floor(Math.random() * 2) ]
    
    const BG_WIDTH = 500
    const BG_HEIGHT = 500
    const ITEM_WIDTH = 10
    const ITEM_HEIGHT = 10
    const LEFT = 100
    const TOP = 100
    const RIGHT = LEFT + BG_WIDTH
    const BOTTOM = TOP + BG_HEIGHT
    const BG = document.body.appendChild(document.createElement('div'))
    const ITEM = document.body.appendChild(document.createElement('div'))
    BG.style.position = ITEM.style.position = 'absolute'
    BG.style.width = BG_WIDTH + 'px'
    BG.style.height = BG_HEIGHT + 'px'
    BG.style.left = LEFT + 'px'
    BG.style.top = TOP + 'px'
    BG.style.background = 'yellow'
    BG.style.whiteSpace = 'pre'
    ITEM.style.width = ITEM_WIDTH + 'px'
    ITEM.style.height = ITEM_HEIGHT + 'px'
    ITEM.style.background = 'red'
    
    let zahyouX
    let zahyouY
    let hayasaX = 0
    let hayasaY = 0
    const FPS = 1000 / 60
    const LOOP = () => {
      if (Math.abs(hayasaX) < .1 || Math.abs(hayasaY) < .1) { //動きが止まったら初期化（.1の値は適当（根拠なし））
        zahyouX = LEFT + BG_WIDTH / 2 - ITEM_WIDTH / 2
        zahyouY = TOP + BG_HEIGHT / 2 - ITEM_HEIGHT / 2
        hayasaX = HAYASA(10, 100)
        hayasaY = HAYASA(10, 100)
      } else { //ゆるやかに減速
        zahyouX += hayasaX
        zahyouY += hayasaY
        hayasaX *= MASATSU
        hayasaY *= MASATSU
      }
      if (zahyouX < LEFT || RIGHT < zahyouX || zahyouY < TOP || BOTTOM < zahyouY) { //はみ出したら一気に減速
        hayasaX /= 2
        hayasaY /= 2
      }
      ITEM.style.left = zahyouX + 'px'
      ITEM.style.top = zahyouY + 'px'
      BG.innerText = [
        'ZAHYOU X : ' + zahyouX,
        'ZAHYOU Y : ' + zahyouY,
        'HAYASA X : ' + hayasaX,
        'HAYASA Y : ' + hayasaY
      ].join('\n')
      setTimeout(LOOP, FPS)
    }
    LOOP()
    
[/code]

## イメージ図

こんな感じにしたいです

[![イメージ図1](https://i.stack.imgur.com/4EXDq.png)](https://i.stack.imgur.com/4EXDq.png)

## イメージ図追記

こんな感じのほうが自然ですか？とにかくいい感じに境界線上で止まりたいです

[![イメージ図2](https://i.stack.imgur.com/9UkEr.png)](https://i.stack.imgur.com/9UkEr.png)


CC BY-SA 4.0
2020-10-24T00:32:35.500
71469
2020-10-25T11:07:37.497
2020-10-24T01:10:15.477
30127
30127
question
javascript アルゴリズム
弧を描いて指定位置で止まりたい


## 簡単なアドバイス

> よりかんたんなコードでそれっぽい動きをするならじっさいの物理（？）の公式じゃなくてもぜんぜんかまいません

実装を見る限り、物理の公式を使わないのであれば、あればcssのkeyframesを利用すると良いでしょう。

  * <https://developer.mozilla.org/ja/docs/Web/CSS/@keyframes>

## 詳細なアドバイス

さて、これは推察ですが、JavaScriptを使って自力で物体の動きを制御したいとお見受けしました。その上で回答をすると、実際の物理の公式を知らずに実装をするのは400年くらいはやいです（冗談ではなく）。

どういうことかというと、何も知らずに作るということは、ニュートン力学を自分で作るようなものなので、素直に物理を学んだほうが良いです（高校物理の力学の範囲で十分）。

位置、速度、時間、加速度、力の関係はざっくばらんに次のような関係になります。

  * 位置 = 速さ x 時間
  * 速度 = 加速度 x 時間
  * 加速度 = 物体にかかる力 / 質量 (運動の第二法則 いわゆる ma = F）

キチンと運動（物体の移動）を定義するならば、加速度を定義する必要があります。  
加速度を定義するためには、「物体にかかる力」を知る必要があります。

今回の質問の実装をみるに、

  * 境界内に物体がいる場合は、動摩擦を受けつつ、運動を行います。
  * 境界の外に物体がいる場合は、境界の方向に向かって落下、引っ張られるような力働きます。また、空気抵抗のようなものもあります。

二次関数的な弧を描きつつ停止したい、というお題からは、境界外では物体が境界方向に一定の力で引っ張られることになります。すなわち、加速度定数は位置によらず一定になります（これは重力と同じ）。これは鉛直投げ上げの問題と同じ考え方ができます。

これら加速度定数、動摩擦係数、空気抵抗の係数を決めてしまえば実装ができます。

### 実装

思いっきり実装を整理してしまっていますが、正しく知っているとこうかける、という参考程度に御覧ください。

[code]

    // FPS = 1秒間に描画する回数
    const FPS = 30;
    // 引力の加速度定数
    const gravityAcceleration = 50;
    // 1秒 = 1000ms
    const ONE_SECOND_MS = 1000;
    // 動摩擦係数
    const DYNAMIC_FRICTION_COEFF = 0.999;
    // 空気抵抗定数(0以上,1以下)
    const airResistanceConstant = 0.98;
    // 処理時間
    let timer = 0;
    
    // 境界情報
    const BORDER = {
      LEFT: 100,
      RIGHT: 600,
      TOP: 100,
      BOTTOM: 600
    };
    
    /**
     * 速さを乱数で作成
     */
    const createRandomSpeed = (min, max) => {
      return (
        (Math.random() * (max - min) + min) * [1, -1][Math.floor(Math.random() * 2)]
      );
    };
    
    /**
     * 表示領域の作成
     */
    const createDisplayArea = () => {
      const field = document.body.appendChild(document.createElement("div"));
      field.style.position = "absolute";
      field.style.width = BORDER.RIGHT - BORDER.LEFT + "px";
      field.style.height = BORDER.BOTTOM - BORDER.TOP + "px";
      field.style.left = BORDER.LEFT + "px";
      field.style.top = BORDER.TOP + "px";
      field.style.background = "yellow";
      field.style.whiteSpace = "pre";
      return field;
    };
    
    /**
     * 物体の作成
     */
    const createRigidBody = (property) => {
      const rigidBody = document.body.appendChild(document.createElement("div"));
      rigidBody.style.width = property.size.width + "px";
      rigidBody.style.height = property.size.height + "px";
      rigidBody.style.left = property.pos.x + "px";
      rigidBody.style.top = property.pos.y + "px";
      rigidBody.style.background = "red";
      rigidBody.style.position = "absolute";
      return rigidBody;
    };
    
    /**
     * 境界内にいるかどうか判定する
     */
    const isInsideBoundary = (pos) => {
      return (
        BORDER.LEFT < pos.x &&
        pos.x < BORDER.RIGHT &&
        pos.y > BORDER.TOP &&
        pos.y < BORDER.BOTTOM
      );
    };
    
    /**
     * 開始地点を計算する
     */
    const calculateStartPosition = () => {
      return {
        x: BORDER.LEFT + (BORDER.RIGHT - BORDER.LEFT) / 2,
        y: BORDER.TOP + (BORDER.BOTTOM - BORDER.TOP) / 2
      };
    };
    
    /**
     * 速さを計算する
     */
    const calculateSpeed = (velocity) => {
      return Math.sqrt(velocity.x ** 2 + velocity.y ** 2);
    };
    
    /**
     * 境界領域から離脱した方向を導き出す
     */
    const getEscapeDirection = (pos) => {
      const escapeDirection = { x: undefined, y: undefined };
      if (BORDER.RIGHT <= pos.x) {
        escapeDirection.x = "RIGHT";
      } else if (pos.x <= BORDER.LEFT) {
        escapeDirection.x = "LEFT";
      }
      if (BORDER.BOTTOM <= pos.y) {
        escapeDirection.y = "BOTTOM";
      } else if (pos.y <= BORDER.TOP) {
        escapeDirection.y = "TOP";
      }
      return escapeDirection;
    };
    
    /**
     * 引力の速度を計算する
     * @param {*} escapeDirection 離脱方向の情報
     * @param {number} escapeTime 境界を離脱してからの時間
     */
    const calculateGravityVelocity = (escapeDirection, escapeTime) => {
      const gravityVelocity = { x: 0, y: 0 };
      if (escapeDirection.x === "RIGHT") {
        gravityVelocity.x = -gravityAcceleration * escapeTime;
      }
      if (escapeDirection.x === "LEFT") {
        gravityVelocity.x = gravityAcceleration * escapeTime;
      }
      if (escapeDirection.y === "BOTTOM") {
        gravityVelocity.y = -gravityAcceleration * escapeTime;
      }
      if (escapeDirection.y === "TOP") {
        gravityVelocity.y = gravityAcceleration * escapeTime;
      }
      return gravityVelocity;
    };
    
    /**
     * 今回動かす物体の情報
     */
    const rigidBodyProperty = {
      // 大きさ
      size: {
        width: 10,
        height: 10
      },
      // 位置
      pos: calculateStartPosition(),
      // 速度
      velocity: {
        x: createRandomSpeed(50, 100),
        y: createRandomSpeed(50, 100)
      },
      // 離脱方向
      escapeDirection: {
        x: undefined,
        y: undefined
      },
      // 離脱時間
      escapeTime: undefined,
      // 境界から離脱したか
      didEscape: false
    };
    
    // 表示領域の作成
    const displayArea = createDisplayArea();
    // 物体の作成
    const rigidBody = createRigidBody(rigidBodyProperty);
    
    const update = () => {
      // 時間間隔
      const deltaTime = FPS / ONE_SECOND_MS;
    
      if (isInsideBoundary(rigidBodyProperty.pos)) {
        // 物体が境界内にいる場合
        if (rigidBodyProperty.didEscape) {
          // 過去に離脱したことがある場合
          // 物体の速度を0にする(停止)
          rigidBodyProperty.velocity = {
            x: 0,
            y: 0
          };
          return;
        }
        rigidBodyProperty.velocity.x *= DYNAMIC_FRICTION_COEFF;
        rigidBodyProperty.velocity.y *= DYNAMIC_FRICTION_COEFF;
      } else {
        // 物体が境界から離脱した場合
    
        if (rigidBodyProperty.escapeTime === undefined) {
          // 離脱時刻が記録されていない場合、現在の時刻を記録する
          rigidBodyProperty.escapeTime = timer;
          // 過去に離脱したことがあるフラグを立てる
          rigidBodyProperty.didEscape = true;
        }
    
        if (
          !rigidBodyProperty.escapeDirection.x ||
          !rigidBodyProperty.escapeDirection.y
        ) {
          // 物体の離脱方向が未定義の場合
    
          // 物体の離脱方向を算出する
          rigidBodyProperty.escapeDirection = getEscapeDirection(
            rigidBodyProperty.pos
          );
        }
    
        // 物体が離脱してから、引力から算出される速度
        const gravityVelocity = calculateGravityVelocity(
          rigidBodyProperty.escapeDirection,
          deltaTime
        );
        // 空気抵抗による減速
        rigidBodyProperty.velocity.x *= airResistanceConstant;
        rigidBodyProperty.velocity.y *= airResistanceConstant;
        // 引力による速度を加算 (dx/dt = v0 + at)
        rigidBodyProperty.velocity.x += gravityVelocity.x;
        rigidBodyProperty.velocity.y += gravityVelocity.y;
      }
    
      // 位置の更新 距離 = 速さ x 時間
      rigidBodyProperty.pos.x += rigidBodyProperty.velocity.x * deltaTime;
      rigidBodyProperty.pos.y += rigidBodyProperty.velocity.y * deltaTime;
    
      // DOMの操作
      rigidBody.style.left = rigidBodyProperty.pos.x + "px";
      rigidBody.style.top = rigidBodyProperty.pos.y + "px";
      displayArea.innerText = [
        "pos.x : " + rigidBodyProperty.pos.x.toFixed(2),
        "pos.y : " + rigidBodyProperty.pos.y.toFixed(2),
        "v.x   : " + rigidBodyProperty.velocity.x.toFixed(2),
        "v.y   : " + rigidBodyProperty.velocity.y.toFixed(2),
        "speed : " + calculateSpeed(rigidBodyProperty.velocity).toFixed(2),
        "escape: (" +
          rigidBodyProperty.escapeDirection.x +
          "," +
          rigidBodyProperty.escapeDirection.y +
          ")"
      ].join("\n");
      // 時刻の更新
      timer += deltaTime;
      // 描画の更新頻度(ms) = 1秒(1000ミリ秒)をFPSで割った値
      setTimeout(update, ONE_SECOND_MS / FPS);
    };
    
    update();
[/code]

### その他参考

もし詳しく知りたいのであれば、高校物理の力学の知識をつけることをおすすめします。  
JavaScriptでの実装上のアドバイスは特にしていませんが、メディア・アート系の情報から今回のような内容を学習するのも有りでしょう（ただし物理の知識は切っても切り離せません）。

  * [HTML 5 canvas要素 + Javascriptで作る、動的コンテンツ](https://yoppa.org/taumedia10/2065.html)


CC BY-SA 4.0
2020-10-24T13:04:27.943
71478
2020-10-25T11:07:37.497
2020-10-25T11:07:37.497
7997
7997
71469
answer