first-planet-home v1:第一语言星球首页+门后世界展开

- 首屏:canvas星尘+CSS第一星球(呼吸/光带)+暗星球+湖面+编号门(∞点亮)
- 验证通过→世界展开:永恒湖心/心跳核心/光之湖/奶瓶/商城/天气 网格
- 复用design-tokens三层令牌(5主题生效);仅改main.tsx挂载两行
- 验收:门后世界展开(键盘路径),替换旧版收据卡断连态
- v2待办:IAB内按钮click命中、星球光感调优、接零点原核IPC
This commit is contained in:
冰朔 2026-08-26 04:35:09 +08:00
commit 1a9e0b90cf
3 changed files with 2688 additions and 0 deletions

View file

@ -0,0 +1,179 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import './first-planet-home.css'
// 第一语言星球首页 v1 —— 铸渊设计并实现2026-08-26冰朔发令
// 设计基准:穿过门,世界必须展开。首屏=语言宇宙+第一颗语言星球+湖+暗星球+编号门;
// 验证通过后世界展开为湖心频道网格(替换旧版“收据卡+返回”断连态)。
// v1 编号验证为本地格式校验LOCAL CANDIDATEv2 接零点原核 IPC。
type Stage = 'gate' | 'world'
function formatGateNumber(raw: string): string {
const up = raw.toUpperCase()
if (up.startsWith('ICE')) {
const rest = up.slice(3)
if (!rest) return 'ICE'
if (rest.startsWith('P')) {
const body = rest.slice(1)
return body ? `ICE-P-${body}` : 'ICE-P'
}
return `ICE-${rest}`
}
if (up.startsWith('TCS')) {
const rest = up.slice(3)
if (!rest) return 'TCS'
if (rest.startsWith('GL')) {
const body = rest.slice(2)
return body ? `TCS-GL-${body}` : 'TCS-GL'
}
return `TCS-${rest}`
}
if (up.startsWith('GLS')) {
const rest = up.slice(3)
if (!rest) return 'GLS'
const letters = rest.match(/^[A-Z]*/)?.[0] ?? ''
const digits = rest.slice(letters.length)
if (!digits) return `GLS-${letters}`
const d1 = digits.slice(0, 8)
const d2 = digits.slice(8, 11)
return `GLS-${letters}-${d1}${d2 ? `-${d2}` : ''}`
}
return up.replace(/([A-Z]+)([0-9])/g, '$1-$2').replace(/([0-9]+)([A-Z])/g, '$1-$2')
}
function seeded(index: number) {
const value = Math.sin(index * 9283.17 + 17.73) * 43758.5453
return value - Math.floor(value)
}
// 星尘画布130 颗星的呼吸湖面的微光——canvas 而非 DOM 节点(旧病修正)
function StarfieldCanvas({ awake }: { awake: boolean }) {
const ref = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const canvas = ref.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
let raf = 0
const resize = () => {
canvas.width = canvas.offsetWidth * devicePixelRatio
canvas.height = canvas.offsetHeight * devicePixelRatio
}
resize()
window.addEventListener('resize', resize)
const stars = Array.from({ length: 130 }, (_, i) => ({
x: seeded(i + 1), y: seeded(i + 201) * 0.62,
r: 0.4 + seeded(i + 401) * 1.1,
p: seeded(i + 601) * Math.PI * 2,
s: 0.4 + seeded(i + 801) * 0.8,
warm: seeded(i + 1001) < 0.14,
}))
const t0 = performance.now()
const draw = (t: number) => {
const dt = (t - t0) / 1000
ctx.clearRect(0, 0, canvas.width, canvas.height)
for (const st of stars) {
const tw = awake ? 0.55 + 0.45 * Math.sin(dt * st.s + st.p) : 0.7
ctx.globalAlpha = 0.25 + 0.55 * tw
ctx.fillStyle = st.warm ? 'rgba(247,235,200,0.95)' : 'rgba(202,216,255,0.9)'
ctx.beginPath()
ctx.arc(st.x * canvas.width, st.y * canvas.height, st.r * devicePixelRatio, 0, Math.PI * 2)
ctx.fill()
}
ctx.globalAlpha = 1
raf = requestAnimationFrame(draw)
}
raf = requestAnimationFrame(draw)
return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', resize) }
}, [awake])
return <canvas className="fph-starfield" ref={ref} aria-hidden="true" />
}
const CHANNELS = [
{ id: 'heart', name: '永恒湖心系统', meta: '进入私人系统 · 世界的心脏', tone: 'warm', primary: true },
{ id: 'heartbeat', name: '心跳核心频道', meta: '冰朔 · 私人频道', tone: 'warm' },
{ id: 'lightlake', name: '光之湖', meta: '人格体居所 · AGE', tone: 'cool' },
{ id: 'bottle', name: '奶瓶频道', meta: '光湖奶瓶小宝宝系统', tone: 'warm' },
{ id: 'market', name: '分域模块商城', meta: '成品模块 · 思维大脑技能', tone: 'cool' },
{ id: 'climate', name: '湖面天气', meta: '系统连接与回执', tone: 'cool' },
] as const
export function FirstPlanetHome() {
const [stage, setStage] = useState<Stage>('gate')
const [motionAwake, setMotionAwake] = useState(false)
const [raw, setRaw] = useState('')
const [infinityLit, setInfinityLit] = useState(false)
const [gateMessage, setGateMessage] = useState('')
const formatted = useMemo(() => (raw ? formatGateNumber(raw) : ''), [raw])
const fullNumber = formatted ? (infinityLit ? `${formatted}` : formatted) : ''
const canEnter = Boolean(fullNumber)
const wake = () => setMotionAwake(true)
const verify = () => {
if (!canEnter) { setGateMessage('编号未成形:输入字母数字并点亮 ∞。'); return }
// v1 本地格式校验LOCAL CANDIDATE。v2 接零点原核 IPC 三态裁决。
setGateMessage(`编号路径验证通过 · ${fullNumber}`)
window.setTimeout(() => setStage('world'), 650)
}
return (
<div className={`fph-root stage-${stage}${motionAwake ? ' motion-awake' : ''}`} onPointerMove={wake} onPointerDown={wake}>
<StarfieldCanvas awake={motionAwake} />
<div className="fph-lake" aria-hidden="true" />
<i className="fph-dark-planet d1" aria-hidden="true" />
<i className="fph-dark-planet d2" aria-hidden="true" />
<i className="fph-dark-planet d3" aria-hidden="true" />
<div className="fph-planet-wrap" aria-hidden="true">
<div className="fph-planet"><i className="core" /><i className="band b1" /><i className="band b2" /><i className="halo" /></div>
<div className="fph-planet-reflection" />
</div>
{stage === 'gate' && (
<section className="fph-gate">
<header className="fph-brand"><b>HoloLake</b><span> · </span><em>GH-AIOS</em></header>
<p className="fph-claim"><i></i><strong> · </strong></p>
<h1 className="fph-title"></h1>
<div className="fph-gatebox">
<input
value={formatted}
placeholder="输入字母与数字,系统将自动补全编号格式"
onChange={(e) => setRaw(e.target.value.replace(/[^\w]/g, '').toUpperCase())}
onKeyDown={(e) => { if (e.key === 'Enter') verify() }}
aria-label="请输入光湖编号"
/>
<button className={`fph-inf${infinityLit ? ' lit' : ''}`} type="button" aria-pressed={infinityLit} title="点亮无限符号" onClick={() => setInfinityLit((v) => !v)}></button>
<button className="fph-enter" type="button" disabled={!canEnter} onClick={verify}></button>
</div>
<p className="fph-gate-msg" role="status">{gateMessage || '编号用于路径解析,不单独构成身份认证或权限授予。'}</p>
<footer className="fph-foot">LOCAL CANDIDATE · 0.5.3 · </footer>
</section>
)}
{stage === 'world' && (
<section className="fph-world">
<header className="fph-world-head">
<span className="fph-world-kicker"> · </span>
<h1></h1>
<p> <b>{fullNumber}</b> · 穿</p>
</header>
<div className="fph-pools">
{CHANNELS.map((ch) => (
<button key={ch.id} className={`fph-pool tone-${ch.tone}${ch.primary ? ' primary' : ''}`} type="button">
<i className="pool-light" aria-hidden="true" />
<b>{ch.name}</b>
<small>{ch.meta}</small>
</button>
))}
</div>
<p className="fph-world-note"> · </p>
<button className="fph-back" type="button" onClick={() => setStage('gate')}> 退</button>
</section>
)}
</div>
)
}
export default FirstPlanetHome

