Demo Scene Tribute - Day 13
Advanced LevelThe plasma effect is a classic demo scene visual created by combining multiple sine waves at different frequencies and phases. Each pixel's color is determined by summing these waves, creating organic, flowing patterns.
// Plasma uses ImageData for direct pixel manipulation
const plasmaBuffer = ctx.createImageData(width, height);
const data = plasmaBuffer.data;
const t = time * 0.02;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
// Combine multiple sine waves
let v = Math.sin(x * 0.02 + t); // Horizontal wave
v += Math.sin(y * 0.02 + t * 0.7); // Vertical wave
v += Math.sin((x + y) * 0.015 + t * 0.5); // Diagonal wave
v += Math.sin(Math.sqrt(x*x + y*y) * 0.02 + t); // Radial wave
v = v / 4; // Normalize to -1..1
// Color cycling with phase offsets
data[i] = Math.sin(v * Math.PI + t) * 127 + 128; // Red
data[i + 1] = Math.sin(v * Math.PI + t + 2) * 127 + 128; // Green
data[i + 2] = Math.sin(v * Math.PI + t + 4) * 127 + 128; // Blue
data[i + 3] = 255; // Alpha
}
}
ctx.putImageData(plasmaBuffer, 0, 0);
For per-pixel effects like plasma, using putImageData() is much faster than drawing thousands of individual rectangles. We manipulate the pixel buffer directly, then blast it to the canvas in one operation.
Plasma was one of the first "impossible" effects on home computers. The Amiga demoscene popularized it in the late 80s using the Copper chip for color cycling.
Copper bars simulate the Amiga's Copper co-processor, which could change colors mid-scanline. We create smooth horizontal gradient bars that wave up and down using sine functions.
// Multiple copper bars with sine wave motion
const numBars = 12;
for (let i = 0; i < numBars; i++) {
// Each bar oscillates at a different phase
const barY = height/2 + Math.sin(time * 0.03 + i * 0.5) * 150;
const barHeight = 20 + Math.sin(time * 0.05 + i) * 10;
// Copper gradient: dark -> bright -> dark (raster bar look)
const gradient = ctx.createLinearGradient(
0, barY - barHeight/2,
0, barY + barHeight/2
);
const hue = (time * 2 + i * 30) % 360;
gradient.addColorStop(0, `hsl(${hue}, 100%, 20%)`);
gradient.addColorStop(0.3, `hsl(${hue}, 100%, 60%)`);
gradient.addColorStop(0.5, `hsl(${hue}, 100%, 90%)`); // Hotspot
gradient.addColorStop(0.7, `hsl(${hue}, 100%, 60%)`);
gradient.addColorStop(1, `hsl(${hue}, 100%, 20%)`);
ctx.fillStyle = gradient;
ctx.fillRect(0, barY - barHeight/2, width, barHeight);
}
The symmetric gradient creates the classic "raster bar" look - dark edges with a bright center that simulates light reflecting off a 3D metallic bar.
The 3D starfield uses perspective projection to create the illusion of flying through space. Stars move toward the viewer, getting larger and brighter as they approach.
// Initialize stars in 3D space
const stars = [];
for (let i = 0; i < 300; i++) {
stars.push({
x: (Math.random() - 0.5) * 2000, // World X
y: (Math.random() - 0.5) * 2000, // World Y
z: Math.random() * 1000 + 100 // Depth
});
}
// Update and project each star
stars.forEach(star => {
// Move toward viewer
star.z -= 5;
// Wrap around when too close
if (star.z < 1) {
star.z = 1000;
star.x = (Math.random() - 0.5) * 2000;
star.y = (Math.random() - 0.5) * 2000;
}
// Perspective projection: scale by 1/z
const scale = 300 / star.z;
const screenX = centerX + star.x * scale;
const screenY = centerY + star.y * scale;
// Size and brightness based on depth
const brightness = (1000 - star.z) / 4;
const size = 4 - star.z / 300;
});
The trail effect is achieved by drawing a gradient line from the star's current position toward where it came from. This creates the streaking effect seen in hyperspace sequences.
The formula screenX = worldX / z is the core of all 3D projection. Dividing by Z makes distant objects appear smaller - the foundation of computer graphics.
The rotating cube demonstrates 3D rotation matrices and perspective projection. We define 8 vertices and 12 edges, then rotate and project them each frame.
// Cube vertices (normalized -1 to 1)
const vertices = [
[-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1],
[-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1]
];
// Rotation angles (different speeds for each axis)
const angleX = time * 0.02;
const angleY = time * 0.03;
const angleZ = time * 0.015;
// Transform each vertex
vertices.map(v => {
let [x, y, z] = v;
// Rotate around X axis
let temp = y;
y = y * Math.cos(angleX) - z * Math.sin(angleX);
z = temp * Math.sin(angleX) + z * Math.cos(angleX);
// Rotate around Y axis
temp = x;
x = x * Math.cos(angleY) + z * Math.sin(angleY);
z = -temp * Math.sin(angleY) + z * Math.cos(angleY);
// Rotate around Z axis
temp = x;
x = x * Math.cos(angleZ) - y * Math.sin(angleZ);
y = temp * Math.sin(angleZ) + y * Math.cos(angleZ);
// Perspective projection
const fov = 4;
const scale = fov / (z + 3);
return {
x: centerX + x * 100 * scale,
y: centerY + y * 100 * scale
};
});
Each rotation uses the 2D rotation formula applied to two axes at a time. X rotation affects Y and Z, Y rotation affects X and Z, and Z rotation affects X and Y.
The sine scroller is an iconic demo effect where text waves up and down while scrolling horizontally. Each character is positioned independently based on its X position and time.
const scrollText = "WELCOME TO THE DEMO SCENE...";
let scrollOffset = 0; // Increases each frame
const charWidth = 32;
// Calculate visible character range
const visibleChars = Math.ceil(width / charWidth) + 2;
const startChar = Math.floor(scrollOffset / charWidth);
for (let i = 0; i < visibleChars; i++) {
const charIndex = (startChar + i) % scrollText.length;
const char = scrollText[charIndex];
// X position with smooth sub-pixel scrolling
const screenX = i * charWidth - (scrollOffset % charWidth);
// Y position from sine wave
const sineOffset = Math.sin((screenX + time * 3) * 0.02) * 60;
const screenY = height / 2 + sineOffset;
// Rainbow color per character
const hue = (time * 2 + screenX * 0.5) % 360;
ctx.fillStyle = `hsl(${hue}, 100%, 70%)`;
ctx.fillText(char, screenX, screenY);
}
scrollOffset += 3; // Scroll speed
The magic is in sin((screenX + time) * frequency) * amplitude. The screenX component makes the wave travel across the text, while time makes it animate.
Using scrollOffset % charWidth gives us sub-pixel precision for smooth scrolling without visible "jumping" between characters.
The tunnel creates a hypnotic infinite corridor effect by drawing concentric rings that shrink toward a center point, with warping and rotation applied.
const centerX = width / 2;
const centerY = height / 2;
const t = time * 0.03;
// Draw rings from outside to inside
for (let ring = 50; ring > 0; ring--) {
const depth = ring / 50; // 0 = closest, 1 = farthest
const radius = 300 * depth;
// Tunnel swaying motion
const offsetX = Math.sin(t + depth * 2) * 50 * (1 - depth);
const offsetY = Math.cos(t * 0.7 + depth * 2) * 50 * (1 - depth);
// Color based on depth and time
const hue = (time * 3 + ring * 5) % 360;
// Draw warped circular ring
ctx.beginPath();
for (let angle = 0; angle < Math.PI * 2; angle += 0.1) {
// Warping factor makes it non-circular
const warp = 1 + Math.sin(angle * 4 + t) * 0.2 * (1 - depth);
const px = centerX + offsetX + Math.cos(angle + t * depth) * radius * warp;
const py = centerY + offsetY + Math.sin(angle + t * depth) * radius * warp;
if (angle === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.stroke();
}
The key to the tunnel illusion is that inner rings (closer to viewer) have more warping and offset motion, while outer rings (farther away) are more stable and circular.
These effects were revolutionary in the 80s-90s when groups like Future Crew, Fairlight, and Sanity pushed hardware to its limits. Today we recreate them with modern web tech!