964 lines
31 KiB
XML
964 lines
31 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 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 20,10 30,10 30,20 40,40 10,40" /> -->
|
|
<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.25 * mult, y: 1 * mult },
|
|
velocity: { x: 0, y: 1 * mult },
|
|
acceleration: { x: 0, y: 0 },
|
|
rotate: 0,
|
|
collision: null,
|
|
isLanded: 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 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;
|
|
});
|
|
|
|
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 };
|
|
});
|
|
|
|
|
|
// console.log("edgeszz", edgeszz);
|
|
// console.log("wall corners", allWallCorners);
|
|
|
|
const allEdgePts = allWallCorners.map(w =>
|
|
w.map((pt, i, arr) => [pt, arr[(i + 1) % arr.length]])
|
|
);
|
|
|
|
// console.log("all edge points", allEdgePts);
|
|
|
|
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 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) {
|
|
const altitudeSlope = 1 / -baseSlope;
|
|
const isx = (-altitudeSlope * xc + yc + baseSlope * xa - ya) / (baseSlope - altitudeSlope);
|
|
const 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 }) {
|
|
// const [[xa, ya], [xb, yb]] = edge.split(' ').map(n => n.split(',').map(n => +n));
|
|
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 }) {
|
|
const shipRadius = 5;
|
|
|
|
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) {
|
|
// console.log("edges to test for collision", edgesToCheck);
|
|
// console.log("walls", walls);
|
|
// this returns the wall with the corner, no the corner
|
|
let actualCorner;
|
|
const corner = walls.find(wall => {
|
|
// console.log("checking corners on wall", wall);
|
|
const c = wall.find(corner => detectCornerCollision(position, corner));
|
|
// console.log("corner", c);
|
|
actualCorner = c;
|
|
// console.log("c", c);
|
|
return c;
|
|
});
|
|
|
|
// console.log("actual corner", actualCorner);
|
|
// if (corner) console.log("corner collision", corner);
|
|
|
|
const edge = z.find(({ edge: pts, node: ee }) => {
|
|
const str = `${pts.xa},${pts.ya} ${pts.xb},${pts.yb}`;
|
|
return detectEdgeCollision(position, pts);
|
|
}
|
|
);
|
|
|
|
|
|
// if (found) console.log("found", found.node, found.node.node.classList.contains("inverse"));
|
|
|
|
// return [
|
|
// [walls, wall => wall.some(corner => detectCornerCollision(position, corner))],
|
|
// [edges, edge => {
|
|
// const coll = detectEdgeCollision(position, edge);
|
|
// if (coll) console.log(edge, slope(edge) == -0);
|
|
// return coll;
|
|
// }]
|
|
// ].some(([t, f]) => t.some(f))
|
|
|
|
return edge || actualCorner;
|
|
}
|
|
|
|
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 x = ((x1*y2-y1*x2)*(x3-x4)-(x1-x2)*(x3*y4-y3*x4))/((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4));
|
|
const y = ((x1*y2-y1*x2)*(y3-y4)-(y1-y2)*(x3*y4-y3*x4))/((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4));
|
|
|
|
return { x: x, y: y };
|
|
}
|
|
|
|
function updateShip(s, elapsed, collE) {
|
|
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);
|
|
let position = [positionX, positionY] = restart ? [0, 0] : wrapPos(changeX + x, changeY + y);
|
|
let [xc, yc] = position;
|
|
|
|
s.collision = detectCollisions(position, allWallCorners, findAllEdges(allEdgePts, position), collE);
|
|
|
|
console.log("future position", xc, yc);
|
|
|
|
if (s.collision) {
|
|
const baseSlope = slope(s.collision.edge);
|
|
const { xa, ya, xb, yb } = s.collision.edge;
|
|
const baseLine = { x1: xa, y1: ya, x2: xb, y2: yb };
|
|
|
|
const velocityLine = {
|
|
x1: s.position.x,
|
|
y1: s.position.y,
|
|
x2: s.position.x + s.velocity.x,
|
|
y2: s.position.y + s.velocity.y
|
|
};
|
|
|
|
const baseNrmlIntxn = perpIntxn(baseSlope, xa, ya, s.position.x, s.position.y);
|
|
const baseVelIntxn = lineIntxnPt(baseLine, velocityLine);
|
|
const baseSegLength = distance(baseNrmlIntxn.x, baseNrmlIntxn.y , baseVelIntxn.x, baseVelIntxn.y);
|
|
const normalSegLength = distance(baseNrmlIntxn.x, baseNrmlIntxn.y, s.position.x, s.position.y);
|
|
const theta = Math.atan(normalSegLength / baseSegLength);
|
|
const shipRadius = 5;
|
|
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', s.position.x);
|
|
cl.setAttribute('y2', s.position.y);
|
|
const clPos = cl.getPointAtLength(h);
|
|
|
|
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 {
|
|
// console.log("c");
|
|
|
|
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, edges, currentPos, futurePos) {
|
|
// console.log("velocity", s.velocity);
|
|
// console.log("collision", s.collision);
|
|
// console.log("edges", edges);
|
|
|
|
// const edgeIds = [...linesContainer.children].map(c => c.id);
|
|
const edgeIds = edges.map(({edge: { xa, ya, xb, yb }}) => `normal${xa}-${ya}-${xb}-${yb}`);
|
|
const nodes = [...linesContainer.children];
|
|
|
|
// console.log("EDGE IDS", edgeIds, "NODES", nodes);
|
|
|
|
nodes.forEach(n => {
|
|
if (!edgeIds.includes(n.id)) n.remove();
|
|
});
|
|
|
|
console.log("update lines", currentPos, futurePos);
|
|
|
|
edges.forEach(({ edge: { 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);
|
|
const baseSlope = slope({ xa, ya, xb, yb });
|
|
const intx = perpIntxn(baseSlope, xa, ya, s.position.x, s.position.y);
|
|
const baseNrmlIntxn = perpIntxn(baseSlope, xa, ya, s.position.x, s.position.y);
|
|
|
|
// make sure this is using the calculated future velocity, not the current
|
|
const baseLine = { x1: xa, y1: ya, x2: xb, y2: yb };
|
|
|
|
const velocityLine = {
|
|
x1: s.position.x,
|
|
y1: s.position.y,
|
|
x2: s.position.x + s.velocity.x,
|
|
y2: s.position.y + s.velocity.y
|
|
};
|
|
|
|
const baseVelIntxn = lineIntxnPt(baseLine, velocityLine);
|
|
const baseSegLength = distance(baseNrmlIntxn.x, baseNrmlIntxn.y , baseVelIntxn.x, baseVelIntxn.y);
|
|
const normalSegLength = distance(baseNrmlIntxn.x, baseNrmlIntxn.y, s.position.x, s.position.y);
|
|
console.log("distance", baseSegLength);
|
|
const theta = Math.atan(normalSegLength / baseSegLength);
|
|
console.log("theta", theta);
|
|
// const contactPos = { x: Math.acos() }
|
|
const h = 5 / Math.sin(theta);
|
|
console.log("h", 5 / Math.sin(theta));
|
|
console.log("cos(theta)", Math.cos(theta));
|
|
el.setAttribute('x2', intx.x);
|
|
el.setAttribute('y2', intx.y);
|
|
el.setAttribute('x1', currentPos.x);
|
|
el.setAttribute('y1', currentPos.y);
|
|
|
|
console.log("normal line length at current position", el.getTotalLength());
|
|
el.setAttribute('x1', futurePos.x);
|
|
el.setAttribute('y1', futurePos.y);
|
|
|
|
console.log("normal line length at future position", el.getTotalLength());
|
|
|
|
el.setAttribute('x1', s.position.x);
|
|
el.setAttribute('y1', s.position.y);
|
|
|
|
star.setAttribute('cx', s.position.x);
|
|
star.setAttribute('cy', baseVelIntxn.y - h);
|
|
|
|
el.setAttribute('id', `normal${xa}-${ya}-${xb}-${yb}`);
|
|
g.appendChild(el);
|
|
g.appendChild(star);
|
|
linesContainer.appendChild(g);
|
|
|
|
const cl = document.createElementNS(namespaceURIsvg, 'line');
|
|
cl.setAttribute('x1', baseVelIntxn.x);
|
|
cl.setAttribute('y1', baseVelIntxn.y);
|
|
cl.setAttribute('x2', s.position.x);
|
|
cl.setAttribute('y2', s.position.y);
|
|
const clPt = cl.getPointAtLength(h);
|
|
console.log(clPt);
|
|
|
|
star.setAttribute('cx', clPt.x);
|
|
star.setAttribute('cy', clPt.y);
|
|
|
|
|
|
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++;
|
|
}
|
|
|
|
|
|
// position = updateShip(s, elapsed);
|
|
const collE = getCollisionEdges(edgeszz, position);
|
|
|
|
const { x, y } = s.position;
|
|
updateShip(s, elapsed, collE);
|
|
// console.log("S POSITION", s.position);
|
|
updateBullets(elapsed);
|
|
// updateEdges(position);
|
|
if (!s.collision) updateLines(elapsed, collE, {x, y}, s.position);
|
|
if (drawCollisionLines) updateTriangles(position);
|
|
|
|
// const collision = detectCollisions(position, allWallCorners, findAllEdges(allEdgePts, position), getCollisionEdges(edgeszz, position));
|
|
|
|
// console.log("collision", collision && collision.hasOwnProperty("edge"));
|
|
// console.log("landable?", isLandable(collision));
|
|
|
|
// console.log("collision", s.collision);
|
|
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;
|
|
s.acceleration.x += -force;
|
|
}
|
|
break;
|
|
case "KeyD":
|
|
case "ArrowRight":
|
|
if (!rightPressed) {
|
|
rightPressed = true;
|
|
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;
|
|
}
|
|
});
|
|
|
|
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;
|
|
s.acceleration.x -= -force;
|
|
}
|
|
break;
|
|
case "KeyD":
|
|
case "ArrowRight":
|
|
if (rightPressed) {
|
|
rightPressed = false;
|
|
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>
|
|
|