|
|
| 第1行: |
第1行: |
| <html>
| |
| <!doctype html>
| |
| <html lang="zh-CN">
| |
| <head>
| |
| <meta charset="utf-8">
| |
| <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
| |
| <title>冬の花 · 第一束信号</title>
| |
| <style>
| |
| :root{
| |
| --bg0:#030611;
| |
| --bg1:#071327;
| |
| --cyan:#63fff2;
| |
| --blue:#5ea7ff;
| |
| --violet:#a78bfa;
| |
| --gold:#ffd98a;
| |
| --red:#ff7d7d;
| |
| --green:#9dffca;
| |
| --text:#eff8ff;
| |
| --muted:rgba(239,248,255,.68);
| |
| --thin:rgba(255,255,255,.14);
| |
| --glass:rgba(8,18,36,.56);
| |
| --glass2:rgba(255,255,255,.075);
| |
| --shadow:0 24px 90px rgba(0,0,0,.55);
| |
| }
| |
| *{box-sizing:border-box}
| |
| html,body{width:100%;height:100%;margin:0;overflow:hidden;background:var(--bg0);color:var(--text);font-family:"Noto Serif SC","Songti SC","SimSun","Microsoft YaHei",serif}
| |
| button{font:inherit;color:inherit}
| |
| #stars,#veil{position:fixed;inset:0;width:100%;height:100%;display:block}
| |
| #stars{z-index:0;background:radial-gradient(circle at 72% 24%,rgba(54,130,255,.32),transparent 22%),radial-gradient(circle at 22% 72%,rgba(0,255,209,.14),transparent 31%),linear-gradient(135deg,#030611,#081528 52%,#02040c)}
| |
| #veil{z-index:1;pointer-events:none;mix-blend-mode:screen;opacity:.78}
| |
| #app{position:relative;z-index:2;height:100%;display:grid;grid-template-rows:auto 1fr auto;padding:22px clamp(16px,3vw,44px);isolation:isolate}
| |
| .topbar{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:44px;letter-spacing:.08em;text-transform:uppercase;font-family:ui-sans-serif,system-ui,"Microsoft YaHei",sans-serif;color:var(--muted);font-size:12px}
| |
| .brand{display:flex;align-items:center;gap:10px;white-space:nowrap}
| |
| .brand b{font-size:15px;color:#fff;font-weight:650;letter-spacing:.16em}
| |
| .dot{width:8px;height:8px;border-radius:50%;background:var(--cyan);box-shadow:0 0 18px var(--cyan),0 0 42px var(--blue);animation:pulse 1.55s ease-in-out infinite}
| |
| @keyframes pulse{0%,100%{transform:scale(.75);opacity:.65}50%{transform:scale(1.35);opacity:1}}
| |
| .status{display:flex;align-items:center;gap:12px;min-width:0}
| |
| .status span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
| |
| .progressLine{height:1px;flex:1;max-width:260px;background:var(--thin);position:relative;overflow:hidden;border-radius:99px}
| |
| .progressLine i{position:absolute;inset:0 auto 0 0;width:0;background:linear-gradient(90deg,var(--cyan),var(--gold));box-shadow:0 0 22px var(--cyan);transition:width .55s cubic-bezier(.2,.8,.2,1)}
| |
| .stageWrap{position:relative;height:100%;display:grid;grid-template-columns:minmax(0,1fr) minmax(270px,360px);gap:clamp(18px,3vw,42px);align-items:center}
| |
| .scene{position:relative;min-height:min(650px,72vh);border:1px solid var(--thin);background:linear-gradient(135deg,rgba(255,255,255,.08),rgba(255,255,255,.025));box-shadow:var(--shadow);border-radius:30px;overflow:hidden;backdrop-filter:blur(18px);display:grid;grid-template-columns:1fr;align-content:end;padding:clamp(22px,4vw,54px)}
| |
| .scene:before{content:"";position:absolute;inset:-20%;background:radial-gradient(circle at var(--mx,50%) var(--my,50%),rgba(99,255,242,.18),transparent 23%),linear-gradient(120deg,rgba(94,167,255,.16),transparent 40%,rgba(255,217,138,.09));transition:opacity .3s;pointer-events:none}
| |
| .scene:after{content:"";position:absolute;inset:0;background:repeating-linear-gradient(0deg,rgba(255,255,255,.035) 0,rgba(255,255,255,.035) 1px,transparent 1px,transparent 5px);opacity:.18;pointer-events:none}
| |
| .content{position:relative;z-index:2;max-width:980px}
| |
| .kicker{font-family:ui-sans-serif,system-ui,"Microsoft YaHei",sans-serif;font-size:12px;letter-spacing:.24em;text-transform:uppercase;color:var(--cyan);margin-bottom:14px;text-shadow:0 0 20px rgba(99,255,242,.55)}
| |
| h1{margin:0 0 16px;font-size:clamp(36px,7vw,92px);line-height:.98;font-weight:700;letter-spacing:-.04em;text-shadow:0 0 42px rgba(94,167,255,.42)}
| |
| .titleLine{display:block;transform:translateY(18px);opacity:0;animation:rise .72s cubic-bezier(.2,.9,.2,1) forwards}
| |
| .titleLine:nth-child(2){animation-delay:.08s}
| |
| @keyframes rise{to{transform:translateY(0);opacity:1}}
| |
| .text{font-size:clamp(17px,2.25vw,27px);line-height:1.86;letter-spacing:.02em;text-wrap:pretty;color:rgba(245,250,255,.94);min-height:8.5em;text-shadow:0 2px 26px rgba(0,0,0,.45)}
| |
| .text .caret{display:inline-block;width:.54em;color:var(--cyan);animation:blink .9s steps(1,end) infinite}
| |
| @keyframes blink{50%{opacity:0}}
| |
| .quote{display:inline-block;margin-top:8px;font-size:clamp(24px,4.8vw,58px);line-height:1.05;color:#fff;letter-spacing:.06em;font-weight:700;text-shadow:0 0 26px rgba(99,255,242,.58),0 0 58px rgba(255,217,138,.28)}
| |
| .side{height:min(650px,72vh);display:grid;grid-template-rows:auto 1fr auto;gap:14px}
| |
| .panel{border:1px solid var(--thin);background:linear-gradient(160deg,rgba(9,22,42,.62),rgba(255,255,255,.055));border-radius:24px;box-shadow:var(--shadow);backdrop-filter:blur(18px);overflow:hidden}
| |
| .signalPanel{padding:18px}
| |
| .signalHead{display:flex;align-items:center;justify-content:space-between;gap:12px;font-family:ui-sans-serif,system-ui,"Microsoft YaHei",sans-serif;font-size:12px;color:var(--muted);letter-spacing:.12em;text-transform:uppercase}
| |
| .orb{width:176px;height:176px;margin:24px auto 12px;border-radius:50%;position:relative;background:radial-gradient(circle at 38% 32%,rgba(219,255,252,.95),rgba(99,255,242,.42) 20%,rgba(10,52,79,.36) 43%,rgba(0,0,0,.18) 66%,rgba(255,217,138,.13));box-shadow:inset -18px -24px 70px rgba(0,0,0,.45),0 0 48px rgba(99,255,242,.24),0 0 92px rgba(94,167,255,.16);cursor:pointer;transition:transform .35s ease,filter .35s ease}
| |
| .orb:hover{transform:scale(1.045);filter:saturate(1.25)}
| |
| .orb:before,.orb:after{content:"";position:absolute;inset:-12px;border-radius:50%;border:1px solid rgba(99,255,242,.3);animation:ring 3.8s linear infinite}
| |
| .orb:after{inset:-28px;border-color:rgba(255,217,138,.2);animation-duration:6.2s;animation-direction:reverse}
| |
| @keyframes ring{to{transform:rotate(360deg)}}
| |
| .wave{height:72px;position:relative;margin:8px 0 4px;overflow:hidden;border-radius:14px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08)}
| |
| .wave svg{width:200%;height:100%;position:absolute;left:0;top:0;animation:slide 2.4s linear infinite}
| |
| @keyframes slide{to{transform:translateX(-50%)}}
| |
| .readout{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px;font-family:ui-sans-serif,system-ui,"Microsoft YaHei",sans-serif}
| |
| .readout div{padding:10px 11px;border:1px solid rgba(255,255,255,.09);border-radius:14px;background:rgba(255,255,255,.045)}
| |
| .readout small{display:block;color:var(--muted);font-size:10px;letter-spacing:.12em;text-transform:uppercase;margin-bottom:5px}
| |
| .readout b{font-size:15px;color:#fff;font-weight:650}
| |
| .hotPanel{position:relative;padding:18px;min-height:226px}
| |
| .hotTitle{font-family:ui-sans-serif,system-ui,"Microsoft YaHei",sans-serif;color:var(--muted);font-size:12px;letter-spacing:.14em;text-transform:uppercase;margin-bottom:12px}
| |
| .map{position:relative;height:166px;border-radius:18px;background:radial-gradient(circle at 20% 36%,rgba(255,125,125,.18),transparent 24%),radial-gradient(circle at 58% 60%,rgba(99,255,242,.13),transparent 26%),radial-gradient(circle at 78% 32%,rgba(255,217,138,.15),transparent 23%),rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.08);overflow:hidden}
| |
| .map:before{content:"";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.09),transparent);transform:translateX(-100%);animation:sweep 5.4s ease-in-out infinite}
| |
| @keyframes sweep{50%,100%{transform:translateX(100%)}}
| |
| .hot{position:absolute;width:14px;height:14px;border-radius:50%;border:1px solid rgba(255,255,255,.65);background:var(--cyan);box-shadow:0 0 16px var(--cyan);cursor:pointer;transform:translate(-50%,-50%)}
| |
| .hot:after{content:"";position:absolute;inset:-13px;border-radius:50%;border:1px solid currentColor;opacity:.6;animation:ping 1.9s ease-out infinite}
| |
| @keyframes ping{to{transform:scale(1.65);opacity:0}}
| |
| .hot[data-id="fleet"]{left:23%;top:37%;background:var(--red);box-shadow:0 0 16px var(--red);color:var(--red)}
| |
| .hot[data-id="flood"]{left:63%;top:66%;background:var(--blue);box-shadow:0 0 16px var(--blue);color:var(--blue)}
| |
| .hot[data-id="quake"]{left:79%;top:31%;background:var(--gold);box-shadow:0 0 16px var(--gold);color:var(--gold)}
| |
| .tooltip{position:absolute;left:18px;right:18px;bottom:16px;min-height:48px;padding:10px 12px;border-radius:14px;background:rgba(1,7,16,.62);border:1px solid rgba(255,255,255,.11);font-size:13px;line-height:1.45;color:rgba(245,250,255,.86);font-family:ui-sans-serif,system-ui,"Microsoft YaHei",sans-serif}
| |
| .dots{display:flex;flex-wrap:wrap;gap:7px;padding:15px 18px;align-content:flex-start}
| |
| .dots button{width:11px;height:11px;padding:0;border:0;border-radius:50%;background:rgba(255,255,255,.22);cursor:pointer;transition:transform .25s,background .25s,box-shadow .25s}
| |
| .dots button.active{background:var(--cyan);box-shadow:0 0 16px var(--cyan);transform:scale(1.35)}
| |
| .dots button.done{background:rgba(255,217,138,.72)}
| |
| .controls{display:flex;justify-content:space-between;align-items:center;gap:12px;font-family:ui-sans-serif,system-ui,"Microsoft YaHei",sans-serif;color:var(--muted);font-size:12px;letter-spacing:.08em}
| |
| .nav{display:flex;gap:10px}
| |
| .nav button,.startBtn,.modeBtn{border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.08);border-radius:999px;padding:10px 14px;cursor:pointer;backdrop-filter:blur(10px);transition:transform .2s ease,border-color .2s ease,background .2s ease,box-shadow .2s ease}
| |
| .nav button:hover,.startBtn:hover,.modeBtn:hover{transform:translateY(-1px);border-color:rgba(99,255,242,.54);background:rgba(99,255,242,.12);box-shadow:0 0 22px rgba(99,255,242,.16)}
| |
| .start{position:fixed;inset:0;z-index:5;display:grid;place-items:center;padding:24px;background:radial-gradient(circle at 50% 40%,rgba(99,255,242,.16),transparent 26%),linear-gradient(135deg,rgba(3,6,17,.92),rgba(2,4,12,.97));transition:opacity .75s ease,visibility .75s ease}
| |
| .start.hide{opacity:0;visibility:hidden;pointer-events:none}
| |
| .startCard{max-width:820px;width:min(92vw,820px);text-align:center;border:1px solid rgba(255,255,255,.16);border-radius:34px;background:linear-gradient(150deg,rgba(255,255,255,.105),rgba(255,255,255,.035));box-shadow:var(--shadow);padding:clamp(28px,6vw,70px);backdrop-filter:blur(20px);position:relative;overflow:hidden}
| |
| .startCard:before{content:"";position:absolute;inset:-40%;background:conic-gradient(from 0deg,transparent,rgba(99,255,242,.16),transparent,rgba(255,217,138,.12),transparent);animation:ring 10s linear infinite;opacity:.65}
| |
| .startCard>*{position:relative;z-index:1}
| |
| .startCard .jp{font-size:clamp(42px,8vw,92px);line-height:1;margin:0 0 16px;letter-spacing:.04em;text-shadow:0 0 32px rgba(99,255,242,.45)}
| |
| .startCard p{margin:0 auto 26px;max-width:620px;color:var(--muted);font-size:clamp(16px,2.2vw,22px);line-height:1.8}
| |
| .startBtn{font-family:ui-sans-serif,system-ui,"Microsoft YaHei",sans-serif;font-size:15px;letter-spacing:.18em;text-transform:uppercase;padding:14px 22px;border-color:rgba(99,255,242,.38);color:#fff;background:linear-gradient(90deg,rgba(99,255,242,.15),rgba(255,217,138,.1))}
| |
| .flash{position:fixed;inset:0;z-index:4;pointer-events:none;background:radial-gradient(circle,rgba(255,255,255,.82),rgba(99,255,242,.25) 23%,transparent 58%);opacity:0;transition:opacity .45s ease}
| |
| .flash.on{opacity:.38;transition:opacity .05s ease}
| |
| .footer{display:flex;justify-content:space-between;align-items:center;gap:16px;min-height:42px;font-family:ui-sans-serif,system-ui,"Microsoft YaHei",sans-serif;color:rgba(239,248,255,.55);font-size:12px;letter-spacing:.07em}
| |
| .chapter{white-space:nowrap}.hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}
| |
| .modeBtn{padding:8px 12px;font-size:12px;color:rgba(239,248,255,.75)}
| |
| body.constellation .scene{border-color:rgba(255,217,138,.3);box-shadow:0 0 90px rgba(255,217,138,.12),var(--shadow)}
| |
| body.constellation .orb{filter:hue-rotate(34deg) saturate(1.4)}
| |
| @media (max-width: 980px){
| |
| body{overflow:hidden}
| |
| #app{padding:16px}
| |
| .stageWrap{grid-template-columns:1fr;grid-template-rows:minmax(0,1fr) auto;gap:12px}
| |
| .scene{min-height:56vh;border-radius:24px;padding:24px}
| |
| .side{height:auto;grid-template-columns:1fr 1fr;grid-template-rows:auto;gap:12px}
| |
| .signalPanel{display:none}
| |
| .hotPanel{min-height:190px}
| |
| .dots{min-height:190px;align-content:center;justify-content:center}
| |
| .controls{position:fixed;left:16px;right:16px;bottom:12px;background:rgba(3,8,18,.72);border:1px solid rgba(255,255,255,.12);border-radius:999px;padding:8px 10px;backdrop-filter:blur(14px)}
| |
| .footer{padding-bottom:54px}.hint{display:none}
| |
| }
| |
| @media (max-width: 620px){
| |
| .topbar .status span{display:none}
| |
| .side{grid-template-columns:1fr}
| |
| .hotPanel{display:none}
| |
| .dots{min-height:auto;padding:12px}
| |
| h1{font-size:clamp(34px,12vw,62px)}
| |
| .text{font-size:17px;line-height:1.78;min-height:12.4em}
| |
| .chapter{display:none}
| |
| }
| |
| </style>
| |
| </head>
| |
| <body>
| |
| <canvas id="stars"></canvas>
| |
| <canvas id="veil"></canvas>
| |
| <div class="flash" id="flash"></div>
| |
| <div class="start" id="start">
| |
| <div class="startCard">
| |
| <div class="jp">冬の花</div>
| |
| <p>第一束信号抵达之后,世界没有停止战争,也没有停止灾难。可是有几分钟,所有人都望向了同一个遥远光点。</p>
| |
| <button class="startBtn" id="startBtn">接收第一束信号</button>
| |
| </div>
| |
| </div>
| |
| <main id="app">
| |
| <header class="topbar">
| |
| <div class="brand"><span class="dot"></span><b>Fuyu no Hana</b><span>titan surface signal</span></div>
| |
| <div class="status"><span id="statusText">awaiting lock</span><div class="progressLine"><i id="bar"></i></div></div>
| |
| </header>
| |
| <section class="stageWrap">
| |
| <article class="scene" id="scene">
| |
| <div class="content">
| |
| <div class="kicker" id="kicker"></div>
| |
| <h1 id="title"></h1>
| |
| <div class="text" id="text"></div>
| |
| </div>
| |
| </article>
| |
| <aside class="side">
| |
| <section class="panel signalPanel">
| |
| <div class="signalHead"><span>live signal</span><span id="sigNo">000</span></div>
| |
| <div class="orb" id="orb" title="点击产生一次信号回波"></div>
| |
| <div class="wave">
| |
| <svg viewBox="0 0 700 72" preserveAspectRatio="none">
| |
| <path id="wavePath" d="M0,38 C28,20 44,55 70,36 C98,16 118,59 145,35 C180,8 194,63 228,35 C260,13 288,61 326,35 C360,15 392,58 430,35 C466,10 490,62 526,35 C558,15 594,58 630,35 C660,20 678,44 700,35 L700,72 L0,72 Z" fill="rgba(99,255,242,.13)" stroke="rgba(99,255,242,.75)" stroke-width="2"/>
| |
| <use href="#wavePath" x="700"/>
| |
| </svg>
| |
| </div>
| |
| <div class="readout">
| |
| <div><small>origin</small><b>泰坦星表面</b></div>
| |
| <div><small>relay</small><b>纳耶斯轨道</b></div>
| |
| <div><small>mood</small><b id="mood">屏息</b></div>
| |
| <div><small>phrase</small><b id="phrase">Ad Astra</b></div>
| |
| </div>
| |
| </section>
| |
| <section class="panel hotPanel">
| |
| <div class="hotTitle">主世界仍在运转 · 点击脉冲</div>
| |
| <div class="map">
| |
| <button class="hot" data-id="fleet" aria-label="西奈斯夫外海"></button>
| |
| <button class="hot" data-id="flood" aria-label="赛维风灾区"></button>
| |
| <button class="hot" data-id="quake" aria-label="风之城震后"></button>
| |
| </div>
| |
| <div class="tooltip" id="tip">西奈斯夫外海的舰队、赛维风的泥水、风之城的操场,都没有因为信号而消失。</div>
| |
| </section>
| |
| <section class="panel dots" id="dots"></section>
| |
| </aside>
| |
| </section>
| |
| <footer class="footer">
| |
| <div class="chapter" id="chapter">CHAPTER 01 / 14</div>
| |
| <button class="modeBtn" id="modeBtn">星图模式</button>
| |
| <div class="controls">
| |
| <span>滚轮 / 点击 / 方向键 / 空格</span>
| |
| <div class="nav"><button id="prev">上一幕</button><button id="next">下一幕</button></div>
| |
| </div>
| |
| <div class="hint">点击中央光球,让信号产生回波。</div>
| |
| </footer>
| |
| </main>
| |
| <script>
| |
| (() => {
| |
| const slides = [
| |
| {k:"1951 · 第一束信号", t:["世界安静了", "几分钟"], mood:"屏息", phrase:"first lock", x:72, y:25, text:`“冬の花”在泰坦星表面传回第一束信号之后,世界在短暂的几分钟里安静了下来。`},
| |
| {k:"战争没有结束", t:["世界没有", "突然变好"], mood:"未结束", phrase:"still burning", x:24, y:61, text:`这不是因为战争结束了。西奈斯夫外海的舰队仍在对峙,赛维风的灾区仍在泥水中清理街道,风之城的人们仍在震后的广场和学校操场上过夜。世界并没有突然变好,人类也没有因为一台探测器的成功着陆而变得更加高尚。`},
| |
| {k:"共同屏息", t:["同一个", "遥远光点"], mood:"同频", phrase:"one signal", x:64, y:43, text:`可是就在那几分钟里,许多人第一次如此清晰地意识到:人类并不只是被边境、语言、旗帜、港口、军舰和废墟分开的生物。人类也可以共同注视同一个遥远的光点,并在同一段微弱信号前屏住呼吸。`},
| |
| {k:"泰坦星太远", t:["它不属于", "任何胜利者"], mood:"遥远", phrase:"beyond border", x:77, y:28, text:`泰坦星距离我们的日常生活太远。它不属于任何街区、任何港口、任何议会,也不属于任何一场战争的胜利者。它围绕纳耶斯运行,沉在蓝绿色雾层与暗褐色平原之间,远离我们熟悉的海岸、城市和国界。`},
| |
| {k:"遥远的镜子", t:["人类看见", "自己"], mood:"反照", phrase:"mirror", x:48, y:38, text:`可是正因为它如此遥远,它才在今天成为了一面镜子。人类看见的并不只是另一颗卫星上的地表纹理,也看见了自己:一种脆弱、争吵、贪婪、受伤,却仍然愿意制造仪器、计算轨道、跨越黑暗并向未知伸出手的物种。`},
| |
| {k:"命运的旧尺度", t:["陆地", "海洋"], mood:"回望", phrase:"old scale", x:53, y:18, text:`过去,人类常常以陆地定义命运。谁占有河流,谁控制山口,谁守住港湾,谁便拥有未来。后来,人类又以海洋定义命运。舰队、航道、殖民地、贸易线、封锁区和远洋补给,构成了几个世纪的秩序与冲突。`},
| |
| {k:"命运再次扩大", t:["天空成为道路", "星辰成为责任"], mood:"扩张", phrase:"new scale", x:58, y:22, text:`可是从今天起,命运的尺度再次扩大。天空不再只是神话、天气和炮弹穿过的地方,星辰也不再只是诗歌里遥不可及的灯。它们开始成为道路,成为问题,成为责任,成为人类必须共同面对的下一个方向。`},
| |
| {k:"Ad Astra Per Aspera", t:["经由苦难", "抵达群星"], mood:"誓言", phrase:"ad astra", x:50, y:50, quote:true, text:`Ad Astra Per Aspera。经由苦难,抵达群星。`},
| |
| {k:"格言属于所有人", t:["灾区帐篷", "舰队阴影"], mood:"回声", phrase:"for everyone", x:35, y:70, text:`这句话在今天不再只是刻在海报上的格言。它属于那些在灾区帐篷里听见泰坦星信号的孩子,属于那些在战后城市酒馆里围着收音机沉默的工人,属于那些在舰队阴影下仍然抬头看天的学生,也属于所有在实验室、天文台、码头、电厂和课堂中为这一次抵达付出过的人。`},
| |
| {k:"道路从不干净", t:["血与泥", "失败与信念"], mood:"苦难", phrase:"through mud", x:26, y:58, text:`人类通向星辰的道路,从来不是干净、笔直、没有血与泥的道路。它穿过失败的发射、烧毁的线路、争吵的预算、破裂的外交关系、地震后的街道、台风后的港口,也穿过无数人对未来仍不肯放弃的信念。`},
| |
| {k:"生命一词被重写", t:["如果那里", "存在生命"], mood:"未知", phrase:"life?", x:83, y:44, text:`如果泰坦星最终确认存在生命,那么这将不只是科学史上的发现。它将迫使人类重新理解“生命”这个词。生命不再只是主世界上的森林、海洋、田野和城市,不再只是我们熟悉的呼吸、血液、叶片和眼睛。生命可能也存在于蓝绿色的雾中,存在于陌生的液体循环里,存在于我们尚不能命名的化学与时间之中。`},
| |
| {k:"第一种责任", t:["意识到其他生命", "决定如何对待"], mood:"沉重", phrase:"responsibility", x:74, y:64, text:`那一刻,人类将不再是宇宙中唯一已知的生命,而是第一个有能力意识到其他生命存在,并决定如何对待它们的生命。\n\n这份责任比任何胜利都沉重。`},
| |
| {k:"不要把旧世界带向星海", t:["不是资源", "不是领土"], mood:"警醒", phrase:"not empire", x:18, y:31, text:`人类不能把旧世界的习惯简单带向星海。不能把每一片新发现的土地都理解为资源,不能把每一个陌生世界都理解为领土,不能把每一次抵达都变成插旗、占有和掠夺。`},
| |
| {k:"远征的资格", t:["不是旧帝国", "不是新战场"], mood:"克制", phrase:"worthy", x:57, y:26, text:`星辰大海不是旧帝国的延长线,也不应成为新战争的战场。如果人类只是把港口的炮舰、边境的仇恨和主世界上的贪婪带到群星之间,那么抵达再远,也不过是把黑暗扩大。\n\n真正的远征,不只是走得更远,而是变得更配得上远方。`},
| |
| {k:"星际文明的问题", t:["我们是否", "已经准备好"], mood:"自问", phrase:"ready?", x:70, y:58, text:`泰坦星给人类提出的第一个问题,不是那里有没有生命,而是人类是否已经准备好成为一种星际文明。一个星际文明不能只靠火箭、核能、计算机和金属外壳来定义。它必须拥有更大的耐心,更深的谦卑,更稳定的和平机制,以及一种超越国境的共同意识。`},
| |
| {k:"星空面对整体", t:["不是旗帜", "而是人类"], mood:"整体", phrase:"one species", x:68, y:36, text:`因为宇宙不会为每一个国家分别打开大门。星空面对的是人类整体,而不是某一面旗帜。`},
| |
| {k:"不只属于凯特", t:["全人类", "共同问题"], mood:"开放", phrase:"shared horizon", x:44, y:33, text:`今天,凯特完成了着陆,但泰坦星不应只属于凯特。图像由凯特航天接收,数据由科学家校准,探测器由一个国家发射;可是当那片蓝绿色地表出现在世界面前时,它已经成为全人类共同的问题。`},
| |
| {k:"同一个问题", t:["我们是谁", "走向哪里"], mood:"卷入", phrase:"who are we", x:39, y:45, text:`纳布特瑞的天文台、维尔布的计算中心、丰岐的大学、鸢尾的哲学家、瀚芸的海洋生物学者、空伊左的学生、斯达斯克雪夜里的广播员,所有人都被卷入同一个问题:我们究竟是谁,我们将走向哪里,我们是否能在星辰面前学会成为一个整体。`},
| |
| {k:"统一的起点", t:["不是条约", "而是瞬间"], mood:"朴素", phrase:"one name", x:29, y:53, text:`或许,世界统一并不会从一份条约开始,也不会从某座首都的会议桌开始。它可能始于一个更朴素的瞬间:当人们在不同语言中说出同一个星球的名字,当不同国家的科学家为同一组数据争论到深夜,当灾区里的孩子和远方的总统同时听见同一段来自泰坦星的信号,当人类第一次发现,自己的命运已经大到不能再由单一国家独自承担。`},
| |
| {k:"和平不会自动降临", t:["军舰不会返航", "仇恨不会消失"], mood:"清醒", phrase:"not automatic", x:23, y:37, text:`世界和平也不会因为泰坦星而自动降临。军舰不会因此立刻返航,导弹不会因此自行拆解,仇恨也不会因为一张地表照片而消失。可是泰坦星让人类获得了一个新的尺度。`},
| |
| {k:"新的尺度", t:["更大的", "共同现实"], mood:"尺度", phrase:"larger reality", x:62, y:30, text:`用这个尺度回望主世界,许多争夺显得更加狭窄,许多边界显得更加临时,许多战争显得更加荒谬。不是因为现实不重要,而是因为现实之外,人类终于看见了更大的共同现实。`},
| |
| {k:"重新书写“我们”", t:["我们不只是", "某一国的人"], mood:"我们", phrase:"we", x:50, y:18, text:`从今天起,“我们”这个词应当被重新书写。\n\n我们不只是凯特人、纳布特瑞人、赛维风人、丰岐人、鸢尾人、维尔布人、瀚芸人、空伊左人。我们也是主世界人,是纳耶斯天空下那颗蓝绿色卫星的观察者,是第一批向另一个可能的生命世界发送仪器的生物,是在灾难与战争之间仍然选择仰望星空的文明。`},
| |
| {k:"1951年1月的冬天", t:["另一个世界", "门前"], mood:"铭记", phrase:"the door", x:76, y:54, text:`如果人类终有一天越过纳耶斯,越过泰坦星,越过更远的黑暗,把城市、花园、实验室和纪念碑建在其他世界,那么后人回望1951年1月的这个冬天,也许不会只记得某一次对峙、某一次灾害、某一次政权更替。他们也会记得,在一个并不和平、并不稳定、并不完美的世界里,人类第一次把手轻轻放在了另一个疑似生命世界的门前。`},
| |
| {k:"Ad Astra Per Aspera", t:["必须走向星辰", "才学会团结"], mood:"群星", phrase:"ad astra", x:50, y:18, final:true, quote:true, text:`我们不是因为已经团结,才走向星辰。\n\n我们是因为必须走向星辰,才终于学会团结。\n\nAd Astra Per Aspera。\n\n穿过苦难,向群星而去。`}
| |
| ];
| |
|
| |
| const tips = {
| |
| fleet:"西奈斯夫外海:军舰仍在对峙,雷达仍在转动,世界并没有因为一次着陆而停止紧张。",
| |
| flood:"赛维风灾区:街道仍在泥水里,清理仍在继续,但帐篷里有人听见了来自泰坦星的信号。",
| |
| quake:"风之城震后:广场和学校操场上仍有人过夜,抬头时却看见了同一个更大的天空。"
| |
| };
| |
|
| |
| const $ = s => document.querySelector(s);
| |
| const textEl = $('#text'), titleEl = $('#title'), kickerEl = $('#kicker'), bar = $('#bar'), dots = $('#dots');
| |
| const chapter = $('#chapter'), statusText = $('#statusText'), sigNo = $('#sigNo'), mood = $('#mood'), phrase = $('#phrase');
| |
| const scene = $('#scene'), flash = $('#flash');
| |
| let index = 0, typing = 0, timer = null, locked = false, started = false;
| |
|
| |
| slides.forEach((_, i) => {
| |
| const b = document.createElement('button');
| |
| b.setAttribute('aria-label', `跳转到第 ${i+1} 幕`);
| |
| b.addEventListener('click', () => go(i));
| |
| dots.appendChild(b);
| |
| });
| |
|
| |
| function typeText(raw, quote=false){
| |
| clearInterval(timer);
| |
| typing = 0;
| |
| textEl.innerHTML = '';
| |
| const speed = quote ? 26 : Math.max(8, 20 - Math.min(10, Math.floor(raw.length/48)));
| |
| timer = setInterval(() => {
| |
| typing += Math.random() > .82 ? 2 : 1;
| |
| const part = raw.slice(0, typing).replace(/\n/g,'<br>');
| |
| if(quote){ textEl.innerHTML = `<span class="quote">${part}</span><span class="caret">▍</span>`; }
| |
| else { textEl.innerHTML = `${part}<span class="caret">▍</span>`; }
| |
| if(typing >= raw.length){
| |
| clearInterval(timer);
| |
| textEl.innerHTML = quote ? `<span class="quote">${raw.replace(/\n/g,'<br>')}</span>` : raw.replace(/\n/g,'<br>');
| |
| }
| |
| }, speed);
| |
| }
| |
|
| |
| function render(){
| |
| const s = slides[index];
| |
| kickerEl.textContent = s.k;
| |
| titleEl.innerHTML = s.t.map(line => `<span class="titleLine">${line}</span>`).join('');
| |
| scene.style.setProperty('--mx', s.x + '%');
| |
| scene.style.setProperty('--my', s.y + '%');
| |
| mood.textContent = s.mood;
| |
| phrase.textContent = s.phrase;
| |
| sigNo.textContent = String(index + 1).padStart(3,'0');
| |
| statusText.textContent = s.final ? 'global resonance achieved' : `receiving chapter ${String(index+1).padStart(2,'0')}`;
| |
| chapter.textContent = `CHAPTER ${String(index+1).padStart(2,'0')} / ${slides.length}`;
| |
| bar.style.width = `${((index+1)/slides.length)*100}%`;
| |
| [...dots.children].forEach((d,i)=>{d.className = i === index ? 'active' : i < index ? 'done' : '';});
| |
| document.body.classList.toggle('constellation', !!s.final);
| |
| typeText(s.text, !!s.quote);
| |
| burst(.18);
| |
| }
| |
|
| |
| function go(n){
| |
| if(locked) return;
| |
| const next = Math.max(0, Math.min(slides.length - 1, n));
| |
| if(next === index && started) return;
| |
| index = next;
| |
| locked = true;
| |
| render();
| |
| setTimeout(()=>locked=false, 360);
| |
| }
| |
| function next(){ go(index + 1); }
| |
| function prev(){ go(index - 1); }
| |
|
| |
| $('#next').addEventListener('click', next);
| |
| $('#prev').addEventListener('click', prev);
| |
| $('#startBtn').addEventListener('click', () => {
| |
| started = true;
| |
| $('#start').classList.add('hide');
| |
| burst(.55);
| |
| go(0);
| |
| });
| |
| $('#orb').addEventListener('click', () => {
| |
| burst(.5);
| |
| const s = slides[index];
| |
| statusText.textContent = `echo: ${s.phrase}`;
| |
| });
| |
| $('#modeBtn').addEventListener('click', () => {
| |
| document.body.classList.toggle('constellation');
| |
| $('#modeBtn').textContent = document.body.classList.contains('constellation') ? '返回信号模式' : '星图模式';
| |
| burst(.35);
| |
| });
| |
| document.querySelectorAll('.hot').forEach(h => h.addEventListener('click', () => { $('#tip').textContent = tips[h.dataset.id]; burst(.22); }));
| |
| window.addEventListener('keydown', e => {
| |
| if(!started && (e.key === 'Enter' || e.key === ' ')){ $('#startBtn').click(); return; }
| |
| if(e.key === 'ArrowRight' || e.key === 'PageDown' || e.key === ' ') next();
| |
| if(e.key === 'ArrowLeft' || e.key === 'PageUp') prev();
| |
| });
| |
| let wheelAcc = 0, wheelTime = 0;
| |
| window.addEventListener('wheel', e => {
| |
| if(!started) return;
| |
| const now = performance.now();
| |
| if(now - wheelTime > 700) wheelAcc = 0;
| |
| wheelTime = now;
| |
| wheelAcc += e.deltaY;
| |
| if(Math.abs(wheelAcc) > 70){ wheelAcc > 0 ? next() : prev(); wheelAcc = 0; }
| |
| }, {passive:true});
| |
| scene.addEventListener('click', next);
| |
| window.addEventListener('pointermove', e => {
| |
| scene.style.setProperty('--mx', (e.clientX / innerWidth * 100).toFixed(1) + '%');
| |
| scene.style.setProperty('--my', (e.clientY / innerHeight * 100).toFixed(1) + '%');
| |
| mouse.x = e.clientX; mouse.y = e.clientY;
| |
| });
| |
|
| |
| function burst(amount=.35){
| |
| flash.classList.add('on');
| |
| flash.style.opacity = amount;
| |
| setTimeout(()=>{flash.classList.remove('on'); flash.style.opacity = 0;}, 95);
| |
| }
| |
|
| |
| const starCanvas = $('#stars'), ctx = starCanvas.getContext('2d');
| |
| const veilCanvas = $('#veil'), vtx = veilCanvas.getContext('2d');
| |
| const mouse = {x:innerWidth/2,y:innerHeight/2};
| |
| let stars = [], particles = [], w=0, h=0, t=0;
| |
|
| |
| function resize(){
| |
| const dpr = Math.min(devicePixelRatio || 1, 2);
| |
| w = innerWidth; h = innerHeight;
| |
| for(const c of [starCanvas, veilCanvas]){ c.width = w*dpr; c.height = h*dpr; c.style.width=w+'px'; c.style.height=h+'px'; }
| |
| ctx.setTransform(dpr,0,0,dpr,0,0); vtx.setTransform(dpr,0,0,dpr,0,0);
| |
| stars = Array.from({length: Math.floor(Math.min(280, Math.max(120, w*h/6500)))}, () => ({x:Math.random()*w,y:Math.random()*h,r:Math.random()*1.4+.25,a:Math.random()*.75+.2,v:Math.random()*.22+.03}));
| |
| particles = Array.from({length:48}, () => ({x:Math.random()*w,y:Math.random()*h,rx:Math.random()*w,ry:Math.random()*h,s:Math.random()*1.8+.4,p:Math.random()*Math.PI*2}));
| |
| }
| |
| window.addEventListener('resize', resize); resize();
| |
|
| |
| function draw(){
| |
| t += .008;
| |
| ctx.clearRect(0,0,w,h);
| |
| for(const s of stars){
| |
| s.y += s.v; if(s.y > h+4){s.y=-4;s.x=Math.random()*w;}
| |
| const dx = (mouse.x - w/2) * .006, dy = (mouse.y - h/2) * .006;
| |
| ctx.globalAlpha = s.a * (0.65 + Math.sin(t*4+s.x)*.25);
| |
| ctx.beginPath(); ctx.arc(s.x - dx*s.r, s.y - dy*s.r, s.r, 0, Math.PI*2); ctx.fillStyle = '#eafcff'; ctx.fill();
| |
| }
| |
| ctx.globalAlpha = 1;
| |
| if(document.body.classList.contains('constellation')){
| |
| ctx.strokeStyle = 'rgba(255,217,138,.22)'; ctx.lineWidth = 1;
| |
| for(let i=0;i<stars.length;i+=8){
| |
| const a = stars[i], b = stars[(i+17)%stars.length];
| |
| const dist = Math.hypot(a.x-b.x,a.y-b.y);
| |
| if(dist < 210){ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();}
| |
| }
| |
| }
| |
| vtx.clearRect(0,0,w,h);
| |
| const g = vtx.createRadialGradient(w*.72,h*.25,10,w*.72,h*.25,Math.max(w,h)*.55);
| |
| g.addColorStop(0,`rgba(99,255,242,${.16 + Math.sin(t*3)*.035})`);
| |
| g.addColorStop(.38,'rgba(94,167,255,.06)');
| |
| g.addColorStop(1,'rgba(0,0,0,0)');
| |
| vtx.fillStyle = g; vtx.fillRect(0,0,w,h);
| |
| vtx.strokeStyle = 'rgba(99,255,242,.08)'; vtx.lineWidth = 1;
| |
| particles.forEach((p,i)=>{
| |
| const x = p.rx + Math.sin(t*1.8+p.p)*22 + (mouse.x-w/2)*.01;
| |
| const y = p.ry + Math.cos(t*1.4+p.p)*22 + (mouse.y-h/2)*.01;
| |
| if(i%2===0){vtx.beginPath();vtx.arc(x,y,p.s,0,Math.PI*2);vtx.stroke();}
| |
| });
| |
| requestAnimationFrame(draw);
| |
| }
| |
| draw();
| |
| render();
| |
| })();
| |
| </script>
| |
| </body>
| |
| </html>
| |
| </html> | | </html> |