Created
June 4, 2025 03:50
-
-
Save jmikedupont2/1f9cbf3703405fb00fc5e1765dcb2d13 to your computer and use it in GitHub Desktop.
primespiral
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Prime Number Polygons</title> | |
<style> | |
body { | |
margin: 0; | |
padding: 20px; | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
font-family: Arial, sans-serif; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
min-height: 100vh; | |
} | |
h1 { | |
color: white; | |
text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
margin-bottom: 10px; | |
} | |
.subtitle { | |
color: rgba(255,255,255,0.8); | |
margin-bottom: 20px; | |
text-align: center; | |
} | |
canvas { | |
background: rgba(255,255,255,0.1); | |
border-radius: 15px; | |
backdrop-filter: blur(10px); | |
box-shadow: 0 8px 32px rgba(0,0,0,0.3); | |
border: 1px solid rgba(255,255,255,0.2); | |
} | |
.controls { | |
margin-top: 20px; | |
display: flex; | |
gap: 15px; | |
align-items: center; | |
flex-wrap: wrap; | |
justify-content: center; | |
} | |
button { | |
padding: 10px 20px; | |
background: rgba(255,255,255,0.2); | |
border: 1px solid rgba(255,255,255,0.3); | |
border-radius: 25px; | |
color: white; | |
cursor: pointer; | |
backdrop-filter: blur(10px); | |
transition: all 0.3s ease; | |
} | |
button:hover { | |
background: rgba(255,255,255,0.3); | |
transform: translateY(-2px); | |
} | |
.speed-control { | |
color: white; | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
} | |
input[type="range"] { | |
accent-color: #fff; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Prime Number Polygons</h1> | |
<div class="subtitle">Visualizing primes [2, 3, 5, 7, 11] as rotating polygons</div> | |
<canvas id="polygonCanvas" width="800" height="600"></canvas> | |
<div class="controls"> | |
<button onclick="toggleAnimation()">Pause/Resume</button> | |
<button onclick="resetAnimation()">Reset</button> | |
<div class="speed-control"> | |
<label>Speed:</label> | |
<input type="range" id="speedSlider" min="0.1" max="3" step="0.1" value="1"> | |
</div> | |
<button onclick="toggleComplexity()">Toggle Complexity</button> | |
</div> | |
<script> | |
const canvas = document.getElementById('polygonCanvas'); | |
const ctx = canvas.getContext('2d'); | |
const primes = [2, 3, 5, 7, 11]; | |
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57']; | |
let animationId; | |
let isAnimating = true; | |
let rotationSpeed = 1; | |
let showComplex = true; | |
let time = 0; | |
// Trail system | |
const trails = primes.map(() => []); | |
const maxTrailLength = 50; | |
function drawPolygon(centerX, centerY, sides, radius, rotation, color, scale = 1, depth = 0) { | |
ctx.save(); | |
ctx.translate(centerX, centerY); | |
ctx.rotate(rotation); | |
ctx.scale(scale, scale); | |
ctx.beginPath(); | |
const angleStep = (2 * Math.PI) / sides; | |
const vertices = []; | |
for (let i = 0; i <= sides; i++) { | |
const angle = i * angleStep; | |
const x = radius * Math.cos(angle); | |
const y = radius * Math.sin(angle); | |
if (i < sides) { | |
vertices.push({x, y, angle}); | |
} | |
if (i === 0) { | |
ctx.moveTo(x, y); | |
} else { | |
ctx.lineTo(x, y); | |
} | |
} | |
// Gradient fill | |
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radius); | |
gradient.addColorStop(0, color + '80'); | |
gradient.addColorStop(1, color + '20'); | |
ctx.fillStyle = gradient; | |
ctx.fill(); | |
// Stroke | |
ctx.strokeStyle = color; | |
ctx.lineWidth = Math.max(1, 3 - depth); | |
ctx.stroke(); | |
ctx.restore(); | |
// Draw smaller polygons at each vertex (only if complex mode is enabled) | |
if (showComplex && depth < 2) { | |
vertices.forEach((vertex, vertexIndex) => { | |
const worldX = centerX + Math.cos(rotation) * vertex.x * scale - Math.sin(rotation) * vertex.y * scale; | |
const worldY = centerY + Math.sin(rotation) * vertex.x * scale + Math.cos(rotation) * vertex.y * scale; | |
primes.forEach((childPrime, childIndex) => { | |
const angleOffset = (childIndex / primes.length) * 2 * Math.PI; | |
const childRadius = radius * (depth === 0 ? 0.25 : 0.15); | |
const childScale = scale * (depth === 0 ? 0.35 : 0.25); | |
const orbitRadius = radius * (depth === 0 ? 0.7 : 0.5); | |
const childX = worldX + Math.cos(time * 0.02 + angleOffset + vertexIndex * 0.5) * orbitRadius * scale; | |
const childY = worldY + Math.sin(time * 0.02 + angleOffset + vertexIndex * 0.5) * orbitRadius * scale; | |
const childRotation = time * 0.03 * rotationSpeed * (childPrime / 4) + vertex.angle; | |
drawPolygon(childX, childY, childPrime, childRadius, childRotation, colors[childIndex], childScale, depth + 1); | |
}); | |
}); | |
} | |
} | |
function drawTrail(trail, color) { | |
if (trail.length < 2) return; | |
ctx.strokeStyle = color + '40'; | |
ctx.lineWidth = 1; | |
ctx.beginPath(); | |
for (let i = 0; i < trail.length - 1; i++) { | |
const alpha = i / trail.length; | |
ctx.globalAlpha = alpha * 0.5; | |
if (i === 0) { | |
ctx.moveTo(trail[i].x, trail[i].y); | |
} else { | |
ctx.lineTo(trail[i].x, trail[i].y); | |
} | |
} | |
ctx.stroke(); | |
ctx.globalAlpha = 1; | |
} | |
function animate() { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
const centerX = canvas.width / 2; | |
const centerY = canvas.height / 2; | |
primes.forEach((prime, index) => { | |
// Calculate position in circular arrangement | |
const angleOffset = (index / primes.length) * 2 * Math.PI; | |
const orbitRadius = 150; | |
const baseRadius = 30 + prime * 8; | |
// Create complex motion | |
const x = centerX + Math.cos(time * 0.01 + angleOffset) * orbitRadius; | |
const y = centerY + Math.sin(time * 0.01 + angleOffset) * orbitRadius; | |
// Rotation based on prime number | |
const rotation = time * 0.02 * rotationSpeed * (prime / 5); | |
// Pulsing scale effect | |
const scale = 0.8 + 0.3 * Math.sin(time * 0.05 + prime); | |
drawPolygon(x, y, prime, baseRadius, rotation, colors[index], scale, 0); | |
// Draw prime number label (only on main polygons) | |
if (baseRadius > 20) { | |
ctx.fillStyle = 'white'; | |
ctx.font = 'bold 16px Arial'; | |
ctx.textAlign = 'center'; | |
ctx.fillText(prime.toString(), x, y + 5); | |
} | |
}); | |
// Draw connecting lines between main polygons | |
if (!showComplex) { | |
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; | |
ctx.lineWidth = 1; | |
ctx.beginPath(); | |
for (let i = 0; i < primes.length; i++) { | |
const angleOffset1 = (i / primes.length) * 2 * Math.PI; | |
const x1 = centerX + Math.cos(time * 0.01 + angleOffset1) * 150; | |
const y1 = centerY + Math.sin(time * 0.01 + angleOffset1) * 150; | |
for (let j = i + 1; j < primes.length; j++) { | |
const angleOffset2 = (j / primes.length) * 2 * Math.PI; | |
const x2 = centerX + Math.cos(time * 0.01 + angleOffset2) * 150; | |
const y2 = centerY + Math.sin(time * 0.01 + angleOffset2) * 150; | |
ctx.moveTo(x1, y1); | |
ctx.lineTo(x2, y2); | |
} | |
} | |
ctx.stroke(); | |
} | |
time += 1; | |
if (isAnimating) { | |
animationId = requestAnimationFrame(animate); | |
} | |
} | |
function toggleAnimation() { | |
isAnimating = !isAnimating; | |
if (isAnimating) { | |
animate(); | |
} | |
} | |
function resetAnimation() { | |
time = 0; | |
trails.forEach(trail => trail.length = 0); | |
if (!isAnimating) { | |
ctx.clearRect(0, 0, canvas.width, canvas.width); | |
animate(); | |
} | |
} | |
function toggleTrails() { | |
showTrails = !showTrails; | |
if (!showTrails) { | |
trails.forEach(trail => trail.length = 0); | |
} | |
} | |
// Speed control | |
document.getElementById('speedSlider').addEventListener('input', (e) => { | |
rotationSpeed = parseFloat(e.target.value); | |
}); | |
// Start animation | |
animate(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment