Separates game loop from doodler draw loop. Each state is in charge of registering and unregistering layers

This commit is contained in:
Emmaline Autumn 2025-02-13 06:07:42 -07:00
parent 43a5268ed5
commit 3befb69f51
10 changed files with 421 additions and 130 deletions

63
GameLoop.ts Normal file
View 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
View File

@ -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);
})(); })();

View File

@ -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
View File

@ -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
View File

@ -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);

View File

@ -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;
} }

View File

@ -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");

View File

@ -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

View File

@ -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);
}
} }
} }

View File

@ -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" });
});
} }
} }