Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| st.set_page_config(page_title="City Evolution Simulator", layout="wide") | |
| st.title("City Evolution Simulator") | |
| st.write("Watch a 3D city grow with lakes, hills, and evolving blocks") | |
| html_code = """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>City Evolution Simulator</title> | |
| <style> | |
| body { margin: 0; overflow: hidden; } | |
| canvas { width: 100%; height: 100%; display: block; } | |
| .ui-panel { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| background: rgba(0,0,0,0.7); | |
| padding: 15px; | |
| border-radius: 5px; | |
| color: white; | |
| font-family: Arial, sans-serif; | |
| z-index: 1000; | |
| } | |
| .ui-panel button { | |
| margin: 5px 0; | |
| padding: 5px 10px; | |
| width: 100%; | |
| background: #4CAF50; | |
| color: white; | |
| border: none; | |
| border-radius: 3px; | |
| cursor: pointer; | |
| } | |
| .ui-panel button:hover { background: #45a049; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="ui-panel"> | |
| <h3>City Controls</h3> | |
| <button id="evolve">Evolve City</button> | |
| <button id="reset">Reset View</button> | |
| <div id="stats"> | |
| <p>Buildings: <span id="building-count">0</span></p> | |
| <p>Blocks: <span id="block-count">0</span></p> | |
| <p>Generation: <span id="generation">0</span></p> | |
| </div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script> | |
| <script> | |
| class BuildingLSystem { | |
| constructor() { | |
| this.axiom = "F"; | |
| this.rules = { | |
| "F": ["F[+F]", "F[-F]", "FF", "F"], | |
| "+": ["+"], | |
| "-": ["-"], | |
| "[": ["["], | |
| "]": ["]"] | |
| }; | |
| this.angle = Math.PI / 6; | |
| } | |
| generate() { | |
| let result = this.axiom; | |
| for (let i = 0; i < 2; i++) { | |
| let newString = ""; | |
| for (let char of result) { | |
| if (this.rules[char]) { | |
| const possible = this.rules[char]; | |
| newString += possible[Math.floor(Math.random() * possible.length)]; | |
| } else { | |
| newString += char; | |
| } | |
| } | |
| result = newString; | |
| } | |
| return result; | |
| } | |
| build(scene, basePos, maxHeight) { | |
| let height = 0; | |
| const stack = []; | |
| let position = basePos.clone(); | |
| let direction = new THREE.Vector3(0, 1, 0); | |
| const structure = new THREE.Group(); | |
| for (let char of this.generate()) { | |
| switch(char) { | |
| case 'F': | |
| if (height < maxHeight) { | |
| const width = 0.8 + Math.random() * 0.4; | |
| const floorHeight = 1 + Math.random() * 2; | |
| const geo = new THREE.BoxGeometry(width, floorHeight, width); | |
| const mat = new THREE.MeshPhongMaterial({ | |
| color: new THREE.Color(0.5 + Math.random() * 0.5, | |
| 0.5 + Math.random() * 0.5, | |
| 0.5 + Math.random() * 0.5) | |
| }); | |
| const floor = new THREE.Mesh(geo, mat); | |
| floor.position.copy(position).add(new THREE.Vector3(0, floorHeight/2, 0)); | |
| structure.add(floor); | |
| position.y += floorHeight; | |
| height += floorHeight; | |
| } | |
| break; | |
| case '+': | |
| direction.applyAxisAngle(new THREE.Vector3(0, 0, 1), this.angle); | |
| break; | |
| case '-': | |
| direction.applyAxisAngle(new THREE.Vector3(0, 0, 1), -this.angle); | |
| break; | |
| case '[': | |
| stack.push(position.clone()); | |
| break; | |
| case ']': | |
| if (stack.length > 0) position = stack.pop(); | |
| break; | |
| } | |
| } | |
| return structure; | |
| } | |
| } | |
| class CitySimulator { | |
| constructor() { | |
| this.blocks = []; | |
| this.blockSize = 10; // 10x10 units per block | |
| this.maxBuildingsPerBlock = 5; | |
| this.generation = 0; | |
| this.lakeCenters = [ | |
| new THREE.Vector2(20, 20), | |
| new THREE.Vector2(-30, 10) | |
| ]; | |
| } | |
| addBlock(scene, x, z) { | |
| const block = { | |
| position: new THREE.Vector2(x, z), | |
| buildings: [], | |
| maxHeight: this.isWaterfront(x, z) ? 15 : 8 | |
| }; | |
| this.blocks.push(block); | |
| this.evolveBlock(scene, block, true); | |
| } | |
| isWaterfront(x, z) { | |
| const pos = new THREE.Vector2(x, z); | |
| return this.lakeCenters.some(center => | |
| pos.distanceTo(center) < 15 && pos.distanceTo(center) > 5); | |
| } | |
| evolveBlock(scene, block, initial = false) { | |
| if (block.buildings.length < this.maxBuildingsPerBlock) { | |
| const lsystem = new BuildingLSystem(); | |
| const gridX = Math.floor(Math.random() * 3) - 1; | |
| const gridZ = Math.floor(Math.random() * 3) - 1; | |
| const basePos = new THREE.Vector3( | |
| block.position.x + gridX * 2, | |
| this.getTerrainHeight(block.position.x, block.position.y), | |
| block.position.y + gridZ * 2 | |
| ); | |
| const building = lsystem.build(scene, basePos, block.maxHeight); | |
| if (this.isWaterfront(block.position.x, block.position.y)) { | |
| building.scale.set(1.5, 2, 1.5); // Lavish waterfront buildings | |
| } | |
| scene.add(building); | |
| block.buildings.push(building); | |
| } | |
| } | |
| evolve() { | |
| this.generation++; | |
| if (this.blocks.length < 20) { | |
| const x = (Math.random() - 0.5) * 140; | |
| const z = (Math.random() - 0.5) * 80; | |
| if (!this.isInLake(x, z)) this.addBlock(scene, x, z); | |
| } | |
| this.blocks.forEach(block => this.evolveBlock(scene, block)); | |
| this.updateStats(); | |
| } | |
| getTerrainHeight(x, z) { | |
| return Math.sin(x * 0.05) * Math.cos(z * 0.05) * 5; | |
| } | |
| isInLake(x, z) { | |
| const pos = new THREE.Vector2(x, z); | |
| return this.lakeCenters.some(center => pos.distanceTo(center) < 10); | |
| } | |
| updateStats() { | |
| const totalBuildings = this.blocks.reduce((sum, block) => sum + block.buildings.length, 0); | |
| document.getElementById('building-count').textContent = totalBuildings; | |
| document.getElementById('block-count').textContent = this.blocks.length; | |
| document.getElementById('generation').textContent = this.generation; | |
| } | |
| } | |
| let scene, camera, renderer, controls; | |
| let city; | |
| function init() { | |
| const container = document.body; | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x87CEEB); | |
| // Camera with 16:9 aspect ratio | |
| camera = new THREE.PerspectiveCamera(75, 16 / 9, 0.1, 1000); | |
| camera.position.set(0, 50, 80); | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight * (9/16)); | |
| container.appendChild(renderer.domElement); | |
| // Lights | |
| scene.add(new THREE.AmbientLight(0x404040)); | |
| const sun = new THREE.DirectionalLight(0xffffff, 0.8); | |
| sun.position.set(50, 50, 50); | |
| scene.add(sun); | |
| // Terrain and Lakes | |
| const groundGeo = new THREE.PlaneGeometry(160, 90, 32, 32); | |
| const groundMat = new THREE.MeshPhongMaterial({ color: 0x4a7043 }); | |
| const ground = new THREE.Mesh(groundGeo, groundMat); | |
| ground.rotation.x = -Math.PI / 2; | |
| ground.position.y = -0.1; | |
| scene.add(ground); | |
| // Add lakes | |
| const lakeGeo = new THREE.CircleGeometry(10, 32); | |
| const lakeMat = new THREE.MeshPhongMaterial({ color: 0x4682b4 }); | |
| city.lakeCenters.forEach(center => { | |
| const lake = new THREE.Mesh(lakeGeo, lakeMat); | |
| lake.rotation.x = -Math.PI / 2; | |
| lake.position.set(center.x, 0, center.y); | |
| scene.add(lake); | |
| }); | |
| // Add bridges | |
| const bridgeGeo = new THREE.BoxGeometry(5, 0.2, 15); | |
| const bridgeMat = new THREE.MeshPhongMaterial({ color: 0x808080 }); | |
| const bridge1 = new THREE.Mesh(bridgeGeo, bridgeMat); | |
| bridge1.position.set(15, 0.2, 20); | |
| scene.add(bridge1); | |
| // Controls | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.target.set(0, 0, 0); | |
| // Initialize city | |
| city = new CitySimulator(); | |
| city.addBlock(scene, 0, 0); | |
| // Events | |
| window.addEventListener('resize', onWindowResize); | |
| document.getElementById('evolve').addEventListener('click', () => city.evolve()); | |
| document.getElementById('reset').addEventListener('click', resetView); | |
| animate(); | |
| } | |
| function resetView() { | |
| camera.position.set(0, 50, 80); | |
| controls.target.set(0, 0, 0); | |
| controls.update(); | |
| } | |
| function onWindowResize() { | |
| const width = window.innerWidth; | |
| const height = width * (9/16); | |
| camera.aspect = 16 / 9; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(width, height); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # Render the HTML component | |
| components.html(html_code, height=600) | |
| st.sidebar.title("City Evolution Simulator") | |
| st.sidebar.write(""" | |
| ## How to Play | |
| Watch a 3D city evolve with lakes, hills, and building blocks. | |
| ### Controls: | |
| - **Evolve City**: Grow the city | |
| - **Reset View**: Return to default view | |
| - **Left-click + drag**: Rotate | |
| - **Right-click + drag**: Pan | |
| - **Scroll**: Zoom | |
| ### Features: | |
| - 16:9 play area | |
| - Blocks (10x10 units) with up to 5 buildings | |
| - Buildings evolve floor-by-floor using L-systems | |
| - Terrain with hills and lakes | |
| - Waterfront properties grow larger | |
| - Bridges connect land masses | |
| """) |