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