// deno-fmt-ignore-file // deno-lint-ignore-file // This code was bundled using `deno bundle` and it's not recommended to edit it manually const Constants = { 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; z; constructor(x = 0, y = 0, z = 0){ this.x = x; this.y = y; this.z = z; } set(v, y, z) { if (arguments.length === 1 && typeof v !== "number") { this.set(v.x || v[0] || 0, v.y || v[1] || 0, v.z || v[2] || 0); } else { this.x = v; this.y = y || 0; this.z = z || 0; } } get() { return new Vector(this.x, this.y, this.z); } mag() { const x = this.x, y = this.y, z = this.z; return Math.sqrt(x * x + y * y + z * z); } magSq() { const x = this.x, y = this.y, z = this.z; return x * x + y * y + z * z; } setMag(v_or_len, len) { if (len === undefined) { len = v_or_len; this.normalize(); this.mult(len); } else { const v = v_or_len; v.normalize(); v.mult(len); return v; } } add(v, y, z) { if (arguments.length === 1 && typeof v !== 'number') { this.x += v.x; this.y += v.y; this.z += v.z; } else if (arguments.length === 2) { this.x += v; this.y += y ?? 0; } else { this.x += v; this.y += y ?? 0; this.z += z ?? 0; } } sub(v, y, z) { if (arguments.length === 1 && typeof v !== 'number') { this.x -= v.x; this.y -= v.y; this.z -= v.z; } else if (arguments.length === 2) { this.x -= v; this.y -= y ?? 0; } else { this.x -= v; this.y -= y ?? 0; this.z -= z ?? 0; } } mult(v) { if (typeof v === 'number') { this.x *= v; this.y *= v; this.z *= v; } else { this.x *= v.x; this.y *= v.y; this.z *= v.z; } return this; } div(v) { if (typeof v === 'number') { this.x /= v; this.y /= v; this.z /= v; } else { this.x /= v.x; this.y /= v.y; this.z /= v.z; } } rotate(angle) { const prev_x = this.x; const c = Math.cos(angle); const s = Math.sin(angle); this.x = c * this.x - s * this.y; this.y = s * prev_x + c * this.y; } dist(v) { const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z; return Math.sqrt(dx * dx + dy * dy + dz * dz); } dot(v, y, z) { if (arguments.length === 1 && typeof v !== 'number') { return this.x * v.x + this.y * v.y + this.z * v.z; } return this.x * v + this.y * y + this.z * z; } cross(v) { const x = this.x, y = this.y, z = this.z; return new Vector(y * v.z - v.y * z, z * v.x - v.z * x, x * v.y - v.x * y); } lerp(v_or_x, amt_or_y, z, amt) { const lerp_val = (start, stop, amt)=>{ return start + (stop - start) * amt; }; let x, y; if (arguments.length === 2 && typeof v_or_x !== 'number') { amt = amt_or_y; x = v_or_x.x; y = v_or_x.y; z = v_or_x.z; } else { x = v_or_x; y = amt_or_y; } this.x = lerp_val(this.x, x, amt); this.y = lerp_val(this.y, y, amt); this.z = lerp_val(this.z, z, amt); } normalize() { const m = this.mag(); if (m > 0) { this.div(m); } return this; } limit(high) { if (this.mag() > high) { this.normalize(); this.mult(high); } } heading() { return -Math.atan2(-this.y, this.x); } heading2D() { return this.heading(); } toString() { return "[" + this.x + ", " + this.y + ", " + this.z + "]"; } array() { return [ this.x, this.y, this.z ]; } copy() { return new Vector(this.x, this.y, this.z); } drawDot() { if (!doodler) return; doodler.dot(this, { weight: 2, color: 'red' }); } static fromAngle(angle, v) { if (v === undefined || v === null) { v = new Vector(); } v.x = Math.cos(angle); v.y = Math.sin(angle); return v; } static random2D(v) { return Vector.fromAngle(Math.random() * (Math.PI * 2), v); } static random3D(v) { const angle = Math.random() * Constants1.TWO_PI; const vz = Math.random() * 2 - 1; const mult = Math.sqrt(1 - vz * vz); const vx = mult * Math.cos(angle); const vy = mult * Math.sin(angle); if (v === undefined || v === null) { v = new Vector(vx, vy, vz); } else { v.set(vx, vy, vz); } return v; } static dist(v1, v2) { return v1.dist(v2); } static dot(v1, v2) { return v1.dot(v2); } static cross(v1, v2) { return v1.cross(v2); } static add(v1, v2) { return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); } static sub(v1, v2) { return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); } static angleBetween(v1, v2) { return Math.acos(v1.dot(v2) / Math.sqrt(v1.magSq() * v2.magSq())); } static lerp(v1, v2, amt) { const retval = new Vector(v1.x, v1.y, v1.z); retval.lerp(v2, amt); return retval; } static vectorProjection(v1, v2) { v2 = v2.copy(); v2.normalize(); const sp = v1.dot(v2); v2.mult(sp); return v2; } static hypot2(a, b) { return Vector.dot(Vector.sub(a, b), Vector.sub(a, b)); } } const init = (opt)=>{ if (window.doodler) throw 'Doodler has already been initialized in this window'; window.doodler = new Doodler(opt); window.doodler.init(); }; class Doodler { ctx; _canvas; layers = []; bg; framerate; get width() { return this.ctx.canvas.width; } get height() { return this.ctx.canvas.height; } draggables = []; constructor({ width , height , canvas , bg , framerate }){ if (!canvas) { canvas = document.createElement('canvas'); document.body.append(canvas); } this.bg = bg || 'white'; this.framerate = framerate || 60; canvas.width = width; canvas.height = height; this._canvas = canvas; const ctx = canvas.getContext('2d'); console.log(ctx); if (!ctx) throw 'Unable to initialize Doodler: Canvas context not found'; this.ctx = ctx; } init() { this._canvas.addEventListener('mousedown', (e)=>this.onClick(e)); this._canvas.addEventListener('mouseup', (e)=>this.offClick(e)); this._canvas.addEventListener('mousemove', (e)=>{ const rect = this._canvas.getBoundingClientRect(); this.mouseX = e.clientX - rect.left; this.mouseY = e.clientY - rect.top; for (const d of this.draggables.filter((d)=>d.beingDragged)){ d.point.add(e.movementX, e.movementY); } }); this.startDrawLoop(); } timer; startDrawLoop() { this.timer = setInterval(()=>this.draw(), 1000 / this.framerate); } draw() { this.ctx.fillStyle = this.bg; this.ctx.fillRect(0, 0, this.width, this.height); for (const [i, l] of (this.layers || []).entries()){ l(this.ctx, i); } this.drawUI(); } createLayer(layer) { this.layers.push(layer); } deleteLayer(layer) { this.layers = this.layers.filter((l)=>l !== layer); } moveLayer(layer, index) { let temp = this.layers.filter((l)=>l !== layer); temp = [ ...temp.slice(0, index), layer, ...temp.slice(index) ]; this.layers = temp; } line(start, end, style) { this.setStyle(style); this.ctx.beginPath(); this.ctx.moveTo(start.x, start.y); this.ctx.lineTo(end.x, end.y); this.ctx.stroke(); } dot(at, style) { this.setStyle({ ...style, weight: 1 }); this.ctx.beginPath(); this.ctx.arc(at.x, at.y, style?.weight || 1, 0, Constants1.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.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.fill(); } drawRect(at, width, height, style) { this.setStyle(style); this.ctx.strokeRect(at.x, at.y, width, height); } fillRect(at, width, height, style) { this.setStyle(style); this.ctx.fillRect(at.x, at.y, width, height); } drawSquare(at, size, style) { this.drawRect(at, size, size, style); } fillSquare(at, size, style) { this.fillRect(at, size, size, style); } drawCenteredRect(at, width, height, style) { this.ctx.save(); this.ctx.translate(-width / 2, -height / 2); this.drawRect(at, width, height, style); this.ctx.restore(); } fillCenteredRect(at, width, height, style) { this.ctx.save(); this.ctx.translate(-width / 2, -height / 2); this.fillRect(at, width, height, style); this.ctx.restore(); } drawCenteredSquare(at, size, style) { this.drawCenteredRect(at, size, size, style); } fillCenteredSquare(at, size, style) { this.fillCenteredRect(at, size, size, style); } drawBezier(a, b, c, d, style) { this.setStyle(style); this.ctx.beginPath(); this.ctx.moveTo(a.x, a.y); this.ctx.bezierCurveTo(b.x, b.y, c.x, c.y, d.x, d.y); this.ctx.stroke(); } drawRotated(origin, angle, cb) { this.ctx.save(); this.ctx.translate(origin.x, origin.y); this.ctx.rotate(angle); this.ctx.translate(-origin.x, -origin.y); cb(); this.ctx.restore(); } setStyle(style) { const ctx = this.ctx; ctx.fillStyle = style?.color || style?.fillColor || 'black'; ctx.strokeStyle = style?.color || style?.strokeColor || 'black'; ctx.lineWidth = style?.weight || 1; } mouseX = 0; mouseY = 0; registerDraggable(point, radius, style) { if (this.draggables.find((d)=>d.point === point)) return; const id = this.addUIElement('circle', point, radius, { fillColor: '#5533ff50', strokeColor: '#5533ff50' }); this.draggables.push({ point, radius, style, id }); } unregisterDraggable(point) { for (const d of this.draggables){ if (d.point === point) { this.removeUIElement(d.id); } } this.draggables = this.draggables.filter((d)=>d.point !== point); } onClick(e) { for (const d of this.draggables){ if (d.point.dist(new Vector(this.mouseX, this.mouseY)) <= d.radius) { d.beingDragged = true; } else d.beingDragged = false; } } offClick(e) { for (const d of this.draggables){ d.beingDragged = false; } } uiElements = new Map(); uiDrawing = { rectangle: (...args)=>{ !args[3].noFill && this.fillRect(args[0], args[1], args[2], args[3]); !args[3].noStroke && this.drawRect(args[0], args[1], args[2], args[3]); }, square: (...args)=>{ !args[2].noFill && this.fillSquare(args[0], args[1], args[2]); !args[2].noStroke && this.drawSquare(args[0], args[1], args[2]); }, circle: (...args)=>{ !args[2].noFill && this.fillCircle(args[0], args[1], args[2]); !args[2].noStroke && this.drawCircle(args[0], args[1], args[2]); } }; drawUI() { for (const [shape, ...args] of this.uiElements.values()){ this.uiDrawing[shape].apply(null, args); } } addUIElement(shape, ...args) { const id = crypto.randomUUID(); for (const arg of args){ delete arg.color; } this.uiElements.set(id, [ shape, ...args ]); return id; } removeUIElement(id) { this.uiElements.delete(id); } } class ComplexPath { points = []; radius = 50; ctx; constructor(points){ points && (this.points = points); } setContext(ctx) { this.ctx = ctx; } 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(); } } class PathSegment { points; ctx; length; constructor(points){ this.points = points; this.length = this.calculateApproxLength(100); } setContext(ctx) { this.ctx = ctx; } draw() { const [a, b, c, d] = this.points; doodler.drawBezier(a, b, c, d, { strokeColor: '#ffffff50' }); } getPointAtT(t) { const [a, b, c, d] = this.points; const res = a.copy(); res.add(Vector.add(a.copy().mult(-3), b.copy().mult(3)).mult(t)); res.add(Vector.add(Vector.add(a.copy().mult(3), b.copy().mult(-6)), c.copy().mult(3)).mult(Math.pow(t, 2))); res.add(Vector.add(Vector.add(a.copy().mult(-1), b.copy().mult(3)), Vector.add(c.copy().mult(-3), d.copy())).mult(Math.pow(t, 3))); return res; } getClosestPoint(v) { const resolution = 1 / 25; let closest = this.points[0]; let closestDistance = this.points[0].dist(v); let closestT = 0; for(let i = 0; i < 25; i++){ const point = this.getPointAtT(i * resolution); const distance = v.dist(point); if (distance < closestDistance) { closest = point; closestDistance = distance; closestT = i * resolution; } } return [ closest, closestDistance, closestT ]; } getPointsWithinRadius(v, r) { const points = []; const resolution = 1 / 25; for(let i = 0; i < 25; i++){ const point = this.getPointAtT(i * resolution); const distance = v.dist(point); if (distance < r) { points.push([ i * resolution, this ]); } } return points; } tangent(t) { const [a, b, c, d] = this.points; const res = Vector.sub(b, a).mult(3 * Math.pow(1 - t, 2)); res.add(Vector.add(Vector.sub(c, b).mult(6 * (1 - t) * t), Vector.sub(d, c).mult(3 * Math.pow(t, 2)))); return res; } doesIntersectCircle(x, y, r) { const v = new Vector(x, y); const resolution = 1 / 25; let distance = Infinity; let t; for(let i = 0; i < 25; i++){ if (i !== 25 - 1) { const a = this.getPointAtT(i * resolution); const b = this.getPointAtT((i + 1) * resolution); const ac = Vector.sub(v, a); const ab = Vector.sub(b, a); const d = Vector.add(Vector.vectorProjection(ac, ab), a); const ad = Vector.sub(d, a); const k = Math.abs(ab.x) > Math.abs(ab.y) ? ad.x / ab.x : ad.y / ab.y; let dist; if (k <= 0.0) { dist = Vector.hypot2(v, a); } else if (k >= 1.0) { dist = Vector.hypot2(v, b); } dist = Vector.hypot2(v, d); if (dist < distance) { distance = dist; t = i * resolution; } } } if (distance < r) return t; return false; } calculateApproxLength(resolution = 25) { const stepSize = 1 / resolution; const points = []; for(let i = 0; i <= resolution; i++){ const current = stepSize * i; points.push(this.getPointAtT(current)); } return points.reduce((acc, cur)=>{ const prev = acc.prev; acc.prev = cur; if (!prev) return acc; acc.length += cur.dist(prev); return acc; }, { prev: undefined, length: 0 }).length; } } 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(); } } } edges() {} } class Track extends PathSegment { editable = false; next; prev; id; constructor(points, next, prev){ super(points); this.id = crypto.randomUUID(); this.next = next || this; this.prev = prev || this; } followTrack(train) { 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) { const [point, distance, t] = this.next.getClosestPoint(p); if (distance < closestDistance) { closest = point; closestDistance = distance; } } if (this.prev !== this) { const [point1, distance1, t1] = this.next.getClosestPoint(p); if (distance1 < closestDistance) { closest = point1; closestDistance = distance1; } } return closest; } getAllPointsInRange(v, r) { const points = this.getPointsWithinRadius(v, r).concat(this.next.getPointsWithinRadius(v, r), this.prev.getPointsWithinRadius(v, r)); return points; } draw() { super.draw(); if (this.editable) for (const e of this.points){ e.drawDot(); } } } class Spline { segments = []; ctx; constructor(segs){ this.segments = segs; } setContext(ctx) { this.ctx = ctx; for (const segment of this.segments){ segment.setContext(ctx); } } draw() { for (const segment of this.segments){ segment.draw(); } } } const generateSquareTrack = ()=>{ const first = new Track([ new Vector(20, 40), new Vector(20, 100), new Vector(20, 300), new Vector(20, 360) ]); const second = new Track([ first.points[3], new Vector(20, 370), new Vector(30, 380), new Vector(40, 380) ]); const third = new Track([ second.points[3], new Vector(100, 380), new Vector(300, 380), new Vector(360, 380) ]); const fourth = new Track([ third.points[3], new Vector(370, 380), new Vector(380, 370), new Vector(380, 360) ]); const fifth = new Track([ fourth.points[3], new Vector(380, 300), new Vector(380, 100), new Vector(380, 40) ]); const sixth = new Track([ fifth.points[3], new Vector(380, 30), new Vector(370, 20), new Vector(360, 20) ]); const seventh = new Track([ sixth.points[3], new Vector(300, 20), new Vector(100, 20), new Vector(40, 20) ]); const eighth = new Track([ seventh.points[3], new Vector(30, 20), new Vector(20, 30), first.points[0] ]); const tracks = [ first, second, third, fourth, fifth, sixth, seventh, eighth ]; for (const [i, track] of tracks.entries()){ track.next = tracks[(i + 1) % tracks.length]; track.prev = tracks.at(i - 1); } return new Spline([ first, second, third, fourth, fifth, sixth, seventh, eighth ]); }; init({ width: 400, height: 400, bg: '#333' }); const path = generateSquareTrack(); let t = 0; let currentSeg = 0; const trains = Array(1).fill(null).map((_, i)=>new Train(path.segments[i % path.segments.length], 5)); doodler.createLayer(()=>{ path.draw(); for (const train of trains){ train.move(); } const seg = path.segments[currentSeg]; const tMod = 1 / seg.length; const start = seg.getPointAtT(t); const tan = seg.tangent(t).normalize().mult(25); doodler.line(start, new Vector(start.x + tan.x, start.y + tan.y), { color: 'red' }); t += tMod; if (t > 1) { t -= 1; currentSeg = (currentSeg + 1) % path.segments.length; } }); document.addEventListener('keyup', (e)=>{ if (e.key === 'd') { console.log(trains); console.log(path.segments.reduce((a, b)=>a + b.calculateApproxLength(1000), 0)); } if (e.key === 'ArrowUp') { for (const train of trains){ train.speed += .1; } } if (e.key === 'ArrowDown') { for (const train1 of trains){ train1.speed -= .1; } } if (e.key === 'e') { for (const t of path.segments){ t.editable = !t.editable; for (const p of t.points){ if (t.editable) doodler.registerDraggable(p, 10); else doodler.unregisterDraggable(p); } } } }); document.addEventListener('keydown', (e)=>{ if (e.ctrlKey && e.key === 's') { e.preventDefault(); path.segments.forEach((s)=>{ s.next = s.next.id; s.prev = s.prev.id; delete s.ctx; }); delete path.ctx; const json = JSON.stringify(path); localStorage.setItem('railPath', json); } });