VibeCoderzVibeCoderz
All Prompts
Interactive 3D Boarding Pass Ticket Scene preview
backgroundthreejstailwindinteractiveanimatedexperimental

Interactive 3D Boarding Pass Ticket Scene

Интерактивная 3D сцена с анимированным посадочным талоном на Three.js. Динамичный фон, управление жестами. Идеально для экспериментальных лендингов.

Prompt

<section class="antialiased flex flex-col items-center justify-center min-h-screen bg-neutral-950 text-neutral-200 overflow-hidden select-none m-0" style="font-family: 'Inter', sans-serif;">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&amp;family=JetBrains+Mono:wght@400;500&amp;display=swap" rel="stylesheet">

<div id="canvas-container" class="absolute top-0 left-0 w-screen h-screen z-10 cursor-grab active:cursor-grabbing"></div>

<div class="absolute bottom-10 left-0 right-0 flex flex-col items-center pointer-events-none z-20">
        <div class="px-5 py-2.5 rounded-full bg-neutral-900/60 backdrop-blur-md border border-neutral-700/50 shadow-lg flex items-center gap-2.5">
            <iconify-icon icon="solar:hand-grab-linear" stroke-width="1.5" class="text-base text-neutral-400"></iconify-icon>
            <p class="text-xs font-medium tracking-wide text-neutral-300 uppercase">
                Grab and drag the ticket
            </p>
        </div>
    </div>

