← Mike Higgins Home
Custom Code Experiment •

Vanta JS - Rings

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vanta JS - Rings</title>

    <!-- Three.js (required by Vanta) -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
    <!-- Vanta.js core -->
    <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.rings.min.js"></script>

    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        html, body {
            width: 100%;
            height: 100%;
            overflow: hidden;
            background: #000000;
        }

        /* ---- Vanta layer ---- */
        #vantajs {
            position: fixed;
            inset: 0;
            width: 100%;
            height: 100%;
            filter: none;
            transition: filter 0.6s ease;
        }
        body.retro #vantajs {
            filter: grayscale(1) sepia(1) hue-rotate(75deg) saturate(6) brightness(0.9) contrast(1.25);
        }

        /* ---- CRT overlay group (hidden in normal mode) ---- */
        .crt-overlay-group {
            position: fixed;
            inset: 0;
            pointer-events: none;
            opacity: 0;
            transition: opacity 0.6s ease;
        }
        body.retro .crt-overlay-group {
            opacity: 1;
        }

        .grain {
            position: fixed;
            top: -50%;
            left: -50%;
            width: 200%;
            height: 200%;
            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
            opacity: 0.05;
            mix-blend-mode: overlay;
            animation: grainMove 0.5s steps(3) infinite;
        }

        .scanlines {
            position: fixed;
            inset: 0;
            background: repeating-linear-gradient(
                to bottom,
                rgba(0, 0, 0, 0) 0px,
                rgba(0, 0, 0, 0) 1px,
                rgba(0, 0, 0, 0.35) 2px,
                rgba(0, 0, 0, 0.35) 3px
            );
            mix-blend-mode: multiply;
        }

        .vignette {
            position: fixed;
            inset: 0;
            background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.7) 100%);
        }

        .crt-flicker {
            position: fixed;
            inset: 0;
            animation: flicker 6s infinite;
        }

        @keyframes grainMove {
            0%   { transform: translate(0, 0); }
            33%  { transform: translate(-4%, 5%); }
            66%  { transform: translate(3%, -4%); }
            100% { transform: translate(0, 0); }
        }

        @keyframes flicker {
            0%, 100% { background: rgba(0, 0, 0, 0); }
            92%      { background: rgba(0, 0, 0, 0); }
            93%      { background: rgba(120, 255, 160, 0.05); }
            94%      { background: rgba(0, 0, 0, 0.08); }
            95%      { background: rgba(120, 255, 160, 0.03); }
            96%      { background: rgba(0, 0, 0, 0); }
        }

        /* ---- toggle button ---- */
        #toggle-btn {
            position: fixed;
            bottom: 24px;
            right: 24px;
            z-index: 20;
            padding: 10px 16px;
            font-family: 'Courier New', monospace;
            font-size: 12px;
            letter-spacing: 0.06em;
            text-transform: uppercase;
            border-radius: 4px;
            cursor: pointer;
            background: rgba(255, 255, 255, 0.15);
            color: #ffffff;
            border: 1px solid rgba(255, 255, 255, 0.35);
            backdrop-filter: blur(4px);
            transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease;
        }
        #toggle-btn:hover {
            background: rgba(255, 255, 255, 0.28);
        }
        body.retro #toggle-btn {
            background: rgba(20, 40, 25, 0.55);
            color: #8affa0;
            border-color: #2f6b45;
        }
        body.retro #toggle-btn:hover {
            background: rgba(30, 60, 38, 0.75);
        }
    </style>
</head>
<body>
    <div id="vantajs"></div>

    <div class="crt-overlay-group">
        <div class="grain"></div>
        <div class="scanlines"></div>
        <div class="vignette"></div>
        <div class="crt-flicker"></div>
    </div>

    <button id="toggle-btn">Switch to Retroscope</button>

    <script>
        document.addEventListener('DOMContentLoaded', function () {
            if (typeof VANTA === 'undefined' || !VANTA.RINGS) {
                console.warn('VANTA.RINGS not available — check script loading.');
                return;
            }

            // Base rendering config — unchanged between modes.
            var effect = VANTA.RINGS({
                el: "#vantajs",
                mouseControls: true,
                touchControls: true,
                gyroControls: false,
                minHeight: 200.00,
                minWidth: 200.00,
                scale: 1.00,
                scaleMobile: 1.00
            });

            // Snapshot Vanta's own defaults so "Switch to Normal" reverts
            // exactly, rather than guessing them.
            var NORMAL_OPTIONS = {
                color: effect.options.color,
                backgroundColor: effect.options.backgroundColor
            };

            var RETRO_OPTIONS = {
                color: 0x3fdc7a,
                backgroundColor: 0x02110a
            };

            var isRetro = false;
            var btn = document.getElementById('toggle-btn');

            btn.addEventListener('click', function () {
                isRetro = !isRetro;
                document.body.classList.toggle('retro', isRetro);
                effect.setOptions(isRetro ? RETRO_OPTIONS : NORMAL_OPTIONS);
                btn.textContent = isRetro ? 'Switch to Normal' : 'Switch to Retroscope';
            });
        });
    </script>
</body>
</html>