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 = { const Constants = {
TWO_PI: Math.PI * 2 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 { class Vector {
x; x;
y; y;
@ -63,6 +59,7 @@ class Vector {
this.y += y ?? 0; this.y += y ?? 0;
this.z += z ?? 0; this.z += z ?? 0;
} }
return this;
} }
sub(v, y, z) { sub(v, y, z) {
if (arguments.length === 1 && typeof v !== 'number') { if (arguments.length === 1 && typeof v !== 'number') {
@ -77,6 +74,7 @@ class Vector {
this.y -= y ?? 0; this.y -= y ?? 0;
this.z -= z ?? 0; this.z -= z ?? 0;
} }
return this;
} }
mult(v) { mult(v) {
if (typeof v === 'number') { if (typeof v === 'number') {
@ -100,6 +98,7 @@ class Vector {
this.y /= v.y; this.y /= v.y;
this.z /= v.z; this.z /= v.z;
} }
return this;
} }
rotate(angle) { rotate(angle) {
const prev_x = this.x; const prev_x = this.x;
@ -107,6 +106,7 @@ class Vector {
const s = Math.sin(angle); const s = Math.sin(angle);
this.x = c * this.x - s * this.y; this.x = c * this.x - s * this.y;
this.y = s * prev_x + c * this.y; this.y = s * prev_x + c * this.y;
return this;
} }
dist(v) { dist(v) {
const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z; 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.x = lerp_val(this.x, x, amt);
this.y = lerp_val(this.y, y, amt); this.y = lerp_val(this.y, y, amt);
this.z = lerp_val(this.z, z, amt); this.z = lerp_val(this.z, z, amt);
return this;
} }
normalize() { normalize() {
const m = this.mag(); const m = this.mag();
@ -152,6 +153,7 @@ class Vector {
this.normalize(); this.normalize();
this.mult(high); this.mult(high);
} }
return this;
} }
heading() { heading() {
return -Math.atan2(-this.y, this.x); return -Math.atan2(-this.y, this.x);
@ -191,7 +193,7 @@ class Vector {
return Vector.fromAngle(Math.random() * (Math.PI * 2), v); return Vector.fromAngle(Math.random() * (Math.PI * 2), v);
} }
static random3D(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 vz = Math.random() * 2 - 1;
const mult = Math.sqrt(1 - vz * vz); const mult = Math.sqrt(1 - vz * vz);
const vx = mult * Math.cos(angle); const vx = mult * Math.cos(angle);
@ -255,6 +257,7 @@ class Doodler {
return this.ctx.canvas.height; return this.ctx.canvas.height;
} }
draggables = []; draggables = [];
clickables = [];
constructor({ width , height , canvas , bg , framerate }){ constructor({ width , height , canvas , bg , framerate }){
if (!canvas) { if (!canvas) {
canvas = document.createElement('canvas'); canvas = document.createElement('canvas');
@ -279,6 +282,10 @@ class Doodler {
this.mouseY = e.clientY - rect.top; this.mouseY = e.clientY - rect.top;
for (const d of this.draggables.filter((d)=>d.beingDragged)){ for (const d of this.draggables.filter((d)=>d.beingDragged)){
d.point.add(e.movementX, e.movementY); d.point.add(e.movementX, e.movementY);
d.onDrag && d.onDrag({
x: e.movementX,
y: e.movementY
});
} }
}); });
this.startDrawLoop(); this.startDrawLoop();
@ -323,19 +330,19 @@ class Doodler {
weight: 1 weight: 1
}); });
this.ctx.beginPath(); 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(); this.ctx.fill();
} }
drawCircle(at, radius, style) { drawCircle(at, radius, style) {
this.setStyle(style); this.setStyle(style);
this.ctx.beginPath(); 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(); this.ctx.stroke();
} }
fillCircle(at, radius, style) { fillCircle(at, radius, style) {
this.setStyle(style); this.setStyle(style);
this.ctx.beginPath(); 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(); this.ctx.fill();
} }
drawRect(at, width, height, style) { drawRect(at, width, height, style) {
@ -385,6 +392,12 @@ class Doodler {
cb(); cb();
this.ctx.restore(); 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) { setStyle(style) {
const ctx = this.ctx; const ctx = this.ctx;
ctx.fillStyle = style?.color || style?.fillColor || 'black'; ctx.fillStyle = style?.color || style?.fillColor || 'black';
@ -414,20 +427,40 @@ class Doodler {
} }
this.draggables = this.draggables.filter((d)=>d.point !== point); 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); const d = this.draggables.find((d)=>d.point === point);
if (d) { if (d) {
d.onDragEnd = onDragEnd; d.onDragEnd = onDragEnd;
d.onDragStart = onDragStart; d.onDragStart = onDragStart;
d.onDrag = onDrag;
} }
} }
onClick(e) { onClick(e) {
const mouse = new Vector(this.mouseX, this.mouseY);
for (const d of this.draggables){ 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.beingDragged = true;
d.onDragStart?.call(null); d.onDragStart?.call(null);
} else d.beingDragged = false; } else d.beingDragged = false;
} }
for (const c of this.clickables){
if (c.checkBound(mouse)) {
c.onClick();
}
}
} }
offClick(e) { offClick(e) {
for (const d of this.draggables){ for (const d of this.draggables){
@ -470,35 +503,79 @@ class Doodler {
this.uiElements.delete(id); this.uiElements.delete(id);
} }
} }
class ComplexPath { class Train {
points = []; nodes = [];
radius = 50; cars = [];
ctx; path;
constructor(points){ t;
points && (this.points = points); 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) { move() {
this.ctx = ctx; 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() { draw() {
if (!this.ctx || !this.points.length) return; if (!this.points) return;
const ctx = this.ctx; const [a, b] = this.points;
ctx.save(); const origin = Vector.add(Vector.sub(a, b).div(2), b);
ctx.lineWidth = 2; const angle = Vector.sub(b, a).heading();
ctx.strokeStyle = 'white'; doodler.drawCircle(origin, 4, {
ctx.setLineDash([ color: 'blue'
21, });
6 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));
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();
} }
} }
class PathSegment { class PathSegment {
@ -604,7 +681,7 @@ class PathSegment {
const current = stepSize * i; const current = stepSize * i;
points.push(this.getPointAtT(current)); points.push(this.getPointAtT(current));
} }
return points.reduce((acc, cur)=>{ this.length = points.reduce((acc, cur)=>{
const prev = acc.prev; const prev = acc.prev;
acc.prev = cur; acc.prev = cur;
if (!prev) return acc; if (!prev) return acc;
@ -614,6 +691,7 @@ class PathSegment {
prev: undefined, prev: undefined,
length: 0 length: 0
}).length; }).length;
return this.length;
} }
calculateEvenlySpacedPoints(spacing, resolution = 1) { calculateEvenlySpacedPoints(spacing, resolution = 1) {
const points = []; const points = [];
@ -638,254 +716,6 @@ class PathSegment {
return points; 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 { class Track extends PathSegment {
editable = false; editable = false;
next; next;
@ -897,38 +727,6 @@ class Track extends PathSegment {
this.next = next || this; this.next = next || this;
this.prev = prev || 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) { getNearestPoint(p) {
let [closest, closestDistance] = this.getClosestPoint(p); let [closest, closestDistance] = this.getClosestPoint(p);
if (this.next !== this) { if (this.next !== this) {
@ -953,8 +751,10 @@ class Track extends PathSegment {
} }
draw() { draw() {
super.draw(); super.draw();
if (this.editable) for (const e of this.points){ if (this.editable) {
e.drawDot(); const [a, b, c, d] = this.points;
doodler.line(a, b);
doodler.line(c, d);
} }
} }
setNext(t) { setNext(t) {
@ -970,9 +770,28 @@ class Spline {
segments = []; segments = [];
ctx; ctx;
evenPoints; evenPoints;
pointSpacing;
get points() {
return Array.from(new Set(this.segments.flatMap((s)=>s.points)));
}
nodes;
constructor(segs){ constructor(segs){
this.segments = segs; this.segments = segs;
this.pointSpacing = 1;
this.evenPoints = this.calculateEvenlySpacedPoints(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) { setContext(ctx) {
this.ctx = ctx; this.ctx = ctx;
@ -986,6 +805,7 @@ class Spline {
} }
} }
calculateEvenlySpacedPoints(spacing, resolution = 1) { calculateEvenlySpacedPoints(spacing, resolution = 1) {
this.pointSpacing = 1;
const points = []; const points = [];
points.push(this.segments[0].points[0]); points.push(this.segments[0].points[0]);
let prev = points[0]; let prev = points[0];
@ -1007,6 +827,7 @@ class Spline {
prev = point; prev = point;
} }
} }
this.evenPoints = points;
return points; return points;
} }
followEvenPoints(t) { followEvenPoints(t) {
@ -1014,10 +835,35 @@ class Spline {
const i = Math.floor(t); const i = Math.floor(t);
const a = this.evenPoints[i]; const a = this.evenPoints[i];
const b = this.evenPoints[(i + 1) % this.evenPoints.length]; 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 { calculateApproxLength() {
console.log(t, i, a, b); 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); 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({ init({
width: 400, width: 400,
height: 400, height: 400,
bg: '#333' bg: '#333'
}); });
const path = loadFromJson(); const path = loadFromJson();
let t = 0;
let speed = 1; 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(()=>{ doodler.createLayer(()=>{
path.draw(); for(let i = 0; i < path.evenPoints.length; i += 10){
const points = Array(5).fill(null).map((_, i)=>path.followEvenPoints(t - i * 15)); const p = path.evenPoints[i];
for (const point of points){ const next = path.evenPoints[(i + 1) % path.evenPoints.length];
point && doodler.drawCircle(point, 5, { const last = path.evenPoints.at(i - 1);
strokeColor: 'green' 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)=>{ document.addEventListener('keyup', (e)=>{
if (e.key === 'd') {} if (e.key === 'd') {}
if (e.key === 'ArrowUp') { if (e.key === 'ArrowUp') {
@ -1135,7 +1018,23 @@ document.addEventListener('keyup', (e)=>{
if (e.key === 'ArrowDown') { if (e.key === 'ArrowDown') {
speed -= .1; 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') { if (e.key === 'e') {
editable = !editable;
for (const t of path.segments){ for (const t of path.segments){
t.editable = !t.editable; t.editable = !t.editable;
for (const p of t.points){ for (const p of t.points){
@ -1144,12 +1043,30 @@ document.addEventListener('keyup', (e)=>{
doodler.addDragEvents({ doodler.addDragEvents({
point: p, point: p,
onDragEnd: ()=>{ onDragEnd: ()=>{
console.log('dragend'); dragEndCounter++;
t.length = t.calculateApproxLength(100); t.length = t.calculateApproxLength(100);
path.evenPoints = path.calculateEvenlySpacedPoints(1); 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": { "imports": {
"drawing": "./drawing/index.ts", "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 { lerp } from "./math/lerp.ts";
import { ComplexPath, PathSegment } from "./math/path.ts"; import { ComplexPath, PathSegment } from "./math/path.ts";
import { Mover } from "./physics/mover.ts"; import { Mover } from "./physics/mover.ts";
import { Train } from "./train.ts"; import { Train, TrainCar } from "./train.ts";
import { fillCircle, drawCircle } from 'drawing'; 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 { drawLine } from "./drawing/line.ts";
import { initializeDoodler, Vector } from 'doodler'; import { initializeDoodler, Vector } from 'doodler';
// for (const mover of trains) { const engineSprites = document.createElement('img');
// mover.setContext(ctx); engineSprites.src = './sprites/EngineSprites.png';
// mover.velocity.add(Vector.random2D()) engineSprites.style.display = 'none';
// } engineSprites.id = 'engine-sprites';
document.body.append(engineSprites);
initializeDoodler({ initializeDoodler({
width: 400, width: 400,
@ -33,43 +34,42 @@ let t = 0;
let currentSeg = 0; let currentSeg = 0;
let speed = 1; let speed = 1;
const trainCount = 1; // const trainCount = 1;
const trains = Array(trainCount).fill(null).map((_, i) => new Train(path.segments[i % path.segments.length], 5)); // 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(() => { 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) { doodler.drawRotated(p, tan.heading(), () => {
// train.move(); 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})
// ctx.strokeStyle = 'red'; doodler.line(p.copy().add(-6,-5), p.copy().add(6,-5), {color: 'grey', weight: 2})
// 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' })
} }
path.draw();
// const point = path.followEvenPoints(t); train.move();
t = (t + (speed / 2)) % path.evenPoints.length; selectedNode?.anchor.drawDot();
selectedNode?.controls.forEach(e => e.drawDot());
// path.segments.forEach(s => s.calculateApproxLength(10000))
}) })
let editable = false;
const clickables = new Map()
let selectedPoint: Vector;
document.addEventListener('keyup', e => { document.addEventListener('keyup', e => {
if (e.key === 'd') { if (e.key === 'd') {
// console.log(trains) // console.log(trains)
@ -90,7 +90,26 @@ document.addEventListener('keyup', e => {
speed -= .1 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') { if (e.key === 'e') {
editable = !editable;
for (const t of path.segments) { for (const t of path.segments) {
t.editable = !t.editable; t.editable = !t.editable;
for (const p of t.points) { for (const p of t.points) {
@ -99,14 +118,34 @@ document.addEventListener('keyup', e => {
doodler.addDragEvents({ doodler.addDragEvents({
point: p, point: p,
onDragEnd: () => { onDragEnd: () => {
console.log('dragend'); dragEndCounter++
t.length = t.calculateApproxLength(100) t.length = t.calculateApproxLength(100)
path.evenPoints = path.calculateEvenlySpacedPoints(1) path.evenPoints = path.calculateEvenlySpacedPoints(1)
},
onDrag: (movement) => {
// todo - remove ! after updating doodler
path.handleNodeEdit(p, movement!)
} }
}) })
} }
else else {
doodler.unregisterDraggable(p) 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; const current = stepSize * i;
points.push(this.getPointAtT(current)) 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; const prev = acc.prev;
acc.prev = cur; acc.prev = cur;
if (!prev) return acc; if (!prev) return acc;
acc.length += cur.dist(prev); acc.length += cur.dist(prev);
return acc; return acc;
}, { prev: undefined, length: 0 }).length }, { prev: undefined, length: 0 }).length
return this.length;
} }
calculateEvenlySpacedPoints(spacing: number, resolution = 1) { 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; this.prev = prev || this;
} }
followTrack(train: Train): [Vector, number] { // followTrack(train: Train): [Vector, number] {
const predict = train.velocity.copy(); // const predict = train.velocity.copy();
predict.normalize(); // predict.normalize();
predict.mult(1); // predict.mult(1);
const predictpos = Vector.add(train.position, predict) // const predictpos = Vector.add(train.position, predict)
// const leading = train.leadingPoint; // // const leading = train.leadingPoint;
// let closest = this.points[0]; // // let closest = this.points[0];
// let closestDistance = this.getClosestPoint(leading); // // let closestDistance = this.getClosestPoint(leading);
let [closest, closestDistance, closestT] = this.getClosestPoint(predictpos); // let [closest, closestDistance, closestT] = this.getClosestPoint(predictpos);
// deno-lint-ignore no-this-alias // // deno-lint-ignore no-this-alias
let mostValid: Track = this; // let mostValid: Track = this;
if (this.next !== this) { // if (this.next !== this) {
const [point, distance, t] = this.next.getClosestPoint(predictpos); // const [point, distance, t] = this.next.getClosestPoint(predictpos);
if (distance < closestDistance) { // if (distance < closestDistance) {
closest = point; // closest = point;
closestDistance = distance; // closestDistance = distance;
mostValid = this.next; // mostValid = this.next;
closestT = t; // closestT = t;
} // }
} // }
if (this.prev !== this) { // if (this.prev !== this) {
const [point, distance, t] = this.next.getClosestPoint(predictpos); // const [point, distance, t] = this.next.getClosestPoint(predictpos);
if (distance < closestDistance) { // if (distance < closestDistance) {
closest = point; // closest = point;
closestDistance = distance; // closestDistance = distance;
mostValid = this.next; // mostValid = this.next;
closestT = t; // closestT = t;
} // }
} // }
train.currentTrack = mostValid; // train.currentTrack = mostValid;
train.arrive(closest); // train.arrive(closest);
// if (predictpos.dist(closest) > 2) train.arrive(closest); // // if (predictpos.dist(closest) > 2) train.arrive(closest);
return [closest, closestT]; // return [closest, closestT];
} // }
getNearestPoint(p: Vector) { getNearestPoint(p: Vector) {
let [closest, closestDistance] = this.getClosestPoint(p); let [closest, closestDistance] = this.getClosestPoint(p);
@ -85,10 +85,11 @@ export class Track extends PathSegment {
draw(): void { draw(): void {
super.draw(); super.draw();
if (this.editable) if (this.editable) {
for (const e of this.points) { const [a, b, c, d] = this.points;
e.drawDot(); doodler.line(a, b);
} doodler.line(c, d);
}
} }
setNext(t: Track) { setNext(t: Track) {
@ -107,9 +108,28 @@ export class Spline<T extends PathSegment = PathSegment> {
ctx?: CanvasRenderingContext2D; ctx?: CanvasRenderingContext2D;
evenPoints: Vector[]; evenPoints: Vector[];
pointSpacing: number;
get points() {
return Array.from(new Set(this.segments.flatMap(s => s.points)));
}
nodes: IControlNode[];
constructor(segs: T[]) { constructor(segs: T[]) {
this.segments = segs; this.segments = segs;
this.pointSpacing = 1;
this.evenPoints = this.calculateEvenlySpacedPoints(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) { setContext(ctx: CanvasRenderingContext2D) {
@ -126,6 +146,7 @@ export class Spline<T extends PathSegment = PathSegment> {
} }
calculateEvenlySpacedPoints(spacing: number, resolution = 1) { calculateEvenlySpacedPoints(spacing: number, resolution = 1) {
this.pointSpacing = 1;
// return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution)); // return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution));
const points: Vector[] = [] const points: Vector[] = []
@ -155,20 +176,51 @@ export class Spline<T extends PathSegment = PathSegment> {
} }
} }
this.evenPoints = points;
return points; return points;
} }
followEvenPoints(t: number) { followEvenPoints(t: number) {
if (t < 0) t+= this.evenPoints.length if (t < 0) t += this.evenPoints.length
const i = Math.floor(t); const i = Math.floor(t);
const a = this.evenPoints[i] const a = this.evenPoints[i]
const b = this.evenPoints[(i + 1) % this.evenPoints.length] 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 { calculateApproxLength() {
console.log(t, i, a, b); 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(); if (!json) return generateSquareTrack();
const segments: Track[] = []; const segments: Track[] = [];
for (const {points} of json.segments) { for (const { points } of json.segments) {
segments.push(new Track(points.map((p:{x:number,y:number}) => new Vector(p.x, p.y)))); segments.push(new Track(points.map((p: { x: number, y: number }) => new Vector(p.x, p.y))));
} }
for (const [i,s] of segments.entries()) { for (const [i, s] of segments.entries()) {
s.setNext(segments[(i+1)%segments.length]) s.setNext(segments[(i + 1) % segments.length])
s.setPrev(segments.at(i-1)!) s.setPrev(segments.at(i - 1)!)
} }
return new Spline<Track>(segments); 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 { Vector } from "doodler";
import { Follower } from "./physics/follower.ts"; import { Follower } from "./physics/follower.ts";
import { Mover } from "./physics/mover.ts"; import { Mover } from "./physics/mover.ts";
import { Track } from "./track.ts"; import { Spline, Track } from "./track.ts";
export class Train extends Follower { export class Train {
nodes?: Vector[]; nodes: Vector[] = [];
currentTrack: Track; cars: TrainCar[] = [];
speed: number; path: Spline<Track>;
t: number;
follower?: TrainCar; engineLength = 40;
spacing = 30;
followers?: TrainCar[]; constructor(track: Spline<Track>, cars: TrainCar[] = []) {
this.path = track;
constructor(track: Track, length: number) { this.t = 0;
super(track.points[0].copy()); this.nodes.push(this.path.followEvenPoints(this.t),)
this.maxSpeed = 2; this.nodes.push(this.path.followEvenPoints(this.t - this.real2Track(40)));
this.speed = 1; this.cars.push(new TrainCar(55, document.getElementById('engine-sprites')! as HTMLImageElement, 80, 20, { at: new Vector(0, 60), width: 80, height: 20 }));
this.currentTrack = track; this.cars[0].points = this.nodes.map(n => n) as [Vector, Vector];
this.velocity = this.currentTrack.tangent(0).normalize().mult(this.maxSpeed); let currentOffset = 40;
for (const car of cars) {
this.addCar(length); currentOffset += this.spacing;
const a = this.path.followEvenPoints(this.t - currentOffset);
this.maxForce = .2; currentOffset += car.length;
} const b = this.path.followEvenPoints(this.t - currentOffset);
car.points = [a,b];
init(): void { this.cars.push(car);
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 { move() {
// if (!this.ctx) return; this.t = (this.t + 1) % this.path.evenPoints.length;
// super.draw() let currentOffset = 0;
// this.ctx.fillStyle = 'red'; for (const car of this.cars) {
// this.position.drawDot(this.ctx); if (!car.points) return;
// this.ctx.fillStyle = 'green'; const [a,b] = car.points;
// this.target?.trailingPoint.drawDot(this.ctx); 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; points?: [Vector, Vector];
// cars: TrainCar[] = []; length: number;
// id: string; constructor(length: number, img: HTMLImageElement, w: number, h: number, sprite?: ISprite) {
// constructor(path: Track); this.img = img;
// constructor(x: number, y: number, segment: Track); this.sprite = sprite;
// constructor(x: number | Track, y?: number, segment?: Track) { 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) { doodler.drawCircle(origin, 4, {color: 'blue'})
// this.currentSegment = x;
// } else if (segment) { doodler.drawRotated(origin, angle, () => {
// this.currentSegment = segment; this.sprite ?
// } else { 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) :
// throw new Error('Path not provided for train construction') doodler.drawImage(this.img, origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2));
// } })
// // 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; interface ISprite {
// this.maxForce = .3; at: Vector;
width: number;
// this.addCar(); height: number;
}
// 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;
// }
// }