Skip to content
LAM
Read Home Blog
Make Projects HTML Tools Games
Touch grass Notes Resume Links
Home Blog HTML Projects
Tools Games Notes Resume Links
Back AI Image World Explorer (Guided) Programming
Download Open
Show description 208 chars · Programming

AI Image World Explorer (Guided)

AI Image World Explorer (Guided)













Start Exploration
Upload & Explore



Look Left
Go Forward
Look Right














Click to start

Use WASD to move & Mouse to look

AI Image World Explorer (Guided)

20,890 bytes · HTML source
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Image World Explorer (Guided)</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
        body {
            font-family: 'Inter', sans-serif;
            overflow: hidden; /* Prevent scrollbars from appearing */
        }
        #canvas-container {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            cursor: pointer;
        }
        canvas {
            display: block;
            width: 100%;
            height: 100%;
        }
        #ui-container {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            padding: 1rem;
            display: flex;
            flex-direction: column;
            align-items: center;
            z-index: 10;
            pointer-events: none; /* Allow clicks to pass through to the canvas */
        }
        #controls {
            pointer-events: auto; /* Re-enable pointer events for the controls */
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 0.75rem;
        }
        .loader {
            border: 8px solid #f3f3f3;
            border-top: 8px solid #3498db;
            border-radius: 50%;
            width: 60px;
            height: 60px;
            animation: spin 2s linear infinite;
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            z-index: 30;
            display: none; /* Hidden by default */
        }
        #instructions {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
            font-size: 1.5rem;
            text-align: center;
            background-color: rgba(0,0,0,0.5);
            padding: 1rem 2rem;
            border-radius: 0.5rem;
            z-index: 20;
            pointer-events: none;
        }
        .action-btn {
            color: white;
            font-weight: bold;
            padding: 0.5rem 1rem;
            border-radius: 0.375rem;
            transition: background-color 0.3s;
            white-space: nowrap;
        }
        .action-btn:hover:not(:disabled) {
            filter: brightness(1.1);
        }
        .action-btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
        @keyframes spin {
            0% { transform: translate(-50%, -50%) rotate(0deg); }
            100% { transform: translate(-50%, -50%) rotate(360deg); }
        }
    </style>
