Synthwave sunset · laguna-s-2.1
Create an animated synthwave sunset: a glowing gradient sun with scanlines setting over an infinite neon grid that scrolls toward the viewer, with a starfield and a reflected horizon.
transcript · 11 messages
Create an animated synthwave sunset: a glowing gradient sun with scanlines setting over an infinite neon grid that scrolls toward the viewer, with a starfield and a reflected horizon.
thinking
The user wants me to create a single self-contained HTML file with an animated synthwave sunset scene. Let me break down the requirements:
1. A glowing gradient sun
2. Scanlines (the classic VHS/synthwave scanline effect)
3. An infinite neon grid that scrolls toward the viewer
4. A starfield in the background
5. A reflected horizon
I need to create this all in one HTML file with CSS and JS inline. No external resources.
Let me think about how to implement each effect:
1. **Glowing gradient sun**: A radial gradient circle with a glow effect, positioned in the sky. Maybe with some pulsing animation.
2. **Scanlines**: A CSS overlay with repeating-linear-gradient to create horizontal lines, possibly with some animation for the scanline sweep effect.
3. **Infinite neon grid**: This is the classic "grid moving toward viewer" effect. I can create this with a canvas or CSS. Using canvas would give more control. The grid starts narrow at the horizon and spreads out as it comes toward the viewer. The lines should be neon-colored (cyan/magenta).
4. **Starfield**: Random small dots in the background, possibly with twinkling animation.
5. **Reflected horizon**: A reflection of the sun and grid on the ground/water surface.
Let me think about the best approach. I could use HTML5 Canvas for the grid and starfield, or I could use pure CSS. Canvas gives more flexibility for the grid animation.
Actually, let me use a combination:
- Canvas for the starfield (static or twinkling dots)
- Canvas for the neon grid (animated, moving toward viewer)
- CSS for the sun (glowing gradient circle)
- CSS for scanlines
- CSS for the overall gradient background
Or I could do everything on canvas for better performance. Let me think...
Actually, let me use multiple canvas elements layered with CSS:
- Background canvas for starfield
- CSS for sun and sky gradient
- Canvas for grid
- CSS overlay for scanlines
- Canvas or CSS for reflection
Let me plan the layers from back to front:
1. Sky gradient background (CSS)
2. Starfield (canvas)
3. Sun (CSS with glow)
4. Neon grid (canvas) - this includes the horizon
5. Reflection (could be part of grid canvas or separate)
6. Scanlines (CSS overlay)
Let me write this out. I'll use canvas for the grid since it needs animation, and CSS for the rest.
Actually, for simplicity and a cohesive look, let me use one main canvas for everything that needs animation (grid + stars), and CSS for static elements (sky gradient, sun, scanlines).
Let me think about the grid effect more carefully. The classic synthwave grid:
- Lines converge to a vanishing point on the horizon
- Lines move upward (toward the viewer / away from horizon)
- The grid gets wider as it approaches the viewer
- Neon color (cyan/blue/pink)
For the grid animation:
- I'll draw lines from the horizon outward
- Animate by shifting the offset so lines appear to move toward the viewer
- Use a cyan/blue neon color with glow
For the reflection:
- Draw a flipped version of the grid below the horizon
- With reduced opacity and possibly distorted
Let me also add some elements like a horizon line and maybe some mountains or silhouettes for depth.
Let me code this up:
```html
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
overflow: hidden;
background: radial-gradient(circle at 50% 30%, #ff006e 0%, #1a002d 50%, #000 100%);
height: 100vh;
position: relative;
}
#starfield {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
#grid {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
}
.sun {
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
background: radial-gradient(circle, #ff006e 0%, #ff79c6 30%, #00b4d8 60%, transparent 80%);
border-radius: 50%;
filter: blur(2px);
box-shadow: 0 0 50px #ff006e, 0 0 100px #ff006e, 0 0 200px #00b4d8;
z-index: 3;
animation: pulse 4s infinite ease-in-out;
}
@keyframes pulse {
0%, 100% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.05); }
}
#scanlines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 2px,
transparent 2px,
transparent 4px
);
z-index: 10;
pointer-events: none;
}
.horizon {
position: absolute;
bottom: 50%;
left: 0;
width: 100%;
height: 2px;
background: #00b4d8;
box-shadow: 0 0 10px #00b4d8, 0 0 20px #00b4d8;
z-index: 5;
}
</style>
</head>
<body>
<canvas id="starfield"></canvas>
<canvas id="grid"></canvas>
<div class="sun"></div>
<div class="horizon"></div>
<div id="scanlines"></div>
<script>
// Starfield
const starCanvas = document.getElementById('starfield');
const starCtx = starCanvas.getContext('2d');
function resizeCanvas(canvas) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas(starCanvas);
const stars = [];
for (let i = 0; i < 200; i++) {
stars.push({
x: Math.random() * starCanvas.width,
y: Math.random() * starCanvas.height,
radius: Math.random() * 1.5,
opacity: Math.random() * 0.8 + 0.2,
twinkle: Math.random() * 0.02
});
}
function animateStars() {
starCtx.clearRect(0, 0, starCanvas.width, starCanvas.height);
for (const star of stars) {
star.opacity += (Math.random() - 0.5) * star.twinkle;
star.opacity = Math.max(0.1, Math.min(0.9, star.opacity));
starCtx.fillStyle = `rgba(255, 255, 255, ${star.opacity})`;
starCtx.beginPath();
starCtx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
starCtx.fill();
}
requestAnimationFrame(animateStars);
}
// Grid
const gridCanvas = document.getElementById('grid');
const gridCtx = gridCanvas.getContext('2d');
resizeCanvas(gridCanvas);
let gridOffset = 0;
function animateGrid() {
gridCtx.clearRect(0, 0, gridCanvas.width, gridCanvas.height);
const width = gridCanvas.width;
const height = gridCanvas.height;
const vanishingY = height / 2; // horizon line at 50% from top
// Grid properties
const gridDepth = 100; // how deep the grid goes
const spacing = 40; // initial line spacing
const neonColor = '#00ffff'; // cyan
const gridColor = '#00d8ff';
// Set composite for glow
gridCtx.shadowBlur = 10;
gridCtx.shadowColor = neonColor;
// Draw grid lines
for (let z = gridDepth; z > 0; z--) {
const perspective = z / gridDepth;
const y = vanishingY - (vanishingY * perspective);
// ... draw lines
}
// Actually, let me use a different approach
// Draw lines from horizon outward, with perspective
for (let i = 0; i < 30; i++) {
const depth = i + gridOffset;
const z = depth / 30;
const scale = z * 0.5;
const y = vanishingY - (vanishingY * (z % 1));
// ...
}
requestAnimationFrame(animateGrid);
}
animateStars();
animateGrid();
window.addEventListener('resize', () => {
resizeCanvas(starCanvas);
resizeCanvas(gridCanvas);
});
</script>
</body>
</html>
```
Let me think more carefully about the grid effect. The classic approach:
The grid consists of:
1. Horizontal lines (parallel to horizon) - these get closer together as they approach the viewer
2. Vertical lines (converging toward vanishing point) - actually in the classic 3D grid, both sets of lines converge
Actually, in the classic synthwave grid:
- There's one set of lines going from the horizon outward (like rings)
- There might be vertical lines converging to the horizon
Let me think about it differently. The grid is like a flat plane on the ground, viewed from above but slightly tilted toward the horizon. The lines on this plane:
- Lines perpendicular to the viewer (horizontal on the ground) - these are arcs that start as points at the horizon and widen as they come toward the viewer
- Lines parallel to the viewer's direction (radial from vanishing point) - these converge to the vanishing point
Let me use a simpler approach:
1. Calculate the position of each grid line based on perspective
2. Draw them with neon color and glow
3. Add a reflection below the horizon
For the grid animation, I'll animate by moving the offset so lines appear to move forward.
Let me refine the approach:
```javascript
function animateGrid() {
gridCtx.clearRect(0, 0, width, height);
const vanishingY = height / 2;
const gridDepth = 40;
// Draw grid lines (arcs from horizon)
for (let i = 0; i < gridDepth; i++) {
const z = i + gridOffset;
const perspective = z / gridDepth;
if (perspective > 1) continue;
const y = vanishingY + (height / 2) * perspective;
const widthAtY = width * (1 - perspective);
// Draw horizontal grid lines (these are actually curved in some versions)
// For a rectangular grid:
gridCtx.lineWidth = 1;
gridCtx.strokeStyle = neonColor;
// Left side lines
for (let j = 0; j < 5; j++) {
const x1 = (width / 2) - (widthAtY / 2) + (widthAtY / 5) * j;
const x2 = (width / 2) - (width / 2) + (width / 2) * (1 - perspective);
// ...
}
}
}
```
Actually, let me think about this more carefully and use a proper perspective grid.
The grid plane is on the z=0 plane (ground). The camera is looking at it from above.
For a 2D representation, I'll use a perspective projection where:
- The horizon line is at some y coordinate (let's say at the middle vertically, or slightly above)
- Grid lines further away are smaller and closer to the horizon
- Grid lines closer to the viewer are larger and further from the horizon
Let me use a simpler approach that looks good:
1. Draw horizontal lines (rings) that start at the horizon and get wider as they come toward the viewer
2. Draw vertical lines that converge to the horizon
Here's my refined approach:
```javascript
function animateGrid() {
const width = gridCanvas.width;
const height = gridCanvas.height;
const horizonY = height * 0.55; // slightly above middle
const gridSpacing = 30; // pixels between grid lines at the viewer
const gridDepth = 60;
gridCtx.clearRect(0, 0, width, height);
gridCtx.shadowBlur = 8;
gridCtx.shadowColor = '#00ffff';
gridCtx.strokeStyle = '#00ffff';
gridCtx.fillStyle = '#00ffff';
for (let i = 0; i < gridDepth; i++) {
const z = i + gridOffset; // distance from viewer
const perspective = z / gridDepth;
if (perspective > 1) continue;
// Calculate y position based on perspective
// y = horizonY - (horizonY - bottom) * (1 - perspective)
// Actually, the closer the grid is (smaller z), the closer to bottom
// The farther (larger z), the closer to horizon
const y = horizonY - (horizonY * perspective);
const gridWidth = width * (1 - perspective) + 100;
// Draw horizontal line
gridCtx.beginPath();
gridCtx.moveTo((width - gridWidth) / 2, y);
gridCtx.lineTo((width + gridWidth) / 2, y);
gridCtx.stroke();
}
}
```
Hmm, this isn't quite right. Let me think about the perspective more carefully.
When looking at a grid plane:
- The grid extends from the horizon (vanishing line) to the bottom of the screen (viewer position)
- At the horizon, the grid lines converge to a point
- At the bottom, the grid lines are at their full width
The standard perspective formula:
- Given a point at distance d from the viewer along the grid
- Its projected y position = horizonY + (d / (d + f)) * (viewerY - horizonY)
Let me just use a simple approach that looks good:
```javascript
const horizonY = height * 0.5;
const viewerDistance = 1; // viewer's distance from the grid plane (arbitrary units)
for (let i = 0; i < gridLines; i++) {
const gridZ = i * gridSpacing + gridOffset; // position on the grid plane
// Perspective projection
const scale = viewerDistance / (viewerDistance + gridZ);
const y = horizonY + (height - horizonY) * (1 - scale);
// Width at this y
const gridWidth = (width * scale * 0.8);
// Draw line across the screen at y with gridWidth
gridCtx.beginPath();
gridCtx.moveTo((width - gridWidth) / 2, y);
gridCtx.lineTo((width + gridWidth) / 2, y);
gridCtx.stroke();
}
```
Wait, I think I'm overcomplicating this. Let me look at it from a simpler angle.
The grid is a series of horizontal bands that:
1. Start wide at the bottom (near the viewer)
2. Get narrower as they approach the horizon
3. The animation moves the bands upward (toward the horizon), creating the illusion of moving forward
And optionally, there are radial lines (vertical grid lines) that converge to the horizon.
Let me use this simpler model:
```javascript
function animateGrid() {
const w = gridCanvas.width;
const h = gridCanvas.height;
const horizonY = h * 0.5; // horizon at middle
gridCtx.clearRect(0, 0, w, h);
gridCtx.shadowBlur = 15;
gridCtx.shadowColor = '#00ffff';
gridCtx.strokeStyle = '#00ffff';
gridCtx.lineWidth = 2;
const bands = 40;
const speed = 0.5;
for (let i = 0; i < bands; i++) {
// Position of this band, with offset for animation
const pos = (i + gridOffset) % bands;
const t = pos / bands; // 0 to 1
// Y position: 0 at horizon, 1 at bottom
const y = horizonY + (h - horizonY) * t;
// Scale: 0 at horizon, 1 at bottom
const scale = t;
// Width at this position
const bandWidth = w * scale * 1.2;
if (t < 0.02) continue; // skip bands too close to horizon
// Draw horizontal line (band)
gridCtx.beginPath();
gridCtx.moveTo((w - bandWidth) / 2, y);
gridCtx.lineTo((w + bandWidth) / 2, y);
gridCtx.stroke();
// Draw side lines (radial)
const leftX = (w - bandWidth) / 2;
const rightX = (w + bandWidth) / 2;
gridCtx.beginPath();
gridCtx.moveTo(leftX, y);
gridCtx.lineTo(w / 2, horizonY);
gridCtx.stroke();
gridCtx.beginPath();
gridCtx.moveTo(rightX, y);
gridCtx.lineTo(w / 2, horizonY);
gridCtx.stroke();
}
}
```
This is better! But the radial lines might look messy. Let me think about what the classic synthwave grid looks like.
Actually, in the classic synthwave grid, it's typically just horizontal bands with side lines converging to the center. Sometimes it's rendered as a wireframe grid.
Let me also add:
1. A gradient fill under the horizon for the "ground"
2. A reflection of the sun
3. Some neon polygons or buildings for extra synthwave flavor
Actually, the user specifically asked for:
- Glowing gradient sun ✓
- Scanlines ✓
- Infinite neon grid scrolling toward viewer ✓
- Starfield ✓
- Reflected horizon ✓
Let me also add a few extras:
- Some distant mountains or buildings in silhouette
- The grid should have a neon glow
For the reflected horizon, I'll draw the grid (or a version of it) below the horizon line, with reduced opacity and a slight stretch to simulate water reflection.
Let me also think about making the grid lines thicker and with a trailing glow effect.
Here's my plan for the final implementation:
1. **Background**: Radial gradient sky (purple to pink to dark blue)
2. **Starfield**: Canvas with white/yellow dots, twinkling
3. **Sun**: CSS radial gradient circle with box-shadow for glow, pulsing
4. **Grid**: Canvas with perspective grid lines, animated, with neon glow
5. **Reflection**: Grid drawn below horizon, flipped and faded, with a water-like distortion
6. **Scanlines**: CSS repeating-linear-gradient overlay with a sweeping scanline
7. **Horizon line**: Thin neon line at the horizon
Let me also add some distant geometric shapes (like low-poly mountains) for extra depth.
Actually, let me focus on making each element look great. Let me refine the grid:
For the grid, I want:
- Lines that are thick enough to be visible
- Good neon glow
- Smooth animation
- The grid should fill the area below the horizon (the "ground")
The grid should also have:
- A gradient opacity (more transparent at the horizon, more visible near the viewer)
- The lines should get closer together as they approach the horizon
Let me also add a fill color under the grid (the ground plane) with a dark gradient.
For the reflection:
- Draw the grid lines below the horizon, but:
- Scale vertically to stretch
- Reduce opacity
- Add a slight wave distortion
- Maybe offset slightly
Let me code this up properly now.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Synthwave Sunset</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
overflow: hidden;
background: linear-gradient(to bottom,
#2c003e 0%,
#6a00ff 20%,
#00b4d8 50%,
#ff006e 70%,
#000 100%);
height: 100vh;
font-family: 'Courier New', monospace;
}
canvas { display: block; }
#starfield {
position: absolute;
top: 0; left: 0;
z-index: 1;
}
#grid {
position: absolute;
top: 0; left: 0;
z-index: 2;
}
.sun {
position: absolute;
top: 25%;
left: 50%;
transform: translate(-50%, -50%);
width: 250px;
height: 250px;
background: radial-gradient(circle,
#ffdd00 0%,
#ff006e 30%,
#ff79c6 50%,
#00b4d8 70%,
transparent 80%);
border-radius: 50%;
filter: blur(1px);
box-shadow:
0 0 30px #ff006e,
0 0 60px #ff006e,
0 0 100px #ff79c6,
0 0 200px #ffdd00;
z-index: 3;
animation: sunPulse 6s infinite ease-in-out;
}
@keyframes sunPulse {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
box-shadow:
0 0 30px #ff006e,
0 0 60px #ff006e,
0 0 100px #ff79c6,
0 0 200px #ffdd00;
}
25% {
transform: translate(-50%, -50%) scale(1.02);
box-shadow:
0 0 40px #ff006e,
0 0 70px #ff006e,
0 0 120px #ff79c6,
0 0 250px #ffdd00;
}
50% {
transform: translate(-50%, -50%) scale(1.05);
box-shadow:
0 0 50px #ff006e,
0 0 80px #ff006e,
0 0 150px #ff79c6,
0 0 300px #ffdd00;
}
75% {
transform: translate(-50%, -50%) scale(1.02);
box-shadow:
0 0 40px #ff006e,
0 0 70px #ff006e,
0 0 120px #ff79c6,
0 0 250px #ffdd00;
}
}
.horizon-line {
position: absolute;
bottom: 50%;
left: 0;
width: 100%;
height: 1px;
background: linear-gradient(to right, transparent, #00ffff, transparent);
box-shadow: 0 0 10px #00ffff;
z-index: 4;
}
#scanlines {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background:
repeating-linear-gradient(
0deg,
rgba(0, 255, 255, 0.03) 0px,
rgba(0, 255, 255, 0.03) 1px,
transparent 1px,
transparent 3px
);
z-index: 10;
pointer-events: none;
}
.scanline-sweep {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90, transparent, rgba(255, 255, 255, 0.4), transparent);
z-index: 11;
animation: sweep 8s infinite linear;
pointer-events: none;
}
@keyframes sweep {
0% { top: 0%; }
100% { top: 100%; }
}
</style>
</head>
<body>
<canvas id="starfield"></canvas>
<canvas id="grid"></canvas>
<div class="sun"></div>
<div class="horizon-line"></div>
<div id="scanlines"></div>
<div class="scanline-sweep"></div>
<script>
// === STARFIELD ===
const starCanvas = document.getElementById('starfield');
const starCtx = starCanvas.getContext('2d');
function resizeCanvas(canvas) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas(starCanvas);
const stars = [];
for (let i = 0; i < 300; i++) {
stars.push({
x: Math.random() * starCanvas.width,
y: Math.random() * starCanvas.height * 0.7, // stars only in upper portion
radius: Math.random() * 1.5,
baseOpacity: Math.random() * 0.8 + 0.2,
twinkleSpeed: Math.random() * 0.02 + 0.005,
hue: Math.random() > 0.8 ? Math.floor(Math.random() * 60) : 0 // mostly white, some yellow/orange
});
}
function animateStars(time) {
starCtx.clearRect(0, 0, starCanvas.width, starCanvas.height);
for (const star of stars) {
const twinkle = Math.sin(time * 0.001 * star.twinkleSpeed * 100) * 0.1;
const opacity = Math.max(0.1, star.baseOpacity + twinkle);
if (star.hue === 0) {
starCtx.fillStyle = `rgba(255, 255, 255, ${opacity})`;
} else {
starCtx.fillStyle = `hsla(${star.hue}, 100%, 70%, ${opacity})`;
}
starCtx.beginPath();
starCtx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
starCtx.fill();
}
requestAnimationFrame(animateStars);
}
// === GRID ===
const gridCanvas = document.getElementById('grid');
const gridCtx = gridCanvas.getContext('2d');
resizeCanvas(gridCanvas);
let gridOffset = 0;
const gridSpeed = 0.3;
function animateGrid(time) {
gridCtx.clearRect(0, 0, gridCanvas.width, gridCanvas.height);
const w = gridCanvas.width;
const h = gridCanvas.height;
const horizonY = h * 0.5;
// Draw ground fill
const groundGradient = gridCtx.createLinearGradient(0, horizonY, 0, h);
groundGradient.addColorStop(0, 'rgba(0, 10, 30, 0.7)');
groundGradient.addColorStop(1, 'rgba(0, 10, 30, 0.9)');
gridCtx.fillStyle = groundGradient;
gridCtx.fillRect(0, horizonY, w, h - horizonY);
// Grid settings
const gridLines = 40;
gridCtx.shadowBlur = 15;
gridCtx.shadowColor = '#00ffff';
gridCtx.strokeStyle = '#00ffff';
gridCtx.lineWidth = 2;
// Animate offset
gridOffset += gridSpeed;
// Draw grid bands
for (let i = 0; i < gridLines; i++) {
const t = (i + gridOffset / 20) % gridLines / gridLines; // 0 to 1
if (t < 0.02) continue; // skip too-close-to-horizon lines
// Y position: 0 at horizon, 1 at bottom
const y = horizonY + (h - horizonY) * t;
// Width at this position
const bandWidth = w * t * 1.2;
// Opacity fades near horizon
const opacity = Math.min(1, t * 2);
const color = `rgba(0, 216, 255, ${opacity})`;
gridCtx.strokeStyle = color;
// Draw horizontal line (band)
const leftX = (w - bandWidth) / 2;
const rightX = (w + bandWidth) / 2;
gridCtx.beginPath();
gridCtx.moveTo(leftX, y);
gridCtx.lineTo(rightX, y);
gridCtx.stroke();
// Draw side lines converging to vanishing point
gridCtx.beginPath();
gridCtx.moveTo(leftX, y);
gridCtx.lineTo(w / 2, horizonY);
gridCtx.stroke();
gridCtx.beginPath();
gridCtx.moveTo(rightX, y);
gridCtx.lineTo(w / 2, horizonY);
gridCtx.stroke();
}
// Draw reflection (below horizon)
animateReflection(gridCtx, w, h, horizonY);
requestAnimationFrame(animateGrid);
}
function animateReflection(ctx, w, h, horizonY) {
const reflectionStart = horizonY;
const reflectionHeight = h - horizonY;
ctx.save();
ctx.globalAlpha = 0.3;
ctx.scale(1, -0.5); // flip vertically and compress
ctx.translate(0, -horizonY * 2 + h); // position
// ... draw reflected grid
ctx.restore();
}
animateStars(0);
animateGrid(0);
window.addEventListener('resize', () => {
resizeCanvas(starCanvas);
resizeCanvas(gridCanvas);
});
</script>
</body>
</html>
```
Hmm, the reflection is tricky. Let me think about a simpler way to do it.
For the reflection, I can:
1. Draw the grid normally (above horizon and below)
2. For the below-horizon part, use a lower opacity and stretch it
Actually, the grid is already drawn below the horizon. The "reflection" effect should be on the sun and maybe the sky.
Let me reconsider the layout:
- The horizon is at 50% height
- Above the horizon: sky, sun, stars
- Below the horizon: ground, grid
- The reflection should be in the ground area
Actually, looking at classic synthwave imagery:
- The grid is on the ground (below horizon)
- The sun is in the sky (above horizon)
- The reflection is of the sun on the ground, and the grid itself creates a reflective surface
For the sun reflection, I can:
1. Draw a semi-transparent sun below the horizon
2. Stretch it vertically
3. Add a wavy distortion
4. Fade it out
Let me also add a gradient overlay for the ground to give it that reflective quality.
Actually, let me simplify. The grid itself will serve as the ground. For the reflection:
- The grid lines below the horizon are the "ground grid"
- For the sun reflection, I'll add a CSS element (a stretched, faded sun) below the horizon
Let me also add some neon geometric shapes (like a low-poly cityscape or mountains) for that authentic synthwave feel.
Let me also think about colors:
- Sky: Dark purple → Pink/Magenta → Cyan → Orange (gradient)
- Sun: Yellow → Orange → Pink → Cyan (radial gradient)
- Grid: Cyan (#00ffff) with glow
- Stars: White with some yellow/orange
- Scanlines: Dark overlay with sweep
OK let me write the complete, polished version now.
For the grid, I want it to look like this:
```
| | | | |
| | | | | | | | | |
Horizon → ───┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──
| | | | | | | | | |
| | | | |
| | |
| |
| |
|
```
The vertical lines converge to the vanishing point on the horizon, and the horizontal lines (rings) get wider as they come toward the viewer.
Actually, in most synthwave grids, it's more like:
- A flat grid with both horizontal and vertical lines
- The grid extends from the horizon to the viewer
- Both sets of lines converge to the vanishing point
Let me draw it like a proper 3D grid viewed from above:
```javascript
// For each point on the grid (x, z) where z is distance from viewer
// Project to screen:
// screenX = centerX + (x / (z + 1)) * focalLength
// screenY = horizonY + (gridY / (z + 1)) * focalLength
const focalLength = width * 0.8;
const gridSize = 20; // grid spacing
const gridRange = 20; // how many grid cells
for (let x = -gridRange; x <= gridRange; x++) {
for (let z = 0; z < gridRange; z++) {
// Project points
const screenX1 = centerX + (x * gridSize / (z + 1)) * focalLength / 100;
const screenY1 = horizonY + (0 / (z + 1)) * focalLength / 100;
const screenX2 = centerX + ((x + 1) * gridSize / (z + 1)) * focalLength / 100;
const screenY2 = horizonY + (gridSize / (z + 1)) * focalLength / 100;
// Draw lines...
}
}
```
Hmm, this is getting complex. Let me use a simpler approach that still looks good.
I'll draw:
1. Horizontal bands (lines parallel to the horizon) - these represent the grid squares
2. Lines from the sides converging to the vanishing point - these represent the grid's side divisions
```javascript
// Grid bands (horizontal lines on the ground plane)
for (let i = 0; i < 50; i++) {
const z = (i + offset) % 50 / 50; // 0 to 1, with offset for animation
const y = horizonY + (h - horizonY) * z;
const width = w * z * 1.5;
const leftX = (w - width) / 2;
const rightX = (w + width) / 2;
// Draw the band
gridCtx.beginPath();
gridCtx.moveTo(leftX, y);
gridCtx.lineTo(rightX, y);
gridCtx.stroke();
}
// Grid radial lines (side divisions)
for (let i = 0; i < 10; i++) {
const x = w * i / 10;
// Draw line from x at bottom to vanishing point at horizon
gridCtx.beginPath();
gridCtx.moveTo(x, h);
gridCtx.lineTo(w / 2, horizonY);
gridCtx.stroke();
}
```
Actually, I think the radial lines should go from each corner to the vanishing point, and the horizontal bands should be the spacing between grid cells.
Let me look at this more carefully. The grid has:
- Vanishing point at the horizon center
- Grid lines extending from the vanishing point outward
- Both horizontal (parallel to horizon) and vertical (radial) grid lines
But in the classic synthwave style, it's often just the horizontal bands with the sides converging. The radial lines might not be necessary if the bands look good.
Let me try a different approach - draw the grid as a series of trapezoids:
```javascript
for (let i = 0; i < bands; i++) {
const t = (i + offset) / bands; // 0 at horizon, 1 at viewer
const y = horizonY + (h - horizonY) * t;
const width = w * t * 1.5;
// Draw two side lines (left and right converging lines)
// And one horizontal line
// Left side line: from (leftX, y) to (w/2, horizonY)
// Right side line: from (rightX, y) to (w/2, horizonY)
// Horizontal line: from (leftX, y) to (rightX, y)
}
```
OK, I think this is the right approach. Let me finalize the code.
For the sun reflection, I'll use a CSS pseudo-element or a separate div that's positioned below the horizon, with:
- A stretched version of the sun
- Reduced opacity
- Vertical flip
- Maybe a wave distortion (using a mask or clip-path, which might be complex)
For simplicity, I'll use a canvas for the reflection or just a CSS element.
Let me also add some mountains or buildings in the background for extra depth.
OK, let me write the final version. I'll aim for a polished, visually striking result.
Let me also think about performance. Using canvas for the grid and starfield should be fine since we're animating with requestAnimationFrame. The sun and reflection can be CSS elements.
Let me think about the grid effect one more time. I want it to look like this:
The grid is a flat plane on the ground. The camera is above the plane, looking down at an angle. The horizon is where the grid meets the sky.
Grid lines:
- "Rings" (horizontal bands) - these are lines of constant distance from the viewer, getting wider and closer to the screen bottom
- "Radial lines" (from vanishing point) - these are lines of constant x-coordinate on the grid, converging to the vanishing point
For the animation:
- The grid moves upward (toward the horizon), creating the illusion of forward motion
- New bands appear from the horizon and grow wider as they approach
Let me write the final code:
```javascript
function animateGrid() {
const w = gridCanvas.width;
const h = gridCanvas.height;
const horizonY = h * 0.55; // horizon slightly above center
gridCtx.clearRect(0, 0, w, h);
// Draw ground plane with gradient
const groundGrad = gridCtx.createLinearGradient(0, horizonY, 0, h);
groundGrad.addColorStop(0, 'rgba(0, 5, 20, 0.6)');
groundGrad.addColorStop(1, 'rgba(0, 0, 5, 0.9)');
gridCtx.fillStyle = groundGrad;
gridCtx.fillRect(0, horizonY, w, h - horizonY);
// Grid settings
const bands = 30;
const radialLines = 12;
const neonColor = '#00f0ff';
gridCtx.shadowBlur = 20;
gridCtx.shadowColor = neonColor;
gridCtx.strokeStyle = neonColor;
gridCtx.lineWidth = 1.5;
// Animate
gridOffset += 0.4;
// Draw radial lines (from vanishing point to screen bottom)
for (let i = 0; i <= radialLines; i++) {
const t = i / radialLines;
const x = w * t;
// Only draw if line is visible (above some y near horizon)
// The radial line goes from (x, h) to (w/2, horizonY)
// But we want lines from both sides
// Left side radial lines
const leftX = (w * 0.2) * t + w * 0.4 * (1 - t);
const rightX = w - leftX;
// Actually, let me draw lines from the vanishing point to the bottom
// and from the corners
// Simple approach: draw lines from left edge to right edge
// Actually, let me just draw lines from screen edges to the vanishing point
// Left half
if (t > 0 && t < 0.5) {
const xNear = w * 0.1 * (t / 0.5); // left edge
const xFar = w / 2 * (1 - t / 0.5); // converging
// This is getting too complex. Let me simplify.
}
}
// Actually, let me just draw:
// 1. Horizontal bands
// 2. Lines from the two bottom corners to the vanishing point
// Draw horizontal bands (rings)
for (let i = 0; i < bands; i++) {
const t = ((i + gridOffset * 0.3) % bands) / bands;
if (t < 0.03) continue; // skip bands too close to horizon
const y = horizonY + (h - horizonY) * t;
const bandWidth = w * t * 1.5;
const leftX = (w - bandWidth) / 2;
const rightX = (w + bandWidth) / 2;
// Fade out near horizon
const opacity = Math.min(1, t * 3);
gridCtx.strokeStyle = `rgba(0, 240, 255, ${opacity})`;
gridCtx.beginPath();
gridCtx.moveTo(leftX, y);
gridCtx.lineTo(rightX, y);
gridCtx.stroke();
}
// Draw converging lines (from bottom corners to vanishing point)
gridCtx.strokeStyle = neonColor;
gridCtx.lineWidth = 1;
for (let i = 0; i < 8; i++) {
const t = i / 8;
// Left side
const nearX = w * 0.1 + (w * 0.4 - w * 0.1) * t;
// Right side (mirrored)
const nearXRight = w - nearX;
// Lines from near position to vanishing point
gridCtx.beginPath();
gridCtx.moveTo(nearX, h);
gridCtx.lineTo(w / 2, horizonY);
gridCtx.stroke();
gridCtx.beginPath();
gridCtx.moveTo(nearXRight, h);
gridCtx.lineTo(w / 2, horizonY);
gridCtx.stroke();
}
requestAnimationFrame(animateGrid);
}
```
Hmm, this is getting messy. Let me take a step back and think about a clean implementation.
I'll use a grid system where:
- The grid is on the ground plane
- Grid lines are at regular intervals on the ground plane
- Each grid line is projected to screen space using perspective
Here's a cleaner approach:
```javascript
// Grid plane: x from -10 to 10 (left to right), z from 0 to 10 (viewer to far)
// Each grid cell is 2x2 units
const centerX = w / 2;
const focalLength = 300; // perspective strength
// Project a point from grid space to screen space
function project(x, z) {
const scale = focalLength / (focalLength + z);
return {
x: centerX + x * scale,
y: horizonY + (0 - z * 0.3) * scale, // grid points at z=0 are at horizonY
// Actually, y should increase (move down) as z increases
y: horizonY + z * 0.5 * scale
};
}
```
Wait, I need to think about this more carefully. The grid is on the ground (y=0 in 3D space). The camera is above the ground looking down.
Camera position: (0, cameraHeight, cameraDistance) looking toward (0, 0, 0)
But for a 2D representation, I'll simplify:
- Grid lines are at z = 0, 1, 2, 3, ... (distance from viewer)
- The grid extends from x = -gridWidth to x = gridWidth
- Perspective projection: screenX = centerX + (gridX / (gridZ + 1)) * focalLength
- screenY = horizonY + (gridZ / (gridZ + 1)) * something
Actually, the simplest correct approach:
The grid is a flat plane. The viewer is at position (0, 0, d) where d is the height above the plane. The plane is at y=0.
For a point (x, 0, z) on the grid (where z=0 is at the horizon and z increases toward the viewer):
- The projected x = centerX + (x / (z + c)) * focalLength
- The projected y = horizonY + (z / (z + c)) * maxHeight
Wait, I'm confusing myself. Let me use a standard 3D to 2D projection.
Viewer at (0, viewHeight, 0) looking toward the negative Z direction.
Grid plane at z = 0 (this is the "ground").
Points on the grid: (x, y, 0) in 3D, where y is the height (0 for ground).
Wait, I need to set up the coordinate system properly.
Let me use:
- X: horizontal (left-right)
- Y: vertical (up-down)
- Z: depth (toward viewer)
Camera at (0, cameraY, cameraZ) looking in the -Z direction.
Grid points at (x, 0, z) where z is the distance from the camera plane.
Perspective projection of point (x, 0, z):
- screenX = centerX + (x * focalLength) / z
- screenY = horizonY + (cameraY * focalLength) / z
Hmm, this is getting complicated. Let me just use a simpler empirical approach that looks good:
```javascript
const horizonY = h * 0.5;
const gridScale = 200; // perspective scaling factor
for (let z = 1; z <= 40; z++) {
const scale = gridScale / z;
const y = horizonY + h * 0.5 * (1 - scale / gridScale);
// The grid width at this z
const width = w * scale / gridScale * 1.5;
// Draw horizontal line (ring)
gridCtx.beginPath();
gridCtx.moveTo((w - width) / 2, y);
gridCtx.lineTo((w + width) / 2, y);
gridCtx.stroke();
// Draw side lines (only for certain z values)
if (z % 5 === 0) {
gridCtx.beginPath();
gridCtx.moveTo((w - width) / 2, y);
gridCtx.lineTo(w / 2, horizonY);
gridCtx.stroke();
gridCtx.beginPath();
gridCtx.moveTo((w + width) / 2, y);
gridCtx.lineTo(w / 2, horizonY);
gridCtx.stroke();
}
}
```
OK, I think I've been overthinking this. Let me just write a clean implementation and test it.
Here's my final plan for the grid:
- Use a perspective projection where grid lines get wider and closer to the screen bottom as they get closer to the viewer
- Animate by shifting the z-offset
- Draw with neon cyan color and glow
- Add a ground fill with gradient
- Add the sun reflection as a separate CSS element
For the sun reflection:
- A stretched, faded, vertically-flipped sun below the horizon
- With a wavy distortion (using canvas or SVG filter)
- Possibly with a gradient mask
Let me also add some elements:
- Distant mountains in silhouette
- Some geometric shapes (low-poly buildings)
- The sun should have a realistic gradient (yellow core, orange/pink middle, cyan edges)
Let me write the complete implementation now:
For the scanlines, I'll use a CSS pattern with:
1. Static scanlines (fine horizontal lines)
2. A moving scanline sweep (for the VHS effect)
For the starfield:
- Random white and yellow dots
- Twinkling animation
- Some larger "shooting stars" occasionally
Let me finalize everything and write the code.
Actually, I realize I should also handle the grid's "reflection" properly. The grid is on the ground, and the ground acts like a reflective surface. The reflection of the grid would be below the ground line, but since the grid is already on the ground, the "reflection" is more about the sun reflecting on the wet pavement.
Let me add:
1. A water/reflection layer below the grid that reflects the sun and part of the sky
2. The grid should have a reflective quality (maybe add some specular highlights on the grid lines)
For the reflection of the sun on the ground, I can:
- Draw a stretched, faded sun below the horizon on the grid canvas
- Add a gradient mask that fades the reflection near the viewer
Let me code this up now. I'll put the reflection in the grid canvas.
Actually, let me keep it simpler. I'll add a separate canvas for the ground/reflection effects, or I'll just draw everything on the grid canvas.
Here's my final approach:
1. CSS: body gradient (sky)
2. Canvas #stars: starfield with twinkling
3. CSS .sun: glowing sun with animation
4. CSS .sun-reflection: stretched, faded reflection of the sun
5. Canvas #grid: neon grid + ground fill + ground reflection of sun
6. CSS .horizon-line: thin neon line
7. CSS #scanlines: scanline overlay
8. CSS .scanline-sweep: moving scanline sweep
Let me write the complete code now. I want to make sure the grid looks really good.
For the grid, let me use this approach:
- Draw vertical lines (radial, converging to horizon center)
- Draw horizontal lines (bands, parallel to horizon)
- Both with neon glow
The grid should look like a 3D wireframe grid viewed from a low camera angle (like looking up at it from below, or looking down slightly).
Actually, in most synthwave grids, it's a top-down view (looking down at the ground), so the grid extends from the horizon to the bottom of the screen.
Let me draw it like this:
- Horizon at 50% height
- Grid extends from horizon (far away) to bottom (near the viewer)
- Grid lines (both horizontal and vertical on the grid plane) converge to the vanishing point at the horizon center
For the grid animation:
- Lines move from the horizon (appearing) to the bottom (moving away/disappearing)
- The speed increases as lines get closer to the viewer
This creates the effect of the viewer moving forward through the grid.
Let me code this properly:
```javascript
const w = gridCanvas.width;
const h = gridCanvas.height;
const horizonY = h * 0.5;
// Perspective settings
const focalLength = 300;
const nearZ = 1; // closest grid point to viewer
const farZ = 50; // farthest grid point
// For a grid point at depth z (1 = far at horizon, 50 = near at bottom):
// screenY = horizonY + (focalLength / z) * something
// screenX = centerX + (gridX / z) * focalLength
// Actually, let me use a simpler parameterization:
// t = 0 at horizon, t = 1 at bottom of screen
// z = 1 / (1 - t) for perspective
const gridSize = 30; // units between grid lines
const maxDistance = 40; // how far the grid goes
for (let i = 0; i < maxDistance; i++) {
const z = i + 1 + gridOffset; // animated depth
if (z > maxDistance) continue; // wrap around
// Project to screen
const scale = focalLength / z;
const y = horizonY + scale * 100; // some scaling factor
const halfWidth = w * 0.4 * scale / focalLength;
// Draw horizontal band
gridCtx.beginPath();
gridCtx.moveTo(centerX - halfWidth, y);
gridCtx.lineTo(centerX + halfWidth, y);
gridCtx.stroke();
}
```
OK, I think I need to just write the code and iterate. Let me write a complete, working implementation.
Let me think about the perspective formula one more time.
Simple perspective:
- Grid extends from z = 0 (horizon) to z = D (bottom of screen)
- For a grid line at depth z:
- screenY = horizonY + (h - horizonY) * (z / D)
- But with perspective, the spacing should be non-linear
- Better: screenY = horizonY + screenHeight * (z / (z + f)) where f is focal length
Actually, the simplest approach that works:
```javascript
// t goes from 0 (at horizon) to 1 (at bottom)
// z is the actual depth
// Using: z = 1/(1-t) * maxDepth / (1 + maxDepth) or something similar
// Let me just use:
const t = (i + offset) / bands; // 0 to 1
const y = horizonY + (h - horizonY) * t * t; // quadratic for faster spacing near horizon
// Or even simpler:
const y = horizonY + (h - horizonY) * t; // linear spacing
// Width at depth t:
const width = w * t * 1.5; // wider at bottom (near viewer)
```
This is simple and looks good. The grid lines get wider and closer together as they approach the horizon.
Let me finalize and write the complete code now. I'll make it visually appealing with:
- Smooth animations
- Good color choices
- Proper glow effects
- Scanlines
- Starfield
- Sun with reflection
- Neon grid
Here's my final implementation plan:
```html
<!DOCTYPE html>
<html>
<head>
<style>
/* CSS for sky, sun, scanlines, horizon */
</style>
</head>
<body>
<!-- Starfield canvas -->
<!-- Grid canvas -->
<!-- Sun (CSS) -->
<!-- Sun reflection (CSS) -->
<!-- Horizon line (CSS) -->
<!-- Scanlines overlay (CSS) -->
<!-- Scanline sweep (CSS) -->
<script>
// Starfield animation
// Grid animation
</script>
</body>
</html>
```
Let me write the complete code now.
Actually, for the sun reflection, it might be better to draw it on the grid canvas so I can apply the ground gradient and fade it properly. Or I can use CSS with opacity and transform.
Let me use CSS for the sun reflection since it's easier to animate and style. I'll position it below the horizon, stretched vertically, with reduced opacity, and add a gradient mask (using a pseudo-element) to fade it.
Wait, I can't use external resources, but CSS features are fine. I can use CSS gradients, transforms, etc. Let me use CSS for the sun reflection.
Actually, for the ground, I want the grid canvas to handle the ground fill (gradient) and the grid lines. The sun reflection can be a CSS element on top of the grid.
But if the grid lines are drawn on the same canvas as the ground fill, the grid lines will appear on top of the ground and the sun reflection. That's what I want.
Let me also add some additional elements:
- Mountains in the background (silhouette)
- Some geometric shapes on the ground (like cubes or buildings)
Actually, let me keep it focused on what the user asked for. I'll add some extras if it doesn't get too complex.
OK, let me write the final code now. I want to make sure it looks really good.
For the grid, I'll draw:
1. Ground plane fill (dark gradient)
2. Grid lines with neon glow (horizontal bands + radial lines)
3. Grid line highlights (add some specular on certain lines)
For the sun:
1. Radial gradient circle (yellow → orange → pink → cyan)
2. Multiple box-shadows for glow
3. Pulsing animation
For the sun reflection:
1. A stretched, faded version of the sun below the horizon
2. With a gradient mask to fade out near the viewer
For the starfield:
1. Random dots (white and some yellow)
2. Twinkling animation
3. Some shooting stars occasionally
For the scanlines:
1. Fine horizontal lines (repeating-linear-gradient)
2. A moving white sweep line (for the VHS scan effect)
Let me write the code now.
I also want to add a few extras:
- A low-poly mountain silhouette in the background
- Maybe some floating geometric shapes
- A slight chromatic aberration effect on the sun (using CSS filter: blur + multiple colored glows)
Let me also think about the color palette:
- Sky: Purple (#2c003e) → Blue (#00b4d8) → Pink (#ff006e) → Orange (#ff79c6)
- Sun: Yellow (#ffdd00) → Orange (#ff79c6) → Pink (#ff006e) → Cyan (#00b4d8)
- Grid: Cyan (#00f0ff) with glow
- Stars: White (#ffffff) with some yellow (#ffdd00)
- Scanlines: Dark overlay with white sweep
Alright, let me write the complete code now. I'll make it polished and visually striking.
Let me think about the structure once more:
1. Body: Sky gradient (radial, with sun color in the middle)
2. Canvas #stars: Starfield (positioned above grid, below sun)
3. Div .sun: Glowing sun (positioned in sky)
4. Div .sun-reflection: Sun reflection (below horizon, on ground)
5. Canvas #grid: Neon grid + ground fill
6. Div .horizon-line: Thin neon line at horizon
7. Div #scanlines: Scanline overlay
8. Div .scanline-sweep: Moving scanline sweep
Wait, I need to manage z-index carefully:
- Stars: z-index 1
- Grid: z-index 2 (this includes ground fill)
- Sun reflection: z-index 3 (on top of grid)
- Sun: z-index 4 (in the sky)
- Horizon line: z-index 5
- Scanlines: z-index 10
- Scanline sweep: z-index 11
Actually, the sun reflection should be on the grid canvas (below the grid lines) or as a separate element below the grid canvas. Let me think...
The sun reflection should appear on the ground, below the grid lines (or maybe behind them). Actually, the grid lines are on the ground, so the reflection should be below the grid lines. But visually, the reflection might be partially obscured by the grid lines, which is fine.
Let me put the sun reflection on the grid canvas, drawn before the grid lines. Or I can use a separate canvas.
Actually, let me draw the sun reflection on the grid canvas, behind the grid lines. This way:
1. Clear canvas
2. Draw ground fill (dark gradient)
3. Draw sun reflection (stretched, faded, with vertical distortion)
4. Draw grid lines (neon cyan with glow)
This makes sense. The grid lines are on top of the ground and reflection.
For the sun reflection, I'll draw it with:
- A stretched radial gradient (sun shape)
- Reduced opacity (30-50%)
- Vertical distortion (sine wave displacement, or just vertical stretch)
- A gradient mask to fade near the viewer
Let me code this up now.
Actually, drawing a stretched, distorted sun on canvas is a bit complex. Let me use a simpler approach:
- Draw an ellipse below the horizon
- Fill it with a radial gradient (similar to the sun)
- Add a gradient overlay that fades it out near the viewer
- Add some wave-like distortion to the edges
For the wave distortion, I can use `gridCtx.transform()` or draw the ellipse with bezier curves. Or I can use a simpler approach: just draw a stretched ellipse with reduced opacity.
Let me keep it simple and just draw a stretched ellipse with a gradient. The wave distortion can be simulated by drawing the ellipse with a wavy bottom edge.
OK, I think I've planned enough. Let me write the complete code now. I'll make it look great.
Here's another thought: for the grid reflection, I could also add a subtle distortion to the grid lines themselves to simulate rippling water. This would involve offsetting the y position of each band by a sine wave.
Let me add that effect too.
Also, for some extra polish:
- Add some neon polygons (like a low-poly cityscape or geometric shapes) on the ground
- Add a "vignette" overlay to darken the corners
- Add some chromatic aberration
Let me write the code now.
Let me also consider: should I use requestAnimationFrame with time parameter for smooth animations? Yes, I'll use the time parameter for the star twinkling and sun pulsing.
For the grid animation, I'll use a simple incrementing offset.
OK here's my final implementation. Let me write it out carefully:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Synthwave Sunset</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
overflow: hidden;
height: 100%;
background: #000;
font-family: 'Courier New', monospace;
}
#scene {
position: relative;
width: 100%;
height: 100%;
background: radial-gradient(ellipse at center,
#2c003e 0%,
#4a006e 25%,
#1a002d 50%,
#00182a 75%,
#000 100%);
overflow: hidden;
}
canvas {
position: absolute;
top: 0;
left: 0;
}
#starfield {
z-index: 1;
}
#grid {
z-index: 2;
}
.sun {
position: absolute;
top: 20%;
left: 50%;
transform: translate(-50%, -50%);
width: 280px;
height: 280px;
background: radial-gradient(circle,
#ffee00 0%,
#ffdd00 10%,
#ff79c6 30%,
#ff006e 50%,
#00b4d8 70%,
transparent 85%);
border-radius: 50%;
filter: blur(1px);
box-shadow:
0 0 20px #ffdd00,
0 0 40px #ff79c6,
0 0 80px #ff006e,
0 0 200px #00b4d8,
0 0 400px #00ffff;
z-index: 3;
animation: sunPulse 5s infinite ease-in-out;
}
.sun::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 350px;
height: 350px;
background: radial-gradient(circle,
rgba(255, 221, 0, 0.3) 0%,
rgba(255, 121, 198, 0.2) 30%,
rgba(0, 180, 216, 0.1) 60%,
transparent 80%);
border-radius: 50%;
filter: blur(5px);
animation: sunGlow 8s infinite ease-in-out;
}
@keyframes sunPulse {
0%, 100% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.08); }
}
@keyframes sunGlow {
0%, 100% { opacity: 0.8; }
50% { opacity: 1; }
}
.horizon {
position: absolute;
bottom: 50%;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg,
transparent 0%,
#00f0ff 20%,
#00ffff 50%,
#00f0ff 80%,
transparent 100%);
box-shadow: 0 0 15px #00ffff;
z-index: 4;
}
.mountains {
position: absolute;
bottom: 50%;
left: 0;
width: 100%;
height: 200px;
z-index: 5;
}
.mountain {
position: absolute;
bottom: 0;
width: 0;
height: 0;
border-left: 80px solid transparent;
border-right: 80px solid transparent;
border-bottom: 100px solid #000;
filter: drop-shadow(0 0 5px #ff006e);
}
#scanlines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.2) 0px,
rgba(0, 0, 0, 0.2) 1px,
transparent 1px,
transparent 3px
),
radial-gradient(
ellipse at center,
transparent 40%,
rgba(0, 0, 0, 0.3) 100%
);
z-index: 10;
pointer-events: none;
}
#scan-sweep {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg,
transparent,
rgba(255, 255, 255, 0.5),
transparent);
z-index: 11;
animation: scanSweep 6s infinite linear;
pointer-events: none;
}
@keyframes scanSweep {
0%, 100% { top: -2px; opacity: 0; }
5% { opacity: 0.6; }
50% { opacity: 0.3; }
95% { opacity: 0.6; }
}
.vignette {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(
ellipse at center,
transparent 30%,
rgba(0, 0, 0, 0.5) 100%
);
z-index: 9;
pointer-events: none;
}
</style>
</head>
<body>
<div id="scene">
<canvas id="starfield"></canvas>
<canvas id="grid"></canvas>
<div class="sun"></div>
<div class="horizon"></div>
<div class="mountains" id="mountains"></div>
<div class="vignette"></div>
<div id="scanlines"></div>
<div id="scan-sweep"></div>
</div>
<script>
// === STARFIELD ===
const starCanvas = document.getElementById('starfield');
const starCtx = starCanvas.getContext('2d');
function resizeCanvas(canvas) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas(starCanvas);
// Generate stars
const stars = [];
const numStars = 400;
for (let i = 0; i < numStars; i++) {
stars.push({
x: Math.random() * starCanvas.width,
y: Math.random() * starCanvas.height * 0.65,
radius: Math.random() * 1.2,
baseAlpha: Math.random() * 0.6 + 0.2,
twinkleOffset: Math.random() * Math.PI * 2,
twinkleSpeed: Math.random() * 0.5 + 0.5,
color: Math.random() > 0.85
? `hsl(${Math.floor(Math.random() * 50)}, 100%, 75%)` // yellow/orange stars
: '#ffffff' // white stars
});
}
function animateStars(time) {
starCtx.clearRect(0, 0, starCanvas.width, starCanvas.height);
for (const star of stars) {
const twinkle = Math.sin(time * 0.001 * star.twinkleSpeed + star.twinkleOffset) * 0.3;
const alpha = Math.max(0.1, star.baseAlpha + twinkle);
starCtx.fillStyle = star.color;
starCtx.globalAlpha = alpha;
starCtx.beginPath();
starCtx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
starCtx.fill();
}
starCtx.globalAlpha = 1;
requestAnimationFrame(animateStars);
}
// === GRID ===
const gridCanvas = document.getElementById('grid');
const gridCtx = gridCanvas.getContext('2d');
resizeCanvas(gridCanvas);
let gridOffset = 0;
function animateGrid(time) {
const w = gridCanvas.width;
const h = gridCanvas.height;
const horizonY = h * 0.5;
gridCtx.clearRect(0, 0, w, h);
// Draw ground fill
const groundGrad = gridCtx.createLinearGradient(0, horizonY, 0, h);
groundGrad.addColorStop(0, 'rgba(10, 0, 30, 0.4)');
groundGrad.addColorStop(0.3, 'rgba(0, 10, 40, 0.6)');
groundGrad.addColorStop(1, 'rgba(0, 0, 10, 0.9)');
gridCtx.fillStyle = groundGrad;
gridCtx.fillRect(0, horizonY, w, h - horizonY);
// Draw sun reflection on ground
drawSunReflection(gridCtx, w, h, horizonY);
// Grid settings
const bands = 50;
gridCtx.shadowBlur = 20;
gridCtx.shadowColor = '#00f0ff';
gridOffset += 0.5;
// Draw grid bands (horizontal lines)
for (let i = 0; i < bands; i++) {
const t = ((i + gridOffset * 0.2) % bands) / bands;
if (t < 0.02 || t > 1) continue;
// Quadratic easing for perspective
const y = horizonY + (h - horizonY) * (t * t);
const bandWidth = w * t * 1.5;
const leftX = (w - bandWidth) / 2;
const rightX = (w + bandWidth) / 2;
if (y > h) continue;
// Opacity fades near horizon
const opacity = Math.min(0.8, t * 4);
const color = `rgba(0, 240, 255, ${opacity})`;
gridCtx.strokeStyle = color;
gridCtx.lineWidth = Math.max(1, 3 * t);
gridCtx.beginPath();
gridCtx.moveTo(leftX, y);
gridCtx.lineTo(rightX, y);
gridCtx.stroke();
}
// Draw radial grid lines (converging to vanishing point)
gridCtx.strokeStyle = 'rgba(0, 240, 255, 0.3)';
gridCtx.lineWidth = 1;
const radialDivisions = 12;
for (let i = 0; i <= radialDivisions; i++) {
const t = i / radialDivisions;
// x position at the bottom of the screen
const bottomX = w * t;
// Vanishing point
const vanishX = w / 2;
gridCtx.beginPath();
gridCtx.moveTo(bottomX, h);
gridCtx.lineTo(vanishX, horizonY);
gridCtx.stroke();
}
requestAnimationFrame(animateGrid);
}
function drawSunReflection(ctx, w, h, horizonY) {
// Sun reflection position
const reflectionY = horizonY + 30;
const reflectionWidth = 300;
const reflectionHeight = 100;
// Create gradient for the reflection
const reflectionGrad = ctx.createRadialGradient(
w / 2, reflectionY, 10,
w / 2, reflectionY, reflectionWidth / 2
);
reflectionGrad.addColorStop(0, 'rgba(255, 221, 0, 0.4)');
reflectionGrad.addColorStop(0.3, 'rgba(255, 121, 198, 0.25)');
reflectionGrad.addColorStop(0.6, 'rgba(255, 0, 110, 0.15)');
reflectionGrad.addColorStop(1, 'rgba(0, 180, 216, 0)');
ctx.save();
ctx.globalAlpha = 0.5;
// Draw wavy reflection
ctx.beginPath();
const waveAmplitude = 5;
const waveFrequency = 0.02;
const waveSpeed = performance.now() * 0.002;
ctx.ellipse(
w / 2, reflectionY,
reflectionWidth / 2, reflectionHeight / 2,
0, 0, Math.PI * 2
);
ctx.fill();
ctx.restore();
// Add gradient mask to fade the reflection near the viewer
const fadeGrad = ctx.createLinearGradient(0, horizonY, 0, h);
fadeGrad.addColorStop(0, 'rgba(0, 0, 0, 1)');
fadeGrad.addColorStop(0.3, 'rgba(0, 0, 10, 0.8)');
fadeGrad.addColorStop(1, 'rgba(0, 0, 10, 0)');
ctx.fillStyle = fadeGrad;
ctx.globalCompositeOperation = 'destination-out';
ctx.fillRect(0, horizonY, w, h - horizonY);
ctx.globalCompositeOperation = 'source-over';
}
// === MOUNTAINS ===
function drawMountains() {
const mountainsDiv = document.getElementById('mountains');
const w = window.innerWidth;
const h = window.innerHeight;
const horizonY = h * 0.5;
let html = '';
const numMountains = 8;
for (let i = 0; i < numMountains; i++) {
const x = (w / numMountains) * i + w / numMountains / 2;
const size = 60 + Math.random() * 60;
const color = i % 2 === 0 ? '#000' : '#0a001a';
html += `<div class="mountain" style="
left: ${x}px;
border-left: ${size}px solid transparent;
border-right: ${size}px solid transparent;
border-bottom: ${size * 0.8}px solid ${color};
filter: drop-shadow(0 0 3px #ff006e);
"></div>`;
}
mountainsDiv.innerHTML = html;
}
// === INIT ===
animateStars(0);
animateGrid(0);
drawMountains();
window.addEventListener('resize', () => {
resizeCanvas(starCanvas);
resizeCanvas(gridCanvas);
drawMountains();
});
</script>
</body>
</html>
```
Wait, I'm using `performance.now()` which should be fine. But let me replace it with `time` parameter from requestAnimationFrame.
Also, I need to pass `time` to the `drawSunReflection` function or use `Date.now()`.
Let me also reconsider the mountains. CSS triangles (using border tricks) might not look great. Let me use canvas for mountains instead, or use CSS clip-path.
Actually, let me use canvas for everything to have better control. I'll combine the starfield and grid into one canvas, or use separate canvases.
Wait, I already have separate canvases. Let me keep the mountains as CSS elements since they're static and don't need animation.
Actually, for a more synthwave look, the mountains should be in silhouette with some neon rim lighting. CSS clip-path would work well:
```css
.mountain {
position: absolute;
bottom: 50%;
clip-path: polygon(0% 100%, 50% 0%, 100% 100%);
background: #000;
filter: drop-shadow(0 0 5px #ff006e);
}
```
But generating different mountain shapes with clip-path would require individual styles. Let me just use the border technique for simplicity.
Actually, let me use canvas for the mountains too. That way I can draw nice silhouette shapes with gradient fills and rim lighting.
Let me restructure:
- Canvas #stars: starfield
- Canvas #grid: grid + ground + sun reflection
- Canvas #mountains: mountain silhouettes
- CSS: sun, horizon, scanlines, scan sweep
Or even simpler, use one canvas for everything except the sun (which is CSS for the glow).
Let me use two canvases:
1. #bg: starfield + mountains
2. #grid: ground fill + grid + sun reflection
And CSS for:
- Sun
- Horizon line
- Scanlines
- Scan sweep
This is cleaner. Let me implement this.
Actually, let me keep it with three canvases for clarity:
1. #stars: starfield (static with twinkling)
2. #grid: ground + grid + sun reflection
3. CSS: sun + mountains + horizon + scanlines
Let me simplify and just go with the original structure. The mountains can be CSS elements.
Let me finalize the code now. I want to make sure the grid looks really good.
For the grid, I want:
- Smooth animation
- Good glow effect
- Proper perspective
- Radial lines for the grid divisions
Let me also add some grid "cells" (filled quadrilaterals) with low opacity to give the grid more substance.
Actually, let me keep it as wireframe lines. Filled cells might make it too busy.
One thing I want to improve: the grid lines near the horizon should be very thin and fade out, while the lines near the viewer should be thicker and brighter. This creates a better sense of depth.
Let me also add some "noise" to the grid lines for a more organic, CRT-like feel.
OK, let me write the final version. I'll aim for a clean, polished implementation.
One more thing: I should add a slight color shift to the grid lines (cyan vs blue) to make them look more neon.
Let me also think about the overall composition. The scene should have:
- Dark purple/blue sky at the top
- Purple/pink middle sky
- Orange/pink sun near the horizon (but above it)
- Cyan grid on the ground
- Dark ground below
- Sun reflection on the ground
- Stars scattered in the sky
- Scanlines overlay
- Moving scan sweep
This is a classic synthwave composition. Let me make it look great.
Let me write the final code now. I'll be careful about:
1. Proper perspective for the grid
2. Smooth animations
3. Good color harmony
4. Proper glow effects
5. Not too many elements (keep it clean)
Here's my final implementation:
Actually, let me also add some "digital rain" or "data stream" effects in the background, or some floating geometric shapes, for extra synthwave flavor. But I don't want to overcomplicate it. Let me focus on the core elements first.
Let me also think about the grid's color. Instead of just cyan, I could alternate between cyan and magenta for different grid lines, which is a common synthwave trope.
OK, I'll write the final code now. Let me be thorough and make it look amazing.
One more consideration: the sun should be partially obscured by the horizon line if it's setting. Let me position it near the horizon, maybe slightly below it, with only the top part visible and the rest reflected.
Actually, "sunset" implies the sun is at or near the horizon. Let me position the sun so that it's partially below the horizon, with the top half in the sky and the bottom half reflected on the ground.
But the sun is a CSS element, and the reflection is on the canvas. I need to coordinate them.
Let me position the sun at about 55% from the top (so it's at the horizon level or slightly below), and draw the reflection on the canvas below the horizon.
Actually, if the sun is below the horizon, the reflection should be even more below and faded. Let me position the sun just above the horizon for a more natural look.
Let me set the sun at about 45% from the top (so it's near the horizon but still above it). The reflection will be below the horizon.
OK, let me finalize the code. I've spent enough time planning. Let me write it out.
For the mountains, let me draw them on the grid canvas (or a separate canvas) with:
- Dark purple/blue silhouette
- Neon rim lighting (pink/cyan)
- Multiple layers for parallax (optional)
- Positioned near the horizon
Actually, let me put the mountains on the starfield canvas (behind everything else) or a separate canvas. They should be between the stars and the grid.
Let me use four elements:
1. Canvas #stars: starfield
2. Canvas #mountains: mountain silhouettes
3. Canvas #grid: ground + grid + sun reflection
4. CSS .sun: glowing sun
5. CSS .horizon: horizon line
6. CSS #scanlines: scanlines overlay
7. CSS #scan-sweep: moving scan sweep
This is getting complex with many layers. Let me simplify:
1. CSS body: sky gradient
2. Canvas #bg: starfield + mountains (static background)
3. Canvas #grid: ground + grid + sun reflection (animated)
4. CSS .sun: glowing sun
5. CSS .horizon: horizon line
6. CSS #scanlines: scanlines overlay
7. CSS #scan-sweep: moving scan sweep
This is cleaner. Let me implement this.
Actually, I realize I should also consider what happens on window resize. I need to redraw the starfield and mountains when the canvas is resized. Or I can pre-generate them with positions relative to the canvas size.
For the starfield, I'll generate stars with normalized positions (0-1) and convert to pixel positions on resize. Same for mountains.
Let me implement this carefully.
OK, I think I've planned enough. Let me write the complete, final code now.
Here's what I'll include:
1. Sky gradient (CSS, with color stops for a beautiful sunset)
2. Starfield (canvas, with twinkling, including some yellow stars)
3. Mountains (canvas, dark silhouettes with neon rim lighting)
4. Neon grid (canvas, with proper perspective, animation, and glow)
5. Sun (CSS, radial gradient with multiple glows and pulsing animation)
6. Sun reflection (on grid canvas, with wave distortion and gradient fade)
7. Horizon line (CSS, thin neon line)
8. Scanlines (CSS, repeating-linear-gradient overlay)
9. Scan sweep (CSS, moving white line)
10. Vignette (CSS, darkening corners)
Let me write this out:
For the mountains, I'll draw several silhouette peaks with:
- Dark fill (purple/black)
- Neon rim lighting on the top edges
- Maybe some snow caps (white/glow)
For the grid, I'll draw:
- Ground fill (dark gradient)
- Grid lines (horizontal bands with proper perspective, plus radial lines)
- Each line has a glow effect
- Animation: lines move toward the viewer
- Some lines are brighter (neon) than others
For the sun reflection:
- An ellipse below the horizon
- Radial gradient (similar to sun colors but faded)
- Wave distortion (using sine wave to offset the path)
- Gradient mask to fade near the viewer
Let me write it all out now. I'll be thorough and make it look amazing.
Actually, I just realized I should also think about the "scanlines" effect more carefully. There are two common types:
1. Fine horizontal lines covering the entire screen (static) - this creates a CRT-like texture
2. A single white line that sweeps across the screen (animated) - this creates a "scan" effect
I'll include both for a more authentic VHS/CRT feel.
For the fine scanlines:
```css
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 2px,
transparent 2px,
transparent 4px
);
```
For the scan sweep:
A 2px tall white line that moves from top to bottom with a fade-in/fade-out effect.
I'll also add a subtle flickering effect to the scanlines.
Let me finalize the code now.
For the grid line glow, I'll use:
```javascript
gridCtx.shadowBlur = 15;
gridCtx.shadowColor = '#00f0ff';
```
And for extra glow, I can draw the line twice: once with full glow, and once without glow for the center.
Or I can just use a single stroke with shadow blur.
Let me also add some "scan" lines on the grid itself (thin lines that sweep across the grid), for extra synthwave feel. But this might be too much. Let me keep it simple.
OK, let me write the final code now. No more planning.
Let me also consider the grid's "infinite" nature. The user asked for "infinite neon grid." The grid should appear to extend infinitely. My implementation with bands wrapping around (using modulo) creates this effect.
For extra polish, I could add some "depth cues":
- Grid lines get thinner near the horizon
- Grid lines get more transparent near the horizon
- Grid lines get wider and brighter near the viewer
- Maybe some fog/atmospheric perspective
These are already in my implementation. Let me finalize.
Let me write the complete HTML file now:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Synthwave Sunset</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
overflow: hidden;
width: 100%;
height: 100%;
background: #000;
font-family: 'Courier New', monospace;
}
#scene {
position: relative;
width: 100%;
height: 100%;
background: radial-gradient(
ellipse at 50% 40%,
#2c003e 0%,
#4a006e 20%,
#6a00ff 35%,
#00b4d8 50%,
#00182a 75%,
#000 100%
);
overflow: hidden;
}
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#stars {
z-index: 1;
}
#mountains {
z-index: 2;
}
#grid {
z-index: 3;
}
.sun {
position: absolute;
top: 48%;
left: 50%;
transform: translate(-50%, -50%);
width: 260px;
height: 260px;
background: radial-gradient(
circle,
#ffee00 0%,
#ffdd00 10%,
#ffaa00 20%,
#ff79c6 35%,
#ff006e 55%,
#00b4d8 75%,
transparent 85%
);
border-radius: 50%;
filter: blur(2px);
box-shadow:
0 0 15px #ffee00,
0 0 30px #ffdd00,
0 0 60px #ff79c6,
0 0 120px #ff006e,
0 0 200px #00b4d8,
0 0 400px #00ffff;
z-index: 4;
animation: sunPulse 5s infinite ease-in-out;
}
.sun::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: 150px;
background: radial-gradient(
ellipse,
rgba(255, 221, 0, 0.3) 0%,
rgba(255, 121, 198, 0.15) 30%,
rgba(255, 0, 110, 0.1) 60%,
transparent 80%
);
border-radius: 50%;
filter: blur(5px);
animation: sunGlow 5s infinite ease-in-out;
}
@keyframes sunPulse {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
box-shadow:
0 0 15px #ffee00,
0 0 30px #ffdd00,
0 0 60px #ff79c6,
0 0 120px #ff006e,
0 0 200px #00b4d8,
0 0 400px #00ffff;
}
50% {
transform: translate(-50%, -50%) scale(1.1);
box-shadow:
0 0 20px #ffee00,
0 0 40px #ffdd00,
0 0 80px #ff79c6,
0 0 150px #ff006e,
0 0 250px #00b4d8,
0 0 500px #00ffff;
}
}
@keyframes sunGlow {
0%, 100% { opacity: 0.7; transform: translate(-50%, -50%) scale(1); }
50% { opacity: 1; transform: translate(-50%, -50%) scale(1.05); }
}
.horizon {
position: absolute;
bottom: 50%;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(
90deg,
transparent 0%,
#00f0ff 20%,
#00ffff 50%,
#00f0ff 80%,
transparent 100%
);
box-shadow:
0 0 10px #00ffff,
0 0 20px #00ffff,
0 0 40px #00ffff;
z-index: 5;
}
#scanlines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.25) 0px,
rgba(0, 0, 0, 0.25) 1px,
transparent 1px,
transparent 3px
);
z-index: 10;
pointer-events: none;
}
#scan-sweep {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.6),
transparent
);
z-index: 11;
animation: scanSweep 7s infinite linear;
pointer-events: none;
}
@keyframes scanSweep {
0% { top: -2px; opacity: 0; }
2% { opacity: 0.8; }
15% { opacity: 0.8; }
50% { opacity: 0.3; }
85% { opacity: 0.8; }
100% { top: 100%; opacity: 0; }
}
.vignette {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(
ellipse at center,
transparent 30%,
rgba(0, 0, 0, 0.5) 70%,
rgba(0, 0, 0, 0.7) 100%
);
z-index: 9;
pointer-events: none;
}
.grid-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
linear-gradient(
180deg,
rgba(0, 0, 0, 0.3) 0%,
transparent 100%
);
z-index: 8;
pointer-events: none;
mix-blend-mode: screen;
}
.noise {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.05;
z-index: 12;
pointer-events: none;
background-image:
repeating-radial-gradient(
circle,
rgba(255, 255, 255, 0.3) 0px,
rgba(255, 255, 255, 0.3) 0.3px,
transparent 0.3px,
transparent 1px
);
}
</style>
</head>
<body>
<div id="scene">
<canvas id="stars"></canvas>
<canvas id="mountains"></canvas>
<canvas id="grid"></canvas>
<div class="sun"></div>
<div class="horizon"></div>
<div class="vignette"></div>
<div id="scanlines"></div>
<div id="scan-sweep"></div>
</div>
<script>
// === UTILITY ===
function resizeCanvas(canvas) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
// === STARFIELD ===
const starCanvas = document.getElementById('stars');
const starCtx = starCanvas.getContext('2d');
resizeCanvas(starCanvas);
const stars = [];
const numStars = 350;
for (let i = 0; i < numStars; i++) {
stars.push({
x: Math.random() * starCanvas.width,
y: Math.random() * starCanvas.height * 0.7,
radius: Math.random() * 1.2 + 0.3,
baseAlpha: Math.random() * 0.5 + 0.3,
twinkleSpeed: Math.random() * 0.8 + 0.4,
twinkleOffset: Math.random() * Math.PI * 2,
color: Math.random() > 0.8
? `hsl(${Math.floor(Math.random() * 40)}, 100%, 70%)`
: '#ffffff'
});
}
function animateStars(time) {
starCtx.clearRect(0, 0, starCanvas.width, starCanvas.height);
for (const star of stars) {
const twinkle = Math.sin(time * 0.0015 * star.twinkleSpeed + star.twinkleOffset) * 0.3;
const alpha = Math.max(0.1, star.baseAlpha + twinkle);
starCtx.fillStyle = star.color;
starCtx.globalAlpha = alpha;
starCtx.beginPath();
starCtx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
starCtx.fill();
}
starCtx.globalAlpha = 1;
requestAnimationFrame(animateStars);
}
// === MOUNTAINS ===
const mountainCanvas = document.getElementById('mountains');
const mountainCtx = mountainCanvas.getContext('2d');
resizeCanvas(mountainCanvas);
function drawMountains() {
const w = mountainCanvas.width;
const h = mountainCanvas.height;
const horizonY = h * 0.5;
mountainCtx.clearRect(0, 0, w, h);
// Draw multiple layers of mountains
const layers = [
{ count: 12, height: 80, color: '#1a002d', rimGlow: 'rgba(255, 0, 110, 0.4)' },
{ count: 8, height: 120, color: '#0a001a', rimGlow: 'rgba(0, 180, 216, 0.3)' },
{ count: 5, height: 60, color: '#000', rimGlow: 'rgba(255, 121, 198, 0.3)' }
];
for (const layer of layers) {
for (let i = 0; i < layer.count; i++) {
const x = (w / layer.count) * i + w / layer.count * 0.5;
const peakHeight = layer.height * (0.7 + Math.random() * 0.6);
const peakWidth = w / layer.count * 0.8 + Math.random() * w / layer.count * 0.4;
// Draw mountain triangle
const leftX = x - peakWidth / 2;
const rightX = x + peakWidth / 2;
const peakY = horizonY - peakHeight;
mountainCtx.fillStyle = layer.color;
mountainCtx.beginPath();
mountainCtx.moveTo(leftX, horizonY);
mountainCtx.lineTo(x, peakY);
mountainCtx.lineTo(rightX, horizonY);
mountainCtx.closePath();
mountainCtx.fill();
// Draw rim glow
mountainCtx.shadowBlur = 20;
mountainCtx.shadowColor = layer.rimGlow;
mountainCtx.strokeStyle = layer.rimGlow;
mountainCtx.lineWidth = 1;
mountainCtx.beginPath();
mountainCtx.moveTo(leftX, horizonY);
mountainCtx.lineTo(x, peakY);
mountainCtx.lineTo(rightX, horizonY);
mountainCtx.stroke();
mountainCtx.shadowBlur = 0;
}
}
}
// === GRID ===
const gridCanvas = document.getElementById('grid');
const gridCtx = gridCanvas.getContext('2d');
resizeCanvas(gridCanvas);
let gridOffset = 0;
function animateGrid(time) {
const w = gridCanvas.width;
const h = gridCanvas.height;
const horizonY = h * 0.5;
// Clear
gridCtx.clearRect(0, 0, w, h);
// Draw ground fill
const groundGrad = gridCtx.createLinearGradient(0, horizonY, 0, h);
groundGrad.addColorStop(0, 'rgba(10, 0, 30, 0.3)');
groundGrad.addColorStop(0.2, 'rgba(0, 15, 40, 0.5)');
groundGrad.addColorStop(0.6, 'rgba(0, 10, 30, 0.7)');
groundGrad.addColorStop(1, 'rgba(0, 0, 10, 0.95)');
gridCtx.fillStyle = groundGrad;
gridCtx.fillRect(0, horizonY, w, h - horizonY);
// Draw sun reflection
drawSunReflection(gridCtx, w, h, horizonY, time);
// Draw grid
const numBands = 60;
const neonColor = '#00f0ff';
gridCtx.shadowBlur = 15;
gridCtx.shadowColor = neonColor;
gridOffset += 0.6;
// Draw grid bands
for (let i = 0; i < numBands; i++) {
const rawT = (i * 1 + gridOffset * 0.15) % numBands;
const t = rawT / numBands;
if (t < 0.03) continue;
// Quadratic for perspective (faster spacing near horizon)
const easeT = 1 - Math.pow(1 - t, 3);
const y = horizonY + (h - horizonY) * easeT;
if (y >= h) continue;
// Width at this depth
const bandWidth = w * (0.2 + t * 1.8);
const leftX = (w - bandWidth) / 2;
const rightX = (w + bandWidth) / 2;
// Opacity and width
const opacity = Math.min(0.8, t * 3);
const lineWidth = Math.max(1, 3 * t);
// Alternate line colors
const hueShift = i % 4 === 0 ? '#ff006e' : neonColor;
gridCtx.strokeStyle = `rgba(${i % 4 === 0 ? '255, 0, 110' : '0, 240, 255'}, ${opacity})`;
gridCtx.lineWidth = lineWidth;
gridCtx.beginPath();
gridCtx.moveTo(leftX, y);
gridCtx.lineTo(rightX, y);
gridCtx.stroke();
}
// Draw radial lines (converging to vanishing point)
const radialCount = 16;
gridCtx.strokeStyle = `rgba(0, 240, 255, 0.25)`;
gridCtx.lineWidth = 1;
gridCtx.shadowBlur = 10;
for (let i = 0; i <= radialCount; i++) {
const t = i / radialCount;
const x = w * t;
// Wave distortion for organic feel
const wave = Math.sin(time * 0.001 + i) * 0.5;
gridCtx.beginPath();
gridCtx.moveTo(x + wave, h);
gridCtx.lineTo(w / 2, horizonY + wave);
gridCtx.stroke();
}
// Draw grid lines with neon highlight (brighter lines)
const highlightBands = 10;
gridCtx.shadowBlur = 30;
gridCtx.shadowColor = '#00ffff';
for (let i = 0; i < numBands; i += 7) {
const rawT = (i * 1 + gridOffset * 0.15) % numBands;
const t = rawT / numBands;
if (t < 0.05) continue;
const easeT = 1 - Math.pow(1 - t, 3);
const y = horizonY + (h - horizonY) * easeT;
const bandWidth = w * (0.2 + t * 1.8);
const leftX = (w - bandWidth) / 2;
const rightX = (w + bandWidth) / 2;
if (y >= h) continue;
const opacity = Math.min(0.9, t * 2);
gridCtx.strokeStyle = `rgba(0, 255, 255, ${opacity})`;
gridCtx.lineWidth = Math.max(2, 4 * t);
gridCtx.beginPath();
gridCtx.moveTo(leftX, y);
gridCtx.lineTo(rightX, y);
gridCtx.stroke();
}
gridCtx.shadowBlur = 0;
requestAnimationFrame(animateGrid);
}
function drawSunReflection(ctx, w, h, horizonY, time) {
const reflectionY = horizonY + 40;
ctx.save();
// Create clipping region for water
ctx.beginPath();
ctx.rect(0, horizonY, w, h - horizonY);
ctx.clip();
// Draw wavy sun reflection
const reflectionHeight = 80;
const reflectionWidth = 280;
ctx.save();
// Distortion
ctx.transform(1, 0, 0.1, 1, 0, 0); // shear
// Actually, let me draw it differently
ctx.restore();
// Create reflection gradient
const reflectionGrad = ctx.createRadialGradient(
w / 2, reflectionY, 5,
w / 2, reflectionY, reflectionWidth / 2
);
reflectionGrad.addColorStop(0, 'rgba(255, 238, 0, 0.4)');
reflectionGrad.addColorStop(0.25, 'rgba(255, 121, 198, 0.2)');
reflectionGrad.addColorStop(0.5, 'rgba(255, 0, 110, 0.15)');
reflectionGrad.addColorStop(0.7, 'rgba(0, 180, 216, 0.1)');
reflectionGrad.addColorStop(1, 'rgba(0, 0, 0, 0)');
// Draw wavy reflection
const path = new Path2D();
const waveAmplitude = 8;
const waveFrequency = 0.01;
const waveOffset = time * 0.002;
path.ellipse(
w / 2, reflectionY,
reflectionWidth / 2, reflectionHeight / 2,
0, 0, Math.PI * 2
);
ctx.fillStyle = reflectionGrad;
ctx.globalAlpha = 0.6;
ctx.fill(path);
// Apply wave distortion to the reflection
// (This is a simplified version - real wave distortion would use a displacement map)
ctx.restore();
// Fade out the reflection near the viewer
const fadeGrad = ctx.createLinearGradient(0, horizonY, 0, h);
fadeGrad.addColorStop(0, 'rgba(0, 0, 10, 0.8)');
fadeGrad.addColorStop(0.5, 'rgba(0, 0, 10, 0.5)');
fadeGrad.addColorStop(1, 'rgba(0, 0, 10, 0)');
ctx.fillStyle = fadeGrad;
ctx.globalCompositeOperation = 'destination-out';
ctx.fillRect(0, horizonY, w, h - horizonY);
ctx.globalCompositeOperation = 'source-over';
}
// === INIT ===
drawMountains();
animateStars(0);
animateGrid(0);
window.addEventListener('resize', () => {
resizeCanvas(starCanvas);
resizeCanvas(mountainCanvas);
resizeCanvas(gridCanvas);
drawMountains();
// Regenerate stars
stars.length = 0;
for (let i = 0; i < numStars; i++) {
stars.push({
x: Math.random() * starCanvas.width,
y: Math.random() * starCanvas.height * 0.7,
radius: Math.random() * 1.2 + 0.3,
baseAlpha: Math.random() * 0.5 + 0.3,
twinkleSpeed: Math.random() * 0.8 + 0.4,
twinkleOffset: Math.random() * Math.PI * 2,
color: Math.random() > 0.8
? `hsl(${Math.floor(Math.random() * 40)}, 100%, 70%)`
: '#ffffff'
});
}
});
</script>
</body>
</html>
```
This is getting quite long. Let me clean it up and make sure everything works together nicely.
A few issues I notice:
1. The sun reflection's wave distortion isn't properly implemented
2. The radial lines might look too cluttered
3. The sun's position needs to be coordinated with the reflection
4. The mountains might not look right with the border technique
Let me fix these issues and write a cleaner version.
For the sun reflection, I'll implement a proper wave distortion using a mesh or by drawing the reflection as a series of horizontal lines with sine wave offsets.
For the radial lines, I'll make them more subtle with lower opacity.
For the mountains, I'll draw them on canvas with proper shapes and rim lighting.
Let me also reconsider the overall structure. I think using canvas for mountains is better than CSS for more control.
Here's my refined plan:
1. Canvas #stars: starfield (twinkling)
2. Canvas #mountains: mountain silhouettes with rim lighting
3. Canvas #grid: ground fill, sun reflection (with waves), grid lines
4. CSS .sun: glowing sun
5. CSS .horizon: horizon line
6. CSS overlays: vignette, scanlines, scan sweep
Let me write the final version now.
Actually, for the sun reflection, I want to implement a proper wave effect. I can do this by:
1. Drawing the reflection as a filled shape
2. Using an image-data manipulation to create a wavy distortion
3. Or by drawing the reflection as a series of horizontal lines with vertical offsets
The simplest approach is to use `ctx.setTransform()` or draw the reflection with multiple horizontal slices, each offset by a sine wave.
Let me use the horizontal slices approach:
```javascript
function drawSunReflection(ctx, w, h, horizonY, time) {
const reflectionY = horizonY + 50;
const reflectionWidth = 280;
const reflectionHeight = 60;
const reflectionGrad = ctx.createRadialGradient(
w / 2, reflectionY, 10,
w / 2, reflectionY, reflectionWidth / 2
);
// ... gradient stops
// Draw reflection with wave distortion
ctx.save();
// Clip to ground area
ctx.beginPath();
ctx.rect(0, horizonY, w, h - horizonY);
ctx.clip();
// Draw the reflection as horizontal slices with wave offset
const slices = 30;
for (let i = 0; i < slices; i++) {
const t = i / slices;
const y = reflectionY - reflectionHeight / 2 + (reflectionHeight * t);
const waveOffset = Math.sin(t * Math.PI * 3 + time * 0.003) * 8;
// Adjust gradient position for each slice
const sliceGrad = ctx.createRadialGradient(
w / 2 + waveOffset, y, 5,
w / 2 + waveOffset, y, reflectionWidth / 2
);
// ... gradient stops with adjusted alpha
ctx.fillStyle = sliceGrad;
ctx.globalAlpha = 0.4 * (1 - t * 0.7); // fade with height
ctx.fillRect(w / 2 - reflectionWidth / 2 + waveOffset, y, reflectionWidth, reflectionHeight / slices);
}
ctx.restore();
// Fade with distance
const fadeGrad = ctx.createLinearGradient(0, horizonY, 0, h);
fadeGrad.addColorStop(0, 'rgba(0, 0, 10, 0.5)');
fadeGrad.addColorStop(0.7, 'rgba(0, 0, 10, 0.2)');
fadeGrad.addColorStop(1, 'rgba(0, 0, 10, 0)');
ctx.fillStyle = fadeGrad;
ctx.globalCompositeOperation = 'destination-out';
ctx.fillRect(0, horizonY, w, h - horizonY);
ctx.globalCompositeOperation = 'source-over';
}
```
Hmm, this is getting complicated. Let me simplify. For the wave distortion, I'll just draw the reflection as an ellipse and then add wave lines on top of it for the distortion effect.
Actually, a simpler approach: draw the reflection normally (without waves), and add some wavy lines (grid lines distorted by waves) on top to show the water surface.
Let me use this approach:
1. Draw the sun reflection as a static ellipse with gradient
2. Draw wavy "ripple" lines on the water surface
3. Draw the grid lines with slight wave distortion
For the grid lines, I can add a sine wave offset to each band's y position based on its horizontal position, creating a wavy grid effect.
Actually, that would make the grid lines wavy, which might not look right. The grid lines should be straight (they're on the ground plane), but the reflection (above the grid) should have waves.
Let me keep it simple:
1. Grid lines are straight (with proper perspective)
2. Sun reflection is a static ellipse with gradient
3. Some wavy lines on the water surface (for water effect)
4. The grid lines have a slight glow that reflects on the water
OK, I think I'm overthinking this. Let me just write a clean implementation and see how it looks.
Let me write the final code now. I'll focus on getting the core effects right:
1. Sky gradient
2. Starfield (with twinkling)
3. Mountains (silhouette with rim lighting)
4. Grid (perspective, animated, neon glow)
5. Sun (glowing, pulsing)
6. Sun reflection (stretched, faded, with some distortion)
7. Horizon line
8. Scanlines
9. Scan sweep
10. Vignette
Here's my final implementation. Let me write it carefully:
Actually, you know what, let me simplify the reflection. Instead of complex wave distortion, I'll just:
1. Draw a stretched ellipse below the horizon for the sun reflection
2. Use a gradient that fades quickly
3. Add a mask (gradient overlay) to fade it near the viewer
4. Add some white highlights for "water ripples"
And for the grid, I'll add some "ripple" effects on certain grid lines to simulate the grid reflecting on water.
OK, final code time. Let me write it.
I also want to add some additional details:
- A few "neon rectangles" or "buildings" on the ground (silhouetted with neon outlines)
- Some particles floating upward from the horizon (like smoke or energy)
But let me not overcomplicate it. The core elements should be the focus.
Let me write the final HTML file now:
I'll also make sure the animations are smooth and the colors are harmonious. I'll use:
- Purple/pink/cyan for the sky
- Yellow/orange/pink for the sun
- Cyan for the grid
- White/yellow for stars
- Dark for mountains
Let me write it:
Actually, I want to also add a "data rain" effect in the background, like the Matrix green code rain, but in a softer cyan color. This is a common synthwave element. But it might distract from the main elements. Let me skip it for now and focus on the core.
One more thing: I should add some "glitch" effects (like color shifts or horizontal line offsets) occasionally to enhance the VHS feel. I can do this with CSS animations that occasionally shift the colors or position.
Let me add a subtle glitch effect:
```css
@keyframes glitch {
0%, 100% { clip-path: inset(0 0 0 0); }
5% { clip-path: inset(10px 0 5px 0); transform: translate(-3px, 2px); }
10% { clip-path: inset(30px 0 20px 0); transform: translate(2px, -1px); }
15% { clip-path: inset(0 0 0 0); }
}
```
Actually, let me not add glitch effects. They might be distracting. Let me focus on the core elements.
OK, here's my truly final implementation. Let me write it out:
Let me also think about one thing: the grid lines should have a "trail" effect, where brighter lines have a longer glow trail. I can achieve this by drawing the lines with a gradient stroke (from opaque in the center to transparent at the edges).
Or I can just use shadow blur and it'll look good enough.
Let me also add some "sparkle" particles on the grid lines where they're brightest, for extra neon effect.
OK, I think I've planned enough. Let me write the complete, final code now. No more changes.
Let me think about performance. We have:
- Starfield: 350 stars, animated with sin (requestAnimationFrame)
- Grid: 60 bands + 16 radial lines, animated with offset (requestAnimationFrame)
- Sun: CSS animation (handled by browser, should be smooth)
- Scan sweep: CSS animation (smooth)
This should run at 60fps on most modern browsers. The canvas operations are simple enough.
For the mountain canvas, I only need to draw it once (it's static), so I'll draw it on load and on resize.
OK, final code:
Let me also add some text (like "SYNTHWAVE" or "RETRO FUTURE") for extra flavor. But the user didn't ask for text, so let me skip it.
Actually wait, let me re-read the user's request:
"Create an animated synthwave sunset: a glowing gradient sun with scanlines setting over an infinite neon grid that scrolls toward the viewer, with a starfield and a reflected horizon."
Key elements:
1. ✅ Animated synthwave sunset
2. ✅ Glowing gradient sun
3. ✅ Scanlines (setting sun = sun setting, and scanlines as a visual effect)
4. ✅ Infinite neon grid that scrolls toward the viewer
5. ✅ Starfield
6. ✅ Reflected horizon
I've covered all the elements. Let me write the final code.
For the "reflected horizon," I interpret this as:
- The horizon line itself is reflected (maybe as a bright line on the ground)
- Or the sun's reflection on the water/ground creates a reflected horizon effect
I'll implement both:
1. A bright line at the horizon
2. The sun's reflection on the ground below
OK, final code. Writing now.
Let me also make sure the grid's radial lines converge to the center of the horizon, not just the left/right sides. The vanishing point should be at the center.
Actually, for the radial lines, I'll draw lines from different points at the bottom of the screen converging to the vanishing point (center of horizon). This creates the grid pattern.
Let me also think about the grid's "fill." Some synthwave grids have a filled surface (like water or asphalt) with grid lines on top. I'll add a semi-transparent fill for the ground.
OK, I'm going to write the final code now. For real this time.
Let me also reconsider: should the grid have a "water" look or a "road" look? The user mentioned "reflected horizon," which suggests water. I'll make the ground look like a dark, reflective surface (like asphalt or water).
For the water effect:
- Dark ground fill with gradient
- Grid lines with reflection (below the center line)
- Some wave distortion on the reflection
- Some specular highlights on the grid lines
Let me also add some "foam" or "sparkle" on the grid lines where they're closest to the viewer, for a wet pavement effect.
OK, final implementation. Here goes:
Let me finalize the sun position. The user said "setting over" - so the sun is setting (near the horizon). I'll position it near the horizon line.
The sun should be at about 45-50% from the top (the horizon is at 50% from top, so the sun should be just above it).
Let me set:
- Horizon at 50% height
- Sun at about 45% height (slightly above horizon)
- Sun reflection at about 55% height (below horizon)
This creates a nice sunset effect where the sun is just starting to set.
Actually, for a more dramatic effect, I could position the sun partially below the horizon, with only the top visible. But that might make the reflection look weird.
Let me keep the sun just above the horizon for now.
OK, writing the final code. For real.
One more thing about the scanlines: I want them to have a subtle flickering effect (like an old CRT monitor). I can achieve this with a CSS animation that changes the opacity of the scanline pattern.
Also, I want the scan sweep to be slow and occasional, not constant. Maybe it sweeps every 5-10 seconds.
Let me add these final touches.
For the scanlines flickering:
```css
#scanlines {
animation: flicker 3s infinite alternate;
}
@keyframes flicker {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.15; }
}
```
And for the scan sweep, I'll make it slower (8-10 seconds):
```css
#scan-sweep {
animation: scanSweep 10s infinite linear;
}
```
OK, final code now. I'll write it all out carefully.
Let me also think about the `destination-out` composite operation for the reflection fade. I need to make sure the order of operations is correct:
1. Draw ground fill
2. Draw sun reflection (with gradient)
3. Apply fade mask (destination-out with gradient)
This should work correctly.
For the wave distortion in the reflection, I'll draw multiple horizontal lines with sine wave offsets, creating a wavy effect.
Here's a simpler approach for the reflection:
1. Draw the sun reflection as an ellipse with radial gradient
2. On top, draw some wavy lines (sine waves) in a lighter color to simulate water ripples
3. Apply a vertical fade gradient (more transparent at the bottom)
The wavy lines can be drawn as:
```javascript
for (let i = 0; i < 5; i++) {
const y = reflectionY + i * 15;
gridCtx.strokeStyle = `rgba(0, 255, 255, 0.2)`;
gridCtx.lineWidth = 1;
gridCtx.beginPath();
for (let x = 0; x < w; x += 10) {
const wave = Math.sin((x + time * 2) * 0.02 + i) * 3;
if (x === 0) {
gridCtx.moveTo(x, y + wave);
} else {
gridCtx.lineTo(x, y + wave);
}
}
gridCtx.stroke();
}
```
This creates a nice wavy water effect on the reflection.
OK, let me write the final code. I'm going to include:
1. Sky gradient (CSS)
2. Starfield (canvas with twinkling)
3. Mountains (canvas with silhouettes and rim lighting)
4. Grid (canvas with ground fill, grid lines, sun reflection, and water ripples)
5. Sun (CSS with glow and pulsing)
6. Horizon line (CSS)
7. Vignette (CSS)
8. Scanlines (CSS with flickering)
9. Scan sweep (CSS)
Here we go:
Actually, I should think about whether the mountains should be behind the grid or in front of it. In a typical scene:
- Sky (background)
- Mountains (middle ground)
- Grid (foreground, on the ground)
- Sun (in the sky)
- Sun reflection (on the ground/grid)
So the mountain canvas should be between the starfield and the grid (behind the grid). This makes sense with my z-index setup.
But wait, the mountains should be partly below the horizon (they're on the ground) and partly above (peaks). Actually, they're typically fully above the horizon (they're distant hills/mountains on the horizon line).
In a synthwave scene, mountains are usually at the horizon, with their peaks slightly below (since they're tall and the sky behind them is visible).
Let me draw the mountains extending from the bottom of the canvas to slightly above the horizon. They'll be dark silhouettes with some neon rim lighting.
OK, let me finalize and write the code. Here it is:
Actually, one more thing. I want to add some "neon polygons" or "floating geometric shapes" in the background, between the mountains and the grid. These could be triangles, boxes, or other shapes that float and pulse with neon colors.
But this might be too much. Let me focus on the core elements and see if I need to add more.
OK, writing the final code now. For absolutely real this time.
Let me structure the code cleanly:
```html
<!DOCTYPE html>
<html>
<head>
<style>
/* All CSS here */
</style>
</head>
<body>
<div id="scene">
<canvas id="stars"></canvas>
<canvas id="mountains"></canvas>
<canvas id="grid"></canvas>
<div class="sun"></div>
<div class="horizon"></div>
<div class="vignette"></div>
<div id="scanlines"></div>
<div id="scan-sweep"></div>
</div>
<script>
/* All JS here */
</script>
</body>
</html>
```
Let me write the complete implementation:
For the grid, I'll use a more sophisticated approach:
- Draw grid bands with perspective
- Draw radial lines from the sides to the vanishing point
- Add glow effects
- Animate the offset for the scrolling effect
For the sun reflection:
- Draw an ellipse below the horizon
- Use a radial gradient
- Add wave distortion using horizontal lines
- Apply a vertical fade
For the starfield:
- Generate random stars with varying sizes, colors, and twinkle speeds
- Animate with sin/cos for twinkling
- Add occasional shooting stars
Let me write it all out now. I'll be careful with the code and make sure everything works.
Here's the complete implementation:
Hmm, I'm going back and forth too much. Let me just commit to a design and write it. Here's what I'll do:
1. Sky: CSS radial gradient (purple to cyan to dark blue)
2. Stars: Canvas particles with twinkling (white + some yellow)
3. Mountains: Canvas silhouettes with rim lighting
4. Grid: Canvas with:
- Ground gradient fill
- Animated grid lines (horizontal bands + radial lines) with neon glow
- Sun reflection (ellipse with gradient + wave distortion)
- Water ripple effects
5. Sun: CSS radial gradient circle with multiple glows, pulsing
6. Horizon: CSS thin neon line
7. Vignette: CSS dark gradient overlay
8. Scanlines: CSS repeating-linear-gradient with flickering
9. Scan sweep: CSS white line moving down
OK here's the actual code:Looking at this request, I need to create a visually rich, animated synthwave scene with specific elements. Let me build this step by step.
{"command":"ls -la /tmp/ws"}total 0 drwxr-sr-x. 2 node node 6 Aug 1 18:01 . drwxrwsrwx. 3 root node 16 Aug 1 18:01 ..
thinking
OK, the directory is empty. Let me create the index.html file with the complete synthwave sunset animation. Let me write a polished, complete implementation:
Let me create a comprehensive animated synthwave sunset scene with all the requested elements:
/tmp/ws/index.html (14633 bytes)Successfully wrote 14633 bytes to /tmp/ws/index.html
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
Glowing synthwave sun with scanlines, mountains, starfield, and reflected grid horizon; static composition with subtle idle motion, no visible input reaction.
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.