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

1686 lines
57 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 100 100" 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;
}
line.bullet {
stroke: black;
opacity: 0.5;
transition: opacity 2s ease-out;
}
line.bullet.fade {
opacity: 0;
}
#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-width: 0.5px;
}
line#velocity-indicator {
stroke: none;
/* stroke-width: 1px; */
}
line#acceleration-indicator {
stroke: none;
/* 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="8" y2="0" 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 -1.5 0 h 3" stroke="black" fill="none" />
<path d="M -3 2 l -2 2 v 2 m -1.5 0 h 3" 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="-10,10 10,10 10,40 -10,40" /> -->
<!-- <polygon class="wall" points="-10,10 10,30 -10,40 -20,20" /> -->
<!-- <polygon class="wall" points="-20,-10 0,10 -20,20 -30,0" /> -->
<polygon class="wall" points="-20,-10 20,10 -20,20 -30,0" />
<!-- <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="-100,-50 -10,-50 -10,-60 -100,-60" /> -->
<!-- <polygon class="wall" points="-100,-60 -10,-60 -10,-50 -100,-50" /> -->
<!-- <polygon class="wall" points="10,50 100,50 100,60 10,60" /> -->
<!-- <polygon class="wall" points="34,56 56,78 45,98 23,89" /> -->
<!-- <polygon class="wall" points="44,55 55,66 33,88 22,66" /> -->
<!-- <polygon class="wall" points="-55,-44 -33,-33 -55,-22 -66,-33" /> -->
<!-- <polygon class="wall" points="77,-22 133,-6 99,0 88,-5" /> -->
<!-- <polygon class="wall" points="-77,99 -66,88 -44,122 -88,133" /> -->
<!-- <polygon class="wall" points="-99,44 -77,44 -88,55" /> -->
<!-- <polygon class="wall" points="-50,50 -40,60 -50,70 -60,60" /> -->
<!-- <polygon class="wall" points="50,-30 40,-60 50,-70 60,-60" /> -->
<!-- <polygon class="wall" points="50,50 60,60 50,70 40,60" /> -->
<!-- <polygon class="wall" points="-50,-50 -60,-60 -50,-70 -40,-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
<br/>
<span id="position" xmlns="http://www.w3.org/1999/xhtml">-,-</span> x,y position
</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">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">stop reading data from elements</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">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>
<li xmlns="http://www.w3.org/1999/xhtml">draw a parabolic line for bullet trajectory in gravity?</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 zeroP = { x: 0, y: 0 };
// entities
const Ships = [{ entity_id: "ship1" }];
// const Walls = [{ entity_id: "wall_1" }, { entity_id: "wall_2" }];
// components
const Velocity = {};
const Position = {};
const Acceleration = {};
// Ships.forEach(({ entity_id }) => {
// Acceleration[entity_id] = zeroP;
// Velocity[entity_id] = zeroP;
// Position[entity_id] = zeroP;
// });
// Points = {
// "wall_1": "0,0 2,0 1,1",
// "wall_2": "0,0 -1,1 -2,0",
// };
// systems
const Move = (() => {
const metersPerMillisecond = 0.001;
// 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 getForwardEdges(edges, { x, y }) {
return edges.filter(({ edge: { xa, ya, xb, yb } }) =>
isClockwise([xa, ya], [xb, yb], [x, y]) && isAcute([xa, ya], [xb, yb], [x, y]));
}
function getForwardCorners(corners, position, velocity) {
const { x: x1, y: y1 } = position;
const { x: x2, y: y2 } = velocity;
const { x: vx, y: vy } = velocity;
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;
// if (a && b) drawLine(a.x, a.y, b.x, b.y);
return corners.filter(({ corner: c }) => {
if (!a || !b) return;
const det = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
return det > 0;
});
}
function getEdgeCollisionBoundary(edge, dist) {
const { xa, ya, xb, yb } = edge;
const length = distance(xa, ya, xb, yb);
const rise = yb - ya;
const run = xb - xa;
const riol = rise / length * dist;
const ruol = run / length * dist;
return { xa: xa + riol, ya: ya - ruol, xb: xb + riol, yb: yb - ruol};
}
function detectEdgeCollision([xc, yc], [x, y], radius, gearDown) {
// console.log("*detectEdgeCollision", xc, yc, x, y, radius, gearDown);
return (collision) => {
if (xc === x && yc === y) return;
const { edge, wall } = collision;
const dist = edge.xa < edge.xb && edge.ya === edge.yb && gearDown ? radius + 1.5 : radius;
const edgeSeg = getEdgeCollisionBoundary(edge, dist);
const positionSeg = { xa: x, ya: y, xb: xc, yb: yc };
const { xa: x1, ya: y1, xb: x2, yb: y2 } = positionSeg;
const { xa: x3, ya: y3, xb: x4, yb: y4 } = edgeSeg;
// https://en.wikipedia.org/wiki/Intersection_(geometry)#Two_line_segments
// https://en.wikipedia.org/wiki/Cramer%27s_rule#Explicit_formulas_for_small_systems
const denom = (x2-x1)*(y4-y3)-(x4-x3)*(y2-y1);
if (denom) {
const s = ((x3-x1)*(y4-y3)-(x4-x3)*(y3-y1))/denom;
const t = -((x2-x1)*(y3-y1)-(x3-x1)*(y2-y1))/denom;
// const roundedT = +t.toFixed(2);
// const roundedS = +s.toFixed(2);
const roundedT = t;
const roundedS = s;
// console.log("checking edge for collision", edge);
// console.log("position edge segs", positionSeg, edgeSeg);
// console.log("s", s, "t", t);
// if (roundedS >= 0 && roundedS <= 1 && roundedT >= 0 && roundedT <= 1) {
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
const xs = (x1 + s * (x2 - x1));
const ys = (y1 + s * (y2 - y1));
// console.log("xs, yx", xs, ys);
// console.log("xc, yc", xc, yc);
collision.position = { x: xs, y: ys };
// collision.position = { x: xc, y: yc };
// console.log("position calculate by detectEdgeCollision", xs, ys);
return true;
}
}
// return roundedS >= 0 && roundedS <= 1 && roundedT >= 0 && roundedT <= 1;
return;
};
}
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 slope({ xa, ya, xb, yb }) {
return (yb - ya) / (xb - xa);
}
function distance(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
function detectCornerCollision([xc, yc], [x, y], radius) {
return c => {
if (xc === x && yc === y) return;
const d = distance(c.corner.x, c.corner.y, xc, yc);
if (d <= radius) return true;
const positionSeg = { xa: xc, ya: yc, xb: x, yb: y };
const posNormIntxn = perpIntxn(slope(positionSeg), x, y, c.corner.x, c.corner.y);
const cornerSeg = { xa: c.corner.x, ya: c.corner.y, xb: posNormIntxn.x, yb: posNormIntxn.y };
const { x: x0, y: y0 } = c.corner;
const { xa: x1, ya: y1, xb: x2, yb: y2 } = positionSeg;
const { xa: x3, ya: y3, xb: x4, yb: y4 } = cornerSeg;
// https://en.wikipedia.org/wiki/Intersection_(geometry)#Two_line_segments
// https://en.wikipedia.org/wiki/Cramer%27s_rule#Explicit_formulas_for_small_systems
const s = ((x3-x1)*(y4-y3)-(x4-x3)*(y3-y1))/((x2-x1)*(y4-y3)-(x4-x3)*(y2-y1));
const t = -((x2-x1)*(y3-y1)-(x3-x1)*(y2-y1))/((x2-x1)*(y4-y3)-(x4-x3)*(y2-y1));
const roundedT = +t.toFixed(2);
if (s >= 0 && roundedT <= 1) {
const xs = (x1 + s * (x2 - x1));
const ys = (y1 + s * (y2 - y1));
const xt = (x3 + roundedT * (x4 - x3));
const yt = (y3 + roundedT * (y4 - y3));
// [xs, ys] and [xt, yt] should be equal ([xs, ys] === [xy, yt])
// (...or about equal, notwithstanding rounding errors)
const sCollisionPt = [xs, ys];
const tCollisionPt = [xt, yt];
// drawCircle(posNormIntxn.x, posNormIntxn.y, "red");
// drawCircle(...tCollisionPt, "blue");
}
return s >= 0 && roundedT <= 1;
};
}
function withinCollisionDistance({ x: x1, y: y1 }, { x: x2, y: y2 }, distance) {
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 }}) => {
const velNormIntxn = perpIntxn(slopev, x1, y1, x0, y0);
const dx = Math.max(x0, velNormIntxn.x) - Math.min(x0, velNormIntxn.x);
const dy = Math.max(y0, velNormIntxn.y) - Math.min(y0, velNormIntxn.y);
const d = Math.sqrt(dy ** 2 + dx ** 2);
return d <= distance;
};
}
function detectContacts(currentPos, intendedPos, velocity, radius, { edges, corners }, gearDown) {
const { x: xc, y: yc } = intendedPos;
const [x, y] = currentPos;
// edges oriented clockwise with entity
const fwdEdges = getForwardEdges(edges, { x, y });
// console.log("forward edges", fwdEdges);
const edgeColl = fwdEdges.filter(detectEdgeCollision([xc, yc], [x, y], radius, gearDown));
// console.log("edgeColl", edgeColl);
// corners ahead of ship
const fwdCorners = getForwardCorners(corners, { x, y }, velocity);
const cornersInPath = fwdCorners.filter(withinCollisionDistance({ x, y }, velocity, radius));
const cornerColl = cornersInPath.filter(detectCornerCollision([xc, yc], [x, y], radius));
return [...edgeColl, ...cornerColl];
}
return {
update: ({ entity_id }, elapsed) => {
const { x: px, y: py } = Position[entity_id];
const { x: vx, y: vy } = Velocity[entity_id];
const { x: ax, y: ay } = Acceleration[entity_id];
const v = {
x: vx > 0 && vx + ax <= 0 ? 0 : vx + ax,
y: vy > 0 && vy + ay <= 0 ? 0 : vy + ay
};
const p = {
x: px + elapsed * v.x * metersPerMillisecond,
y: py + elapsed * v.y * metersPerMillisecond
};
const contacts = detectContacts([px, py], p, v, s.radius, map, false);
console.log("CONTACTS", contacts);
if (contacts.length !== 0) {
console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
let posP;
const contact = contacts[0];
if (contact.corner) {
Velocity[entity_id] = { x: 0, y: 0 };
Position[entity_id] = cornerContactPosition(p.x, p.y, px, py, contact.corner, s.radius);
} else if (contact.edge) {
// if (isLandable(contact.edge) && s.gearDown) s.isLanded = true;
const isHorizontal = contact.edge.ya === contact.edge.yb;
const isVertical = contact.edge.xa === contact.edge.xb;
console.log("CURRENT POSITION", px, py);
console.log("INTENDED POSITION", p.x, p.y);
console.log("CONTACT POSITION", contact.position);
if (isHorizontal) {
if (contact.edge.xa < contact.edge.xb) {
if (v.y <= 0) {
Velocity[entity_id] = { x: v.x, y: v.y };
Position[entity_id] = { x: p.x, y: p.y };
} else if (v.y > 0) {
Velocity[entity_id] = { x: v.x, y: 0 };
Position[entity_id] = { x: p.x, y: contact.position.y };
}
} else if (contact.edge.xa > contact.edge.xb) {
if (v.y <= 0) {
Velocity[entity_id] = { x: v.x, y: 0 };
Position[entity_id] = { x: p.x, y: contact.position.y };
} else if (v.y > 0) {
Velocity[entity_id] = { x: v.x, y: v.y };
Position[entity_id] = { x: p.x, y: p.y };
}
}
} else if (isVertical) {
if (contact.edge.ya > contact.edge.yb) {
if (v.x <= 0) {
Velocity[entity_id] = { x: v.x, y: v.y };
Position[entity_id] = { x: p.x, y: p.y };
} else if (v.x > 0) {
Velocity[entity_id] = { x: 0, y: v.y };
Position[entity_id] = { x: contact.position.x, y: p.y };
}
} else if (contact.edge.ya < contact.edge.yb) {
if (v.x <= 0) {
Velocity[entity_id] = { x: 0, y: v.y };
Position[entity_id] = { x: contact.position.x, y: p.y };
} else if (v.x > 0) {
Velocity[entity_id] = { x: v.x, y: v.y };
Position[entity_id] = { x: p.x, y: p.y };
}
}
} else {
if (contact.edge.xa < contact.edge.xb && contact.edge.ya < contact.edge.yb) {
// const m1 = slope({ xa: vx, ya: vy, xb: v.x, yb: v.y });
// const m2 = slope(contact.edge);
// const theta = Math.atan(Math.abs((m1-m2)/(1+m1*m2)));
const thetaY = Math.atan((contact.edge.yb - contact.edge.ya)/(contact.edge.xb-contact.edge.xa));
const thetaX = Math.atan((contact.edge.xb-contact.edge.xa)/(contact.edge.yb - contact.edge.ya));
// const thetaX = Math.atan((contact.edge.xb-contact.edge.xa)/(contact.edge.yb - contact.edge.ya));
const edgeSlope = slope(contact.edge);
const rise = contact.edge.yb-contact.edge.ya;
const run = contact.edge.xb-contact.edge.xa;
console.log("rise", rise, "run", run);
const edgeAngleTheta = Math.atan(rise/run);
console.log("EDGEANGLETETASADFSDDFSF", edgeAngleTheta, "degrees", edgeAngleTheta * 180 / Math.PI);
const edgeVector = { x: run, y: rise };
const normalVector = { x: rise, y: -run };
console.log("edgeVector", edgeVector, "normalVector", normalVector);
const edgeVectorMagnitude = Math.sqrt(edgeVector.x**2+edgeVector.y**2);
const velocityVectorMagnitude = Math.sqrt(v.y**2+v.x**2);
const dotProduct = edgeVector.y*v.y + edgeVector.x*v.x;
const normalVectorDotProduct = normalVector.y*v.y + normalVector.x*v.x;
// const velocityVectorCrossNormalVector = v.x * normalVector.y - v.y * normalVector.x;
const velocityVectorCrossNormalVector = v.y * normalVector.x - v.x * normalVector.y;
// console.log("dotProduct", dotProduct, "normalVectorDotProduct", normalVectorDotProduct);
const edgeTheta = Math.acos(dotProduct / (edgeVectorMagnitude * velocityVectorMagnitude));
const normalTheta = Math.acos(normalVectorDotProduct / (edgeVectorMagnitude * velocityVectorMagnitude));
console.log("VELOCITY", v);
drawLine(p.x - v.x, p.y - v.y, p.x, p.y, "blue");
drawLine(contact.edge.xa, contact.edge.ya, contact.edge.xb, contact.edge.yb, "green");
// drawLine(p.x, p.x, p.x + v.x, p.y + v.y, "blue");
// console.log("theta between edge vector and velocity vector", edgeTheta, "degrees", edgeTheta * 180 / Math.PI);
// console.log("theta between normal vector and velocity vector", normalTheta, "degrees", normalTheta * 180 / Math.PI);
// console.log("cross product normal and velocity", velocityVectorCrossNormalVector);
// const newTheta = (Math.PI - normalTheta) * 2;
const newTheta = edgeTheta;
// const newY = -Math.cos(newTheta) * velocityVectorMagnitude;
// const newX = Math.sin(newTheta) * velocityVectorMagnitude;
// console.log("theta", newTheta, "degrees", newTheta * 180 / Math.PI);
console.log("theta between velocity vector and edge vector", newTheta, "degrees", newTheta * 180 / Math.PI);
let newY;
let newX;
// if (velocityVectorCrossNormalVector < 0) {
// if (velocityVectorCrossNormalVector > 0) {
//
// newX = Math.cos(newTheta) * velocityVectorMagnitude;
// newY = Math.sin(newTheta) * velocityVectorMagnitude;
// } else {
// newX = Math.sin(newTheta) * velocityVectorMagnitude;
// newY = Math.cos(newTheta) * velocityVectorMagnitude;
// }
// newX = Math.cos(newTheta) * v.x;
// newY = -Math.sin(newTheta) * v.y;
// newX = Math.cos(edgeAngleTheta) * velocityVectorMagnitude;
// newY = -Math.sin(edgeAngleTheta) * velocityVectorMagnitude;
function vector(x, y) {
const vector = { x: x, y: y };
vector.magnitude = Math.sqrt(x**2+y**2);
vector.dx = vector.x / vector.magnitude;
vector.dy = vector.y / vector.magnitude;
vector.rightNormal = { x: vector.y, y: -vector.x };
vector.rightNormal.dx = vector.rightNormal.x / vector.magnitude;
vector.rightNormal.dy = vector.rightNormal.y / vector.magnitude;
return vector;
}
const vec1 = vector(v.x, v.y);
const vec2 = vector(run, rise);
// From https://stackoverflow.com/a/14886099
// 1. Find the dot product of vec1 and vec2
// Note: dx and dy are vx and vy divided over the length of the vector (magnitude)
// var dpA:Number = vec1.vx * vec2.dx + vec1.vy * vec2.dy;
const dpA = vec1.x * vec2.dx + vec1.y * vec2.dy;
// 2. Project vec1 over vec2
// var prA_vx:Number = dpA * vec2.dx;
const prAvx = dpA * vec2.dx;
// var prA_vy:Number = dpA * vec2.dy;
const prAvy = dpA * vec2.dy;
// 3. Find the dot product of vec1 and vec2's normal
// (left or right normal depending on line's direction, let's say left)
// vec.leftNormal --> vx = vec.vy; vy = -vec.vx;
// vec.rightNormal --> vx = -vec.vy; vy = vec.vx;
// var dpB:Number = vec1.vx * vec2.leftNormal.dx + vec1.vy * vec2.leftNormal.dy;
const dpB = vec1.x * vec2.rightNormal.dx + vec1.y * vec2.rightNormal.dy;
// 4. Project vec1 over vec2's left normal
// var prB_vx:Number = dpB * vec2.leftNormal.dx;
const prBvx = dpB * vec2.rightNormal.dx;
// var prB_vy:Number = dpB * vec2.leftNormal.dy;
const prBvy = dpB * vec2.rightNormal.dy;
// 5. Add the first projection prA to the reverse of the second -prB
// var new_vx:Number = prA_vx - prB_vx;
// var new_vy:Number = prA_vy - prB_vy;
newX = prAvx - prBvx;
newY = prAvy - prBvy;
// newX = v.x;
// newY = -v.y;
// what vector x,y for a vector that is newTheta away from edge vector?
console.log("newX", newX, "newY", newY);
drawLine(p.x, p.y, p.x + normalVector.x , p.y + normalVector.y, "black");
drawLine(p.x, p.y, p.x + newX , p.y + newY, "blue");
Velocity[entity_id] = {
// x: v.x * Math.sin(thetaX) + v.y * Math.cos(thetaY),
// y: v.y * Math.sin(thetaY) + v.x * Math.cos(thetaX)
x: newX,
// y: v.y * Math.sin(thetaY) + v.x * Math.cos(thetaX)
y: newY
};
console.log("VELOCITY AFTER", Velocity[entity_id]);
// Position[entity_id] = { x: p.x, y: p.y };
// console.log("contact", contact.edge);
// Position[entity_id] = contact.position;
} else {
}
}
}
} else {
Velocity[entity_id] = { x: v.x, y: v.y };
Position[entity_id] = { x: p.x, y: p.y };
}
}
};
})();
const namespaceURIsvg = 'http://www.w3.org/2000/svg';
const bullets = [];
const halfPi = Math.PI / 2;
const maxSpeed = 100;
const drawCollisionLines = false;
let previous, zero, frameCount = 0;
let rotate = 0;
const s = {};
s.node = document.querySelector(".ship");
s.radius = +s.node.querySelector("#body").getAttribute('r');
const gun = s.node.querySelector('#cannon');
const legs = s.node.querySelector("#legs");
const svg = document.querySelector('svg');
const bg = svg.querySelector('#bg');
const fps = document.querySelector("#fps");
const time = document.querySelector("#time");
const positionEl = document.querySelector("#position");
const debug = document.querySelector("#debug");
const wallElements = 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 map = (function(els) {
let corners, edges;
return {
walls: [...els].map(node => {
const 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 edges = corners.map(({ x: xa, y: ya }, i, arr) => {
const { x: xb, y: yb } = arr[(i + 1) % arr.length];
return { xa: xa, ya: ya, xb: xb, yb: yb };
});
return { node, corners, edges };
}),
get corners() {
if (corners) return corners;
return this.walls.reduce((acc, wall) =>
[...acc, ...wall.corners.map(c => ({ corner: c, wall: wall }))], []);
},
get edges() {
if (edges) return edges;
return this.walls.reduce((acc, wall) =>
[...acc, ...wall.edges.map(e => ({ edge: e, wall: wall }))], []);
}
};
})(wallElements);
let allStartingEdges;
let rotationSpeed = 0.25;
let started = false;
let restart = false;
let isReadingKeys = true;
function init() {
started = false;
const mult = 10;
s.position = { x: 0, y: -10 };
// s.velocity = { x: 0, y: -10 };
s.velocity = { x: 10, y: 20 };
s.angularVelocity = 0;
// s. velocity = { x: -5*mult, y: 7*mult };
s.acceleration = { x: 0, y: 0 };
Ships.forEach(({ entity_id }) => {
// Acceleration[entity_id] = s.acceleration;
// Velocity[entity_id] = s.velocity;
// Position[entity_id] = s.position;
Acceleration[entity_id] = { x: s.acceleration.x, y: s.acceleration.y };
Velocity[entity_id] = { x: s.velocity.x, y: s.velocity.y };
Position[entity_id] = { x: s.position.x, y: s.position.y };
});
s.angularAcceleration = 0;
s.rotate = 0;
s.degrees = 0;
s.collision = null;
s.isLanded = false;
s.gearDown = false;
[...edgeContainer.children].forEach(c => c.remove());;
s.node.style.transform = `translate(${s.position.x}px, ${s.position.y}px)`;
wallElements.forEach(w => w.setAttribute('fill', 'black'));
velIndic.setAttribute('x2', 0);
velIndic.setAttribute('y2', 0);
acclIndic.setAttribute('x2', 0);
acclIndic.setAttribute('y2', 0);
time.innerText = "0";
}
// const b = map.edges.map(({ edge }) => getEdgeCollisionBoundary(edge, shipRadius));
// b.forEach(b => drawLine(b.xa, b.ya, b.xb, b.yb, "orange"));
function distance(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
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 getForwardEdges(edges, { x, y }) {
return edges.filter(({ edge: { xa, ya, xb, yb } }) =>
isClockwise([xa, ya], [xb, yb], [x, y]) && isAcute([xa, ya], [xb, yb], [x, y]));
}
function getForwardCorners(corners, position, velocity) {
const { x: x1, y: y1 } = position;
const { x: x2, y: y2 } = velocity;
const { x: vx, y: vy } = velocity;
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;
// if (a && b) drawLine(a.x, a.y, b.x, b.y);
return corners.filter(({ corner: c }) => {
if (!a || !b) return;
const det = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
return det > 0;
});
}
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(position, degrees) {
const cannonLength = 8;
const radians = degrees * Math.PI / 180; // toFixed(15)?
const rise = Math.sin(radians) * cannonLength;
const run = Math.cos(radians) * cannonLength;
const bulletOrigin = {
x: position.x + run,
y: position.y + rise
}
const bulletDestination = {
x: bulletOrigin.x + run * 50,
y: bulletOrigin.y + rise * 50
}
// Options for the observer (which mutations to observe)
const config = { attributes: false, childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = (mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === "childList") {
// console.log("A child node has been added or removed.");
// console.log('added nodes', mutation.addedNodes);
// console.log('children', bulletsContainer.children.length);
// [...mutation.addedNodes].forEach(b => b.classList.add('fade'));
// mutation.addedNodes.forEach(bullet => bullet.classList.add('fade'));
} else if (mutation.type === "attributes") {
// console.log(`The ${mutation.attributeName} attribute was modified.`);
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
// observer.observe(bulletsContainer, config);
const lineEl = document.createElementNS(namespaceURIsvg, 'line');
lineEl.classList.add('bullet');
lineEl.setAttribute('x1', bulletOrigin.x);
lineEl.setAttribute('y1', bulletOrigin.y);
lineEl.setAttribute('x2', bulletDestination.x);
lineEl.setAttribute('y2', bulletDestination.y);
// lineEl.addEventListener('transitionrun', e => console.log('transitionrun', e));
// lineEl.addEventListener('transitionstart', e => console.log('transitionstart', e));
lineEl.addEventListener('transitionend', e => e.target.remove());
const startTime = performance.now()
let pt, hit;
for (let i = 0; i <= lineEl.getTotalLength(); i++) {
pt = lineEl.getPointAtLength(i);
hit = [...wallElements].find(el => el.isPointInFill(pt));
if (hit) break;
}
const endTime = performance.now()
console.log(`Took ${endTime - startTime} milliseconds`)
// const screenCTM = svg.getScreenCTM();
// const startTime = performance.now()
//
// let pt, hit;
// for (let i = 0; i <= lineEl.getTotalLength(); i++) {
// pt = lineEl.getPointAtLength(i);
// const domPt = pt.matrixTransform(screenCTM);
// const elements = document.elementsFromPoint(domPt.x, domPt.y);
// hit = elements.find(el => el.classList.contains('wall'));
//
// if (hit) break;
// }
//
// const endTime = performance.now()
// console.log(`Took ${endTime - startTime} milliseconds`)
if (hit) {
lineEl.setAttribute('x2', pt.x);
lineEl.setAttribute('y2', pt.y);
}
const appended = bulletsContainer.appendChild(lineEl);
// I don't know why I have to delay it
setTimeout(() => appended.classList.add('fade'), 1000);
}
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, color = "black") {
const el = document.createElementNS(namespaceURIsvg, 'line');
el.setAttribute('x1', xa);
el.setAttribute('y1', ya);
el.setAttribute('x2', xb);
el.setAttribute('y2', yb);
el.setAttribute('stroke', color);
const arrow = document.createElementNS(namespaceURIsvg, 'circle');
arrow.setAttribute('cx', xb);
arrow.setAttribute('cy', yb);
arrow.setAttribute('r', 1);
arrow.setAttribute('fill', 'teal');
svg.appendChild(arrow);
svg.appendChild(el);
return el;
}
function drawCircle(cx, cy, color = "black", r = 1) {
const el = document.createElementNS(namespaceURIsvg, 'circle');
el.setAttribute('cx', cx);
el.setAttribute('cy', cy);
el.setAttribute('r', r);
el.setAttribute('fill', color);
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 edge.xa < edge.xb && edge.ya === edge.yb;
// return Object.is(slope(edge), +0);
}
function getEdgeCollisionBoundary(edge, dist) {
const { xa, ya, xb, yb } = edge;
const length = distance(xa, ya, xb, yb);
const rise = yb - ya;
const run = xb - xa;
const riol = rise / length * dist;
const ruol = run / length * dist;
return { xa: xa + riol, ya: ya - ruol, xb: xb + riol, yb: yb - ruol};
}
function detectEdgeCollision([xc, yc], [x, y], radius, gearDown) {
// console.log("detectEdgeCollision", xc, yc, x, y, radius, gearDown);
return (collision) => {
if (xc === x && yc === y) return;
const { edge, wall } = collision;
const dist = edge.xa < edge.xb && edge.ya === edge.yb && gearDown ? radius + 1.5 : radius;
const edgeSeg = getEdgeCollisionBoundary(edge, dist);
const positionSeg = { xa: x, ya: y, xb: xc, yb: yc };
const { xa: x1, ya: y1, xb: x2, yb: y2 } = positionSeg;
const { xa: x3, ya: y3, xb: x4, yb: y4 } = edgeSeg;
// https://en.wikipedia.org/wiki/Intersection_(geometry)#Two_line_segments
// https://en.wikipedia.org/wiki/Cramer%27s_rule#Explicit_formulas_for_small_systems
const s = ((x3-x1)*(y4-y3)-(x4-x3)*(y3-y1))/((x2-x1)*(y4-y3)-(x4-x3)*(y2-y1));
const t = -((x2-x1)*(y3-y1)-(x3-x1)*(y2-y1))/((x2-x1)*(y4-y3)-(x4-x3)*(y2-y1));
const roundedT = +t.toFixed(2);
const roundedS = +s.toFixed(2);
if (roundedS >= 0 && roundedS <= 1 && roundedT >= 0 && roundedT <= 1) {
const xs = (x1 + s * (x2 - x1));
const ys = (y1 + s * (y2 - y1));
collision.position = { x: xs, y: ys };
return true;
}
// return roundedS >= 0 && roundedS <= 1 && roundedT >= 0 && roundedT <= 1;
return;
};
}
function detectCornerCollision([xc, yc], [x, y], radius) {
return c => {
if (xc === x && yc === y) return;
const d = distance(c.corner.x, c.corner.y, xc, yc);
if (d <= radius) return true;
const positionSeg = { xa: xc, ya: yc, xb: x, yb: y };
const posNormIntxn = perpIntxn(slope(positionSeg), x, y, c.corner.x, c.corner.y);
const cornerSeg = { xa: c.corner.x, ya: c.corner.y, xb: posNormIntxn.x, yb: posNormIntxn.y };
const { x: x0, y: y0 } = c.corner;
const { xa: x1, ya: y1, xb: x2, yb: y2 } = positionSeg;
const { xa: x3, ya: y3, xb: x4, yb: y4 } = cornerSeg;
// https://en.wikipedia.org/wiki/Intersection_(geometry)#Two_line_segments
// https://en.wikipedia.org/wiki/Cramer%27s_rule#Explicit_formulas_for_small_systems
const s = ((x3-x1)*(y4-y3)-(x4-x3)*(y3-y1))/((x2-x1)*(y4-y3)-(x4-x3)*(y2-y1));
const t = -((x2-x1)*(y3-y1)-(x3-x1)*(y2-y1))/((x2-x1)*(y4-y3)-(x4-x3)*(y2-y1));
const roundedT = +t.toFixed(2);
if (s >= 0 && roundedT <= 1) {
const xs = (x1 + s * (x2 - x1));
const ys = (y1 + s * (y2 - y1));
const xt = (x3 + roundedT * (x4 - x3));
const yt = (y3 + roundedT * (y4 - y3));
// [xs, ys] and [xt, yt] should be equal ([xs, ys] === [xy, yt])
// (...or about equal, notwithstanding rounding errors)
const sCollisionPt = [xs, ys];
const tCollisionPt = [xt, yt];
// drawCircle(posNormIntxn.x, posNormIntxn.y, "red");
// drawCircle(...tCollisionPt, "blue");
}
return s >= 0 && roundedT <= 1;
};
}
function detectCollision(currentPos, intendedPos, velocity, radius, { edges, corners }, gearDown) {
const { x: xc, y: yc } = intendedPos;
const [x, y] = currentPos;
// edges oriented clockwise with ship
const fwdEdges = getForwardEdges(edges, { x, y })
const edgeColl = fwdEdges.find(detectEdgeCollision([xc, yc], [x, y], radius, gearDown));
if (edgeColl) return edgeColl;
// corners ahead of ship
const fwdCorners = getForwardCorners(corners, { x, y }, velocity);
const cornersInPath = fwdCorners.filter(withinCollisionDistance({ x, y }, velocity, radius));
const cornerColl = cornersInPath.find(detectCornerCollision([xc, yc], [x, y], radius));
if (cornerColl) return cornerColl;
}
function withinCollisionDistance({ x: x1, y: y1 }, { x: x2, y: y2 }, distance) {
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 }}) => {
const velNormIntxn = perpIntxn(slopev, x1, y1, x0, y0);
const dx = Math.max(x0, velNormIntxn.x) - Math.min(x0, velNormIntxn.x);
const dy = Math.max(y0, velNormIntxn.y) - Math.min(y0, velNormIntxn.y);
const d = Math.sqrt(dy ** 2 + dx ** 2);
return d <= distance;
};
}
function cornerContactPosition(xc, yc, x, y, corner, cLength) {
const positionSeg = { xa: xc, ya: yc, xb: x, yb: y };
const posNormIntxn = perpIntxn(slope(positionSeg), x, y, corner.x, corner.y);
// shortest distance between corner and path
const aLength = distance(corner.x, corner.y, posNormIntxn.x, posNormIntxn.y);
// distance from position/normal intersection
const bLength = Math.sqrt(Math.abs(cLength ** 2 - aLength ** 2));
const intxnSeg = document.createElementNS(namespaceURIsvg, 'line');
intxnSeg.setAttribute('x1', posNormIntxn.x);
intxnSeg.setAttribute('y1', posNormIntxn.y);
intxnSeg.setAttribute('x2', x);
intxnSeg.setAttribute('y2', y);
return intxnSeg.getPointAtLength(bLength);
}
function edgeContactPosition(xc, yc, x, y, edge, radius) {
const baseSlope = slope(edge);
// if (Object.is(baseSlope, 0)) s.isLanded = true;
let { xa, ya, xb, yb } = edge;
const positionSeg = { x1: x, y1: y, x2: xc, y2: yc };
const edgeSeg = { x1: xa, y1: ya, x2: xb, y2: yb };
drawLine(x, y, xc, yc, "green");
const baseNrmlIntxn = perpIntxn(baseSlope, xa, ya, x, y);
const basePosIntxn = lineIntxnPt(edgeSeg, positionSeg);
const baseSegLength = distance(baseNrmlIntxn.x, baseNrmlIntxn.y, basePosIntxn.x, basePosIntxn.y);
const normalSegLength = distance(baseNrmlIntxn.x, baseNrmlIntxn.y, x, y);
drawLine(baseNrmlIntxn.x, baseNrmlIntxn.y, basePosIntxn.x, basePosIntxn.y, "blue");
drawLine(baseNrmlIntxn.x, baseNrmlIntxn.y, x, y, "purple");
const theta = Math.atan(normalSegLength / baseSegLength);
const h = (radius)/ Math.sin(theta);
const cl = document.createElementNS(namespaceURIsvg, 'line');
cl.setAttribute('x1', basePosIntxn.x);
cl.setAttribute('y1', basePosIntxn.y);
cl.setAttribute('x2', x);
cl.setAttribute('y2', y);
const pt = cl.getPointAtLength(h);
drawLine(basePosIntxn.x, basePosIntxn.y, pt.x, pt.y, "yellow");
// return cl.getPointAtLength(h);
return pt;
}
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) {
// const gravity = 0.25;
const gravity = 0;
// if (rotate > 0) {
// s.degrees = (s.degrees + rotationSpeed * elapsed) % 360;
// } else if (rotate < 0) {
// s.degrees = (s.degrees - rotationSpeed * elapsed) % 360;
// }
gun.style.transform = `rotate(${s.degrees}deg)`;
const { x: px, y: py } = s.position;
const { x: vx, y: vy } = s.velocity;
// const { x: ax, y: ay } = s.acceleration;
let { x: ax, y: ay } = s.acceleration;
const angularVel = s.angularVelocity;
const angularAcc = s.angularAcceleration;
const degrees = s.degrees;
ay += gravity;
s.velocity = {
x: vx > 0 && vx + ax <= 0 ? 0 : vx + ax,
y: vy > 0 && vy + ay <= 0 ? 0 : vy + ay
};
s.angularVelocity = angularVel + angularAcc;
const friction = 0.05;
const limit = 3;
if (s.angularVelocity > 0) {
if (s.angularVelocity > limit) s.angularVelocity = limit;
s.angularVelocity -= s.angularVelocity > friction ? friction : s.angularVelocity;
} else if (s.angularVelocity < 0) {
if (s.angularVelocity < -limit) s.angularVelocity = -limit;
s.angularVelocity += -s.angularVelocity > friction ? friction : -s.angularVelocity;
}
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 metersPerMillisecond = 0.001;
const pDelta = {
x: elapsed * s.velocity.x * metersPerMillisecond,
y: elapsed * s.velocity.y * metersPerMillisecond
};
const p = { x: pDelta.x + px, y: pDelta.y + py };
const turnRadians = elapsed * s.angularVelocity * metersPerMillisecond;
const radians = degrees * Math.PI / 180; // toFixed(15)?
const dDelta = turnRadians * 180 / Math.PI;
s.degrees = degrees + dDelta;
current = s.collision;
s.collision = detectCollision([px, py], p, s.velocity, s.radius, map, s.gearDown);
if (!current && s.collision) console.log("COLLISION", s.collision);
legs.style.display = s.gearDown ? "initial" : "none";
if (s.collision) {
// console.log("111111111111111111111111111");
let posP;
if (s.collision.corner) {
posP = cornerContactPosition(p.x, p.y, px, py, s.collision.corner, s.radius);
} else if (s.collision.edge) {
if (isLandable(s.collision.edge) && s.gearDown) s.isLanded = true;
posP = s.collision.position;
}
s.velocity = { x: 0, y: 0 };
// s.position = { x: posP.x, y: posP.y }
s.position = { x: p.x, y: p.y };
// s.node.style.transform = `translate(${s.position.x}px, ${s.position.y}px)`;
// } else if (current && s.collision) {
// console.log("2222222222222222222222222222222");
// if (s.isLanded && s.velocity.y < 0) {
// s.gearDown = false;
// s.isLanded = false;
// s.position = { x: p.x, y: p.y };
// s.node.style.transform = `translate(${s.position.x}px, ${s.position.y}px)`;
// s.collision = null;
// } else {
// s.velocity = { x: 0, y: 0 };
// }
} else {
// console.log("33333333333333333333333333333");
s.position = { x: p.x, y: p.y };
// s.node.style.transform = `translate(${s.position.x}px, ${s.position.y}px)`;
}
return s.position;
}
function updateEdges(position) {
// const collisionEdges = findAllEdges(allEdgePts, position);
const collisionEdges = [];
[...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();
});
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 firstFrame(timestamp) {
zero = timestamp;
zeroForTimer = timestamp;
previous = timestamp;
animate(timestamp);
}
function animate(timestamp) {
const elapsed = timestamp - previous;
const delta = timestamp - zero;
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++;
}
// s.node.style.transform = `translate(${s.position.x}px, ${s.position.y}px)`;
// Move.update(Ships[0], elapsed);
Ships.forEach(({ entity_id }) => {
// Acceleration[entity_id] = zeroP;
// Velocity[entity_id] = zeroP;
// Position[entity_id] = zeroP;
Move.update({ entity_id }, elapsed);
});
let newPos = updateShip(s, elapsed);
newPos = Position[Ships[0].entity_id];
s.node.style.transform = `translate(${newPos.x}px, ${newPos.y}px)`;
positionEl.innerText = `${newPos.x.toFixed(1)},${newPos.y.toFixed(1)}`;
// updateEdges(position);
if (drawCollisionLines) updateTriangles(position);
// stop game if ship touches a wall
// if (s.collision && !s.isLanded) {
// started = false;
// isReadingKeys = false;
// s.collision.wall.node.setAttribute('fill', 'red');
// }
if (restart) {
started = false;
restart = false;
init();
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 torque = 0.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;
const { entity_id } = Ships[0];
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;
fireBullet(s.position, s.degrees);
}
break;
case "KeyW":
case "ArrowUp":
if (!upPressed) {
upPressed = true;
s.acceleration.y += -force;
Acceleration[entity_id].y += -force;
}
break;
case "KeyS":
case "ArrowDown":
if (!downPressed) {
downPressed = true;
s.acceleration.y += force;
Acceleration[entity_id].y += force;
}
break;
case "KeyA":
case "ArrowLeft":
if (!leftPressed) {
leftPressed = true;
if (!s.gearDown) s.acceleration.x += -force;
if (!s.gearDown) Acceleration[entity_id].x += -force;
}
break;
case "KeyD":
case "ArrowRight":
if (!rightPressed) {
rightPressed = true;
if (!s.gearDown) s.acceleration.x += force;
if (!s.gearDown) Acceleration[entity_id].x += force;
}
break;
case "KeyQ":
case "Comma":
if (!rotateCCWPressed) {
rotateCCWPressed = true;
s.angularAcceleration -= torque;
}
break;
case "KeyE":
case "Period":
if (!rotateCWPressed) {
rotateCWPressed = true;
s.angularAcceleration += torque;
}
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() : restart = true;
break;
case "KeyW":
case "ArrowUp":
if (upPressed) {
upPressed = false;
s.acceleration.y -= -force;
Acceleration[entity_id].y -= -force;
}
break;
case "KeyS":
case "ArrowDown":
if (downPressed) {
downPressed = false;
s.acceleration.y -= force;
Acceleration[entity_id].y -= force;
}
break;
case "KeyA":
case "ArrowLeft":
if (leftPressed) {
leftPressed = false;
if (!s.gearDown) s.acceleration.x -= -force;
if (!s.gearDown) Acceleration[entity_id].x -= -force;
}
break;
case "KeyD":
case "ArrowRight":
if (rightPressed) {
rightPressed = false;
if (!s.gearDown) s.acceleration.x -= force;
if (!s.gearDown) Acceleration[entity_id].x -= force;
}
break;
case "KeyQ":
case "Comma":
if (rotateCCWPressed) {
rotateCCWPressed = false;
s.angularAcceleration += torque;
}
break;
case "KeyE":
case "Period":
if (rotateCWPressed) {
rotateCWPressed = false;
s.angularAcceleration -= torque;
}
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)) {
xp.innerText = Math.trunc(svgP.x);
yp.innerText = Math.trunc(svgP.y);
}
});
init();
//]]></script>
</svg>