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 0000000..4298f22 Binary files /dev/null and b/sprites/BlueEngine.png differ diff --git a/sprites/Engine.png b/sprites/Engine.png new file mode 100644 index 0000000..664b655 Binary files /dev/null and b/sprites/Engine.png differ diff --git a/sprites/EngineSprites.png b/sprites/EngineSprites.png new file mode 100644 index 0000000..6ea9b0a Binary files /dev/null and b/sprites/EngineSprites.png differ diff --git a/sprites/GreenEngine.png b/sprites/GreenEngine.png new file mode 100644 index 0000000..2f9556b Binary files /dev/null and b/sprites/GreenEngine.png differ diff --git a/sprites/PurpleEngine.png b/sprites/PurpleEngine.png new file mode 100644 index 0000000..b9632e2 Binary files /dev/null and b/sprites/PurpleEngine.png differ diff --git a/sprites/RedEngine.png b/sprites/RedEngine.png new file mode 100644 index 0000000..52cb1b6 Binary files /dev/null and b/sprites/RedEngine.png differ 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; +}