xxxxxxxxxx
693
// todo: learn from https://p5js.org/examples/hello-p5-flocking.html
// instead of trying to invent what a square stone wheel is
// more todo
// - [x] fix occurences where plane starts with y>400 (move to y<200 pls)
// - [ ] add sounds. yes
// - [ ] easter egg plane controls with bullets to shoot rockets idk
// - [ ] clean up the dirty codes
// - [ ] stats
// - [x] add some sort of gravity (change for big funy)
// - [x] draw plane path to buffer and make toggle to draw it
// - [ ] missle lifetime
// - [ ] add more sliders to change missle turn acceleration/rate, max speeds, initial launch speed...
/*
the intercept missile tries to find a more optimal interception point with the 'plane'
by using ABC formula
*/
p5.Vector.prototype.toArray = function() {
return [this.x, this.y];
};
var p;
var ps;
var bullet;
var missile;
var homing_missile;
var intercept_missile;
var intercept_speed;
var missile_max_speed = 2;
var missile_max_acc = 0.04;
var missle_speed_at_launch = 0.1;
var gun;
const WITH_GRAVITY = true;
var gravity = 0.05;
var gravitySlider;
const TIME_BETWEEN_PATH_CHANGE = 1500;
var timeForUpdate = TIME_BETWEEN_PATH_CHANGE;
const fadingPoints = [];
var DEBUG = false;
var debugBtn;
var SHOW_NAMES = true;
var showNamesBtn;
// its actually not a path, but just some smoke circles.
var DRAWPLANEPATH = true;
var drawPlanePathBtn;
var planeHistoryBtn;
var drawWholePlaneHistory = false;
let history;
var missileHistoryBtn;
var drawWholeMissileHistory = false;
let missile_history;
const turn_speed = 0.025;
/* 2=plane, 1=homing, 0=intercept */
const paths = []; // very very clunky
var t = 0;
var dt = 0.005;
// WIP interval for capturing missile path history
const PATH_INTERVAL = 0;
var dp = 0
var planeTarget;
const planePath = [];
const MAX_PATH_LENGTH = 100;
var focusOnMissile_checkbox;
const sign = function (x) {
return x < 0.0 ? -1 : 1;
};
let zoom = 1; // Zoom factor, starts at 1
let offsetX = 0; // X-axis offset for panning
let offsetY = 0; // Y-axis offset for panning
let zoomFactor = 1.1;
let zoomX = 200;
let zoomY = 200;
function resetView() {
zoom = 1;
offsetX = 0;
offsetY = 0;
}
function mouseWheel(event) {
if (mouseX < 0 || mouseY > height || mouseX > width || mouseY < 0) return;
zoom -= event.delta * 0.001; // Adjust the zoom factor
zoom = constrain(zoom, 0.2, 5); // Constrain the zoom level between 0.5x and 3x
//print(event.delta); // Debug the delta value
}
var selectBox;
function setup() {
createCanvas(400, 400);
selectBox = createSelect();
history = createGraphics(400,400);
history.noStroke();
history.fill(255,255,255,10)
missile_history = createGraphics(400,400);
// gui elements
gravitySlider = createSlider(-200, 200, gravity * 10);
gravitySlider.input(function (e) {
gravity = gravitySlider.value() / 100;
print(`new gravity = ${gravity}`)
});
gravitySlider.mouseReleased((e)=>updatePlanePath(0));
showNamesBtn = createButton('showNames').mouseReleased((e)=>
SHOW_NAMES=!SHOW_NAMES)
drawPlanePathBtn = createButton('drawPlanePath').mouseReleased((e)=>
DRAWPLANEPATH=!DRAWPLANEPATH)
debugBtn = createButton('debug').mouseReleased((e)=>
DEBUG=!DEBUG)
planeHistoryBtn = createButton('plane_history').mouseReleased((e)=>
drawWholePlaneHistory=!drawWholePlaneHistory)
missileHistoryBtn = createButton('missile_history').mouseReleased((e)=>
drawWholeMissileHistory=!drawWholeMissileHistory)
rectMode(CENTER);
// starting coordinates for the weird planepath
resetPlanePath();
p = createVector(planePath[0], planePath[1]);
ps = p5.Vector.random2D();
paths.push([]);
planeTarget = createVector([planePath[6], planePath[7]]);
intercept_missile = createVector(200, 400);
intercept_speed = createVector(0, -missle_speed_at_launch); //UP
paths.push([]);
homing_missile = createVector(200, 400);
homing_speed = createVector(0, -missle_speed_at_launch); //UP
paths.push([]);
missileMaxSpeedSlider = createSlider(0.1, 10, missile_max_speed, 0.025);
missileMaxSpeedSlider.input(function (e) {
// can never be less than the gravity to account for gravity forces
missile_max_speed = missileMaxSpeedSlider.value();
});
missileMaxSpeedSlider.mouseReleased((e)=>updatePlanePath(0));
// 0.04
missileMaxAccSlider = createSlider(0.005, 0.1, missile_max_acc, 0.005);
missileMaxAccSlider.input(function (e) {
// can never be less than the gravity to account for gravity forces
missile_max_acc = missileMaxAccSlider.value();
});
missileMaxAccSlider.mouseReleased((e)=>updatePlanePath(0));
/*
sliders :
- gravity -2, 2, starts at 0.05
-
*/
selectBox.option('static');
selectBox.option('plane');
selectBox.option('homing');
selectBox.option('intercept');
createButton('reset view').input(function(e) {zoom = 1})
}
function resetPlanePath() {
planePath.splice(0,planePath.length)
planePath.push(random(150));
planePath.push(random(100));
planePath.push(random(250, 350));
planePath.push(random(100));
planePath.push(random(150));
planePath.push(random(100, 200));
planePath.push(random(250, 350));
planePath.push(random(100, 200));
}
class FadingPoint {
constructor(x, y) {
this.x = x;
this.y = y;
this.life = 255;
}
draw() {}
}
function updatePlanePath(manually = 0) {
// update the plane and reset it to the start of the curve
// note: this does not rotate the plane correctly, so it has
// to adjust itself. the curve is just to add randomness
// and the plane doesn't need to follow it correctly
if (!manually) {
planePath.shift();
planePath.shift();
// add mouse coords as the new point in the curve
planePath.push(random(0, 400));
planePath.push(random(0, 200));
timeForUpdate = TIME_BETWEEN_PATH_CHANGE;
}
const [x1, y1, x2, y2, x3, y3, x4, y4] = planePath;
const nx = bezierPoint(x1, x2, x3, x4, 0);
const ny = bezierPoint(y1, y2, y3, y4, 0);
p.x = nx;
p.y = ny;
t = 0 + abs(dt);
// re-fire the projectiles
intercept_missile = createVector(200, 400);
intercept_speed = createVector(0, -missle_speed_at_launch); //UP
print(missle_speed_at_launch)
homing_missile = createVector(200, 400);
homing_speed = createVector(0, -missle_speed_at_launch); //UP
// reset the paths. there are 3.
for (const i of [0, 1, 2]) {
paths.shift();
paths.push([]);
}
}
function mouseClicked() {
// only fire within the canvas
// and if mouseY > 300, limit it to 300
if (0 < mouseX && mouseX < 401 && 0 < mouseY && mouseY < 401) {
// remove previous coordinates from plane path
planePath.shift();
planePath.shift();
// add mouse coords as the new point in the curve
planePath.push(mouseX);
planePath.push(constrain(mouseY, 0, 300));
timeForUpdate = TIME_BETWEEN_PATH_CHANGE;
updatePlanePath(1);
}
}
function drawArrow(base, vec, myColor) {
// from here: https://p5js.org/reference/#/p5.Vector/angleBetween
push();
stroke(myColor);
strokeWeight(3);
fill(myColor);
translate(base.x, base.y);
if (DEBUG) {
line(0, 0, vec.x, vec.y);
}
rotate(vec.heading());
let arrowSize = 7;
translate(vec.mag() - arrowSize, 0);
triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
pop();
}
function updatePlane() {
const [x1, y1, x2, y2, x3, y3, x4, y4] = planePath;
const nx = bezierPoint(x1, x2, x3, x4, t);
const ny = bezierPoint(y1, y2, y3, y4, t);
planeTarget.x = nx;
planeTarget.y = ny;
var planeDir = p5.Vector.sub(planeTarget, p);
planeDir.setMag(turn_speed);
ps = p5.Vector.lerp(ps, planeDir, 0.5);
ps.normalize();
p.add(ps);
if (WITH_GRAVITY) {
p.add(createVector(0, gravity));
}
if (DEBUG) {
line(p.x, p.y, nx, ny);
}
// drawArrow(p, planeTarget, 'black');
// drawArrow(p, ps, 'green');
t += dt;
if (t > 1 || t < 0) {
dt = -dt;
}
// refire projectiles if they hit the target
if (intercept_missile.dist(p) < 7) {
/* 2=plane, 1=homing, 0=intercept */
// letting it fade was fun too
// paths[0].splice(0,paths[0].length);
intercept_missile = createVector(200, 400);
intercept_speed = createVector(0, -missile_max_speed); //UP
}
if (homing_missile.dist(p) < 7) {
/* 2=plane, 1=homing, 0=intercept */
// paths[1].splice(0,paths[0].length);
homing_missile = createVector(200, 400);
homing_speed = createVector(0, -missile_max_speed); //UP
}
paths[2].push([p.x, p.y, random(25) / 10]);
if (paths[2].length > MAX_PATH_LENGTH / 4) {
paths[2].shift();
}
}
function updateAndDrawFadingPoints() {
for (const [x, y] of fadingPoints) {
}
}
function drawPlane() {
const planeLength = 10;
const planeWidth = 5;
push();
translate(p.x, p.y);
rotate(ps.heading());
rect(0, 0, planeLength, planeWidth);
pop();
if (DRAWPLANEPATH) { // its actually not a path, but just some smoke circles.
// draw the plane path onto a buffer
// - [ ] todo: noise the color;
history.circle(p.x,p.y, 3);
push();
noStroke();
for (const [i, [x, y, r]] of paths[2].entries()) {
fill(`rgba(128,128,128,${i/paths[2].length*0.2})`); // smokey trail lol
circle(x, y, r * map(i, 0, paths[2].length, 3, 1));
}
pop();
}
}
function spawn() {}
function updateInterceptingMissile() {
const m = intercept_missile; // Position of missile [x, y]
const s = intercept_speed; // Speed vector of missile [dx, dy]
const ms = intercept_speed; // Speed vector of missile [dx, dy]
const targetvec = p; // Target position (plane) [x, y]
const targetspd = ps; // Target speed vector
// Find the vector pointing at the target from the missile
var target = p5.Vector.sub(p, m);
// Implementing guidance logic based on the YouTube video (https://youtu.be/T2fPKUfmnKo?t=100)
// Calculate next position of plane
const targetnxt = p5.Vector.add(targetvec, targetspd);
// Calculate the vector from the missile to the plane (line of sight)
const line_of_sight = p5.Vector.sub(p, m);
// Calculate the intersection of a circle centered on the next plane position
// with the line of sight, where the radius equals the missile speed.
// Refer to https://stackoverflow.com/a/57892466/6934388 for the math involved.
const cpt = targetnxt; // Previously calculated next position of plane
const p1 = m; // Intercepting missile
const p2 = p; // Plane
const r = ms.mag(); // Magnitude (speed) of the missile
let x1 = p1.copy().sub(cpt); // Vector from circle center to missile
let x2 = p2.copy().sub(cpt); // Vector from circle center to plane
let dv = x2.copy().sub(x1); // Difference between x1 and x2
let dr = dv.mag(); // Magnitude of dv
let D = x1.x * x2.y - x2.x * x1.y; // D value in the circle-line intersection formula
let di = r * r * dr * dr - D * D; // Discriminant
// push(); // Save current drawing settings
// strokeWeight(5); // Set point size
// // Draw and label points
// point(cpt.x, cpt.y); // Circle center (next position of plane)
// stroke(255, 0, 0); // Set line color to red
// line(cpt.x, cpt.y, x1.x + cpt.x, x1.y + cpt.y); // Line from circle center to missile position
// textAlign(CENTER, CENTER);
// text('this is x1', x1.x + cpt.x, x1.y + cpt.y - 10); // Text slightly above the point
// stroke(0, 255, 0); // Set line color to green
// line(cpt.x, cpt.y, x2.x + cpt.x, x2.y + cpt.y); // Line from circle center to plane position
// textAlign(CENTER, CENTER);
// text('this is x2', x2.x + cpt.x, x2.y + cpt.y - 10); // Text slightly above the point
// stroke(0, 0, 255); // Set line color to blue
// line(x1.x + cpt.x, x1.y + cpt.y, x2.x + cpt.x, x2.y + cpt.y); // Line from missile position to plane position
// textAlign(CENTER, CENTER);
// text('this is dv', (x1.x + x2.x) / 2 + cpt.x, (x1.y + x2.y) / 2 + cpt.y - 10); // Text slightly above the midpoint of the line
// pop(); // Restore saved drawing settings
if (di < 0.0) {
_=1; // we love wasting cpu power for unused assignments here
// print("horrible error");
// print(di)
// print(sqrt(di))
} else {
let tt = sqrt(di);
ip = [];
ip.push(
new p5.Vector(
D * dv.y + sign(dv.y) * dv.x * tt,
-D * dv.x + abs(dv.y) * tt
)
.div(dr * dr)
.add(cpt)
);
if (di > 0.0) {
ip.push(
new p5.Vector(
D * dv.y - sign(dv.y) * dv.x * tt,
-D * dv.x - abs(dv.y) * tt
)
.div(dr * dr)
.add(cpt)
);
}
// idk why this was here
// push();
// line(ip[0].x, ip[0].y, targetnxt.x, targetnxt.y);
// pop();
let v = p2.copy().sub(p1);
let d = v.mag();
v = v.normalize();
let vx = ip[0].copy().sub(p1);
let dx = v.dot(vx);
var intersection;
if (dx >= 0 && dx <= d) {
// ip[0] between p1 and p2
intersection = ip[0];
} else {
// its probably ip[1]
intersection = ip[1];
}
const desired_vec = p5.Vector.sub(targetnxt, intersection);
// VISUALISATIONS
const neededVectorVisualiser = p5.Vector.mult(desired_vec, 10);
// push();
// translate(targetnxt.x, targetnxt.y);
// line(0,0,neededVectorVisualiser.x, neededVectorVisualiser.y);
// pop();
// show needed vec on missile
push();
translate(m.x, m.y);
if (DEBUG) {
line(0, 0, neededVectorVisualiser.x, neededVectorVisualiser.y);
}
pop();
var ratio =
dist(p.x, p.y, m.x, m.y) / dist(intersection.x, intersection.y, p.x, p.y);
const predictedLocation = p5.Vector.add(
m,
p5.Vector.mult(desired_vec, ratio)
);
push();
translate(predictedLocation.x, predictedLocation.y);
stroke(255, 0, 0, 50);
line(-10, -10, 10, 10);
line(-10, 10, 10, -10);
pop();
// target = p5.Vector.sub(p, m); original
target = p5.Vector.sub(predictedLocation, m);
}
target.normalize();
target.mult(missile_max_speed);
var steer = p5.Vector.sub(target, s);
steer.limit(missile_max_acc);
s.add(steer);
s.limit(missile_max_speed);
m.add(s);
if (WITH_GRAVITY) {
m.add(createVector(0, gravity));
}
if (dp >= PATH_INTERVAL) {paths[0].push([m.x, m.y]); dp=0;}
missile_history.stroke(0,255,0,10);
missile_history.point(m.x, m.y)
dp+=1
if (paths[0].length > MAX_PATH_LENGTH) {
paths[0].shift();
}
}
function drawInterceptingMissile() {
const m = intercept_missile;
const s = intercept_speed;
push();
translate(m.x, m.y);
rotate(s.heading());
fill("blue");
if (SHOW_NAMES) {
textAlign(RIGHT);
text("Intercepting ", 0, 0);
}
rect(0, 0, 9, 2);
pop();
for (const [i,[x, y]] of paths[0].entries()) {
push();
stroke(0,0,0,i*2)
point(x, y);
pop();
}
}
function updateHomingMissile() {
const m = homing_missile;
const s = homing_speed;
var target = p5.Vector.sub(p, m);
target.normalize();
target.mult(missile_max_speed);
var steer = p5.Vector.sub(target, s);
steer.limit(missile_max_acc);
s.add(steer);
s.limit(missile_max_speed);
m.add(s);
if (WITH_GRAVITY) {
m.add(createVector(0, gravity));
}
paths[1].push([m.x, m.y]);
missile_history.stroke(0,0,255,10);
missile_history.point(m.x, m.y)
if (paths[1].length > MAX_PATH_LENGTH) {
paths[1].shift();
}
}
function drawHomingMissile() {
const m = homing_missile;
const s = homing_speed;
push();
translate(m.x, m.y);
rotate(s.heading());
fill("green");
if (SHOW_NAMES) {
textAlign(RIGHT);
text("Homing ", 0, 0);
}
rect(0, 0, 9, 2);
pop();
for (const [i,[x, y]] of paths[1].entries()) {
push();
stroke(0,0,0,i*2)
point(x, y);
pop();
}
}
function draw() {
background(220);
// translate(offsetX, offsetY);
// scale(zoom);
switch (selectBox.value()) {
case 'plane':
zoomX = p.x;
zoomY = p.y;
break;
case 'homing':
zoomX = homing_missile.x;
zoomY = homing_missile.y;
break;
case 'intercept':
zoomX = intercept_missile.x;
zoomY = intercept_missile.y;
break;
default:
zoomX = 200;
zoomY = 200;
break;
}
translate(width / 2, height / 2); // Move to the center of the canvas
scale(zoom); // Scale by the zoom factor
translate(-zoomX, -zoomY); // Move the origin to stay in the center
line(0,400,400,400)
if (drawWholePlaneHistory) {
image(history,0,0);
}
if (drawWholeMissileHistory){
image(missile_history,0,0)
}
// DEBUGLINES: PLANE PATH
if (DEBUG) {
push();
const [x1, y1, x2, y2, x3, y3, x4, y4] = planePath;
noFill();
textAlign(CENTER);
stroke(0, 0, 0, 66);
circle(x1, y1, 2); text(1,x1,y1);
circle(x2, y2, 2); text(2,x2,y2);
circle(x3, y3, 2); text(3,x3,y3);
circle(x4, y4, 2); text(4,x4,y4);
stroke(0, 0, 0, 50);
bezier(x1, y1, x2, y2, x3, y3, x4, y4);
pop();
} //endDEBUGLINES
updatePlane();
updateHomingMissile();
updateInterceptingMissile();
drawPlane();
drawHomingMissile();
drawInterceptingMissile();
push();
fill("green");
text("homing", 10, 10);
fill("rgb(0,11,255)");
text("intercepting", 10, 25);
fill("red");
text(`gravity ${gravity.toFixed(3)}`, 10, 50);
pop();
timeForUpdate -= 1;
text(`reset in:${timeForUpdate}`, 10, 37);
if (timeForUpdate < 0) {
updatePlanePath();
print("updated plane path.");
}
}