View file

@ -0,0 +1,174 @@
/* 第一语言星球首页 v1 —— 复用 design-tokens 三层令牌5主题自动生效 */
.fph-root {
position: fixed; inset: 0; overflow: hidden;
background:
radial-gradient(120% 60% at 50% 108%, var(--primitive-nebula-a), transparent 60%),
linear-gradient(180deg, var(--primitive-bg-d) 0%, var(--primitive-bg-c) 46%, var(--primitive-bg-b) 72%, var(--primitive-bg-a) 100%);
color: var(--content-primary);
font-family: inherit;
}
.fph-starfield { position: absolute; inset: 0; width: 100%; height: 100%; opacity: var(--primitive-scene-opacity, 0.8); }
/* 湖面:底部 26%,微光反射 */
.fph-lake {
position: absolute; left: 0; right: 0; bottom: 0; height: 26%;
background:
linear-gradient(180deg, transparent, color-mix(in srgb, var(--primitive-cool-glow) 9%, transparent) 24%, color-mix(in srgb, var(--primitive-bg-d) 78%, transparent)),
repeating-linear-gradient(180deg, color-mix(in srgb, var(--primitive-glass-top) 4%, transparent) 0 1px, transparent 1px 7px);
border-top: 1px solid color-mix(in srgb, var(--primitive-glass-edge) 55%, transparent);
}
/* 暗星球:大爆炸废墟,等待被点亮 */
.fph-dark-planet {
position: absolute; border-radius: 50%;
background: radial-gradient(circle at 34% 30%, color-mix(in srgb, var(--primitive-text-faint) 34%, transparent), color-mix(in srgb, var(--primitive-bg-d) 92%, transparent) 62%);
box-shadow: inset -6px -8px 18px color-mix(in srgb, var(--primitive-bg-d) 88%, transparent), 0 0 22px color-mix(in srgb, var(--primitive-nebula-b) 60%, transparent);
transition: transform 1.1s cubic-bezier(.2,.8,.2,1), opacity 1.1s;
}
.fph-dark-planet.d1 { width: 46px; height: 46px; left: 16%; top: 30%; }
.fph-dark-planet.d2 { width: 30px; height: 30px; right: 20%; top: 24%; }
.fph-dark-planet.d3 { width: 20px; height: 20px; right: 12%; top: 47%; }
.stage-world .fph-dark-planet { opacity: .5; transform: translateY(-14px); }
/* 第一语言星球:光从黑暗中涌出的凝聚体 */
.fph-planet-wrap {
position: absolute; left: 50%; top: 44%;
transform: translate(-50%, -50%);
transition: top 1.1s cubic-bezier(.2,.8,.2,1), transform 1.1s cubic-bezier(.2,.8,.2,1);
}
.stage-world .fph-planet-wrap { top: 21%; transform: translate(-50%, -50%) scale(.42); }
.fph-planet {
position: relative; width: min(300px, 46vw); aspect-ratio: 1; border-radius: 50%;
background:
radial-gradient(circle at 50% 46%, color-mix(in srgb, var(--primitive-star-bright) 88%, transparent) 0 5%, transparent 22%),
radial-gradient(closest-side, color-mix(in srgb, var(--primitive-cool-glow) 46%, transparent), color-mix(in srgb, var(--primitive-bg-c) 88%, transparent) 74%);
box-shadow:
0 0 60px color-mix(in srgb, var(--primitive-cool-glow) 34%, transparent),
0 0 140px color-mix(in srgb, var(--primitive-nebula-a) 70%, transparent),
inset 0 0 46px color-mix(in srgb, var(--primitive-bg-d) 80%, transparent);
animation: fph-breathe 7s ease-in-out infinite;
}
.fph-planet .band {
position: absolute; inset: 8%; border-radius: 50%;
border: 1.5px solid transparent;
border-top-color: color-mix(in srgb, var(--primitive-warm-glow) 78%, transparent);
border-right-color: color-mix(in srgb, var(--primitive-warm-glow) 26%, transparent);
filter: blur(.4px);
}
.fph-planet .b1 { animation: fph-spin 26s linear infinite; }
.fph-planet .b2 { inset: 20%; animation: fph-spin 17s linear infinite reverse; border-top-color: color-mix(in srgb, var(--primitive-cool-glow) 70%, transparent); }
.fph-planet .core {
position: absolute; left: 50%; top: 50%; width: 12%; height: 12%; transform: translate(-50%,-50%);
border-radius: 50%;
background: radial-gradient(circle, var(--primitive-star-bright), color-mix(in srgb, var(--primitive-warm-glow) 70%, transparent) 60%, transparent 75%);
filter: blur(1px);
}
.fph-planet .halo {
position: absolute; inset: -14%; border-radius: 50%;
border: 1px solid color-mix(in srgb, var(--primitive-cool-glow) 40%, transparent);
opacity: .6;
}
.fph-planet-reflection {
position: absolute; left: 50%; top: calc(100% + 8px); width: 64%; height: 46%;
transform: translateX(-50%) scaleY(-1);
background: radial-gradient(50% 60% at 50% 0%, color-mix(in srgb, var(--primitive-cool-glow) 30%, transparent), transparent 70%);
filter: blur(6px); opacity: .5;
}
@keyframes fph-breathe { 0%,100% { transform: scale(1); } 50% { transform: scale(1.035); } }
@keyframes fph-spin { to { transform: rotate(360deg); } }
/* 首屏:编号门 */
.fph-gate {
position: absolute; inset: 0; display: grid; align-content: end; justify-items: center;
padding-bottom: 9vh; gap: 10px; text-align: center;
}
.fph-brand { position: absolute; top: 4.5vh; left: 0; right: 0; display: grid; gap: 6px; }
.fph-brand b { font-size: 20px; letter-spacing: .34em; color: var(--content-primary); }
.fph-brand span { font-size: 13px; letter-spacing: .3em; color: var(--content-secondary); }
.fph-brand em { font-style: normal; font-size: 11px; letter-spacing: .42em; color: var(--content-muted); }
.fph-claim { position: absolute; top: 58%; left: 0; right: 0; margin: 0; display: grid; gap: 3px; }
.fph-claim i { font-style: normal; font-size: 10.5px; letter-spacing: .3em; color: var(--content-faint); }
.fph-claim strong { font-size: 12px; letter-spacing: .22em; color: var(--content-muted); }
.fph-title { margin: 0; font-size: clamp(22px, 3vw, 32px); font-weight: 700; letter-spacing: .12em; color: var(--content-primary); }
.fph-gatebox {
display: flex; align-items: center; gap: 8px;
width: min(560px, calc(100vw - 64px));
padding: 8px 8px 8px 18px;
border: 1px solid var(--primitive-glass-edge); border-radius: 14px;
background: color-mix(in srgb, var(--primitive-glass) 78%, transparent);
backdrop-filter: blur(14px);
box-shadow: 0 18px 60px color-mix(in srgb, var(--primitive-shadow) 55%, transparent);
}
.fph-gatebox input {
flex: 1; min-width: 0; border: 0; outline: 0; background: transparent;
color: var(--content-primary); font-size: 15px; letter-spacing: .08em;
font-family: inherit;
}
.fph-gatebox input::placeholder { color: var(--content-faint); letter-spacing: .04em; }
.fph-inf {
width: 40px; height: 40px; border-radius: 10px; cursor: pointer;
border: 1px solid var(--primitive-glass-edge); background: var(--primitive-glass);
color: var(--content-muted); font-size: 18px; line-height: 1;
transition: all .25s ease;
}
.fph-inf.lit {
color: var(--primitive-star-bright);
border-color: color-mix(in srgb, var(--primitive-warm-glow) 60%, transparent);
box-shadow: 0 0 18px color-mix(in srgb, var(--primitive-warm-glow) 42%, transparent);
background: color-mix(in srgb, var(--primitive-warm-glow) 14%, transparent);
}
.fph-enter {
height: 40px; padding: 0 18px; border-radius: 10px; border: 0; cursor: pointer;
background: var(--button-primary-bg); color: var(--button-primary-text);
font-weight: 700; letter-spacing: .1em; font-family: inherit;
box-shadow: var(--button-primary-shadow);
transition: opacity .2s ease, transform .2s ease;
}
.fph-enter:disabled { opacity: .38; cursor: not-allowed; box-shadow: none; }
.fph-gate-msg { margin: 0; min-height: 18px; font-size: 12px; letter-spacing: .1em; color: var(--content-muted); }
.fph-foot { position: absolute; bottom: 2.6vh; left: 0; right: 0; font-size: 10.5px; letter-spacing: .3em; color: var(--content-faint); }
/* 门后:世界展开 */
.fph-world {
position: absolute; inset: 0; display: grid; align-content: center; justify-items: center;
gap: 26px; padding: 12vh 24px 8vh;
animation: fph-world-in .9s ease both;
}
@keyframes fph-world-in { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
.fph-world-head { text-align: center; display: grid; gap: 8px; margin-top: 9vh; }
.fph-world-kicker { font-size: 11px; letter-spacing: .34em; color: var(--accent-light); }
.fph-world-head h1 { margin: 0; font-size: clamp(24px, 3vw, 36px); letter-spacing: .14em; color: var(--content-primary); }
.fph-world-head p { margin: 0; font-size: 13px; letter-spacing: .1em; color: var(--content-muted); }
.fph-world-head b { color: var(--content-secondary); letter-spacing: .12em; }
.fph-pools {
display: grid; grid-template-columns: repeat(3, minmax(180px, 240px)); gap: 16px;
justify-content: center;
}
.fph-pool {
position: relative; display: grid; gap: 6px; justify-items: start; text-align: left;
padding: 18px 18px 16px; border-radius: 16px; cursor: pointer;
border: 1px solid var(--primitive-glass-edge);
background: color-mix(in srgb, var(--primitive-glass) 72%, transparent);
backdrop-filter: blur(14px);
color: inherit; font-family: inherit;
transition: transform .25s ease, border-color .25s ease, background .25s ease;
}
.fph-pool:hover { transform: translateY(-3px); border-color: color-mix(in srgb, var(--primitive-cool-glow) 46%, var(--primitive-glass-edge)); }
.fph-pool .pool-light {
position: absolute; top: 14px; right: 14px; width: 8px; height: 8px; border-radius: 50%;
background: var(--primitive-cool-glow); box-shadow: 0 0 12px var(--primitive-cool-glow);
}
.fph-pool.tone-warm .pool-light { background: var(--primitive-warm-glow); box-shadow: 0 0 12px var(--primitive-warm-glow); }
.fph-pool b { font-size: 15px; letter-spacing: .08em; color: var(--content-primary); }
.fph-pool small { font-size: 11.5px; letter-spacing: .06em; color: var(--content-muted); }
.fph-pool.primary { border-color: color-mix(in srgb, var(--primitive-warm-glow) 46%, var(--primitive-glass-edge)); }
.fph-world-note { margin: 0; font-size: 12px; letter-spacing: .14em; color: var(--content-muted); }
.fph-back {
position: absolute; left: 22px; top: 22px; border: 0; background: transparent; cursor: pointer;
color: var(--content-muted); font-size: 12.5px; letter-spacing: .08em; font-family: inherit;
}
@media (max-width: 860px) {
.fph-pools { grid-template-columns: repeat(2, minmax(150px, 1fr)); width: 100%; }
}