← Mike Higgins Home
Custom Code Experiment •

3D Tiles

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, maximum-scale=1.0, user-scalable=no">
    <title>3D Hover Tiles</title>
    <style>
        * {
            box-sizing: border-box;
        }

        body {
            background: #000000;
            overflow: hidden;
            margin: 0;
            padding: 0;
            height: 100vh;
            display: flex;
            flex-direction: column;
            justify-content: flex-start; /* Changed from center to flex-start */
            align-items: center;
            -webkit-tap-highlight-color: transparent;
            -webkit-touch-callout: none;
            touch-action: manipulation;
        }

        p {
            font-family: Arial, Helvetica, sans-serif;
            font-size: clamp(1em, 4vw, 2em);
            position: absolute;
            top: 1em;
            left: 1em;
            margin: 0;
            color: #6A8EAE;
            z-index: 10;
            text-shadow: 0 0 10px rgba(106, 142, 174, 0.5);
            padding: 0 1rem;
        }

        #Container {
            perspective: 1000px;
            width: 100%;
            display: flex;
            justify-content: center;
            align-items: flex-start; /* Changed from center to flex-start */
            flex-grow: 1;
            padding: 1rem;
            overflow: hidden;
            margin-top: 0; /* Ensure no top margin */
        }

        #Tiles {
            width: 90vmin;
            height: 90vmin;
            max-width: 630px;
            max-height: 630px;
            position: relative;
            transform-style: preserve-3d;
            /* Reduced rotation for mobile to prevent distortion */
            transform: rotateX(65deg) rotateY(0deg) rotateZ(45deg);
            margin-top: 0; /* Ensure no top margin */
        }

        .title {
            width: calc(100% / 3 - 6px);
            height: calc(100% / 3 - 6px);
            position: absolute;
            cursor: pointer;
            transform-style: preserve-3d;
            touch-action: manipulation;
        }

        /* Position each tile in the 3x3 grid */
        .title:nth-child(1) { top: 0; left: 0; }
        .title:nth-child(2) { top: 0; left: calc(100% / 3); }
        .title:nth-child(3) { top: 0; left: calc(200% / 3); }
        .title:nth-child(4) { top: calc(100% / 3); left: 0; }
        .title:nth-child(5) { top: calc(100% / 3); left: calc(100% / 3); }
        .title:nth-child(6) { top: calc(100% / 3); left: calc(200% / 3); }
        .title:nth-child(7) { top: calc(200% / 3); left: 0; }
        .title:nth-child(8) { top: calc(200% / 3); left: calc(100% / 3); }
        .title:nth-child(9) { top: calc(200% / 3); left: calc(200% / 3); }

        .tile-face {
            position: absolute;
            width: 100%;
            height: 100%;
            background: #4A6FA5;
            transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
            transform: translateZ(0);
            backface-visibility: hidden;
            -webkit-backface-visibility: hidden;
            border-radius: 2px;
            /* Mobile-friendly hover/active state */
            cursor: pointer;
        }

        .tile-face::after {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 50%);
            pointer-events: none;
            border-radius: 2px;
        }

        /* Hover effect for desktop */
        @media (hover: hover) and (pointer: fine) {
            .title:hover .tile-face {
                background: #E76F51;
                transform: translateZ(40px);
                box-shadow: 15px 15px 15px rgba(0,0,0,0.5);
            }
        }

        /* Active/touch effect for mobile */
        .title.active .tile-face,
        .title:active .tile-face {
            background: #E76F51;
            transform: translateZ(20px);
            box-shadow: 10px 10px 10px rgba(0,0,0,0.4);
            transition: transform 0.15s ease, background-color 0.15s ease;
        }

        .instructions {
            position: absolute;
            bottom: 1em;
            right: 1em;
            color: #6A8EAE;
            font-family: Arial, Helvetica, sans-serif;
            font-size: clamp(0.8em, 3vw, 1em);
            text-align: right;
            max-width: min(300px, 90vw);
            line-height: 1.5;
            text-shadow: 0 0 5px rgba(106, 142, 174, 0.5);
            padding: 0 1rem;
        }

        /* Mobile-specific optimizations */
        @media (max-width: 768px) {
            body {
                justify-content: flex-start; /* Already set, but ensures mobile */
                padding-top: 0.5vh; /* Minimal top padding */
                margin-top: 0;
            }

            #Container {
                perspective: 600px;
                padding: 0.25rem 0.5rem; /* Greatly reduced padding */
                padding-top: 0; /* No top padding */
                margin-top: 0; /* No top margin */
                align-items: flex-start; /* Align to top */
                min-height: auto; /* Don't force full height */
                height: auto;
                flex-grow: 0; /* Don't take up all space */
            }

            #Tiles {
                width: 85vmin; /* Slightly smaller on mobile */
                height: 85vmin;
                transform: rotateX(60deg) rotateY(0deg) rotateZ(45deg);
                margin-top: 2vh; /* Minimal top margin */
            }

            p {
                top: 0.25em; /* Much closer to top */
                left: 0.25em;
                font-size: clamp(0.8em, 4vw, 1.2em); /* Slightly smaller */
                padding: 0 0.5rem; /* Reduced padding */
            }

            .instructions {
                bottom: 0.25em; /* Closer to bottom */
                right: 0.25em;
                font-size: clamp(0.7em, 3.5vw, 0.9em);
                padding: 0 0.5rem; /* Reduced padding */
            }

            .title {
                width: calc(100% / 3 - 4px);
                height: calc(100% / 3 - 4px);
            }
        }

        @media (max-width: 480px) {
            body {
                padding-top: 0.25vh; /* Even smaller padding */
                justify-content: flex-start;
            }

            #Container {
                perspective: 400px;
                padding: 0.125rem 0.25rem; /* Minimal padding */
                padding-top: 0;
                margin-top: 0;
                height: auto;
            }

            #Tiles {
                transform: rotateX(55deg) rotateY(0deg) rotateZ(45deg);
                margin-top: 1vh; /* Minimal top margin */
                width: 80vmin; /* Even smaller on very small screens */
                height: 80vmin;
            }

            body {
                overflow: auto;
                -webkit-overflow-scrolling: touch;
            }

            /* Reduce 3D effect on very small screens */
            .title.active .tile-face,
            .title:active .tile-face {
                transform: translateZ(15px);
                box-shadow: 5px 5px 8px rgba(0,0,0,0.3);
            }

            @media (hover: hover) and (pointer: fine) {
                .title:hover .tile-face {
                    transform: translateZ(25px);
                    box-shadow: 8px 8px 12px rgba(0,0,0,0.4);
                }
            }

            p {
                top: 0.125em; /* Minimal distance from top */
                left: 0.125em;
                font-size: clamp(0.7em, 3.5vw, 1em);
            }
        }

        /* Landscape orientation optimization */
        @media (max-height: 500px) and (orientation: landscape) {
            body {
                padding-top: 0;
                justify-content: flex-start;
            }

            #Container {
                padding: 0.125rem;
                padding-top: 0;
                margin-top: 0;
            }

            #Tiles {
                width: 70vmin; /* Smaller in landscape */
                height: 70vmin;
                transform: rotateX(50deg) rotateY(0deg) rotateZ(45deg);
                margin-top: 0.5vh; /* Very small top margin */
            }

            p {
                font-size: 0.9em;
                top: 0.125em; /* Minimal top spacing */
                left: 0.125em;
            }

            .instructions {
                display: none;
            }
        }

        /* Very small height devices */
        @media (max-height: 600px) {
            body {
                padding-top: 0;
            }

            #Container {
                padding-top: 0;
                margin-top: 0;
            }

            #Tiles {
                margin-top: 1vh;
            }
        }

        /* High DPI screens */
        @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
            .tile-face::after {
                background: linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0) 50%);
            }
        }
    </style>
