// 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 }; 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; } return this; } sub(v, y, z) { if (arguments.length === 1 && typeof v !== 'number') { this.x -= v.x; this.y -= v.y; this.z -= v.z; } else if (arguments.length === 2) { this.x -= v; this.y -= y ?? 0; } else { this.x -= v; this.y -= y ?? 0; this.z -= z ?? 0; } return this; } mult(v) { if (typeof v === 'number') { this.x *= v; this.y *= v; this.z *= v; } else { this.x *= v.x; this.y *= v.y; this.z *= v.z; } return this; } div(v) { if (typeof v === 'number') { this.x /= v; this.y /= v; this.z /= v; } else { this.x /= v.x; this.y /= v.y; this.z /= v.z; } return this; } rotate(angle) { const prev_x = this.x; const c = Math.cos(angle); const s = Math.sin(angle); this.x = c * this.x - s * this.y; this.y = s * prev_x + c * this.y; return this; } dist(v) { const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z; 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); return this; } normalize() { const m = this.mag(); if (m > 0) { this.div(m); } return this; } limit(high) { if (this.mag() > high) { this.normalize(); this.mult(high); } return this; } heading() { return -Math.atan2(-this.y, this.x); } heading2D() { return this.heading(); } toString() { return "[" + this.x + ", " + this.y + ", " + this.z + "]"; } array() { return [ this.x, this.y, this.z ]; } copy() { return new Vector(this.x, this.y, this.z); } drawDot() { 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() * Constants.TWO_PI; const vz = Math.random() * 2 - 1; const mult = Math.sqrt(1 - vz * vz); const vx = mult * Math.cos(angle); const vy = mult * Math.sin(angle); if (v === undefined || v === null) { v = new Vector(vx, vy, vz); } else { v.set(vx, vy, vz); } return v; } 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 = []; clickables = []; constructor({ width , height , canvas , bg , framerate }){ if (!canvas) { canvas = document.createElement('canvas'); document.body.append(canvas); } this.bg = bg || 'white'; this.framerate = framerate || 60; canvas.width = width; canvas.height = height; this._canvas = canvas; const ctx = canvas.getContext('2d'); console.log(ctx); if (!ctx) throw 'Unable to initialize Doodler: Canvas context not found'; this.ctx = ctx; } init() { this._canvas.addEventListener('mousedown', (e)=>this.onClick(e)); this._canvas.addEventListener('mouseup', (e)=>this.offClick(e)); this._canvas.addEventListener('mousemove', (e)=>{ const rect = this._canvas.getBoundingClientRect(); this.mouseX = e.clientX - rect.left; this.mouseY = e.clientY - rect.top; for (const d of this.draggables.filter((d)=>d.beingDragged)){ d.point.add(e.movementX, e.movementY); d.onDrag && d.onDrag({ x: e.movementX, y: e.movementY }); } }); this.startDrawLoop(); } 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, Constants.TWO_PI); this.ctx.fill(); } drawCircle(at, radius, style) { this.setStyle(style); this.ctx.beginPath(); this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI); this.ctx.stroke(); } fillCircle(at, radius, style) { this.setStyle(style); this.ctx.beginPath(); this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI); this.ctx.fill(); } drawRect(at, width, height, style) { this.setStyle(style); this.ctx.strokeRect(at.x, at.y, width, height); } fillRect(at, width, height, style) { this.setStyle(style); this.ctx.fillRect(at.x, at.y, width, height); } drawSquare(at, size, style) { this.drawRect(at, size, size, style); } fillSquare(at, size, style) { this.fillRect(at, size, size, style); } drawCenteredRect(at, width, height, style) { this.ctx.save(); this.ctx.translate(-width / 2, -height / 2); this.drawRect(at, width, height, style); this.ctx.restore(); } fillCenteredRect(at, width, height, style) { this.ctx.save(); this.ctx.translate(-width / 2, -height / 2); this.fillRect(at, width, height, style); this.ctx.restore(); } drawCenteredSquare(at, size, style) { this.drawCenteredRect(at, size, size, style); } fillCenteredSquare(at, size, style) { this.fillCenteredRect(at, size, size, style); } drawBezier(a, b, c, d, style) { this.setStyle(style); this.ctx.beginPath(); this.ctx.moveTo(a.x, a.y); this.ctx.bezierCurveTo(b.x, b.y, c.x, c.y, d.x, d.y); this.ctx.stroke(); } drawRotated(origin, angle, cb) { this.ctx.save(); this.ctx.translate(origin.x, origin.y); this.ctx.rotate(angle); this.ctx.translate(-origin.x, -origin.y); cb(); this.ctx.restore(); } 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'; 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); } 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(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){ d.beingDragged = false; d.onDragEnd?.call(null); } } 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 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); } } 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.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 { 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)); } this.length = points.reduce((acc, cur)=>{ const prev = acc.prev; acc.prev = cur; if (!prev) return acc; acc.length += cur.dist(prev); return acc; }, { prev: undefined, length: 0 }).length; return this.length; } calculateEvenlySpacedPoints(spacing, resolution = 1) { const points = []; points.push(this.points[0]); let prev = points[0]; let distSinceLastEvenPoint = 0; let t = 0; const div = Math.ceil(this.length * resolution * 10); while(t < 1){ t += 1 / div; const point = this.getPointAtT(t); distSinceLastEvenPoint += prev.dist(point); if (distSinceLastEvenPoint >= spacing) { const overshoot = distSinceLastEvenPoint - spacing; const evenPoint = Vector.add(point, Vector.sub(point, prev).normalize().mult(overshoot)); distSinceLastEvenPoint = overshoot; points.push(evenPoint); prev = evenPoint; } prev = point; } return points; } } 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; } 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) { const [a, b, c, d] = this.points; doodler.line(a, b); doodler.line(c, d); } } setNext(t) { this.next = t; this.next.points[0] = this.points[3]; } setPrev(t) { this.prev = t; this.prev.points[3] = this.points[0]; } } 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; for (const segment of this.segments){ segment.setContext(ctx); } } draw() { for (const segment of this.segments){ segment.draw(); } } calculateEvenlySpacedPoints(spacing, resolution = 1) { this.pointSpacing = 1; const points = []; points.push(this.segments[0].points[0]); let prev = points[0]; let distSinceLastEvenPoint = 0; for (const seg of this.segments){ let t = 0; const div = Math.ceil(seg.length * resolution * 10); while(t < 1){ t += 1 / div; const point = seg.getPointAtT(t); distSinceLastEvenPoint += prev.dist(point); if (distSinceLastEvenPoint >= spacing) { const overshoot = distSinceLastEvenPoint - spacing; const evenPoint = Vector.add(point, Vector.sub(point, prev).normalize().mult(overshoot)); distSinceLastEvenPoint = overshoot; points.push(evenPoint); prev = evenPoint; } prev = point; } } this.evenPoints = points; return points; } followEvenPoints(t) { if (t < 0) t += this.evenPoints.length; const i = Math.floor(t); const a = this.evenPoints[i]; const b = this.evenPoints[(i + 1) % this.evenPoints.length]; return Vector.lerp(a, b, t % 1); } calculateApproxLength() { for (const s of this.segments){ s.calculateApproxLength(); } } toggleNodeTangent(p) { const node = this.nodes.find((n)=>n.anchor === p); node && (node.tangent = !node.tangent); } toggleNodeMirrored(p) { const node = this.nodes.find((n)=>n.anchor === p); node && (node.mirrored = !node.mirrored); } handleNodeEdit(p, movement) { const node = this.nodes.find((n)=>n.anchor === p || n.controls.includes(p)); if (!node || !(node.mirrored || node.tangent)) return; if (node.anchor !== p) { if (node.mirrored || node.tangent) { const mover = node.controls.find((e)=>e !== p); const v = Vector.sub(node.anchor, p); if (!node.mirrored) v.setMag(Vector.sub(node.anchor, mover).mag()); mover.set(Vector.add(v, node.anchor)); } } else { for (const control of node.controls){ control.add(movement.x, movement.y); } } } } 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 ]); }; const loadFromJson = ()=>{ const json = JSON.parse(localStorage.getItem('railPath') || ''); if (!json) return generateSquareTrack(); const segments = []; for (const { points } of json.segments){ segments.push(new Track(points.map((p)=>new Vector(p.x, p.y)))); } for (const [i, s] of segments.entries()){ s.setNext(segments[(i + 1) % segments.length]); s.setPrev(segments.at(i - 1)); } return new Spline(segments); }; const engineSprites = document.createElement('img'); engineSprites.src = './sprites/EngineSprites.png'; engineSprites.style.display = 'none'; engineSprites.id = 'engine-sprites'; document.body.append(engineSprites); init({ width: 400, height: 400, bg: '#333' }); const path = loadFromJson(); let speed = 1; const car = new TrainCar(55, engineSprites, 80, 20, { at: new Vector(0, 80), height: 20, width: 80 }); const train = new Train(path, [ car ]); let dragEndCounter = 0; let selectedNode; doodler.createLayer(()=>{ for(let i = 0; i < path.evenPoints.length; i += 10){ const p = path.evenPoints[i]; const next = path.evenPoints[(i + 1) % path.evenPoints.length]; const last = path.evenPoints.at(i - 1); if (!last) break; const tan = Vector.sub(last, next); doodler.drawRotated(p, tan.heading(), ()=>{ doodler.line(p, p.copy().add(0, 10), { color: '#291b17', weight: 4 }); doodler.line(p, p.copy().add(0, -10), { color: '#291b17', weight: 4 }); doodler.line(p.copy().add(-6, 5), p.copy().add(6, 5), { color: 'grey', weight: 2 }); doodler.line(p.copy().add(-6, -5), p.copy().add(6, -5), { color: 'grey', weight: 2 }); }); } 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') { speed += .1; } 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){ if (t.editable) { doodler.registerDraggable(p, 10); doodler.addDragEvents({ point: p, onDragEnd: ()=>{ dragEndCounter++; t.length = t.calculateApproxLength(100); path.evenPoints = path.calculateEvenlySpacedPoints(1); }, onDrag: (movement)=>{ path.handleNodeEdit(p, movement); } }); } else { doodler.unregisterDraggable(p); } } } for (const p1 of path.points){ if (editable) { const onClick = ()=>{ selectedPoint = p1; selectedNode = path.nodes.find((e)=>e.anchor === p1 || e.controls.includes(p1)); }; clickables.set(p1, onClick); doodler.registerClickable(p1.copy().sub(10, 10), p1.copy().add(10, 10), onClick); } else { const the = clickables.get(p1); doodler.unregisterClickable(the); } } } }); 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); } });