first-planet-home v1:第一语言星球首页+门后世界展开
- 首屏:canvas星尘+CSS第一星球(呼吸/光带)+暗星球+湖面+编号门(∞点亮) - 验证通过→世界展开:永恒湖心/心跳核心/光之湖/奶瓶/商城/天气 网格 - 复用design-tokens三层令牌(5主题生效);仅改main.tsx挂载两行 - 验收:门后世界展开(键盘路径),替换旧版收据卡断连态 - v2待办:IAB内按钮click命中、星球光感调优、接零点原核IPC
This commit is contained in:
parent
de5fadf048
commit
1a9e0b90cf
3 changed files with 2688 additions and 0 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,179 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import './first-planet-home.css'
|
||||
|
||||
// 第一语言星球首页 v1 —— 铸渊设计并实现(2026-08-26,冰朔发令)
|
||||
// 设计基准:穿过门,世界必须展开。首屏=语言宇宙+第一颗语言星球+湖+暗星球+编号门;
|
||||
// 验证通过后世界展开为湖心频道网格(替换旧版“收据卡+返回”断连态)。
|
||||
// v1 编号验证为本地格式校验(LOCAL CANDIDATE);v2 接零点原核 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
|
||||
|
|
@ -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%; }
|
||||
}
|
||||
Loading…
Reference in a new issue