Files
miti.sh/html/images/space.svg

1101 lines
36 KiB
XML

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="-200 -150 400 300" version="1.1" xmlns="http://www.w3.org/2000/svg">
<!-- <svg viewBox="-10 -10 60 60" version="1.1" xmlns="http://www.w3.org/2000/svg"> -->
<style>
foreignObject {
font-size: 4pt;
font-family: courier;
color: white;
}
#info {
position: absolute;
right: 0px;
padding: 1px;
}
#pointer {
position: absolute;
right: 0px;
bottom: 0px;
padding: 1px;
}
rect#bg {
fill: gray;
}
.ship circle {
fill: white;
}
circle.bullet {
fill: yellow;
}
#triangles polygon {
fill-opacity: 0.2;
stroke-width: 1px;
fill: none;
stroke: none;
}
#triangles polygon.clockwise-orientation {
fill: white;
stroke: red;
}
#triangles polygon.obtuse {
stroke: orangered;
stroke-dasharray: 5 10;
}
#triangles polygon.anti-clockwise {
stroke: orange;
stroke-dasharray: 1 5;
}
#edges line {
stroke: gold;
}
#legs {
display: none;
}
.wall {
opacity: 0.5;
}
.wall.inverse {
fill: gray;
}
line:not(#cannon) {
stroke: green;
stroke-width: 0.5px;
}
line#velocity-indicator {
stroke: blue;
/* stroke-width: 1px; */
}
line#acceleration-indicator {
stroke: maroon;
/* stroke-width: 0.5px; */
}
#lines circle {
fill: purple;
opacity: 0.2;
r: 5px;
}
</style>
<rect id="bg" x="-200" y="-150" width="400" height="300"/>
<!-- <polygon class="wall inverse" points="-180,-40 -170,-120 40,-130 170,-120 180,40 170,130 -40,130 -170,120" /> -->
<!-- <polygon class="wall inverse" points="-160,-40 -170,-120 40,-110 170,-120 160,40 170,130 -40,110 -170,120" /> -->
<g id="player" class="ship">
<line id="cannon" x1="0" y1="0" x2="0" y2="8" stroke="black"/>
<circle id="body" cx="0" cy="0" r="5"/>
<line id="velocity-indicator" x1="0" y1="0" x2="0" y2="0"/>
<line id="acceleration-indicator" x1="0" y1="0" x2="0" y2="0"/>
<g id="legs">
<path d="M 3 2 l 2 2 v 2 m -2 0 h 4" stroke="black" fill="none" />
<path d="M -3 2 l -2 2 v 2 m -2 0 h 4" stroke="black" fill="none" />
</g>
</g>
<!-- <polygon class="wall" points="20,20 30,20 40,40 20,40" /> -->
<!-- <polygon class="wall" points="20,20 50,20 70,40 70,70 50,90 20,90 0,70 0,40" /> -->
<!-- <polygon class="wall" points="-10,-10 -20,-10 -20,-20 -10,-20" /> -->
<!-- <polygon class="wall" points="-50,-50 -60,-50 -60,-60 -50,-60" /> -->
<!-- <polygon class="wall" points="10,-50 3,-50 3,-60 10,-60" /> -->
<!-- <polygon class="wall" points="-10,50 -3,50 -3,60 -10,60" /> -->
<polygon class="wall" points="20,-50 10,-50 10,-60 20,-60" />
<polygon class="wall" points="-20,50 -10,50 -10,60 -20,60" />
<!-- <polygon class="wall" points="-10,20 10,10 10,20" /> -->
<!-- <polygon class="wall" points="20,20 40,20 40,40 20,40" /> -->
<!-- <polygon class="wall" points="10,10 20,10 20,20 10,20" /> -->
<!-- <polygon class="wall" points="20,-50 -50,-50 -60,-70 -50,-100 80,-100 80,-90 -20,-90 -20,-60 40,-60 40,40 20,40" /> -->
<!-- <polygon class="wall" points="-10,-30 -10,-40 30,-50 60,-30 80,0 150,0 150,10 60,50 -10,40 -20,20 20,20 20,-20" /> -->
<!-- <g> -->
<!-- <polygon class="wall" points="-130,-80 -40,-70 -70,-10" /> -->
<!-- <polygon class="wall" points="50,70 90,-10 130,70" /> -->
<!-- </g> -->
<!-- <g> -->
<!-- <polygon class="wall" points="-130,-80 -40,-70 -70,-10" /> -->
<!-- <polygon class="wall" points="50,70 90,-10 130,70" /> -->
<!-- <polygon class="wall" points="-130,100 -70,50 -40,110" /> -->
<!-- </g> -->
<!-- <g> -->
<!-- <polygon class="wall" points="-5,-25 -20,-40 20,-40 5,-25" /> -->
<!-- <polygon class="wall" points="-30,20 -20,30 -20,50 -50,20" /> -->
<!-- <polygon class="wall" points="20,30 30,20 50,20 20,50" /> -->
<!-- </g> -->
<g id="triangles"></g>
<g id="edges"></g>
<g id="lines"></g>
<g id="bullets"></g>
<foreignObject x="-200" y="-150" width="100%" height="100%">
<div id="info" xmlns="http://www.w3.org/1999/xhtml">
<span id="time" xmlns="http://www.w3.org/1999/xhtml">0</span> s
<span id="fps" xmlns="http://www.w3.org/1999/xhtml">-</span> fps
</div>
<ul xmlns="http://www.w3.org/1999/xhtml">
<li xmlns="http://www.w3.org/1999/xhtml">bounce from collisions</li>
<li xmlns="http://www.w3.org/1999/xhtml">gravity</li>
<li xmlns="http://www.w3.org/1999/xhtml">fall off screen after crash</li>
<li xmlns="http://www.w3.org/1999/xhtml">make ship a helicopter</li>
<li xmlns="http://www.w3.org/1999/xhtml">use paths for walls</li>
<li xmlns="http://www.w3.org/1999/xhtml">no walls</li>
<li xmlns="http://www.w3.org/1999/xhtml">stop reading data from elements</li>
<li xmlns="http://www.w3.org/1999/xhtml">ability to land</li>
<li xmlns="http://www.w3.org/1999/xhtml">limited fuel</li>
<li xmlns="http://www.w3.org/1999/xhtml">additional cannon firing modes</li>
<li xmlns="http://www.w3.org/1999/xhtml">only light up the collided wall</li>
<li xmlns="http://www.w3.org/1999/xhtml">keep ship position at 0,0 actual</li>
<li xmlns="http://www.w3.org/1999/xhtml">only start on movement not just any keypress</li>
</ul>
<pre id="debug" xmlns="http://www.w3.org/1999/xhtml"></pre>
<div id="pointer" xmlns="http://www.w3.org/1999/xhtml">
x: <span class="x" xmlns="http://www.w3.org/1999/xhtml">-</span>,
y: <span class="y" xmlns="http://www.w3.org/1999/xhtml">-</span>
</div>
</foreignObject>
<script type="text/javascript">//<![CDATA[
const namespaceURIsvg = 'http://www.w3.org/2000/svg';
const degsRegex = /(-?\d*\.{0,1}\d+)deg/g;
const regex = /(-?\d*\.{0,1}\d+)px/g;
const bullets = [];
const halfPi = Math.PI / 2;
const maxSpeed = 100;
const drawCollisionLines = false;
let previous, zero, frameCount = 0;
// let position = [0, 0]; // meters
// let velocity = [0, 0]; // meters per second
// let acceleration = [0, 0]; // meters per second per second
let position; // meters
let velocity; // meters per second
let acceleration; // meters per second per second
let rotate = 0;
let mult = 20;
const s = {
position: { x: 0, y: 0 },
// velocity: { x: 0, y: -10 },
// velocity: { x: 10, y: 10 },
// velocity: { x: -10, y: -10 },
// velocity: { x: 10, y: -10 },
// velocity: { x: -10, y: 10 },
velocity: { x: -10, y: 0 },
// velocity: { x: 10, y: 0 },
// velocity: { x: 0, y: -5000 },
// velocity: { x: 0.25 * mult, y: 1 * mult },
// velocity: { x: 0, y: 1 * mult },
acceleration: { x: 0, y: 0 },
rotate: 0,
collision: null,
isLanded: false,
gearDown: false,
node: null
};
let friction = 0;
let rotationSpeed = 0.25;
let started = false;
let restart = false;
let isReadingKeys = true;
const svg = document.querySelector('svg');
const bg = svg.querySelector('#bg');
const fps = document.querySelector("#fps");
const time = document.querySelector("#time");
const debug = document.querySelector("#debug");
const ship = document.querySelector(".ship");
s.node = ship;
const gun = ship.querySelector('#cannon');
const shipBody = ship.querySelector("#body");
const shipRadius = +shipBody.getAttribute('r');
const legs = ship.querySelector("#legs");
// const walls = document.querySelectorAll('.wall:not(.inverse)');
const walls = document.querySelectorAll('.wall');
const bulletsContainer = document.querySelector("#bullets");
const triangleContainer = document.querySelector('#triangles');
const linesContainer = document.querySelector("#lines");
const edgeContainer = document.querySelector('#edges');
const velIndic = document.querySelector('#velocity-indicator');
const acclIndic = document.querySelector('#acceleration-indicator');
const bulletPt = svg.createSVGPoint();
const cornerPt = svg.createSVGPoint();
const allWallCorners = [...walls].map(wall => {
const cs = wall.getAttribute('points').split(' ').map(coords => {
const [x, y] = coords.split(',');
return [+x, +y];
});
return wall.classList.contains("inverse") ? cs.reverse() : cs;
});
// console.log("allWallCorners", allWallCorners);
const ws = [...walls].map(node => {
return {
node,
corners: node.getAttribute('points').split(' ').map(coords => {
const [x, y] = coords.split(',');
const pt = svg.createSVGPoint();
pt.x = +x;
pt.y = +y;
return pt;
})
};
});
const edgeszz = [...walls].map(wall => {
const es = wall.getAttribute('points').split(' ').map((coords, i, arr) => {
const [x, y] = coords.split(',');
// return [+x, +y];
// const [xb, yb] = arr[i]++;
const [xb, yb] = arr[(i + 1) % arr.length].split(',');
// console.log("coords", { xa: +x, ya: +y, xb: +xb, yb: +yb, inverse: wall.classList.contains("inverse") });
// return `${+x},${+y} ${+xb},${+yb}`;
return {
xa: +x,
ya: +y,
xb: +xb,
yb: +yb,
};
});
// return wall.classList.contains("inverse") ? cs.reverse() : cs;
return { node: wall, edges: es };
});
const allEdgePts = allWallCorners.map(w =>
w.map((pt, i, arr) => [pt, arr[(i + 1) % arr.length]])
);
let allStartingEdges;
init(allEdgePts);
function distance(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
function drawAllEdges(walls) {
walls.forEach(edges => drawEdges(edges));
}
function drawEdges(pts) {
let edges = pts.map(e => e.join(' '));
edges.forEach(e => {
const [x, y] = e.split(' ');
const [x1, y1] = x.split(',');
const [x2, y2] = y.split(',');
const el = document.createElementNS(namespaceURIsvg, 'line');
el.setAttribute('x1', x1);
el.setAttribute('y1', y1);
el.setAttribute('x2', x2);
el.setAttribute('y2', y2);
edgeContainer.appendChild(el)
});
}
function drawTriangles(container, walls, [positionX, positionY]) {
walls.forEach(pts =>
pts.forEach(([[x1, y1], [x2, y2]]) => {
const el = document.createElementNS(namespaceURIsvg, 'polygon');
const attr = `${x1},${y1} ${x2},${y2} ${positionX},${positionY}`
el.setAttribute('points', attr);
container.appendChild(el);
})
);
}
// Triangle has a clockwise orientation
function isClockwise([xa, ya], [xb, yb], [xc, yc]) {
// https://en.wikipedia.org/wiki/Curve_orientation#Practical_considerations
// Determinant for a convex polygon
const det = (+xb - +xa) * (+yc - +ya) - (+xc - +xa) * (+yb - +ya);
return det < 0;
}
function isAcute([xa, ya], [xb, yb], [xc, yc]) {
const da = distance(xa, ya, xc, yc);
const db = distance(xb, yb, xc, yc);
const dc = distance(xa, ya, xb, yb);
// https://en.wikipedia.org/wiki/Law_of_cosines
// Solve for angles alpha and beta with inverse cosine (arccosine)
const alpha = Math.acos((db ** 2 + dc ** 2 - da ** 2) / (2 * db * dc));
const beta = Math.acos((da ** 2 + dc ** 2 - db ** 2) / (2 * da * dc));
return alpha < halfPi && beta < halfPi;
}
function findEdges(verts, [xc, yc]) {
return verts.reduce((acc, [a, b]) => {
const isFound = [isClockwise, isAcute].every(c => c(a, b, [xc, yc]));
// return isFound ? [...acc, `${xa},${ya} ${xb},${yb}`] : acc;
return isFound ? [...acc, `${a[0]},${a[1]} ${b[0]},${b[1]}`] : acc;
}, []);
}
// function findAllEdges(verts, [xc, yc] = [0, 0]) {
function findAllEdges(verts, [xc, yc]) {
return verts.reduce((acc, points) => {
points.forEach(([a, b]) => {
const isFound = [isClockwise, isAcute].every(c => c(a, b, [xc, yc]));
if (isFound) acc.push(`${a[0]},${a[1]} ${b[0]},${b[1]}`);
});
return acc;
}, []);
}
function getCollisionEdges(walls, [xc, yc]) {
return walls.reduce((acc, wall) => {
return [...acc, ...wall.edges.filter(({ xa, ya, xb, yb }) => {
return [isClockwise, isAcute].every(c => c([xa, ya], [xb, yb], [xc, yc]));
}).map(e => { return { edge: e, node: wall }; })];
}, []);
}
// function getCollisionCorners(ws, { x: px, y: py }, { x: vx, y: vy }) {
function getCollisionCorners(ws, { x: x1, y: y1 }, { x: x2, y: y2 }) {
const diffx = x2;
const diffy = y2;
const detv = x2 * y1 - y2 * x1;
const dv = Math.sqrt(diffy ** 2 + diffx ** 2);
const slopev = slope({ xa: x1, ya: y1, xb: x1 + x2, yb: y1 + y2 });
return ws.reduce((acc, w) => {
const filtered = w.corners.filter(c => {
const { x: x0, y: y0 } = c;
const velNormIntxn = perpIntxn(slopev, x1, y1, x0, y0);
// const d = distance(velNormIntxn.x, velNormIntxn.y , x0, y0);
// https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_two_points
const d = Math.abs(diffy * x0 - diffx * y0 + detv) / dv;
// console.log("corner", c, "d", d, "shipRadius", shipRadius);
return d <= shipRadius;
}).map(c => ({ corner: c, node: w.node }));
// });
return [...acc, ...filtered];
}, []);
}
function getForwardCollisionCorners(walls, position, velocity) {
const { x: x1, y: y1 } = position;
const { x: x2, y: y2 } = velocity;
// const a = { x: -100, y: 0 };
// const b = { x: 100, y: 0 };
// console.log("VELOCITY", velocity);
const { x: vx, y: vy } = velocity;
// console.log("velocity", velocity, "position", position);
let perppts;
if (vx === 0 && vy === 0) {
// none
} else if (vx === 0 && vy > 0) {
perppts = { a: { x: x1 - 1, y: y1 }, b: { x: x1 + 1, y: y1 }};
} else if (vx === 0 && vy < 0) {
perppts = { a: { x: x1 + 1, y: y1 }, b: { x: x1 - 1, y: y1 }};
} else if (vy === 0 && vx > 0) {
perppts = { a: { x: x1, y: y1 + 1 }, b: { x: x1, y: y1 - 1 }};
} else if (vy === 0 && vx < 0) {
perppts = { a: { x: x1, y: y1 - 1 }, b: { x: x1, y: y1 + 1 }};
} else if (vy > 0 && vx > 0) {
const vslope = vy / vx;
const pslope = 1 / -vslope;
// Point-slope line equation
const pya = pslope * (x1 - 1) - pslope * x1 + y1;
const pyb = pslope * (x1 + 1) - pslope * x1 + y1;
perppts = { a: { x: x1 - 1, y: pya }, b: { x: x1 + 1, y: pyb }};
} else if (vy > 0 && vx < 0) {
const vslope = vy / vx;
const pslope = 1 / -vslope;
const pya = pslope * (x1 - 1) - pslope * x1 + y1;
const pyb = pslope * (x1 + 1) - pslope * x1 + y1;
perppts = { a: { x: x1 - 1, y: pya }, b: { x: x1 + 1, y: pyb }};
} else if (vy < 0 && vx > 0) {
const vslope = vy / vx;
const pslope = 1 / -vslope;
const pya = pslope * (x1 + 1) - pslope * x1 + y1;
const pyb = pslope * (x1 - 1) - pslope * x1 + y1;
perppts = { a: { x: x1 + 1, y: pya }, b: { x: x1 - 1, y: pyb }};
} else if (vy < 0 && vx < 0) {
const vslope = vy / vx;
const pslope = 1 / -vslope;
const pya = pslope * (x1 + 1) - pslope * x1 + y1;
const pyb = pslope * (x1 - 1) - pslope * x1 + y1;
perppts = { a: { x: x1 + 1, y: pya }, b: { x: x1 - 1, y: pyb }};
} else {
//
}
const { a, b } = perppts;
drawLine(a.x, a.y, b.x, b.y);
return walls.reduce((acc, w) => {
const filtered = w.corners.filter(c => {
// https://stackoverflow.com/a/1560510
const det = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
return det > 0;
});
return [...acc, ...filtered];
}, []);
}
function updateTriangles([positionX, positionY]) {
const delim = ' ';
const className = 'clockwise-orientation';
if (!triangleContainer.childElementCount)
drawTriangles(triangleContainer, allEdgePts, position);
const triangles = triangleContainer.querySelectorAll('polygon');
triangles.forEach(t => {
const attr = t.getAttribute('points').split(delim);
const [a, b,] = attr.map(t => t.split(','));
const cw = isClockwise(a, b, [positionX, positionY]);
const acute = isAcute(a, b, [positionX, positionY]);
const pos = `${positionX},${positionY}`;
if (pos !== attr.pop()) {
attr.push(pos);
t.setAttribute('points', attr.join(delim));
}
t.classList[cw && acute ? "add" : "remove"](className);
t.classList[cw && !acute ? "add" : "remove"]("obtuse");
t.classList[!cw ? "add" : "remove"]("anti-clockwise");
});
}
function wrapPos(positionX, positionY) {
let x, y;
if (positionY > 150) y = positionY - 300;
else if (positionY < -150) y = positionY + 300;
else y = positionY;
if (positionX > 200) x = positionX - 400;
else if (positionX < -200) x = positionX + 400;
else x = positionX;
return [x, y];
}
function fireBullet(x, y, velocity) {
const speed = 200; // meters per second
const degrees = getRotate(gun);
const radians = degrees * Math.PI / 180; // toFixed(15)?
const vx = -Math.sin(radians);
const vy = Math.cos(radians);
const bulletTimeout = 5000; // miliseconds
const cannonLength = 8;
const el = document.createElementNS(namespaceURIsvg, 'circle');
el.classList.add('bullet');
el.setAttribute('r', 1);
el.setAttribute('cx', 0);
el.setAttribute('cy', 0);
const bullet = {
x: x + vx * cannonLength,
y: y + vy * cannonLength,
vx: vx * speed + velocity[0],
vy: vy * speed + velocity[1],
time: bulletTimeout,
node: bulletsContainer.appendChild(el)
}
bullets.push(bullet);
}
function getTranslate(el) {
let x, y;
if (el.style.transform.length === 0) {
x = 0;
y = 0;
} else {
[[, x], [, y] = ["0px", "0"]] = [...el.style.transform.matchAll(regex)];
}
return [+x, +y];
}
function getRotate(el) {
let [[, degrees] = ["0deg", "0"]] = [...el.style.transform.matchAll(degsRegex)];
return +degrees;
}
function updateBullets(elapsed) {
const deleteCount = 1;
[...bullets].forEach((bullet, index) => {
bullet.time -= elapsed;
const x = bullet.x + 0.001 * elapsed * bullet.vx;
const y = bullet.y + 0.001 * elapsed * bullet.vy;
bulletPt.x = x;
bulletPt.y = y;
if (bullet.time > 0 && ![...walls].some(w => w.isPointInFill(bulletPt))) {
[bullet.x, bullet.y] = wrapPos(x, y);
bullet.node.style.transform = `translate(${bullet.x}px, ${bullet.y}px)`;
} else {
bullet.node.remove();
bullets.splice(index, deleteCount);
}
});
}
function perpIntxn(baseSlope, xa, ya, xc, yc) {
let isx, isy;
// base is vertical
if (baseSlope === -Infinity || baseSlope === Infinity) {
isx = xa;
isy = yc;
} else if (baseSlope === 0) { // base is horizontal
isx = xc;
isy = ya;
} else {
const altitudeSlope = 1 / -baseSlope;
isx = (-altitudeSlope * xc + yc + baseSlope * xa - ya) / (baseSlope - altitudeSlope);
isy = altitudeSlope * isx - altitudeSlope * xc + yc;
}
return { x: isx, y: isy };
}
function drawLine(xa, ya, xb, yb) {
const el = document.createElementNS(namespaceURIsvg, 'line');
el.setAttribute('x1', xa);
el.setAttribute('y1', ya);
el.setAttribute('x2', xb);
el.setAttribute('y2', yb);
svg.appendChild(el);
return el;
}
function slope({ xa, ya, xb, yb }) {
return (yb - ya) / (xb - xa);
}
function isLandable(edge) {
// console.log("edge", edge, "slope", slope(edge));
return Object.is(slope(edge), +0);
}
function detectEdgeCollision([xc, yc], { xa, ya, xb, yb }, shipRadius) {
const da = distance(xa, ya, xc, yc);
const db = distance(xb, yb, xc, yc);
// TODO: calculate this one ahead of time
const dc = distance(xa, ya, xb, yb);
// https://en.wikipedia.org/wiki/Altitude_(triangle)#Altitude_in_terms_of_the_sides
// Find altitude of side c (the base)
const s = (1 / 2) * (da + db + dc);
const hc = (2 / dc) * Math.sqrt(s * (s - da) * (s - db) * (s - dc));
return +hc.toFixed(2) <= shipRadius;
}
function detectCornerCollision([xc, yc], [x, y]) {
cornerPt.x = x - xc;
cornerPt.y = y - yc;
return shipBody.isPointInFill(cornerPt);
}
// function detectCollisions(position, walls, edges, z) {
function detectCollisions(position, walls, z) {
// console.log("detectCollision", position, walls, z);
let actualCorner;
const corner = walls.find(wall => {
const c = wall.find(corner => detectCornerCollision(position, corner));
actualCorner = c;
return c;
});
const edge = z.find(({ edge: pts, node: ee }) => {
const str = `${pts.xa},${pts.ya} ${pts.xb},${pts.yb}`;
const r = Object.is(slope(pts), 0) && s.gearDown ? shipRadius + 1 : shipRadius;
return detectEdgeCollision(position, pts, r);
});
// return edge || actualCorner;
return actualCorner;
}
function cornerCollisionPosition({ x: x1, y: y1 }, { x: x2, y: y2 }) {
// todo1: i only want the collisions ahead, not behind
// todo2: how do we tell if we've passed the collision point already?
const diffx = x2;
const diffy = y2;
const detv = x2 * y1 - y2 * x1;
const dv = Math.sqrt(diffy ** 2 + diffx ** 2);
const slopev = slope({ xa: x1, ya: y1, xb: x1 + x2, yb: y1 + y2 });
return ({ corner: { x: x0, y: y0 }, node: n }) => {
const velNormIntxn = perpIntxn(slopev, x1, y1, x0, y0);
const d = Math.abs(diffy * x0 - diffx * y0 + detv) / dv;
const b = Math.sqrt(shipRadius ** 2 - d ** 2);
const cl = document.createElementNS(namespaceURIsvg, 'line');
cl.setAttribute('x1', velNormIntxn.x);
cl.setAttribute('y1', velNormIntxn.y);
cl.setAttribute('x2', x1);
cl.setAttribute('y2', y1);
return cl.getPointAtLength(b);
};
}
function collisionPosition(edge, position, velocity, radius) {
const baseSlope = slope(edge);
// if (Object.is(baseSlope, 0)) s.isLanded = true;
const { xa, ya, xb, yb } = edge;
const baseLine = { x1: xa, y1: ya, x2: xb, y2: yb };
const velocityLine = {
x1: position.x,
y1: position.y,
x2: position.x + velocity.x,
y2: position.y + velocity.y
};
// console.log("COLLISION DETECTED", s.collision);
// console.log("baseSlope", baseSlope);
const baseNrmlIntxn = perpIntxn(baseSlope, xa, ya, position.x, position.y);
const baseVelIntxn = lineIntxnPt(baseLine, velocityLine);
// console.log("baseNrmlIntxn", baseNrmlIntxn, "baseVelIntxn", baseVelIntxn);
const baseSegLength = distance(baseNrmlIntxn.x, baseNrmlIntxn.y , baseVelIntxn.x, baseVelIntxn.y);
const normalSegLength = distance(baseNrmlIntxn.x, baseNrmlIntxn.y, position.x, position.y);
// console.log("baseSegLength", baseSegLength, "normalSegLength", normalSegLength);
const theta = Math.atan(normalSegLength / baseSegLength);
const shipRadius = radius;
const h = shipRadius / Math.sin(theta);
const cl = document.createElementNS(namespaceURIsvg, 'line');
cl.setAttribute('x1', baseVelIntxn.x);
cl.setAttribute('y1', baseVelIntxn.y);
cl.setAttribute('x2', position.x);
cl.setAttribute('y2', position.y);
// console.log("h", h, "theta", theta);
const clPos = cl.getPointAtLength(h);
return clPos;
}
function lineIntxnPt({ x1, y1, x2, y2 }, { x1: x3, y1: y3, x2: x4, y2: y4 }) {
// https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line
const denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
const l1Det = x1 * y2 - y1 * x2;
const l2Det = x3 * y4 - y3 * x4;
const x = (l1Det * (x3 - x4) - (x1 - x2) * l2Det) / denominator;
const y = (l1Det * (y3 - y4) - (y1 - y2) * l2Det) / denominator;
return { x: x, y: y };
}
function updateShip(s, elapsed, edgeszz) {
const degrees = getRotate(gun);
if (rotate > 0) gun.style.transform = `rotate(${(+degrees + rotationSpeed * elapsed) % 360}deg)`;
else if (rotate < 0) gun.style.transform = `rotate(${(+degrees - rotationSpeed * elapsed) % 360}deg)`;
let { x: velocityX, y: velocityY } = s.velocity;
let { x: accelerationX, y: accelerationY } = s.acceleration;
velocityX = velocityX > 0 && velocityX + accelerationX < 0 ? 0 : velocityX + accelerationX;
velocityY = velocityY > 0 && velocityY + accelerationY < 0 ? 0 : velocityY + accelerationY;
s.velocity = { x: velocityX, y: velocityY };
velIndic.setAttribute('x2', s.velocity.x);
velIndic.setAttribute('y2', s.velocity.y);
acclIndic.setAttribute('x2', s.acceleration.x);
acclIndic.setAttribute('y2', s.acceleration.y);
const changeX = 0.001 * elapsed * velocityX;
const changeY = 0.001 * elapsed * velocityY;
let { x, y } = s.position;
// console.log("current position", x, y);
// console.log("elapsed", elapsed, "changeX", changeX, "changeY", changeY);
// let position = [positionX, positionY] = restart ? [0, 0] : wrapPos(changeX + x, changeY + y);
let position = [positionX, positionY] = [changeX + x, changeY + y];
let [xc, yc] = position;
// console.log("future position", xc, yc);
const collE = getCollisionEdges(edgeszz, position);
// console.log("position", { x, y }, "velocity", s.velocity);
const collC = getCollisionCorners(ws, { x, y }, s.velocity);
const fCollC = getForwardCollisionCorners(ws, { x, y }, s.velocity);
// console.log("collision corners", collC);
console.log("forward collision corners", fCollC);
ccps = collC.map(cornerCollisionPosition({ x, y }, s.velocity));
// console.log("corner collision position", ccps);
current = s.collision;
s.collision = detectCollisions(position, allWallCorners, collE);
// legs.style.display = !legs.style.display || legs.style.display === "none" ? "initial" : "none";
legs.style.display = s.gearDown ? "initial" : "none";
if (!current && s.collision) {
const baseSlope = slope(s.collision.edge);
if (Object.is(baseSlope, 0) && s.gearDown) s.isLanded = true;
const clPos = collisionPosition(s.collision.edge, s.position, s.velocity, s.gearDown && Object.is(baseSlope, 0) ? shipRadius + 1 : shipRadius);
s.velocity = { x: 0, y: 0 };
s.position = { x: clPos.x, y: clPos.y }
s.node.style.transform = `translate(${s.position.x}px, ${s.position.y}px)`;
} else if (current && s.collision) {
s.velocity = { x: 0, y: 0 };
} else {
if (s.isLanded) {
s.gearDown = false;
s.isLanded = false;
}
s.position = { x: positionX, y: positionY }
s.node.style.transform = `translate(${positionX}px, ${positionY}px)`;
}
}
function updateEdges(position) {
const collisionEdges = findAllEdges(allEdgePts, position);
[...edgeContainer.children].forEach(l => {
const x1 = l.getAttribute('x1');
const y1 = l.getAttribute('y1');
const x2 = l.getAttribute('x2');
const y2 = l.getAttribute('y2');
const edge = `${x1},${y1} ${x2},${y2}`;
if (collisionEdges.includes(edge))
if ([
edgeContainer.childElementCount <= allStartingEdges.length,
!allStartingEdges.includes(edge)
].some(c => c))
l.remove();
});
}
function updateLines(elapsed, walls, position, velocity) {
const edges = walls.reduce((acc, wall) => {
return [...acc, ...wall.edges];
}, []);
const edgeIds = edges.map(e => `normal${e.xa}-${e.ya}-${e.xb}-${e.yb}`);
const nodes = [...linesContainer.children];
nodes.forEach(n => {
if (!edgeIds.includes(n.id)) n.remove();
});
// console.log("EDGES", edges);
edges.forEach(({ xa, ya, xb, yb }) => {
const id = `normal${xa}-${ya}-${xb}-${yb}`;
const g = linesContainer.querySelector(`#${id}`) || document.createElementNS(namespaceURIsvg, 'g');
const el = g.querySelector('line') || document.createElementNS(namespaceURIsvg, 'line');
const star = g.querySelector('circle') || document.createElementNS(namespaceURIsvg, 'circle');
star.setAttribute('r', 1);
// console.log(position, velocity, xa, ya, xb, yb);
const baseSlope = slope({ xa, ya, xb, yb });
let isx, isy;
// console.log("BASESLOPE", baseSlope);
if (baseSlope === -Infinity || baseSlope === Infinity) {
isx = xa;
isy = position.y;
} else if (baseSlope === 0) { // base is horizontal
isx = position.x;
isy = ya;
} else {
// const clPt = collisionPosition({ xa, ya, xb, yb }, position, velocity);
// console.log(clPt);
// isx = clPt.x;
// isy = clPt.y;
star.setAttribute('cx', 0);
star.setAttribute('cy', 0);
}
return g;
});
}
function init(edgePts) {
started = false;
position = [0, 0]; // meters
velocity = [0, 0]; // meters per second
acceleration = [0, 0]; // meters per second per second
rotate = 0;
[...edgeContainer.children].forEach(c => c.remove());;
// drawAllEdges(edgePts);
allStartingEdges = findAllEdges(edgePts, position);
ship.style.transform = "";
walls.forEach(w => w.setAttribute('fill', 'black'));
// bg.style.fill = 'black';
time.innerText = "0";
}
function firstFrame(timestamp) {
zero = timestamp;
zeroForTimer = timestamp;
previous = timestamp;
animate(timestamp);
}
function animate(timestamp) {
// console.log("current timestamp", timestamp, "previous", previous);
const elapsed = timestamp - previous;
const delta = timestamp - zero;
let degrees = getRotate(gun);
previous = timestamp;
if (delta >= 1000) {
fps.innerText = frameCount;
// debug.innerText = `velocity ${velocity}\n`
// + 'bullets\nx\ty\tvx\tvy\n'
// + bullets.map(b => {
// return `${b.x.toFixed(2)}\t${b.y.toFixed(2)}\t${b.vx.toFixed(2)}\t${b.vy.toFixed(2)}`;
// }).join("\n");
zero = timestamp;
frameCount = 0;
} else {
frameCount++;
}
// console.log("COLLISION EDGES", edgeszz);
updateShip(s, elapsed, edgeszz);
updateBullets(elapsed);
// updateEdges(position);
// if (!s.collision) updateLines(elapsed, edgeszz, s.position, s.velocity);
if (drawCollisionLines) updateTriangles(position);
if (s.collision && !s.isLanded) {
started = false;
isReadingKeys = false;
walls.forEach(w => w.setAttribute('fill', 'red'));
// bg.style.fill = 'red';
}
if (restart) {
started = false;
restart = false;
init(allEdgePts);
time.innerText = 0;
}
// const finished = edgeContainer.childElementCount <= 0;
// if (finished) started = false;
if (started) {
time.innerText = ((timestamp - zeroForTimer) * 0.001).toFixed(3);
// requestAnimationFrame(t => animate(t));
requestAnimationFrame(animate);
}
}
let force = 1;
let spacePressed = false;
let restartPressed = false;
let upPressed = false;
let downPressed = false;
let leftPressed = false;
let rightPressed = false;
let rotateCWPressed = false;
let rotateCCWPressed = false;
document.addEventListener("keydown", function(e) {
if (!isReadingKeys) return;
if (!started) {
started = true;
frameCount = 0;
requestAnimationFrame(firstFrame);
}
switch (e.code) {
case "Space":
if (!spacePressed) {
spacePressed = true;
const [x, y] = getTranslate(ship);
fireBullet(x, y, velocity);
}
break;
case "KeyW":
case "ArrowUp":
if (!upPressed) {
upPressed = true;
s.acceleration.y += -force;
}
break;
case "KeyS":
case "ArrowDown":
if (!downPressed) {
downPressed = true;
s.acceleration.y += force;
}
break;
case "KeyA":
case "ArrowLeft":
if (!leftPressed) {
leftPressed = true;
if (!s.gearDown) s.acceleration.x += -force;
}
break;
case "KeyD":
case "ArrowRight":
if (!rightPressed) {
rightPressed = true;
if (!s.gearDown) s.acceleration.x += force;
}
break;
case "KeyQ":
case "Comma":
if (!rotateCCWPressed) {
rotateCCWPressed = true;
rotate += -1;
}
break;
case "KeyE":
case "Period":
if (!rotateCWPressed) {
rotateCWPressed = true;
rotate += 1;
}
break;
case "KeyP": // Pause
started = !started;
break;
case "KeyG": // Landing gear
s.gearDown = !s.gearDown;
break;
}
});
document.addEventListener("keyup", function(e) {
switch (e.code) {
case "Space":
spacePressed = false;
break;
case "KeyR":
isReadingKeys = true;
!started ? init(allEdgePts) : restart = true;
break;
case "KeyW":
case "ArrowUp":
if (upPressed) {
upPressed = false;
s.acceleration.y -= -force;
}
break;
case "KeyS":
case "ArrowDown":
if (downPressed) {
downPressed = false;
s.acceleration.y -= force;
}
break;
case "KeyA":
case "ArrowLeft":
if (leftPressed) {
leftPressed = false;
if (!s.gearDown) s.acceleration.x -= -force;
}
break;
case "KeyD":
case "ArrowRight":
if (rightPressed) {
rightPressed = false;
if (!s.gearDown) s.acceleration.x -= force;
}
break;
case "KeyQ":
case "Comma":
if (rotateCCWPressed) {
rotateCCWPressed = false;
rotate -= -1;
}
break;
case "KeyE":
case "Period":
if (rotateCWPressed) {
rotateCWPressed = false;
rotate -= 1;
}
break;
}
});
const pointer = svg.querySelector('#pointer');
const xp = pointer.querySelector('.x');
const yp = pointer.querySelector('.y');
const pointerPt = svg.createSVGPoint();
svg.addEventListener("pointermove", function({ clientX, clientY }) {
pointerPt.x = clientX;
pointerPt.y = clientY;
// https://www.sitepoint.com/how-to-translate-from-dom-to-svg-coordinates-and-back-again/
const svgP = pointerPt.matrixTransform(svg.getScreenCTM().inverse());
if (bg.isPointInFill(svgP)) {
// console.log(Math.trunc(svgP.x), Math.trunc(svgP.y));
xp.innerText = Math.trunc(svgP.x);
yp.innerText = Math.trunc(svgP.y);
}
});
//]]></script>
</svg>