Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D Rocket Launcher</title> | |
| <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://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/GLTFLoader.js"></script> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| font-family: 'Arial', sans-serif; | |
| } | |
| canvas { | |
| display: block; | |
| } | |
| #ui { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| padding: 20px; | |
| color: white; | |
| text-align: center; | |
| pointer-events: none; | |
| z-index: 100; | |
| } | |
| #crosshair { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 20px; | |
| height: 20px; | |
| transform: translate(-50%, -50%); | |
| pointer-events: none; | |
| z-index: 101; | |
| } | |
| #crosshair::before, #crosshair::after { | |
| content: ''; | |
| position: absolute; | |
| background: rgba(255, 255, 255, 0.8); | |
| } | |
| #crosshair::before { | |
| width: 2px; | |
| height: 20px; | |
| left: 9px; | |
| top: 0; | |
| } | |
| #crosshair::after { | |
| width: 20px; | |
| height: 2px; | |
| left: 0; | |
| top: 9px; | |
| } | |
| #explosion { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 200px; | |
| height: 200px; | |
| transform: translate(-50%, -50%); | |
| border-radius: 50%; | |
| background: radial-gradient(circle, rgba(255,100,0,0.8) 0%, rgba(255,50,0,0) 70%); | |
| opacity: 0; | |
| pointer-events: none; | |
| z-index: 99; | |
| transition: opacity 0.5s, transform 0.5s; | |
| } | |
| .button { | |
| pointer-events: auto; | |
| background: rgba(255, 255, 255, 0.2); | |
| border: 1px solid rgba(255, 255, 255, 0.4); | |
| color: white; | |
| padding: 8px 16px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .button:hover { | |
| background: rgba(255, 255, 255, 0.4); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="ui"> | |
| <h1 class="text-3xl font-bold mb-2">3D Rocket Launcher</h1> | |
| <p class="mb-4">Click anywhere to launch rockets!</p> | |
| <div class="flex justify-center gap-4"> | |
| <div class="button" id="fireRocket">Fire Rocket</div> | |
| <div class="button" id="autoFire">Auto Fire</div> | |
| </div> | |
| <div class="mt-4"> | |
| <span id="rocketCount">0</span> Rockets Active | |
| </div> | |
| </div> | |
| <div id="crosshair"></div> | |
| <div id="explosion"></div> | |
| <script> | |
| // Scene setup | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x111122); | |
| // Camera setup | |
| const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.z = 10; | |
| camera.position.y = 5; | |
| camera.lookAt(0, 0, 0); | |
| // Renderer setup | |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| document.body.appendChild(renderer.domElement); | |
| // Controls | |
| const controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| // Lights | |
| const ambientLight = new THREE.AmbientLight(0x404040); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 1); | |
| directionalLight.position.set(1, 1, 1); | |
| scene.add(directionalLight); | |
| // Rocket materials | |
| const rocketBodyMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0xff3333, | |
| shininess: 100, | |
| specular: 0x111111 | |
| }); | |
| const rocketNoseMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0xffffff, | |
| shininess: 100, | |
| specular: 0x111111 | |
| }); | |
| const rocketFinMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x3333ff, | |
| shininess: 50, | |
| specular: 0x111111 | |
| }); | |
| // Create a procedural rocket model | |
| function createRocket() { | |
| const rocketGroup = new THREE.Group(); | |
| // Rocket body (cylinder with more segments for better quality) | |
| const bodyGeometry = new THREE.CylinderGeometry(0.15, 0.15, 1, 32, 32, false); | |
| const body = new THREE.Mesh(bodyGeometry, rocketBodyMaterial); | |
| body.position.y = 0.5; | |
| body.castShadow = true; | |
| rocketGroup.add(body); | |
| // Rocket nose (cone with more segments) | |
| const noseGeometry = new THREE.ConeGeometry(0.15, 0.3, 32, 1, true); | |
| const nose = new THREE.Mesh(noseGeometry, rocketNoseMaterial); | |
| nose.position.y = 1.15; | |
| nose.castShadow = true; | |
| rocketGroup.add(nose); | |
| // Rocket fins (4 fins for better stability) | |
| const finGeometry = new THREE.BoxGeometry(0.3, 0.05, 0.15); | |
| for (let i = 0; i < 4; i++) { | |
| const fin = new THREE.Mesh(finGeometry, rocketFinMaterial); | |
| fin.position.y = 0.25; | |
| fin.position.z = 0.075; | |
| fin.rotation.x = Math.PI / 2; | |
| fin.rotation.y = (i * Math.PI * 2 / 4); | |
| fin.position.x = Math.sin(fin.rotation.y) * 0.25; | |
| fin.position.z = Math.cos(fin.rotation.y) * 0.25; | |
| fin.castShadow = true; | |
| rocketGroup.add(fin); | |
| } | |
| // Rocket engine (small cylinder with more detail) | |
| const engineGeometry = new THREE.CylinderGeometry(0.1, 0.1, 0.15, 32); | |
| const engine = new THREE.Mesh(engineGeometry, new THREE.MeshPhongMaterial({ | |
| color: 0x222222, | |
| shininess: 30 | |
| })); | |
| engine.position.y = 0; | |
| engine.castShadow = true; | |
| rocketGroup.add(engine); | |
| // Engine nozzle (cone) | |
| const nozzleGeometry = new THREE.ConeGeometry(0.1, 0.1, 32); | |
| const nozzle = new THREE.Mesh(nozzleGeometry, new THREE.MeshPhongMaterial({ | |
| color: 0x444444, | |
| shininess: 50 | |
| })); | |
| nozzle.position.y = -0.05; | |
| nozzle.rotation.x = Math.PI; | |
| nozzle.castShadow = true; | |
| rocketGroup.add(nozzle); | |
| // Add some details (rings around the body) | |
| const ringGeometry = new THREE.TorusGeometry(0.15, 0.01, 16, 32); | |
| const ringMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff }); | |
| const ring1 = new THREE.Mesh(ringGeometry, ringMaterial); | |
| ring1.position.y = 0.8; | |
| ring1.rotation.x = Math.PI / 2; | |
| rocketGroup.add(ring1); | |
| const ring2 = new THREE.Mesh(ringGeometry, ringMaterial); | |
| ring2.position.y = 0.2; | |
| ring2.rotation.x = Math.PI / 2; | |
| rocketGroup.add(ring2); | |
| return rocketGroup; | |
| } | |
| // Particle systems | |
| function createFireParticles() { | |
| const particleCount = 150; | |
| const particles = new THREE.BufferGeometry(); | |
| const positions = new Float32Array(particleCount * 3); | |
| const colors = new Float32Array(particleCount * 3); | |
| const sizes = new Float32Array(particleCount); | |
| for (let i = 0; i < particleCount; i++) { | |
| positions[i * 3] = (Math.random() - 0.5) * 0.15; | |
| positions[i * 3 + 1] = Math.random() * -0.5; | |
| positions[i * 3 + 2] = (Math.random() - 0.5) * 0.15; | |
| // Fire colors (red, orange, yellow) | |
| const color = new THREE.Color( | |
| 0.9 + Math.random() * 0.1, | |
| 0.3 + Math.random() * 0.3, | |
| Math.random() * 0.2 | |
| ); | |
| colors[i * 3] = color.r; | |
| colors[i * 3 + 1] = color.g; | |
| colors[i * 3 + 2] = color.b; | |
| sizes[i] = 0.1 + Math.random() * 0.2; | |
| } | |
| particles.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| particles.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
| particles.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); | |
| const particleMaterial = new THREE.PointsMaterial({ | |
| size: 0.1, | |
| vertexColors: true, | |
| transparent: true, | |
| opacity: 0.8, | |
| blending: THREE.AdditiveBlending | |
| }); | |
| const particleSystem = new THREE.Points(particles, particleMaterial); | |
| return particleSystem; | |
| } | |
| function createSmokeParticles() { | |
| const particleCount = 80; | |
| const particles = new THREE.BufferGeometry(); | |
| const positions = new Float32Array(particleCount * 3); | |
| const colors = new Float32Array(particleCount * 3); | |
| const sizes = new Float32Array(particleCount); | |
| for (let i = 0; i < particleCount; i++) { | |
| positions[i * 3] = (Math.random() - 0.5) * 0.3; | |
| positions[i * 3 + 1] = Math.random() * -1; | |
| positions[i * 3 + 2] = (Math.random() - 0.5) * 0.3; | |
| // Smoke colors (gray to white) | |
| const gray = 0.5 + Math.random() * 0.5; | |
| const color = new THREE.Color(gray, gray, gray); | |
| colors[i * 3] = color.r; | |
| colors[i * 3 + 1] = color.g; | |
| colors[i * 3 + 2] = color.b; | |
| sizes[i] = 0.2 + Math.random() * 0.3; | |
| } | |
| particles.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| particles.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
| particles.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); | |
| const particleMaterial = new THREE.PointsMaterial({ | |
| size: 0.2, | |
| vertexColors: true, | |
| transparent: true, | |
| opacity: 0.6, | |
| blending: THREE.NormalBlending | |
| }); | |
| const particleSystem = new THREE.Points(particles, particleMaterial); | |
| return particleSystem; | |
| } | |
| // Rocket class | |
| class Rocket { | |
| constructor(startPosition) { | |
| this.group = new THREE.Group(); | |
| this.rocket = createRocket(); | |
| this.fireParticles = createFireParticles(); | |
| this.smokeParticles = createSmokeParticles(); | |
| this.group.add(this.rocket); | |
| this.group.add(this.fireParticles); | |
| this.group.add(this.smokeParticles); | |
| this.group.position.copy(startPosition); | |
| // Create a random bezier curve back to center | |
| const endPoint = new THREE.Vector3(0, 0, 0); | |
| const control1 = new THREE.Vector3( | |
| (Math.random() - 0.5) * 10, | |
| Math.random() * 5, | |
| (Math.random() - 0.5) * 10 | |
| ); | |
| const control2 = new THREE.Vector3( | |
| (Math.random() - 0.5) * 10, | |
| Math.random() * 5, | |
| (Math.random() - 0.5) * 10 | |
| ); | |
| this.curve = new THREE.CubicBezierCurve3( | |
| startPosition, | |
| control1, | |
| control2, | |
| endPoint | |
| ); | |
| this.progress = 0; | |
| this.speed = 0.002 + Math.random() * 0.003; | |
| this.alive = true; | |
| this.exploded = false; | |
| // Store previous position for orientation calculation | |
| this.prevPosition = startPosition.clone(); | |
| scene.add(this.group); | |
| activeRockets.push(this); | |
| updateRocketCount(); | |
| } | |
| update() { | |
| if (!this.alive) return; | |
| this.progress += this.speed; | |
| if (this.progress >= 1) { | |
| this.explode(); | |
| return; | |
| } | |
| // Move along curve | |
| const point = this.curve.getPoint(this.progress); | |
| this.group.position.copy(point); | |
| // Calculate direction vector | |
| const direction = new THREE.Vector3().subVectors(point, this.prevPosition).normalize(); | |
| // Only update orientation if we have significant movement | |
| if (direction.length() > 0.001) { | |
| // Create a quaternion that orients the rocket along the direction vector | |
| const targetQuaternion = new THREE.Quaternion(); | |
| const up = new THREE.Vector3(0, 1, 0); | |
| // Create a matrix that looks in the direction we're moving | |
| const rotationMatrix = new THREE.Matrix4(); | |
| rotationMatrix.lookAt(new THREE.Vector3(0, 0, 0), direction, up); | |
| // Extract quaternion from matrix | |
| targetQuaternion.setFromRotationMatrix(rotationMatrix); | |
| // Apply rotation to the rocket (not the whole group) | |
| this.rocket.quaternion.copy(targetQuaternion); | |
| // Store current position for next frame | |
| this.prevPosition.copy(point); | |
| } | |
| // Update particles | |
| this.updateParticles(); | |
| } | |
| updateParticles() { | |
| // Fire particles | |
| const firePositions = this.fireParticles.geometry.attributes.position.array; | |
| for (let i = 0; i < firePositions.length; i += 3) { | |
| firePositions[i + 1] -= 0.01; | |
| if (firePositions[i + 1] < -0.5) { | |
| firePositions[i] = (Math.random() - 0.5) * 0.15; | |
| firePositions[i + 1] = 0; | |
| firePositions[i + 2] = (Math.random() - 0.5) * 0.15; | |
| } | |
| } | |
| this.fireParticles.geometry.attributes.position.needsUpdate = true; | |
| // Smoke particles | |
| const smokePositions = this.smokeParticles.geometry.attributes.position.array; | |
| for (let i = 0; i < smokePositions.length; i += 3) { | |
| smokePositions[i] += (Math.random() - 0.5) * 0.01; | |
| smokePositions[i + 1] += 0.01; | |
| smokePositions[i + 2] += (Math.random() - 0.5) * 0.01; | |
| if (smokePositions[i + 1] > 0) { | |
| smokePositions[i] = (Math.random() - 0.5) * 0.3; | |
| smokePositions[i + 1] = -1 + Math.random() * 0.5; | |
| smokePositions[i + 2] = (Math.random() - 0.5) * 0.3; | |
| } | |
| } | |
| this.smokeParticles.geometry.attributes.position.needsUpdate = true; | |
| } | |
| explode() { | |
| this.alive = false; | |
| this.exploded = true; | |
| // Show explosion effect | |
| const explosion = document.getElementById('explosion'); | |
| explosion.style.opacity = '0.8'; | |
| explosion.style.transform = 'translate(-50%, -50%) scale(1)'; | |
| setTimeout(() => { | |
| explosion.style.opacity = '0'; | |
| explosion.style.transform = 'translate(-50%, -50%) scale(0.5)'; | |
| }, 500); | |
| // Remove from scene after a delay | |
| setTimeout(() => { | |
| scene.remove(this.group); | |
| const index = activeRockets.indexOf(this); | |
| if (index > -1) { | |
| activeRockets.splice(index, 1); | |
| } | |
| updateRocketCount(); | |
| }, 1000); | |
| } | |
| } | |
| // Active rockets array | |
| let activeRockets = []; | |
| // UI functions | |
| function updateRocketCount() { | |
| document.getElementById('rocketCount').textContent = activeRockets.length; | |
| } | |
| // Mouse position to 3D coordinates | |
| function getMouseWorldPosition(event, targetZ = 0) { | |
| const mouse = new THREE.Vector2(); | |
| mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
| mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
| const raycaster = new THREE.Raycaster(); | |
| raycaster.setFromCamera(mouse, camera); | |
| const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), targetZ); | |
| const intersection = new THREE.Vector3(); | |
| raycaster.ray.intersectPlane(plane, intersection); | |
| return intersection; | |
| } | |
| // Event listeners | |
| document.addEventListener('click', (event) => { | |
| if (event.target.closest('.button')) return; | |
| const mousePos = getMouseWorldPosition(event, 0); | |
| new Rocket(mousePos); | |
| }); | |
| document.getElementById('fireRocket').addEventListener('click', () => { | |
| const x = (Math.random() - 0.5) * 10; | |
| const y = (Math.random() - 0.5) * 10; | |
| const z = (Math.random() - 0.5) * 10; | |
| new Rocket(new THREE.Vector3(x, y, z)); | |
| }); | |
| let autoFireInterval = null; | |
| document.getElementById('autoFire').addEventListener('click', function() { | |
| if (autoFireInterval) { | |
| clearInterval(autoFireInterval); | |
| autoFireInterval = null; | |
| this.textContent = 'Auto Fire'; | |
| } else { | |
| autoFireInterval = setInterval(() => { | |
| const x = (Math.random() - 0.5) * 10; | |
| const y = (Math.random() - 0.5) * 10; | |
| const z = (Math.random() - 0.5) * 10; | |
| new Rocket(new THREE.Vector3(x, y, z)); | |
| }, 500); | |
| this.textContent = 'Stop Auto Fire'; | |
| } | |
| }); | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| // Animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| controls.update(); | |
| // Update all rockets | |
| for (let i = 0; i < activeRockets.length; i++) { | |
| activeRockets[i].update(); | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=LukasBe/3d-rockets" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |