Separates game loop from doodler draw loop. Each state is in charge of registering and unregistering layers
This commit is contained in:
parent
43a5268ed5
commit
3befb69f51
63
GameLoop.ts
Normal file
63
GameLoop.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Doodler } from "@bearmetal/doodler";
|
||||||
|
import { StateMachine } from "./state/machine.ts";
|
||||||
|
import { getContextItem } from "./lib/context.ts";
|
||||||
|
|
||||||
|
export class GameLoop<T> {
|
||||||
|
lastTime: number;
|
||||||
|
running: boolean;
|
||||||
|
targetFps: number;
|
||||||
|
|
||||||
|
constructor(targetFps: number = 60) {
|
||||||
|
this.lastTime = performance.now();
|
||||||
|
this.running = false;
|
||||||
|
this.targetFps = targetFps;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(state: StateMachine<T>) {
|
||||||
|
if (this.running) return;
|
||||||
|
this.running = true;
|
||||||
|
this.lastTime = performance.now();
|
||||||
|
|
||||||
|
while (this.running) {
|
||||||
|
const currentTime = performance.now();
|
||||||
|
const deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
|
||||||
|
this.lastTime = currentTime;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wait for state update to complete before continuing
|
||||||
|
await state.update(deltaTime);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in game loop:", error);
|
||||||
|
this.stop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use setTimeout to prevent immediate loop continuation
|
||||||
|
// and allow other tasks to run
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Usage example:
|
||||||
|
// const gameState = {
|
||||||
|
// update: async (deltaTime) => {
|
||||||
|
// console.log(`Updating with delta time: ${deltaTime.toFixed(3)}s`);
|
||||||
|
// // Simulate some async work
|
||||||
|
// await new Promise(resolve => setTimeout(resolve, 16)); // ~60fps
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Create and start the loop
|
||||||
|
// const loop = new GameLoop();
|
||||||
|
// loop.start(gameState);
|
||||||
|
|
||||||
|
// // Stop the loop after 5 seconds (example)
|
||||||
|
// setTimeout(() => {
|
||||||
|
// loop.stop();
|
||||||
|
// console.log('Loop stopped');
|
||||||
|
// }, 5000);
|
234
bundle.js
234
bundle.js
@ -78,12 +78,12 @@
|
|||||||
}, 2);
|
}, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/geometry/constants.ts
|
// https://jsr.io/@bearmetal/doodler/0.0.5-b/geometry/constants.ts
|
||||||
var Constants = {
|
var Constants = {
|
||||||
TWO_PI: Math.PI * 2
|
TWO_PI: Math.PI * 2
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/geometry/vector.ts
|
// https://jsr.io/@bearmetal/doodler/0.0.5-b/geometry/vector.ts
|
||||||
var Vector = class _Vector {
|
var Vector = class _Vector {
|
||||||
x;
|
x;
|
||||||
y;
|
y;
|
||||||
@ -368,7 +368,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/FPSCounter.ts
|
// https://jsr.io/@bearmetal/doodler/0.0.5-b/FPSCounter.ts
|
||||||
var FPSCounter = class {
|
var FPSCounter = class {
|
||||||
frameTimes = [];
|
frameTimes = [];
|
||||||
maxSamples;
|
maxSamples;
|
||||||
@ -396,7 +396,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/canvas.ts
|
// https://jsr.io/@bearmetal/doodler/0.0.5-b/canvas.ts
|
||||||
var Doodler = class {
|
var Doodler = class {
|
||||||
ctx;
|
ctx;
|
||||||
_canvas;
|
_canvas;
|
||||||
@ -488,9 +488,14 @@
|
|||||||
// Layer management
|
// Layer management
|
||||||
createLayer(layer) {
|
createLayer(layer) {
|
||||||
this.layers.push(layer);
|
this.layers.push(layer);
|
||||||
|
return this.layers.length - 1;
|
||||||
}
|
}
|
||||||
deleteLayer(layer) {
|
deleteLayer(layer) {
|
||||||
this.layers = this.layers.filter((l) => l !== layer);
|
if (typeof layer === "number") {
|
||||||
|
this.layers = this.layers.filter((_, i) => i !== layer);
|
||||||
|
} else {
|
||||||
|
this.layers = this.layers.filter((l) => l !== layer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
moveLayer(layer, index) {
|
moveLayer(layer, index) {
|
||||||
let temp = this.layers.filter((l) => l !== layer);
|
let temp = this.layers.filter((l) => l !== layer);
|
||||||
@ -759,13 +764,13 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/timing/EaseInOut.ts
|
// https://jsr.io/@bearmetal/doodler/0.0.5-b/timing/EaseInOut.ts
|
||||||
var easeInOut = (x) => x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
var easeInOut = (x) => x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
||||||
|
|
||||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/timing/Map.ts
|
// https://jsr.io/@bearmetal/doodler/0.0.5-b/timing/Map.ts
|
||||||
var map = (value, x1, y1, x2, y2) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;
|
var map = (value, x1, y1, x2, y2) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;
|
||||||
|
|
||||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/zoomableCanvas.ts
|
// https://jsr.io/@bearmetal/doodler/0.0.5-b/zoomableCanvas.ts
|
||||||
var ZoomableDoodler = class extends Doodler {
|
var ZoomableDoodler = class extends Doodler {
|
||||||
scale = 1;
|
scale = 1;
|
||||||
dragging = false;
|
dragging = false;
|
||||||
@ -1167,6 +1172,12 @@
|
|||||||
update(dt, ctx2) {
|
update(dt, ctx2) {
|
||||||
this.currentState?.update(dt, ctx2);
|
this.currentState?.update(dt, ctx2);
|
||||||
}
|
}
|
||||||
|
optimizePerformance(percent) {
|
||||||
|
const ctx2 = getContext();
|
||||||
|
if (percent < 0.5) {
|
||||||
|
ctx2.track.optimize(percent);
|
||||||
|
}
|
||||||
|
}
|
||||||
get current() {
|
get current() {
|
||||||
return this.currentState;
|
return this.currentState;
|
||||||
}
|
}
|
||||||
@ -1420,6 +1431,12 @@
|
|||||||
get lastSegment() {
|
get lastSegment() {
|
||||||
return this.segments.values().toArray().pop();
|
return this.segments.values().toArray().pop();
|
||||||
}
|
}
|
||||||
|
optimize(percent) {
|
||||||
|
console.log("Optimizing track", percent * 100 / 4);
|
||||||
|
for (const segment of this.segments.values()) {
|
||||||
|
segment.recalculateRailPoints(Math.round(percent * 100 / 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
registerSegment(segment) {
|
registerSegment(segment) {
|
||||||
segment.setTrack(this);
|
segment.setTrack(this);
|
||||||
this.segments.set(segment.id, segment);
|
this.segments.set(segment.id, segment);
|
||||||
@ -1508,9 +1525,14 @@
|
|||||||
}
|
}
|
||||||
generatePath() {
|
generatePath() {
|
||||||
if (!this.firstSegment) throw new Error("No first segment");
|
if (!this.firstSegment) throw new Error("No first segment");
|
||||||
|
const flags = { looping: true };
|
||||||
const rightOnlyPath = [
|
const rightOnlyPath = [
|
||||||
this.firstSegment.copy(),
|
this.firstSegment.copy(),
|
||||||
...this.findRightPath(this.firstSegment, /* @__PURE__ */ new Set([this.firstSegment.id]))
|
...this.findRightPath(
|
||||||
|
this.firstSegment,
|
||||||
|
/* @__PURE__ */ new Set([this.firstSegment.id]),
|
||||||
|
flags
|
||||||
|
)
|
||||||
];
|
];
|
||||||
rightOnlyPath.forEach((s, i, arr) => {
|
rightOnlyPath.forEach((s, i, arr) => {
|
||||||
if (i === 0) return;
|
if (i === 0) return;
|
||||||
@ -1519,9 +1541,17 @@
|
|||||||
s.prev = prev;
|
s.prev = prev;
|
||||||
prev.next = s;
|
prev.next = s;
|
||||||
});
|
});
|
||||||
|
if (flags.looping) {
|
||||||
|
const first = rightOnlyPath[0];
|
||||||
|
const last = rightOnlyPath[rightOnlyPath.length - 1];
|
||||||
|
first.points[0] = last.points[3];
|
||||||
|
last.points[3] = first.points[0];
|
||||||
|
first.prev = last;
|
||||||
|
last.next = first;
|
||||||
|
}
|
||||||
return new Spline(rightOnlyPath);
|
return new Spline(rightOnlyPath);
|
||||||
}
|
}
|
||||||
*findRightPath(start, seen) {
|
*findRightPath(start, seen, flags) {
|
||||||
if (start.frontNeighbours.length === 0) {
|
if (start.frontNeighbours.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1542,12 +1572,17 @@
|
|||||||
rightMost = segment;
|
rightMost = segment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (seen.has(rightMost.id)) return;
|
if (seen.has(rightMost.id)) {
|
||||||
|
if (seen.values().next().value === rightMost.id) {
|
||||||
|
flags.looping = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
seen.add(rightMost.id);
|
seen.add(rightMost.id);
|
||||||
yield rightMost.copy();
|
yield rightMost.copy();
|
||||||
yield* this.findRightPath(rightMost, seen);
|
yield* this.findRightPath(rightMost, seen, flags);
|
||||||
}
|
}
|
||||||
*findLeftPath(start, seen) {
|
*findLeftPath(start, seen, flags) {
|
||||||
if (start.frontNeighbours.length === 0) {
|
if (start.frontNeighbours.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1568,10 +1603,15 @@
|
|||||||
leftMost = segment;
|
leftMost = segment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (seen.has(leftMost.id)) return;
|
if (seen.has(leftMost.id)) {
|
||||||
|
if (seen.values().next().value === leftMost.id) {
|
||||||
|
flags.looping = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
seen.add(leftMost.id);
|
seen.add(leftMost.id);
|
||||||
yield leftMost.copy();
|
yield leftMost.copy();
|
||||||
yield* this.findLeftPath(leftMost, seen);
|
yield* this.findLeftPath(leftMost, seen, flags);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var TrackSegment = class _TrackSegment extends PathSegment {
|
var TrackSegment = class _TrackSegment extends PathSegment {
|
||||||
@ -1579,10 +1619,25 @@
|
|||||||
backNeighbours = [];
|
backNeighbours = [];
|
||||||
track;
|
track;
|
||||||
doodler;
|
doodler;
|
||||||
|
normalPoints = [];
|
||||||
|
antiNormalPoints = [];
|
||||||
constructor(p, id) {
|
constructor(p, id) {
|
||||||
super(p);
|
super(p);
|
||||||
this.doodler = getContextItem("doodler");
|
this.doodler = getContextItem("doodler");
|
||||||
this.id = id ?? crypto.randomUUID();
|
this.id = id ?? crypto.randomUUID();
|
||||||
|
this.recalculateRailPoints();
|
||||||
|
}
|
||||||
|
recalculateRailPoints(resolution = 100) {
|
||||||
|
this.normalPoints = [];
|
||||||
|
this.antiNormalPoints = [];
|
||||||
|
for (let i = 0; i <= resolution; i++) {
|
||||||
|
const t = i / resolution;
|
||||||
|
const normal = this.tangent(t).rotate(Math.PI / 2);
|
||||||
|
normal.setMag(6);
|
||||||
|
const p = this.getPointAtT(t);
|
||||||
|
this.normalPoints.push(p.copy().add(normal));
|
||||||
|
this.antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setTrack(t) {
|
setTrack(t) {
|
||||||
this.track = t;
|
this.track = t;
|
||||||
@ -1619,21 +1674,16 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const lineResolution = 100;
|
|
||||||
const normalPoints = [];
|
|
||||||
const antiNormalPoints = [];
|
|
||||||
for (let i = 0; i <= lineResolution; i++) {
|
|
||||||
const t = i / lineResolution;
|
|
||||||
const normal = this.tangent(t).rotate(Math.PI / 2);
|
|
||||||
normal.setMag(6);
|
|
||||||
const p = this.getPointAtT(t);
|
|
||||||
normalPoints.push(p.copy().add(normal));
|
|
||||||
antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
|
|
||||||
}
|
|
||||||
this.doodler.deferDrawing(
|
this.doodler.deferDrawing(
|
||||||
() => {
|
() => {
|
||||||
this.doodler.drawLine(normalPoints, { color: "grey" });
|
this.doodler.drawLine(this.normalPoints, {
|
||||||
this.doodler.drawLine(antiNormalPoints, { color: "grey" });
|
color: "grey",
|
||||||
|
weight: 1.5
|
||||||
|
});
|
||||||
|
this.doodler.drawLine(this.antiNormalPoints, {
|
||||||
|
color: "grey",
|
||||||
|
weight: 1.5
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1737,21 +1787,12 @@
|
|||||||
looped = false;
|
looped = false;
|
||||||
constructor(segs) {
|
constructor(segs) {
|
||||||
this.segments = segs;
|
this.segments = segs;
|
||||||
|
if (this.segments.at(-1)?.next === this.segments[0]) {
|
||||||
|
this.looped = true;
|
||||||
|
}
|
||||||
this.pointSpacing = 1;
|
this.pointSpacing = 1;
|
||||||
this.evenPoints = this.calculateEvenlySpacedPoints(1);
|
this.evenPoints = this.calculateEvenlySpacedPoints(1);
|
||||||
this.nodes = [];
|
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) {
|
// setContext(ctx: CanvasRenderingContext2D) {
|
||||||
// this.ctx = ctx;
|
// this.ctx = ctx;
|
||||||
@ -1761,7 +1802,10 @@
|
|||||||
// }
|
// }
|
||||||
draw() {
|
draw() {
|
||||||
for (const segment of this.segments) {
|
for (const segment of this.segments) {
|
||||||
segment.draw();
|
const doodler2 = getContextItem("doodler");
|
||||||
|
doodler2.drawWithAlpha(0.5, () => {
|
||||||
|
doodler2.drawBezier(...segment.points, { color: "red" });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
calculateEvenlySpacedPoints(spacing, resolution = 1) {
|
calculateEvenlySpacedPoints(spacing, resolution = 1) {
|
||||||
@ -1934,6 +1978,7 @@
|
|||||||
ghostSegment;
|
ghostSegment;
|
||||||
ghostRotated = false;
|
ghostRotated = false;
|
||||||
closestEnd;
|
closestEnd;
|
||||||
|
layers = [];
|
||||||
update(dt) {
|
update(dt) {
|
||||||
const inputManager2 = getContextItem("inputManager");
|
const inputManager2 = getContextItem("inputManager");
|
||||||
const track = getContextItem("track");
|
const track = getContextItem("track");
|
||||||
@ -2015,19 +2060,6 @@
|
|||||||
this.ghostSegment = void 0;
|
this.ghostSegment = void 0;
|
||||||
this.ghostRotated = false;
|
this.ghostRotated = false;
|
||||||
}
|
}
|
||||||
this.selectedSegment?.draw();
|
|
||||||
if (this.ghostSegment) {
|
|
||||||
doodler2.drawWithAlpha(0.5, () => {
|
|
||||||
if (!this.ghostSegment) return;
|
|
||||||
this.ghostSegment.draw();
|
|
||||||
if (getContextItemOrDefault("debug", false)) {
|
|
||||||
const colors2 = getContextItem("colors");
|
|
||||||
for (const [i, point] of this.ghostSegment.points.entries() ?? []) {
|
|
||||||
doodler2.fillCircle(point, 4, { color: colors2[i + 3] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const translation = new Vector(0, 0);
|
const translation = new Vector(0, 0);
|
||||||
if (inputManager2.getKeyState("ArrowUp")) {
|
if (inputManager2.getKeyState("ArrowUp")) {
|
||||||
@ -2045,9 +2077,27 @@
|
|||||||
if (translation.x !== 0 || translation.y !== 0) {
|
if (translation.x !== 0 || translation.y !== 0) {
|
||||||
track.translate(translation);
|
track.translate(translation);
|
||||||
}
|
}
|
||||||
track.draw(true);
|
|
||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
|
const doodler2 = getContextItem("doodler");
|
||||||
|
this.layers.push(
|
||||||
|
doodler2.createLayer(() => {
|
||||||
|
this.selectedSegment?.draw();
|
||||||
|
if (this.ghostSegment) {
|
||||||
|
doodler2.drawWithAlpha(0.5, () => {
|
||||||
|
if (!this.ghostSegment) return;
|
||||||
|
this.ghostSegment.draw();
|
||||||
|
if (getContextItemOrDefault("debug", false)) {
|
||||||
|
const colors2 = getContextItem("colors");
|
||||||
|
for (const [i, point] of this.ghostSegment.points.entries() ?? []) {
|
||||||
|
doodler2.fillCircle(point, 4, { color: colors2[i + 3] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
track.draw(true);
|
||||||
|
})
|
||||||
|
);
|
||||||
setContextItem("trackSegments", [
|
setContextItem("trackSegments", [
|
||||||
void 0,
|
void 0,
|
||||||
new StraightTrack(),
|
new StraightTrack(),
|
||||||
@ -2129,6 +2179,9 @@
|
|||||||
}
|
}
|
||||||
redoBuffer = [];
|
redoBuffer = [];
|
||||||
stop() {
|
stop() {
|
||||||
|
for (const layer of this.layers) {
|
||||||
|
getContextItem("doodler").deleteLayer(layer);
|
||||||
|
}
|
||||||
const inputManager2 = getContextItem("inputManager");
|
const inputManager2 = getContextItem("inputManager");
|
||||||
inputManager2.offKey("e");
|
inputManager2.offKey("e");
|
||||||
inputManager2.offKey("w");
|
inputManager2.offKey("w");
|
||||||
@ -2577,16 +2630,27 @@
|
|||||||
2 /* PAUSED */,
|
2 /* PAUSED */,
|
||||||
3 /* EDIT_TRACK */
|
3 /* EDIT_TRACK */
|
||||||
]);
|
]);
|
||||||
|
layers = [];
|
||||||
update(dt) {
|
update(dt) {
|
||||||
const ctx2 = getContext();
|
const ctx2 = getContext();
|
||||||
const input = getContextItem("inputManager");
|
|
||||||
ctx2.track.draw();
|
|
||||||
for (const train of ctx2.trains) {
|
for (const train of ctx2.trains) {
|
||||||
train.move(dt);
|
train.move(dt);
|
||||||
train.draw();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
|
const doodler2 = getContextItem("doodler");
|
||||||
|
this.layers.push(
|
||||||
|
doodler2.createLayer(() => {
|
||||||
|
const track2 = getContextItem("track");
|
||||||
|
track2.draw();
|
||||||
|
}),
|
||||||
|
doodler2.createLayer(() => {
|
||||||
|
const trains = getContextItem("trains");
|
||||||
|
for (const train of trains) {
|
||||||
|
train.draw();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
const inputManager2 = getContextItem("inputManager");
|
const inputManager2 = getContextItem("inputManager");
|
||||||
const track = getContextItem("track");
|
const track = getContextItem("track");
|
||||||
const ctx2 = getContext();
|
const ctx2 = getContext();
|
||||||
@ -2608,6 +2672,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
stop() {
|
stop() {
|
||||||
|
for (const layer of this.layers) {
|
||||||
|
getContextItem("doodler").deleteLayer(layer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2631,6 +2698,7 @@
|
|||||||
validTransitions = /* @__PURE__ */ new Set([
|
validTransitions = /* @__PURE__ */ new Set([
|
||||||
1 /* RUNNING */
|
1 /* RUNNING */
|
||||||
]);
|
]);
|
||||||
|
layers = [];
|
||||||
update() {
|
update() {
|
||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
@ -2648,6 +2716,13 @@
|
|||||||
resources2.ready().then(() => {
|
resources2.ready().then(() => {
|
||||||
this.stateMachine.transitionTo(1 /* RUNNING */);
|
this.stateMachine.transitionTo(1 /* RUNNING */);
|
||||||
});
|
});
|
||||||
|
const doodler2 = getContextItem("doodler");
|
||||||
|
this.layers.push(doodler2.createLayer((_, __, dTime) => {
|
||||||
|
doodler2.clearRect(new Vector(0, 0), doodler2.width, doodler2.height);
|
||||||
|
doodler2.fillRect(new Vector(0, 0), doodler2.width, doodler2.height, {
|
||||||
|
color: "#302040"
|
||||||
|
});
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
stop() {
|
stop() {
|
||||||
}
|
}
|
||||||
@ -2675,6 +2750,39 @@
|
|||||||
return stateMachine;
|
return stateMachine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GameLoop.ts
|
||||||
|
var GameLoop = class {
|
||||||
|
lastTime;
|
||||||
|
running;
|
||||||
|
targetFps;
|
||||||
|
constructor(targetFps = 60) {
|
||||||
|
this.lastTime = performance.now();
|
||||||
|
this.running = false;
|
||||||
|
this.targetFps = targetFps;
|
||||||
|
}
|
||||||
|
async start(state2) {
|
||||||
|
if (this.running) return;
|
||||||
|
this.running = true;
|
||||||
|
this.lastTime = performance.now();
|
||||||
|
while (this.running) {
|
||||||
|
const currentTime = performance.now();
|
||||||
|
const deltaTime = (currentTime - this.lastTime) / 1e3;
|
||||||
|
this.lastTime = currentTime;
|
||||||
|
try {
|
||||||
|
await state2.update(deltaTime);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in game loop:", error);
|
||||||
|
this.stop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// main.ts
|
// main.ts
|
||||||
var inputManager = new InputManager();
|
var inputManager = new InputManager();
|
||||||
var resources = new ResourceManager();
|
var resources = new ResourceManager();
|
||||||
@ -2704,9 +2812,6 @@
|
|||||||
var state = bootstrapGameStateMachine();
|
var state = bootstrapGameStateMachine();
|
||||||
setContextItem("state", state);
|
setContextItem("state", state);
|
||||||
doodler.init();
|
doodler.init();
|
||||||
doodler.createLayer((_, __, dTime) => {
|
|
||||||
state.update(dTime);
|
|
||||||
});
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -2718,12 +2823,19 @@
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const doodler2 = getContextItem("doodler");
|
const doodler2 = getContextItem("doodler");
|
||||||
const frameRate = doodler2.fps;
|
const frameRate = doodler2.fps;
|
||||||
|
if (frameRate < 0.5) return;
|
||||||
let fpsEl = document.getElementById("fps");
|
let fpsEl = document.getElementById("fps");
|
||||||
if (!fpsEl) {
|
if (!fpsEl) {
|
||||||
fpsEl = document.createElement("div");
|
fpsEl = document.createElement("div");
|
||||||
fpsEl.id = "fps";
|
fpsEl.id = "fps";
|
||||||
document.body.appendChild(fpsEl);
|
document.body.appendChild(fpsEl);
|
||||||
}
|
}
|
||||||
|
const fPerc = frameRate / 60;
|
||||||
|
if (fPerc < 0.6) {
|
||||||
|
state.optimizePerformance(fPerc);
|
||||||
|
}
|
||||||
fpsEl.textContent = frameRate.toFixed(1) + " fps";
|
fpsEl.textContent = frameRate.toFixed(1) + " fps";
|
||||||
}, 1e3);
|
}, 1e3);
|
||||||
|
var gameLoop = new GameLoop();
|
||||||
|
gameLoop.start(state);
|
||||||
})();
|
})();
|
||||||
|
@ -13,6 +13,6 @@
|
|||||||
"dev": "deno run -RWEN --allow-run dev.ts dev"
|
"dev": "deno run -RWEN --allow-run dev.ts dev"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-a"
|
"@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-b"
|
||||||
}
|
}
|
||||||
}
|
}
|
8
deno.lock
generated
8
deno.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "4",
|
"version": "4",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
"jsr:@bearmetal/doodler@0.0.5-a": "0.0.5-a",
|
"jsr:@bearmetal/doodler@0.0.5-b": "0.0.5-b",
|
||||||
"jsr:@luca/esbuild-deno-loader@*": "0.11.0",
|
"jsr:@luca/esbuild-deno-loader@*": "0.11.0",
|
||||||
"jsr:@luca/esbuild-deno-loader@0.11.1": "0.11.1",
|
"jsr:@luca/esbuild-deno-loader@0.11.1": "0.11.1",
|
||||||
"jsr:@std/assert@*": "1.0.10",
|
"jsr:@std/assert@*": "1.0.10",
|
||||||
@ -25,8 +25,8 @@
|
|||||||
"@bearmetal/doodler@0.0.4": {
|
"@bearmetal/doodler@0.0.4": {
|
||||||
"integrity": "b631083cff84994c513f70d1f09e6a9256edabcb224112c93a9ca6a87c88a389"
|
"integrity": "b631083cff84994c513f70d1f09e6a9256edabcb224112c93a9ca6a87c88a389"
|
||||||
},
|
},
|
||||||
"@bearmetal/doodler@0.0.5-a": {
|
"@bearmetal/doodler@0.0.5-b": {
|
||||||
"integrity": "c59d63f071623ad4c7588e24b464874786759e56a6b12782689251a5cf3a1c08"
|
"integrity": "94f265ea21162f943291526800de7f3f6560634a4fe762a38cd73892685b6742"
|
||||||
},
|
},
|
||||||
"@luca/esbuild-deno-loader@0.11.0": {
|
"@luca/esbuild-deno-loader@0.11.0": {
|
||||||
"integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
|
"integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
|
||||||
@ -232,7 +232,7 @@
|
|||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@bearmetal/doodler@0.0.5-a"
|
"jsr:@bearmetal/doodler@0.0.5-b"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
17
main.ts
17
main.ts
@ -11,8 +11,9 @@ import { ResourceManager } from "./lib/resources.ts";
|
|||||||
import { addButton } from "./ui/button.ts";
|
import { addButton } from "./ui/button.ts";
|
||||||
import { TrackSystem } from "./track/system.ts";
|
import { TrackSystem } from "./track/system.ts";
|
||||||
import { StraightTrack } from "./track/shapes.ts";
|
import { StraightTrack } from "./track/shapes.ts";
|
||||||
import { StateMachine } from "./state/machine.ts";
|
import { State, StateMachine } from "./state/machine.ts";
|
||||||
import { bootstrapGameStateMachine } from "./state/states/index.ts";
|
import { bootstrapGameStateMachine } from "./state/states/index.ts";
|
||||||
|
import { GameLoop } from "./GameLoop.ts";
|
||||||
|
|
||||||
const inputManager = new InputManager();
|
const inputManager = new InputManager();
|
||||||
const resources = new ResourceManager();
|
const resources = new ResourceManager();
|
||||||
@ -50,9 +51,9 @@ setContextItem("state", state);
|
|||||||
|
|
||||||
doodler.init();
|
doodler.init();
|
||||||
|
|
||||||
doodler.createLayer((_, __, dTime) => {
|
// doodler.createLayer((_, __, dTime) => {
|
||||||
state.update(dTime);
|
// state.update(dTime);
|
||||||
});
|
// });
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||||
@ -66,11 +67,19 @@ document.addEventListener("keydown", (e) => {
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const doodler = getContextItem<Doodler>("doodler");
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
const frameRate = doodler.fps;
|
const frameRate = doodler.fps;
|
||||||
|
if (frameRate < 0.5) return;
|
||||||
let fpsEl = document.getElementById("fps");
|
let fpsEl = document.getElementById("fps");
|
||||||
if (!fpsEl) {
|
if (!fpsEl) {
|
||||||
fpsEl = document.createElement("div");
|
fpsEl = document.createElement("div");
|
||||||
fpsEl.id = "fps";
|
fpsEl.id = "fps";
|
||||||
document.body.appendChild(fpsEl);
|
document.body.appendChild(fpsEl);
|
||||||
}
|
}
|
||||||
|
const fPerc = frameRate / 60;
|
||||||
|
if (fPerc < 0.6) {
|
||||||
|
state.optimizePerformance(fPerc);
|
||||||
|
}
|
||||||
fpsEl.textContent = frameRate.toFixed(1) + " fps";
|
fpsEl.textContent = frameRate.toFixed(1) + " fps";
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
const gameLoop = new GameLoop();
|
||||||
|
gameLoop.start(state);
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import { getContext } from "../lib/context.ts";
|
||||||
|
import { TrackSystem } from "../track/system.ts";
|
||||||
|
import { Train } from "../train.old.ts";
|
||||||
|
|
||||||
export class StateMachine<T> {
|
export class StateMachine<T> {
|
||||||
private _states: Map<T, State<T>> = new Map();
|
private _states: Map<T, State<T>> = new Map();
|
||||||
private currentState?: State<T>;
|
private currentState?: State<T>;
|
||||||
@ -6,6 +10,13 @@ export class StateMachine<T> {
|
|||||||
this.currentState?.update(dt, ctx);
|
this.currentState?.update(dt, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
optimizePerformance(percent: number) {
|
||||||
|
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
|
||||||
|
if (percent < 0.5) {
|
||||||
|
ctx.track.optimize(percent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get current() {
|
get current() {
|
||||||
return this.currentState;
|
return this.currentState;
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,8 @@ export class EditTrackState extends State<States> {
|
|||||||
private ghostRotated = false;
|
private ghostRotated = false;
|
||||||
private closestEnd?: End;
|
private closestEnd?: End;
|
||||||
|
|
||||||
|
layers: number[] = [];
|
||||||
|
|
||||||
override update(dt: number): void {
|
override update(dt: number): void {
|
||||||
const inputManager = getContextItem<InputManager>("inputManager");
|
const inputManager = getContextItem<InputManager>("inputManager");
|
||||||
const track = getContextItem<TrackSystem>("track");
|
const track = getContextItem<TrackSystem>("track");
|
||||||
@ -138,22 +140,6 @@ export class EditTrackState extends State<States> {
|
|||||||
this.ghostSegment = undefined;
|
this.ghostSegment = undefined;
|
||||||
this.ghostRotated = false;
|
this.ghostRotated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedSegment?.draw();
|
|
||||||
if (this.ghostSegment) {
|
|
||||||
doodler.drawWithAlpha(0.5, () => {
|
|
||||||
if (!this.ghostSegment) return;
|
|
||||||
this.ghostSegment.draw();
|
|
||||||
if (getContextItemOrDefault("debug", false)) {
|
|
||||||
const colors = getContextItem<string[]>("colors");
|
|
||||||
for (
|
|
||||||
const [i, point] of this.ghostSegment.points.entries() ?? []
|
|
||||||
) {
|
|
||||||
doodler.fillCircle(point, 4, { color: colors[i + 3] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// manipulate only end of segment while maintaining length
|
// manipulate only end of segment while maintaining length
|
||||||
@ -263,13 +249,34 @@ export class EditTrackState extends State<States> {
|
|||||||
track.translate(translation);
|
track.translate(translation);
|
||||||
}
|
}
|
||||||
|
|
||||||
track.draw(true);
|
|
||||||
// TODO
|
// TODO
|
||||||
// Draw ui
|
// Draw ui
|
||||||
// Draw track points
|
// Draw track points
|
||||||
// Draw track tangents
|
// Draw track tangents
|
||||||
}
|
}
|
||||||
override start(): void {
|
override start(): void {
|
||||||
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
|
this.layers.push(
|
||||||
|
doodler.createLayer(() => {
|
||||||
|
this.selectedSegment?.draw();
|
||||||
|
if (this.ghostSegment) {
|
||||||
|
doodler.drawWithAlpha(0.5, () => {
|
||||||
|
if (!this.ghostSegment) return;
|
||||||
|
this.ghostSegment.draw();
|
||||||
|
if (getContextItemOrDefault("debug", false)) {
|
||||||
|
const colors = getContextItem<string[]>("colors");
|
||||||
|
for (
|
||||||
|
const [i, point] of this.ghostSegment.points.entries() ?? []
|
||||||
|
) {
|
||||||
|
doodler.fillCircle(point, 4, { color: colors[i + 3] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
track.draw(true);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
setContextItem("trackSegments", [
|
setContextItem("trackSegments", [
|
||||||
undefined,
|
undefined,
|
||||||
new StraightTrack(),
|
new StraightTrack(),
|
||||||
@ -376,9 +383,19 @@ export class EditTrackState extends State<States> {
|
|||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// Cache trains and save
|
// Cache trains and save
|
||||||
|
|
||||||
|
// const trackCount = 2000;
|
||||||
|
// for (let i = 0; i < trackCount; i++) {
|
||||||
|
// const seg = new StraightTrack();
|
||||||
|
// track.registerSegment(seg);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
redoBuffer: TrackSegment[] = [];
|
redoBuffer: TrackSegment[] = [];
|
||||||
override stop(): void {
|
override stop(): void {
|
||||||
|
for (const layer of this.layers) {
|
||||||
|
getContextItem<Doodler>("doodler").deleteLayer(layer);
|
||||||
|
}
|
||||||
|
|
||||||
const inputManager = getContextItem<InputManager>("inputManager");
|
const inputManager = getContextItem<InputManager>("inputManager");
|
||||||
inputManager.offKey("e");
|
inputManager.offKey("e");
|
||||||
inputManager.offKey("w");
|
inputManager.offKey("w");
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||||
import { bootstrapInputs } from "../../inputs.ts";
|
import { bootstrapInputs } from "../../inputs.ts";
|
||||||
import { setContextItem } from "../../lib/context.ts";
|
import { getContextItem, setContextItem } from "../../lib/context.ts";
|
||||||
import { InputManager } from "../../lib/input.ts";
|
import { InputManager } from "../../lib/input.ts";
|
||||||
import { ResourceManager } from "../../lib/resources.ts";
|
import { ResourceManager } from "../../lib/resources.ts";
|
||||||
import { StraightTrack } from "../../track/shapes.ts";
|
import { StraightTrack } from "../../track/shapes.ts";
|
||||||
@ -13,6 +14,8 @@ export class LoadState extends State<States> {
|
|||||||
States.RUNNING,
|
States.RUNNING,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
layers: number[] = [];
|
||||||
|
|
||||||
override update(): void {
|
override update(): void {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
@ -37,6 +40,14 @@ export class LoadState extends State<States> {
|
|||||||
resources.ready().then(() => {
|
resources.ready().then(() => {
|
||||||
this.stateMachine.transitionTo(States.RUNNING);
|
this.stateMachine.transitionTo(States.RUNNING);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
|
this.layers.push(doodler.createLayer((_, __, dTime) => {
|
||||||
|
doodler.clearRect(new Vector(0, 0), doodler.width, doodler.height);
|
||||||
|
doodler.fillRect(new Vector(0, 0), doodler.width, doodler.height, {
|
||||||
|
color: "#302040",
|
||||||
|
});
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
override stop(): void {
|
override stop(): void {
|
||||||
// noop
|
// noop
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Doodler } from "@bearmetal/doodler";
|
||||||
import { getContext, getContextItem } from "../../lib/context.ts";
|
import { getContext, getContextItem } from "../../lib/context.ts";
|
||||||
import { InputManager } from "../../lib/input.ts";
|
import { InputManager } from "../../lib/input.ts";
|
||||||
import { TrackSystem } from "../../track/system.ts";
|
import { TrackSystem } from "../../track/system.ts";
|
||||||
@ -14,26 +15,40 @@ export class RunningState extends State<States> {
|
|||||||
States.PAUSED,
|
States.PAUSED,
|
||||||
States.EDIT_TRACK,
|
States.EDIT_TRACK,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
layers: number[] = [];
|
||||||
|
|
||||||
override update(dt: number): void {
|
override update(dt: number): void {
|
||||||
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
|
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
|
||||||
// const ctx = getContext() as { trains: DotFollower[]; track: TrackSystem };
|
// const ctx = getContext() as { trains: DotFollower[]; track: TrackSystem };
|
||||||
const input = getContextItem<InputManager>("inputManager");
|
|
||||||
// TODO
|
// TODO
|
||||||
// Update trains
|
// Update trains
|
||||||
// Update world
|
// Update world
|
||||||
// Handle input
|
// Handle input
|
||||||
// Draw (maybe via a layer system that syncs with doodler)
|
// Draw (maybe via a layer system that syncs with doodler)
|
||||||
ctx.track.draw();
|
|
||||||
for (const train of ctx.trains) {
|
|
||||||
// if (input.getKeyState("ArrowUp")) {
|
|
||||||
// train.acceleration.x += 10;
|
|
||||||
// }
|
|
||||||
train.move(dt);
|
|
||||||
train.draw();
|
|
||||||
}
|
|
||||||
// Monitor world events
|
// Monitor world events
|
||||||
|
for (const train of ctx.trains) {
|
||||||
|
train.move(dt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
override start(): void {
|
override start(): void {
|
||||||
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
|
this.layers.push(
|
||||||
|
doodler.createLayer(() => {
|
||||||
|
const track = getContextItem<TrackSystem>("track");
|
||||||
|
track.draw();
|
||||||
|
}),
|
||||||
|
doodler.createLayer(() => {
|
||||||
|
const trains = getContextItem<Train[]>("trains");
|
||||||
|
for (const train of trains) {
|
||||||
|
// if (input.getKeyState("ArrowUp")) {
|
||||||
|
// train.acceleration.x += 10;
|
||||||
|
// }
|
||||||
|
train.draw();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// noop
|
// noop
|
||||||
const inputManager = getContextItem<InputManager>("inputManager");
|
const inputManager = getContextItem<InputManager>("inputManager");
|
||||||
const track = getContextItem<TrackSystem>("track");
|
const track = getContextItem<TrackSystem>("track");
|
||||||
@ -46,7 +61,7 @@ export class RunningState extends State<States> {
|
|||||||
const train = new Train(track.path, [new RedEngine(), new Tender()]);
|
const train = new Train(track.path, [new RedEngine(), new Tender()]);
|
||||||
ctx.trains.push(train);
|
ctx.trains.push(train);
|
||||||
});
|
});
|
||||||
// const trainCount = 1000;
|
// const trainCount = 2000;
|
||||||
// for (let i = 0; i < trainCount; i++) {
|
// for (let i = 0; i < trainCount; i++) {
|
||||||
// const train = new Train(track.path, [new RedEngine(), new Tender()]);
|
// const train = new Train(track.path, [new RedEngine(), new Tender()]);
|
||||||
// ctx.trains.push(train);
|
// ctx.trains.push(train);
|
||||||
@ -66,6 +81,8 @@ export class RunningState extends State<States> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
override stop(): void {
|
override stop(): void {
|
||||||
// noop
|
for (const layer of this.layers) {
|
||||||
|
getContextItem<Doodler>("doodler").deleteLayer(layer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
113
track/system.ts
113
track/system.ts
@ -22,6 +22,13 @@ export class TrackSystem {
|
|||||||
return this.segments.values().toArray().pop();
|
return this.segments.values().toArray().pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
optimize(percent: number) {
|
||||||
|
console.log("Optimizing track", percent * 100 / 4);
|
||||||
|
for (const segment of this.segments.values()) {
|
||||||
|
segment.recalculateRailPoints(Math.round(percent * 100 / 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
registerSegment(segment: TrackSegment) {
|
registerSegment(segment: TrackSegment) {
|
||||||
segment.setTrack(this);
|
segment.setTrack(this);
|
||||||
this.segments.set(segment.id, segment);
|
this.segments.set(segment.id, segment);
|
||||||
@ -147,9 +154,14 @@ export class TrackSystem {
|
|||||||
|
|
||||||
generatePath() {
|
generatePath() {
|
||||||
if (!this.firstSegment) throw new Error("No first segment");
|
if (!this.firstSegment) throw new Error("No first segment");
|
||||||
|
const flags = { looping: true };
|
||||||
const rightOnlyPath = [
|
const rightOnlyPath = [
|
||||||
this.firstSegment.copy(),
|
this.firstSegment.copy(),
|
||||||
...this.findRightPath(this.firstSegment, new Set([this.firstSegment.id])),
|
...this.findRightPath(
|
||||||
|
this.firstSegment,
|
||||||
|
new Set([this.firstSegment.id]),
|
||||||
|
flags,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
rightOnlyPath.forEach((s, i, arr) => {
|
rightOnlyPath.forEach((s, i, arr) => {
|
||||||
@ -159,6 +171,14 @@ export class TrackSystem {
|
|||||||
s.prev = prev;
|
s.prev = prev;
|
||||||
prev.next = s;
|
prev.next = s;
|
||||||
});
|
});
|
||||||
|
if (flags.looping) {
|
||||||
|
const first = rightOnlyPath[0];
|
||||||
|
const last = rightOnlyPath[rightOnlyPath.length - 1];
|
||||||
|
first.points[0] = last.points[3];
|
||||||
|
last.points[3] = first.points[0];
|
||||||
|
first.prev = last;
|
||||||
|
last.next = first;
|
||||||
|
}
|
||||||
|
|
||||||
return new Spline<TrackSegment>(rightOnlyPath);
|
return new Spline<TrackSegment>(rightOnlyPath);
|
||||||
}
|
}
|
||||||
@ -166,6 +186,7 @@ export class TrackSystem {
|
|||||||
*findRightPath(
|
*findRightPath(
|
||||||
start: TrackSegment,
|
start: TrackSegment,
|
||||||
seen: Set<string>,
|
seen: Set<string>,
|
||||||
|
flags: { looping: boolean },
|
||||||
): Generator<TrackSegment> {
|
): Generator<TrackSegment> {
|
||||||
if (start.frontNeighbours.length === 0) {
|
if (start.frontNeighbours.length === 0) {
|
||||||
return;
|
return;
|
||||||
@ -187,14 +208,20 @@ export class TrackSystem {
|
|||||||
rightMost = segment;
|
rightMost = segment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (seen.has(rightMost.id)) return;
|
if (seen.has(rightMost.id)) {
|
||||||
|
if (seen.values().next().value === rightMost.id) {
|
||||||
|
flags.looping = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
seen.add(rightMost.id);
|
seen.add(rightMost.id);
|
||||||
yield rightMost.copy();
|
yield rightMost.copy();
|
||||||
yield* this.findRightPath(rightMost, seen);
|
yield* this.findRightPath(rightMost, seen, flags);
|
||||||
}
|
}
|
||||||
*findLeftPath(
|
*findLeftPath(
|
||||||
start: TrackSegment,
|
start: TrackSegment,
|
||||||
seen: Set<string>,
|
seen: Set<string>,
|
||||||
|
flags: { looping: boolean },
|
||||||
): Generator<TrackSegment> {
|
): Generator<TrackSegment> {
|
||||||
if (start.frontNeighbours.length === 0) {
|
if (start.frontNeighbours.length === 0) {
|
||||||
return;
|
return;
|
||||||
@ -216,10 +243,15 @@ export class TrackSystem {
|
|||||||
leftMost = segment;
|
leftMost = segment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (seen.has(leftMost.id)) return;
|
if (seen.has(leftMost.id)) {
|
||||||
|
if (seen.values().next().value === leftMost.id) {
|
||||||
|
flags.looping = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
seen.add(leftMost.id);
|
seen.add(leftMost.id);
|
||||||
yield leftMost.copy();
|
yield leftMost.copy();
|
||||||
yield* this.findLeftPath(leftMost, seen);
|
yield* this.findLeftPath(leftMost, seen, flags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,11 +264,27 @@ export class TrackSegment extends PathSegment {
|
|||||||
track?: TrackSystem;
|
track?: TrackSystem;
|
||||||
|
|
||||||
doodler: Doodler;
|
doodler: Doodler;
|
||||||
|
normalPoints: Vector[] = [];
|
||||||
|
antiNormalPoints: Vector[] = [];
|
||||||
|
|
||||||
constructor(p: VectorSet, id?: string) {
|
constructor(p: VectorSet, id?: string) {
|
||||||
super(p);
|
super(p);
|
||||||
this.doodler = getContextItem<Doodler>("doodler");
|
this.doodler = getContextItem<Doodler>("doodler");
|
||||||
this.id = id ?? crypto.randomUUID();
|
this.id = id ?? crypto.randomUUID();
|
||||||
|
this.recalculateRailPoints();
|
||||||
|
}
|
||||||
|
|
||||||
|
recalculateRailPoints(resolution = 100) {
|
||||||
|
this.normalPoints = [];
|
||||||
|
this.antiNormalPoints = [];
|
||||||
|
for (let i = 0; i <= resolution; i++) {
|
||||||
|
const t = i / resolution;
|
||||||
|
const normal = this.tangent(t).rotate(Math.PI / 2);
|
||||||
|
normal.setMag(6);
|
||||||
|
const p = this.getPointAtT(t);
|
||||||
|
this.normalPoints.push(p.copy().add(normal));
|
||||||
|
this.antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTrack(t: TrackSystem) {
|
setTrack(t: TrackSystem) {
|
||||||
@ -299,21 +347,17 @@ export class TrackSegment extends PathSegment {
|
|||||||
// });
|
// });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const lineResolution = 100;
|
|
||||||
const normalPoints: Vector[] = [];
|
|
||||||
const antiNormalPoints: Vector[] = [];
|
|
||||||
for (let i = 0; i <= lineResolution; i++) {
|
|
||||||
const t = i / lineResolution;
|
|
||||||
const normal = this.tangent(t).rotate(Math.PI / 2);
|
|
||||||
normal.setMag(6);
|
|
||||||
const p = this.getPointAtT(t);
|
|
||||||
normalPoints.push(p.copy().add(normal));
|
|
||||||
antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
|
|
||||||
}
|
|
||||||
this.doodler.deferDrawing(
|
this.doodler.deferDrawing(
|
||||||
() => {
|
() => {
|
||||||
this.doodler.drawLine(normalPoints, { color: "grey" });
|
this.doodler.drawLine(this.normalPoints, {
|
||||||
this.doodler.drawLine(antiNormalPoints, { color: "grey" });
|
color: "grey",
|
||||||
|
weight: 1.5,
|
||||||
|
});
|
||||||
|
this.doodler.drawLine(this.antiNormalPoints, {
|
||||||
|
color: "grey",
|
||||||
|
weight: 1.5,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// this.doodler.drawCircle(p, 2, {
|
// this.doodler.drawCircle(p, 2, {
|
||||||
@ -438,21 +482,24 @@ export class Spline<T extends PathSegment = PathSegment> {
|
|||||||
|
|
||||||
constructor(segs: T[]) {
|
constructor(segs: T[]) {
|
||||||
this.segments = segs;
|
this.segments = segs;
|
||||||
|
if (this.segments.at(-1)?.next === this.segments[0]) {
|
||||||
|
this.looped = true;
|
||||||
|
}
|
||||||
this.pointSpacing = 1;
|
this.pointSpacing = 1;
|
||||||
this.evenPoints = this.calculateEvenlySpacedPoints(1);
|
this.evenPoints = this.calculateEvenlySpacedPoints(1);
|
||||||
this.nodes = [];
|
this.nodes = [];
|
||||||
for (let i = 0; i < this.points.length; i += 3) {
|
// for (let i = 0; i < this.points.length; i += 3) {
|
||||||
const node: IControlNode = {
|
// const node: IControlNode = {
|
||||||
anchor: this.points[i],
|
// anchor: this.points[i],
|
||||||
controls: [
|
// controls: [
|
||||||
this.points.at(i - 1)!,
|
// this.points.at(i - 1)!,
|
||||||
this.points[(i + 1) % this.points.length],
|
// this.points[(i + 1) % this.points.length],
|
||||||
],
|
// ],
|
||||||
mirrored: false,
|
// mirrored: false,
|
||||||
tangent: true,
|
// tangent: true,
|
||||||
};
|
// };
|
||||||
this.nodes.push(node);
|
// this.nodes.push(node);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// setContext(ctx: CanvasRenderingContext2D) {
|
// setContext(ctx: CanvasRenderingContext2D) {
|
||||||
@ -464,7 +511,11 @@ export class Spline<T extends PathSegment = PathSegment> {
|
|||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
for (const segment of this.segments) {
|
for (const segment of this.segments) {
|
||||||
segment.draw();
|
// segment.draw();
|
||||||
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
|
doodler.drawWithAlpha(0.5, () => {
|
||||||
|
doodler.drawBezier(...segment.points, { color: "red" });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user