<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
<script>
        // --- 1. Generate Flight Ticket Texture via Canvas ---
        function createTicketTexture() {
            const canvas = document.createElement('canvas');
            canvas.width = 512;
            canvas.height = 1024;
            const ctx = canvas.getContext('2d');

            // Dark Ticket Background
            ctx.fillStyle = '#0f0f0f';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            // Edge border
            ctx.strokeStyle = '#262626';
            ctx.lineWidth = 4;
            ctx.strokeRect(2, 2, 508, 1020);

            const t = (txt, x, y, font, align, color) => {
                ctx.font = font; ctx.textAlign = align; ctx.fillStyle = color; ctx.fillText(txt, x, y);
            };

            ctx.textBaseline = 'top';
            let y = 60;

            // Header
            t('BOARDING PASS', 256, y, '500 36px "JetBrains Mono"', 'center', '#f5f5f5'); y += 50;
            t('FIRST CLASS TICKET', 256, y, '400 18px "JetBrains Mono"', 'center', '#737373'); y += 60;

            // Divider
            ctx.setLineDash([6, 6]); ctx.lineWidth = 2; ctx.strokeStyle = '#333';
            ctx.beginPath(); ctx.moveTo(40, y); ctx.lineTo(472, y); ctx.stroke(); y += 40;
            ctx.setLineDash([]);

            // Passenger
            t('PASSENGER', 40, y, '400 14px "JetBrains Mono"', 'left', '#737373');
            t('ALEXANDER W.', 40, y + 20, '500 24px "JetBrains Mono"', 'left', '#f5f5f5'); y += 80;

            // Flight Info
            t('FLIGHT', 40, y, '400 14px "JetBrains Mono"', 'left', '#737373');
            t('DATE', 256, y, '400 14px "JetBrains Mono"', 'left', '#737373');
            t('GL-842', 40, y + 20, '500 24px "JetBrains Mono"', 'left', '#f5f5f5');
            t('23 OCT 2026', 256, y + 20, '500 24px "JetBrains Mono"', 'left', '#f5f5f5'); y += 80;

            // Route
            t('FROM', 40, y, '400 14px "JetBrains Mono"', 'left', '#737373');
            t('TO', 256, y, '400 14px "JetBrains Mono"', 'left', '#737373');
            t('JFK', 40, y + 20, '500 42px "JetBrains Mono"', 'left', '#f5f5f5');
            t('SFO', 256, y + 20, '500 42px "JetBrains Mono"', 'left', '#f5f5f5'); y += 100;

            // Seat & Gate
            t('GATE', 40, y, '400 14px "JetBrains Mono"', 'left', '#737373');
            t('SEAT', 180, y, '400 14px "JetBrains Mono"', 'left', '#737373');
            t('ZONE', 320, y, '400 14px "JetBrains Mono"', 'left', '#737373');
            t('42', 40, y + 20, '500 28px "JetBrains Mono"', 'left', '#f5f5f5');
            t('04A', 180, y + 20, '500 28px "JetBrains Mono"', 'left', '#f5f5f5');
            t('1', 320, y + 20, '500 28px "JetBrains Mono"', 'left', '#f5f5f5'); y += 80;

            // Divider
            ctx.beginPath(); ctx.moveTo(40, y); ctx.lineTo(472, y); ctx.stroke(); y += 50;

            // Barcode Simulation
            ctx.fillStyle = '#f5f5f5';
            for(let i = 40; i < 472; i += Math.random() * 8 + 3) {
                const w = Math.random() * 4 + 1;
                ctx.fillRect(i, y, w, 70);
            }
            y += 90;
            t('01234567890123456789', 256, y, '400 14px "JetBrains Mono"', 'center', '#737373');

            const texture = new THREE.CanvasTexture(canvas);
            texture.anisotropy = 16;
            return texture;
        }

        // --- 2. Three.js Setup ---
        const container = document.getElementById('canvas-container');
        const scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0x0a0a0a, 0.02);
        
        const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
        camera.position.set(0, 0, 11);

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

        // Lighting (Dark Mode Adjusted)
        scene.add(new THREE.AmbientLight(0xffffff, 0.3));
        
        const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
        dirLight.position.set(5, 10, 7);
        dirLight.castShadow = true;
        dirLight.shadow.mapSize.width = 1024;
        dirLight.shadow.mapSize.height = 1024;
        scene.add(dirLight);

        const fillLight = new THREE.DirectionalLight(0x4466aa, 0.8);
        fillLight.position.set(-5, -2, -5);
        scene.add(fillLight);

        // --- 3. WebGL Background (Waves & Shooting Lines) ---
        const bgGroup = new THREE.Group();
        scene.add(bgGroup);

        const waveGeo = new THREE.PlaneGeometry(80, 60, 50, 40);
        waveGeo.rotateX(-Math.PI / 2);
        
        const wavePointsMat = new THREE.PointsMaterial({ color: 0x88aaff, size: 0.06, transparent: true, opacity: 0.4 });
        const wavePoints = new THREE.Points(waveGeo, wavePointsMat);
        bgGroup.add(wavePoints);

        const waveLinesMat = new THREE.MeshBasicMaterial({ color: 0x335588, wireframe: true, transparent: true, opacity: 0.1 });
        const waveMesh = new THREE.Mesh(waveGeo, waveLinesMat);
        bgGroup.add(waveMesh);

        bgGroup.position.set(0, -10, -15);

        const shootingLines = [];
        for(let i = 0; i < 12; i++) {
            const sGeo = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0,0,0), new THREE.Vector3(0,0,-3)]);
            const sMat = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.6 });
            const sLine = new THREE.Line(sGeo, sMat);
            sLine.position.set((Math.random()-0.5)*50, (Math.random()-0.5)*30, -40 + Math.random()*30);
            sLine.userData = { speed: Math.random() * 0.8 + 0.3 };
            scene.add(sLine);
            shootingLines.push(sLine);
        }

        // --- 4. Cloth Geometry & Material ---
        const width = 3.5, height = 7, segX = 25, segY = 50;
        const geometry = new THREE.PlaneGeometry(width, height, segX, segY);
        
        const material = new THREE.MeshStandardMaterial({
            map: createTicketTexture(),
            side: THREE.DoubleSide,
            roughness: 0.8,
            metalness: 0.1,
            color: 0xdddddd
        });

        const clothMesh = new THREE.Mesh(geometry, material);
        clothMesh.castShadow = true;
        clothMesh.receiveShadow = true;
        scene.add(clothMesh);

        // --- 5. Custom Verlet Physics System ---
        const particles = [], constraints = [];
        const positionAttr = geometry.attributes.position;

        for (let i = 0; i < positionAttr.count; i++) {
            const x = positionAttr.getX(i), y = positionAttr.getY(i), z = positionAttr.getZ(i);
            particles.push({
                pos: new THREE.Vector3(x, y, z),
                prev: new THREE.Vector3(x, y, z),
                mass: y === height / 2 ? 0 : 1
            });
        }

        const index = (u, v) => u + v * (segX + 1);
        const addConstraint = (p1, p2) => constraints.push({ p1, p2, dist: particles[p1].pos.distanceTo(particles[p2].pos) });

        for (let v = 0; v <= segY; v++) {
            for (let u = 0; u <= segX; u++) {
                if (u < segX) addConstraint(index(u, v), index(u + 1, v));
                if (v < segY) addConstraint(index(u, v), index(u, v + 1));
                if (u < segX && v < segY) {
                    addConstraint(index(u, v), index(u + 1, v + 1));
                    addConstraint(index(u + 1, v), index(u, v + 1));
                }
                if (u < segX - 1) addConstraint(index(u, v), index(u + 2, v));
                if (v < segY - 1) addConstraint(index(u, v), index(u, v + 2));
            }
        }

        const gravity = new THREE.Vector3(0, -0.008, 0), damping = 0.96, constraintIterations = 7;

        function simulatePhysics() {
            for (let i = 0; i < particles.length; i++) {
                const p = particles[i];
                if (p.mass === 0 || i === grabbedIndex) continue;
                const velocity = p.pos.clone().sub(p.prev).multiplyScalar(damping);
                p.prev.copy(p.pos);
                p.pos.add(velocity).add(gravity);
            }

            for (let iter = 0; iter < constraintIterations; iter++) {
                for (let i = 0; i < constraints.length; i++) {
                    const c = constraints[i], p1 = particles[c.p1], p2 = particles[c.p2];
                    const delta = p2.pos.clone().sub(p1.pos);
                    const currentDist = delta.length();
                    if (currentDist === 0) continue;
                    const offset = delta.multiplyScalar(0.5 * ((currentDist - c.dist) / currentDist));
                    if (p1.mass !== 0 && c.p1 !== grabbedIndex) p1.pos.add(offset);
                    if (p2.mass !== 0 && c.p2 !== grabbedIndex) p2.pos.sub(offset);
                }
            }

            for (let i = 0; i < particles.length; i++) {
                positionAttr.setXYZ(i, particles[i].pos.x, particles[i].pos.y, particles[i].pos.z);
            }
            positionAttr.needsUpdate = true;
            geometry.computeVertexNormals();
        }

        // --- 6. Interaction ---
        const raycaster = new THREE.Raycaster(), mouse = new THREE.Vector2();
        let grabbedIndex = -1;
        const dragPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), intersectionPoint = new THREE.Vector3();

        const indicator = new THREE.Mesh(
            new THREE.SphereGeometry(0.15, 16, 16),
            new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.3, depthTest: false })
        );
        indicator.visible = false;
        scene.add(indicator);

        const updateMouse = (e) => {
            mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
            mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
        };

        window.addEventListener('pointerdown', (e) => {
            updateMouse(e); raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObject(clothMesh);
            if (intersects.length > 0) {
                let minDist = Infinity;
                for (let i = 0; i < particles.length; i++) {
                    if (particles[i].mass === 0) continue;
                    const dist = particles[i].pos.distanceTo(intersects[0].point);
                    if (dist < minDist) { minDist = dist; grabbedIndex = i; }
                }
                if (grabbedIndex !== -1) {
                    indicator.visible = true;
                    dragPlane.setFromNormalAndCoplanarPoint(camera.getWorldDirection(new THREE.Vector3()), particles[grabbedIndex].pos);
                }
            }
        });

        window.addEventListener('pointermove', (e) => {
            if (grabbedIndex !== -1) {
                updateMouse(e); raycaster.setFromCamera(mouse, camera);
                raycaster.ray.intersectPlane(dragPlane, intersectionPoint);
                particles[grabbedIndex].pos.copy(intersectionPoint);
                indicator.position.copy(intersectionPoint);
            }
        });

        const release = () => { grabbedIndex = -1; indicator.visible = false; };
        window.addEventListener('pointerup', release);
        window.addEventListener('pointerleave', release);

        // --- 7. Render Loop ---
        const clock = new THREE.Clock();
        function animate() {
            requestAnimationFrame(animate);
            const time = clock.getElapsedTime();

            // Animate Waves
            const wavePos = waveGeo.attributes.position;
            for(let i = 0; i < wavePos.count; i++) {
                const x = wavePos.getX(i), z = wavePos.getZ(i);
                wavePos.setY(i, Math.sin(x * 0.2 + time) * Math.cos(z * 0.2 + time) * 1.5);
            }
            wavePos.needsUpdate = true;

            // Animate Shooting Lines
            shootingLines.forEach(line => {
                line.position.z += line.userData.speed;
                if(line.position.z > -5) {
                    line.position.z = -40;
                    line.position.x = (Math.random()-0.5)*50;
                    line.position.y = (Math.random()-0.5)*30;
                }
            });

            simulatePhysics();
            renderer.render(scene, camera);
        }

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

        animate();
    </script>
</section>
All Prompts