80s Visual Toys

SYNTHWAVE HORIZON

Animated 80s Retrowave horizon visualizer. Sliced neon sun, moving perspective grid, VHS scanline overlay, and screenshot export.

Grid Speed 1x
Sun Size 90px
Grid Color
Sun Color
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

const canvas = document.getElementById('synth-canvas');
    const ctx = canvas.getContext('2d');
    const wrap = document.getElementById('canvas-wrap');

    const speedInput = document.getElementById('input-speed');
    const sunInput = document.getElementById('input-sun');
    const gridColorInput = document.getElementById('input-grid');
    const sunColorInput = document.getElementById('input-sun-col');
    const toast = document.getElementById('toast');

    let gridOffset = 0;

    function resize() {
      canvas.width = wrap.clientWidth;
      canvas.height = wrap.clientHeight;
    }

    function drawSynthwave() {
      const w = canvas.width;
      const h = canvas.height;
      const horizonY = h * 0.55;

      // Sky Background Gradient
      const skyGrad = ctx.createLinearGradient(0, 0, 0, horizonY);
      skyGrad.addColorStop(0, '#05010d');
      skyGrad.addColorStop(0.7, '#240046');
      skyGrad.addColorStop(1, '#ff0077');
      ctx.fillStyle = skyGrad;
      ctx.fillRect(0, 0, w, horizonY);

      // Sun
      const sunR = parseInt(sunInput.value);
      const sunX = w / 2;
      const sunY = horizonY - 10;
      document.getElementById('val-sun').textContent = sunR + 'px';

      const sunGrad = ctx.createLinearGradient(0, sunY - sunR, 0, sunY + sunR);
      sunGrad.addColorStop(0, '#ffff00');
      sunGrad.addColorStop(1, sunColorInput.value);
      ctx.fillStyle = sunGrad;
      ctx.beginPath();
      ctx.arc(sunX, sunY, sunR, Math.PI, 0, false);
      ctx.fill();

      // Sun Slices
      ctx.fillStyle = '#05010d';
      for (let i = 0; i < 6; i++) {
        const sliceY = sunY - (i * (sunR / 7));
        const sliceH = (i + 1) * 2;
        ctx.fillRect(sunX - sunR - 10, sliceY, (sunR + 10) * 2, sliceH);
      }

      // Ground Background
      ctx.fillStyle = '#0a0014';
      ctx.fillRect(0, horizonY, w, h - horizonY);

      // Perspective Grid Lines
      const speed = parseFloat(speedInput.value);
      document.getElementById('val-speed').textContent = speed + 'x';
      gridOffset = (gridOffset + speed * 1.5) % 40;

      ctx.strokeStyle = gridColorInput.value;
      ctx.lineWidth = 1.5;

      // Horizontal moving lines
      for (let y = horizonY; y < h; y += 40) {
        const drawY = y + (gridOffset * ((y - horizonY) / (h - horizonY)));
        if (drawY <= h) {
          ctx.beginPath();
          ctx.moveTo(0, drawY);
          ctx.lineTo(w, drawY);
          ctx.stroke();
        }
      }

      // Vertical perspective lines
      const numLines = 24;
      for (let i = -numLines; i <= numLines; i++) {
        ctx.beginPath();
        ctx.moveTo(w / 2, horizonY);
        ctx.lineTo(w / 2 + i * 40, h);
        ctx.stroke();
      }

      requestAnimationFrame(drawSynthwave);
    }

    function showToast(msg) {
      toast.textContent = msg;
      toast.classList.add('show');
      setTimeout(() => toast.classList.remove('show'), 2000);
    }

    document.getElementById('btn-fullscreen').addEventListener('click', () => {
      if (!document.fullscreenElement) {
        wrap.requestFullscreen();
      } else {
        document.exitFullscreen();
      }
    });

    document.getElementById('btn-screenshot').addEventListener('click', () => {
      const a = document.createElement('a');
      a.href = canvas.toDataURL('image/png');
      a.download = 'synthwave.png';
      a.click();
      showToast('Screenshot Saved!');
    });

    window.addEventListener('resize', resize);
    resize();
    drawSynthwave();
Screenshot Saved!