From ed0daeef2baf12c08ee740632dd3e77385e252e3 Mon Sep 17 00:00:00 2001
From: Emma
Date: Sun, 2 Feb 2025 23:29:40 -0700
Subject: [PATCH] fixes deprecation
---
.vscode/settings.json | 1 -
bundle.js | 2097 ++++++++++++++++++++++++---------------
deno.jsonc => deno.json | 13 +-
deno.lock | 197 ++++
dev.ts | 141 +++
index.html | 10 +
main.ts | 210 ++--
test/bundle.js | 1455 +++++++++++++++++++++++++++
train.ts | 69 +-
9 files changed, 3283 insertions(+), 910 deletions(-)
rename deno.jsonc => deno.json (54%)
create mode 100644 deno.lock
create mode 100644 dev.ts
create mode 100644 test/bundle.js
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 736d3dc..8f41100 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,7 +1,6 @@
{
"deno.enable": true,
"deno.unstable": true,
- "deno.config": "./deno.jsonc",
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#520088",
"activityBar.background": "#520088",
diff --git a/bundle.js b/bundle.js
index 7fca96c..3fc58ae 100644
--- a/bundle.js
+++ b/bundle.js
@@ -1,1086 +1,1533 @@
-// deno-fmt-ignore-file
-// deno-lint-ignore-file
-// This code was bundled using `deno bundle` and it's not recommended to edit it manually
-
-const Constants = {
+(() => {
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/geometry/constants.ts
+ var Constants = {
TWO_PI: Math.PI * 2
-};
-class Vector {
+ };
+
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/geometry/vector.ts
+ var Vector = class _Vector {
x;
y;
z;
- constructor(x = 0, y = 0, z = 0){
+ 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;
+ }
}
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;
- }
+ 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);
+ 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);
+ 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;
+ const x = this.x, y = this.y, z = this.z;
+ return x * x + y * y + z * z;
}
setMag(v_or_len, len) {
- if (len === undefined) {
- len = v_or_len;
- this.normalize();
- this.mult(len);
- } else {
- const v = v_or_len;
- v.normalize();
- v.mult(len);
- return v;
- }
+ 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;
+ 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;
- } 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;
+ 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;
+ 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;
+ 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;
+ 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(v) {
- const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z;
- return Math.sqrt(dx * dx + dy * dy + dz * dz);
+ const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - (v.z || 0);
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
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;
+ 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);
+ 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, amt)=>{
- return start + (stop - start) * amt;
- };
- 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;
+ 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;
+ 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;
+ if (this.mag() > high) {
+ this.normalize();
+ this.mult(high);
+ }
+ return this;
}
heading() {
- return -Math.atan2(-this.y, this.x);
+ return -Math.atan2(-this.y, this.x);
}
heading2D() {
- return this.heading();
+ return this.heading();
}
toString() {
- return "[" + this.x + ", " + this.y + ", " + this.z + "]";
+ return "[" + this.x + ", " + this.y + ", " + this.z + "]";
}
array() {
- return [
- this.x,
- this.y,
- this.z
- ];
+ return [this.x, this.y, this.z];
}
copy() {
- return new Vector(this.x, this.y, this.z);
+ return new _Vector(this.x, this.y, this.z);
}
- drawDot() {
- if (!doodler) return;
- doodler.dot(this, {
- weight: 2,
- color: 'red'
- });
+ drawDot(color) {
+ if (!doodler) return;
+ doodler.dot(this, { weight: 2, color: color || "red" });
+ }
+ draw(origin) {
+ if (!doodler) return;
+ const startPoint = origin ? new _Vector(origin) : new _Vector();
+ 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 === undefined || v === null) {
- v = new Vector();
- }
- v.x = Math.cos(angle);
- v.y = Math.sin(angle);
- return 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);
+ return _Vector.fromAngle(Math.random() * (Math.PI * 2), v);
}
static random3D(v) {
- const angle = Math.random() * Constants.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 === undefined || v === null) {
- v = new Vector(vx, vy, vz);
- } else {
- v.set(vx, vy, vz);
- }
- return v;
+ const angle = Math.random() * Constants.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);
+ return v1.dist(v2);
}
static dot(v1, v2) {
- return v1.dot(v2);
+ return v1.dot(v2);
}
static cross(v1, v2) {
- return v1.cross(v2);
+ return v1.cross(v2);
}
static add(v1, v2) {
- return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z);
+ 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);
+ 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()));
+ return Math.acos(v1.dot(v2) / Math.sqrt(v1.magSq() * v2.magSq()));
}
static lerp(v1, v2, amt) {
- const retval = new Vector(v1.x, v1.y, v1.z);
- retval.lerp(v2, amt);
- return retval;
+ 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;
+ 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));
+ return _Vector.dot(_Vector.sub(a, b), _Vector.sub(a, b));
}
-}
-const init = (opt)=>{
- if (window.doodler) throw 'Doodler has already been initialized in this window';
- window.doodler = new Doodler(opt);
- window.doodler.init();
-};
-class Doodler {
+ };
+ var OriginVector = class _OriginVector extends Vector {
+ origin;
+ get halfwayPoint() {
+ return {
+ x: this.mag() / 2 * Math.sin(this.heading()) + this.origin.x,
+ y: this.mag() / 2 * Math.cos(this.heading()) + this.origin.y
+ };
+ }
+ constructor(origin, p) {
+ super(p.x, p.y, p.z);
+ this.origin = origin;
+ }
+ static from(origin, p) {
+ const v = {
+ x: p.x - origin.x,
+ y: p.y - origin.y
+ };
+ return new _OriginVector(origin, v);
+ }
+ };
+
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/canvas.ts
+ var Doodler = class {
ctx;
_canvas;
layers = [];
bg;
framerate;
get width() {
- return this.ctx.canvas.width;
+ return this.ctx.canvas.width;
}
get height() {
- return this.ctx.canvas.height;
+ return this.ctx.canvas.height;
}
draggables = [];
clickables = [];
- constructor({ width , height , canvas , bg , framerate }){
- if (!canvas) {
- canvas = document.createElement('canvas');
- document.body.append(canvas);
- }
- this.bg = bg || 'white';
- this.framerate = framerate || 60;
- canvas.width = width;
- canvas.height = height;
- this._canvas = canvas;
- const ctx = canvas.getContext('2d');
- console.log(ctx);
- if (!ctx) throw 'Unable to initialize Doodler: Canvas context not found';
- this.ctx = ctx;
+ dragTarget;
+ constructor({
+ width,
+ height,
+ fillScreen,
+ canvas,
+ bg,
+ framerate
+ }, postInit) {
+ if (!canvas) {
+ canvas = document.createElement("canvas");
+ document.body.append(canvas);
+ }
+ this.bg = bg || "white";
+ this.framerate = framerate;
+ canvas.width = fillScreen ? document.body.clientWidth : width;
+ canvas.height = fillScreen ? document.body.clientHeight : height;
+ if (fillScreen) {
+ const resizeObserver = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ this._canvas.width = entry.target.clientWidth;
+ this._canvas.height = entry.target.clientHeight;
+ }
+ });
+ resizeObserver.observe(document.body);
+ }
+ this._canvas = canvas;
+ const ctx = canvas.getContext("2d");
+ if (!ctx) throw "Unable to initialize Doodler: Canvas context not found";
+ this.ctx = ctx;
+ postInit?.(this.ctx);
}
init() {
- this._canvas.addEventListener('mousedown', (e)=>this.onClick(e));
- this._canvas.addEventListener('mouseup', (e)=>this.offClick(e));
- this._canvas.addEventListener('mousemove', (e)=>{
- const rect = this._canvas.getBoundingClientRect();
- this.mouseX = e.clientX - rect.left;
- this.mouseY = e.clientY - rect.top;
- for (const d of this.draggables.filter((d)=>d.beingDragged)){
- d.point.add(e.movementX, e.movementY);
- d.onDrag && d.onDrag({
- x: e.movementX,
- y: e.movementY
- });
- }
- });
- this.startDrawLoop();
+ this._canvas.addEventListener("mousedown", (e) => this.onClick(e));
+ this._canvas.addEventListener("mouseup", (e) => this.offClick(e));
+ this._canvas.addEventListener("mousemove", (e) => this.onDrag(e));
+ this.startDrawLoop();
}
timer;
+ lastFrameAt = 0;
startDrawLoop() {
- this.timer = setInterval(()=>this.draw(), 1000 / this.framerate);
+ this.lastFrameAt = Date.now();
+ if (this.framerate) {
+ this.timer = setInterval(
+ () => this.draw(Date.now()),
+ 1e3 / this.framerate
+ );
+ } else {
+ const cb = (t) => {
+ this.draw(t);
+ requestAnimationFrame(cb);
+ };
+ requestAnimationFrame(cb);
+ }
}
- draw() {
- this.ctx.fillStyle = this.bg;
- this.ctx.fillRect(0, 0, this.width, this.height);
- for (const [i, l] of (this.layers || []).entries()){
- l(this.ctx, i);
- }
- this.drawUI();
+ draw(time) {
+ const frameTime = time - this.lastFrameAt;
+ this.ctx.clearRect(0, 0, this.width, this.height);
+ this.ctx.fillStyle = this.bg;
+ this.ctx.fillRect(0, 0, this.width, this.height);
+ for (const [i, l] of (this.layers || []).entries()) {
+ l(this.ctx, i, frameTime);
+ this.drawDeferred();
+ }
+ this.drawUI();
+ this.lastFrameAt = time;
}
+ // Layer management
createLayer(layer) {
- this.layers.push(layer);
+ this.layers.push(layer);
}
deleteLayer(layer) {
- this.layers = this.layers.filter((l)=>l !== layer);
+ this.layers = this.layers.filter((l) => l !== layer);
}
moveLayer(layer, index) {
- let temp = this.layers.filter((l)=>l !== layer);
- temp = [
- ...temp.slice(0, index),
- layer,
- ...temp.slice(index)
- ];
- this.layers = temp;
+ let temp = this.layers.filter((l) => l !== layer);
+ temp = [...temp.slice(0, index), layer, ...temp.slice(index)];
+ this.layers = temp;
}
+ // Drawing
line(start, end, style) {
- this.setStyle(style);
- this.ctx.beginPath();
- this.ctx.moveTo(start.x, start.y);
- this.ctx.lineTo(end.x, end.y);
- this.ctx.stroke();
+ this.setStyle(style);
+ this.ctx.beginPath();
+ this.ctx.moveTo(start.x, start.y);
+ this.ctx.lineTo(end.x, end.y);
+ this.ctx.stroke();
}
dot(at, style) {
- this.setStyle({
- ...style,
- weight: 1
- });
- this.ctx.beginPath();
- this.ctx.arc(at.x, at.y, style?.weight || 1, 0, Constants.TWO_PI);
- this.ctx.fill();
+ this.setStyle({ ...style, weight: 1 });
+ this.ctx.beginPath();
+ this.ctx.arc(at.x, at.y, style?.weight || 1, 0, Constants.TWO_PI);
+ this.ctx.fill();
}
drawCircle(at, radius, style) {
- this.setStyle(style);
- this.ctx.beginPath();
- this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI);
- this.ctx.stroke();
+ this.setStyle(style);
+ this.ctx.beginPath();
+ this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI);
+ this.ctx.stroke();
}
fillCircle(at, radius, style) {
- this.setStyle(style);
- this.ctx.beginPath();
- this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI);
- this.ctx.fill();
+ this.setStyle(style);
+ this.ctx.beginPath();
+ this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI);
+ this.ctx.fill();
}
drawRect(at, width, height, style) {
- this.setStyle(style);
- this.ctx.strokeRect(at.x, at.y, width, height);
+ this.setStyle(style);
+ this.ctx.strokeRect(at.x, at.y, width, height);
}
fillRect(at, width, height, style) {
- this.setStyle(style);
- this.ctx.fillRect(at.x, at.y, width, height);
+ this.setStyle(style);
+ this.ctx.fillRect(at.x, at.y, width, height);
}
drawSquare(at, size, style) {
- this.drawRect(at, size, size, style);
+ this.drawRect(at, size, size, style);
}
fillSquare(at, size, style) {
- this.fillRect(at, size, size, style);
+ this.fillRect(at, size, size, style);
}
drawCenteredRect(at, width, height, style) {
- this.ctx.save();
- this.ctx.translate(-width / 2, -height / 2);
- this.drawRect(at, width, height, style);
- this.ctx.restore();
+ this.ctx.save();
+ this.ctx.translate(-width / 2, -height / 2);
+ this.drawRect(at, width, height, style);
+ this.ctx.restore();
}
fillCenteredRect(at, width, height, style) {
- this.ctx.save();
- this.ctx.translate(-width / 2, -height / 2);
- this.fillRect(at, width, height, style);
- this.ctx.restore();
+ this.ctx.save();
+ this.ctx.translate(-width / 2, -height / 2);
+ this.fillRect(at, width, height, style);
+ this.ctx.restore();
}
drawCenteredSquare(at, size, style) {
- this.drawCenteredRect(at, size, size, style);
+ this.drawCenteredRect(at, size, size, style);
}
fillCenteredSquare(at, size, style) {
- this.fillCenteredRect(at, size, size, style);
+ this.fillCenteredRect(at, size, size, style);
}
drawBezier(a, b, c, d, style) {
- this.setStyle(style);
- this.ctx.beginPath();
- this.ctx.moveTo(a.x, a.y);
- this.ctx.bezierCurveTo(b.x, b.y, c.x, c.y, d.x, d.y);
- this.ctx.stroke();
+ this.setStyle(style);
+ this.ctx.beginPath();
+ this.ctx.moveTo(a.x, a.y);
+ this.ctx.bezierCurveTo(b.x, b.y, c.x, c.y, d.x, d.y);
+ this.ctx.stroke();
}
drawRotated(origin, angle, cb) {
- this.ctx.save();
- this.ctx.translate(origin.x, origin.y);
- this.ctx.rotate(angle);
- this.ctx.translate(-origin.x, -origin.y);
- cb();
- this.ctx.restore();
+ this.ctx.save();
+ this.ctx.translate(origin.x, origin.y);
+ this.ctx.rotate(angle);
+ this.ctx.translate(-origin.x, -origin.y);
+ cb();
+ this.ctx.restore();
+ }
+ drawScaled(scale, cb) {
+ this.ctx.save();
+ this.ctx.transform(scale, 0, 0, scale, 0, 0);
+ cb();
+ this.ctx.restore();
+ }
+ drawWithAlpha(alpha, cb) {
+ this.ctx.save();
+ this.ctx.globalAlpha = Math.min(Math.max(alpha, 0), 1);
+ cb();
+ this.ctx.restore();
}
drawImage(img, at, w, h) {
- w && h ? this.ctx.drawImage(img, at.x, at.y, w, h) : this.ctx.drawImage(img, at.x, at.y);
+ w && h ? this.ctx.drawImage(img, at.x, at.y, w, h) : this.ctx.drawImage(img, at.x, at.y);
+ }
+ drawImageWithOutline(img, at, w, h, style) {
+ this.ctx.save();
+ const s = (typeof w === "number" || !w ? style?.weight : w.weight) || 1;
+ this.ctx.shadowColor = (typeof w === "number" || !w ? style?.color || style?.fillColor : w.color || w.strokeColor) || "red";
+ this.ctx.shadowBlur = 0;
+ for (let x = -s; x <= s; x++) {
+ for (let y = -s; y <= s; y++) {
+ this.ctx.shadowOffsetX = x;
+ this.ctx.shadowOffsetY = y;
+ typeof w === "number" && h ? this.ctx.drawImage(img, at.x, at.y, w, h) : this.ctx.drawImage(img, at.x, at.y);
+ }
+ }
+ this.ctx.restore();
}
drawSprite(img, spritePos, sWidth, sHeight, at, width, height) {
- this.ctx.drawImage(img, spritePos.x, spritePos.y, sWidth, sHeight, at.x, at.y, width, height);
+ this.ctx.drawImage(
+ img,
+ spritePos.x,
+ spritePos.y,
+ sWidth,
+ sHeight,
+ at.x,
+ at.y,
+ width,
+ height
+ );
+ }
+ deferredDrawings = [];
+ deferDrawing(cb) {
+ this.deferredDrawings.push(cb);
+ }
+ drawDeferred() {
+ while (this.deferredDrawings.length) {
+ this.deferredDrawings.pop()?.();
+ }
}
setStyle(style) {
- const ctx = this.ctx;
- ctx.fillStyle = style?.color || style?.fillColor || 'black';
- ctx.strokeStyle = style?.color || style?.strokeColor || 'black';
- ctx.lineWidth = style?.weight || 1;
+ const ctx = this.ctx;
+ ctx.fillStyle = style?.color || style?.fillColor || "black";
+ ctx.strokeStyle = style?.color || style?.strokeColor || "black";
+ ctx.lineWidth = style?.weight || 1;
+ ctx.textAlign = style?.textAlign || ctx.textAlign;
+ ctx.textBaseline = style?.textBaseline || ctx.textBaseline;
}
+ fillText(text, pos, maxWidth, style) {
+ this.setStyle(style);
+ this.ctx.fillText(text, pos.x, pos.y, maxWidth);
+ }
+ strokeText(text, pos, maxWidth, style) {
+ this.setStyle(style);
+ this.ctx.strokeText(text, pos.x, pos.y, maxWidth);
+ }
+ clearRect(at, width, height) {
+ this.ctx.clearRect(at.x, at.y, width, height);
+ }
+ // Interaction
mouseX = 0;
mouseY = 0;
registerDraggable(point, radius, style) {
- if (this.draggables.find((d)=>d.point === point)) return;
- const id = this.addUIElement('circle', point, radius, {
- fillColor: '#5533ff50',
- strokeColor: '#5533ff50'
- });
- this.draggables.push({
- point,
- radius,
- style,
- id
- });
+ if (this.draggables.find((d) => d.point === point)) return;
+ const id = this.addUIElement("circle", point, radius, {
+ fillColor: "#5533ff50",
+ strokeColor: "#5533ff50"
+ });
+ this.draggables.push({ point, radius, style, id });
}
unregisterDraggable(point) {
- for (const d of this.draggables){
- if (d.point === point) {
- this.removeUIElement(d.id);
- }
+ for (const d of this.draggables) {
+ if (d.point === point) {
+ this.removeUIElement(d.id);
}
- this.draggables = this.draggables.filter((d)=>d.point !== point);
+ }
+ this.draggables = this.draggables.filter((d) => d.point !== point);
}
registerClickable(p1, p2, cb) {
- const top = Math.min(p1.y, p2.y);
- const left = Math.min(p1.x, p2.x);
- const bottom = Math.max(p1.y, p2.y);
- const right = Math.max(p1.x, p2.x);
- this.clickables.push({
- onClick: cb,
- checkBound: (p)=>p.y >= top && p.x >= left && p.y <= bottom && p.x <= right
- });
+ const top = Math.min(p1.y, p2.y);
+ const left = Math.min(p1.x, p2.x);
+ const bottom = Math.max(p1.y, p2.y);
+ const right = Math.max(p1.x, p2.x);
+ this.clickables.push({
+ onClick: cb,
+ checkBound: (p) => p.y >= top && p.x >= left && p.y <= bottom && p.x <= right
+ });
}
unregisterClickable(cb) {
- this.clickables = this.clickables.filter((c)=>c.onClick !== cb);
+ this.clickables = this.clickables.filter((c) => c.onClick !== cb);
}
- addDragEvents({ onDragEnd , onDragStart , onDrag , point }) {
- const d = this.draggables.find((d)=>d.point === point);
- if (d) {
- d.onDragEnd = onDragEnd;
- d.onDragStart = onDragStart;
- d.onDrag = onDrag;
- }
+ addDragEvents({
+ onDragEnd,
+ onDragStart,
+ onDrag,
+ point
+ }) {
+ const d = this.draggables.find((d2) => d2.point === point);
+ if (d) {
+ d.onDragEnd = onDragEnd;
+ d.onDragStart = onDragStart;
+ d.onDrag = onDrag;
+ }
}
onClick(e) {
- const mouse = new Vector(this.mouseX, this.mouseY);
- for (const d of this.draggables){
- if (d.point.dist(mouse) <= d.radius) {
- d.beingDragged = true;
- d.onDragStart?.call(null);
- } else d.beingDragged = false;
- }
- for (const c of this.clickables){
- if (c.checkBound(mouse)) {
- c.onClick();
- }
+ const mouse = new Vector(this.mouseX, this.mouseY);
+ for (const d of this.draggables) {
+ if (d.point.dist(mouse) <= d.radius) {
+ d.beingDragged = true;
+ d.onDragStart?.call(null);
+ this.dragTarget = d;
+ } else d.beingDragged = false;
+ }
+ for (const c of this.clickables) {
+ if (c.checkBound(mouse)) {
+ c.onClick();
}
+ }
}
offClick(e) {
- for (const d of this.draggables){
- d.beingDragged = false;
- d.onDragEnd?.call(null);
- }
+ for (const d of this.draggables) {
+ d.beingDragged = false;
+ d.onDragEnd?.call(null);
+ }
+ this.dragTarget = void 0;
}
- uiElements = new Map();
+ onDrag(e) {
+ const rect = this._canvas.getBoundingClientRect();
+ this.mouseX = e.offsetX;
+ this.mouseY = e.offsetY;
+ for (const d of this.draggables.filter((d2) => d2.beingDragged)) {
+ d.point.add(e.movementX, e.movementY);
+ d.onDrag && d.onDrag({ x: e.movementX, y: e.movementY });
+ }
+ }
+ // UI Layer
+ uiElements = /* @__PURE__ */ new Map();
uiDrawing = {
- rectangle: (...args)=>{
- !args[3].noFill && this.fillRect(args[0], args[1], args[2], args[3]);
- !args[3].noStroke && this.drawRect(args[0], args[1], args[2], args[3]);
- },
- square: (...args)=>{
- !args[2].noFill && this.fillSquare(args[0], args[1], args[2]);
- !args[2].noStroke && this.drawSquare(args[0], args[1], args[2]);
- },
- circle: (...args)=>{
- !args[2].noFill && this.fillCircle(args[0], args[1], args[2]);
- !args[2].noStroke && this.drawCircle(args[0], args[1], args[2]);
- }
+ rectangle: (...args) => {
+ !args[3].noFill && this.fillRect(args[0], args[1], args[2], args[3]);
+ !args[3].noStroke && this.drawRect(args[0], args[1], args[2], args[3]);
+ },
+ square: (...args) => {
+ !args[2].noFill && this.fillSquare(args[0], args[1], args[2]);
+ !args[2].noStroke && this.drawSquare(args[0], args[1], args[2]);
+ },
+ circle: (...args) => {
+ !args[2].noFill && this.fillCircle(args[0], args[1], args[2]);
+ !args[2].noStroke && this.drawCircle(args[0], args[1], args[2]);
+ }
};
drawUI() {
- for (const [shape, ...args] of this.uiElements.values()){
- this.uiDrawing[shape].apply(null, args);
- }
+ for (const [shape, ...args] of this.uiElements.values()) {
+ this.uiDrawing[shape].apply(null, args);
+ }
}
addUIElement(shape, ...args) {
- const id = crypto.randomUUID();
- for (const arg of args){
- delete arg.color;
- }
- this.uiElements.set(id, [
- shape,
- ...args
- ]);
- return id;
+ const id = crypto.randomUUID();
+ for (const arg of args) {
+ delete arg.color;
+ }
+ this.uiElements.set(id, [shape, ...args]);
+ return id;
}
removeUIElement(id) {
- this.uiElements.delete(id);
+ this.uiElements.delete(id);
}
-}
-class Train {
+ };
+
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/timing/EaseInOut.ts
+ var easeInOut = (x) => x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
+
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/timing/Map.ts
+ var map = (value, x1, y1, x2, y2) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;
+
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/zoomableCanvas.ts
+ var ZoomableDoodler = class extends Doodler {
+ scale = 1;
+ dragging = false;
+ origin = {
+ x: 0,
+ y: 0
+ };
+ mouse = {
+ x: 0,
+ y: 0
+ };
+ previousTouchLength;
+ touchTimer;
+ hasDoubleTapped = false;
+ zooming = false;
+ scaleAround = { x: 0, y: 0 };
+ maxScale = 4;
+ minScale = 1;
+ constructor(options, postInit) {
+ super(options, postInit);
+ this._canvas.addEventListener("wheel", (e) => {
+ this.scaleAtMouse(e.deltaY < 0 ? 1.1 : 0.9);
+ if (this.scale === 1) {
+ this.origin.x = 0;
+ this.origin.y = 0;
+ }
+ });
+ this._canvas.addEventListener("dblclick", (e) => {
+ e.preventDefault();
+ this.scale = 1;
+ this.origin.x = 0;
+ this.origin.y = 0;
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0);
+ });
+ this._canvas.addEventListener("mousedown", (e) => {
+ e.preventDefault();
+ this.dragging = true;
+ });
+ this._canvas.addEventListener("mouseup", (e) => {
+ e.preventDefault();
+ this.dragging = false;
+ });
+ this._canvas.addEventListener("mouseleave", (_e) => {
+ this.dragging = false;
+ });
+ this._canvas.addEventListener("mousemove", (e) => {
+ const prev = this.mouse;
+ this.mouse = {
+ x: e.offsetX,
+ y: e.offsetY
+ };
+ if (this.dragging && !this.dragTarget) this.drag(prev);
+ });
+ this._canvas.addEventListener("touchstart", (e) => {
+ e.preventDefault();
+ if (e.touches.length === 1) {
+ const t1 = e.touches.item(0);
+ if (t1) {
+ this.mouse = this.getTouchOffset({
+ x: t1.clientX,
+ y: t1.clientY
+ });
+ }
+ } else {
+ clearTimeout(this.touchTimer);
+ }
+ });
+ this._canvas.addEventListener("touchend", (e) => {
+ if (e.touches.length !== 2) {
+ this.previousTouchLength = void 0;
+ }
+ switch (e.touches.length) {
+ case 1:
+ break;
+ case 0:
+ if (!this.zooming) {
+ this.events.get("touchend")?.map((cb) => cb(e));
+ }
+ break;
+ }
+ this.dragging = e.touches.length === 1;
+ clearTimeout(this.touchTimer);
+ });
+ this._canvas.addEventListener("touchmove", (e) => {
+ e.preventDefault();
+ if (e.touches.length === 2) {
+ const t1 = e.touches.item(0);
+ const t2 = e.touches.item(1);
+ if (t1 && t2) {
+ const vect = OriginVector.from(
+ this.getTouchOffset({
+ x: t1.clientX,
+ y: t1.clientY
+ }),
+ {
+ x: t2.clientX,
+ y: t2.clientY
+ }
+ );
+ if (this.previousTouchLength) {
+ const diff = this.previousTouchLength - vect.mag();
+ this.scaleAt(vect.halfwayPoint, diff < 0 ? 1.01 : 0.99);
+ this.scaleAround = { ...vect.halfwayPoint };
+ }
+ this.previousTouchLength = vect.mag();
+ }
+ }
+ if (e.touches.length === 1) {
+ this.dragging === true;
+ const t1 = e.touches.item(0);
+ if (t1) {
+ const prev = this.mouse;
+ this.mouse = this.getTouchOffset({
+ x: t1.clientX,
+ y: t1.clientY
+ });
+ this.drag(prev);
+ }
+ }
+ });
+ this._canvas.addEventListener("touchstart", (e) => {
+ if (e.touches.length !== 1) return false;
+ if (!this.hasDoubleTapped) {
+ this.hasDoubleTapped = true;
+ setTimeout(() => this.hasDoubleTapped = false, 300);
+ return false;
+ }
+ if (this.scale > 1) {
+ this.frameCounter = map(this.scale, this.maxScale, 1, 0, 59);
+ this.zoomDirection = -1;
+ } else {
+ this.frameCounter = 0;
+ this.zoomDirection = 1;
+ }
+ if (this.zoomDirection > 0) {
+ this.scaleAround = { ...this.mouse };
+ }
+ this.events.get("doubletap")?.map((cb) => cb(e));
+ });
+ }
+ worldToScreen(x, y) {
+ x = x * this.scale + this.origin.x;
+ y = y * this.scale + this.origin.y;
+ return { x, y };
+ }
+ screenToWorld(x, y) {
+ x = (x - this.origin.x) / this.scale;
+ y = (y - this.origin.y) / this.scale;
+ return { x, y };
+ }
+ scaleAtMouse(scaleBy) {
+ if (this.scale === this.maxScale && scaleBy > 1) return;
+ this.scaleAt({
+ x: this.mouse.x,
+ y: this.mouse.y
+ }, scaleBy);
+ }
+ scaleAt(p, scaleBy) {
+ this.scale = Math.min(
+ Math.max(this.scale * scaleBy, this.minScale),
+ this.maxScale
+ );
+ this.origin.x = p.x - (p.x - this.origin.x) * scaleBy;
+ this.origin.y = p.y - (p.y - this.origin.y) * scaleBy;
+ this.constrainOrigin();
+ }
+ moveOrigin(motion) {
+ if (this.scale > 1) {
+ this.origin.x += motion.x;
+ this.origin.y += motion.y;
+ this.constrainOrigin();
+ }
+ }
+ drag(prev) {
+ if (this.scale > 1) {
+ const xOffset = this.mouse.x - prev.x;
+ const yOffset = this.mouse.y - prev.y;
+ this.origin.x += xOffset;
+ this.origin.y += yOffset;
+ this.constrainOrigin();
+ }
+ }
+ constrainOrigin() {
+ this.origin.x = Math.min(
+ Math.max(
+ this.origin.x,
+ -this._canvas.width * this.scale + this._canvas.width
+ ),
+ 0
+ );
+ this.origin.y = Math.min(
+ Math.max(
+ this.origin.y,
+ -this._canvas.height * this.scale + this._canvas.height
+ ),
+ 0
+ );
+ }
+ draw(time) {
+ this.ctx.setTransform(
+ this.scale,
+ 0,
+ 0,
+ this.scale,
+ this.origin.x,
+ this.origin.y
+ );
+ this.animateZoom();
+ this.ctx.fillStyle = this.bg;
+ this.ctx.fillRect(0, 0, this.width / this.scale, this.height / this.scale);
+ super.draw(time);
+ }
+ getTouchOffset(p) {
+ const { x, y } = this._canvas.getBoundingClientRect();
+ const offsetX = p.x - x;
+ const offsetY = p.y - y;
+ return {
+ x: offsetX,
+ y: offsetY
+ };
+ }
+ onDrag(e) {
+ const d = {
+ ...e,
+ movementX: e.movementX / this.scale,
+ movementY: e.movementY / this.scale
+ };
+ super.onDrag(d);
+ const { x, y } = this.screenToWorld(e.offsetX, e.offsetY);
+ this.mouseX = x;
+ this.mouseY = y;
+ }
+ zoomDirection = -1;
+ frameCounter = 60;
+ animateZoom() {
+ if (this.frameCounter < 60) {
+ const frame = easeInOut(map(this.frameCounter, 0, 59, 0, 1));
+ switch (this.zoomDirection) {
+ case 1:
+ {
+ this.scale = map(frame, 0, 1, 1, this.maxScale);
+ }
+ break;
+ case -1:
+ {
+ this.scale = map(frame, 0, 1, this.maxScale, 1);
+ }
+ break;
+ }
+ this.origin.x = this.scaleAround.x - this.scaleAround.x * this.scale;
+ this.origin.y = this.scaleAround.y - this.scaleAround.y * this.scale;
+ this.constrainOrigin();
+ this.frameCounter++;
+ }
+ }
+ events = /* @__PURE__ */ new Map();
+ registerEvent(eventName, cb) {
+ let events = this.events.get(eventName);
+ if (!events) events = this.events.set(eventName, []).get(eventName);
+ events.push(cb);
+ }
+ };
+
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/init.ts
+ function init(opt, zoomable, postInit) {
+ if (window.doodler) {
+ throw "Doodler has already been initialized in this window";
+ }
+ window.doodler = zoomable ? new ZoomableDoodler(opt, postInit) : new Doodler(opt, postInit);
+ window.doodler.init();
+ }
+
+ // train.ts
+ var Train = class {
nodes = [];
cars = [];
path;
t;
engineLength = 40;
spacing = 30;
- constructor(track, cars = []){
- this.path = track;
- this.t = 0;
- this.nodes.push(this.path.followEvenPoints(this.t));
- this.nodes.push(this.path.followEvenPoints(this.t - this.real2Track(40)));
- this.cars.push(new TrainCar(55, document.getElementById('engine-sprites'), 80, 20, {
- at: new Vector(0, 60),
- width: 80,
- height: 20
- }));
- this.cars[0].points = this.nodes.map((n)=>n);
- let currentOffset = 40;
- for (const car of cars){
- currentOffset += this.spacing;
- const a = this.path.followEvenPoints(this.t - currentOffset);
- currentOffset += car.length;
- const b = this.path.followEvenPoints(this.t - currentOffset);
- car.points = [
- a,
- b
- ];
- this.cars.push(car);
- }
+ speed = 0;
+ constructor(track, cars2 = []) {
+ this.path = track;
+ this.t = 0;
+ this.nodes.push(this.path.followEvenPoints(this.t));
+ this.nodes.push(this.path.followEvenPoints(this.t - this.real2Track(40)));
+ const engineSprites2 = document.getElementById(
+ "engine-sprites"
+ );
+ this.cars.push(
+ new TrainCar(
+ 55,
+ engineSprites2,
+ 80,
+ 20,
+ { at: new Vector(0, 60), width: 80, height: 20 }
+ ),
+ new TrainCar(
+ 25,
+ engineSprites2,
+ 40,
+ 20,
+ { at: new Vector(80, 0), width: 40, height: 20 }
+ )
+ );
+ this.cars[0].points = this.nodes.map((n) => n);
+ this.cars[1].points = this.nodes.map((n) => n);
+ let currentOffset = 40;
+ for (const car of cars2) {
+ currentOffset += this.spacing;
+ const a = this.path.followEvenPoints(this.t - currentOffset);
+ currentOffset += car.length;
+ const b = this.path.followEvenPoints(this.t - currentOffset);
+ car.points = [a, b];
+ this.cars.push(car);
+ }
}
- move() {
- this.t = (this.t + 1) % this.path.evenPoints.length;
- let currentOffset = 0;
- for (const car of this.cars){
- if (!car.points) return;
- const [a, b] = car.points;
- a.set(this.path.followEvenPoints(this.t - currentOffset));
- currentOffset += car.length;
- b.set(this.path.followEvenPoints(this.t - currentOffset));
- currentOffset += this.spacing;
- car.draw();
- }
+ move(dTime) {
+ this.t = (this.t + this.speed * dTime * 10) % this.path.evenPoints.length;
+ let currentOffset = 0;
+ for (const car of this.cars) {
+ if (!car.points) return;
+ const [a, b] = car.points;
+ a.set(this.path.followEvenPoints(this.t - currentOffset));
+ currentOffset += car.length;
+ b.set(this.path.followEvenPoints(this.t - currentOffset));
+ currentOffset += this.spacing;
+ car.draw();
+ }
}
- real2Track(length) {
- return length / this.path.pointSpacing;
+ // draw() {
+ // for (const [i, node] of this.nodes.entries()) {
+ // doodler.drawCircle(node.point, 10, { color: 'purple', weight: 3 })
+ // // 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))
+ // // }
+ // }
+ // }
+ real2Track(length2) {
+ return length2 / this.path.pointSpacing;
}
-}
-class TrainCar {
+ };
+ var TrainCar = class {
img;
imgWidth;
imgHeight;
sprite;
points;
length;
- constructor(length, img, w, h, sprite){
- this.img = img;
- this.sprite = sprite;
- this.imgWidth = w;
- this.imgHeight = h;
- this.length = length;
+ constructor(length2, img, w, h, sprite) {
+ this.img = img;
+ this.sprite = sprite;
+ this.imgWidth = w;
+ this.imgHeight = h;
+ this.length = length2;
}
draw() {
- if (!this.points) return;
- const [a, b] = this.points;
- const origin = Vector.add(Vector.sub(a, b).div(2), b);
- const angle = Vector.sub(b, a).heading();
- doodler.drawCircle(origin, 4, {
- color: 'blue'
- });
- doodler.drawRotated(origin, angle, ()=>{
- this.sprite ? doodler.drawSprite(this.img, this.sprite.at, this.sprite.width, this.sprite.height, origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2), this.imgWidth, this.imgHeight) : doodler.drawImage(this.img, origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2));
- });
+ if (!this.points) return;
+ const [a, b] = this.points;
+ const origin = Vector.add(Vector.sub(a, b).div(2), b);
+ const angle = Vector.sub(b, a).heading();
+ doodler.drawCircle(origin, 4, { color: "blue" });
+ doodler.drawRotated(origin, angle, () => {
+ this.sprite ? doodler.drawSprite(
+ this.img,
+ this.sprite.at,
+ this.sprite.width,
+ this.sprite.height,
+ origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2),
+ this.imgWidth,
+ this.imgHeight
+ ) : doodler.drawImage(
+ this.img,
+ origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2)
+ );
+ });
}
-}
-class PathSegment {
+ };
+
+ // math/path.ts
+ var PathSegment = class {
points;
ctx;
length;
- constructor(points){
- this.points = points;
- this.length = this.calculateApproxLength(100);
+ constructor(points) {
+ this.points = points;
+ this.length = this.calculateApproxLength(100);
}
setContext(ctx) {
- this.ctx = ctx;
+ this.ctx = ctx;
}
draw() {
- const [a, b, c, d] = this.points;
- doodler.drawBezier(a, b, c, d, {
- strokeColor: '#ffffff50'
- });
+ const [a, b, c, d] = this.points;
+ doodler.drawBezier(a, b, c, d, {
+ strokeColor: "#ffffff50"
+ });
}
getPointAtT(t) {
- 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)));
- return res;
+ 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)));
+ return res;
}
getClosestPoint(v) {
- const resolution = 1 / 25;
- let closest = this.points[0];
- let closestDistance = this.points[0].dist(v);
- let closestT = 0;
- for(let i = 0; i < 25; i++){
- const point = this.getPointAtT(i * resolution);
- const distance = v.dist(point);
- if (distance < closestDistance) {
- closest = point;
- closestDistance = distance;
- closestT = i * resolution;
- }
+ const samples = 25;
+ const resolution = 1 / samples;
+ let closest = this.points[0];
+ let closestDistance = this.points[0].dist(v);
+ let closestT = 0;
+ for (let i = 0; i < samples; i++) {
+ const point = this.getPointAtT(i * resolution);
+ const distance = v.dist(point);
+ if (distance < closestDistance) {
+ closest = point;
+ closestDistance = distance;
+ closestT = i * resolution;
}
- return [
- closest,
- closestDistance,
- closestT
- ];
+ }
+ return [closest, closestDistance, closestT];
}
getPointsWithinRadius(v, r) {
- const points = [];
- const resolution = 1 / 25;
- for(let i = 0; i < 25; i++){
- const point = this.getPointAtT(i * resolution);
- const distance = v.dist(point);
- if (distance < r) {
- points.push([
- i * resolution,
- this
- ]);
- }
+ const points = [];
+ const samples = 25;
+ const resolution = 1 / samples;
+ for (let i = 0; i < samples; i++) {
+ const point = this.getPointAtT(i * resolution);
+ const distance = v.dist(point);
+ if (distance < r) {
+ points.push([i * resolution, this]);
}
- return points;
+ }
+ return points;
}
tangent(t) {
- 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))));
- return res;
+ 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))));
+ return res;
}
doesIntersectCircle(x, y, r) {
- const v = new Vector(x, y);
- const resolution = 1 / 25;
- let distance = Infinity;
- let t;
- for(let i = 0; i < 25; i++){
- if (i !== 25 - 1) {
- const a = this.getPointAtT(i * resolution);
- const b = this.getPointAtT((i + 1) * resolution);
- const ac = Vector.sub(v, a);
- const ab = Vector.sub(b, a);
- const d = Vector.add(Vector.vectorProjection(ac, ab), a);
- const ad = Vector.sub(d, a);
- const k = Math.abs(ab.x) > Math.abs(ab.y) ? ad.x / ab.x : ad.y / ab.y;
- let dist;
- if (k <= 0.0) {
- dist = Vector.hypot2(v, a);
- } else if (k >= 1.0) {
- dist = Vector.hypot2(v, b);
- }
- dist = Vector.hypot2(v, d);
- if (dist < distance) {
- distance = dist;
- t = i * resolution;
- }
- }
+ const v = new Vector(x, y);
+ const samples = 25;
+ const resolution = 1 / samples;
+ let distance = Infinity;
+ let t;
+ for (let i = 0; i < samples; i++) {
+ if (i !== samples - 1) {
+ const a = this.getPointAtT(i * resolution);
+ const b = this.getPointAtT((i + 1) * resolution);
+ const ac = Vector.sub(v, a);
+ const ab = Vector.sub(b, a);
+ const d = Vector.add(Vector.vectorProjection(ac, ab), a);
+ const ad = Vector.sub(d, a);
+ const k = Math.abs(ab.x) > Math.abs(ab.y) ? ad.x / ab.x : ad.y / ab.y;
+ let dist;
+ if (k <= 0) {
+ dist = Vector.hypot2(v, a);
+ } else if (k >= 1) {
+ dist = Vector.hypot2(v, b);
+ }
+ dist = Vector.hypot2(v, d);
+ if (dist < distance) {
+ distance = dist;
+ t = i * resolution;
+ }
}
- if (distance < r) return t;
- return false;
+ }
+ if (distance < r) return t;
+ return false;
}
calculateApproxLength(resolution = 25) {
- const stepSize = 1 / resolution;
- const points = [];
- for(let i = 0; i <= resolution; i++){
- const current = stepSize * i;
- points.push(this.getPointAtT(current));
- }
- this.length = points.reduce((acc, cur)=>{
- const prev = acc.prev;
- acc.prev = cur;
- if (!prev) return acc;
- acc.length += cur.dist(prev);
- return acc;
- }, {
- prev: undefined,
- length: 0
- }).length;
- return this.length;
+ const stepSize = 1 / resolution;
+ const points = [];
+ for (let i = 0; i <= resolution; i++) {
+ const current = stepSize * i;
+ points.push(this.getPointAtT(current));
+ }
+ this.length = points.reduce((acc, cur) => {
+ const prev = acc.prev;
+ acc.prev = cur;
+ if (!prev) return acc;
+ acc.length += cur.dist(prev);
+ return acc;
+ }, { prev: void 0, length: 0 }).length;
+ return this.length;
}
calculateEvenlySpacedPoints(spacing, resolution = 1) {
- const points = [];
- points.push(this.points[0]);
- let prev = points[0];
- let distSinceLastEvenPoint = 0;
- let t = 0;
- const div = Math.ceil(this.length * resolution * 10);
- while(t < 1){
- 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;
+ const points = [];
+ points.push(this.points[0]);
+ let prev = points[0];
+ let distSinceLastEvenPoint = 0;
+ let t = 0;
+ const div = Math.ceil(this.length * resolution * 10);
+ while (t < 1) {
+ 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;
}
- return points;
+ prev = point;
+ }
+ return points;
}
-}
-class Track extends PathSegment {
+ };
+
+ // track.ts
+ var Track = class extends PathSegment {
editable = false;
next;
prev;
id;
- constructor(points, next, prev){
- super(points);
- this.id = crypto.randomUUID();
- this.next = next || this;
- this.prev = prev || this;
+ constructor(points, next, prev) {
+ super(points);
+ this.id = crypto.randomUUID();
+ this.next = next || this;
+ this.prev = prev || this;
}
+ // followTrack(train: Train): [Vector, number] {
+ // const predict = train.velocity.copy();
+ // predict.normalize();
+ // predict.mult(1);
+ // const predictpos = Vector.add(train.position, predict)
+ // // const leading = train.leadingPoint;
+ // // let closest = this.points[0];
+ // // let closestDistance = this.getClosestPoint(leading);
+ // let [closest, closestDistance, closestT] = this.getClosestPoint(predictpos);
+ // // deno-lint-ignore no-this-alias
+ // let mostValid: Track = this;
+ // if (this.next !== this) {
+ // const [point, distance, t] = this.next.getClosestPoint(predictpos);
+ // if (distance < closestDistance) {
+ // closest = point;
+ // closestDistance = distance;
+ // mostValid = this.next;
+ // closestT = t;
+ // }
+ // }
+ // if (this.prev !== this) {
+ // const [point, distance, t] = this.next.getClosestPoint(predictpos);
+ // if (distance < closestDistance) {
+ // closest = point;
+ // closestDistance = distance;
+ // mostValid = this.next;
+ // closestT = t;
+ // }
+ // }
+ // train.currentTrack = mostValid;
+ // train.arrive(closest);
+ // // if (predictpos.dist(closest) > 2) train.arrive(closest);
+ // return [closest, closestT];
+ // }
getNearestPoint(p) {
- let [closest, closestDistance] = this.getClosestPoint(p);
- if (this.next !== this) {
- const [point, distance, t] = this.next.getClosestPoint(p);
- if (distance < closestDistance) {
- closest = point;
- closestDistance = distance;
- }
+ let [closest, closestDistance] = this.getClosestPoint(p);
+ if (this.next !== this) {
+ const [point, distance, t] = this.next.getClosestPoint(p);
+ if (distance < closestDistance) {
+ closest = point;
+ closestDistance = distance;
}
- if (this.prev !== this) {
- const [point1, distance1, t1] = this.next.getClosestPoint(p);
- if (distance1 < closestDistance) {
- closest = point1;
- closestDistance = distance1;
- }
+ }
+ if (this.prev !== this) {
+ const [point, distance, t] = this.next.getClosestPoint(p);
+ if (distance < closestDistance) {
+ closest = point;
+ closestDistance = distance;
}
- return closest;
+ }
+ return closest;
}
getAllPointsInRange(v, r) {
- const points = this.getPointsWithinRadius(v, r).concat(this.next.getPointsWithinRadius(v, r), this.prev.getPointsWithinRadius(v, r));
- return points;
+ const points = this.getPointsWithinRadius(v, r).concat(this.next.getPointsWithinRadius(v, r), this.prev.getPointsWithinRadius(v, r));
+ return points;
}
draw() {
- super.draw();
- if (this.editable) {
- const [a, b, c, d] = this.points;
- doodler.line(a, b);
- doodler.line(c, d);
- }
+ super.draw();
+ if (this.editable) {
+ const [a, b, c, d] = this.points;
+ doodler.line(a, b);
+ doodler.line(c, d);
+ }
}
setNext(t) {
- this.next = t;
- this.next.points[0] = this.points[3];
+ this.next = t;
+ this.next.points[0] = this.points[3];
}
setPrev(t) {
- this.prev = t;
- this.prev.points[3] = this.points[0];
+ this.prev = t;
+ this.prev.points[3] = this.points[0];
}
-}
-class Spline {
+ };
+ var Spline = class {
segments = [];
ctx;
evenPoints;
pointSpacing;
get points() {
- return Array.from(new Set(this.segments.flatMap((s)=>s.points)));
+ return Array.from(new Set(this.segments.flatMap((s) => s.points)));
}
nodes;
- constructor(segs){
- this.segments = segs;
- this.pointSpacing = 1;
- this.evenPoints = this.calculateEvenlySpacedPoints(1);
- this.nodes = [];
- for(let i = 0; i < this.points.length; i += 3){
- const node = {
- anchor: this.points[i],
- controls: [
- this.points.at(i - 1),
- this.points[(i + 1) % this.points.length]
- ],
- mirrored: false,
- tangent: true
- };
- this.nodes.push(node);
- }
+ constructor(segs) {
+ this.segments = segs;
+ this.pointSpacing = 1;
+ this.evenPoints = this.calculateEvenlySpacedPoints(1);
+ this.nodes = [];
+ for (let i = 0; i < this.points.length; i += 3) {
+ const node = {
+ anchor: this.points[i],
+ controls: [this.points.at(i - 1), this.points[(i + 1) % this.points.length]],
+ mirrored: false,
+ tangent: true
+ };
+ this.nodes.push(node);
+ }
}
setContext(ctx) {
- this.ctx = ctx;
- for (const segment of this.segments){
- segment.setContext(ctx);
- }
+ this.ctx = ctx;
+ for (const segment of this.segments) {
+ segment.setContext(ctx);
+ }
}
draw() {
- for (const segment of this.segments){
- segment.draw();
- }
+ for (const segment of this.segments) {
+ segment.draw();
+ }
}
calculateEvenlySpacedPoints(spacing, resolution = 1) {
- this.pointSpacing = 1;
- const points = [];
- points.push(this.segments[0].points[0]);
- let prev = points[0];
- let distSinceLastEvenPoint = 0;
- for (const seg of this.segments){
- let t = 0;
- const div = Math.ceil(seg.length * resolution * 10);
- while(t < 1){
- t += 1 / div;
- const point = seg.getPointAtT(t);
- distSinceLastEvenPoint += prev.dist(point);
- if (distSinceLastEvenPoint >= spacing) {
- const overshoot = distSinceLastEvenPoint - spacing;
- const evenPoint = Vector.add(point, Vector.sub(point, prev).normalize().mult(overshoot));
- distSinceLastEvenPoint = overshoot;
- points.push(evenPoint);
- prev = evenPoint;
- }
- prev = point;
- }
+ this.pointSpacing = 1;
+ const points = [];
+ points.push(this.segments[0].points[0]);
+ let prev = points[0];
+ let distSinceLastEvenPoint = 0;
+ for (const seg of this.segments) {
+ let t = 0;
+ const div = Math.ceil(seg.length * resolution * 10);
+ while (t < 1) {
+ t += 1 / div;
+ const point = seg.getPointAtT(t);
+ distSinceLastEvenPoint += prev.dist(point);
+ if (distSinceLastEvenPoint >= spacing) {
+ const overshoot = distSinceLastEvenPoint - spacing;
+ const evenPoint = Vector.add(point, Vector.sub(point, prev).normalize().mult(overshoot));
+ distSinceLastEvenPoint = overshoot;
+ points.push(evenPoint);
+ prev = evenPoint;
+ }
+ prev = point;
}
- this.evenPoints = points;
- return points;
+ }
+ this.evenPoints = points;
+ return points;
}
followEvenPoints(t) {
- if (t < 0) t += this.evenPoints.length;
- const i = Math.floor(t);
- const a = this.evenPoints[i];
- const b = this.evenPoints[(i + 1) % this.evenPoints.length];
- return Vector.lerp(a, b, t % 1);
+ if (t < 0) t += this.evenPoints.length;
+ const i = Math.floor(t);
+ const a = this.evenPoints[i];
+ const b = this.evenPoints[(i + 1) % this.evenPoints.length];
+ return Vector.lerp(a, b, t % 1);
}
calculateApproxLength() {
- for (const s of this.segments){
- s.calculateApproxLength();
- }
+ for (const s of this.segments) {
+ s.calculateApproxLength();
+ }
}
toggleNodeTangent(p) {
- const node = this.nodes.find((n)=>n.anchor === p);
- node && (node.tangent = !node.tangent);
+ const node = this.nodes.find((n) => n.anchor === p);
+ node && (node.tangent = !node.tangent);
}
toggleNodeMirrored(p) {
- const node = this.nodes.find((n)=>n.anchor === p);
- node && (node.mirrored = !node.mirrored);
+ const node = this.nodes.find((n) => n.anchor === p);
+ node && (node.mirrored = !node.mirrored);
}
handleNodeEdit(p, movement) {
- const node = this.nodes.find((n)=>n.anchor === p || n.controls.includes(p));
- if (!node || !(node.mirrored || node.tangent)) return;
- if (node.anchor !== p) {
- if (node.mirrored || node.tangent) {
- const mover = node.controls.find((e)=>e !== p);
- const v = Vector.sub(node.anchor, p);
- if (!node.mirrored) v.setMag(Vector.sub(node.anchor, mover).mag());
- mover.set(Vector.add(v, node.anchor));
- }
- } else {
- for (const control of node.controls){
- control.add(movement.x, movement.y);
- }
+ const node = this.nodes.find((n) => n.anchor === p || n.controls.includes(p));
+ if (!node || !(node.mirrored || node.tangent)) return;
+ if (node.anchor !== p) {
+ if (node.mirrored || node.tangent) {
+ const mover = node.controls.find((e) => e !== p);
+ const v = Vector.sub(node.anchor, p);
+ if (!node.mirrored) v.setMag(Vector.sub(node.anchor, mover).mag());
+ mover.set(Vector.add(v, node.anchor));
}
+ } else {
+ for (const control of node.controls) {
+ control.add(movement.x, movement.y);
+ }
+ }
}
-}
-const generateSquareTrack = ()=>{
- const first = new Track([
- new Vector(20, 40),
- new Vector(20, 100),
- new Vector(20, 300),
- new Vector(20, 360)
- ]);
- const second = new Track([
- first.points[3],
- new Vector(20, 370),
- new Vector(30, 380),
- new Vector(40, 380)
- ]);
- const third = new Track([
- second.points[3],
- new Vector(100, 380),
- new Vector(300, 380),
- new Vector(360, 380)
- ]);
- const fourth = new Track([
- third.points[3],
- new Vector(370, 380),
- new Vector(380, 370),
- new Vector(380, 360)
- ]);
- const fifth = new Track([
- fourth.points[3],
- new Vector(380, 300),
- new Vector(380, 100),
- new Vector(380, 40)
- ]);
- const sixth = new Track([
- fifth.points[3],
- new Vector(380, 30),
- new Vector(370, 20),
- new Vector(360, 20)
- ]);
- const seventh = new Track([
- sixth.points[3],
- new Vector(300, 20),
- new Vector(100, 20),
- new Vector(40, 20)
- ]);
- const eighth = new Track([
- seventh.points[3],
- new Vector(30, 20),
- new Vector(20, 30),
- first.points[0]
- ]);
- const tracks = [
- first,
- second,
- third,
- fourth,
- fifth,
- sixth,
- seventh,
- eighth
- ];
- for (const [i, track] of tracks.entries()){
- track.next = tracks[(i + 1) % tracks.length];
- track.prev = tracks.at(i - 1);
+ };
+ var generateSquareTrack = () => {
+ const first = new Track([new Vector(20, 40), new Vector(20, 100), new Vector(20, 300), new Vector(20, 360)]);
+ const second = new Track([first.points[3], new Vector(20, 370), new Vector(30, 380), new Vector(40, 380)]);
+ const third = new Track([second.points[3], new Vector(100, 380), new Vector(300, 380), new Vector(360, 380)]);
+ const fourth = new Track([third.points[3], new Vector(370, 380), new Vector(380, 370), new Vector(380, 360)]);
+ const fifth = new Track([fourth.points[3], new Vector(380, 300), new Vector(380, 100), new Vector(380, 40)]);
+ const sixth = new Track([fifth.points[3], new Vector(380, 30), new Vector(370, 20), new Vector(360, 20)]);
+ const seventh = new Track([sixth.points[3], new Vector(300, 20), new Vector(100, 20), new Vector(40, 20)]);
+ const eighth = new Track([seventh.points[3], new Vector(30, 20), new Vector(20, 30), first.points[0]]);
+ const tracks = [first, second, third, fourth, fifth, sixth, seventh, eighth];
+ for (const [i, track] of tracks.entries()) {
+ track.next = tracks[(i + 1) % tracks.length];
+ track.prev = tracks.at(i - 1);
}
- return new Spline([
- first,
- second,
- third,
- fourth,
- fifth,
- sixth,
- seventh,
- eighth
- ]);
-};
-const loadFromJson = ()=>{
- const json = JSON.parse(localStorage.getItem('railPath') || '');
+ return new Spline([first, second, third, fourth, fifth, sixth, seventh, eighth]);
+ };
+ var loadFromJson = () => {
+ const json = JSON.parse(localStorage.getItem("railPath") || "");
if (!json) return generateSquareTrack();
const segments = [];
- for (const { points } of json.segments){
- segments.push(new Track(points.map((p)=>new Vector(p.x, p.y))));
+ for (const { points } of json.segments) {
+ segments.push(new Track(points.map((p) => new Vector(p.x, p.y))));
}
- for (const [i, s] of segments.entries()){
- s.setNext(segments[(i + 1) % segments.length]);
- s.setPrev(segments.at(i - 1));
+ for (const [i, s] of segments.entries()) {
+ s.setNext(segments[(i + 1) % segments.length]);
+ s.setPrev(segments.at(i - 1));
}
return new Spline(segments);
-};
-const engineSprites = document.createElement('img');
-engineSprites.src = './sprites/EngineSprites.png';
-engineSprites.style.display = 'none';
-engineSprites.id = 'engine-sprites';
-document.body.append(engineSprites);
-init({
- width: 400,
- height: 400,
- bg: '#333'
-});
-const path = loadFromJson();
-let speed = 1;
-const car = new TrainCar(55, engineSprites, 80, 20, {
- at: new Vector(0, 80),
- height: 20,
- width: 80
-});
-const train = new Train(path, [
- car
-]);
-let dragEndCounter = 0;
-let selectedNode;
-doodler.createLayer(()=>{
- for(let i = 0; i < path.evenPoints.length; i += 10){
- const p = path.evenPoints[i];
- const next = path.evenPoints[(i + 1) % path.evenPoints.length];
- const last = path.evenPoints.at(i - 1);
- if (!last) break;
- const tan = Vector.sub(last, next);
- doodler.drawRotated(p, tan.heading(), ()=>{
- doodler.line(p, p.copy().add(0, 10), {
- color: '#291b17',
- weight: 4
- });
- doodler.line(p, p.copy().add(0, -10), {
- color: '#291b17',
- weight: 4
- });
- doodler.line(p.copy().add(-6, 5), p.copy().add(6, 5), {
- color: 'grey',
- weight: 2
- });
- doodler.line(p.copy().add(-6, -5), p.copy().add(6, -5), {
- color: 'grey',
- weight: 2
- });
+ };
+
+ // main.ts
+ var engineSprites = document.createElement("img");
+ engineSprites.src = "./sprites/EngineSprites.png";
+ engineSprites.style.display = "none";
+ engineSprites.id = "engine-sprites";
+ document.body.append(engineSprites);
+ init({
+ fillScreen: true,
+ bg: "#333"
+ }, true);
+ var doodler2 = window.doodler;
+ var path;
+ try {
+ path = loadFromJson();
+ } catch {
+ path = generateSquareTrack();
+ }
+ var speed = 1;
+ var length = Math.floor(Math.random() * 7);
+ var cars = Array.from(
+ { length },
+ () => new TrainCar(40, engineSprites, 61, 20, {
+ at: new Vector(80, 20 * Math.ceil(Math.random() * 3)),
+ width: 61,
+ height: 20
+ })
+ );
+ var train = new Train(path, cars);
+ var dragEndCounter = 0;
+ var selectedNode;
+ doodler2.createLayer((_1, _2, _3) => {
+ _1.imageSmoothingEnabled = false;
+ const dTime = (_3 < 0 ? 1 : _3) / 1e3;
+ for (let i = 0; i < path.evenPoints.length; i += 10) {
+ const p = path.evenPoints[i];
+ const next = path.evenPoints[(i + 1) % path.evenPoints.length];
+ const last = path.evenPoints.at(i - 1);
+ if (!last) break;
+ const tan = Vector.sub(last, next);
+ doodler2.drawRotated(p, tan.heading(), () => {
+ doodler2.line(p, p.copy().add(0, 10), { color: "#291b17", weight: 4 });
+ doodler2.line(p, p.copy().add(0, -10), { color: "#291b17", weight: 4 });
+ doodler2.line(p.copy().add(-6, 5), p.copy().add(6, 5), {
+ color: "grey",
+ weight: 2
});
+ doodler2.line(p.copy().add(-6, -5), p.copy().add(6, -5), {
+ color: "grey",
+ weight: 2
+ });
+ });
}
path.draw();
- train.move();
+ train.move(dTime);
selectedNode?.anchor.drawDot();
- selectedNode?.controls.forEach((e)=>e.drawDot());
-});
-let editable = false;
-const clickables = new Map();
-let selectedPoint;
-document.addEventListener('keyup', (e)=>{
- if (e.key === 'd') {}
- if (e.key === 'ArrowUp') {
- speed += .1;
+ selectedNode?.controls.forEach((e) => e.drawDot());
+ });
+ var editable = false;
+ var clickables = /* @__PURE__ */ new Map();
+ var selectedPoint;
+ document.addEventListener("keyup", (e) => {
+ if (e.key === "d") {
}
- if (e.key === 'ArrowDown') {
- speed -= .1;
+ if (e.key === "ArrowUp") {
+ speed += 0.1;
+ train.speed += 1;
}
- if (e.key === 'm' && selectedPoint) {
- const points = path.points;
- const index = points.findIndex((p)=>p === selectedPoint);
- if (index > -1) {
- const prev = points.at(index - 1);
- const next = points[(index + 1) % points.length];
- const toPrev = Vector.sub(prev, selectedPoint);
- toPrev.setMag(next.dist(selectedPoint));
- toPrev.rotate(Math.PI);
- const toNext = Vector.add(toPrev, selectedPoint);
- next.set(toNext);
- path.calculateApproxLength();
- path.calculateEvenlySpacedPoints(1);
+ if (e.key === "ArrowDown") {
+ speed -= 0.1;
+ train.speed -= 1;
+ }
+ if (e.key === "m" && selectedPoint) {
+ const points = path.points;
+ const index = points.findIndex((p) => p === selectedPoint);
+ if (index > -1) {
+ const prev = points.at(index - 1);
+ const next = points[(index + 1) % points.length];
+ const toPrev = Vector.sub(prev, selectedPoint);
+ toPrev.setMag(next.dist(selectedPoint));
+ toPrev.rotate(Math.PI);
+ const toNext = Vector.add(toPrev, selectedPoint);
+ next.set(toNext);
+ path.calculateApproxLength();
+ path.calculateEvenlySpacedPoints(1);
+ }
+ }
+ let translate = false;
+ if (e.key === "e" && !translate) {
+ editable = !editable;
+ for (const t of path.segments) {
+ t.editable = !t.editable;
+ for (const p of t.points) {
+ if (t.editable) {
+ doodler2.registerDraggable(p, 10);
+ doodler2.addDragEvents({
+ point: p,
+ onDragEnd: () => {
+ dragEndCounter++;
+ t.length = t.calculateApproxLength(100);
+ path.evenPoints = path.calculateEvenlySpacedPoints(1);
+ },
+ onDrag: (movement) => {
+ path.handleNodeEdit(p, movement);
+ }
+ });
+ } else {
+ doodler2.unregisterDraggable(p);
+ }
}
- }
- if (e.key === 'e') {
- editable = !editable;
- for (const t of path.segments){
- t.editable = !t.editable;
- for (const p of t.points){
- if (t.editable) {
- doodler.registerDraggable(p, 10);
- doodler.addDragEvents({
- point: p,
- onDragEnd: ()=>{
- dragEndCounter++;
- t.length = t.calculateApproxLength(100);
- path.evenPoints = path.calculateEvenlySpacedPoints(1);
- },
- onDrag: (movement)=>{
- path.handleNodeEdit(p, movement);
- }
- });
- } else {
- doodler.unregisterDraggable(p);
- }
- }
- }
- for (const p1 of path.points){
- if (editable) {
- const onClick = ()=>{
- selectedPoint = p1;
- selectedNode = path.nodes.find((e)=>e.anchor === p1 || e.controls.includes(p1));
- };
- clickables.set(p1, onClick);
- doodler.registerClickable(p1.copy().sub(10, 10), p1.copy().add(10, 10), onClick);
- } else {
- const the = clickables.get(p1);
- doodler.unregisterClickable(the);
- }
+ }
+ for (const p of path.points) {
+ if (editable) {
+ const onClick = () => {
+ selectedPoint = p;
+ selectedNode = path.nodes.find(
+ (e2) => e2.anchor === p || e2.controls.includes(p)
+ );
+ };
+ clickables.set(p, onClick);
+ doodler2.registerClickable(
+ p.copy().sub(10, 10),
+ p.copy().add(10, 10),
+ onClick
+ );
+ } else {
+ const the = clickables.get(p);
+ doodler2.unregisterClickable(the);
}
+ }
}
-});
-document.addEventListener('keydown', (e)=>{
- if (e.ctrlKey && e.key === 's') {
- e.preventDefault();
- path.segments.forEach((s)=>{
- s.next = s.next.id;
- s.prev = s.prev.id;
- delete s.ctx;
- });
- delete path.ctx;
- const json = JSON.stringify(path);
- localStorage.setItem('railPath', json);
+ let x = 0;
+ let y = 0;
+ const onDrag = (e2) => {
+ x += e2.movementX;
+ y += e2.movementY;
+ console.log("draggin");
+ };
+ const dragEnd = () => {
+ x = 0;
+ y = 0;
+ for (const t of path.points) {
+ t.add(x, y);
+ }
+ };
+ if (e.key === "t" && editable) {
+ for (const t of path.points) {
+ t.add(100, 100);
+ }
+ path.calculateEvenlySpacedPoints(1);
}
-});
+ });
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "s") {
+ e.preventDefault();
+ path.segments.forEach((s) => {
+ s.next = s.next.id;
+ s.prev = s.prev.id;
+ delete s.ctx;
+ });
+ delete path.ctx;
+ const json = JSON.stringify(path);
+ localStorage.setItem("railPath", json);
+ }
+ });
+})();
diff --git a/deno.jsonc b/deno.json
similarity index 54%
rename from deno.jsonc
rename to deno.json
index 42b9f3a..61fc12d 100644
--- a/deno.jsonc
+++ b/deno.json
@@ -1,17 +1,18 @@
{
"compilerOptions": {
"lib": [
- // "deno.window"
- "DOM",
+ "deno.ns",
+ "deno.window",
+ "dom",
+ "dom.iterable",
"ES2021",
"ESNext"
]
},
"tasks": {
- "dev": "deno bundle --watch main.ts bundle.js"
+ "dev": "deno run -RWEN --allow-run --unstable dev.ts dev"
},
"imports": {
- "drawing": "./drawing/index.ts",
- "doodler": "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.7a/mod.ts"
+ "doodler": "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/mod.ts"
}
-}
\ No newline at end of file
+}
diff --git a/deno.lock b/deno.lock
new file mode 100644
index 0000000..86a2ecd
--- /dev/null
+++ b/deno.lock
@@ -0,0 +1,197 @@
+{
+ "version": "4",
+ "specifiers": {
+ "jsr:@luca/esbuild-deno-loader@*": "0.11.0",
+ "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/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",
+ "npm:esbuild@*": "0.24.2"
+ },
+ "jsr": {
+ "@luca/esbuild-deno-loader@0.11.0": {
+ "integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
+ "dependencies": [
+ "jsr:@std/bytes",
+ "jsr:@std/encoding",
+ "jsr:@std/path@^1.0.6"
+ ]
+ },
+ "@std/bytes@1.0.2": {
+ "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57"
+ },
+ "@std/cli@1.0.9": {
+ "integrity": "557e5865af000efbf3f737dcfea5b8ab86453594f4a9cd8d08c9fa83d8e3f3bc"
+ },
+ "@std/encoding@1.0.6": {
+ "integrity": "ca87122c196e8831737d9547acf001766618e78cd8c33920776c7f5885546069"
+ },
+ "@std/fmt@1.0.3": {
+ "integrity": "97765c16aa32245ff4e2204ecf7d8562496a3cb8592340a80e7e554e0bb9149f"
+ },
+ "@std/html@1.0.3": {
+ "integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988"
+ },
+ "@std/http@1.0.12": {
+ "integrity": "85246d8bfe9c8e2538518725b158bdc31f616e0869255f4a8d9e3de919cab2aa",
+ "dependencies": [
+ "jsr:@std/cli",
+ "jsr:@std/encoding",
+ "jsr:@std/fmt",
+ "jsr:@std/html",
+ "jsr:@std/media-types",
+ "jsr:@std/net",
+ "jsr:@std/path@^1.0.8",
+ "jsr:@std/streams"
+ ]
+ },
+ "@std/media-types@1.1.0": {
+ "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
+ },
+ "@std/net@1.0.4": {
+ "integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852"
+ },
+ "@std/path@1.0.8": {
+ "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
+ },
+ "@std/streams@1.0.8": {
+ "integrity": "b41332d93d2cf6a82fe4ac2153b930adf1a859392931e2a19d9fabfb6f154fb3"
+ }
+ },
+ "npm": {
+ "@esbuild/aix-ppc64@0.24.2": {
+ "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="
+ },
+ "@esbuild/android-arm64@0.24.2": {
+ "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg=="
+ },
+ "@esbuild/android-arm@0.24.2": {
+ "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="
+ },
+ "@esbuild/android-x64@0.24.2": {
+ "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw=="
+ },
+ "@esbuild/darwin-arm64@0.24.2": {
+ "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA=="
+ },
+ "@esbuild/darwin-x64@0.24.2": {
+ "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA=="
+ },
+ "@esbuild/freebsd-arm64@0.24.2": {
+ "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg=="
+ },
+ "@esbuild/freebsd-x64@0.24.2": {
+ "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q=="
+ },
+ "@esbuild/linux-arm64@0.24.2": {
+ "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg=="
+ },
+ "@esbuild/linux-arm@0.24.2": {
+ "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA=="
+ },
+ "@esbuild/linux-ia32@0.24.2": {
+ "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw=="
+ },
+ "@esbuild/linux-loong64@0.24.2": {
+ "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ=="
+ },
+ "@esbuild/linux-mips64el@0.24.2": {
+ "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw=="
+ },
+ "@esbuild/linux-ppc64@0.24.2": {
+ "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw=="
+ },
+ "@esbuild/linux-riscv64@0.24.2": {
+ "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q=="
+ },
+ "@esbuild/linux-s390x@0.24.2": {
+ "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw=="
+ },
+ "@esbuild/linux-x64@0.24.2": {
+ "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q=="
+ },
+ "@esbuild/netbsd-arm64@0.24.2": {
+ "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw=="
+ },
+ "@esbuild/netbsd-x64@0.24.2": {
+ "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw=="
+ },
+ "@esbuild/openbsd-arm64@0.24.2": {
+ "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A=="
+ },
+ "@esbuild/openbsd-x64@0.24.2": {
+ "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA=="
+ },
+ "@esbuild/sunos-x64@0.24.2": {
+ "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig=="
+ },
+ "@esbuild/win32-arm64@0.24.2": {
+ "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ=="
+ },
+ "@esbuild/win32-ia32@0.24.2": {
+ "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA=="
+ },
+ "@esbuild/win32-x64@0.24.2": {
+ "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="
+ },
+ "esbuild@0.24.2": {
+ "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
+ "dependencies": [
+ "@esbuild/aix-ppc64",
+ "@esbuild/android-arm",
+ "@esbuild/android-arm64",
+ "@esbuild/android-x64",
+ "@esbuild/darwin-arm64",
+ "@esbuild/darwin-x64",
+ "@esbuild/freebsd-arm64",
+ "@esbuild/freebsd-x64",
+ "@esbuild/linux-arm",
+ "@esbuild/linux-arm64",
+ "@esbuild/linux-ia32",
+ "@esbuild/linux-loong64",
+ "@esbuild/linux-mips64el",
+ "@esbuild/linux-ppc64",
+ "@esbuild/linux-riscv64",
+ "@esbuild/linux-s390x",
+ "@esbuild/linux-x64",
+ "@esbuild/netbsd-arm64",
+ "@esbuild/netbsd-x64",
+ "@esbuild/openbsd-arm64",
+ "@esbuild/openbsd-x64",
+ "@esbuild/sunos-x64",
+ "@esbuild/win32-arm64",
+ "@esbuild/win32-ia32",
+ "@esbuild/win32-x64"
+ ]
+ }
+ },
+ "remote": {
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.7a/canvas.ts": "aadfb4b2e9acce34d4a5da3f9027be642c93229bbfc2641cb55301542cbb87bf",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.7a/geometry/constants.ts": "4f4cf7bf49ac871d984e9b43896783b0cc8ab0ea60d0fc4c8c582f7e00c3df5a",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.7a/geometry/vector.ts": "a08ecff64c5436a28c6451a31c68fc912d25f941aabafb79418fa0a1aeffa9d2",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.7a/mod.ts": "766bdedc7e28b89d3cb3e83ee55c612bfaeabe252a14ff47e5e676535e033d88",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/animation/gif.ts": "6f8b77cb55b252bd7c18b04fa7ff4e88b4459cf1158d8daef538b2e471433420",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/animation/sprite.ts": "64adc3843b48a0d74ad96cbf4a4d26426c1e909a03ca935f73d5ec5545080f20",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/canvas.ts": "5af9d684e1144a374f0fbee46c710f9d493d5491e90b17356d910c6ade32bb50",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/collision/aa.ts": "c27a1deee0b2ed02e3a88e4e0b370ca2dfa0f57bf783724fa5c099e9eeabc5c9",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/collision/circular.ts": "962703eacb19cc849f3fb355815edfd71e12d06f8e72f517a7c038ff2d1c1729",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/collision/sat.ts": "f221540a984c908c96b4cc86a8eddacf3d3a5dfa5367ba538c02bcf7f7038247",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/geometry/constants.ts": "4f4cf7bf49ac871d984e9b43896783b0cc8ab0ea60d0fc4c8c582f7e00c3df5a",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/geometry/polygon.ts": "6c7edf576bebd7f24b1358ecba70d561d5905e0185701e12437ba7ccdacc66a9",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/geometry/spline.ts": "3521ea5b57902001fb9a248580bd66f12f563a581eff137f5c67e2edc0305ba0",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/geometry/vector.ts": "0143daf300032d6faf5a073fffa5c298fdcd74ba2d6bcd10a2d96ab54e55bc69",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/init.ts": "0e08fdf4c896f88308e6a6a2fb8842fe3a67a3a47a5ad722ecbce37737f8694d",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/mod.ts": "ffcbd74b612db108d50f5e2e1ba7425c7e6fac87f3fe7fb43c10a5283501513e",
+ "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/processing/gif.ts": "e97456fd55806086aa90d9bc46193d355c2f6093f376f4141ca959942193e4dc",
+ "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"
+ }
+}
diff --git a/dev.ts b/dev.ts
new file mode 100644
index 0000000..e4214aa
--- /dev/null
+++ b/dev.ts
@@ -0,0 +1,141 @@
+///
+
+import * as esbuild from "npm:esbuild";
+import { denoPlugins } from "jsr:@luca/esbuild-deno-loader";
+import { serveDir } from "jsr:@std/http";
+
+async function dev() {
+ const paths = [];
+ const ignoredFiles = ["bundler", "bundle", "dev"];
+
+ for (const path of Deno.readDirSync("./")) {
+ if (
+ path.name.endsWith(".ts") &&
+ !ignoredFiles.find((file) => path.name.includes(file))
+ ) {
+ paths.push(path.name);
+ }
+ }
+ await build();
+
+ const watcher = Deno.watchFs(paths);
+
+ for await (const event of watcher) {
+ if (event.kind === "modify") {
+ console.log("File modified, bundling...");
+ await build();
+ }
+ }
+}
+async function build() {
+ const cfg = await import("./deno.json", {
+ with: { type: "json" },
+ });
+ const importMap = {
+ imports: cfg.default.imports,
+ };
+ const importMapURL = "data:application/json," + JSON.stringify(importMap);
+ console.log("File modified, bundling...");
+ try {
+ const result = await esbuild.build({
+ entryPoints: ["./main.ts"],
+ bundle: true,
+ outfile: "bundle.js",
+ plugins: [...denoPlugins({
+ importMapURL,
+ lockPath: "./deno.lock",
+ })],
+ loader: {
+ ".ts": "ts",
+ ".js": "js",
+ ".jsx": "jsx",
+ ".tsx": "tsx",
+ },
+ });
+ esbuild.stop();
+ console.log("Bundled successfully!");
+ sendSSE("data: build\n\n");
+ } catch (e) {
+ console.error(e);
+ // Deno.exit(1);
+ }
+}
+
+let sseStreams: ReadableStreamDefaultController[] = [];
+
+function sendSSE(message: string) {
+ sseStreams.filter((stream) => {
+ try {
+ stream.enqueue(new TextEncoder().encode(message));
+ return true;
+ } catch {
+ return false;
+ }
+ });
+}
+
+function sse(r: Request) {
+ let controller: ReadableStreamDefaultController;
+ const body = new ReadableStream({
+ start(controller) {
+ sseStreams.push(controller);
+ },
+ cancel() {
+ sseStreams = sseStreams.filter((stream) => stream !== controller);
+ },
+ });
+
+ return new Response(body, {
+ status: 200,
+ headers: {
+ "Content-Type": "text/event-stream",
+ "Cache-Control": "no-cache",
+ Connection: "keep-alive",
+ },
+ });
+}
+
+if (Deno.args.includes("dev")) {
+ dev();
+ Deno.serve(async (r) => {
+ if (r.url.endsWith("sse")) {
+ return sse(r);
+ }
+
+ const d = await serveDir(r, {
+ fsRoot: ".",
+ showIndex: true,
+ });
+
+ if (d.headers.get("content-type")?.startsWith("text/html")) {
+ const body = await d.text();
+ return new Response(
+ body.replace(
+ "
diff --git a/main.ts b/main.ts
index a3e3ee6..1c00e42 100644
--- a/main.ts
+++ b/main.ts
@@ -2,33 +2,37 @@ import { lerp } from "./math/lerp.ts";
import { ComplexPath, PathSegment } from "./math/path.ts";
import { Mover } from "./physics/mover.ts";
import { Train, TrainCar } from "./train.ts";
-import { fillCircle, drawCircle } from 'drawing';
+import { drawCircle, fillCircle } from "drawing";
import { generateSquareTrack, IControlNode, loadFromJson } from "./track.ts";
import { drawLine } from "./drawing/line.ts";
-import { initializeDoodler, Vector } from 'doodler';
+import { Doodler, initializeDoodler, Vector } from "doodler";
-
-
-const engineSprites = document.createElement('img');
-engineSprites.src = './sprites/EngineSprites.png';
-engineSprites.style.display = 'none';
-engineSprites.id = 'engine-sprites';
+const engineSprites = document.createElement("img");
+engineSprites.src = "./sprites/EngineSprites.png";
+engineSprites.style.display = "none";
+engineSprites.id = "engine-sprites";
document.body.append(engineSprites);
initializeDoodler({
- width: 400,
- height: 400,
- bg: '#333'
-});
+ fillScreen: true,
+ bg: "#333",
+}, true);
+const doodler = window.doodler as Doodler;
-const path = loadFromJson();
+let path;
+
+try {
+ path = loadFromJson();
+} catch {
+ path = generateSquareTrack();
+}
const controls = {
ArrowUp: false,
ArrowRight: false,
ArrowDown: false,
ArrowLeft: false,
-}
+};
let t = 0;
let currentSeg = 0;
@@ -36,70 +40,96 @@ let speed = 1;
// const trainCount = 1;
// const trains = Array(trainCount).fill(null).map((_, i) => new Train(path.segments[i % path.segments.length], 5));
-const car = new TrainCar(55, engineSprites, 80, 20, {at: new Vector(0, 80), height: 20, width: 80})
-const train = new Train(path, [car]);
+// const car = new TrainCar(55, engineSprites, 80, 20, {
+// at: new Vector(0, 80),
+// height: 20,
+// width: 80,
+// });
+const length = Math.floor(Math.random() * 7);
+const cars = Array.from(
+ { length },
+ () =>
+ new TrainCar(40, engineSprites, 61, 20, {
+ at: new Vector(80, 20 * Math.ceil(Math.random() * 3)),
+ width: 61,
+ height: 20,
+ }),
+);
+const train = new Train(path, cars);
-let dragEndCounter = 0
+let dragEndCounter = 0;
let selectedNode: IControlNode | undefined;
-doodler.createLayer(() => {
- for (let i = 0; i < path.evenPoints.length; i+=10) {
+doodler.createLayer((_1, _2, _3) => {
+ // console.log(_1, _2, _3);
+ _1.imageSmoothingEnabled = false;
+ const dTime = (_3 < 0 ? 1 : _3) / 1000;
+ // console.log(dTime);
+ for (let i = 0; i < path.evenPoints.length; i += 10) {
const p = path.evenPoints[i];
- const next = path.evenPoints[(i + 1)%path.evenPoints.length];
+ const next = path.evenPoints[(i + 1) % path.evenPoints.length];
const last = path.evenPoints.at(i - 1);
if (!last) break;
const tan = Vector.sub(last, next);
doodler.drawRotated(p, tan.heading(), () => {
- doodler.line(p, p.copy().add(0,10), {color: '#291b17', weight: 4})
- doodler.line(p, p.copy().add(0,-10), {color: '#291b17', weight: 4})
- doodler.line(p.copy().add(-6,5), p.copy().add(6,5), {color: 'grey', weight: 2})
- doodler.line(p.copy().add(-6,-5), p.copy().add(6,-5), {color: 'grey', weight: 2})
- })
+ doodler.line(p, p.copy().add(0, 10), { color: "#291b17", weight: 4 });
+ doodler.line(p, p.copy().add(0, -10), { color: "#291b17", weight: 4 });
+ doodler.line(p.copy().add(-6, 5), p.copy().add(6, 5), {
+ color: "grey",
+ weight: 2,
+ });
+ doodler.line(p.copy().add(-6, -5), p.copy().add(6, -5), {
+ color: "grey",
+ weight: 2,
+ });
+ });
}
path.draw();
- train.move();
+ train.move(dTime);
selectedNode?.anchor.drawDot();
- selectedNode?.controls.forEach(e => e.drawDot());
-})
+ selectedNode?.controls.forEach((e) => e.drawDot());
+});
let editable = false;
-const clickables = new Map()
+const clickables = new Map();
let selectedPoint: Vector;
-document.addEventListener('keyup', e => {
- if (e.key === 'd') {
+document.addEventListener("keyup", (e) => {
+ if (e.key === "d") {
// console.log(trains)
// console.log(path.segments.reduce((a,b) => a + b.calculateApproxLength(1000), 0))
// console.log(path.evenPoints);
}
- if (e.key === 'ArrowUp') {
+ if (e.key === "ArrowUp") {
// for (const train of trains) {
// train.speed += .1;
// }
- speed += .1
+ speed += .1;
+ train.speed += 1;
}
- if (e.key === 'ArrowDown') {
+ if (e.key === "ArrowDown") {
// for (const train of trains) {
// train.speed -= .1;
// }
- speed -= .1
+ speed -= .1;
+ train.speed -= 1;
}
- if (e.key === 'm' && selectedPoint) {
+ if (e.key === "m" && selectedPoint) {
const points = path.points;
- const index = points.findIndex(p => p === selectedPoint);
+ const index = points.findIndex((p) => p === selectedPoint);
if (index > -1) {
const prev = points.at(index - 1)!;
const next = points[(index + 1) % points.length];
const toPrev = Vector.sub(prev, selectedPoint);
toPrev.setMag(next.dist(selectedPoint));
- toPrev.rotate(Math.PI)
+ toPrev.rotate(Math.PI);
const toNext = Vector.add(toPrev, selectedPoint);
next.set(toNext);
@@ -107,29 +137,29 @@ document.addEventListener('keyup', e => {
path.calculateEvenlySpacedPoints(1);
}
}
+ let translate: boolean = false;
- if (e.key === 'e') {
+ if (e.key === "e" && !translate) {
editable = !editable;
for (const t of path.segments) {
t.editable = !t.editable;
for (const p of t.points) {
if (t.editable) {
- doodler.registerDraggable(p, 10)
+ doodler.registerDraggable(p, 10);
doodler.addDragEvents({
point: p,
onDragEnd: () => {
- dragEndCounter++
- t.length = t.calculateApproxLength(100)
- path.evenPoints = path.calculateEvenlySpacedPoints(1)
+ dragEndCounter++;
+ t.length = t.calculateApproxLength(100);
+ path.evenPoints = path.calculateEvenlySpacedPoints(1);
},
onDrag: (movement) => {
// todo - remove ! after updating doodler
- path.handleNodeEdit(p, movement!)
- }
- })
- }
- else {
- doodler.unregisterDraggable(p)
+ path.handleNodeEdit(p, movement!);
+ },
+ });
+ } else {
+ doodler.unregisterDraggable(p);
}
}
}
@@ -137,19 +167,73 @@ document.addEventListener('keyup', e => {
if (editable) {
const onClick = () => {
selectedPoint = p;
- selectedNode = path.nodes.find(e => e.anchor === p || e.controls.includes(p));
- }
+ selectedNode = path.nodes.find((e) =>
+ e.anchor === p || e.controls.includes(p)
+ );
+ };
clickables.set(p, onClick);
- doodler.registerClickable(p.copy().sub(10, 10), p.copy().add(10, 10), onClick);
- }
- else {
+ doodler.registerClickable(
+ p.copy().sub(10, 10),
+ p.copy().add(10, 10),
+ onClick,
+ );
+ } else {
const the = clickables.get(p);
doodler.unregisterClickable(the);
}
}
}
-})
+
+ let x = 0;
+ let y = 0;
+ const onDrag = (e: MouseEvent) => {
+ x += e.movementX;
+ y += e.movementY;
+ console.log("draggin");
+ };
+ const dragEnd = () => {
+ x = 0;
+ y = 0;
+ for (const t of path.points) {
+ t.add(x, y);
+ }
+ };
+ if (e.key === "t" && editable) {
+ // translate = !translate;
+
+ // console.log(translate);
+
+ for (const t of path.points) {
+ t.add(100, 100);
+ }
+ path.calculateEvenlySpacedPoints(1);
+
+ // switch (translate) {
+ // case true:
+ // console.log("adding");
+ // ((doodler as any)._canvas as HTMLCanvasElement).addEventListener(
+ // "drag",
+ // onDrag,
+ // );
+ // ((doodler as any)._canvas as HTMLCanvasElement).addEventListener(
+ // "dragend",
+ // dragEnd,
+ // );
+ // break;
+ // case false:
+ // ((doodler as any)._canvas as HTMLCanvasElement).removeEventListener(
+ // "drag",
+ // onDrag,
+ // );
+ // ((doodler as any)._canvas as HTMLCanvasElement).removeEventListener(
+ // "dragend",
+ // dragEnd,
+ // );
+ // break;
+ // }
+ }
+});
// document.addEventListener('keydown', e => {
// const valid = ["ArrowUp",
@@ -185,16 +269,16 @@ document.addEventListener('keyup', e => {
// return force;
// }
-document.addEventListener('keydown', e => {
- if (e.ctrlKey && e.key === 's') {
+document.addEventListener("keydown", (e) => {
+ if (e.key === "s") {
e.preventDefault();
path.segments.forEach((s: any) => {
- s.next = s.next.id
- s.prev = s.prev.id
- delete s.ctx
- })
+ s.next = s.next.id;
+ s.prev = s.prev.id;
+ delete s.ctx;
+ });
delete path.ctx;
const json = JSON.stringify(path);
- localStorage.setItem('railPath', json);
+ localStorage.setItem("railPath", json);
}
-})
\ No newline at end of file
+});
diff --git a/test/bundle.js b/test/bundle.js
new file mode 100644
index 0000000..49962a6
--- /dev/null
+++ b/test/bundle.js
@@ -0,0 +1,1455 @@
+(() => {
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/geometry/constants.ts
+ var Constants = {
+ TWO_PI: Math.PI * 2
+ };
+
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/geometry/vector.ts
+ var Vector = class _Vector {
+ x;
+ y;
+ z;
+ 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;
+ }
+ }
+ 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(v) {
+ const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - (v.z || 0);
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
+ }
+ 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 + ", " + this.y + ", " + this.z + "]";
+ }
+ array() {
+ return [this.x, this.y, this.z];
+ }
+ copy() {
+ return new _Vector(this.x, this.y, this.z);
+ }
+ drawDot(color) {
+ if (!doodler) return;
+ doodler.dot(this, { weight: 2, color: color || "red" });
+ }
+ draw(origin) {
+ if (!doodler) return;
+ const startPoint = origin ? new _Vector(origin) : new _Vector();
+ 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() * Constants.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));
+ }
+ };
+ var OriginVector = class _OriginVector extends Vector {
+ origin;
+ get halfwayPoint() {
+ return {
+ x: this.mag() / 2 * Math.sin(this.heading()) + this.origin.x,
+ y: this.mag() / 2 * Math.cos(this.heading()) + this.origin.y
+ };
+ }
+ constructor(origin, p) {
+ super(p.x, p.y, p.z);
+ this.origin = origin;
+ }
+ static from(origin, p) {
+ const v = {
+ x: p.x - origin.x,
+ y: p.y - origin.y
+ };
+ return new _OriginVector(origin, v);
+ }
+ };
+
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/canvas.ts
+ var Doodler = class {
+ ctx;
+ _canvas;
+ layers = [];
+ bg;
+ framerate;
+ get width() {
+ return this.ctx.canvas.width;
+ }
+ get height() {
+ return this.ctx.canvas.height;
+ }
+ draggables = [];
+ clickables = [];
+ dragTarget;
+ constructor({
+ width,
+ height,
+ fillScreen,
+ canvas,
+ bg,
+ framerate
+ }, postInit) {
+ if (!canvas) {
+ canvas = document.createElement("canvas");
+ document.body.append(canvas);
+ }
+ this.bg = bg || "white";
+ this.framerate = framerate;
+ canvas.width = fillScreen ? document.body.clientWidth : width;
+ canvas.height = fillScreen ? document.body.clientHeight : height;
+ if (fillScreen) {
+ const resizeObserver = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ this._canvas.width = entry.target.clientWidth;
+ this._canvas.height = entry.target.clientHeight;
+ }
+ });
+ resizeObserver.observe(document.body);
+ }
+ this._canvas = canvas;
+ const ctx = canvas.getContext("2d");
+ if (!ctx) throw "Unable to initialize Doodler: Canvas context not found";
+ this.ctx = ctx;
+ postInit?.(this.ctx);
+ }
+ init() {
+ this._canvas.addEventListener("mousedown", (e) => this.onClick(e));
+ this._canvas.addEventListener("mouseup", (e) => this.offClick(e));
+ this._canvas.addEventListener("mousemove", (e) => this.onDrag(e));
+ this.startDrawLoop();
+ }
+ timer;
+ lastFrameAt = 0;
+ startDrawLoop() {
+ this.lastFrameAt = Date.now();
+ if (this.framerate) {
+ this.timer = setInterval(
+ () => this.draw(Date.now()),
+ 1e3 / this.framerate
+ );
+ } else {
+ const cb = (t) => {
+ this.draw(t);
+ requestAnimationFrame(cb);
+ };
+ requestAnimationFrame(cb);
+ }
+ }
+ draw(time) {
+ const frameTime = time - this.lastFrameAt;
+ this.ctx.clearRect(0, 0, this.width, this.height);
+ this.ctx.fillStyle = this.bg;
+ this.ctx.fillRect(0, 0, this.width, this.height);
+ for (const [i, l] of (this.layers || []).entries()) {
+ l(this.ctx, i, frameTime);
+ this.drawDeferred();
+ }
+ this.drawUI();
+ this.lastFrameAt = time;
+ }
+ // Layer management
+ createLayer(layer) {
+ this.layers.push(layer);
+ }
+ deleteLayer(layer) {
+ this.layers = this.layers.filter((l) => l !== layer);
+ }
+ moveLayer(layer, index) {
+ let temp = this.layers.filter((l) => l !== layer);
+ temp = [...temp.slice(0, index), layer, ...temp.slice(index)];
+ this.layers = temp;
+ }
+ // Drawing
+ line(start, end, style) {
+ this.setStyle(style);
+ this.ctx.beginPath();
+ this.ctx.moveTo(start.x, start.y);
+ this.ctx.lineTo(end.x, end.y);
+ this.ctx.stroke();
+ }
+ dot(at, style) {
+ this.setStyle({ ...style, weight: 1 });
+ this.ctx.beginPath();
+ this.ctx.arc(at.x, at.y, style?.weight || 1, 0, Constants.TWO_PI);
+ this.ctx.fill();
+ }
+ drawCircle(at, radius, style) {
+ this.setStyle(style);
+ this.ctx.beginPath();
+ this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI);
+ this.ctx.stroke();
+ }
+ fillCircle(at, radius, style) {
+ this.setStyle(style);
+ this.ctx.beginPath();
+ this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI);
+ this.ctx.fill();
+ }
+ drawRect(at, width, height, style) {
+ this.setStyle(style);
+ this.ctx.strokeRect(at.x, at.y, width, height);
+ }
+ fillRect(at, width, height, style) {
+ this.setStyle(style);
+ this.ctx.fillRect(at.x, at.y, width, height);
+ }
+ drawSquare(at, size, style) {
+ this.drawRect(at, size, size, style);
+ }
+ fillSquare(at, size, style) {
+ this.fillRect(at, size, size, style);
+ }
+ drawCenteredRect(at, width, height, style) {
+ this.ctx.save();
+ this.ctx.translate(-width / 2, -height / 2);
+ this.drawRect(at, width, height, style);
+ this.ctx.restore();
+ }
+ fillCenteredRect(at, width, height, style) {
+ this.ctx.save();
+ this.ctx.translate(-width / 2, -height / 2);
+ this.fillRect(at, width, height, style);
+ this.ctx.restore();
+ }
+ drawCenteredSquare(at, size, style) {
+ this.drawCenteredRect(at, size, size, style);
+ }
+ fillCenteredSquare(at, size, style) {
+ this.fillCenteredRect(at, size, size, style);
+ }
+ drawBezier(a, b, c, d, style) {
+ this.setStyle(style);
+ this.ctx.beginPath();
+ this.ctx.moveTo(a.x, a.y);
+ this.ctx.bezierCurveTo(b.x, b.y, c.x, c.y, d.x, d.y);
+ this.ctx.stroke();
+ }
+ drawRotated(origin, angle, cb) {
+ this.ctx.save();
+ this.ctx.translate(origin.x, origin.y);
+ this.ctx.rotate(angle);
+ this.ctx.translate(-origin.x, -origin.y);
+ cb();
+ this.ctx.restore();
+ }
+ drawScaled(scale, cb) {
+ this.ctx.save();
+ this.ctx.transform(scale, 0, 0, scale, 0, 0);
+ cb();
+ this.ctx.restore();
+ }
+ drawWithAlpha(alpha, cb) {
+ this.ctx.save();
+ this.ctx.globalAlpha = Math.min(Math.max(alpha, 0), 1);
+ cb();
+ this.ctx.restore();
+ }
+ drawImage(img, at, w, h) {
+ w && h ? this.ctx.drawImage(img, at.x, at.y, w, h) : this.ctx.drawImage(img, at.x, at.y);
+ }
+ drawImageWithOutline(img, at, w, h, style) {
+ this.ctx.save();
+ const s = (typeof w === "number" || !w ? style?.weight : w.weight) || 1;
+ this.ctx.shadowColor = (typeof w === "number" || !w ? style?.color || style?.fillColor : w.color || w.strokeColor) || "red";
+ this.ctx.shadowBlur = 0;
+ for (let x = -s; x <= s; x++) {
+ for (let y = -s; y <= s; y++) {
+ this.ctx.shadowOffsetX = x;
+ this.ctx.shadowOffsetY = y;
+ typeof w === "number" && h ? this.ctx.drawImage(img, at.x, at.y, w, h) : this.ctx.drawImage(img, at.x, at.y);
+ }
+ }
+ this.ctx.restore();
+ }
+ drawSprite(img, spritePos, sWidth, sHeight, at, width, height) {
+ this.ctx.drawImage(
+ img,
+ spritePos.x,
+ spritePos.y,
+ sWidth,
+ sHeight,
+ at.x,
+ at.y,
+ width,
+ height
+ );
+ }
+ deferredDrawings = [];
+ deferDrawing(cb) {
+ this.deferredDrawings.push(cb);
+ }
+ drawDeferred() {
+ while (this.deferredDrawings.length) {
+ this.deferredDrawings.pop()?.();
+ }
+ }
+ setStyle(style) {
+ const ctx = this.ctx;
+ ctx.fillStyle = style?.color || style?.fillColor || "black";
+ ctx.strokeStyle = style?.color || style?.strokeColor || "black";
+ ctx.lineWidth = style?.weight || 1;
+ ctx.textAlign = style?.textAlign || ctx.textAlign;
+ ctx.textBaseline = style?.textBaseline || ctx.textBaseline;
+ }
+ fillText(text, pos, maxWidth, style) {
+ this.setStyle(style);
+ this.ctx.fillText(text, pos.x, pos.y, maxWidth);
+ }
+ strokeText(text, pos, maxWidth, style) {
+ this.setStyle(style);
+ this.ctx.strokeText(text, pos.x, pos.y, maxWidth);
+ }
+ clearRect(at, width, height) {
+ this.ctx.clearRect(at.x, at.y, width, height);
+ }
+ // Interaction
+ mouseX = 0;
+ mouseY = 0;
+ registerDraggable(point, radius, style) {
+ if (this.draggables.find((d) => d.point === point)) return;
+ const id = this.addUIElement("circle", point, radius, {
+ fillColor: "#5533ff50",
+ strokeColor: "#5533ff50"
+ });
+ this.draggables.push({ point, radius, style, id });
+ }
+ unregisterDraggable(point) {
+ for (const d of this.draggables) {
+ if (d.point === point) {
+ this.removeUIElement(d.id);
+ }
+ }
+ this.draggables = this.draggables.filter((d) => d.point !== point);
+ }
+ registerClickable(p1, p2, cb) {
+ const top = Math.min(p1.y, p2.y);
+ const left = Math.min(p1.x, p2.x);
+ const bottom = Math.max(p1.y, p2.y);
+ const right = Math.max(p1.x, p2.x);
+ this.clickables.push({
+ onClick: cb,
+ checkBound: (p) => p.y >= top && p.x >= left && p.y <= bottom && p.x <= right
+ });
+ }
+ unregisterClickable(cb) {
+ this.clickables = this.clickables.filter((c) => c.onClick !== cb);
+ }
+ addDragEvents({
+ onDragEnd,
+ onDragStart,
+ onDrag,
+ point
+ }) {
+ const d = this.draggables.find((d2) => d2.point === point);
+ if (d) {
+ d.onDragEnd = onDragEnd;
+ d.onDragStart = onDragStart;
+ d.onDrag = onDrag;
+ }
+ }
+ onClick(e) {
+ const mouse = new Vector(this.mouseX, this.mouseY);
+ for (const d of this.draggables) {
+ if (d.point.dist(mouse) <= d.radius) {
+ d.beingDragged = true;
+ d.onDragStart?.call(null);
+ this.dragTarget = d;
+ } else d.beingDragged = false;
+ }
+ for (const c of this.clickables) {
+ if (c.checkBound(mouse)) {
+ c.onClick();
+ }
+ }
+ }
+ offClick(e) {
+ for (const d of this.draggables) {
+ d.beingDragged = false;
+ d.onDragEnd?.call(null);
+ }
+ this.dragTarget = void 0;
+ }
+ onDrag(e) {
+ const rect = this._canvas.getBoundingClientRect();
+ this.mouseX = e.offsetX;
+ this.mouseY = e.offsetY;
+ for (const d of this.draggables.filter((d2) => d2.beingDragged)) {
+ d.point.add(e.movementX, e.movementY);
+ d.onDrag && d.onDrag({ x: e.movementX, y: e.movementY });
+ }
+ }
+ // UI Layer
+ uiElements = /* @__PURE__ */ new Map();
+ uiDrawing = {
+ rectangle: (...args) => {
+ !args[3].noFill && this.fillRect(args[0], args[1], args[2], args[3]);
+ !args[3].noStroke && this.drawRect(args[0], args[1], args[2], args[3]);
+ },
+ square: (...args) => {
+ !args[2].noFill && this.fillSquare(args[0], args[1], args[2]);
+ !args[2].noStroke && this.drawSquare(args[0], args[1], args[2]);
+ },
+ circle: (...args) => {
+ !args[2].noFill && this.fillCircle(args[0], args[1], args[2]);
+ !args[2].noStroke && this.drawCircle(args[0], args[1], args[2]);
+ }
+ };
+ drawUI() {
+ for (const [shape, ...args] of this.uiElements.values()) {
+ this.uiDrawing[shape].apply(null, args);
+ }
+ }
+ addUIElement(shape, ...args) {
+ const id = crypto.randomUUID();
+ for (const arg of args) {
+ delete arg.color;
+ }
+ this.uiElements.set(id, [shape, ...args]);
+ return id;
+ }
+ removeUIElement(id) {
+ this.uiElements.delete(id);
+ }
+ };
+
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/timing/EaseInOut.ts
+ var easeInOut = (x) => x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
+
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/timing/Map.ts
+ var map = (value, x1, y1, x2, y2) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;
+
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/zoomableCanvas.ts
+ var ZoomableDoodler = class extends Doodler {
+ scale = 1;
+ dragging = false;
+ origin = {
+ x: 0,
+ y: 0
+ };
+ mouse = {
+ x: 0,
+ y: 0
+ };
+ previousTouchLength;
+ touchTimer;
+ hasDoubleTapped = false;
+ zooming = false;
+ scaleAround = { x: 0, y: 0 };
+ maxScale = 4;
+ minScale = 1;
+ constructor(options, postInit) {
+ super(options, postInit);
+ this._canvas.addEventListener("wheel", (e) => {
+ this.scaleAtMouse(e.deltaY < 0 ? 1.1 : 0.9);
+ if (this.scale === 1) {
+ this.origin.x = 0;
+ this.origin.y = 0;
+ }
+ });
+ this._canvas.addEventListener("dblclick", (e) => {
+ e.preventDefault();
+ this.scale = 1;
+ this.origin.x = 0;
+ this.origin.y = 0;
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0);
+ });
+ this._canvas.addEventListener("mousedown", (e) => {
+ e.preventDefault();
+ this.dragging = true;
+ });
+ this._canvas.addEventListener("mouseup", (e) => {
+ e.preventDefault();
+ this.dragging = false;
+ });
+ this._canvas.addEventListener("mouseleave", (_e) => {
+ this.dragging = false;
+ });
+ this._canvas.addEventListener("mousemove", (e) => {
+ const prev = this.mouse;
+ this.mouse = {
+ x: e.offsetX,
+ y: e.offsetY
+ };
+ if (this.dragging && !this.dragTarget) this.drag(prev);
+ });
+ this._canvas.addEventListener("touchstart", (e) => {
+ e.preventDefault();
+ if (e.touches.length === 1) {
+ const t1 = e.touches.item(0);
+ if (t1) {
+ this.mouse = this.getTouchOffset({
+ x: t1.clientX,
+ y: t1.clientY
+ });
+ }
+ } else {
+ clearTimeout(this.touchTimer);
+ }
+ });
+ this._canvas.addEventListener("touchend", (e) => {
+ if (e.touches.length !== 2) {
+ this.previousTouchLength = void 0;
+ }
+ switch (e.touches.length) {
+ case 1:
+ break;
+ case 0:
+ if (!this.zooming) {
+ this.events.get("touchend")?.map((cb) => cb(e));
+ }
+ break;
+ }
+ this.dragging = e.touches.length === 1;
+ clearTimeout(this.touchTimer);
+ });
+ this._canvas.addEventListener("touchmove", (e) => {
+ e.preventDefault();
+ if (e.touches.length === 2) {
+ const t1 = e.touches.item(0);
+ const t2 = e.touches.item(1);
+ if (t1 && t2) {
+ const vect = OriginVector.from(
+ this.getTouchOffset({
+ x: t1.clientX,
+ y: t1.clientY
+ }),
+ {
+ x: t2.clientX,
+ y: t2.clientY
+ }
+ );
+ if (this.previousTouchLength) {
+ const diff = this.previousTouchLength - vect.mag();
+ this.scaleAt(vect.halfwayPoint, diff < 0 ? 1.01 : 0.99);
+ this.scaleAround = { ...vect.halfwayPoint };
+ }
+ this.previousTouchLength = vect.mag();
+ }
+ }
+ if (e.touches.length === 1) {
+ this.dragging === true;
+ const t1 = e.touches.item(0);
+ if (t1) {
+ const prev = this.mouse;
+ this.mouse = this.getTouchOffset({
+ x: t1.clientX,
+ y: t1.clientY
+ });
+ this.drag(prev);
+ }
+ }
+ });
+ this._canvas.addEventListener("touchstart", (e) => {
+ if (e.touches.length !== 1) return false;
+ if (!this.hasDoubleTapped) {
+ this.hasDoubleTapped = true;
+ setTimeout(() => this.hasDoubleTapped = false, 300);
+ return false;
+ }
+ if (this.scale > 1) {
+ this.frameCounter = map(this.scale, this.maxScale, 1, 0, 59);
+ this.zoomDirection = -1;
+ } else {
+ this.frameCounter = 0;
+ this.zoomDirection = 1;
+ }
+ if (this.zoomDirection > 0) {
+ this.scaleAround = { ...this.mouse };
+ }
+ this.events.get("doubletap")?.map((cb) => cb(e));
+ });
+ }
+ worldToScreen(x, y) {
+ x = x * this.scale + this.origin.x;
+ y = y * this.scale + this.origin.y;
+ return { x, y };
+ }
+ screenToWorld(x, y) {
+ x = (x - this.origin.x) / this.scale;
+ y = (y - this.origin.y) / this.scale;
+ return { x, y };
+ }
+ scaleAtMouse(scaleBy) {
+ if (this.scale === this.maxScale && scaleBy > 1) return;
+ this.scaleAt({
+ x: this.mouse.x,
+ y: this.mouse.y
+ }, scaleBy);
+ }
+ scaleAt(p, scaleBy) {
+ this.scale = Math.min(
+ Math.max(this.scale * scaleBy, this.minScale),
+ this.maxScale
+ );
+ this.origin.x = p.x - (p.x - this.origin.x) * scaleBy;
+ this.origin.y = p.y - (p.y - this.origin.y) * scaleBy;
+ this.constrainOrigin();
+ }
+ moveOrigin(motion) {
+ if (this.scale > 1) {
+ this.origin.x += motion.x;
+ this.origin.y += motion.y;
+ this.constrainOrigin();
+ }
+ }
+ drag(prev) {
+ if (this.scale > 1) {
+ const xOffset = this.mouse.x - prev.x;
+ const yOffset = this.mouse.y - prev.y;
+ this.origin.x += xOffset;
+ this.origin.y += yOffset;
+ this.constrainOrigin();
+ }
+ }
+ constrainOrigin() {
+ this.origin.x = Math.min(
+ Math.max(
+ this.origin.x,
+ -this._canvas.width * this.scale + this._canvas.width
+ ),
+ 0
+ );
+ this.origin.y = Math.min(
+ Math.max(
+ this.origin.y,
+ -this._canvas.height * this.scale + this._canvas.height
+ ),
+ 0
+ );
+ }
+ draw(time) {
+ this.ctx.setTransform(
+ this.scale,
+ 0,
+ 0,
+ this.scale,
+ this.origin.x,
+ this.origin.y
+ );
+ this.animateZoom();
+ this.ctx.fillStyle = this.bg;
+ this.ctx.fillRect(0, 0, this.width / this.scale, this.height / this.scale);
+ super.draw(time);
+ }
+ getTouchOffset(p) {
+ const { x, y } = this._canvas.getBoundingClientRect();
+ const offsetX = p.x - x;
+ const offsetY = p.y - y;
+ return {
+ x: offsetX,
+ y: offsetY
+ };
+ }
+ onDrag(e) {
+ const d = {
+ ...e,
+ movementX: e.movementX / this.scale,
+ movementY: e.movementY / this.scale
+ };
+ super.onDrag(d);
+ const { x, y } = this.screenToWorld(e.offsetX, e.offsetY);
+ this.mouseX = x;
+ this.mouseY = y;
+ }
+ zoomDirection = -1;
+ frameCounter = 60;
+ animateZoom() {
+ if (this.frameCounter < 60) {
+ const frame = easeInOut(map(this.frameCounter, 0, 59, 0, 1));
+ switch (this.zoomDirection) {
+ case 1:
+ {
+ this.scale = map(frame, 0, 1, 1, this.maxScale);
+ }
+ break;
+ case -1:
+ {
+ this.scale = map(frame, 0, 1, this.maxScale, 1);
+ }
+ break;
+ }
+ this.origin.x = this.scaleAround.x - this.scaleAround.x * this.scale;
+ this.origin.y = this.scaleAround.y - this.scaleAround.y * this.scale;
+ this.constrainOrigin();
+ this.frameCounter++;
+ }
+ }
+ events = /* @__PURE__ */ new Map();
+ registerEvent(eventName, cb) {
+ let events = this.events.get(eventName);
+ if (!events) events = this.events.set(eventName, []).get(eventName);
+ events.push(cb);
+ }
+ };
+
+ // https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/init.ts
+ function init(opt, zoomable, postInit) {
+ if (window.doodler) {
+ throw "Doodler has already been initialized in this window";
+ }
+ window.doodler = zoomable ? new ZoomableDoodler(opt, postInit) : new Doodler(opt, postInit);
+ window.doodler.init();
+ }
+
+ // train.ts
+ var Train = class {
+ nodes = [];
+ cars = [];
+ path;
+ t;
+ engineLength = 40;
+ spacing = 30;
+ constructor(track, cars = []) {
+ this.path = track;
+ this.t = 0;
+ this.nodes.push(this.path.followEvenPoints(this.t));
+ this.nodes.push(this.path.followEvenPoints(this.t - this.real2Track(40)));
+ this.cars.push(new TrainCar(55, document.getElementById("engine-sprites"), 80, 20, { at: new Vector(0, 60), width: 80, height: 20 }));
+ this.cars[0].points = this.nodes.map((n) => n);
+ let currentOffset = 40;
+ for (const car2 of cars) {
+ currentOffset += this.spacing;
+ const a = this.path.followEvenPoints(this.t - currentOffset);
+ currentOffset += car2.length;
+ const b = this.path.followEvenPoints(this.t - currentOffset);
+ car2.points = [a, b];
+ this.cars.push(car2);
+ }
+ }
+ move() {
+ this.t = (this.t + 1) % this.path.evenPoints.length;
+ let currentOffset = 0;
+ for (const car2 of this.cars) {
+ if (!car2.points) return;
+ const [a, b] = car2.points;
+ a.set(this.path.followEvenPoints(this.t - currentOffset));
+ currentOffset += car2.length;
+ b.set(this.path.followEvenPoints(this.t - currentOffset));
+ currentOffset += this.spacing;
+ car2.draw();
+ }
+ }
+ // draw() {
+ // for (const [i, node] of this.nodes.entries()) {
+ // doodler.drawCircle(node.point, 10, { color: 'purple', weight: 3 })
+ // // 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))
+ // // }
+ // }
+ // }
+ real2Track(length) {
+ return length / this.path.pointSpacing;
+ }
+ };
+ var TrainCar = class {
+ img;
+ imgWidth;
+ imgHeight;
+ sprite;
+ points;
+ length;
+ constructor(length, img, w, h, sprite) {
+ this.img = img;
+ this.sprite = sprite;
+ this.imgWidth = w;
+ this.imgHeight = h;
+ this.length = length;
+ }
+ draw() {
+ if (!this.points) return;
+ const [a, b] = this.points;
+ const origin = Vector.add(Vector.sub(a, b).div(2), b);
+ const angle = Vector.sub(b, a).heading();
+ doodler.drawCircle(origin, 4, { color: "blue" });
+ doodler.drawRotated(origin, angle, () => {
+ this.sprite ? doodler.drawSprite(this.img, this.sprite.at, this.sprite.width, this.sprite.height, origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2), this.imgWidth, this.imgHeight) : doodler.drawImage(this.img, origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2));
+ });
+ }
+ };
+
+ // math/path.ts
+ var PathSegment = class {
+ points;
+ ctx;
+ length;
+ constructor(points) {
+ this.points = points;
+ this.length = this.calculateApproxLength(100);
+ }
+ setContext(ctx) {
+ this.ctx = ctx;
+ }
+ draw() {
+ const [a, b, c, d] = this.points;
+ doodler.drawBezier(a, b, c, d, {
+ strokeColor: "#ffffff50"
+ });
+ }
+ getPointAtT(t) {
+ 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)));
+ return res;
+ }
+ getClosestPoint(v) {
+ const samples = 25;
+ const resolution = 1 / samples;
+ let closest = this.points[0];
+ let closestDistance = this.points[0].dist(v);
+ let closestT = 0;
+ for (let i = 0; i < samples; i++) {
+ const point = this.getPointAtT(i * resolution);
+ const distance = v.dist(point);
+ if (distance < closestDistance) {
+ closest = point;
+ closestDistance = distance;
+ closestT = i * resolution;
+ }
+ }
+ return [closest, closestDistance, closestT];
+ }
+ getPointsWithinRadius(v, r) {
+ const points = [];
+ const samples = 25;
+ const resolution = 1 / samples;
+ for (let i = 0; i < samples; i++) {
+ const point = this.getPointAtT(i * resolution);
+ const distance = v.dist(point);
+ if (distance < r) {
+ points.push([i * resolution, this]);
+ }
+ }
+ return points;
+ }
+ tangent(t) {
+ 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))));
+ return res;
+ }
+ doesIntersectCircle(x, y, r) {
+ const v = new Vector(x, y);
+ const samples = 25;
+ const resolution = 1 / samples;
+ let distance = Infinity;
+ let t;
+ for (let i = 0; i < samples; i++) {
+ if (i !== samples - 1) {
+ const a = this.getPointAtT(i * resolution);
+ const b = this.getPointAtT((i + 1) * resolution);
+ const ac = Vector.sub(v, a);
+ const ab = Vector.sub(b, a);
+ const d = Vector.add(Vector.vectorProjection(ac, ab), a);
+ const ad = Vector.sub(d, a);
+ const k = Math.abs(ab.x) > Math.abs(ab.y) ? ad.x / ab.x : ad.y / ab.y;
+ let dist;
+ if (k <= 0) {
+ dist = Vector.hypot2(v, a);
+ } else if (k >= 1) {
+ dist = Vector.hypot2(v, b);
+ }
+ dist = Vector.hypot2(v, d);
+ if (dist < distance) {
+ distance = dist;
+ t = i * resolution;
+ }
+ }
+ }
+ if (distance < r) return t;
+ return false;
+ }
+ calculateApproxLength(resolution = 25) {
+ const stepSize = 1 / resolution;
+ const points = [];
+ for (let i = 0; i <= resolution; i++) {
+ const current = stepSize * i;
+ points.push(this.getPointAtT(current));
+ }
+ this.length = points.reduce((acc, cur) => {
+ const prev = acc.prev;
+ acc.prev = cur;
+ if (!prev) return acc;
+ acc.length += cur.dist(prev);
+ return acc;
+ }, { prev: void 0, length: 0 }).length;
+ return this.length;
+ }
+ calculateEvenlySpacedPoints(spacing, resolution = 1) {
+ const points = [];
+ points.push(this.points[0]);
+ let prev = points[0];
+ let distSinceLastEvenPoint = 0;
+ let t = 0;
+ const div = Math.ceil(this.length * resolution * 10);
+ while (t < 1) {
+ 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;
+ }
+ };
+
+ // track.ts
+ var Track = class extends PathSegment {
+ editable = false;
+ next;
+ prev;
+ id;
+ constructor(points, next, prev) {
+ super(points);
+ this.id = crypto.randomUUID();
+ this.next = next || this;
+ this.prev = prev || this;
+ }
+ // followTrack(train: Train): [Vector, number] {
+ // const predict = train.velocity.copy();
+ // predict.normalize();
+ // predict.mult(1);
+ // const predictpos = Vector.add(train.position, predict)
+ // // const leading = train.leadingPoint;
+ // // let closest = this.points[0];
+ // // let closestDistance = this.getClosestPoint(leading);
+ // let [closest, closestDistance, closestT] = this.getClosestPoint(predictpos);
+ // // deno-lint-ignore no-this-alias
+ // let mostValid: Track = this;
+ // if (this.next !== this) {
+ // const [point, distance, t] = this.next.getClosestPoint(predictpos);
+ // if (distance < closestDistance) {
+ // closest = point;
+ // closestDistance = distance;
+ // mostValid = this.next;
+ // closestT = t;
+ // }
+ // }
+ // if (this.prev !== this) {
+ // const [point, distance, t] = this.next.getClosestPoint(predictpos);
+ // if (distance < closestDistance) {
+ // closest = point;
+ // closestDistance = distance;
+ // mostValid = this.next;
+ // closestT = t;
+ // }
+ // }
+ // train.currentTrack = mostValid;
+ // train.arrive(closest);
+ // // if (predictpos.dist(closest) > 2) train.arrive(closest);
+ // return [closest, closestT];
+ // }
+ getNearestPoint(p) {
+ let [closest, closestDistance] = this.getClosestPoint(p);
+ if (this.next !== this) {
+ const [point, distance, t] = this.next.getClosestPoint(p);
+ if (distance < closestDistance) {
+ closest = point;
+ closestDistance = distance;
+ }
+ }
+ if (this.prev !== this) {
+ const [point, distance, t] = this.next.getClosestPoint(p);
+ if (distance < closestDistance) {
+ closest = point;
+ closestDistance = distance;
+ }
+ }
+ return closest;
+ }
+ getAllPointsInRange(v, r) {
+ const points = this.getPointsWithinRadius(v, r).concat(this.next.getPointsWithinRadius(v, r), this.prev.getPointsWithinRadius(v, r));
+ return points;
+ }
+ draw() {
+ super.draw();
+ if (this.editable) {
+ const [a, b, c, d] = this.points;
+ doodler.line(a, b);
+ doodler.line(c, d);
+ }
+ }
+ setNext(t) {
+ this.next = t;
+ this.next.points[0] = this.points[3];
+ }
+ setPrev(t) {
+ this.prev = t;
+ this.prev.points[3] = this.points[0];
+ }
+ };
+ var Spline = class {
+ segments = [];
+ ctx;
+ evenPoints;
+ pointSpacing;
+ get points() {
+ return Array.from(new Set(this.segments.flatMap((s) => s.points)));
+ }
+ nodes;
+ constructor(segs) {
+ this.segments = segs;
+ this.pointSpacing = 1;
+ this.evenPoints = this.calculateEvenlySpacedPoints(1);
+ this.nodes = [];
+ for (let i = 0; i < this.points.length; i += 3) {
+ const node = {
+ anchor: this.points[i],
+ controls: [this.points.at(i - 1), this.points[(i + 1) % this.points.length]],
+ mirrored: false,
+ tangent: true
+ };
+ this.nodes.push(node);
+ }
+ }
+ setContext(ctx) {
+ this.ctx = ctx;
+ for (const segment of this.segments) {
+ segment.setContext(ctx);
+ }
+ }
+ draw() {
+ for (const segment of this.segments) {
+ segment.draw();
+ }
+ }
+ calculateEvenlySpacedPoints(spacing, resolution = 1) {
+ this.pointSpacing = 1;
+ const points = [];
+ points.push(this.segments[0].points[0]);
+ let prev = points[0];
+ let distSinceLastEvenPoint = 0;
+ for (const seg of this.segments) {
+ let t = 0;
+ const div = Math.ceil(seg.length * resolution * 10);
+ while (t < 1) {
+ t += 1 / div;
+ const point = seg.getPointAtT(t);
+ distSinceLastEvenPoint += prev.dist(point);
+ if (distSinceLastEvenPoint >= spacing) {
+ const overshoot = distSinceLastEvenPoint - spacing;
+ const evenPoint = Vector.add(point, Vector.sub(point, prev).normalize().mult(overshoot));
+ distSinceLastEvenPoint = overshoot;
+ points.push(evenPoint);
+ prev = evenPoint;
+ }
+ prev = point;
+ }
+ }
+ this.evenPoints = points;
+ return points;
+ }
+ followEvenPoints(t) {
+ if (t < 0) t += this.evenPoints.length;
+ const i = Math.floor(t);
+ const a = this.evenPoints[i];
+ const b = this.evenPoints[(i + 1) % this.evenPoints.length];
+ return Vector.lerp(a, b, t % 1);
+ }
+ calculateApproxLength() {
+ for (const s of this.segments) {
+ s.calculateApproxLength();
+ }
+ }
+ toggleNodeTangent(p) {
+ const node = this.nodes.find((n) => n.anchor === p);
+ node && (node.tangent = !node.tangent);
+ }
+ toggleNodeMirrored(p) {
+ const node = this.nodes.find((n) => n.anchor === p);
+ node && (node.mirrored = !node.mirrored);
+ }
+ handleNodeEdit(p, movement) {
+ const node = this.nodes.find((n) => n.anchor === p || n.controls.includes(p));
+ if (!node || !(node.mirrored || node.tangent)) return;
+ if (node.anchor !== p) {
+ if (node.mirrored || node.tangent) {
+ const mover = node.controls.find((e) => e !== p);
+ const v = Vector.sub(node.anchor, p);
+ if (!node.mirrored) v.setMag(Vector.sub(node.anchor, mover).mag());
+ mover.set(Vector.add(v, node.anchor));
+ }
+ } else {
+ for (const control of node.controls) {
+ control.add(movement.x, movement.y);
+ }
+ }
+ }
+ };
+ var generateSquareTrack = () => {
+ const first = new Track([new Vector(20, 40), new Vector(20, 100), new Vector(20, 300), new Vector(20, 360)]);
+ const second = new Track([first.points[3], new Vector(20, 370), new Vector(30, 380), new Vector(40, 380)]);
+ const third = new Track([second.points[3], new Vector(100, 380), new Vector(300, 380), new Vector(360, 380)]);
+ const fourth = new Track([third.points[3], new Vector(370, 380), new Vector(380, 370), new Vector(380, 360)]);
+ const fifth = new Track([fourth.points[3], new Vector(380, 300), new Vector(380, 100), new Vector(380, 40)]);
+ const sixth = new Track([fifth.points[3], new Vector(380, 30), new Vector(370, 20), new Vector(360, 20)]);
+ const seventh = new Track([sixth.points[3], new Vector(300, 20), new Vector(100, 20), new Vector(40, 20)]);
+ const eighth = new Track([seventh.points[3], new Vector(30, 20), new Vector(20, 30), first.points[0]]);
+ const tracks = [first, second, third, fourth, fifth, sixth, seventh, eighth];
+ for (const [i, track] of tracks.entries()) {
+ track.next = tracks[(i + 1) % tracks.length];
+ track.prev = tracks.at(i - 1);
+ }
+ return new Spline([first, second, third, fourth, fifth, sixth, seventh, eighth]);
+ };
+
+ // main.ts
+ var engineSprites = document.createElement("img");
+ engineSprites.src = "./sprites/EngineSprites.png";
+ engineSprites.style.display = "none";
+ engineSprites.id = "engine-sprites";
+ document.body.append(engineSprites);
+ init({
+ width: 400,
+ height: 400,
+ bg: "#333"
+ });
+ var path = generateSquareTrack();
+ var speed = 1;
+ var car = new TrainCar(55, engineSprites, 80, 20, {
+ at: new Vector(0, 80),
+ height: 20,
+ width: 80
+ });
+ var train = new Train(path, [car]);
+ var dragEndCounter = 0;
+ var selectedNode;
+ doodler.createLayer(() => {
+ for (let i = 0; i < path.evenPoints.length; i += 10) {
+ const p = path.evenPoints[i];
+ const next = path.evenPoints[(i + 1) % path.evenPoints.length];
+ const last = path.evenPoints.at(i - 1);
+ if (!last) break;
+ const tan = Vector.sub(last, next);
+ doodler.drawRotated(p, tan.heading(), () => {
+ doodler.line(p, p.copy().add(0, 10), { color: "#291b17", weight: 4 });
+ doodler.line(p, p.copy().add(0, -10), { color: "#291b17", weight: 4 });
+ doodler.line(p.copy().add(-6, 5), p.copy().add(6, 5), {
+ color: "grey",
+ weight: 2
+ });
+ doodler.line(p.copy().add(-6, -5), p.copy().add(6, -5), {
+ color: "grey",
+ weight: 2
+ });
+ });
+ }
+ path.draw();
+ train.move();
+ selectedNode?.anchor.drawDot();
+ selectedNode?.controls.forEach((e) => e.drawDot());
+ });
+ var editable = false;
+ var clickables = /* @__PURE__ */ new Map();
+ var selectedPoint;
+ document.addEventListener("keyup", (e) => {
+ if (e.key === "d") {
+ }
+ if (e.key === "ArrowUp") {
+ speed += 0.1;
+ }
+ if (e.key === "ArrowDown") {
+ speed -= 0.1;
+ }
+ if (e.key === "m" && selectedPoint) {
+ const points = path.points;
+ const index = points.findIndex((p) => p === selectedPoint);
+ if (index > -1) {
+ const prev = points.at(index - 1);
+ const next = points[(index + 1) % points.length];
+ const toPrev = Vector.sub(prev, selectedPoint);
+ toPrev.setMag(next.dist(selectedPoint));
+ toPrev.rotate(Math.PI);
+ const toNext = Vector.add(toPrev, selectedPoint);
+ next.set(toNext);
+ path.calculateApproxLength();
+ path.calculateEvenlySpacedPoints(1);
+ }
+ }
+ if (e.key === "e") {
+ editable = !editable;
+ for (const t of path.segments) {
+ t.editable = !t.editable;
+ for (const p of t.points) {
+ if (t.editable) {
+ doodler.registerDraggable(p, 10);
+ doodler.addDragEvents({
+ point: p,
+ onDragEnd: () => {
+ dragEndCounter++;
+ t.length = t.calculateApproxLength(100);
+ path.evenPoints = path.calculateEvenlySpacedPoints(1);
+ },
+ onDrag: (movement) => {
+ path.handleNodeEdit(p, movement);
+ }
+ });
+ } else {
+ doodler.unregisterDraggable(p);
+ }
+ }
+ }
+ for (const p of path.points) {
+ if (editable) {
+ const onClick = () => {
+ selectedPoint = p;
+ selectedNode = path.nodes.find(
+ (e2) => e2.anchor === p || e2.controls.includes(p)
+ );
+ };
+ clickables.set(p, onClick);
+ doodler.registerClickable(
+ p.copy().sub(10, 10),
+ p.copy().add(10, 10),
+ onClick
+ );
+ } else {
+ const the = clickables.get(p);
+ doodler.unregisterClickable(the);
+ }
+ }
+ }
+ });
+ document.addEventListener("keydown", (e) => {
+ if (e.ctrlKey && e.key === "s") {
+ e.preventDefault();
+ path.segments.forEach((s) => {
+ s.next = s.next.id;
+ s.prev = s.prev.id;
+ delete s.ctx;
+ });
+ delete path.ctx;
+ const json = JSON.stringify(path);
+ localStorage.setItem("railPath", json);
+ }
+ });
+})();
diff --git a/train.ts b/train.ts
index 4290318..ed53e37 100644
--- a/train.ts
+++ b/train.ts
@@ -16,30 +16,52 @@ export class Train {
engineLength = 40;
spacing = 30;
+ speed = 0;
+
constructor(track: Spline