HANSOL commited on
Commit
2be4f60
ยท
1 Parent(s): d7747e3

v4.3: Multi-Page structure + 5 new landforms (uvala, tower_karst, karren, transverse_dune, star_dune)

Browse files
app.py CHANGED
@@ -1,14 +1,102 @@
1
- # Geo-Lab AI - ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ
2
- # HuggingFace Spaces Entry Point
 
 
 
3
 
4
- import sys
5
- import os
 
 
 
6
 
7
- # Add parent directory to path
8
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
 
 
 
 
 
 
 
9
 
10
- # Import and run main app
11
- from app.main import main
12
 
13
- if __name__ == "__main__":
14
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๐ŸŒ Geo-Lab AI - ํ™ˆ
3
+ HuggingFace Spaces Entry Point (Multi-Page Streamlit)
4
+ """
5
+ import streamlit as st
6
 
7
+ st.set_page_config(
8
+ page_title="๐ŸŒ Geo-Lab AI",
9
+ page_icon="๐ŸŒ",
10
+ layout="wide"
11
+ )
12
 
13
+ # ========== ์ตœ์ƒ๋‹จ: ์ œ์ž‘์ž ์ •๋ณด ==========
14
+ st.markdown("""
15
+ <div style='background: linear-gradient(90deg, #1565C0, #42A5F5); padding: 12px 20px; border-radius: 10px; margin-bottom: 15px;'>
16
+ <div style='display: flex; justify-content: space-between; align-items: center; color: white;'>
17
+ <span style='font-size: 1.1rem;'>๐ŸŒ <b>Geo-Lab AI</b> - ์ด์ƒ์  ์ง€ํ˜• ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ</span>
18
+ <span style='font-size: 0.85rem;'>์ œ์ž‘: 2025 ํ•œ๋ฐฑ๊ณ ๋“ฑํ•™๊ต ๊น€ํ•œ์†”T</span>
19
+ </div>
20
+ </div>
21
+ """, unsafe_allow_html=True)
22
 
23
+ st.title("๐ŸŒ Geo-Lab AI")
24
+ st.subheader("_๊ต์‚ฌ๋ฅผ ์œ„ํ•œ ์ง€ํ˜• ํ˜•์„ฑ๊ณผ์ • ์‹œ๊ฐํ™” ๋„๊ตฌ_")
25
 
26
+ st.markdown("---")
27
+
28
+ # ========== ๊ธฐ๋Šฅ ์†Œ๊ฐœ ==========
29
+ col1, col2, col3 = st.columns(3)
30
+
31
+ with col1:
32
+ st.markdown("""
33
+ ### ๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ
34
+ - 31์ข…+ ๊ต๊ณผ์„œ์  ์ง€ํ˜• ๋ชจ๋ธ
35
+ - 7๊ฐœ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„๋ฅ˜
36
+ - 2D/3D ์‹œ๊ฐํ™”
37
+
38
+ **๐Ÿ‘ˆ ์™ผ์ชฝ ์‚ฌ์ด๋“œ๋ฐ”์—์„œ ํŽ˜์ด์ง€ ์„ ํƒ**
39
+ """)
40
+
41
+ with col2:
42
+ st.markdown("""
43
+ ### ๐ŸŽฌ ํ˜•์„ฑ ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜
44
+ - 0% โ†’ 100% ์Šฌ๋ผ์ด๋”
45
+ - ์‹ค์‹œ๊ฐ„ ์ง€ํ˜• ๋ณ€ํ™” ๊ด€์ฐฐ
46
+ - ๋ฌผ๋ฆฌ ๊ธฐ๋ฐ˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
47
+ """)
48
+
49
+ with col3:
50
+ st.markdown("""
51
+ ### ๐ŸŒ ์ง€ํ˜• ์‹œ๋‚˜๋ฆฌ์˜ค
52
+ - ๋‹ค์ค‘ ์ด๋ก  ๋ชจ๋ธ ๋น„๊ต
53
+ - ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ ˆ
54
+ - ๊ณผํ•™์  ์‹œ๋ฎฌ๋ ˆ์ด์…˜
55
+ """)
56
+
57
+ st.markdown("---")
58
+
59
+ # ========== ์‚ฌ์šฉ๋ฒ• ==========
60
+ st.info("""
61
+ ### ๐Ÿ’ก ์‚ฌ์šฉ๋ฒ•
62
+
63
+ 1. **์™ผ์ชฝ ์‚ฌ์ด๋“œ๋ฐ”**์—์„œ ์›ํ•˜๋Š” ํŽ˜์ด์ง€ ์„ ํƒ
64
+ 2. **๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ** - ๊ต๊ณผ์„œ์  ์ง€ํ˜• ํ™•์ธ
65
+ 3. **๐ŸŒ ์ง€ํ˜• ์‹œ๋‚˜๋ฆฌ์˜ค** - ์ƒ์„ธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹คํ–‰
66
+
67
+ > โš ๏ธ **๊ฐ ํŽ˜์ด์ง€๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค** - ํŽ˜์ด์ง€ ์ด๋™ ์‹œ ์ด์ „ 3D๊ฐ€ ํ•ด์ œ๋˜์–ด ์•ˆ์ •์ ์œผ๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.
68
+ """)
69
+
70
+ # ========== ์ง€์› ์ง€ํ˜• ๋ชฉ๋ก ==========
71
+ with st.expander("๐Ÿ“‹ ์ง€์› ์ง€ํ˜• ๋ชฉ๋ก (36์ข…)", expanded=False):
72
+ st.markdown("""
73
+ | ์นดํ…Œ๊ณ ๋ฆฌ | ์ง€ํ˜• |
74
+ |----------|------|
75
+ | ๐ŸŒŠ ํ•˜์ฒœ | ์„ ์ƒ์ง€, ์ž์œ ๊ณก๋ฅ˜, ๊ฐ์ž…๊ณก๋ฅ˜, V์ž๊ณก, ๋ง์ƒํ•˜์ฒœ, ํญํฌ |
76
+ | ๐Ÿ”บ ์‚ผ๊ฐ์ฃผ | ์ผ๋ฐ˜, ์กฐ์กฑ์ƒ, ํ˜ธ์ƒ, ์ฒจ๋‘์ƒ |
77
+ | โ„๏ธ ๋น™ํ•˜ | U์ž๊ณก, ๊ถŒ๊ณก, ํ˜ธ๋ฅธ, ํ”ผ์˜ค๋ฅด๋“œ, ๋“œ๋Ÿผ๋ฆฐ, ๋น™ํ‡ด์„ |
78
+ | ๐ŸŒ‹ ํ™”์‚ฐ | ์ˆœ์ƒํ™”์‚ฐ, ์„ฑ์ธตํ™”์‚ฐ, ์นผ๋ฐ๋ผ, ํ™”๊ตฌํ˜ธ, ์šฉ์•”๋Œ€์ง€ |
79
+ | ๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ | ๋Œ๋ฆฌ๋„ค, **์šฐ๋ฐœ๋ผ, ํƒ‘์นด๋ฅด์ŠคํŠธ, ์นด๋ Œ** |
80
+ | ๐Ÿœ๏ธ ๊ฑด์กฐ | ๋ฐ”๋ฅดํ•œ, **ํšก์‚ฌ๊ตฌ, ์„ฑ์‚ฌ๊ตฌ**, ๋ฉ”์‚ฌ/๋ทฐํŠธ |
81
+ | ๐Ÿ–๏ธ ํ•ด์•ˆ | ํ•ด์•ˆ์ ˆ๋ฒฝ, ์‚ฌ์ทจ+์„ํ˜ธ, ์œก๊ณ„์‚ฌ์ฃผ, ๋ฆฌ์•„์Šคํ•ด์•ˆ, ํ•ด์‹์•„์น˜, ํ•ด์•ˆ์‚ฌ๊ตฌ |
82
+ """)
83
+
84
+ # ========== ์—…๋ฐ์ดํŠธ ๋‚ด์—ญ ==========
85
+ with st.expander("๐Ÿ“‹ ์—…๋ฐ์ดํŠธ ๋‚ด์—ญ", expanded=False):
86
+ st.markdown("""
87
+ **v4.3 (2025-12-14)** ๐Ÿ†•
88
+ - ์ƒˆ ์ง€ํ˜• ์ถ”๊ฐ€: ์šฐ๋ฐœ๋ผ, ํƒ‘์นด๋ฅด์ŠคํŠธ, ์นด๋ Œ, ํšก์‚ฌ๊ตฌ, ์„ฑ์‚ฌ๊ตฌ
89
+ - ๋ฆฌ์•„์Šค ํ•ด์•ˆ, ํ•ด์‹์•„์น˜ ๊ฐœ์„ 
90
+ - ํ˜•์„ฑ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐœ์„  (ํญํฌ ๋‘๋ถ€์นจ์‹, ํ”ผ์˜ค๋ฅด๋“œ ๋น™ํ•˜โ†’๋ฌผ)
91
+
92
+ **v4.2 (2025-12-14)**
93
+ - Multi-Page ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝ (์•ˆ์ •์„ฑ ํ–ฅ์ƒ)
94
+ - WebGL ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ ๊ฐœ์„ 
95
+
96
+ **v4.1 (2025-12-14)**
97
+ - ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ 31์ข… ์ถ”๊ฐ€
98
+ - ํ˜•์„ฑ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ธฐ๋Šฅ
99
+ """)
100
+
101
+ st.markdown("---")
102
+ st.caption("ยฉ 2025 ํ•œ๋ฐฑ๊ณ ๋“ฑํ•™๊ต ๊น€ํ•œ์†”T | Geo-Lab AI")
app/Home.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๐ŸŒ Geo-Lab AI - ํ™ˆ
3
+ Multi-page Streamlit ์•ฑ์˜ ๋ฉ”์ธ ํŽ˜์ด์ง€
4
+ """
5
+ import streamlit as st
6
+
7
+ st.set_page_config(
8
+ page_title="๐ŸŒ Geo-Lab AI",
9
+ page_icon="๐ŸŒ",
10
+ layout="wide"
11
+ )
12
+
13
+ # ========== ์ตœ์ƒ๋‹จ: ์ œ์ž‘์ž ์ •๋ณด ==========
14
+ st.markdown("""
15
+ <div style='background: linear-gradient(90deg, #1565C0, #42A5F5); padding: 12px 20px; border-radius: 10px; margin-bottom: 15px;'>
16
+ <div style='display: flex; justify-content: space-between; align-items: center; color: white;'>
17
+ <span style='font-size: 1.1rem;'>๐ŸŒ <b>Geo-Lab AI</b> - ์ด์ƒ์  ์ง€ํ˜• ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ</span>
18
+ <span style='font-size: 0.85rem;'>์ œ์ž‘: 2025 ํ•œ๋ฐฑ๊ณ ๋“ฑํ•™๊ต ๊น€ํ•œ์†”T</span>
19
+ </div>
20
+ </div>
21
+ """, unsafe_allow_html=True)
22
+
23
+ st.title("๐ŸŒ Geo-Lab AI")
24
+ st.subheader("_๊ต์‚ฌ๋ฅผ ์œ„ํ•œ ์ง€ํ˜• ํ˜•์„ฑ๊ณผ์ • ์‹œ๊ฐํ™” ๋„๊ตฌ_")
25
+
26
+ st.markdown("---")
27
+
28
+ # ========== ๊ธฐ๋Šฅ ์†Œ๊ฐœ ==========
29
+ col1, col2, col3 = st.columns(3)
30
+
31
+ with col1:
32
+ st.markdown("""
33
+ ### ๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ
34
+ - 31์ข… ๊ต๊ณผ์„œ์  ์ง€ํ˜• ๋ชจ๋ธ
35
+ - 7๊ฐœ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„๋ฅ˜
36
+ - 2D/3D ์‹œ๊ฐํ™”
37
+
38
+ **๐Ÿ‘ˆ ์™ผ์ชฝ ์‚ฌ์ด๋“œ๋ฐ”์—์„œ ํŽ˜์ด์ง€ ์„ ํƒ**
39
+ """)
40
+
41
+ with col2:
42
+ st.markdown("""
43
+ ### ๐ŸŽฌ ํ˜•์„ฑ ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜
44
+ - 0% โ†’ 100% ์Šฌ๋ผ์ด๋”
45
+ - ์‹ค์‹œ๊ฐ„ ์ง€ํ˜• ๋ณ€ํ™” ๊ด€์ฐฐ
46
+ - ๋ฌผ๋ฆฌ ๊ธฐ๋ฐ˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
47
+ """)
48
+
49
+ with col3:
50
+ st.markdown("""
51
+ ### ๐ŸŒ ์ง€ํ˜• ์‹œ๋‚˜๋ฆฌ์˜ค
52
+ - ๋‹ค์ค‘ ์ด๋ก  ๋ชจ๋ธ ๋น„๊ต
53
+ - ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ ˆ
54
+ - ๊ณผํ•™์  ์‹œ๋ฎฌ๋ ˆ์ด์…˜
55
+ """)
56
+
57
+ st.markdown("---")
58
+
59
+ # ========== ์‚ฌ์šฉ๋ฒ• ==========
60
+ st.info("""
61
+ ### ๐Ÿ’ก ์‚ฌ์šฉ๋ฒ•
62
+
63
+ 1. **์™ผ์ชฝ ์‚ฌ์ด๋“œ๋ฐ”**์—์„œ ์›ํ•˜๋Š” ํŽ˜์ด์ง€ ์„ ํƒ
64
+ 2. **๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ** - ๊ต๊ณผ์„œ์  ์ง€ํ˜• ํ™•์ธ
65
+ 3. **๐ŸŒ ์ง€ํ˜• ์‹œ๋‚˜๋ฆฌ์˜ค** - ์ƒ์„ธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹คํ–‰
66
+
67
+ > โš ๏ธ **๊ฐ ํŽ˜์ด์ง€๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค** - ํŽ˜์ด์ง€ ์ด๋™ ์‹œ ์ด์ „ 3D๊ฐ€ ํ•ด์ œ๋˜์–ด ์•ˆ์ •์ ์œผ๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.
68
+ """)
69
+
70
+ # ========== ์ง€์› ์ง€ํ˜• ๋ชฉ๋ก ==========
71
+ with st.expander("๐Ÿ“‹ ์ง€์› ์ง€ํ˜• ๋ชฉ๋ก (31์ข…)", expanded=False):
72
+ st.markdown("""
73
+ | ์นดํ…Œ๊ณ ๋ฆฌ | ์ง€ํ˜• |
74
+ |----------|------|
75
+ | ๐ŸŒŠ ํ•˜์ฒœ | ์„ ์ƒ์ง€, ์ž์œ ๊ณก๋ฅ˜, ๊ฐ์ž…๊ณก๋ฅ˜, V์ž๊ณก, ๋ง์ƒํ•˜์ฒœ, ํญํฌ |
76
+ | ๐Ÿ”บ ์‚ผ๊ฐ์ฃผ | ์ผ๋ฐ˜, ์กฐ์กฑ์ƒ, ํ˜ธ์ƒ, ์ฒจ๋‘์ƒ |
77
+ | โ„๏ธ ๋น™ํ•˜ | U์ž๊ณก, ๊ถŒ๊ณก, ํ˜ธ๋ฅธ, ํ”ผ์˜ค๋ฅด๋“œ, ๋“œ๋Ÿผ๋ฆฐ, ๋น™ํ‡ด์„ |
78
+ | ๐ŸŒ‹ ํ™”์‚ฐ | ์ˆœ์ƒํ™”์‚ฐ, ์„ฑ์ธตํ™”์‚ฐ, ์นผ๋ฐ๋ผ, ํ™”๊ตฌํ˜ธ, ์šฉ์•”๋Œ€์ง€ |
79
+ | ๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ | ๋Œ๋ฆฌ๋„ค |
80
+ | ๐Ÿœ๏ธ ๊ฑด์กฐ | ๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ, ๋ฉ”์‚ฌ/๋ทฐํŠธ |
81
+ | ๐Ÿ–๏ธ ํ•ด์•ˆ | ํ•ด์•ˆ์ ˆ๋ฒฝ, ์‚ฌ์ทจ+์„ํ˜ธ, ์œก๊ณ„์‚ฌ์ฃผ, ๋ฆฌ์•„์Šคํ•ด์•ˆ, ํ•ด์‹์•„์น˜, ํ•ด์•ˆ์‚ฌ๊ตฌ |
82
+ """)
83
+
84
+ # ========== ์—…๋ฐ์ดํŠธ ๋‚ด์—ญ ==========
85
+ with st.expander("๐Ÿ“‹ ์—…๋ฐ์ดํŠธ ๋‚ด์—ญ", expanded=False):
86
+ st.markdown("""
87
+ **v4.2 (2025-12-14)** ๐Ÿ†•
88
+ - Multi-Page ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝ (์•ˆ์ •์„ฑ ํ–ฅ์ƒ)
89
+ - WebGL ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ ๊ฐœ์„ 
90
+
91
+ **v4.1 (2025-12-14)**
92
+ - ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ 31์ข… ์ถ”๊ฐ€
93
+ - ํ˜•์„ฑ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ธฐ๋Šฅ
94
+ - 7๊ฐœ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„๋ฅ˜
95
+
96
+ **v4.0**
97
+ - Project Genesis ํ†ตํ•ฉ ๋ฌผ๋ฆฌ ์—”์ง„
98
+ - ์ง€ํ˜• ์‹œ๋‚˜๋ฆฌ์˜ค ํƒญ
99
+ """)
100
+
101
+ st.markdown("---")
102
+ st.caption("ยฉ 2025 ํ•œ๋ฐฑ๊ณ ๋“ฑํ•™๊ต ๊น€ํ•œ์†”T | Geo-Lab AI")
app/main.py CHANGED
@@ -66,27 +66,6 @@ st.markdown("""
66
  """, unsafe_allow_html=True)
67
 
68
 
69
- # ============ ์ด๋ฏธ์ง€ ์•ˆ์ „ ๋กœ๋“œ ํ—ฌํผ ============
70
- def safe_image(path, caption="", use_column_width=True):
71
- """์ด๋ฏธ์ง€ ํŒŒ์ผ์ด ์—†์–ด๋„ ์—๋Ÿฌ ์—†์ด ์ฒ˜๋ฆฌ"""
72
- if os.path.exists(path):
73
- st.image(path, caption=caption, use_column_width=use_column_width)
74
- else:
75
- st.info(f"๐Ÿ“ท {caption} (์ด๋ฏธ์ง€ ๋ฏธํฌํ•จ)")
76
-
77
-
78
- # ============ ์„œ๋ฒ„์‚ฌ์ด๋“œ 3D ๋ Œ๋”๋ง (WebGL ์—†์ด) ============
79
- def render_3d_as_image(fig_plotly, width=800, height=600):
80
- """Plotly 3D figure๋ฅผ ์„œ๋ฒ„์‚ฌ์ด๋“œ์—์„œ PNG ์ด๋ฏธ์ง€๋กœ ๋ Œ๋”๋ง (WebGL ๋ถˆํ•„์š”)"""
81
- import io
82
- try:
83
- img_bytes = fig_plotly.to_image(format="png", width=width, height=height, engine="kaleido")
84
- return img_bytes
85
- except Exception as e:
86
- st.error(f"3D ๋ Œ๋”๋ง ์˜ค๋ฅ˜: {e}")
87
- return None
88
-
89
-
90
  # ============ ์ด๋ก  ์ •์˜ ============
91
 
92
  V_VALLEY_THEORIES = {
@@ -2547,8 +2526,14 @@ def render_terrain_3d(elevation, title, add_water=True, water_level=0, view_elev
2547
  return fig
2548
 
2549
 
2550
- def render_terrain_plotly(elevation, title, add_water=True, water_level=0, texture_path=None, force_camera=True, water_depth_grid=None, sediment_grid=None):
2551
- """Plotly ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D Surface - ์‚ฌ์‹ค์  ํ…์Šค์ฒ˜(Biome) ๋˜๋Š” ์œ„์„ฑ ์ด๋ฏธ์ง€ ์ ์šฉ"""
 
 
 
 
 
 
2552
  h, w = elevation.shape
2553
  x = np.arange(w)
2554
  y = np.arange(h)
@@ -2581,27 +2566,70 @@ def render_terrain_plotly(elevation, title, add_water=True, water_level=0, textu
2581
  biome[is_deposit] = 0
2582
 
2583
  # ์•”์„ (๊ฒฝ์‚ฌ๊ฐ€ ๊ธ‰ํ•œ ๊ณณ) - ์ ˆ๋ฒฝ
2584
- # ๊ณ ๋„์ฐจ 1.5m/grid ์ด์ƒ์ด๋ฉด ๊ธ‰๊ฒฝ์‚ฌ๋กœ ๊ฐ„์ฃผ (์‹คํ—˜์  ์ˆ˜์น˜)
2585
- biome[slope > 1.2] = 2 # Threshold lowered to show more rock detail
2586
-
2587
- # ๋ˆˆ (๋†’์€ ์‚ฐ) - ๊ณ ๋„ 250m ์ด์ƒ
2588
- biome[elevation > 220] = 3
 
 
 
 
 
 
 
 
 
 
 
 
 
2589
 
2590
  # ์กฐ๊ธˆ ๋” ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ: ๋…ธ์ด์ฆˆ ์ถ”๊ฐ€ (๊ฒฝ๊ณ„๋ฉด ๋ธ”๋ Œ๋”ฉ ํšจ๊ณผ ํ‰๋‚ด)
2591
  noise = np.random.normal(0, 0.2, elevation.shape)
2592
  biome_noisy = np.clip(biome + noise, 0, 3).round(2)
2593
 
2594
  # ์ปค์Šคํ…€ ์ปฌ๋Ÿฌ์Šค์ผ€์ผ (Discrete)
2595
- # 0: Soil/Sand (Yellowish), 1: Grass (Green), 2: Rock (Gray), 3: Snow (White)
2596
- realistic_colorscale = [
2597
- [0.0, '#E6C288'], [0.25, '#E6C288'], # Sand/Soil
2598
- [0.25, '#556B2F'], [0.5, '#556B2F'], # Grass (Darker Green)
2599
- [0.5, '#808080'], [0.75, '#808080'], # Rock (Gray)
2600
- [0.75, '#FFFFFF'], [1.0, '#FFFFFF'] # Snow
2601
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2602
 
2603
  # ์ง€ํ˜• ๋…ธ์ด์ฆˆ (Fractal Roughness) - ์‹œ๊ฐ์  ๋””ํ…Œ์ผ ์ถ”๊ฐ€
2604
- visual_z = (elevation + np.random.normal(0, 0.2, elevation.shape)).round(2) # Reduced noise
2605
 
2606
  # ํ…์Šค์ฒ˜ ๋กœ์ง (์ด๋ฏธ์ง€ ๋งคํ•‘)
2607
  final_surface_color = biome_noisy
@@ -2611,7 +2639,7 @@ def render_terrain_plotly(elevation, title, add_water=True, water_level=0, textu
2611
  final_colorbar = dict(
2612
  title=dict(text="์ง€ํ‘œ ์ƒํƒœ", font=dict(color='white')),
2613
  tickvals=[0.37, 1.12, 1.87, 2.62],
2614
- ticktext=["ํ‡ด์ (ๅœŸ)", "์‹์ƒ(่‰)", "์•”์„(ๅฒฉ)", "๋งŒ๋…„์„ค(้›ช)"],
2615
  tickfont=dict(color='white')
2616
  )
2617
 
@@ -3131,41 +3159,44 @@ def main():
3131
  elevation = np.zeros((gallery_grid_size, gallery_grid_size))
3132
 
3133
  with col_view:
3134
- # 2D/3D ์„ ํƒ (์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์œผ๋กœ WebGL ๋ฌธ์ œ ํ•ด๊ฒฐ)
3135
- view_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["2D ํ‰๋ฉด๋„", "3D ์ž…์ฒด๋„ (์„œ๋ฒ„ ๋ Œ๋”๋ง)"], horizontal=True, key="gallery_view_mode")
 
3136
 
3137
- if view_mode == "2D ํ‰๋ฉด๋„":
3138
- # 2D matplotlib
3139
- import matplotlib.pyplot as plt
3140
- fig_2d, ax = plt.subplots(figsize=(8, 8))
3141
- cmap = plt.cm.terrain
3142
- water_mask = elevation < 0
3143
- im = ax.imshow(elevation, cmap=cmap, origin='upper')
3144
- if water_mask.any():
3145
- water_overlay = np.ma.masked_where(~water_mask, np.ones_like(elevation))
3146
- ax.imshow(water_overlay, cmap='Blues', alpha=0.6, origin='upper')
3147
- ax.set_title(f"{selected_landform}", fontsize=14)
3148
- ax.axis('off')
3149
- plt.colorbar(im, ax=ax, shrink=0.6, label='๊ณ ๋„ (m)')
3150
- st.pyplot(fig_2d)
3151
- plt.close(fig_2d)
3152
- else:
3153
- # 3D ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (WebGL ์‚ฌ์šฉ ์•ˆ ํ•จ!)
3154
- with st.spinner("3D ๋ Œ๋”๋ง ์ค‘..."):
3155
- fig_3d = render_terrain_plotly(
3156
- elevation,
3157
- f"{selected_landform} - 3D",
3158
- add_water=(landform_key in ["delta", "meander", "coastal_cliff", "fjord", "ria_coast", "spit_lagoon"]),
3159
- water_level=0 if landform_key in ["delta", "coastal_cliff"] else -999,
3160
- force_camera=True
3161
- )
3162
- img_bytes = render_3d_as_image(fig_3d, width=800, height=600)
3163
- if img_bytes:
3164
- st.image(img_bytes, caption=f"{selected_landform} - 3D (์„œ๋ฒ„ ๋ Œ๋”๋ง)", use_column_width=True)
3165
- else:
3166
- st.error("3D ๋ Œ๋”๋ง์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.")
3167
 
3168
- st.caption("๐Ÿ’ก 3D ์ž…์ฒด๋„๋Š” ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง๋˜์–ด WebGL ์—†์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3169
 
3170
  # Educational Description
3171
  descriptions = {
@@ -3226,42 +3257,54 @@ def main():
3226
  anim_func = ANIMATED_LANDFORM_GENERATORS[landform_key]
3227
  stage_elev = anim_func(gallery_grid_size, stage_value)
3228
 
3229
- # 2D/3D ํ† ๊ธ€ (์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง)
3230
- anim_view_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["2D ํ‰๋ฉด๋„", "3D ์ž…์ฒด๋„"], horizontal=True, key="anim_view_mode")
 
3231
 
3232
- if anim_view_mode == "2D ํ‰๋ฉด๋„":
3233
- # 2D matplotlib
3234
- fig_2d, ax_2d = plt.subplots(figsize=(10, 8))
3235
- im = ax_2d.imshow(stage_elev, cmap='terrain', origin='upper')
3236
- water_mask = stage_elev < 0
3237
- if water_mask.any():
3238
- water_overlay = np.ma.masked_where(~water_mask, np.ones_like(stage_elev))
3239
- ax_2d.imshow(water_overlay, cmap='Blues', alpha=0.6, origin='upper')
3240
- ax_2d.set_title(f"{selected_landform} - {int(stage_value*100)}%", fontsize=14)
3241
- ax_2d.axis('off')
3242
- plt.colorbar(im, ax=ax_2d, shrink=0.6, label='๊ณ ๋„ (m)')
3243
- st.pyplot(fig_2d)
3244
- plt.close(fig_2d)
3245
- else:
3246
- # 3D ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (WebGL ์‚ฌ์šฉ ์•ˆ ํ•จ!)
3247
- with st.spinner("3D ๋ Œ๋”๋ง ์ค‘..."):
3248
- stage_water = np.maximum(0, -stage_elev + 1.0)
3249
- stage_water[stage_elev > 2] = 0
3250
- fig_3d = render_terrain_plotly(
3251
- stage_elev,
3252
- f"{selected_landform} - {int(stage_value*100)}%",
3253
- add_water=True,
3254
- water_depth_grid=stage_water,
3255
- water_level=-999,
3256
- force_camera=True
3257
- )
3258
- img_bytes = render_3d_as_image(fig_3d, width=800, height=600)
3259
- if img_bytes:
3260
- st.image(img_bytes, caption=f"{selected_landform} - {int(stage_value*100)}% (3D)", use_column_width=True)
3261
- else:
3262
- st.error("3D ๋ Œ๋”๋ง์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.")
3263
 
3264
- st.caption("๐Ÿ’ก ์Šฌ๋ผ์ด๋”๋ฅผ ์กฐ์ ˆํ•˜์—ฌ ํ˜•์„ฑ ๋‹จ๊ณ„๋ฅผ ํ™•์ธํ•˜์„ธ์š”. (0% = ์‹œ์ž‘, 100% = ์™„์„ฑ)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3265
 
3266
  # 3. Scenarios Sub-tabs
3267
  with t_scenarios:
@@ -3337,7 +3380,7 @@ def main():
3337
  fig_step = render_terrain_plotly(r_step['elevation'],
3338
  f"V์ž๊ณก ({t:,}๋…„)",
3339
  add_water=True, water_level=r_step['elevation'].min() + 3,
3340
- texture_path="https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/v_valley_texture.png", force_camera=False, water_depth_grid=r_step.get('water_depth'))
3341
  plot_container.plotly_chart(fig_step, use_container_width=True, key="v_plot_shared")
3342
  anim_prog.progress(min(1.0, t / v_time))
3343
  time.sleep(0.1)
@@ -3358,12 +3401,12 @@ def main():
3358
  result['elevation'],
3359
  f"V์ž๊ณก | ๊นŠ์ด: {result['depth']:.0f}m | {v_time:,}๋…„",
3360
  add_water=True, water_level=result['elevation'].min() + 3,
3361
- texture_path="https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/v_valley_texture.png",
3362
  water_depth_grid=result.get('water_depth')
3363
  )
3364
  plot_container.plotly_chart(plotly_fig, use_container_width=True, key="v_plot_shared")
3365
  else:
3366
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/v_valley_satellite_1765437288622.png",
3367
  caption="V์ž๊ณก - Google Earth ์Šคํƒ€์ผ (AI ์ƒ์„ฑ)",
3368
  use_column_width=True)
3369
 
@@ -3425,7 +3468,7 @@ def main():
3425
  r_step['elevation'],
3426
  f"์ž์œ  ๊ณก๋ฅ˜ ({t:,}๋…„)",
3427
  water_depth_grid=r_step['water_depth'],
3428
- texture_path="https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/meander_texture.png"
3429
  )
3430
  anim_chart.plotly_chart(fig_step, use_container_width=True, key=f"m_anim_{t}")
3431
 
@@ -3440,11 +3483,11 @@ def main():
3440
  result['elevation'],
3441
  f"์ž์œ  ๊ณก๋ฅ˜ - {MEANDER_THEORIES[m_theory].get('description', '')[:20]}...",
3442
  water_depth_grid=result['water_depth'],
3443
- texture_path="https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/meander_texture.png"
3444
  )
3445
  st.plotly_chart(fig, use_container_width=True, key="m_plot")
3446
  else:
3447
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/meander_satellite_1765437309640.png",
3448
  caption="๊ณก๋ฅ˜ ํ•˜์ฒœ - Google Earth ์Šคํƒ€์ผ (AI ์ƒ์„ฑ)",
3449
  use_column_width=True)
3450
 
@@ -3515,7 +3558,7 @@ def main():
3515
  fig_step = render_terrain_plotly(r_step['elevation'],
3516
  f"{r_step['delta_type']} ({t:,}๋…„)",
3517
  add_water=True, water_level=0,
3518
- texture_path="https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/delta_texture.png", force_camera=False)
3519
  plot_container.plotly_chart(fig_step, use_container_width=True, key="d_plot_shared")
3520
  anim_prog.progress(min(1.0, t / d_time))
3521
  # time.sleep(0.1)
@@ -3538,12 +3581,12 @@ def main():
3538
  result['elevation'],
3539
  f"{result['delta_type']} | ๋ฉด์ : {result['area']:.2f} kmยฒ | {d_time:,}๋…„",
3540
  add_water=True, water_level=0,
3541
- texture_path="https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/delta_texture.png",
3542
  water_depth_grid=result.get('water_depth')
3543
  )
