From 1c3878e8b7a9d638f45196f2baa647bc5fa47694 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Fri, 27 Dec 2024 14:10:14 -0600 Subject: [PATCH 1/6] feat: wavefunction speeds + labels --- components/waveforms.js | 25 ++++++++++++++++++------- {static => files}/Ryan-Hill-CV.pdf | Bin index.html | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) rename {static => files}/Ryan-Hill-CV.pdf (100%) diff --git a/components/waveforms.js b/components/waveforms.js index 6445309..4bf1c5b 100644 --- a/components/waveforms.js +++ b/components/waveforms.js @@ -17,8 +17,10 @@ class WaveFunction { this.x = Math.random() * (canvas.width - 2 * this.radius) + this.radius; this.y = Math.random() * (canvas.height - 2 * this.radius) + this.radius; const baseSpeed = (Math.random() - 0.5) * 0.5; - this.speedX = isLink ? baseSpeed : baseSpeed * 2; - this.speedY = isLink ? baseSpeed : baseSpeed * 2; + const normSpeed = + Math.abs(baseSpeed) >= 0.05 ? baseSpeed : Math.sign(baseSpeed) * 0.05; + this.speedX = isLink ? normSpeed : normSpeed * 2; + this.speedY = isLink ? normSpeed : normSpeed * 2; this.alpha = 0.7; this.isCollapsing = false; this.collapseDuration = 500; @@ -50,7 +52,16 @@ class WaveFunction { ctx.translate(this.x, this.y); ctx.scale(0.5, 0.5); ctx.rotate(0.01); - ctx.fillText(this.label, 0, 0); + + const lines = this.label.split('\n'); + if (lines.length === 1) { + ctx.fillText(this.label, 0, 0); + } else if (lines.length === 2) { + ctx.fillText(lines[0], 0, -15); + ctx.fillText(lines[1], 0, 15); + } else { + throw new Error('Too many lines in label'); + } ctx.restore(); } @@ -70,10 +81,10 @@ class WaveFunction { this.x += this.speedX * deltaTime * 60; this.y += this.speedY * deltaTime * 60; - if (this.x + this.radius > canvas.width || this.x - this.radius < 0) { + if (this.x + this.radius >= canvas.width || this.x - this.radius <= 0) { this.speedX = -this.speedX; } - if (this.y + this.radius > canvas.height || this.y - this.radius < 0) { + if (this.y + this.radius >= canvas.height || this.y - this.radius <= 0) { this.speedY = -this.speedY; } } @@ -100,13 +111,13 @@ waveFunctions.push( waveFunctions.push( new WaveFunction( true, - 'QCSE', + 'Stack\nExchange', 'https://quantumcomputing.stackexchange.com/users/13991/ryanhill1?tab=profile', '#F48024', ), ); waveFunctions.push( - new WaveFunction(true, 'CV', '/static/Ryan-Hill-CV.pdf', '#C0C0C0'), + new WaveFunction(true, 'CV', '/files/Ryan-Hill-CV.pdf', '#C0C0C0'), ); setInterval(() => { diff --git a/static/Ryan-Hill-CV.pdf b/files/Ryan-Hill-CV.pdf similarity index 100% rename from static/Ryan-Hill-CV.pdf rename to files/Ryan-Hill-CV.pdf diff --git a/index.html b/index.html index b95e88f..25d7196 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - ryanhill.tech + Ryan Hill From 109ddd2a95474d0fa672c5b7d44818af049b0dd6 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Fri, 27 Dec 2024 16:20:00 -0600 Subject: [PATCH 2/6] feat: combine waveforms --- components/waveforms.js | 193 ++++++++++++++++++++++++++++++++++------ 1 file changed, 167 insertions(+), 26 deletions(-) diff --git a/components/waveforms.js b/components/waveforms.js index 4bf1c5b..7a95db7 100644 --- a/components/waveforms.js +++ b/components/waveforms.js @@ -9,13 +9,40 @@ function resizeCanvas() { canvas.height = window.innerHeight; } +const MAX_WAVE_FUNCTIONS = 20; + let waveFunctions = []; +function combinedAreaRadius(radius1, radius2) { + const area1 = Math.PI * radius1 * radius1; + const area2 = Math.PI * radius2 * radius2; + const combinedArea = area1 + area2; + const newRadius = Math.sqrt(combinedArea / Math.PI); + return newRadius; +} + class WaveFunction { constructor(isLink = false, label = '', url = '', color = '') { - this.radius = isLink ? 50 : 20 + Math.random() * 30; - this.x = Math.random() * (canvas.width - 2 * this.radius) + this.radius; - this.y = Math.random() * (canvas.height - 2 * this.radius) + this.radius; + const initialRadius = isLink ? 50 : 20 + Math.random() * 30; + this.radius = initialRadius; + let position = null; + + for (let i = 1; i <= 5; i++) { + position = WaveFunction.findValidPosition(this.radius); + if (position) { + break; + } + this.radius = initialRadius / (i + 1); + } + + if (position) { + this.x = position.x; + this.y = position.y; + } else { + this.radius = initialRadius / 5; + this.x = Math.random() * (canvas.width - 2 * this.radius) + this.radius; + this.y = Math.random() * (canvas.height - 2 * this.radius) + this.radius; + } const baseSpeed = (Math.random() - 0.5) * 0.5; const normSpeed = Math.abs(baseSpeed) >= 0.05 ? baseSpeed : Math.sign(baseSpeed) * 0.05; @@ -30,6 +57,36 @@ class WaveFunction { this.label = label; this.url = url; this.color = isLink ? color : 'rgba(100, 100, 255, 1)'; + this.mass = this.radius; + } + + static findValidPosition(radius) { + let attempts = 0; + const maxAttempts = 100; + + while (attempts < maxAttempts) { + const x = Math.random() * (canvas.width - 2 * radius) + radius; + const y = Math.random() * (canvas.height - 2 * radius) + radius; + + let isValid = true; + for (let wf of waveFunctions) { + const dx = x - wf.x; + const dy = y - wf.y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance < radius + wf.radius) { + isValid = false; + break; + } + } + + if (isValid) { + return { x, y }; + } + + attempts++; + } + + return null; } draw() { @@ -95,33 +152,97 @@ class WaveFunction { this.collapseStartTime = Date.now(); } } + + checkCollision(other) { + const dx = this.x - other.x; + const dy = this.y - other.y; + const distance = Math.sqrt(dx * dx + dy * dy); + return distance < this.radius + other.radius; + } + + resolveCollision(other) { + const dx = other.x - this.x; + const dy = other.y - this.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance === 0) return; // Avoid division by zero + + const normalX = dx / distance; + const normalY = dy / distance; + const tangentX = -normalY; + const tangentY = normalX; + + const v1n = this.speedX * normalX + this.speedY * normalY; + const v1t = this.speedX * tangentX + this.speedY * tangentY; + const v2n = other.speedX * normalX + other.speedY * normalY; + const v2t = other.speedX * tangentX + other.speedY * tangentY; + + const v1n_after = + (v1n * (this.mass - other.mass) + 2 * other.mass * v2n) / + (this.mass + other.mass); + const v2n_after = + (v2n * (other.mass - this.mass) + 2 * this.mass * v1n) / + (this.mass + other.mass); + + this.speedX = v1n_after * normalX + v1t * tangentX; + this.speedY = v1n_after * normalY + v1t * tangentY; + other.speedX = v2n_after * normalX + v2t * tangentX; + other.speedY = v2n_after * normalY + v2t * tangentY; + } + + combine(other) { + const newRadius = combinedAreaRadius(this.radius, other.radius); + const newSpeedX = (this.speedX + other.speedX) / 2; + const newSpeedY = (this.speedY + other.speedY) / 2; + if (this.mass > other.mass) { + this.radius = newRadius; + this.mass = newRadius; + this.speedX = newSpeedX; + this.speedY = newSpeedY; + other.collapse(); + other.markedForRemoval = true; + } else { + other.radius = newRadius; + other.mass = newRadius; + other.speedX = newSpeedX; + other.speedY = newSpeedY; + this.collapse(); + this.markedForRemoval = true; + } + } } -waveFunctions.push( - new WaveFunction( - true, - 'LinkedIn', - 'https://www.linkedin.com/in/ryan-james-hill/', - '#0077B5', - ), -); -waveFunctions.push( - new WaveFunction(true, 'GitHub', 'https://github.com/ryanhill1', '#171515'), -); -waveFunctions.push( - new WaveFunction( - true, - 'Stack\nExchange', - 'https://quantumcomputing.stackexchange.com/users/13991/ryanhill1?tab=profile', - '#F48024', - ), -); -waveFunctions.push( - new WaveFunction(true, 'CV', '/files/Ryan-Hill-CV.pdf', '#C0C0C0'), -); +const linkData = [ + { + name: 'LinkedIn', + link: 'https://www.linkedin.com/in/ryan-james-hill/', + color: '#0077B5', + }, + { + name: 'GitHub', + link: 'https://github.com/ryanhill1', + color: '#171515', + }, + { + name: 'Stack\nExchange', + link: 'https://quantumcomputing.stackexchange.com/users/13991/ryanhill1?tab=profile', + color: '#F48024', + }, + { + name: 'CV', + link: '/files/Ryan-Hill-CV.pdf', + color: '#C0C0C0', + }, +]; + +linkData.forEach(({ name, link, color }) => { + waveFunctions.push(new WaveFunction(true, name, link, color)); +}); setInterval(() => { - waveFunctions.push(new WaveFunction()); + if (waveFunctions.length <= MAX_WAVE_FUNCTIONS) { + waveFunctions.push(new WaveFunction()); + } }, 2000); const FIXED_TIME_STEP = 1000 / 60; // 60 FPS @@ -135,6 +256,22 @@ function animate(currentTime) { if (deltaTime >= FIXED_TIME_STEP / 1000) { ctx.clearRect(0, 0, canvas.width, canvas.height); + for (let i = 0; i < waveFunctions.length; i++) { + for (let j = i + 1; j < waveFunctions.length; j++) { + if (waveFunctions[i].checkCollision(waveFunctions[j])) { + if ( + waveFunctions.length >= MAX_WAVE_FUNCTIONS && + !waveFunctions[i].isLink && + !waveFunctions[j].isLink + ) { + waveFunctions[i].combine(waveFunctions[j]); + } else { + waveFunctions[i].resolveCollision(waveFunctions[j]); + } + } + } + } + waveFunctions.forEach((wf) => { wf.update(deltaTime); wf.draw(); @@ -164,4 +301,8 @@ canvas.addEventListener('click', function (event) { break; } } + + if (waveFunctions.length <= linkData.length + 1) { + waveFunctions.push(new WaveFunction()); + } }); From 90db255903dbfa30232e1c92402f1c63ed2848f0 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Fri, 27 Dec 2024 17:06:16 -0600 Subject: [PATCH 3/6] fix: islink wf collision overlap bug --- components/waveforms.js | 95 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 7 deletions(-) diff --git a/components/waveforms.js b/components/waveforms.js index 7a95db7..e1039fc 100644 --- a/components/waveforms.js +++ b/components/waveforms.js @@ -9,7 +9,7 @@ function resizeCanvas() { canvas.height = window.innerHeight; } -const MAX_WAVE_FUNCTIONS = 20; +const MAX_WAVE_FUNCTIONS = 100; let waveFunctions = []; @@ -58,6 +58,11 @@ class WaveFunction { this.url = url; this.color = isLink ? color : 'rgba(100, 100, 255, 1)'; this.mass = this.radius; + this.isGrowing = false; + this.growthStartRadius = 0; + this.growthTargetRadius = 0; + this.growthStartTime = 0; + this.growthDuration = 500; } static findValidPosition(radius) { @@ -188,22 +193,59 @@ class WaveFunction { this.speedY = v1n_after * normalY + v1t * tangentY; other.speedX = v2n_after * normalX + v2t * tangentX; other.speedY = v2n_after * normalY + v2t * tangentY; + + const overlap = this.radius + other.radius - distance; + if (overlap > 0) { + const separationX = normalX * overlap * 0.5; + const separationY = normalY * overlap * 0.5; + this.x -= separationX; + this.y -= separationY; + other.x += separationX; + other.y += separationY; + } + } + + startGrowth(targetRadius) { + this.isGrowing = true; + this.growthStartRadius = this.radius; + this.growthTargetRadius = targetRadius; + this.growthStartTime = Date.now(); + } + + updateGrowth() { + if (!this.isGrowing) return; + + const elapsedTime = Date.now() - this.growthStartTime; + const progress = Math.min(elapsedTime / this.growthDuration, 1); + + // Use easeOutQuad for smoother animation + const easeOutQuad = (t) => t * (2 - t); + const easedProgress = easeOutQuad(progress); + + this.radius = + this.growthStartRadius + + (this.growthTargetRadius - this.growthStartRadius) * easedProgress; + + if (progress >= 1) { + this.isGrowing = false; + this.radius = this.growthTargetRadius; + this.mass = this.radius; + } } combine(other) { const newRadius = combinedAreaRadius(this.radius, other.radius); const newSpeedX = (this.speedX + other.speedX) / 2; const newSpeedY = (this.speedY + other.speedY) / 2; + if (this.mass > other.mass) { - this.radius = newRadius; - this.mass = newRadius; + this.startGrowth(newRadius); this.speedX = newSpeedX; this.speedY = newSpeedY; other.collapse(); other.markedForRemoval = true; } else { - other.radius = newRadius; - other.mass = newRadius; + other.startGrowth(newRadius); other.speedX = newSpeedX; other.speedY = newSpeedY; this.collapse(); @@ -247,16 +289,50 @@ setInterval(() => { const FIXED_TIME_STEP = 1000 / 60; // 60 FPS let lastTime = 0; +let frameCount = 0; + +function checkAndSeparateOverlaps() { + for (let i = 0; i < waveFunctions.length; i++) { + for (let j = i + 1; j < waveFunctions.length; j++) { + const wf1 = waveFunctions[i]; + const wf2 = waveFunctions[j]; + const dx = wf2.x - wf1.x; + const dy = wf2.y - wf1.y; + const distance = Math.sqrt(dx * dx + dy * dy); + const overlap = wf1.radius + wf2.radius - distance; + + if (overlap > 0) { + const separationX = (dx / distance) * overlap * 0.5; + const separationY = (dy / distance) * overlap * 0.5; + wf1.x -= separationX; + wf1.y -= separationY; + wf2.x += separationX; + wf2.y += separationY; + + // Add a small random velocity to help separate stuck wavefunctions + if (!wf1.isLink) { + wf1.speedX += (Math.random() - 0.5) * 0.5; + wf1.speedY += (Math.random() - 0.5) * 0.5; + } + if (!wf2.isLink) { + wf2.speedX += (Math.random() - 0.5) * 0.5; + wf2.speedY += (Math.random() - 0.5) * 0.5; + } + } + } + } +} function animate(currentTime) { requestAnimationFrame(animate); - const deltaTime = (currentTime - lastTime) / 1000; if (deltaTime >= FIXED_TIME_STEP / 1000) { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i < waveFunctions.length; i++) { + waveFunctions[i].updateGrowth(); + for (let j = i + 1; j < waveFunctions.length; j++) { if (waveFunctions[i].checkCollision(waveFunctions[j])) { if ( @@ -278,9 +354,14 @@ function animate(currentTime) { }); waveFunctions = waveFunctions.filter((wf) => !wf.markedForRemoval); - lastTime = currentTime; } + + if (frameCount % 60 === 0) { + checkAndSeparateOverlaps(); + } + + frameCount++; } animate(0); From 682912b1d70d18461e182f95a3f3ec94d60399c2 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Fri, 27 Dec 2024 17:33:17 -0600 Subject: [PATCH 4/6] feat: collapse all button --- components/waveforms.js | 393 ++++++++++++++++++++-------------------- index.html | 7 + 2 files changed, 206 insertions(+), 194 deletions(-) diff --git a/components/waveforms.js b/components/waveforms.js index e1039fc..f3991c9 100644 --- a/components/waveforms.js +++ b/components/waveforms.js @@ -1,96 +1,102 @@ +const collapseAllButton = document.getElementById('collapseAllButton'); const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); -resizeCanvas(); -window.addEventListener('resize', resizeCanvas); +const MAX_WAVE_FUNCTIONS = 100; +const FIXED_TIME_STEP = 1000 / 60; // 60 FPS +const LINK_DATA = [ + { + name: 'LinkedIn', + link: 'https://www.linkedin.com/in/ryan-james-hill/', + color: '#0077B5', + }, + { name: 'GitHub', link: 'https://github.com/ryanhill1', color: '#171515' }, + { + name: 'Stack\nExchange', + link: 'https://quantumcomputing.stackexchange.com/users/13991/ryanhill1?tab=profile', + color: '#F48024', + }, + { name: 'CV', link: '/files/Ryan-Hill-CV.pdf', color: '#C0C0C0' }, +]; + +let waveFunctions = []; +let lastTime = 0; +let frameCount = 0; function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } -const MAX_WAVE_FUNCTIONS = 100; - -let waveFunctions = []; - function combinedAreaRadius(radius1, radius2) { - const area1 = Math.PI * radius1 * radius1; - const area2 = Math.PI * radius2 * radius2; - const combinedArea = area1 + area2; - const newRadius = Math.sqrt(combinedArea / Math.PI); - return newRadius; + const combinedArea = Math.PI * (radius1 * radius1 + radius2 * radius2); + return Math.sqrt(combinedArea / Math.PI); } class WaveFunction { constructor(isLink = false, label = '', url = '', color = '') { - const initialRadius = isLink ? 50 : 20 + Math.random() * 30; + this.isLink = isLink; + this.label = label; + this.url = url; + this.color = isLink ? color : 'rgba(100, 100, 255, 1)'; + this.initializeProperties(); + } + + initializeProperties() { + const initialRadius = this.isLink ? 50 : 20 + Math.random() * 30; this.radius = initialRadius; - let position = null; + this.setPosition(); + this.setSpeed(); + this.alpha = 0.7; + this.isCollapsing = false; + this.collapseDuration = 500; + this.collapseStartTime = null; + this.markedForRemoval = false; + this.mass = this.radius; + this.isGrowing = false; + this.growthStartRadius = 0; + this.growthTargetRadius = 0; + this.growthStartTime = 0; + this.growthDuration = 500; + } + setPosition() { + let position = null; for (let i = 1; i <= 5; i++) { position = WaveFunction.findValidPosition(this.radius); - if (position) { - break; - } - this.radius = initialRadius / (i + 1); + if (position) break; + this.radius = this.radius / (i + 1); } - if (position) { this.x = position.x; this.y = position.y; } else { - this.radius = initialRadius / 5; + this.radius = this.radius / 5; this.x = Math.random() * (canvas.width - 2 * this.radius) + this.radius; this.y = Math.random() * (canvas.height - 2 * this.radius) + this.radius; } + } + + setSpeed() { const baseSpeed = (Math.random() - 0.5) * 0.5; const normSpeed = Math.abs(baseSpeed) >= 0.05 ? baseSpeed : Math.sign(baseSpeed) * 0.05; - this.speedX = isLink ? normSpeed : normSpeed * 2; - this.speedY = isLink ? normSpeed : normSpeed * 2; - this.alpha = 0.7; - this.isCollapsing = false; - this.collapseDuration = 500; - this.collapseStartTime = null; - this.markedForRemoval = false; - this.isLink = isLink; - this.label = label; - this.url = url; - this.color = isLink ? color : 'rgba(100, 100, 255, 1)'; - this.mass = this.radius; - this.isGrowing = false; - this.growthStartRadius = 0; - this.growthTargetRadius = 0; - this.growthStartTime = 0; - this.growthDuration = 500; + this.speedX = this.isLink ? normSpeed : normSpeed * 2; + this.speedY = this.isLink ? normSpeed : normSpeed * 2; } static findValidPosition(radius) { - let attempts = 0; - const maxAttempts = 100; - - while (attempts < maxAttempts) { + for (let i = 0; i < 100; i++) { const x = Math.random() * (canvas.width - 2 * radius) + radius; const y = Math.random() * (canvas.height - 2 * radius) + radius; - - let isValid = true; - for (let wf of waveFunctions) { - const dx = x - wf.x; - const dy = y - wf.y; - const distance = Math.sqrt(dx * dx + dy * dy); - if (distance < radius + wf.radius) { - isValid = false; - break; - } - } - - if (isValid) { + if ( + waveFunctions.every( + (wf) => Math.hypot(x - wf.x, y - wf.y) >= radius + wf.radius, + ) + ) { return { x, y }; } - - attempts++; } - return null; } @@ -103,46 +109,60 @@ class WaveFunction { ctx.shadowBlur = 15 * this.alpha; ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fill(); - ctx.closePath(); if (this.isLink) { - ctx.fillStyle = 'white'; - ctx.font = 'bold 32px Arial'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.save(); - ctx.translate(this.x, this.y); - ctx.scale(0.5, 0.5); - ctx.rotate(0.01); - - const lines = this.label.split('\n'); - if (lines.length === 1) { - ctx.fillText(this.label, 0, 0); - } else if (lines.length === 2) { - ctx.fillText(lines[0], 0, -15); - ctx.fillText(lines[1], 0, 15); - } else { - throw new Error('Too many lines in label'); - } - ctx.restore(); + this.drawLabel(); } ctx.restore(); } + drawLabel() { + ctx.fillStyle = 'white'; + ctx.font = 'bold 32px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.save(); + ctx.translate(this.x, this.y); + ctx.scale(0.5, 0.5); + ctx.rotate(0.01); + + const lines = this.label.split('\n'); + if (lines.length === 1) { + ctx.fillText(this.label, 0, 0); + } else if (lines.length === 2) { + ctx.fillText(lines[0], 0, -15); + ctx.fillText(lines[1], 0, 15); + } else { + throw new Error('Too many lines in label'); + } + ctx.restore(); + } + update(deltaTime) { if (this.isCollapsing) { - const elapsed = Date.now() - this.collapseStartTime; - this.alpha = Math.max(0, 0.7 * (1 - elapsed / this.collapseDuration)); - if (this.alpha <= 0) { - this.markedForRemoval = true; - } - return; + this.updateCollapse(); + } else { + this.updatePosition(deltaTime); + } + this.updateGrowth(); + } + + updateCollapse() { + const elapsed = Date.now() - this.collapseStartTime; + this.alpha = Math.max(0, 0.7 * (1 - elapsed / this.collapseDuration)); + if (this.alpha <= 0) { + this.markedForRemoval = true; } + } + updatePosition(deltaTime) { this.x += this.speedX * deltaTime * 60; this.y += this.speedY * deltaTime * 60; + this.bounceOffWalls(); + } + bounceOffWalls() { if (this.x + this.radius >= canvas.width || this.x - this.radius <= 0) { this.speedX = -this.speedX; } @@ -159,28 +179,28 @@ class WaveFunction { } checkCollision(other) { - const dx = this.x - other.x; - const dy = this.y - other.y; - const distance = Math.sqrt(dx * dx + dy * dy); - return distance < this.radius + other.radius; + return ( + Math.hypot(this.x - other.x, this.y - other.y) < + this.radius + other.radius + ); } resolveCollision(other) { const dx = other.x - this.x; const dy = other.y - this.y; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance === 0) return; // Avoid division by zero - - const normalX = dx / distance; - const normalY = dy / distance; - const tangentX = -normalY; - const tangentY = normalX; - - const v1n = this.speedX * normalX + this.speedY * normalY; - const v1t = this.speedX * tangentX + this.speedY * tangentY; - const v2n = other.speedX * normalX + other.speedY * normalY; - const v2t = other.speedX * tangentX + other.speedY * tangentY; + const distance = Math.hypot(dx, dy); + if (distance === 0) return; + + const [normalX, normalY] = [dx / distance, dy / distance]; + const [tangentX, tangentY] = [-normalY, normalX]; + const [v1n, v1t] = [ + this.speedX * normalX + this.speedY * normalY, + this.speedX * tangentX + this.speedY * tangentY, + ]; + const [v2n, v2t] = [ + other.speedX * normalX + other.speedY * normalY, + other.speedX * tangentX + other.speedY * tangentY, + ]; const v1n_after = (v1n * (this.mass - other.mass) + 2 * other.mass * v2n) / @@ -189,11 +209,19 @@ class WaveFunction { (v2n * (other.mass - this.mass) + 2 * this.mass * v1n) / (this.mass + other.mass); - this.speedX = v1n_after * normalX + v1t * tangentX; - this.speedY = v1n_after * normalY + v1t * tangentY; - other.speedX = v2n_after * normalX + v2t * tangentX; - other.speedY = v2n_after * normalY + v2t * tangentY; + [this.speedX, this.speedY] = [ + v1n_after * normalX + v1t * tangentX, + v1n_after * normalY + v1t * tangentY, + ]; + [other.speedX, other.speedY] = [ + v2n_after * normalX + v2t * tangentX, + v2n_after * normalY + v2t * tangentY, + ]; + + this.separateOverlap(other, normalX, normalY, distance); + } + separateOverlap(other, normalX, normalY, distance) { const overlap = this.radius + other.radius - distance; if (overlap > 0) { const separationX = normalX * overlap * 0.5; @@ -214,18 +242,15 @@ class WaveFunction { updateGrowth() { if (!this.isGrowing) return; - - const elapsedTime = Date.now() - this.growthStartTime; - const progress = Math.min(elapsedTime / this.growthDuration, 1); - - // Use easeOutQuad for smoother animation + const progress = Math.min( + (Date.now() - this.growthStartTime) / this.growthDuration, + 1, + ); const easeOutQuad = (t) => t * (2 - t); - const easedProgress = easeOutQuad(progress); - this.radius = this.growthStartRadius + - (this.growthTargetRadius - this.growthStartRadius) * easedProgress; - + (this.growthTargetRadius - this.growthStartRadius) * + easeOutQuad(progress); if (progress >= 1) { this.isGrowing = false; this.radius = this.growthTargetRadius; @@ -235,81 +260,52 @@ class WaveFunction { combine(other) { const newRadius = combinedAreaRadius(this.radius, other.radius); - const newSpeedX = (this.speedX + other.speedX) / 2; - const newSpeedY = (this.speedY + other.speedY) / 2; - + const [newSpeedX, newSpeedY] = [ + (this.speedX + other.speedX) / 2, + (this.speedY + other.speedY) / 2, + ]; if (this.mass > other.mass) { this.startGrowth(newRadius); - this.speedX = newSpeedX; - this.speedY = newSpeedY; + [this.speedX, this.speedY] = [newSpeedX, newSpeedY]; other.collapse(); other.markedForRemoval = true; } else { other.startGrowth(newRadius); - other.speedX = newSpeedX; - other.speedY = newSpeedY; + [other.speedX, other.speedY] = [newSpeedX, newSpeedY]; this.collapse(); this.markedForRemoval = true; } } } -const linkData = [ - { - name: 'LinkedIn', - link: 'https://www.linkedin.com/in/ryan-james-hill/', - color: '#0077B5', - }, - { - name: 'GitHub', - link: 'https://github.com/ryanhill1', - color: '#171515', - }, - { - name: 'Stack\nExchange', - link: 'https://quantumcomputing.stackexchange.com/users/13991/ryanhill1?tab=profile', - color: '#F48024', - }, - { - name: 'CV', - link: '/files/Ryan-Hill-CV.pdf', - color: '#C0C0C0', - }, -]; - -linkData.forEach(({ name, link, color }) => { - waveFunctions.push(new WaveFunction(true, name, link, color)); -}); +function initializeWaveFunctions() { + LINK_DATA.forEach(({ name, link, color }) => { + waveFunctions.push(new WaveFunction(true, name, link, color)); + }); +} -setInterval(() => { +function addNewWaveFunction() { if (waveFunctions.length <= MAX_WAVE_FUNCTIONS) { waveFunctions.push(new WaveFunction()); } -}, 2000); - -const FIXED_TIME_STEP = 1000 / 60; // 60 FPS -let lastTime = 0; -let frameCount = 0; +} function checkAndSeparateOverlaps() { for (let i = 0; i < waveFunctions.length; i++) { for (let j = i + 1; j < waveFunctions.length; j++) { - const wf1 = waveFunctions[i]; - const wf2 = waveFunctions[j]; - const dx = wf2.x - wf1.x; - const dy = wf2.y - wf1.y; - const distance = Math.sqrt(dx * dx + dy * dy); + const [wf1, wf2] = [waveFunctions[i], waveFunctions[j]]; + const [dx, dy] = [wf2.x - wf1.x, wf2.y - wf1.y]; + const distance = Math.hypot(dx, dy); const overlap = wf1.radius + wf2.radius - distance; - if (overlap > 0) { - const separationX = (dx / distance) * overlap * 0.5; - const separationY = (dy / distance) * overlap * 0.5; + const [separationX, separationY] = [ + (dx / distance) * overlap * 0.5, + (dy / distance) * overlap * 0.5, + ]; wf1.x -= separationX; wf1.y -= separationY; wf2.x += separationX; wf2.y += separationY; - - // Add a small random velocity to help separate stuck wavefunctions if (!wf1.isLink) { wf1.speedX += (Math.random() - 0.5) * 0.5; wf1.speedY += (Math.random() - 0.5) * 0.5; @@ -326,54 +322,45 @@ function checkAndSeparateOverlaps() { function animate(currentTime) { requestAnimationFrame(animate); const deltaTime = (currentTime - lastTime) / 1000; - if (deltaTime >= FIXED_TIME_STEP / 1000) { ctx.clearRect(0, 0, canvas.width, canvas.height); - - for (let i = 0; i < waveFunctions.length; i++) { - waveFunctions[i].updateGrowth(); - - for (let j = i + 1; j < waveFunctions.length; j++) { - if (waveFunctions[i].checkCollision(waveFunctions[j])) { - if ( - waveFunctions.length >= MAX_WAVE_FUNCTIONS && - !waveFunctions[i].isLink && - !waveFunctions[j].isLink - ) { - waveFunctions[i].combine(waveFunctions[j]); - } else { - waveFunctions[i].resolveCollision(waveFunctions[j]); - } - } - } - } - - waveFunctions.forEach((wf) => { - wf.update(deltaTime); - wf.draw(); - }); - + updateWaveFunctions(deltaTime); waveFunctions = waveFunctions.filter((wf) => !wf.markedForRemoval); lastTime = currentTime; } - if (frameCount % 60 === 0) { checkAndSeparateOverlaps(); } - frameCount++; } -animate(0); +function updateWaveFunctions(deltaTime) { + for (let i = 0; i < waveFunctions.length; i++) { + waveFunctions[i].update(deltaTime); + for (let j = i + 1; j < waveFunctions.length; j++) { + if (waveFunctions[i].checkCollision(waveFunctions[j])) { + if ( + !waveFunctions[i].isLink && + !waveFunctions[j].isLink && + (waveFunctions.length >= MAX_WAVE_FUNCTIONS || Math.random() < 0.05) + ) { + waveFunctions[i].combine(waveFunctions[j]); + } else { + waveFunctions[i].resolveCollision(waveFunctions[j]); + } + } + } + waveFunctions[i].draw(); + } +} -canvas.addEventListener('click', function (event) { +function handleCanvasClick(event) { const rect = canvas.getBoundingClientRect(); const clickX = event.clientX - rect.left; const clickY = event.clientY - rect.top; for (let wf of waveFunctions) { - const distance = Math.hypot(wf.x - clickX, wf.y - clickY); - if (distance < wf.radius) { + if (Math.hypot(wf.x - clickX, wf.y - clickY) < wf.radius) { if (wf.isLink) { window.open(wf.url, '_blank'); } else if (!wf.isCollapsing) { @@ -383,7 +370,25 @@ canvas.addEventListener('click', function (event) { } } - if (waveFunctions.length <= linkData.length + 1) { + if (waveFunctions.length <= LINK_DATA.length + 1) { waveFunctions.push(new WaveFunction()); } -}); +} + +function handleCollapseAll() { + waveFunctions.forEach((wf) => { + wf.collapse(); + }); +} + +function init() { + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + canvas.addEventListener('click', handleCanvasClick); + collapseAllButton.addEventListener('click', handleCollapseAll); + initializeWaveFunctions(); + setInterval(addNewWaveFunction, 2000); + animate(0); +} + +init(); diff --git a/index.html b/index.html index 25d7196..ac1d1b4 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,13 @@

Ryan Hill

Try not to let too many wavefunctions crowd the content.

+ + From fb1991e9b1c1c202e27b88d76a7d7d5375ff8df7 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Fri, 27 Dec 2024 17:41:58 -0600 Subject: [PATCH 5/6] docs: update readme for linters --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bf9c514..a2f5c2b 100644 --- a/README.md +++ b/README.md @@ -38,18 +38,40 @@ This will start a local server and automatically open your default web browser a ## Formatting Code +## JavaScript, HTML, and CSS + To format all project files (HTML, CSS, JS), run: ```bash npm run format ``` -You can also check the formatting without making changes using: +To check formatting without making changes: ```bash npm run format:check ``` +## Python + +For Python files, we use `tox` to manage formatting and linting. First, install tox: + +```bash +pip install tox +``` + +To format Python files, run: + +```bash +tox -e linters +``` + +To check Python formatting without making changes: + +```bash +tox -e format-check +``` + ## Commit Messages [Commitlint](https://github.com/conventional-changelog/commitlint/#what-is-commitlint) supported commit subjects list: From 21f4df6afec6619e567a2cfbcd11e9bef7fe7a56 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Fri, 27 Dec 2024 17:45:34 -0600 Subject: [PATCH 6/6] style: fix readme layout --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a2f5c2b..74cda4e 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ This will start a local server and automatically open your default web browser a ## Formatting Code -## JavaScript, HTML, and CSS +### JavaScript, HTML, and CSS To format all project files (HTML, CSS, JS), run: @@ -52,7 +52,7 @@ To check formatting without making changes: npm run format:check ``` -## Python +### Python For Python files, we use `tox` to manage formatting and linting. First, install tox: