one step forward, one step back
This commit is contained in:
345
bundle.js
345
bundle.js
@@ -1213,6 +1213,8 @@
|
||||
points;
|
||||
length;
|
||||
startingLength;
|
||||
next;
|
||||
prev;
|
||||
constructor(points) {
|
||||
this.points = points;
|
||||
this.length = this.calculateApproxLength(100);
|
||||
@@ -1382,6 +1384,8 @@
|
||||
this.points[3].set(points[curveLength]);
|
||||
}
|
||||
}
|
||||
draw() {
|
||||
}
|
||||
};
|
||||
|
||||
// track/system.ts
|
||||
@@ -1458,6 +1462,7 @@
|
||||
const neighborMap = /* @__PURE__ */ new Map();
|
||||
for (const segment of data) {
|
||||
track.segments.set(segment.id, TrackSegment.deserialize(segment));
|
||||
neighborMap.set(segment.id, [segment.fNeighbors, segment.bNeighbors]);
|
||||
}
|
||||
for (const segment of track.segments.values()) {
|
||||
segment.setTrack(track);
|
||||
@@ -1471,6 +1476,7 @@
|
||||
).filter((s) => s);
|
||||
}
|
||||
}
|
||||
console.log(track.segments);
|
||||
return track;
|
||||
}
|
||||
translate(v) {
|
||||
@@ -1478,6 +1484,54 @@
|
||||
segment.translate(v);
|
||||
}
|
||||
}
|
||||
_path;
|
||||
get path() {
|
||||
if (!this._path) {
|
||||
this._path = this.generatePath();
|
||||
}
|
||||
return this._path;
|
||||
}
|
||||
generatePath() {
|
||||
if (!this.firstSegment) throw new Error("No first segment");
|
||||
const rightOnlyPath = [
|
||||
this.firstSegment.copy(),
|
||||
...this.findRightPath(this.firstSegment)
|
||||
];
|
||||
rightOnlyPath.forEach((s, i, arr) => {
|
||||
if (i === 0) return;
|
||||
const prev = arr[i - 1];
|
||||
s.points[0] = prev.points[3];
|
||||
s.prev = prev;
|
||||
prev.next = s;
|
||||
});
|
||||
console.log(rightOnlyPath);
|
||||
return new Spline(rightOnlyPath);
|
||||
}
|
||||
*findRightPath(start) {
|
||||
if (start.frontNeighbours.length === 0) {
|
||||
yield start;
|
||||
return;
|
||||
}
|
||||
let rightMost = start.frontNeighbours[0].copy();
|
||||
for (const segment of start.frontNeighbours) {
|
||||
if (segment.id === rightMost.id) continue;
|
||||
const rotatedSegment = segment.copy();
|
||||
rotatedSegment.rotateAboutPoint(
|
||||
rotatedSegment.tangent(0).heading(),
|
||||
rotatedSegment.points[0]
|
||||
);
|
||||
const rotatedRightMost = rightMost.copy();
|
||||
rotatedRightMost.rotateAboutPoint(
|
||||
rotatedRightMost.tangent(0).heading(),
|
||||
rotatedRightMost.points[0]
|
||||
);
|
||||
if (rotatedSegment.points[3].y > rotatedRightMost.points[3].y) {
|
||||
rightMost = segment;
|
||||
}
|
||||
}
|
||||
yield rightMost.copy();
|
||||
yield* this.findRightPath(rightMost);
|
||||
}
|
||||
};
|
||||
var TrackSegment = class _TrackSegment extends PathSegment {
|
||||
frontNeighbours = [];
|
||||
@@ -1606,6 +1660,112 @@
|
||||
});
|
||||
}
|
||||
};
|
||||
var Spline = class {
|
||||
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: CanvasRenderingContext2D) {
|
||||
// this.ctx = ctx;
|
||||
// for (const segment of this.segments) {
|
||||
// segment.setContext(ctx);
|
||||
// }
|
||||
// }
|
||||
draw() {
|
||||
for (const segment of this.segments) {
|
||||
segment.draw();
|
||||
}
|
||||
}
|
||||
calculateEvenlySpacedPoints(spacing, resolution = 1) {
|
||||
this.pointSpacing = 1;
|
||||
const points = [];
|
||||
points.push(this.segments[0].points[0]);
|
||||
let prev = points[0];
|
||||
let distSinceLastEvenPoint = 0;
|
||||
for (const seg of this.segments) {
|
||||
let t = 0;
|
||||
const div = Math.ceil(seg.length * resolution * 10);
|
||||
while (t < 1) {
|
||||
t += 1 / div;
|
||||
const point = seg.getPointAtT(t);
|
||||
distSinceLastEvenPoint += prev.dist(point);
|
||||
if (distSinceLastEvenPoint >= spacing) {
|
||||
const overshoot = distSinceLastEvenPoint - spacing;
|
||||
const evenPoint = Vector.add(
|
||||
point,
|
||||
Vector.sub(point, prev).normalize().mult(overshoot)
|
||||
);
|
||||
distSinceLastEvenPoint = overshoot;
|
||||
points.push(evenPoint);
|
||||
prev = evenPoint;
|
||||
}
|
||||
prev = point;
|
||||
}
|
||||
}
|
||||
this.evenPoints = points;
|
||||
return points;
|
||||
}
|
||||
followEvenPoints(t) {
|
||||
if (t < 0) t += this.evenPoints.length;
|
||||
const i = Math.floor(t) % this.evenPoints.length;
|
||||
const a = this.evenPoints[i];
|
||||
const b = this.evenPoints[(i + 1) % this.evenPoints.length];
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// track/shapes.ts
|
||||
var StraightTrack = class extends TrackSegment {
|
||||
@@ -1635,20 +1795,50 @@
|
||||
this.points[3].add(0, 25);
|
||||
}
|
||||
};
|
||||
var BankLeft = class extends StraightTrack {
|
||||
var BankLeft = class extends TrackSegment {
|
||||
constructor(start) {
|
||||
start = start || new Vector(100, 100);
|
||||
super(start);
|
||||
this.points[2].add(0, -25);
|
||||
this.points[3].add(0, 25);
|
||||
const p1 = start.copy();
|
||||
const p2 = start.copy();
|
||||
const p3 = start.copy();
|
||||
const p4 = start.copy();
|
||||
const scale = 33;
|
||||
p2.add(new Vector(1, 0).mult(scale));
|
||||
p3.set(p2);
|
||||
const dirToP3 = Vector.fromAngle(-Math.PI / 12).mult(scale);
|
||||
p3.add(dirToP3);
|
||||
p4.set(p3);
|
||||
const dirToP4 = Vector.fromAngle(-Math.PI / 6).mult(scale);
|
||||
p4.add(dirToP4);
|
||||
super([
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
p4
|
||||
]);
|
||||
}
|
||||
};
|
||||
var BankRight = class extends StraightTrack {
|
||||
var BankRight = class extends TrackSegment {
|
||||
constructor(start) {
|
||||
start = start || new Vector(100, 100);
|
||||
super(start);
|
||||
this.points[2].add(0, 25);
|
||||
this.points[3].add(0, -25);
|
||||
const p1 = start.copy();
|
||||
const p2 = start.copy();
|
||||
const p3 = start.copy();
|
||||
const p4 = start.copy();
|
||||
const scale = 33;
|
||||
p2.add(new Vector(1, 0).mult(scale));
|
||||
p3.set(p2);
|
||||
const dirToP3 = Vector.fromAngle(Math.PI / 12).mult(scale);
|
||||
p3.add(dirToP3);
|
||||
p4.set(p3);
|
||||
const dirToP4 = Vector.fromAngle(Math.PI / 6).mult(scale);
|
||||
p4.add(dirToP4);
|
||||
super([
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
p4
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1881,6 +2071,133 @@
|
||||
}
|
||||
};
|
||||
|
||||
// train/train.ts
|
||||
var Train = class {
|
||||
nodes = [];
|
||||
cars = [];
|
||||
path;
|
||||
t;
|
||||
engineLength = 40;
|
||||
spacing = 30;
|
||||
speed = 10;
|
||||
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)));
|
||||
const resources2 = getContextItem("resources");
|
||||
const engineSprites = resources2.get("engine-sprites");
|
||||
console.log(engineSprites);
|
||||
this.cars = cars;
|
||||
this.cars[0].points = this.nodes.map((n) => n);
|
||||
this.cars[1].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);
|
||||
}
|
||||
}
|
||||
move(dTime) {
|
||||
this.t = (this.t + this.speed * dTime * 10) % 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;
|
||||
}
|
||||
}
|
||||
// draw() {
|
||||
// const doodler = getContextItem<Doodler>("doodler");
|
||||
// this.path.draw();
|
||||
// for (const [i, node] of this.nodes.entries()) {
|
||||
// // doodler.drawCircle(node, 10, { color: "purple", weight: 3 });
|
||||
// doodler.fillCircle(node, 2, { color: "purple" });
|
||||
// // 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))
|
||||
// // }
|
||||
// }
|
||||
// }
|
||||
draw() {
|
||||
for (const car of this.cars) {
|
||||
car.draw();
|
||||
}
|
||||
}
|
||||
real2Track(length) {
|
||||
return length / this.path.pointSpacing;
|
||||
}
|
||||
};
|
||||
var TrainCar = class {
|
||||
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.points) return;
|
||||
const doodler2 = getContextItem("doodler");
|
||||
const [a, b] = this.points;
|
||||
const origin = Vector.add(Vector.sub(a, b).div(2), b);
|
||||
const angle = Vector.sub(b, a).heading();
|
||||
doodler2.drawCircle(origin, 4, { color: "blue" });
|
||||
doodler2.drawRotated(origin, angle, () => {
|
||||
this.sprite ? doodler2.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
|
||||
) : doodler2.drawImage(
|
||||
this.img,
|
||||
origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2)
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// train/cars.ts
|
||||
var Tender = class extends TrainCar {
|
||||
constructor() {
|
||||
const resources2 = getContextItem("resources");
|
||||
super(25, resources2.get("engine-sprites"), 40, 20, {
|
||||
at: new Vector(80, 0),
|
||||
width: 40,
|
||||
height: 20
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// train/engines.ts
|
||||
var RedEngine = class extends TrainCar {
|
||||
constructor() {
|
||||
const resources2 = getContextItem("resources");
|
||||
super(55, resources2.get("engine-sprites"), 80, 20, {
|
||||
at: new Vector(0, 60),
|
||||
width: 80,
|
||||
height: 20
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// state/states/RunningState.ts
|
||||
var RunningState = class extends State {
|
||||
name = 1 /* RUNNING */;
|
||||
@@ -1892,10 +2209,18 @@
|
||||
const ctx2 = getContext();
|
||||
ctx2.track.draw();
|
||||
for (const train of ctx2.trains) {
|
||||
train.move(dt);
|
||||
train.draw();
|
||||
}
|
||||
}
|
||||
start() {
|
||||
const inputManager2 = getContextItem("inputManager");
|
||||
const track = getContextItem("track");
|
||||
const ctx2 = getContext();
|
||||
inputManager2.onKey(" ", () => {
|
||||
const train = new Train(track.path, [new RedEngine(), new Tender()]);
|
||||
ctx2.trains.push(train);
|
||||
});
|
||||
}
|
||||
stop() {
|
||||
}
|
||||
@@ -1928,7 +2253,11 @@
|
||||
const inputManager2 = new InputManager();
|
||||
setContextItem("inputManager", inputManager2);
|
||||
bootstrapInputs();
|
||||
resources2.set("engine-sprites", new Image());
|
||||
resources2.get("engine-sprites").src = "/sprites/EngineSprites.png";
|
||||
resources2.ready().then(() => {
|
||||
this.stateMachine.transitionTo(1 /* RUNNING */);
|
||||
});
|
||||
}
|
||||
stop() {
|
||||
}
|
||||
|
53
math/path.ts
53
math/path.ts
@@ -2,10 +2,12 @@ import { Vector } from "@bearmetal/doodler";
|
||||
|
||||
export class ComplexPath {
|
||||
points: Vector[] = [];
|
||||
segments: PathSegment[] = [];
|
||||
|
||||
radius = 50;
|
||||
|
||||
ctx?: CanvasRenderingContext2D;
|
||||
evenPoints: Vector[] = [];
|
||||
|
||||
constructor(points?: Vector[]) {
|
||||
points && (this.points = points);
|
||||
@@ -35,6 +37,52 @@ export class ComplexPath {
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
followEvenPoints(t: number) {
|
||||
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];
|
||||
|
||||
return Vector.lerp(a, b, t % 1);
|
||||
}
|
||||
|
||||
calculateEvenlySpacedPoints(spacing: number, resolution = 1) {
|
||||
// this.pointSpacing = 1;
|
||||
// return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution));
|
||||
const points: Vector[] = [];
|
||||
|
||||
points.push(this.segments[0].points[0]);
|
||||
let prev = points[0];
|
||||
let distSinceLastEvenPoint = 0;
|
||||
for (const seg of this.segments) {
|
||||
let t = 0;
|
||||
|
||||
const div = Math.ceil(seg.length * resolution * 10);
|
||||
while (t < 1) {
|
||||
t += 1 / div;
|
||||
const point = seg.getPointAtT(t);
|
||||
distSinceLastEvenPoint += prev.dist(point);
|
||||
|
||||
if (distSinceLastEvenPoint >= spacing) {
|
||||
const overshoot = distSinceLastEvenPoint - spacing;
|
||||
const evenPoint = Vector.add(
|
||||
point,
|
||||
Vector.sub(point, prev).normalize().mult(overshoot),
|
||||
);
|
||||
distSinceLastEvenPoint = overshoot;
|
||||
points.push(evenPoint);
|
||||
prev = evenPoint;
|
||||
}
|
||||
|
||||
prev = point;
|
||||
}
|
||||
}
|
||||
|
||||
this.evenPoints = points;
|
||||
|
||||
return points;
|
||||
}
|
||||
}
|
||||
|
||||
export class PathSegment {
|
||||
@@ -43,6 +91,9 @@ export class PathSegment {
|
||||
length: number;
|
||||
startingLength: number;
|
||||
|
||||
next?: PathSegment;
|
||||
prev?: PathSegment;
|
||||
|
||||
constructor(points: [Vector, Vector, Vector, Vector]) {
|
||||
this.points = points;
|
||||
this.length = this.calculateApproxLength(100);
|
||||
@@ -254,4 +305,6 @@ export class PathSegment {
|
||||
this.points[3].set(points[curveLength]);
|
||||
}
|
||||
}
|
||||
|
||||
draw(): void {}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { lerp } from "./math/lerp.ts";
|
||||
import { ComplexPath, PathSegment } from "./math/path.ts";
|
||||
import { Mover } from "./physics/mover.ts";
|
||||
import { Train, TrainCar } from "./train/train.ts";
|
||||
import { Train, TrainCar } from "./train/train.old.ts";
|
||||
import { generateSquareTrack, IControlNode, loadFromJson } from "./track.ts";
|
||||
import { drawLine } from "./drawing/line.ts";
|
||||
import { initializeDoodler, Vector } from "doodler";
|
||||
|
@@ -31,7 +31,12 @@ export class LoadState extends State<States> {
|
||||
|
||||
bootstrapInputs();
|
||||
|
||||
resources.set("engine-sprites", new Image());
|
||||
resources.get<HTMLImageElement>("engine-sprites")!.src =
|
||||
"/sprites/EngineSprites.png";
|
||||
resources.ready().then(() => {
|
||||
this.stateMachine.transitionTo(States.RUNNING);
|
||||
});
|
||||
}
|
||||
override stop(): void {
|
||||
// noop
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { getContext } from "../../lib/context.ts";
|
||||
import { getContext, getContextItem } from "../../lib/context.ts";
|
||||
import { InputManager } from "../../lib/input.ts";
|
||||
import { TrackSystem } from "../../track/system.ts";
|
||||
import { Tender } from "../../train/cars.ts";
|
||||
import { RedEngine } from "../../train/engines.ts";
|
||||
import { Train } from "../../train/train.ts";
|
||||
import { State } from "../machine.ts";
|
||||
import { States } from "./index.ts";
|
||||
@@ -19,12 +22,20 @@ export class RunningState extends State<States> {
|
||||
// Draw (maybe via a layer system that syncs with doodler)
|
||||
ctx.track.draw();
|
||||
for (const train of ctx.trains) {
|
||||
train.move(dt);
|
||||
train.draw();
|
||||
}
|
||||
// Monitor world events
|
||||
}
|
||||
override start(): void {
|
||||
// noop
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
const track = getContextItem<TrackSystem>("track");
|
||||
const ctx = getContext() as { trains: Train[] };
|
||||
inputManager.onKey(" ", () => {
|
||||
const train = new Train(track.path, [new RedEngine(), new Tender()]);
|
||||
ctx.trains.push(train);
|
||||
});
|
||||
}
|
||||
override stop(): void {
|
||||
// noop
|
||||
|
@@ -30,19 +30,55 @@ export class SBendRight extends StraightTrack {
|
||||
}
|
||||
}
|
||||
|
||||
export class BankLeft extends StraightTrack {
|
||||
export class BankLeft extends TrackSegment {
|
||||
constructor(start?: Vector) {
|
||||
start = start || new Vector(100, 100);
|
||||
super(start);
|
||||
this.points[2].add(0, -25);
|
||||
this.points[3].add(0, 25);
|
||||
|
||||
const p1 = start.copy();
|
||||
const p2 = start.copy();
|
||||
const p3 = start.copy();
|
||||
const p4 = start.copy();
|
||||
const scale = 33;
|
||||
|
||||
p2.add(new Vector(1, 0).mult(scale));
|
||||
p3.set(p2);
|
||||
const dirToP3 = Vector.fromAngle(-Math.PI / 12).mult(scale);
|
||||
p3.add(dirToP3);
|
||||
p4.set(p3);
|
||||
const dirToP4 = Vector.fromAngle(-Math.PI / 6).mult(scale);
|
||||
p4.add(dirToP4);
|
||||
|
||||
super([
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
p4,
|
||||
]);
|
||||
}
|
||||
}
|
||||
export class BankRight extends StraightTrack {
|
||||
export class BankRight extends TrackSegment {
|
||||
constructor(start?: Vector) {
|
||||
start = start || new Vector(100, 100);
|
||||
super(start);
|
||||
this.points[2].add(0, 25);
|
||||
this.points[3].add(0, -25);
|
||||
|
||||
const p1 = start.copy();
|
||||
const p2 = start.copy();
|
||||
const p3 = start.copy();
|
||||
const p4 = start.copy();
|
||||
const scale = 33;
|
||||
|
||||
p2.add(new Vector(1, 0).mult(scale));
|
||||
p3.set(p2);
|
||||
const dirToP3 = Vector.fromAngle(Math.PI / 12).mult(scale);
|
||||
p3.add(dirToP3);
|
||||
p4.set(p3);
|
||||
const dirToP4 = Vector.fromAngle(Math.PI / 6).mult(scale);
|
||||
p4.add(dirToP4);
|
||||
|
||||
super([
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
p4,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
200
track/system.ts
200
track/system.ts
@@ -1,5 +1,5 @@
|
||||
import { Doodler, Point, Vector } from "@bearmetal/doodler";
|
||||
import { PathSegment } from "../math/path.ts";
|
||||
import { ComplexPath, PathSegment } from "../math/path.ts";
|
||||
import { getContextItem, setDefaultContext } from "../lib/context.ts";
|
||||
|
||||
export class TrackSystem {
|
||||
@@ -103,12 +103,13 @@ export class TrackSystem {
|
||||
return track;
|
||||
}
|
||||
|
||||
static deserialize(data: any) {
|
||||
static deserialize(data: SerializedTrackSegment[]) {
|
||||
if (data.length === 0) return undefined;
|
||||
const track = new TrackSystem([]);
|
||||
const neighborMap = new Map<string, [string[], string[]]>();
|
||||
for (const segment of data) {
|
||||
track.segments.set(segment.id, TrackSegment.deserialize(segment));
|
||||
neighborMap.set(segment.id, [segment.fNeighbors, segment.bNeighbors]);
|
||||
}
|
||||
for (const segment of track.segments.values()) {
|
||||
segment.setTrack(track);
|
||||
@@ -122,6 +123,7 @@ export class TrackSystem {
|
||||
).filter((s) => s) as TrackSegment[];
|
||||
}
|
||||
}
|
||||
console.log(track.segments);
|
||||
return track;
|
||||
}
|
||||
|
||||
@@ -130,6 +132,61 @@ export class TrackSystem {
|
||||
segment.translate(v);
|
||||
}
|
||||
}
|
||||
|
||||
private _path?: Spline<TrackSegment>;
|
||||
|
||||
get path() {
|
||||
if (!this._path) {
|
||||
this._path = this.generatePath();
|
||||
}
|
||||
return this._path;
|
||||
}
|
||||
|
||||
generatePath() {
|
||||
if (!this.firstSegment) throw new Error("No first segment");
|
||||
const rightOnlyPath = [
|
||||
this.firstSegment.copy(),
|
||||
...this.findRightPath(this.firstSegment),
|
||||
];
|
||||
|
||||
rightOnlyPath.forEach((s, i, arr) => {
|
||||
if (i === 0) return;
|
||||
const prev = arr[i - 1];
|
||||
s.points[0] = prev.points[3];
|
||||
s.prev = prev;
|
||||
prev.next = s;
|
||||
});
|
||||
|
||||
console.log(rightOnlyPath);
|
||||
|
||||
return new Spline<TrackSegment>(rightOnlyPath);
|
||||
}
|
||||
|
||||
*findRightPath(start: TrackSegment): Generator<TrackSegment> {
|
||||
if (start.frontNeighbours.length === 0) {
|
||||
yield start;
|
||||
return;
|
||||
}
|
||||
let rightMost = start.frontNeighbours[0].copy();
|
||||
for (const segment of start.frontNeighbours) {
|
||||
if (segment.id === rightMost.id) continue;
|
||||
const rotatedSegment = segment.copy();
|
||||
rotatedSegment.rotateAboutPoint(
|
||||
rotatedSegment.tangent(0).heading(),
|
||||
rotatedSegment.points[0],
|
||||
);
|
||||
const rotatedRightMost = rightMost.copy();
|
||||
rotatedRightMost.rotateAboutPoint(
|
||||
rotatedRightMost.tangent(0).heading(),
|
||||
rotatedRightMost.points[0],
|
||||
);
|
||||
if (rotatedSegment.points[3].y > rotatedRightMost.points[3].y) {
|
||||
rightMost = segment;
|
||||
}
|
||||
}
|
||||
yield rightMost.copy();
|
||||
yield* this.findRightPath(rightMost);
|
||||
}
|
||||
}
|
||||
|
||||
type VectorSet = [Vector, Vector, Vector, Vector];
|
||||
@@ -154,7 +211,7 @@ export class TrackSegment extends PathSegment {
|
||||
this.track = t;
|
||||
}
|
||||
|
||||
draw(showControls = false) {
|
||||
override draw(showControls = false) {
|
||||
this.doodler.drawBezier(
|
||||
this.points[0],
|
||||
this.points[1],
|
||||
@@ -180,7 +237,7 @@ export class TrackSegment extends PathSegment {
|
||||
}
|
||||
}
|
||||
|
||||
serialize() {
|
||||
serialize(): SerializedTrackSegment {
|
||||
return {
|
||||
p: this.points.map((p) => p.array()),
|
||||
id: this.id,
|
||||
@@ -278,3 +335,138 @@ export class TrackSegment extends PathSegment {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class Spline<T extends PathSegment = PathSegment> {
|
||||
segments: T[] = [];
|
||||
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) {
|
||||
// this.ctx = ctx;
|
||||
// for (const segment of this.segments) {
|
||||
// segment.setContext(ctx);
|
||||
// }
|
||||
// }
|
||||
|
||||
draw() {
|
||||
for (const segment of this.segments) {
|
||||
segment.draw();
|
||||
}
|
||||
}
|
||||
|
||||
calculateEvenlySpacedPoints(spacing: number, resolution = 1) {
|
||||
this.pointSpacing = 1;
|
||||
// return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution));
|
||||
const points: Vector[] = [];
|
||||
|
||||
points.push(this.segments[0].points[0]);
|
||||
let prev = points[0];
|
||||
let distSinceLastEvenPoint = 0;
|
||||
for (const seg of this.segments) {
|
||||
let t = 0;
|
||||
|
||||
const div = Math.ceil(seg.length * resolution * 10);
|
||||
while (t < 1) {
|
||||
t += 1 / div;
|
||||
const point = seg.getPointAtT(t);
|
||||
distSinceLastEvenPoint += prev.dist(point);
|
||||
|
||||
if (distSinceLastEvenPoint >= spacing) {
|
||||
const overshoot = distSinceLastEvenPoint - spacing;
|
||||
const evenPoint = Vector.add(
|
||||
point,
|
||||
Vector.sub(point, prev).normalize().mult(overshoot),
|
||||
);
|
||||
distSinceLastEvenPoint = overshoot;
|
||||
points.push(evenPoint);
|
||||
prev = evenPoint;
|
||||
}
|
||||
|
||||
prev = point;
|
||||
}
|
||||
}
|
||||
|
||||
this.evenPoints = points;
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
followEvenPoints(t: number) {
|
||||
if (t < 0) t += this.evenPoints.length;
|
||||
const i = Math.floor(t) % this.evenPoints.length;
|
||||
const a = this.evenPoints[i];
|
||||
const b = this.evenPoints[(i + 1) % this.evenPoints.length];
|
||||
|
||||
return Vector.lerp(a, b, t % 1);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IControlNode {
|
||||
anchor: Vector;
|
||||
controls: [Vector, Vector];
|
||||
tangent: boolean;
|
||||
mirrored: boolean;
|
||||
}
|
||||
|
55
train/cars.ts
Normal file
55
train/cars.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Vector } from "https://jsr.io/@bearmetal/doodler/0.0.4/geometry/vector.ts";
|
||||
import { TrainCar } from "./train.ts";
|
||||
import { ResourceManager } from "../lib/resources.ts";
|
||||
import { getContextItem } from "../lib/context.ts";
|
||||
|
||||
export class Tender extends TrainCar {
|
||||
constructor() {
|
||||
const resources = getContextItem<ResourceManager>("resources");
|
||||
super(25, resources.get<HTMLImageElement>("engine-sprites")!, 40, 20, {
|
||||
at: new Vector(80, 0),
|
||||
width: 40,
|
||||
height: 20,
|
||||
});
|
||||
}
|
||||
}
|
||||
export class Tank extends TrainCar {
|
||||
constructor() {
|
||||
const resources = getContextItem<ResourceManager>("resources");
|
||||
super(50, resources.get<HTMLImageElement>("engine-sprites")!, 70, 20, {
|
||||
at: new Vector(80, 20),
|
||||
width: 70,
|
||||
height: 20,
|
||||
});
|
||||
}
|
||||
}
|
||||
export class YellowDumpCar extends TrainCar {
|
||||
constructor() {
|
||||
const resources = getContextItem<ResourceManager>("resources");
|
||||
super(50, resources.get<HTMLImageElement>("engine-sprites")!, 70, 20, {
|
||||
at: new Vector(80, 40),
|
||||
width: 70,
|
||||
height: 20,
|
||||
});
|
||||
}
|
||||
}
|
||||
export class GrayDumpCar extends TrainCar {
|
||||
constructor() {
|
||||
const resources = getContextItem<ResourceManager>("resources");
|
||||
super(50, resources.get<HTMLImageElement>("engine-sprites")!, 70, 20, {
|
||||
at: new Vector(80, 60),
|
||||
width: 70,
|
||||
height: 20,
|
||||
});
|
||||
}
|
||||
}
|
||||
export class NullCar extends TrainCar {
|
||||
constructor() {
|
||||
const resources = getContextItem<ResourceManager>("resources");
|
||||
super(50, resources.get<HTMLImageElement>("engine-sprites")!, 70, 20, {
|
||||
at: new Vector(80, 80),
|
||||
width: 70,
|
||||
height: 20,
|
||||
});
|
||||
}
|
||||
}
|
55
train/engines.ts
Normal file
55
train/engines.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Vector } from "@bearmetal/doodler";
|
||||
import { TrainCar } from "./train.ts";
|
||||
import { getContextItem } from "../lib/context.ts";
|
||||
import { ResourceManager } from "../lib/resources.ts";
|
||||
|
||||
export class RedEngine extends TrainCar {
|
||||
constructor() {
|
||||
const resources = getContextItem<ResourceManager>("resources");
|
||||
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
|
||||
at: new Vector(0, 60),
|
||||
width: 80,
|
||||
height: 20,
|
||||
});
|
||||
}
|
||||
}
|
||||
export class PurpleEngine extends TrainCar {
|
||||
constructor() {
|
||||
const resources = getContextItem<ResourceManager>("resources");
|
||||
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
|
||||
at: new Vector(0, 60),
|
||||
width: 80,
|
||||
height: 20,
|
||||
});
|
||||
}
|
||||
}
|
||||
export class GreenEngine extends TrainCar {
|
||||
constructor() {
|
||||
const resources = getContextItem<ResourceManager>("resources");
|
||||
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
|
||||
at: new Vector(0, 40),
|
||||
width: 80,
|
||||
height: 20,
|
||||
});
|
||||
}
|
||||
}
|
||||
export class GrayEngine extends TrainCar {
|
||||
constructor() {
|
||||
const resources = getContextItem<ResourceManager>("resources");
|
||||
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
|
||||
at: new Vector(0, 20),
|
||||
width: 80,
|
||||
height: 20,
|
||||
});
|
||||
}
|
||||
}
|
||||
export class BlueEngine extends TrainCar {
|
||||
constructor() {
|
||||
const resources = getContextItem<ResourceManager>("resources");
|
||||
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
|
||||
at: new Vector(0, 0),
|
||||
width: 80,
|
||||
height: 20,
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,47 +1,49 @@
|
||||
import { ComplexPath, PathSegment } from "../math/path.ts";
|
||||
import { Follower } from "../physics/follower.ts";
|
||||
import { Mover } from "../physics/mover.ts";
|
||||
import { Spline, Track } from "../track.ts";
|
||||
import { getContextItem } from "../lib/context.ts";
|
||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||
import { Spline, TrackSegment } from "../track/system.ts";
|
||||
import { ResourceManager } from "../lib/resources.ts";
|
||||
|
||||
export class Train {
|
||||
nodes: Vector[] = [];
|
||||
|
||||
cars: TrainCar[] = [];
|
||||
|
||||
path: Spline<Track>;
|
||||
path: Spline<TrackSegment>;
|
||||
t: number;
|
||||
|
||||
engineLength = 40;
|
||||
spacing = 30;
|
||||
|
||||
speed = 0;
|
||||
speed = 10;
|
||||
|
||||
constructor(track: Spline<Track>, cars: TrainCar[] = []) {
|
||||
constructor(track: Spline<TrackSegment>, 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)));
|
||||
const engineSprites = document.getElementById(
|
||||
"engine-sprites",
|
||||
)! as HTMLImageElement;
|
||||
this.cars.push(
|
||||
new TrainCar(
|
||||
55,
|
||||
engineSprites,
|
||||
80,
|
||||
20,
|
||||
{ at: new Vector(0, 60), width: 80, height: 20 },
|
||||
),
|
||||
new TrainCar(
|
||||
25,
|
||||
engineSprites,
|
||||
40,
|
||||
20,
|
||||
{ at: new Vector(80, 0), width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
const resources = getContextItem<ResourceManager>("resources");
|
||||
const engineSprites = resources.get<HTMLImageElement>("engine-sprites")!;
|
||||
console.log(engineSprites);
|
||||
this.cars = cars;
|
||||
// this.cars.push(
|
||||
// new TrainCar(
|
||||
// 55,
|
||||
// engineSprites,
|
||||
// 80,
|
||||
// 20,
|
||||
// { at: new Vector(0, 60), width: 80, height: 20 },
|
||||
// ),
|
||||
// new TrainCar(
|
||||
// 25,
|
||||
// engineSprites,
|
||||
// 40,
|
||||
// 20,
|
||||
// { at: new Vector(80, 0), width: 40, height: 20 },
|
||||
// ),
|
||||
// );
|
||||
this.cars[0].points = this.nodes.map((n) => n) as [Vector, Vector];
|
||||
this.cars[1].points = this.nodes.map((n) => n) as [Vector, Vector];
|
||||
let currentOffset = 40;
|
||||
@@ -66,14 +68,17 @@ export class Train {
|
||||
currentOffset += car.length;
|
||||
b.set(this.path.followEvenPoints(this.t - currentOffset));
|
||||
currentOffset += this.spacing;
|
||||
car.draw();
|
||||
// car.draw();
|
||||
}
|
||||
// this.draw();
|
||||
}
|
||||
|
||||
// draw() {
|
||||
// const doodler = getContextItem<Doodler>("doodler");
|
||||
// this.path.draw();
|
||||
// for (const [i, node] of this.nodes.entries()) {
|
||||
// doodler.drawCircle(node.point, 10, { color: 'purple', weight: 3 })
|
||||
// // doodler.drawCircle(node, 10, { color: "purple", weight: 3 });
|
||||
// doodler.fillCircle(node, 2, { color: "purple" });
|
||||
// // const next = this.nodes[i + 1];
|
||||
// // if (next) {
|
||||
// // const to = Vector.sub(node.point, next.point);
|
||||
|
Reference in New Issue
Block a user