| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Bratislava Castle - Cinematic 3D Experience</title> |
| <style> |
| body { |
| margin: 0; |
| overflow: hidden; |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| } |
| #container { |
| position: relative; |
| width: 100vw; |
| height: 100vh; |
| background: linear-gradient(to bottom, #1a2980, #26d0ce); |
| } |
| #canvas { |
| display: block; |
| } |
| #ui { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| padding: 20px; |
| box-sizing: border-box; |
| color: white; |
| text-align: center; |
| pointer-events: none; |
| z-index: 10; |
| } |
| #title { |
| font-size: 2.5rem; |
| text-shadow: 0 0 10px rgba(0,0,0,0.7); |
| margin-bottom: 10px; |
| letter-spacing: 2px; |
| } |
| #subtitle { |
| font-size: 1.2rem; |
| max-width: 600px; |
| margin: 0 auto; |
| text-shadow: 0 0 5px rgba(0,0,0,0.7); |
| } |
| #controls { |
| position: absolute; |
| bottom: 20px; |
| left: 20px; |
| background: rgba(0,0,0,0.6); |
| padding: 15px; |
| border-radius: 10px; |
| color: white; |
| font-size: 0.9rem; |
| pointer-events: auto; |
| } |
| #showcase-btn { |
| position: absolute; |
| bottom: 20px; |
| right: 20px; |
| padding: 12px 25px; |
| background: #e63946; |
| color: white; |
| border: none; |
| border-radius: 30px; |
| font-size: 1rem; |
| cursor: pointer; |
| box-shadow: 0 4px 10px rgba(0,0,0,0.3); |
| transition: all 0.3s ease; |
| pointer-events: auto; |
| } |
| #showcase-btn:hover { |
| background: #d62828; |
| transform: translateY(-2px); |
| box-shadow: 0 6px 15px rgba(0,0,0,0.4); |
| } |
| #loading { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: rgba(0,0,0,0.8); |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| color: white; |
| z-index: 100; |
| } |
| .loader { |
| border: 5px solid #f3f3f3; |
| border-top: 5px solid #e63946; |
| border-radius: 50%; |
| width: 50px; |
| height: 50px; |
| animation: spin 1s linear infinite; |
| margin-bottom: 20px; |
| } |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| #progress-bar { |
| width: 300px; |
| height: 10px; |
| background: #333; |
| border-radius: 5px; |
| margin-top: 20px; |
| overflow: hidden; |
| } |
| #progress { |
| height: 100%; |
| background: #e63946; |
| width: 0%; |
| transition: width 0.3s; |
| } |
| #hud { |
| position: absolute; |
| top: 20px; |
| right: 20px; |
| background: rgba(0,0,0,0.6); |
| padding: 10px; |
| border-radius: 5px; |
| color: white; |
| font-size: 0.8rem; |
| } |
| </style> |
| </head> |
| <body> |
| <div id="container"> |
| <div id="loading"> |
| <div class="loader"></div> |
| <h2>Creating Bratislava Castle Experience</h2> |
| <p>Loading 3D assets and textures...</p> |
| <div id="progress-bar"> |
| <div id="progress"></div> |
| </div> |
| </div> |
| |
| <div id="ui"> |
| <h1 id="title">BRATISLAVA CASTLE</h1> |
| <p id="subtitle">A cinematic journey through Slovakia's historic landmark with surrounding Danube scenery</p> |
| </div> |
| |
| <div id="hud"> |
| <div>Camera Mode: <span id="mode">Orbit</span></div> |
| <div>Position: <span id="position">0,0,0</span></div> |
| </div> |
| |
| <div id="controls"> |
| <strong>CONTROLS:</strong><br> |
| Orbit Mode: Drag to rotate, Scroll to zoom<br> |
| Fly Mode: WASD + Mouse, E/Q for up/down, Shift for boost<br> |
| Press F to toggle modes |
| </div> |
| |
| <button id="showcase-btn">START CINEMATIC SHOWCASE</button> |
| |
| <canvas id="canvas"></canvas> |
| </div> |
|
|
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/PointerLockControls.min.js"></script> |
| <script> |
| |
| let scene, camera, renderer, controls, flyControls; |
| let castle, danube, bridge, cathedral; |
| let isShowcaseActive = false; |
| let showcaseStartTime = 0; |
| let clock = new THREE.Clock(); |
| let mode = 'orbit'; |
| |
| |
| function init() { |
| |
| scene = new THREE.Scene(); |
| scene.background = new THREE.Color(0x87CEEB); |
| scene.fog = new THREE.Fog(0x87CEEB, 200, 1000); |
| |
| |
| camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 5000); |
| camera.position.set(0, 50, 200); |
| |
| |
| renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true }); |
| renderer.setSize(window.innerWidth, window.innerHeight); |
| renderer.shadowMap.enabled = true; |
| |
| |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); |
| scene.add(ambientLight); |
| |
| const sunLight = new THREE.DirectionalLight(0xffffff, 0.8); |
| sunLight.position.set(100, 200, 100); |
| sunLight.castShadow = true; |
| sunLight.shadow.mapSize.width = 2048; |
| sunLight.shadow.mapSize.height = 2048; |
| scene.add(sunLight); |
| |
| |
| controls = new THREE.OrbitControls(camera, renderer.domElement); |
| controls.enableDamping = true; |
| controls.dampingFactor = 0.05; |
| |
| flyControls = new THREE.PointerLockControls(camera, document.body); |
| |
| |
| createEnvironment(); |
| |
| |
| createCastle(); |
| |
| |
| createScenery(); |
| |
| |
| window.addEventListener('resize', onWindowResize); |
| document.getElementById('showcase-btn').addEventListener('click', startShowcase); |
| document.addEventListener('keydown', onKeyDown); |
| |
| |
| setTimeout(() => { |
| document.getElementById('loading').style.display = 'none'; |
| }, 3000); |
| |
| |
| animate(); |
| } |
| |
| |
| function createEnvironment() { |
| |
| const groundGeometry = new THREE.PlaneGeometry(2000, 2000); |
| const groundMaterial = new THREE.MeshStandardMaterial({ |
| color: 0x3d8c40, |
| roughness: 0.9, |
| metalness: 0.1 |
| }); |
| const ground = new THREE.Mesh(groundGeometry, groundMaterial); |
| ground.rotation.x = -Math.PI / 2; |
| ground.receiveShadow = true; |
| scene.add(ground); |
| |
| |
| const skyGeometry = new THREE.SphereGeometry(1000, 32, 32); |
| const skyMaterial = new THREE.MeshBasicMaterial({ |
| color: 0x87CEEB, |
| side: THREE.BackSide |
| }); |
| const sky = new THREE.Mesh(skyGeometry, skyMaterial); |
| scene.add(sky); |
| |
| |
| const sunGeometry = new THREE.SphereGeometry(20, 32, 32); |
| const sunMaterial = new THREE.MeshBasicMaterial({ color: 0xFFFF00 }); |
| const sun = new THREE.Mesh(sunGeometry, sunMaterial); |
| sun.position.set(100, 150, -100); |
| scene.add(sun); |
| } |
| |
| |
| function createCastle() { |
| |
| const baseGeometry = new THREE.BoxGeometry(150, 80, 150); |
| const baseMaterial = new THREE.MeshStandardMaterial({ |
| color: 0xD2B48C, |
| roughness: 0.8 |
| }); |
| const base = new THREE.Mesh(baseGeometry, baseMaterial); |
| base.position.y = 40; |
| base.castShadow = true; |
| base.receiveShadow = true; |
| scene.add(base); |
| |
| |
| const towerGeometry = new THREE.CylinderGeometry(25, 25, 100, 32); |
| const towerMaterial = new THREE.MeshStandardMaterial({ |
| color: 0xA0522D, |
| roughness: 0.9 |
| }); |
| |
| const tower1 = new THREE.Mesh(towerGeometry, towerMaterial); |
| tower1.position.set(-60, 90, -60); |
| tower1.castShadow = true; |
| scene.add(tower1); |
| |
| const tower2 = new THREE.Mesh(towerGeometry, towerMaterial); |
| tower2.position.set(60, 90, -60); |
| tower2.castShadow = true; |
| scene.add(tower2); |
| |
| const tower3 = new THREE.Mesh(towerGeometry, towerMaterial); |
| tower3.position.set(-60, 90, 60); |
| tower3.castShadow = true; |
| scene.add(tower3); |
| |
| const tower4 = new THREE.Mesh(towerGeometry, towerMaterial); |
| tower4.position.set(60, 90, 60); |
| tower4.castShadow = true; |
| scene.add(tower4); |
| |
| |
| const roofGeometry = new THREE.ConeGeometry(100, 40, 4); |
| const roofMaterial = new THREE.MeshStandardMaterial({ |
| color: 0xB22222, |
| roughness: 0.7 |
| }); |
| const roof = new THREE.Mesh(roofGeometry, roofMaterial); |
| roof.position.y = 130; |
| roof.rotation.y = Math.PI / 4; |
| roof.castShadow = true; |
| scene.add(roof); |
| |
| |
| const flagPoleGeometry = new THREE.CylinderGeometry(1, 1, 30, 8); |
| const flagPoleMaterial = new THREE.MeshStandardMaterial({ color: 0xC0C0C0 }); |
| const flagPole = new THREE.Mesh(flagPoleGeometry, flagPoleMaterial); |
| flagPole.position.set(0, 155, 0); |
| scene.add(flagPole); |
| |
| const flagGeometry = new THREE.PlaneGeometry(20, 12); |
| const flagMaterial = new THREE.MeshStandardMaterial({ |
| color: 0xFFFFFF, |
| side: THREE.DoubleSide, |
| roughness: 0.9 |
| }); |
| const flag = new THREE.Mesh(flagGeometry, flagMaterial); |
| flag.position.set(10, 160, 0); |
| flag.rotation.y = Math.PI / 2; |
| scene.add(flag); |
| |
| castle = { base, towers: [tower1, tower2, tower3, tower4], roof, flag }; |
| } |
| |
| |
| function createScenery() { |
| |
| const riverGeometry = new THREE.PlaneGeometry(1000, 100); |
| const riverMaterial = new THREE.MeshStandardMaterial({ |
| color: 0x1E90FF, |
| roughness: 0.9, |
| metalness: 0.8 |
| }); |
| const river = new THREE.Mesh(riverGeometry, riverMaterial); |
| river.rotation.x = -Math.PI / 2; |
| river.position.set(0, 0.1, 300); |
| scene.add(river); |
| danube = river; |
| |
| |
| const bridgeBaseGeometry = new THREE.BoxGeometry(300, 10, 20); |
| const bridgeBaseMaterial = new THREE.MeshStandardMaterial({ color: 0x808080 }); |
| const bridgeBase = new THREE.Mesh(bridgeBaseGeometry, bridgeBaseMaterial); |
| bridgeBase.position.set(0, 20, 300); |
| bridgeBase.castShadow = true; |
| scene.add(bridgeBase); |
| |
| |
| const towerGeometry = new THREE.CylinderGeometry(5, 5, 50, 16); |
| const towerMaterial = new THREE.MeshStandardMaterial({ color: 0xA9A9A9 }); |
| |
| const tower1 = new THREE.Mesh(towerGeometry, towerMaterial); |
| tower1.position.set(-100, 45, 300); |
| scene.add(tower1); |
| |
| const tower2 = new THREE.Mesh(towerGeometry, towerMaterial); |
| tower2.position.set(100, 45, 300); |
| scene.add(tower2); |
| |
| |
| const cableMaterial = new THREE.LineBasicMaterial({ color: 0xC0C0C0 }); |
| |
| for (let i = 0; i < 10; i++) { |
| const points = []; |
| points.push(new THREE.Vector3(-100 + i * 22, 45, 300)); |
| points.push(new THREE.Vector3(0, 70 + Math.sin(i) * 10, 300)); |
| points.push(new THREE.Vector3(100 - i * 22, 45, 300)); |
| |
| const cableGeometry = new THREE.BufferGeometry().setFromPoints(points); |
| const cable = new THREE.Line(cableGeometry, cableMaterial); |
| scene.add(cable); |
| } |
| |
| bridge = { base: bridgeBase, towers: [tower1, tower2] }; |
| |
| |
| const cathedralBaseGeometry = new THREE.BoxGeometry(60, 80, 40); |
| const cathedralBaseMaterial = new THREE.MeshStandardMaterial({ color: 0xF5F5DC }); |
| const cathedralBase = new THREE.Mesh(cathedralBaseGeometry, cathedralBaseMaterial); |
| cathedralBase.position.set(-200, 40, 100); |
| cathedralBase.castShadow = true; |
| scene.add(cathedralBase); |
| |
| const spireGeometry = new THREE.ConeGeometry(15, 60, 8); |
| const spireMaterial = new THREE.MeshStandardMaterial({ color: 0x708090 }); |
| const spire = new THREE.Mesh(spireGeometry, spireMaterial); |
| spire.position.set(-200, 110, 100); |
| scene.add(spire); |
| |
| cathedral = { base: cathedralBase, spire }; |
| |
| |
| for (let i = 0; i < 30; i++) { |
| const treeTrunkGeometry = new THREE.CylinderGeometry(2, 2, 15, 8); |
| const treeTrunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513 }); |
| const treeTrunk = new THREE.Mesh(treeTrunkGeometry, treeTrunkMaterial); |
| |
| const treeTopGeometry = new THREE.ConeGeometry(8, 20, 8); |
| const treeTopMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 }); |
| const treeTop = new THREE.Mesh(treeTopGeometry, treeTopMaterial); |
| treeTop.position.y = 17.5; |
| |
| const tree = new THREE.Group(); |
| tree.add(treeTrunk); |
| tree.add(treeTop); |
| |
| |
| const angle = Math.random() * Math.PI * 2; |
| const distance = 100 + Math.random() * 150; |
| tree.position.set( |
| Math.cos(angle) * distance, |
| 7.5, |
| Math.sin(angle) * distance + 200 |
| ); |
| |
| scene.add(tree); |
| } |
| |
| |
| const hillGeometry = new THREE.ConeGeometry(300, 100, 32); |
| const hillMaterial = new THREE.MeshStandardMaterial({ |
| color: 0x2E8B57, |
| roughness: 0.9 |
| }); |
| const hill = new THREE.Mesh(hillGeometry, hillMaterial); |
| hill.position.set(0, -50, -200); |
| hill.rotation.x = Math.PI; |
| scene.add(hill); |
| } |
| |
| |
| function startShowcase() { |
| isShowcaseActive = true; |
| showcaseStartTime = clock.getElapsedTime(); |
| document.getElementById('showcase-btn').textContent = "SHOWCASE ACTIVE"; |
| document.getElementById('showcase-btn').disabled = true; |
| } |
| |
| |
| function updateShowcase() { |
| const elapsed = clock.getElapsedTime() - showcaseStartTime; |
| const progress = elapsed / 20; |
| |
| if (progress >= 1) { |
| isShowcaseActive = false; |
| document.getElementById('showcase-btn').textContent = "REPLAY SHOWCASE"; |
| document.getElementById('showcase-btn').disabled = false; |
| return; |
| } |
| |
| |
| |
| |
| |
| |
| |
| if (progress < 0.25) { |
| |
| const t = progress * 4; |
| camera.position.set( |
| -200 + t * 200, |
| 10 + Math.sin(t * Math.PI) * 5, |
| 400 - t * 100 |
| ); |
| camera.lookAt(0, 30, 300); |
| } else if (progress < 0.5) { |
| |
| const t = (progress - 0.25) * 4; |
| camera.position.set( |
| -50 + t * 50, |
| 30 + t * 70, |
| 100 - t * 50 |
| ); |
| camera.lookAt(0, 80, 0); |
| } else if (progress < 0.75) { |
| |
| const t = (progress - 0.5) * 4; |
| const angle = Math.PI + t * Math.PI; |
| camera.position.set( |
| Math.cos(angle) * 100, |
| 80 + Math.sin(t * Math.PI) * 30, |
| Math.sin(angle) * 100 |
| ); |
| camera.lookAt(0, 50, 0); |
| } else { |
| |
| const t = (progress - 0.75) * 4; |
| camera.position.set( |
| Math.sin(t * 0.5) * 50, |
| 150 + t * 50, |
| 200 + t * 100 |
| ); |
| camera.lookAt(0, 80, 0); |
| } |
| } |
| |
| |
| function onWindowResize() { |
| camera.aspect = window.innerWidth / window.innerHeight; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(window.innerWidth, window.innerHeight); |
| } |
| |
| |
| function onKeyDown(event) { |
| if (event.key === 'f' || event.key === 'F') { |
| toggleCameraMode(); |
| } |
| } |
| |
| |
| function toggleCameraMode() { |
| mode = mode === 'orbit' ? 'fly' : 'orbit'; |
| document.getElementById('mode').textContent = mode.charAt(0).toUpperCase() + mode.slice(1); |
| |
| if (mode === 'fly') { |
| flyControls.lock(); |
| } else { |
| flyControls.unlock(); |
| } |
| } |
| |
| |
| function animate() { |
| requestAnimationFrame(animate); |
| |
| const delta = clock.getDelta(); |
| |
| |
| if (mode === 'orbit' && !isShowcaseActive) { |
| controls.update(); |
| } |
| |
| |
| if (isShowcaseActive) { |
| updateShowcase(); |
| } |
| |
| |
| const pos = camera.position; |
| document.getElementById('position').textContent = |
| `${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)}`; |
| |
| |
| renderer.render(scene, camera); |
| } |
| |
| |
| function simulateLoading() { |
| let progress = 0; |
| const interval = setInterval(() => { |
| progress += Math.random() * 5; |
| if (progress >= 100) { |
| progress = 100; |
| clearInterval(interval); |
| } |
| document.getElementById('progress').style.width = progress + '%'; |
| }, 100); |
| } |
| |
| |
| simulateLoading(); |
| |
| |
| window.onload = init; |
| </script> |
| </body> |
| </html> |