Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <body>
  <div id="container" style="width: 340px;height:1000px;">
    <div>
      <canvas id="can" width="680" height="680" style="width: 340px; height: 340px; display: block;"></canvas>
    </div>
    <div id="ctl">
      <div id="ctl_1" style="white-space: nowrap; line-height: 40px;">
        <span style="
                            width: 30%;
                            white-space: nowrap;
                            display: inline-block;
                        ">
          <button id="switch-bt" style="width: 5em;" onclick="if(window.en){window.en=false;this.innerHTML='Start';}else{window.en=true;this.innerHTML='Stop';}">
            Start
          </button>
          <span onclick="document.getElementById('options').style.display=document.getElementById('options').style.display=='none'?'':'none';">
            ⚙️
          </span> </span><span style="
                            width: 40%;
                            white-space: nowrap;
                            display: inline-block;
                            text-align: center;
                        ">
          <span id="time">00:00:00</span> </span><span style="
                            width: 30%;
                            white-space: nowrap;
                            display: inline-block;
                        ">
          <span style="float: right;">
            <span onclick='document.getElementById("timeScale").selectedIndex-=document.getElementById("timeScale").selectedIndex>0?1:0;timeScale=+document.getElementById("timeScale").options[document.getElementById("timeScale").selectedIndex].value;'>
              ◀
            </span>
            <select id="timeScale" onchange="window.timeScale=+this.options[this.selectedIndex].value;">
              <option value="1">x1</option>
              <option value="10">x10</option>
              <option value="50">x50</option>
              <option value="100">x100</option>
              <option value="500">x500</option>
            </select>
            <span onclick='document.getElementById("timeScale").selectedIndex+=document.getElementById("timeScale").selectedIndex<=document.getElementById("timeScale").options.length-2?1:0;timeScale=+document.getElementById("timeScale").options[document.getElementById("timeScale").selectedIndex].value;'>
              ▶
            </span></span>
        </span>
      </div>
      <div id="options" style="
                        border: 1px solid black;
                        padding: 5px 2px 2px 5px;
                        display: none;
                        margin-bottom: 10px;
                    ">
        <span style="margin-right: 0.5em; display: inline-block;"><input id="c1" type="checkbox" onchange="window.monitor.visibility.actual=!window.monitor.visibility.actual;" /><label for="c1">Actual</label></span>
        <span style="margin-right: 0.5em; display: inline-block;"><input id="c2" type="checkbox" checked="checked" onchange="window.monitor.visibility.observed=!window.monitor.visibility.observed;" /><label for="c2">Observed</label></span>
        <span style="margin-right: 0.5em; display: inline-block;"><input id="c3" type="checkbox" checked="checked" onchange="window.monitor.visibility.scanLine=!window.monitor.visibility.scanLine;" /><label for="c3">Scan Line</label></span>
        <span style="margin-right: 0.5em; display: inline-block;"><input id="c4" type="checkbox" checked="checked" onchange="window.monitor.visibility.enemyInfo=!window.monitor.visibility.enemyInfo;" /><label for="c4">Infomation</label></span>
      </div>
      <div>
        <textarea id="report" style="
                            width: 100%;
                            height: 6em;
                            box-sizing: border-box;
                            display: block;
                        "></textarea>
      </div>
    </div>
  </div>
</body>
              
            
!

CSS

              
                #ctl_1 * {
  vertical-align: middle;
}

@media (max-width: 450px) {
  #container {
    margin-left: auto;
    margin-right: auto;
  }
}

              
            
!

JS

              
                /*
お題: 一辺2000メートルの正方形の平面上にランダムに動く物体5個がある。正方形の中心に自機があり、自機を中心にレーダー電波が一定速度で回転しながら常に放射されている。
物体がレーダー電波に当たるとその瞬間にその物体までの距離と方向がわかる仕組みになっている。
各物体が直線等速運動をしていると仮定して随時に各物体の速度を推定し、物体が100メートルより近いときは、それが当たりそうな場所に向かってミサイルを自動的に発射する次のようなアルゴリズムを作れ。
1. 毎分ごとに物体5個の位置(Pxn, Pyn)と推定速度(Vxn, Vyn)を報告すること。
2. 自機は動かない。
3. ミサイルを発射するときは方向(Tn; ラジアン)を報告すること。ミサイルの速さは毎秒5メートルである。
4. ミサイルの個数には制限がない。
5. ミサイルと物体座標が2メートル以内であれば、ミサイル命中と見なす。命中すれば物体は消える。実際に消えたときは消えた位置を報告する。
*/

window.world;
window.monitor;
window.controller;

window.timeScale = 1;
window.en = false;
window.dt = 0.0025; //[s]

window.onload = function () {
  window.world = new World();
  window.monitor = new Monitor(world, document.getElementById("can"));
  window.controller = new Controller(world);
  setInterval(window.step, 50);
};

window.step = () => {
  // 早送り速度に応じた回数だけ繰り返す
  for (let i = 0; i < (0.05 / dt) * window.timeScale; i++) {
    if (window.en) {
      window.world.step();
    }
  }

  window.monitor.update();
  window.controller.update();
};

class World {
  constructor() {
    this.time;
    this.width = 2000; //[m]
    this.height = 2000; //[m]
    this.enemies;
    this.missiles;
    this.defense;

    this.init();
  }

  init() {
    this.time = 0.0; //[s]
    this.enemies = [];
    this.missiles = [];
    this.defense = new Defense(this);
    this.addRandomEnemies(5);
  }

  addRandomEnemies(count) {
    let genRandomEnemy = () => {
      let r = Utility.randBetween(0, (0.8 * this.width) / 2);
      let theta = Utility.randBetween(-Math.PI, Math.PI);

      return new Enemy(
        this,
        r * Math.cos(theta),
        r * Math.sin(theta),
        Utility.randBetween(2.0, 5.0),
        Utility.randBetween(-Math.PI, Math.PI)
      );
    };

    for (let i = 0; i < count; i++) {
      let i = 0;
      while (true) {
        if (i++ > 100) return false;

        let newEnemy = genRandomEnemy();

        // 中心に近い場合はやり直し
        if (
          Math.hypot(newEnemy.x, newEnemy.y) <
          this.defense.launcher.readyMissile.range
        )
          continue;

        // 他機と近い場合はやり直し
        let closedToOthers = false;
        for (const enemy of this.enemies) {
          if (
            Math.hypot(enemy.x - newEnemy.x, enemy.y - newEnemy.y) <
            2 * 5.0 * this.defense.observer.rader.period
          ) {
            closedToOthers = true;
            continue;
          }
        }
        if (closedToOthers) continue;

        this.enemies.push(newEnemy);
        break;
      }
    }

    return true;
  }

  step() {
    this.time += window.dt;

    for (const enemy of this.enemies) enemy.step();
    for (const missile of this.missiles) missile.step();
    this.defense.step();

    this.dropOutOfRangeMissiles();
    this.dropCollidedObjects();
  }

  dropOutOfRangeMissiles() {
    this.missiles = this.missiles.filter(
      (item) => Math.hypot(item.x, item.y) < item.range
    );
  }

  dropCollidedObjects() {
    let hitMissiles = [];
    for (const missile of this.missiles) {
      let hitEnemies = this.enemies.filter(
        (item) =>
          (item.x - missile.x) ** 2 + (item.y - missile.y) ** 2 < 2.0 ** 2
      );

      if (hitEnemies.length > 0) {
        this.enemies = this.enemies.filter(
          (item) => hitEnemies.indexOf(item) == -1
        );

        hitMissiles.push(missile);
      }
    }

    this.missiles = this.missiles.filter(
      (item) => hitMissiles.indexOf(item) == -1
    );
  }

  render(monitor, layer) {
    for (const enemy of this.enemies) enemy.render(monitor, layer);
    for (const missile of this.missiles) missile.render(monitor, layer);

    if (layer == 0) {
    } else if (layer == 1) {
    } else if (layer == 2) {
      // ワールドの境界線(見えない)
      monitor.ctx.strokeStyle = "rgba(0,0,0,1)";
      monitor.ctx.lineWidth = 2;
      monitor.ctx.strokeRect(
        (-monitor.cx - this.width / 2) * monitor.scaler + monitor.width / 2 - 2,
        (-monitor.cy - this.height / 2) * monitor.scaler +
          monitor.height / 2 -
          2,
        this.width * monitor.scaler + 4,
        this.height * monitor.scaler + 4
      );

      // ワールドの中心
      monitor.ctx.strokeStyle = "rgba(0,0,0,1)";
      monitor.ctx.lineWidth = 4;
      monitor.ctx.beginPath();
      monitor.ctx.moveTo(
        -monitor.cx * monitor.scaler + monitor.width / 2 - 12,
        -monitor.cy * monitor.scaler + monitor.height / 2
      );
      monitor.ctx.lineTo(
        -monitor.cx * monitor.scaler + monitor.width / 2 + 12,
        -monitor.cy * monitor.scaler + monitor.height / 2
      );
      monitor.ctx.stroke();
      monitor.ctx.moveTo(
        -monitor.cx * monitor.scaler + monitor.width / 2,
        -monitor.cy * monitor.scaler + monitor.height / 2 - 12
      );
      monitor.ctx.lineTo(
        -monitor.cx * monitor.scaler + monitor.width / 2,
        -monitor.cy * monitor.scaler + monitor.height / 2 + 12
      );
      monitor.ctx.stroke();
    }
  }
}

class Enemy {
  constructor(world, x, y, vabs, vtheta) {
    this.world = world;
    this.x = x; //[m]
    this.y = y; //[m]

    this._vabs = vabs; //[m/s]
    this._vtheta = vtheta; //[m/s]

    this.aabs = 0.0; //[m/s^2]
    this.atheta = 0.0; //[m/s^2]
  }

  get vabs() {
    return this._vabs;
  }

  set vabs(val) {
    this._vabs = val;
    this._vabs = this._vabs < 2 ? 2 : this._vabs;
    this._vabs = this._vabs > 5 ? 5 : this._vabs;
  }

  get vtheta() {
    return this._vtheta;
  }

  set vtheta(val) {
    this._vtheta = val;

    this._vtheta =
      this._vtheta > Math.PI ? this._vtheta - 2 * Math.PI : this._vtheta;
    this._vtheta =
      this._vtheta < -Math.PI ? this._vtheta + 2 * Math.PI : this._vtheta;
  }

  step() {
    this.move();
  }

  move() {
    // 平滑化されたランダム加速度を与える
    this.aabs =
      this.aabs * (1 - dt * 0.9) +
      Utility.randBetween(-1.0, 1.0) * (window.dt * 0.9);
    this.atheta =
      this.atheta * (1 - dt * 0.9) +
      Utility.randBetween(-1.5, 1.5) * (window.dt * 0.9);

    this.vabs += this.aabs * window.dt;
    this.vtheta += this.atheta * window.dt;

    // 範囲外に出ないように引き戻す
    if (this.x * this.x + this.y * this.y > 600 ** 2) {
      let vx = this.vabs * Math.cos(-this.vtheta);
      let vy = this.vabs * Math.sin(-this.vtheta);

      vx +=
        (-Math.cos(Math.atan2(this.y, this.x)) * 10.0 * window.dt) /
        (1000 - Math.hypot(this.x, this.y));
      vy +=
        (-Math.sin(Math.atan2(this.y, this.x)) * 10.0 * window.dt) /
        (1000 - Math.hypot(this.x, this.y));

      this.vabs = Math.sqrt(vx * vx + vy * vy);
      this.vtheta = -Math.atan2(vy, vx);
    }

    this.x += this.vabs * Math.cos(-this.vtheta) * window.dt;
    this.y += this.vabs * Math.sin(-this.vtheta) * window.dt;
  }

  render(monitor, layer) {
    if (layer == 0) {
    } else if (layer == 1) {
      // 機体を表示
      if (monitor.visibility.actual) {
        monitor.ctx.lineWidth = 4;
        monitor.ctx.fillStyle = "rgba(255,0,0,0.3)";
        monitor.ctx.beginPath();
        monitor.ctx.arc(
          (this.x - monitor.cx) * monitor.scaler + monitor.width / 2,
          (this.y - monitor.cy) * monitor.scaler + monitor.height / 2,
          2.0 * monitor.scaler,
          0,
          Math.PI * 2,
          false
        );
        monitor.ctx.fill();

        monitor.ctx.lineWidth = 4;
        monitor.ctx.strokeStyle = "red";
        monitor.ctx.beginPath();
        monitor.ctx.moveTo(
          (this.x - monitor.cx) * monitor.scaler + monitor.width / 2 - 5,
          (this.y - monitor.cy) * monitor.scaler + monitor.height / 2 - 5
        );
        monitor.ctx.lineTo(
          (this.x - monitor.cx) * monitor.scaler + monitor.width / 2 + 5,
          (this.y - monitor.cy) * monitor.scaler + monitor.height / 2 + 5
        );
        monitor.ctx.stroke();
        monitor.ctx.beginPath();
        monitor.ctx.moveTo(
          (this.x - monitor.cx) * monitor.scaler + monitor.width / 2 - 5,
          (this.y - monitor.cy) * monitor.scaler + monitor.height / 2 + 5
        );
        monitor.ctx.lineTo(
          (this.x - monitor.cx) * monitor.scaler + monitor.width / 2 + 5,
          (this.y - monitor.cy) * monitor.scaler + monitor.height / 2 - 5
        );
        monitor.ctx.stroke();
      }
    } else if (layer == 2) {
    }
  }
}

class Defense {
  constructor(world) {
    this.world = world;
    this.observer = new Observer(this);
    this.launcher = new Launcher(this.observer);
    this.hitHistory = [];
  }

  step() {
    this.observer.step();
    this.launcher.step();
    this.judgeMissileHit();
  }

  judgeMissileHit() {
    for (const launched of this.launcher.launchHistory) {
      if (
        this.observer.observed
          .filter((data) => data.lost)
          .every((data) => data.id != launched.data.id)
      )
        continue;

      if (this.hitHistory.some((item) => item.data.id == launched.data.id))
        continue;

      this.hitHistory.push({
        time: this.world.time,
        data: this.observer.observed.filter(
          (data) => data.lost && data.id == launched.data.id
        )[0]
      });
    }
  }

  render(monitor, layer) {
    this.observer.render(monitor, layer);
    this.launcher.render(monitor, layer);
  }
}

class Observer {
  constructor(defense) {
    this.world = defense.world;
    this.defense = defense;
    this.rader = new Rader(this);
    this.observed = [];
    this.prevSignal = [];
    this.nextId = 1;
    this.newCoordPool = [];
    this.lastDataAddedTime = this.world.time;
  }

  step() {
    this.rader.step();
    this.process();
  }

  process() {
    // 連続信号をひとまとめにする
    for (const distance of this.rader.signal) {
      let existPrevData = this.prevSignal.some(
        (d) => Math.abs(distance - d) < 1.00001 * 5.0 * window.dt
      );

      if (!existPrevData) {
        this.newCoordPool.push({
          time: this.world.time,
          x: distance * Math.cos(-this.rader.direction),
          y: distance * Math.sin(-this.rader.direction)
        });
      }
    }

    this.prevSignal = Array.from(this.rader.signal);

    // レーダーの方向が0[rad]になったときに実行
    if (
      this.rader.direction >= 0 &&
      this.rader.direction < Math.PI &&
      this.world.time - this.lastDataAddedTime > this.rader.period * 0.75
    ) {
      // this.newCoordPoolを用いてthis.observedに最新データを追加
      this.addNewDataFromNewCoordPool();

      this.lastDataAddedTime = this.world.time;
    }

    // 最新座標が観測されなくなったデータにlostフラグを立てる
    for (const data of this.observed) {
      if (!data.latest || data.lost) continue;

      if (this.world.time - data.time > 3 * this.rader.period) data.lost = true;
    }

    // 古いデータを捨てる
    this.observed = this.observed.filter(
      (item) => this.world.time - item.time < 6 * this.rader.period
    );
  }