</head>
<body>

    <div id="Container">
        <div id="Tiles">
            <div class="title" tabindex="0" role="button" aria-label="Tile 1"><div class="tile-face"></div></div>
            <div class="title" tabindex="0" role="button" aria-label="Tile 2"><div class="tile-face"></div></div>
            <div class="title" tabindex="0" role="button" aria-label="Tile 3"><div class="tile-face"></div></div>
            <div class="title" tabindex="0" role="button" aria-label="Tile 4"><div class="tile-face"></div></div>
            <div class="title" tabindex="0" role="button" aria-label="Tile 5"><div class="tile-face"></div></div>
            <div class="title" tabindex="0" role="button" aria-label="Tile 6"><div class="tile-face"></div></div>
            <div class="title" tabindex="0" role="button" aria-label="Tile 7"><div class="tile-face"></div></div>
            <div class="title" tabindex="0" role="button" aria-label="Tile 8"><div class="tile-face"></div></div>
            <div class="title" tabindex="0" role="button" aria-label="Tile 9"><div class="tile-face"></div></div>
        </div>
    </div>

    <script>
        // Mobile-friendly touch handling
        document.querySelectorAll('.title').forEach(tile => {
            let isTouching = false;
            let touchTimer = null;

            // Touch events
            tile.addEventListener('touchstart', function(e) {
                e.preventDefault();
                isTouching = true;
                this.classList.add('active');

                // Clear any existing timer
                if (touchTimer) clearTimeout(touchTimer);

                // Set timer to remove active class if touch is held
                touchTimer = setTimeout(() => {
                    if (isTouching) {
                        this.classList.remove('active');
                    }
                }, 500);
            }, { passive: false });

            tile.addEventListener('touchend', function(e) {
                e.preventDefault();
                isTouching = false;
                this.classList.remove('active');

                if (touchTimer) {
                    clearTimeout(touchTimer);
                    touchTimer = null;
                }

                // Trigger click effect on touch end
                triggerClickEffect(this);
            }, { passive: false });

            tile.addEventListener('touchcancel', function() {
                isTouching = false;
                this.classList.remove('active');

                if (touchTimer) {
                    clearTimeout(touchTimer);
                    touchTimer = null;
                }
            });

            // Click event (for both mouse and touch)
            tile.addEventListener('click', function(e) {
                // For mouse clicks, trigger effect
                if (!isTouching) {
                    triggerClickEffect(this);
                }
            });

            // Mouse events
            tile.addEventListener('mousedown', function() {
                this.classList.add('active');
            });

            tile.addEventListener('mouseup', function() {
                this.classList.remove('active');
            });

            tile.addEventListener('mouseleave', function() {
                this.classList.remove('active');
            });
        });

        function triggerClickEffect(element) {
            const colors = ['#E76F51', '#2A9D8F', '#E9C46A', '#264653', '#F4A261'];
            const randomColor = colors[Math.floor(Math.random() * colors.length)];
            const tileFace = element.querySelector('.tile-face');

            // Store original styles
            const originalBg = tileFace.style.backgroundColor;
            const originalTransform = tileFace.style.transform;
            const originalBoxShadow = tileFace.style.boxShadow;

            // Flash effect with mobile-optimized values
            const isMobile = window.innerWidth <= 768;
            const liftAmount = isMobile ? '25px' : '60px';
            const scaleAmount = isMobile ? '1.02' : '1.05';

            tileFace.style.backgroundColor = randomColor;
            tileFace.style.transform = `translateZ(${liftAmount}) scale(${scaleAmount})`;
            tileFace.style.boxShadow = isMobile
                ? '15px 15px 15px rgba(0,0,0,0.6)'
                : '40px 40px 30px rgba(0,0,0,0.8)';

            // Reset after animation
            setTimeout(() => {
                tileFace.style.backgroundColor = originalBg;
                tileFace.style.transform = originalTransform;
                tileFace.style.boxShadow = originalBoxShadow;
            }, 300);
        }

        // Keyboard navigation for accessibility
        document.addEventListener('keydown', (e) => {
            const tiles = Array.from(document.querySelectorAll('.title'));
            const currentIndex = tiles.findIndex(tile => tile === document.activeElement);

            if (e.key === 'Tab' && !e.shiftKey && currentIndex === tiles.length - 1) {
                e.preventDefault();
                tiles[0].focus();
            } else if (e.key === 'Tab' && e.shiftKey && currentIndex <= 0) {
                e.preventDefault();
                tiles[tiles.length - 1].focus();
            } else if (e.key === 'Enter' && document.activeElement.classList.contains('title')) {
                document.activeElement.click();
            }
        });

        // Make tiles focusable for accessibility
        document.querySelectorAll('.title').forEach(tile => {
            tile.setAttribute('tabindex', '0');

            tile.addEventListener('focus', function() {
                this.querySelector('.tile-face').style.outline = '2px solid #6A8EAE';
                this.querySelector('.tile-face').style.outlineOffset = '3px';
            });

            tile.addEventListener('blur', function() {
                this.querySelector('.tile-face').style.outline = '';
            });
        });

        // Prevent zoom on double tap for mobile
        let lastTouchEnd = 0;
        document.addEventListener('touchend', function(event) {
            const now = (new Date()).getTime();
            if (now - lastTouchEnd <= 300) {
                event.preventDefault();
            }
            lastTouchEnd = now;
        }, false);
    </script>
</body>
</html>