3544
  plot_container.plotly_chart(plotly_fig, use_container_width=True, key="d_plot_shared")
3545
  else:
3546
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/delta_satellite_1765437326499.png",
3547
  caption="์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ - Google Earth ์Šคํƒ€์ผ (AI ์ƒ์„ฑ)",
3548
  use_column_width=True)
3549
 
@@ -3615,7 +3658,7 @@ def main():
3615
  plt.close()
3616
  else:
3617
  st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „, ์Šคํฌ๋กค๋กœ ์คŒ**")
3618
- plotly_fig = render_terrain_plotly(result['elevation'], f"์„ ์ƒ์ง€ | ๋ฉด์ : {result['area']:.2f}kmยฒ | {af_time:,}๋…„", add_water=False, texture_path="https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/alluvial_fan_texture.png", water_depth_grid=result.get('water_depth'))
3619
  plot_container.plotly_chart(plotly_fig, use_container_width=True, key="af_plot_shared")
3620
 
3621
  # ํ•˜์•ˆ๋‹จ๊ตฌ
@@ -3769,7 +3812,7 @@ def main():
3769
  plotly_fig = render_terrain_plotly(result['elevation'], f"{result['type']} | ๊นŠ์ด: {result['depth']:.0f}m | {em_time:,}๋…„", add_water=True, water_level=result['elevation'].min()+2, water_depth_grid=result.get('water_depth'))
3770
  plot_container.plotly_chart(plotly_fig, use_container_width=True, key="em_plot_shared")
3771
  else:
3772
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/entrenched_meander_ref_1765496053723.png", caption="๊ฐ์ž… ๊ณก๋ฅ˜ (Entrenched Meander) - AI ์ƒ์„ฑ", use_column_width=True)
3773
 
3774
  # ๋ง์ƒํ•˜์ฒœ
3775
  with river_sub[7]:
@@ -3801,10 +3844,10 @@ def main():
3801
 
3802
  v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="bs_v")
3803
  if "3D" in v_mode:
3804
- fig = render_terrain_plotly(result['elevation'], f"๋ง์ƒํ•˜์ฒœ ({bs_time}๋…„)", add_water=True, water_level=result['elevation'].min()+0.5, texture_path="https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/braided_river_texture.png", water_depth_grid=result.get('water_depth'))
3805
  plot_container.plotly_chart(fig, use_container_width=True, key="bs_plot_shared")
3806
  else:
3807
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/braided_river_1765410638302.png", caption="๋ง์ƒ ํ•˜์ฒœ (AI ์ƒ์„ฑ)", use_column_width=True)
3808
 
3809
  # ํญํฌ
3810
  with river_sub[8]:
@@ -3837,7 +3880,7 @@ def main():
3837
  fig = render_terrain_plotly(result['elevation'], f"ํญํฌ ({wf_time}๋…„)", add_water=True, water_level=90, water_depth_grid=result.get('water_depth'))
3838
  plot_container.plotly_chart(fig, use_container_width=True, key="wf_plot_shared")
3839
  else:
3840
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/waterfall_gorge_formation_1765410495876.png", caption="ํญํฌ ๋ฐ ํ˜‘๊ณก (AI ์ƒ์„ฑ)", use_column_width=True)
3841
 
3842
  # ๋ฒ”๋žŒ์› ์ƒ์„ธ
3843
  with river_sub[9]:
@@ -3870,7 +3913,7 @@ def main():
3870
  fig = render_terrain_plotly(result['elevation'], f"๋ฒ”๋žŒ์› ์ƒ์„ธ ({lv_time}๋…„)", add_water=True, water_level=42, water_depth_grid=result.get('water_depth'))
3871
  plot_container.plotly_chart(fig, use_container_width=True, key="lv_plot_shared")
3872
  else:
3873
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/floodplain_landforms_1765436731483.png", caption="๋ฒ”๋žŒ์› - ์ž์—ฐ์ œ๋ฐฉ๊ณผ ๋ฐฐํ›„์Šต์ง€ (AI ์ƒ์„ฑ)", use_column_width=True)
3874
 
3875
  # ===== ํ•ด์•ˆ ์ง€ํ˜• =====
3876
  with tab_coast:
@@ -4009,9 +4052,9 @@ def main():
4009
  plot_container.plotly_chart(plotly_fig, use_container_width=True, key="co_plot_shared")
4010
  else:
4011
  if theory_key == "cliff_retreat":
4012
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/sea_stack_arch_ref_1765495979396.png", caption="์‹œ์Šคํƒ & ํ•ด์‹์•„์น˜ - AI ์ƒ์„ฑ", use_column_width=True)
4013
  elif theory_key in ["tombolo", "spit"]:
4014
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/tombolo_sandbar_ref_1765495999194.png", caption="์œก๊ณ„๋„ & ์‚ฌ์ทจ - AI ์ƒ์„ฑ", use_column_width=True)
4015
  else:
4016
  st.info("์ด ์ง€ํ˜•์— ๋Œ€ํ•œ ์ฐธ๊ณ  ์‚ฌ์ง„์ด ์•„์ง ์—†์Šต๋‹ˆ๋‹ค.")
4017
 
@@ -4057,7 +4100,7 @@ def main():
4057
  f = render_terrain_plotly(result['elevation'], f"๋Œ๋ฆฌ๋„ค | {ka_time:,}๋…„", add_water=False)
4058
  plot_container.plotly_chart(f, use_container_width=True, key="ka_plot_shared")
4059
  else:
4060
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/doline_sinkhole_1765436375545.png", caption="๋Œ๋ฆฌ๋„ค (AI ์ƒ์„ฑ)", use_column_width=True)
4061
 
4062
  # ํƒ‘ ์นด๋ฅด์ŠคํŠธ
4063
  with ka_subs[1]:
@@ -4090,10 +4133,10 @@ def main():
4090
  plot_container.plotly_chart(f, use_container_width=True, key="tk_plot_shared")
4091
  elif "3D" in v_mode:
4092
  st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „/์คŒ**")
4093
- f = render_terrain_plotly(result['elevation'], f"ํƒ‘ ์นด๋ฅด์ŠคํŠธ | {tk_time:,}๋…„", add_water=False, texture_path="https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/tower_karst_texture.png")
4094
  plot_container.plotly_chart(f, use_container_width=True, key="tk_plot_shared")
4095
  else:
4096
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/tower_karst_ref.png", caption="ํƒ‘ ์นด๋ฅด์ŠคํŠธ (Guilin) - AI ์ƒ์„ฑ", use_column_width=True)
4097
 
4098
  # ์„ํšŒ๋™๊ตด
4099
  with ka_subs[2]:
@@ -4129,7 +4172,7 @@ def main():
4129
  f = render_terrain_plotly(result['elevation'], f"์„ํšŒ๋™๊ตด | {cv_time:,}๋…„", add_water=False)
4130
  plot_container.plotly_chart(f, use_container_width=True, key="cv_plot_shared")
4131
  else:
4132
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/cave_ref.png", caption="์„ํšŒ๋™๊ตด ๋‚ด๋ถ€ - AI ์ƒ์„ฑ", use_column_width=True)
4133
 
4134
  # ===== ํ™”์‚ฐ =====
4135
  with tab_volcano:
@@ -4161,19 +4204,19 @@ def main():
4161
  for _ in range(n_reps):
