Compare commits
4 Commits
oldness
...
8dc0af650f
Author | SHA1 | Date | |
---|---|---|---|
8dc0af650f | |||
791ba42ceb | |||
623a324625 | |||
952b5dd57f |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
bundle.js
|
||||
dist/
|
@@ -1,18 +0,0 @@
|
||||
|
||||
type ClickEvent = {
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
}
|
||||
type ClickEventHandler = (e: ClickEvent) => void;
|
||||
|
||||
export class Canvas {
|
||||
clickables: ClickEventHandler[] = [];
|
||||
|
||||
constructor();
|
||||
constructor(width: number, height: number);
|
||||
constructor(width?: number, height?: number) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width || 400;
|
||||
canvas.height = height || 400;
|
||||
}
|
||||
}
|
@@ -10,9 +10,9 @@
|
||||
]
|
||||
},
|
||||
"tasks": {
|
||||
"dev": "deno run -RWEN --allow-run --unstable dev.ts dev"
|
||||
"dev": "deno run -RWEN --allow-run dev.ts dev"
|
||||
},
|
||||
"imports": {
|
||||
"doodler": "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/mod.ts"
|
||||
"@bearmetal/doodler": "jsr:@bearmetal/doodler@^0.0.3"
|
||||
}
|
||||
}
|
38
deno.lock
generated
38
deno.lock
generated
@@ -1,21 +1,30 @@
|
||||
{
|
||||
"version": "4",
|
||||
"specifiers": {
|
||||
"jsr:@bearmetal/doodler@^0.0.3": "0.0.3",
|
||||
"jsr:@luca/esbuild-deno-loader@*": "0.11.0",
|
||||
"jsr:@luca/esbuild-deno-loader@0.11.1": "0.11.1",
|
||||
"jsr:@std/assert@*": "1.0.10",
|
||||
"jsr:@std/assert@^1.0.10": "1.0.10",
|
||||
"jsr:@std/bytes@^1.0.2": "1.0.2",
|
||||
"jsr:@std/cli@^1.0.8": "1.0.9",
|
||||
"jsr:@std/encoding@^1.0.5": "1.0.6",
|
||||
"jsr:@std/fmt@^1.0.3": "1.0.3",
|
||||
"jsr:@std/html@^1.0.3": "1.0.3",
|
||||
"jsr:@std/http@*": "1.0.12",
|
||||
"jsr:@std/internal@^1.0.5": "1.0.5",
|
||||
"jsr:@std/media-types@^1.1.0": "1.1.0",
|
||||
"jsr:@std/net@^1.0.4": "1.0.4",
|
||||
"jsr:@std/path@^1.0.6": "1.0.8",
|
||||
"jsr:@std/path@^1.0.8": "1.0.8",
|
||||
"jsr:@std/streams@^1.0.8": "1.0.8",
|
||||
"jsr:@std/testing@*": "1.0.8",
|
||||
"npm:esbuild@*": "0.24.2"
|
||||
},
|
||||
"jsr": {
|
||||
"@bearmetal/doodler@0.0.3": {
|
||||
"integrity": "42c04b672f4a6bc7ebd45ad936197a2e32856364b66a9a9fe2b81a4aa45c7a08"
|
||||
},
|
||||
"@luca/esbuild-deno-loader@0.11.0": {
|
||||
"integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
|
||||
"dependencies": [
|
||||
@@ -24,6 +33,20 @@
|
||||
"jsr:@std/path@^1.0.6"
|
||||
]
|
||||
},
|
||||
"@luca/esbuild-deno-loader@0.11.1": {
|
||||
"integrity": "dc020d16d75b591f679f6b9288b10f38bdb4f24345edb2f5732affa1d9885267",
|
||||
"dependencies": [
|
||||
"jsr:@std/bytes",
|
||||
"jsr:@std/encoding",
|
||||
"jsr:@std/path@^1.0.6"
|
||||
]
|
||||
},
|
||||
"@std/assert@1.0.10": {
|
||||
"integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/bytes@1.0.2": {
|
||||
"integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57"
|
||||
},
|
||||
@@ -52,6 +75,9 @@
|
||||
"jsr:@std/streams"
|
||||
]
|
||||
},
|
||||
"@std/internal@1.0.5": {
|
||||
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
|
||||
},
|
||||
"@std/media-types@1.1.0": {
|
||||
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
|
||||
},
|
||||
@@ -63,6 +89,13 @@
|
||||
},
|
||||
"@std/streams@1.0.8": {
|
||||
"integrity": "b41332d93d2cf6a82fe4ac2153b930adf1a859392931e2a19d9fabfb6f154fb3"
|
||||
},
|
||||
"@std/testing@1.0.8": {
|
||||
"integrity": "ceef535808fb7568e91b0f8263599bd29b1c5603ffb0377227f00a8ca9fe42a2",
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@^1.0.10",
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
@@ -193,5 +226,10 @@
|
||||
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/timing/EaseInOut.ts": "9eba3d8f5bf5e03220c93916cff6f0bbc24ecdf7550f21fd99e3aaf310f625b0",
|
||||
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/timing/Map.ts": "3948648f8bdf8f1ecea83120c41211f5543c7933dbe3e49b367285a98ed50a9a",
|
||||
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/zoomableCanvas.ts": "395f80ddaef83e2b37a2884d7fffefae80c2bcecb72269405f53899d5dfc9956"
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@bearmetal/doodler@^0.0.3"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
24
dev.ts
24
dev.ts
@@ -1,19 +1,31 @@
|
||||
/// <reference lib="deno.ns" />
|
||||
|
||||
import * as esbuild from "npm:esbuild";
|
||||
import { denoPlugins } from "jsr:@luca/esbuild-deno-loader";
|
||||
import { denoPlugins } from "jsr:@luca/esbuild-deno-loader@0.11.1";
|
||||
import { serveDir } from "jsr:@std/http";
|
||||
|
||||
async function* crawl(dir: string): AsyncIterable<string> {
|
||||
for await (const file of Deno.readDir(dir)) {
|
||||
const fullPath = dir + "/" + file.name;
|
||||
|
||||
if (file.isDirectory) {
|
||||
yield* crawl(fullPath);
|
||||
} else {
|
||||
yield fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function dev() {
|
||||
const paths = [];
|
||||
const ignoredFiles = ["bundler", "bundle", "dev"];
|
||||
const ignoredFiles = ["bundler", "bundle", "dev", "test"];
|
||||
|
||||
for (const path of Deno.readDirSync("./")) {
|
||||
for await (const path of crawl("./")) {
|
||||
if (
|
||||
path.name.endsWith(".ts") &&
|
||||
!ignoredFiles.find((file) => path.name.includes(file))
|
||||
path.endsWith(".ts") &&
|
||||
!ignoredFiles.find((file) => path.includes(file))
|
||||
) {
|
||||
paths.push(path.name);
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
await build();
|
||||
|
@@ -1,16 +0,0 @@
|
||||
import { Constants } from "../math/constants.ts";
|
||||
import { Vector } from "doodler";
|
||||
|
||||
const circle = (ctx: CanvasRenderingContext2D, center: Vector, radius: number) => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(center.x, center.y, radius, 0, Constants.TWO_PI);
|
||||
}
|
||||
|
||||
export const drawCircle = (ctx: CanvasRenderingContext2D, center: Vector, radius: number) => {
|
||||
circle(ctx, center, radius);
|
||||
ctx.stroke();
|
||||
}
|
||||
export const fillCircle = (ctx: CanvasRenderingContext2D, center: Vector, radius: number) => {
|
||||
circle(ctx, center, radius);
|
||||
ctx.fill();
|
||||
}
|
@@ -1 +0,0 @@
|
||||
export { drawCircle, fillCircle } from './circle.ts'
|
@@ -1,6 +0,0 @@
|
||||
export const drawLine = (ctx: CanvasRenderingContext2D, x1:number, y1:number, x2:number, y2: number) => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1,y1);
|
||||
ctx.lineTo(x2,y2);
|
||||
ctx.stroke();
|
||||
}
|
12
index.html
12
index.html
@@ -14,6 +14,18 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
#context {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
12
inputs.ts
Normal file
12
inputs.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getContextItem } from "./lib/context.ts";
|
||||
import { InputManager } from "./lib/input.ts";
|
||||
import { StateMachine } from "./state/machine.ts";
|
||||
import { States } from "./state/states/index.ts";
|
||||
|
||||
export function bootstrapInputs() {
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
inputManager.onKey("e", () => {
|
||||
const state = getContextItem<StateMachine<States>>("state");
|
||||
state.transitionTo(States.EDIT_TRACK);
|
||||
});
|
||||
}
|
@@ -3,6 +3,8 @@ type ContextStore = Record<string, any>;
|
||||
const contextStack: ContextStore[] = [];
|
||||
const defaultContext: ContextStore = {};
|
||||
|
||||
const debug = JSON.parse(localStorage.getItem("debug") || "false");
|
||||
|
||||
export function setDefaultContext(context: ContextStore) {
|
||||
Object.assign(defaultContext, context);
|
||||
}
|
||||
@@ -23,12 +25,63 @@ export const ctx = new Proxy(
|
||||
for (let i = contextStack.length - 1; i >= 0; i--) {
|
||||
if (prop in contextStack[i]) return contextStack[i][prop];
|
||||
}
|
||||
if (prop in defaultContext) return defaultContext[prop]; // ✅ Fallback to default
|
||||
if (prop in defaultContext) return defaultContext[prop];
|
||||
throw new Error(`Context variable '${prop}' is not defined.`);
|
||||
},
|
||||
},
|
||||
) as Record<string, any>;
|
||||
) as Record<string, unknown>;
|
||||
|
||||
export function getContext() {
|
||||
return ctx;
|
||||
}
|
||||
export function getContextItem<T>(prop: string): T {
|
||||
return ctx[prop] as T;
|
||||
}
|
||||
export function setContextItem<T>(prop: string, value: T) {
|
||||
Object.assign(contextStack[contextStack.length - 1] ?? defaultContext, {
|
||||
[prop]: value,
|
||||
});
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
setInterval(() => {
|
||||
let ctxEl = document.getElementById("context");
|
||||
if (!ctxEl) {
|
||||
ctxEl = document.createElement("div");
|
||||
ctxEl.id = "context";
|
||||
document.body.append(ctxEl);
|
||||
}
|
||||
ctxEl.innerHTML = "";
|
||||
const div = document.createElement("div");
|
||||
const pre = document.createElement("pre");
|
||||
const h3 = document.createElement("h3");
|
||||
h3.textContent = "Default";
|
||||
div.append(h3);
|
||||
pre.textContent = safeStringify(defaultContext);
|
||||
div.append(pre);
|
||||
ctxEl.append(div);
|
||||
for (const [idx, ctx] of contextStack.entries()) {
|
||||
const div = document.createElement("div");
|
||||
const pre = document.createElement("pre");
|
||||
const h3 = document.createElement("h3");
|
||||
h3.textContent = "CTX " + idx;
|
||||
div.append(h3);
|
||||
pre.textContent = safeStringify(ctx);
|
||||
div.append(pre);
|
||||
ctxEl.append(div);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function safeStringify(obj: any) {
|
||||
const seen = new WeakSet();
|
||||
return JSON.stringify(obj, (key, value) => {
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return "[Circular]"; // Replace circular references
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
}, 2);
|
||||
}
|
||||
|
42
lib/input.ts
42
lib/input.ts
@@ -1,17 +1,26 @@
|
||||
import { Vector, ZoomableDoodler } from "@bearmetal/doodler";
|
||||
import { getContextItem } from "./context.ts";
|
||||
|
||||
export class InputManager {
|
||||
private keyStates: Map<string | number, boolean> = new Map();
|
||||
private mouseStates: Map<string | number, boolean> = new Map();
|
||||
private mouseLocation: { x: number; y: number } = { x: 0, y: 0 };
|
||||
private mouseDelta: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
private keyEvents: Map<string | number, () => void> = new Map();
|
||||
private mouseEvents: Map<string | number, () => void> = new Map();
|
||||
|
||||
constructor() {
|
||||
document.addEventListener("keydown", (e) => {
|
||||
this.keyStates.set(e.key, true);
|
||||
this.keyEvents.get(e.key)?.call(e);
|
||||
});
|
||||
document.addEventListener("keyup", (e) => {
|
||||
this.keyStates.set(e.key, false);
|
||||
});
|
||||
document.addEventListener("mousedown", (e) => {
|
||||
this.mouseStates.set(e.button, true);
|
||||
this.mouseEvents.get(e.button)?.call(e);
|
||||
});
|
||||
document.addEventListener("mouseup", (e) => {
|
||||
this.mouseStates.set(e.button, false);
|
||||
@@ -33,9 +42,42 @@ export class InputManager {
|
||||
return this.mouseStates.get(key);
|
||||
}
|
||||
getMouseLocation() {
|
||||
if (getContextItem("doodler") instanceof ZoomableDoodler) {
|
||||
return getContextItem<ZoomableDoodler>("doodler").screenToWorld(
|
||||
this.mouseLocation.x,
|
||||
this.mouseLocation.y,
|
||||
);
|
||||
}
|
||||
return this.mouseLocation;
|
||||
}
|
||||
getMouseLocationV() {
|
||||
if (getContextItem("doodler") instanceof ZoomableDoodler) {
|
||||
return new Vector(
|
||||
getContextItem<ZoomableDoodler>("doodler").screenToWorld(
|
||||
this.mouseLocation.x,
|
||||
this.mouseLocation.y,
|
||||
),
|
||||
);
|
||||
}
|
||||
return new Vector(this.mouseLocation);
|
||||
}
|
||||
getMouseDelta() {
|
||||
return this.mouseDelta;
|
||||
}
|
||||
|
||||
onKey(key: string | number, cb: () => void) {
|
||||
this.keyEvents.set(key, cb);
|
||||
}
|
||||
onMouse(key: string | number, cb: () => void) {
|
||||
this.mouseEvents.set(key, cb);
|
||||
}
|
||||
|
||||
offKey(key: string | number) {
|
||||
const events = this.keyEvents.get(key);
|
||||
this.keyEvents.delete(key);
|
||||
return events;
|
||||
}
|
||||
offMouse(key: string | number) {
|
||||
this.mouseEvents.delete(key);
|
||||
}
|
||||
}
|
||||
|
38
lib/resources.ts
Normal file
38
lib/resources.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export class ResourceManager {
|
||||
private resources: Map<string, unknown> = new Map();
|
||||
private statuses: Map<string, Promise<boolean>> = new Map();
|
||||
|
||||
get<T>(name: string): T {
|
||||
if (!this.resources.has(name)) {
|
||||
throw new Error(`Resource ${name} not found`);
|
||||
}
|
||||
return this.resources.get(name) as T;
|
||||
}
|
||||
|
||||
set(name: string, value: unknown) {
|
||||
if (typeof (value as EventSource).addEventListener === "function") {
|
||||
this.statuses.set(
|
||||
name,
|
||||
new Promise((resolve) => {
|
||||
const onload = () => {
|
||||
this.resources.set(name, value);
|
||||
resolve(true);
|
||||
(value as EventSource).removeEventListener("load", onload);
|
||||
};
|
||||
(value as EventSource).addEventListener("load", onload);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.warn("Resource added was not a loadable resource");
|
||||
}
|
||||
this.resources.set(name, value);
|
||||
}
|
||||
|
||||
delete(name: string) {
|
||||
this.resources.delete(name);
|
||||
}
|
||||
|
||||
ready() {
|
||||
return Promise.all(Array.from(this.statuses.values()));
|
||||
}
|
||||
}
|
54
main.ts
54
main.ts
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
getContext,
|
||||
setContextItem,
|
||||
setDefaultContext,
|
||||
} from "./lib/context.ts";
|
||||
import { InputManager } from "./lib/input.ts";
|
||||
|
||||
import { Doodler, Vector, ZoomableDoodler } from "@bearmetal/doodler";
|
||||
import { ResourceManager } from "./lib/resources.ts";
|
||||
import { addButton } from "./ui/button.ts";
|
||||
import { TrackSystem } from "./track/system.ts";
|
||||
import { StraightTrack } from "./track/shapes.ts";
|
||||
import { StateMachine } from "./state/machine.ts";
|
||||
import { bootstrapGameStateMachine } from "./state/states/index.ts";
|
||||
|
||||
const inputManager = new InputManager();
|
||||
const resources = new ResourceManager();
|
||||
const doodler = new ZoomableDoodler({
|
||||
fillScreen: true,
|
||||
bg: "#302040",
|
||||
});
|
||||
// doodler.minScale = 0.1;
|
||||
(doodler as any).scale = doodler.maxScale;
|
||||
|
||||
setDefaultContext({
|
||||
inputManager,
|
||||
doodler,
|
||||
resources,
|
||||
debug: true,
|
||||
showEnds: true,
|
||||
});
|
||||
|
||||
const state = bootstrapGameStateMachine();
|
||||
setContextItem("state", state);
|
||||
|
||||
doodler.init();
|
||||
addButton({
|
||||
text: "Hello World!",
|
||||
onClick: () => {
|
||||
console.log("Hello World!");
|
||||
},
|
||||
at: [
|
||||
new Vector(10, doodler.height - 50),
|
||||
new Vector(110, doodler.height - 10),
|
||||
],
|
||||
style: {
|
||||
fillColor: "blue",
|
||||
color: "white",
|
||||
},
|
||||
});
|
||||
|
||||
doodler.createLayer((_, __, dTime) => {
|
||||
state.update(dTime);
|
||||
});
|
||||
|
13
math/lerp.ts
13
math/lerp.ts
@@ -1,8 +1,11 @@
|
||||
import { Vector } from "doodler";
|
||||
|
||||
export const lerp = (a: number, b: number, t: number) => {
|
||||
return (a * t) + (b * (1 - t));
|
||||
}
|
||||
};
|
||||
|
||||
export const map = (value: number, x1: number, y1: number, x2: number, y2: number) =>
|
||||
(value - x1) * (y2 - x2) / (y1 - x1) + x2;
|
||||
export const map = (
|
||||
value: number,
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;
|
||||
|
141
math/path.ts
141
math/path.ts
@@ -1,7 +1,6 @@
|
||||
import { Vector } from "doodler";
|
||||
import { Vector } from "@bearmetal/doodler";
|
||||
|
||||
export class ComplexPath {
|
||||
|
||||
points: Vector[] = [];
|
||||
|
||||
radius = 50;
|
||||
@@ -22,10 +21,10 @@ export class ComplexPath {
|
||||
|
||||
ctx.save();
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.setLineDash([21, 6])
|
||||
ctx.strokeStyle = "white";
|
||||
ctx.setLineDash([21, 6]);
|
||||
|
||||
let last = this.points[this.points.length - 1]
|
||||
let last = this.points[this.points.length - 1];
|
||||
|
||||
for (const point of this.points) {
|
||||
ctx.beginPath();
|
||||
@@ -39,54 +38,34 @@ export class ComplexPath {
|
||||
}
|
||||
|
||||
export class PathSegment {
|
||||
points: [Vector, Vector, Vector, Vector]
|
||||
ctx?: CanvasRenderingContext2D;
|
||||
points: [Vector, Vector, Vector, Vector];
|
||||
|
||||
length: number;
|
||||
startingLength: number;
|
||||
|
||||
constructor(points: [Vector, Vector, Vector, Vector]) {
|
||||
this.points = points;
|
||||
this.length = this.calculateApproxLength(100);
|
||||
}
|
||||
|
||||
setContext(ctx: CanvasRenderingContext2D) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
draw() {
|
||||
const [a, b, c, d] = this.points;
|
||||
doodler.drawBezier(a, b, c, d, {
|
||||
strokeColor: '#ffffff50'
|
||||
})
|
||||
// if (!this.ctx) return;
|
||||
// const ctx = this.ctx;
|
||||
|
||||
// ctx.save();
|
||||
// ctx.beginPath();
|
||||
// ctx.moveTo(this.points[0].x, this.points[0].y);
|
||||
|
||||
// ctx.bezierCurveTo(
|
||||
// this.points[1].x,
|
||||
// this.points[1].y,
|
||||
// this.points[2].x,
|
||||
// this.points[2].y,
|
||||
// this.points[3].x,
|
||||
// this.points[3].y,
|
||||
// );
|
||||
|
||||
// ctx.strokeStyle = '#ffffff50';
|
||||
// ctx.lineWidth = 2;
|
||||
// ctx.stroke();
|
||||
// ctx.restore();
|
||||
this.startingLength = Math.round(this.length);
|
||||
}
|
||||
|
||||
getPointAtT(t: number) {
|
||||
const [a, b, c, d] = this.points;
|
||||
const res = a.copy();
|
||||
|
||||
res.add(Vector.add(a.copy().mult(-3), b.copy().mult(3)).mult(t))
|
||||
res.add(Vector.add(Vector.add(a.copy().mult(3), b.copy().mult(-6)), c.copy().mult(3)).mult(Math.pow(t, 2)));
|
||||
res.add(Vector.add(Vector.add(a.copy().mult(-1), b.copy().mult(3)), Vector.add(c.copy().mult(-3), d.copy())).mult(Math.pow(t, 3)));
|
||||
res.add(Vector.add(a.copy().mult(-3), b.copy().mult(3)).mult(t));
|
||||
res.add(
|
||||
Vector.add(
|
||||
Vector.add(a.copy().mult(3), b.copy().mult(-6)),
|
||||
c.copy().mult(3),
|
||||
).mult(Math.pow(t, 2)),
|
||||
);
|
||||
res.add(
|
||||
Vector.add(
|
||||
Vector.add(a.copy().mult(-1), b.copy().mult(3)),
|
||||
Vector.add(c.copy().mult(-3), d.copy()),
|
||||
).mult(Math.pow(t, 3)),
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -123,7 +102,7 @@ export class PathSegment {
|
||||
points.push([i * resolution, this]);
|
||||
}
|
||||
}
|
||||
return points
|
||||
return points;
|
||||
}
|
||||
|
||||
tangent(t: number) {
|
||||
@@ -131,7 +110,12 @@ export class PathSegment {
|
||||
const [a, b, c, d] = this.points;
|
||||
|
||||
const res = Vector.sub(b, a).mult(3 * Math.pow(1 - t, 2));
|
||||
res.add(Vector.add(Vector.sub(c, b).mult(6 * (1 - t) * t), Vector.sub(d, c).mult(3 * Math.pow(t, 2))));
|
||||
res.add(
|
||||
Vector.add(
|
||||
Vector.sub(c, b).mult(6 * (1 - t) * t),
|
||||
Vector.sub(d, c).mult(3 * Math.pow(t, 2)),
|
||||
),
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -158,12 +142,12 @@ export class PathSegment {
|
||||
|
||||
let dist;
|
||||
if (k <= 0.0) {
|
||||
dist = Vector.hypot2(v, a)
|
||||
dist = Vector.hypot2(v, a);
|
||||
} else if (k >= 1.0) {
|
||||
dist = Vector.hypot2(v, b)
|
||||
dist = Vector.hypot2(v, b);
|
||||
}
|
||||
|
||||
dist = Vector.hypot2(v, d)
|
||||
dist = Vector.hypot2(v, d);
|
||||
|
||||
if (dist < distance) {
|
||||
distance = dist;
|
||||
@@ -179,27 +163,32 @@ export class PathSegment {
|
||||
|
||||
calculateApproxLength(resolution = 25) {
|
||||
const stepSize = 1 / resolution;
|
||||
const points: Vector[] = []
|
||||
const points: Vector[] = [];
|
||||
for (let i = 0; i <= resolution; i++) {
|
||||
const current = stepSize * i;
|
||||
points.push(this.getPointAtT(current))
|
||||
points.push(this.getPointAtT(current));
|
||||
}
|
||||
this.length = points.reduce((acc: { prev?: Vector, length: number }, cur) => {
|
||||
this.length =
|
||||
points.reduce((acc: { prev?: Vector; length: number }, cur) => {
|
||||
const prev = acc.prev;
|
||||
acc.prev = cur;
|
||||
if (!prev) return acc;
|
||||
acc.length += cur.dist(prev);
|
||||
return acc;
|
||||
}, { prev: undefined, length: 0 }).length
|
||||
}, { prev: undefined, length: 0 }).length;
|
||||
return this.length;
|
||||
}
|
||||
|
||||
calculateEvenlySpacedPoints(spacing: number, resolution = 1) {
|
||||
const points: Vector[] = []
|
||||
calculateEvenlySpacedPoints(
|
||||
spacing: number,
|
||||
resolution = 1,
|
||||
targetLength?: number,
|
||||
) {
|
||||
const points: Vector[] = [];
|
||||
|
||||
points.push(this.points[0]);
|
||||
let prev = points[0];
|
||||
let distSinceLastEvenPoint = 0
|
||||
let distSinceLastEvenPoint = 0;
|
||||
|
||||
let t = 0;
|
||||
|
||||
@@ -209,10 +198,12 @@ export class PathSegment {
|
||||
const point = this.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))
|
||||
const evenPoint = Vector.add(
|
||||
point,
|
||||
Vector.sub(point, prev).normalize().mult(overshoot),
|
||||
);
|
||||
distSinceLastEvenPoint = overshoot;
|
||||
points.push(evenPoint);
|
||||
prev = evenPoint;
|
||||
@@ -221,6 +212,46 @@ export class PathSegment {
|
||||
prev = point;
|
||||
}
|
||||
|
||||
if (targetLength && points.length < targetLength) {
|
||||
while (points.length < targetLength) {
|
||||
t += 1 / div;
|
||||
const point = this.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;
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
calculateSubdividedPoints(numberOfPoints: number) {
|
||||
const points: Vector[] = [];
|
||||
|
||||
for (let i = 0; i < numberOfPoints; i++) {
|
||||
const point = this.getPointAtT(i / numberOfPoints);
|
||||
points.push(point);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
clampLength() {
|
||||
const curveLength = this.startingLength;
|
||||
const points = this.calculateEvenlySpacedPoints(1, 1, curveLength + 1);
|
||||
if (points.length >= curveLength) {
|
||||
this.points[3].set(points[curveLength]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
76
state/machine.ts
Normal file
76
state/machine.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
export class StateMachine<T> {
|
||||
private _states: Map<T, State<T>> = new Map();
|
||||
private currentState?: State<T>;
|
||||
|
||||
update(dt: number, ctx?: CanvasRenderingContext2D) {
|
||||
this.currentState?.update(dt, ctx);
|
||||
}
|
||||
|
||||
get current() {
|
||||
return this.currentState;
|
||||
}
|
||||
|
||||
get states() {
|
||||
return this._states;
|
||||
}
|
||||
|
||||
addState(state: State<T>) {
|
||||
this.states.set(state.name, state);
|
||||
}
|
||||
|
||||
transitionTo(state: T) {
|
||||
if (!this.current) {
|
||||
this.currentState = this._states.get(state)!;
|
||||
this.currentState.start();
|
||||
return;
|
||||
}
|
||||
if (this.current?.canTransitionTo(state) && this._states.has(state)) {
|
||||
this.current.stop();
|
||||
this.currentState = this._states.get(state)!;
|
||||
this.current.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class State<T> {
|
||||
protected stateMachine: StateMachine<T>;
|
||||
protected abstract validTransitions: Set<T>;
|
||||
|
||||
abstract readonly name: T;
|
||||
|
||||
constructor(
|
||||
stateMachine: StateMachine<T>,
|
||||
) {
|
||||
this.stateMachine = stateMachine;
|
||||
}
|
||||
|
||||
abstract update(dt: number, ctx?: CanvasRenderingContext2D): void;
|
||||
abstract start(): void;
|
||||
abstract stop(): void;
|
||||
|
||||
canTransitionTo(state: T) {
|
||||
return this.validTransitions.has(state);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class ExtensibleState<T> extends State<T> {
|
||||
extensions: Map<string, (...args: unknown[]) => void> = new Map();
|
||||
registerExtension(name: string, cb: (...args: unknown[]) => void) {
|
||||
this.extensions.set(name, cb);
|
||||
}
|
||||
|
||||
constructor(stateMachine: StateMachine<T>) {
|
||||
super(stateMachine);
|
||||
const oldUpdate = this.update;
|
||||
this.update = function (dt: number, ctx?: CanvasRenderingContext2D) {
|
||||
oldUpdate.apply(this, [dt, ctx]);
|
||||
this.runExtensions(dt, ctx);
|
||||
};
|
||||
}
|
||||
|
||||
runExtensions(...args: unknown[]) {
|
||||
for (const [name, cb] of this.extensions) {
|
||||
cb(...args);
|
||||
}
|
||||
}
|
||||
}
|
165
state/states/EditTrackState.ts
Normal file
165
state/states/EditTrackState.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||
import { getContextItem, setContextItem } from "../../lib/context.ts";
|
||||
import { InputManager } from "../../lib/input.ts";
|
||||
import { TrackSystem } from "../../track/system.ts";
|
||||
import { State, StateMachine } from "../machine.ts";
|
||||
import { States } from "./index.ts";
|
||||
import { StraightTrack } from "../../track/shapes.ts";
|
||||
import { TrackSegment } from "../../track/system.ts";
|
||||
|
||||
export class EditTrackState extends State<States> {
|
||||
override name: States = States.EDIT_TRACK;
|
||||
override validTransitions: Set<States> = new Set([
|
||||
States.RUNNING,
|
||||
States.PAUSED,
|
||||
]);
|
||||
|
||||
private heldEvents: Map<string | number, (() => void) | undefined> =
|
||||
new Map();
|
||||
|
||||
private currentSegment?: TrackSegment;
|
||||
|
||||
override update(dt: number): void {
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
const track = getContextItem<TrackSystem>("track");
|
||||
|
||||
// For moving a segment, i.e. the currently active one
|
||||
// const segment = track.lastSegment;
|
||||
// if (segment) {
|
||||
// const firstPoint = segment.points[0].copy();
|
||||
// const { x, y } = inputManager.getMouseLocation();
|
||||
// segment.points.forEach((p, i) => {
|
||||
// const relativePoint = Vector.sub(p, firstPoint);
|
||||
// p.set(x, y);
|
||||
// p.add(relativePoint);
|
||||
// });
|
||||
// }
|
||||
|
||||
// manipulate only end of segment while maintaining length
|
||||
// const segment = track.lastSegment;
|
||||
// if (segment) {
|
||||
// const p3 = segment.points[2];
|
||||
// const p4 = segment.points[3];
|
||||
// let curveLength = Math.round(segment.calculateApproxLength());
|
||||
// this.startingLength = this.startingLength ?? curveLength;
|
||||
// curveLength = this.startingLength;
|
||||
// const { x, y } = inputManager.getMouseLocation();
|
||||
// p4.set(x, y);
|
||||
// const points = segment.calculateEvenlySpacedPoints(1);
|
||||
// if (points.length > curveLength) p4.set(points[curveLength - 1]);
|
||||
|
||||
// // doodler.fillText(curveLength.toFixed(2), p3.copy().add(10, 0), 100);
|
||||
// }
|
||||
const doodler = getContextItem<Doodler>("doodler");
|
||||
|
||||
// Adjust angles until tangent points to mouse
|
||||
const segment = this.currentSegment;
|
||||
if (segment) {
|
||||
segment.propagate();
|
||||
|
||||
const mousePos = inputManager.getMouseLocationV();
|
||||
const p1 = segment.points[0];
|
||||
const p2 = segment.points[1];
|
||||
const p3 = segment.points[2];
|
||||
const p4 = segment.points[3];
|
||||
|
||||
const prevp3 = p3.copy();
|
||||
const dirToMouse = Vector.sub(mousePos, p2).normalize();
|
||||
const angleToMouse = dirToMouse.heading();
|
||||
const angleToP1 = Vector.sub(p2, p1).heading();
|
||||
const p2DistToMouse = Vector.dist(p2, mousePos);
|
||||
const p3DistToMouse = Vector.dist(p3, mousePos);
|
||||
const distToP3 = Vector.dist(p2, p3);
|
||||
const distToP4 = Vector.dist(prevp3, p4);
|
||||
if (
|
||||
Math.abs(angleToMouse - angleToP1) < .6 &&
|
||||
p2DistToMouse > distToP3 &&
|
||||
p3DistToMouse > distToP4
|
||||
) {
|
||||
{
|
||||
const dirToNewP3 = dirToMouse.copy().rotate(
|
||||
-(angleToMouse - angleToP1) / 2,
|
||||
);
|
||||
dirToNewP3.setMag(distToP3);
|
||||
p3.set(Vector.add(p2, dirToNewP3));
|
||||
doodler.line(p2, Vector.add(p2, dirToNewP3), { color: "blue" });
|
||||
doodler.line(p2, Vector.add(p2, dirToMouse.mult(100)), {
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
{
|
||||
const dirToMouse = Vector.sub(mousePos, p3).normalize();
|
||||
dirToMouse.setMag(distToP4);
|
||||
p4.set(Vector.add(p3, dirToMouse));
|
||||
doodler.line(p3, Vector.add(p3, dirToMouse), { color: "green" });
|
||||
}
|
||||
segment.clampLength();
|
||||
}
|
||||
doodler.fillText(
|
||||
segment.calculateApproxLength().toFixed(2),
|
||||
p2.copy().add(10, 0),
|
||||
100,
|
||||
);
|
||||
}
|
||||
|
||||
track.draw(true);
|
||||
// TODO
|
||||
// Draw ui
|
||||
// Draw track points
|
||||
// Draw track tangents
|
||||
}
|
||||
override start(): void {
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
this.heldEvents.set("e", inputManager.offKey("e"));
|
||||
this.heldEvents.set("Escape", inputManager.offKey("Escape"));
|
||||
inputManager.onKey("e", () => {
|
||||
const state = getContextItem<StateMachine<States>>("state");
|
||||
state.transitionTo(States.RUNNING);
|
||||
});
|
||||
|
||||
const track = getContextItem<TrackSystem>("track");
|
||||
setContextItem("trackCopy", track.copy());
|
||||
inputManager.onKey("Escape", () => {
|
||||
const trackCopy = getContextItem<TrackSystem>("trackCopy");
|
||||
setContextItem("track", trackCopy);
|
||||
setContextItem("trackCopy", undefined);
|
||||
const state = getContextItem<StateMachine<States>>("state");
|
||||
state.transitionTo(States.RUNNING);
|
||||
});
|
||||
|
||||
inputManager.onKey("w", () => {
|
||||
const track = getContextItem<TrackSystem>("track");
|
||||
const segment = track.lastSegment;
|
||||
if (!segment) return;
|
||||
const n = new StraightTrack(segment.points[3]);
|
||||
const t = segment.tangent(1).heading();
|
||||
n.rotate(t);
|
||||
segment.frontNeighbours.push(n);
|
||||
track.registerSegment(n);
|
||||
this.currentSegment = n;
|
||||
});
|
||||
|
||||
inputManager.onKey("1", () => {
|
||||
this.currentSegment = track.firstSegment;
|
||||
});
|
||||
|
||||
this.currentSegment = track.lastSegment;
|
||||
|
||||
// TODO
|
||||
// Cache trains and save
|
||||
}
|
||||
override stop(): void {
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
inputManager.offKey("e");
|
||||
inputManager.offKey("Escape");
|
||||
if (this.heldEvents.size > 0) {
|
||||
for (const [key, cb] of this.heldEvents) {
|
||||
if (cb) {
|
||||
getContextItem<InputManager>("inputManager").onKey(key, cb);
|
||||
}
|
||||
this.heldEvents.delete(key);
|
||||
}
|
||||
}
|
||||
setContextItem("trackCopy", undefined);
|
||||
}
|
||||
}
|
25
state/states/EditTrainState.ts
Normal file
25
state/states/EditTrainState.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { State } from "../machine.ts";
|
||||
import { States } from "./index.ts";
|
||||
|
||||
export class EditTrainState extends State<States> {
|
||||
override name: States = States.EDIT_TRAIN;
|
||||
override validTransitions: Set<States> = new Set([
|
||||
States.RUNNING,
|
||||
States.PAUSED,
|
||||
]);
|
||||
|
||||
override update(dt: number): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override start(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
// TODO
|
||||
// Cache trains
|
||||
// Stash train in context
|
||||
// Draw track
|
||||
// Draw train (filtered by train ID)
|
||||
}
|
||||
override stop(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
51
state/states/LoadState.ts
Normal file
51
state/states/LoadState.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { bootstrapInputs } from "../../inputs.ts";
|
||||
import { setContextItem } from "../../lib/context.ts";
|
||||
import { InputManager } from "../../lib/input.ts";
|
||||
import { ResourceManager } from "../../lib/resources.ts";
|
||||
import { StraightTrack } from "../../track/shapes.ts";
|
||||
import { TrackSystem } from "../../track/system.ts";
|
||||
import { State } from "../machine.ts";
|
||||
import { States } from "./index.ts";
|
||||
|
||||
export class LoadState extends State<States> {
|
||||
override name: States = States.LOAD;
|
||||
override validTransitions: Set<States> = new Set([
|
||||
States.RUNNING,
|
||||
]);
|
||||
|
||||
override update(): void {
|
||||
// noop
|
||||
}
|
||||
override start(): void {
|
||||
const track = this.loadTrack() ?? new TrackSystem([new StraightTrack()]);
|
||||
setContextItem("track", track);
|
||||
|
||||
const trains = this.loadTrains() ?? [];
|
||||
setContextItem("trains", trains);
|
||||
|
||||
const resources = new ResourceManager();
|
||||
setContextItem("resources", resources);
|
||||
|
||||
const inputManager = new InputManager();
|
||||
setContextItem("inputManager", inputManager);
|
||||
|
||||
bootstrapInputs();
|
||||
|
||||
this.stateMachine.transitionTo(States.RUNNING);
|
||||
}
|
||||
override stop(): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
private loadTrack() {
|
||||
const track = TrackSystem.deserialize(
|
||||
JSON.parse(localStorage.getItem("track") || "[]"),
|
||||
);
|
||||
return track;
|
||||
}
|
||||
|
||||
private loadTrains() {
|
||||
const trains = JSON.parse(localStorage.getItem("trains") || "[]");
|
||||
return trains;
|
||||
}
|
||||
}
|
30
state/states/PausedState.ts
Normal file
30
state/states/PausedState.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { State } from "../machine.ts";
|
||||
import { States } from "./index.ts";
|
||||
|
||||
export class PausedState extends State<States> {
|
||||
override name: States = States.PAUSED;
|
||||
override validTransitions: Set<States> = new Set([
|
||||
States.LOAD,
|
||||
States.RUNNING,
|
||||
States.EDIT_TRACK,
|
||||
States.EDIT_TRAIN,
|
||||
]);
|
||||
override update(dt: number): void {
|
||||
throw new Error("Method not implemented.");
|
||||
// TODO
|
||||
// Handle input
|
||||
// Draw ui
|
||||
}
|
||||
override start(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
// TODO
|
||||
// Save tracks to cache
|
||||
// Save trains to cache
|
||||
// Save resources to cache
|
||||
}
|
||||
override stop(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
// TODO
|
||||
// Do nothing
|
||||
}
|
||||
}
|
32
state/states/RunningState.ts
Normal file
32
state/states/RunningState.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getContext } from "../../lib/context.ts";
|
||||
import { TrackSystem } from "../../track/system.ts";
|
||||
import { Train } from "../../train/train.ts";
|
||||
import { State } from "../machine.ts";
|
||||
import { States } from "./index.ts";
|
||||
|
||||
export class RunningState extends State<States> {
|
||||
override name: States = States.RUNNING;
|
||||
override validTransitions: Set<States> = new Set([
|
||||
States.PAUSED,
|
||||
States.EDIT_TRACK,
|
||||
]);
|
||||
override update(dt: number): void {
|
||||
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
|
||||
// TODO
|
||||
// Update trains
|
||||
// Update world
|
||||
// Handle input
|
||||
// Draw (maybe via a layer system that syncs with doodler)
|
||||
ctx.track.draw();
|
||||
for (const train of ctx.trains) {
|
||||
train.draw();
|
||||
}
|
||||
// Monitor world events
|
||||
}
|
||||
override start(): void {
|
||||
// noop
|
||||
}
|
||||
override stop(): void {
|
||||
// noop
|
||||
}
|
||||
}
|
26
state/states/index.ts
Normal file
26
state/states/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { StateMachine } from "../machine.ts";
|
||||
import { Track } from "../../track.ts";
|
||||
import { EditTrainState } from "./EditTrainState.ts";
|
||||
import { EditTrackState } from "./EditTrackState.ts";
|
||||
import { PausedState } from "./PausedState.ts";
|
||||
import { RunningState } from "./RunningState.ts";
|
||||
import { LoadState } from "./LoadState.ts";
|
||||
|
||||
export enum States {
|
||||
LOAD,
|
||||
RUNNING,
|
||||
PAUSED,
|
||||
EDIT_TRACK,
|
||||
EDIT_TRAIN,
|
||||
}
|
||||
|
||||
export function bootstrapGameStateMachine() {
|
||||
const stateMachine = new StateMachine<States>();
|
||||
stateMachine.addState(new LoadState(stateMachine));
|
||||
stateMachine.addState(new RunningState(stateMachine));
|
||||
stateMachine.addState(new PausedState(stateMachine));
|
||||
stateMachine.addState(new EditTrackState(stateMachine));
|
||||
stateMachine.addState(new EditTrainState(stateMachine));
|
||||
stateMachine.transitionTo(States.LOAD);
|
||||
return stateMachine;
|
||||
}
|
38
test/bench.ts
Normal file
38
test/bench.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { assert } from "jsr:@std/assert";
|
||||
import { describe, it } from "jsr:@std/testing/bdd";
|
||||
|
||||
/**
|
||||
* Tests if a function can run a given number of iterations within a target frame time.
|
||||
* @param fn The function to test.
|
||||
* @param iterations Number of times to run the function per frame.
|
||||
* @param fps Target frames per second.
|
||||
*/
|
||||
export function testPerformance(
|
||||
fn: () => unknown,
|
||||
iterations: number,
|
||||
fps: number,
|
||||
) {
|
||||
console.log(`Performance Test - ${iterations} iterations at ${fps} FPS`);
|
||||
const frameTime = 1000 / fps;
|
||||
const startTime = performance.now();
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
fn();
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const elapsed = endTime - startTime;
|
||||
|
||||
console.log(
|
||||
`Elapsed time: ${elapsed.toFixed(2)}ms (Target: ≤${
|
||||
frameTime.toFixed(2)
|
||||
}ms)`,
|
||||
);
|
||||
assert(
|
||||
elapsed <= frameTime,
|
||||
`Function took too long: ${elapsed.toFixed(2)}ms (Target: ≤${
|
||||
frameTime.toFixed(2)
|
||||
}ms)`,
|
||||
);
|
||||
// });
|
||||
}
|
1455
test/bundle.js
1455
test/bundle.js
File diff suppressed because it is too large
Load Diff
49
test/contextBench.test.js
Normal file
49
test/contextBench.test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
getContextItem,
|
||||
setContextItem,
|
||||
setDefaultContext,
|
||||
withContext,
|
||||
} from "../lib/context.ts"; // adjust path as needed
|
||||
import { testPerformance } from "./bench.ts";
|
||||
|
||||
/**
|
||||
* Benchmarks the performance of setting and getting context items.
|
||||
* All context transactions should run 10000 times within the 60 FPS frame time.
|
||||
* getContextItem should run 100000 times within the 240 FPS frame time to ensure adequate performance.
|
||||
*/
|
||||
Deno.test("Context Benchmark", () => {
|
||||
console.log("Context Benchmark - run within frame time");
|
||||
testPerformance(
|
||||
() => {
|
||||
setDefaultContext({ a: 1 });
|
||||
},
|
||||
10000,
|
||||
60,
|
||||
);
|
||||
|
||||
testPerformance(
|
||||
() => {
|
||||
withContext({ a: 1 }, () => {
|
||||
getContextItem("a");
|
||||
});
|
||||
},
|
||||
10000,
|
||||
60,
|
||||
);
|
||||
|
||||
testPerformance(
|
||||
() => {
|
||||
getContextItem("a");
|
||||
},
|
||||
100000,
|
||||
240,
|
||||
);
|
||||
|
||||
testPerformance(
|
||||
() => {
|
||||
setContextItem("a", 1);
|
||||
},
|
||||
10000,
|
||||
60,
|
||||
);
|
||||
});
|
14
track/shapes.ts
Normal file
14
track/shapes.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Vector } from "@bearmetal/doodler";
|
||||
import { TrackSegment } from "./system.ts";
|
||||
|
||||
export class StraightTrack extends TrackSegment {
|
||||
constructor(start?: Vector) {
|
||||
start = start || new Vector(100, 100);
|
||||
super([
|
||||
start,
|
||||
start.copy().add(25, 0),
|
||||
start.copy().add(75, 0),
|
||||
start.copy().add(100, 0),
|
||||
]);
|
||||
}
|
||||
}
|
234
track/system.ts
Normal file
234
track/system.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||
import { PathSegment } from "../math/path.ts";
|
||||
import { getContextItem, setDefaultContext } from "../lib/context.ts";
|
||||
|
||||
export class TrackSystem {
|
||||
private segments: Map<string, TrackSegment> = new Map();
|
||||
private doodler: Doodler;
|
||||
|
||||
constructor(segments: TrackSegment[]) {
|
||||
this.doodler = getContextItem<Doodler>("doodler");
|
||||
for (const segment of segments) {
|
||||
this.segments.set(segment.id, segment);
|
||||
}
|
||||
}
|
||||
|
||||
get firstSegment() {
|
||||
return this.segments.values().next().value;
|
||||
}
|
||||
|
||||
get lastSegment() {
|
||||
return this.segments.values().toArray().pop();
|
||||
}
|
||||
|
||||
registerSegment(segment: TrackSegment) {
|
||||
segment.setTrack(this);
|
||||
this.segments.set(segment.id, segment);
|
||||
}
|
||||
|
||||
draw(showControls = false) {
|
||||
for (const segment of this.segments.values()) {
|
||||
segment.draw(showControls);
|
||||
}
|
||||
|
||||
try {
|
||||
if (getContextItem<boolean>("showEnds")) {
|
||||
const ends = this.findEnds();
|
||||
for (const end of ends) {
|
||||
this.doodler.fillCircle(end.pos, 2, {
|
||||
color: "red",
|
||||
// weight: 3,
|
||||
});
|
||||
if (getContextItem<boolean>("debug")) {
|
||||
this.doodler.line(
|
||||
end.pos,
|
||||
end.pos.copy().add(end.tangent.copy().mult(20)),
|
||||
{
|
||||
color: "blue",
|
||||
// weight: 3,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setDefaultContext({ showEnds: false });
|
||||
}
|
||||
}
|
||||
|
||||
findEnds() {
|
||||
const ends: { pos: Vector; segment: TrackSegment; tangent: Vector }[] = [];
|
||||
for (const segment of this.segments.values()) {
|
||||
const [a, b, c, d] = segment.points;
|
||||
{
|
||||
const tangent = Vector.sub(a, b).normalize();
|
||||
const pos = a.copy();
|
||||
ends.push({ pos, segment, tangent });
|
||||
}
|
||||
{
|
||||
const tangent = Vector.sub(d, c).normalize();
|
||||
const pos = d.copy();
|
||||
ends.push({ pos, segment, tangent });
|
||||
}
|
||||
}
|
||||
return ends;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return this.segments.values().map((s) => s.serialize()).toArray();
|
||||
}
|
||||
|
||||
copy() {
|
||||
const track = new TrackSystem([]);
|
||||
for (const segment of this.segments.values()) {
|
||||
track.segments.set(segment.id, segment.copy());
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
static deserialize(data: any) {
|
||||
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));
|
||||
}
|
||||
for (const segment of track.segments.values()) {
|
||||
segment.setTrack(track);
|
||||
const neighbors = neighborMap.get(segment.id);
|
||||
if (neighbors) {
|
||||
segment.backNeighbours = neighbors[1].map((id) =>
|
||||
track.segments.get(id)
|
||||
).filter((s) => s) as TrackSegment[];
|
||||
segment.frontNeighbours = neighbors[0].map((id) =>
|
||||
track.segments.get(id)
|
||||
).filter((s) => s) as TrackSegment[];
|
||||
}
|
||||
}
|
||||
return track;
|
||||
}
|
||||
}
|
||||
|
||||
type VectorSet = [Vector, Vector, Vector, Vector];
|
||||
|
||||
export class TrackSegment extends PathSegment {
|
||||
frontNeighbours: TrackSegment[] = [];
|
||||
backNeighbours: TrackSegment[] = [];
|
||||
|
||||
track?: TrackSystem;
|
||||
|
||||
doodler: Doodler;
|
||||
|
||||
id: string;
|
||||
|
||||
constructor(p: VectorSet, id?: string) {
|
||||
super(p);
|
||||
this.doodler = getContextItem<Doodler>("doodler");
|
||||
this.id = id ?? crypto.randomUUID();
|
||||
}
|
||||
|
||||
setTrack(t: TrackSystem) {
|
||||
this.track = t;
|
||||
}
|
||||
|
||||
draw(showControls = false) {
|
||||
this.doodler.drawBezier(
|
||||
this.points[0],
|
||||
this.points[1],
|
||||
this.points[2],
|
||||
this.points[3],
|
||||
{
|
||||
strokeColor: "#ffffff50",
|
||||
},
|
||||
);
|
||||
if (showControls) {
|
||||
// this.doodler.drawCircle(this.points[0], 4, {
|
||||
// color: "red",
|
||||
// weight: 3,
|
||||
// });
|
||||
this.doodler.drawCircle(this.points[1], 4, {
|
||||
color: "red",
|
||||
weight: 3,
|
||||
});
|
||||
this.doodler.drawCircle(this.points[2], 4, {
|
||||
color: "red",
|
||||
weight: 3,
|
||||
});
|
||||
// this.doodler.drawCircle(this.points[3], 4, {
|
||||
// color: "red",
|
||||
// weight: 3,
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
p: this.points.map((p) => p.array()),
|
||||
id: this.id,
|
||||
bNeighbors: this.backNeighbours.map((n) => n.id),
|
||||
fNeighbors: this.frontNeighbours.map((n) => n.id),
|
||||
};
|
||||
}
|
||||
|
||||
copy() {
|
||||
return new TrackSegment(
|
||||
this.points.map((p) => p.copy()) as VectorSet,
|
||||
this.id,
|
||||
);
|
||||
}
|
||||
|
||||
propagate() {
|
||||
const [_, __, p3, p4] = this.points;
|
||||
const tangent = Vector.sub(p4, p3);
|
||||
for (const fNeighbour of this.frontNeighbours) {
|
||||
fNeighbour.receivePropagation(tangent);
|
||||
}
|
||||
}
|
||||
|
||||
lastHeading?: number;
|
||||
|
||||
receivePropagation(tangent: Vector) {
|
||||
const [p1, p2, p3, p4] = this.points;
|
||||
// const angle = tangent.heading() - (this.lastHeading ?? 0);
|
||||
// this.lastHeading = tangent.heading();
|
||||
// const newP2 = Vector.add(p1, tangent);
|
||||
// const p2ToP3 = Vector.sub(p3, p2);
|
||||
// p2ToP3.rotate(angle);
|
||||
// p3.set(Vector.add(newP2, p2ToP3));
|
||||
// const p2Top4 = Vector.sub(p4, p2);
|
||||
// p2Top4.rotate(angle);
|
||||
// p4.set(Vector.add(newP2, p2Top4));
|
||||
// p2.set(newP2);
|
||||
this.rotate(tangent);
|
||||
this.propagate();
|
||||
}
|
||||
|
||||
// TODO: this duplicates receivePropagation, but for some reason it doesn't work when called from receivePropagation
|
||||
rotate(angle: number | Vector) {
|
||||
const [p1, p2, p3, p4] = this.points;
|
||||
let newP2;
|
||||
if (angle instanceof Vector) {
|
||||
const tan = angle;
|
||||
angle = tan.heading() - (this.lastHeading ?? 0);
|
||||
this.lastHeading = tan.heading();
|
||||
newP2 = Vector.add(p1, tan);
|
||||
} else {
|
||||
const p1ToP2 = Vector.sub(p2, p1);
|
||||
p1ToP2.rotate(angle);
|
||||
newP2 = Vector.add(p1, p1ToP2);
|
||||
}
|
||||
const p2ToP3 = Vector.sub(p3, p2);
|
||||
p2ToP3.rotate(angle);
|
||||
p3.set(Vector.add(newP2, p2ToP3));
|
||||
const p2Top4 = Vector.sub(p4, p2);
|
||||
p2Top4.rotate(angle);
|
||||
p4.set(Vector.add(newP2, p2Top4));
|
||||
p2.set(newP2);
|
||||
}
|
||||
static deserialize(data: any) {
|
||||
return new TrackSegment(
|
||||
data.p.map((p: [number, number, number]) => new Vector(p[0], p[1], p[2])),
|
||||
data.id,
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
import { drawLine } from "../drawing/line.ts";
|
||||
import { ComplexPath, PathSegment } from "../math/path.ts";
|
||||
import { Vector } from "doodler";
|
||||
import { Follower } from "../physics/follower.ts";
|
||||
import { Mover } from "../physics/mover.ts";
|
||||
import { Spline, Track } from "../track.ts";
|
||||
import { getContextItem } from "../lib/context.ts";
|
||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||
|
||||
export class Train {
|
||||
nodes: Vector[] = [];
|
||||
@@ -83,6 +83,12 @@ export class Train {
|
||||
// }
|
||||
// }
|
||||
|
||||
draw() {
|
||||
for (const car of this.cars) {
|
||||
car.draw();
|
||||
}
|
||||
}
|
||||
|
||||
real2Track(length: number) {
|
||||
return length / this.path.pointSpacing;
|
||||
}
|
||||
@@ -113,6 +119,7 @@ export class TrainCar {
|
||||
|
||||
draw() {
|
||||
if (!this.points) return;
|
||||
const doodler = getContextItem<Doodler>("doodler");
|
||||
const [a, b] = this.points;
|
||||
const origin = Vector.add(Vector.sub(a, b).div(2), b);
|
||||
const angle = Vector.sub(b, a).heading();
|
||||
|
39
ui/button.ts
Normal file
39
ui/button.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||
import { getContext, getContextItem } from "../lib/context.ts";
|
||||
|
||||
export function addButton(props: {
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
style?: {
|
||||
color?: string;
|
||||
fillColor?: string;
|
||||
strokeColor?: string;
|
||||
weight?: number;
|
||||
noStroke?: boolean;
|
||||
noFill?: boolean;
|
||||
};
|
||||
at: [Vector, Vector];
|
||||
}) {
|
||||
const doodler = getContextItem<Doodler>("doodler");
|
||||
const { text, onClick, style } = props;
|
||||
const { x, y } = props.at[1].copy().sub(props.at[0]);
|
||||
const id = doodler.addUIElement(
|
||||
"rectangle",
|
||||
props.at[0],
|
||||
x,
|
||||
y,
|
||||
style,
|
||||
);
|
||||
doodler.registerClickable(props.at[0], props.at[1], onClick);
|
||||
return {
|
||||
id,
|
||||
text,
|
||||
onClick,
|
||||
style,
|
||||
};
|
||||
}
|
||||
|
||||
export function removeButton(id: string, onClick: () => void) {
|
||||
getContextItem<Doodler>("doodler").removeUIElement(id);
|
||||
getContextItem<Doodler>("doodler").unregisterClickable(onClick);
|
||||
}
|
Reference in New Issue
Block a user