CARS and also better node editing

This commit is contained in:
Emma 2023-02-13 16:38:58 -07:00
parent 657f228d47
commit 08d63395e3
13 changed files with 832 additions and 710 deletions

583
bundle.js
View File

@ -5,10 +5,6 @@
const Constants = {
TWO_PI: Math.PI * 2
};
const map = (value, x1, y1, x2, y2)=>(value - x1) * (y2 - x2) / (y1 - x1) + x2;
const Constants1 = {
TWO_PI: Math.PI * 2
};
class Vector {
x;
y;
@ -63,6 +59,7 @@ class Vector {
this.y += y ?? 0;
this.z += z ?? 0;
}
return this;
}
sub(v, y, z) {
if (arguments.length === 1 && typeof v !== 'number') {
@ -77,6 +74,7 @@ class Vector {
this.y -= y ?? 0;
this.z -= z ?? 0;
}
return this;
}
mult(v) {
if (typeof v === 'number') {
@ -100,6 +98,7 @@ class Vector {
this.y /= v.y;
this.z /= v.z;
}
return this;
}
rotate(angle) {
const prev_x = this.x;
@ -107,6 +106,7 @@ class Vector {
const s = Math.sin(angle);
this.x = c * this.x - s * this.y;
this.y = s * prev_x + c * this.y;
return this;
}
dist(v) {
const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z;
@ -139,6 +139,7 @@ class Vector {
this.x = lerp_val(this.x, x, amt);
this.y = lerp_val(this.y, y, amt);
this.z = lerp_val(this.z, z, amt);
return this;
}
normalize() {
const m = this.mag();
@ -152,6 +153,7 @@ class Vector {
this.normalize();
this.mult(high);
}
return this;
}
heading() {
return -Math.atan2(-this.y, this.x);
@ -191,7 +193,7 @@ class Vector {
return Vector.fromAngle(Math.random() * (Math.PI * 2), v);
}
static random3D(v) {
const angle = Math.random() * Constants1.TWO_PI;
const angle = Math.random() * Constants.TWO_PI;
const vz = Math.random() * 2 - 1;
const mult = Math.sqrt(1 - vz * vz);
const vx = mult * Math.cos(angle);
@ -255,6 +257,7 @@ class Doodler {
return this.ctx.canvas.height;
}
draggables = [];
clickables = [];
constructor({ width , height , canvas , bg , framerate }){
if (!canvas) {
canvas = document.createElement('canvas');
@ -279,6 +282,10 @@ class Doodler {
this.mouseY = e.clientY - rect.top;
for (const d of this.draggables.filter((d)=>d.beingDragged)){
d.point.add(e.movementX, e.movementY);
d.onDrag && d.onDrag({
x: e.movementX,
y: e.movementY
});
}
});
this.startDrawLoop();
@ -323,19 +330,19 @@ class Doodler {
weight: 1
});
this.ctx.beginPath();
this.ctx.arc(at.x, at.y, style?.weight || 1, 0, Constants1.TWO_PI);
this.ctx.arc(at.x, at.y, style?.weight || 1, 0, Constants.TWO_PI);
this.ctx.fill();
}
drawCircle(at, radius, style) {
this.setStyle(style);
this.ctx.beginPath();
this.ctx.arc(at.x, at.y, radius, 0, Constants1.TWO_PI);
this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI);
this.ctx.stroke();
}
fillCircle(at, radius, style) {
this.setStyle(style);
this.ctx.beginPath();
this.ctx.arc(at.x, at.y, radius, 0, Constants1.TWO_PI);
this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI);
this.ctx.fill();
}
drawRect(at, width, height, style) {
@ -385,6 +392,12 @@ class Doodler {
cb();
this.ctx.restore();
}
drawImage(img, at, w, h) {
w && h ? this.ctx.drawImage(img, at.x, at.y, w, h) : this.ctx.drawImage(img, at.x, at.y);
}
drawSprite(img, spritePos, sWidth, sHeight, at, width, height) {
this.ctx.drawImage(img, spritePos.x, spritePos.y, sWidth, sHeight, at.x, at.y, width, height);
}
setStyle(style) {
const ctx = this.ctx;
ctx.fillStyle = style?.color || style?.fillColor || 'black';
@ -414,20 +427,40 @@ class Doodler {
}
this.draggables = this.draggables.filter((d)=>d.point !== point);
}
addDragEvents({ onDragEnd , onDragStart , point }) {
registerClickable(p1, p2, cb) {
const top = Math.min(p1.y, p2.y);
const left = Math.min(p1.x, p2.x);
const bottom = Math.max(p1.y, p2.y);
const right = Math.max(p1.x, p2.x);
this.clickables.push({
onClick: cb,
checkBound: (p)=>p.y >= top && p.x >= left && p.y <= bottom && p.x <= right
});
}
unregisterClickable(cb) {
this.clickables = this.clickables.filter((c)=>c.onClick !== cb);
}
addDragEvents({ onDragEnd , onDragStart , onDrag , point }) {
const d = this.draggables.find((d)=>d.point === point);
if (d) {
d.onDragEnd = onDragEnd;
d.onDragStart = onDragStart;
d.onDrag = onDrag;
}
}
onClick(e) {
const mouse = new Vector(this.mouseX, this.mouseY);
for (const d of this.draggables){
if (d.point.dist(new Vector(this.mouseX, this.mouseY)) <= d.radius) {
if (d.point.dist(mouse) <= d.radius) {
d.beingDragged = true;
d.onDragStart?.call(null);
} else d.beingDragged = false;
}
for (const c of this.clickables){
if (c.checkBound(mouse)) {
c.onClick();
}
}
}
offClick(e) {
for (const d of this.draggables){
@ -470,35 +503,79 @@ class Doodler {
this.uiElements.delete(id);
}
}
class ComplexPath {
points = [];
radius = 50;
ctx;
constructor(points){
points && (this.points = points);
class Train {
nodes = [];
cars = [];
path;
t;
engineLength = 40;
spacing = 30;
constructor(track, cars = []){
this.path = track;
this.t = 0;
this.nodes.push(this.path.followEvenPoints(this.t));
this.nodes.push(this.path.followEvenPoints(this.t - this.real2Track(40)));
this.cars.push(new TrainCar(55, document.getElementById('engine-sprites'), 80, 20, {
at: new Vector(0, 60),
width: 80,
height: 20
}));
this.cars[0].points = this.nodes.map((n)=>n);
let currentOffset = 40;
for (const car of cars){
currentOffset += this.spacing;
const a = this.path.followEvenPoints(this.t - currentOffset);
currentOffset += car.length;
const b = this.path.followEvenPoints(this.t - currentOffset);
car.points = [
a,
b
];
this.cars.push(car);
}
}
setContext(ctx) {
this.ctx = ctx;
move() {
this.t = (this.t + 1) % this.path.evenPoints.length;
let currentOffset = 0;
for (const car of this.cars){
if (!car.points) return;
const [a, b] = car.points;
a.set(this.path.followEvenPoints(this.t - currentOffset));
currentOffset += car.length;
b.set(this.path.followEvenPoints(this.t - currentOffset));
currentOffset += this.spacing;
car.draw();
}
}
real2Track(length) {
return length / this.path.pointSpacing;
}
}
class TrainCar {
img;
imgWidth;
imgHeight;
sprite;
points;
length;
constructor(length, img, w, h, sprite){
this.img = img;
this.sprite = sprite;
this.imgWidth = w;
this.imgHeight = h;
this.length = length;
}
draw() {
if (!this.ctx || !this.points.length) return;
const ctx = this.ctx;
ctx.save();
ctx.lineWidth = 2;
ctx.strokeStyle = 'white';
ctx.setLineDash([
21,
6
]);
let last = this.points[this.points.length - 1];
for (const point of this.points){
ctx.beginPath();
ctx.moveTo(last.x, last.y);
ctx.lineTo(point.x, point.y);
ctx.stroke();
last = point;
}
ctx.restore();
if (!this.points) return;
const [a, b] = this.points;
const origin = Vector.add(Vector.sub(a, b).div(2), b);
const angle = Vector.sub(b, a).heading();
doodler.drawCircle(origin, 4, {
color: 'blue'
});
doodler.drawRotated(origin, angle, ()=>{
this.sprite ? doodler.drawSprite(this.img, this.sprite.at, this.sprite.width, this.sprite.height, origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2), this.imgWidth, this.imgHeight) : doodler.drawImage(this.img, origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2));
});
}
}
class PathSegment {
@ -604,7 +681,7 @@ class PathSegment {
const current = stepSize * i;
points.push(this.getPointAtT(current));
}
return points.reduce((acc, cur)=>{
this.length = points.reduce((acc, cur)=>{
const prev = acc.prev;
acc.prev = cur;
if (!prev) return acc;
@ -614,6 +691,7 @@ class PathSegment {
prev: undefined,
length: 0
}).length;
return this.length;
}
calculateEvenlySpacedPoints(spacing, resolution = 1) {
const points = [];
@ -638,254 +716,6 @@ class PathSegment {
return points;
}
}
class Mover {
position;
velocity;
acceleration;
maxSpeed;
maxForce;
_trailingPoint;
_leadingPoint;
get trailingPoint() {
const desired = this.velocity.copy();
desired.normalize();
desired.mult(-this._trailingPoint);
return Vector.add(this.position, desired);
}
get leadingPoint() {
const desired = this.velocity.copy();
desired.normalize();
desired.mult(this._leadingPoint);
return Vector.add(this.position, desired);
}
ctx;
boundingBox;
constructor(posOrRandom, vel, acc){
if (typeof posOrRandom === 'boolean' && posOrRandom) {
this.position = Vector.random2D(new Vector());
this.velocity = Vector.random2D(new Vector());
this.acceleration = new Vector();
} else {
this.position = posOrRandom || new Vector();
this.velocity = vel || new Vector();
this.acceleration = acc || new Vector();
}
this.boundingBox = {
size: new Vector(20, 10),
pos: new Vector(this.position.x - 10, this.position.y - 5)
};
this.maxSpeed = 3;
this.maxForce = .3;
this._trailingPoint = 0;
this._leadingPoint = 0;
this.init();
}
init() {}
move() {
this.velocity.limit(this.maxSpeed);
this.acceleration.limit(this.maxForce);
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
this.edges();
this.draw();
}
edges() {
if (!this.ctx) return;
if (this.position.x > this.ctx.canvas.width) this.position.x = 0;
if (this.position.y > this.ctx.canvas.height) this.position.y = 0;
if (this.position.x < 0) this.position.x = this.ctx.canvas.width;
if (this.position.y < 0) this.position.y = this.ctx.canvas.height;
}
draw() {
doodler.drawRotated(this.position, this.velocity.heading() || 0, ()=>{
doodler.fillCenteredRect(this.position, this.boundingBox.size.x, this.boundingBox.size.y, {
fillColor: 'white'
});
});
if (!this.ctx) return;
this.ctx.fillStyle = 'white';
this.ctx.save();
this.ctx.translate(this.position.x, this.position.y);
this.ctx.rotate(this.velocity.heading() || 0);
this.ctx.translate(-this.position.x, -this.position.y);
this.ctx.translate(-(this.boundingBox.size.x / 2), -(this.boundingBox.size.y / 2));
this.ctx.fillRect(this.position.x, this.position.y, this.boundingBox.size.x, this.boundingBox.size.y);
this.ctx.restore();
}
setContext(ctx) {
this.ctx = ctx;
}
applyForce(force) {
this.acceleration.add(force);
}
static edges(point, width, height) {
if (point.x > width) point.x = 0;
if (point.y > height) point.y = 0;
if (point.x < 0) point.x = width;
if (point.y < 0) point.y = height;
}
}
class Follower extends Mover {
debug = true;
follow(toFollow) {
if (toFollow instanceof ComplexPath) {
const predict = this.velocity.copy();
predict.normalize();
predict.mult(25);
const predictpos = Vector.add(this.position, predict);
if (this.ctx) Mover.edges(predict, this.ctx.canvas.width, this.ctx.canvas.height);
let normal = null;
let target = null;
let worldRecord = 1000000;
for(let i = 0; i < toFollow.points.length; i++){
let a = toFollow.points[i];
let b = toFollow.points[(i + 1) % toFollow.points.length];
let normalPoint = getNormalPoint(predictpos, a, b);
let dir = Vector.sub(b, a);
if (normalPoint.x < Math.min(a.x, b.x) || normalPoint.x > Math.max(a.x, b.x) || normalPoint.y < Math.min(a.y, b.y) || normalPoint.y > Math.max(a.y, b.y)) {
normalPoint = b.copy();
a = toFollow.points[(i + 1) % toFollow.points.length];
b = toFollow.points[(i + 2) % toFollow.points.length];
dir = Vector.sub(b, a);
}
const d = Vector.dist(predictpos, normalPoint);
if (d < worldRecord) {
worldRecord = d;
normal = normalPoint;
dir.normalize();
dir.mult(25);
target = normal.copy();
target.add(dir);
}
if (worldRecord > toFollow.radius) {
return this.seek(target);
}
}
if (this.debug && this.ctx) {
this.ctx.strokeStyle = 'red';
this.ctx.fillStyle = 'pink';
this.ctx.beginPath();
this.ctx.moveTo(this.position.x, this.position.y);
this.ctx.lineTo(predictpos.x, predictpos.y);
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.arc(predictpos.x, predictpos.y, 4, 0, Constants.TWO_PI);
this.ctx.fill();
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.arc(normal.x, normal.y, 4, 0, Constants.TWO_PI);
this.ctx.fill();
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.moveTo(predictpos.x, predictpos.y);
this.ctx.lineTo(target.x, target.y);
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.arc(target.x, target.y, 8, 0, Constants.TWO_PI);
this.ctx.fill();
this.ctx.stroke();
}
}
}
seek(target, strength = 1) {
const desired = Vector.sub(target, this.position);
desired.normalize();
desired.mult(this.maxSpeed);
const steer = Vector.sub(desired, this.velocity);
steer.limit(this.maxForce);
this.applyForce(steer.mult(strength));
}
link(target) {
this.position = target.trailingPoint;
this.seek(target.trailingPoint);
}
arrive(target) {
const desired = Vector.sub(target, this.position);
const d = desired.mag();
let speed = this.maxSpeed;
if (d < 10) {
speed = map(d, 0, 100, 0, this.maxSpeed);
}
desired.setMag(speed);
const steer = Vector.sub(desired, this.velocity);
steer.limit(this.maxForce);
this.applyForce(steer);
}
}
function getNormalPoint(p, a, b) {
const ap = Vector.sub(p, a);
const ab = Vector.sub(b, a);
ab.normalize();
ab.mult(ap.dot(ab));
const normalPoint = Vector.add(a, ab);
return normalPoint;
}
class Train extends Follower {
nodes;
currentTrack;
speed;
follower;
followers;
constructor(track, length){
super(track.points[0].copy());
this.maxSpeed = 2;
this.speed = 1;
this.currentTrack = track;
this.velocity = this.currentTrack.tangent(0).normalize().mult(this.maxSpeed);
this.addCar(length);
this.maxForce = .2;
}
init() {
this.boundingBox.size.set(30, 10);
this._trailingPoint = 30;
}
move() {
this.follow();
super.move();
this.follower?.move();
}
follow() {
const [_, t] = this.currentTrack.followTrack(this);
this.velocity = this.currentTrack.tangent(t);
this.velocity.normalize().mult(this.speed || this.maxSpeed);
}
setContext(ctx) {
super.setContext(ctx);
this.follower?.setContext(ctx);
}
addCar(length) {
console.log(length);
if (length) this.follower = new TrainCar(this.currentTrack, length - 1);
this.follower?.setTarget(this);
this.follower?.position.set(this.trailingPoint);
this._trailingPoint -= 2;
}
}
class TrainCar extends Train {
target;
setTarget(t) {
this.target = t;
}
init() {
this.boundingBox.size.set(20, 10);
this._trailingPoint = 25;
this.maxSpeed = this.maxSpeed * 2;
this.maxForce = this.maxForce * 2;
}
move() {
if (this.target) {
if (this.position.dist(this.target.position) > this.target.position.dist(this.target.trailingPoint)) {
this.arrive(this.currentTrack.getNearestPoint(this.target.trailingPoint));
this.speed = this.target.speed;
super.move();
} else {
this.draw();
this.follower?.draw();
}
}
}
edges() {}
}
class Track extends PathSegment {
editable = false;
next;
@ -897,38 +727,6 @@ class Track extends PathSegment {
this.next = next || this;
this.prev = prev || this;
}
followTrack(train) {
const predict = train.velocity.copy();
predict.normalize();
predict.mult(1);
const predictpos = Vector.add(train.position, predict);
let [closest, closestDistance, closestT] = this.getClosestPoint(predictpos);
let mostValid = this;
if (this.next !== this) {
const [point, distance, t] = this.next.getClosestPoint(predictpos);
if (distance < closestDistance) {
closest = point;
closestDistance = distance;
mostValid = this.next;
closestT = t;
}
}
if (this.prev !== this) {
const [point1, distance1, t1] = this.next.getClosestPoint(predictpos);
if (distance1 < closestDistance) {
closest = point1;
closestDistance = distance1;
mostValid = this.next;
closestT = t1;
}
}
train.currentTrack = mostValid;
train.arrive(closest);
return [
closest,
closestT
];
}
getNearestPoint(p) {
let [closest, closestDistance] = this.getClosestPoint(p);
if (this.next !== this) {
@ -953,8 +751,10 @@ class Track extends PathSegment {
}
draw() {
super.draw();
if (this.editable) for (const e of this.points){
e.drawDot();
if (this.editable) {
const [a, b, c, d] = this.points;
doodler.line(a, b);
doodler.line(c, d);
}
}
setNext(t) {
@ -970,9 +770,28 @@ class Spline {
segments = [];
ctx;
evenPoints;
pointSpacing;
get points() {
return Array.from(new Set(this.segments.flatMap((s)=>s.points)));
}
nodes;
constructor(segs){
this.segments = segs;
this.pointSpacing = 1;
this.evenPoints = this.calculateEvenlySpacedPoints(1);
this.nodes = [];
for(let i = 0; i < this.points.length; i += 3){
const node = {
anchor: this.points[i],
controls: [
this.points.at(i - 1),
this.points[(i + 1) % this.points.length]
],
mirrored: false,
tangent: true
};
this.nodes.push(node);
}
}
setContext(ctx) {
this.ctx = ctx;
@ -986,6 +805,7 @@ class Spline {
}
}
calculateEvenlySpacedPoints(spacing, resolution = 1) {
this.pointSpacing = 1;
const points = [];
points.push(this.segments[0].points[0]);
let prev = points[0];
@ -1007,6 +827,7 @@ class Spline {
prev = point;
}
}
this.evenPoints = points;
return points;
}
followEvenPoints(t) {
@ -1014,10 +835,35 @@ class Spline {
const i = Math.floor(t);
const a = this.evenPoints[i];
const b = this.evenPoints[(i + 1) % this.evenPoints.length];
try {
return Vector.lerp(a, b, t % 1);
} catch {
console.log(t, i, a, b);
return Vector.lerp(a, b, t % 1);
}
calculateApproxLength() {
for (const s of this.segments){
s.calculateApproxLength();
}
}
toggleNodeTangent(p) {
const node = this.nodes.find((n)=>n.anchor === p);
node && (node.tangent = !node.tangent);
}
toggleNodeMirrored(p) {
const node = this.nodes.find((n)=>n.anchor === p);
node && (node.mirrored = !node.mirrored);
}
handleNodeEdit(p, movement) {
const node = this.nodes.find((n)=>n.anchor === p || n.controls.includes(p));
if (!node || !(node.mirrored || node.tangent)) return;
if (node.anchor !== p) {
if (node.mirrored || node.tangent) {
const mover = node.controls.find((e)=>e !== p);
const v = Vector.sub(node.anchor, p);
if (!node.mirrored) v.setMag(Vector.sub(node.anchor, mover).mag());
mover.set(Vector.add(v, node.anchor));
}
} else {
for (const control of node.controls){
control.add(movement.x, movement.y);
}
}
}
}
@ -1108,25 +954,62 @@ const loadFromJson = ()=>{
}
return new Spline(segments);
};
const engineSprites = document.createElement('img');
engineSprites.src = './sprites/EngineSprites.png';
engineSprites.style.display = 'none';
engineSprites.id = 'engine-sprites';
document.body.append(engineSprites);
init({
width: 400,
height: 400,
bg: '#333'
});
const path = loadFromJson();
let t = 0;
let speed = 1;
Array(1).fill(null).map((_, i)=>new Train(path.segments[i % path.segments.length], 5));
const car = new TrainCar(55, engineSprites, 80, 20, {
at: new Vector(0, 80),
height: 20,
width: 80
});
const train = new Train(path, [
car
]);
let dragEndCounter = 0;
let selectedNode;
doodler.createLayer(()=>{
path.draw();
const points = Array(5).fill(null).map((_, i)=>path.followEvenPoints(t - i * 15));
for (const point of points){
point && doodler.drawCircle(point, 5, {
strokeColor: 'green'
for(let i = 0; i < path.evenPoints.length; i += 10){
const p = path.evenPoints[i];
const next = path.evenPoints[(i + 1) % path.evenPoints.length];
const last = path.evenPoints.at(i - 1);
if (!last) break;
const tan = Vector.sub(last, next);
doodler.drawRotated(p, tan.heading(), ()=>{
doodler.line(p, p.copy().add(0, 10), {
color: '#291b17',
weight: 4
});
doodler.line(p, p.copy().add(0, -10), {
color: '#291b17',
weight: 4
});
doodler.line(p.copy().add(-6, 5), p.copy().add(6, 5), {
color: 'grey',
weight: 2
});
doodler.line(p.copy().add(-6, -5), p.copy().add(6, -5), {
color: 'grey',
weight: 2
});
});
}
t = (t + speed / 2) % path.evenPoints.length;
path.draw();
train.move();
selectedNode?.anchor.drawDot();
selectedNode?.controls.forEach((e)=>e.drawDot());
});
let editable = false;
const clickables = new Map();
let selectedPoint;
document.addEventListener('keyup', (e)=>{
if (e.key === 'd') {}
if (e.key === 'ArrowUp') {
@ -1135,7 +1018,23 @@ document.addEventListener('keyup', (e)=>{
if (e.key === 'ArrowDown') {
speed -= .1;
}
if (e.key === 'm' && selectedPoint) {
const points = path.points;
const index = points.findIndex((p)=>p === selectedPoint);
if (index > -1) {
const prev = points.at(index - 1);
const next = points[(index + 1) % points.length];
const toPrev = Vector.sub(prev, selectedPoint);
toPrev.setMag(next.dist(selectedPoint));
toPrev.rotate(Math.PI);
const toNext = Vector.add(toPrev, selectedPoint);
next.set(toNext);
path.calculateApproxLength();
path.calculateEvenlySpacedPoints(1);
}
}
if (e.key === 'e') {
editable = !editable;
for (const t of path.segments){
t.editable = !t.editable;
for (const p of t.points){
@ -1144,12 +1043,30 @@ document.addEventListener('keyup', (e)=>{
doodler.addDragEvents({
point: p,
onDragEnd: ()=>{
console.log('dragend');
dragEndCounter++;
t.length = t.calculateApproxLength(100);
path.evenPoints = path.calculateEvenlySpacedPoints(1);
},
onDrag: (movement)=>{
path.handleNodeEdit(p, movement);
}
});
} else doodler.unregisterDraggable(p);
} else {
doodler.unregisterDraggable(p);
}
}
}
for (const p1 of path.points){
if (editable) {
const onClick = ()=>{
selectedPoint = p1;
selectedNode = path.nodes.find((e)=>e.anchor === p1 || e.controls.includes(p1));
};
clickables.set(p1, onClick);
doodler.registerClickable(p1.copy().sub(10, 10), p1.copy().add(10, 10), onClick);
} else {
const the = clickables.get(p1);
doodler.unregisterClickable(the);
}
}
}

View File

@ -12,6 +12,6 @@
},
"imports": {
"drawing": "./drawing/index.ts",
"doodler": "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.4a/mod.ts"
"doodler": "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.7a/mod.ts"
}
}

115
main.ts
View File

@ -1,18 +1,19 @@
import { lerp } from "./math/lerp.ts";
import { ComplexPath, PathSegment } from "./math/path.ts";
import { Mover } from "./physics/mover.ts";
import { Train } from "./train.ts";
import { Train, TrainCar } from "./train.ts";
import { fillCircle, drawCircle } from 'drawing';
import { generateSquareTrack, loadFromJson } from "./track.ts";
import { generateSquareTrack, IControlNode, loadFromJson } from "./track.ts";
import { drawLine } from "./drawing/line.ts";
import { initializeDoodler, Vector } from 'doodler';
// for (const mover of trains) {
// mover.setContext(ctx);
// mover.velocity.add(Vector.random2D())
// }
const engineSprites = document.createElement('img');
engineSprites.src = './sprites/EngineSprites.png';
engineSprites.style.display = 'none';
engineSprites.id = 'engine-sprites';
document.body.append(engineSprites);
initializeDoodler({
width: 400,
@ -33,43 +34,42 @@ let t = 0;
let currentSeg = 0;
let speed = 1;
const trainCount = 1;
const trains = Array(trainCount).fill(null).map((_, i) => new Train(path.segments[i % path.segments.length], 5));
// const trainCount = 1;
// const trains = Array(trainCount).fill(null).map((_, i) => new Train(path.segments[i % path.segments.length], 5));
const car = new TrainCar(55, engineSprites, 80, 20, {at: new Vector(0, 80), height: 20, width: 80})
const train = new Train(path, [car]);
let dragEndCounter = 0
let selectedNode: IControlNode | undefined;
doodler.createLayer(() => {
path.draw();
for (let i = 0; i < path.evenPoints.length; i+=10) {
const p = path.evenPoints[i];
const next = path.evenPoints[(i + 1)%path.evenPoints.length];
const last = path.evenPoints.at(i - 1);
if (!last) break;
const tan = Vector.sub(last, next);
// for (const train of trains) {
// train.move();
// }
// ctx.strokeStyle = 'red';
// ctx.lineWidth = 4;
// const seg = path.segments[currentSeg];
// const start = seg.getPointAtT(t);
// const tan = seg.tangent(t).normalize().mult(25);
// const tan = seg.tangent(t);
// for (const p of path.evenPoints) {
// p.drawDot();
// }
// doodler.line(start, new Vector(start.x + tan.x, start.y + tan.y), {color: 'blue'});
// doodler.fillCircle(start, 5, {fillColor: 'blue'})
const points = Array(5).fill(null).map((_,i) => path.followEvenPoints(t - (i * 15)))
for (const point of points) {
point &&
doodler.drawCircle(point, 5, { strokeColor: 'green' })
doodler.drawRotated(p, tan.heading(), () => {
doodler.line(p, p.copy().add(0,10), {color: '#291b17', weight: 4})
doodler.line(p, p.copy().add(0,-10), {color: '#291b17', weight: 4})
doodler.line(p.copy().add(-6,5), p.copy().add(6,5), {color: 'grey', weight: 2})
doodler.line(p.copy().add(-6,-5), p.copy().add(6,-5), {color: 'grey', weight: 2})
})
}
// const point = path.followEvenPoints(t);
path.draw();
train.move();
t = (t + (speed / 2)) % path.evenPoints.length;
// path.segments.forEach(s => s.calculateApproxLength(10000))
selectedNode?.anchor.drawDot();
selectedNode?.controls.forEach(e => e.drawDot());
})
let editable = false;
const clickables = new Map()
let selectedPoint: Vector;
document.addEventListener('keyup', e => {
if (e.key === 'd') {
// console.log(trains)
@ -90,7 +90,26 @@ document.addEventListener('keyup', e => {
speed -= .1
}
if (e.key === 'm' && selectedPoint) {
const points = path.points;
const index = points.findIndex(p => p === selectedPoint);
if (index > -1) {
const prev = points.at(index - 1)!;
const next = points[(index + 1) % points.length];
const toPrev = Vector.sub(prev, selectedPoint);
toPrev.setMag(next.dist(selectedPoint));
toPrev.rotate(Math.PI)
const toNext = Vector.add(toPrev, selectedPoint);
next.set(toNext);
path.calculateApproxLength();
path.calculateEvenlySpacedPoints(1);
}
}
if (e.key === 'e') {
editable = !editable;
for (const t of path.segments) {
t.editable = !t.editable;
for (const p of t.points) {
@ -99,14 +118,34 @@ document.addEventListener('keyup', e => {
doodler.addDragEvents({
point: p,
onDragEnd: () => {
console.log('dragend');
dragEndCounter++
t.length = t.calculateApproxLength(100)
path.evenPoints = path.calculateEvenlySpacedPoints(1)
},
onDrag: (movement) => {
// todo - remove ! after updating doodler
path.handleNodeEdit(p, movement!)
}
})
}
else
else {
doodler.unregisterDraggable(p)
}
}
}
for (const p of path.points) {
if (editable) {
const onClick = () => {
selectedPoint = p;
selectedNode = path.nodes.find(e => e.anchor === p || e.controls.includes(p));
}
clickables.set(p, onClick);
doodler.registerClickable(p.copy().sub(10, 10), p.copy().add(10, 10), onClick);
}
else {
const the = clickables.get(p);
doodler.unregisterClickable(the);
}
}
}

View File

@ -184,13 +184,14 @@ export class PathSegment {
const current = stepSize * i;
points.push(this.getPointAtT(current))
}
return points.reduce((acc: { prev?: Vector, length: number }, cur) => {
this.length = points.reduce((acc: { prev?: Vector, length: number }, cur) => {
const prev = acc.prev;
acc.prev = cur;
if (!prev) return acc;
acc.length += cur.dist(prev);
return acc;
}, { prev: undefined, length: 0 }).length
return this.length;
}
calculateEvenlySpacedPoints(spacing: number, resolution = 1) {

BIN
sprites/BlueEngine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
sprites/Engine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
sprites/EngineSprites.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
sprites/GreenEngine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
sprites/PurpleEngine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
sprites/RedEngine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

155
track.ts
View File

@ -18,43 +18,43 @@ export class Track extends PathSegment {
this.prev = prev || this;
}
followTrack(train: Train): [Vector, number] {
const predict = train.velocity.copy();
predict.normalize();
predict.mult(1);
const predictpos = Vector.add(train.position, predict)
// followTrack(train: Train): [Vector, number] {
// const predict = train.velocity.copy();
// predict.normalize();
// predict.mult(1);
// const predictpos = Vector.add(train.position, predict)
// const leading = train.leadingPoint;
// let closest = this.points[0];
// let closestDistance = this.getClosestPoint(leading);
let [closest, closestDistance, closestT] = this.getClosestPoint(predictpos);
// deno-lint-ignore no-this-alias
let mostValid: Track = this;
// // const leading = train.leadingPoint;
// // let closest = this.points[0];
// // let closestDistance = this.getClosestPoint(leading);
// let [closest, closestDistance, closestT] = this.getClosestPoint(predictpos);
// // deno-lint-ignore no-this-alias
// let mostValid: Track = this;
if (this.next !== this) {
const [point, distance, t] = this.next.getClosestPoint(predictpos);
if (distance < closestDistance) {
closest = point;
closestDistance = distance;
mostValid = this.next;
closestT = t;
}
}
if (this.prev !== this) {
const [point, distance, t] = this.next.getClosestPoint(predictpos);
if (distance < closestDistance) {
closest = point;
closestDistance = distance;
mostValid = this.next;
closestT = t;
}
}
// if (this.next !== this) {
// const [point, distance, t] = this.next.getClosestPoint(predictpos);
// if (distance < closestDistance) {
// closest = point;
// closestDistance = distance;
// mostValid = this.next;
// closestT = t;
// }
// }
// if (this.prev !== this) {
// const [point, distance, t] = this.next.getClosestPoint(predictpos);
// if (distance < closestDistance) {
// closest = point;
// closestDistance = distance;
// mostValid = this.next;
// closestT = t;
// }
// }
train.currentTrack = mostValid;
train.arrive(closest);
// if (predictpos.dist(closest) > 2) train.arrive(closest);
return [closest, closestT];
}
// train.currentTrack = mostValid;
// train.arrive(closest);
// // if (predictpos.dist(closest) > 2) train.arrive(closest);
// return [closest, closestT];
// }
getNearestPoint(p: Vector) {
let [closest, closestDistance] = this.getClosestPoint(p);
@ -85,10 +85,11 @@ export class Track extends PathSegment {
draw(): void {
super.draw();
if (this.editable)
for (const e of this.points) {
e.drawDot();
}
if (this.editable) {
const [a, b, c, d] = this.points;
doodler.line(a, b);
doodler.line(c, d);
}
}
setNext(t: Track) {
@ -107,9 +108,28 @@ export class Spline<T extends PathSegment = PathSegment> {
ctx?: CanvasRenderingContext2D;
evenPoints: Vector[];
pointSpacing: number;
get points() {
return Array.from(new Set(this.segments.flatMap(s => s.points)));
}
nodes: IControlNode[];
constructor(segs: T[]) {
this.segments = segs;
this.pointSpacing = 1;
this.evenPoints = this.calculateEvenlySpacedPoints(1);
this.nodes = [];
for (let i = 0; i < this.points.length; i += 3) {
const node: IControlNode = {
anchor: this.points[i],
controls: [this.points.at(i - 1)!, this.points[(i + 1) % this.points.length]],
mirrored: false,
tangent: true
}
this.nodes.push(node);
}
}
setContext(ctx: CanvasRenderingContext2D) {
@ -126,6 +146,7 @@ export class Spline<T extends PathSegment = PathSegment> {
}
calculateEvenlySpacedPoints(spacing: number, resolution = 1) {
this.pointSpacing = 1;
// return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution));
const points: Vector[] = []
@ -155,20 +176,51 @@ export class Spline<T extends PathSegment = PathSegment> {
}
}
this.evenPoints = points;
return points;
}
followEvenPoints(t: number) {
if (t < 0) t+= this.evenPoints.length
if (t < 0) t += this.evenPoints.length
const i = Math.floor(t);
const a = this.evenPoints[i]
const b = this.evenPoints[(i + 1) % this.evenPoints.length]
try {
return Vector.lerp(a, b, t % 1);
return Vector.lerp(a, b, t % 1);
}
} catch {
console.log(t, i, a, b);
calculateApproxLength() {
for (const s of this.segments) {
s.calculateApproxLength();
}
}
toggleNodeTangent(p: Vector) {
const node = this.nodes.find(n => n.anchor === p);
node && (node.tangent = !node.tangent);
}
toggleNodeMirrored(p: Vector) {
const node = this.nodes.find(n => n.anchor === p);
node && (node.mirrored = !node.mirrored);
}
handleNodeEdit(p: Vector, movement: { x: number, y: number }) {
const node = this.nodes.find(n => n.anchor === p || n.controls.includes(p));
if (!node || !(node.mirrored || node.tangent)) return;
if (node.anchor !== p) {
if (node.mirrored || node.tangent) {
const mover = node.controls.find(e => e !== p)!;
const v = Vector.sub(node.anchor, p);
if (!node.mirrored) v.setMag(Vector.sub(node.anchor, mover).mag());
mover.set(Vector.add(v, node.anchor));
}
} else {
for (const control of node.controls) {
control.add(movement.x, movement.y)
}
}
}
}
@ -209,14 +261,21 @@ export const loadFromJson = () => {
if (!json) return generateSquareTrack();
const segments: Track[] = [];
for (const {points} of json.segments) {
segments.push(new Track(points.map((p:{x:number,y:number}) => new Vector(p.x, p.y))));
for (const { points } of json.segments) {
segments.push(new Track(points.map((p: { x: number, y: number }) => new Vector(p.x, p.y))));
}
for (const [i,s] of segments.entries()) {
s.setNext(segments[(i+1)%segments.length])
s.setPrev(segments.at(i-1)!)
for (const [i, s] of segments.entries()) {
s.setNext(segments[(i + 1) % segments.length])
s.setPrev(segments.at(i - 1)!)
}
return new Spline<Track>(segments);
}
export interface IControlNode {
anchor: Vector;
controls: [Vector, Vector];
tangent: boolean;
mirrored: boolean;
}

312
train.old.ts Normal file
View File

@ -0,0 +1,312 @@
import { drawLine } from "./drawing/line.ts";
import { ComplexPath, PathSegment } from "./math/path.ts";
import { Vector } from "doodler";
import { Follower } from "./physics/follower.ts";
import { Mover } from "./physics/mover.ts";
import { Track } from "./track.ts";
export class Train extends Follower {
nodes?: Vector[];
currentTrack: Track;
speed: number;
follower?: TrainCar;
followers?: TrainCar[];
constructor(track: Track, length: number) {
super(track.points[0].copy());
this.maxSpeed = 2;
this.speed = 1;
this.currentTrack = track;
this.velocity = this.currentTrack.tangent(0).normalize().mult(this.maxSpeed);
this.addCar(length);
this.maxForce = .2;
}
init(): void {
this.boundingBox.size.set(30, 10);
this._trailingPoint = 30;
}
move(): void {
this.follow();
super.move();
this.follower?.move()
// this.draw();
}
follow(): void {
// const [_, t] = this.currentTrack.followTrack(this);
// this.position = this.currentTrack.getPointAtT(t);
// this.velocity = this.currentTrack.tangent(t);
this.velocity.normalize().mult(this.speed || this.maxSpeed);
// if (nearest.dist(this.position) > 10)
// this.seek(nearest);
}
// draw(): void {
// if (!this.ctx) return;
// const ctx = this.ctx;
// // const [a, b] = this.nodes;
// ctx.strokeStyle = 'blue'
// ctx.lineWidth = 10;
// // drawLine(ctx, a.x, a.y, b.x, b.y);
// super.draw()
// }
setContext(ctx: CanvasRenderingContext2D): void {
super.setContext(ctx);
this.follower?.setContext(ctx);
}
addCar(length: number,) {
console.log(length);
if (length)
this.follower = new TrainCar(this.currentTrack, length - 1);
this.follower?.setTarget(this);
this.follower?.position.set(this.trailingPoint);
this._trailingPoint -= 2;
}
}
class TrainCar extends Train {
// constructor(n: [Vector, Vector], track: Track) {
// super(track);
// this.nodes = n;
// }
target?: Train;
setTarget(t: Train) {
this.target = t;
}
init(): void {
this.boundingBox.size.set(20, 10)
this._trailingPoint = 25;
this.maxSpeed = this.maxSpeed * 2;
this.maxForce = this.maxForce * 2;
// this.speed = 0;
}
// follow(): void {
// if (!this.target) return;
// const points = this.currentTrack.getAllPointsInRange(this.target.position, this.target._trailingPoint);
// let closest = this.target.position;
// let closestTan = this.target.velocity;
// for (const [t, path] of points) {
// const point = path.getPointAtT(t);
// if (point.dist(this.target.trailingPoint) < this.target.trailingPoint.dist(closest)) {
// closest = point;
// closestTan = path.tangent(t);
// }
// }
// // this.position.set(closest);
// this.seek(closest);
// this.velocity.set(closestTan.normalize().mult(this.target.speed));
// }
move(): void {
// if (!this.target) return;
// const r = 30;
// const points = this.currentTrack.getAllPointsInRange(this.target.position, this.target._trailingPoint);
// let closest = this.target.position;
// let closestTan = this.target.velocity;
// for (const [t, path] of points) {
// const point = path.getPointAtT(t);
// if (point.dist(this.target.trailingPoint) < this.target.trailingPoint.dist(closest)) {
// closest = point;
// closestTan = path.tangent(t);
// }
// }
// // this.position.set(closest);
// // this.seek(closest);
// this.velocity.set(closestTan.normalize().mult(this.target.speed));
// super.move();
// if (this.target && this.position.dist(this.target.trailingPoint) < 2) {
// this.velocity.setMag(0);
// } else if (this.target) {
// this.velocity.setMag(this.target.velocity.mag());
// }
// if (this.target) {
// this.position.set(this.target.trailingPoint);
// this.speed = this.target.speed;
// }
// const [pos,t] = this.currentTrack.followTrack(this);
// this.position = pos.copy()
// if (this.target) {
// const points = this.currentTrack.getPointWithinRadius(this.target.position, 30);
// let closest = this.target.position;
// for (const [i,point] of points.entries()) {
// if (typeof point !== "number") break;
// const tracks = [this.currentTrack, this.currentTrack.next, this.currentTrack.prev];
// const a = tracks[i].getPointAtT(point);
// if (a.dist(this.target.trailingPoint) < closest.dist(this.target.trailingPoint)) {
// closest = a;
// }
// }
// this.position = closest;
// }
// this.draw();
if (this.target) {
if (this.position.dist(this.target.position) > this.target.position.dist(this.target.trailingPoint)) {
// this.velocity = this.currentTrack.tangent(t);
// this.velocity.normalize().mult(this.speed);
this.arrive(this.currentTrack.getNearestPoint(this.target.trailingPoint));
// if (this.position.dist())
// this.move()
this.speed = this.target.speed;
super.move();
} else {
this.draw()
this.follower?.draw();
}
}
// this.draw()
// this.follower?.move()
}
// draw(): void {
// if (!this.ctx) return;
// super.draw()
// this.ctx.fillStyle = 'red';
// this.position.drawDot(this.ctx);
// this.ctx.fillStyle = 'green';
// this.target?.trailingPoint.drawDot(this.ctx);
// }
edges(): void {
}
}
// export class Train extends Follower {
// currentSegment: Track;
// cars: TrainCar[] = [];
// id: string;
// constructor(path: Track);
// constructor(x: number, y: number, segment: Track);
// constructor(x: number | Track, y?: number, segment?: Track) {
// super(x instanceof Track ? x.points[0].copy() : new Vector(x, y))
// if (x instanceof Track) {
// this.currentSegment = x;
// } else if (segment) {
// this.currentSegment = segment;
// } else {
// throw new Error('Path not provided for train construction')
// }
// // super(new Vector(Math.floor(Math.random() * 200),Math.floor(Math.random() * 200)), Vector.random2D());
// this.id = crypto.randomUUID()
// this.boundingBox.size.set(40, 10)
// this.maxSpeed = 3;
// this.maxForce = .3;
// this.addCar();
// this._trailingPoint = 40;
// this._leadingPoint = 15;
// }
// move(): void {
// for (const car of this.cars) {
// car.move();
// }
// this.follow(this.currentSegment)
// super.move();
// }
// draw(): void {
// if (!this.ctx) return;
// // this.ctx.save();
// this.ctx.fillStyle = 'white';
// this.ctx.strokeStyle = 'red';
// super.draw();
// // this.ctx.restore();
// }
// addCar() {
// const last = this.cars[this.cars.length - 1];
// this.cars.push(new TrainCar(this, (last || this).velocity.copy().normalize().mult(-30), last));
// }
// setContext(ctx: CanvasRenderingContext2D): void {
// super.setContext(ctx);
// for (const car of this.cars) {
// car.setContext(ctx);
// }
// }
// follow(toFollow: Track): void {
// // const predict = this.velocity.copy();
// // predict.normalize();
// // predict.mult(25);
// // const predictpos = Vector.add(this.position, predict)
// const nearest = toFollow.getMostValidTrack(this);
// this.seek(nearest);
// }
// }
// export class TrainCar extends Follower {
// train?: Train;
// prevCar?: Mover;
// constructor(train: Train, pos: Vector, prevCar: Mover) {
// super(pos);
// this.train = train;
// this.boundingBox.size.set(20, 15);
// this.prevCar = prevCar || train;
// this.maxSpeed = 2;
// this.maxForce = .3;
// this._trailingPoint = 25;
// this._leadingPoint = 25;
// }
// move(): void {
// if (this.train && this.prevCar) {
// this.link(this.prevCar);
// // super.move();
// this.edges();
// this.ctx && (this.ctx.fillStyle = 'orange')
// this.draw();
// }
// else super.move();
// }
// edges(): void {
// if (!this.ctx || !this.train) return;
// if (this.train.position.x > this.ctx.canvas.width) this.position.x -= this.ctx.canvas.width;
// if (this.train.position.y > this.ctx.canvas.height) this.position.y -= this.ctx.canvas.height;
// if (this.train.position.x < 0) this.position.x += this.ctx.canvas.width;
// if (this.train.position.y < 0) this.position.y += this.ctx.canvas.height;
// }
// }

372
train.ts
View File

@ -3,310 +3,104 @@ import { ComplexPath, PathSegment } from "./math/path.ts";
import { Vector } from "doodler";
import { Follower } from "./physics/follower.ts";
import { Mover } from "./physics/mover.ts";
import { Track } from "./track.ts";
import { Spline, Track } from "./track.ts";
export class Train extends Follower {
nodes?: Vector[];
export class Train {
nodes: Vector[] = [];
currentTrack: Track;
cars: TrainCar[] = [];
speed: number;
path: Spline<Track>;
t: number;
follower?: TrainCar;
engineLength = 40;
spacing = 30;
followers?: TrainCar[];
constructor(track: Track, length: number) {
super(track.points[0].copy());
this.maxSpeed = 2;
this.speed = 1;
this.currentTrack = track;
this.velocity = this.currentTrack.tangent(0).normalize().mult(this.maxSpeed);
this.addCar(length);
this.maxForce = .2;
}
init(): void {
this.boundingBox.size.set(30, 10);
this._trailingPoint = 30;
}
move(): void {
this.follow();
super.move();
this.follower?.move()
// this.draw();
}
follow(): void {
const [_, t] = this.currentTrack.followTrack(this);
// this.position = this.currentTrack.getPointAtT(t);
this.velocity = this.currentTrack.tangent(t);
this.velocity.normalize().mult(this.speed || this.maxSpeed);
// if (nearest.dist(this.position) > 10)
// this.seek(nearest);
}
// draw(): void {
// if (!this.ctx) return;
// const ctx = this.ctx;
// // const [a, b] = this.nodes;
// ctx.strokeStyle = 'blue'
// ctx.lineWidth = 10;
// // drawLine(ctx, a.x, a.y, b.x, b.y);
// super.draw()
// }
setContext(ctx: CanvasRenderingContext2D): void {
super.setContext(ctx);
this.follower?.setContext(ctx);
}
addCar(length: number,) {
console.log(length);
if (length)
this.follower = new TrainCar(this.currentTrack, length - 1);
this.follower?.setTarget(this);
this.follower?.position.set(this.trailingPoint);
this._trailingPoint -= 2;
}
}
class TrainCar extends Train {
// constructor(n: [Vector, Vector], track: Track) {
// super(track);
// this.nodes = n;
// }
target?: Train;
setTarget(t: Train) {
this.target = t;
}
init(): void {
this.boundingBox.size.set(20, 10)
this._trailingPoint = 25;
this.maxSpeed = this.maxSpeed * 2;
this.maxForce = this.maxForce * 2;
// this.speed = 0;
}
// follow(): void {
// if (!this.target) return;
// const points = this.currentTrack.getAllPointsInRange(this.target.position, this.target._trailingPoint);
// let closest = this.target.position;
// let closestTan = this.target.velocity;
// for (const [t, path] of points) {
// const point = path.getPointAtT(t);
// if (point.dist(this.target.trailingPoint) < this.target.trailingPoint.dist(closest)) {
// closest = point;
// closestTan = path.tangent(t);
// }
// }
// // this.position.set(closest);
// this.seek(closest);
// this.velocity.set(closestTan.normalize().mult(this.target.speed));
// }
move(): void {
// if (!this.target) return;
// const r = 30;
// const points = this.currentTrack.getAllPointsInRange(this.target.position, this.target._trailingPoint);
// let closest = this.target.position;
// let closestTan = this.target.velocity;
// for (const [t, path] of points) {
// const point = path.getPointAtT(t);
// if (point.dist(this.target.trailingPoint) < this.target.trailingPoint.dist(closest)) {
// closest = point;
// closestTan = path.tangent(t);
// }
// }
// // this.position.set(closest);
// // this.seek(closest);
// this.velocity.set(closestTan.normalize().mult(this.target.speed));
// super.move();
// if (this.target && this.position.dist(this.target.trailingPoint) < 2) {
// this.velocity.setMag(0);
// } else if (this.target) {
// this.velocity.setMag(this.target.velocity.mag());
// }
// if (this.target) {
// this.position.set(this.target.trailingPoint);
// this.speed = this.target.speed;
// }
// const [pos,t] = this.currentTrack.followTrack(this);
// this.position = pos.copy()
// if (this.target) {
// const points = this.currentTrack.getPointWithinRadius(this.target.position, 30);
// let closest = this.target.position;
// for (const [i,point] of points.entries()) {
// if (typeof point !== "number") break;
// const tracks = [this.currentTrack, this.currentTrack.next, this.currentTrack.prev];
// const a = tracks[i].getPointAtT(point);
// if (a.dist(this.target.trailingPoint) < closest.dist(this.target.trailingPoint)) {
// closest = a;
// }
// }
// this.position = closest;
// }
// this.draw();
if (this.target) {
if (this.position.dist(this.target.position) > this.target.position.dist(this.target.trailingPoint)) {
// this.velocity = this.currentTrack.tangent(t);
// this.velocity.normalize().mult(this.speed);
this.arrive(this.currentTrack.getNearestPoint(this.target.trailingPoint));
// if (this.position.dist())
// this.move()
this.speed = this.target.speed;
super.move();
} else {
this.draw()
this.follower?.draw();
}
constructor(track: Spline<Track>, cars: TrainCar[] = []) {
this.path = track;
this.t = 0;
this.nodes.push(this.path.followEvenPoints(this.t),)
this.nodes.push(this.path.followEvenPoints(this.t - this.real2Track(40)));
this.cars.push(new TrainCar(55, document.getElementById('engine-sprites')! as HTMLImageElement, 80, 20, { at: new Vector(0, 60), width: 80, height: 20 }));
this.cars[0].points = this.nodes.map(n => n) as [Vector, Vector];
let currentOffset = 40;
for (const car of cars) {
currentOffset += this.spacing;
const a = this.path.followEvenPoints(this.t - currentOffset);
currentOffset += car.length;
const b = this.path.followEvenPoints(this.t - currentOffset);
car.points = [a,b];
this.cars.push(car);
}
// this.draw()
// this.follower?.move()
}
// draw(): void {
// if (!this.ctx) return;
// super.draw()
// this.ctx.fillStyle = 'red';
// this.position.drawDot(this.ctx);
// this.ctx.fillStyle = 'green';
// this.target?.trailingPoint.drawDot(this.ctx);
move() {
this.t = (this.t + 1) % this.path.evenPoints.length;
let currentOffset = 0;
for (const car of this.cars) {
if (!car.points) return;
const [a,b] = car.points;
a.set(this.path.followEvenPoints(this.t - currentOffset));
currentOffset += car.length;
b.set(this.path.followEvenPoints(this.t - currentOffset));
currentOffset += this.spacing;
car.draw();
}
// this.draw();
}
// draw() {
// for (const [i, node] of this.nodes.entries()) {
// doodler.drawCircle(node.point, 10, { color: 'purple', weight: 3 })
// // const next = this.nodes[i + 1];
// // if (next) {
// // const to = Vector.sub(node.point, next.point);
// // to.setMag(40);
// // doodler.line(next.point, Vector.add(to, next.point))
// // }
// }
// }
edges(): void {
real2Track(length: number) {
return length / this.path.pointSpacing
}
}
// export class Train extends Follower {
export class TrainCar {
img: HTMLImageElement;
imgWidth: number;
imgHeight: number;
sprite?: ISprite;
// currentSegment: Track;
// cars: TrainCar[] = [];
points?: [Vector, Vector];
length: number;
// id: string;
// constructor(path: Track);
// constructor(x: number, y: number, segment: Track);
// constructor(x: number | Track, y?: number, segment?: Track) {
constructor(length: number, img: HTMLImageElement, w: number, h: number, sprite?: ISprite) {
this.img = img;
this.sprite = sprite;
this.imgWidth = w;
this.imgHeight = h;
this.length = length;
}
// super(x instanceof Track ? x.points[0].copy() : new Vector(x, y))
draw() {
if (!this.points) return;
const [a, b] = this.points;
const origin = Vector.add(Vector.sub(a, b).div(2), b);
const angle = Vector.sub(b, a).heading();
// if (x instanceof Track) {
// this.currentSegment = x;
// } else if (segment) {
// this.currentSegment = segment;
// } else {
// throw new Error('Path not provided for train construction')
// }
// // super(new Vector(Math.floor(Math.random() * 200),Math.floor(Math.random() * 200)), Vector.random2D());
// this.id = crypto.randomUUID()
// this.boundingBox.size.set(40, 10)
doodler.drawCircle(origin, 4, {color: 'blue'})
doodler.drawRotated(origin, angle, () => {
this.sprite ?
doodler.drawSprite(this.img, this.sprite.at, this.sprite.width, this.sprite.height, origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2), this.imgWidth, this.imgHeight) :
doodler.drawImage(this.img, origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2));
})
}
}
// this.maxSpeed = 3;
// this.maxForce = .3;
// this.addCar();
// this._trailingPoint = 40;
// this._leadingPoint = 15;
// }
// move(): void {
// for (const car of this.cars) {
// car.move();
// }
// this.follow(this.currentSegment)
// super.move();
// }
// draw(): void {
// if (!this.ctx) return;
// // this.ctx.save();
// this.ctx.fillStyle = 'white';
// this.ctx.strokeStyle = 'red';
// super.draw();
// // this.ctx.restore();
// }
// addCar() {
// const last = this.cars[this.cars.length - 1];
// this.cars.push(new TrainCar(this, (last || this).velocity.copy().normalize().mult(-30), last));
// }
// setContext(ctx: CanvasRenderingContext2D): void {
// super.setContext(ctx);
// for (const car of this.cars) {
// car.setContext(ctx);
// }
// }
// follow(toFollow: Track): void {
// // const predict = this.velocity.copy();
// // predict.normalize();
// // predict.mult(25);
// // const predictpos = Vector.add(this.position, predict)
// const nearest = toFollow.getMostValidTrack(this);
// this.seek(nearest);
// }
// }
// export class TrainCar extends Follower {
// train?: Train;
// prevCar?: Mover;
// constructor(train: Train, pos: Vector, prevCar: Mover) {
// super(pos);
// this.train = train;
// this.boundingBox.size.set(20, 15);
// this.prevCar = prevCar || train;
// this.maxSpeed = 2;
// this.maxForce = .3;
// this._trailingPoint = 25;
// this._leadingPoint = 25;
// }
// move(): void {
// if (this.train && this.prevCar) {
// this.link(this.prevCar);
// // super.move();
// this.edges();
// this.ctx && (this.ctx.fillStyle = 'orange')
// this.draw();
// }
// else super.move();
// }
// edges(): void {
// if (!this.ctx || !this.train) return;
// if (this.train.position.x > this.ctx.canvas.width) this.position.x -= this.ctx.canvas.width;
// if (this.train.position.y > this.ctx.canvas.height) this.position.y -= this.ctx.canvas.height;
// if (this.train.position.x < 0) this.position.x += this.ctx.canvas.width;
// if (this.train.position.y < 0) this.position.y += this.ctx.canvas.height;
// }
// }
interface ISprite {
at: Vector;
width: number;
height: number;
}