DEV Community

Andrés Villarreal
Andrés Villarreal

Posted on • Originally published at kaeruct.github.io on

Starfield visualization in JavaScript

Starfield Visualization

This is a simple, straightforward implementation of a visualization reminiscent of the classic Windows 95 starfield screensaver.

It is also interactive: you can touch the screen or use the accelerometer to influence the direction of the movement.

This is how it works:

  • Create a bunch of particles (100), each in a random position.
  • Every frame, move each particle further away from the center*. The further the particle is from the center, the more visible it will become. This gives the illusion that the particles are moving closer to the viewer, or that the viewer is going further into space.
  • The center is not really the center of the screen, but a variable point which can be influenced by the user by moving their cursor or tilting their device.
  • When the particles go outside of the view, put them near the center again, this keeps the visualization going on in perpetuity.

In this blog post, I want to share the heavily-commented source code to demonstrate how simple it is to create visually appealing animations with a few lines of code and basic math knowledge.

Please click here to see the visualization in action!

The code is available in this Gist:

<!DOCTYPE html>
<head>
<style>
/** All of these styles are to ensure the canvas takes up the whole screen, and that the orig canvas is not visible */
body,html{font-family:sans-serif;color:#aaa;background:#000;margin:0;padding:0;height: 100%;}
#wrap{display: flex; height: 100%; align-items: center; justify-content: center;}
#orig{position: absolute; top: -99999px; left: -99999px;}
#debug{position: absolute;}
</style>
<meta name=viewport content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
</head>
<body>
<div id=debug></div>
<div id=wrap>
<canvas id=orig></canvas> <!-- This is the canvas element that the animation is rendered on. It is hidden from view. -->
<canvas id=scaled></canvas> <!-- This is the displayed canvas element. We will scale and copy the contents from the orig scaled. -->
</div>
<script>
const $ = window // Shortcut to reference the global window object
e = document.documentElement, // Shortcut to reference the root element (html)
b = document.getElementsByTagName('body')[0], // Shortcut to reference the body element
orig = document.getElementById('orig'), // Shortcut for the original canvas element
scaled = document.getElementById('scaled'), // Shortcut for the scale canvas element
ctx = orig.getContext('2d'), // Shortcut for the original canvas 2d context
scaledCtx = scaled.getContext('2d'), // Shortcut for the scaled canvas 2d context
particles = [], // Array to hold the particle objects
c = { x: 0, y: 0 }; // Object to hold the current position of the cursor
// Whether debug is enabled. You can enable it by adding #debug to the URL.
const debug = window.location.hash.includes("debug");
let shouldClear = true, // Whether the canvas should be cleared each frame
w = 0, // Width of the orig canvas
h = 0, // Height of the orig canvas
maxd = 0, // Maximum distance from the center of the canvas -- at this distance, the alpha value of the particle will be 1. Particles will be more transparent, the closer they are to the center.
speed = 0, // Speed of the particles
t = 0; // Time variable, will increase each frame
/**
* Utility function to clamp a value between a minimum and maximum value.
* This means that the value will always be within the range of min and max.
*/
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
/**
* Calculates the distance between two points.
*/
function dist(p1, p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
/**
* Scales the width and height of the canvas to fit within the maximum width and height.
* The aspect ratio is maintained.
*/
function fit(w, h, mw, mh) {
const ratio = Math.min(mw / w, mh / h);
const rw = w * ratio;
const rh = h * ratio;
if (rw < w || rh < h) {
rw = w;
rh = h * ratio;
}
return [rw, rh];
}
/**
* Generates a random number between min and max.
*/
function randRange(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
/**
* Resizes the canvas to fit the screen.
* This will be called whenever the window is resized or the orientation changes.
*/
function resize() {
// Check if the window is in landscape orientation and set the width and height accordingly.
// These are the real dimensions of the canvas, before scaling.
if (window.innerWidth > window.innerHeight) {
// Landscape mode
w = 256;
h = 144;
} else {
// Portrait mode
w = 144;
h = 256;
}
// Calculate the maximum distance, using half the diagonal of the canvas.
maxd = dist({ x: 0, y: 0 }, { x: w, y: h }) / 2;
// Set the cursor position to the center of the canvas.
c.x = w / 2;
c.y = h / 2;
// Set the width and height of the orig canvas.
orig.width = w;
orig.height = h;
// Set the width and height of the scaled canvas, before fitting to the aspect ratio of the orig canvas.
rw = $.innerWidth || e.clientWidth || b.clientWidth;
rh = $.innerHeight || e.clientHeight || b.clientHeight;
// Fit the scaled canvas to the aspect ratio of the orig canvas.
const d = fit(w, h, rw, rh);
scaled.width = d[0];
scaled.height = d[1];
rw = scaled.width;
rh = scaled.height;
// Disable image smoothing for both canvases to get a pixelated look.
ctx.imageSmoothingEnabled = false;
scaledCtx.imageSmoothingEnabled = false;
}
/**
* Event handler for the devicemotion event.
* This will be called whenever the device is moved.
*
* It will adjust the current position of the cursor depending on the device orientation.
*/
function devicemotion(e) {
// The calculation below is to ensure that the cursor position is within the bounds of the canvas.
c.x = w / 2 + w * clamp(e.accelerationIncludingGravity.x, -10, 10) / 20;
c.y = h * clamp(e.accelerationIncludingGravity.y, -10, 10) / 20;
}
/**
* Event handler for the mousemove event.
* This will be called whenever the mouse is moved.
*
* It will set the current position of the cursor to the position of the mouse.
*/
function mousemove(e) {
c.x = (e.clientX - e.currentTarget.offsetLeft) / rw * w;
c.y = (e.clientY - e.currentTarget.offsetTop) / rh * h;
}
/**
* Event handler for the click event.
* It will toggle the shouldClear flag, which determines whether the canvas should be cleared each frame.
*/
function click() {
shouldClear = !shouldClear;
}
/**
* Initializes the particles array. This will create 100 particles with random positions.
*/
function initParticles() {
particles.length = 0;
for (let i = 0; i < 100; i++) {
particles.push({ x: randRange(0, w), y: randRange(0, h) });
}
}
/**
* The main animation loop. This will be called every frame.
*/
function frame() {
if (shouldClear) {
// Clear the canvas if shouldClear is true.
// If not, the previous frame will be visible, creating a trail effect for the particles.
ctx.fillStyle = '#012';
ctx.fillRect(0, 0, w, h);
}
// Modify the speed of particles. By using sine, we can create a smooth oscillation effect.
speed += Math.sin(t) * 0.01;
// Loop through all the particles, render them, and update their position.
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
const a = dist(p, c) / maxd; // Calculate the alpha value of the particle based on distance from the center.
// Set the color based on the calculated alpha value and draw the particle as a rectangle.
ctx.fillStyle = "rgba(255, 255, 255, " + a + ")";
ctx.fillRect(p.x, p.y, 1, 1);
// Update the position of the particle based on the cursor position and speed.
p.x -= (c.x - p.x) * speed;
p.y -= (c.y - p.y) * speed;
// If the particle is outside the canvas, reset its position to a random location near the cursor.
if (p.y <= 0 || p.y > h) p.y = c.y + randRange(-h * 0.1, h * 0.1);
if (p.x <= 0 || p.x > w) p.x = c.x + randRange(-w * 0.1, w * 0.1);
}
// Copy the contents of the orig canvas to the scaled canvas.
scaledCtx.drawImage(orig, 0, 0, w, h, 0, 0, rw, rh);
t++; // Increase the time variable
if (debug) {
// If debug is enabled, display the cursor position
document.getElementById('debug').textContent = `${c.x},${c.y}`;
}
// Request the browser to call our frame rendering function again on the next frame.
$.requestAnimationFrame(frame);
}
// Add event listeners to the scaled canvas
scaled.addEventListener('mousemove', mousemove);
scaled.addEventListener('click', click);
// Add event listeners to the window
$.addEventListener('resize', resize);
$.addEventListener('orientationchange', resize);
$.addEventListener('devicemotion', devicemotion, true);
// Call the resize function to initialize the canvas.
resize();
// Initialize the particles array.
initParticles();
// Start the animation loop by calling the frame rendering function.
frame();
</script>
</body>
</html>
view raw starfield.html hosted with ❤ by GitHub

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

The Most Contextual AI Development Assistant

Pieces.app image

Our centralized storage agent works on-device, unifying various developer tools to proactively capture and enrich useful materials, streamline collaboration, and solve complex problems through a contextual understanding of your unique workflow.

👥 Ideal for solo developers, teams, and cross-company projects

Learn more