  addNewDataFromNewCoordPool() {
    // 前回までの最新データを取得
    let latestData = this.observed
      .filter((data) => data.latest && !data.lost)
      .sort((a, b) => a.time - b.time);

    // dataの次の座標をthis.newCoordPoolから決定し,それを用いて最新データを生成
    const getNextByEstRoute = (data) => {
      if (
        data == null ||
        data.vx == null ||
        data.vy == null ||
        this.newCoordPool.length == 0
      )
        return null;

      const calcDiff = (coord) => {
        const estCoord = {
          x: data.x + data.vx * (coord.time - data.time),
          y: data.y + data.vy * (coord.time - data.time)
        };

        return (
          Math.hypot(coord.x - estCoord.x, coord.y - estCoord.y) /
          (Math.hypot(data.vx, data.vy) * (coord.time - data.time))
        );
      };

      const estNextCoord =
        this.newCoordPool
          .filter((coord) => Math.abs(calcDiff(coord)) < 0.5)
          .sort(
            (a, b) =>
              Math.hypot(data.x - a.x, data.y - a.y) -
              Math.hypot(data.x - b.x, data.y - b.y)
          )[0] || null;

      if (estNextCoord) {
        this.newCoordPool = this.newCoordPool.filter(
          (coord) => coord != estNextCoord
        );

        return {
          id: data.id,
          x: estNextCoord.x,
          y: estNextCoord.y,
          time: estNextCoord.time,
          vx: (estNextCoord.x - data.x) / (estNextCoord.time - data.time),
          vy: (estNextCoord.y - data.y) / (estNextCoord.time - data.time),
          latest: true,
          lost: false
        };
      } else {
        return null;
      }
    };

    let nextData = latestData.map(getNextByEstRoute);

    for (let i = 0; i < nextData.length; i++) {
      if (nextData[i] != null) {
        latestData[i].latest = false;
      }
    }

    let nextNextData = nextData.map(getNextByEstRoute);

    for (let i = 0; i < nextNextData.length; i++) {
      if (nextNextData[i] != null) {
        nextData[i].latest = false;
      }
    }

    // dataの次の座標をthis.newCoordPoolから決定し,それを用いて最新データを生成
    const getNextByDistance = (data) => {
      if (data == null || this.newCoordPool.length == 0) return null;

      let nearList = this.newCoordPool.filter(
        (coord) =>
          Math.hypot(coord.x - data.x, coord.y - data.y) <
          1.2 * 5.0 * this.rader.period
      );

      if (nearList.length > 0) {
        this.newCoordPool = this.newCoordPool.filter(
          (coord) => coord != nearList[0]
        );

        return {
          id: data.id,
          x: nearList[0].x,
          y: nearList[0].y,
          time: nearList[0].time,
          vx: (nearList[0].x - data.x) / (nearList[0].time - data.time),
          vy: (nearList[0].y - data.y) / (nearList[0].time - data.time),
          latest: true,
          lost: false
        };
      } else {
        return null;
      }
    };

    let noNextData = [];
    for (let i = 0; i < latestData.length; i++) {
      if (nextData[i] == null) {
        noNextData.push(latestData[i]);
      } else {
        noNextData.push(null);
      }
    }

    let exNextData = noNextData.map(getNextByDistance);

    for (let i = 0; i < exNextData.length; i++) {
      if (exNextData[i] != null) {
        latestData[i].latest = false;
      }
    }

    for (let i = 0; i < exNextData.length; i++) {
      if (exNextData[i] != null) {
        nextData[i] = exNextData[i];
      }
    }

    let nextExNextData = exNextData.map(getNextByEstRoute);

    for (let i = 0; i < nextExNextData.length; i++) {
      if (nextExNextData[i] != null) {
        nextData[i].latest = false;
      }
    }

    for (let i = 0; i < nextExNextData.length; i++) {
      if (nextExNextData[i] != null) {
        nextNextData[i] = nextExNextData[i];
      }
    }

    let noNextNextData = [];
    for (let i = 0; i < nextData.length; i++) {
      if (nextNextData[i] == null) {
        noNextNextData.push(nextData[i]);
      } else {
        noNextNextData.push(null);
      }
    }

    let exNextNextData = noNextNextData.map(getNextByDistance);

    for (let i = 0; i < exNextNextData.length; i++) {
      if (exNextNextData[i] != null) {
        nextData[i].latest = false;
      }
    }

    for (let i = 0; i < exNextNextData.length; i++) {
      if (exNextNextData[i] != null) {
        nextNextData[i] = exNextNextData[i];
      }
    }

    //生成された最新データをthis.observedへ追加(1)
    for (const data of nextData) {
      if (data != null) {
        this.observed.push(data);
      }
    }

    //生成された最新データをthis.observedへ追加(2)
    for (const data of nextNextData) {
      if (data != null) {
        this.observed.push(data);
      }
    }

    // 残った機体座標は新規データとしてthis.observedへ追加
    for (const coord of this.newCoordPool) {
      this.observed.push({
        id: this.nextId++,
        x: coord.x,
        y: coord.y,
        time: coord.time,
        vx: null,
        vy: null,
        latest: true,
        lost: false
      });

      this.newCoordPool = this.newCoordPool.filter((coord) => coord != coord);
    }
  }

  render(monitor, layer) {
    this.rader.render(monitor, layer);

    if (layer == 0) {
    } else if (layer == 1) {
      if (monitor.visibility.observed) {
        // 新しく観測された敵影を表示
        for (const coord of this.newCoordPool) {
          monitor.ctx.fillStyle = "rgba(0,0,0,0.1)";
          monitor.ctx.beginPath();
          monitor.ctx.arc(
            (coord.x - monitor.cx) * monitor.scaler + monitor.width / 2,
            (coord.y - monitor.cy) * monitor.scaler + monitor.height / 2,
            2.0 * monitor.scaler,
            0,
            Math.PI * 2,
            false
          );
          monitor.ctx.fill();

          monitor.ctx.fillStyle = "rgba(0,0,0,0.4)";
          monitor.ctx.beginPath();
          monitor.ctx.arc(
            (coord.x - monitor.cx) * monitor.scaler + monitor.width / 2,
            (coord.y - monitor.cy) * monitor.scaler + monitor.height / 2,
            7,
            0,
            Math.PI * 2,
            false
          );
          monitor.ctx.fill();
        }

        // 各機の最新データを表示
        for (const data of this.observed.filter((data) => data.latest)) {
          monitor.ctx.fillStyle = data.lost
            ? "rgba(255,0,0,0.2)"
            : "rgba(0,0,0,0.2)";
          monitor.ctx.beginPath();
          monitor.ctx.arc(
            (data.x - monitor.cx) * monitor.scaler + monitor.width / 2,
            (data.y - monitor.cy) * monitor.scaler + monitor.height / 2,
            2.0 * monitor.scaler,
            0,
            Math.PI * 2,
            false
          );
          monitor.ctx.fill();

          monitor.ctx.fillStyle = data.lost
            ? "rgba(255,0,0,1)"
            : "rgba(0,0,0,1)";
          monitor.ctx.beginPath();
          monitor.ctx.arc(
            (data.x - monitor.cx) * monitor.scaler + monitor.width / 2,
            (data.y - monitor.cy) * monitor.scaler + monitor.height / 2,
            7,
            0,
            Math.PI * 2,
            false
          );
          monitor.ctx.fill();

          if (monitor.visibility.enemyInfo) {
            monitor.ctx.textAlign = "left";
            monitor.ctx.font = "24px courier";
            if (data.vx != null && data.vy != null) {
              monitor.ctx.fillText(
                Math.hypot(data.vx, data.vy).toFixed(1) + "m/s",
                (data.x - monitor.cx) * monitor.scaler + monitor.width / 2 + 10,
                (data.y - monitor.cy) * monitor.scaler + monitor.height / 2 + 7
              );
            }
          }
        }
      }
    } else if (layer == 2) {
    }
  }
}

