awacke1's picture
Update app.py
cbd7594 verified
import streamlit as st
import streamlit.components.v1 as components
st.set_page_config(page_title="3D City Evolution Simulator", layout="wide")
st.title("3D City Evolution Simulator")
st.write("Watch a 3D city grow with roads, vegetation, and dynamic weather")
# Sliders for container size with initial 3:4 aspect ratio
max_width = min(1200, st.session_state.get('window_width', 1200))
max_height = min(1600, st.session_state.get('window_height', 1600))
col1, col2 = st.columns(2)
with col1:
container_width = st.slider("Container Width (px)", 300, max_width, 768, step=50)
with col2:
container_height = st.slider("Container Height (px)", 400, max_height, 1024, step=50)
html_code = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>3D City Evolution Simulator</title>
<style>
body {{ margin: 0; overflow: hidden; }}
#container {{ width: {container_width}px; height: {container_height}px; margin: 0 auto; }}
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 id="container"></div>
<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();
let baseWidth = 1.5;
// Add small attached buildings horizontally
const attachCount = Math.floor(Math.random() * 3); // 0-2 attachments
for (let i = 0; i < attachCount; i++) {{
const attachWidth = baseWidth * 0.6;
const attachHeight = 1 + Math.random() * 2;
const geo = new THREE.BoxGeometry(attachWidth, attachHeight, attachWidth);
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 attach = new THREE.Mesh(geo, mat);
const offsetX = (i + 1) * (baseWidth + attachWidth) * (Math.random() > 0.5 ? 1 : -1);
attach.position.set(position.x + offsetX, attachHeight / 2, position.z);
attach.castShadow = true;
structure.add(attach);
}}
for (let char of this.generate()) {{
switch(char) {{
case 'F':
if (height < maxHeight) {{
const width = baseWidth * (1 - height / maxHeight);
const floorHeight = 2 + 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));
floor.castShadow = true;
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.roads = [];
this.blockSize = 10;
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) ? 20 : 12
}};
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);
}}
scene.add(building);
block.buildings.push(building);
}}
}}
addRoad(scene, start, end) {{
const distance = start.distanceTo(end);
const roadGeo = new THREE.PlaneGeometry(2, distance);
const roadMat = new THREE.MeshPhongMaterial({{ color: 0x555555 }});
const road = new THREE.Mesh(roadGeo, roadMat);
road.rotation.x = -Math.PI / 2;
const midPoint = start.clone().add(end).multiplyScalar(0.5);
road.position.set(midPoint.x, 0.02, midPoint.y);
road.lookAt(new THREE.Vector3(end.x, 0, end.y));
road.receiveShadow = true;
scene.add(road);
this.roads.push(road);
}}
addVegetation(scene) {{
const treeGeo = new THREE.ConeGeometry(1, 3, 8);
const treeMat = new THREE.MeshPhongMaterial({{ color: 0x228B22 }});
const shrubGeo = new THREE.SphereGeometry(0.5, 8, 8);
const shrubMat = new THREE.MeshPhongMaterial({{ color: 0x32CD32 }});
for (let i = 0; i < 10; i++) {{
const x = (Math.random() - 0.5) * 90;
const z = (Math.random() - 0.5) * 120;
if (!this.isInLake(x, z)) {{
const tree = new THREE.Mesh(treeGeo, treeMat);
tree.position.set(x, this.getTerrainHeight(x, z) + 1.5, z);
tree.castShadow = true;
scene.add(tree);
const shrub = new THREE.Mesh(shrubGeo, shrubMat);
shrub.position.set(x + 1, this.getTerrainHeight(x, z) + 0.5, z + 1);
shrub.castShadow = true;
scene.add(shrub);
}}
}}
}}
evolve(scene) {{
this.generation++;
if (this.blocks.length < 20) {{
const x = (Math.random() - 0.5) * 90;
const z = (Math.random() - 0.5) * 120;
if (!this.isInLake(x, z)) {{
this.addBlock(scene, x, z);
if (this.blocks.length > 1) {{
const lastBlock = this.blocks[this.blocks.length - 2];
this.addRoad(scene, lastBlock.position, this.blocks[this.blocks.length - 1].position);
}}
}}
}}
this.blocks.forEach(block => this.evolveBlock(scene, block));
this.addVegetation(scene);
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;
function init() {{
const container = document.getElementById('container');
if (!container) {{
console.error('Container not found');
return;
}}
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
// Camera with 3:4 aspect ratio
camera = new THREE.PerspectiveCamera(75, 3 / 4, 0.1, 1000);
camera.position.set(0, 50, 60);
// Renderer
renderer = new THREE.WebGLRenderer({{ antialias: true }});
renderer.setSize({container_width}, {container_height});
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
// Lights
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const sun = new THREE.DirectionalLight(0xffffff, 0.8);
sun.position.set(50, 50, 50);
sun.castShadow = true;
sun.shadow.mapSize.width = 1024;
sun.shadow.mapSize.height = 1024;
sun.shadow.camera.near = 0.5;
sun.shadow.camera.far = 500;
scene.add(sun);
// Ground with bump mapping
const groundGeo = new THREE.PlaneGeometry(1000, 1000, 32, 32); // Extended to horizon
const groundMat = new THREE.MeshPhongMaterial({{
color: 0x4a7043,
bumpScale: 0.5,
shininess: 10
}});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.1;
ground.receiveShadow = true;
// Simple bump map (noise)
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
for (let x = 0; x < 256; x++) {{
for (let y = 0; y < 256; y++) {{
const grayValue = Math.random() * 255; // Renamed 'value' to 'grayValue' for clarity
ctx.fillStyle = 'rgb(' + grayValue + ',' + grayValue + ',' + grayValue + ')';
ctx.fillRect(x, y, 1, 1);
}}
}}
const bumpTexture = new THREE.Texture(canvas);
bumpTexture.needsUpdate = true;
groundMat.bumpMap = bumpTexture;
scene.add(ground);
// Lakes
const lakeGeo = new THREE.CircleGeometry(10, 32);
const lakeMat = new THREE.MeshPhongMaterial({{ color: 0x4682b4 }});
const lakeCenters = [new THREE.Vector2(20, 20), new THREE.Vector2(-30, 10)];
lakeCenters.forEach(center => {{
const lake = new THREE.Mesh(lakeGeo, lakeMat);
lake.rotation.x = -Math.PI / 2;
lake.position.set(center.x, 0.01, center.y);
lake.receiveShadow = true;
scene.add(lake);
}});
// Bridge
const bridgeGeo = new THREE.BoxGeometry(5, 0.2, 15);
const bridgeMat = new THREE.MeshPhongMaterial({{ color: 0x808080 }});
const bridge = new THREE.Mesh(bridgeGeo, bridgeMat);
bridge.position.set(15, 0.2, 20);
bridge.castShadow = true;
bridge.receiveShadow = true;
scene.add(bridge);
// Clouds
const cloudGeo = new THREE.SphereGeometry(5, 8, 8);
const cloudMat = new THREE.MeshPhongMaterial({{ color: 0xFFFFFF, opacity: 0.8, transparent: true }});
for (let i = 0; i < 5; i++) {{
const cloud = new THREE.Mesh(cloudGeo, cloudMat);
cloud.position.set(
(Math.random() - 0.5) * 200,
50 + Math.random() * 20,
(Math.random() - 0.5) * 200
);
cloud.castShadow = true;
scene.add(cloud);
}}
// Controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 0, 0);
// City
const city = new CitySimulator();
city.addBlock(scene, 0, 0);
// Events
window.addEventListener('resize', onWindowResize);
document.getElementById('evolve').addEventListener('click', () => city.evolve(scene));
document.getElementById('reset').addEventListener('click', resetView);
animate();
}}
function resetView() {{
camera.position.set(0, 50, 60);
controls.target.set(0, 0, 0);
controls.update();
}}
function onWindowResize() {{
const width = {container_width};
const height = {container_height};
camera.aspect = 3 / 4;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}}
function animate() {{
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}}
window.onload = init;
</script>
</body>
</html>
"""
# Render the HTML component with dynamic size
components.html(html_code, width=container_width, height=container_height)
st.sidebar.title("3D City Evolution Simulator")
st.sidebar.write("""
## How to Play
Watch a 3D city evolve with roads, vegetation, and dynamic weather.
### Controls:
- **Evolve City**: Grow the city
- **Reset View**: Return to default view
- **Left-click + drag**: Rotate
- **Right-click + drag**: Pan
- **Scroll**: Zoom
- **Sliders**: Adjust play area size
### Features:
- 3:4 initial play area (768x1024)
- Roads connect blocks
- Trees and shrubs added each evolution
- Extended green ground with bump mapping
- Clouds and sunlight with shadows
- Buildings with horizontal attachments
""")