class Shape { } class Circle extends Shape { radius; constructor(radius) { super(); this.radius = radius; } } class Rectangle extends Shape { width; height; constructor(width, height) { super(); this.width = width; this.height = height; } } class Line extends Shape { x2; y2; constructor(x2, y2) { super(); this.x2 = x2; this.x2 = y2; } } class Solid { x; y; shape; constructor(x, y, shape) { this.x = x; this.y = y; this.shape = shape; } } class Momentum { x; y; //position vx; vy; //velocity ax; ay; //acceleration constructor(x = 0, y = 0, vx = 0, vy = 0, ax = 0, ay = 0) { this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.ax = ax; this.ay = ay; } } class Actor { shape; momentum; constructor(shape, momentum) { this.shape = shape; if (typeof momentum == "undefined") { this.momentum = new Momentum(); } else { this.momentum = momentum; } } } class CollisionDetection_Ctx2d { static get DEBUG_PARENT_OBJECT() { return true; }; static get AUTO_CONTINUE_ON_FOCUS() { return true; }; static get SHOW_FPS_INTERVAL() { return 1000 / 4; }; // four times per second, in milliseconds // static get TICK_TIME() { return 1 / 4; }; static get KEY_ARROW_UP() { return 'ArrowUp'; }; static get KEY_ARROW_DOWN() { return 'ArrowDown'; }; static get KEY_ARROW_LEFT() { return 'ArrowLeft'; }; static get KEY_ARROW_RIGHT() { return 'ArrowRight'; }; static get KEY_W() { return 'w'; }; static get KEY_S() { return 's'; }; static get KEY_A() { return 'a'; }; static get KEY_D() { return 'd'; }; static get KEY_ENTER() { return 'Enter'; }; static get KEY_SPACEBAR() { return ' '; }; static get KEY_ESCAPE() { return 'Escape'; }; running = false; animationRequestId = 0; solidsList = []; actor; gravity = 200; consistency = 0.998; borderBounce = 0.75; movePower = 500; constructor(divId, width, height, zoom = 1, showFps = false) { this.createCanvas(divId, width, height, zoom); this.canvasEl.title = "Playing: Test-Collision"; this.ctx = this.canvasEl.getContext("2d"); this.ctx.textAlign = "center"; this.newSimulation(); this.btnGravity = document.createElement('button'); this.btnGravity.innerHTML = "gravity=" + this.gravity; this.btnGravity.onclick = () => this.switchGravity(); this.btnConsistency = document.createElement('button'); this.btnConsistency.innerHTML = "consistency=" + this.consistency; this.btnConsistency.onclick = () => this.switchConsistency(); this.btnBorderBounce = document.createElement('button'); this.btnBorderBounce.innerHTML = "bounce=" + this.borderBounce; this.btnBorderBounce.onclick = () => this.switchBorderBounce(); this.btnShape = document.createElement('button'); this.btnShape.innerHTML = "shape=" + this.actor.shape.constructor.name; this.btnShape.onclick = () => { this.switchActorShape(); this.drawCanvas(); }; let paragraph = document.createElement('p'); paragraph.appendChild(this.btnGravity); paragraph.appendChild(this.btnConsistency); paragraph.appendChild(this.btnBorderBounce); paragraph.appendChild(this.btnShape); this.divEl.appendChild(paragraph); this.btnPlayPause = document.createElement('button'); this.btnPlayPause.innerHTML = "running=" + this.running; this.btnPlayPause.onclick = () => this.playPauseSimulation(); this.btnReset = document.createElement('button'); this.btnReset.innerHTML = "Reset simulation"; this.btnReset.onclick = () => { this.newSimulation(); this.drawCanvas(); }; paragraph = document.createElement('p'); paragraph.appendChild(this.btnPlayPause); paragraph.appendChild(this.btnReset); this.divEl.appendChild(paragraph); this.showFps = showFps; if (showFps) { // add framecounter and fps variables and html this.frameCounter = 0; this.initTime = performance.now(); const el = document.createElement('p'); this.frameLabel = document.createElement('span'); this.frameLabel.appendChild(document.createTextNode('0')); const floatRight = document.createElement('span'); floatRight.style = "float: right;"; this.fpsCounter = 0; this.fpsLabel = document.createElement('span'); this.fpsLabel.appendChild(document.createTextNode('/')); el.appendChild(document.createTextNode('fps: ')); el.appendChild(this.fpsLabel); floatRight.appendChild(document.createTextNode('frame: ')); floatRight.appendChild(this.frameLabel); el.appendChild(floatRight); this.divEl.appendChild(el); } this.canvasEl.addEventListener("mousedown", (e) => this.onMouseDown(e)); // this.canvasEl.addEventListener("mousemove", (e) => this.onMouseMove(e)); // this.canvasEl.addEventListener("mouseup", (e) => this.onMouseUp(e)); this.canvasEl.addEventListener("keydown", (e) => this.onKeyDown(e)); this.canvasEl.addEventListener("keyup", (e) => this.onKeyUp(e)); this.canvasEl.addEventListener("blur", (e) => this.onBlur(e)); this.canvasEl.addEventListener("focus", (e) => this.onFocus(e)); if (CollisionDetection_Ctx2d.DEBUG_PARENT_OBJECT) window.game = this; this.drawCanvas(); } switchGravity() { if (this.gravity == 0) { this.gravity = 100; } else if (this.gravity == 100) { this.gravity = 200; } else { this.gravity = 0; } this.btnGravity.innerHTML = "gravity=" + this.gravity; } switchConsistency() { if (this.consistency == 1) { this.consistency = 0.998; } else if (this.consistency == 0.998) { this.consistency = 0.99; } else if (this.consistency == 0.99) { this.consistency = 0.98; } else { this.consistency = 1; } this.btnConsistency.innerHTML = "consistency=" + this.consistency; } switchBorderBounce() { if (this.borderBounce == 1) { this.borderBounce = 0.75; } else if (this.borderBounce == 0.75) { this.borderBounce = 0.5; } else if (this.borderBounce == 0.5) { this.borderBounce = 0; } else { this.borderBounce = 1; } this.btnBorderBounce.innerHTML = "bounce=" + this.borderBounce; } switchActorShape() { if (this.actor.shape instanceof Rectangle) { this.actor.shape = new Circle(20); } else { this.actor.shape = new Rectangle(40, 40); } this.btnShape.innerHTML = "shape=" + this.actor.shape.constructor.name; } newSimulation() { this.keyUp = false; this.keyDown = false; this.keyLeft = false; this.keyRight = false; this.solidsList = []; this.solidsList.push(new Solid(100, 100, new Rectangle(100, 50))); this.solidsList.push(new Solid(300, 100, new Circle(30))); // this.actor = new Actor(new Rectangle(20, 20), new Momentum(120, 50)); this.actor = new Actor(new Circle(20), new Momentum(120, 250, 180, 30)); } startRunning(fn) { if (this.animationRequestId != 0) cancelAnimationFrame(this.animationRequestId); this.running = true; this.btnPlayPause.innerHTML = "running=" + this.running; this.prevNow = performance.now(); if (this.showFps) { this.initTime = performance.now(); } this.animationRequestId = requestAnimationFrame(fn); } playPauseSimulation() { this.running = !this.running; this.btnPlayPause.innerHTML = "running=" + this.running; if (this.running) { this.startRunning(() => this.updateCanvas()); } } createCanvas(divId, width, height, zoom) { this.divEl = document.getElementById(divId); if (this.divEl === null) throw new Error("elementId not found: " + divId); while (this.divEl.firstChild) { this.divEl.removeChild(this.divEl.lastChild); } this.canvasEl = this.divEl.appendChild(document.createElement("canvas")); const c = this.canvasEl; this.width = width; this.height = height; if (width > 0 && height > 0) { c.style.width = width * zoom + 'px'; c.style.height = height * zoom + 'px'; c.width = width; c.height = height; } c.tabIndex = 0; // improtant for keyboard focus! // c.style.imageRendering = 'pixelated'; } updateCanvas() { const now = performance.now(); const timeDelta = (now - this.prevNow) / 1000; //timeDelta = (milli - milli) / toSeconds this.prevNow = now; //Simulate something with timeDelta if (this.keyLeft) { this.actor.momentum.ax -= this.movePower; } if (this.keyRight) { this.actor.momentum.ax += this.movePower; } if (this.keyUp) { this.actor.momentum.ay += this.movePower; } if (this.keyDown) { this.actor.momentum.ay -= this.movePower; } if (this.gravity != 0) { this.actor.momentum.ay -= this.gravity; } let oldMomentum = this.actor.momentum; if (this.consistency != 1) { oldMomentum.vx *= this.consistency; oldMomentum.vy *= this.consistency; } let newVX = oldMomentum.vx + oldMomentum.ax * timeDelta; let newVY = oldMomentum.vy + oldMomentum.ay * timeDelta; let newMomentum = new Momentum(oldMomentum.x + newVX * timeDelta, oldMomentum.y + newVY * timeDelta, newVX, newVY) //canvas boundry if (this.actor.shape instanceof Rectangle) { if (newMomentum.x < 0) { if (this.borderBounce == 0) { newMomentum.x = 0; if (newMomentum.vx < 0) newMomentum.vx = 0; } else { newMomentum.x = -newMomentum.x; newMomentum.vx = -newMomentum.vx * this.borderBounce; } } else if (newMomentum.x + this.actor.shape.width > this.width) { if (this.borderBounce == 0) { newMomentum.x = this.width - this.actor.shape.width; if (newMomentum.vx > 0) newMomentum.vx = 0; } else { newMomentum.x = this.width - this.actor.shape.width; newMomentum.vx = -newMomentum.vx * this.borderBounce; } } if (newMomentum.y < 0) { if (this.borderBounce == 0) { newMomentum.y = 0; if (newMomentum.vy < 0) newMomentum.vy = 0; } else { newMomentum.y = -newMomentum.y; newMomentum.vy = -newMomentum.vy * this.borderBounce; } } else if (newMomentum.y + this.actor.shape.height > this.height) { if (this.borderBounce == 0) { newMomentum.y = this.height - this.actor.shape.height; if (newMomentum.vy > 0) newMomentum.vy = 0; } else { newMomentum.y = this.height - this.actor.shape.height; newMomentum.vy = -newMomentum.vy * this.borderBounce; } } } else if (this.actor.shape instanceof Circle) { if (newMomentum.x - this.actor.shape.radius < 0) { if (this.borderBounce == 0) { newMomentum.x = this.actor.shape.radius; if (newMomentum.vx < 0) newMomentum.vx = 0; } else { newMomentum.x = this.actor.shape.radius + this.actor.shape.radius - newMomentum.x; newMomentum.vx = -newMomentum.vx * this.borderBounce; } } else if (newMomentum.x + this.actor.shape.radius > this.width) { if (this.borderBounce == 0) { newMomentum.x = this.width - this.actor.shape.radius; if (newMomentum.vx > 0) newMomentum.vx = 0; } else { newMomentum.x = this.width - this.actor.shape.radius; newMomentum.vx = -newMomentum.vx * this.borderBounce; } } if (newMomentum.y - this.actor.shape.radius < 0) { if (this.borderBounce == 0) { newMomentum.y = this.actor.shape.radius; if (newMomentum.vy < 0) newMomentum.vy = 0; } else { newMomentum.y = this.actor.shape.radius + this.actor.shape.radius - newMomentum.y; newMomentum.vy = -newMomentum.vy * this.borderBounce; if (newMomentum.vy < 1) newMomentum.vy = 0; } } else if (newMomentum.y + this.actor.shape.radius > this.height) { if (this.borderBounce == 0) { newMomentum.y = this.height - this.actor.shape.radius; if (newMomentum.vy > 0) newMomentum.vy = 0; } else { newMomentum.y = this.height - this.actor.shape.radius; newMomentum.vy = -newMomentum.vy * this.borderBounce; if (newMomentum.vy > -1) newMomentum.vy = 0; } } } this.actor.momentum = newMomentum; this.actor.isInSolid = false; for (let solid of this.solidsList) { if (this.isActorOverSolid(this.actor, solid)) { // this.running = false; // console.log("actor bumped into solid!"); this.actor.isInSolid = true; break; } } this.drawCanvas(); if (this.running) { // loop this.animationRequestId = requestAnimationFrame(() => this.updateCanvas()); } else { // not looping this.animationRequestId = 0; } } isPointInsideShape(pointX, pointY, shapeX, shapeY, shape) { if (shape instanceof Rectangle) { if (pointX > shapeX && pointX < shapeX + shape.width && pointY > shapeY && pointY < shapeY + shape.height) return true; } else if (shape instanceof Circle) { if (Math.sqrt(Math.pow(pointX - shapeX, 2) + Math.pow(pointY - shapeY, 2)) < shape.radius) return true; } return false; } isActorOverSolid(actor, solid) { if (actor.shape instanceof Circle) {//cirlce if (solid.shape instanceof Circle) { const distance = Math.sqrt(Math.pow(actor.momentum.x - solid.x, 2) + Math.pow(actor.momentum.y - solid.y, 2)); if (distance < actor.shape.radius + solid.shape.radius) return true; } else if (solid.shape instanceof Rectangle) { if (actor.momentum.x > solid.x && actor.momentum.x < solid.x + solid.shape.width && actor.momentum.y + actor.shape.radius > solid.y && actor.momentum.y - actor.shape.radius < solid.y + solid.shape.height) return true; if (actor.momentum.x + actor.shape.radius > solid.x && actor.momentum.x - actor.shape.radius < solid.x + solid.shape.width && actor.momentum.y > solid.y && actor.momentum.y < solid.y + solid.shape.height) return true; return this.isPointInsideShape(solid.x, solid.y, actor.momentum.x, actor.momentum.y, actor.shape) || this.isPointInsideShape(solid.x + solid.shape.width, solid.y, actor.momentum.x, actor.momentum.y, actor.shape) || this.isPointInsideShape(solid.x + solid.shape.width, solid.y + solid.shape.height, actor.momentum.x, actor.momentum.y, actor.shape) || this.isPointInsideShape(solid.x, solid.y + solid.shape.height, actor.momentum.x, actor.momentum.y, actor.shape); } } else if (actor.shape instanceof Rectangle) {//recangle if (solid.shape instanceof Circle) { if (actor.momentum.x + actor.shape.width > solid.x && actor.momentum.x < solid.x && actor.momentum.y + actor.shape.height > solid.y - solid.shape.radius && actor.momentum.y < solid.y + solid.shape.radius) return true; if (actor.momentum.x + actor.shape.width > solid.x - solid.shape.radius && actor.momentum.x < solid.x + solid.shape.radius && actor.momentum.y + actor.shape.height > solid.y && actor.momentum.y < solid.y) return true; return this.isPointInsideShape(actor.momentum.x, actor.momentum.y, solid.x, solid.y, solid.shape) || this.isPointInsideShape(actor.momentum.x + actor.shape.width, actor.momentum.y, solid.x, solid.y, solid.shape) || this.isPointInsideShape(actor.momentum.x + actor.shape.width, actor.momentum.y + actor.shape.height, solid.x, solid.y, solid.shape) || this.isPointInsideShape(actor.momentum.x, actor.momentum.y + actor.shape.height, solid.x, solid.y, solid.shape); } else if (solid.shape instanceof Rectangle) { if (actor.momentum.x < solid.x + solid.shape.width && actor.momentum.x + actor.shape.width > solid.x && actor.momentum.y < solid.y + solid.shape.height && actor.momentum.y + actor.shape.height > solid.y) return true; } } return false; } drawCanvas() { if (this.showFps) { this.frameCounter++; this.fpsCounter++; const now = performance.now(); const diff = now - this.initTime; if (CollisionDetection_Ctx2d.SHOW_FPS_INTERVAL < diff || !this.running) { this.initTime = now; const seconds = diff / 1000; const fps = this.fpsCounter / seconds; this.fpsCounter = 0; if (this.frameLabel) this.frameLabel.innerHTML = this.frameCounter; if (this.fpsLabel) { this.fpsLabel.innerHTML = Math.round((fps + Number.EPSILON) * 100) / 100; if (!this.running) this.fpsLabel.innerHTML += " (not running)"; } } } const ctx = this.ctx; ctx.clearRect(0, 0, this.width, this.height); this.ctx.fillStyle = "#04c"; for (const solid of this.solidsList) { if (solid.shape instanceof Rectangle) { ctx.fillRect(solid.x, this.height - solid.y, solid.shape.width, -solid.shape.height); } else if (solid.shape instanceof Circle) { ctx.beginPath(); ctx.arc(solid.x, this.height - solid.y, solid.shape.radius, 0, 2 * Math.PI, false); ctx.fill(); } } if (this.actor.isInSolid) { ctx.strokeStyle = 'red'; } else { ctx.strokeStyle = 'green'; } if (this.actor.shape instanceof Rectangle) { ctx.strokeRect(this.actor.momentum.x, this.height - this.actor.momentum.y, this.actor.shape.width, -this.actor.shape.height); } else if (this.actor.shape instanceof Circle) { ctx.beginPath(); ctx.arc(this.actor.momentum.x, this.height - this.actor.momentum.y, this.actor.shape.radius, 0, 2 * Math.PI, false); ctx.stroke(); } // ctx.fillStyle = 'black'; // ctx.fillRect(this.rect.shapeX, this.rect.shapeY, this.rect.shapeWidth, this.rect.shapeHeight); // ctx.beginPath(); // ctx.moveTo(this.line.x1, this.line.y1); // ctx.lineTo(this.line.x2, this.line.y2); // ctx.stroke(); const FONT_SIZE = this.width > 440 ? 24 : 16; if (!this.running) { const x = this.width / 2; const y = this.height / 2; const y2 = y - FONT_SIZE / 2; ctx.strokeStyle = '#444'; const g = ctx.createLinearGradient(0, 0, this.width, 0); g.addColorStop(0, '#404040a0'); g.addColorStop(0.2, '#404040df'); g.addColorStop(0.8, '#404040df'); g.addColorStop(1, '#404040a0'); ctx.fillStyle = g; ctx.fillRect(0, y2 - 2, this.width + 1, FONT_SIZE + 5 * 2); ctx.strokeRect(0, y2 - 2, this.width + 1, FONT_SIZE + 5 * 2); ctx.fillStyle = 'gray'; ctx.font = 4 * FONT_SIZE + "px serif"; let text = "Paused"; ctx.fillText(text, x, y - 2 * FONT_SIZE); ctx.strokeText(text, x, y - 2 * FONT_SIZE); ctx.fillStyle = this.isFocused ? 'goldenrod' : 'lightgray'; ctx.font = FONT_SIZE + "px serif"; text = this.isFocused ? `Press space or enter to continue.` : "Click here to continue. (unfocused)"; ctx.strokeText(text, x, y2 + FONT_SIZE); ctx.fillText(text, x, y2 + FONT_SIZE); } } onBlur() { // this.audioCtx.suspend(); this.isFocused = false; this.canvasEl.style.borderColor = null; if (this.running) { this.playPauseSimulation(); } else { this.drawCanvas(); } } onFocus() { // this.audioCtx.resume(); this.isFocused = true; this.canvasEl.style.borderColor = "red"; if (!this.running && CollisionDetection_Ctx2d.AUTO_CONTINUE_ON_FOCUS) { this.playPauseSimulation(); } else { this.drawCanvas(); } } // onMouseMove(e) { // return false; // } onMouseDown(e) { let rect = this.canvasEl.getBoundingClientRect(); if (e.button == 0) { // left mouse button const mouseX = e.clientX - rect.left; const mouseY = this.height - (e.clientY - rect.top); console.log('left-click at x=' + mouseX + ' y=' + mouseY); console.log(' is on actor ' + this.actor.shape.constructor.name + ': ' + this.isPointInsideShape(mouseX, mouseY, this.actor.momentum.x, this.actor.momentum.y, this.actor.shape)); for (let solid of this.solidsList) { console.log(' is on solid ' + solid.shape.constructor.name + ': ' + this.isPointInsideShape(mouseX, mouseY, solid.x, solid.y, solid.shape)); } } else return true; // no button of interest return false; } // onMouseUp(e) { // return false; // } onKeyDown(e) { if (e.key == CollisionDetection_Ctx2d.KEY_ESCAPE) { if (this.running) this.playPauseSimulation() e.preventDefault(); return false; } if (this.running) { if (e.key == 'r') { this.newSimulation(); console.log('simulation reset'); e.preventDefault(); return; } if (e.key == 's') { this.switchActorShape(); console.log('actor.shape=' + this.actor.shape.constructor.name); e.preventDefault(); return; } if (e.key == 'b') { this.switchBorderBounce(); console.log('borderBounce=' + this.borderBounce); e.preventDefault(); return; } if (e.key == 'c') { this.switchConsistency(); console.log('consistency=' + this.consistency); e.preventDefault(); return; } if (e.key == 'g') { this.switchGravity(); console.log('gravity=' + this.gravity); e.preventDefault(); return; } if (e.key == 'p') { if (e.ctrlKey) { getWTerminal('dropdown').terminalCommand('pop game.actor.momentum'); } else { console.log('actor', this.actor); } e.preventDefault(); return; } if (e.key == CollisionDetection_Ctx2d.KEY_ARROW_UP || e.key == CollisionDetection_Ctx2d.KEY_W) { this.keyUp = true; e.preventDefault(); return false; } else if (e.key == CollisionDetection_Ctx2d.KEY_ARROW_DOWN || e.key == CollisionDetection_Ctx2d.KEY_S) { this.keyDown = true; e.preventDefault(); return false; } else if (e.key == CollisionDetection_Ctx2d.KEY_ARROW_LEFT || e.key == CollisionDetection_Ctx2d.KEY_A) { this.keyLeft = true; e.preventDefault(); return false; } else if (e.key == CollisionDetection_Ctx2d.KEY_ARROW_RIGHT || e.key == CollisionDetection_Ctx2d.KEY_D) { this.keyRight = true; e.preventDefault(); return false; } } if (e.key == CollisionDetection_Ctx2d.KEY_SPACEBAR || e.key == CollisionDetection_Ctx2d.KEY_ENTER) { this.playPauseSimulation(); e.preventDefault(); return false; } return true; } onKeyUp(e) { if (this.running) { if (e.key == CollisionDetection_Ctx2d.KEY_ARROW_UP || e.key == CollisionDetection_Ctx2d.KEY_W) { this.keyUp = false; e.preventDefault(); return false; } else if (e.key == CollisionDetection_Ctx2d.KEY_ARROW_DOWN || e.key == CollisionDetection_Ctx2d.KEY_S) { this.keyDown = false; e.preventDefault(); return false; } else if (e.key == CollisionDetection_Ctx2d.KEY_ARROW_LEFT || e.key == CollisionDetection_Ctx2d.KEY_A) { this.keyLeft = false; e.preventDefault(); return false; } else if (e.key == CollisionDetection_Ctx2d.KEY_ARROW_RIGHT || e.key == CollisionDetection_Ctx2d.KEY_D) { this.keyRight = false; e.preventDefault(); return false; } } return true; } } function startCollisionDetection(divId, width, height, zoom = 1, showFps = true) { return new CollisionDetection_Ctx2d(divId, width, height, zoom, showFps); }