class Rader {
  constructor(observer) {
    this.world = observer.defense.world;
    this.defense = observer.defense;
    this.observer = observer;
    this.speed = Math.PI / 3; //[rad/s]
    this.signal = [];

    this._direction = 0.0; //[rad]
  }

  get direction() {
    return this._direction;
  }

  get period() {
    return (2 * Math.PI) / this.speed;
  }

  set direction(val) {
    this._direction = val;
    this._direction =
      this._direction > Math.PI
        ? this._direction - 2 * Math.PI
        : this._direction;
    this._direction =
      this._direction <= -Math.PI
        ? this._direction + 2 * Math.PI
        : this._direction;
  }

  step() {
    this.rotate();
    this.detect();
  }

  rotate() {
    this.direction += this.speed * window.dt;
  }

  detect() {
    //  角度幅 10 * this.speed * window.dt のビームで検出
    this.signal = [];
    for (const enemy of this.world.enemies) {
      let enemy_direction = -Math.atan2(enemy.y, enemy.x);
      let enemy_distance = Math.hypot(enemy.x, enemy.y);

      let onBeam = false;
      if (
        enemy_direction <= this.direction &&
        enemy_direction > this.direction - 10 * this.speed * window.dt
      )
        onBeam = true;

      if (this.direction - 10 * this.speed * window.dt < -Math.PI)
        if (
          enemy_direction >
          this.direction - 10 * this.speed * window.dt + 2 * Math.PI
        )
          onBeam = true;

      if (onBeam) this.signal.push(enemy_distance);
    }
  }

  render(monitor, layer) {
    if (layer == 0) {
    } else if (layer == 1) {
    } else if (layer == 2) {
      // レーダーのビームを描画
      if (monitor.visibility.scanLine) {
        monitor.ctx.lineWidth = 4;
        monitor.ctx.strokeStyle =
          "rgba(0, 0, 0," +
          (window.timeScale < 20 ? 1 / timeScale ** 0.5 : 0) +
          ")";
        monitor.ctx.beginPath();
        monitor.ctx.moveTo(
          -monitor.cx * monitor.scaler + monitor.width / 2,
          -monitor.cy * monitor.scaler + monitor.height / 2
        );
        monitor.ctx.lineTo(
          -monitor.cx * monitor.scaler +
            (this.world.width / 2) *
              Math.cos(-this.direction) *
              monitor.scaler *
              0.995 +
            monitor.width / 2,
          -monitor.cy * monitor.scaler +
            (this.world.width / 2) *
              Math.sin(-this.direction) *
              monitor.scaler *
              0.995 +
            monitor.height / 2
        );
        monitor.ctx.stroke();
      }
    }
  }
}

class Launcher {
  constructor(observer) {
    this.world = observer.defense.world;
    this.defense = observer.defense;
    this.observer = observer;
    this.launchHistory = [];
    this.recentCalced = [];
    this.readyMissile = new Missile(this.world);
  }

  step() {
    for (const data of this.observer.observed) {
      if (!data.latest || data.lost) continue;
      if (Math.hypot(data.x, data.y) > this.readyMissile.range) continue;
      if (data.vx == null || data.vy == null) continue;

      let alreadyCalced = this.recentCalced.some(
        (item) => item.data.id == data.id && item.data.time == data.time
      );
      if (alreadyCalced) continue;

      // ミサイル発射
      this.lauchTo(data);
    }

    this.recentCalced = this.recentCalced.filter(
      (item) => this.world.time - item.time < 2 * this.observer.rader.period
    );
  }

  lauchTo(data) {
    let enemyRouteLine = {
      start: {
        x: data.x + data.vx * (this.world.time - data.time),
        y: data.y + data.vy * (this.world.time - data.time)
      },
      end: {
        x:
          data.x +
          2 * this.readyMissile.range * Math.cos(Math.atan2(data.vy, data.vx)),
        y:
          data.y +
          2 * this.readyMissile.range * Math.sin(Math.atan2(data.vy, data.vx))
      }
    };

    let launchDirection = 0.0;
    let bestRatio = Infinity;
    let tmpEmemyRoute = null;

    // 360度どの方向が一番当たりやすいかスキャン
    for (
      let theta = -Math.PI;
      theta < Math.PI;
      theta += this.observer.rader.speed * window.dt
    ) {
      let missileRouteLine = {
        start: { x: 0, y: 0 },
        end: {
          x: this.readyMissile.range * Math.cos(-theta),
          y: this.readyMissile.range * Math.sin(-theta)
        }
      };

      let inter = Utility.lineIntersection(enemyRouteLine, missileRouteLine);

      if (inter != null) {
        let enemyRouteDistance = Math.hypot(
          inter.x - enemyRouteLine.start.x,
          inter.y - enemyRouteLine.start.y
        );

        let missileRouteDistance = Math.hypot(inter.x, inter.y);

        let enemyArrivalTime =
          enemyRouteDistance / Math.hypot(data.vx, data.vy);

        let misssileArrivalTime =
          missileRouteDistance / this.readyMissile.speed;

        let ratio = enemyArrivalTime / misssileArrivalTime;

        if (Math.abs(ratio - 1) < Math.abs(bestRatio - 1)) {
          bestRatio = ratio;
          launchDirection = theta;

          tmpEmemyRoute = {
            start: enemyRouteLine.start,
            end: inter
          };
        }
      }
    }

    // もっとも当たりやすい発射方向で充分当たりそうな場合に発射
    if (Math.abs(bestRatio - 1) < 0.01) {
      this.readyMissile.theta = launchDirection;
      this.readyMissile.launchTime = this.world.time;
      this.world.missiles.push(this.readyMissile);
      this.readyMissile = new Missile(this.world);

      this.launchHistory.push({
        time: this.world.time,
        data: data,
        route: tmpEmemyRoute
      });
    }

    // 一度計算されたデータは次から計算されないようにする
    this.recentCalced.push({ time: this.world.time, data: data });
  }

