Synthwave sunset · deepseek-v4-flash
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 · 10 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-page web app with an animated synthwave sunset scene. Let me plan the components:
1. **Glowing gradient sun** - a large circle with a neon gradient, setting over the horizon
2. **Scanlines** - horizontal lines across the screen for that retro CRT effect
3. **Infinite neon grid** - a perspective grid that scrolls toward the viewer (like a road into the distance)
4. **Starfield** - twinkling stars in the sky
5. **Reflected horizon** - the sun and grid reflected below the horizon line
Let me build this all in one HTML file with inline CSS and JS.
I'll create a canvas-based approach for performance and more control over the rendering.
Let me think about the design:
- Dark purple/blue sky gradient at top, transitioning to warm orange/pink near the horizon
- A large gradient sun (orange/yellow/pink) just above the horizon line
- A perspective grid (like the classic synthwave grid) that extends from the horizon toward the viewer, with neon pink/purple lines
- The grid scrolls/animated toward the viewer
- Scanlines overlay across the entire screen
- Stars twinkling in the upper portion of the sky
- A reflection of the sun and grid below the horizon (water/ground reflection)
Let me write this out:
I'll use a canvas for the main rendering with requestAnimationFrame for smooth animation.
Let me design the layout:
1. Sky gradient (top to horizon)
2. Stars (random dots, some twinkling)
3. Sun (large circle with radial gradient, positioned at horizon)
4. Horizon line
5. Reflection (flipped sun + grid below)
6. Grid (perspective lines converging to horizon)
7. Scanlines overlay
Let me code this up:
```html
<!DOCTYPE html>
<html>
<head>
<style>
/* styles */
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
// all the rendering code
</script>
</body>
</html>
```
Let me think carefully about the grid. A synthwave grid is typically a perspective grid where lines go from near the viewer (bottom of screen) to a vanishing point at the horizon. The lines are spaced further apart at the bottom and get closer together near the horizon.
For the scrolling animation, I'll shift the grid lines downward over time and wrap them around.
Let me write the full implementation:
For the grid:
- Horizontal lines: y positions that follow perspective (closer near horizon, further at bottom)
- Vertical lines: x positions that converge to a vanishing point at center-horizon
For the animation:
- The grid lines scroll downward (toward the viewer)
- The sun stays in place but maybe pulses
- Stars twinkle
- Maybe the sun slightly moves or the colors shift
Let me write this comprehensively.
Actually, let me think about the reflection. The reflection should show:
- The sun reflected below the horizon (upside down)
- The grid reflected below the horizon (also in perspective, but inverted)
For the reflection, I can simply mirror the rendering or draw a flipped version.
Let me think about the structure more carefully:
1. Draw sky gradient (full canvas)
2. Draw stars (in upper portion)
3. Draw sun (above horizon)
4. Draw reflection area (below horizon) - darker/mirrored
5. Draw reflected sun (below horizon, flipped)
6. Draw grid (both above and below? or just below)
7. Draw scanlines
8. Draw horizon line (glowing)
Actually, traditionally the grid is below the horizon (the ground), and the sky is above. The reflection might be on water below the grid, or the grid itself could be reflected.
Let me go with:
- Top portion: sky with stars and sun
- Middle: horizon line
- Bottom portion: the neon grid (perspective road) with a reflection of the sun and grid below
Wait, actually in classic synthwave art:
- The grid is the "ground" plane
- The sun is in the sky
- The grid stretches from the horizon to the viewer
- Sometimes there's a reflection of the sun on the grid/water
Let me design it as:
- Upper 60%: sky with stars, sun sitting on the horizon line
- Lower 40%: neon grid (perspective) with the sun's reflection on the "water" surface
For the reflection, I'll add a water-like reflection below the horizon that mirrors the sun and the grid colors.
Let me implement this now:
```javascript
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let W, H;
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// Animation variables
let time = 0;
const horizonRatio = 0.55; // horizon at 55% from top
function drawStars(skyHeight) {
// Draw stars in the sky area
}
function drawSun() {
// Draw the gradient sun
}
function drawGrid() {
// Draw the perspective grid
}
function drawScanlines() {
// Draw scanlines overlay
}
function drawReflection() {
// Draw the reflection
}
function animate() {
time += 0.016; // ~60fps
// Clear and draw everything
requestAnimationFrame(animate);
}
```
Let me now write the full, complete implementation. I'll make it really polished with:
- Smooth gradients
- Glow effects using shadows
- Twinkling stars
- Animated scrolling grid
- Scanlines with reduced opacity
- A beautiful color palette (synthwave)
Let me also add some nice touches:
- The sun pulses slightly
- The grid lines have a neon glow
- The horizon has a bright glow line
- Gradient sky with beautiful colors
Here's my full implementation plan:
Color palette:
- Sky top: dark purple (#0a0a2e)
- Sky mid: magenta/pink (#2a0a4a)
- Sky near horizon: orange/pink (#ff6b35, #ff2d78)
- Sun: bright yellow center (#fffb00) to orange (#ff6b35) to pink edges
- Grid: neon pink (#ff00ff) and cyan (#00ffff)
- Stars: white with varying brightness
Let me write the complete code now.
Actually, I want to make this really good. Let me carefully plan each step.
For the grid, I'll use a perspective formula. The vanishing point is at (W/2, H * horizonRatio). Grid lines extend from the vanishing point to the bottom of the screen.
For horizontal lines (perpendicular to the viewing direction), I'll calculate their y-positions based on perspective. The spacing between lines follows a geometric progression.
For vertical lines (parallel to the viewing direction), they radiate from the vanishing point.
The animation scrolls the horizontal lines toward the viewer (downward), cycling them.
Let me code this up properly.
For perspective horizontal lines:
- In 3D, lines are evenly spaced along the z-axis
- Projected to 2D using perspective division
- y = horizonY + (bottomY - horizonY) / (1 + d/z) where z increases
Actually, a simpler approach: use a 1/z curve.
For the grid:
- Let vanishing point be at (cx, horizonY)
- For each horizontal line at z-distance, its y-position on screen follows: y = horizonY + (H - horizonY) * (1 / (1 + k * z))
- Where z is the distance from the viewer
Let me use a simpler approach:
- y positions follow: y = horizonY + (H - horizonY) * (1 - 1/(1 + i * spacing))
- Where i is the line index
For the scrolling, I'll offset the z positions.
Let me define the grid more concretely:
```javascript
const horizonY = H * horizonRatio;
const cx = W / 2; // vanishing point x
// Horizontal lines - perspective spacing
const numLines = 40;
const lineSpacing = 0.15; // controls how quickly lines converge
function drawGrid() {
const offset = time * 0.5; // scroll speed
for (let i = 0; i < numLines; i++) {
const z = (i + offset) % numLines;
const t = z / numLines;
// y position: horizonY at t=0, H at t=1
// Use a sigmoid-like curve for perspective
const y = horizonY + (H - horizonY) * Math.pow(t, 0.4);
// Draw horizontal line
ctx.beginPath();
// The line goes from left edge to right edge, but with perspective
// it should be wider at the bottom
const spread = 1 + t * 2; // some perspective scaling
ctx.moveTo(cx - W/2 * spread, y);
ctx.lineTo(cx + W/2 * spread, y);
ctx.stroke();
}
// Vertical lines - radiating from vanishing point
const numVertLines = 20;
for (let i = 0; i < numVertLines; i++) {
const angle = (i / numVertLines - 0.5) * Math.PI * 0.8;
// Line from vanishing point to bottom
const dx = Math.sin(angle) * W;
const dy = Math.cos(angle) * H;
ctx.beginPath();
ctx.moveTo(cx, horizonY);
ctx.lineTo(cx + dx, H);
ctx.stroke();
}
}
```
Hmm, this is getting complex. Let me just write the full thing now and iterate if needed.
Let me think about the reflection more. The reflection will be below the horizon. I'll draw:
1. A darker/more purple version of the sky gradient below the horizon
2. The sun reflected (flipped vertically)
3. The grid reflected (flipped vertically, but the grid already exists below the horizon)
Actually, for the reflection, I think the simplest approach is:
- The grid IS the reflection area (or the ground)
- The sun has a reflection below the horizon (on the water/ground)
- The reflection is slightly darker and distorted
Let me go with this design:
- Sky (top 60%): gradient dark purple to orange/pink, with stars and sun
- Horizon line (glowing)
- Ground/water (bottom 40%): grid with reflection of the sun
For the reflection of the sun, I'll draw it flipped and with some horizontal distortion (wavy water effect).
Let me also add a "sun glow" that extends below the horizon.
OK, let me just write the full code now. I'll make it beautiful.
Let me think about some specific implementation details:
For the sun gradient:
```javascript
const sunGradient = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, sunRadius);
sunGradient.addColorStop(0, '#ffffff');
sunGradient.addColorStop(0.1, '#fff5a0');
sunGradient.addColorStop(0.3, '#ffcc00');
sunGradient.addColorStop(0.5, '#ff6b35');
sunGradient.addColorStop(0.7, '#ff2d78');
sunGradient.addColorStop(1, 'rgba(255, 45, 120, 0)');
```
For the sky gradient:
```javascript
const skyGradient = ctx.createLinearGradient(0, 0, 0, horizonY);
skyGradient.addColorStop(0, '#0a0a2e');
skyGradient.addColorStop(0.3, '#1a0a3e');
skyGradient.addColorStop(0.6, '#3a0a6e');
skyGradient.addColorStop(0.8, '#ff2d78');
skyGradient.addColorStop(1, '#ff6b35');
```
For the grid, neon colors:
- Pink: #ff00ff
- Cyan: #00ffff
- Or maybe just use pink/magenta lines
For the scanlines:
```javascript
function drawScanlines() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
for (let y = 0; y < H; y += 3) {
ctx.fillRect(0, y, W, 1);
}
}
```
OK, let me write the full implementation now. I'll be thorough.
Let me also add a nice chroma/glow effect to the sun and grid lines using shadow blur.
Actually, shadow blur on canvas can be expensive. Let me use it sparingly.
Let me think about the final structure of the code:
```
1. Setup canvas, resize
2. Draw sky gradient (full height with gradient)
3. Draw stars (generated once, animated twinkle)
4. Draw sun (with glow)
5. Draw horizon glow
6. Draw reflection area (water/ground)
7. Draw reflected sun (with distortion)
8. Draw grid (perspective lines, animated)
9. Draw reflected grid
10. Draw scanlines overlay
11. Draw horizon line
12. Animate (requestAnimationFrame)
```
Wait, actually the grid and reflection overlap. Let me think about the layering:
1. Sky gradient (top portion)
2. Stars (in sky)
3. Sun (at horizon, in sky)
4. Ground/water gradient (bottom portion)
5. Grid (on the ground)
6. Reflected sun (on the water, with distortion)
7. Scanlines (overlay)
8. Horizon line (glowing, on top)
Actually, the reflection of the sun should be on the water surface. In a typical synthwave scene, the grid is the ground and the sun is reflected in it. But the grid is usually a neon grid, and the reflection of the sun is separate.
Let me simplify:
- The grid IS the ground
- The reflection of the sun appears on the ground/water area
- The grid lines go through/over the reflection
I think the best approach is:
1. Draw the ground/water area (dark gradient)
2. Draw the sun's reflection on the water (with some distortion/ripple)
3. Draw the grid on top
4. This way the grid lines appear over the reflection
Actually, let me look at this from the perspective of the classic synthwave aesthetic:
The classic Outrun/synthwave scene has:
- A dark sky with a gradient (dark purple to orange/pink near horizon)
- A large sun (often with horizontal lines through it, like a retro sunset)
- A grid that extends from the horizon to the bottom of the image
- The grid is typically on a dark ground
- Scanlines overlay everything
- Sometimes a reflection of the sun on the ground
Let me go with this. The grid will be the ground, and I'll add a subtle reflection of the sun on the ground area.
Here's my final plan for the drawing order:
1. Sky gradient (top to horizon)
2. Stars (random positions, twinkling)
3. Sun (with glow, positioned at horizon)
4. Ground gradient (horizon to bottom, darker)
5. Sun reflection on ground (flipped, distorted with sine waves, lower opacity)
6. Grid (horizontal and vertical perspective lines, animated scrolling)
7. Horizon glow line
8. Scanlines overlay
Let me now write the complete code.
For the grid animation, I'll scroll the horizontal lines toward the viewer. The vertical lines radiate from the vanishing point.
For the horizontal grid lines:
- I'll compute y-positions using perspective
- The lines will be evenly spaced in "z" (distance from viewer)
- Animation shifts the z values, wrapping around
For the vertical grid lines:
- They radiate from the vanishing point at the horizon
- Evenly spaced angles
Let me code this up properly now.
Actually, thinking about the grid more carefully: in classic synthwave, the grid is made of:
1. Horizontal lines that get closer together as they approach the horizon (perspective)
2. Vertical lines that radiate from the vanishing point
Both are typically neon pink or cyan.
For the horizontal lines, the y-position formula:
```
y = horizonY + (H - horizonY) * (1 - exp(-z * factor))
```
where z goes from 0 (at horizon) to some max value (at bottom).
Or more simply, using a power function:
```
y = horizonY + (H - horizonY) * Math.pow(z, 0.5)
```
where z goes from 0 to 1.
For the scrolling, I offset z:
```
z = ((i + scrollOffset) % numLines) / numLines
```
Let me write the full implementation now.
Actually, I want to make the grid look really good. Let me use a different approach for the horizontal lines.
For perspective, the apparent distance between lines shrinks as they approach the horizon. If lines are evenly spaced in 3D space, their 2D positions follow:
```
y_screen = horizonY + (H - horizonY) * (z / (z + d))
```
where z is the 3D distance and d is a constant.
But for animation, it's easier to just compute the y positions directly:
```javascript
const numLines = 30;
const scrollOffset = (time * 50) % 100; // or something
for (let i = 0; i < numLines; i++) {
// Calculate the normalized position (0=horizon, 1=bottom)
const t = ((i / numLines) + scrollOffset) % 1;
// Apply perspective compression
const y = horizonY + (H - horizonY) * Math.pow(t, 0.3);
// Or use: y = horizonY + (H - horizonY) * (1 - Math.pow(1 - t, 3));
// Draw horizontal line at y
}
```
Hmm, `Math.pow(t, 0.3)` gives too much compression at the top. Let me use `1 - Math.pow(1 - t, 2)` which gives a quadratic acceleration - lines are sparse at the bottom and get dense at the top.
Actually, let me think about this differently. In a perspective view:
- Near the viewer (bottom): lines are far apart
- Near the horizon (top): lines are close together
So the y-position should accelerate as t increases:
```
y = horizonY + (H - horizonY) * t^2
```
Where t goes from 0 to 1. This gives few lines at the bottom and many at the top. But we want evenly spaced lines in 3D...
Actually, for regularly spaced lines in 3D receding from the viewer:
- If lines are at z = 0, 1, 2, 3, ... in 3D
- And the viewer is at z = 0.1 (slightly in front)
- Then y = f * (z / (z + d)) where f is the focal length and d is the distance
Let me just use a simple formula and tune it:
```javascript
// z goes from 0 (at horizon) to maxZ (at bottom)
// y = horizonY + (H - horizonY) * (z / (z + perspectiveConstant))
```
Where perspectiveConstant controls how quickly the lines converge.
For the animation, I'll increase z for all lines over time, wrapping around.
Let me just write the code now. I'll iterate on the visual if needed.
Let me write a complete, polished implementation:
```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 { width: 100%; height: 100%; overflow: hidden; background: #000; }
canvas { display: block; width: 100%; height: 100%; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H, horizonY, cx;
const CONFIG = {
horizonRatio: 0.55,
gridColors: ['#ff00ff', '#ff0080'], // pink to magenta
sunColors: ['#ffffff', '#fff5a0', '#ffcc00', '#ff6b35', '#ff2d78'],
skyColors: [
{ stop: 0, color: '#0a0a2e' },
{ stop: 0.3, color: '#1a0a3e' },
{ stop: 0.5, color: '#5a0a8e' },
{ stop: 0.7, color: '#ff2d78' },
{ stop: 1, color: '#ff6b35' }
],
numStars: 200,
numGridLinesH: 35,
numGridLinesV: 25,
scanlineOpacity: 0.07,
gridScrollSpeed: 0.3,
};
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
horizonY = H * CONFIG.horizonRatio;
cx = W / 2;
}
resize();
window.addEventListener('resize', resize);
// Stars
const stars = [];
for (let i = 0; i < CONFIG.numStars; i++) {
stars.push({
x: Math.random(),
y: Math.random() * CONFIG.horizonRatio * 0.9,
size: 0.5 + Math.random() * 2,
brightness: 0.3 + Math.random() * 0.7,
twinkleSpeed: 0.5 + Math.random() * 2,
twinklePhase: Math.random() * Math.PI * 2,
});
}
function drawSky() {
const grad = ctx.createLinearGradient(0, 0, 0, horizonY);
for (const s of CONFIG.skyColors) {
grad.addColorStop(s.stop, s.color);
}
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, horizonY);
}
function drawStars(time) {
for (const star of stars) {
const twinkle = 0.5 + 0.5 * Math.sin(time * star.twinkleSpeed + star.twinklePhase);
const alpha = star.brightness * twinkle;
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.beginPath();
ctx.arc(star.x * W, star.y * H, star.size * (0.5 + 0.5 * twinkle), 0, Math.PI * 2);
ctx.fill();
}
}
function drawSun(time) {
const sunRadius = Math.min(W, H) * 0.18;
const sunY = horizonY;
const sunX = cx;
// Outer glow
const glowGrad = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, sunRadius * 2.5);
glowGrad.addColorStop(0, 'rgba(255, 100, 50, 0.3)');
glowGrad.addColorStop(0.5, 'rgba(255, 45, 120, 0.15)');
glowGrad.addColorStop(1, 'rgba(255, 45, 120, 0)');
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(sunX, sunY, sunRadius * 2.5, 0, Math.PI * 2);
ctx.fill();
// Sun body
const sunGrad = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, sunRadius);
sunGrad.addColorStop(0, '#ffffff');
sunGrad.addColorStop(0.1, '#fff5a0');
sunGrad.addColorStop(0.3, '#ffcc00');
sunGrad.addColorStop(0.5, '#ff6b35');
sunGrad.addColorStop(0.7, '#ff2d78');
sunGrad.addColorStop(0.9, 'rgba(255, 45, 120, 0.5)');
sunGrad.addColorStop(1, 'rgba(255, 45, 120, 0)');
ctx.fillStyle = sunGrad;
ctx.beginPath();
ctx.arc(sunX, sunY, sunRadius, 0, Math.PI * 2);
ctx.fill();
// Sun horizontal lines (classic synthwave sun)
ctx.save();
ctx.beginPath();
ctx.arc(sunX, sunY, sunRadius, 0, Math.PI * 2);
ctx.clip();
for (let i = 0; i < 20; i++) {
const lineY = sunY - sunRadius + (i / 20) * sunRadius * 2;
ctx.fillStyle = `rgba(0, 0, 0, ${0.08 + 0.04 * Math.sin(i * 0.5)})`;
ctx.fillRect(sunX - sunRadius, lineY, sunRadius * 2, 1.5);
}
ctx.restore();
}
function drawGround(time) {
// Dark gradient for the ground
const grad = ctx.createLinearGradient(0, horizonY, 0, H);
grad.addColorStop(0, '#1a0a2e');
grad.addColorStop(0.2, '#0f0520');
grad.addColorStop(1, '#050210');
ctx.fillStyle = grad;
ctx.fillRect(0, horizonY, W, H - horizonY);
}
function drawSunReflection(time) {
const sunRadius = Math.min(W, H) * 0.18;
const sunX = cx;
// Reflection of the sun on the water/ground
const reflectionHeight = sunRadius * 1.5;
const reflectionGrad = ctx.createLinearGradient(0, horizonY, 0, horizonY + reflectionHeight);
reflectionGrad.addColorStop(0, 'rgba(255, 100, 50, 0.4)');
reflectionGrad.addColorStop(0.3, 'rgba(255, 45, 120, 0.2)');
reflectionGrad.addColorStop(0.6, 'rgba(255, 45, 120, 0.08)');
reflectionGrad.addColorStop(1, 'rgba(255, 45, 120, 0)');
ctx.fillStyle = reflectionGrad;
ctx.fillRect(sunX - sunRadius * 1.5, horizonY, sunRadius * 3, reflectionHeight);
// Wavy reflection lines
ctx.strokeStyle = 'rgba(255, 100, 50, 0.15)';
ctx.lineWidth = 2;
for (let i = 0; i < 15; i++) {
const y = horizonY + (i / 15) * reflectionHeight;
ctx.beginPath();
for (let x = 0; x < W; x += 2) {
const wave = Math.sin(x * 0.02 + time * 2 + i * 0.5) * 5;
const x2 = x - W / 2;
const intensity = Math.max(0, 1 - Math.abs(x2) / (sunRadius * 1.5));
if (x === 0) ctx.moveTo(x, y + wave);
else ctx.lineTo(x, y + wave);
}
ctx.stroke();
}
}
function drawGrid(time) {
const numH = CONFIG.numGridLinesH;
const numV = CONFIG.numGridLinesV;
const scrollSpeed = CONFIG.gridScrollSpeed;
const gridColor = '#ff00ff';
const gridColor2 = '#ff0080';
const gridColor3 = '#cc00ff';
// Horizontal lines (perspective, scrolling toward viewer)
for (let i = 0; i < numH; i++) {
const t = ((i / numH) + (time * scrollSpeed) % 1) % 1;
// Perspective: lines get closer together near horizon
const y = horizonY + (H - horizonY) * (1 - Math.pow(1 - t, 2.5));
if (y > H) continue;
// Opacity and brightness based on distance
const distFromHorizon = (y - horizonY) / (H - horizonY);
const alpha = 0.15 + 0.85 * (1 - distFromHorizon);
const lineWidth = 1 + 3 * (1 - distFromHorizon);
// Width of line in perspective
const perspectiveScale = 0.3 + 0.7 * (1 - distFromHorizon);
const halfWidth = W * 0.5 * (1 + perspectiveScale * 0.5);
const gradient = ctx.createLinearGradient(cx - halfWidth, y, cx + halfWidth, y);
gradient.addColorStop(0, 'rgba(255, 0, 255, 0)');
gradient.addColorStop(0.1, `rgba(255, 0, 255, ${alpha * 0.5})`);
gradient.addColorStop(0.3, `rgba(255, 0, 128, ${alpha})`);
gradient.addColorStop(0.5, `rgba(255, 0, 255, ${alpha})`);
gradient.addColorStop(0.7, `rgba(255, 0, 128, ${alpha})`);
gradient.addColorStop(0.9, `rgba(255, 0, 255, ${alpha * 0.5})`);
gradient.addColorStop(1, 'rgba(255, 0, 255, 0)');
ctx.strokeStyle = gradient;
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.moveTo(cx - halfWidth, y);
ctx.lineTo(cx + halfWidth, y);
ctx.stroke();
}
// Vertical lines (radiating from vanishing point)
for (let i = 0; i < numV; i++) {
const angle = ((i / numV) - 0.5) * Math.PI * 0.7;
const dx = Math.sin(angle);
const dy = Math.cos(angle);
// Line from vanishing point to bottom
const endY = H;
const endX = cx + dx * (endY - horizonY) / dy;
if (endX < -W * 0.5 || endX > W * 1.5) continue;
const distFromCenter = Math.abs(angle) / (Math.PI * 0.35);
const alpha = 0.2 + 0.8 * (1 - distFromCenter);
ctx.strokeStyle = `rgba(255, 0, 255, ${alpha * 0.6})`;
ctx.lineWidth = 1 + 2 * (1 - distFromCenter);
ctx.beginPath();
ctx.moveTo(cx, horizonY + 5);
ctx.lineTo(endX, endY);
ctx.stroke();
}
}
function drawScanlines() {
ctx.fillStyle = `rgba(0, 0, 0, ${CONFIG.scanlineOpacity})`;
for (let y = 0; y < H; y += 2) {
ctx.fillRect(0, y, W, 1);
}
}
function drawHorizonGlow() {
// Glowing horizon line
const glowGrad = ctx.createLinearGradient(0, horizonY - 10, 0, horizonY + 10);
glowGrad.addColorStop(0, 'rgba(255, 100, 50, 0)');
glowGrad.addColorStop(0.5, 'rgba(255, 100, 50, 0.8)');
glowGrad.addColorStop(1, 'rgba(255, 100, 50, 0)');
ctx.fillStyle = glowGrad;
ctx.fillRect(0, horizonY - 10, W, 20);
// Sharp horizon line
ctx.strokeStyle = 'rgba(255, 150, 80, 0.6)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, horizonY);
ctx.lineTo(W, horizonY);
ctx.stroke();
}
function drawVignette() {
const grad = ctx.createRadialGradient(cx, H / 2, H * 0.3, cx, H / 2, H * 0.8);
grad.addColorStop(0, 'rgba(0, 0, 0, 0)');
grad.addColorStop(1, 'rgba(0, 0, 0, 0.5)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
function animate(timestamp) {
const time = timestamp / 1000;
// Clear
ctx.clearRect(0, 0, W, H);
// Draw in order
drawSky();
drawStars(time);
drawSun(time);
drawGround(time);
drawSunReflection(time);
drawGrid(time);
drawHorizonGlow();
drawScanlines();
drawVignette();
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
</script>
</body>
</html>
```
This is a solid implementation. Let me refine it a bit more and make sure everything is polished.
Let me think about what could be improved:
1. The grid gradient colors - make them more vibrant
2. The sun horizontal lines - make them more visible and classic synthwave style
3. The reflection - make it look more like water
4. Add some chromatic aberration or color fringing for that retro feel
Let me also add:
- A subtle grid on the ground (the classic synthwave grid with both horizontal and vertical lines)
- The reflection of the grid as well
- Maybe some particle effects or subtle animations
Actually, let me also make the grid more 3D-looking. The horizontal lines should also have perspective in their width - wider at the bottom, narrower at the top.
Let me also think about the color palette more carefully. Synthwave/outrun typically uses:
- Dark purples and blues for the sky
- Neon pink, magenta, and cyan for the grid
- Orange/yellow for the sun
- The whole thing has a warm, nostalgic feel
Let me also add a subtle "chromatic aberration" effect by offsetting the red and blue channels of the sun slightly.
I think my current implementation is good. Let me refine it a bit and write the final version.
One more thing - I should add some "glow" or bloom effect to the bright parts. I can simulate this with radial gradients.
Let me finalize the code now. I'll write the complete file.
Actually, let me reconsider the grid. In classic synthwave:
The grid is typically drawn with horizontal lines that are parallel to the horizon, and vertical lines that radiate from the vanishing point. The vertical lines give the perspective/depth effect.
The horizontal lines scroll toward the viewer (downward), creating the illusion of movement.
Let me also make the vertical lines more prominent - they should have a neon glow.
OK, let me finalize and write the code. I'll make it really polished.
One more thought: for the "reflected horizon" part of the user's request, I should make the reflection more prominent. Let me add:
1. A reflected/upside-down sun below the horizon
2. Reflected grid lines below the horizon
3. The reflection should have a water-like shimmer
Let me update my implementation to include a proper reflected grid as well, where the grid continues below the horizon but flipped.
Actually, the grid IS the ground/water. So the grid already extends from the horizon to the bottom. The reflection of the sun sits on top of the grid. This is the classic look.
Let me finalize the code now. I'll make it self-contained and polished.
Let me also think about making the reflection more "water-like" with animated horizontal wave distortion.
I think I have a good plan. Let me write the final code.
Actually, I want to reconsider the "reflected horizon" part. The user said "with a reflected horizon". This could mean:
1. The horizon is reflected in water below
2. Or there's a mirror-like reflection of the entire scene
I think option 1 is more in line with synthwave. Let me add a water reflection that shows the horizon (the sun and the sky) reflected below.
So the drawing order would be:
1. Sky gradient
2. Stars
3. Sun
4. Water/ground area (darker gradient)
5. Reflection of the sky and sun below the horizon (flipped, with distortion)
6. Grid (on top of the reflection)
7. Horizon glow
8. Scanlines
9. Vignette
This way the reflection shows through the grid, creating a beautiful layered effect.
Let me implement this now.
Actually, for the reflection of the sky below the horizon, I can draw a flipped version of the sky gradient with lower opacity. And for the sun reflection, I'll draw a flipped sun with wavy distortion.
Let me write the final code now. I'll be thorough and make it look amazing.
Here's my refined approach for the reflection:
```javascript
function drawReflection(time) {
// Draw reflected sky gradient
const grad = ctx.createLinearGradient(0, horizonY, 0, H);
// Mirror the sky colors but darker
const skyColors = [...CONFIG.skyColors].reverse();
for (const s of skyColors) {
grad.addColorStop(1 - s.stop * 0.7, s.color.replace(')', ', 0.15)').replace('rgb', 'rgba'));
}
// Actually, let me just use a simple gradient
...
}
```
Hmm, this is getting complicated. Let me simplify.
For the reflection, I'll draw:
1. A dark gradient for the water/ground
2. The reflection of the sun (flipped, wavy, with lower opacity)
3. The grid on top
The reflection of the sun will be drawn as a series of horizontal glowing lines that get more distorted and fade out as they go lower.
Let me write the final implementation now. I'll keep it clean and beautiful.
Let me also add the "sun horizontal lines" effect that's classic in synthwave - where the sun has horizontal cut lines through it.
OK, I'm going to write the complete file now. Let me be comprehensive.
Here is my final plan for the complete code:
```
1. HTML structure with canvas
2. CSS for full-screen black background
3. JavaScript:
a. Canvas setup and resize handler
b. Star generation
c. Drawing functions:
- drawSky(): linear gradient sky
- drawStars(): animated twinkling stars
- drawSun(): radial gradient sun with glow, horizontal lines
- drawGround(): dark gradient for ground/water
- drawReflection(): flipped sun reflection with wavy distortion
- drawGrid(): perspective grid with horizontal (scrolling) and vertical lines
- drawHorizonGlow(): glowing horizon line
- drawScanlines(): CRT scanlines overlay
- drawVignette(): dark corners
d. Animation loop with requestAnimationFrame
```
Let me write it all now. I'll aim for visual quality and smooth animation.
One more consideration: the grid should look like it's on a flat plane receding into the distance. The horizontal lines get closer together as they approach the horizon, and the vertical lines converge at the vanishing point.
For the horizontal lines, I'll use the formula:
```
y = horizonY + (H - horizonY) * (1 - exp(-z * factor))
```
where z is evenly spaced.
Or more simply:
```
t = (i / numLines) // 0 = horizon, 1 = bottom
y = horizonY + (H - horizonY) * t^2 // quadratic gives perspective
```
For the scrolling, I offset i:
```
i_eff = (i + offset) % numLines
t = i_eff / numLines
```
This should give a nice scrolling effect.
Let me finalize and write the code!
I realize I should also add the grid reflection. In the classic synthwave look, the grid continues below the horizon with a reflection. But actually, in most interpretations, the grid IS the ground, and it's reflected in the water below. Or the grid is on the ground and there's a separate water reflection.
Let me go with: the grid is on the ground, and the sun's reflection appears on the ground (water surface) with distortion. The grid lines pass through/over the reflection.
OK let me just write it. I've been thinking too much./workspace/index.html (11184 bytes)Successfully wrote 11184 bytes to /workspace/index.html
thinking
The file has been written. Let me verify it's well-formed and check the file size to make sure everything is there.
{"command":"wc -l /workspace/index.html && wc -c /workspace/index.html"}381 /workspace/index.html 11658 /workspace/index.html
thinking
Let me also verify the HTML is well-formed by checking the structure.
{"path":"/workspace/index.html","limit":5}<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> [377 more lines in file. Use offset=6 to continue.]
thinking
Let me verify the file ends properly.
{"path":"/workspace/index.html","offset":375}requestAnimationFrame(animate); } requestAnimationFrame(animate); </script> </body> </html>
thinking
The file is complete and well-formed. Let me provide a summary of what was built.
The file is complete — **`/workspace/index.html`** (381 lines, ~11.7 KB). Here's what it renders: **🔮 Synthwave Sunset** — a self-contained animated canvas scene with: | Layer | Details | |---|---| | **Sky** | Linear gradient from deep purple `#050110` → magenta `#ff2070` → orange `#ff6b35` → gold `#ffaa40` at the horizon | | **Starfield** | 220 twinkling stars with randomized size, brightness, and twinkle speed/phase | | **Sun** | Radial gradient with white/yellow core → orange → pink edge, plus classic horizontal cut-lines, outer glow halos, and subtle chromatic aberration (red/cyan offset) | | **Horizon glow** | Pulsing warm glow band with a sharp bright line at the horizon | | **Reflected horizon** | Below the horizon: mirrored sun glow with animated sine-wave distortion lines (water ripple effect), fading out with depth | | **Neon grid** | 40 horizontal perspective lines scrolling toward the viewer (wrapping infinite scroll), and 28 radiating vertical lines converging at the vanishing point. Neon pink/magenta with glow shadows | | **Scanlines** | CRT-style alternating dark lines at low opacity | | **Vignette** | Subtle darkening at the corners for atmosphere | | **Chromatic aberration** | Faint full-screen red/cyan fringe for retro VHS feel | All rendering is done on a single `<canvas>` with `requestAnimationFrame` — no external assets, CDNs, or network requests.
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
Animated synthwave sunset with scanlined sun, neon grid, starfield, and reflected horizon; no interactivity beyond passive animation.
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.