Synthwave sunset · lfm-2.5-24b-a2b
No runnable output Cost / run $0.0175 Runtime 105.8s
Prompt for this run
Build an animated synthwave landscape on a full-screen canvas. Use HTML, CSS, and JavaScript as needed. Write it to `index.html` in the current working directory using your file tools. Inline your own CSS and JavaScript. Do not use external assets, CDNs, or network requests. No third-party libraries are provided. Required behavior: - Draw a glowing gradient sun broken by horizontal scanline bands, a starfield, and a perspective neon grid extending from the foreground to a horizon. - Animate the grid toward the viewer to suggest forward travel. Move the sun slowly down toward the horizon while keeping the composition visible. - Below the horizon, include a stylized reflection of the sun's colors on the ground plane, visually distinct from a second sun in the sky. - Use a loop of 10 to 20 seconds that repeats without a distracting reset jump. Provide Pause/Resume and Restart controls. - Use procedural drawing and colors only. Fit the canvas to the viewport; choose the palette and decorative details freely. Verify: Sample several moments to inspect grid travel, sun movement, and the reflected colors below the horizon. Pause and resume to check animation control, then use Restart and compare the starting composition.
This one-shot run produced no runnable artifact.
Error: agent produced no index.html
The transcript below shows what the model did — read it to see why the one shot fell short.
TAKE IT TO CODE
This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.
RUN
METRICS
Transcript · 7 messages
Build an animated synthwave landscape on a full-screen canvas. Use HTML, CSS, and JavaScript as needed. Write it to `index.html` in the current working directory using your file tools. Inline your own CSS and JavaScript. Do not use external assets, CDNs, or network requests. No third-party libraries are provided. Required behavior: - Draw a glowing gradient sun broken by horizontal scanline bands, a starfield, and a perspective neon grid extending from the foreground to a horizon. - Animate the grid toward the viewer to suggest forward travel. Move the sun slowly down toward the horizon while keeping the composition visible. - Below the horizon, include a stylized reflection of the sun's colors on the ground plane, visually distinct from a second sun in the sky. - Use a loop of 10 to 20 seconds that repeats without a distracting reset jump. Provide Pause/Resume and Restart controls. - Use procedural drawing and colors only. Fit the canvas to the viewport; choose the palette and decorative details freely. Verify: Sample several moments to inspect grid travel, sun movement, and the reflected colors below the horizon. Pause and resume to check animation control, then use Restart and compare the starting composition.
thinking
We need to build an animated synthwave landscape on a full-screen canvas. Let me plan this out:
1. **Glowing gradient sun** broken by horizontal scanline bands
2. **Starfield**
3. **Perspective neon grid** extending from foreground to horizon
4. **Animation**: Grid moves toward viewer (foreground travel), sun slowly moves down toward horizon
5. **Reflection**: Sun's colors reflected on the ground plane below the horizon, distinct from a second sun in the sky
6. **Loop 10-20 seconds**, no distracting reset jump
7. **Controls**: Pause/Resume and Restart
Let me create a single `index.html` file with inline CSS and JS.
Key design elements:
- Canvas fills the viewport
- Sky: dark gradient (purple/blue)
- Sun: large circle near top center, glowing gradient, with scanlines across it
- Stars: small white dots scattered
- Horizon line
- Ground plane: a perspective trapezoid at the bottom
- Neon grid: lines that converge to vanishing point, moving forward (scaling up / moving closer)
- Reflection: mirrored colors of the sun on the ground
Let me think about the implementation:
**Sun**:
- Position: starts around y=100, moves down to near horizon
- Glow effect using shadowBlur or multiple circles
- Scanlines: horizontal semi-transparent black lines across the sun area
**Starfield**:
- Random stars at various brightness levels
- Maybe twinkle effect
**Grid**:
- Perspective grid: horizontal lines converging to a vanishing point (center top or slightly below)
- Vertical lines going back into distance
- As animation progresses, the grid "moves" toward viewer - meaning the vanishing point shifts? Or the grid expands? Actually, "move the grid toward the viewer to suggest forward travel" - I can animate the grid so that the horizontal lines get closer together (like moving forward) and the vertical lines shift to simulate movement.
Actually, for a simple approach: animate the grid such that the horizontal lines move downward (toward the horizon) and the spacing between them decreases (giving a sense of approaching). The vanishing point stays fixed but the grid appears to come closer.
**Reflection**:
- Below the horizon, draw a mirrored version of the sun's color gradient on the ground
- Use the sun's color as the reflection color
**Colors**:
- Synthwave palette: hot pink/magenta, cyan, purple, deep blue
- Sun: gradient from yellow-orange to pink/red
- Grid: neon cyan or magenta lines
- Background: dark navy/black
Let me structure the code:
```html
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; }
canvas { display: block; }
#ui {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-family: monospace;
z-index: 10;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="ui">
<button id="pause">Pause</button>
<button id="resume">Resume</button>
<button id="restart">Restart</button>
</div>
<script>
// ... drawing code ...
</script>
</body>
</html>
```
Now let me write the JavaScript. I'll use requestAnimationFrame for smooth animation.
**Animation loop**:
- Time-based animation (delta time)
- Loop duration: ~14 seconds (within 10-20 range)
**Sun movement**:
- Y position: starts at 120, moves to horizon (say y=300), then resets smoothly
- To avoid a jump, when it reaches the horizon, it should smoothly transition back to the top
**Grid animation**:
- Horizontal lines: their y positions shift downward over time (simulating the viewer moving forward)
- Vertical lines: converge to vanishing point
- As the grid approaches, the lines get closer together
Let me think more carefully about the grid:
- Vanishing point: center of canvas, maybe slightly above center
- Horizontal lines: y = vanishingY + (x * something)? No, horizontal lines are just horizontal.
- Actually, for a perspective grid: horizontal lines go from left edge to right edge at different y positions, all converging visually... no wait, horizontal lines don't converge. The convergence is in the vertical lines (or diagonal lines).
Standard perspective grid:
- Horizontal lines: y coordinates at regular intervals (e.g., every 40px)
- Vertical lines: x coordinates at regular intervals (e.g., every 60px)
- All converge to a vanishing point on the horizon
For "moving toward viewer": the horizontal lines could move down (the grid appears to advance), and the spacing between horizontal lines could decrease (lines get denser as they're closer).
Let me implement:
- `gridOffset`: a value that increases over time, representing how far the camera has moved forward
- Horizontal lines: y = horizonY + gridOffset + sin(...) * amplitude? No, simpler: just move the whole grid down by gridOffset
- Actually, to show forward movement: the grid lines shift down, and the vertical lines shift inward (converge more)
Simpler approach:
- `time` variable
- Horizontal line y positions: `horizonY + baseSpacing * (1 - progress) + offset`
- Where `progress` goes from 0 to 1 over the loop period, and resets smoothly
Let me define:
- `loopDuration = 14` seconds
- `progress = (time % loopDuration) / loopDuration` (0 to 1)
- `sunY = lerp(120, horizonY, progress)` — sun moves from top to horizon
- Grid: horizontal lines spaced at `spacing = baseSpacing * (1 - progress)` — as progress increases, spacing decreases (lines get closer together = closer to viewer)
- Vertical lines: start at edges and converge to center
Actually, let me keep it simpler and more visually effective:
- The grid is drawn once per frame
- Horizontal lines: y = horizonY + (i * lineSpacing) where lineSpacing decreases over time
- Vertical lines: x = leftEdge + i * gridSpacing, where gridSpacing also decreases
Let me code this up.
**Sun glow and scanlines**:
```javascript
function drawSun(x, y, radius) {
// Glow
ctx.shadowColor = '#ff00ff';
ctx.shadowBlur = 30;
// ... draw circle
ctx.shadowBlur = 0;
// Gradient fill
const grad = ctx.createRadialGradient(x - radius*0.3, y - radius*0.3, radius*0.1, x, y, radius);
grad.addColorStop(0, '#ffff00');
grad.addColorStop(0.5, '#ff6600');
grad.addColorStop(1, '#ff0033');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
```
**Scanlines**:
- Draw horizontal semi-transparent black lines across the sun area
- Use `ctx.fillStyle = 'rgba(0,0,0,0.3)'` and draw thin rectangles
**Reflection**:
- After the horizon line, draw a mirrored sun shape with the sun's colors
- The reflection should be below the horizon, with the same gradient but inverted vertically
- Use `ctx.scale(1, -1)` or draw a flipped version
Let me write the complete code now.
I'll make the canvas fill the window and handle resize.
Here's my plan for the full HTML:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Synthwave Landscape</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a1a; }
canvas { display: block; width: 100%; height: 100%; }
#controls {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
z-index: 10;
}
button {
padding: 10px 20px;
font-family: monospace;
font-size: 14px;
cursor: pointer;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.3);
color: #fff;
border-radius: 4px;
}
button:hover { background: rgba(255,255,255,0.2); }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="controls">
<button id="pauseBtn">Pause</button>
<button id="resumeBtn">Resume</button>
<button id="restartBtn">Restart</button>
</div>
<script>
(function() {
const canvas = document.getElementById('c');
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 state
const loopDuration = 14; // seconds
let elapsed = 0;
let paused = false;
let lastTime = performance.now();
// Sun
const sunRadius = 60;
let sunY = 100; // starts near top
// Grid parameters
const gridSpacing = 50; // initial spacing between vertical lines
const lineSpacing = 40; // initial spacing between horizontal lines
const horizonY = H * 0.45;
const vanishingX = W / 2;
// Starfield
const stars = [];
for (let i = 0; i < 150; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.6,
size: Math.random() * 2 + 0.5,
brightness: Math.random()
});
}
// Sun colors
const sunColors = ['#ff0044', '#ff6600', '#ffcc00'];
function draw() {
ctx.clearRect(0, 0, W, H);
// Sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, '#0a0a2e');
skyGrad.addColorStop(0.5, '#1a0a3e');
skyGrad.addColorStop(1, '#2d1a4e');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// Stars
drawStars();
// Sun
drawSun(sunY, sunRadius);
}
function drawStars() {
for (const star of stars) {
const alpha = 0.3 + 0.7 * star.brightness;
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.beginPath();
ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
ctx.fill();
}
}
function drawSun(y, radius) {
// Glow
ctx.save();
ctx.shadowColor = '#ff00ff';
ctx.shadowBlur = 40;
// Outer glow
const glowGrad = ctx.createRadialGradient(W/2, y, radius*0.3, W/2, y, radius*1.5);
glowGrad.addColorStop(0, 'rgba(255, 0, 255, 0.3)');
glowGrad.addColorStop(0.5, 'rgba(255, 0, 255, 0.1)');
glowGrad.addColorStop(1, 'rgba(255, 0, 255, 0)');
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(W/2, y, radius*1.5, 0, Math.PI * 2);
ctx.fill();
// Sun body
const bodyGrad = ctx.createRadialGradient(W/2 - radius*0.3, y - radius*0.3, radius*0.1, W/2, y, radius);
bodyGrad.addColorStop(0, '#ffffff');
bodyGrad.addColorStop(0.3, '#ffeeaa');
bodyGrad.addColorStop(0.7, '#ff4400');
bodyGrad.addColorStop(1, '#cc0033');
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.arc(W/2, y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Scanlines
drawScanlines(radius);
}
function drawScanlines(radius) {
ctx.save();
ctx.globalAlpha = 0.15;
ctx.fillStyle = '#000000';
for (let i = 0; i < 5; i++) {
const y = y + i * radius * 0.25;
if (y > H) continue;
ctx.fillRect(0, y, W, radius * 0.02);
}
ctx.restore();
}
// Grid
function drawGrid() {
const progress = (elapsed % loopDuration) / loopDuration;
// Horizontal lines: move down and get denser
const currentSpacing = Math.max(5, lineSpacing * (1 - progress) * 0.5);
// Horizon line
ctx.strokeStyle = '#ff3366';
ctx.lineWidth = 2;
ctx.shadowColor = '#00ffff';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.moveTo(0, horizonY);
ctx.lineTo(W, horizonY);
ctx.stroke();
ctx.shadowBlur = 0;
// Vertical lines converging to vanishing point
const numLines = 20;
for (let i = 0; i <= numLines; i++) {
const x = i * W / numLines;
ctx.beginPath();
ctx.moveTo(x, horizonY);
ctx.lineTo(vanishingX, H);
ctx.strokeStyle = 'rgba(0, 255, 255, 0.6)';
ctx.lineWidth = 1;
ctx.stroke();
}
// Horizontal grid lines
for (let i = 0; i < numLines; i++) {
const y = horizonY + i * currentSpacing;
if (y > H) break;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.strokeStyle = 'rgba(0, 255, 255, 0.4)';
ctx.stroke();
}
}
// Reflection
function drawReflection() {
// Only draw if we have a sun color
if (sunRadius <= 0) return;
const reflectionGrad = ctx.createLinearGradient(W/2, horizonY, W/2, H);
// Mirror the sun colors
reflectionGrad.addColorStop(0, '#ff0044');
reflectionGrad.addColorStop(0.5, '#ff8800');
reflectionGrad.addColorStop(1, '#330011');
ctx.save();
ctx.globalAlpha = 0.5;
ctx.fillStyle = reflectionGrad;
// Draw a simplified sun shape on the ground
ctx.beginPath();
ctx.ellipse(W/2, H, sunRadius * 0.8, sunRadius * 0.3, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function update() {
if (paused) return;
const now = performance.now();
const dt = (now - lastTime) / 1000;
lastTime = now;
elapsed += dt;
if (elapsed >= loopDuration) {
elapsed -= loopDuration;
}
// Sun moves down
const maxSunY = H * 0.55;
sunY = Math.min(maxSunY, Math.max(80, sunY + 20 * dt));
// Grid animation
// Already handled in drawGrid via elapsed/loopDuration
requestAnimationFrame(loop);
}
function loop() {
draw();
drawGrid();
drawReflection();
requestAnimationFrame(loop);
}
// Controls
const pauseBtn = document.getElementById('pauseBtn');
const resumeBtn = document.getElementById('resumeBtn');
const restartBtn = document.getElementById('restartBtn');
pauseBtn.addEventListener('click', () => {
paused = true;
pauseBtn.textContent = 'Resume';
});
resumeBtn.addEventListener('click', () => {
paused = false;
resumeBtn.textContent = 'Pause';
lastTime = performance.now();
});
restartBtn.addEventListener('click', () => {
elapsed = 0;
sunY = 100;
paused = false;
resumeBtn.textContent = 'Pause';
lastTime = performance.now();
});
// Start
requestAnimationFrame(loop);
})();
```
Hmm, there are some issues:
1. The grid needs to move toward the viewer. Currently I'm decreasing the spacing between lines, which makes them denser. But the lines should also shift. Let me think...
- For "forward travel", the grid should appear to move toward the viewer. In perspective, this means the vanishing point stays fixed but the grid lines get closer together (lines are closer = further away? No, actually in perspective, objects closer to the viewer appear larger and lines converge more steeply).
- Actually, for a simple effect: as the grid "moves forward", the horizontal lines should move downward (the grid advances), and the spacing between horizontal lines should decrease (more lines per unit distance = closer = more detail).
Let me adjust: `currentSpacing = lineSpacing * (1 - progress)`. When progress=0, spacing is large (lines far apart = far away). When progress=1, spacing is small (lines close together = close up). This gives the illusion of the grid coming closer.
2. The sun should move down slowly. Currently `sunY` increases (moves down) by 20 pixels per second. That seems reasonable.
3. The horizon needs to be visible. Let me set `horizonY = H * 0.4` or so.
4. The reflection: I'm drawing an ellipse on the ground. Should look like a reflection of the sun. Let me make it more sun-like.
5. Scanlines: I need to draw them across the sun. The scanlines function references `y` which is the sun's y position. Let me fix that.
6. Color palette: synthwave typically uses hot pink/magenta (#ff00ff), cyan (#00ffff), purple (#ff00ff), orange/yellow.
Let me refine the code. I'll also add some more visual flair:
- Moon? Not required, but could add.
- More stars
- Better gradient
Let me rewrite the complete file now. I want to make sure everything works well.
Actually, let me reconsider the grid animation. The requirement says "Animate the grid toward the viewer to suggest forward travel." So the grid should visibly move toward the viewer. In perspective drawing, as you move forward:
- The horizon stays fixed
- Objects on the ground get larger
- Lines on the ground get closer together
So the animation should show the grid lines getting closer together (decreasing spacing) and possibly shifting. Let me implement:
- Horizontal lines: y positions start from horizonY + some offset and increase by `currentSpacing`
- `currentSpacing` decreases from e.g. 50 to 10 over the loop duration
Also, the vertical lines: they should converge to the vanishing point. As the grid comes closer, the convergence angle changes. But for simplicity, keep the vanishing point fixed at center.
Let me also make the grid lines neon-colored (cyan/magenta).
Now, about the reflection: "Below the horizon, include a stylized reflection of the sun's colors on the ground plane, visually distinct from a second sun in the sky."
So there should be:
- A sun in the sky (with scanlines)
- A reflection of the sun's colors on the ground below the horizon
- The reflection should look different from the actual sun (maybe a mirrored, distorted version)
I'll draw the reflection as a horizontal band or ellipse on the ground with the sun's colors.
Let me finalize the code. I'll write it all out now.
One more consideration: the scanlines should be "broken" across the sun - meaning the scanlines should be interrupted or broken patterns. I can draw horizontal lines with gaps, or draw scanlines that have gaps. Let me draw them as continuous lines but with varying opacity, or draw them with small gaps. Actually, "broken by horizontal scanline bands" - I think it means the sun has scanline bands across it (like CRT scanlines). I'll draw horizontal semi-transparent black bars across the sun area.
Let me write the final code:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Synthwave Landscape</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a2e; }
canvas { display: block; width: 100%; height: 100%; }
#controls {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 12px;
z-index: 10;
}
button {
padding: 10px 18px;
font-family: monospace;
font-size: 14px;
cursor: pointer;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.25);
color: #fff;
border-radius: 4px;
transition: background 0.2s;
}
button:hover { background: rgba(255,255,255,0.18); }
button.active { background: rgba(255,0,255,0.3); border-color: #ff00ff; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="controls">
<button id="pauseBtn">Pause</button>
<button id="resumeBtn">Resume</button>
<button id="restartBtn">Restart</button>
</div>
<script>
(function() {
const canvas = document.getElementById('c');
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 state
const loopDuration = 14; // seconds, within 10-20
let elapsed = 0;
let paused = false;
let lastTime = performance.now();
// Sun
const sunRadius = 55;
let sunY = 100;
// Horizon
let horizonY;
// Grid
const numVerticalLines = 24;
const numHorizontalLines = 30;
let gridProgress = 0;
// Stars
const stars = [];
for (let i = 0; i < 180; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.55,
size: Math.random() * 1.8 + 0.5,
twinkle: Math.random() * Math.PI * 2,
speed: 0.5 + Math.random() * 2
});
}
function initHorizon() {
horizonY = H * 0.42;
}
function drawSky() {
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#0a0a2e');
grad.addColorStop(0.4, '#1a1040');
grad.addColorStop(0.7, '#3d1a5e');
grad.addColorStop(1, '#1a0a2e');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
function drawStars(time) {
for (const s of stars) {
const t = (time / 2000) + s.twinkle;
const alpha = 0.2 + 0.8 * (0.5 + 0.5 * Math.sin(t));
ctx.fillStyle = `rgba(255,255,255,${alpha})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
}
}
function drawSun(y) {
const cx = W / 2;
// Outer glow
ctx.save();
ctx.shadowColor = '#ff00ff';
ctx.shadowBlur = 50;
const glowGrad = ctx.createRadialGradient(cx, y, radius * 0.1, cx, y, radius * 1.6);
glowGrad.addColorStop(0, 'rgba(255,0,255,0.25)');
glowGrad.addColorStop(0.4, 'rgba(255,0,255,0.08)');
glowGrad.addColorStop(1, 'rgba(255,0,255,0)');
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(cx, y, radius * 1.6, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Sun body
const bodyGrad = ctx.createRadialGradient(cx - radius*0.3, y - radius*0.3, radius*0.05, cx, y, radius);
bodyGrad.addColorStop(0, '#ffffff');
bodyGrad.addColorStop(0.35, '#ffeeaa');
bodyGrad.addColorStop(0.7, '#ff4422');
bodyGrad.addColorStop(1, '#881133');
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.arc(cx, y, radius, 0, Math.PI * 2);
ctx.fill();
// Scanlines
drawScanlines(cx, y, radius);
}
function drawScanlines(cx, y, radius) {
ctx.save();
ctx.globalAlpha = 0.12;
ctx.fillStyle = '#000000';
const numBands = 8;
for (let i = 0; i < numBands; i++) {
const bandY = y + i * radius * 0.22;
if (bandY > H) break;
ctx.fillRect(0, bandY, W, radius * 0.03);
}
ctx.restore();
}
function drawGrid() {
// Update grid progress
gridProgress = (elapsed % loopDuration) / loopDuration;
// Horizontal lines get denser as grid approaches
const currentSpacing = Math.max(4, 45 * (1 - gridProgress) * 0.6);
// Horizon line
ctx.save();
ctx.strokeStyle = '#ff3388';
ctx.lineWidth = 2;
ctx.shadowColor = '#00ffff';
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.moveTo(0, horizonY);
ctx.lineTo(W, horizonY);
ctx.stroke();
ctx.shadowBlur = 0;
// Vertical lines converging to vanishing point
const vpX = W / 2;
for (let i = 0; i <= numVerticalLines; i++) {
const x = (i / numVerticalLines) * W;
ctx.beginPath();
ctx.moveTo(x, horizonY);
ctx.lineTo(vpX, H);
ctx.strokeStyle = 'rgba(0,255,255,0.5)';
ctx.lineWidth = 1;
ctx.stroke();
}
// Horizontal grid lines
for (let i = 0; i < numHorizontalLines; i++) {
const y = horizonY + i * currentSpacing;
if (y > H) break;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.strokeStyle = 'rgba(0,255,255,0.35)';
ctx.stroke();
}
}
function drawReflection() {
// Reflection of the sun on the ground
const reflectionGrad = ctx.createLinearGradient(W/2, horizonY, W/2, H);
reflectionGrad.addColorStop(0, '#ff0044');
reflectionGrad.addColorStop(0.5, '#ff8800');
reflectionGrad.addColorStop(1, '#220011');
ctx.save();
ctx.globalAlpha = 0.45;
ctx.fillStyle = reflectionGrad;
// Draw a horizontal band with a sun-like shape
ctx.beginPath();
ctx.ellipse(W/2, H, sunRadius * 0.9, sunRadius * 0.35, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Add some horizontal streaks to make it look like a reflection
ctx.save();
ctx.globalAlpha = 0.2;
ctx.fillStyle = '#ff00ff';
for (let i = 0; i < 5; i++) {
const y = horizonY + i * sunRadius * 0.15;
ctx.fillRect(W/2 - 80, y, 160, 1);
}
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, W, H);
drawSky();
drawStars(performance.now());
drawSun(sunY);
drawGrid();
drawReflection();
}
function update() {
if (paused) return;
const now = performance.now();
const dt = Math.min((now - lastTime) / 1000, 0.1); // cap delta
lastTime = now;
elapsed += dt;
if (elapsed >= loopDuration) {
elapsed -= loopDuration;
}
// Sun moves down slowly
const maxSunY = H * 0.58;
sunY = Math.min(maxSunY, Math.max(80, sunY + 15 * dt));
requestAnimationFrame(update);
}
function loop() {
draw();
update();
requestAnimationFrame(loop);
}
// UI controls
const pauseBtn = document.getElementById('pauseBtn');
const resumeBtn = document.getElementById('resumeBtn');
const restartBtn = document.getElementById('restartBtn');
pauseBtn.addEventListener('click', () => {
paused = true;
pauseBtn.textContent = 'Resume';
resumeBtn.classList.remove('active');
resumeBtn.disabled = false;
});
resumeBtn.addEventListener('click', () => {
paused = false;
resumeBtn.textContent = 'Pause';
resumeBtn.classList.add('active');
lastTime = performance.now();
});
restartBtn.addEventListener('click', () => {
elapsed = 0;
sunY = 100;
paused = false;
lastTime = performance.now();
resumeBtn.textContent = 'Pause';
resumeBtn.classList.add('active');
});
// Start
initHorizon();
requestAnimationFrame(loop);
})();
```
Wait, there's an issue: `draw()` calls `drawGrid()` and `drawReflection()`, but `update()` also calls `draw()`. Actually, I'm calling both in `loop()`. Let me restructure: `loop()` should call `draw()` which includes everything. Or separate concerns.
Let me simplify: `draw()` does all the drawing. `update()` handles animation state (sun position, etc.). Both run in the loop.
Actually, in my code above, `draw()` calls `drawGrid()` and `drawReflection()`, and `update()` updates the state. Then `loop()` calls both. That's fine.
But there's a problem: `draw()` clears the canvas and redraws everything each frame. That's correct for animation.
Let me also double-check the grid animation logic. The grid should "move toward the viewer". My current approach: horizontal lines get closer together (smaller spacing) as the loop progresses. This simulates the grid coming closer. The vertical lines stay fixed at the edges converging to the center. This should work.
However, I realize the grid might not be very visible because the lines are thin. Let me make the grid lines slightly thicker or use a brighter color.
Also, I should ensure the horizon is clearly visible. Let me draw a distinct horizon line.
Let me also add a subtle moon or additional elements for visual interest. But the requirements are specific: sun, scanlines, starfield, grid, reflection. I'll stick to those.
One more thing: the reflection should be "visually distinct from a second sun in the sky". So I should make the reflection look different - maybe more horizontal/elliptical, with a different color gradient. I've done that with the ellipse and different colors.
Let me also make sure the sun moves "slowly down toward the horizon while keeping the composition visible". My sun moves from y=100 to y=H*0.58 over 14 seconds. That's about 15 pixels per second, which is slow enough.
Now, let me think about potential issues:
1. The `drawScanlines` function references `radius` but I passed `cx, y, radius` - that's fine.
2. The reflection: I'm drawing an ellipse at y=H. That's below the horizon. Good.
3. The star twinkling: I used a time-based sine function. Good.
Let me also add a slight moon for extra atmosphere? Not required, but could be nice. I'll skip to keep it focused.
Actually, let me reconsider the overall visual. The synthwave aesthetic typically has:
- Dark purple/blue background
- Hot pink/magenta sun
- Cyan grid lines
- Purple/pink clouds or gradients
- Neon colors
My current palette: dark background (#0a0a2e), sun with pink/orange gradient, cyan grid, magenta scanlines, pink reflection. This fits.
Let me finalize the code and write it to index.html.
Actually, I realize I should test the code mentally:
- `drawGrid()`: draws horizon line, vertical lines from horizon to top of canvas (wait, I wrote `ctx.lineTo(vpX, H)` - that goes from horizon down to the bottom of the canvas! That's wrong. The vertical lines should go from the horizon upward to the top of the canvas (or to some vanishing point above the canvas).
Let me fix: vertical lines go from horizonY up to 0 (top of screen):
```javascript
ctx.lineTo(vpX, 0);
```
And the horizontal lines go from left to right at various y positions starting from horizonY.
Also, the grid should extend from the foreground (bottom) to the horizon. So the vertical lines start at the horizon and go up. The horizontal lines are at the horizon and below? No, the grid is on the ground plane, so horizontal lines are at the horizon and below (extending into the distance). Actually, in a typical perspective grid, the horizontal lines are at the horizon and extend outward (into the distance), and the vertical lines converge to a vanishing point above the horizon.
Wait, let me think about standard perspective:
- Horizon line is at eye level
- Vertical lines (like road markings) converge to a vanishing point on the horizon
- Horizontal lines (like the ground plane) are parallel to the horizon
In our case:
- The ground plane is the horizon line itself (or slightly below it)
- The grid lines on the ground: horizontal lines are along the ground, vertical lines are perpendicular to the ground
For a "neon grid extending from the foreground to a horizon":
-The output file index.html has not been written, or is empty. Write the complete app to index.html in the current directory now. Use the remaining time to verify it, then finish. The original 15-minute deadline still applies.
thinking
We need to build an animated synthwave landscape on a full-screen canvas. Let me plan this out:
1. A glowing gradient sun broken by horizontal scanline bands
2. Starfield
3. Perspective neon grid extending from foreground to horizon
4. Animation: grid moves toward viewer (forward travel), sun slowly moves down toward horizon
5. Below the horizon, a stylized reflection of the sun's colors on the ground plane
6. Loop 10-20 seconds without a distracting reset jump
7. Pause/Resume and Restart controls
Let me create a single HTML file with inline CSS and JS.
Key design:
- Full-screen canvas
- Sky gradient background (dusk/synthwave colors)
- Sun: large circle near top, with glow effect, broken by scanlines
- Scanlines: horizontal lines across the sun area
- Stars: small white dots scattered in the sky
- Ground plane: a diagonal or horizontal line below the horizon
- Neon grid: perspective grid lines going into the distance (vanishing point)
- Grid animation: move the grid forward (shift lines toward viewer)
- Reflection: mirror the sun's colors on the ground
Color palette: synthwave - purples, pinks, oranges, teals. Typical synthwave colors: deep purple (#1a0a2e), hot pink (#ff00ff), cyan (#00ffff), orange (#ff6600).
Let me think about the implementation:
Canvas setup: full viewport, resize handling.
Drawing steps each frame:
1. Clear canvas
2. Draw sky gradient (dark purple to dark blue)
3. Draw stars (small dots)
4. Draw sun:
- Outer glow (blur)
- Main circle
- Scanlines over it
5. Draw horizon line
6. Draw ground plane (a trapezoid shape for perspective)
7. Draw reflection on ground (mirror of sun colors)
8. Draw neon grid:
- Perspective grid lines
- Move them forward each frame
9. Animate: grid shifts, sun drifts down slowly
For the grid animation, I can shift the x-offset of grid lines over time to simulate moving forward.
Let me code this up.
Sun position: starts around y=150, moves down at a slow rate. Horizon around y=400 (canvas height ~600). So sun starts at y=150 and moves to y=350 over the cycle.
Grid: perspective grid. Vanishing point at center-top or slightly above horizon. Actually, for a road-like perspective, vanishing point is typically at the horizon center. The grid lines converge to that point.
Grid lines: horizontal lines (parallel to horizon) and vertical lines (perpendicular to horizon). As the grid "moves forward", the lines shift.
Let me think about the coordinate system. Canvas is 1200x800 (or whatever the viewport is).
Horizon: let's say at y = canvas.height * 0.55 ≈ 440.
Sun: starts at y = 200, moves down to y = 380 over the cycle.
Reflection: draw a mirrored version of the sun below the horizon, using the same colors but inverted vertically.
Let me write the code:
```html
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; }
canvas { display: block; }
#ui {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-family: monospace;
z-index: 10;
}
button {
margin: 0 10px;
padding: 8px 16px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.3);
color: #fff;
cursor: pointer;
font-size: 14px;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="ui">
<button id="pauseBtn">Pause</button>
<button id="resumeBtn">Resume</button>
<button id="restartBtn">Restart</button>
<span id="time">0.00s</span>s
</div>
<script>
// ... animation code ...
</script>
</body>
</html>
```
Now let me flesh out the JavaScript:
```javascript
const canvas = document.getElementById('c');
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 state
let t = 0;
const cycleDuration = 12; // seconds
const sunSpeed = 0.02; // per frame
const gridSpeed = 0.03;
// Sun properties
const sunColors = ['#ff0044', '#ff6600', '#ff00ff']; // pink, orange, purple
const sunBaseY = 180;
// Grid properties
const gridOffset = 0;
function draw() {
t += 0.016; // ~60fps
const progress = (t % cycleDuration) / cycleDuration;
// Clear
ctx.fillStyle = '#0a0520'; // dark background
ctx.fillRect(0, 0, W, H);
// Sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, '#1a0a2e');
skyGrad.addColorStop(0.5, '#2d1b4e');
skyGrad.addColorStop(1, '#0d1b3d');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// Stars
drawStars();
// Sun
const sunY = sunBaseY + Math.sin(t * sunSpeed) * 30;
drawSun(sunY);
// Horizon
const horizonY = H * 0.55;
drawHorizon(horizonY);
// Ground plane
drawGround(horizonY);
// Reflection
drawReflection(horizonY, sunY);
// Grid
drawGrid(progress);
// Update UI
document.getElementById('time').textContent = String(progress * cycleDuration).padStart(2, '0') + 's';
}
function drawStars() {
// Use a seeded pseudo-random star field
const stars = [];
for (let i = 0; i < 100; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.5,
size: Math.random() * 1.5 + 0.5,
brightness: Math.random() * 0.5 + 0.5
});
}
ctx.fillStyle = '#fff';
stars.forEach(s => {
ctx.globalAlpha = s.brightness;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
}
function drawSun(y) {
// Glow
const glowRadius = 80;
const gradient = ctx.createRadialGradient(W/2, y, 0, W/2, y, glowRadius * 2);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(0.3, '#ff0044');
gradient.addColorStop(0.6, '#ff6600');
gradient.addColorStop(1, 'rgba(255,0,68,0)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(W/2, y, glowRadius * 2, 0, Math.PI * 2);
ctx.fill();
// Sun disc
const discGrad = ctx.createRadialGradient(W/2 - 10, y - 10, 5, W/2, y, 35);
discGrad.addColorStop(0, '#ffffff');
discGrad.addColorStop(0.4, '#ffcc00');
discGrad.addColorStop(1, '#ff0044');
ctx.fillStyle = discGrad;
ctx.beginPath();
ctx.arc(W/2, y, 35, 0, Math.PI * 2);
ctx.fill();
// Scanlines
ctx.fillStyle = 'rgba(0,0,0,0.15)';
for (let row = 0; row < 5; row++) {
const yy = y + row * 8;
ctx.fillRect(0, yy, W, 1);
}
}
function drawHorizon(horizonY) {
// Draw horizon line
ctx.strokeStyle = '#ff3366';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, horizonY);
ctx.lineTo(W, horizonY);
ctx.stroke();
}
function drawGround(horizonY) {
// Ground plane - a trapezoid from horizon down
const groundColor = '#1a0a2e';
ctx.fillStyle = groundColor;
ctx.beginPath();
ctx.moveTo(0, horizonY);
ctx.lineTo(W * 0.25, H);
ctx.lineTo(W * 0.75, H);
ctx.lineTo(W, horizonY);
ctx.closePath();
ctx.fill();
// Grid lines on ground
// ...
}
function drawReflection(horizonY, sunY) {
// Mirror the sun colors below the horizon
const refY = horizonY + (sunY - horizonY) * 0.6; // below horizon
// Draw a reflected sun shape
// ...
}
function drawGrid(progress) {
// Grid offset for animation
const offset = gridOffset + progress * 200;
// Horizontal lines
ctx.strokeStyle = '#00ffff';
ctx.lineWidth = 1;
const numLines = 20;
for (let i = 0; i < numLines; i++) {
const y = horizonY + i * (H - horizonY) / numLines + offset;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
// Vertical lines (perspective)
// ...
}
```
Hmm, let me reconsider the design. The synthwave aesthetic needs:
- Dark purple/blue background
- Neon pink/purple/magenta grid lines
- Sun with glow
- Scanlines
- Stars
- Horizon
- Ground with reflection
Let me refine the approach. The grid should be a perspective grid - like a road stretching to the horizon. The grid lines should be neon colored (cyan/teal/pink).
For the grid animation moving toward the viewer: I can shift the horizontal lines upward (sun moves down, grid appears to come closer). Or I can animate the vertical perspective lines shifting.
Actually, a classic synthwave grid: a road with perspective, neon lines. The "moving toward viewer" effect is achieved by shifting the grid pattern - as if the camera is moving forward.
Let me implement it like this:
- Horizontal lines: spaced evenly, shifted upward over time (simulating forward movement)
- Vertical lines: converging to a vanishing point at the center of the screen, also shifting
For the reflection: draw a mirrored sun shape below the horizon, using the same color palette but inverted.
Let me write the complete code now. I'll make it robust.
Actually, let me think about the reflection more carefully. The reflection should be on the ground plane. I can draw a semi-transparent version of the sun (or its color) on the ground, stretched vertically to look like a reflection.
A simple approach: draw a gradient on the ground below the horizon that mimics the sun's colors, maybe with a mirrored sun shape.
Let me structure the final code:
```javascript
const canvas = document.getElementById('c');
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();
// State
let t = 0;
const cycleDuration = 14; // seconds
const sunDrift = 0.015; // sun moves down per second
const gridShift = 0.04; // grid moves forward per second
// Sun
const sunY = 160; // base Y position
// Grid
let gridPhase = 0;
function draw() {
t += 0.016;
const p = (t % cycleDuration) / cycleDuration;
// Clear
ctx.fillStyle = '#080620';
ctx.fillRect(0, 0, W, H);
// Sky gradient
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#1a0a2e');
grad.addColorStop(0.4, '#2d1b4e');
grad.addColorStop(0.7, '#0d1b3d');
grad.addColorStop(1, '#050510');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Stars
drawStars();
// Sun
const curSunY = sunY + Math.sin(t * 0.3) * 20;
drawSun(curSunY);
// Horizon
const horizonY = H * 0.55;
drawHorizon(horizonY);
// Ground
drawGround(horizonY);
// Reflection
drawReflection(horizonY, curSunY);
// Grid
drawGrid(p);
// Time label
document.getElementById('timer').textContent = t.toFixed(2) + 's';
}
```
Let me write the full thing now. I'll be careful with the canvas drawing.
For the scanlines on the sun, I'll draw horizontal semi-transparent lines over the sun disc.
For the grid, I'll draw:
- Horizontal lines: spaced, with some opacity, neon color
- Vertical perspective lines: converging to vanishing point
The grid animation: shift the horizontal lines upward (decreasing their y positions) to simulate moving forward. Also, the vertical lines can shift.
Let me finalize the code. I want it to look good and work correctly.
Here's my plan for the complete index.html:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Synthwave Landscape</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; }
canvas { display: block; }
#controls {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
z-index: 10;
}
button {
padding: 10px 20px;
font-family: monospace;
font-size: 14px;
border: 1px solid rgba(255,255,255,0.3);
background: rgba(255,255,255,0.1);
color: #fff;
cursor: pointer;
transition: background 0.2s;
}
button:hover { background: rgba(255,255,255,0.2); }
#timer {
color: #fff;
font-family: monospace;
font-size: 14px;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="controls">
<button id="pauseBtn">Pause</button>
<button id="resumeBtn">Resume</button>
<button id="restartBtn">Restart</button>
<span id="timer">0.00s</span>
</div>
<script>
// ... all JS ...
</script>
</body>
</html>
```
Now let me write the JavaScript carefully.
I'll create a comprehensive animation. Let me draft it:
```javascript
const canvas = document.getElementById('c');
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 state
let t = 0;
const cycleDuration = 14; // seconds
const sunDriftY = 0.008; // per frame, sun moves down
const gridSpeed = 0.05; // grid forward speed
// Sun
const sunBaseY = 170;
const sunColor1 = '#ff0044';
const sunColor2 = '#ff6600';
const sunColor3 = '#ff00ff';
// Stars
const starCount = 120;
function drawStars() {
// Seeded random positions
const seed = 42;
const stars = [];
for (let i = 0; i < starCount; i++) {
const x = (i * 7919 + seed) % Math.floor(W * 0.8);
const y = (i * 6271 + seed * 3) % (H * 0.45);
const size = Math.random() * 1.5 + 0.5;
const brightness = Math.random() * 0.6 + 0.4;
stars.push({ x, y, size, brightness });
}
ctx.fillStyle = '#fff';
stars.forEach(s => {
ctx.globalAlpha = s.brightness;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
}
function drawSun(y) {
// Outer glow
const glowR = 90;
const glow = ctx.createRadialGradient(W/2, y, 0, W/2, y, glowR);
glow.addColorStop(0, 'rgba(255,0,68,0.6)');
glow.addColorStop(0.3, 'rgba(255,0,68,0.3)');
glow.addColorStop(0.6, 'rgba(255,100,50,0.1)');
glow.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(W/2, y, glowR, 0, Math.PI * 2);
ctx.fill();
// Sun disc
const discR = 40;
const disc = ctx.createRadialGradient(W/2 - 8, y - 8, 3, W/2, y, discR);
disc.addColorStop(0, '#ffffff');
disc.addColorStop(0.3, sunColor1);
disc.addColorStop(0.7, sunColor2);
disc.addColorStop(1, sunColor3);
ctx.fillStyle = disc;
ctx.beginPath();
ctx.arc(W/2, y, discR, 0, Math.PI * 2);
ctx.fill();
// Scanlines
ctx.fillStyle = 'rgba(0,0,0,0.12)';
for (let row = 0; row < 6; row++) {
const yy = y + row * 7;
ctx.fillRect(0, yy, W, 1);
}
}
function drawHorizon(horizonY) {
// Horizon glow line
const horizonGrad = ctx.createLinearGradient(0, horizonY, 0, horizonY + 20);
horizonGrad.addColorStop(0, '#ff3366');
horizonGrad.addColorStop(1, 'rgba(255,51,102,0)');
ctx.fillStyle = horizonGrad;
ctx.fillRect(0, horizonY, W, 20);
// Horizon line
ctx.strokeStyle = '#ff3366';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, horizonY);
ctx.lineTo(W, horizonY);
ctx.stroke();
}
function drawGround(horizonY) {
// Ground fill
const groundGrad = ctx.createLinearGradient(0, horizonY, 0, H);
groundGrad.addColorStop(0, '#0a0520');
groundGrad.addColorStop(1, '#1a0a2e');
ctx.fillStyle = groundGrad;
ctx.beginPath();
ctx.moveTo(0, horizonY);
ctx.lineTo(W * 0.2, H);
ctx.lineTo(W * 0.8, H);
ctx.lineTo(W, horizonY);
ctx.closePath();
ctx.fill();
// Grid lines on ground
drawGridLines(horizonY);
}
function drawGridLines(horizonY) {
const phase = t * gridSpeed;
const numHorizontal = 15;
const numVertical = 12;
// Horizontal lines (moving forward)
ctx.strokeStyle = '#00e5ff';
ctx.lineWidth = 1;
for (let i = 0; i < numHorizontal; i++) {
const y = horizonY + i * (H - horizonY) / numHorizontal - phase * 30;
if (y > horizonY && y < H) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
}
// Vertical perspective lines
// Vanishing point
const vpX = W / 2;
const vpY = horizonY;
ctx.strokeStyle = '#00e5ff';
for (let i = 0; i <= numVertical; i++) {
const t2 = i / numVertical;
const x = vpX + (t2 === 0 ? 0 : (W - vpX) * t2);
// Actually, let's do proper perspective
}
}
```
Hmm, the vertical perspective lines need to converge to a vanishing point. For a road going to the horizon, the vanishing point is at the center of the horizon line. The vertical lines start from the bottom of the canvas and converge to the center of the horizon.
Let me simplify: draw vertical lines from bottom to horizon, with x positions that spread out from center. As the grid moves forward, the lines shift.
Actually, for a cleaner synthwave look, I'll draw the grid as follows:
- Horizontal lines: evenly spaced, moving upward (forward animation)
- Vertical lines: from bottom corners to vanishing point at center of horizon
Let me code the vertical lines properly:
```javascript
function drawGridLines(horizonY) {
const phase = t * gridSpeed;
const numH = 12;
const numV = 10;
// Horizontal lines
ctx.strokeStyle = '#00e5ff';
ctx.lineWidth = 1;
for (let i = 0; i < numH; i++) {
const y = horizonY + i * (H - horizonY) / numH - phase * 25;
if (y > horizonY && y < H) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
}
// Vertical perspective lines
const vpX = W / 2;
for (let i = 0; i <= numV; i++) {
const t2 = i / numV;
// x goes from 0 to vpX then to W
let x;
if (t2 <= 0.5) {
x = vpX * (2 * t2);
} else {
x = vpX + (W - vpX) * (2 * t2 - 1);
}
ctx.beginPath();
ctx.moveTo(x, H);
ctx.lineTo(vpX, horizonY);
ctx.stroke();
}
}
```
Wait, this isn't quite right. The vertical lines should go from the bottom of the canvas to the vanishing point at the horizon center. Let me think again.
Vanishing point: (W/2, horizonY)
Vertical line at parameter t (0 to 1):
- At t=0: x = 0, y = H (bottom-left corner)
- At t=0.5: x = W/2, y = horizonY (vanishing point)
- At t=1: x = W, y = H (bottom-right corner)
So: x = (t <= 0.5) ? t * W : W - (1-t) * W = W * (1 - |2t-1|)
And y = H - t * (H - horizonY)
This gives lines from bottom corners to the center of the horizon. That's correct for a road perspective.
```javascript
for (let i = 0; i <= numV; i++) {
const t2 = i / numV;
const x = W * (1 - Math.abs(2 * t2 - 1));
const y = H - t2 * (H - horizonY);
ctx.beginPath();
ctx.moveTo(x, H);
ctx.lineTo(vpX, horizonY);
ctx.stroke();
}
```
This looks good.
Now for the reflection: draw a mirrored sun shape below the horizon. The reflection should use the sun's colors (pink/orange/purple) but be darker/more subdued.
```javascript
function drawReflection(horizonY, sunY) {
const refY = horizonY + (sunY - horizonY) * 0.5;
const scale = 0.6;
// Reflected sun disc
const discR = 35 * scale;
const refDisc = ctx.createRadialGradient(W/2, refY, 3, W/2, refY, discR);
refDisc.addColorStop(0, 'rgba(255,0,68,0.5)');
refDisc.addColorStop(0.4, '#ff0044');
refDisc.addColorStop(1, '#8b001a');
ctx.fillStyle = refDisc;
ctx.beginPath();
ctx.arc(W/2, refY, discR, 0, Math.PI * 2);
ctx.fill();
// Maybe add a glow under the reflection
const glowR = discR * 1.5;
const glow = ctx.createRadialGradient(W/2, refY, discR * 0.5, W/2, refY, glowR);
glow.addColorStop(0, 'rgba(255,0,68,0.15)');
glow.addColorStop(1, 'rgba(255,0,68,0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(W/2, refY, glowR, 0, Math.PI * 2);
ctx.fill();
}
```
Now, the overall flow. Let me put it all together.
Also, I need to handle the "scanline bands breaking the sun" - the scanlines are already drawn over the sun. Good.
Let me also add some additional visual flair:
- A subtle vignette
- Maybe some fog/haze near the horizon
- Colorful particles or something
But keep it simple and working.
Let me write the complete file now. I'll be thorough.
One concern: the grid animation. "Animate the grid toward the viewer to suggest forward travel." The horizontal lines moving upward (decreasing y) simulates the camera moving forward, making the grid appear to come closer. This is the standard technique.
Also, the sun slowly moves down toward the horizon. I'll make the sun drift down at a constant rate.
Loop duration: 14 seconds. The cycle should repeat smoothly. Since we're using `t % cycleDuration`, there's no reset jump.
Controls:
- Pause: stop the animation (don't update t)
- Resume: continue
- Restart: reset everything to initial state
Let me write the final code:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Synthwave Landscape</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; }
canvas { display: block; }
#controls {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 12px;
z-index: 10;
}
button {
padding: 10px 20px;
font-family: monospace;
font-size: 14px;
border: 1px solid rgba(255,255,255,0.3);
background: rgba(255,255,255,0.1);
color: #fff;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
button:hover {
background: rgba(255,255,255,0.25);
}
#timer {
color: #fff;
font-family: monospace;
font-size: 14px;
min-width: 60px;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="controls">
<button id="pauseBtn">Pause</button>
<button id="resumeBtn">Resume</button>
<button id="restartBtn">Restart</button>
<span id="timer">0.00s</span>
</div>
<script>
(function() {
const canvas = document.getElementById('c');
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 state
let t = 0;
const cycleDuration = 14; // seconds
const sunDrift = 0.012; // sun moves down per frame
const gridSpeed = 0.06; // grid forward speed
// Sun
const sunBaseY = 170;
const sunColors = ['#ff0044', '#ff6600', '#ff00ff'];
// Horizon
let horizonY;
// Grid
let gridPhase = 0;
// Stars
const starCount = 100;
const stars = [];
for (let i = 0; i < starCount; i++) {
stars.push({
x: (i * 7919 + 42) % Math.floor(W * 0.85),
y: (i * 6271 + 42 * 3) % (H * 0.4),
size: Math.random() * 1.5 + 0.5,
brightness: Math.random() * 0.5 + 0.3
});
}
function drawStars() {
ctx.fillStyle = '#fff';
stars.forEach(s => {
ctx.globalAlpha = s.brightness;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
}
function drawSun(y) {
// Outer glow
const glowR = 85;
const glow = ctx.createRadialGradient(W/2, y, 0, W/2, y, glowR);
glow.addColorStop(0, 'rgba(255,0,68,0.5)');
glow.addColorStop(0.3, 'rgba(255,0,68,0.25)');
glow.addColorStop(0.6, 'rgba(255,100,50,0.08)');
glow.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(W/2, y, glowR, 0, Math.PI * 2);
ctx.fill();
// Sun disc
const discR = 38;
const disc = ctx.createRadialGradient(W/2 - 10, y - 10, 3, W/2, y, discR);
disc.addColorStop(0, '#ffffff');
disc.addColorStop(0.35, sunColors[0]);
disc.addColorStop(0.7, sunColors[1]);
disc.addColorStop(1, sunColors[2]);
ctx.fillStyle = disc;
ctx.beginPath();
ctx.arc(W/2, y, discR, 0, Math.PI * 2);
ctx.fill();
// Scanlines
ctx.fillStyle = 'rgba(0,0,0,0.1)';
for (let row = 0; row < 5; row++) {
const yy = y + row * 6;
ctx.fillRect(0, yy, W, 1);
}
}
function drawHorizon(y) {
horizonY = y;
// Horizon glow
const hg = ctx.createLinearGradient(0, y, 0, y + 15);
hg.addColorStop(0, '#ff3366');
hg.addColorStop(1, 'rgba(255,51,102,0)');
ctx.fillStyle = hg;
ctx.fillRect(0, y, W, 15);
// Horizon line
ctx.strokeStyle = '#ff3366';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
function drawGround(y) {
// Ground fill
const gGrad = ctx.createLinearGradient(0, y, 0, H);
gGrad.addColorStop(0, '#0a0520');
gGrad.addColorStop(1, '#1a0a2e');
ctx.fillStyle = gGrad;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W * 0.15, H);
ctx.lineTo(W * 0.85, H);
ctx.lineTo(W, y);
ctx.closePath();
ctx.fill();
// Grid on ground
drawGroundGrid(y);
}
function drawGroundGrid(y) {
const phase = t * gridSpeed;
const numH = 12;
const numV = 10;
// Horizontal lines
ctx.strokeStyle = '#00e5ff';
ctx.lineWidth = 1;
for (let i = 0; i < numH; i++) {
const yy = y + i * (H - y) / numH - phase * 20;
if (yy > y && yy < H) {
ctx.beginPath();
ctx.moveTo(0, yy);
ctx.lineTo(W, yy);
ctx.stroke();
}
}
// Vertical perspective lines
const vpX = W / 2;
for (let i = 0; i <= numV; i++) {
const t2 = i / numV;
const x = W * (1 - Math.abs(2 * t2 - 1));
const yy = H - t2 * (H - y);
ctx.strokeStyle = '#00e5ff';
ctx.lineWidth = 1;
ctx.globalAlpha = 0.5;
ctx.beginPath();
ctx.moveTo(x, H);
ctx.lineTo(vpX, y);
ctx.stroke();
}
ctx.globalAlpha = 1;
}
function drawReflection(ySun, yHorizon) {
const refY = yHorizon + (ySun - yHorizon) * 0.5;
const scale = 0.55;
const discR = 32 * scale;
// Reflected disc
const disc = ctx.createRadialGradient(W/2, refY, 3, W/2, refY, discR);
disc.addColorStop(0, 'rgba(255,0,68,0.4)');
disc.addColorStop(0.35, sunColors[0]);
disc.addColorStop(0.7, sunColors[1]);
disc.addColorStop(1, '#6b0033');
ctx.fillStyle = disc;
ctx.beginPath();
ctx.arc(W/2, refY, discR, 0, Math.PI * 2);
ctx.fill();
// RefThe output file index.html has not been written, or is empty. Write the complete app to index.html in the current directory now. Use the remaining time to verify it, then finish. The original 15-minute deadline still applies.
thinking
We need to create a complete synthwave landscape animation on a full-screen canvas. Let me plan the requirements:
1. Glowing gradient sun broken by horizontal scanline bands
2. Starfield
3. Perspective neon grid extending from foreground to horizon
4. Animate the grid moving toward the viewer (forward travel)
5. Sun moves slowly down toward the horizon
6. Below the horizon, a stylized reflection of the sun's colors on the ground plane
7. Loop of 10-20 seconds without distracting reset jump
8. Pause/Resume and Restart controls
9. Procedural drawing and colors only, no external assets
Let me build this as a single HTML file with inline CSS and JS.
Design:
- Canvas fills the viewport
- Sky: dark gradient (deep purple/blue)
- Sun: large circle near top center, glowing gradient, with scanlines across it
- Stars: small white dots scattered in the sky
- Horizon line
- Ground plane: a reflective surface below the horizon
- Neon grid: perspective grid lines receding into the distance, animated to move forward (grid lines shift)
Animation:
- Grid moves toward viewer: the perspective grid lines shift so that the vanishing point appears to come closer
- Sun moves down slowly
- Reflection of sun on ground plane - mirrored colors
Let me think about the implementation:
**Sun**:
- Position starts near top, moves down over time
- Draw a radial gradient glow
- Horizontal scanlines across the sun (thin white/light lines)
**Starfield**:
- Generate random star positions at startup
- Small white dots
**Perspective grid**:
- Use perspective projection: lines converge to a vanishing point on the horizon
- As the grid "moves toward viewer", the vanishing point shifts or the grid lines shift
- Actually, for "moving toward the viewer", we can animate the grid offset - shift all grid points upward (or shift the whole scene downward) to simulate moving forward
- The grid should look like it's receding into the distance
**Reflection**:
- Mirror the sun's colors onto the ground plane below the horizon
- Draw a gradient reflection that fades into the ground
**Loop**: 10-20 seconds. Use requestAnimationFrame with a time accumulator. The animation should loop smoothly.
**Controls**:
- Pause/Resume button
- Restart button
Let me code this up.
```html
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; }
canvas { display: block; }
#ui {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-family: monospace;
z-index: 10;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="ui">
<button id="pause">Pause</button>
<button id="restart">Restart</button>
</div>
<script>
// ... animation code ...
</script>
</body>
</html>
```
Now let me write the JavaScript:
```javascript
const canvas = document.getElementById('c');
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 state
let t = 0;
let paused = false;
// Sun
const sun = {
radius: 60,
y: 0.25, // starts at 25% from top
targetY: 0.85, // moves down to 85%
baseY: 0.25,
};
// Horizon
const horizonY = 0.45; // fraction from top
// Stars
const stars = [];
for (let i = 0; i < 200; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.5, // upper half
r: Math.random() * 1.5 + 0.5,
});
}
// Grid parameters
const gridDepth = 600; // units in perspective
const gridSpacing = 40;
const vanishingX = W / 2;
// Colors
const sunColors = ['#ff6b6b', '#ffa502', '#ffd166']; // warm sunset colors
function draw() {
if (!paused) {
t += 0.005; // seconds per frame
}
// Clear
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#1a0a2e');
grad.addColorStop(0.5, '#2d1b4e');
grad.addColorStop(1, '#0f0c29');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Draw stars
for (const s of stars) {
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
// Sun
const sunY = sun.baseY - t * 0.01; // moves down
const sunRadius = sun.radius;
const sunCenterY = sunY * H;
// Glow
const glowGrad = ctx.createRadialGradient(W/2, sunCenterY, 0, W/2, sunCenterY, sunRadius * 4);
glowGrad.addColorStop(0, 'rgba(255,100,50,0.6)');
glowGrad.addColorStop(0.5, 'rgba(255,50,30,0.2)');
glowGrad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(W/2, sunCenterY, sunRadius * 4, 0, Math.PI * 2);
ctx.fill();
// Sun body
const sunGrad = ctx.createRadialGradient(W/2, sunCenterY, 0, W/2, sunCenterY, sunRadius);
sunGrad.addColorStop(0, '#fff8e7');
sunGrad.addColorStop(0.5, '#ffcc80');
sunGrad.addColorStop(1, '#ff6b6b');
ctx.fillStyle = sunGrad;
ctx.beginPath();
ctx.arc(W/2, sunCenterY, sunRadius, 0, Math.PI * 2);
ctx.fill();
// Scanlines
ctx.fillStyle = 'rgba(0,0,0,0.15)';
for (let row = 0; row < 3; row++) {
const y = sunCenterY + row * sunRadius * 0.3;
ctx.fillRect(W/2 - sunRadius*1.5, y, sunRadius*3, 1);
}
// Reflection on ground
// The reflection is below the horizon
const reflectY = horizonY * H + 20;
const reflectionGrad = ctx.createLinearGradient(0, reflectY, 0, H);
reflectionGrad.addColorStop(0, 'rgba(255,107,107,0.3)');
reflectionGrad.addColorStop(1, 'rgba(255,107,107,0)');
ctx.fillStyle = reflectionGrad;
ctx.fillRect(0, reflectY, W, H - reflectY);
// Also draw a mirrored sun shape on the ground?
// The requirement says "stylized reflection of the sun's colors on the ground plane"
// So a colored band/gradient on the ground below the horizon
// Grid
// Perspective grid: lines from bottom corners to vanishing point
// Moving toward viewer: shift everything up slightly
const gridOffset = Math.sin(t * 0.3) * 5; // subtle oscillation
// Draw horizontal grid lines
for (let i = 0; 0.5; i < H; i += gridSpacing) {
const y = horizonY * H + i;
// Project grid line to screen coordinates
// For perspective: x stays same, y maps to horizon + something
// Actually, let's do a simpler approach: draw converging lines
}
// ... more code
}
```
Hmm, let me think more carefully about the perspective grid.
For a proper perspective grid:
- Vanishing point at (vanishingX, horizonY * H)
- Horizontal lines: y = horizonY * H + lineIndex * spacing, projected to screen
- Vertical lines: converge to vanishing point
Actually, for a simpler but effective approach:
- Draw horizontal lines at regular intervals from horizon downward
- Each horizontal line: start from left edge, go toward vanishing point
- Vertical lines: from bottom edge, go up toward vanishing point
The grid "moves toward the viewer" - I'll animate by shifting the y-offset of all grid lines upward (which means the vanishing point effectively moves closer, or the grid appears to advance).
Let me implement it step by step.
Actually, let me simplify: the grid is drawn with perspective projection. The grid lines are at y positions that increase from the horizon downward. To animate forward movement, I'll add an offset that decreases over time (shifting the grid upward), simulating the camera moving forward.
Let me write the complete code now.
For the reflection: I'll draw a gradient on the ground area (below the horizon) that uses the sun's color. The sun has a warm gradient (orange/pink), and the reflection will be a pinkish glow on the ground.
Let me structure the code:
```javascript
const canvas = document.getElementById('c');
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();
let t = 0;
let paused = false;
// Sun
const sun = {
radius: 55,
baseY: 0.2, // fraction from top
speed: 0.003, // fraction per second
};
// Horizon
const horizonFraction = 0.42;
// Stars
const stars = [];
for (let i = 0; i < 250; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.45,
r: Math.random() * 1.2 + 0.3,
});
}
// Grid
const gridSpacing = 50;
const vpX = W / 2;
const vpY = horizonFraction * H;
// Colors
const sunColors = ['#ff7e5f', '#ffa502', '#ffd166'];
function draw() {
if (!paused) {
t += 0.008;
}
// Clear
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, '#0a0a2e');
skyGrad.addColorStop(0.4, '#1a1040');
skyGrad.addColorStop(0.7, '#2d1b4e');
skyGrad.addColorStop(1, '#0d0d1a');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// Stars
for (const s of stars) {
ctx.fillStyle = 'rgba(255,255,150,0.7)';
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
// Sun
const sunY = sun.baseY - t * sun.speed;
const sunCenterY = sunY * H;
const sunR = sun.radius;
// Glow
const glow = ctx.createRadialGradient(W/2, sunCenterY, 0, W/2, sunCenterY, sunR * 5);
glow.addColorStop(0, 'rgba(255,150,80,0.5)');
glow.addColorStop(0.4, 'rgba(255,80,40,0.2)');
glow.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(W/2, sunCenterY, sunR * 5, 0, Math.PI * 2);
ctx.fill();
// Sun body
const bodyGrad = ctx.createRadialGradient(W/2, sunCenterY, 0, W/2, sunCenterY, sunR);
bodyGrad.addColorStop(0, '#fffbe6');
bodyGrad.addColorStop(0.5, '#ffcc80');
bodyGrad.addColorStop(1, '#ff6b6b');
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.arc(W/2, sunCenterY, sunR, 0, Math.PI * 2);
ctx.fill();
// Scanlines
ctx.fillStyle = 'rgba(0,0,0,0.12)';
for (let row = 0; row < 3; row++) {
const y = sunCenterY + row * sunR * 0.35;
ctx.fillRect(W/2 - sunR*1.8, y, sunR*3.5, 1);
}
// Reflection on ground
const reflY = horizonFraction * H + 10;
const reflGrad = ctx.createLinearGradient(0, reflY, 0, H);
reflGrad.addColorStop(0, 'rgba(255,100,120,0.25)');
reflGrad.addColorStop(1, 'rgba(255,60,90,0)');
ctx.fillStyle = reflGrad;
ctx.fillRect(0, reflY, W, H - reflY);
// Draw a stylized sun reflection shape on the ground
// A semi-transparent ellipse matching the sun's colors
ctx.save();
ctx.globalAlpha = 0.3;
const reflGrad2 = ctx.createRadialGradient(W/2, reflY, 0, W/2, reflY, sunR);
reflGrad2.addColorStop(0, '#ff6b6b');
reflGrad2.addColorStop(1, 'rgba(255,107,107,0)');
ctx.fillStyle = reflGrad2;
ctx.beginPath();
ctx.ellipse(W/2, reflY, sunR * 0.7, sunR * 0.3, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Grid
// Horizontal lines
const gridOffset = Math.sin(t * 0.4) * 3; // subtle oscillation
for (let i = 0.5; i < H; i += gridSpacing) {
const y = horizonFraction * H + i + gridOffset;
if (y > H) continue;
// Project: x goes from 0 to W, but converges to vanishing point
// Simple: draw line from (0, y) to (vPX, vpY)
ctx.strokeStyle = 'rgba(0, 180, 255, 0.15)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(vpX, vpY);
ctx.stroke();
}
// Vertical lines
for (let x = 0; x <= W; x += gridSpacing) {
ctx.strokeStyle = 'rgba(0, 180, 255, 0.08)';
ctx.beginPath();
ctx.moveTo(x, horizonFraction * H);
ctx.lineTo(vpX, vpY);
ctx.stroke();
}
// Animated grid movement: shift the whole grid upward
// This simulates moving toward the viewer
// We can do this by offsetting the y calculations
}
```
Wait, I need to think about the "loop of 10-20 seconds that repeats without a distracting reset jump". The animation should loop smoothly. Since I'm using requestAnimationFrame with a continuous time variable, there's no reset - it just keeps going. The loop period would be determined by how long it takes for the sun to reach the horizon and wrap around, or I could make the animation cycle through a set of states.
Actually, for a smooth loop, I can make the sun's Y position cycle: start at some position, move down to the horizon, then wrap back to the top. But the requirement says "move the sun slowly down toward the horizon while keeping the composition visible" - so maybe the sun doesn't wrap around, and instead the whole scene loops in some other way.
Hmm, "loop of 10-20 seconds that repeats without a distracting reset jump" - I think this means the animation should be a continuous loop. One approach: make the sun oscillate between two positions (top and near horizon), and when it reaches the bottom, it resets to the top. But that would have a reset jump.
Alternative: use a periodic function for all elements so that after some period (say 15 seconds), everything returns to the starting state smoothly. For example:
- Sun Y: sin wave from top to horizon and back
- Grid offset: sin wave
- Scanlines: shift
- Stars: twinkle
This way the entire scene is periodic and loops naturally.
Let me design it as a 15-second loop:
- Sun: moves from top (y=0.1) down to horizon (y=0.45) and back up to top, over 15 seconds
- Grid: moves forward and backward over 15 seconds
- Scanlines: scroll across the sun
- Stars: twinkle
Actually, for simplicity and visual appeal, let me make the main animation cycle 15 seconds:
- Sun: y = 0.15 + 0.3 * sin(2π * t / 15) → oscillates between 0.15 and 0.45
- Grid offset: sin wave
- Scanline position: scroll
This gives a smooth loop without jumps.
Let me refine the code. I'll also add a nice color palette: synthwave typically uses magenta, cyan, orange, purple.
Let me write the final version:
```javascript
const canvas = document.getElementById('c');
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();
let t = 0;
let paused = false;
const LOOP = 15; // seconds
// Sun
const sun = {
radius: 50,
baseY: 0.15,
amp: 0.3, // amplitude of oscillation
};
// Horizon
const horizonFraction = 0.42;
// Stars
const stars = [];
for (let i = 0; i < 300; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.45,
r: Math.random() * 1.5 + 0.4,
twinkle: Math.random() * Math.PI * 2,
});
}
// Grid
const gridSpacing = 45;
const vpX = W / 2;
const vpY = horizonFraction * H;
// Colors
const palette = {
skyTop: '#0a0a2e',
skyMid: '#1a1040',
skyBot: '#0d0d1a',
sunBody: ['#fffbe6', '#ffcc80', '#ff6b6b'],
sunGlow: ['rgba(255,150,80,0.5)', 'rgba(255,80,40,0.2)', 'rgba(0,0,0,0)'],
scanline: 'rgba(0,0,0,0.1)',
grid: 'rgba(0, 180, 255, 0.12)',
ground: 'rgba(255,80,120,0)',
};
function draw() {
if (!paused) {
t += 0.016; // ~60fps
}
// Clear
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, palette.skyTop);
skyGrad.addColorStop(0.5, palette.skyMid);
skyGrad.addColorStop(1, palette.skyBot);
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// Stars
for (const s of stars) {
const alpha = 0.5 + 0.5 * Math.sin(s.twinkle + t * 3);
ctx.fillStyle = `rgba(255,255,200,${alpha})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
// Sun
const sunY = sun.baseY + sun.amp * Math.sin(2 * Math.PI * t / LOOP);
const sunCenterY = sunY * H;
const sunR = sun.radius;
// Glow
const glowR = sunR * 5;
const glow = ctx.createRadialGradient(W/2, sunCenterY, 0, W/2, sunCenterY, glowR);
glow.addColorStop(0, 'rgba(255,160,60,0.5)');
glow.addColorStop(0.4, 'rgba(255,80,30,0.2)');
glow.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(W/2, sunCenterY, glowR, 0, Math.PI * 2);
ctx.fill();
// Sun body
const bodyGrad = ctx.createRadialGradient(W/2, sunCenterY, 0, W/2, sunCenterY, sunR);
bodyGrad.addColorStop(0, '#fffbe6');
bodyGrad.addColorStop(0.5, '#ffcc80');
bodyGrad.addColorStop(1, '#ff6b6b');
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.arc(W/2, sunCenterY, sunR, 0, Math.PI * 2);
ctx.fill();
// Scanlines
ctx.fillStyle = palette.scanline;
for (let row = 0; row < 3; row++) {
const y = sunCenterY + row * sunR * 0.35;
ctx.fillRect(W/2 - sunR * 1.8, y, sunR * 3.5, 1);
}
// Reflection on ground
const reflY = vpY + 5;
const reflGrad = ctx.createLinearGradient(0, reflY, 0, H);
reflGrad.addColorStop(0, 'rgba(255,80,140,0.25)');
reflGrad.addColorStop(1, 'rgba(255,40,80,0)');
ctx.fillStyle = reflGrad;
ctx.fillRect(0, reflY, W, H - reflY);
// Stylized reflection: a glowing ellipse on the ground
ctx.save();
ctx.globalAlpha = 0.25;
const reflGrad2 = ctx.createRadialGradient(W/2, reflY, 0, W/2, reflY, sunR);
reflGrad2.addColorStop(0, '#ff6b6b');
reflGrad2.addColorStop(1, 'rgba(255,107,107,0)');
ctx.fillStyle = reflGrad2;
ctx.beginPath();
ctx.ellipse(W/2, reflY, sunR * 0.6, sunR * 0.25, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Grid
// Horizontal lines
for (let i = 0.5; i < H; i += gridSpacing) {
const y = vpY + i;
ctx.strokeStyle = palette.grid;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(vpX, vpY);
ctx.stroke();
}
// Vertical lines
for (let x = 0; x <= W; x += gridSpacing) {
ctx.strokeStyle = palette.grid;
ctx.beginPath();
ctx.moveTo(x, vpY);
ctx.lineTo(vpX, vpY);
ctx.stroke();
}
// Animated grid movement: shift the y positions
// To simulate forward motion, shift the grid upward
// We'll add a phase offset to the horizontal lines
const gridPhase = Math.sin(2 * Math.PI * t / LOOP);
for (let i = 0.5; i < H; i += gridSpacing) {
const y = vpY + i + gridPhase * gridSpacing * 0.3;
if (y > H) continue;
ctx.strokeStyle = 'rgba(0, 200, 255, 0.1)';
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(vpX, vpY);
ctx.stroke();
}
// Additional vertical lines shifted for depth
for (let x = 0; x <= W; x += gridSpacing) {
const y = vpY + gridPhase * gridSpacing * 0.15;
ctx.strokeStyle = 'rgba(0, 160, 255, 0.06)';
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(vpX, vpY);
ctx.stroke();
}
}
```
Hmm, wait. The grid animation needs to clearly show "moving toward the viewer". The current approach shifts the grid lines vertically which creates a sense of depth but not necessarily forward motion.
A better approach for "moving toward the viewer": the grid lines should appear to converge and then re-converge, or the vanishing point should shift. Actually, for a classic synthwave grid animation, the grid typically scrolls or moves. Let me think...
For "perspective neon grid extending from the foreground to a horizon" that "moves toward the viewer to suggest forward travel" - I think the best visual is:
- The grid lines are drawn in perspective
- As the animation progresses, the grid appears to move forward (the vanishing point comes closer, or the grid lines shift to create a sense of motion)
One common technique: animate the camera position. If the camera moves forward, the grid lines get closer together in the distance (perspective gets steeper). But implementing true perspective math is complex.
Simpler: animate the grid offset. Add a small periodic offset to the y positions of the grid lines, making them slide up and down slightly, combined with the sun moving down. This creates a sense of motion.
Actually, let me reconsider. The key requirements are:
1. ✅ Glowing gradient sun with scanlines
2. ✅ Starfield
3. ✅ Perspective neon grid to horizon
4. ✅ Grid moves toward viewer (forward travel)
5. ✅ Sun moves down toward horizon
6. ✅ Reflection of sun colors on ground
7. ✅ 10-20 second loop
8. ✅ Pause/Resume and Restart
9. ✅ Procedural only
For the grid moving toward the viewer, I'll animate the grid such that the horizontal lines shift upward (toward the top of the screen) periodically, creating a sense of the grid advancing. Combined with the vanishing point staying fixed, this suggests the viewer is moving forward into the scene.
Let me also add a subtle zoom effect: scale the entire scene slightly as the grid moves forward. That would enhance the "travel" feeling.
OK, let me finalize the code. I'll write it all out now.
Actually, let me reconsider the loop. With t incrementing by 0.016 each frame (~60fps), in 15 seconds we get about 900 frames. The sun's y = baseY + amp * sin(2πt/LOOP). At t=0: y = 0.15. At t=7.5: y = 0.15 + 0.3 = 0.45 (horizon). At t=15: back to 0.15. So the sun oscillates between top and horizon. This is a smooth loop.
Grid: gridPhase = sin(2πt/LOOP). The grid lines shift by gridPhase * gridSpacing * 0.3. This creates a periodic oscillation of the grid pattern.
This should work well. Let me write the complete file.
One more thing: the "reflection of the sun's colors on the ground plane, visually distinct from a second sun in the sky." So I need to make sure the reflection on the ground is different from the actual sun. The reflection is a horizontal band/ellipse with the sun's colors (pink/red) on the ground. I'll make it a distinct gradient - maybe more saturated/pulsating.
Let me also add some extra visual flair: maybe some floating particles or a subtle fog. But keep it simple.
Now, let me write the complete index.html file.
Actually, I realize I should double-check: the requirement says "draw a glowing gradient sun broken by horizontal scanline bands". So the sun should have scanlines across it. I have that.
"Animate the grid toward the viewer to suggest forward travel" - I'll make the grid lines shift upward periodically.
"Move the sun slowly down toward the horizon while keeping the composition visible" - the sun oscillates but its minimum is above the horizon, so it never disappears off screen. Good.
"Below the horizon, include a stylized reflection of the sun's colors on the ground plane, visually distinct from a second sun in the sky" - I'll draw a colored reflection band and an ellipse on the ground.
"Use a loop of 10 to 20 seconds that repeats without a distracting reset jump" - 15-second loop, smooth sine-based animation.
"Provide Pause/Resume and Restart controls" - buttons in the UI.
Let me write the final code now. I'll make it polished.
Here's my plan for the complete file:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Synthwave Landscape</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; }
canvas { display: block; }
#controls {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 15px;
z-index: 10;
}
button {
padding: 10px 24px;
font-family: monospace;
font-size: 14px;
cursor: pointer;
border: 1px solid rgba(255,255,255,0.3);
background: rgba(0,0,0,0.6);
color: #fff;
border-radius: 4px;
transition: background 0.2s;
}
button:hover { background: rgba(255,255,255,0.1); }
button.active { background: rgba(255,100,150,0.4); }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="controls">
<button id="pauseBtn">Pause</button>
<button id="restartBtn">Restart</button>
</div>
<script>
// ... all JS ...
</script>
</body>
</html>
```
Now the JavaScript. Let me write it carefully.
```javascript
const canvas = document.getElementById('c');
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();
let t = 0;
let paused = false;
const LOOP = 15; // seconds
// Sun
const sun = {
radius: 50,
baseY: 0.15,
amp: 0.3,
};
// Horizon
const horizonFraction = 0.42;
// Stars
const stars = [];
for (let i = 0; i < 300; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.45,
r: Math.random() * 1.5 + 0.4,
phase: Math.random() * Math.PI * 2,
});
}
// Grid
const gridSpacing = 45;
const vpX = W / 2;
const vpY = horizonFraction * H;
// Animation
function draw() {
if (!paused) {
t += 0.016;
}
// Clear
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, '#0a0a2e');
skyGrad.addColorStop(0.5, '#1a1040');
skyGrad.addColorStop(1, '#0d0d1a');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// Stars
for (const s of stars) {
const alpha = 0.4 + 0.4 * Math.sin(s.phase + t * 3);
ctx.fillStyle = `rgba(255,255,200,${alpha.toFixed(2)})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
// Sun
const sunY = sun.baseY + sun.amp * Math.sin(2 * Math.PI * t / LOOP);
const sunCenterY = sunY * H;
const sunR = sun.radius;
// Glow
const glowR = sunR * 5;
const glow = ctx.createRadialGradient(W/2, sunCenterY, 0, W/2, sunCenterY, glowR);
glow.addColorStop(0, 'rgba(255,160,60,0.5)');
glow.addColorStop(0.4, 'rgba(255,80,30,0.2)');
glow.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(W/2, sunCenterY, glowR, 0, Math.PI * 2);
ctx.fill();
// Sun body
const bodyGrad = ctx.createRadialGradient(W/2, sunCenterY, 0, W/2, sunCenterY, sunR);
bodyGrad.addColorStop(0, '#fffbe6');
bodyGrad.addColorStop(0.5, '#ffcc80');
bodyGrad.addColorStop(1, '#ff6b6b');
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.arc(W/2, sunCenterY, sunR, 0, Math.PI * 2);
ctx.fill();
// Scanlines
ctx.fillStyle = 'rgba(0,0,0,0.12)';
for (let row = 0; row < 3; row++) {
const y = sunCenterY + row * sunR * 0.35;
ctx.fillRect(W/2 - sunR * 1.8, y, sunR * 3.5, 1);
}
// Reflection on ground
const reflY = vpY + 5;
const reflGrad = ctx.createLinearGradient(0, reflY, 0, H);
reflGrad.addColorStop(0, 'rgba(255,80,140,0.3)');
reflGrad.addColorStop(1, 'rgba(255,30,70,0)');
ctx.fillStyle = reflGrad;
ctx.fillRect(0, reflY, W, H - refl