  render(monitor, layer) {
    if (layer == 0) {
      // ミサイル射程圏を描画
      monitor.ctx.fillStyle = "rgba(0,0,0,0.06)";
      monitor.ctx.beginPath();
      monitor.ctx.arc(
        -monitor.cx * monitor.scaler + monitor.width / 2,
        -monitor.cy * monitor.scaler + monitor.height / 2,
        this.readyMissile.range * monitor.scaler,
        0,
        Math.PI * 2,
        false
      );
      monitor.ctx.fill();

      // 予測ルートを描画
      if (monitor.visibility.actual && monitor.visibility.enemyInfo) {
        for (const item of this.launchHistory) {
          if (
            this.world.time - item.time >
            this.readyMissile.range / this.readyMissile.speed +
              this.observer.rader.period
          )
            continue;

          monitor.ctx.strokeStyle = "rgba(210,210,210,1)";
          monitor.ctx.lineWidth = 2;
          monitor.ctx.setLineDash([5, 5]);

          monitor.ctx.beginPath();
          monitor.ctx.moveTo(
            -monitor.cx * monitor.scaler + monitor.width / 2,
            -monitor.cy * monitor.scaler + monitor.height / 2
          );
          monitor.ctx.lineTo(
            (-monitor.cx + item.route.end.x) * monitor.scaler +
              monitor.width / 2,
            (-monitor.cy + item.route.end.y) * monitor.scaler +
              monitor.height / 2
          );
          monitor.ctx.lineTo(
            (-monitor.cx + item.route.start.x) * monitor.scaler +
              monitor.width / 2,
            (-monitor.cy + item.route.start.y) * monitor.scaler +
              monitor.height / 2
          );
          monitor.ctx.stroke();
          monitor.ctx.setLineDash([]);
        }
      }
    } else if (layer == 1) {
    } else if (layer == 2) {
    }
  }
}

class Missile {
  constructor(world) {
    this.world = world;
    this.x = 0.0;
    this.y = 0.0;
    this.theta = 0.0;
    this.launchTime = Infinity;
    this.speed = 5.0; //[m/s]
    this.range = 100; //[m]
  }

  step() {
    this.move();
  }

  move() {
    this.x += this.speed * Math.cos(-this.theta) * window.dt;
    this.y += this.speed * Math.sin(-this.theta) * window.dt;
  }

  render(monitor, layer) {
    if (layer == 0) {
    } else if (layer == 1) {
      // ミサイルを表示
      if (monitor.visibility.actual) {
        monitor.ctx.fillStyle = "rgb(255, 220, 0)";
        monitor.ctx.beginPath();
        monitor.ctx.arc(
          (this.x - monitor.cx) * monitor.scaler + monitor.width / 2,
          (this.y - monitor.cy) * monitor.scaler + monitor.height / 2,
          7,
          0,
          Math.PI * 2,
          false
        );
        monitor.ctx.fill();
      }
    } else if (layer == 2) {
    }
  }
}

class Monitor {
  constructor(world, can) {
    this.can = can;
    this.world = world;
    this.ctx = can.getContext("2d");
    this.width = can.width;
    this.height = can.height;
    this.visibility = {
      actual: false,
      observed: true,
      scanLine: true,
      enemyInfo: true
    };

    this._cx = 0.0;
    this._cy = 0.0;
    this._zoom = 1.0;

    this.orgTouchX;
    this.orgTouchY;
    this.orgZoom;
    this.orgDist;
    this.orgCx;
    this.orgCy;
    this.originMouseX;
    this.originMouseY;
    this.isMouseDown = false;

    this.addEventListeners();
  }

  get scaler() {
    if (!this.world) throw new Error("World does not set.");
    return (this.width / this.world.width) * this.zoom;
  }

  get zoom() {
    return this._zoom;
  }

  set zoom(val) {
    this._zoom = val > 60000 ? 60000 : val;
    this._zoom = this._zoom < 1 ? 1 : this._zoom;
    this.cx = this.cx;
    this.cy = this.cy;
  }

  get cx() {
    return this._cx;
  }

  set cx(val) {
    if (val - this.world.width / 2 / this.zoom < -this.world.width / 2)
      this._cx = -this.world.width / 2 + this.world.width / 2 / this.zoom;
    else if (val + this.world.width / 2 / this.zoom > this.world.width / 2)
      this._cx = this.world.width / 2 - this.world.width / 2 / this.zoom;
    else this._cx = val;
  }

  get cy() {
    return this._cy;
  }

  set cy(val) {
    if (val - this.world.height / 2 / this.zoom < -this.world.height / 2)
      this._cy = -this.world.height / 2 + this.world.height / 2 / this.zoom;
    else if (val + this.world.height / 2 / this.zoom > this.world.height / 2)
      this._cy = this.world.height / 2 - this.world.height / 2 / this.zoom;
    else this._cy = val;
  }

