Spaces:
Running on Zero
Running on Zero
Front map: systems orbit a central star as live mini solar-systems
Browse filesReplace the scattered chart layout with a circular orbit: the 7 systems are
evenly spaced on a slowly revolving ring around a central pulsing star, with
labels counter-rotated to stay upright and the orbit pausing on hover. Each
system is now a hand-built animated SVG (rotating gears/rays/rune-ticks,
planet-dots orbiting at varied speeds, pulsing suns). Borderless and
theme-tinted; collapses to a tappable 2-up grid on mobile. Navigation runs
through the JS click-bridge; on_nav no-ops the empty load-time value so the map
is not auto-navigated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -11,6 +11,7 @@ adapted from the Coppermind wiki (CC BY-NC-SA). Unofficial fan project, not
|
|
| 11 |
affiliated with or endorsed by Brandon Sanderson or Dragonsteel.
|
| 12 |
"""
|
| 13 |
|
|
|
|
| 14 |
import re
|
| 15 |
from threading import Thread
|
| 16 |
|
|
@@ -141,17 +142,174 @@ def build_orrery(shard, mode="full"):
|
|
| 141 |
)
|
| 142 |
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
for i, sid in enumerate(SHARD_ORDER):
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
| 150 |
delay = (i * 0.7) % 4
|
| 151 |
-
|
| 152 |
-
f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
)
|
| 154 |
-
return
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
|
| 157 |
def build_system_panel(shard):
|
|
@@ -199,6 +357,20 @@ def enter_shard(shard_id):
|
|
| 199 |
)
|
| 200 |
|
| 201 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
def travel_home():
|
| 203 |
"""Clear the active voice and return to the star map."""
|
| 204 |
return (
|
|
@@ -277,34 +449,82 @@ CSS = """
|
|
| 277 |
#codex-header .intro { color:#9c97bd; font-size:.95rem; max-width:660px; margin:8px auto 0; }
|
| 278 |
#map-hint { text-align:center; color:#8d88ad; font-size:.9rem; margin:4px 0 6px; letter-spacing:.04em; }
|
| 279 |
|
| 280 |
-
/*
|
| 281 |
-
#
|
| 282 |
-
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
}
|
| 285 |
-
.
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
animation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
}
|
| 290 |
-
.
|
| 291 |
-
|
| 292 |
}
|
| 293 |
-
.
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
}
|
| 303 |
-
.sys-emblem:hover .sys-img-comp img { transform:scale(1.08); filter:drop-shadow(0 0 18px var(--c)); }
|
| 304 |
-
.sys-label { margin-top:6px; pointer-events:none; }
|
| 305 |
-
.sys-name { font:600 .8rem/1.25 'Georgia',serif; color:#e9e4f6; letter-spacing:.04em; }
|
| 306 |
-
.sys-shard { font:italic 1rem/1.25 'Georgia',serif; color:var(--c); text-shadow:0 0 12px var(--c); }
|
| 307 |
-
@keyframes floaty { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-11px); } }
|
| 308 |
|
| 309 |
/* ---- orrery ---- */
|
| 310 |
.orrery { position:relative; margin:0 auto; }
|
|
@@ -374,7 +594,32 @@ CSS = """
|
|
| 374 |
/* footer */
|
| 375 |
#codex-footer { text-align:center; color:#7d789c; font-size:.78rem; margin:22px auto 8px; max-width:760px; line-height:1.5; }
|
| 376 |
#codex-footer a { color:#a89ce0; }
|
| 377 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
|
| 379 |
|
| 380 |
# ---------------------------------------------------------------------------- #
|
|
@@ -382,6 +627,7 @@ CSS = """
|
|
| 382 |
# ---------------------------------------------------------------------------- #
|
| 383 |
with gr.Blocks(title="Cosmere Codex") as demo:
|
| 384 |
active_shard = gr.State("")
|
|
|
|
| 385 |
|
| 386 |
gr.HTML(
|
| 387 |
"""
|
|
@@ -398,24 +644,7 @@ with gr.Blocks(title="Cosmere Codex") as demo:
|
|
| 398 |
# ---------------------------- MAP VIEW ---------------------------------- #
|
| 399 |
with gr.Column(visible=True) as map_view:
|
| 400 |
gr.HTML('<div id="map-hint">✦ Click a floating system to enter ✦</div>')
|
| 401 |
-
|
| 402 |
-
emblems = {}
|
| 403 |
-
for sid in SHARD_ORDER:
|
| 404 |
-
s = SHARDS[sid]
|
| 405 |
-
with gr.Column(elem_id=f"emb-{sid}", elem_classes=["sys-emblem"], min_width=140):
|
| 406 |
-
emblems[sid] = gr.Image(
|
| 407 |
-
value=s["map_image"],
|
| 408 |
-
elem_classes=["sys-img-comp"],
|
| 409 |
-
show_label=False,
|
| 410 |
-
interactive=False,
|
| 411 |
-
container=False,
|
| 412 |
-
buttons=[],
|
| 413 |
-
)
|
| 414 |
-
gr.HTML(
|
| 415 |
-
f'<div class="sys-label">'
|
| 416 |
-
f'<div class="sys-name">{s["system"]}</div>'
|
| 417 |
-
f'<div class="sys-shard">{s["name"]}</div></div>'
|
| 418 |
-
)
|
| 419 |
|
| 420 |
# -------------------------- CONVERSATION VIEW --------------------------- #
|
| 421 |
with gr.Column(visible=False) as chat_view:
|
|
@@ -457,14 +686,10 @@ with gr.Blocks(title="Cosmere Codex") as demo:
|
|
| 457 |
shard_skin, system_orrery, user_box, send_btn,
|
| 458 |
]
|
| 459 |
|
| 460 |
-
# A click
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
return _enter
|
| 465 |
-
|
| 466 |
-
for sid, img in emblems.items():
|
| 467 |
-
img.select(_make_enter(sid), inputs=None, outputs=nav_outputs)
|
| 468 |
|
| 469 |
travel_btn.click(
|
| 470 |
travel_home, inputs=None,
|
|
@@ -476,4 +701,4 @@ with gr.Blocks(title="Cosmere Codex") as demo:
|
|
| 476 |
|
| 477 |
|
| 478 |
if __name__ == "__main__":
|
| 479 |
-
demo.queue().launch(css=CSS, theme=gr.themes.Base())
|
|
|
|
| 11 |
affiliated with or endorsed by Brandon Sanderson or Dragonsteel.
|
| 12 |
"""
|
| 13 |
|
| 14 |
+
import math
|
| 15 |
import re
|
| 16 |
from threading import Thread
|
| 17 |
|
|
|
|
| 142 |
)
|
| 143 |
|
| 144 |
|
| 145 |
+
# ---------------------------------------------------------------------------- #
|
| 146 |
+
# Front-page system symbols — hand-built vector glyphs, one per system.
|
| 147 |
+
# Each <svg> is borderless and uses currentColor (the system's theme color, set
|
| 148 |
+
# via .sys-symbol{color:var(--c)}) so it tints + glows to match the voice.
|
| 149 |
+
# ---------------------------------------------------------------------------- #
|
| 150 |
+
def _ring(r, sw=1.0, op=0.7, color="currentColor"):
|
| 151 |
+
return (f'<circle cx="50" cy="50" r="{r}" fill="none" '
|
| 152 |
+
f'stroke="{color}" stroke-width="{sw}" opacity="{op}"/>')
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _around(r, n, fn, phase=0.0):
|
| 156 |
+
"""Place n items evenly around a circle of radius r; fn(x, y, i) -> svg."""
|
| 157 |
+
out = []
|
| 158 |
+
for i in range(n):
|
| 159 |
+
a = phase + i * (2 * math.pi / n)
|
| 160 |
+
out.append(fn(50 + r * math.cos(a), 50 + r * math.sin(a), i))
|
| 161 |
+
return "".join(out)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _rays(r0, r1, n, sw=1.4, color="currentColor", op=0.9):
|
| 165 |
+
def line(_x, _y, i):
|
| 166 |
+
a = i * (2 * math.pi / n)
|
| 167 |
+
x0, y0 = 50 + r0 * math.cos(a), 50 + r0 * math.sin(a)
|
| 168 |
+
x1, y1 = 50 + r1 * math.cos(a), 50 + r1 * math.sin(a)
|
| 169 |
+
return (f'<line x1="{x0:.1f}" y1="{y0:.1f}" x2="{x1:.1f}" y2="{y1:.1f}" '
|
| 170 |
+
f'stroke="{color}" stroke-width="{sw}" opacity="{op}"/>')
|
| 171 |
+
return _around(r0, n, line)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def _dots(r, n, rad, color="currentColor", op=0.95, phase=0.0):
|
| 175 |
+
return _around(
|
| 176 |
+
r, n,
|
| 177 |
+
lambda x, y, i: f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{rad}" fill="{color}" opacity="{op}"/>',
|
| 178 |
+
phase,
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def _svg(body):
|
| 183 |
+
return (f'<svg class="sym-art" viewBox="0 0 100 100" '
|
| 184 |
+
f'xmlns="http://www.w3.org/2000/svg">{body}</svg>')
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _spin(body, sp, rev=False):
|
| 188 |
+
"""Wrap SVG in a group that rotates about the symbol center (view-box origin)."""
|
| 189 |
+
cls = "spin-vb spin-rev" if rev else "spin-vb"
|
| 190 |
+
return f'<g class="{cls}" style="--sp:{sp}s">{body}</g>'
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def _pulse(body, pp=4.5):
|
| 194 |
+
return f'<g class="pulse" style="--pp:{pp}s">{body}</g>'
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def build_symbol(sid):
|
| 198 |
+
"""Return an inline SVG glyph for a system — a little moving solar system:
|
| 199 |
+
rings hold, planet-dots/rays/gears orbit at varied speeds, suns pulse."""
|
| 200 |
+
c2 = SHARDS[sid]["accent"]
|
| 201 |
+
sun = '<circle cx="50" cy="50" r="6.5" fill="#fff7e0"/>'
|
| 202 |
+
|
| 203 |
+
if sid == "drominad": # Hoid — concentric rings, orbiting motes, pulsing sun
|
| 204 |
+
body = (
|
| 205 |
+
"".join(_ring(r, 1, 0.55) for r in (45, 38, 31, 24, 17))
|
| 206 |
+
+ _pulse(sun + _ring(7.5, 1.4, 0.9), 5)
|
| 207 |
+
+ _spin(_dots(45, 1, 2.2), 40)
|
| 208 |
+
+ _spin(_dots(31, 1, 2.0, phase=2.1), 28, rev=True)
|
| 209 |
+
+ _spin(_dots(24, 1, 1.8, phase=1.0), 34)
|
| 210 |
+
)
|
| 211 |
+
return _svg(body)
|
| 212 |
+
|
| 213 |
+
if sid == "harmony": # Scadrian — turning clockwork gear + radiant pulsing sun
|
| 214 |
+
gear = ('<circle cx="50" cy="50" r="40" fill="none" stroke="currentColor" '
|
| 215 |
+
'stroke-width="6" stroke-dasharray="4.2 4.2" opacity="0.85"/>')
|
| 216 |
+
body = (
|
| 217 |
+
_spin(gear, 46)
|
| 218 |
+
+ _ring(31, 1, 0.5)
|
| 219 |
+
+ _spin(_rays(13, 23, 16, 1.2, "#fde6b0", 0.85), 22, rev=True)
|
| 220 |
+
+ _pulse('<circle cx="50" cy="50" r="11" fill="#f6e2a0"/>' + _ring(11, 1.5, 0.95), 4.5)
|
| 221 |
+
)
|
| 222 |
+
return _svg(body)
|
| 223 |
+
|
| 224 |
+
if sid == "devotion_dominion": # Selish — sigil ring of orbiting nodes + dual core
|
| 225 |
+
dual = (
|
| 226 |
+
'<circle cx="44" cy="50" r="10" fill="none" stroke="currentColor" stroke-width="1.6" opacity="0.95"/>'
|
| 227 |
+
f'<circle cx="56" cy="50" r="10" fill="none" stroke="{c2}" stroke-width="1.6" opacity="0.95"/>'
|
| 228 |
+
'<circle cx="50" cy="50" r="2.4" fill="#eaf3ff"/>'
|
| 229 |
+
)
|
| 230 |
+
body = (
|
| 231 |
+
_ring(43, 1, 0.7) + _ring(35, 1, 0.45)
|
| 232 |
+
+ _spin(_dots(39, 8, 2.0, op=0.85), 52)
|
| 233 |
+
+ _pulse(dual, 5.5)
|
| 234 |
+
)
|
| 235 |
+
return _svg(body)
|
| 236 |
+
|
| 237 |
+
if sid == "autonomy": # Taldain — slowly spinning figure-8 binary loops
|
| 238 |
+
big = (
|
| 239 |
+
'<circle cx="50" cy="34" r="19" fill="none" stroke="currentColor" stroke-width="2" opacity="0.9"/>'
|
| 240 |
+
'<circle cx="50" cy="66" r="19" fill="none" stroke="currentColor" stroke-width="2" opacity="0.9"/>'
|
| 241 |
+
)
|
| 242 |
+
inner = (
|
| 243 |
+
'<circle cx="50" cy="34" r="13" fill="none" stroke="currentColor" stroke-width="1" opacity="0.5"/>'
|
| 244 |
+
'<circle cx="50" cy="66" r="13" fill="none" stroke="currentColor" stroke-width="1" opacity="0.5"/>'
|
| 245 |
+
'<circle cx="50" cy="34" r="3" fill="#fff"/>'
|
| 246 |
+
'<circle cx="50" cy="66" r="3" fill="#cfe0ff"/>'
|
| 247 |
+
)
|
| 248 |
+
body = _spin(big, 55) + _spin(inner, 34, rev=True)
|
| 249 |
+
return _svg(body)
|
| 250 |
+
|
| 251 |
+
if sid == "endowment": # Nalthian — iridescent rings, each planet on its own orbit
|
| 252 |
+
cols = ["#e85aa0", "#f2c14e", "#5ad1c8", "#7aa0ff"]
|
| 253 |
+
radii = [44, 35, 26, 17]
|
| 254 |
+
speeds = [40, 30, 22, 15]
|
| 255 |
+
body = "".join(_ring(r, 1.4, 0.85, col) for r, col in zip(radii, cols))
|
| 256 |
+
body += "".join(
|
| 257 |
+
_spin(_dots(r, 1, 2.4, col, phase=i * 1.3), sp, rev=(i % 2 == 1))
|
| 258 |
+
for i, (r, col, sp) in enumerate(zip(radii, cols, speeds))
|
| 259 |
+
)
|
| 260 |
+
body += _pulse('<circle cx="50" cy="50" r="7" fill="#fff6ef"/>', 4)
|
| 261 |
+
return _svg(body)
|
| 262 |
+
|
| 263 |
+
if sid in ("odium", "cultivation"): # Rosharan — turning rune ring + glowing core
|
| 264 |
+
body = (
|
| 265 |
+
_ring(44, 1, 0.7) + _ring(36, 1, 0.55)
|
| 266 |
+
+ _spin(_rays(36, 44, 28, 1.0, "currentColor", 0.6), 64) # rune ticks
|
| 267 |
+
+ _ring(21, 1, 0.5)
|
| 268 |
+
+ _spin(_dots(28, 3, 1.6, "currentColor", op=0.7), 30, rev=True)
|
| 269 |
+
+ _pulse('<circle cx="50" cy="50" r="8" fill="currentColor" opacity="0.9"/>'
|
| 270 |
+
'<circle cx="50" cy="50" r="3.4" fill="#fff" opacity="0.85"/>', 4.2)
|
| 271 |
+
)
|
| 272 |
+
return _svg(body)
|
| 273 |
+
|
| 274 |
+
return _svg(_ring(40) + sun) # fallback
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def build_core():
|
| 278 |
+
"""The central star the systems orbit (a nod to Adonalsium). Non-clickable."""
|
| 279 |
+
return _svg(
|
| 280 |
+
_spin(_rays(20, 33, 24, 1.0, "#ffe7a8", 0.55), 90)
|
| 281 |
+
+ '<circle cx="50" cy="50" r="30" fill="#f7d98a" opacity="0.10"/>'
|
| 282 |
+
+ _pulse('<circle cx="50" cy="50" r="13" fill="#fff2c8"/>'
|
| 283 |
+
'<circle cx="50" cy="50" r="13" fill="none" stroke="#ffe7a8" stroke-width="1.5"/>', 5)
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def build_cosmos_scene():
|
| 288 |
+
"""The front page: systems arranged in a circle, slowly orbiting a central star,
|
| 289 |
+
each one its own little moving solar system."""
|
| 290 |
+
n = len(SHARD_ORDER)
|
| 291 |
+
slots = []
|
| 292 |
for i, sid in enumerate(SHARD_ORDER):
|
| 293 |
+
s = SHARDS[sid]
|
| 294 |
+
angle = -90 + i * 360.0 / n # start at top, even spacing
|
| 295 |
+
sz = 132
|
| 296 |
+
dur = 7 + (i % 4) * 1.3 # gentle, varied bob
|
| 297 |
delay = (i * 0.7) % 4
|
| 298 |
+
slots.append(
|
| 299 |
+
f'<div class="slot" style="--a:{angle:.2f}deg">'
|
| 300 |
+
f'<div class="deslot"><div class="counter">'
|
| 301 |
+
f'<div class="sys-symbol" data-shard="{sid}" '
|
| 302 |
+
f'style="--c:{s["color"]};--c2:{s["accent"]};--sz:{sz}px;'
|
| 303 |
+
f'--dur:{dur}s;--delay:-{delay}s;">'
|
| 304 |
+
f'<div class="sym-float">{build_symbol(sid)}'
|
| 305 |
+
f'<div class="sys-label"><div class="sys-name">{s["system"]}</div>'
|
| 306 |
+
f'<div class="sys-shard">{s["name"]}</div></div></div>'
|
| 307 |
+
f'</div></div></div></div>'
|
| 308 |
)
|
| 309 |
+
return (
|
| 310 |
+
f'<div id="cosmos"><div class="ring">{"".join(slots)}</div>'
|
| 311 |
+
f'<div class="core-star">{build_core()}</div></div>'
|
| 312 |
+
)
|
| 313 |
|
| 314 |
|
| 315 |
def build_system_panel(shard):
|
|
|
|
| 357 |
)
|
| 358 |
|
| 359 |
|
| 360 |
+
def on_nav(value):
|
| 361 |
+
"""Handle a click from the floating map, relayed via the hidden JS proxy box.
|
| 362 |
+
|
| 363 |
+
The value is "<shard_id>|<nonce>"; the nonce guarantees a fresh change event
|
| 364 |
+
even when the same system is chosen twice in a row. Gradio also emits an
|
| 365 |
+
empty value on initial page load — we treat that (and any unknown id) as a
|
| 366 |
+
no-op so the map is not auto-navigated."""
|
| 367 |
+
sid = (value or "").split("|", 1)[0]
|
| 368 |
+
if sid not in SHARDS:
|
| 369 |
+
return (gr.update(), gr.update(), "", gr.update(),
|
| 370 |
+
gr.update(), gr.update(), gr.update(), gr.update())
|
| 371 |
+
return enter_shard(sid)
|
| 372 |
+
|
| 373 |
+
|
| 374 |
def travel_home():
|
| 375 |
"""Clear the active voice and return to the star map."""
|
| 376 |
return (
|
|
|
|
| 449 |
#codex-header .intro { color:#9c97bd; font-size:.95rem; max-width:660px; margin:8px auto 0; }
|
| 450 |
#map-hint { text-align:center; color:#8d88ad; font-size:.9rem; margin:4px 0 6px; letter-spacing:.04em; }
|
| 451 |
|
| 452 |
+
/* hidden proxy that the JS click-bridge writes into to trigger navigation */
|
| 453 |
+
#nav-proxy { position:absolute !important; left:-9999px !important; top:0 !important;
|
| 454 |
+
width:1px !important; height:1px !important; overflow:hidden !important;
|
| 455 |
+
opacity:0 !important; pointer-events:none !important; }
|
| 456 |
+
|
| 457 |
+
/* ---- systems in a circle, orbiting a central star (a "moving solar system") ---- */
|
| 458 |
+
#cosmos { position:relative; height:clamp(470px, 50vw, 580px); margin:0 auto; }
|
| 459 |
+
|
| 460 |
+
/* the ring slowly revolves; .counter spins back so each symbol stays upright */
|
| 461 |
+
.ring { position:absolute; inset:0; animation:ring-spin var(--T,150s) linear infinite; }
|
| 462 |
+
.slot {
|
| 463 |
+
position:absolute; left:50%; top:50%; width:0; height:0;
|
| 464 |
+
transform:rotate(var(--a)) translate(var(--R,205px));
|
| 465 |
}
|
| 466 |
+
.deslot { position:absolute; left:0; top:0; width:0; height:0;
|
| 467 |
+
transform:rotate(calc(-1 * var(--a))); }
|
| 468 |
+
.counter { position:absolute; left:0; top:0; width:0; height:0;
|
| 469 |
+
animation:ring-spin var(--T,150s) linear infinite reverse; }
|
| 470 |
+
/* hovering the map gently pauses the orbit so a symbol is easy to read/click */
|
| 471 |
+
#cosmos:hover .ring, #cosmos:hover .counter { animation-play-state:paused; }
|
| 472 |
+
@keyframes ring-spin { to { transform:rotate(360deg); } }
|
| 473 |
+
|
| 474 |
+
.sys-symbol {
|
| 475 |
+
position:absolute; left:0; top:0; width:var(--sz);
|
| 476 |
+
transform:translate(-50%,-50%); cursor:pointer; color:var(--c); text-align:center;
|
| 477 |
}
|
| 478 |
+
.sym-float {
|
| 479 |
+
animation:floaty var(--dur,8s) ease-in-out infinite; animation-delay:var(--delay,0s);
|
| 480 |
}
|
| 481 |
+
.sym-art {
|
| 482 |
+
display:block; width:100%; height:auto; overflow:visible;
|
| 483 |
+
filter:drop-shadow(0 0 5px color-mix(in srgb, var(--c) 45%, transparent));
|
| 484 |
+
transition:transform .35s ease, filter .35s ease;
|
| 485 |
+
}
|
| 486 |
+
.sys-symbol:hover { z-index:5; }
|
| 487 |
+
.sys-symbol:hover .sym-art { transform:scale(1.12); filter:drop-shadow(0 0 22px var(--c)); }
|
| 488 |
+
.sys-label { margin-top:2px; pointer-events:none; transition:opacity .35s ease; }
|
| 489 |
+
.sys-name { font:600 .78rem/1.25 'Georgia',serif; color:#d7d2ea; letter-spacing:.04em; }
|
| 490 |
+
.sys-shard { font:italic 1.02rem/1.25 'Georgia',serif; color:var(--c); text-shadow:0 0 12px var(--c); }
|
| 491 |
+
.sys-symbol:hover .sys-name { color:#f3eeff; }
|
| 492 |
+
@keyframes floaty { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-9px); } }
|
| 493 |
+
|
| 494 |
+
/* central star */
|
| 495 |
+
.core-star {
|
| 496 |
+
position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
|
| 497 |
+
width:128px; height:128px; pointer-events:none; color:#ffe7a8;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
/* inner symbol motion: groups that orbit / pulse about the symbol center */
|
| 501 |
+
.spin-vb { transform-box:view-box; transform-origin:50px 50px;
|
| 502 |
+
animation:svg-spin var(--sp,30s) linear infinite; }
|
| 503 |
+
.spin-vb.spin-rev { animation-direction:reverse; }
|
| 504 |
+
.pulse { transform-box:fill-box; transform-origin:center;
|
| 505 |
+
animation:sun-pulse var(--pp,4.5s) ease-in-out infinite; }
|
| 506 |
+
@keyframes svg-spin { to { transform:rotate(360deg); } }
|
| 507 |
+
@keyframes sun-pulse { 0%,100% { opacity:.9; transform:scale(1); }
|
| 508 |
+
50% { opacity:1; transform:scale(1.14); } }
|
| 509 |
+
|
| 510 |
+
/* mobile: unwind the circle into a clean, tappable 2-up grid */
|
| 511 |
+
@media (max-width:760px) {
|
| 512 |
+
#cosmos { height:auto; }
|
| 513 |
+
.ring {
|
| 514 |
+
position:static; inset:auto; transform:none !important; animation:none !important;
|
| 515 |
+
display:flex; flex-wrap:wrap; justify-content:center; align-items:flex-start;
|
| 516 |
+
gap:16px 4%; padding:6px 0 12px;
|
| 517 |
+
}
|
| 518 |
+
.slot, .deslot, .counter {
|
| 519 |
+
position:static; width:auto; height:auto;
|
| 520 |
+
transform:none !important; animation:none !important; display:contents;
|
| 521 |
+
}
|
| 522 |
+
.sys-symbol {
|
| 523 |
+
position:static !important; transform:none !important;
|
| 524 |
+
width:43% !important; max-width:200px;
|
| 525 |
+
}
|
| 526 |
+
.core-star { display:none; }
|
| 527 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
|
| 529 |
/* ---- orrery ---- */
|
| 530 |
.orrery { position:relative; margin:0 auto; }
|
|
|
|
| 594 |
/* footer */
|
| 595 |
#codex-footer { text-align:center; color:#7d789c; font-size:.78rem; margin:22px auto 8px; max-width:760px; line-height:1.5; }
|
| 596 |
#codex-footer a { color:#a89ce0; }
|
| 597 |
+
"""
|
| 598 |
+
|
| 599 |
+
# Injected into <head> so it runs as a real script: one document-level click
|
| 600 |
+
# delegate that turns any element carrying data-shard into a navigation event by
|
| 601 |
+
# writing "<id>|<nonce>" into the hidden nav-proxy textbox and firing its input.
|
| 602 |
+
HEAD_JS = """
|
| 603 |
+
<script>
|
| 604 |
+
(function () {
|
| 605 |
+
function go(id) {
|
| 606 |
+
var t = document.querySelector('#nav-proxy textarea') ||
|
| 607 |
+
document.querySelector('#nav-proxy input');
|
| 608 |
+
if (!t) { return; }
|
| 609 |
+
var proto = (t.tagName === 'TEXTAREA')
|
| 610 |
+
? window.HTMLTextAreaElement.prototype
|
| 611 |
+
: window.HTMLInputElement.prototype;
|
| 612 |
+
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
| 613 |
+
setter.call(t, id + '|' + Date.now());
|
| 614 |
+
t.dispatchEvent(new Event('input', { bubbles: true }));
|
| 615 |
+
}
|
| 616 |
+
document.addEventListener('click', function (e) {
|
| 617 |
+
var el = e.target.closest ? e.target.closest('[data-shard]') : null;
|
| 618 |
+
if (el) { go(el.getAttribute('data-shard')); }
|
| 619 |
+
});
|
| 620 |
+
})();
|
| 621 |
+
</script>
|
| 622 |
+
"""
|
| 623 |
|
| 624 |
|
| 625 |
# ---------------------------------------------------------------------------- #
|
|
|
|
| 627 |
# ---------------------------------------------------------------------------- #
|
| 628 |
with gr.Blocks(title="Cosmere Codex") as demo:
|
| 629 |
active_shard = gr.State("")
|
| 630 |
+
nav_proxy = gr.Textbox(elem_id="nav-proxy", show_label=False)
|
| 631 |
|
| 632 |
gr.HTML(
|
| 633 |
"""
|
|
|
|
| 644 |
# ---------------------------- MAP VIEW ---------------------------------- #
|
| 645 |
with gr.Column(visible=True) as map_view:
|
| 646 |
gr.HTML('<div id="map-hint">✦ Click a floating system to enter ✦</div>')
|
| 647 |
+
gr.HTML(build_cosmos_scene())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
|
| 649 |
# -------------------------- CONVERSATION VIEW --------------------------- #
|
| 650 |
with gr.Column(visible=False) as chat_view:
|
|
|
|
| 686 |
shard_skin, system_orrery, user_box, send_btn,
|
| 687 |
]
|
| 688 |
|
| 689 |
+
# A click on any floating symbol writes "<id>|<nonce>" into nav_proxy (via the
|
| 690 |
+
# head script's click delegate); .change handles it. on_nav no-ops the empty
|
| 691 |
+
# value Gradio emits at load, so the map is not auto-navigated.
|
| 692 |
+
nav_proxy.change(on_nav, inputs=nav_proxy, outputs=nav_outputs)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
|
| 694 |
travel_btn.click(
|
| 695 |
travel_home, inputs=None,
|
|
|
|
| 701 |
|
| 702 |
|
| 703 |
if __name__ == "__main__":
|
| 704 |
+
demo.queue().launch(css=CSS, theme=gr.themes.Base(), head=HEAD_JS)
|