</head>
<body class="bg-gray-900 text-white">

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

    <div id="ui-container">
        <div id="controls" class="bg-gray-800 bg-opacity-80 p-4 rounded-lg shadow-lg w-full max-w-4xl">
            <div class="flex flex-col sm:flex-row items-center gap-4 w-full">
                <input type="text" id="prompt-input" class="w-full bg-gray-700 text-white border border-gray-600 rounded-md px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="e.g., a vibrant alien jungle at night">
                <button id="generate-btn" class="action-btn w-full sm:w-auto bg-blue-600">Start Exploration</button>
                <button id="upload-btn" class="action-btn w-full sm:w-auto bg-green-600">Upload & Explore</button>
            </div>
            <div id="direction-controls" class="flex items-center justify-center gap-2 mt-3">
                <button id="btn-left" class="action-btn bg-gray-600" disabled>Look Left</button>
                <button id="btn-forward" class="action-btn bg-gray-600" disabled>Go Forward</button>
                <button id="btn-right" class="action-btn bg-gray-600" disabled>Look Right</button>
            </div>
        </div>
        <p id="status-message" class="mt-2 text-gray-400"></p>
    </div>
    
    <div id="loader" class="loader"></div>
    <input type="file" id="upload-input" class="hidden" accept="image/*">

    <div id="instructions">
        <p>Click to start</p>
        <p class="text-sm mt-2">Use WASD to move & Mouse to look</p>
    </div>

    <script type="module">
        import { PointerLockControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/PointerLockControls.js';

        let scene, camera, renderer, controls;
        let particles;
        let isGenerating = false;
        let currentPrompt = "";
        let lastImageBase64 = null;

        const moveState = { forward: false, backward: false, left: false, right: false };
        const moveSpeed = 20.0;
        const clock = new THREE.Clock();
        
        const canvasContainer = document.getElementById('canvas-container');
        const promptInput = document.getElementById('prompt-input');
        const generateBtn = document.getElementById('generate-btn');
        const uploadBtn = document.getElementById('upload-btn');
        const uploadInput = document.getElementById('upload-input');
        const loader = document.getElementById('loader');
        const statusMessage = document.getElementById('status-message');
        const instructions = document.getElementById('instructions');
        const btnLeft = document.getElementById('btn-left');
        const btnForward = document.getElementById('btn-forward');
        const btnRight = document.getElementById('btn-right');

        function init() {
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x111827);

            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
            camera.position.set(0, 50, 250);

            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            canvasContainer.appendChild(renderer.domElement);

            controls = new PointerLockControls(camera, renderer.domElement);
            scene.add(controls.getObject());

            promptInput.value = "A bioluminescent forest on an alien planet, glowing mushrooms and strange flora";
            currentPrompt = promptInput.value;

            addEventListeners();
            animate();
        }

        function addEventListeners() {
            canvasContainer.addEventListener('click', () => controls.lock());
            controls.addEventListener('lock', () => { instructions.style.display = 'none'; });
            controls.addEventListener('unlock', () => { instructions.style.display = 'block'; });
            window.addEventListener('resize', onWindowResize, false);
            document.addEventListener('keydown', onKeyDown, false);
            document.addEventListener('keyup', onKeyUp, false);
            generateBtn.addEventListener('click', () => handleExploration('initial'));
            uploadBtn.addEventListener('click', () => uploadInput.click());
            uploadInput.addEventListener('change', handleFileUpload);
            btnLeft.addEventListener('click', () => handleExploration('left'));
            btnForward.addEventListener('click', () => handleExploration('forward'));
            btnRight.addEventListener('click', () => handleExploration('right'));
        }

        async function handleFileUpload(event) {
            const file = event.target.files[0];
            if (!file || isGenerating) return;

            setUIState(true);
            statusMessage.textContent = "Processing uploaded image...";

            const reader = new FileReader();
            reader.onload = async (e) => {
                try {
                    const fullDataUrl = e.target.result;
                    const base64Data = fullDataUrl.split(',')[1];
                    
                    lastImageBase64 = base64Data;

                    await createParticleWorld(fullDataUrl);
                    statusMessage.textContent = "World created. Asking AI for a description...";

                    currentPrompt = await describeImage(base64Data);
                    promptInput.value = currentPrompt;

                    statusMessage.textContent = "Ready to explore! Choose your direction.";
                } catch (error) {
                    console.error("Error processing upload:", error);
                    statusMessage.textContent = "Failed to process uploaded image.";
                } finally {
                    setUIState(false);
                    uploadInput.value = ''; 
                }
            };
            reader.readAsDataURL(file);
        }

        async function handleExploration(direction) {
            if (isGenerating) return;
            setUIState(true);

            try {
                if (direction === 'initial') {
                    currentPrompt = promptInput.value;
                    lastImageBase64 = null;
                    statusMessage.textContent = "Generating initial world...";
                } else {
                    statusMessage.textContent = `Imagining the scene to the ${direction}...`;
                    await evolvePrompt(direction);
                }
                
                promptInput.value = currentPrompt;
                
                const imageData = await generateImageWithRetry(currentPrompt, lastImageBase64);
                lastImageBase64 = imageData.base64;
                
                statusMessage.textContent = "Image generated. Creating 3D particle world...";
                
                await createParticleWorld(`data:image/png;base64,${lastImageBase64}`);
                
                statusMessage.textContent = "World updated! Choose your next direction.";

            } catch (error) {
                console.error('Error during generation cycle:', error);
                statusMessage.textContent = "Failed to generate world. Check console.";
            } finally {
                setUIState(false);
            }
        }

        function setUIState(generating) {
            isGenerating = generating;
            loader.style.display = generating ? 'block' : 'none';
            generateBtn.disabled = generating;
            uploadBtn.disabled = generating;
            
            const directionButtonsDisabled = generating || !lastImageBase64;
            btnLeft.disabled = directionButtonsDisabled;
            btnForward.disabled = directionButtonsDisabled;
            btnRight.disabled = directionButtonsDisabled;
        }

        async function describeImage(base64ImageData) {
            const prompt = "Describe this image in a creative and descriptive way, as if it were a scene in a virtual world. Be concise.";
            const parts = [{ text: prompt }, { inlineData: { mimeType: "image/png", data: base64ImageData } }];
            
            const payload = { contents: [{ role: "user", parts: parts }] };
            const apiKey = "";
            const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`;

            try {
                const response = await fetch(apiUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(payload)
                });
                if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                const result = await response.json();
                if (result.candidates?.[0]?.content?.parts?.[0]?.text) {
                    return result.candidates[0].content.parts[0].text.trim();
                }
            } catch (error) {
                console.error("Error describing image:", error);
            }
            return "An uploaded custom scene."; // Fallback description
        }

        async function evolvePrompt(direction) {
            const directionPhrase = {
                left: "if they turned to look to their left",
                right: "if they turned to look to their right",
                forward: "if they took a few steps forward"
            };
            const metaPrompt = `You are a creative world-building assistant. The user is exploring a virtual world. The current scene is: "${currentPrompt}". Describe what they would see ${directionPhrase[direction]}. Keep the style and key elements consistent with the previous scene. Be descriptive, imaginative, and concise. Only provide the description of the new scene.`;
            
            const payload = { contents: [{ role: "user", parts: [{ text: metaPrompt }] }] };
            const apiKey = "";
            const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`;

            try {
                const response = await fetch(apiUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(payload)
                });
                if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                const result = await response.json();
                if (result.candidates?.[0]?.content?.parts?.[0]?.text) {
                    currentPrompt = result.candidates[0].content.parts[0].text.trim();
                } else {
                    console.warn("Could not evolve prompt, reusing the old one.");
                }
            } catch (error) {
                console.error("Error evolving prompt:", error);
            }
        }

        async function generateImageWithRetry(prompt, base64ImageData = null) {
            const parts = [{ text: prompt }];
            if (base64ImageData) {
                parts.push({ inlineData: { mimeType: "image/png", data: base64ImageData } });
            }

            const payload = {
                contents: [{ role: "user", parts: parts }],
                generationConfig: { responseModalities: ['TEXT', 'IMAGE'] },
            };
            const apiKey = "";
            const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-preview-image-generation:generateContent?key=${apiKey}`;

            for (let attempt = 0; attempt < 3; attempt++) {
                try {
                    const response = await fetch(apiUrl, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(payload)
                    });
                    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                    
                    const result = await response.json();
                    const imagePart = result?.candidates?.[0]?.content?.parts?.find(p => p.inlineData);
                    
                    if (imagePart?.inlineData?.data) {
                        return { base64: imagePart.inlineData.data };
                    } else {
                        throw new Error("Invalid image response from API.");
                    }
                } catch (error) {
                    console.warn(`Image generation attempt ${attempt + 1} failed. Retrying...`, error);
                    if (attempt >= 2) throw error;
                    await new Promise(res => setTimeout(res, 1000 * Math.pow(2, attempt)));
                }
            }
        }

        function createParticleWorld(imageUrl) {
            return new Promise((resolve, reject) => {
                if (particles) {
                    scene.remove(particles);
                    particles.geometry.dispose();
                    particles.material.dispose();
                }

                const image = new Image();
                image.crossOrigin = "Anonymous";
                image.src = imageUrl;
                image.onload = () => {
                    const canvas = document.createElement('canvas');
                    const context = canvas.getContext('2d');
                    const imgWidth = image.width;
                    const imgHeight = image.height;
                    canvas.width = imgWidth;
                    canvas.height = imgHeight;
                    context.drawImage(image, 0, 0);

                    const imageData = context.getImageData(0, 0, imgWidth, imgHeight).data;
                    
                    const geometry = new THREE.BufferGeometry();
                    const positions = [];
                    const colors = [];
                    const particleDensity = 0.5;
                    const depthFactor = 200;

                    for (let y = 0; y < imgHeight; y++) {
                        for (let x = 0; x < imgWidth; x++) {
                            if (Math.random() > particleDensity) continue;

                            const i = (y * imgWidth + x) * 4;
                            const r = imageData[i] / 255;
                            const g = imageData[i + 1] / 255;
                            const b = imageData[i + 2] / 255;
                            
                            const brightness = (r + g + b) / 3;
                            const z = (brightness - 0.5) * depthFactor;

                            positions.push(x - imgWidth / 2, -y + imgHeight / 2, z);
                            colors.push(r, g, b);
                        }
                    }

                    geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
                    geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
                    geometry.computeBoundingSphere();

                    const material = new THREE.PointsMaterial({
                        size: 1.5,
                        vertexColors: true,
                        sizeAttenuation: true
                    });

                    particles = new THREE.Points(geometry, material);
                    scene.add(particles);
                    resolve();
                };
                image.onerror = (err) => {
                    console.error("Error loading image for particle world:", err);
                    reject(err);
                }
            });
        }

        function onKeyDown(event) {
            switch (event.code) {
                case 'KeyW': case 'ArrowUp': moveState.forward = true; break;
                case 'KeyS': case 'ArrowDown': moveState.backward = true; break;
                case 'KeyA': case 'ArrowLeft': moveState.left = true; break;
                case 'KeyD': case 'ArrowRight': moveState.right = true; break;
            }
        }

        function onKeyUp(event) {
            switch (event.code) {
                case 'KeyW': case 'ArrowUp': moveState.forward = false; break;
                case 'KeyS': case 'ArrowDown': moveState.backward = false; break;
                case 'KeyA': case 'ArrowLeft': moveState.left = false; break;
                case 'KeyD': case 'ArrowRight': moveState.right = false; break;
            }
        }

        function updateMovement(delta) {
            if (!controls.isLocked) return;
            const direction = new THREE.Vector3();
            direction.z = Number(moveState.forward) - Number(moveState.backward);
            direction.x = Number(moveState.right) - Number(moveState.left);
            direction.normalize(); 
            
            if (moveState.forward || moveState.backward) controls.moveForward(direction.z * moveSpeed * delta);
            if (moveState.left || moveState.right) controls.moveRight(direction.x * moveSpeed * delta);
        }

        function animate() {
            requestAnimationFrame(animate);
            const delta = clock.getDelta();
            updateMovement(delta);
            renderer.render(scene, camera);
        }

        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

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