  addEventListeners() {
    this.can.addEventListener("touchstart", (e) => {
      if (e.touches.length == 1) {
        this.orgTouchX = e.touches[0].clientX;
        this.orgTouchY = e.touches[0].clientY;
      } else if (e.touches.length == 2) {
        this.orgDist = Math.hypot(
          e.touches[0].clientX - e.touches[1].clientX,
          e.touches[0].clientY - e.touches[1].clientY
        );
        this.orgTouchX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
        this.orgTouchY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
      }
      this.orgZoom = this.zoom;
      this.orgCx = this.cx;
      this.orgCy = this.cy;
    });

    this.can.addEventListener("touchmove", (e) => {
      e.preventDefault();

      if (e.touches.length == 1) {
        let newTouchX = e.touches[0].clientX;
        let newTouchY = e.touches[0].clientY;
        this.cx = this.orgCx - (2 * (newTouchX - this.orgTouchX)) / this.scaler;
        this.cy = this.orgCy - (2 * (newTouchY - this.orgTouchY)) / this.scaler;
      } else if (e.touches.length == 2) {
        let newDist = Math.hypot(
          e.touches[0].clientX - e.touches[1].clientX,
          e.touches[0].clientY - e.touches[1].clientY
        );
        this.zoom = this.orgZoom * (newDist / this.orgDist);
        let newTouchX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
        let newTouchY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
        this.cx = this.orgCx - (2 * (newTouchX - this.orgTouchX)) / this.scaler;
        this.cy = this.orgCy - (2 * (newTouchY - this.orgTouchY)) / this.scaler;
      }

      this.draw();
    });

    this.can.addEventListener("touchend", (e) => {
      if (e.touches.length == 1) {
        this.orgTouchX = e.touches[0].clientX;
        this.orgTouchY = e.touches[0].clientY;
        this.orgCx = this.cx;
        this.orgCy = this.cy;
      }
    });

    this.can.addEventListener("mousedown", (e) => {
      this.originMouseX = e.clientX - this.can.getBoundingClientRect().left;
      this.originMouseY = e.clientY - this.can.getBoundingClientRect().top;
      this.originCx = this.cx;
      this.originCy = this.cy;
      this.isMouseDown = true;
    });

    window.addEventListener("mousemove", (e) => {
      if (this.isMouseDown) {
        e.preventDefault();
        let dx =
          e.clientX - this.can.getBoundingClientRect().left - this.originMouseX;
        let dy =
          e.clientY - this.can.getBoundingClientRect().top - this.originMouseY;
        this.cx = this.originCx - (2 * dx) / this.scaler;
        this.cy = this.originCy - (2 * dy) / this.scaler;

        this.draw();
      }
    });

    window.addEventListener("mouseup", (e) => {
      this.isMouseDown = false;
      this.draw();
    });

    this.can.addEventListener("mousewheel", (e) => {
      e.preventDefault();

      let origMouseX =
        ((e.clientX - this.can.getBoundingClientRect().left) * 2 -
          this.width / 2) /
        this.scaler;
      let origMouseY =
        ((e.clientY - this.can.getBoundingClientRect().top) * 2 -
          this.height / 2) /
        this.scaler;

      if (e.wheelDelta > 0) this.zoom *= 1.2;
      else this.zoom /= 1.2;

      let afterMouseX =
        ((e.clientX - this.can.getBoundingClientRect().left) * 2 -
          this.width / 2) /
        this.scaler;
      let afterMouseY =
        ((e.clientY - this.can.getBoundingClientRect().top) * 2 -
          this.height / 2) /
        this.scaler;

      this.cx -= afterMouseX - origMouseX;
      this.cy -= afterMouseY - origMouseY;

      this.draw();
    });
  }

  update() {
    this.draw();
  }

  draw() {
    this.ctx.clearRect(0, 0, this.width, this.height);

    for (let i = 0; i < 3; i++) {
      this.world.render(this, i);
      this.world.defense.render(this, i);
      this.render(this, i);
    }
  }

  render(monitor, layer) {
    if (layer == 0) {
      if (!window.debugMode) {
        monitor.ctx.lineWidth = 2;

        monitor.ctx.strokeStyle = "rgba(230,230,230)";
        this.ctx.strokeRect(1, 1, 678, 678);

        for (let i = 1; i <= 3; i++) {
          if (i < 3) monitor.ctx.strokeStyle = "rgba(0, 0, 0, 0.1)";
          else monitor.ctx.strokeStyle = "rgba(0, 0, 0, 1)";

          monitor.ctx.beginPath();
          monitor.ctx.arc(
            -monitor.cx * monitor.scaler + monitor.width / 2,
            -monitor.cy * monitor.scaler + monitor.height / 2,
            (this.world.height / 6) * i * monitor.scaler * 0.995,
            0,
            Math.PI * 2,
            false
          );
          monitor.ctx.stroke();
        }

        for (let i = -11; i <= 11; i++) {
          if (i % 4 == 0) monitor.ctx.strokeStyle = "rgba(0, 0, 0, 0.1)";
          else monitor.ctx.strokeStyle = "rgba(0, 0, 0, 0.04)";

          monitor.ctx.beginPath();
          monitor.ctx.moveTo(
            -monitor.cx * monitor.scaler -
              ((i * this.world.height) / 24) * monitor.scaler * 0.995 +
              monitor.width / 2,
            -monitor.cy * monitor.scaler +
              (this.world.height / 2) * monitor.scaler +
              monitor.height / 2
          );
          monitor.ctx.lineTo(
            -monitor.cx * monitor.scaler -
              ((i * this.world.height) / 24) * monitor.scaler * 0.995 +
              monitor.width / 2,
            -monitor.cy * monitor.scaler -
              (this.world.height / 2) * monitor.scaler +
              monitor.height / 2
          );
          monitor.ctx.stroke();

          monitor.ctx.beginPath();
          monitor.ctx.moveTo(
            -monitor.cx * monitor.scaler +
              (this.world.height / 2) * monitor.scaler +
              monitor.width / 2,
            -monitor.cy * monitor.scaler -
              ((i * this.world.height) / 24) * monitor.scaler * 0.995 +
              monitor.height / 2
          );
          monitor.ctx.lineTo(
            -monitor.cx * monitor.scaler -
              (this.world.height / 2) * monitor.scaler +
              monitor.width / 2,
            -monitor.cy * monitor.scaler -
              ((i * this.world.height) / 24) * monitor.scaler * 0.995 +
              monitor.height / 2
          );
          monitor.ctx.stroke();
        }

        monitor.ctx.strokeStyle = "rgba(0, 0, 0, 1)";
        monitor.ctx.beginPath();
        for (let i = 0; i < 360; i++) {
          let len = i % 10 == 0 ? this.world.width / 20 : this.world.width / 40;
          monitor.ctx.moveTo(
            -monitor.cx * monitor.scaler -
              (this.world.height / 2) *
                this.scaler *
                Math.cos((i * Math.PI) / 180) *
                0.995 +
              monitor.width / 2,
            -monitor.cy * monitor.scaler -
              (this.world.height / 2) *
                this.scaler *
                Math.sin((i * Math.PI) / 180) *
                0.995 +
              monitor.height / 2
          );
          monitor.ctx.lineTo(
            -monitor.cx * monitor.scaler -
              (this.world.height / 2 - len) *
                this.scaler *
                Math.cos((i * Math.PI) / 180) *
                0.995 +
              monitor.width / 2,
            -monitor.cy * monitor.scaler -
              (this.world.height / 2 - len) *
                this.scaler *
                Math.sin((i * Math.PI) / 180) *
                0.995 +
              monitor.height / 2
          );
        }
        monitor.ctx.stroke();
      }
    } else if (layer == 1) {
    } else if (layer == 2) {
    }
  }
}