4162
  for t in range(0, vo_time+1, max(1, vo_time//20)):
4163
  r = simulate_volcanic(VOLCANIC_THEORIES[vo_theory]['key'], t, params, grid_size=grid_size)
4164
- f = render_terrain_plotly(r['elevation'], f"{r['type']} ({t:,}๋…„)", add_water=False, texture_path="https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/volcano_texture.png", force_camera=False)
4165
  plot_container.plotly_chart(f, use_container_width=True, key="vo_plot_shared")
4166
  time.sleep(0.1)
4167
  v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="vo_v")
4168
  if "3D" in v_mode:
4169
- f = render_terrain_plotly(result['elevation'], f"{result['type']} ({vo_time:,}๋…„)", add_water=False, texture_path="https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/volcano_texture.png")
4170
  plot_container.plotly_chart(f, use_container_width=True, key="vo_plot_shared")
4171
  else:
4172
  # ํ™”์‚ฐ ์œ ํ˜•์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ด๋ฏธ์ง€
4173
  if "shield" in VOLCANIC_THEORIES[vo_theory]['key']:
4174
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/shield_vs_stratovolcano_1765436448576.png", caption="์ˆœ์ƒ ํ™”์‚ฐ (AI ์ƒ์„ฑ)", use_column_width=True)
4175
  else:
4176
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/caldera_formation_1765436466778.png", caption="์นผ๋ฐ๋ผ (AI ์ƒ์„ฑ)", use_column_width=True)
4177
 
4178
  # ์šฉ์•” ๋Œ€์ง€
4179
  with vo_subs[1]:
@@ -4209,7 +4252,7 @@ def main():
4209
  f = render_terrain_plotly(result['elevation'], f"์šฉ์•”๋Œ€์ง€ | {lp_time:,}๋…„", add_water=False)
4210
  plot_container.plotly_chart(f, use_container_width=True, key="lp_plot_shared")
4211
  else:
4212
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/lava_plateau_ref.png", caption="์šฉ์•”๋Œ€์ง€ (Iceland) - AI ์ƒ์„ฑ", use_column_width=True)
4213
 
4214
  # ์ฃผ์ƒ์ ˆ๋ฆฌ
4215
  with vo_subs[2]:
@@ -4245,7 +4288,7 @@ def main():
4245
  f = render_terrain_plotly(result['elevation'], f"์ฃผ์ƒ์ ˆ๋ฆฌ | {cj_time:,}๋…„", add_water=True, water_level=80)
4246
  plot_container.plotly_chart(f, use_container_width=True, key="cj_plot_shared")
4247
  else:
4248
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/columnar_ref.png", caption="์ฃผ์ƒ์ ˆ๋ฆฌ (Basalt Columns) - AI ์ƒ์„ฑ", use_column_width=True)
4249
 
4250
  # ===== ๋น™ํ•˜ =====
4251
  with tab_glacial:
@@ -4275,18 +4318,18 @@ def main():
4275
  for _ in range(n_reps):
4276
  for t in range(0, gl_time+1, max(1, gl_time//20)):
4277
  r = simulate_glacial(key, t, {'ice_thickness': gl_ice}, grid_size=grid_size)
4278
- tex_path = "https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/fjord_texture.png" if key == "fjord" else None
4279
  f = render_terrain_plotly(r['elevation'], f"{gl_type} ({t:,}๋…„)", add_water=(key=="fjord"), water_level=100 if key=="fjord" else 0, texture_path=tex_path, force_camera=False)
4280
  plot_container.plotly_chart(f, use_container_width=True, key="gl_plot_shared")
4281
  time.sleep(0.1)
4282
 
4283
- tex_path = "https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/fjord_texture.png" if key == "fjord" else None
4284
  f = render_terrain_plotly(result['elevation'], f"{gl_type} ({gl_time:,}๋…„)", add_water=(key=="fjord"), water_level=100 if key=="fjord" else 0, texture_path=tex_path)
4285
  v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="gl_v")
4286
  if "3D" in v_mode:
4287
  plot_container.plotly_chart(f, use_container_width=True, key="gl_plot_shared")
4288
  else:
4289
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/fjord_valley_ref_1765495963491.png", caption="ํ”ผ์˜ค๋ฅด (Fjord) - AI ์ƒ์„ฑ", use_column_width=True)
4290
 
4291
  # ๊ถŒ๊ณก
4292
  with gl_subs[1]:
@@ -4319,10 +4362,10 @@ def main():
4319
  plot_container.plotly_chart(f, use_container_width=True, key="cq_plot_shared")
4320
  elif "3D" in v_mode:
4321
  st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „/์คŒ**")
4322
- f = render_terrain_plotly(result['elevation'], f"๊ถŒ๊ณก | {cq_time:,}๋…„", add_water=False, texture_path="https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/cirque_texture.png")
4323
  plot_container.plotly_chart(f, use_container_width=True, key="cq_plot_shared")
4324
  else:
4325
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/cirque_ref.png", caption="๊ถŒ๊ณก (Glacial Cirque) - AI ์ƒ์„ฑ", use_column_width=True)
4326
 
4327
  # ๋ชจ๋ ˆ์ธ
4328
  with gl_subs[2]:
@@ -4358,7 +4401,7 @@ def main():
4358
  f = render_terrain_plotly(result['elevation'], f"๋ชจ๋ ˆ์ธ | {mo_time:,}๋…„", add_water=False)
4359
  plot_container.plotly_chart(f, use_container_width=True, key="mo_plot_shared")
4360
  else:
4361
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/moraine_ref.png", caption="๋ชจ๋ ˆ์ธ (Moraine) - AI ์ƒ์„ฑ", use_column_width=True)
4362
 
4363
  # ===== ๊ฑด์กฐ =====
4364
  with tab_arid:
@@ -4419,7 +4462,7 @@ def main():
4419
  # ๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ์ธ ๊ฒฝ์šฐ ํ…์Šค์ฒ˜ ์ ์šฉ
4420
  tex_path = None
4421
  if ARID_THEORIES[ar_theory]['key'] == "barchan":
4422
- tex_path = "https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/barchan_dune_texture_topdown_1765496401371.png"
4423
 
4424
  plotly_fig = render_terrain_plotly(result['elevation'],
4425
  f"{result['type']} | {ar_time:,}๋…„",
@@ -4430,9 +4473,9 @@ def main():
4430
  # ์ด๋ก  ํ‚ค์— ๋”ฐ๋ผ ์ด๋ฏธ์ง€ ๋ถ„๊ธฐ
4431
  tk = ARID_THEORIES[ar_theory]['key']
4432
  if tk == "barchan":
4433
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/barchan_dune_ref_1765496023768.png", caption="๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ - AI ์ƒ์„ฑ", use_column_width=True)
4434
  elif tk == "mesa":
4435
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/mesa_butte_ref_1765496038880.png", caption="๋ฉ”์‚ฌ & ๋ทฐํŠธ - AI ์ƒ์„ฑ", use_column_width=True)
4436
  else:
4437
  st.info("์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค.")
4438
 
@@ -4584,7 +4627,7 @@ for i in range(steps):
4584
  st.session_state['script_grid'] = WorldGrid(100, 100, 10.0)
4585
  st.experimental_rerun()
4586
  else:
4587
- safe_image("https://raw.githubusercontent.com/skyblue3925-svg/geo-lab-images/main/peneplain_erosion_cycle_1765436750353.png", caption="ํ‰์•ผ - ์ค€ํ‰์›ํ™” ๊ณผ์ • (AI ์ƒ์„ฑ)", use_column_width=True)
4588
 
4589
  # ===== Project Genesis (Unified Engine) =====
4590
  with tab_genesis:
 
66
  """, unsafe_allow_html=True)
67
 
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  # ============ ์ด๋ก  ์ •์˜ ============
70
 
71
  V_VALLEY_THEORIES = {
 
2526
  return fig
2527
 
2528
 
2529
+ def render_terrain_plotly(elevation, title, add_water=True, water_level=0, texture_path=None, force_camera=True, water_depth_grid=None, sediment_grid=None, landform_type=None):
2530
+ """Plotly ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D Surface - ์‚ฌ์‹ค์  ํ…์Šค์ฒ˜(Biome) ๋˜๋Š” ์œ„์„ฑ ์ด๋ฏธ์ง€ ์ ์šฉ
2531
+
2532
+ Args:
2533
+ landform_type: 'river', 'coastal', 'glacial', 'volcanic', 'karst', 'arid' ์ค‘ ํ•˜๋‚˜.
2534
+ None์ด๋ฉด ๊ธฐ์กด ๋กœ์ง ์‚ฌ์šฉ.
2535
+ 'glacial'๋งŒ ๋งŒ๋…„์„ค ํ‘œ์‹œ, ๋‚˜๋จธ์ง€๋Š” ๋ฌผ/ํ’€/์•”์„๋งŒ
2536
+ """
2537
  h, w = elevation.shape
2538
  x = np.arange(w)
2539
  y = np.arange(h)
 
2566
  biome[is_deposit] = 0
2567
 
2568
  # ์•”์„ (๊ฒฝ์‚ฌ๊ฐ€ ๊ธ‰ํ•œ ๊ณณ) - ์ ˆ๋ฒฝ
2569
+ biome[slope > 1.2] = 2
2570
+
2571
+ # ์ง€ํ˜• ์œ ํ˜•๋ณ„ ์ฒ˜๋ฆฌ
2572
+ if landform_type == 'glacial':
2573
+ # ๋น™ํ•˜ ์ง€ํ˜•: ์ „์ฒด์ ์œผ๋กœ ๋น™ํ•˜/๋ˆˆ ํ‘œ์‹œ (๋†’์€ ๊ณณ + U์ž๊ณก ๋ฐ”๋‹ฅ๋„)
2574
+ biome[elevation > 50] = 3 # ๋น™ํ•˜ ์ง€ํ˜•์€ ์ „์ฒด์— ๋ˆˆ/๋น™ํ•˜
2575
+ biome[slope > 1.5] = 2 # ๊ฐ€ํŒŒ๋ฅธ ์ ˆ๋ฒฝ๋งŒ ์•”์„
2576
+ elif landform_type in ['river', 'coastal']:
2577
+ # ํ•˜์ฒœ/ํ•ด์•ˆ: ๋ฌผ ์˜์—ญ ๋ช…์‹œ์  ํ‘œ์‹œ (biome=0์„ ๋ฌผ์ƒ‰์œผ๋กœ)
2578
+ if water_depth_grid is not None:
2579
+ is_water = water_depth_grid > 0.5
2580
+ biome[is_water] = 0 # ๋ฌผ ์˜์—ญ
2581
+ # ์Œ์ˆ˜ ๊ณ ๋„ = ๋ฐ”๋‹ค/ํ˜ธ์ˆ˜
2582
+ biome[elevation < 0] = 0
2583
+ elif landform_type == 'arid':
2584
+ # ๊ฑด์กฐ: ์ „์ฒด ๋ชจ๋ž˜์ƒ‰
2585
+ biome[slope < 0.8] = 0 # ํ‰ํƒ„ํ•œ ๊ณณ์€ ๋ชจ๋ž˜
2586
+ # else: ๊ธฐ๋ณธ (ํ™”์‚ฐ, ์นด๋ฅด์ŠคํŠธ) - ๋งŒ๋…„์„ค ์—†์Œ, ํ’€/์•”์„๋งŒ
2587
 
2588
  # ์กฐ๊ธˆ ๋” ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ: ๋…ธ์ด์ฆˆ ์ถ”๊ฐ€ (๊ฒฝ๊ณ„๋ฉด ๋ธ”๋ Œ๋”ฉ ํšจ๊ณผ ํ‰๋‚ด)
2589
  noise = np.random.normal(0, 0.2, elevation.shape)
2590
  biome_noisy = np.clip(biome + noise, 0, 3).round(2)
2591
 
2592
  # ์ปค์Šคํ…€ ์ปฌ๋Ÿฌ์Šค์ผ€์ผ (Discrete)
2593
+ # 0: Soil/Sand (Yellowish), 1: Grass (Green), 2: Rock (Gray), 3: Snow/Water
2594
+ if landform_type == 'glacial':
2595
+ # ๋น™ํ•˜ ์ง€ํ˜•: ๋ˆˆ/๋น™ํ•˜ ํ‘œ์‹œ
2596
+ realistic_colorscale = [
2597
+ [0.0, '#E6C288'], [0.25, '#E6C288'], # Sand/Soil
2598
+ [0.25, '#556B2F'], [0.5, '#556B2F'], # Grass
2599
+ [0.5, '#808080'], [0.75, '#808080'], # Rock
2600
+ [0.75, '#E0FFFF'], [1.0, '#FFFFFF'] # Ice/Snow (๋ฐ์€ ์ฒญ๋ฐฑ์ƒ‰)
2601
+ ]
2602
+ colorbar_labels = ["ํ‡ด์ (ๅœŸ)", "์‹์ƒ(่‰)", "์•”์„(ๅฒฉ)", "๋น™ํ•˜(ๆฐท)"]
2603
+ elif landform_type in ['river', 'coastal']:
2604
+ # ํ•˜์ฒœ/ํ•ด์•ˆ: ๋ฌผ ํ‘œ์‹œ (ํŒŒ๋ž€์ƒ‰)
2605
+ realistic_colorscale = [
2606
+ [0.0, '#4682B4'], [0.25, '#4682B4'], # Water (Steel Blue)
2607
+ [0.25, '#556B2F'], [0.5, '#556B2F'], # Grass
2608
+ [0.5, '#808080'], [0.75, '#808080'], # Rock
2609
+ [0.75, '#D2B48C'], [1.0, '#D2B48C'] # Sand (๋ฐ์€ ๊ฐˆ์ƒ‰)
2610
+ ]
2611
+ colorbar_labels = ["์ˆ˜์—ญ(ๆฐด)", "์‹์ƒ(่‰)", "์•”์„(ๅฒฉ)", "์‚ฌ์งˆ(็ ‚)"]
2612
+ elif landform_type == 'arid':
2613
+ # ๊ฑด์กฐ ์ง€ํ˜•: ์‚ฌ๋ง‰ ํ‘œ์‹œ (๊ฐˆ์ƒ‰/์ฃผํ™ฉ)
2614
+ realistic_colorscale = [
2615
+ [0.0, '#EDC9AF'], [0.25, '#EDC9AF'], # Desert Sand
2616
+ [0.25, '#CD853F'], [0.5, '#CD853F'], # Brown
2617
+ [0.5, '#808080'], [0.75, '#808080'], # Rock
2618
+ [0.75, '#DAA520'], [1.0, '#DAA520'] # Gold Sand
2619
+ ]
2620
+ colorbar_labels = ["์‚ฌ๋ง‰(็ ‚)", "์•”์งˆ(ๅท–)", "์•”์„(ๅฒฉ)", "๋ชจ๋ž˜(ๆฒ™)"]
2621
+ else:
2622
+ # ๊ธฐ๋ณธ (ํ™”์‚ฐ, ์นด๋ฅด์ŠคํŠธ ๋“ฑ)
2623
+ realistic_colorscale = [
2624
+ [0.0, '#E6C288'], [0.25, '#E6C288'],
2625
+ [0.25, '#556B2F'], [0.5, '#556B2F'],
2626
+ [0.5, '#808080'], [0.75, '#808080'],
2627
+ [0.75, '#A0522D'], [1.0, '#A0522D'] # ๊ฐˆ์ƒ‰ (๋งŒ๋…„์„ค ์ œ๊ฑฐ)
2628
+ ]
2629
+ colorbar_labels = ["ํ‡ด์ (ๅœŸ)", "์‹์ƒ(่‰)", "์•”์„(ๅฒฉ)", "ํ‘œํ† (ๅœŸ)"]
2630
 
2631
  # ์ง€ํ˜• ๋…ธ์ด์ฆˆ (Fractal Roughness) - ์‹œ๊ฐ์  ๋””ํ…Œ์ผ ์ถ”๊ฐ€
2632
+ visual_z = (elevation + np.random.normal(0, 0.2, elevation.shape)).round(2)
2633
 
2634
  # ํ…์Šค์ฒ˜ ๋กœ์ง (์ด๋ฏธ์ง€ ๋งคํ•‘)
2635
  final_surface_color = biome_noisy
 
2639
  final_colorbar = dict(
2640
  title=dict(text="์ง€ํ‘œ ์ƒํƒœ", font=dict(color='white')),
2641
  tickvals=[0.37, 1.12, 1.87, 2.62],
2642
+ ticktext=colorbar_labels,
2643
  tickfont=dict(color='white')
2644
  )
2645
 
 
3159
  elevation = np.zeros((gallery_grid_size, gallery_grid_size))
3160
 
3161
  with col_view:
3162
+ # ๊ธฐ๋ณธ: 2D ํ‰๋ฉด๋„ (matplotlib) - WebGL ์ปจํ…์ŠคํŠธ ์‚ฌ์šฉ ์•ˆ ํ•จ
3163
+ import matplotlib.pyplot as plt
3164
+ import matplotlib.colors as mcolors
3165
 
3166
+ fig_2d, ax = plt.subplots(figsize=(8, 8))
3167
+
3168
+ # ์ง€ํ˜• ์ƒ‰์ƒ ๋งต
3169
+ cmap = plt.cm.terrain
3170
+
3171
+ # ๋ฌผ์ด ์žˆ๋Š” ์ง€ํ˜•์€ ํŒŒ๋ž€์ƒ‰ ์˜ค๋ฒ„๋ ˆ์ด
3172
+ water_mask = elevation < 0
3173
+
3174
+ im = ax.imshow(elevation, cmap=cmap, origin='upper')
3175
+
3176
+ # ๋ฌผ ์˜์—ญ ํ‘œ์‹œ
3177
+ if water_mask.any():
3178
+ water_overlay = np.ma.masked_where(~water_mask, np.ones_like(elevation))
3179
+ ax.imshow(water_overlay, cmap='Blues', alpha=0.6, origin='upper')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3180
 
3181
+ ax.set_title(f"{selected_landform}", fontsize=14)
3182
+ ax.axis('off')
3183
+
3184
+ # ์ปฌ๋Ÿฌ๋ฐ”
3185
+ cbar = plt.colorbar(im, ax=ax, shrink=0.6, label='๊ณ ๋„ (m)')
3186
+
3187
+ st.pyplot(fig_2d)
3188
+ plt.close(fig_2d)
3189
+
3190
+ # 3D ๋ณด๊ธฐ (๋ฒ„ํŠผ ํด๋ฆญ ์‹œ์—๋งŒ)
3191
+ if st.button("๐Ÿ”ฒ 3D ๋ทฐ ๋ณด๊ธฐ", key="show_3d_view"):
3192
+ fig_3d = render_terrain_plotly(
3193
+ elevation,
3194
+ f"{selected_landform} - 3D",
3195
+ add_water=(landform_key in ["delta", "meander", "coastal_cliff", "fjord", "ria_coast", "spit_lagoon"]),
3196
+ water_level=0 if landform_key in ["delta", "coastal_cliff"] else -999,
3197
+ force_camera=True
3198
+ )
3199
+ st.plotly_chart(fig_3d, use_container_width=True)
3200
 
3201
  # Educational Description
3202
  descriptions = {
 
3257
  anim_func = ANIMATED_LANDFORM_GENERATORS[landform_key]
3258
  stage_elev = anim_func(gallery_grid_size, stage_value)
3259
 
3260
+ # ๋ฌผ ์ƒ์„ฑ
3261
+ stage_water = np.maximum(0, -stage_elev + 1.0)
3262
+ stage_water[stage_elev > 2] = 0
3263
 
3264
+ # ํŠน์ • ์ง€ํ˜• ๋ฌผ ์ฒ˜๋ฆฌ
3265
+ if landform_key == "alluvial_fan":
3266
+ apex_y = int(gallery_grid_size * 0.15)
3267
+ center = gallery_grid_size // 2
3268
+ for r in range(apex_y + 5):
3269
+ for dc in range(-2, 3):
3270
+ c = center + dc
3271
+ if 0 <= c < gallery_grid_size:
3272
+ stage_water[r, c] = 3.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3273
 
3274
+ # ๋‹จ์ผ 3D ๋ Œ๋”๋ง (WebGL ์ปจํ…์ŠคํŠธ ์ ˆ์•ฝ)
3275
+ fig_stage = render_terrain_plotly(
3276
+ stage_elev,
3277
+ f"{selected_landform} - {int(stage_value*100)}%",
3278
+ add_water=True,
3279
+ water_depth_grid=stage_water,
3280
+ water_level=-999,
3281
+ force_camera=True
3282
+ )
3283
+ st.plotly_chart(fig_stage, use_container_width=True, key="stage_view")
3284
+
3285
+ # ์ž๋™ ์žฌ์ƒ ๋ฒ„ํŠผ
3286
+ if st.button("โ–ถ๏ธ ์ž๋™ ์žฌ์ƒ (0%โ†’100%)", key="auto_play"):
3287
+ stage_container = st.empty()
3288
+ prog = st.progress(0)
3289
+
3290
+ for i in range(11):
3291
+ s = i / 10.0
3292
+ elev = anim_func(gallery_grid_size, s)
3293
+ water = np.maximum(0, -elev + 1.0)
3294
+ water[elev > 2] = 0
3295
+
3296
+ fig = render_terrain_plotly(
3297
+ elev, f"{selected_landform} - {int(s*100)}%",
3298
+ add_water=True, water_depth_grid=water,
3299
+ water_level=-999, force_camera=False
3300
+ )
3301
+ stage_container.plotly_chart(fig, use_container_width=True)
3302
+ prog.progress(s)
3303
+
3304
+ import time
3305
+ time.sleep(0.4)
3306
+
3307
+ st.success("โœ… ์™„๋ฃŒ!")
3308
 
3309
  # 3. Scenarios Sub-tabs
3310
  with t_scenarios:
 
3380
  fig_step = render_terrain_plotly(r_step['elevation'],
3381
  f"V์ž๊ณก ({t:,}๋…„)",
3382
  add_water=True, water_level=r_step['elevation'].min() + 3,
3383
+ texture_path="assets/reference/v_valley_texture.png", force_camera=False, water_depth_grid=r_step.get('water_depth'))
3384
  plot_container.plotly_chart(fig_step, use_container_width=True, key="v_plot_shared")
3385
  anim_prog.progress(min(1.0, t / v_time))
3386
  time.sleep(0.1)
 
3401
  result['elevation'],
3402
  f"V์ž๊ณก | ๊นŠ์ด: {result['depth']:.0f}m | {v_time:,}๋…„",
3403
  add_water=True, water_level=result['elevation'].min() + 3,
3404
+ texture_path="assets/reference/v_valley_texture.png",
3405
  water_depth_grid=result.get('water_depth')
3406
  )
3407
  plot_container.plotly_chart(plotly_fig, use_container_width=True, key="v_plot_shared")
3408
  else:
3409
+ st.image("assets/reference/v_valley_satellite_1765437288622.png",
3410
  caption="V์ž๊ณก - Google Earth ์Šคํƒ€์ผ (AI ์ƒ์„ฑ)",
3411
  use_column_width=True)
3412
 
 
3468
  r_step['elevation'],
3469
  f"์ž์œ  ๊ณก๋ฅ˜ ({t:,}๋…„)",
3470
  water_depth_grid=r_step['water_depth'],
3471
+ texture_path="assets/reference/meander_texture.png"
3472
  )
3473
  anim_chart.plotly_chart(fig_step, use_container_width=True, key=f"m_anim_{t}")
3474
 
 
3483
  result['elevation'],
3484
  f"์ž์œ  ๊ณก๋ฅ˜ - {MEANDER_THEORIES[m_theory].get('description', '')[:20]}...",
3485
  water_depth_grid=result['water_depth'],
3486
+ texture_path="assets/reference/meander_texture.png"
3487
  )
3488
  st.plotly_chart(fig, use_container_width=True, key="m_plot")
3489
  else:
3490
+ st.image("assets/reference/meander_satellite_1765437309640.png",
3491
  caption="๊ณก๋ฅ˜ ํ•˜์ฒœ - Google Earth ์Šคํƒ€์ผ (AI ์ƒ์„ฑ)",
3492
  use_column_width=True)
3493
 
 
3558
  fig_step = render_terrain_plotly(r_step['elevation'],
3559
  f"{r_step['delta_type']} ({t:,}๋…„)",
3560
  add_water=True, water_level=0,
3561
+ texture_path="assets/reference/delta_texture.png", force_camera=False)
3562
  plot_container.plotly_chart(fig_step, use_container_width=True, key="d_plot_shared")
3563
  anim_prog.progress(min(1.0, t / d_time))
3564
  # time.sleep(0.1)
 
3581
  result['elevation'],
3582
  f"{result['delta_type']} | ๋ฉด์ : {result['area']:.2f} kmยฒ | {d_time:,}๋…„",
3583
  add_water=True, water_level=0,
3584
+ texture_path="assets/reference/delta_texture.png",
3585
  water_depth_grid=result.get('water_depth')
3586
  )
3587
  plot_container.plotly_chart(plotly_fig, use_container_width=True, key="d_plot_shared")
3588
  else:
3589
+ st.image("assets/reference/delta_satellite_1765437326499.png",
3590
  caption="์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ - Google Earth ์Šคํƒ€์ผ (AI ์ƒ์„ฑ)",
3591
  use_column_width=True)
3592
 
 
3658
  plt.close()
3659
  else:
3660
  st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „, ์Šคํฌ๋กค๋กœ ์คŒ**")
3661
+ plotly_fig = render_terrain_plotly(result['elevation'], f"์„ ์ƒ์ง€ | ๋ฉด์ : {result['area']:.2f}kmยฒ | {af_time:,}๋…„", add_water=False, texture_path="assets/reference/alluvial_fan_texture.png", water_depth_grid=result.get('water_depth'))
3662
  plot_container.plotly_chart(plotly_fig, use_container_width=True, key="af_plot_shared")
3663
 
3664
  # ํ•˜์•ˆ๋‹จ๊ตฌ
 
3812
  plotly_fig = render_terrain_plotly(result['elevation'], f"{result['type']} | ๊นŠ์ด: {result['depth']:.0f}m | {em_time:,}๋…„", add_water=True, water_level=result['elevation'].min()+2, water_depth_grid=result.get('water_depth'))
3813
  plot_container.plotly_chart(plotly_fig, use_container_width=True, key="em_plot_shared")
3814
  else:
3815
+ st.image("assets/reference/entrenched_meander_ref_1765496053723.png", caption="๊ฐ์ž… ๊ณก๋ฅ˜ (Entrenched Meander) - AI ์ƒ์„ฑ", use_column_width=True)
3816
 
3817
  # ๋ง์ƒํ•˜์ฒœ
3818
  with river_sub[7]:
 
3844
 
3845
  v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="bs_v")
3846
  if "3D" in v_mode:
3847
+ fig = render_terrain_plotly(result['elevation'], f"๋ง์ƒํ•˜์ฒœ ({bs_time}๋…„)", add_water=True, water_level=result['elevation'].min()+0.5, texture_path="assets/reference/braided_river_texture.png", water_depth_grid=result.get('water_depth'))
3848
  plot_container.plotly_chart(fig, use_container_width=True, key="bs_plot_shared")
3849
  else:
3850
+ st.image("assets/reference/braided_river_1765410638302.png", caption="๋ง์ƒ ํ•˜์ฒœ (AI ์ƒ์„ฑ)", use_column_width=True)
3851
 
3852
  # ํญํฌ
3853
  with river_sub[8]:
 
3880
  fig = render_terrain_plotly(result['elevation'], f"ํญํฌ ({wf_time}๋…„)", add_water=True, water_level=90, water_depth_grid=result.get('water_depth'))
3881
  plot_container.plotly_chart(fig, use_container_width=True, key="wf_plot_shared")
3882
  else:
3883
+ st.image("assets/reference/waterfall_gorge_formation_1765410495876.png", caption="ํญํฌ ๋ฐ ํ˜‘๊ณก (AI ์ƒ์„ฑ)", use_column_width=True)
3884
 
3885
  # ๋ฒ”๋žŒ์› ์ƒ์„ธ
3886
  with river_sub[9]:
 
3913
  fig = render_terrain_plotly(result['elevation'], f"๋ฒ”๋žŒ์› ์ƒ์„ธ ({lv_time}๋…„)", add_water=True, water_level=42, water_depth_grid=result.get('water_depth'))
3914
  plot_container.plotly_chart(fig, use_container_width=True, key="lv_plot_shared")
3915
  else:
3916
+ st.image("assets/reference/floodplain_landforms_1765436731483.png", caption="๋ฒ”๋žŒ์› - ์ž์—ฐ์ œ๋ฐฉ๊ณผ ๋ฐฐํ›„์Šต์ง€ (AI ์ƒ์„ฑ)", use_column_width=True)
3917
 
3918
  # ===== ํ•ด์•ˆ ์ง€ํ˜• =====
3919
  with tab_coast:
 
4052
  plot_container.plotly_chart(plotly_fig, use_container_width=True, key="co_plot_shared")
4053
  else:
4054
  if theory_key == "cliff_retreat":
4055
+ st.image("assets/reference/sea_stack_arch_ref_1765495979396.png", caption="์‹œ์Šคํƒ & ํ•ด์‹์•„์น˜ - AI ์ƒ์„ฑ", use_column_width=True)
4056
  elif theory_key in ["tombolo", "spit"]:
4057
+ st.image("assets/reference/tombolo_sandbar_ref_1765495999194.png", caption="์œก๊ณ„๋„ & ์‚ฌ์ทจ - AI ์ƒ์„ฑ", use_column_width=True)
4058
  else:
4059
  st.info("์ด ์ง€ํ˜•์— ๋Œ€ํ•œ ์ฐธ๊ณ  ์‚ฌ์ง„์ด ์•„์ง ์—†์Šต๋‹ˆ๋‹ค.")
4060
 
 
4100
  f = render_terrain_plotly(result['elevation'], f"๋Œ๋ฆฌ๋„ค | {ka_time:,}๋…„", add_water=False)
4101
  plot_container.plotly_chart(f, use_container_width=True, key="ka_plot_shared")
4102
  else:
4103
+ st.image("assets/reference/doline_sinkhole_1765436375545.png", caption="๋Œ๋ฆฌ๋„ค (AI ์ƒ์„ฑ)", use_column_width=True)
4104
 
4105
  # ํƒ‘ ์นด๋ฅด์ŠคํŠธ
4106
  with ka_subs[1]:
 
4133
  plot_container.plotly_chart(f, use_container_width=True, key="tk_plot_shared")
4134
  elif "3D" in v_mode:
4135
  st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „/์คŒ**")
4136
+ f = render_terrain_plotly(result['elevation'], f"ํƒ‘ ์นด๋ฅด์ŠคํŠธ | {tk_time:,}๋…„", add_water=False, texture_path="assets/reference/tower_karst_texture.png")
4137
  plot_container.plotly_chart(f, use_container_width=True, key="tk_plot_shared")
4138
  else:
4139
+ st.image("assets/reference/tower_karst_ref.png", caption="ํƒ‘ ์นด๋ฅด์ŠคํŠธ (Guilin) - AI ์ƒ์„ฑ", use_column_width=True)
4140
 
4141
  # ์„ํšŒ๋™๊ตด
4142
  with ka_subs[2]:
 
4172
  f = render_terrain_plotly(result['elevation'], f"์„ํšŒ๋™๊ตด | {cv_time:,}๋…„", add_water=False)
4173
  plot_container.plotly_chart(f, use_container_width=True, key="cv_plot_shared")
4174
  else:
4175
+ st.image("assets/reference/cave_ref.png", caption="์„ํšŒ๋™๊ตด ๋‚ด๋ถ€ - AI ์ƒ์„ฑ", use_column_width=True)
4176
 
4177
  # ===== ํ™”์‚ฐ =====
4178
  with tab_volcano:
 
4204
  for _ in range(n_reps):
4205
  for t in range(0, vo_time+1, max(1, vo_time//20)):
4206
  r = simulate_volcanic(VOLCANIC_THEORIES[vo_theory]['key'], t, params, grid_size=grid_size)
4207
+ f = render_terrain_plotly(r['elevation'], f"{r['type']} ({t:,}๋…„)", add_water=False, texture_path="assets/reference/volcano_texture.png", force_camera=False)
4208
  plot_container.plotly_chart(f, use_container_width=True, key="vo_plot_shared")
4209
  time.sleep(0.1)
4210
  v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="vo_v")
4211
  if "3D" in v_mode:
4212
+ f = render_terrain_plotly(result['elevation'], f"{result['type']} ({vo_time:,}๋…„)", add_water=False, texture_path="assets/reference/volcano_texture.png")
4213
  plot_container.plotly_chart(f, use_container_width=True, key="vo_plot_shared")
4214
  else:
4215
  # ํ™”์‚ฐ ์œ ํ˜•์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ด๋ฏธ์ง€
4216
  if "shield" in VOLCANIC_THEORIES[vo_theory]['key']:
4217
+ st.image("assets/reference/shield_vs_stratovolcano_1765436448576.png", caption="์ˆœ์ƒ ํ™”์‚ฐ (AI ์ƒ์„ฑ)", use_column_width=True)
4218
  else:
4219
+ st.image("assets/reference/caldera_formation_1765436466778.png", caption="์นผ๋ฐ๋ผ (AI ์ƒ์„ฑ)", use_column_width=True)
4220
 
4221
  # ์šฉ์•” ๋Œ€์ง€
4222
  with vo_subs[1]:
 
4252
  f = render_terrain_plotly(result['elevation'], f"์šฉ์•”๋Œ€์ง€ | {lp_time:,}๋…„", add_water=False)
4253
  plot_container.plotly_chart(f, use_container_width=True, key="lp_plot_shared")
4254
  else:
4255
+ st.image("assets/reference/lava_plateau_ref.png", caption="์šฉ์•”๋Œ€์ง€ (Iceland) - AI ์ƒ์„ฑ", use_column_width=True)
4256
 
4257
  # ์ฃผ์ƒ์ ˆ๋ฆฌ
4258
  with vo_subs[2]:
 
4288
  f = render_terrain_plotly(result['elevation'], f"์ฃผ์ƒ์ ˆ๋ฆฌ | {cj_time:,}๋…„", add_water=True, water_level=80)
4289
  plot_container.plotly_chart(f, use_container_width=True, key="cj_plot_shared")
4290
  else:
4291
+ st.image("assets/reference/columnar_ref.png", caption="์ฃผ์ƒ์ ˆ๋ฆฌ (Basalt Columns) - AI ์ƒ์„ฑ", use_column_width=True)
4292
 
4293
  # ===== ๋น™ํ•˜ =====
4294
  with tab_glacial:
 
4318
  for _ in range(n_reps):
4319
  for t in range(0, gl_time+1, max(1, gl_time//20)):
4320
  r = simulate_glacial(key, t, {'ice_thickness': gl_ice}, grid_size=grid_size)
4321
+ tex_path = "assets/reference/fjord_texture.png" if key == "fjord" else None
4322
  f = render_terrain_plotly(r['elevation'], f"{gl_type} ({t:,}๋…„)", add_water=(key=="fjord"), water_level=100 if key=="fjord" else 0, texture_path=tex_path, force_camera=False)
4323
  plot_container.plotly_chart(f, use_container_width=True, key="gl_plot_shared")
4324
  time.sleep(0.1)
4325
 
4326
+ tex_path = "assets/reference/fjord_texture.png" if key == "fjord" else None
4327
  f = render_terrain_plotly(result['elevation'], f"{gl_type} ({gl_time:,}๋…„)", add_water=(key=="fjord"), water_level=100 if key=="fjord" else 0, texture_path=tex_path)
4328
  v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="gl_v")
4329
  if "3D" in v_mode:
4330
  plot_container.plotly_chart(f, use_container_width=True, key="gl_plot_shared")
4331
  else:
4332
+ st.image("assets/reference/fjord_valley_ref_1765495963491.png", caption="ํ”ผ์˜ค๋ฅด (Fjord) - AI ์ƒ์„ฑ", use_column_width=True)
4333
 
4334
  # ๊ถŒ๊ณก
4335
  with gl_subs[1]:
 
4362
  plot_container.plotly_chart(f, use_container_width=True, key="cq_plot_shared")
4363
  elif "3D" in v_mode:
4364
  st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „/์คŒ**")
4365
+ f = render_terrain_plotly(result['elevation'], f"๊ถŒ๊ณก | {cq_time:,}๋…„", add_water=False, texture_path="assets/reference/cirque_texture.png")
4366
  plot_container.plotly_chart(f, use_container_width=True, key="cq_plot_shared")
4367
  else:
4368
+ st.image("assets/reference/cirque_ref.png", caption="๊ถŒ๊ณก (Glacial Cirque) - AI ์ƒ์„ฑ", use_column_width=True)
4369
 
4370
  # ๋ชจ๋ ˆ์ธ
4371
  with gl_subs[2]:
 
4401
  f = render_terrain_plotly(result['elevation'], f"๋ชจ๋ ˆ์ธ | {mo_time:,}๋…„", add_water=False)
4402
  plot_container.plotly_chart(f, use_container_width=True, key="mo_plot_shared")
4403
  else:
4404
+ st.image("assets/reference/moraine_ref.png", caption="๋ชจ๋ ˆ์ธ (Moraine) - AI ์ƒ์„ฑ", use_column_width=True)
4405
 
4406
  # ===== ๊ฑด์กฐ =====
4407
  with tab_arid:
 
4462
  # ๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ์ธ ๊ฒฝ์šฐ ํ…์Šค์ฒ˜ ์ ์šฉ
4463
  tex_path = None
4464
  if ARID_THEORIES[ar_theory]['key'] == "barchan":
4465
+ tex_path = "assets/reference/barchan_dune_texture_topdown_1765496401371.png"
4466
 
4467
  plotly_fig = render_terrain_plotly(result['elevation'],
4468
  f"{result['type']} | {ar_time:,}๋…„",
 
4473
  # ์ด๋ก  ํ‚ค์— ๋”ฐ๋ผ ์ด๋ฏธ์ง€ ๋ถ„๊ธฐ
4474
  tk = ARID_THEORIES[ar_theory]['key']
4475
  if tk == "barchan":
4476
+ st.image("assets/reference/barchan_dune_ref_1765496023768.png", caption="๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ - AI ์ƒ์„ฑ", use_column_width=True)
4477
  elif tk == "mesa":
4478
+ st.image("assets/reference/mesa_butte_ref_1765496038880.png", caption="๋ฉ”์‚ฌ & ๋ทฐํŠธ - AI ์ƒ์„ฑ", use_column_width=True)
4479
  else:
4480
  st.info("์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค.")
4481
 
 
4627
  st.session_state['script_grid'] = WorldGrid(100, 100, 10.0)
4628
  st.experimental_rerun()
4629
  else:
4630
+ st.image("assets/reference/peneplain_erosion_cycle_1765436750353.png", caption="ํ‰์•ผ - ์ค€ํ‰์›ํ™” ๊ณผ์ • (AI ์ƒ์„ฑ)", use_column_width=True)
4631
 
4632
  # ===== Project Genesis (Unified Engine) =====
4633
  with tab_genesis:
app/pages/1_๐Ÿ“–_Gallery.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ
3
+ 31์ข…์˜ ๊ต๊ณผ์„œ์  ์ง€ํ˜•์„ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
4
+ """
5
+ import streamlit as st
6
+ import numpy as np
7
+ import matplotlib.pyplot as plt
8
+ import sys
9
+ import os
10
+
11
+ # ์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๊ฒฝ๋กœ์— ์ถ”๊ฐ€
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
14
+
15
+ from engine.ideal_landforms import IDEAL_LANDFORM_GENERATORS, ANIMATED_LANDFORM_GENERATORS
16
+ from app.main import render_terrain_plotly
17
+
18
+ st.header("๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ")
19
+ st.markdown("_๊ต๊ณผ์„œ์ ์ธ ์ง€ํ˜• ํ˜•ํƒœ๋ฅผ ๊ธฐํ•˜ํ•™์  ๋ชจ๋ธ๋กœ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค._")
20
+
21
+ # ๊ฐ•์กฐ ๋ฉ”์‹œ์ง€
22
+ st.info("๐Ÿ’ก **Tip:** ์ง€ํ˜• ์„ ํƒ ํ›„ **์•„๋ž˜๋กœ ์Šคํฌ๋กค**ํ•˜๋ฉด **๐ŸŽฌ ํ˜•์„ฑ ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜**์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!")
23
+
24
+ # ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง€ํ˜•
25
+ st.sidebar.subheader("๐Ÿ—‚๏ธ ์ง€ํ˜• ์นดํ…Œ๊ณ ๋ฆฌ")
26
+ category = st.sidebar.radio("์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ", [
27
+ "๐ŸŒŠ ํ•˜์ฒœ ์ง€ํ˜•",
28
+ "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•",
29
+ "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•",
30
+ "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•",
31
+ "๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ ์ง€ํ˜•",
32
+ "๐Ÿœ๏ธ ๊ฑด์กฐ ์ง€ํ˜•",
33
+ "๐Ÿ–๏ธ ํ•ด์•ˆ ์ง€ํ˜•"
34
+ ], key="gallery_cat")
35
+
36
+ # ์นดํ…Œ๊ณ ๋ฆฌ โ†’ landform_type ๋งคํ•‘
37
+ CATEGORY_TO_TYPE = {
38
+ "๐ŸŒŠ ํ•˜์ฒœ ์ง€ํ˜•": "river",
39
+ "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•": "river",
40
+ "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•": "glacial",
41
+ "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•": "volcanic",
42
+ "๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ ์ง€ํ˜•": "karst",
43
+ "๐Ÿœ๏ธ ๊ฑด์กฐ ์ง€ํ˜•": "arid",
44
+ "๐Ÿ–๏ธ ํ•ด์•ˆ ์ง€ํ˜•": "coastal"
45
+ }
46
+ landform_type = CATEGORY_TO_TYPE.get(category, None)
47
+
48
+ # ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์˜ต์…˜
49
+ if category == "๐ŸŒŠ ํ•˜์ฒœ ์ง€ํ˜•":
50
+ landform_options = {
51
+ "๐Ÿ“ ์„ ์ƒ์ง€ (Alluvial Fan)": "alluvial_fan",
52
+ "๐Ÿ ์ž์œ ๊ณก๋ฅ˜ (Free Meander)": "free_meander",
53
+ "โ›ฐ๏ธ ๊ฐ์ž…๊ณก๋ฅ˜+ํ•˜์•ˆ๋‹จ๊ตฌ (Incised Meander)": "incised_meander",
54
+ "๐Ÿ”๏ธ V์ž๊ณก (V-Valley)": "v_valley",
55
+ "๐ŸŒŠ ๋ง์ƒํ•˜์ฒœ (Braided River)": "braided_river",
56
+ "๐Ÿ’ง ํญํฌ (Waterfall)": "waterfall",
57
+ }
58
+ elif category == "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•":
59
+ landform_options = {
60
+ "๐Ÿ”บ ์ผ๋ฐ˜ ์‚ผ๊ฐ์ฃผ (Delta)": "delta",
61
+ "๐Ÿฆถ ์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ (Bird-foot)": "bird_foot_delta",
62
+ "๐ŸŒ™ ํ˜ธ์ƒ ์‚ผ๊ฐ์ฃผ (Arcuate)": "arcuate_delta",
63
+ "๐Ÿ“ ์ฒจ๋‘์ƒ ์‚ผ๊ฐ์ฃผ (Cuspate)": "cuspate_delta",
64
+ }
65
+ elif category == "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•":
66
+ landform_options = {
67
+ "โ„๏ธ U์ž๊ณก (U-Valley)": "u_valley",
68
+ "๐Ÿฅฃ ๊ถŒ๊ณก (Cirque)": "cirque",
69
+ "๐Ÿ”๏ธ ํ˜ธ๋ฅธ (Horn)": "horn",
70
+ "๐ŸŒŠ ํ”ผ์˜ค๋ฅด๋“œ (Fjord)": "fjord",
71
+ "๐Ÿฅš ๋“œ๋Ÿผ๋ฆฐ (Drumlin)": "drumlin",
72
+ "๐Ÿชจ ๋น™ํ‡ด์„ (Moraine)": "moraine",
73
+ }
74
+ elif category == "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•":
75
+ landform_options = {
76
+ "๐Ÿ›ก๏ธ ์ˆœ์ƒํ™”์‚ฐ (Shield)": "shield_volcano",
77
+ "๐Ÿ—ป ์„ฑ์ธตํ™”์‚ฐ (Stratovolcano)": "stratovolcano",
78
+ "๐Ÿ•ณ๏ธ ์นผ๋ฐ๋ผ (Caldera)": "caldera",
79
+ "๐Ÿ’ง ํ™”๊ตฌํ˜ธ (Crater Lake)": "crater_lake",
80
+ "๐ŸŸซ ์šฉ์•”๋Œ€์ง€ (Lava Plateau)": "lava_plateau",
81
+ }
82
+ elif category == "๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ ์ง€ํ˜•":
83
+ landform_options = {
84
+ "๐Ÿ•ณ๏ธ ๋Œ๋ฆฌ๋„ค (Doline)": "karst_doline",
85
+ "๐Ÿฅ‹ ์šฐ๋ฐœ๋ผ (Uvala)": "uvala",
86
+ "๐Ÿ—ผ ํƒ‘์นด๋ฅด์ŠคํŠธ (Tower Karst)": "tower_karst",
87
+ "๐Ÿชจ ์นด๋ Œ (Karren)": "karren",
88
+ }
89
+ elif category == "๐Ÿœ๏ธ ๊ฑด์กฐ ์ง€ํ˜•":
90
+ landform_options = {
91
+ "๐ŸŒ™ ๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ (Barchan)": "barchan",
92
+ "๐ŸŸฐ ํšก์‚ฌ๊ตฌ (Transverse Dune)": "transverse_dune",
93
+ "โญ ์„ฑ์‚ฌ๊ตฌ (Star Dune)": "star_dune",
94
+ "๐Ÿ—ฟ ๋ฉ”์‚ฌ/๋ทฐํŠธ (Mesa/Butte)": "mesa_butte",
95
+ }
96
+ else: # ํ•ด์•ˆ ์ง€ํ˜•
97
+ landform_options = {
98
+ "๐Ÿ–๏ธ ํ•ด์•ˆ ์ ˆ๋ฒฝ (Coastal Cliff)": "coastal_cliff",
99
+ "๐ŸŒŠ ์‚ฌ์ทจ+์„ํ˜ธ (Spit+Lagoon)": "spit_lagoon",
100
+ "๐Ÿ๏ธ ์œก๊ณ„์‚ฌ์ฃผ (Tombolo)": "tombolo",
101
+ "๐ŸŒ€ ๋ฆฌ์•„์Šค ํ•ด์•ˆ (Ria Coast)": "ria_coast",
102
+ "๐ŸŒ‰ ํ•ด์‹์•„์น˜ (Sea Arch)": "sea_arch",
103
+ "๐Ÿ–๏ธ ํ•ด์•ˆ์‚ฌ๊ตฌ (Coastal Dune)": "coastal_dune",
104
+ }
105
+
106
+ col_sel, col_view = st.columns([1, 3])
107
+
108
+ with col_sel:
109
+ selected_landform = st.selectbox("์ง€ํ˜• ์„ ํƒ", list(landform_options.keys()))
110
+ landform_key = landform_options[selected_landform]
111
+
112
+ st.markdown("---")
113
+ st.subheader("โš™๏ธ ํŒŒ๋ผ๋ฏธํ„ฐ")
114
+
115
+ gallery_grid_size = st.slider("ํ•ด์ƒ๋„", 50, 150, 80, 10, key="gallery_res")
116
+
117
+ # ๋™์  ์ง€ํ˜• ์ƒ์„ฑ
118
+ if landform_key in IDEAL_LANDFORM_GENERATORS:
119
+ generator = IDEAL_LANDFORM_GENERATORS[landform_key]
120
+ try:
121
+ elevation = generator(gallery_grid_size)
122
+ except TypeError:
123
+ elevation = generator(gallery_grid_size, 1.0)
124
+ else:
125
+ st.error(f"์ง€ํ˜• '{landform_key}' ์ƒ์„ฑ๊ธฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
126
+ elevation = np.zeros((gallery_grid_size, gallery_grid_size))
127
+
128
+ with col_view:
129
+ # 2D ํ‰๋ฉด๋„
130
+ fig_2d, ax = plt.subplots(figsize=(8, 8))
131
+ cmap = plt.cm.terrain
132
+ water_mask = elevation < 0
133
+
134
+ im = ax.imshow(elevation, cmap=cmap, origin='upper')
135
+
136
+ if water_mask.any():
137
+ water_overlay = np.ma.masked_where(~water_mask, np.ones_like(elevation))
138
+ ax.imshow(water_overlay, cmap='Blues', alpha=0.6, origin='upper')
139
+
140
+ ax.set_title(f"{selected_landform}", fontsize=14)
141
+ ax.axis('off')
142
+ plt.colorbar(im, ax=ax, shrink=0.6, label='๊ณ ๋„ (m)')
143
+
144
+ st.pyplot(fig_2d)
145
+ plt.close(fig_2d)
146
+
147
+ # 3D ๋ณด๊ธฐ ๋ฒ„ํŠผ
148
+ if st.button("๐Ÿ”ฒ 3D ๋ทฐ ๋ณด๊ธฐ", key="show_3d_view"):
149
+ fig_3d = render_terrain_plotly(
150
+ elevation,
151
+ f"{selected_landform} - 3D",
152
+ add_water=(landform_key in ["delta", "meander", "coastal_cliff", "fjord", "ria_coast", "spit_lagoon"]),
153
+ water_level=0 if landform_key in ["delta", "coastal_cliff"] else -999,
154
+ force_camera=True,
155
+ landform_type=landform_type # ์นดํ…Œ๊ณ ๋ฆฌ์— ๋งž๋Š” ์ƒ‰์ƒ ์ ์šฉ
156
+ )
157
+ st.plotly_chart(fig_3d, use_container_width=True, key="gallery_3d")
158
+
159
+ # ์„ค๋ช…
160
+ descriptions = {
161
+ "delta": "**์‚ผ๊ฐ์ฃผ**: ํ•˜์ฒœ์ด ๋ฐ”๋‹ค๋‚˜ ํ˜ธ์ˆ˜์— ์œ ์ž…๋  ๋•Œ ์œ ์†์ด ๊ฐ์†Œํ•˜์—ฌ ์šด๋ฐ˜ ์ค‘์ด๋˜ ํ‡ด์ ๋ฌผ์ด ์Œ“์—ฌ ํ˜•์„ฑ๋ฉ๋‹ˆ๋‹ค.",
162
+ "alluvial_fan": "**์„ ์ƒ์ง€**: ์‚ฐ์ง€์—์„œ ํ‰์ง€๋กœ ๋‚˜์˜ค๋Š” ๊ณณ์—์„œ ๊ฒฝ์‚ฌ๊ฐ€ ๊ธ‰๊ฐํ•˜์—ฌ ์šด๋ฐ˜๋ ฅ์ด ์ค„์–ด๋“ค๋ฉด์„œ ํ‡ด์ ๋ฌผ์ด ๋ถ€์ฑ„๊ผด๋กœ ์Œ“์ž…๋‹ˆ๋‹ค.",
163
+ "free_meander": "**์ž์œ ๊ณก๋ฅ˜**: ๋ฒ”๋žŒ์› ์œ„๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์‚ฌํ–‰ํ•˜๋Š” ๊ณก๋ฅ˜. ์ž์—ฐ์ œ๋ฐฉ(Levee)๊ณผ ๋ฐฐํ›„์Šต์ง€๊ฐ€ ํŠน์ง•์ž…๋‹ˆ๋‹ค.",
164
+ "incised_meander": "**๊ฐ์ž…๊ณก๋ฅ˜**: ์œต๊ธฐ๋กœ ์ธํ•ด ๊ณก๋ฅ˜๊ฐ€ ๊ธฐ๋ฐ˜์•”์„ ํŒŒ๊ณ ๋“ค๋ฉด์„œ ํ˜•์„ฑ. ํ•˜์•ˆ๋‹จ๊ตฌ(River Terrace)๊ฐ€ ํ•จ๊ป˜ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.",
165
+ "v_valley": "**V์ž๊ณก**: ํ•˜์ฒœ์˜ ํ•˜๋ฐฉ ์นจ์‹์ด ์šฐ์„ธํ•˜๊ฒŒ ์ž‘์šฉํ•˜์—ฌ ํ˜•์„ฑ๋œ V์ž ๋‹จ๋ฉด์˜ ๊ณจ์งœ๊ธฐ.",
166
+ "braided_river": "**๋ง์ƒํ•˜์ฒœ**: ํ‡ด์ ๋ฌผ์ด ๋งŽ๊ณ  ๊ฒฝ์‚ฌ๊ฐ€ ๊ธ‰ํ•  ๋•Œ ์—ฌ๋Ÿฌ ์ˆ˜๋กœ๊ฐ€ ๊ฐˆ๋ผ์กŒ๋‹ค ํ•ฉ์ณ์ง€๋Š” ํ•˜์ฒœ.",
167
+ "waterfall": "**ํญํฌ**: ๊ฒฝ์•”๊ณผ ์—ฐ์•”์˜ ์ฐจ๋ณ„์นจ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ๊ธ‰๊ฒฝ์‚ฌ ๋‚™์ฐจ. ํ›„ํ‡ดํ•˜๋ฉฐ ํ˜‘๊ณก ํ˜•์„ฑ.",
168
+ "bird_foot_delta": "**์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ**: ๋ฏธ์‹œ์‹œํ”ผ๊ฐ•ํ˜•. ํŒŒ๋ž‘ ์•ฝํ•˜๊ณ  ํ‡ด์ ๋ฌผ ๊ณต๊ธ‰ ๋งŽ์„ ๋•Œ ์ƒˆ๋ฐœ ๋ชจ์–‘์œผ๋กœ ๊ธธ๊ฒŒ ๋ป—์Šต๋‹ˆ๋‹ค.",
169
+ "arcuate_delta": "**ํ˜ธ์ƒ ์‚ผ๊ฐ์ฃผ**: ๋‚˜์ผ๊ฐ•ํ˜•. ํŒŒ๋ž‘๊ณผ ํ‡ด์ ๋ฌผ ๊ณต๊ธ‰์ด ๊ท ํ˜•์„ ์ด๋ฃจ์–ด ๋ถ€๋“œ๋Ÿฌ์šด ํ˜ธ(Arc) ํ˜•ํƒœ.",
170
+ "cuspate_delta": "**์ฒจ๋‘์ƒ ์‚ผ๊ฐ์ฃผ**: ํ‹ฐ๋ฒ ๋ฅด๊ฐ•ํ˜•. ํŒŒ๋ž‘์ด ๊ฐ•ํ•ด ์‚ผ๊ฐ์ฃผ๊ฐ€ ๋พฐ์กฑํ•œ ํ™”์‚ด์ด‰ ๋ชจ์–‘์œผ๋กœ ํ˜•์„ฑ.",
171
+ "u_valley": "**U์ž๊ณก**: ๋น™ํ•˜์˜ ์นจ์‹์œผ๋กœ ํ˜•์„ฑ๋œ U์ž ๋‹จ๋ฉด์˜ ๊ณจ์งœ๊ธฐ. ์ธก๋ฒฝ์ด ๊ธ‰ํ•˜๊ณ  ๋ฐ”๋‹ฅ์ด ํ‰ํƒ„ํ•ฉ๋‹ˆ๋‹ค.",
172
+ "cirque": "**๊ถŒ๊ณก**: ๋น™ํ•˜์˜ ์‹œ์ž‘์ . ๋ฐ˜์›ํ˜• ์›€ํ‘น ํŒŒ์ธ ์ง€ํ˜•์œผ๋กœ, ๋น™ํ•˜ ์œตํ•ด ํ›„ ํ˜ธ์ˆ˜(Tarn)๊ฐ€ ํ˜•์„ฑ๋ฉ๋‹ˆ๋‹ค.",
173
+ "horn": "**ํ˜ธ๋ฅธ**: ์—ฌ๋Ÿฌ ๊ถŒ๊ณก์ด ๋งŒ๋‚˜๋Š” ๊ณณ์—์„œ ์นจ์‹๋˜์ง€ ์•Š๊ณ  ๋‚จ์€ ๋พฐ์กฑํ•œ ํ”ผ๋ผ๋ฏธ๋“œํ˜• ๋ด‰์šฐ๋ฆฌ.",
174
+ "fjord": "**ํ”ผ์˜ค๋ฅด๋“œ**: ๋น™ํ•˜๊ฐ€ ํŒŒ๋‚ธ U์ž๊ณก์— ๋ฐ”๋‹ค๊ฐ€ ์œ ์ž…๋œ ์ข๊ณ  ๊นŠ์€ ๋งŒ.",
175
+ "drumlin": "**๋“œ๋Ÿผ๋ฆฐ**: ๋น™ํ•˜ ํ‡ด์ ๋ฌผ์ด ๋น™ํ•˜ ํ๋ฆ„ ๋ฐฉํ–ฅ์œผ๋กœ ๊ธธ์ญ‰ํ•˜๊ฒŒ ์Œ“์ธ ํƒ€์›ํ˜• ์–ธ๋•.",
176
+ "moraine": "**๋น™ํ‡ด์„**: ๋น™ํ•˜๊ฐ€ ์šด๋ฐ˜ํ•œ ์•”์„ค์ด ํ‡ด์ ๋œ ์ง€ํ˜•. ์ธกํ‡ด์„, ์ข…ํ‡ด์„ ๋“ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.",
177
+ "shield_volcano": "**์ˆœ์ƒํ™”์‚ฐ**: ์œ ๋™์„ฑ ๋†’์€ ํ˜„๋ฌด์•”์งˆ ์šฉ์•”์ด ์™„๋งŒํ•˜๊ฒŒ ์Œ“์—ฌ ๋ฐฉํŒจ ํ˜•ํƒœ.",
178
+ "stratovolcano": "**์„ฑ์ธตํ™”์‚ฐ**: ์šฉ์•”๊ณผ ํ™”์‚ฐ์‡„์„ค๋ฌผ์ด ๊ต๋Œ€๋กœ ์Œ“์—ฌ ๊ธ‰ํ•œ ์›๋ฟ”ํ˜•.",
179
+ "caldera": "**์นผ๋ฐ๋ผ**: ๋Œ€๊ทœ๋ชจ ๋ถ„ํ™” ํ›„ ๋งˆ๊ทธ๋งˆ๋ฐฉ ํ•จ๋ชฐ๋กœ ํ˜•์„ฑ๋œ ๊ฑฐ๋Œ€ํ•œ ๋ถ„์ง€.",
180
+ "crater_lake": "**ํ™”๊ตฌํ˜ธ**: ํ™”๊ตฌ๋‚˜ ์นผ๋ฐ๋ผ์— ๋ฌผ์ด ๊ณ ์—ฌ ํ˜•์„ฑ๋œ ํ˜ธ์ˆ˜.",
181
+ "lava_plateau": "**์šฉ์•”๋Œ€์ง€**: ์—ด๊ทน ๋ถ„์ถœ๋กœ ํ˜„๋ฌด์•”์งˆ ์šฉ์•”์ด ๋„“๊ฒŒ ํŽผ์ณ์ ธ ํ‰ํƒ„ํ•œ ๋Œ€์ง€ ํ˜•์„ฑ.",
182
+ "barchan": "**๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ**: ๋ฐ”๋žŒ์ด ํ•œ ๋ฐฉํ–ฅ์—์„œ ๋ถˆ ๋•Œ ํ˜•์„ฑ๋˜๋Š” ์ดˆ์Šน๋‹ฌ ๋ชจ์–‘์˜ ์‚ฌ๊ตฌ.",
183
+ "mesa_butte": "**๋ฉ”์‚ฌ/๋ทฐํŠธ**: ์ฐจ๋ณ„์นจ์‹์œผ๋กœ ๋‚จ์€ ํƒ์ƒ์ง€. ๋ฉ”์‚ฌ๋Š” ํฌ๊ณ  ํ‰ํƒ„, ๋ทฐํŠธ๋Š” ์ž‘๊ณ  ๋†’์Šต๋‹ˆ๋‹ค.",
184
+ "karst_doline": "**๋Œ๋ฆฌ๋„ค**: ์„ํšŒ์•” ์šฉ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ์›€ํ‘น ํŒŒ์ธ ์™€์ง€.",
185
+ "coastal_cliff": "**ํ•ด์•ˆ ์ ˆ๋ฒฝ**: ํŒŒ๋ž‘์˜ ์นจ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ์ ˆ๋ฒฝ.",
186
+ "spit_lagoon": "**์‚ฌ์ทจ+์„ํ˜ธ**: ์—ฐ์•ˆ๋ฅ˜์— ์˜ํ•ด ํ‡ด์ ๋ฌผ์ด ๊ธธ๊ฒŒ ์Œ“์ธ ์‚ฌ์ทจ๊ฐ€ ๋งŒ์„ ๋ง‰์•„ ์„ํ˜ธ๋ฅผ ํ˜•์„ฑํ•ฉ๋‹ˆ๋‹ค.",
187
+ "tombolo": "**์œก๊ณ„์‚ฌ์ฃผ**: ์—ฐ์•ˆ๋ฅ˜์— ์˜ํ•œ ํ‡ด์ ์œผ๋กœ ์œก์ง€์™€ ์„ฌ์ด ๋ชจ๋ž˜ํ†ฑ์œผ๋กœ ์—ฐ๊ฒฐ๋œ ์ง€ํ˜•.",
188
+ "ria_coast": "**๋ฆฌ์•„์Šค์‹ ํ•ด์•ˆ**: ๊ณผ๊ฑฐ ํ•˜๊ณก์ด ํ•ด์ˆ˜๋ฉด ์ƒ์Šน์œผ๋กœ ์นจ์ˆ˜๋˜์–ด ํ˜•์„ฑ๋œ ํ†ฑ๋‹ˆ ๋ชจ์–‘ ํ•ด์•ˆ์„ .",
189
+ "sea_arch": "**ํ•ด์‹์•„์น˜**: ๊ณถ์—์„œ ํŒŒ๋ž‘ ์นจ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ์•„์น˜ํ˜• ์ง€ํ˜•.",
190
+ "coastal_dune": "**ํ•ด์•ˆ์‚ฌ๊ตฌ**: ํ•ด๋นˆ์˜ ๋ชจ๋ž˜๊ฐ€ ๋ฐ”๋žŒ์— ์˜ํ•ด ์œก์ง€ ์ชฝ์œผ๋กœ ์šด๋ฐ˜๋˜์–ด ํ˜•์„ฑ๋œ ๋ชจ๋ž˜ ์–ธ๋•.",
191
+ # ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ์ง€ํ˜•
192
+ "uvala": "**์šฐ๋ฐœ๋ผ**: ์—ฌ๋Ÿฌ ๋Œ๋ฆฌ๋„ค๊ฐ€ ํ•ฉ๏ฟฝ๏ฟฝ์ ธ ํ˜•์„ฑ๋œ ๋ณตํ•ฉ ์™€์ง€. ๋Œ๋ฆฌ๋„ค๋ณด๋‹ค ํฌ๊ณ  ๋ถˆ๊ทœ์น™ํ•œ ํ˜•ํƒœ.",
193
+ "tower_karst": "**ํƒ‘์นด๋ฅด์ŠคํŠธ**: ์ˆ˜์ง ์ ˆ๋ฒฝ์„ ๊ฐ€์ง„ ํƒ‘ ๋ชจ์–‘ ์„ํšŒ์•” ๋ด‰์šฐ๋ฆฌ. ์ค‘๊ตญ ๊ตฌ์ด๋ฆฐ์ด ๋Œ€ํ‘œ์ .",
194
+ "karren": "**์นด๋ Œ**: ๋น—๋ฌผ์— ์˜ํ•œ ์šฉ์‹์œผ๋กœ ์„ํšŒ์•” ํ‘œ๋ฉด์— ํ˜•์„ฑ๋œ ํ™ˆ๊ณผ ๋ฆฟ์ง€. ํด๋ฆฐํŠธ/๊ทธ๋ผ์ดํฌ ํฌํ•จ.",
195
+ "transverse_dune": "**ํšก์‚ฌ๊ตฌ**: ๋ฐ”๋žŒ ๋ฐฉํ–ฅ์— ์ˆ˜์ง์œผ๋กœ ๊ธธ๊ฒŒ ํ˜•์„ฑ๋œ ์‚ฌ๊ตฌ์—ด. ๋ชจ๋ž˜ ๊ณต๊ธ‰์ด ํ’๋ถ€ํ•  ๋•Œ ๋ฐœ๋‹ฌ.",
196
+ "star_dune": "**์„ฑ์‚ฌ๊ตฌ**: ๋‹ค๋ฐฉํ–ฅ ๋ฐ”๋žŒ์— ์˜ํ•ด ๋ณ„ ๋ชจ์–‘์œผ๋กœ ํ˜•์„ฑ๋œ ์‚ฌ๊ตฌ. ๋†’์ด๊ฐ€ ๋†’๊ณ  ์ด๋™์ด ์ ์Œ.",
197
+ }
198
+ st.info(descriptions.get(landform_key, "์„ค๋ช… ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค."))
199
+
200
+ # ========== ํ˜•์„ฑ ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜ ==========
201
+ if landform_key in ANIMATED_LANDFORM_GENERATORS:
202
+ st.markdown("---")
203
+ st.subheader("๐ŸŽฌ ํ˜•์„ฑ ๊ณผ์ •")
204
+
205
+ # ์ž๋™ ์žฌ์ƒ ์ค‘์ด๋ฉด session_state์˜ stage ์‚ฌ์šฉ
206
+ if st.session_state.get('auto_playing', False):
207
+ stage_value = st.session_state.get('auto_stage', 0.0)
208
+ st.slider(
209
+ "ํ˜•์„ฑ ๋‹จ๊ณ„ (์ž๋™ ์žฌ์ƒ ์ค‘...)",
210
+ 0.0, 1.0, stage_value, 0.05,
211
+ key="gallery_stage_slider",
212
+ disabled=True
213
+ )
214
+ else:
215
+ stage_value = st.slider(
216
+ "ํ˜•์„ฑ ๋‹จ๊ณ„ (0% = ์‹œ์ž‘, 100% = ์™„์„ฑ)",
217
+ 0.0, 1.0, 1.0, 0.05,
218
+ key="gallery_stage_slider"
219
+ )
220
+
221
+ anim_func = ANIMATED_LANDFORM_GENERATORS[landform_key]
222
+ stage_elev = anim_func(gallery_grid_size, stage_value)
223
+
224
+ # ๋ฌผ ์ƒ์„ฑ
225
+ stage_water = np.maximum(0, -stage_elev + 1.0)
226
+ stage_water[stage_elev > 2] = 0
227
+
228
+ # ์„ ์ƒ์ง€ ๋ฌผ ์ฒ˜๋ฆฌ
229
+ if landform_key == "alluvial_fan":
230
+ apex_y = int(gallery_grid_size * 0.15)
231
+ center = gallery_grid_size // 2
232
+ for r in range(apex_y + 5):
233
+ for dc in range(-2, 3):
234
+ c = center + dc
235
+ if 0 <= c < gallery_grid_size:
236
+ stage_water[r, c] = 3.0
237
+
238
+ # 3D ๋ Œ๋”๋ง
239
+ fig_stage = render_terrain_plotly(
240
+ stage_elev,
241
+ f"{selected_landform} - {int(stage_value*100)}%",
242
+ add_water=True,
243
+ water_depth_grid=stage_water,
244
+ water_level=-999,
245
+ force_camera=False, # ์นด๋ฉ”๋ผ ์ด๋™ ํ—ˆ์šฉ
246
+ landform_type=landform_type
247
+ )
248
+ st.plotly_chart(fig_stage, use_container_width=True, key="stage_view")
249
+
250
+ # ์ž๋™ ์žฌ์ƒ (์„ธ์…˜ ์ƒํƒœ ํ™œ์šฉ)
251
+ col_play, col_step = st.columns(2)
252
+ with col_play:
253
+ if st.button("โ–ถ๏ธ ์ž๋™ ์žฌ์ƒ ์‹œ์ž‘", key="auto_play"):
254
+ st.session_state['auto_playing'] = True
255
+ st.session_state['auto_stage'] = 0.0
256
+ with col_step:
257
+ if st.button("โน๏ธ ์ •์ง€", key="stop_play"):
258
+ st.session_state['auto_playing'] = False
259
+
260
+ # ์ž๋™ ์žฌ์ƒ ์ค‘์ด๋ฉด stage ์ž๋™ ์ฆ๊ฐ€
261
+ if st.session_state.get('auto_playing', False):
262
+ current_stage = st.session_state.get('auto_stage', 0.0)
263
+ if current_stage < 1.0:
264
+ st.session_state['auto_stage'] = current_stage + 0.1
265
+ import time
266
+ time.sleep(0.5)
267
+ st.rerun()
268
+ else:
269
+ st.session_state['auto_playing'] = False
270
+ st.success("โœ… ์™„๋ฃŒ!")
271
+
272
+ st.caption("๐Ÿ’ก **Tip:** ์นด๋ฉ”๋ผ ๊ฐ๋„๋ฅผ ๋จผ์ € ์กฐ์ •ํ•œ ํ›„ ์ž๋™ ์žฌ์ƒํ•˜๋ฉด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.")
engine/ideal_landforms.py CHANGED
@@ -484,58 +484,131 @@ def create_alluvial_fan_animated(grid_size: int, stage: float,
484
 
485
  def create_meander_animated(grid_size: int, stage: float,
486
  amplitude: float = 0.3, num_bends: int = 3) -> np.ndarray:
487
- """๊ณก๋ฅ˜ ํ˜•์„ฑ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜ (์ง์„  -> ์‚ฌํ–‰ -> ์šฐ๊ฐํ˜ธ)"""
 
 
 
 
 
 
488
  h, w = grid_size, grid_size
489
  elevation = np.zeros((h, w))
490
- elevation[:, :] = 10.0 # ๋ฒ”๋žŒ์›
491
 
492
  center_x = w // 2
493
  channel_width = max(3, w // 20)
494
 
495
- # Stage์— ๋”ฐ๋ฅธ ์‚ฌํ–‰ ์ง„ํญ ๋ณ€ํ™” (์ง์„  -> ๊ตฝ์Œ)
496
- current_amp = w * amplitude * stage
497
- wl = h / num_bends
 
 
 
 
498
 
 
499
  for r in range(h):
500
  theta = 2 * np.pi * r / wl
501
  meander_x = center_x + current_amp * np.sin(theta)
502
 
 
 
 
 
503
  for c in range(w):
504
- dist = abs(c - meander_x)
505
- if dist < channel_width:
506
- elevation[r, c] = 5.0 - (channel_width - dist) * 0.3
507
- elif dist < channel_width * 3:
508
- elevation[r, c] = 10.5
 
 
509
 
510
- # ์šฐ๊ฐํ˜ธ (stage > 0.8)
511
- if stage > 0.8:
512
- oxbow_intensity = (stage - 0.8) / 0.2
513
- oxbow_y = h // 2
514
- oxbow_amp = current_amp * 1.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
 
516
- for dy in range(-int(wl/4), int(wl/4)):
517
- r = oxbow_y + dy
 
 
 
518
  if 0 <= r < h:
519
- theta = 2 * np.pi * dy / (wl/2)
520
- ox_x = center_x + oxbow_amp * np.sin(theta)
521
  for dc in range(-channel_width, channel_width + 1):
522
- c = int(ox_x + dc)
523
  if 0 <= c < w:
524
- elevation[r, c] = 4.0 * oxbow_intensity + elevation[r, c] * (1 - oxbow_intensity)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
 
526
  return elevation
527
 
528
 
529
  def create_u_valley_animated(grid_size: int, stage: float,
530
  valley_depth: float = 100.0, valley_width: float = 0.4) -> np.ndarray:
531
- """U์ž๊ณก ํ˜•์„ฑ๊ณผ์ • (V์ž -> U์ž ๋ณ€ํ™˜)"""
 
 
 
 
 
532
  h, w = grid_size, grid_size
533
  elevation = np.zeros((h, w))
534
  center = w // 2
535
 
536
- # V์ž์—์„œ U์ž๋กœ ๋ณ€ํ™˜
537
- # stage 0: ์™„์ „ V, stage 1: ์™„์ „ U
538
- half_width = int(w * valley_width / 2) * stage # U ๋ฐ”๋‹ฅ ๋„ˆ๋น„
 
 
 
 
539
 
540
  for r in range(h):
541
  for c in range(w):
@@ -546,14 +619,49 @@ def create_u_valley_animated(grid_size: int, stage: float,
546
  elevation[r, c] = 0
547
  else:
548
  # V์—์„œ U๋กœ ์ „ํ™˜
549
- # V: linear, U: parabolic
550
  normalized_x = (dx - half_width) / max(1, w // 2 - half_width)
551
  v_height = valley_depth * normalized_x # V shape
552
  u_height = valley_depth * (normalized_x ** 2) # U shape
553
- elevation[r, c] = v_height * (1 - stage) + u_height * stage
554
 
 
555
  elevation[r, :] += (h - r) / h * 30.0
556
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
557
  return elevation
558
 
559
 
@@ -642,47 +750,91 @@ def create_v_valley_animated(grid_size: int, stage: float,
642
 
643
  def create_barchan_animated(grid_size: int, stage: float,
644
  num_dunes: int = 3) -> np.ndarray:
645
- """๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ ํ˜•์„ฑ๊ณผ์ • (ํ‰ํƒ„ ์‚ฌ๋ง‰ -> ๋ชจ๋ž˜ ์ถ•์  -> ์ดˆ์Šน๋‹ฌ ํ˜•์„ฑ)"""
 
 
 
 
 
 
 
 
646
  h, w = grid_size, grid_size
647
  elevation = np.zeros((h, w))
648
 
649
  # ์‚ฌ๋ง‰ ๊ธฐ๋ฐ˜๋ฉด
650
  elevation[:, :] = 5.0
651
 
652
- # Stage์— ๋”ฐ๋ฅธ ์‚ฌ๊ตฌ ์„ฑ์žฅ
653
- np.random.seed(42) # ์ผ๊ด€๋œ ์œ„์น˜
 
 
654
 
655
  for i in range(num_dunes):
656
- cy = h // 4 + i * (h // (num_dunes + 1))
657
- cx = w // 2 + (i - num_dunes // 2) * (w // 5)
658
-
659
- # ์ตœ์ข… ๋†’์ด * stage
660
- final_height = 15.0 + (i * 5.0)
661
- dune_height = final_height * stage
662
 
663
- # ์‚ฌ๊ตฌ ํฌ๊ธฐ๋„ stage์— ๋น„๋ก€
664
- dune_length = int((w // 5) * (0.3 + 0.7 * stage))
665
- dune_width = int((w // 8) * (0.3 + 0.7 * stage))
666
-
667
- if dune_length < 1 or dune_width < 1:
668
  continue
669
 
 
 
 
 
 
 
670
  for r in range(h):
671
  for c in range(w):
672
  dy = r - cy
673
  dx = c - cx
674
 
675
- dist = np.sqrt((dy / max(dune_length, 1)) ** 2 + (dx / max(dune_width, 1)) ** 2)
 
676
 
677
- if dist < 1.0:
678
- if dy < 0: # ๋ฐ”๋žŒ๋ฐ›์ด
679
- z = dune_height * (1 - dist) * (1 - abs(dy) / max(dune_length, 1))
680
- else: # ๋ฐ”๋žŒ๊ทธ๋Š˜
681
- z = dune_height * (1 - dist) * max(0, 1 - dy / (dune_length * 0.5))
682
-
683
- horn_factor = 1 + 0.5 * abs(dx / max(dune_width, 1))
684
- elevation[r, c] = max(elevation[r, c], 5.0 + z * horn_factor)
 
685
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
  return elevation
687
  # ============================================
688
  # ํ™•์žฅ ์ง€ํ˜• (Extended Landforms)
@@ -1121,35 +1273,85 @@ def create_spit_lagoon(grid_size: int = 100, stage: float = 1.0) -> np.ndarray:
1121
  # ============================================
1122
 
1123
  def create_fjord(grid_size: int = 100, stage: float = 1.0) -> np.ndarray:
1124
- """ํ”ผ์˜ค๋ฅด๋“œ (Fjord) - ๋น™ํ•˜๊ฐ€ ํŒŒ๋‚ธ U์ž๊ณก์— ๋ฐ”๋‹ค ์œ ์ž…"""
 
 
 
 
 
1125
  h, w = grid_size, grid_size
1126
  elevation = np.zeros((h, w))
1127
 
1128
- # ์‚ฐ์•… ์ง€ํ˜•
1129
- elevation[:, :] = 80.0
1130
 
1131
  center = w // 2
1132
- valley_width = int(w * 0.2)
1133
-
1134
- # U์ž๊ณก + ๋ฐ”๋‹ค ์œ ์ž…
1135
- sea_line = int(h * 0.7)
1136
 
 
1137
  for r in range(h):
1138
  for c in range(w):
1139
  dx = abs(c - center)
1140
 
1141
  if dx < valley_width:
1142
  # U์ž ๋ฐ”๋‹ฅ
1143
- if r < sea_line:
1144
- elevation[r, c] = 10.0 # ์œก์ง€ ๋ฐ”๋‹ฅ
1145
- else:
1146
- elevation[r, c] = -30.0 * stage # ๋ฐ”๋‹ค
1147
- elif dx < valley_width + 10:
1148
- # U์ž ์ธก๋ฒฝ
1149
- t = (dx - valley_width) / 10
1150
- base = -30.0 if r >= sea_line else 10.0
1151
- elevation[r, c] = base + 70.0 * t
1152
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1153
  return elevation
1154
 
1155
 
@@ -1274,46 +1476,79 @@ def create_braided_river(grid_size: int = 100, stage: float = 1.0) -> np.ndarray
1274
 
1275
  def create_waterfall(grid_size: int = 100, stage: float = 1.0,
1276
  drop_height: float = 50.0) -> np.ndarray:
1277
- """ํญํฌ (Waterfall) - ์ฐจ๋ณ„์นจ์‹"""
 
 
 
 
 
 
 
1278
  h, w = grid_size, grid_size
1279
  elevation = np.zeros((h, w))
1280
-
1281
  center = w // 2
1282
- fall_r = int(h * 0.4)
 
 
 
 
 
1283
 
1284
  # ์ƒ๋ฅ˜ (๋†’์€ ๊ฒฝ์•”์ธต)
 
1285
  for r in range(fall_r):
1286
  for c in range(w):
1287
- elevation[r, c] = drop_height + 20.0 + (fall_r - r) * 0.5
1288
-
1289
- # ํญํฌ (๊ธ‰๊ฒฝ์‚ฌ)
1290
- for r in range(fall_r, fall_r + 5):
 
 
 
1291
  for c in range(w):
1292
- t = (r - fall_r) / 5
1293
- elevation[r, c] = drop_height * (1 - t) + 20.0
1294
-
1295
- # ํ•˜๋ฅ˜
1296
- for r in range(fall_r + 5, h):
 
1297
  for c in range(w):
1298
- elevation[r, c] = 20.0 - (r - fall_r - 5) * 0.2
1299
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1300
  # ํ•˜์ฒœ ์ˆ˜๋กœ
1301
  for r in range(h):
1302
  for dc in range(-4, 5):
1303
  c = center + dc
1304
  if 0 <= c < w:
1305
- elevation[r, c] -= 5.0
1306
-
1307
- # ํ”Œ๋Ÿฐ์ง€ํ’€ (ํญํ˜ธ)
1308
- pool_r = fall_r + 5
1309
- for dr in range(-5, 6):
1310
- for dc in range(-6, 7):
 
1311
  r, c = pool_r + dr, center + dc
1312
  if 0 <= r < h and 0 <= c < w:
1313
  dist = np.sqrt(dr**2 + dc**2)
1314
- if dist < 6:
1315
- elevation[r, c] = min(elevation[r, c], 10.0)
1316
-
 
1317
  return elevation
1318
 
1319
 
@@ -1344,34 +1579,48 @@ def create_karst_doline(grid_size: int = 100, stage: float = 1.0,
1344
 
1345
 
1346
  def create_ria_coast(grid_size: int = 100, stage: float = 1.0) -> np.ndarray:
1347
- """๋ฆฌ์•„์Šค์‹ ํ•ด์•ˆ (Ria Coast) - ์นจ์ˆ˜๋œ ํ•˜๊ณก"""
 
 
 
 
 
1348
  h, w = grid_size, grid_size
1349
  elevation = np.zeros((h, w))
1350
 
1351
- # ์‚ฐ์ง€ ๋ฐฐ๊ฒฝ
1352
- elevation[:, :] = 40.0
1353
 
1354
- # ์—ฌ๋Ÿฌ ๊ฐœ์˜ V์ž๊ณก (์นจ์ˆ˜๋จ)
1355
- num_valleys = 4
1356
- sea_level = int(h * 0.6)
1357
 
1358
  for i in range(num_valleys):
1359
- valley_x = int(w * 0.15 + i * w * 0.2)
 
 
1360
 
1361
  for r in range(h):
1362
  for c in range(w):
1363
  dx = abs(c - valley_x)
1364
 
1365
- if dx < 8:
1366
- # V์ž๊ณก
1367
- depth = 30.0 * (1 - dx / 8)
1368
- elevation[r, c] -= depth
1369
 
1370
- # ํ•ด์ˆ˜๋ฉด ์ดํ•˜ = ๋ฐ”๋‹ค
1371
- for r in range(sea_level, h):
 
 
 
 
 
 
1372
  for c in range(w):
1373
- if elevation[r, c] < 10:
1374
- elevation[r, c] = -5.0 * stage # ์นจ์ˆ˜
 
1375
 
1376
  return elevation
1377
 
@@ -1416,40 +1665,64 @@ def create_tombolo(grid_size: int = 100, stage: float = 1.0) -> np.ndarray:
1416
 
1417
 
1418
  def create_sea_arch(grid_size: int = 100, stage: float = 1.0) -> np.ndarray:
1419
- """ํ•ด์‹์•„์น˜ (Sea Arch) - ๋™๊ตด์ด ๊ด€ํ†ต"""
 
 
 
 
1420
  h, w = grid_size, grid_size
1421
  elevation = np.zeros((h, w))
1422
 
1423
- # ๋ฐ”๋‹ค (์•„๋ž˜)
1424
- sea_line = int(h * 0.5)
1425
- elevation[sea_line:, :] = -5.0
1426
 
1427
  # ์œก์ง€ ์ ˆ๋ฒฝ
1428
- cliff_height = 30.0
1429
  for r in range(sea_line):
1430
- elevation[r, :] = cliff_height
1431
-
1432
- # ๋Œ์ถœ๋ถ€ (๊ณถ)
1433
- headland_width = int(w * 0.3)
 
 
1434
  headland_cx = w // 2
1435
- headland_length = int(h * 0.3)
 
1436
 
1437
  for r in range(sea_line, sea_line + headland_length):
1438
- for c in range(headland_cx - headland_width // 2, headland_cx + headland_width // 2):
 
 
 
 
1439
  if 0 <= c < w:
1440
- elevation[r, c] = cliff_height * (1 - (r - sea_line) / headland_length * 0.3)
1441
-
1442
- # ์•„์น˜ (๊ด€ํ†ต)
 
 
1443
  arch_r = sea_line + int(headland_length * 0.5)
1444
- arch_width = int(headland_width * 0.4 * stage)
 
1445
 
1446
- for dr in range(-5, 6):
1447
- for dc in range(-arch_width // 2, arch_width // 2 + 1):
1448
- r, c = arch_r + dr, headland_cx + dc
 
 
1449
  if 0 <= r < h and 0 <= c < w:
1450
- if abs(dr) < 3: # ์•„์น˜ ๋†’์ด
1451
- elevation[r, c] = -5.0 # ๊ด€ํ†ต
1452
-
 
 
 
 
 
 
 
 
1453
  return elevation
1454
 
1455
 
@@ -1481,30 +1754,75 @@ def create_crater_lake(grid_size: int = 100, stage: float = 1.0,
1481
 
1482
 
1483
  def create_lava_plateau(grid_size: int = 100, stage: float = 1.0) -> np.ndarray:
1484
- """์šฉ์•”๋Œ€์ง€ (Lava Plateau) - ํ‰ํƒ„ํ•œ ์šฉ์•”"""
 
 
 
 
 
1485
  h, w = grid_size, grid_size
1486
  elevation = np.zeros((h, w))
 
1487
 
1488
- # ๊ธฐ๋ฐ˜
1489
- elevation[:, :] = 5.0
1490
-
1491
- # ์šฉ์•”๋Œ€์ง€ ์˜์—ญ
1492
- plateau_height = 30.0 * stage
1493
- margin = int(w * 0.15)
 
 
 
 
 
 
 
 
 
 
 
 
1494
 
1495
- for r in range(margin, h - margin):
1496
- for c in range(margin, w - margin):
1497
- # ๊ฑฐ์˜ ํ‰ํƒ„ํ•˜์ง€๋งŒ ์•ฝ๊ฐ„์˜ ๊ตด๊ณก
1498
- noise = np.sin(r * 0.2) * np.cos(c * 0.2) * 2.0
1499
- elevation[r, c] = plateau_height + noise
1500
 
1501
- # ๊ฐ€์žฅ์ž๋ฆฌ ์ ˆ๋ฒฝ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1502
  for r in range(h):
1503
  for c in range(w):
1504
  edge_dist = min(r, h - r - 1, c, w - c - 1)
1505
  if edge_dist < margin:
1506
  t = edge_dist / margin
1507
- elevation[r, c] = 5.0 + (elevation[r, c] - 5.0) * t
1508
 
1509
  return elevation
1510
 
@@ -1545,6 +1863,206 @@ def create_coastal_dune(grid_size: int = 100, stage: float = 1.0,
1545
  return elevation
1546
 
1547
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1548
  # ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ๊ธฐ ๋งคํ•‘
1549
  ANIMATED_LANDFORM_GENERATORS = {
1550
  'delta': create_delta_animated,
@@ -1580,6 +2098,12 @@ ANIMATED_LANDFORM_GENERATORS = {
1580
  'crater_lake': create_crater_lake,
1581
  'lava_plateau': create_lava_plateau,
1582
  'coastal_dune': create_coastal_dune,
 
 
 
 
 
 
1583
  }
1584
 
1585
  # ์ง€ํ˜• ์ƒ์„ฑ ํ•จ์ˆ˜ ๋งคํ•‘
@@ -1617,5 +2141,11 @@ IDEAL_LANDFORM_GENERATORS = {
1617
  'crater_lake': lambda gs: create_crater_lake(gs, 1.0),
1618
  'lava_plateau': lambda gs: create_lava_plateau(gs, 1.0),
1619
  'coastal_dune': lambda gs: create_coastal_dune(gs, 1.0),
 
 
 
 
 
 
1620
  }
1621
 
 
484
 
485
  def create_meander_animated(grid_size: int, stage: float,
486
  amplitude: float = 0.3, num_bends: int = 3) -> np.ndarray:
487
+ """๊ณก๋ฅ˜ ํ˜•์„ฑ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜ (์ง์„  โ†’ ์‚ฌํ–‰ โ†’ ์šฐ๊ฐํ˜ธ โ†’ ํ•˜์ค‘๋„)
488
+
489
+ Stage 0.0~0.3: ์ง์„  ํ•˜์ฒœ โ†’ ์•ฝํ•œ ์‚ฌํ–‰ ์‹œ์ž‘
490
+ Stage 0.3~0.6: ์‚ฌํ–‰ ๋ฐœ๋‹ฌ + ๊ณต๊ฒฉ์‚ฌ๋ฉด ์นจ์‹ + ํ™œ์ฃผ์‚ฌ๋ฉด ํ‡ด์ 
491
+ Stage 0.6~0.8: ๊ณก๋ฅ˜ ๋ชฉ ์ ˆ๋‹จ โ†’ ์šฐ๊ฐํ˜ธ ํ˜•์„ฑ
492
+ Stage 0.8~1.0: ํ•˜์ค‘๋„(river island) ํ˜•์„ฑ + ๊ตฌํ•˜๋„ ์•ˆ์ •ํ™”
493
+ """
494
  h, w = grid_size, grid_size
495
  elevation = np.zeros((h, w))
496
+ elevation[:, :] = 10.0 # ๋ฒ”๋žŒ์› ๊ธฐ์ค€๋ฉด
497
 
498
  center_x = w // 2
499
  channel_width = max(3, w // 20)
500
 
501
+ # Stage์— ๋”ฐ๋ฅธ ์‚ฌํ–‰ ์ง„ํญ ๋ณ€ํ™”
502
+ if stage < 0.6:
503
+ current_amp = w * amplitude * (stage / 0.6)
504
+ else:
505
+ current_amp = w * amplitude # ์ตœ๋Œ€ ์ง„ํญ ์œ ์ง€
506
+
507
+ wl = h / num_bends # ํŒŒ์žฅ
508
 
509
+ # ๋ฉ”์ธ ํ•˜์ฒœ ๊ทธ๋ฆฌ๊ธฐ
510
  for r in range(h):
511
  theta = 2 * np.pi * r / wl
512
  meander_x = center_x + current_amp * np.sin(theta)
513
 
514
+ # ๊ณต๊ฒฉ์‚ฌ๋ฉด (attack slope) - ๋ฐ”๊นฅ์ชฝ, ์นจ์‹
515
+ # ํ™œ์ฃผ์‚ฌ๋ฉด (slip-off slope) - ์•ˆ์ชฝ, ํ‡ด์ 
516
+ dtheta = np.cos(theta) # ๊ณก๋ฅ  ๋ฐฉํ–ฅ
517
+
518
  for c in range(w):
519
+ dist = c - meander_x
520
+
521
+ # ํ•˜์ฒœ ์ฑ„๋„
522
+ if abs(dist) < channel_width:
523
+ # ์ˆ˜์‹ฌ (์ค‘์•™์ด ๊นŠ์Œ)
524
+ depth_factor = 1 - (abs(dist) / channel_width)
525
+ elevation[r, c] = 5.0 - depth_factor * 3.0 # 2~5m
526
 
527
+ # ๊ณต๊ฒฉ์‚ฌ๋ฉด (์™ธ์ธก) - ์ ˆ๋ฒฝ
528
+ elif dist * dtheta > 0 and abs(dist) < channel_width * 2:
529
+ # ์™ธ์ธก์€ ์นจ์‹์œผ๋กœ ๊ฐ€ํŒŒ๋ฆ„
530
+ erosion_factor = (abs(dist) - channel_width) / channel_width
531
+ elevation[r, c] = 8.0 + erosion_factor * 3.0
532
+
533
+ # ํ™œ์ฃผ์‚ฌ๋ฉด (๋‚ด์ธก) - ํฌ์ธํŠธ๋ฐ”
534
+ elif dist * dtheta < 0 and abs(dist) < channel_width * 3:
535
+ # ๋‚ด์ธก์€ ํ‡ด์ ์œผ๋กœ ์™„๋งŒ
536
+ deposit_factor = (abs(dist) - channel_width) / (channel_width * 2)
537
+ elevation[r, c] = 6.0 + deposit_factor * 4.0
538
+
539
+ # ์ž์—ฐ์ œ๋ฐฉ (levee)
540
+ elif abs(dist) < channel_width * 4:
541
+ levee_height = 11.0 - (abs(dist) - channel_width * 2) * 0.5
542
+ elevation[r, c] = max(levee_height, 10.0)
543
+
544
+ # ์šฐ๊ฐํ˜ธ ํ˜•์„ฑ (stage > 0.6)
545
+ if stage > 0.6:
546
+ oxbow_intensity = min((stage - 0.6) / 0.2, 1.0)
547
 
548
+ # ๊ณก๋ฅ˜ ๋ชฉ ์ง์„ ํ™” (cutoff)
549
+ cutoff_y = int(h * 0.5)
550
+ cutoff_width = int(wl * 0.3)
551
+
552
+ for r in range(cutoff_y - cutoff_width // 2, cutoff_y + cutoff_width // 2):
553
  if 0 <= r < h:
554
+ # ์ง์„  ์ฑ„๋„
 
555
  for dc in range(-channel_width, channel_width + 1):
556
+ c = center_x + dc
557
  if 0 <= c < w:
558
+ new_elev = 4.0 * oxbow_intensity + elevation[r, c] * (1 - oxbow_intensity)
559
+ elevation[r, c] = new_elev
560
+
561
+ # ๊ตฌํ•˜๋„ (์šฐ๊ฐํ˜ธ) - ๋ฌผ์ด ๊ณ ์ธ ๊ณณ
562
+ for r in range(cutoff_y - int(wl * 0.4), cutoff_y + int(wl * 0.4)):
563
+ if 0 <= r < h:
564
+ theta = 2 * np.pi * r / wl
565
+ old_channel_x = center_x + current_amp * np.sin(theta)
566
+
567
+ # ๊ตฌํ•˜๋„๊ฐ€ ๋ฉ”์ธ ์ฑ„๋„๊ณผ ๊ฒน์น˜์ง€ ์•Š๋Š” ๊ณณ๋งŒ
568
+ if abs(old_channel_x - center_x) > channel_width * 2:
569
+ for dc in range(-channel_width, channel_width + 1):
570
+ c = int(old_channel_x + dc)
571
+ if 0 <= c < w:
572
+ # ๊ตฌํ•˜๋„๋Š” ๋ฌผ์ด ๊ณ ์—ฌ ๋‚ฎ์Œ
573
+ elevation[r, c] = 3.0 * oxbow_intensity + elevation[r, c] * (1 - oxbow_intensity)
574
+
575
+ # ํ•˜์ค‘๋„ ํ˜•์„ฑ (stage > 0.8)
576
+ if stage > 0.8:
577
+ island_intensity = (stage - 0.8) / 0.2
578
+
579
+ # ํ•˜๋ฅ˜์— ํ•˜์ค‘๋„ ์ƒ์„ฑ
580
+ island_y = int(h * 0.75)
581
+ island_size = max(3, channel_width // 2)
582
+
583
+ for dy in range(-island_size, island_size + 1):
584
+ for dx in range(-island_size, island_size + 1):
585
+ if dy**2 + dx**2 < island_size**2:
586
+ r, c = island_y + dy, center_x + dx
587
+ if 0 <= r < h and 0 <= c < w:
588
+ elevation[r, c] = 7.0 * island_intensity + elevation[r, c] * (1 - island_intensity)
589
 
590
  return elevation
591
 
592
 
593
  def create_u_valley_animated(grid_size: int, stage: float,
594
  valley_depth: float = 100.0, valley_width: float = 0.4) -> np.ndarray:
595
+ """U์ž๊ณก ํ˜•์„ฑ๊ณผ์ • (๋น™ํ•˜ ์„ฑ์žฅ โ†’ ์นจ์‹ โ†’ ๋น™ํ•˜ ํ›„ํ‡ด โ†’ U์ž๊ณก)
596
+
597
+ Stage 0.0~0.3: ๋น™ํ•˜ ์„ฑ์žฅ (V์ž๊ณก์— ๋น™ํ•˜ ์ฑ„์›Œ์ง)
598
+ Stage 0.3~0.6: ๋น™ํ•˜ ์นจ์‹ (U์ž ํ˜•ํƒœ๋กœ ๋ณ€ํ˜•)
599
+ Stage 0.6~1.0: ๋น™ํ•˜ ํ›„ํ‡ด (๋น™ํ•˜ ๋…น์œผ๋ฉด์„œ U์ž๊ณก ๋“œ๋Ÿฌ๋‚จ)
600
+ """
601
  h, w = grid_size, grid_size
602
  elevation = np.zeros((h, w))
603
  center = w // 2
604
 
605
+ # 1๋‹จ๊ณ„: V์ž๊ณก โ†’ U์ž๊ณก ๋ณ€ํ˜• (์นจ์‹)
606
+ if stage < 0.6:
607
+ u_factor = min(stage / 0.6, 1.0) # 0~1๋กœ ์ •๊ทœํ™”
608
+ else:
609
+ u_factor = 1.0 # ์™„์ „ U์ž
610
+
611
+ half_width = int(w * valley_width / 2) * u_factor # U ๋ฐ”๋‹ฅ ๋„ˆ๋น„
612
 
613
  for r in range(h):
614
  for c in range(w):
 
619
  elevation[r, c] = 0
620
  else:
621
  # V์—์„œ U๋กœ ์ „ํ™˜
 
622
  normalized_x = (dx - half_width) / max(1, w // 2 - half_width)
623
  v_height = valley_depth * normalized_x # V shape
624
  u_height = valley_depth * (normalized_x ** 2) # U shape
625
+ elevation[r, c] = v_height * (1 - u_factor) + u_height * u_factor
626
 
627
+ # ์ƒ๋ฅ˜๋กœ ๊ฐˆ์ˆ˜๋ก ๋†’์•„์ง
628
  elevation[r, :] += (h - r) / h * 30.0
629
+
630
+ # 2๋‹จ๊ณ„: ๋น™ํ•˜ ์ถ”๊ฐ€ (stage์— ๋”ฐ๋ผ ์„ฑ์žฅ/ํ›„ํ‡ด)
631
+ # stage 0~0.3: ๋น™ํ•˜ ์„ฑ์žฅ (ํ•˜๋ฅ˜๋กœ ์ „์ง„)
632
+ # stage 0.3~0.6: ์ตœ๋Œ€ ๋ฒ”์œ„
633
+ # stage 0.6~1.0: ๋น™ํ•˜ ํ›„ํ‡ด (์ƒ๋ฅ˜๋กœ ํ›„ํ‡ด)
634
+
635
+ glacier_grid = np.zeros((h, w))
636
+
637
+ if stage < 0.3:
638
+ # ๋น™ํ•˜ ์„ฑ์žฅ: ์ƒ๋ฅ˜์—์„œ ํ•˜๋ฅ˜๋กœ ์ „์ง„
639
+ glacier_extent = int(h * (stage / 0.3) * 0.8) # ์ตœ๋Œ€ 80%๊นŒ์ง€ ์ „์ง„
640
+ glacier_start = 0
641
+ glacier_end = glacier_extent
642
+ elif stage < 0.6:
643
+ # ์ตœ๋Œ€ ๋น™ํ•˜ ๋ฒ”์œ„
644
+ glacier_start = 0
645
+ glacier_end = int(h * 0.8)
646
+ else:
647
+ # ๋น™ํ•˜ ํ›„ํ‡ด
648
+ retreat_factor = (stage - 0.6) / 0.4
649
+ glacier_start = int(h * 0.8 * retreat_factor) # ํ•˜๋ฅ˜์—์„œ ๋…น์Œ
650
+ glacier_end = int(h * 0.8 * (1 - retreat_factor * 0.5)) # ์ƒ๋ฅ˜๋„ ์ค„์–ด๋“ฆ
651
+
652
+ # ๋น™ํ•˜ ํ‘œ์‹œ (๊ณจ์งœ๊ธฐ ์ฑ„์›€)
653
+ for r in range(glacier_start, min(glacier_end, h)):
654
+ for c in range(w):
655
+ dx = abs(c - center)
656
+ if dx < half_width + 5: # U์ž๊ณก ๋ฐ”๋‹ฅ + ์•ฝ๊ฐ„ ๋„“๊ฒŒ
657
+ glacier_thickness = 20.0 * (1 - abs(c - center) / (half_width + 5))
658
+ if stage < 0.6:
659
+ elevation[r, c] += glacier_thickness
660
+ else:
661
+ # ํ›„ํ‡ด ์ค‘: ๋น™ํ•˜ ๋†’์ด ๊ฐ์†Œ
662
+ retreat_factor = (stage - 0.6) / 0.4
663
+ elevation[r, c] += glacier_thickness * (1 - retreat_factor)
664
+
665
  return elevation
666
 
667
 
 
750
 
751
  def create_barchan_animated(grid_size: int, stage: float,
752
  num_dunes: int = 3) -> np.ndarray:
753
+ """๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ ์ด๋™ ์• ๋‹ˆ๋ฉ”์ด์…˜
754
+
755
+ ์œ„์—์„œ ๋ณผ ๋•Œ ์ดˆ์Šน๋‹ฌ(๐ŸŒ™) ๋ชจ์–‘:
756
+ - ๋ณผ๋ก๋ฉด(convex): ๋ฐ”๋žŒ ๋ถˆ์–ด์˜ค๋Š” ์ชฝ (์ƒ๋‹จ)
757
+ - ์˜ค๋ชฉ๋ฉด(concave): ๋ฐ”๋žŒ ๊ฐ€๋Š” ์ชฝ (ํ•˜๋‹จ) + ๋ฟ”
758
+ - ๋ฟ”(horns): ๋ฐ”๋žŒ ๋ฐฉํ–ฅ์œผ๋กœ ๋ป—์Œ
759
+
760
+ ๋ฐ”๋žŒ ๋ฐฉํ–ฅ: ์œ„ โ†’ ์•„๋ž˜
761
+ """
762
  h, w = grid_size, grid_size
763
  elevation = np.zeros((h, w))
764
 
765
  # ์‚ฌ๋ง‰ ๊ธฐ๋ฐ˜๋ฉด
766
  elevation[:, :] = 5.0
767
 
768
+ np.random.seed(42)
769
+
770
+ # ์‚ฌ๊ตฌ ์ด๋™
771
+ move_distance = int(h * 0.5 * stage)
772
 
773
  for i in range(num_dunes):
774
+ # ์œ„์น˜
775
+ initial_y = h // 5 + i * (h // (num_dunes + 1))
776
+ cx = w // 4 + (i % 2) * (w // 2)
777
+ cy = initial_y + move_distance
 
 
778
 
779
+ if cy >= h - 20:
 
 
 
 
780
  continue
781
 
782
+ # ์‚ฌ๊ตฌ ํฌ๊ธฐ
783
+ dune_height = 10.0 + i * 2.0
784
+ outer_r = w // 7 # ๋ฐ”๊นฅ ์› ๋ฐ˜์ง€๋ฆ„
785
+ inner_r = outer_r * 0.6 # ์•ˆ์ชฝ ์› ๋ฐ˜์ง€๋ฆ„
786
+ inner_offset = outer_r * 0.5 # ์•ˆ์ชฝ ์› ์˜คํ”„์…‹ (์•„๋ž˜๋กœ)
787
+
788
  for r in range(h):
789
  for c in range(w):
790
  dy = r - cy
791
  dx = c - cx
792
 
793
+ # ๋ฐ”๊นฅ ์› (๋ณผ๋ก๋ฉด - ์ƒ๋‹จ)
794
+ dist_outer = np.sqrt(dx**2 + dy**2)
795
 
796
+ # ์•ˆ์ชฝ ์› (์˜ค๋ชฉ๋ฉด - ํ•˜๋‹จ์œผ๋กœ ์˜คํ”„์…‹)
797
+ dist_inner = np.sqrt(dx**2 + (dy - inner_offset)**2)
798
+
799
+ # ์ดˆ์Šน๋‹ฌ ์˜์—ญ: ๋ฐ”๊นฅ ์› ์•ˆ AND ์•ˆ์ชฝ ์› ๋ฐ–
800
+ in_crescent = (dist_outer < outer_r) and (dist_inner > inner_r)
801
+
802
+ if in_crescent:
803
+ # ๋†’์ด ๊ณ„์‚ฐ: ์ค‘์‹ฌ์—์„œ ๋ฉ€์ˆ˜๋ก ๋‚ฎ์•„์ง
804
+ height_factor = 1 - (dist_outer / outer_r)
805
 
806
+ # ๋ฐ”๋žŒ๋ฐ›์ด(์ƒ๋‹จ) ์™„๋งŒ, ๋ฐ”๋žŒ๊ทธ๋Š˜(ํ•˜๋‹จ) ๊ธ‰
807
+ if dy < 0:
808
+ # ๋ฐ”๋žŒ๋ฐ›์ด: ์™„๋งŒํ•œ ๊ฒฝ์‚ฌ
809
+ slope = height_factor * 0.8
810
+ else:
811
+ # ๋ฐ”๋žŒ๊ทธ๋Š˜: ๋” ๋†’๊ฒŒ (๊ธ‰๊ฒฝ์‚ฌ ํšจ๊ณผ)
812
+ slope = height_factor * 1.2
813
+
814
+ z = dune_height * slope
815
+ elevation[r, c] = max(elevation[r, c], 5.0 + z)
816
+
817
+ # ๋ฟ” (horn) - ์–‘์ชฝ์œผ๋กœ ๋ฐ”๋žŒ ๋ฐฉํ–ฅ์œผ๋กœ ๋ป—์Œ
818
+ horn_width = outer_r * 0.3
819
+ horn_length = outer_r * 0.8
820
+
821
+ for side in [-1, 1]: # ์™ผ์ชฝ, ์˜ค๋ฅธ์ชฝ ๋ฟ”
822
+ horn_cx = cx + side * (outer_r - horn_width)
823
+ horn_cy = cy + inner_offset
824
+
825
+ dx_horn = c - horn_cx
826
+ dy_horn = r - horn_cy
827
+
828
+ # ๋ฟ” ์˜์—ญ (๋ฐ”๋žŒ ๋ฐฉํ–ฅ์œผ๋กœ ๊ธธ์ญ‰)
829
+ if abs(dx_horn) < horn_width and 0 < dy_horn < horn_length:
830
+ # ๋ฟ” ๋†’์ด: ๋์œผ๋กœ ๊ฐˆ์ˆ˜๋ก ๋‚ฎ์•„์ง
831
+ horn_factor = 1 - dy_horn / horn_length
832
+ width_factor = 1 - abs(dx_horn) / horn_width
833
+ z = dune_height * 0.5 * horn_factor * width_factor
834
+
835
+ if z > 0.3:
836
+ elevation[r, c] = max(elevation[r, c], 5.0 + z)
837
+
838
  return elevation
839
  # ============================================
840
  # ํ™•์žฅ ์ง€ํ˜• (Extended Landforms)
 
1273
  # ============================================
1274
 
1275
  def create_fjord(grid_size: int = 100, stage: float = 1.0) -> np.ndarray:
1276
+ """ํ”ผ์˜ค๋ฅด๋“œ (Fjord) - ๋น™ํ•˜ ํ›„ํ‡ด ํ›„ ๋ฐ”๋‹ค ์œ ์ž…
1277
+
1278
+ Stage 0.0~0.4: ๋น™ํ•˜๊ฐ€ U์ž๊ณก์„ ์ฑ„์›€ (๋น™ํ•˜๊ธฐ)
1279
+ Stage 0.4~0.7: ๋น™ํ•˜ ํ›„ํ‡ด ์‹œ์ž‘ (๋ฐ”๋‹ค ์œ ์ž… ์‹œ์ž‘)
1280
+ Stage 0.7~1.0: ๋น™ํ•˜ ์™„์ „ ํ›„ํ‡ด (ํ”ผ์˜ค๋ฅด๋“œ ์™„์„ฑ)
1281
+ """
1282
  h, w = grid_size, grid_size
1283
  elevation = np.zeros((h, w))
1284
 
1285
+ # ์‚ฐ์•… ์ง€ํ˜• (๋†’์€ ์‚ฐ)
1286
+ elevation[:, :] = 100.0
1287
 
1288
  center = w // 2
1289
+ valley_width = int(w * 0.25)
1290
+ valley_depth = 60.0
 
 
1291
 
1292
+ # U์ž๊ณก ํ˜•์„ฑ
1293
  for r in range(h):
1294
  for c in range(w):
1295
  dx = abs(c - center)
1296
 
1297
  if dx < valley_width:
1298
  # U์ž ๋ฐ”๋‹ฅ
1299
+ base_height = 10.0
1300
+ elevation[r, c] = base_height
1301
+ elif dx < valley_width + 15:
1302
+ # U์ž ์ธก๋ฒฝ (์ˆ˜์ง์— ๊ฐ€๊นŒ์›€)
1303
+ t = (dx - valley_width) / 15
1304
+ elevation[r, c] = 10.0 + 90.0 * (t ** 0.5) # ๊ธ‰๊ฒฝ์‚ฌ
1305
+
1306
+ # ๋น™ํ•˜ / ๋ฐ”๋‹ค ์ƒํƒœ
1307
+ if stage < 0.4:
1308
+ # ๋น™ํ•˜๊ธฐ: U์ž๊ณก์— ๋น™ํ•˜ ์ฑ„์›€
1309
+ glacier_extent = int(h * 0.9) # ๊ฑฐ์˜ ์ „์ฒด ์ฑ„์›€
1310
+ glacier_thickness = 40.0
1311
+
1312
+ for r in range(glacier_extent):
1313
+ for c in range(w):
1314
+ dx = abs(c - center)
1315
+ if dx < valley_width:
1316
+ # ๋น™ํ•˜ ํ‘œ๋ฉด (๋ณผ๋ก)
1317
+ cross_profile = glacier_thickness * (1 - (dx / valley_width) ** 2)
1318
+ elevation[r, c] = 10.0 + cross_profile
1319
+
1320
+ elif stage < 0.7:
1321
+ # ๋น™ํ•˜ ํ›„ํ‡ด ์ค‘: ์ผ๋ถ€ ๋น™ํ•˜ + ๋ฐ”๋‹ค ์œ ์ž…
1322
+ retreat_factor = (stage - 0.4) / 0.3
1323
+
1324
+ # ๋น™ํ•˜ ์ž”๋ฅ˜ (์ƒ๋ฅ˜์—๋งŒ)
1325
+ glacier_end = int(h * (0.9 - 0.6 * retreat_factor))
1326
+ glacier_thickness = 40.0 * (1 - retreat_factor * 0.5)
1327
+
1328
+ for r in range(glacier_end):
1329
+ for c in range(w):
1330
+ dx = abs(c - center)
1331
+ if dx < valley_width:
1332
+ cross_profile = glacier_thickness * (1 - (dx / valley_width) ** 2)
1333
+ elevation[r, c] = 10.0 + cross_profile
1334
+
1335
+ # ๋ฐ”๋‹ค ์œ ์ž… (ํ•˜๋ฅ˜๋ถ€ํ„ฐ)
1336
+ sea_start = glacier_end
1337
+ for r in range(sea_start, h):
1338
+ for c in range(w):
1339
+ dx = abs(c - center)
1340
+ if dx < valley_width:
1341
+ # ๊นŠ์€ ๋ฐ”๋‹ค
1342
+ elevation[r, c] = -30.0 * retreat_factor
1343
+ else:
1344
+ # ํ”ผ์˜ค๋ฅด๋“œ ์™„์„ฑ: ๊นŠ์€ ๋ฐ”๋‹ค๋งŒ
1345
+ sea_depth = -50.0 # ๊นŠ์€ ํ”ผ์˜ค๋ฅด๋“œ
1346
+
1347
+ for r in range(h):
1348
+ for c in range(w):
1349
+ dx = abs(c - center)
1350
+ if dx < valley_width:
1351
+ # ์ƒ๋ฅ˜๋กœ ๊ฐˆ์ˆ˜๋ก ์–•์•„์ง
1352
+ depth_gradient = 1 - (r / h) * 0.3
1353
+ elevation[r, c] = sea_depth * depth_gradient
1354
+
1355
  return elevation
1356
 
1357
 
 
1476
 
1477
  def create_waterfall(grid_size: int = 100, stage: float = 1.0,
1478
  drop_height: float = 50.0) -> np.ndarray:
1479
+ """ํญํฌ (Waterfall) - ๋‘๋ถ€์นจ์‹์œผ๋กœ ํ›„ํ‡ด
1480
+
1481
+ Stage 0.0: ํญํฌ๊ฐ€ ํ•˜๋ฅ˜์— ์œ„์น˜
1482
+ Stage 1.0: ํญํฌ๊ฐ€ ์ƒ๋ฅ˜๋กœ ํ›„ํ‡ด (๋‘๋ถ€์นจ์‹)
1483
+ - ๊ฒฝ์•”์ธต๊ณผ ์—ฐ์•”์ธต์˜ ์ฐจ๋ณ„์นจ์‹
1484
+ - ํ”Œ๋Ÿฐ์ง€ํ’€(ํญํ˜ธ) ๋ฐœ๋‹ฌ
1485
+ - ํ›„ํ‡ดํ•˜๋ฉด์„œ ํ˜‘๊ณก ํ˜•์„ฑ
1486
+ """
1487
  h, w = grid_size, grid_size
1488
  elevation = np.zeros((h, w))
 
1489
  center = w // 2
1490
+
1491
+ # ํญํฌ ์œ„์น˜ (stage์— ๋”ฐ๋ผ ์ƒ๋ฅ˜๋กœ ํ›„ํ‡ด)
1492
+ # stage 0: ํ•˜๋ฅ˜(h*0.7), stage 1: ์ƒ๋ฅ˜(h*0.3)
1493
+ initial_fall = int(h * 0.7)
1494
+ final_fall = int(h * 0.3)
1495
+ fall_r = int(initial_fall - (initial_fall - final_fall) * stage)
1496
 
1497
  # ์ƒ๋ฅ˜ (๋†’์€ ๊ฒฝ์•”์ธต)
1498
+ hard_rock_height = drop_height + 30.0
1499
  for r in range(fall_r):
1500
  for c in range(w):
1501
+ # ์ƒ๋ฅ˜๋กœ ๊ฐˆ์ˆ˜๋ก ๋†’์•„์ง
1502
+ upstream_rise = (fall_r - r) * 0.3
1503
+ elevation[r, c] = hard_rock_height + upstream_rise
1504
+
1505
+ # ํญํฌ ์ ˆ๋ฒฝ (๊ธ‰๊ฒฝ์‚ฌ)
1506
+ cliff_width = 5
1507
+ for r in range(fall_r, min(fall_r + cliff_width, h)):
1508
  for c in range(w):
1509
+ t = (r - fall_r) / cliff_width
1510
+ # ์ˆ˜์ง ๋‚™ํ•˜
1511
+ elevation[r, c] = hard_rock_height * (1 - t) + 10.0 * t
1512
+
1513
+ # ํ•˜๋ฅ˜ (์—ฐ์•”์ธต ์นจ์‹๋จ)
1514
+ for r in range(fall_r + cliff_width, h):
1515
  for c in range(w):
1516
+ # ํ•˜๋ฅ˜๋กœ ๊ฐˆ์ˆ˜๋ก ๋‚ฎ์•„์ง
1517
+ downstream_drop = (r - fall_r - cliff_width) * 0.2
1518
+ elevation[r, c] = 10.0 - downstream_drop
1519
+
1520
+ # ํ˜‘๊ณก (ํญํฌ ํ›„ํ‡ด ๊ฒฝ๋กœ)
1521
+ gorge_start = fall_r + cliff_width
1522
+ gorge_end = initial_fall + 10 # ์›๋ž˜ ํญํฌ ์œ„์น˜๊นŒ์ง€
1523
+ gorge_depth = 8.0
1524
+
1525
+ for r in range(gorge_start, min(gorge_end, h)):
1526
+ for dc in range(-6, 7):
1527
+ c = center + dc
1528
+ if 0 <= c < w:
1529
+ # V์ž ํ˜‘๊ณก ๋‹จ๋ฉด
1530
+ depth = gorge_depth * (1 - abs(dc) / 6)
1531
+ elevation[r, c] -= depth
1532
+
1533
  # ํ•˜์ฒœ ์ˆ˜๋กœ
1534
  for r in range(h):
1535
  for dc in range(-4, 5):
1536
  c = center + dc
1537
  if 0 <= c < w:
1538
+ elevation[r, c] -= 3.0
1539
+
1540
+ # ํ”Œ๋Ÿฐ์ง€ํ’€ (ํญํ˜ธ) - ํญํฌ ๋ฐ”๋กœ ์•„๋ž˜
1541
+ pool_r = fall_r + cliff_width + 2
1542
+ pool_depth = 15.0
1543
+ for dr in range(-6, 7):
1544
+ for dc in range(-7, 8):
1545
  r, c = pool_r + dr, center + dc
1546
  if 0 <= r < h and 0 <= c < w:
1547
  dist = np.sqrt(dr**2 + dc**2)
1548
+ if dist < 7:
1549
+ pool_effect = pool_depth * (1 - dist / 7)
1550
+ elevation[r, c] = min(elevation[r, c], 5.0 - pool_effect)
1551
+
1552
  return elevation
1553
 
1554
 
 
1579
 
1580
 
1581
  def create_ria_coast(grid_size: int = 100, stage: float = 1.0) -> np.ndarray:
1582
+ """๋ฆฌ์•„์Šค์‹ ํ•ด์•ˆ (Ria Coast) - ์นจ์ˆ˜๋œ ํ•˜๊ณก
1583
+
1584
+ ํ•ด์ˆ˜๋ฉด ์ƒ์Šน์œผ๋กœ V์ž๊ณก์ด ์นจ์ˆ˜๋˜์–ด ํ˜•์„ฑ
1585
+ - ํ†ฑ๋‹ˆ ๋ชจ์–‘ ํ•ด์•ˆ์„ 
1586
+ - ์ข๊ณ  ๊นŠ์€ ๋งŒ (๋ฆฌ์•„)
1587
+ """
1588
  h, w = grid_size, grid_size
1589
  elevation = np.zeros((h, w))
1590
 
1591
+ # ์‚ฐ์ง€ ๋ฐฐ๊ฒฝ (๋†’์€ ์œก์ง€)
1592
+ elevation[:, :] = 50.0
1593
 
1594
+ # ์—ฌ๋Ÿฌ ๊ฐœ์˜ V์ž ํ•˜๊ณก
1595
+ num_valleys = 5
1596
+ valley_spacing = w // (num_valleys + 1)
1597
 
1598
  for i in range(num_valleys):
1599
+ valley_x = valley_spacing * (i + 1)
1600
+ valley_width = 12 + (i % 2) * 4 # ์•ฝ๊ฐ„์˜ ๋ณ€ํ™”
1601
+ valley_depth = 40.0 + (i % 3) * 10
1602
 
1603
  for r in range(h):
1604
  for c in range(w):
1605
  dx = abs(c - valley_x)
1606
 
1607
+ if dx < valley_width:
1608
+ # V์ž๊ณก (์ƒ๋ฅ˜๋กœ ๊ฐˆ์ˆ˜๋ก ์ข์•„์ง)
1609
+ upstream_factor = 1 - r / h * 0.5
1610
+ effective_width = valley_width * upstream_factor
1611
 
1612
+ if dx < effective_width:
1613
+ depth = valley_depth * (1 - dx / effective_width)
1614
+ elevation[r, c] = min(elevation[r, c], 50.0 - depth)
1615
+
1616
+ # ํ•ด์ˆ˜๋ฉด (stage์— ๋”ฐ๋ผ ์ƒ์Šน)
1617
+ sea_level = 15.0 * stage # ๋†’์„์ˆ˜๋ก ๋งŽ์ด ์นจ์ˆ˜
1618
+
1619
+ for r in range(h):
1620
  for c in range(w):
1621
+ if elevation[r, c] < sea_level:
1622
+ # ํ•ด์ˆ˜๋ฉด ์•„๋ž˜ = ๋ฐ”๋‹ค (๋ฆฌ์•„)
1623
+ elevation[r, c] = -10.0 - (sea_level - elevation[r, c]) * 0.3
1624
 
1625
  return elevation
1626
 
 
1665
 
1666
 
1667
  def create_sea_arch(grid_size: int = 100, stage: float = 1.0) -> np.ndarray:
1668
+ """ํ•ด์‹์•„์น˜ (Sea Arch) - ํ•ด์‹๋™๊ตด์ด ๊ด€ํ†ต
1669
+
1670
+ ๊ณถ์˜ ์–‘์ชฝ์—์„œ ํŒŒ๋ž‘ ์นจ์‹ โ†’ ํ•ด์‹๋™๊ตด โ†’ ๊ด€ํ†ต = ์•„์น˜
1671
+ Stage: ์•„์น˜ ํฌ๊ธฐ ๋ฐœ๋‹ฌ
1672
+ """
1673
  h, w = grid_size, grid_size
1674
  elevation = np.zeros((h, w))
1675
 
1676
+ # ๋ฐ”๋‹ค (ํ•˜๋‹จ)
1677
+ sea_line = int(h * 0.4)
1678
+ elevation[sea_line:, :] = -8.0
1679
 
1680
  # ์œก์ง€ ์ ˆ๋ฒฝ
1681
+ cliff_height = 35.0
1682
  for r in range(sea_line):
1683
+ for c in range(w):
1684
+ # ๊ฑฐ๋ฆฌ์— ๋”ฐ๋ฅธ ์œก์ง€ ๋†’์ด
1685
+ dist_from_edge = min(r, c, w - c - 1)
1686
+ elevation[r, c] = cliff_height
1687
+
1688
+ # ๋Œ์ถœ๋ถ€ (๊ณถ - headland)
1689
  headland_cx = w // 2
1690
+ headland_width = int(w * 0.35)
1691
+ headland_length = int(h * 0.4)
1692
 
1693
  for r in range(sea_line, sea_line + headland_length):
1694
+ # ๊ณถ ํญ์ด ๋์œผ๋กœ ๊ฐˆ์ˆ˜๋ก ์ข์•„์ง
1695
+ taper = 1 - (r - sea_line) / headland_length * 0.5
1696
+ current_width = int(headland_width * taper)
1697
+
1698
+ for c in range(headland_cx - current_width // 2, headland_cx + current_width // 2):
1699
  if 0 <= c < w:
1700
+ # ๊ณถ ๋†’์ด (๋์œผ๋กœ ๊ฐˆ์ˆ˜๋ก ์•ฝ๊ฐ„ ๋‚ฎ์•„์ง)
1701
+ height = cliff_height * (1 - (r - sea_line) / headland_length * 0.2)
1702
+ elevation[r, c] = height
1703
+
1704
+ # ํ•ด์‹์•„์น˜ (๊ณถ ์ค‘๊ฐ„์— ๊ด€ํ†ต)
1705
  arch_r = sea_line + int(headland_length * 0.5)
1706
+ arch_height = int(cliff_height * 0.6 * stage) # ์•„์น˜ ๋†’์ด
1707
+ arch_width = int(headland_width * 0.3 * stage) # ์•„์น˜ ํญ
1708
 
1709
+ for dr in range(-8, 9):
1710
+ for dc in range(-arch_width, arch_width + 1):
1711
+ r = arch_r + dr
1712
+ c = headland_cx + dc
1713
+
1714
  if 0 <= r < h and 0 <= c < w:
1715
+ # ์•„์น˜ ํ˜•ํƒœ (๋ฐ˜์›ํ˜• ํ„ฐ๋„)
1716
+ arch_profile = arch_height * np.sqrt(max(0, 1 - (dc / max(arch_width, 1))**2))
1717
+
1718
+ if abs(dr) < 3 and arch_profile > 5:
1719
+ # ํ„ฐ๋„ ๊ด€ํ†ต
1720
+ elevation[r, c] = -5.0
1721
+ elif abs(dr) < 5:
1722
+ # ์•„์น˜ ์ฒœ์žฅ
1723
+ if elevation[r, c] > arch_profile:
1724
+ elevation[r, c] = min(elevation[r, c], cliff_height - arch_profile * 0.3)
1725
+
1726
  return elevation
1727
 
1728
 
 
1754
 
1755
 
1756
  def create_lava_plateau(grid_size: int = 100, stage: float = 1.0) -> np.ndarray:
1757
+ """์šฉ์•”๋Œ€์ง€ (Lava Plateau) - ํ•œํƒ„๊ฐ• ํ˜•์„ฑ๊ณผ์ •
1758
+
1759
+ Stage 0.0~0.3: ์›๋ž˜ V์ž๊ณก ์กด์žฌ
1760
+ Stage 0.3~0.6: ์—ดํ•˜๋ถ„์ถœ๋กœ V์ž๊ณก ๋ฉ”์›Œ์ง (์šฉ์•”๋Œ€์ง€ ํ˜•์„ฑ)
1761
+ Stage 0.6~1.0: ํ•˜์ฒœ ์žฌ์นจ์‹์œผ๋กœ ์ƒˆ๋กœ์šด ํ˜‘๊ณก ํ˜•์„ฑ
1762
+ """
1763
  h, w = grid_size, grid_size
1764
  elevation = np.zeros((h, w))
1765
+ center = w // 2
1766
 
1767
+ # ๊ธฐ๋ฐ˜ ๊ณ ์› ๋†’์ด
1768
+ plateau_base = 30.0
1769
+
1770
+ if stage < 0.3:
1771
+ # ์›๋ž˜ V์ž๊ณก ์ƒํƒœ
1772
+ v_factor = 1.0
1773
+ lava_fill = 0.0
1774
+ new_valley = 0.0
1775
+ elif stage < 0.6:
1776
+ # ์—ดํ•˜๋ถ„์ถœ๋กœ V์ž๊ณก ๋ฉ”์›Œ์ง
1777
+ v_factor = 1.0 - ((stage - 0.3) / 0.3) # V์ž๊ณก ์ ์  ์‚ฌ๋ผ์ง
1778
+ lava_fill = (stage - 0.3) / 0.3 # ์šฉ์•” ์ฑ„์›Œ์ง
1779
+ new_valley = 0.0
1780
+ else:
1781
+ # ์ƒˆ ํ˜‘๊ณก ํ˜•์„ฑ
1782
+ v_factor = 0.0 # ์›๋ž˜ V์ž๊ณก ์™„์ „ํžˆ ๋ฎ์ž„
1783
+ lava_fill = 1.0
1784
+ new_valley = (stage - 0.6) / 0.4 # ์ƒˆ ํ˜‘๊ณก ๋ฐœ๋‹ฌ
1785
 
1786
+ for r in range(h):
1787
+ for c in range(w):
1788
+ dx = abs(c - center)
 
 
1789
 
1790
+ # ๊ธฐ๋ณธ ๊ณ ์›
1791
+ elevation[r, c] = plateau_base
1792
+
1793
+ # ์›๋ž˜ V์ž๊ณก (์—ดํ•˜๋ถ„์ถœ ์ „)
1794
+ if v_factor > 0:
1795
+ valley_depth = 25.0 * v_factor
1796
+ if dx < 15:
1797
+ v_shape = valley_depth * (1 - dx / 15)
1798
+ elevation[r, c] -= v_shape
1799
+
1800
+ # ์šฉ์•” ์ฑ„์›€ (ํ‰ํƒ„ํ™”)
1801
+ if lava_fill > 0:
1802
+ # ์šฉ์•”์ด V์ž๊ณก์„ ๋ฉ”์›€
1803
+ if dx < 15:
1804
+ fill_amount = 25.0 * lava_fill * (1 - dx / 15)
1805
+ elevation[r, c] += fill_amount * 0.8 # ์•ฝ๊ฐ„ ๋‚ฎ๊ฒŒ
1806
+
1807
+ # ์ƒˆ๋กœ์šด ํ˜‘๊ณก (ํ•˜์ฒœ ์žฌ์นจ์‹)
1808
+ if new_valley > 0:
1809
+ # ์ƒˆ ํ•˜์ฒœ์ด ์šฉ์•”๋Œ€์ง€๋ฅผ ํŒŒ๊ณ ๋“ฆ
1810
+ new_valley_width = int(8 * new_valley)
1811
+ new_valley_depth = 20.0 * new_valley
1812
+
1813
+ if dx < new_valley_width:
1814
+ # ๋” ์ข๊ณ  ๊นŠ์€ ํ˜‘๊ณก
1815
+ gorge_shape = new_valley_depth * (1 - dx / max(new_valley_width, 1))
1816
+ elevation[r, c] -= gorge_shape
1817
+
1818
+ # ๊ฐ€์žฅ์ž๋ฆฌ ๊ฒฝ์‚ฌ
1819
+ margin = int(w * 0.1)
1820
  for r in range(h):
1821
  for c in range(w):
1822
  edge_dist = min(r, h - r - 1, c, w - c - 1)
1823
  if edge_dist < margin:
1824
  t = edge_dist / margin
1825
+ elevation[r, c] = elevation[r, c] * t + 5.0 * (1 - t)
1826
 
1827
  return elevation
1828
 
 
1863
  return elevation
1864
 
1865
 
1866
+ # ============================================
1867
+ # ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ์ง€ํ˜•๋“ค
1868
+ # ============================================
1869
+
1870
+ def create_uvala(grid_size: int = 100, stage: float = 1.0,
1871
+ num_dolines: int = 4) -> np.ndarray:
1872
+ """์šฐ๋ฐœ๋ผ (Uvala) - ๋ณตํ•ฉ ๋Œ๋ฆฌ๋„ค
1873
+
1874
+ ์—ฌ๋Ÿฌ ๋Œ๋ฆฌ๋„ค๊ฐ€ ํ•ฉ์ณ์ ธ์„œ ํ˜•์„ฑ๋œ ํฐ ์™€์ง€
1875
+ Stage 0~0.5: ๊ฐœ๋ณ„ ๋Œ๋ฆฌ๋„ค ํ˜•์„ฑ
1876
+ Stage 0.5~1.0: ๋Œ๋ฆฌ๋„ค๋“ค์ด ํ•ฉ์ณ์ง
1877
+ """
1878
+ h, w = grid_size, grid_size
1879
+ elevation = np.zeros((h, w))
1880
+ elevation[:, :] = 30.0 # ์„ํšŒ์•” ๋Œ€์ง€
1881
+
1882
+ center = w // 2
1883
+
1884
+ # ๋Œ๋ฆฌ๋„ค ์œ„์น˜๋“ค
1885
+ doline_positions = [
1886
+ (h // 3, center - w // 6),
1887
+ (h // 3, center + w // 6),
1888
+ (h * 2 // 3, center - w // 6),
1889
+ (h * 2 // 3, center + w // 6),
1890
+ ]
1891
+
1892
+ doline_radius = int(w * 0.15)
1893
+ doline_depth = 20.0 * stage
1894
+
1895
+ for i, (cy, cx) in enumerate(doline_positions[:num_dolines]):
1896
+ for r in range(h):
1897
+ for c in range(w):
1898
+ dist = np.sqrt((r - cy)**2 + (c - cx)**2)
1899
+ if dist < doline_radius:
1900
+ # ๋Œ๋ฆฌ๋„ค ํ˜•ํƒœ (๊ฐ€์žฅ์ž๋ฆฌ ๋†’๊ณ  ์ค‘์•™ ๋‚ฎ์Œ)
1901
+ depth = doline_depth * (1 - dist / doline_radius)
1902
+ elevation[r, c] = min(elevation[r, c], 30.0 - depth)
1903
+
1904
+ # Stage > 0.5: ๋Œ๋ฆฌ๋„ค ์‚ฌ์ด ์—ฐ๊ฒฐ (ํ•ฉ์ณ์ง)
1905
+ if stage > 0.5:
1906
+ merge_factor = (stage - 0.5) / 0.5
1907
+ merge_depth = 10.0 * merge_factor
1908
+
1909
+ # ์ค‘์•™ ์—ฐ๊ฒฐ๋ถ€
1910
+ for r in range(h):
1911
+ for c in range(w):
1912
+ dist_center = np.sqrt((r - h//2)**2 + (c - center)**2)
1913
+ if dist_center < doline_radius * 1.5:
1914
+ elevation[r, c] = min(elevation[r, c], 30.0 - merge_depth)
1915
+
1916
+ return elevation
1917
+
1918
+
1919
+ def create_tower_karst(grid_size: int = 100, stage: float = 1.0,
1920
+ num_towers: int = 6) -> np.ndarray:
1921
+ """ํƒ‘์นด๋ฅด์ŠคํŠธ (Tower Karst) - ๋ด‰์šฐ๋ฆฌ ํ˜•ํƒœ ์นด๋ฅด์ŠคํŠธ
1922
+
1923
+ ์ค‘๊ตญ ๊ตฌ์ด๋ฆฐ ๊ฐ™์€ ํƒ‘ ๋ชจ์–‘ ์„ํšŒ์•” ๋ด‰์šฐ๋ฆฌ
1924
+ """
1925
+ h, w = grid_size, grid_size
1926
+ elevation = np.zeros((h, w))
1927
+ elevation[:, :] = 5.0 # ์ €์ง€๋Œ€
1928
+
1929
+ np.random.seed(42)
1930
+
1931
+ for i in range(num_towers):
1932
+ cy = int(h * 0.2 + (i % 3) * h * 0.3)
1933
+ cx = int(w * 0.2 + (i // 3) * w * 0.3 + np.random.randint(-10, 10))
1934
+
1935
+ tower_height = (40.0 + np.random.rand() * 30) * stage
1936
+ tower_radius = int(w * 0.08 + np.random.rand() * w * 0.04)
1937
+
1938
+ for r in range(h):
1939
+ for c in range(w):
1940
+ dist = np.sqrt((r - cy)**2 + (c - cx)**2)
1941
+ if dist < tower_radius:
1942
+ # ์ˆ˜์ง ์ ˆ๋ฒฝ ํ˜•ํƒœ (๊ฐ€ํŒŒ๋ฅธ ์ธก๋ฉด)
1943
+ edge_factor = 1 - (dist / tower_radius) ** 3
1944
+ z = tower_height * edge_factor
1945
+ elevation[r, c] = max(elevation[r, c], 5.0 + z)
1946
+
1947
+ return elevation
1948
+
1949
+
1950
+ def create_karren(grid_size: int = 100, stage: float = 1.0) -> np.ndarray:
1951
+ """์นด๋ Œ (Karren/Lapies) - ์„ํšŒ์•” ์šฉ์‹ ํ™ˆ
1952
+
1953
+ ๋น—๋ฌผ์— ์˜ํ•œ ์šฉ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ํ™ˆ๊ณผ ๋ฆฟ์ง€
1954
+ """
1955
+ h, w = grid_size, grid_size
1956
+ elevation = np.zeros((h, w))
1957
+ elevation[:, :] = 20.0 # ์„ํšŒ์•” ํ‘œ๋ฉด
1958
+
1959
+ # ์šฉ์‹ ํ™ˆ (Rillenkarren) - ํ‰ํ–‰ํ•œ ํ™ˆ
1960
+ groove_spacing = max(3, w // 20)
1961
+ groove_depth = 3.0 * stage
1962
+
1963
+ for c in range(w):
1964
+ if c % groove_spacing < groove_spacing // 2:
1965
+ for r in range(h):
1966
+ # ๊ธธ์ญ‰ํ•œ ํ™ˆ
1967
+ depth = groove_depth * (1 - abs(c % groove_spacing - groove_spacing // 4) / (groove_spacing // 4))
1968
+ elevation[r, c] -= depth
1969
+
1970
+ # ํด๋ฆฐํŠธ/๊ทธ๋ผ์ดํฌ (Clint/Grike) - ์ง๊ฐ ํŒจํ„ด
1971
+ block_size = max(8, w // 8)
1972
+ grike_depth = 5.0 * stage
1973
+ grike_width = 2
1974
+
1975
+ for r in range(h):
1976
+ for c in range(w):
1977
+ if r % block_size < grike_width or c % block_size < grike_width:
1978
+ elevation[r, c] -= grike_depth
1979
+
1980
+ return elevation
1981
+
1982
+
1983
+ def create_transverse_dune(grid_size: int = 100, stage: float = 1.0,
1984
+ num_ridges: int = 4) -> np.ndarray:
1985
+ """ํšก์‚ฌ๊ตฌ (Transverse Dune) - ๋ฐ”๋žŒ์— ์ง๊ฐ์ธ ์‚ฌ๊ตฌ์—ด
1986
+
1987
+ ๋ฐ”๋žŒ ๋ฐฉํ–ฅ์— ์ˆ˜์ง์œผ๋กœ ํ˜•์„ฑ๋œ ๊ธด ์‚ฌ๊ตฌ
1988
+ """
1989
+ h, w = grid_size, grid_size
1990
+ elevation = np.zeros((h, w))
1991
+ elevation[:, :] = 5.0 # ์‚ฌ๋ง‰ ๊ธฐ๋ฐ˜
1992
+
1993
+ # ํšก์‚ฌ๊ตฌ (๋ฐ”๋žŒ ๋ฐฉํ–ฅ ์ƒโ†’ํ•˜์— ์ˆ˜์ง = ์ขŒ์šฐ๋กœ ๊ธธ๊ฒŒ)
1994
+ ridge_spacing = h // (num_ridges + 1)
1995
+ ridge_height = 12.0 * stage
1996
+ ridge_width = max(5, h // 10)
1997
+
1998
+ for i in range(num_ridges):
1999
+ ridge_r = ridge_spacing * (i + 1)
2000
+
2001
+ for r in range(h):
2002
+ for c in range(w):
2003
+ dr = r - ridge_r
2004
+
2005
+ if abs(dr) < ridge_width:
2006
+ # ๋น„๋Œ€์นญ: ๋ฐ”๋žŒ๋ฐ›์ด ์™„๋งŒ, ๋ฐ”๋žŒ๊ทธ๋Š˜ ๊ธ‰
2007
+ if dr < 0:
2008
+ # ๋ฐ”๋žŒ๋ฐ›์ด
2009
+ z = ridge_height * (1 - abs(dr) / (ridge_width * 1.5))
2010
+ else:
2011
+ # ๋ฐ”๋žŒ๊ทธ๋Š˜
2012
+ z = ridge_height * (1 - dr / (ridge_width * 0.6))
2013
+ z = max(0, z)
2014
+ elevation[r, c] = max(elevation[r, c], 5.0 + z)
2015
+
2016
+ return elevation
2017
+
2018
+
2019
+ def create_star_dune(grid_size: int = 100, stage: float = 1.0,
2020
+ num_dunes: int = 2) -> np.ndarray:
2021
+ """์„ฑ์‚ฌ๊ตฌ (Star Dune) - ๋ณ„ ๋ชจ์–‘ ์‚ฌ๊ตฌ
2022
+
2023
+ ๋‹ค๋ฐฉํ–ฅ ๋ฐ”๋žŒ์œผ๋กœ ํ˜•์„ฑ๋œ ๋ฐฉ์‚ฌ์ƒ ์‚ฌ๊ตฌ
2024
+ """
2025
+ h, w = grid_size, grid_size
2026
+ elevation = np.zeros((h, w))
2027
+ elevation[:, :] = 5.0 # ์‚ฌ๋ง‰ ๊ธฐ๋ฐ˜
2028
+
2029
+ for d in range(num_dunes):
2030
+ cy = h // 3 + d * h // 3
2031
+ cx = w // 3 + d * w // 3
2032
+
2033
+ dune_height = 20.0 * stage
2034
+ arm_length = int(w * 0.2)
2035
+ arm_width = max(3, w // 20)
2036
+ num_arms = 5 # ๋ณ„ ๋ชจ์–‘ ํŒ” ๊ฐœ์ˆ˜
2037
+
2038
+ for r in range(h):
2039
+ for c in range(w):
2040
+ dx = c - cx
2041
+ dy = r - cy
2042
+ dist = np.sqrt(dx**2 + dy**2)
2043
+
2044
+ # ์ค‘์•™ ๋ด‰์šฐ๋ฆฌ
2045
+ if dist < arm_width * 2:
2046
+ z = dune_height * (1 - dist / (arm_width * 2))
2047
+ elevation[r, c] = max(elevation[r, c], 5.0 + z)
2048
+
2049
+ # ํŒ” (๋ฐฉ์‚ฌ์ƒ)
2050
+ for arm in range(num_arms):
2051
+ angle = arm * 2 * np.pi / num_arms
2052
+ # ํŒ” ์ค‘์‹ฌ์„ ๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ
2053
+ arm_dir = np.array([np.cos(angle), np.sin(angle)])
2054
+ pos = np.array([dx, dy])
2055
+ proj = np.dot(pos, arm_dir)
2056
+ perp = np.abs(np.cross(arm_dir, pos))
2057
+
2058
+ if proj > 0 and proj < arm_length and perp < arm_width:
2059
+ # ํŒ” ๋†’์ด: ์ค‘์•™์—์„œ ๋ฉ€์–ด์งˆ์ˆ˜๋ก ๋‚ฎ์•„์ง
2060
+ z = dune_height * 0.6 * (1 - proj / arm_length) * (1 - perp / arm_width)
2061
+ elevation[r, c] = max(elevation[r, c], 5.0 + z)
2062
+
2063
+ return elevation
2064
+
2065
+
2066
  # ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ๊ธฐ ๋งคํ•‘
2067
  ANIMATED_LANDFORM_GENERATORS = {
2068
  'delta': create_delta_animated,
 
2098
  'crater_lake': create_crater_lake,
2099
  'lava_plateau': create_lava_plateau,
2100
  'coastal_dune': create_coastal_dune,
2101
+ # ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ์ง€ํ˜•
2102
+ 'uvala': create_uvala,
2103
+ 'tower_karst': create_tower_karst,
2104
+ 'karren': create_karren,
2105
+ 'transverse_dune': create_transverse_dune,
2106
+ 'star_dune': create_star_dune,
2107
  }
2108
 
2109
  # ์ง€ํ˜• ์ƒ์„ฑ ํ•จ์ˆ˜ ๋งคํ•‘
 
2141
  'crater_lake': lambda gs: create_crater_lake(gs, 1.0),
2142
  'lava_plateau': lambda gs: create_lava_plateau(gs, 1.0),
2143
  'coastal_dune': lambda gs: create_coastal_dune(gs, 1.0),
2144
+ # ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ์ง€ํ˜•
2145
+ 'uvala': lambda gs: create_uvala(gs, 1.0),
2146
+ 'tower_karst': lambda gs: create_tower_karst(gs, 1.0),
2147
+ 'karren': lambda gs: create_karren(gs, 1.0),
2148
+ 'transverse_dune': lambda gs: create_transverse_dune(gs, 1.0),
2149
+ 'star_dune': lambda gs: create_star_dune(gs, 1.0),
2150
  }
2151
 
pages/1_๐Ÿ“–_Gallery.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ
3
+ 31์ข…์˜ ๊ต๊ณผ์„œ์  ์ง€ํ˜•์„ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
4
+ """
5
+ import streamlit as st
6
+ import numpy as np
7
+ import matplotlib.pyplot as plt
8
+ import sys
9
+ import os
10
+
11
+ # ์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๊ฒฝ๋กœ์— ์ถ”๊ฐ€
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
14
+
15
+ from engine.ideal_landforms import IDEAL_LANDFORM_GENERATORS, ANIMATED_LANDFORM_GENERATORS
16
+ from app.main import render_terrain_plotly
17
+
18
+ st.header("๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ")
19
+ st.markdown("_๊ต๊ณผ์„œ์ ์ธ ์ง€ํ˜• ํ˜•ํƒœ๋ฅผ ๊ธฐํ•˜ํ•™์  ๋ชจ๋ธ๋กœ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค._")
20
+
21
+ # ๊ฐ•์กฐ ๋ฉ”์‹œ์ง€
22
+ st.info("๐Ÿ’ก **Tip:** ์ง€ํ˜• ์„ ํƒ ํ›„ **์•„๋ž˜๋กœ ์Šคํฌ๋กค**ํ•˜๋ฉด **๐ŸŽฌ ํ˜•์„ฑ ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜**์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!")
23
+
24
+ # ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง€ํ˜•
25
+ st.sidebar.subheader("๐Ÿ—‚๏ธ ์ง€ํ˜• ์นดํ…Œ๊ณ ๋ฆฌ")
26
+ category = st.sidebar.radio("์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ", [
27
+ "๐ŸŒŠ ํ•˜์ฒœ ์ง€ํ˜•",
28
+ "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•",
29
+ "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•",
30
+ "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•",
31
+ "๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ ์ง€ํ˜•",
32
+ "๐Ÿœ๏ธ ๊ฑด์กฐ ์ง€ํ˜•",
33
+ "๐Ÿ–๏ธ ํ•ด์•ˆ ์ง€ํ˜•"
34
+ ], key="gallery_cat")
35
+
36
+ # ์นดํ…Œ๊ณ ๋ฆฌ โ†’ landform_type ๋งคํ•‘
37
+ CATEGORY_TO_TYPE = {
38
+ "๐ŸŒŠ ํ•˜์ฒœ ์ง€ํ˜•": "river",
39
+ "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•": "river",
40
+ "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•": "glacial",
41
+ "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•": "volcanic",
42
+ "๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ ์ง€ํ˜•": "karst",
43
+ "๐Ÿœ๏ธ ๊ฑด์กฐ ์ง€ํ˜•": "arid",
44
+ "๐Ÿ–๏ธ ํ•ด์•ˆ ์ง€ํ˜•": "coastal"
45
+ }
46
+ landform_type = CATEGORY_TO_TYPE.get(category, None)
47
+
48
+ # ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์˜ต์…˜
49
+ if category == "๐ŸŒŠ ํ•˜์ฒœ ์ง€ํ˜•":
50
+ landform_options = {
51
+ "๐Ÿ“ ์„ ์ƒ์ง€ (Alluvial Fan)": "alluvial_fan",
52
+ "๐Ÿ ์ž์œ ๊ณก๋ฅ˜ (Free Meander)": "free_meander",
53
+ "โ›ฐ๏ธ ๊ฐ์ž…๊ณก๋ฅ˜+ํ•˜์•ˆ๋‹จ๊ตฌ (Incised Meander)": "incised_meander",
54
+ "๐Ÿ”๏ธ V์ž๊ณก (V-Valley)": "v_valley",
55
+ "๐ŸŒŠ ๋ง์ƒํ•˜์ฒœ (Braided River)": "braided_river",
56
+ "๐Ÿ’ง ํญํฌ (Waterfall)": "waterfall",
57
+ }
58
+ elif category == "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•":
59
+ landform_options = {
60
+ "๐Ÿ”บ ์ผ๋ฐ˜ ์‚ผ๊ฐ์ฃผ (Delta)": "delta",
61
+ "๐Ÿฆถ ์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ (Bird-foot)": "bird_foot_delta",
62
+ "๐ŸŒ™ ํ˜ธ์ƒ ์‚ผ๊ฐ์ฃผ (Arcuate)": "arcuate_delta",
63
+ "๐Ÿ“ ์ฒจ๋‘์ƒ ์‚ผ๊ฐ์ฃผ (Cuspate)": "cuspate_delta",
64
+ }
65
+ elif category == "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•":
66
+ landform_options = {
67
+ "โ„๏ธ U์ž๊ณก (U-Valley)": "u_valley",
68
+ "๐Ÿฅฃ ๊ถŒ๊ณก (Cirque)": "cirque",
69
+ "๐Ÿ”๏ธ ํ˜ธ๋ฅธ (Horn)": "horn",
70
+ "๐ŸŒŠ ํ”ผ์˜ค๋ฅด๋“œ (Fjord)": "fjord",
71
+ "๐Ÿฅš ๋“œ๋Ÿผ๋ฆฐ (Drumlin)": "drumlin",
72
+ "๐Ÿชจ ๋น™ํ‡ด์„ (Moraine)": "moraine",
73
+ }
74
+ elif category == "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•":
75
+ landform_options = {
76
+ "๐Ÿ›ก๏ธ ์ˆœ์ƒํ™”์‚ฐ (Shield)": "shield_volcano",
77
+ "๐Ÿ—ป ์„ฑ์ธตํ™”์‚ฐ (Stratovolcano)": "stratovolcano",
78
+ "๐Ÿ•ณ๏ธ ์นผ๋ฐ๋ผ (Caldera)": "caldera",
79
+ "๐Ÿ’ง ํ™”๊ตฌํ˜ธ (Crater Lake)": "crater_lake",
80
+ "๐ŸŸซ ์šฉ์•”๋Œ€์ง€ (Lava Plateau)": "lava_plateau",
81
+ }
82
+ elif category == "๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ ์ง€ํ˜•":
83
+ landform_options = {
84
+ "๐Ÿ•ณ๏ธ ๋Œ๋ฆฌ๋„ค (Doline)": "karst_doline",
85
+ "๐Ÿฅ‹ ์šฐ๋ฐœ๋ผ (Uvala)": "uvala",
86
+ "๐Ÿ—ผ ํƒ‘์นด๋ฅด์ŠคํŠธ (Tower Karst)": "tower_karst",
87
+ "๐Ÿชจ ์นด๋ Œ (Karren)": "karren",
88
+ }
89
+ elif category == "๐Ÿœ๏ธ ๊ฑด์กฐ ์ง€ํ˜•":
90
+ landform_options = {
91
+ "๐ŸŒ™ ๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ (Barchan)": "barchan",
92
+ "๐ŸŸฐ ํšก์‚ฌ๊ตฌ (Transverse Dune)": "transverse_dune",
93
+ "โญ ์„ฑ์‚ฌ๊ตฌ (Star Dune)": "star_dune",
94
+ "๐Ÿ—ฟ ๋ฉ”์‚ฌ/๋ทฐํŠธ (Mesa/Butte)": "mesa_butte",
95
+ }
96
+ else: # ํ•ด์•ˆ ์ง€ํ˜•
97
+ landform_options = {
98
+ "๐Ÿ–๏ธ ํ•ด์•ˆ ์ ˆ๋ฒฝ (Coastal Cliff)": "coastal_cliff",
99
+ "๐ŸŒŠ ์‚ฌ์ทจ+์„ํ˜ธ (Spit+Lagoon)": "spit_lagoon",
100
+ "๐Ÿ๏ธ ์œก๊ณ„์‚ฌ์ฃผ (Tombolo)": "tombolo",
101
+ "๐ŸŒ€ ๋ฆฌ์•„์Šค ํ•ด์•ˆ (Ria Coast)": "ria_coast",
102
+ "๐ŸŒ‰ ํ•ด์‹์•„์น˜ (Sea Arch)": "sea_arch",
103
+ "๐Ÿ–๏ธ ํ•ด์•ˆ์‚ฌ๊ตฌ (Coastal Dune)": "coastal_dune",
104
+ }
105
+
106
+ col_sel, col_view = st.columns([1, 3])
107
+
108
+ with col_sel:
109
+ selected_landform = st.selectbox("์ง€ํ˜• ์„ ํƒ", list(landform_options.keys()))
110
+ landform_key = landform_options[selected_landform]
111
+
112
+ st.markdown("---")
113
+ st.subheader("โš™๏ธ ํŒŒ๋ผ๋ฏธํ„ฐ")
114
+
115
+ gallery_grid_size = st.slider("ํ•ด์ƒ๋„", 50, 150, 80, 10, key="gallery_res")
116
+
117
+ # ๋™์  ์ง€ํ˜• ์ƒ์„ฑ
118
+ if landform_key in IDEAL_LANDFORM_GENERATORS:
119
+ generator = IDEAL_LANDFORM_GENERATORS[landform_key]
120
+ try:
121
+ elevation = generator(gallery_grid_size)
122
+ except TypeError:
123
+ elevation = generator(gallery_grid_size, 1.0)
124
+ else:
125
+ st.error(f"์ง€ํ˜• '{landform_key}' ์ƒ์„ฑ๊ธฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
126
+ elevation = np.zeros((gallery_grid_size, gallery_grid_size))
127
+
128
+ with col_view:
129
+ # 2D ํ‰๋ฉด๋„
130
+ fig_2d, ax = plt.subplots(figsize=(8, 8))
131
+ cmap = plt.cm.terrain
132
+ water_mask = elevation < 0
133
+
134
+ im = ax.imshow(elevation, cmap=cmap, origin='upper')
135
+
136
+ if water_mask.any():
137
+ water_overlay = np.ma.masked_where(~water_mask, np.ones_like(elevation))
138
+ ax.imshow(water_overlay, cmap='Blues', alpha=0.6, origin='upper')
139
+
140
+ ax.set_title(f"{selected_landform}", fontsize=14)
141
+ ax.axis('off')
142
+ plt.colorbar(im, ax=ax, shrink=0.6, label='๊ณ ๋„ (m)')
143
+
144
+ st.pyplot(fig_2d)
145
+ plt.close(fig_2d)
146
+
147
+ # 3D ๋ณด๊ธฐ ๋ฒ„ํŠผ
148
+ if st.button("๐Ÿ”ฒ 3D ๋ทฐ ๋ณด๊ธฐ", key="show_3d_view"):
149
+ fig_3d = render_terrain_plotly(
150
+ elevation,
151
+ f"{selected_landform} - 3D",
152
+ add_water=(landform_key in ["delta", "meander", "coastal_cliff", "fjord", "ria_coast", "spit_lagoon"]),
153
+ water_level=0 if landform_key in ["delta", "coastal_cliff"] else -999,
154
+ force_camera=True,
155
+ landform_type=landform_type # ์นดํ…Œ๊ณ ๋ฆฌ์— ๋งž๋Š” ์ƒ‰์ƒ ์ ์šฉ
156
+ )
157
+ st.plotly_chart(fig_3d, use_container_width=True, key="gallery_3d")
158
+
159
+ # ์„ค๋ช…
160
+ descriptions = {
161
+ "delta": "**์‚ผ๊ฐ์ฃผ**: ํ•˜์ฒœ์ด ๋ฐ”๋‹ค๋‚˜ ํ˜ธ์ˆ˜์— ์œ ์ž…๋  ๋•Œ ์œ ์†์ด ๊ฐ์†Œํ•˜์—ฌ ์šด๋ฐ˜ ์ค‘์ด๋˜ ํ‡ด์ ๋ฌผ์ด ์Œ“์—ฌ ํ˜•์„ฑ๋ฉ๋‹ˆ๋‹ค.",
162
+ "alluvial_fan": "**์„ ์ƒ์ง€**: ์‚ฐ์ง€์—์„œ ํ‰์ง€๋กœ ๋‚˜์˜ค๋Š” ๊ณณ์—์„œ ๊ฒฝ์‚ฌ๊ฐ€ ๊ธ‰๊ฐํ•˜์—ฌ ์šด๋ฐ˜๋ ฅ์ด ์ค„์–ด๋“ค๋ฉด์„œ ํ‡ด์ ๋ฌผ์ด ๋ถ€์ฑ„๊ผด๋กœ ์Œ“์ž…๋‹ˆ๋‹ค.",
163
+ "free_meander": "**์ž์œ ๊ณก๋ฅ˜**: ๋ฒ”๋žŒ์› ์œ„๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์‚ฌํ–‰ํ•˜๋Š” ๊ณก๋ฅ˜. ์ž์—ฐ์ œ๋ฐฉ(Levee)๊ณผ ๋ฐฐํ›„์Šต์ง€๊ฐ€ ํŠน์ง•์ž…๋‹ˆ๋‹ค.",
164
+ "incised_meander": "**๊ฐ์ž…๊ณก๋ฅ˜**: ์œต๊ธฐ๋กœ ์ธํ•ด ๊ณก๋ฅ˜๊ฐ€ ๊ธฐ๋ฐ˜์•”์„ ํŒŒ๊ณ ๋“ค๋ฉด์„œ ํ˜•์„ฑ. ํ•˜์•ˆ๋‹จ๊ตฌ(River Terrace)๊ฐ€ ํ•จ๊ป˜ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.",
165
+ "v_valley": "**V์ž๊ณก**: ํ•˜์ฒœ์˜ ํ•˜๋ฐฉ ์นจ์‹์ด ์šฐ์„ธํ•˜๊ฒŒ ์ž‘์šฉํ•˜์—ฌ ํ˜•์„ฑ๋œ V์ž ๋‹จ๋ฉด์˜ ๊ณจ์งœ๊ธฐ.",
166
+ "braided_river": "**๋ง์ƒํ•˜์ฒœ**: ํ‡ด์ ๋ฌผ์ด ๋งŽ๊ณ  ๊ฒฝ์‚ฌ๊ฐ€ ๊ธ‰ํ•  ๋•Œ ์—ฌ๋Ÿฌ ์ˆ˜๋กœ๊ฐ€ ๊ฐˆ๋ผ์กŒ๋‹ค ํ•ฉ์ณ์ง€๋Š” ํ•˜์ฒœ.",
167
+ "waterfall": "**ํญํฌ**: ๊ฒฝ์•”๊ณผ ์—ฐ์•”์˜ ์ฐจ๋ณ„์นจ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ๊ธ‰๊ฒฝ์‚ฌ ๋‚™์ฐจ. ํ›„ํ‡ดํ•˜๋ฉฐ ํ˜‘๊ณก ํ˜•์„ฑ.",
168
+ "bird_foot_delta": "**์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ**: ๋ฏธ์‹œ์‹œํ”ผ๊ฐ•ํ˜•. ํŒŒ๋ž‘ ์•ฝํ•˜๊ณ  ํ‡ด์ ๋ฌผ ๊ณต๊ธ‰ ๋งŽ์„ ๋•Œ ์ƒˆ๋ฐœ ๋ชจ์–‘์œผ๋กœ ๊ธธ๊ฒŒ ๋ป—์Šต๋‹ˆ๋‹ค.",
169
+ "arcuate_delta": "**ํ˜ธ์ƒ ์‚ผ๊ฐ์ฃผ**: ๋‚˜์ผ๊ฐ•ํ˜•. ํŒŒ๋ž‘๊ณผ ํ‡ด์ ๋ฌผ ๊ณต๊ธ‰์ด ๊ท ํ˜•์„ ์ด๋ฃจ์–ด ๋ถ€๋“œ๋Ÿฌ์šด ํ˜ธ(Arc) ํ˜•ํƒœ.",
170
+ "cuspate_delta": "**์ฒจ๋‘์ƒ ์‚ผ๊ฐ์ฃผ**: ํ‹ฐ๋ฒ ๋ฅด๊ฐ•ํ˜•. ํŒŒ๋ž‘์ด ๊ฐ•ํ•ด ์‚ผ๊ฐ์ฃผ๊ฐ€ ๋พฐ์กฑํ•œ ํ™”์‚ด์ด‰ ๋ชจ์–‘์œผ๋กœ ํ˜•์„ฑ.",
171
+ "u_valley": "**U์ž๊ณก**: ๋น™ํ•˜์˜ ์นจ์‹์œผ๋กœ ํ˜•์„ฑ๋œ U์ž ๋‹จ๋ฉด์˜ ๊ณจ์งœ๊ธฐ. ์ธก๋ฒฝ์ด ๊ธ‰ํ•˜๊ณ  ๋ฐ”๋‹ฅ์ด ํ‰ํƒ„ํ•ฉ๋‹ˆ๋‹ค.",
172
+ "cirque": "**๊ถŒ๊ณก**: ๋น™ํ•˜์˜ ์‹œ์ž‘์ . ๋ฐ˜์›ํ˜• ์›€ํ‘น ํŒŒ์ธ ์ง€ํ˜•์œผ๋กœ, ๋น™ํ•˜ ์œตํ•ด ํ›„ ํ˜ธ์ˆ˜(Tarn)๊ฐ€ ํ˜•์„ฑ๋ฉ๋‹ˆ๋‹ค.",
173
+ "horn": "**ํ˜ธ๋ฅธ**: ์—ฌ๋Ÿฌ ๊ถŒ๊ณก์ด ๋งŒ๋‚˜๋Š” ๊ณณ์—์„œ ์นจ์‹๋˜์ง€ ์•Š๊ณ  ๋‚จ์€ ๋พฐ์กฑํ•œ ํ”ผ๋ผ๋ฏธ๋“œํ˜• ๋ด‰์šฐ๋ฆฌ.",
174
+ "fjord": "**ํ”ผ์˜ค๋ฅด๋“œ**: ๋น™ํ•˜๊ฐ€ ํŒŒ๋‚ธ U์ž๊ณก์— ๋ฐ”๋‹ค๊ฐ€ ์œ ์ž…๋œ ์ข๊ณ  ๊นŠ์€ ๋งŒ.",
175
+ "drumlin": "**๋“œ๋Ÿผ๋ฆฐ**: ๋น™ํ•˜ ํ‡ด์ ๋ฌผ์ด ๋น™ํ•˜ ํ๋ฆ„ ๋ฐฉํ–ฅ์œผ๋กœ ๊ธธ์ญ‰ํ•˜๊ฒŒ ์Œ“์ธ ํƒ€์›ํ˜• ์–ธ๋•.",
176
+ "moraine": "**๋น™ํ‡ด์„**: ๋น™ํ•˜๊ฐ€ ์šด๋ฐ˜ํ•œ ์•”์„ค์ด ํ‡ด์ ๋œ ์ง€ํ˜•. ์ธกํ‡ด์„, ์ข…ํ‡ด์„ ๋“ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.",
177
+ "shield_volcano": "**์ˆœ์ƒํ™”์‚ฐ**: ์œ ๋™์„ฑ ๋†’์€ ํ˜„๋ฌด์•”์งˆ ์šฉ์•”์ด ์™„๋งŒํ•˜๊ฒŒ ์Œ“์—ฌ ๋ฐฉํŒจ ํ˜•ํƒœ.",
178
+ "stratovolcano": "**์„ฑ์ธตํ™”์‚ฐ**: ์šฉ์•”๊ณผ ํ™”์‚ฐ์‡„์„ค๋ฌผ์ด ๊ต๋Œ€๋กœ ์Œ“์—ฌ ๊ธ‰ํ•œ ์›๋ฟ”ํ˜•.",
179
+ "caldera": "**์นผ๋ฐ๋ผ**: ๋Œ€๊ทœ๋ชจ ๋ถ„ํ™” ํ›„ ๋งˆ๊ทธ๋งˆ๋ฐฉ ํ•จ๋ชฐ๋กœ ํ˜•์„ฑ๋œ ๊ฑฐ๋Œ€ํ•œ ๋ถ„์ง€.",
180
+ "crater_lake": "**ํ™”๊ตฌํ˜ธ**: ํ™”๊ตฌ๋‚˜ ์นผ๋ฐ๋ผ์— ๋ฌผ์ด ๊ณ ์—ฌ ํ˜•์„ฑ๋œ ํ˜ธ์ˆ˜.",
181
+ "lava_plateau": "**์šฉ์•”๋Œ€์ง€**: ์—ด๊ทน ๋ถ„์ถœ๋กœ ํ˜„๋ฌด์•”์งˆ ์šฉ์•”์ด ๋„“๊ฒŒ ํŽผ์ณ์ ธ ํ‰ํƒ„ํ•œ ๋Œ€์ง€ ํ˜•์„ฑ.",
182
+ "barchan": "**๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ**: ๋ฐ”๋žŒ์ด ํ•œ ๋ฐฉํ–ฅ์—์„œ ๋ถˆ ๋•Œ ํ˜•์„ฑ๋˜๋Š” ์ดˆ์Šน๋‹ฌ ๋ชจ์–‘์˜ ์‚ฌ๊ตฌ.",
183
+ "mesa_butte": "**๋ฉ”์‚ฌ/๋ทฐํŠธ**: ์ฐจ๋ณ„์นจ์‹์œผ๋กœ ๋‚จ์€ ํƒ์ƒ์ง€. ๋ฉ”์‚ฌ๋Š” ํฌ๊ณ  ํ‰ํƒ„, ๋ทฐํŠธ๋Š” ์ž‘๊ณ  ๋†’์Šต๋‹ˆ๋‹ค.",
184
+ "karst_doline": "**๋Œ๋ฆฌ๋„ค**: ์„ํšŒ์•” ์šฉ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ์›€ํ‘น ํŒŒ์ธ ์™€์ง€.",
185
+ "coastal_cliff": "**ํ•ด์•ˆ ์ ˆ๋ฒฝ**: ํŒŒ๋ž‘์˜ ์นจ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ์ ˆ๋ฒฝ.",
186
+ "spit_lagoon": "**์‚ฌ์ทจ+์„ํ˜ธ**: ์—ฐ์•ˆ๋ฅ˜์— ์˜ํ•ด ํ‡ด์ ๋ฌผ์ด ๊ธธ๊ฒŒ ์Œ“์ธ ์‚ฌ์ทจ๊ฐ€ ๋งŒ์„ ๋ง‰์•„ ์„ํ˜ธ๋ฅผ ํ˜•์„ฑํ•ฉ๋‹ˆ๋‹ค.",
187
+ "tombolo": "**์œก๊ณ„์‚ฌ์ฃผ**: ์—ฐ์•ˆ๋ฅ˜์— ์˜ํ•œ ํ‡ด์ ์œผ๋กœ ์œก์ง€์™€ ์„ฌ์ด ๋ชจ๋ž˜ํ†ฑ์œผ๋กœ ์—ฐ๊ฒฐ๋œ ์ง€ํ˜•.",
188
+ "ria_coast": "**๋ฆฌ์•„์Šค์‹ ํ•ด์•ˆ**: ๊ณผ๊ฑฐ ํ•˜๊ณก์ด ํ•ด์ˆ˜๋ฉด ์ƒ์Šน์œผ๋กœ ์นจ์ˆ˜๋˜์–ด ํ˜•์„ฑ๋œ ํ†ฑ๋‹ˆ ๋ชจ์–‘ ํ•ด์•ˆ์„ .",
189
+ "sea_arch": "**ํ•ด์‹์•„์น˜**: ๊ณถ์—์„œ ํŒŒ๋ž‘ ์นจ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ์•„์น˜ํ˜• ์ง€ํ˜•.",
190
+ "coastal_dune": "**ํ•ด์•ˆ์‚ฌ๊ตฌ**: ํ•ด๋นˆ์˜ ๋ชจ๋ž˜๊ฐ€ ๋ฐ”๋žŒ์— ์˜ํ•ด ์œก์ง€ ์ชฝ์œผ๋กœ ์šด๋ฐ˜๋˜์–ด ํ˜•์„ฑ๋œ ๋ชจ๋ž˜ ์–ธ๋•.",
191
+ # ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ์ง€ํ˜•
192
+ "uvala": "**์šฐ๋ฐœ๋ผ**: ์—ฌ๋Ÿฌ ๋Œ๋ฆฌ๋„ค๊ฐ€ ํ•ฉ๏ฟฝ๏ฟฝ์ ธ ํ˜•์„ฑ๋œ ๋ณตํ•ฉ ์™€์ง€. ๋Œ๋ฆฌ๋„ค๋ณด๋‹ค ํฌ๊ณ  ๋ถˆ๊ทœ์น™ํ•œ ํ˜•ํƒœ.",
193
+ "tower_karst": "**ํƒ‘์นด๋ฅด์ŠคํŠธ**: ์ˆ˜์ง ์ ˆ๋ฒฝ์„ ๊ฐ€์ง„ ํƒ‘ ๋ชจ์–‘ ์„ํšŒ์•” ๋ด‰์šฐ๋ฆฌ. ์ค‘๊ตญ ๊ตฌ์ด๋ฆฐ์ด ๋Œ€ํ‘œ์ .",
194
+ "karren": "**์นด๋ Œ**: ๋น—๋ฌผ์— ์˜ํ•œ ์šฉ์‹์œผ๋กœ ์„ํšŒ์•” ํ‘œ๋ฉด์— ํ˜•์„ฑ๋œ ํ™ˆ๊ณผ ๋ฆฟ์ง€. ํด๋ฆฐํŠธ/๊ทธ๋ผ์ดํฌ ํฌํ•จ.",
195
+ "transverse_dune": "**ํšก์‚ฌ๊ตฌ**: ๋ฐ”๋žŒ ๋ฐฉํ–ฅ์— ์ˆ˜์ง์œผ๋กœ ๊ธธ๊ฒŒ ํ˜•์„ฑ๋œ ์‚ฌ๊ตฌ์—ด. ๋ชจ๋ž˜ ๊ณต๊ธ‰์ด ํ’๋ถ€ํ•  ๋•Œ ๋ฐœ๋‹ฌ.",
196
+ "star_dune": "**์„ฑ์‚ฌ๊ตฌ**: ๋‹ค๋ฐฉํ–ฅ ๋ฐ”๋žŒ์— ์˜ํ•ด ๋ณ„ ๋ชจ์–‘์œผ๋กœ ํ˜•์„ฑ๋œ ์‚ฌ๊ตฌ. ๋†’์ด๊ฐ€ ๋†’๊ณ  ์ด๋™์ด ์ ์Œ.",
197
+ }
198
+ st.info(descriptions.get(landform_key, "์„ค๋ช… ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค."))
199
+
200
+ # ========== ํ˜•์„ฑ ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜ ==========
201
+ if landform_key in ANIMATED_LANDFORM_GENERATORS:
202
+ st.markdown("---")
203
+ st.subheader("๐ŸŽฌ ํ˜•์„ฑ ๊ณผ์ •")
204
+
205
+ # ์ž๋™ ์žฌ์ƒ ์ค‘์ด๋ฉด session_state์˜ stage ์‚ฌ์šฉ
206
+ if st.session_state.get('auto_playing', False):
207
+ stage_value = st.session_state.get('auto_stage', 0.0)
208
+ st.slider(
209
+ "ํ˜•์„ฑ ๋‹จ๊ณ„ (์ž๋™ ์žฌ์ƒ ์ค‘...)",
210
+ 0.0, 1.0, stage_value, 0.05,
211
+ key="gallery_stage_slider",
212
+ disabled=True
213
+ )
214
+ else:
215
+ stage_value = st.slider(
216
+ "ํ˜•์„ฑ ๋‹จ๊ณ„ (0% = ์‹œ์ž‘, 100% = ์™„์„ฑ)",
217
+ 0.0, 1.0, 1.0, 0.05,
218
+ key="gallery_stage_slider"
219
+ )
220
+
221
+ anim_func = ANIMATED_LANDFORM_GENERATORS[landform_key]
222
+ stage_elev = anim_func(gallery_grid_size, stage_value)
223
+
224
+ # ๋ฌผ ์ƒ์„ฑ
225
+ stage_water = np.maximum(0, -stage_elev + 1.0)
226
+ stage_water[stage_elev > 2] = 0
227
+
228
+ # ์„ ์ƒ์ง€ ๋ฌผ ์ฒ˜๋ฆฌ
229
+ if landform_key == "alluvial_fan":
230
+ apex_y = int(gallery_grid_size * 0.15)
231
+ center = gallery_grid_size // 2
232
+ for r in range(apex_y + 5):
233
+ for dc in range(-2, 3):
234
+ c = center + dc
235
+ if 0 <= c < gallery_grid_size:
236
+ stage_water[r, c] = 3.0
237
+
238
+ # 3D ๋ Œ๋”๋ง
239
+ fig_stage = render_terrain_plotly(
240
+ stage_elev,
241
+ f"{selected_landform} - {int(stage_value*100)}%",
242
+ add_water=True,
243
+ water_depth_grid=stage_water,
244
+ water_level=-999,
245
+ force_camera=False, # ์นด๋ฉ”๋ผ ์ด๋™ ํ—ˆ์šฉ
246
+ landform_type=landform_type
247
+ )
248
+ st.plotly_chart(fig_stage, use_container_width=True, key="stage_view")
249
+
250
+ # ์ž๋™ ์žฌ์ƒ (์„ธ์…˜ ์ƒํƒœ ํ™œ์šฉ)
251
+ col_play, col_step = st.columns(2)
252
+ with col_play:
253
+ if st.button("โ–ถ๏ธ ์ž๋™ ์žฌ์ƒ ์‹œ์ž‘", key="auto_play"):
254
+ st.session_state['auto_playing'] = True
255
+ st.session_state['auto_stage'] = 0.0
256
+ with col_step:
257
+ if st.button("โน๏ธ ์ •์ง€", key="stop_play"):
258
+ st.session_state['auto_playing'] = False
259
+
260
+ # ์ž๋™ ์žฌ์ƒ ์ค‘์ด๋ฉด stage ์ž๋™ ์ฆ๊ฐ€
261
+ if st.session_state.get('auto_playing', False):
262
+ current_stage = st.session_state.get('auto_stage', 0.0)
263
+ if current_stage < 1.0:
264
+ st.session_state['auto_stage'] = current_stage + 0.1
265
+ import time
266
+ time.sleep(0.5)
267
+ st.rerun()
268
+ else:
269
+ st.session_state['auto_playing'] = False
270
+ st.success("โœ… ์™„๋ฃŒ!")
271
+
272
+ st.caption("๐Ÿ’ก **Tip:** ์นด๋ฉ”๋ผ ๊ฐ๋„๋ฅผ ๋จผ์ € ์กฐ์ •ํ•œ ํ›„ ์ž๋™ ์žฌ์ƒํ•˜๋ฉด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.")
terrain_processes.html ADDED
@@ -0,0 +1,474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ๏ปฟ<!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>์ง€ํ˜• ํ˜•์„ฑ ์ž‘์šฉ | Geo Lab</title>
7
+ <style>
8
+ @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=Noto+Sans+KR:wght@400;500;700&display=swap");
9
+
10
+ :root {
11
+ --ink: #e7eef9;
12
+ --muted: #a5b8d4;
13
+ --panel: #0f1c32;
14
+ --panel-2: #0c1628;
15
+ --stroke: #1f2c46;
16
+ --accent: #f59e0b;
17
+ --accent-2: #7dd3fc;
18
+ --accent-3: #34d399;
19
+ --glow: rgba(126, 200, 255, 0.14);
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ min-height: 100vh;
29
+ font-family: "Noto Sans KR", "Space Grotesk", system-ui, -apple-system, sans-serif;
30
+ color: var(--ink);
31
+ background:
32
+ radial-gradient(circle at 15% 20%, #192742 0%, #0b1220 35%),
33
+ radial-gradient(circle at 80% 0%, #13233f 0%, transparent 30%),
34
+ linear-gradient(120deg, #0c1424 0%, #0b1220 70%);
35
+ overflow-x: hidden;
36
+ }
37
+
38
+ .canvas {
39
+ position: relative;
40
+ max-width: 1200px;
41
+ margin: 0 auto;
42
+ padding: 48px 22px 64px;
43
+ }
44
+
45
+ .grid-bg {
46
+ position: fixed;
47
+ inset: 0;
48
+ pointer-events: none;
49
+ background-image:
50
+ linear-gradient(var(--glow) 1px, transparent 1px),
51
+ linear-gradient(90deg, var(--glow) 1px, transparent 1px);
52
+ background-size: 48px 48px;
53
+ mask-image: radial-gradient(circle at 60% 40%, #000 0%, rgba(0, 0, 0, 0.12) 50%, transparent 70%);
54
+ }
55
+
56
+ header.hero {
57
+ display: grid;
58
+ grid-template-columns: 1.2fr 0.8fr;
59
+ gap: 28px;
60
+ align-items: end;
61
+ margin-bottom: 28px;
62
+ }
63
+
64
+ .eyebrow {
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: 8px;
68
+ padding: 8px 12px;
69
+ border-radius: 999px;
70
+ background: rgba(255, 255, 255, 0.05);
71
+ border: 1px solid var(--stroke);
72
+ font-size: 13px;
73
+ letter-spacing: 0.6px;
74
+ text-transform: uppercase;
75
+ color: var(--muted);
76
+ }
77
+
78
+ h1 {
79
+ margin: 10px 0 12px;
80
+ font-family: "Space Grotesk", "Noto Sans KR", sans-serif;
81
+ font-size: clamp(30px, 4vw, 42px);
82
+ letter-spacing: -0.5px;
83
+ }
84
+
85
+ .lede {
86
+ margin: 0;
87
+ font-size: 17px;
88
+ line-height: 1.5;
89
+ color: var(--muted);
90
+ }
91
+
92
+ .tags {
93
+ display: flex;
94
+ flex-wrap: wrap;
95
+ gap: 10px;
96
+ margin-top: 16px;
97
+ }
98
+
99
+ .tag {
100
+ padding: 7px 11px;
101
+ border-radius: 10px;
102
+ border: 1px solid var(--stroke);
103
+ color: var(--ink);
104
+ background: rgba(255, 255, 255, 0.03);
105
+ font-size: 13px;
106
+ }
107
+
108
+ .panel {
109
+ background: linear-gradient(140deg, var(--panel) 0%, var(--panel-2) 100%);
110
+ border: 1px solid var(--stroke);
111
+ border-radius: 18px;
112
+ padding: 20px;
113
+ position: relative;
114
+ overflow: hidden;
115
+ }
116
+
117
+ .panel::before {
118
+ content: "";
119
+ position: absolute;
120
+ inset: 0;
121
+ background: radial-gradient(circle at 20% 20%, rgba(255, 255, 255, 0.04), transparent 35%);
122
+ pointer-events: none;
123
+ }
124
+
125
+ .panel h3 {
126
+ margin: 0 0 8px;
127
+ font-size: 17px;
128
+ letter-spacing: -0.2px;
129
+ }
130
+
131
+ .panel p {
132
+ margin: 0;
133
+ color: var(--muted);
134
+ line-height: 1.5;
135
+ font-size: 14px;
136
+ }
137
+
138
+ .stack {
139
+ display: grid;
140
+ gap: 14px;
141
+ }
142
+
143
+ .process-grid {
144
+ display: grid;
145
+ gap: 16px;
146
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
147
+ margin: 18px 0 32px;
148
+ }
149
+
150
+ .pill {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ gap: 7px;
154
+ padding: 6px 10px;
155
+ border-radius: 999px;
156
+ font-size: 12px;
157
+ color: var(--ink);
158
+ border: 1px solid var(--stroke);
159
+ background: rgba(255, 255, 255, 0.03);
160
+ }
161
+
162
+ .pill.accent {
163
+ background: rgba(245, 158, 11, 0.12);
164
+ border-color: rgba(245, 158, 11, 0.5);
165
+ color: #ffd58f;
166
+ }
167
+
168
+ .pill.cool {
169
+ background: rgba(125, 211, 252, 0.12);
170
+ border-color: rgba(125, 211, 252, 0.5);
171
+ color: #c8f1ff;
172
+ }
173
+
174
+ .pill.green {
175
+ background: rgba(52, 211, 153, 0.12);
176
+ border-color: rgba(52, 211, 153, 0.5);
177
+ color: #c1ffe3;
178
+ }
179
+
180
+ .timeline {
181
+ display: grid;
182
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
183
+ gap: 14px;
184
+ margin: 10px 0;
185
+ }
186
+
187
+ .step {
188
+ border-left: 2px solid var(--stroke);
189
+ padding-left: 14px;
190
+ position: relative;
191
+ }
192
+
193
+ .step::before {
194
+ content: "";
195
+ position: absolute;
196
+ width: 9px;
197
+ height: 9px;
198
+ border-radius: 50%;
199
+ background: var(--accent-2);
200
+ border: 2px solid #081020;
201
+ left: -6px;
202
+ top: 6px;
203
+ box-shadow: 0 0 12px rgba(125, 211, 252, 0.5);
204
+ }
205
+
206
+ .step strong {
207
+ display: block;
208
+ margin-bottom: 6px;
209
+ font-size: 14px;
210
+ letter-spacing: -0.2px;
211
+ }
212
+
213
+ .legend {
214
+ display: flex;
215
+ flex-wrap: wrap;
216
+ gap: 12px;
217
+ margin-top: 10px;
218
+ }
219
+
220
+ .legend span {
221
+ display: inline-flex;
222
+ align-items: center;
223
+ gap: 8px;
224
+ font-size: 13px;
225
+ color: var(--muted);
226
+ }
227
+
228
+ .legend b {
229
+ display: inline-block;
230
+ width: 12px;
231
+ height: 12px;
232
+ border-radius: 4px;
233
+ }
234
+
235
+ .flow {
236
+ display: grid;
237
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
238
+ gap: 16px;
239
+ margin-top: 18px;
240
+ }
241
+
242
+ details {
243
+ border: 1px solid var(--stroke);
244
+ border-radius: 14px;
245
+ padding: 12px 14px;
246
+ background: rgba(255, 255, 255, 0.02);
247
+ transition: border-color 150ms ease, background 150ms ease;
248
+ }
249
+
250
+ details[open] {
251
+ border-color: rgba(125, 211, 252, 0.6);
252
+ background: rgba(125, 211, 252, 0.04);
253
+ }
254
+
255
+ summary {
256
+ cursor: pointer;
257
+ font-weight: 600;
258
+ font-size: 14px;
259
+ outline: none;
260
+ }
261
+
262
+ .note {
263
+ margin: 10px 0 0;
264
+ color: var(--muted);
265
+ font-size: 13px;
266
+ line-height: 1.5;
267
+ }
268
+
269
+ @media (max-width: 900px) {
270
+ header.hero {
271
+ grid-template-columns: 1fr;
272
+ }
273
+ }
274
+ </style>
275
+ </head>
276
+ <body>
277
+ <div class="grid-bg"></div>
278
+ <div class="canvas">
279
+ <header class="hero">
280
+ <div>
281
+ <div class="eyebrow">Geomorph ยท Process Map</div>
282
+ <h1>์ง€ํ˜• ํ˜•์„ฑ ์ž‘์šฉ์˜ ํ๋ฆ„์„ ํ•œ๋ˆˆ์—</h1>
283
+ <p class="lede">
284
+ ๋‚ด์ (ํŒ๊ตฌ์กฐยทํ™”์‚ฐ) ์—๋„ˆ์ง€์™€ ์™ธ์ (๊ธฐํ›„ยท์ค‘๋ ฅ) ์—๋„ˆ์ง€๊ฐ€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋ฉฐ ์ง€ํ‘œ๋ฅผ ๊นŽ๊ณ  ์Œ“๊ณ  ๋“ค์–ด์˜ฌ๋ฆฝ๋‹ˆ๋‹ค.
285
+ ์ฃผ์š” ๋‹จ๊ณ„์™€ ์ง€ํ˜• ๊ฒฐ๊ณผ๋ฅผ ๋น ๋ฅด๊ฒŒ ํ›‘์–ด๋ณผ ์ˆ˜ ์žˆ๋Š” ์š”์•ฝ HTML์ž…๋‹ˆ๋‹ค.
286
+ </p>
287
+ <div class="tags">
288
+ <div class="tag">๋‚ด์  ์—๋„ˆ์ง€: ํŒ๊ตฌ์กฐ, ์œต๊ธฐ, ํ™”์‚ฐ</div>
289
+ <div class="tag">์™ธ์  ์—๋„ˆ์ง€: ํ’ํ™”, ์ค‘๋ ฅ, ์นจ์‹ยท์šด๋ฐ˜ยทํ‡ด์ </div>
290
+ <div class="tag">์‹œ๊ฐ„ ์Šค์ผ€์ผ: ๋‹จ๋ฐœ์„ฑ(์‚ฌ๋ฉด ๋ถ•๊ดด) โ‡„ ์žฅ๊ธฐ(์œต๊ธฐยทํ‰์ค€ํ™”)</div>
291
+ </div>
292
+ </div>
293
+ <div class="panel stack">
294
+ <div class="pill accent">ํ•ต์‹ฌ ๋งฅ๋ฝ</div>
295
+ <h3>์ง€ํ˜•์„ ๋งŒ๋“œ๋Š” ์„ธ ํž˜</h3>
296
+ <p>
297
+ 1) ์ง€๊ตฌ ๋‚ด๋ถ€ ์—๋„ˆ์ง€(ํŒ๊ตฌ์กฐ ์šด๋™, ๋งˆ๊ทธ๋งˆ)๋กœ ์ง€ํ‘œ๊ฐ€ ์œต๊ธฐยท๋ณ€ํ˜•๋œ๋‹ค.
298
+ 2) ๊ธฐํ›„๊ฐ€ ์ง€๋ฐฐํ•˜๋Š” ํ’ํ™”ยท์นจ์‹ยท์šด๋ฐ˜ยทํ‡ด์ ์ด ๋†’์ด๋ฅผ ๊นŽ๊ณ  ๋ฉ”์šด๋‹ค.
299
+ 3) ์ค‘๋ ฅ์€ ์‚ฌ๋ฉด ์•ˆ์ •์„ฑ์„ ํ†ต์ œํ•˜๋ฉฐ ๋Œ๋ฐœ์  ๋ถ•๊ดด๋กœ ๊ท ํ˜•์„ ์žฌ์กฐ์ •ํ•œ๋‹ค.
300
+ </p>
301
+ <div class="legend">
302
+ <span><b style="background: var(--accent);"></b> ๋‚ด์  ์ž‘์šฉ</span>
303
+ <span><b style="background: var(--accent-2);"></b> ์™ธ์  ์ž‘์šฉ</span>
304
+ <span><b style="background: var(--accent-3);"></b> ๊ฒฐ๊ณผ ์ง€ํ˜•</span>
305
+ </div>
306
+ </div>
307
+ </header>
308
+
309
+ <section class="process-grid">
310
+ <div class="panel">
311
+ <div class="pill accent">๋‚ด์  ยท ์—๋„ˆ์ง€ ๊ณต๊ธ‰</div>
312
+ <h3>ํŒ๊ตฌ์กฐ ์šด๋™ & ์œต๊ธฐ</h3>
313
+ <p>
314
+ ์••์ถ•ยท์ธ์žฅยทํšก์ด๋™์— ๋”ฐ๋ผ ์ง€๊ฐ์ด ์ ‘ํž˜ยท๋‹จ์ธตยท์œต๊ธฐํ•˜๋ฉฐ ๊ณ ๋„ ๋Œ€๋น„๋ฅผ ๋งŒ๋“ ๋‹ค.
315
+ ๋†’์€ ์œ„์น˜์— ๋†“์ธ ์•”์ฒด๋Š” ์ดํ›„ ์™ธ์  ์ž‘์šฉ์˜ ํ‘œ์ ์ด ๋œ๋‹ค.
316
+ </p>
317
+ </div>
318
+ <div class="panel">
319
+ <div class="pill accent">๋‚ด์  ยท ๋ฌผ์งˆ ๊ณต๊ธ‰</div>
320
+ <h3>ํ™”์‚ฐ ํ™œ๋™ / ๊ด€์ž…</h3>
321
+ <p>
322
+ ๋งˆ๊ทธ๋งˆ ๋ถ„์ถœยท๊ด€์ž…์œผ๋กœ ์ƒˆ๋กœ์šด ์•”์„์ด ์Œ“์ด๊ณ  ์—ด๋ณ€ํ˜•์ด ์ผ์–ด๋‚œ๋‹ค.
323
+ ์šฉ์•”๋Œ€์ง€, ์ˆœ์ƒยท๋ณตํ•ฉํ™”์‚ฐ, ํ™”์‚ฐ๋”, ์‘ํšŒํ™˜ ๋“ฑ์ด ํ˜•์„ฑ๋œ๋‹ค.
324
+ </p>
325
+ </div>
326
+ <div class="panel">
327
+ <div class="pill cool">์™ธ์  ยท ์žฌ๋ฃŒ ์•ฝํ™”</div>
328
+ <h3>ํ’ํ™” (๋ฌผ๋ฆฌยทํ™”ํ•™)</h3>
329
+ <p>
330
+ ๋™๊ฒฐ์œตํ•ดยท๋ฐ•๋ฆฌยทๅกฉ์ •ยทํƒ„์‚ฐยท์‚ฐํ™” ๋“ฑ์ด ์•”์„์„ ๋ถ„์‡„/์šฉํ•ดํ•ด ์ž…๋„๋ฅผ ์ค„์ด๊ณ 
331
+ ์‚ฌ๋ฉด์„ ๋А์Šจํ•˜๊ฒŒ ๋งŒ๋“ค์–ด ์นจ์‹๊ณผ ๋ถ•๊ดด๋ฅผ ์ค€๋น„ํ•œ๋‹ค.
332
+ </p>
333
+ </div>
334
+ <div class="panel">
335
+ <div class="pill cool">์™ธ์  ยท ์ค‘๋ ฅ</div>
336
+ <h3>์‚ฌ๋ฉด ๋ถ•๊ดด / ์งˆ๋Ÿ‰ ์ด๋™</h3>
337
+ <p>
338
+ ๋‚™์„, ์‚ฌํƒœ, ์ง€ํ˜๋ฆผ, ํฌํ–‰ ๋“ฑ์œผ๋กœ ๊ณ ๋„์ฐจ๊ฐ€ ๊ธ‰๊ฒฉํžˆ ์™„๋งŒํ•ด์ง„๋‹ค.
339
+ ๊ฐ•์šฐยท์ง€์ง„ยทํ’ํ™” ์‹ฌํ™”๊ฐ€ ๋ฐฉ์•„์‡  ์—ญํ• ์„ ํ•œ๋‹ค.
340
+ </p>
341
+ </div>
342
+ <div class="panel">
343
+ <div class="pill cool">์™ธ์  ยท ์นจ์‹ยท์šด๋ฐ˜</div>
344
+ <h3>๋ฌผ ยท ๋ฐ”๋žŒ ยท ์–ผ์Œ</h3>
345
+ <p>
346
+ ํ•˜์ฒœ(์ˆ˜์‹), ํ•ด์•ˆ(ํŒŒ์‹), ๋น™ํ•˜(์„ค๋งˆ์ฐฐ), ์‚ฌ๋ง‰(ํ’์‹)์ด ์žฌ๋ฃŒ๋ฅผ ๊นŽ๊ณ  ๋จผ ๊ณณ์œผ๋กœ ์šด๋ฐ˜ํ•œ๋‹ค.
347
+ ์†Œ์šฉ๋Œ์ดยท๋‚œ๋ฅ˜ยท๋น™์••์ด ์ฃผ์š” ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด๋‹ค.
348
+ </p>
349
+ </div>
350
+ <div class="panel">
351
+ <div class="pill cool">์™ธ์  ยท ์ถ•์ </div>
352
+ <h3>ํ‡ด์  & ๊ณ ๊ฒฐ</h3>
353
+ <p>
354
+ ์—๋„ˆ์ง€๊ฐ€ ๋‚ฎ์•„์ง€๋Š” ์ง€์ (์‚ผ๊ฐ์ฃผ, ์ถฉ์ ์„ ์ƒ์ง€, ๋น™ํ‡ด์„, ์‚ฌ๊ตฌ, ํ˜ธ์•ˆ)์— ์ž…์ž๊ฐ€ ์Œ“์—ฌ
355
+ ์ธต๋ฆฌ๊ฐ€ ๋ฐœ๋‹ฌํ•˜๊ณ  ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ๊ณ ๊ฒฐยท๊ต๊ฒฐ๋œ๋‹ค.
356
+ </p>
357
+ </div>
358
+ </section>
359
+
360
+ <section class="panel">
361
+ <div class="pill green">๋‹จ๊ณ„๋ณ„ ํ๋ฆ„</div>
362
+ <h3>์ง€ํ˜• ํ˜•์„ฑ ํƒ€์ž„๋ผ์ธ</h3>
363
+ <div class="timeline">
364
+ <div class="step">
365
+ <strong>1. ์—๋„ˆ์ง€ ๋ถ€์—ฌ (๋‚ด์ )</strong>
366
+ ํŒ๊ตฌ์กฐ ๋ณ€ํ˜•ยท์œต๊ธฐยทํ™”์‚ฐ ๋ถ„์ถœ๋กœ ๊ณ ๋„ ๋Œ€๋น„๊ฐ€ ์ปค์ง€๊ณ  ์ƒˆ๋กœ์šด ์•”์„์ด ๋…ธ์ถœ๋œ๋‹ค.
367
+ </div>
368
+ <div class="step">
369
+ <strong>2. ์•ฝํ™” (ํ’ํ™”)</strong>
370
+ ๋™๊ฒฐ์œตํ•ด, ์—ดํŒฝ์ฐฝ, ํ™”ํ•™ ์šฉํ•ด๊ฐ€ ๊ท ์—ด์„ ๋„“ํ˜€ ์•”์ฒด ๊ฐ•๋„๋ฅผ ๋‚ฎ์ถ˜๋‹ค.
371
+ </div>
372
+ <div class="step">
373
+ <strong>3. ์ค‘๋ ฅ ๋ถˆ์•ˆ์ •</strong>
374
+ ์‚ฌ๋ฉด ๊ฒฝ์‚ฌ์™€ ์ง€ํ•˜์ˆ˜ ์ƒ์Šน์ด ์ž„๊ณ„ ์ƒํƒœ๋ฅผ ๋งŒ๋“ค๊ณ  ๋ถ•๊ดดยทํ™œ๊ฐ•์ด ๋ฐœ์ƒํ•œ๋‹ค.
375
+ </div>
376
+ <div class="step">
377
+ <strong>4. ์นจ์‹ ยท ์šด๋ฐ˜</strong>
378
+ ํ•˜์ฒœยท๋น™ํ•˜ยทํŒŒ๋ž‘ยท๋ฐ”๋žŒ์ด ์ž…์ž๋ฅผ ๋–ผ์–ด๋‚ด์–ด ํ•˜๋ฅ˜๋‚˜ ํ•ด์•ˆ์œผ๋กœ ์ด๋™์‹œํ‚จ๋‹ค.
379
+ </div>
380
+ <div class="step">
381
+ <strong>5. ํ‡ด์  ยท ์ถ•์ </strong>
382
+ ์œ ์†/ํ’์†์ด ๊ฐ์†Œํ•˜๋Š” ์™„์‚ฌ๋ฉดยท๊ณก๋ฅ˜ ์•ˆ์ชฝยทํ•˜๊ตฌยทํ˜ธ์•ˆยท๋น™ํ•˜ ๋ง๋‹จ์— ์žฌ๋ฃŒ๊ฐ€ ์Œ“์ธ๋‹ค.
383
+ </div>
384
+ <div class="step">
385
+ <strong>6. ์žฌ๊ท ํ˜•</strong>
386
+ ์žฅ๊ธฐ์ ์œผ๋กœ ์œต๊ธฐ์™€ ์นจ์‹์ด ๋งž๋ฌผ๋ ค ํ‰์ค€ํ™”๊ฐ€ ์ง„ํ–‰๋˜๋ฉฐ, ์ƒˆ๋กœ์šด ์œต๊ธฐ๊ฐ€ ์‹œ์ž‘๋˜๋ฉด ์ฃผ๊ธฐ๊ฐ€ ๋ฐ˜๋ณต๋œ๋‹ค.
387
+ </div>
388
+ </div>
389
+ </section>
390
+
391
+ <section class="flow">
392
+ <div class="panel stack">
393
+ <div class="pill accent">๋‚ด์  ์ž‘์šฉ โ†’ ๊ฒฐ๊ณผ ์ง€ํ˜•</div>
394
+ <details>
395
+ <summary>์ง€๊ฐ ์ˆ˜์ถ•ยท์ธ์žฅ</summary>
396
+ <p class="note">
397
+ ์กฐ์‚ฐ๋Œ€: ์Šต๊ณก์‚ฐ๋งฅ, ์ถฉ์ƒ๋‹จ์ธต, ์Šต๊ณก ๊ณก์ •. <br />
398
+ ์ธ์žฅ๋Œ€: ๊ทธ๋žฉ๋ฒค(๋ฅต), ํ˜ธ๋ฅด์ŠคํŠธ, ๋ฆฌํ”„ํŠธ ๊ณ„๊ณก. <br />
399
+ ์ „๋‹จ๋Œ€: ์‚ฌ๊ต๋‹จ์ธต, ํŽธ๋งˆ์•” ํŽธ๋ฆฌ ๋ฐœ๋‹ฌ.
400
+ </p>
401
+ </details>
402
+ <details>
403
+ <summary>ํ™”์‚ฐ / ์—ด์ˆ˜ ํ™œ๋™</summary>
404
+ <p class="note">
405
+ ์šฉ์•”๋Œ€์ง€, ์ˆœ์ƒยท๋ณตํ•ฉยท์‘ํšŒํ™”์‚ฐ, ์นผ๋ฐ๋ผ, ํ™”์‚ฐ๋”, ์—ด์ˆ˜ ์˜จ์ฒœ ํ…Œ๋ผ์Šค.
406
+ </p>
407
+ </details>
408
+ </div>
409
+
410
+ <div class="panel stack">
411
+ <div class="pill cool">์™ธ์  ์ž‘์šฉ โ†’ ๊ฒฐ๊ณผ ์ง€ํ˜•</div>
412
+ <details open>
413
+ <summary>ํ’ํ™” + ์ค‘๋ ฅ</summary>
414
+ <p class="note">
415
+ ์ ˆ๋ฆฌ ๋ฐœ๋‹ฌ ์•”๊ดด๋ฅ˜, ํ† ๋ฅด, ํƒ€ํ”„๋‹ˆ(์ฐจ๋ณ„ํ’ํ™”), ํƒค๋Ÿฌ์Šค ์‚ฌ๋ฉด, ์Šคํฌ๋ฆฌ ์ฝ˜.
416
+ </p>
417
+ </details>
418
+ <details>
419
+ <summary>ํ•˜์ฒœ ยท ํ˜ธ์ˆ˜</summary>
420
+ <p class="note">
421
+ ์ˆ˜์‹๊ณก, V์ž๊ณก, ํฌํŠธํ™€, ๊ณก๋ฅ˜ยท์šฐ๊ฐํ˜ธ, ๋ฒ”๋žŒ์›, ํ•˜์ฒœ๋‹จ๊ตฌ, ์‚ผ๊ฐ์ฃผ/์„ ์ƒ์ง€.
422
+ </p>
423
+ </details>
424
+ <details>
425
+ <summary>ํ•ด์•ˆ</summary>
426
+ <p class="note">
427
+ ํ•ด์‹์• ยทํ•ด์‹๋Œ€, ํŒŒ์‹๋Œ€์ง€, ํ•ด์‹๋™/์ฃผ์ƒ์ ˆ๋ฆฌ ์ ˆ๋ฒฝ, ์‹œ์Šคํƒยท์•„์น˜,
428
+ ์‚ฌ๋นˆยท์„ํ˜ธยท์‚ฌ์ทจยทํ†ฐ๋ณผ๋กœ.
429
+ </p>
430
+ </details>
431
+ <details>
432
+ <summary>๋น™ํ•˜</summary>
433
+ <p class="note">
434
+ U์ž๊ณก, ์„œํ‚ทยท์•„๋ ˆํŠธยทํ˜ผ, ํ˜„๋ฌด์•”์ฃผ์ƒ ๋ฐ ๋กœ์Šˆ๋ฌดํ†ค๋„ค,
435
+ ๋ชจ๋ ˆ์ธ(์ข…ยท๋‹จยท์ธกยทํ‡ด์„์„ฑ), ๋“œ๋Ÿผ๋ฆฐ, ์—์Šค์ปค, ์ผ€์ž„.
436
+ </p>
437
+ </details>
438
+ <details>
439
+ <summary>๊ฑด์กฐ ยท ํ’์‹</summary>
440
+ <p class="note">
441
+ ํฌํŠธํ™€ํ˜• ์‚ฌ๋ง‰ํฌํŠธ, ๋ฒ„ํ„ฐ๋งํฌ, ์‚ฌ๊ตฌ(๋ฐ”๋ฅดํ•œยทํŒŒ๋ผ๋ณผ๋ฆญยท์Šคํƒ€), ๋ฐํ”Œ๋ ˆ์ด์…˜ ์„ค,
442
+ ๋ฒค์น˜ํ˜• ํŽ˜๋ฐ์Šคํƒˆ, ๋ฉ”์‚ฌยท๋ทฐํŠธ.
443
+ </p>
444
+ </details>
445
+ </div>
446
+
447
+ <div class="panel stack">
448
+ <div class="pill green">๋ณ€์ˆ˜์™€ ๊ด€์ </div>
449
+ <details open>
450
+ <summary>๊ธฐํ›„</summary>
451
+ <p class="note">
452
+ ์ˆ˜๋ถ„ยท์˜จ๋„ ์ง„ํญ์ด ํ’ํ™” ์–‘์‹์„ ๊ฒฐ์ •ํ•˜๊ณ , ๋ชฌ์ˆœ/ํญ์šฐ๋Š” ์‚ฌ๋ฉด ํŒŒ๊ดด์™€ ํ•˜์ฒœ
453
+ ์—๋„ˆ์ง€๋ฅผ ์ฆํญ์‹œํ‚จ๋‹ค.
454
+ </p>
455
+ </details>
456
+ <details>
457
+ <summary>์•”์„ํ•™</summary>
458
+ <p class="note">
459
+ ์•”์„ ๊ฐ•๋„ยท๊ท ์งˆ์„ฑยท์ ˆ๋ฆฌ ๊ฐ„๊ฒฉ์ด ์นจ์‹ ์ €ํ•ญ์„ ์ขŒ์šฐํ•˜๋ฉฐ ์ฐจ๋ณ„์นจ์‹ ์ง€ํ˜•์„ ๋งŒ๋“ ๋‹ค.
460
+ </p>
461
+ </details>
462
+ <details>
463
+ <summary>์‹œ๊ฐ„ ์Šค์ผ€์ผ</summary>
464
+ <p class="note">
465
+ ์ˆœ๊ฐ„: ๋‚™์„ยท์‚ฌํƒœยทํ™”์‚ฐ ๋ถ„์ถœ. <br />
466
+ ์ˆ˜~์ˆ˜๋งŒ ๋…„: ๊ณก๋ฅ˜ ๋ฐœ๋‹ฌ, ๋น™ํ•˜ ์ „์ง„ยทํ›„ํ‡ด. <br />
467
+ ๋ฐฑ๋งŒ ๋…„+: ์œต๊ธฐยทํ‰์ค€ํ™”(ํŽ˜๋‹ˆํ”Œ๋ ˆ์ธ) ์‚ฌ์ดํด.
468
+ </p>
469
+ </details>
470
+ </div>
471
+ </section>
472
+ </div>
473
+ </body>
474
+ </html>
terrain_processes_3d.html ADDED
The diff for this file is too large to render. See raw diff
 
terrain_processes_stages.html ADDED
The diff for this file is too large to render. See raw diff