← Mike Higgins Home
Custom Code Experiment •

Fractal SVG

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, user-scalable=no">
    <title>Fractal SVG (MouseOver Effect)</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: #0a0a1a;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            overflow: hidden;
            font-family: 'Segoe UI', 'Monaco', monospace;
        }

        /* fractal container fills viewport with immersive bg */
        .fractal-bg {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 0;
            background: #03030c;
            cursor: crosshair;
        }

        svg {
            width: 100%;
            height: 100%;
            display: block;
            filter: drop-shadow(0 0 6px rgba(0,0,0,0.4));
        }

        /* floating UI – minimal & elegant */
        .info {
            position: fixed;
            bottom: 20px;
            left: 20px;
            background: rgba(15, 15, 30, 0.75);
            backdrop-filter: blur(8px);
            border-radius: 2rem;
            padding: 8px 20px;
            color: #ffe1aa;
            font-size: 0.85rem;
            font-weight: 500;
            font-family: monospace;
            letter-spacing: 1px;
            border-left: 3px solid #ffcc77;
            z-index: 10;
            pointer-events: none;
            box-shadow: 0 4px 18px rgba(0,0,0,0.4);
        }

        .badge {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: rgba(0,0,0,0.6);
            backdrop-filter: blur(4px);
            padding: 5px 14px;
            border-radius: 40px;
            font-size: 0.7rem;
            color: #ccb87c;
            font-family: monospace;
            pointer-events: none;
            z-index: 10;
        }

        .info span {
            color: #ffdf99;
            font-weight: bold;
            animation: softPulse 2s infinite alternate;
        }

        @keyframes softPulse {
            0% { text-shadow: 0 0 0px #ffdf99; opacity: 0.8; }
            100% { text-shadow: 0 0 4px #ffbb44; opacity: 1; }
        }

        /* subtle overlay effect while hovering */
        .fractal-bg:hover svg {
            filter: drop-shadow(0 0 12px rgba(255,200,100,0.2));
            transition: filter 0.2s ease;
        }
    </style>
</head>
<body>
<div class="fractal-bg">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800" preserveAspectRatio="xMidYMid slice">
        <defs>
            <!-- glowing gradients for depth -->
            <radialGradient id="glowGold" cx="50%" cy="50%" r="50%">
                <stop offset="0%" stop-color="#ffd966" stop-opacity="0.9"/>
                <stop offset="80%" stop-color="#cc9933" stop-opacity="0.3"/>
                <stop offset="100%" stop-color="#442200" stop-opacity="0"/>
            </radialGradient>

            <linearGradient id="metalDark" x1="0%" y1="0%" x2="100%" y2="100%">
                <stop offset="0%" stop-color="#2a2418"/>
                <stop offset="100%" stop-color="#0e0b07"/>
            </linearGradient>

            <filter id="glowEffect" x="-20%" y="-20%" width="140%" height="140%">
                <feGaussianBlur in="SourceAlpha" stdDeviation="2"/>
                <feMerge>
                    <feMergeNode in="offsetblur" />
                    <feMergeNode in="SourceGraphic" />
                </feMerge>
            </filter>

            <!-- pattern for micro fill in deep space -->
            <pattern id="stardust" patternUnits="userSpaceOnUse" width="40" height="40">
                <circle cx="5" cy="8" r="0.8" fill="#ffdd99" opacity="0.5"/>
                <circle cx="28" cy="22" r="0.5" fill="#ffbb77" opacity="0.6"/>
                <circle cx="12" cy="35" r="0.6" fill="#ffcc88"/>
                <circle cx="35" cy="12" r="0.4" fill="#ffe0aa"/>
            </pattern>
        </defs>

        <!-- background starry canvas -->
        <rect width="100%" height="100%" fill="url(#stardust)" opacity="0.3"/>
        <rect width="100%" height="100%" fill="url(#metalDark)" opacity="0.85"/>

        <!-- fractal groups: will be populated recursively via JS -->
        <g id="fractalRoot" stroke-linecap="round" stroke-linejoin="round"></g>

        <!-- decorative floating orbs (ambient) -->
        <circle cx="15%" cy="20%" r="45" fill="url(#glowGold)" opacity="0.2">
            <animate attributeName="opacity" values="0.1;0.3;0.1" dur="8s" repeatCount="indefinite"/>
        </circle>
        <circle cx="85%" cy="75%" r="70" fill="url(#glowGold)" opacity="0.15">
            <animate attributeName="r" values="70;85;70" dur="12s" repeatCount="indefinite"/>
        </circle>
    </svg>
</div>

<div class="badge">
    🔺 recursive depth 5 • mouse morph 🌀
</div>

<script>
    (function(){
        // ---------- ADVANCED FRACTAL SVG ENGINE ----------
        // builds a recursive Sierpinski triangle / tetrahedral fractal variant
        // with hover effect: colors shift + subtle local transforms

        const svgNS = "http://www.w3.org/2000/svg";
        const rootGroup = document.getElementById("fractalRoot");
        if (!rootGroup) return;

        // fractal parameters
        const WIDTH = 1200;
        const HEIGHT = 800;
        const MAX_DEPTH = 5;       // complexity: 5 levels = 243 triangles (dense)
        let currentDepth = MAX_DEPTH;

        // color palettes (rich, advanced metallic / neon)
        const PALETTE_DEFAULT = [
            "#ffbe76", "#f6ae72", "#e09d5e", "#c2814a", "#a26338", "#7e4a2a", "#5a3520", "#3e2618"
        ];
        const PALETTE_HOVER = [
            "#9b5de5", "#f15fbb", "#fee440", "#00bbf9", "#00f5d4", "#ff006e", "#fb8b67", "#ca7df9"
        ];

        let hoverActive = false;
        let currentPalette = PALETTE_DEFAULT;

        // store generated fractal elements for efficient hover update
        let fractalElements = []; // each: { element, baseFill, originalTransform, depth }

        // --- recursive fractal: Sierpinski triangle (vertex-based) with beautiful subdivisions
        // we create triangles at each depth, filled with gradient-like solid colors based on depth & position
        function generateSierpinski(p1, p2, p3, depth, maxDepth, group) {
            if (depth > maxDepth) return;

            // compute midpoints for subdivision
            const mid12 = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
            const mid23 = { x: (p2.x + p3.x) / 2, y: (p2.y + p3.y) / 2 };
            const mid31 = { x: (p3.x + p1.x) / 2, y: (p3.y + p1.y) / 2 };

            // draw main triangle at this depth (except at deepest level we draw all)
            // but classic sierpinski: draw only the central inverted triangle as "hole" and recurse on corners
            // instead we draw the corner triangles recursively, and the central triangle is omitted (hole)
            // to create a more intricate lace-like pattern, we actually fill the corner triangles and leave center empty.
            // However for visual richness we will fill all three corner triangles and also add decorative edges.

            // fill corner triangles (with colors)
            const cornerTriangles = [
                { points: [p1, mid12, mid31], depthOffset: 0 },
                { points: [p2, mid12, mid23], depthOffset: 1 },
                { points: [p3, mid23, mid31], depthOffset: 2 }
            ];

            cornerTriangles.forEach((tri, idx) => {
                const fillColor = getColorForDepth(depth, idx, tri.points[0].x, tri.points[0].y);
                const trianglePoly = document.createElementNS(svgNS, "polygon");
                const pointsStr = tri.points.map(p => `${p.x},${p.y}`).join(" ");
                trianglePoly.setAttribute("points", pointsStr);
                trianglePoly.setAttribute("fill", fillColor);
                trianglePoly.setAttribute("stroke", "#f0cf8a");
                trianglePoly.setAttribute("stroke-width", depth === 0 ? "1.2" : "0.55");
                trianglePoly.setAttribute("stroke-opacity", "0.65");
                trianglePoly.setAttribute("class", "fractal-triangle");
                // add slight glow on edges
                trianglePoly.setAttribute("filter", depth <= 2 ? "url(#glowEffect)" : "");
                group.appendChild(trianglePoly);

                // store for hover effects
                fractalElements.push({
                    element: trianglePoly,
                    baseFill: fillColor,
                    depth: depth,
                    originalStroke: "#f0cf8a",
                    idx: idx
                });

                // recursive call for deeper levels
                generateSierpinski(tri.points[0], tri.points[1], tri.points[2], depth + 1, maxDepth, group);
            });

            // optional: draw decorative central small shape (inverted star) only at certain depths
            if (depth <= maxDepth - 2) {
                const centerPoly = document.createElementNS(svgNS, "polygon");
                const centerPoints = [mid12, mid23, mid31];
                const pointsC = centerPoints.map(p => `${p.x},${p.y}`).join(" ");
                centerPoly.setAttribute("points", pointsC);
                centerPoly.setAttribute("fill", "none");
                centerPoly.setAttribute("stroke", "#ffd58c");
                centerPoly.setAttribute("stroke-width", "0.8");
                centerPoly.setAttribute("stroke-dasharray", "4 2");
                centerPoly.setAttribute("opacity", "0.5");
                group.appendChild(centerPoly);
                fractalElements.push({
                    element: centerPoly,
                    baseFill: "none",
                    depth: depth,
                    originalStroke: "#ffd58c",
                    isEdge: true
                });
            }
        }

        // advanced color mapping: based on depth, position (x), and hover state
        function getColorForDepth(depth, subIdx, x, y) {
            if (hoverActive) {
                // psychedelic shifting: use palette hover + modulate by position
                const hueShift = ((x * 0.003 + y * 0.002) * 20 + depth * 15 + subIdx * 40) % 360;
                // pick from hover palette with rotation
                const pal = PALETTE_HOVER;
                const idx = (Math.floor(hueShift / 45) + depth * 2 + subIdx) % pal.length;
                return pal[idx];
            } else {
                // default: warm gold / ember fractal
                const baseIdx = (depth * 2 + subIdx) % PALETTE_DEFAULT.length;
                let col = PALETTE_DEFAULT[baseIdx];
                // vary brightness with x,y to create organic variation
                const brightFactor = 0.7 + (Math.sin(x * 0.02 + y * 0.015) * 0.2);
                // color adjustment (lighten/darken via simple rgb mix)
                return adjustColorBrightness(col, brightFactor);
            }
        }

        function adjustColorBrightness(hex, factor) {
            // simple helper to make color richer
            let r = parseInt(hex.slice(1,3), 16);
            let g = parseInt(hex.slice(3,5), 16);
            let b = parseInt(hex.slice(5,7), 16);
            r = Math.min(255, Math.floor(r * factor));
            g = Math.min(255, Math.floor(g * factor));
            b = Math.min(255, Math.floor(b * factor));
            return `rgb(${r}, ${g}, ${b})`;
        }

        // function to rebuild fractal (full generation)
        function buildFractal(depthLevel) {
            // clear previous
            while (rootGroup.firstChild) {
                rootGroup.removeChild(rootGroup.firstChild);
            }
            fractalElements = [];

            // Define three main vertices of the big Sierpinski triangle (centered & majestic)
            // Use viewBox 1200x800, create isosceles triangle covering most area.
            const top = { x: 600, y: 80 };
            const left = { x: 180, y: 720 };
            const right = { x: 1020, y: 720 };

            // recursive generation
            generateSierpinski(top, left, right, 0, depthLevel, rootGroup);

            // extra embellishment: fractal dust particles (tiny dots) inside deepest triangles
            addFractalDust(rootGroup);
        }

        // add delicate particle system for extra 'advanced fractal' look
        function addFractalDust(group) {
            for (let i = 0; i < 340; i++) {
                let x = 150 + Math.random() * 900;
                let y = 80 + Math.random() * 680;
                // only place inside plausible area
                let radius = 0.9 + Math.random() * 2.2;
                let dot = document.createElementNS(svgNS, "circle");
                dot.setAttribute("cx", x);
                dot.setAttribute("cy", y);
                dot.setAttribute("r", radius);
                dot.setAttribute("fill", `rgba(255, 210, 130, ${0.3 + Math.random() * 0.5})`);
                dot.setAttribute("class", "dust-particle");
                group.appendChild(dot);
                fractalElements.push({ element: dot, isDust: true, baseOpacity: 0.3 + Math.random() * 0.5 });
            }
        }

        // ---------- HOVER EFFECT: dynamic color morph & tiny rotations ----------
        function applyHoverEffect(isHover) {
            hoverActive = isHover;
            // update all triangle colors based on new palette
            fractalElements.forEach(item => {
                if (item.element.tagName === "polygon") {
                    const depth = item.depth;
                    const subIdx = item.idx !== undefined ? item.idx : 0;
                    // get bounding box center for positional color variation
                    let bbox;
                    try {
                        bbox = item.element.getBBox();
                    } catch(e) { bbox = { x: 300, y: 300 }; }
                    const centerX = bbox.x + bbox.width/2;
                    const centerY = bbox.y + bbox.height/2;
                    const newFill = getColorForDepth(depth, subIdx, centerX, centerY);
                    item.element.setAttribute("fill", newFill);

                    // stroke magic: on hover, strokes become neon
                    if (isHover) {
                        item.element.setAttribute("stroke", "#ffdd77");
                        item.element.setAttribute("stroke-width", "1.1");
                        item.element.setAttribute("stroke-opacity", "0.95");
                        // subtle transform scale per triangle (local)
                        if (depth <= 2) {
                            item.element.setAttribute("transform", "scale(1.01) translate(-0.5%, -0.5%)");
                        }
                    } else {
                        item.element.setAttribute("stroke", "#f0cf8a");
                        item.element.setAttribute("stroke-width", depth === 0 ? "1.2" : "0.55");
                        item.element.setAttribute("stroke-opacity", "0.65");
                        item.element.removeAttribute("transform");
                    }
                }
                else if (item.element.tagName === "polyline" || item.element.tagName === "line") {
                    // optional edge handling
                    if (isHover) {
                        item.element.setAttribute("stroke", "#ffcc88");
                        item.element.setAttribute("stroke-width", "1.4");
                    } else {
                        item.element.setAttribute("stroke", "#ffd58c");
                        item.element.setAttribute("stroke-width", "0.8");
                    }
                }
                else if (item.element.classList && item.element.classList.contains("dust-particle")) {
                    // dust particles react: increase opacity and glow on hover
                    const newOpacity = isHover ? 0.85 : (item.baseOpacity || 0.4);
                    item.element.setAttribute("fill", isHover ? `rgba(255, 220, 100, ${newOpacity})` : `rgba(255, 210, 130, ${newOpacity})`);
                    if (isHover) {
                        let rad = parseFloat(item.element.getAttribute("r")) || 1.2;
                        item.element.setAttribute("r", rad * 1.6);
                    } else {
                        let rad = parseFloat(item.element.getAttribute("originalR")) || 1.2;
                        if (!item.element.getAttribute("originalR")) {
                            let orig = parseFloat(item.element.getAttribute("r")) || 1.2;
                            item.element.setAttribute("originalR", orig);
                            rad = orig;
                        } else {
                            rad = parseFloat(item.element.getAttribute("originalR"));
                        }
                        item.element.setAttribute("r", rad);
                    }
                }
            });

            // add extra floating animated effect: rotate the entire root slightly on hover? (subtle)
            if (isHover) {
                rootGroup.setAttribute("transform", "rotate(0.2, 600, 400)");
            } else {
                rootGroup.removeAttribute("transform");
            }
        }

        // ---------- Mouse tracking + hover state ----------
        const container = document.querySelector(".fractal-bg");
        let hoverTimeout = null;

        function onMouseMoveWithin(event) {
            if (!hoverActive) {
                applyHoverEffect(true);
                // also shift color palette dynamically based on mouse position for extra 'advanced'
                updateDynamicColorShift(event);
            } else {
                updateDynamicColorShift(event);
            }
            // reset debounce for leaving
            if (hoverTimeout) clearTimeout(hoverTimeout);
            hoverTimeout = setTimeout(() => {
                // not leaving yet, but keep fresh
            }, 50);
        }

        function updateDynamicColorShift(event) {
            if (!hoverActive) return;
            // Extra interactivity: color shift based on mouse X/Y position (RGB modulation)
            const rect = container.getBoundingClientRect();
            const mouseNormX = (event.clientX - rect.left) / rect.width;
            const mouseNormY = (event.clientY - rect.top) / rect.height;
            // apply subtle 'chromatic aberration' to strokes/fills on the fly
            fractalElements.forEach(item => {
                if (item.element.tagName === "polygon" && item.depth !== undefined) {
                    const depth = item.depth;
                    let hueOffset = Math.floor(mouseNormX * 8 + mouseNormY * 5) % PALETTE_HOVER.length;
                    let colIndex = (depth + hueOffset) % PALETTE_HOVER.length;
                    let newCol = PALETTE_HOVER[colIndex];
                    // also modulate brightness
                    let bright = 0.7 + (mouseNormX * 0.5);
                    let finalCol = adjustColorBrightness(newCol, bright);
                    item.element.setAttribute("fill", finalCol);
                    // additional warp: tiny rotation offset for inner triangles
                    if (depth <= 2) {
                        let rotAngle = (mouseNormX - 0.5) * 2.5;
                        item.element.setAttribute("transform", `rotate(${rotAngle}, 0, 0) scale(1.01)`);
                    }
                }
            });
        }

        function onMouseLeaveContainer() {
            if (hoverActive) {
                applyHoverEffect(false);
                // reset any dynamic transforms on polygons
                fractalElements.forEach(item => {
                    if (item.element.tagName === "polygon" && item.depth !== undefined && item.depth <= 2) {
                        item.element.removeAttribute("transform");
                    }
                });
                if (hoverTimeout) clearTimeout(hoverTimeout);
            }
        }

        // rebuild fractal on window load (ensures smooth)
        function init() {
            buildFractal(MAX_DEPTH);
            // attach hover events
            container.addEventListener("mousemove", onMouseMoveWithin);
            container.addEventListener("mouseleave", onMouseLeaveContainer);
            // also add mousemove throttling is fine
            // optional: add window resize adaptation? we keep viewBox fixed so fractal stays centered
            // but for extra responsiveness, we might rebuild on orientation? not mandatory.

            // little extra: add animated legend to UI using pseudo effect
            const modeSpan = document.querySelector(".info span");
            if (modeSpan) {
                setInterval(() => {
                    if (hoverActive) {
                        modeSpan.textContent = "🌀 FRACTAL WARP ACTIVE 🌀";
                    } else {
                        modeSpan.textContent = "SIERPINSKI FRACTAL TETRA";
                    }
                }, 300);
            }
        }

        // also export a regeneration on demand? no needed
        init();
    })();
</script>
</body>
</html>