From 08d63395e378ecae8ea9db86e8e3706efc86d4d4 Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 13 Feb 2023 16:38:58 -0700 Subject: [PATCH] CARS and also better node editing --- bundle.js | 583 ++++++++++++++++---------------------- deno.jsonc | 2 +- main.ts | 115 +++++--- math/path.ts | 3 +- sprites/BlueEngine.png | Bin 0 -> 1556 bytes sprites/Engine.png | Bin 0 -> 1281 bytes sprites/EngineSprites.png | Bin 0 -> 23055 bytes sprites/GreenEngine.png | Bin 0 -> 1486 bytes sprites/PurpleEngine.png | Bin 0 -> 1578 bytes sprites/RedEngine.png | Bin 0 -> 1545 bytes track.ts | 155 ++++++---- train.old.ts | 312 ++++++++++++++++++++ train.ts | 372 ++++++------------------ 13 files changed, 832 insertions(+), 710 deletions(-) create mode 100644 sprites/BlueEngine.png create mode 100644 sprites/Engine.png create mode 100644 sprites/EngineSprites.png create mode 100644 sprites/GreenEngine.png create mode 100644 sprites/PurpleEngine.png create mode 100644 sprites/RedEngine.png create mode 100644 train.old.ts diff --git a/bundle.js b/bundle.js index fc3cd70..7fca96c 100644 --- a/bundle.js +++ b/bundle.js @@ -5,10 +5,6 @@ const Constants = { TWO_PI: Math.PI * 2 }; -const map = (value, x1, y1, x2, y2)=>(value - x1) * (y2 - x2) / (y1 - x1) + x2; -const Constants1 = { - TWO_PI: Math.PI * 2 -}; class Vector { x; y; @@ -63,6 +59,7 @@ class Vector { this.y += y ?? 0; this.z += z ?? 0; } + return this; } sub(v, y, z) { if (arguments.length === 1 && typeof v !== 'number') { @@ -77,6 +74,7 @@ class Vector { this.y -= y ?? 0; this.z -= z ?? 0; } + return this; } mult(v) { if (typeof v === 'number') { @@ -100,6 +98,7 @@ class Vector { this.y /= v.y; this.z /= v.z; } + return this; } rotate(angle) { const prev_x = this.x; @@ -107,6 +106,7 @@ class Vector { 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; @@ -139,6 +139,7 @@ class Vector { 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(); @@ -152,6 +153,7 @@ class Vector { this.normalize(); this.mult(high); } + return this; } heading() { return -Math.atan2(-this.y, this.x); @@ -191,7 +193,7 @@ class Vector { return Vector.fromAngle(Math.random() * (Math.PI * 2), v); } static random3D(v) { - const angle = Math.random() * Constants1.TWO_PI; + 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); @@ -255,6 +257,7 @@ class Doodler { return this.ctx.canvas.height; } draggables = []; + clickables = []; constructor({ width , height , canvas , bg , framerate }){ if (!canvas) { canvas = document.createElement('canvas'); @@ -279,6 +282,10 @@ class Doodler { 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(); @@ -323,19 +330,19 @@ class Doodler { weight: 1 }); this.ctx.beginPath(); - this.ctx.arc(at.x, at.y, style?.weight || 1, 0, Constants1.TWO_PI); + 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, Constants1.TWO_PI); + 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, Constants1.TWO_PI); + this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI); this.ctx.fill(); } drawRect(at, width, height, style) { @@ -385,6 +392,12 @@ class Doodler { 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); + } + drawSprite(img, spritePos, sWidth, sHeight, at, width, height) { + this.ctx.drawImage(img, spritePos.x, spritePos.y, sWidth, sHeight, at.x, at.y, width, height); + } setStyle(style) { const ctx = this.ctx; ctx.fillStyle = style?.color || style?.fillColor || 'black'; @@ -414,20 +427,40 @@ class Doodler { } this.draggables = this.draggables.filter((d)=>d.point !== point); } - addDragEvents({ onDragEnd , onDragStart , 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((d)=>d.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(new Vector(this.mouseX, this.mouseY)) <= d.radius) { + 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(); + } + } } offClick(e) { for (const d of this.draggables){ @@ -470,35 +503,79 @@ class Doodler { this.uiElements.delete(id); } } -class ComplexPath { - points = []; - radius = 50; - ctx; - constructor(points){ - points && (this.points = points); +class Train { + 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); + } } - setContext(ctx) { - this.ctx = ctx; + 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(); + } + } + real2Track(length) { + return length / this.path.pointSpacing; + } +} +class TrainCar { + 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.ctx || !this.points.length) return; - const ctx = this.ctx; - ctx.save(); - ctx.lineWidth = 2; - ctx.strokeStyle = 'white'; - ctx.setLineDash([ - 21, - 6 - ]); - let last = this.points[this.points.length - 1]; - for (const point of this.points){ - ctx.beginPath(); - ctx.moveTo(last.x, last.y); - ctx.lineTo(point.x, point.y); - ctx.stroke(); - last = point; - } - ctx.restore(); + 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 { @@ -604,7 +681,7 @@ class PathSegment { const current = stepSize * i; points.push(this.getPointAtT(current)); } - return points.reduce((acc, cur)=>{ + this.length = points.reduce((acc, cur)=>{ const prev = acc.prev; acc.prev = cur; if (!prev) return acc; @@ -614,6 +691,7 @@ class PathSegment { prev: undefined, length: 0 }).length; + return this.length; } calculateEvenlySpacedPoints(spacing, resolution = 1) { const points = []; @@ -638,254 +716,6 @@ class PathSegment { return points; } } -class Mover { - position; - velocity; - acceleration; - maxSpeed; - maxForce; - _trailingPoint; - _leadingPoint; - get trailingPoint() { - const desired = this.velocity.copy(); - desired.normalize(); - desired.mult(-this._trailingPoint); - return Vector.add(this.position, desired); - } - get leadingPoint() { - const desired = this.velocity.copy(); - desired.normalize(); - desired.mult(this._leadingPoint); - return Vector.add(this.position, desired); - } - ctx; - boundingBox; - constructor(posOrRandom, vel, acc){ - if (typeof posOrRandom === 'boolean' && posOrRandom) { - this.position = Vector.random2D(new Vector()); - this.velocity = Vector.random2D(new Vector()); - this.acceleration = new Vector(); - } else { - this.position = posOrRandom || new Vector(); - this.velocity = vel || new Vector(); - this.acceleration = acc || new Vector(); - } - this.boundingBox = { - size: new Vector(20, 10), - pos: new Vector(this.position.x - 10, this.position.y - 5) - }; - this.maxSpeed = 3; - this.maxForce = .3; - this._trailingPoint = 0; - this._leadingPoint = 0; - this.init(); - } - init() {} - move() { - this.velocity.limit(this.maxSpeed); - this.acceleration.limit(this.maxForce); - this.velocity.add(this.acceleration); - this.position.add(this.velocity); - this.edges(); - this.draw(); - } - edges() { - if (!this.ctx) return; - if (this.position.x > this.ctx.canvas.width) this.position.x = 0; - if (this.position.y > this.ctx.canvas.height) this.position.y = 0; - if (this.position.x < 0) this.position.x = this.ctx.canvas.width; - if (this.position.y < 0) this.position.y = this.ctx.canvas.height; - } - draw() { - doodler.drawRotated(this.position, this.velocity.heading() || 0, ()=>{ - doodler.fillCenteredRect(this.position, this.boundingBox.size.x, this.boundingBox.size.y, { - fillColor: 'white' - }); - }); - if (!this.ctx) return; - this.ctx.fillStyle = 'white'; - this.ctx.save(); - this.ctx.translate(this.position.x, this.position.y); - this.ctx.rotate(this.velocity.heading() || 0); - this.ctx.translate(-this.position.x, -this.position.y); - this.ctx.translate(-(this.boundingBox.size.x / 2), -(this.boundingBox.size.y / 2)); - this.ctx.fillRect(this.position.x, this.position.y, this.boundingBox.size.x, this.boundingBox.size.y); - this.ctx.restore(); - } - setContext(ctx) { - this.ctx = ctx; - } - applyForce(force) { - this.acceleration.add(force); - } - static edges(point, width, height) { - if (point.x > width) point.x = 0; - if (point.y > height) point.y = 0; - if (point.x < 0) point.x = width; - if (point.y < 0) point.y = height; - } -} -class Follower extends Mover { - debug = true; - follow(toFollow) { - if (toFollow instanceof ComplexPath) { - const predict = this.velocity.copy(); - predict.normalize(); - predict.mult(25); - const predictpos = Vector.add(this.position, predict); - if (this.ctx) Mover.edges(predict, this.ctx.canvas.width, this.ctx.canvas.height); - let normal = null; - let target = null; - let worldRecord = 1000000; - for(let i = 0; i < toFollow.points.length; i++){ - let a = toFollow.points[i]; - let b = toFollow.points[(i + 1) % toFollow.points.length]; - let normalPoint = getNormalPoint(predictpos, a, b); - let dir = Vector.sub(b, a); - if (normalPoint.x < Math.min(a.x, b.x) || normalPoint.x > Math.max(a.x, b.x) || normalPoint.y < Math.min(a.y, b.y) || normalPoint.y > Math.max(a.y, b.y)) { - normalPoint = b.copy(); - a = toFollow.points[(i + 1) % toFollow.points.length]; - b = toFollow.points[(i + 2) % toFollow.points.length]; - dir = Vector.sub(b, a); - } - const d = Vector.dist(predictpos, normalPoint); - if (d < worldRecord) { - worldRecord = d; - normal = normalPoint; - dir.normalize(); - dir.mult(25); - target = normal.copy(); - target.add(dir); - } - if (worldRecord > toFollow.radius) { - return this.seek(target); - } - } - if (this.debug && this.ctx) { - this.ctx.strokeStyle = 'red'; - this.ctx.fillStyle = 'pink'; - this.ctx.beginPath(); - this.ctx.moveTo(this.position.x, this.position.y); - this.ctx.lineTo(predictpos.x, predictpos.y); - this.ctx.stroke(); - this.ctx.beginPath(); - this.ctx.arc(predictpos.x, predictpos.y, 4, 0, Constants.TWO_PI); - this.ctx.fill(); - this.ctx.stroke(); - this.ctx.beginPath(); - this.ctx.arc(normal.x, normal.y, 4, 0, Constants.TWO_PI); - this.ctx.fill(); - this.ctx.stroke(); - this.ctx.beginPath(); - this.ctx.moveTo(predictpos.x, predictpos.y); - this.ctx.lineTo(target.x, target.y); - this.ctx.stroke(); - this.ctx.beginPath(); - this.ctx.arc(target.x, target.y, 8, 0, Constants.TWO_PI); - this.ctx.fill(); - this.ctx.stroke(); - } - } - } - seek(target, strength = 1) { - const desired = Vector.sub(target, this.position); - desired.normalize(); - desired.mult(this.maxSpeed); - const steer = Vector.sub(desired, this.velocity); - steer.limit(this.maxForce); - this.applyForce(steer.mult(strength)); - } - link(target) { - this.position = target.trailingPoint; - this.seek(target.trailingPoint); - } - arrive(target) { - const desired = Vector.sub(target, this.position); - const d = desired.mag(); - let speed = this.maxSpeed; - if (d < 10) { - speed = map(d, 0, 100, 0, this.maxSpeed); - } - desired.setMag(speed); - const steer = Vector.sub(desired, this.velocity); - steer.limit(this.maxForce); - this.applyForce(steer); - } -} -function getNormalPoint(p, a, b) { - const ap = Vector.sub(p, a); - const ab = Vector.sub(b, a); - ab.normalize(); - ab.mult(ap.dot(ab)); - const normalPoint = Vector.add(a, ab); - return normalPoint; -} -class Train extends Follower { - nodes; - currentTrack; - speed; - follower; - followers; - constructor(track, length){ - super(track.points[0].copy()); - this.maxSpeed = 2; - this.speed = 1; - this.currentTrack = track; - this.velocity = this.currentTrack.tangent(0).normalize().mult(this.maxSpeed); - this.addCar(length); - this.maxForce = .2; - } - init() { - this.boundingBox.size.set(30, 10); - this._trailingPoint = 30; - } - move() { - this.follow(); - super.move(); - this.follower?.move(); - } - follow() { - const [_, t] = this.currentTrack.followTrack(this); - this.velocity = this.currentTrack.tangent(t); - this.velocity.normalize().mult(this.speed || this.maxSpeed); - } - setContext(ctx) { - super.setContext(ctx); - this.follower?.setContext(ctx); - } - addCar(length) { - console.log(length); - if (length) this.follower = new TrainCar(this.currentTrack, length - 1); - this.follower?.setTarget(this); - this.follower?.position.set(this.trailingPoint); - this._trailingPoint -= 2; - } -} -class TrainCar extends Train { - target; - setTarget(t) { - this.target = t; - } - init() { - this.boundingBox.size.set(20, 10); - this._trailingPoint = 25; - this.maxSpeed = this.maxSpeed * 2; - this.maxForce = this.maxForce * 2; - } - move() { - if (this.target) { - if (this.position.dist(this.target.position) > this.target.position.dist(this.target.trailingPoint)) { - this.arrive(this.currentTrack.getNearestPoint(this.target.trailingPoint)); - this.speed = this.target.speed; - super.move(); - } else { - this.draw(); - this.follower?.draw(); - } - } - } - edges() {} -} class Track extends PathSegment { editable = false; next; @@ -897,38 +727,6 @@ class Track extends PathSegment { this.next = next || this; this.prev = prev || this; } - followTrack(train) { - const predict = train.velocity.copy(); - predict.normalize(); - predict.mult(1); - const predictpos = Vector.add(train.position, predict); - let [closest, closestDistance, closestT] = this.getClosestPoint(predictpos); - let mostValid = 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 [point1, distance1, t1] = this.next.getClosestPoint(predictpos); - if (distance1 < closestDistance) { - closest = point1; - closestDistance = distance1; - mostValid = this.next; - closestT = t1; - } - } - train.currentTrack = mostValid; - train.arrive(closest); - return [ - closest, - closestT - ]; - } getNearestPoint(p) { let [closest, closestDistance] = this.getClosestPoint(p); if (this.next !== this) { @@ -953,8 +751,10 @@ class Track extends PathSegment { } draw() { super.draw(); - if (this.editable) for (const e of this.points){ - e.drawDot(); + if (this.editable) { + const [a, b, c, d] = this.points; + doodler.line(a, b); + doodler.line(c, d); } } setNext(t) { @@ -970,9 +770,28 @@ class Spline { 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; @@ -986,6 +805,7 @@ class Spline { } } calculateEvenlySpacedPoints(spacing, resolution = 1) { + this.pointSpacing = 1; const points = []; points.push(this.segments[0].points[0]); let prev = points[0]; @@ -1007,6 +827,7 @@ class Spline { prev = point; } } + this.evenPoints = points; return points; } followEvenPoints(t) { @@ -1014,10 +835,35 @@ class Spline { const i = Math.floor(t); const a = this.evenPoints[i]; const b = this.evenPoints[(i + 1) % this.evenPoints.length]; - try { - return Vector.lerp(a, b, t % 1); - } catch { - console.log(t, i, a, b); + 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); + } } } } @@ -1108,25 +954,62 @@ const loadFromJson = ()=>{ } 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 t = 0; let speed = 1; -Array(1).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 +]); +let dragEndCounter = 0; +let selectedNode; doodler.createLayer(()=>{ - path.draw(); - const points = Array(5).fill(null).map((_, i)=>path.followEvenPoints(t - i * 15)); - for (const point of points){ - point && doodler.drawCircle(point, 5, { - strokeColor: 'green' + 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 + }); }); } - t = (t + speed / 2) % path.evenPoints.length; + path.draw(); + train.move(); + 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') { @@ -1135,7 +1018,23 @@ document.addEventListener('keyup', (e)=>{ if (e.key === 'ArrowDown') { 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 === 'e') { + editable = !editable; for (const t of path.segments){ t.editable = !t.editable; for (const p of t.points){ @@ -1144,12 +1043,30 @@ document.addEventListener('keyup', (e)=>{ doodler.addDragEvents({ point: p, onDragEnd: ()=>{ - console.log('dragend'); + dragEndCounter++; t.length = t.calculateApproxLength(100); path.evenPoints = path.calculateEvenlySpacedPoints(1); + }, + onDrag: (movement)=>{ + path.handleNodeEdit(p, movement); } }); - } else doodler.unregisterDraggable(p); + } 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); } } } diff --git a/deno.jsonc b/deno.jsonc index 6379d7c..42b9f3a 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -12,6 +12,6 @@ }, "imports": { "drawing": "./drawing/index.ts", - "doodler": "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.4a/mod.ts" + "doodler": "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.7a/mod.ts" } } \ No newline at end of file diff --git a/main.ts b/main.ts index 345129b..a3e3ee6 100644 --- a/main.ts +++ b/main.ts @@ -1,18 +1,19 @@ import { lerp } from "./math/lerp.ts"; import { ComplexPath, PathSegment } from "./math/path.ts"; import { Mover } from "./physics/mover.ts"; -import { Train } from "./train.ts"; +import { Train, TrainCar } from "./train.ts"; import { fillCircle, drawCircle } from 'drawing'; -import { generateSquareTrack, loadFromJson } from "./track.ts"; +import { generateSquareTrack, IControlNode, loadFromJson } from "./track.ts"; import { drawLine } from "./drawing/line.ts"; import { initializeDoodler, Vector } from 'doodler'; -// for (const mover of trains) { -// mover.setContext(ctx); -// mover.velocity.add(Vector.random2D()) -// } +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, @@ -33,43 +34,42 @@ let t = 0; let currentSeg = 0; let speed = 1; -const trainCount = 1; -const trains = Array(trainCount).fill(null).map((_, i) => new Train(path.segments[i % path.segments.length], 5)); +// 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]); + +let dragEndCounter = 0 +let selectedNode: IControlNode | undefined; doodler.createLayer(() => { - path.draw(); + 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); - // for (const train of trains) { - // train.move(); - // } - - // ctx.strokeStyle = 'red'; - // ctx.lineWidth = 4; - // const seg = path.segments[currentSeg]; - // const start = seg.getPointAtT(t); - // const tan = seg.tangent(t).normalize().mult(25); - // const tan = seg.tangent(t); - - // for (const p of path.evenPoints) { - // p.drawDot(); - // } - // doodler.line(start, new Vector(start.x + tan.x, start.y + tan.y), {color: 'blue'}); - // doodler.fillCircle(start, 5, {fillColor: 'blue'}) - - const points = Array(5).fill(null).map((_,i) => path.followEvenPoints(t - (i * 15))) - for (const point of points) { - point && - doodler.drawCircle(point, 5, { strokeColor: 'green' }) - + 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}) + }) } - - // const point = path.followEvenPoints(t); + path.draw(); + train.move(); - t = (t + (speed / 2)) % path.evenPoints.length; - - // path.segments.forEach(s => s.calculateApproxLength(10000)) + selectedNode?.anchor.drawDot(); + selectedNode?.controls.forEach(e => e.drawDot()); }) +let editable = false; + +const clickables = new Map() + +let selectedPoint: Vector; + document.addEventListener('keyup', e => { if (e.key === 'd') { // console.log(trains) @@ -90,7 +90,26 @@ document.addEventListener('keyup', e => { 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 === 'e') { + editable = !editable; for (const t of path.segments) { t.editable = !t.editable; for (const p of t.points) { @@ -99,14 +118,34 @@ document.addEventListener('keyup', e => { doodler.addDragEvents({ point: p, onDragEnd: () => { - console.log('dragend'); + dragEndCounter++ t.length = t.calculateApproxLength(100) path.evenPoints = path.calculateEvenlySpacedPoints(1) + }, + onDrag: (movement) => { + // todo - remove ! after updating doodler + path.handleNodeEdit(p, movement!) } }) } - else + else { doodler.unregisterDraggable(p) + } + } + } + for (const p of path.points) { + if (editable) { + const onClick = () => { + selectedPoint = 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 { + const the = clickables.get(p); + doodler.unregisterClickable(the); } } } diff --git a/math/path.ts b/math/path.ts index 618e057..05ae072 100644 --- a/math/path.ts +++ b/math/path.ts @@ -184,13 +184,14 @@ export class PathSegment { const current = stepSize * i; points.push(this.getPointAtT(current)) } - return points.reduce((acc: { prev?: Vector, length: number }, cur) => { + this.length = points.reduce((acc: { prev?: Vector, length: number }, cur) => { const prev = acc.prev; acc.prev = cur; if (!prev) return acc; acc.length += cur.dist(prev); return acc; }, { prev: undefined, length: 0 }).length + return this.length; } calculateEvenlySpacedPoints(spacing: number, resolution = 1) { diff --git a/sprites/BlueEngine.png b/sprites/BlueEngine.png new file mode 100644 index 0000000000000000000000000000000000000000..4298f2218a9bdd00b17b6be8c5a89ce531b37145 GIT binary patch literal 1556 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 CjFnmd literal 0 HcmV?d00001 diff --git a/sprites/GreenEngine.png b/sprites/GreenEngine.png new file mode 100644 index 0000000000000000000000000000000000000000..2f9556b9304eca61dc292934b4bfed3e36a5b0a6 GIT binary patch literal 1486 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 literal 0 HcmV?d00001 diff --git a/sprites/RedEngine.png b/sprites/RedEngine.png new file mode 100644 index 0000000000000000000000000000000000000000..52cb1b662db709c788758f077d51c9449ea08de3 GIT binary patch literal 1545 zcmV+k2KM=hP)EX>4Tx04R}tkv&MmKpe$iQ>CI62P=wn$WWauh>AE$6^me@v=v%)FuC+YXws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;tBaGOi`@g@7ImB8&lvnR+6}v_(bAarW+RVI`Qbkx9)Fhls^u8_R9XN`^{2MI2F7jq-)8 z%L?Z$&T6^Jn)l={4Cb}vG}mc{5yv7DNJ4~+DmGAtg($5WDJD|1AM@}JJN_iOWO8kQ zkz*besE`~#_#gc4)+|g;xJkhn(D`E9A0t3u7iiRM`}^3o8z+GO8Mx9~{z@H~`6Rv8 z(jrGd-!^b@-O}Ve;Bp5TdeS9BawI=Zp-=$c&*+sb!z3TSX+{ftykfE-YZh(VB zV6;ftYaZ|JYVYmeGtK^f0A?(5qPs&0RR91024YJ`L;xZHA^;++>lASS000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2j>C|02~Y$LVPp;000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}000BvNklNqsE$1`*9Vd3%A zj%UW>#6eb%H1hcBn=^C2bIy0~xdKqH*L8PySL*e;`o7PrWS>82(!^ZZPk{2cwlOg- zwOUP=mzOy^JEL4K)9?2uK%ed%@&^$Wq|nPYa2E!D3x_uL$^?eMayj+k2r%(g#1;u3p>v>*#E0j3W8+>PaR3{82Ph79#K|<%_@fsAuQT|a_RchO zn$4ydV`w&;k`=)|U{Ioh*COGs+dP0qqak?_9K9$Q^+mzk8T{s%$s=%@-h?Pod_T1Q zy?4NIE_|(}{OCz4o~05<94D&~h9d%#sgz z=gaSsEVQxi43u*~p0$=JipEwaE5hN%9qiMi0lrkizV!xv$)nrrp{jU}LuI*2`FR^L zX3~F#eevx_y!ZY(whzBNJmSGOKNrdzGlvXJBhd4_Yy!_)2gyT&EL7~szDrdT@&KYJ zqEsrqEVI?uSvold5v=3T?ez$TcoF)Q8#vVp-oG7GC;g|{2cN9rw9ELX!o7Fb`JuqX z*Kw#8++XHdALY+kO1STzCmIu=m=Vo!3ka?q!OKWyi46X!fN@u#*vlOaj0E@dY>Hhisw)nyj|2JIKRf*@_6( z+5tJE_%MKH$9T(CeLGoIhngs=ELn?>pw_)>851ZjbW`VKm5#&IQMMsdj5#?b+Nqk>PPx)`AemTMTn? zHG6u5uHH(?5W$>0pTw(Xo^+)8&p8<>iZ^rF1?H4+tyWWj;cs3EqykP_6GNqVV#PPc zdTn?!r)=hX{mnE-5UZE2&? vknQbl?Q}Z4O1ARX0l^$p&;Vg=b7K5I6fuH5ToG!l00000NkvXXu0mjfgksQ8 literal 0 HcmV?d00001 diff --git a/track.ts b/track.ts index 880bc2e..3370956 100644 --- a/track.ts +++ b/track.ts @@ -18,43 +18,43 @@ export class Track extends PathSegment { 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) + // 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; + // // 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; - } - } + // 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]; - } + // train.currentTrack = mostValid; + // train.arrive(closest); + // // if (predictpos.dist(closest) > 2) train.arrive(closest); + // return [closest, closestT]; + // } getNearestPoint(p: Vector) { let [closest, closestDistance] = this.getClosestPoint(p); @@ -85,10 +85,11 @@ export class Track extends PathSegment { draw(): void { super.draw(); - if (this.editable) - for (const e of this.points) { - e.drawDot(); - } + if (this.editable) { + const [a, b, c, d] = this.points; + doodler.line(a, b); + doodler.line(c, d); + } } setNext(t: Track) { @@ -107,9 +108,28 @@ export class Spline { ctx?: CanvasRenderingContext2D; evenPoints: Vector[]; + pointSpacing: number; + + get points() { + return Array.from(new Set(this.segments.flatMap(s => s.points))); + } + + nodes: IControlNode[]; + constructor(segs: T[]) { 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: IControlNode = { + anchor: this.points[i], + controls: [this.points.at(i - 1)!, this.points[(i + 1) % this.points.length]], + mirrored: false, + tangent: true + } + this.nodes.push(node); + } } setContext(ctx: CanvasRenderingContext2D) { @@ -126,6 +146,7 @@ export class Spline { } calculateEvenlySpacedPoints(spacing: number, resolution = 1) { + this.pointSpacing = 1; // return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution)); const points: Vector[] = [] @@ -155,20 +176,51 @@ export class Spline { } } + this.evenPoints = points; + return points; } followEvenPoints(t: number) { - if (t < 0) t+= this.evenPoints.length + 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] - try { - return Vector.lerp(a, b, t % 1); + return Vector.lerp(a, b, t % 1); + } - } catch { - console.log(t, i, a, b); + calculateApproxLength() { + for (const s of this.segments) { + s.calculateApproxLength(); + } + } + + toggleNodeTangent(p: Vector) { + const node = this.nodes.find(n => n.anchor === p); + + node && (node.tangent = !node.tangent); + } + toggleNodeMirrored(p: Vector) { + const node = this.nodes.find(n => n.anchor === p); + + node && (node.mirrored = !node.mirrored); + } + handleNodeEdit(p: Vector, movement: { x: number, y: number }) { + 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) + } } } } @@ -209,14 +261,21 @@ export const loadFromJson = () => { if (!json) return generateSquareTrack(); const segments: Track[] = []; - for (const {points} of json.segments) { - segments.push(new Track(points.map((p:{x:number,y:number}) => new Vector(p.x, p.y)))); + for (const { points } of json.segments) { + segments.push(new Track(points.map((p: { x: number, y: number }) => 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); } + +export interface IControlNode { + anchor: Vector; + controls: [Vector, Vector]; + tangent: boolean; + mirrored: boolean; +} diff --git a/train.old.ts b/train.old.ts new file mode 100644 index 0000000..f183fa6 --- /dev/null +++ b/train.old.ts @@ -0,0 +1,312 @@ +import { drawLine } from "./drawing/line.ts"; +import { ComplexPath, PathSegment } from "./math/path.ts"; +import { Vector } from "doodler"; +import { Follower } from "./physics/follower.ts"; +import { Mover } from "./physics/mover.ts"; +import { Track } from "./track.ts"; + +export class Train extends Follower { + nodes?: Vector[]; + + currentTrack: Track; + + speed: number; + + follower?: TrainCar; + + followers?: TrainCar[]; + + constructor(track: Track, length: number) { + super(track.points[0].copy()); + this.maxSpeed = 2; + this.speed = 1; + this.currentTrack = track; + this.velocity = this.currentTrack.tangent(0).normalize().mult(this.maxSpeed); + + this.addCar(length); + + this.maxForce = .2; + } + + init(): void { + this.boundingBox.size.set(30, 10); + this._trailingPoint = 30; + } + + move(): void { + this.follow(); + + super.move(); + this.follower?.move() + // this.draw(); + } + + follow(): void { + // const [_, t] = this.currentTrack.followTrack(this); + + // this.position = this.currentTrack.getPointAtT(t); + // this.velocity = this.currentTrack.tangent(t); + this.velocity.normalize().mult(this.speed || this.maxSpeed); + // if (nearest.dist(this.position) > 10) + // this.seek(nearest); + } + + // draw(): void { + // if (!this.ctx) return; + // const ctx = this.ctx; + // // const [a, b] = this.nodes; + + // ctx.strokeStyle = 'blue' + // ctx.lineWidth = 10; + // // drawLine(ctx, a.x, a.y, b.x, b.y); + // super.draw() + // } + + setContext(ctx: CanvasRenderingContext2D): void { + super.setContext(ctx); + this.follower?.setContext(ctx); + } + + addCar(length: number,) { + console.log(length); + if (length) + this.follower = new TrainCar(this.currentTrack, length - 1); + this.follower?.setTarget(this); + this.follower?.position.set(this.trailingPoint); + this._trailingPoint -= 2; + } +} + +class TrainCar extends Train { + // constructor(n: [Vector, Vector], track: Track) { + // super(track); + // this.nodes = n; + // } + target?: Train; + setTarget(t: Train) { + this.target = t; + } + + init(): void { + this.boundingBox.size.set(20, 10) + this._trailingPoint = 25; + this.maxSpeed = this.maxSpeed * 2; + this.maxForce = this.maxForce * 2; + // this.speed = 0; + } + + // follow(): void { + // if (!this.target) return; + + // const points = this.currentTrack.getAllPointsInRange(this.target.position, this.target._trailingPoint); + // let closest = this.target.position; + // let closestTan = this.target.velocity; + // for (const [t, path] of points) { + // const point = path.getPointAtT(t); + // if (point.dist(this.target.trailingPoint) < this.target.trailingPoint.dist(closest)) { + // closest = point; + // closestTan = path.tangent(t); + // } + // } + + // // this.position.set(closest); + // this.seek(closest); + // this.velocity.set(closestTan.normalize().mult(this.target.speed)); + // } + + move(): void { + // if (!this.target) return; + + // const r = 30; + // const points = this.currentTrack.getAllPointsInRange(this.target.position, this.target._trailingPoint); + // let closest = this.target.position; + // let closestTan = this.target.velocity; + // for (const [t, path] of points) { + // const point = path.getPointAtT(t); + // if (point.dist(this.target.trailingPoint) < this.target.trailingPoint.dist(closest)) { + // closest = point; + // closestTan = path.tangent(t); + // } + // } + + // // this.position.set(closest); + // // this.seek(closest); + // this.velocity.set(closestTan.normalize().mult(this.target.speed)); + // super.move(); + // if (this.target && this.position.dist(this.target.trailingPoint) < 2) { + // this.velocity.setMag(0); + // } else if (this.target) { + // this.velocity.setMag(this.target.velocity.mag()); + // } + + // if (this.target) { + // this.position.set(this.target.trailingPoint); + // this.speed = this.target.speed; + // } + // const [pos,t] = this.currentTrack.followTrack(this); + + // this.position = pos.copy() + // if (this.target) { + // const points = this.currentTrack.getPointWithinRadius(this.target.position, 30); + + // let closest = this.target.position; + // for (const [i,point] of points.entries()) { + // if (typeof point !== "number") break; + + // const tracks = [this.currentTrack, this.currentTrack.next, this.currentTrack.prev]; + + // const a = tracks[i].getPointAtT(point); + + // if (a.dist(this.target.trailingPoint) < closest.dist(this.target.trailingPoint)) { + // closest = a; + // } + // } + + // this.position = closest; + // } + // this.draw(); + if (this.target) { + if (this.position.dist(this.target.position) > this.target.position.dist(this.target.trailingPoint)) { + + // this.velocity = this.currentTrack.tangent(t); + // this.velocity.normalize().mult(this.speed); + + this.arrive(this.currentTrack.getNearestPoint(this.target.trailingPoint)); + // if (this.position.dist()) + // this.move() + this.speed = this.target.speed; + super.move(); + } else { + this.draw() + this.follower?.draw(); + } + } + // this.draw() + // this.follower?.move() + } + + // draw(): void { + // if (!this.ctx) return; + // super.draw() + // this.ctx.fillStyle = 'red'; + // this.position.drawDot(this.ctx); + // this.ctx.fillStyle = 'green'; + // this.target?.trailingPoint.drawDot(this.ctx); + // } + + edges(): void { + + } +} + +// export class Train extends Follower { + +// currentSegment: Track; +// cars: TrainCar[] = []; + +// id: string; +// constructor(path: Track); +// constructor(x: number, y: number, segment: Track); +// constructor(x: number | Track, y?: number, segment?: Track) { + +// super(x instanceof Track ? x.points[0].copy() : new Vector(x, y)) + +// if (x instanceof Track) { +// this.currentSegment = x; +// } else if (segment) { +// this.currentSegment = segment; +// } else { +// throw new Error('Path not provided for train construction') +// } +// // super(new Vector(Math.floor(Math.random() * 200),Math.floor(Math.random() * 200)), Vector.random2D()); +// this.id = crypto.randomUUID() +// this.boundingBox.size.set(40, 10) + +// this.maxSpeed = 3; +// this.maxForce = .3; + +// this.addCar(); + +// this._trailingPoint = 40; +// this._leadingPoint = 15; +// } + +// move(): void { +// for (const car of this.cars) { +// car.move(); +// } +// this.follow(this.currentSegment) +// super.move(); +// } + +// draw(): void { +// if (!this.ctx) return; +// // this.ctx.save(); +// this.ctx.fillStyle = 'white'; +// this.ctx.strokeStyle = 'red'; +// super.draw(); +// // this.ctx.restore(); +// } + +// addCar() { +// const last = this.cars[this.cars.length - 1]; +// this.cars.push(new TrainCar(this, (last || this).velocity.copy().normalize().mult(-30), last)); +// } + +// setContext(ctx: CanvasRenderingContext2D): void { +// super.setContext(ctx); +// for (const car of this.cars) { +// car.setContext(ctx); +// } +// } + +// follow(toFollow: Track): void { +// // const predict = this.velocity.copy(); +// // predict.normalize(); +// // predict.mult(25); +// // const predictpos = Vector.add(this.position, predict) + +// const nearest = toFollow.getMostValidTrack(this); + +// this.seek(nearest); +// } +// } + +// export class TrainCar extends Follower { +// train?: Train; +// prevCar?: Mover; +// constructor(train: Train, pos: Vector, prevCar: Mover) { +// super(pos); +// this.train = train; + +// this.boundingBox.size.set(20, 15); + +// this.prevCar = prevCar || train; + +// this.maxSpeed = 2; +// this.maxForce = .3; + +// this._trailingPoint = 25; +// this._leadingPoint = 25; + +// } + +// move(): void { +// if (this.train && this.prevCar) { +// this.link(this.prevCar); +// // super.move(); +// this.edges(); +// this.ctx && (this.ctx.fillStyle = 'orange') +// this.draw(); +// } +// else super.move(); +// } + +// edges(): void { +// if (!this.ctx || !this.train) return; +// if (this.train.position.x > this.ctx.canvas.width) this.position.x -= this.ctx.canvas.width; +// if (this.train.position.y > this.ctx.canvas.height) this.position.y -= this.ctx.canvas.height; +// if (this.train.position.x < 0) this.position.x += this.ctx.canvas.width; +// if (this.train.position.y < 0) this.position.y += this.ctx.canvas.height; +// } +// } \ No newline at end of file diff --git a/train.ts b/train.ts index 4926429..4290318 100644 --- a/train.ts +++ b/train.ts @@ -3,310 +3,104 @@ import { ComplexPath, PathSegment } from "./math/path.ts"; import { Vector } from "doodler"; import { Follower } from "./physics/follower.ts"; import { Mover } from "./physics/mover.ts"; -import { Track } from "./track.ts"; +import { Spline, Track } from "./track.ts"; -export class Train extends Follower { - nodes?: Vector[]; +export class Train { + nodes: Vector[] = []; - currentTrack: Track; + cars: TrainCar[] = []; - speed: number; + path: Spline; + t: number; - follower?: TrainCar; + engineLength = 40; + spacing = 30; - followers?: TrainCar[]; - - constructor(track: Track, length: number) { - super(track.points[0].copy()); - this.maxSpeed = 2; - this.speed = 1; - this.currentTrack = track; - this.velocity = this.currentTrack.tangent(0).normalize().mult(this.maxSpeed); - - this.addCar(length); - - this.maxForce = .2; - } - - init(): void { - this.boundingBox.size.set(30, 10); - this._trailingPoint = 30; - } - - move(): void { - this.follow(); - - super.move(); - this.follower?.move() - // this.draw(); - } - - follow(): void { - const [_, t] = this.currentTrack.followTrack(this); - - // this.position = this.currentTrack.getPointAtT(t); - this.velocity = this.currentTrack.tangent(t); - this.velocity.normalize().mult(this.speed || this.maxSpeed); - // if (nearest.dist(this.position) > 10) - // this.seek(nearest); - } - - // draw(): void { - // if (!this.ctx) return; - // const ctx = this.ctx; - // // const [a, b] = this.nodes; - - // ctx.strokeStyle = 'blue' - // ctx.lineWidth = 10; - // // drawLine(ctx, a.x, a.y, b.x, b.y); - // super.draw() - // } - - setContext(ctx: CanvasRenderingContext2D): void { - super.setContext(ctx); - this.follower?.setContext(ctx); - } - - addCar(length: number,) { - console.log(length); - if (length) - this.follower = new TrainCar(this.currentTrack, length - 1); - this.follower?.setTarget(this); - this.follower?.position.set(this.trailingPoint); - this._trailingPoint -= 2; - } -} - -class TrainCar extends Train { - // constructor(n: [Vector, Vector], track: Track) { - // super(track); - // this.nodes = n; - // } - target?: Train; - setTarget(t: Train) { - this.target = t; - } - - init(): void { - this.boundingBox.size.set(20, 10) - this._trailingPoint = 25; - this.maxSpeed = this.maxSpeed * 2; - this.maxForce = this.maxForce * 2; - // this.speed = 0; - } - - // follow(): void { - // if (!this.target) return; - - // const points = this.currentTrack.getAllPointsInRange(this.target.position, this.target._trailingPoint); - // let closest = this.target.position; - // let closestTan = this.target.velocity; - // for (const [t, path] of points) { - // const point = path.getPointAtT(t); - // if (point.dist(this.target.trailingPoint) < this.target.trailingPoint.dist(closest)) { - // closest = point; - // closestTan = path.tangent(t); - // } - // } - - // // this.position.set(closest); - // this.seek(closest); - // this.velocity.set(closestTan.normalize().mult(this.target.speed)); - // } - - move(): void { - // if (!this.target) return; - - // const r = 30; - // const points = this.currentTrack.getAllPointsInRange(this.target.position, this.target._trailingPoint); - // let closest = this.target.position; - // let closestTan = this.target.velocity; - // for (const [t, path] of points) { - // const point = path.getPointAtT(t); - // if (point.dist(this.target.trailingPoint) < this.target.trailingPoint.dist(closest)) { - // closest = point; - // closestTan = path.tangent(t); - // } - // } - - // // this.position.set(closest); - // // this.seek(closest); - // this.velocity.set(closestTan.normalize().mult(this.target.speed)); - // super.move(); - // if (this.target && this.position.dist(this.target.trailingPoint) < 2) { - // this.velocity.setMag(0); - // } else if (this.target) { - // this.velocity.setMag(this.target.velocity.mag()); - // } - - // if (this.target) { - // this.position.set(this.target.trailingPoint); - // this.speed = this.target.speed; - // } - // const [pos,t] = this.currentTrack.followTrack(this); - - // this.position = pos.copy() - // if (this.target) { - // const points = this.currentTrack.getPointWithinRadius(this.target.position, 30); - - // let closest = this.target.position; - // for (const [i,point] of points.entries()) { - // if (typeof point !== "number") break; - - // const tracks = [this.currentTrack, this.currentTrack.next, this.currentTrack.prev]; - - // const a = tracks[i].getPointAtT(point); - - // if (a.dist(this.target.trailingPoint) < closest.dist(this.target.trailingPoint)) { - // closest = a; - // } - // } - - // this.position = closest; - // } - // this.draw(); - if (this.target) { - if (this.position.dist(this.target.position) > this.target.position.dist(this.target.trailingPoint)) { - - // this.velocity = this.currentTrack.tangent(t); - // this.velocity.normalize().mult(this.speed); - - this.arrive(this.currentTrack.getNearestPoint(this.target.trailingPoint)); - // if (this.position.dist()) - // this.move() - this.speed = this.target.speed; - super.move(); - } else { - this.draw() - this.follower?.draw(); - } + constructor(track: Spline, cars: TrainCar[] = []) { + 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')! as HTMLImageElement, 80, 20, { at: new Vector(0, 60), width: 80, height: 20 })); + this.cars[0].points = this.nodes.map(n => n) as [Vector, Vector]; + 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); } - // this.draw() - // this.follower?.move() } - // draw(): void { - // if (!this.ctx) return; - // super.draw() - // this.ctx.fillStyle = 'red'; - // this.position.drawDot(this.ctx); - // this.ctx.fillStyle = 'green'; - // this.target?.trailingPoint.drawDot(this.ctx); + 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(); + } + // this.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)) + // // } + // } // } - edges(): void { - + real2Track(length: number) { + return length / this.path.pointSpacing } } -// export class Train extends Follower { +export class TrainCar { + img: HTMLImageElement; + imgWidth: number; + imgHeight: number; + sprite?: ISprite; -// currentSegment: Track; -// cars: TrainCar[] = []; + points?: [Vector, Vector]; + length: number; -// id: string; -// constructor(path: Track); -// constructor(x: number, y: number, segment: Track); -// constructor(x: number | Track, y?: number, segment?: Track) { + constructor(length: number, img: HTMLImageElement, w: number, h: number, sprite?: ISprite) { + this.img = img; + this.sprite = sprite; + this.imgWidth = w; + this.imgHeight = h; + this.length = length; + } -// super(x instanceof Track ? x.points[0].copy() : new Vector(x, y)) + 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(); -// if (x instanceof Track) { -// this.currentSegment = x; -// } else if (segment) { -// this.currentSegment = segment; -// } else { -// throw new Error('Path not provided for train construction') -// } -// // super(new Vector(Math.floor(Math.random() * 200),Math.floor(Math.random() * 200)), Vector.random2D()); -// this.id = crypto.randomUUID() -// this.boundingBox.size.set(40, 10) + 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)); + }) + } +} -// this.maxSpeed = 3; -// this.maxForce = .3; - -// this.addCar(); - -// this._trailingPoint = 40; -// this._leadingPoint = 15; -// } - -// move(): void { -// for (const car of this.cars) { -// car.move(); -// } -// this.follow(this.currentSegment) -// super.move(); -// } - -// draw(): void { -// if (!this.ctx) return; -// // this.ctx.save(); -// this.ctx.fillStyle = 'white'; -// this.ctx.strokeStyle = 'red'; -// super.draw(); -// // this.ctx.restore(); -// } - -// addCar() { -// const last = this.cars[this.cars.length - 1]; -// this.cars.push(new TrainCar(this, (last || this).velocity.copy().normalize().mult(-30), last)); -// } - -// setContext(ctx: CanvasRenderingContext2D): void { -// super.setContext(ctx); -// for (const car of this.cars) { -// car.setContext(ctx); -// } -// } - -// follow(toFollow: Track): void { -// // const predict = this.velocity.copy(); -// // predict.normalize(); -// // predict.mult(25); -// // const predictpos = Vector.add(this.position, predict) - -// const nearest = toFollow.getMostValidTrack(this); - -// this.seek(nearest); -// } -// } - -// export class TrainCar extends Follower { -// train?: Train; -// prevCar?: Mover; -// constructor(train: Train, pos: Vector, prevCar: Mover) { -// super(pos); -// this.train = train; - -// this.boundingBox.size.set(20, 15); - -// this.prevCar = prevCar || train; - -// this.maxSpeed = 2; -// this.maxForce = .3; - -// this._trailingPoint = 25; -// this._leadingPoint = 25; - -// } - -// move(): void { -// if (this.train && this.prevCar) { -// this.link(this.prevCar); -// // super.move(); -// this.edges(); -// this.ctx && (this.ctx.fillStyle = 'orange') -// this.draw(); -// } -// else super.move(); -// } - -// edges(): void { -// if (!this.ctx || !this.train) return; -// if (this.train.position.x > this.ctx.canvas.width) this.position.x -= this.ctx.canvas.width; -// if (this.train.position.y > this.ctx.canvas.height) this.position.y -= this.ctx.canvas.height; -// if (this.train.position.x < 0) this.position.x += this.ctx.canvas.width; -// if (this.train.position.y < 0) this.position.y += this.ctx.canvas.height; -// } -// } \ No newline at end of file +interface ISprite { + at: Vector; + width: number; + height: number; +}