class Controller {
  constructor(world) {
    this.world = world;
    //this.lastReportTime = world.time;
    this.loggedHitInfo = [];
    this.loggedLaunchInfo = [];

    this.addEventListeners();
  }

  addEventListeners() {
    document.getElementById("timeScale").addEventListener("mousewheel", (e) => {
      e.preventDefault();
      if (e.wheelDelta > 0) {
        if (e.target.selectedIndex > 0) e.target.selectedIndex--;
      } else {
        if (e.target.selectedIndex < e.target.options.length - 1)
          e.target.selectedIndex++;
      }
      e.target.onchange();
    });
  }

  update() {
    document.getElementById("time").innerText = Utility.timeToString(
      this.world.time
    );

    /*if (this.world.time - this.lastReportTime >= 60) {
            let reportElem = document.getElementById("report1");

            while (reportElem.firstChild)
                reportElem.removeChild(reportElem.firstChild);

            for (const data of Array.from(
                this.world.defense.observer.observed
            ).sort((a, b) => a.id - b.id)) {
                if (!data.latest) continue;
                if (data.lost) continue;

                let line = document.createElement("tr");
                let idElem = document.createElement("td");
                let xElem = document.createElement("td");
                let yElem = document.createElement("td");
                let vxElem = document.createElement("td");
                let vyElem = document.createElement("td");
                idElem.innerHTML = "#" + data.id;
                xElem.innerHTML = data.x.toFixed(1);
                yElem.innerHTML = data.y.toFixed(1);
                vxElem.innerHTML = data.vx ? data.vx.toFixed(2) : "-";
                vyElem.innerHTML = data.vx ? data.vy.toFixed(2) : "-";
                line.appendChild(idElem);
                line.appendChild(xElem);
                line.appendChild(yElem);
                line.appendChild(vxElem);
                line.appendChild(vyElem);
                reportElem.appendChild(line);
            }

            this.lastReportTime = this.world.time;
        }*/

    let newHit = this.world.defense.hitHistory.filter(
      (item) => this.loggedHitInfo.indexOf(item) == -1
    );
    if (newHit.length > 0)
      document.getElementById("report").value +=
        newHit
          .map(
            (item) =>
              Utility.timeToString(this.world.time) +
              " Shot at (" +
              item.data.x.toFixed(1) +
              "m, " +
              item.data.y.toFixed(1) +
              "m)"
          )
          .join("\r\n") + "\r\n";
    this.loggedHitInfo = this.loggedHitInfo.concat(newHit);

    let newLaunched = this.world.defense.launcher.launchHistory.filter(
      (item) => this.loggedLaunchInfo.indexOf(item) == -1
    );
    if (newLaunched.length > 0)
      document.getElementById("report").value +=
        newLaunched
          .map(
            (item) =>
              Utility.timeToString(this.world.time) +
              " Launched to " +
              -Math.atan2(item.route.end.y, item.route.end.x).toFixed(2) +
              "rad"
          )
          .join("\r\n") + "\r\n";
    this.loggedLaunchInfo = this.loggedLaunchInfo.concat(newLaunched);

    if (newHit.length > 0 || newLaunched.length > 0) {
      document.getElementById("report").scrollTop = document.getElementById(
        "report"
      ).scrollHeight;
    }
  }
}

class Utility {
  static randBetween(min, max) {
    return Math.random() * (max - min) + min;
  }

  static lineIntersection(line1, line2) {
    let a = line1.start;
    let b = line1.end;
    let c = line2.start;
    let d = line2.end;
    let r = (b.x - a.x) * (d.y - c.y) - (b.y - a.y) * (d.x - c.x);
    let u = ((c.x - a.x) * (d.y - c.y) - (c.y - a.y) * (d.x - c.x)) / r;
    let v = ((c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x)) / r;

    if (u < 0 || u > 1 || v < 0 || v > 1) return null;

    let x = a.x + u * (b.x - a.x);
    let y = a.y + u * (b.y - a.y);
    return { x: x, y: y };
  }

  static timeToString(time) {
    let hour = Math.floor(time / 3600);
    let min = Math.floor((time % 3600) / 60);
    let sec = Math.floor(time % 60);
    let timeText =
      (hour < 10 ? "0" + hour : hour) +
      ":" +
      (min < 10 ? "0" + min : min) +
      ":" +
      (sec < 10 ? "0" + sec : sec);
    return timeText;
  }

  static groupBy(array, getKey) {
    return array.reduce((obj, cur, idx, src) => {
      const key = getKey(cur, idx, src);
      (obj[key] || (obj[key] = [])).push(cur);
      return obj;
    }, {});
  }
}

              
            
!
999px

Console