Update app.py
Browse files
app.py
CHANGED
@@ -6,17 +6,27 @@ st.set_page_config(page_title="3D City Evolution Simulator", layout="wide")
|
|
6 |
st.title("3D City Evolution Simulator")
|
7 |
st.write("Watch a 3D city grow with lakes, hills, and evolving blocks")
|
8 |
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
<!DOCTYPE html>
|
11 |
<html>
|
12 |
<head>
|
13 |
<meta charset="utf-8">
|
14 |
<title>3D City Evolution Simulator</title>
|
15 |
<style>
|
16 |
-
body { margin: 0; overflow: hidden; }
|
17 |
-
#container { width:
|
18 |
-
canvas { width: 100%; height: 100%; display: block; }
|
19 |
-
.ui-panel {
|
20 |
position: absolute;
|
21 |
top: 10px;
|
22 |
right: 10px;
|
@@ -26,8 +36,8 @@ html_code = """
|
|
26 |
color: white;
|
27 |
font-family: Arial, sans-serif;
|
28 |
z-index: 1000;
|
29 |
-
}
|
30 |
-
.ui-panel button {
|
31 |
margin: 5px 0;
|
32 |
padding: 5px 10px;
|
33 |
width: 100%;
|
@@ -36,8 +46,8 @@ html_code = """
|
|
36 |
border: none;
|
37 |
border-radius: 3px;
|
38 |
cursor: pointer;
|
39 |
-
}
|
40 |
-
.ui-panel button:hover { background: #45a049; }
|
41 |
</style>
|
42 |
</head>
|
43 |
<body>
|
@@ -56,37 +66,37 @@ html_code = """
|
|
56 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
|
57 |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
|
58 |
<script>
|
59 |
-
class BuildingLSystem {
|
60 |
-
constructor() {
|
61 |
this.axiom = "F";
|
62 |
-
this.rules = {
|
63 |
"F": ["F[+F]", "F[-F]", "FF", "F"],
|
64 |
"+": ["+"],
|
65 |
"-": ["-"],
|
66 |
"[": ["["],
|
67 |
"]": ["]"]
|
68 |
-
};
|
69 |
this.angle = Math.PI / 6;
|
70 |
-
}
|
71 |
|
72 |
-
generate() {
|
73 |
let result = this.axiom;
|
74 |
-
for (let i = 0; i < 2; i++) {
|
75 |
let newString = "";
|
76 |
-
for (let char of result) {
|
77 |
-
if (this.rules[char]) {
|
78 |
const possible = this.rules[char];
|
79 |
newString += possible[Math.floor(Math.random() * possible.length)];
|
80 |
-
} else {
|
81 |
newString += char;
|
82 |
-
}
|
83 |
-
}
|
84 |
result = newString;
|
85 |
-
}
|
86 |
return result;
|
87 |
-
}
|
88 |
|
89 |
-
build(scene, basePos, maxHeight) {
|
90 |
let height = 0;
|
91 |
const stack = [];
|
92 |
let position = basePos.clone();
|
@@ -94,24 +104,24 @@ html_code = """
|
|
94 |
const structure = new THREE.Group();
|
95 |
let baseWidth = 1.5; // Starting wider base
|
96 |
|
97 |
-
for (let char of this.generate()) {
|
98 |
-
switch(char) {
|
99 |
case 'F':
|
100 |
-
if (height < maxHeight) {
|
101 |
const width = baseWidth * (1 - height / maxHeight); // Narrower as it goes up
|
102 |
const floorHeight = 2 + Math.random() * 2; // Taller floors
|
103 |
const geo = new THREE.BoxGeometry(width, floorHeight, width);
|
104 |
-
const mat = new THREE.MeshPhongMaterial({
|
105 |
color: new THREE.Color(0.5 + Math.random() * 0.5,
|
106 |
0.5 + Math.random() * 0.5,
|
107 |
0.5 + Math.random() * 0.5)
|
108 |
-
});
|
109 |
const floor = new THREE.Mesh(geo, mat);
|
110 |
floor.position.copy(position).add(new THREE.Vector3(0, floorHeight/2, 0));
|
111 |
structure.add(floor);
|
112 |
position.y += floorHeight;
|
113 |
height += floorHeight;
|
114 |
-
}
|
115 |
break;
|
116 |
case '+':
|
117 |
direction.applyAxisAngle(new THREE.Vector3(0, 0, 1), this.angle);
|
@@ -125,14 +135,14 @@ html_code = """
|
|
125 |
case ']':
|
126 |
if (stack.length > 0) position = stack.pop();
|
127 |
break;
|
128 |
-
}
|
129 |
-
}
|
130 |
return structure;
|
131 |
-
}
|
132 |
-
}
|
133 |
|
134 |
-
class CitySimulator {
|
135 |
-
constructor() {
|
136 |
this.blocks = [];
|
137 |
this.blockSize = 10;
|
138 |
this.maxBuildingsPerBlock = 5;
|
@@ -141,26 +151,26 @@ html_code = """
|
|
141 |
new THREE.Vector2(20, 20),
|
142 |
new THREE.Vector2(-30, 10)
|
143 |
];
|
144 |
-
}
|
145 |
|
146 |
-
addBlock(scene, x, z) {
|
147 |
-
const block = {
|
148 |
position: new THREE.Vector2(x, z),
|
149 |
buildings: [],
|
150 |
-
maxHeight: this.isWaterfront(x, z) ? 20 : 12
|
151 |
-
};
|
152 |
this.blocks.push(block);
|
153 |
this.evolveBlock(scene, block, true);
|
154 |
-
}
|
155 |
|
156 |
-
isWaterfront(x, z) {
|
157 |
const pos = new THREE.Vector2(x, z);
|
158 |
return this.lakeCenters.some(center =>
|
159 |
pos.distanceTo(center) < 15 && pos.distanceTo(center) > 5);
|
160 |
-
}
|
161 |
|
162 |
-
evolveBlock(scene, block, initial = false) {
|
163 |
-
if (block.buildings.length < this.maxBuildingsPerBlock) {
|
164 |
const lsystem = new BuildingLSystem();
|
165 |
const gridX = Math.floor(Math.random() * 3) - 1;
|
166 |
const gridZ = Math.floor(Math.random() * 3) - 1;
|
@@ -171,63 +181,63 @@ html_code = """
|
|
171 |
);
|
172 |
|
173 |
const building = lsystem.build(scene, basePos, block.maxHeight);
|
174 |
-
if (this.isWaterfront(block.position.x, block.position.y)) {
|
175 |
building.scale.set(1.5, 2, 1.5);
|
176 |
-
}
|
177 |
scene.add(building);
|
178 |
block.buildings.push(building);
|
179 |
-
}
|
180 |
-
}
|
181 |
|
182 |
-
evolve(scene) {
|
183 |
this.generation++;
|
184 |
-
if (this.blocks.length < 20) {
|
185 |
-
const x = (Math.random() - 0.5) *
|
186 |
-
const z = (Math.random() - 0.5) *
|
187 |
if (!this.isInLake(x, z)) this.addBlock(scene, x, z);
|
188 |
-
}
|
189 |
this.blocks.forEach(block => this.evolveBlock(scene, block));
|
190 |
this.updateStats();
|
191 |
-
}
|
192 |
|
193 |
-
getTerrainHeight(x, z) {
|
194 |
return Math.sin(x * 0.05) * Math.cos(z * 0.05) * 5;
|
195 |
-
}
|
196 |
|
197 |
-
isInLake(x, z) {
|
198 |
const pos = new THREE.Vector2(x, z);
|
199 |
return this.lakeCenters.some(center => pos.distanceTo(center) < 10);
|
200 |
-
}
|
201 |
|
202 |
-
updateStats() {
|
203 |
const totalBuildings = this.blocks.reduce((sum, block) => sum + block.buildings.length, 0);
|
204 |
document.getElementById('building-count').textContent = totalBuildings;
|
205 |
document.getElementById('block-count').textContent = this.blocks.length;
|
206 |
document.getElementById('generation').textContent = this.generation;
|
207 |
-
}
|
208 |
-
}
|
209 |
|
210 |
let scene, camera, renderer, controls;
|
211 |
let city;
|
212 |
|
213 |
-
function init() {
|
214 |
const container = document.getElementById('container');
|
215 |
-
if (!container) {
|
216 |
console.error('Container not found');
|
217 |
return;
|
218 |
-
}
|
219 |
|
220 |
// Scene
|
221 |
scene = new THREE.Scene();
|
222 |
scene.background = new THREE.Color(0x87CEEB);
|
223 |
|
224 |
-
// Camera with
|
225 |
-
camera = new THREE.PerspectiveCamera(75,
|
226 |
-
camera.position.set(0,
|
227 |
|
228 |
// Renderer
|
229 |
-
renderer = new THREE.WebGLRenderer({ antialias: true });
|
230 |
-
renderer.setSize(
|
231 |
container.appendChild(renderer.domElement);
|
232 |
|
233 |
// Lights
|
@@ -237,9 +247,9 @@ html_code = """
|
|
237 |
sun.position.set(50, 50, 50);
|
238 |
scene.add(sun);
|
239 |
|
240 |
-
// Ground with
|
241 |
-
const groundGeo = new THREE.PlaneGeometry(90,
|
242 |
-
const groundMat = new THREE.MeshPhongMaterial({ color: 0x4a7043 });
|
243 |
const ground = new THREE.Mesh(groundGeo, groundMat);
|
244 |
ground.rotation.x = -Math.PI / 2;
|
245 |
ground.position.y = -0.1;
|
@@ -247,18 +257,18 @@ html_code = """
|
|
247 |
|
248 |
// Lakes
|
249 |
const lakeGeo = new THREE.CircleGeometry(10, 32);
|
250 |
-
const lakeMat = new THREE.MeshPhongMaterial({ color: 0x4682b4 });
|
251 |
const lakeCenters = [new THREE.Vector2(20, 20), new THREE.Vector2(-30, 10)];
|
252 |
-
lakeCenters.forEach(center => {
|
253 |
const lake = new THREE.Mesh(lakeGeo, lakeMat);
|
254 |
lake.rotation.x = -Math.PI / 2;
|
255 |
lake.position.set(center.x, 0.01, center.y);
|
256 |
scene.add(lake);
|
257 |
-
});
|
258 |
|
259 |
// Bridge
|
260 |
const bridgeGeo = new THREE.BoxGeometry(5, 0.2, 15);
|
261 |
-
const bridgeMat = new THREE.MeshPhongMaterial({ color: 0x808080 });
|
262 |
const bridge = new THREE.Mesh(bridgeGeo, bridgeMat);
|
263 |
bridge.position.set(15, 0.2, 20);
|
264 |
scene.add(bridge);
|
@@ -278,27 +288,27 @@ html_code = """
|
|
278 |
document.getElementById('reset').addEventListener('click', resetView);
|
279 |
|
280 |
animate();
|
281 |
-
}
|
282 |
|
283 |
-
function resetView() {
|
284 |
-
camera.position.set(0,
|
285 |
controls.target.set(0, 0, 0);
|
286 |
controls.update();
|
287 |
-
}
|
288 |
|
289 |
-
function onWindowResize() {
|
290 |
-
const width =
|
291 |
-
const height =
|
292 |
-
camera.aspect =
|
293 |
camera.updateProjectionMatrix();
|
294 |
renderer.setSize(width, height);
|
295 |
-
}
|
296 |
|
297 |
-
function animate() {
|
298 |
requestAnimationFrame(animate);
|
299 |
controls.update();
|
300 |
renderer.render(scene, camera);
|
301 |
-
}
|
302 |
|
303 |
window.onload = init;
|
304 |
</script>
|
@@ -306,8 +316,8 @@ html_code = """
|
|
306 |
</html>
|
307 |
"""
|
308 |
|
309 |
-
# Render the HTML component
|
310 |
-
components.html(html_code, height=
|
311 |
|
312 |
st.sidebar.title("3D City Evolution Simulator")
|
313 |
st.sidebar.write("""
|
@@ -321,9 +331,10 @@ Watch a 3D city evolve with lakes, hills, and building blocks.
|
|
321 |
- **Left-click + drag**: Rotate
|
322 |
- **Right-click + drag**: Pan
|
323 |
- **Scroll**: Zoom
|
|
|
324 |
|
325 |
### Features:
|
326 |
-
-
|
327 |
- Blocks (10x10 units) with up to 5 buildings
|
328 |
- Buildings start wide, grow taller with smaller floors
|
329 |
- Terrain with hills and lakes
|
|
|
6 |
st.title("3D City Evolution Simulator")
|
7 |
st.write("Watch a 3D city grow with lakes, hills, and evolving blocks")
|
8 |
|
9 |
+
# Sliders for container size with initial 3:4 aspect ratio
|
10 |
+
max_width = min(1200, st.session_state.get('window_width', 1200)) # Use a reasonable max or screen width
|
11 |
+
max_height = min(1600, st.session_state.get('window_height', 1600)) # Use a reasonable max or screen height
|
12 |
+
|
13 |
+
col1, col2 = st.columns(2)
|
14 |
+
with col1:
|
15 |
+
container_width = st.slider("Container Width (px)", 300, max_width, 768, step=50)
|
16 |
+
with col2:
|
17 |
+
container_height = st.slider("Container Height (px)", 400, max_height, 1024, step=50)
|
18 |
+
|
19 |
+
html_code = f"""
|
20 |
<!DOCTYPE html>
|
21 |
<html>
|
22 |
<head>
|
23 |
<meta charset="utf-8">
|
24 |
<title>3D City Evolution Simulator</title>
|
25 |
<style>
|
26 |
+
body {{ margin: 0; overflow: hidden; }}
|
27 |
+
#container {{ width: {container_width}px; height: {container_height}px; margin: 0 auto; }}
|
28 |
+
canvas {{ width: 100%; height: 100%; display: block; }}
|
29 |
+
.ui-panel {{
|
30 |
position: absolute;
|
31 |
top: 10px;
|
32 |
right: 10px;
|
|
|
36 |
color: white;
|
37 |
font-family: Arial, sans-serif;
|
38 |
z-index: 1000;
|
39 |
+
}}
|
40 |
+
.ui-panel button {{
|
41 |
margin: 5px 0;
|
42 |
padding: 5px 10px;
|
43 |
width: 100%;
|
|
|
46 |
border: none;
|
47 |
border-radius: 3px;
|
48 |
cursor: pointer;
|
49 |
+
}}
|
50 |
+
.ui-panel button:hover {{ background: #45a049; }}
|
51 |
</style>
|
52 |
</head>
|
53 |
<body>
|
|
|
66 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
|
67 |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
|
68 |
<script>
|
69 |
+
class BuildingLSystem {{
|
70 |
+
constructor() {{
|
71 |
this.axiom = "F";
|
72 |
+
this.rules = {{
|
73 |
"F": ["F[+F]", "F[-F]", "FF", "F"],
|
74 |
"+": ["+"],
|
75 |
"-": ["-"],
|
76 |
"[": ["["],
|
77 |
"]": ["]"]
|
78 |
+
}};
|
79 |
this.angle = Math.PI / 6;
|
80 |
+
}}
|
81 |
|
82 |
+
generate() {{
|
83 |
let result = this.axiom;
|
84 |
+
for (let i = 0; i < 2; i++) {{
|
85 |
let newString = "";
|
86 |
+
for (let char of result) {{
|
87 |
+
if (this.rules[char]) {{
|
88 |
const possible = this.rules[char];
|
89 |
newString += possible[Math.floor(Math.random() * possible.length)];
|
90 |
+
}} else {{
|
91 |
newString += char;
|
92 |
+
}}
|
93 |
+
}}
|
94 |
result = newString;
|
95 |
+
}}
|
96 |
return result;
|
97 |
+
}}
|
98 |
|
99 |
+
build(scene, basePos, maxHeight) {{
|
100 |
let height = 0;
|
101 |
const stack = [];
|
102 |
let position = basePos.clone();
|
|
|
104 |
const structure = new THREE.Group();
|
105 |
let baseWidth = 1.5; // Starting wider base
|
106 |
|
107 |
+
for (let char of this.generate()) {{
|
108 |
+
switch(char) {{
|
109 |
case 'F':
|
110 |
+
if (height < maxHeight) {{
|
111 |
const width = baseWidth * (1 - height / maxHeight); // Narrower as it goes up
|
112 |
const floorHeight = 2 + Math.random() * 2; // Taller floors
|
113 |
const geo = new THREE.BoxGeometry(width, floorHeight, width);
|
114 |
+
const mat = new THREE.MeshPhongMaterial({{
|
115 |
color: new THREE.Color(0.5 + Math.random() * 0.5,
|
116 |
0.5 + Math.random() * 0.5,
|
117 |
0.5 + Math.random() * 0.5)
|
118 |
+
}});
|
119 |
const floor = new THREE.Mesh(geo, mat);
|
120 |
floor.position.copy(position).add(new THREE.Vector3(0, floorHeight/2, 0));
|
121 |
structure.add(floor);
|
122 |
position.y += floorHeight;
|
123 |
height += floorHeight;
|
124 |
+
}}
|
125 |
break;
|
126 |
case '+':
|
127 |
direction.applyAxisAngle(new THREE.Vector3(0, 0, 1), this.angle);
|
|
|
135 |
case ']':
|
136 |
if (stack.length > 0) position = stack.pop();
|
137 |
break;
|
138 |
+
}}
|
139 |
+
}}
|
140 |
return structure;
|
141 |
+
}}
|
142 |
+
}}
|
143 |
|
144 |
+
class CitySimulator {{
|
145 |
+
constructor() {{
|
146 |
this.blocks = [];
|
147 |
this.blockSize = 10;
|
148 |
this.maxBuildingsPerBlock = 5;
|
|
|
151 |
new THREE.Vector2(20, 20),
|
152 |
new THREE.Vector2(-30, 10)
|
153 |
];
|
154 |
+
}}
|
155 |
|
156 |
+
addBlock(scene, x, z) {{
|
157 |
+
const block = {{
|
158 |
position: new THREE.Vector2(x, z),
|
159 |
buildings: [],
|
160 |
+
maxHeight: this.isWaterfront(x, z) ? 20 : 12
|
161 |
+
}};
|
162 |
this.blocks.push(block);
|
163 |
this.evolveBlock(scene, block, true);
|
164 |
+
}}
|
165 |
|
166 |
+
isWaterfront(x, z) {{
|
167 |
const pos = new THREE.Vector2(x, z);
|
168 |
return this.lakeCenters.some(center =>
|
169 |
pos.distanceTo(center) < 15 && pos.distanceTo(center) > 5);
|
170 |
+
}}
|
171 |
|
172 |
+
evolveBlock(scene, block, initial = false) {{
|
173 |
+
if (block.buildings.length < this.maxBuildingsPerBlock) {{
|
174 |
const lsystem = new BuildingLSystem();
|
175 |
const gridX = Math.floor(Math.random() * 3) - 1;
|
176 |
const gridZ = Math.floor(Math.random() * 3) - 1;
|
|
|
181 |
);
|
182 |
|
183 |
const building = lsystem.build(scene, basePos, block.maxHeight);
|
184 |
+
if (this.isWaterfront(block.position.x, block.position.y)) {{
|
185 |
building.scale.set(1.5, 2, 1.5);
|
186 |
+
}}
|
187 |
scene.add(building);
|
188 |
block.buildings.push(building);
|
189 |
+
}}
|
190 |
+
}}
|
191 |
|
192 |
+
evolve(scene) {{
|
193 |
this.generation++;
|
194 |
+
if (this.blocks.length < 20) {{
|
195 |
+
const x = (Math.random() - 0.5) * 90; // Adjusted for 3:4
|
196 |
+
const z = (Math.random() - 0.5) * 120;
|
197 |
if (!this.isInLake(x, z)) this.addBlock(scene, x, z);
|
198 |
+
}}
|
199 |
this.blocks.forEach(block => this.evolveBlock(scene, block));
|
200 |
this.updateStats();
|
201 |
+
}}
|
202 |
|
203 |
+
getTerrainHeight(x, z) {{
|
204 |
return Math.sin(x * 0.05) * Math.cos(z * 0.05) * 5;
|
205 |
+
}}
|
206 |
|
207 |
+
isInLake(x, z) {{
|
208 |
const pos = new THREE.Vector2(x, z);
|
209 |
return this.lakeCenters.some(center => pos.distanceTo(center) < 10);
|
210 |
+
}}
|
211 |
|
212 |
+
updateStats() {{
|
213 |
const totalBuildings = this.blocks.reduce((sum, block) => sum + block.buildings.length, 0);
|
214 |
document.getElementById('building-count').textContent = totalBuildings;
|
215 |
document.getElementById('block-count').textContent = this.blocks.length;
|
216 |
document.getElementById('generation').textContent = this.generation;
|
217 |
+
}}
|
218 |
+
}}
|
219 |
|
220 |
let scene, camera, renderer, controls;
|
221 |
let city;
|
222 |
|
223 |
+
function init() {{
|
224 |
const container = document.getElementById('container');
|
225 |
+
if (!container) {{
|
226 |
console.error('Container not found');
|
227 |
return;
|
228 |
+
}}
|
229 |
|
230 |
// Scene
|
231 |
scene = new THREE.Scene();
|
232 |
scene.background = new THREE.Color(0x87CEEB);
|
233 |
|
234 |
+
// Camera with 3:4 aspect ratio
|
235 |
+
camera = new THREE.PerspectiveCamera(75, 3 / 4, 0.1, 1000);
|
236 |
+
camera.position.set(0, 50, 60);
|
237 |
|
238 |
// Renderer
|
239 |
+
renderer = new THREE.WebGLRenderer({{ antialias: true }});
|
240 |
+
renderer.setSize({container_width}, {container_height});
|
241 |
container.appendChild(renderer.domElement);
|
242 |
|
243 |
// Lights
|
|
|
247 |
sun.position.set(50, 50, 50);
|
248 |
scene.add(sun);
|
249 |
|
250 |
+
// Ground with 3:4 ratio (90x120)
|
251 |
+
const groundGeo = new THREE.PlaneGeometry(90, 120, 32, 32);
|
252 |
+
const groundMat = new THREE.MeshPhongMaterial({{ color: 0x4a7043 }});
|
253 |
const ground = new THREE.Mesh(groundGeo, groundMat);
|
254 |
ground.rotation.x = -Math.PI / 2;
|
255 |
ground.position.y = -0.1;
|
|
|
257 |
|
258 |
// Lakes
|
259 |
const lakeGeo = new THREE.CircleGeometry(10, 32);
|
260 |
+
const lakeMat = new THREE.MeshPhongMaterial({{ color: 0x4682b4 }});
|
261 |
const lakeCenters = [new THREE.Vector2(20, 20), new THREE.Vector2(-30, 10)];
|
262 |
+
lakeCenters.forEach(center => {{
|
263 |
const lake = new THREE.Mesh(lakeGeo, lakeMat);
|
264 |
lake.rotation.x = -Math.PI / 2;
|
265 |
lake.position.set(center.x, 0.01, center.y);
|
266 |
scene.add(lake);
|
267 |
+
}});
|
268 |
|
269 |
// Bridge
|
270 |
const bridgeGeo = new THREE.BoxGeometry(5, 0.2, 15);
|
271 |
+
const bridgeMat = new THREE.MeshPhongMaterial({{ color: 0x808080 }});
|
272 |
const bridge = new THREE.Mesh(bridgeGeo, bridgeMat);
|
273 |
bridge.position.set(15, 0.2, 20);
|
274 |
scene.add(bridge);
|
|
|
288 |
document.getElementById('reset').addEventListener('click', resetView);
|
289 |
|
290 |
animate();
|
291 |
+
}}
|
292 |
|
293 |
+
function resetView() {{
|
294 |
+
camera.position.set(0, 50, 60);
|
295 |
controls.target.set(0, 0, 0);
|
296 |
controls.update();
|
297 |
+
}}
|
298 |
|
299 |
+
function onWindowResize() {{
|
300 |
+
const width = {container_width};
|
301 |
+
const height = {container_height};
|
302 |
+
camera.aspect = 3 / 4;
|
303 |
camera.updateProjectionMatrix();
|
304 |
renderer.setSize(width, height);
|
305 |
+
}}
|
306 |
|
307 |
+
function animate() {{
|
308 |
requestAnimationFrame(animate);
|
309 |
controls.update();
|
310 |
renderer.render(scene, camera);
|
311 |
+
}}
|
312 |
|
313 |
window.onload = init;
|
314 |
</script>
|
|
|
316 |
</html>
|
317 |
"""
|
318 |
|
319 |
+
# Render the HTML component with dynamic size
|
320 |
+
components.html(html_code, width=container_width, height=container_height)
|
321 |
|
322 |
st.sidebar.title("3D City Evolution Simulator")
|
323 |
st.sidebar.write("""
|
|
|
331 |
- **Left-click + drag**: Rotate
|
332 |
- **Right-click + drag**: Pan
|
333 |
- **Scroll**: Zoom
|
334 |
+
- **Sliders**: Adjust play area size
|
335 |
|
336 |
### Features:
|
337 |
+
- 3:4 initial play area (768x1024)
|
338 |
- Blocks (10x10 units) with up to 5 buildings
|
339 |
- Buildings start wide, grow taller with smaller floors
|
340 |
- Terrain with hills and lakes
|