Testing die roll html

https://svonberg.org/wp-content/uploads/2025/11/Dicehtm.html

https://svonberg.org/wp-content/uploads/2025/11/Dicehtm.html

Code

<!DOCTYPE html>
<html lang=”en”>
<head>
    <meta charset=”UTF-8″>
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
    <title>Appalachian Dice Roller</title>
    <script src=”https://cdn.tailwindcss.com”></script>
    <link href=”https://fonts.googleapis.com/css2?family=Merriweather:wght@700&family=Open+Sans:wght@400;600&display=swap” rel=”stylesheet”>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            font-family: ‘Open Sans’, sans-serif;
            color: #d8dee9;
            background: linear-gradient(to bottom, #4a6c8a, #78909c, #a7b7be);
            background-image: url(‘https://image.pollinations.ai/prompt/Blue%20Ridge%20Mountains%20at%20dawn,%20misty%20valleys,%20river,%20pine%20trees,%20photorealistic,%20cinematic%20lighting’);
            background-size: cover;
            background-position: center;
        }
        #canvas-container {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 1;
            background-color: rgba(0, 0, 0, 0.3);
        }
        #ui-container {
            position: relative;
            z-index: 10;
            pointer-events: none;
            text-shadow: 1px 1px 3px rgba(0,0,0,0.5);
        }
        .interactive { pointer-events: auto; }
       
        h1, h2 { font-family: ‘Merriweather’, serif; }

        /* Custom Styles for Appalachian feel */
        .wood-panel {
            background-color: rgba(30, 40, 50, 0.85);
            border: 1px solid #4a6c8a;
            box-shadow: 0 4px 6px rgba(0,0,0,0.3), inset 0 0 8px rgba(74, 108, 138, 0.4);
        }
        .stone-input {
            background-color: rgba(20, 30, 40, 0.9);
            border: 1px solid #5a7d9b;
            color: #e0f2f1;
            text-shadow: 1px 1px 2px rgba(0,0,0,0.7);
        }
        .stone-button {
            background-color: #3b5a6d;
            border: 1px solid #6f8fa3;
            color: #ffffff;
            font-family: ‘Merriweather’, serif;
            text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
            box-shadow: 0 2px 4px rgba(0,0,0,0.3), inset 0 1px 2px rgba(255,255,255,0.1);
            transition: all 0.2s ease-in-out;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            line-height: 1.1;
        }
        .stone-button:hover {
            background-color: #4a6c8a;
            border-color: #8bb1c7;
            box-shadow: 0 3px 5px rgba(0,0,0,0.4), inset 0 1px 3px rgba(255,255,255,0.15);
            transform: translateY(-1px);
        }
        .stone-button:active {
            transform: translateY(0);
            box-shadow: 0 1px 2px rgba(0,0,0,0.2), inset 0 0 5px rgba(0,0,0,0.3);
        }

        /* Animations */
        @keyframes pulse-crit {
            0% { text-shadow: 0 0 8px #ffd700; transform: scale(1); color: #ffd700; }
            50% { text-shadow: 0 0 15px #ffd700, 0 0 30px #e74c3c; transform: scale(1.1); color: #ffd700; }
            100% { text-shadow: 0 0 8px #ffd700; transform: scale(1); color: #ffd700; }
        }
        .crit-anim { animation: pulse-crit 0.5s ease-in-out infinite; }
       
        @keyframes pulse-fail {
            0% { text-shadow: 0 0 5px #c0392b; color: #c0392b; }
            50% { text-shadow: 0 0 15px #c0392b; color: #c0392b; }
            100% { text-shadow: 0 0 5px #c0392b; color: #c0392b; }
        }
        .fail-anim { animation: pulse-fail 1s ease-in-out infinite; }

        .result-text {
            color: #d8dee9;
            text-shadow: 1px 1px 5px rgba(0,0,0,0.6);
        }
    </style>
</head>
<body class=”h-screen w-screen flex flex-col”>

    <div id=”canvas-container”></div>

    <div id=”ui-container” class=”h-full w-full flex flex-col justify-between p-4 sm:p-6″>
       
        <!– Header –>
        <div class=”w-full flex justify-between items-start”>
            <div>
                <h1 class=”text-2xl sm:text-3xl font-bold text-teal-200 tracking-wide”>MOUNTAIN <span class=”text-gray-400″>ROLL</span></h1>
                <p class=”text-xs sm:text-sm text-blue-300 mt-1″>ANCIENT STONES // ACTIVE</p>
            </div>
        </div>

        <!– Result Display –>
        <div class=”absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-center w-full pointer-events-none”>
            <div id=”result-label” class=”text-sm sm:text-lg text-blue-200 tracking-wider mb-2 sm:mb-3 opacity-0 transition-opacity duration-300″>TOTAL SUM</div>
            <div id=”result-display” class=”text-6xl sm:text-8xl font-bold result-text opacity-0 transition-all duration-200″>–</div>
            <div id=”result-breakdown” class=”text-base sm:text-xl text-green-300 mt-2 sm:mt-3 opacity-0 tracking-wider font-semibold”></div>
        </div>

        <!– Controls –>
        <div class=”w-full max-w-lg mx-auto p-3 sm:p-4 rounded-lg wood-panel interactive”>
            <div class=”flex flex-col gap-3 sm:gap-4″>
               
                <!– Input Row –>
                <div class=”flex items-end justify-between gap-2 sm:gap-4″>
                    <div class=”w-1/5 sm:w-1/4″>
                        <label class=”block text-[10px] sm:text-xs text-blue-300 mb-1 uppercase tracking-wider text-center”>Count</label>
                        <input type=”number” id=”count-input” value=”3″ min=”1″ max=”25″
                            class=”w-full p-2 rounded text-center text-lg sm:text-xl focus:outline-none stone-input”>
                    </div>
                    <div class=”text-blue-300 pb-2 sm:pb-3 font-bold text-xl sm:text-2xl”>X</div>
                    <div class=”w-1/5 sm:w-1/4″>
                        <label class=”block text-[10px] sm:text-xs text-blue-300 mb-1 uppercase tracking-wider text-center”>Faces</label>
                        <input type=”number” id=”faces-input” value=”6″ min=”2″ max=”100″
                            class=”w-full p-2 rounded text-center text-lg sm:text-xl focus:outline-none stone-input”>
                    </div>
                    <div class=”flex-1 min-w-0″> <!– min-w-0 prevents flex item from overflowing –>
                        <label class=”block text-[10px] sm:text-xs text-blue-300 mb-1 uppercase tracking-wider text-center”>Action</label>
                        <button id=”roll-btn”
                            class=”w-full py-2 px-1 sm:px-4 rounded stone-button uppercase h-[44px] sm:h-[46px] text-xs sm:text-sm font-bold tracking-wide whitespace-nowrap overflow-hidden text-ellipsis”>
                            ROLL STONES
                        </button>
                    </div>
                </div>

                <!– Quick Presets –>
                <div class=”grid grid-cols-5 gap-2 mt-1″>
                    <button class=”preset-btn py-2 stone-button text-xs sm:text-sm w-full” data-count=”1″ data-faces=”20″>1d20</button>
                    <button class=”preset-btn py-2 stone-button text-xs sm:text-sm w-full” data-count=”2″ data-faces=”20″>2d20</button>
                    <button class=”preset-btn py-2 stone-button text-xs sm:text-sm w-full” data-count=”3″ data-faces=”6″>3d6</button>
                    <button class=”preset-btn py-2 stone-button text-xs sm:text-sm w-full” data-count=”4″ data-faces=”6″>4d6</button>
                    <button class=”preset-btn py-2 stone-button text-xs sm:text-sm w-full” data-count=”2″ data-faces=”8″>2d8</button>
                </div>
            </div>
        </div>
    </div>

    <!– Three.js –>
    <script src=”https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js”></script>

    <script>
        // — SCENE SETUP —
        const container = document.getElementById(‘canvas-container’);
        const scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0x4a6c8a, 0.02);

        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.z = 10;

        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        container.appendChild(renderer.domElement);
        renderer.setClearColor(0x000000, 0);

        // — LIGHTING —
        const ambientLight = new THREE.AmbientLight(0x708090);
        scene.add(ambientLight);
       
        const lights = [];
        const lightColors = [0x8fbc8f, 0xb0e0e6, 0xd8bfd8];
       
        lightColors.forEach((col, i) => {
            const l = new THREE.PointLight(col, 0.6, 50);
            l.position.set(Math.sin(i*2) * 10, Math.cos(i*2) * 10, 10);
            scene.add(l);
            lights.push(l);
        });

        // — MATERIALS —
        const fillMaterial = new THREE.MeshLambertMaterial({
            color: 0x5e6e7b,
            reflectivity: 0.1,
            shininess: 10,
            polygonOffset: true,
            polygonOffsetFactor: 1,
            polygonOffsetUnits: 1
        });
       
        const wireMaterial = new THREE.LineBasicMaterial({
            color: 0x8fbc8f,
            transparent: true,
            opacity: 0.7
        });

        // — STATE VARIABLES —
        let diceObjects = [];
        const mainGroup = new THREE.Group();
        scene.add(mainGroup);

        const countInput = document.getElementById(‘count-input’);
        const facesInput = document.getElementById(‘faces-input’);
        const rollBtn = document.getElementById(‘roll-btn’);
        const resultDisplay = document.getElementById(‘result-display’);
        const resultLabel = document.getElementById(‘result-label’);
        const resultBreakdown = document.getElementById(‘result-breakdown’);
        const presetBtns = document.querySelectorAll(‘.preset-btn’);

        let isRolling = false;

        // — GEOMETRY GENERATOR —
        function getGeometry(faces) {
            faces = parseInt(faces);
            switch(faces) {
                case 4: return new THREE.TetrahedronGeometry(0.9);
                case 6: return new THREE.BoxGeometry(1.2, 1.2, 1.2);
                case 8: return new THREE.OctahedronGeometry(0.9);
                case 12: return new THREE.DodecahedronGeometry(0.9);
                case 20: return new THREE.IcosahedronGeometry(0.9);
                default: return new THREE.IcosahedronGeometry(0.9, 1);
            }
        }

        // — LAYOUT ENGINE —
        function updateScene() {
            while(mainGroup.children.length > 0){
                const child = mainGroup.children[0];
                if(child.geometry) child.geometry.dispose();
                mainGroup.remove(child);
            }
            diceObjects = [];

            const count = parseInt(countInput.value) || 1;
            const faces = parseInt(facesInput.value) || 6;
           
            const cols = Math.ceil(Math.sqrt(count));
            const rows = Math.ceil(count / cols);
            const spacing = 3.0;
           
            const startX = -((cols – 1) * spacing) / 2;
            const startY = ((rows – 1) * spacing) / 2;

            const maxDim = Math.max(cols, rows);
            const targetZ = 5 + (maxDim * 2.0);
            camera.position.z = targetZ;

            const geometry = getGeometry(faces);
            const edges = new THREE.EdgesGeometry(geometry);

            for(let i = 0; i < count; i++) {
                const col = i % cols;
                const row = Math.floor(i / cols);

                const mesh = new THREE.Mesh(geometry, fillMaterial);
                const wireframe = new THREE.LineSegments(edges, wireMaterial.clone());
               
                mesh.add(wireframe);

                mesh.position.x = startX + (col * spacing);
                mesh.position.y = startY – (row * spacing);
               
                diceObjects.push({
                    mesh: mesh,
                    wire: wireframe,
                    speed: { x: 0.002, y: 0.002 },
                    baseY: mesh.position.y
                });

                mainGroup.add(mesh);
            }
        }

        // — INPUT LISTENERS —
        function handleUpdate() {
            if(isRolling) return;
            let c = parseInt(countInput.value);
            if(c > 25) { c = 25; countInput.value = 25; }
            if(c < 1) { c = 1; countInput.value = 1; }
            let f = parseInt(facesInput.value);
            if(f < 2) { f = 2; facesInput.value = 2; }
            updateScene();
        }

        countInput.addEventListener(‘change’, handleUpdate);
        facesInput.addEventListener(‘change’, handleUpdate);

        presetBtns.forEach(btn => {
            btn.addEventListener(‘click’, () => {
                if(isRolling) return;
                countInput.value = btn.getAttribute(‘data-count’);
                facesInput.value = btn.getAttribute(‘data-faces’);
                updateScene();
            });
        });

        // — ROLL LOGIC —
        rollBtn.addEventListener(‘click’, () => {
            if(isRolling) return;
            isRolling = true;
           
            const count = parseInt(countInput.value);
            const faces = parseInt(facesInput.value);

            resultDisplay.style.opacity = 0;
            resultLabel.style.opacity = 0;
            resultBreakdown.style.opacity = 0;
            resultDisplay.className = “text-6xl sm:text-8xl font-bold result-text opacity-0 transition-all duration-200”;
           
            // Store original text but show calculating text
            const originalText = rollBtn.innerText;
            rollBtn.innerText = “DIVINING…”;
            rollBtn.classList.add(‘opacity-50’, ‘cursor-not-allowed’);

            diceObjects.forEach(obj => {
                obj.speed = {
                    x: (Math.random() – 0.5) * 1.5,
                    y: (Math.random() – 0.5) * 1.5
                };
            });

            const results = [];
            let total = 0;
            for(let i=0; i<count; i++) {
                const r = Math.floor(Math.random() * faces) + 1;
                results.push(r);
                total += r;
            }

            setTimeout(() => {
                showResult(total, results, faces);
            }, 1000);
        });

        function showResult(total, results, faces) {
            const decayInt = setInterval(() => {
                let allStopped = true;
                diceObjects.forEach(obj => {
                    obj.speed.x *= 0.8;
                    obj.speed.y *= 0.8;
                    if(Math.abs(obj.speed.x) > 0.005) allStopped = false;
                });

                if(allStopped) {
                    clearInterval(decayInt);
                    diceObjects.forEach(obj => obj.speed = { x: 0.002, y: 0.002 });
                    isRolling = false;
                    rollBtn.innerText = “ROLL STONES”;
                    rollBtn.classList.remove(‘opacity-50’, ‘cursor-not-allowed’);
                }
            }, 50);

            resultLabel.style.opacity = 1;
            resultDisplay.textContent = total;
            resultDisplay.style.opacity = 1;
            resultBreakdown.textContent = `[ ${results.join(‘, ‘)} ]`;
            resultBreakdown.style.opacity = 1;

            const maxPossible = results.length * faces;
            const minPossible = results.length;

            diceObjects.forEach(obj => obj.wire.material.color.setHex(0x8fbc8f));

            if(total === maxPossible) {
                resultDisplay.classList.add(‘crit-anim’);
                diceObjects.forEach(obj => obj.wire.material.color.setHex(0xffd700));
            } else if (total === minPossible) {
                resultDisplay.classList.add(‘fail-anim’);
                diceObjects.forEach(obj => obj.wire.material.color.setHex(0xc0392b));
            } else {
                resultDisplay.classList.add(‘result-text’);
            }
        }

        // — ANIMATION LOOP —
        const clock = new THREE.Clock();
       
        function animate() {
            requestAnimationFrame(animate);
            const time = clock.getElapsedTime();

            diceObjects.forEach((obj, index) => {
                obj.mesh.rotation.x += obj.speed.x;
                obj.mesh.rotation.y += obj.speed.y;

                if(!isRolling) {
                    obj.mesh.position.y = obj.baseY + Math.sin(time + index) * 0.08;
                }
               
                obj.wire.material.opacity = 0.6 + Math.sin(time * 1.5) * 0.2;
            });

            renderer.render(scene, camera);
        }

        updateScene();
        animate();

        window.addEventListener(‘resize’, () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });

    </script>
</body>
</html>

Leave a Reply