<!DOCTYPE html>
<!--
Testing grok for a quick win98 starfield animation canvas in html~
Only slightly finetuned after, fixing window resize etc
Twily 2025
twily.info
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Starfield Animation</title>
<style>
html,body { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; background-color: transparent; }
*:focus { outline: none; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="starfield"></canvas>
<script>
const canvas = document.getElementById('starfield');
const ctx = canvas.getContext('2d');
// Starfield parameters
var numStars = 2500; // Number of stars
var speed = 0.25; // Speed of movement forward
var focalLength = 45; // Perspective scale (adjust for field of view)
var maxZ = 100; // Maximum depth
var minZ = 0.1; // Minimum depth before reset
var brightnessPower = 20; // Power for non-linear brightness (higher = quicker fade-in to full brightness)
var reverse = false; // Toggle for reverse direction (moving backward)
// Toggle reverse on 'r' key press
window.addEventListener('keydown', (e) => {
if (e.key === 'r') {
reverse = !reverse;
}
});
var centerX;
var centerY;
function window_resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
centerX = canvas.width / 2;
centerY = canvas.height / 2;
}
// Handle resize
window.addEventListener('resize', () => {
window_resize();
});
window_resize(); // first load
// Star class
class Star {
constructor() {
this.reset(true); // Initial reset with random z
}
reset(initial = false) {
this.x = (Math.random() - 0.5) * canvas.width * 2; // Wide spread
this.y = (Math.random() - 0.5) * canvas.height * 2;
if (initial) {
this.z = minZ + Math.random() * (maxZ - minZ); // Avoid z=0 initially
} else {
this.z = reverse ? minZ : maxZ;
}
}
update() {
const deltaZ = reverse ? speed : -speed;
this.z += deltaZ;
if ((!reverse && this.z <= minZ) || (reverse && this.z >= maxZ)) {
this.reset();
}
}
draw() {
const scale = focalLength / this.z;
const screenX = this.x * scale + centerX;
const screenY = this.y * scale + centerY;
// Only draw if within canvas bounds
if (screenX >= 0 && screenX <= canvas.width && screenY >= 0 && screenY <= canvas.height) {
const normZ = this.z / maxZ;
const brightness = 1 - normZ; // Closer stars brighter
//const brightness = 1 - Math.pow(normZ, brightnessPower); // Non-linear: quick to full brightness
ctx.fillStyle = `rgba(255, 255, 255, ${brightness})`;
//const size = brightness * 2 + 1; // Size based on distance
const size = brightness * .5 + 1; // Size based on distance
ctx.beginPath();
ctx.arc(screenX, screenY, size, 0, Math.PI * 2);
ctx.fill();
}
}
}
// Initialize stars
const stars = [];
for (let i = 0; i < numStars; i++) {
stars.push(new Star());
}
// Animation loop
var fps=1000/60; // 60 fps = 1000/60 ms
var saT;
function animate() {
clearTimeout(saT);
//ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; // Trail effect for smoothness
//ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = 'transparent';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Update center in case of resize
const currentCenterX = canvas.width / 2;
const currentCenterY = canvas.height / 2;
stars.forEach(star => {
star.update();
star.draw();
});
//requestAnimationFrame(animate);
saT=setTimeout(function() { animate(); },fps);
}
animate();
window.addEventListener('message', (event) => {
//if (event.origin === 'https://links.analiestar.com') {
console.log('Message from parent:', event.data);
switch(event.data) {
case "new_dir":
reverse=!reverse;
break;
default:
}
//}
});
</script>
</body>
</html>
Top