|
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 city grow using L-Grammar and Three.js") |
|
|
|
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>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 CityLGrammar { |
|
constructor() { |
|
this.axiom = "F"; |
|
this.rules = { |
|
"F": ["F[+F]F[-F]", "F[+F][-F]F", "FF", "F[+F]"], |
|
"+": ["+"], |
|
"-": ["-"], |
|
"[": ["["], |
|
"]": ["]"] |
|
}; |
|
this.generation = 0; |
|
this.angle = Math.PI / 4; |
|
this.currentString = this.axiom; |
|
} |
|
|
|
evolve() { |
|
this.generation++; |
|
let result = this.currentString; |
|
let newString = ""; |
|
|
|
for (let char of result) { |
|
if (this.rules[char]) { |
|
const possibleRules = this.rules[char]; |
|
const selectedRule = possibleRules[Math.floor(Math.random() * possibleRules.length)]; |
|
newString += selectedRule; |
|
} else { |
|
newString += char; |
|
} |
|
} |
|
this.currentString = newString; |
|
return this.currentString; |
|
} |
|
|
|
buildCity(scene) { |
|
while(scene.children.length > 2) { // Keep lights |
|
scene.remove(scene.children[2]); |
|
} |
|
|
|
const stack = []; |
|
let position = new THREE.Vector3(0, 0, 0); |
|
let direction = new THREE.Vector3(0, 1, 0); |
|
let buildingCount = 0; |
|
|
|
// Add ground plane |
|
const groundGeo = new THREE.PlaneGeometry(100, 100); |
|
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); |
|
|
|
for (let char of this.currentString) { |
|
switch(char) { |
|
case 'F': |
|
const height = 2 + Math.random() * 6; |
|
const width = 1 + Math.random() * 2; |
|
|
|
// Create building |
|
const buildingGeo = new THREE.BoxGeometry(width, height, width); |
|
const buildingMat = 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), |
|
shininess: 30 |
|
}); |
|
const building = new THREE.Mesh(buildingGeo, buildingMat); |
|
|
|
building.position.copy(position); |
|
building.position.y = height / 2; |
|
building.rotation.y = Math.random() * Math.PI; |
|
scene.add(building); |
|
|
|
position.add(direction.clone().multiplyScalar(height)); |
|
buildingCount++; |
|
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: position.clone(), |
|
direction: direction.clone() |
|
}); |
|
break; |
|
|
|
case ']': |
|
if (stack.length > 0) { |
|
const state = stack.pop(); |
|
position = state.position; |
|
direction = state.direction; |
|
} |
|
break; |
|
} |
|
} |
|
|
|
document.getElementById('building-count').textContent = buildingCount; |
|
document.getElementById('generation').textContent = this.generation; |
|
} |
|
} |
|
|
|
// Three.js setup |
|
let scene, camera, renderer, controls; |
|
let city; |
|
|
|
function init() { |
|
const container = document.body; |
|
|
|
// Scene |
|
scene = new THREE.Scene(); |
|
scene.background = new THREE.Color(0x87CEEB); |
|
|
|
// Camera |
|
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); |
|
camera.position.set(0, 20, 30); |
|
|
|
// Renderer |
|
renderer = new THREE.WebGLRenderer({ antialias: true }); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
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); |
|
|
|
// Controls |
|
controls = new THREE.OrbitControls(camera, renderer.domElement); |
|
controls.enableDamping = true; |
|
controls.target.set(0, 0, 0); |
|
|
|
// Initialize city |
|
city = new CityLGrammar(); |
|
city.buildCity(scene); |
|
|
|
// Events |
|
window.addEventListener('resize', onWindowResize); |
|
document.getElementById('evolve').addEventListener('click', evolveCity); |
|
document.getElementById('reset').addEventListener('click', resetView); |
|
|
|
animate(); |
|
} |
|
|
|
function evolveCity() { |
|
city.evolve(); |
|
city.buildCity(scene); |
|
} |
|
|
|
function resetView() { |
|
camera.position.set(0, 20, 30); |
|
controls.target.set(0, 0, 0); |
|
controls.update(); |
|
} |
|
|
|
function onWindowResize() { |
|
camera.aspect = window.innerWidth / window.innerHeight; |
|
camera.updateProjectionMatrix(); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
} |
|
|
|
function animate() { |
|
requestAnimationFrame(animate); |
|
controls.update(); |
|
renderer.render(scene, camera); |
|
} |
|
|
|
init(); |
|
</script> |
|
</body> |
|
</html> |
|
""" |
|
|
|
|
|
components.html(html_code, height=600) |
|
|
|
st.sidebar.title("City Evolution Simulator") |
|
st.sidebar.write(""" |
|
## How to Play |
|
|
|
Watch a city evolve through generations using L-System grammar rules. |
|
|
|
### Controls: |
|
- **Evolve City**: Generate next city generation |
|
- **Reset View**: Return to default view |
|
- **Left-click + drag**: Rotate view |
|
- **Right-click + drag**: Pan view |
|
- **Scroll**: Zoom in/out |
|
|
|
### Evolution Rules: |
|
- Each generation adds more buildings |
|
- Buildings branch out from existing structures |
|
- Random heights and sizes create variety |
|
- Roads implicitly form between buildings |
|
|
|
Track progress with building count and generation number! |
|
""") |