← Mike Higgins Home
Custom Code Experiment •

Quartic Plane Curve 2

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>Quartic Plane Curve 2</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: linear-gradient(135deg, #000000, #1a1a1a, #0a0a0a);
            overflow: hidden;
            font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            touch-action: pan-y;
            color: #ffffff;
        }

        #canvas {
            display: block;
            background: transparent;
            cursor: crosshair;
            image-rendering: pixelated;
            touch-action: none;
        }

        #canvas:active {
            cursor: grabbing;
        }

        .fab {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 56px;
            height: 56px;
            background: rgba(255, 255, 255, 0.95);
            border: 2px solid #ffffff;
            border-radius: 50%;
            box-shadow: 0 4px 20px rgba(255, 255, 255, 0.2);
            display: none;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            z-index: 1001;
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            font-size: 24px;
            color: #000;
            font-weight: bold;
            transform-origin: center;
        }

        .fab:hover {
            transform: scale(1.1);
            box-shadow: 0 6px 30px rgba(255, 255, 255, 0.4);
        }

        .fab:active {
            transform: scale(0.95);
        }

        .fab.active {
            background: rgba(0, 0, 0, 0.95);
            border-color: #ffffff;
            color: #ffffff;
            transform: rotate(90deg);
        }

        .control-panel {
            position: fixed;
            top: 20px;
            right: 20px;
            background: rgba(0, 0, 0, 0.92);
            backdrop-filter: blur(20px);
            border: 1px solid rgba(255, 255, 255, 0.15);
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
            padding: 20px;
            color: #ffffff;
            font-size: 12px;
            max-width: 300px;
            z-index: 1000;
            letter-spacing: 0.5px;
            border-radius: 16px;
            transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
            transform: translateY(0);
            opacity: 1;
            overflow: hidden;
        }

        .control-panel::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            height: 1px;
            background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5), transparent);
        }

        .control-panel.hidden {
            opacity: 0;
            pointer-events: none;
            transform: translateY(-30px) scale(0.95);
        }

        .control-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 18px;
            padding-bottom: 12px;
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        }

        .control-title {
            font-weight: 700;
            color: #ffffff;
            font-size: 14px;
            letter-spacing: 1px;
            position: relative;
            padding-left: 8px;
        }

        .control-title::before {
            content: '';
            position: absolute;
            left: 0;
            top: 50%;
            transform: translateY(-50%);
            width: 3px;
            height: 16px;
            background: #ffffff;
        }

        .close-btn {
            width: 28px;
            height: 28px;
            background: rgba(255, 255, 255, 0.1);
            border: 1px solid rgba(255, 255, 255, 0.2);
            border-radius: 50%;
            display: none;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            transition: all 0.2s;
            font-size: 18px;
            color: #ffffff;
            line-height: 1;
        }

        .close-btn:hover {
            background: rgba(255, 255, 255, 0.2);
            transform: rotate(90deg);
        }

        .control-group {
            margin-bottom: 18px;
            touch-action: pan-y;
            position: relative;
            padding-bottom: 10px;
        }

        .control-group::after {
            content: '';
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            height: 1px;
            background: rgba(255, 255, 255, 0.05);
        }

        .control-group:last-child {
            margin-bottom: 0;
            padding-bottom: 0;
        }

        .control-group:last-child::after {
            display: none;
        }

        label {
            display: block;
            margin-bottom: 10px;
            font-weight: 600;
            color: #ffffff;
            font-size: 11px;
            text-transform: uppercase;
            letter-spacing: 1px;
            display: flex;
            align-items: center;
        }

        label::before {
            content: "";
            width: 6px;
            height: 6px;
            margin-right: 8px;
            background: #ffffff;
            border-radius: 50%;
            display: inline-block;
            transition: transform 0.2s;
        }

        .control-group:hover label::before {
            transform: scale(1.3);
        }

        .slider-container {
            position: relative;
            height: 24px;
            display: flex;
            align-items: center;
        }

        input[type="range"] {
            width: 100%;
            height: 4px;
            background: rgba(255, 255, 255, 0.1);
            outline: none;
            -webkit-appearance: none;
            appearance: none;
            border-radius: 10px;
            touch-action: pan-x;
            position: relative;
            z-index: 1;
        }

        input[type="range"]::-webkit-slider-thumb {
            appearance: none;
            width: 20px;
            height: 20px;
            background: #ffffff;
            cursor: pointer;
            border-radius: 50%;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
            transition: all 0.2s;
            border: 2px solid #000000;
        }

        input[type="range"]::-webkit-slider-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 4px 15px rgba(255, 255, 255, 0.3);
        }

        input[type="range"]::-moz-range-thumb {
            width: 20px;
            height: 20px;
            background: #ffffff;
            cursor: pointer;
            border-radius: 50%;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
            transition: all 0.2s;
            border: 2px solid #000000;
        }

        input[type="range"]::-moz-range-thumb:hover {
            transform: scale(1.2);
            box-shadow: 0 4px 15px rgba(255, 255, 255, 0.3);
        }

        .slider-fill {
            position: absolute;
            height: 4px;
            background: #ffffff;
            border-radius: 10px;
            pointer-events: none;
            z-index: 0;
            left: 0;
            top: 50%;
            transform: translateY(-50%);
        }

        .button-group {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 10px;
            margin-top: 5px;
        }

        button {
            padding: 12px 10px;
            border: 1px solid rgba(255, 255, 255, 0.2);
            background: rgba(255, 255, 255, 0.05);
            color: #ffffff;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            font-size: 11px;
            text-transform: uppercase;
            font-family: 'Inter', sans-serif;
            border-radius: 8px;
            touch-action: manipulation;
            letter-spacing: 0.5px;
            position: relative;
            overflow: hidden;
        }

        button::before {
            content: '';
            position: absolute;
            top: 0;
            left: -100%;
            width: 100%;
            height: 100%;
            background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
            transition: left 0.6s;
        }

        button:hover::before {
            left: 100%;
        }

        button:hover {
            background: rgba(255, 255, 255, 0.15);
            border-color: rgba(255, 255, 255, 0.4);
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
        }

        button:active {
            transform: translateY(0);
            transition: all 0.1s;
        }

        .value-display {
            font-size: 11px;
            color: rgba(255, 255, 255, 0.7);
            text-align: right;
            margin-top: 8px;
            font-family: 'Courier New', monospace;
            letter-spacing: 0.5px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .value-display span {
            background: rgba(255, 255, 255, 0.05);
            padding: 4px 10px;
            border-radius: 12px;
            border: 1px solid rgba(255, 255, 255, 0.1);
        }

        .toggle-container {
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 15px;
            padding: 12px;
            background: rgba(255, 255, 255, 0.03);
            border-radius: 10px;
            border: 1px solid rgba(255, 255, 255, 0.1);
            transition: all 0.3s;
            cursor: pointer;
        }

        .toggle-container:hover {
            background: rgba(255, 255, 255, 0.05);
            border-color: rgba(255, 255, 255, 0.2);
        }

        .toggle-label {
            margin-bottom: 0;
            flex-grow: 1;
            cursor: pointer;
            font-size: 12px;
            font-weight: 600;
            color: #ffffff;
            display: flex;
            align-items: center;
        }

        .toggle-label::before {
            content: "";
            width: 6px;
            height: 6px;
            margin-right: 8px;
            background: #ffffff;
            border-radius: 50%;
            display: inline-block;
            transition: transform 0.2s;
        }

        .toggle-switch {
            position: relative;
            width: 50px;
            height: 24px;
            margin-left: 10px;
        }

        .toggle-switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }

        .toggle-slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: rgba(255, 255, 255, 0.1);
            transition: .4s;
            border-radius: 24px;
            border: 1px solid rgba(255, 255, 255, 0.2);
        }

        .toggle-slider:before {
            position: absolute;
            content: "";
            height: 18px;
            width: 18px;
            left: 3px;
            bottom: 2px;
            background-color: #ffffff;
            transition: .4s;
            border-radius: 50%;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
        }

        .toggle-switch input:checked + .toggle-slider {
            background-color: rgba(255, 255, 255, 0.3);
        }

        .toggle-switch input:checked + .toggle-slider:before {
            transform: translateX(26px);
        }

        .toggle-container.active .toggle-slider {
            background-color: rgba(255, 255, 255, 0.3);
        }

        .toggle-container.active .toggle-slider:before {
            transform: translateX(26px);
        }

        .tooltip {
            position: relative;
            display: inline-block;
            margin-left: 8px;
            cursor: help;
        }

        .tooltip-icon {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 16px;
            height: 16px;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 50%;
            font-size: 10px;
            color: rgba(255, 255, 255, 0.7);
            transition: all 0.3s;
        }

        .tooltip:hover .tooltip-icon {
            background: rgba(255, 255, 255, 0.2);
            color: #ffffff;
        }

        .tooltip .tooltip-text {
            visibility: hidden;
            width: 200px;
            background-color: rgba(0, 0, 0, 0.95);
            color: #fff;
            text-align: left;
            border-radius: 8px;
            padding: 12px;
            position: absolute;
            z-index: 1002;
            bottom: 125%;
            left: 50%;
            transform: translateX(-50%);
            opacity: 0;
            transition: opacity 0.3s;
            font-size: 11px;
            line-height: 1.4;
            border: 1px solid rgba(255, 255, 255, 0.1);
            text-transform: none;
            letter-spacing: 0;
            font-weight: normal;
            backdrop-filter: blur(10px);
        }

        .tooltip:hover .tooltip-text {
            visibility: visible;
            opacity: 1;
        }

        .tooltip .tooltip-text::after {
            content: "";
            position: absolute;
            top: 100%;
            left: 50%;
            margin-left: -5px;
            border-width: 5px;
            border-style: solid;
            border-color: rgba(0, 0, 0, 0.95) transparent transparent transparent;
        }

        @media (max-width: 768px) {
            .fab {
                display: flex;
            }

            .close-btn {
                display: flex;
            }

            .control-panel {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                right: auto;
                bottom: auto;
                width: calc(100% - 40px);
                max-width: 360px;
                max-height: none;
                border: 1px solid rgba(255, 255, 255, 0.2);
                border-radius: 20px;
                padding: 20px;
                display: flex;
                flex-direction: column;
                overflow-y: visible;
                background: rgba(0, 0, 0, 0.95);
                backdrop-filter: blur(30px);
            }

            .control-panel.hidden {
                transform: translate(-50%, -50%) scale(0.9);
            }

            .control-header {
                margin-bottom: 15px;
                padding-bottom: 10px;
            }

            .control-title {
                font-size: 13px;
            }

            .close-btn {
                width: 28px;
                height: 28px;
                font-size: 18px;
            }

            .control-group {
                margin-bottom: 15px;
            }

            label {
                font-size: 10px;
                margin-bottom: 8px;
            }

            .button-group {
                grid-template-columns: 1fr;
                gap: 10px;
            }

            button {
                padding: 14px 10px;
                font-size: 11px;
            }

            .value-display {
                font-size: 10px;
                margin-top: 6px;
            }
        }

        @media (min-width: 769px) {
            .control-panel {
                display: block !important;
                opacity: 1 !important;
                pointer-events: all !important;
                transform: none !important;
            }
        }

        @keyframes slideIn {
            from {
                opacity: 0;
                transform: translateY(-20px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .control-panel:not(.hidden) {
            animation: slideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
        }
    </style>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
    <canvas id="canvas"></canvas>

    <div class="fab" id="fab">⚙</div>

    <div class="control-panel hidden" id="controlPanel">
        <div class="control-header">
            <div class="control-title">QUARTIC CURVE CONTROLS</div>
            <div class="close-btn" id="closeBtn">×</div>
        </div>

        <div class="control-group">
            <label>Animation Speed
                <span class="tooltip">
                    <span class="tooltip-icon">?</span>
                    <span class="tooltip-text">Controls the speed of the curve animation. Higher values create faster movement.</span>
                </span>
            </label>
            <div class="slider-container">
                <div class="slider-fill" id="speedFill"></div>
                <input type="range" id="animationSpeed" min="0.1" max="3" value="1" step="0.1">
            </div>
            <div class="value-display">
                <span>Speed</span>
                <span id="speedValue">1.0</span>
            </div>
        </div>

        <div class="control-group">
            <label>Curve Detail
                <span class="tooltip">
                    <span class="tooltip-icon">?</span>
                    <span class="tooltip-text">Adjusts the number of points on the curve. Higher values create smoother curves.</span>
                </span>
            </label>
            <div class="slider-container">
                <div class="slider-fill" id="detailFill"></div>
                <input type="range" id="curveDetail" min="50" max="300" value="150" step="10">
            </div>
            <div class="value-display">
                <span>Detail</span>
                <span id="detailValue">150</span>
            </div>
        </div>

        <div class="control-group">
            <label>Curve Type
                <span class="tooltip">
                    <span class="tooltip-icon">?</span>
                    <span class="tooltip-text">Selects the mathematical formula used to generate the curve.</span>
                </span>
            </label>
            <div class="slider-container">
                <div class="slider-fill" id="typeFill"></div>
                <input type="range" id="curveType" min="0" max="4" value="0" step="1">
            </div>
            <div class="value-display">
                <span>Type</span>
                <span id="typeValue">Rose (0)</span>
            </div>
        </div>

        <div class="control-group">
            <label>Trail Length
                <span class="tooltip">
                    <span class="tooltip-icon">?</span>
                    <span class="tooltip-text">Controls how long the trail of the curve remains visible.</span>
                </span>
            </label>
            <div class="slider-container">
                <div class="slider-fill" id="trailFill"></div>
                <input type="range" id="trailLength" min="0.5" max="0.95" value="0.75" step="0.05">
            </div>
            <div class="value-display">
                <span>Trail</span>
                <span id="trailValue">0.75</span>
            </div>
        </div>

        <!-- Fixed Toggle Container - Now properly connected -->
        <div class="toggle-container" id="gridToggleContainer">
            <div class="toggle-label">Show Grid
                <span class="tooltip">
                    <span class="tooltip-icon">?</span>
                    <span class="tooltip-text">Toggles the background grid display on and off.</span>
                </span>
            </div>
            <div class="toggle-switch">
                <input type="checkbox" id="gridToggle" checked>
                <span class="toggle-slider"></span>
            </div>
        </div>

        <div class="control-group">
            <div class="button-group">
                <button id="resetBtn">Reset</button>
                <button id="pauseBtn">Pause</button>
                <button id="randomBtn">Random</button>
            </div>
        </div>
    </div>

    <script>
        const canvas = document.getElementById("canvas");
        const ctx = canvas.getContext("2d");
        const fab = document.getElementById("fab");
        const controlPanel = document.getElementById("controlPanel");
        const closeBtn = document.getElementById("closeBtn");

        let panelOpen = false;

        fab.addEventListener('click', () => {
            panelOpen = !panelOpen;
            controlPanel.classList.toggle('hidden', !panelOpen);
            fab.classList.toggle('active', panelOpen);
            fab.textContent = panelOpen ? '×' : '⚙';
        });

        closeBtn.addEventListener('click', () => {
            panelOpen = false;
            controlPanel.classList.add('hidden');
            fab.classList.remove('active');
            fab.textContent = '⚙';
        });

        if (window.innerWidth <= 768) {
            controlPanel.classList.add('hidden');
        }

        let isDragging = false;
        let lastTouchX = 0;
        let lastTouchY = 0;

        let animationId;
        let isPaused = false;
        let time = 0;

        let animationSpeed = 1;
        let curveDetail = 150;
        let curveType = 0;
        let trailLength = 0.75;
        let showGrid = true;
        let gridDensity = 80;

        let gridBursts = new Map();
        const burstDuration = 40;

        const bwColors = ["#ffffff", "#f0f0f0", "#d0d0d0", "#b0b0b0"];

        function updateSliderFills() {
            const speedSlider = document.getElementById("animationSpeed");
            const detailSlider = document.getElementById("curveDetail");
            const typeSlider = document.getElementById("curveType");
            const trailSlider = document.getElementById("trailLength");

            const speedFill = document.getElementById("speedFill");
            const detailFill = document.getElementById("detailFill");
            const typeFill = document.getElementById("typeFill");
            const trailFill = document.getElementById("trailFill");

            const speedPercent = (speedSlider.value - speedSlider.min) / (speedSlider.max - speedSlider.min) * 100;
            const detailPercent = (detailSlider.value - detailSlider.min) / (detailSlider.max - detailSlider.min) * 100;
            const typePercent = (typeSlider.value - typeSlider.min) / (typeSlider.max - typeSlider.min) * 100;
            const trailPercent = (trailSlider.value - trailSlider.min) / (trailSlider.max - trailSlider.min) * 100;

            speedFill.style.width = `${speedPercent}%`;
            detailFill.style.width = `${detailPercent}%`;
            typeFill.style.width = `${typePercent}%`;
            trailFill.style.width = `${trailPercent}%`;
        }

        function updateGridToggleUI() {
            const gridToggle = document.getElementById("gridToggle");
            const gridToggleContainer = document.getElementById("gridToggleContainer");

            gridToggle.checked = showGrid;
            if (showGrid) {
                gridToggleContainer.classList.add("active");
            } else {
                gridToggleContainer.classList.remove("active");
            }
        }

        function resizeCanvas() {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;

            ctx.imageSmoothingEnabled = false;
            ctx.webkitImageSmoothingEnabled = false;
            ctx.mozImageSmoothingEnabled = false;
            ctx.msImageSmoothingEnabled = false;
        }

        function getBWColor(index) {
            return bwColors[index % bwColors.length];
        }

        function drawNonagon(x, y, size) {
            ctx.beginPath();
            for (let i = 0; i < 9; i++) {
                const angle = (i * 2 * Math.PI) / 9;
                const px = x + Math.cos(angle) * size;
                const py = y + Math.sin(angle) * size;
                if (i === 0) {
                    ctx.moveTo(px, py);
                } else {
                    ctx.lineTo(px, py);
                }
            }
            ctx.closePath();
        }

        function getNonagonGridPosition(row, col) {
            const nonWidth = gridDensity;
            const nonHeight = gridDensity * 0.866;
            const x = col * nonWidth * 0.8;
            const y = row * nonHeight + ((col % 2) * nonHeight) / 2;
            return { x, y };
        }

        function getNearestNonagonNode(x, y) {
            const nonWidth = gridDensity;
            const nonHeight = gridDensity * 0.866;

            const col = Math.round(x / (nonWidth * 0.8));
            const row = Math.round((y - ((col % 2) * nonHeight) / 2) / nonHeight);

            const nonPos = getNonagonGridPosition(row, col);

            let closestDist = Infinity;
            let closestPos = nonPos;

            for (let dr = -1; dr <= 1; dr++) {
                for (let dc = -1; dc <= 1; dc++) {
                    const testPos = getNonagonGridPosition(row + dr, col + dc);
                    const dist = Math.sqrt((x - testPos.x) ** 2 + (y - testPos.y) ** 2);
                    if (dist < closestDist) {
                        closestDist = dist;
                        closestPos = testPos;
                    }
                }
            }

            return { pos: closestPos, distance: closestDist };
        }

        function checkNodeCollision(x, y) {
            const nonResult = getNearestNonagonNode(x, y);
            const distance = nonResult.distance;

            if (distance < gridDensity * 0.4) {
                const nodeKey = `${nonResult.pos.x.toFixed(0)},${nonResult.pos.y.toFixed(0)}`;
                if (!gridBursts.has(nodeKey)) {
                    gridBursts.set(nodeKey, {
                        x: nonResult.pos.x,
                        y: nonResult.pos.y,
                        frame: 0,
                        intensity: 1,
                        color: getBWColor(Math.floor(Math.random() * bwColors.length))
                    });
                }
            }
        }

        function updateBursts() {
            for (let [key, burst] of gridBursts) {
                burst.frame++;
                burst.intensity = Math.max(0, 1 - burst.frame / burstDuration);

                if (burst.frame >= burstDuration) {
                    gridBursts.delete(key);
                }
            }
        }

        function drawBursts() {
            for (let [key, burst] of gridBursts) {
                const size = 8 + (1 - burst.intensity) * 15;
                const alpha = burst.intensity;

                ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.8})`;
                ctx.lineWidth = 2;
                drawNonagon(burst.x, burst.y, size);
                ctx.stroke();

                ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
                ctx.fillRect(burst.x - 1, burst.y - 1, 2, 2);
            }
        }

        function createQuarticCurvePoints(t) {
            const points = [];
            const scale = Math.min(canvas.width, canvas.height) * 0.4;
            const centerX = canvas.width / 2;
            const centerY = canvas.height / 2;

            for (let i = 0; i < curveDetail; i++) {
                const param = (i / curveDetail) * Math.PI * 2;
                let x, y;

                switch (curveType) {
                    case 0:
                        const k = 4;
                        const r = Math.cos(k * param + t);
                        x = r * Math.cos(param);
                        y = r * Math.sin(param);
                        break;

                    case 1:
                        const a = 1;
                        const cos2t = Math.cos(2 * param + t);
                        if (cos2t >= 0) {
                            const r2 = a * a * cos2t;
                            const r_lem = Math.sqrt(r2);
                            x = r_lem * Math.cos(param);
                            y = r_lem * Math.sin(param);
                        } else {
                            continue;
                        }
                        break;

                    case 2:
                        const r_spiral = Math.pow(param / (Math.PI * 2), 0.25) + 0.3;
                        x = r_spiral * Math.cos(param + t);
                        y = r_spiral * Math.sin(param + t);
                        break;

                    case 3:
                        const r_butterfly =
                            Math.exp(Math.sin(param + t)) -
                            2 * Math.cos(4 * param) +
                            Math.pow(Math.sin((2 * param - Math.PI) / 24), 5);
                        x = r_butterfly * Math.cos(param) * 0.3;
                        y = r_butterfly * Math.sin(param) * 0.3;
                        break;

                    case 4:
                        const r_clover = Math.sin(2 * param + t) * Math.cos(2 * param);
                        x = r_clover * Math.cos(param);
                        y = r_clover * Math.sin(param);
                        break;

                    default:
                        x = Math.cos(param + t);
                        y = Math.sin(param + t);
                }

                points.push({
                    x: centerX + x * scale,
                    y: centerY + y * scale,
                    param: param,
                    index: i
                });
            }

            return points;
        }

        function drawGrid() {
            if (!showGrid) return;

            const nonSize = gridDensity * 0.15;
            const nonWidth = gridDensity;
            const nonHeight = gridDensity * 0.866;

            ctx.strokeStyle = "rgba(255, 255, 255, 0.05)";
            ctx.lineWidth = 1;

            const cols = Math.ceil(canvas.width / (nonWidth * 0.8)) + 2;
            const rows = Math.ceil(canvas.height / nonHeight) + 2;

            for (let row = -1; row < rows; row++) {
                for (let col = -1; col < cols; col++) {
                    const pos = getNonagonGridPosition(row, col);
                    if (
                        pos.x > -nonWidth &&
                        pos.x < canvas.width + nonWidth &&
                        pos.y > -nonHeight &&
                        pos.y < canvas.height + nonHeight
                    ) {
                        drawNonagon(pos.x, pos.y, nonSize);
                        ctx.stroke();
                    }
                }
            }

            const nodeSize = 2;
            let nodeIndex = 0;

            for (let row = -1; row < rows; row++) {
                for (let col = -1; col < cols; col++) {
                    const pos = getNonagonGridPosition(row, col);
                    if (
                        pos.x > -nonWidth &&
                        pos.x < canvas.width + nonWidth &&
                        pos.y > -nonHeight &&
                        pos.y < canvas.height + nonHeight
                    ) {
                        ctx.fillStyle = `rgba(255, 255, 255, ${0.2 + 0.1 * (nodeIndex % 3)})`;
                        ctx.fillRect(
                            pos.x - nodeSize / 2,
                            pos.y - nodeSize / 2,
                            nodeSize,
                            nodeSize
                        );

                        nodeIndex++;
                    }
                }
            }
        }

        function animate() {
            if (isPaused) {
                animationId = requestAnimationFrame(animate);
                return;
            }

            time += 0.02 * animationSpeed;

            ctx.fillStyle = `rgba(0, 0, 0, ${1 - trailLength})`;
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            updateBursts();
            drawGrid();
            drawBursts();

            const curvePoints = createQuarticCurvePoints(time);

            for (let i = 0; i < curvePoints.length - 1; i += 2) {
                const p1 = curvePoints[i];
                const p2 = curvePoints[i + 1];

                if (p1 && p2) {
                    const distance = Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
                    if (distance < 50) {
                        const alpha = Math.max(0, 1 - distance / 50) * 0.15;
                        ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`;

                        ctx.beginPath();
                        ctx.moveTo(p1.x, p1.y);
                        ctx.lineTo(p2.x, p2.y);
                        ctx.stroke();
                    }
                }
            }

            curvePoints.forEach((point) => {
                if (point) {
                    checkNodeCollision(point.x, point.y);

                    const size = 3 + Math.sin(point.param + time) * 1;
                    const colorIntensity = 0.7 + 0.3 * Math.sin(point.index * 0.1 + time);

                    ctx.fillStyle = `rgba(255, 255, 255, ${colorIntensity})`;
                    ctx.fillRect(point.x - size / 2, point.y - size / 2, size, size);

                    ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
                    ctx.fillRect(point.x - size / 4, point.y - size / 4, size / 2, size / 2);
                }
            });

            animationId = requestAnimationFrame(animate);
        }

        const curveNames = ['Rose', 'Lemniscate', 'Spiral', 'Butterfly', 'Clover'];

        function updateAllSliders() {
            updateSliderFills();
            updateGridToggleUI();

            document.getElementById("speedValue").textContent = animationSpeed.toFixed(1);
            document.getElementById("detailValue").textContent = curveDetail;
            document.getElementById("typeValue").textContent = `${curveNames[curveType]} (${curveType})`;
            document.getElementById("trailValue").textContent = trailLength.toFixed(2);
        }

        document.getElementById("animationSpeed").addEventListener("input", (e) => {
            animationSpeed = parseFloat(e.target.value);
            updateAllSliders();
        });

        document.getElementById("curveDetail").addEventListener("input", (e) => {
            curveDetail = parseInt(e.target.value);
            updateAllSliders();
        });

        document.getElementById("curveType").addEventListener("input", (e) => {
            curveType = parseInt(e.target.value);
            updateAllSliders();
        });

        document.getElementById("trailLength").addEventListener("input", (e) => {
            trailLength = parseFloat(e.target.value);
            updateAllSliders();
        });

        const gridToggle = document.getElementById("gridToggle");
        const gridToggleContainer = document.getElementById("gridToggleContainer");

        gridToggle.addEventListener("change", (e) => {
            showGrid = e.target.checked;
            updateGridToggleUI();
        });

        gridToggleContainer.addEventListener("click", (e) => {
            if (e.target !== gridToggle && e.target.type !== "checkbox") {
                showGrid = !showGrid;
                gridToggle.checked = showGrid;
                updateGridToggleUI();
            }
        });

        document.getElementById("resetBtn").addEventListener("click", () => {
            time = 0;
            gridBursts.clear();
        });

        document.getElementById("pauseBtn").addEventListener("click", (e) => {
            isPaused = !isPaused;
            e.target.textContent = isPaused ? "Play" : "Pause";

            e.target.style.background = isPaused ?
                "rgba(255, 255, 255, 0.2)" :
                "rgba(255, 255, 255, 0.05)";
            e.target.style.borderColor = isPaused ?
                "rgba(255, 255, 255, 0.5)" :
                "rgba(255, 255, 255, 0.2)";
        });

        document.getElementById("randomBtn").addEventListener("click", () => {
            animationSpeed = Math.random() * 2.9 + 0.1;
            curveDetail = Math.floor(Math.random() * 251) + 50;
            curveType = Math.floor(Math.random() * 5);
            trailLength = Math.random() * 0.45 + 0.5;

            document.getElementById("animationSpeed").value = animationSpeed;
            document.getElementById("curveDetail").value = curveDetail;
            document.getElementById("curveType").value = curveType;
            document.getElementById("trailLength").value = trailLength;

            updateAllSliders();
        });

        updateAllSliders();

        canvas.addEventListener("touchstart", (e) => {
            if (panelOpen && window.innerWidth <= 768) return;

            e.preventDefault();
            isDragging = true;
            const touch = e.touches[0];
            lastTouchX = touch.clientX;
            lastTouchY = touch.clientY;
        }, { passive: false });

        canvas.addEventListener("touchmove", (e) => {
            if (!isDragging) return;

            e.preventDefault();
            const touch = e.touches[0];
            const deltaX = (touch.clientX - lastTouchX) * 0.01;
            const deltaY = (touch.clientY - lastTouchY) * 0.01;

            time += deltaX + deltaY;

            lastTouchX = touch.clientX;
            lastTouchY = touch.clientY;
        }, { passive: false });

        canvas.addEventListener("touchend", (e) => {
            e.preventDefault();
            isDragging = false;
        }, { passive: false });

        canvas.addEventListener("mousedown", (e) => {
            isDragging = true;
            lastTouchX = e.clientX;
            lastTouchY = e.clientY;
        });

        canvas.addEventListener("mousemove", (e) => {
            if (!isDragging) return;

            const deltaX = (e.clientX - lastTouchX) * 0.01;
            const deltaY = (e.clientY - lastTouchY) * 0.01;

            time += deltaX + deltaY;

            lastTouchX = e.clientX;
            lastTouchY = e.clientY;
        });

        canvas.addEventListener("mouseup", () => {
            isDragging = false;
        });

        const controlGroups = document.querySelectorAll('.control-group');
        controlGroups.forEach(group => {
            group.addEventListener('mouseenter', () => {
                group.style.transform = 'translateX(5px)';
            });

            group.addEventListener('mouseleave', () => {
                group.style.transform = 'translateX(0)';
            });
        });

        window.addEventListener("resize", () => {
            resizeCanvas();

            if (window.innerWidth > 768) {
                controlPanel.classList.remove('hidden');
                panelOpen = false;
            } else {
                controlPanel.classList.add('hidden');
                panelOpen = false;
            }
        });

        resizeCanvas();

        if (window.innerWidth > 768) {
            controlPanel.classList.remove('hidden');
        }

        animate();
    </script>
</body>
</html>