HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
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.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
If the stylesheet you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
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.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
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.
Using packages here is powered by Skypack, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ES6 import
usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<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>
#ctl_1 * {
vertical-align: middle;
}
@media (max-width: 450px) {
#container {
margin-left: auto;
margin-right: auto;
}
}
/*
お題: 一辺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;
}, {});
}
}
Also see: Tab Triggers