From 43a5268ed5576632b184f6ddf2b4f693898507c2 Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 13 Feb 2025 03:23:37 -0700 Subject: [PATCH] track drawing and shape tweaks, train controls, fps counter, non-looping --- bundle.js | 492 +++++++++++++++++++++++++++++---- deno.json | 2 +- deno.lock | 8 +- index.html | 12 + inputs.ts | 5 + main.ts | 14 + math/path.ts | 2 + state/states/EditTrackState.ts | 1 - state/states/RunningState.ts | 18 ++ track/shapes.ts | 22 +- track/system.ts | 118 ++++++-- train/train.ts | 65 +++-- 12 files changed, 631 insertions(+), 128 deletions(-) diff --git a/bundle.js b/bundle.js index d9008bc..8af9caa 100644 --- a/bundle.js +++ b/bundle.js @@ -78,12 +78,12 @@ }, 2); } - // https://jsr.io/@bearmetal/doodler/0.0.4/geometry/constants.ts + // https://jsr.io/@bearmetal/doodler/0.0.5-a/geometry/constants.ts var Constants = { TWO_PI: Math.PI * 2 }; - // https://jsr.io/@bearmetal/doodler/0.0.4/geometry/vector.ts + // https://jsr.io/@bearmetal/doodler/0.0.5-a/geometry/vector.ts var Vector = class _Vector { x; y; @@ -368,7 +368,7 @@ } }; - // https://jsr.io/@bearmetal/doodler/0.0.4/FPSCounter.ts + // https://jsr.io/@bearmetal/doodler/0.0.5-a/FPSCounter.ts var FPSCounter = class { frameTimes = []; maxSamples; @@ -396,7 +396,7 @@ } }; - // https://jsr.io/@bearmetal/doodler/0.0.4/canvas.ts + // https://jsr.io/@bearmetal/doodler/0.0.5-a/canvas.ts var Doodler = class { ctx; _canvas; @@ -505,6 +505,15 @@ this.ctx.lineTo(end.x, end.y); this.ctx.stroke(); } + drawLine(points, style) { + this.setStyle(style); + this.ctx.beginPath(); + this.ctx.moveTo(points[0].x, points[0].y); + for (const p of points.slice(1)) { + this.ctx.lineTo(p.x, p.y); + } + this.ctx.stroke(); + } dot(at, style) { this.setStyle({ ...style, weight: 1 }); this.ctx.beginPath(); @@ -750,13 +759,13 @@ } }; - // https://jsr.io/@bearmetal/doodler/0.0.4/timing/EaseInOut.ts + // https://jsr.io/@bearmetal/doodler/0.0.5-a/timing/EaseInOut.ts 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.4/timing/Map.ts + // https://jsr.io/@bearmetal/doodler/0.0.5-a/timing/Map.ts var map = (value, x1, y1, x2, y2) => (value - x1) * (y2 - x2) / (y1 - x1) + x2; - // https://jsr.io/@bearmetal/doodler/0.0.4/zoomableCanvas.ts + // https://jsr.io/@bearmetal/doodler/0.0.5-a/zoomableCanvas.ts var ZoomableDoodler = class extends Doodler { scale = 1; dragging = false; @@ -1210,12 +1219,14 @@ // math/path.ts var PathSegment = class { + id; points; length; startingLength; next; prev; constructor(points) { + this.id = crypto.randomUUID(); this.points = points; this.length = this.calculateApproxLength(100); this.startingLength = Math.round(this.length); @@ -1388,6 +1399,11 @@ } }; + // math/clamp.ts + function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); + } + // track/system.ts var TrackSystem = class _TrackSystem { segments = /* @__PURE__ */ new Map(); @@ -1416,7 +1432,7 @@ } } draw(showControls = false) { - for (const segment of this.segments.values()) { + for (const [i, segment] of this.segments.entries()) { segment.draw(showControls); } } @@ -1563,7 +1579,6 @@ backNeighbours = []; track; doodler; - id; constructor(p, id) { super(p); this.doodler = getContextItem("doodler"); @@ -1573,29 +1588,54 @@ 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.fillCircle(this.points[0], 1, { - color: "red" - }); - this.doodler.fillCircle(this.points[1], 1, { - color: "red" - }); - this.doodler.fillCircle(this.points[2], 1, { - color: "red" - }); - this.doodler.fillCircle(this.points[3], 1, { - color: "red" + this.doodler.deferDrawing(() => { + this.doodler.fillCircle(this.points[0], 1, { + color: "red" + }); + this.doodler.fillCircle(this.points[1], 1, { + color: "red" + }); + this.doodler.fillCircle(this.points[2], 1, { + color: "red" + }); + this.doodler.fillCircle(this.points[3], 1, { + color: "red" + }); }); } + const ties = Math.ceil(this.length / 10); + for (let i = 0; i < ties; i++) { + const t = i / ties; + const p = this.getPointAtT(t); + this.doodler.drawRotated(p, this.tangent(t).heading(), () => { + this.doodler.line(p, p.copy().add(0, 10), { + color: "#291b17", + weight: 4 + }); + this.doodler.line(p, p.copy().add(0, -10), { + color: "#291b17", + weight: 4 + }); + }); + } + 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.drawLine(normalPoints, { color: "grey" }); + this.doodler.drawLine(antiNormalPoints, { color: "grey" }); + } + ); } serialize() { return { @@ -1694,6 +1734,7 @@ return Array.from(new Set(this.segments.flatMap((s) => s.points))); } nodes; + looped = false; constructor(segs) { this.segments = segs; this.pointSpacing = 1; @@ -1753,10 +1794,17 @@ return points; } followEvenPoints(t) { - if (t < 0) t += this.evenPoints.length; - const i = Math.floor(t) % this.evenPoints.length; - const a = this.evenPoints[i]; - const b = this.evenPoints[(i + 1) % this.evenPoints.length]; + if (this.looped) { + if (t < 0) t += this.evenPoints.length; + const i2 = Math.floor(t) % this.evenPoints.length; + const a2 = this.evenPoints[i2]; + const b2 = this.evenPoints[(i2 + 1) % this.evenPoints.length]; + return Vector.lerp(a2, b2, t % 1); + } + t = clamp(t, 0, this.evenPoints.length - 1); + const i = clamp(Math.floor(t), 0, this.evenPoints.length - 1); + const a = this.evenPoints[clamp(i, 0, this.evenPoints.length - 1)]; + const b = this.evenPoints[clamp((i + 1) % this.evenPoints.length, 0, this.evenPoints.length - 1)]; return Vector.lerp(a, b, t % 1); } calculateApproxLength() { @@ -1804,20 +1852,26 @@ ]); } }; - var SBendLeft = class extends StraightTrack { + var SBendLeft = class extends TrackSegment { constructor(start) { start = start || new Vector(100, 100); - super(start); - this.points[2].add(0, -25); - this.points[3].add(0, -25); + super([ + start, + start.copy().add(60, 0), + start.copy().add(90, -25), + start.copy().add(150, -25) + ]); } }; - var SBendRight = class extends StraightTrack { + var SBendRight = class extends TrackSegment { constructor(start) { start = start || new Vector(100, 100); - super(start); - this.points[2].add(0, 25); - this.points[3].add(0, 25); + super([ + start, + start.copy().add(60, 0), + start.copy().add(90, 25), + start.copy().add(150, 25) + ]); } }; var BankLeft = class extends TrackSegment { @@ -2049,7 +2103,6 @@ } }); inputManager2.onNumberKey((i) => { - console.log(i); const segments = getContextItem("trackSegments"); this.selectedSegment = segments[i]; this.ghostRotated = false; @@ -2114,6 +2167,276 @@ } }; + // https://jsr.io/@bearmetal/doodler/0.0.4/geometry/constants.ts + var Constants2 = { + TWO_PI: Math.PI * 2 + }; + + // https://jsr.io/@bearmetal/doodler/0.0.4/geometry/vector.ts + var Vector2 = class _Vector { + x; + y; + z; + doodler; + constructor(x = 0, y = 0, z = 0) { + if (typeof x === "number") { + this.x = x; + this.y = y; + this.z = z; + } else { + this.x = x.x; + this.y = x.y || y; + this.z = x.z || z; + } + } + initializeDoodler(doodler2) { + this.doodler = doodler2; + } + set(v, y, z) { + if (arguments.length === 1 && typeof v !== "number") { + this.set( + v.x || v[0] || 0, + v.y || v[1] || 0, + v.z || v[2] || 0 + ); + } else { + this.x = v; + this.y = y || 0; + this.z = z || 0; + } + } + get() { + return new _Vector(this.x, this.y, this.z); + } + mag() { + const x = this.x, y = this.y, z = this.z; + return Math.sqrt(x * x + y * y + z * z); + } + magSq() { + const x = this.x, y = this.y, z = this.z; + return x * x + y * y + z * z; + } + setMag(v_or_len, len) { + if (len === void 0) { + len = v_or_len; + this.normalize(); + this.mult(len); + } else { + const v = v_or_len; + v.normalize(); + v.mult(len); + return v; + } + } + add(v, y, z) { + if (arguments.length === 1 && typeof v !== "number") { + this.x += v.x; + this.y += v.y; + this.z += v.z; + } else if (arguments.length === 2) { + this.x += v; + this.y += y ?? 0; + } else { + this.x += v; + this.y += y ?? 0; + this.z += z ?? 0; + } + return this; + } + sub(v, y, z) { + if (arguments.length === 1 && typeof v !== "number") { + this.x -= v.x; + this.y -= v.y; + this.z -= v.z || 0; + } else if (arguments.length === 2) { + this.x -= v; + this.y -= y ?? 0; + } else { + this.x -= v; + this.y -= y ?? 0; + this.z -= z ?? 0; + } + return this; + } + mult(v) { + if (typeof v === "number") { + this.x *= v; + this.y *= v; + this.z *= v; + } else { + this.x *= v.x; + this.y *= v.y; + this.z *= v.z; + } + return this; + } + div(v) { + if (typeof v === "number") { + this.x /= v; + this.y /= v; + this.z /= v; + } else { + this.x /= v.x; + this.y /= v.y; + this.z /= v.z; + } + return this; + } + rotate(angle) { + const prev_x = this.x; + const c = Math.cos(angle); + const s = Math.sin(angle); + this.x = c * this.x - s * this.y; + this.y = s * prev_x + c * this.y; + return this; + } + dist(other) { + return Math.sqrt( + Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2) + Math.pow(this.z - other.z, 2) + ); + } + dot(v, y, z) { + if (arguments.length === 1 && typeof v !== "number") { + return this.x * v.x + this.y * v.y + this.z * v.z; + } + return this.x * v + this.y * y + this.z * z; + } + cross(v) { + const x = this.x, y = this.y, z = this.z; + return new _Vector(y * v.z - v.y * z, z * v.x - v.z * x, x * v.y - v.x * y); + } + lerp(v_or_x, amt_or_y, z, amt) { + const lerp_val = (start, stop, amt2) => { + return start + (stop - start) * amt2; + }; + let x, y; + if (arguments.length === 2 && typeof v_or_x !== "number") { + amt = amt_or_y; + x = v_or_x.x; + y = v_or_x.y; + z = v_or_x.z; + } else { + x = v_or_x; + y = amt_or_y; + } + this.x = lerp_val(this.x, x, amt); + this.y = lerp_val(this.y, y, amt); + this.z = lerp_val(this.z, z, amt); + return this; + } + normalize() { + const m = this.mag(); + if (m > 0) { + this.div(m); + } + return this; + } + limit(high) { + if (this.mag() > high) { + this.normalize(); + this.mult(high); + } + return this; + } + heading() { + return -Math.atan2(-this.y, this.x); + } + heading2D() { + return this.heading(); + } + toString() { + return "[" + this.x.toFixed(2) + ", " + this.y.toFixed(2) + ", " + this.z.toFixed(2) + "]"; + } + array() { + return [this.x, this.y, this.z]; + } + copy() { + return new _Vector(this.x, this.y, this.z); + } + drawDot(color) { + if (!this.doodler) return; + this.doodler.dot(this, { weight: 2, color: color || "red" }); + } + draw(origin) { + if (!this.doodler) return; + const startPoint = origin ? new _Vector(origin) : new _Vector(); + this.doodler.line( + startPoint, + startPoint.copy().add(this.copy().normalize().mult(100)) + ); + } + normal(v) { + if (!v) return new _Vector(-this.y, this.x); + const dx = v.x - this.x; + const dy = v.y - this.y; + return new _Vector(-dy, dx); + } + static fromAngle(angle, v) { + if (v === void 0 || v === null) { + v = new _Vector(); + } + v.x = Math.cos(angle); + v.y = Math.sin(angle); + return v; + } + static random2D(v) { + return _Vector.fromAngle(Math.random() * (Math.PI * 2), v); + } + static random3D(v) { + const angle = Math.random() * Constants2.TWO_PI; + const vz = Math.random() * 2 - 1; + const mult = Math.sqrt(1 - vz * vz); + const vx = mult * Math.cos(angle); + const vy = mult * Math.sin(angle); + if (v === void 0 || v === null) { + v = new _Vector(vx, vy, vz); + } else { + v.set(vx, vy, vz); + } + return v; + } + static dist(v1, v2) { + return v1.dist(v2); + } + static dot(v1, v2) { + return v1.dot(v2); + } + static cross(v1, v2) { + return v1.cross(v2); + } + static add(v1, v2) { + return new _Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); + } + static sub(v1, v2) { + return new _Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); + } + static angleBetween(v1, v2) { + return Math.acos(v1.dot(v2) / Math.sqrt(v1.magSq() * v2.magSq())); + } + static lerp(v1, v2, amt) { + const val = new _Vector(v1.x, v1.y, v1.z); + val.lerp(v2, amt); + return val; + } + static vectorProjection(v1, v2) { + v2 = v2.copy(); + v2.normalize(); + const sp = v1.dot(v2); + v2.mult(sp); + return v2; + } + static vectorProjectionAndDot(v1, v2) { + v2 = v2.copy(); + v2.normalize(); + const sp = v1.dot(v2); + v2.mult(sp); + return [v2, sp]; + } + static hypot2(a, b) { + return _Vector.dot(_Vector.sub(a, b), _Vector.sub(a, b)); + } + }; + // train/train.ts var Train = class { nodes = []; @@ -2128,19 +2451,30 @@ this.t = 0; const resources2 = getContextItem("resources"); this.cars = cars; - console.log(track); let currentOffset = 0; - for (const car of this.cars) { - currentOffset += this.spacing; - const a = this.path.followEvenPoints(this.t - currentOffset); - currentOffset += car.length; - const b = this.path.followEvenPoints(this.t - currentOffset); - car.points = [a, b]; - this.nodes.push(a, b); + try { + for (const car of this.cars) { + currentOffset += this.spacing; + const a = this.path.followEvenPoints(this.t - currentOffset); + currentOffset += car.length; + const b = this.path.followEvenPoints(this.t - currentOffset); + car.points = [a, b]; + this.nodes.push(a, b); + } + } catch { + currentOffset = 0; + for (const car of this.cars.toReversed()) { + currentOffset += this.spacing; + const a = this.path.followEvenPoints(this.t - currentOffset); + currentOffset += car.length; + const b = this.path.followEvenPoints(this.t - currentOffset); + car.points = [a, b]; + this.nodes.push(a, b); + } } } move(dTime) { - this.t = (this.t + this.speed * dTime * 10) % this.path.evenPoints.length; + this.t = this.t + this.speed * dTime * 10; let currentOffset = 0; for (const car of this.cars) { if (!car.points) return; @@ -2151,18 +2485,25 @@ currentOffset += this.spacing; } } - draw() { - const doodler2 = getContextItem("doodler"); - this.path.draw(); - for (const [i, node] of this.nodes.entries()) { - doodler2.fillCircle(node, 2, { color: "purple" }); - } - } // draw() { - // for (const car of this.cars) { - // car.draw(); + // const doodler = getContextItem("doodler"); + // this.path.draw(); + // for (const [i, node] of this.nodes.entries()) { + // // doodler.drawCircle(node, 10, { color: "purple", weight: 3 }); + // doodler.fillCircle(node, 2, { color: "purple" }); + // // const next = this.nodes[i + 1]; + // // if (next) { + // // const to = Vector.sub(node.point, next.point); + // // to.setMag(40); + // // doodler.line(next.point, Vector.add(to, next.point)) + // // } // } // } + draw() { + for (const car of this.cars) { + car.draw(); + } + } real2Track(length) { return length / this.path.pointSpacing; } @@ -2210,7 +2551,7 @@ constructor() { const resources2 = getContextItem("resources"); super(25, resources2.get("engine-sprites"), 40, 20, { - at: new Vector(80, 0), + at: new Vector2(80, 0), width: 40, height: 20 }); @@ -2253,6 +2594,18 @@ const train = new Train(track.path, [new RedEngine(), new Tender()]); ctx2.trains.push(train); }); + inputManager2.onKey("ArrowUp", () => { + const trains = getContextItem("trains"); + for (const train of trains) { + train.speed += 1; + } + }); + inputManager2.onKey("ArrowDown", () => { + const trains = getContextItem("trains"); + for (const train of trains) { + train.speed -= 1; + } + }); } stop() { } @@ -2265,6 +2618,11 @@ const state2 = getContextItem("state"); state2.transitionTo(3 /* EDIT_TRACK */); }); + inputManager2.onKey("Delete", () => { + if (inputManager2.getKeyState("Control")) { + localStorage.removeItem("track"); + } + }); } // state/states/LoadState.ts @@ -2324,6 +2682,7 @@ fillScreen: true, bg: "#302040" }); + doodler.ctx.imageSmoothingEnabled = false; var colors = [ "red", "orange", @@ -2356,4 +2715,15 @@ console.log("Saved track to local storage"); } }); + setInterval(() => { + const doodler2 = getContextItem("doodler"); + const frameRate = doodler2.fps; + let fpsEl = document.getElementById("fps"); + if (!fpsEl) { + fpsEl = document.createElement("div"); + fpsEl.id = "fps"; + document.body.appendChild(fpsEl); + } + fpsEl.textContent = frameRate.toFixed(1) + " fps"; + }, 1e3); })(); diff --git a/deno.json b/deno.json index 7b7dc6d..6fadef5 100644 --- a/deno.json +++ b/deno.json @@ -13,6 +13,6 @@ "dev": "deno run -RWEN --allow-run dev.ts dev" }, "imports": { - "@bearmetal/doodler": "jsr:@bearmetal/doodler@^0.0.4" + "@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-a" } } \ No newline at end of file diff --git a/deno.lock b/deno.lock index 56d8002..88cbca4 100644 --- a/deno.lock +++ b/deno.lock @@ -1,8 +1,7 @@ { "version": "4", "specifiers": { - "jsr:@bearmetal/doodler@*": "0.0.4", - "jsr:@bearmetal/doodler@^0.0.4": "0.0.4", + "jsr:@bearmetal/doodler@0.0.5-a": "0.0.5-a", "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", @@ -26,6 +25,9 @@ "@bearmetal/doodler@0.0.4": { "integrity": "b631083cff84994c513f70d1f09e6a9256edabcb224112c93a9ca6a87c88a389" }, + "@bearmetal/doodler@0.0.5-a": { + "integrity": "c59d63f071623ad4c7588e24b464874786759e56a6b12782689251a5cf3a1c08" + }, "@luca/esbuild-deno-loader@0.11.0": { "integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c", "dependencies": [ @@ -230,7 +232,7 @@ }, "workspace": { "dependencies": [ - "jsr:@bearmetal/doodler@^0.0.4" + "jsr:@bearmetal/doodler@0.0.5-a" ] } } diff --git a/index.html b/index.html index 2a2d557..86872f7 100644 --- a/index.html +++ b/index.html @@ -26,6 +26,18 @@ max-height: 50vh; overflow-y: auto; } + #fps { + position: absolute; + top: 0; + right: 0; + background-color: rgba(0, 0, 0, 0.5); + color: white; + padding: 10px; + display: flex; + gap: 10px; + max-height: 50vh; + overflow-y: auto; + } diff --git a/inputs.ts b/inputs.ts index 8655e9e..4793d0f 100644 --- a/inputs.ts +++ b/inputs.ts @@ -9,4 +9,9 @@ export function bootstrapInputs() { const state = getContextItem>("state"); state.transitionTo(States.EDIT_TRACK); }); + inputManager.onKey("Delete", () => { + if (inputManager.getKeyState("Control")) { + localStorage.removeItem("track"); + } + }); } diff --git a/main.ts b/main.ts index 9f34628..45db8aa 100644 --- a/main.ts +++ b/main.ts @@ -20,6 +20,8 @@ const doodler = new ZoomableDoodler({ fillScreen: true, bg: "#302040", }); +(doodler as any as { ctx: CanvasRenderingContext2D }).ctx + .imageSmoothingEnabled = false; // doodler.minScale = 0.1; // (doodler as any).scale = doodler.maxScale; @@ -60,3 +62,15 @@ document.addEventListener("keydown", (e) => { console.log("Saved track to local storage"); } }); + +setInterval(() => { + const doodler = getContextItem("doodler"); + const frameRate = doodler.fps; + let fpsEl = document.getElementById("fps"); + if (!fpsEl) { + fpsEl = document.createElement("div"); + fpsEl.id = "fps"; + document.body.appendChild(fpsEl); + } + fpsEl.textContent = frameRate.toFixed(1) + " fps"; +}, 1000); diff --git a/math/path.ts b/math/path.ts index 3f9f894..f471093 100644 --- a/math/path.ts +++ b/math/path.ts @@ -86,6 +86,7 @@ export class ComplexPath { } export class PathSegment { + id: string; points: [Vector, Vector, Vector, Vector]; length: number; @@ -95,6 +96,7 @@ export class PathSegment { prev?: PathSegment; constructor(points: [Vector, Vector, Vector, Vector]) { + this.id = crypto.randomUUID(); this.points = points; this.length = this.calculateApproxLength(100); this.startingLength = Math.round(this.length); diff --git a/state/states/EditTrackState.ts b/state/states/EditTrackState.ts index f80d166..19cd24c 100644 --- a/state/states/EditTrackState.ts +++ b/state/states/EditTrackState.ts @@ -348,7 +348,6 @@ export class EditTrackState extends State { // }); inputManager.onNumberKey((i) => { - console.log(i); const segments = getContextItem("trackSegments"); this.selectedSegment = segments[i]; this.ghostRotated = false; diff --git a/state/states/RunningState.ts b/state/states/RunningState.ts index 83c86ba..06d7a6a 100644 --- a/state/states/RunningState.ts +++ b/state/states/RunningState.ts @@ -46,6 +46,24 @@ export class RunningState extends State { const train = new Train(track.path, [new RedEngine(), new Tender()]); ctx.trains.push(train); }); + // const trainCount = 1000; + // for (let i = 0; i < trainCount; i++) { + // const train = new Train(track.path, [new RedEngine(), new Tender()]); + // ctx.trains.push(train); + // } + + inputManager.onKey("ArrowUp", () => { + const trains = getContextItem("trains"); + for (const train of trains) { + train.speed += 1; + } + }); + inputManager.onKey("ArrowDown", () => { + const trains = getContextItem("trains"); + for (const train of trains) { + train.speed -= 1; + } + }); } override stop(): void { // noop diff --git a/track/shapes.ts b/track/shapes.ts index 756386b..74a92f0 100644 --- a/track/shapes.ts +++ b/track/shapes.ts @@ -13,20 +13,26 @@ export class StraightTrack extends TrackSegment { } } -export class SBendLeft extends StraightTrack { +export class SBendLeft extends TrackSegment { constructor(start?: Vector) { start = start || new Vector(100, 100); - super(start); - this.points[2].add(0, -25); - this.points[3].add(0, -25); + super([ + start, + start.copy().add(60, 0), + start.copy().add(90, -25), + start.copy().add(150, -25), + ]); } } -export class SBendRight extends StraightTrack { +export class SBendRight extends TrackSegment { constructor(start?: Vector) { start = start || new Vector(100, 100); - super(start); - this.points[2].add(0, 25); - this.points[3].add(0, 25); + super([ + start, + start.copy().add(60, 0), + start.copy().add(90, 25), + start.copy().add(150, 25), + ]); } } diff --git a/track/system.ts b/track/system.ts index 70c2f86..02beb10 100644 --- a/track/system.ts +++ b/track/system.ts @@ -1,6 +1,7 @@ import { Doodler, Point, Vector } from "@bearmetal/doodler"; import { ComplexPath, PathSegment } from "../math/path.ts"; import { getContextItem, setDefaultContext } from "../lib/context.ts"; +import { clamp } from "../math/clamp.ts"; export class TrackSystem { private segments: Map = new Map(); @@ -34,7 +35,7 @@ export class TrackSystem { } draw(showControls = false) { - for (const segment of this.segments.values()) { + for (const [i, segment] of this.segments.entries()) { segment.draw(showControls); } @@ -232,8 +233,6 @@ export class TrackSegment extends PathSegment { doodler: Doodler; - id: string; - constructor(p: VectorSet, id?: string) { super(p); this.doodler = getContextItem("doodler"); @@ -245,29 +244,82 @@ export class TrackSegment extends PathSegment { } override draw(showControls = false) { - this.doodler.drawBezier( - this.points[0], - this.points[1], - this.points[2], - this.points[3], - { - strokeColor: "#ffffff50", - }, - ); + // if (showControls) { + // this.doodler.drawBezier( + // this.points[0], + // this.points[1], + // this.points[2], + // this.points[3], + // { + // strokeColor: "#ffffff50", + // }, + // ); + // } if (showControls) { - this.doodler.fillCircle(this.points[0], 1, { - color: "red", - }); - this.doodler.fillCircle(this.points[1], 1, { - color: "red", - }); - this.doodler.fillCircle(this.points[2], 1, { - color: "red", - }); - this.doodler.fillCircle(this.points[3], 1, { - color: "red", + this.doodler.deferDrawing(() => { + this.doodler.fillCircle(this.points[0], 1, { + color: "red", + }); + this.doodler.fillCircle(this.points[1], 1, { + color: "red", + }); + this.doodler.fillCircle(this.points[2], 1, { + color: "red", + }); + this.doodler.fillCircle(this.points[3], 1, { + color: "red", + }); }); } + + const ties = Math.ceil(this.length / 10); + for (let i = 0; i < ties; i++) { + const t = i / ties; + const p = this.getPointAtT(t); + // this.doodler.drawCircle(p, 2, { + // color: "red", + // weight: 3, + // }); + this.doodler.drawRotated(p, this.tangent(t).heading(), () => { + this.doodler.line(p, p.copy().add(0, 10), { + color: "#291b17", + weight: 4, + }); + this.doodler.line(p, p.copy().add(0, -10), { + color: "#291b17", + weight: 4, + }); + // this.doodler.line(p.copy().add(-6, 5), p.copy().add(6, 5), { + // color: "grey", + // weight: 1, + // }); + // this.doodler.line(p.copy().add(-6, -5), p.copy().add(6, -5), { + // color: "grey", + // weight: 1, + // }); + }); + } + 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.drawLine(normalPoints, { color: "grey" }); + this.doodler.drawLine(antiNormalPoints, { color: "grey" }); + }, + ); + // this.doodler.drawCircle(p, 2, { + // color: "red", + // weight: 3, + // }); } serialize(): SerializedTrackSegment { @@ -382,6 +434,8 @@ export class Spline { nodes: IControlNode[]; + looped = false; + constructor(segs: T[]) { this.segments = segs; this.pointSpacing = 1; @@ -452,10 +506,20 @@ export class Spline { } followEvenPoints(t: number) { - if (t < 0) t += this.evenPoints.length; - const i = Math.floor(t) % this.evenPoints.length; - const a = this.evenPoints[i]; - const b = this.evenPoints[(i + 1) % this.evenPoints.length]; + if (this.looped) { + if (t < 0) t += this.evenPoints.length; + const i = Math.floor(t) % this.evenPoints.length; + const a = this.evenPoints[i]; + const b = this.evenPoints[(i + 1) % this.evenPoints.length]; + return Vector.lerp(a, b, t % 1); + } + t = clamp(t, 0, this.evenPoints.length - 1); + const i = clamp(Math.floor(t), 0, this.evenPoints.length - 1); + const a = this.evenPoints[clamp(i, 0, this.evenPoints.length - 1)]; + const b = this + .evenPoints[ + clamp((i + 1) % this.evenPoints.length, 0, this.evenPoints.length - 1) + ]; return Vector.lerp(a, b, t % 1); } diff --git a/train/train.ts b/train/train.ts index 8074d63..c78b08c 100644 --- a/train/train.ts +++ b/train/train.ts @@ -24,7 +24,6 @@ export class Train { this.t = 0; const resources = getContextItem("resources"); this.cars = cars; - console.log(track); // this.cars.push( // new TrainCar( // 55, @@ -42,19 +41,31 @@ export class Train { // ), // ); let currentOffset = 0; - for (const car of this.cars) { - currentOffset += this.spacing; - const a = this.path.followEvenPoints(this.t - currentOffset); - currentOffset += car.length; - const b = this.path.followEvenPoints(this.t - currentOffset); - car.points = [a, b]; - this.nodes.push(a, b); - // this.cars.push(car); + try { + for (const car of this.cars) { + currentOffset += this.spacing; + const a = this.path.followEvenPoints(this.t - currentOffset); + currentOffset += car.length; + const b = this.path.followEvenPoints(this.t - currentOffset); + car.points = [a, b]; + this.nodes.push(a, b); + } + } catch { + currentOffset = 0; + for (const car of this.cars.toReversed()) { + currentOffset += this.spacing; + const a = this.path.followEvenPoints(this.t - currentOffset); + currentOffset += car.length; + const b = this.path.followEvenPoints(this.t - currentOffset); + car.points = [a, b]; + this.nodes.push(a, b); + } } } move(dTime: number) { - this.t = (this.t + this.speed * dTime * 10) % this.path.evenPoints.length; + this.t = this.t + this.speed * dTime * 10; + // % this.path.evenPoints.length; // This should probably be on the track system // console.log(this.t); let currentOffset = 0; for (const car of this.cars) { @@ -69,27 +80,27 @@ export class Train { // this.draw(); } - draw() { - const doodler = getContextItem("doodler"); - this.path.draw(); - for (const [i, node] of this.nodes.entries()) { - // doodler.drawCircle(node, 10, { color: "purple", weight: 3 }); - doodler.fillCircle(node, 2, { color: "purple" }); - // const next = this.nodes[i + 1]; - // if (next) { - // const to = Vector.sub(node.point, next.point); - // to.setMag(40); - // doodler.line(next.point, Vector.add(to, next.point)) - // } - } - } - // draw() { - // for (const car of this.cars) { - // car.draw(); + // const doodler = getContextItem("doodler"); + // this.path.draw(); + // for (const [i, node] of this.nodes.entries()) { + // // doodler.drawCircle(node, 10, { color: "purple", weight: 3 }); + // doodler.fillCircle(node, 2, { color: "purple" }); + // // const next = this.nodes[i + 1]; + // // if (next) { + // // const to = Vector.sub(node.point, next.point); + // // to.setMag(40); + // // doodler.line(next.point, Vector.add(to, next.point)) + // // } // } // } + draw() { + for (const car of this.cars) { + car.draw(); + } + } + real2Track(length: number) { return length / this.path.pointSpacing; }