2402 lines
71 KiB
HTML
2402 lines
71 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<meta name="theme-color" content="#ececea">
|
|
<meta name="description" content="三国鼎立:滚动驱动的三国战役立体档案。">
|
|
<title>三国鼎立|战役档案</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: dark;
|
|
--ink: #090908;
|
|
--paper: #e7e1d2;
|
|
--muted: #aaa79d;
|
|
--bronze: #b98b56;
|
|
--bronze-soft: rgba(185, 139, 86, .32);
|
|
--line: rgba(231, 225, 210, .16);
|
|
--mx: 0;
|
|
--my: 0;
|
|
--vh: 1vh;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html {
|
|
scroll-snap-type: y mandatory;
|
|
scrollbar-width: none;
|
|
background: var(--ink);
|
|
}
|
|
|
|
html::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
overflow-x: hidden;
|
|
color: var(--paper);
|
|
background: var(--ink);
|
|
font-family: "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", sans-serif;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
button,
|
|
a {
|
|
font: inherit;
|
|
}
|
|
|
|
button {
|
|
color: inherit;
|
|
}
|
|
|
|
button:focus-visible,
|
|
a:focus-visible {
|
|
outline: 1px solid var(--paper);
|
|
outline-offset: 5px;
|
|
}
|
|
|
|
.loading-screen {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 100;
|
|
display: grid;
|
|
place-items: center;
|
|
background: #090908;
|
|
transition: opacity .8s cubic-bezier(.16, 1, .3, 1), visibility .8s;
|
|
}
|
|
|
|
.loading-screen.is-ready {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.loader {
|
|
width: min(320px, 72vw);
|
|
}
|
|
|
|
.loader__label {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 14px;
|
|
color: #d3cdbf;
|
|
font-size: 11px;
|
|
letter-spacing: .22em;
|
|
}
|
|
|
|
.loader__track {
|
|
height: 1px;
|
|
overflow: hidden;
|
|
background: rgba(255, 255, 255, .14);
|
|
}
|
|
|
|
.loader__bar {
|
|
width: 0;
|
|
height: 100%;
|
|
background: var(--bronze);
|
|
transition: width .35s ease;
|
|
}
|
|
|
|
.experience {
|
|
position: fixed;
|
|
inset: 0;
|
|
min-height: 100dvh;
|
|
overflow: hidden;
|
|
isolation: isolate;
|
|
background:
|
|
radial-gradient(circle at 71% 48%, rgba(112, 91, 64, .18), transparent 30%),
|
|
radial-gradient(circle at 18% 76%, rgba(126, 117, 95, .09), transparent 28%),
|
|
#090908;
|
|
}
|
|
|
|
.atmosphere,
|
|
.ink-landscape,
|
|
.grid-wall,
|
|
.grid-floor,
|
|
.vignette,
|
|
.grain,
|
|
.battle-glyphs,
|
|
#particleCanvas {
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.ink-landscape {
|
|
inset: -4%;
|
|
width: 108%;
|
|
height: 108%;
|
|
object-fit: cover;
|
|
opacity: .14;
|
|
filter: invert(1) grayscale(1) contrast(1.45) brightness(.64);
|
|
mix-blend-mode: screen;
|
|
transform: translate3d(calc(var(--mx) * -7px), calc(var(--my) * -4px), 0) scale(1.04);
|
|
transition: transform .2s linear;
|
|
}
|
|
|
|
.grid-wall {
|
|
inset: -18% -9% 8%;
|
|
opacity: .34;
|
|
background-image:
|
|
linear-gradient(rgba(213, 205, 188, .16) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(213, 205, 188, .16) 1px, transparent 1px);
|
|
background-size: 54px 54px;
|
|
mask-image: linear-gradient(to bottom, #000 0%, #000 68%, transparent 100%);
|
|
transform: perspective(760px) rotateY(calc(var(--mx) * 1.3deg)) rotateX(calc(var(--my) * -1deg)) scale(1.08);
|
|
transform-origin: 50% 45%;
|
|
}
|
|
|
|
.grid-floor {
|
|
inset: 61% -28% -42%;
|
|
opacity: .28;
|
|
background-image:
|
|
linear-gradient(rgba(213, 205, 188, .17) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(213, 205, 188, .17) 1px, transparent 1px);
|
|
background-size: 58px 58px;
|
|
mask-image: linear-gradient(to bottom, #000, transparent 76%);
|
|
transform: perspective(520px) rotateX(64deg) translateY(-6%);
|
|
transform-origin: 50% 0;
|
|
}
|
|
|
|
.vignette {
|
|
z-index: 3;
|
|
background:
|
|
linear-gradient(90deg, rgba(5, 5, 4, .74) 0%, rgba(5, 5, 4, .21) 44%, transparent 66%),
|
|
radial-gradient(ellipse at center, transparent 35%, rgba(0, 0, 0, .72) 115%);
|
|
}
|
|
|
|
.grain {
|
|
z-index: 40;
|
|
opacity: .07;
|
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.88' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.38'/%3E%3C/svg%3E");
|
|
mix-blend-mode: soft-light;
|
|
}
|
|
|
|
.battle-glyphs {
|
|
overflow: hidden;
|
|
color: rgba(224, 217, 202, .045);
|
|
font-family: "STKaiti", "KaiTi", serif;
|
|
font-weight: 700;
|
|
user-select: none;
|
|
}
|
|
|
|
.battle-glyphs span {
|
|
position: absolute;
|
|
line-height: 1;
|
|
transform: translate3d(calc(var(--mx) * var(--gx)), calc(var(--my) * var(--gy)), 0);
|
|
transition: transform .35s ease-out;
|
|
}
|
|
|
|
.battle-glyphs span:nth-child(1) { top: -8vh; left: 32vw; font-size: 56vw; --gx: -8px; --gy: -5px; }
|
|
.battle-glyphs span:nth-child(2) { top: 12vh; right: -5vw; font-size: 36vw; --gx: 13px; --gy: 7px; }
|
|
.battle-glyphs span:nth-child(3) { bottom: -14vh; left: -4vw; font-size: 31vw; --gx: 5px; --gy: 11px; }
|
|
|
|
.meta-field {
|
|
display: none;
|
|
position: absolute;
|
|
z-index: 6;
|
|
inset: 0;
|
|
overflow: hidden;
|
|
pointer-events: none;
|
|
contain: layout style paint;
|
|
}
|
|
|
|
.meta-zone {
|
|
position: absolute;
|
|
overflow: hidden;
|
|
pointer-events: none;
|
|
contain: layout style paint;
|
|
}
|
|
|
|
.meta-zone--top {
|
|
top: 0;
|
|
right: 0;
|
|
left: 0;
|
|
height: 17vh;
|
|
}
|
|
|
|
.meta-zone--bottom {
|
|
right: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
height: 9vh;
|
|
}
|
|
|
|
.meta-zone--left {
|
|
display: none;
|
|
}
|
|
|
|
.meta-zone--right {
|
|
display: none;
|
|
}
|
|
|
|
.meta-mote {
|
|
position: absolute;
|
|
width: clamp(38px, 3.1vw, 52px);
|
|
aspect-ratio: 1;
|
|
opacity: var(--meta-opacity);
|
|
transform:
|
|
translate3d(var(--fluid-x, 0px), var(--fluid-y, 0px), 0)
|
|
rotate(calc(var(--meta-rotation) + var(--fluid-rotation, 0deg)))
|
|
scale(var(--meta-scale));
|
|
transform-origin: 50% 50%;
|
|
}
|
|
|
|
.meta-field.is-disturbed .meta-mote {
|
|
will-change: transform;
|
|
}
|
|
|
|
.meta-mote__shape {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
user-select: none;
|
|
animation: meta-drift var(--drift-duration) ease-in-out var(--drift-delay) infinite alternate;
|
|
}
|
|
|
|
.meta-mote.is-vermilion .meta-mote__shape {
|
|
filter: invert(19%) sepia(72%) saturate(2555%) hue-rotate(338deg) brightness(80%) contrast(98%);
|
|
}
|
|
|
|
.meta-field.is-paused .meta-mote__shape {
|
|
animation-play-state: paused;
|
|
}
|
|
|
|
@keyframes meta-drift {
|
|
0% {
|
|
transform: translate3d(var(--drift-x-start), var(--drift-y-start), 0) rotate(var(--drift-rotation-start));
|
|
}
|
|
52% {
|
|
transform: translate3d(var(--drift-x), var(--drift-y), 0) rotate(var(--drift-rotation));
|
|
}
|
|
100% {
|
|
transform: translate3d(var(--drift-x-end), var(--drift-y-end), 0) rotate(var(--drift-rotation-end));
|
|
}
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.meta-zone--top { height: 15vh; }
|
|
.meta-zone--bottom { height: 11vh; }
|
|
}
|
|
|
|
@media (max-width: 560px) {
|
|
.meta-zone--top { height: 14vh; }
|
|
.meta-zone--bottom { height: 14vh; }
|
|
.meta-zone--left,
|
|
.meta-zone--right { display: none; }
|
|
}
|
|
|
|
#particleCanvas {
|
|
z-index: 2;
|
|
width: 100%;
|
|
height: 100%;
|
|
opacity: .72;
|
|
}
|
|
|
|
.header {
|
|
position: absolute;
|
|
z-index: 50;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
display: grid;
|
|
grid-template-columns: 1fr auto 1fr;
|
|
align-items: center;
|
|
gap: 20px;
|
|
padding: clamp(22px, 3vw, 42px) clamp(24px, 4vw, 70px);
|
|
}
|
|
|
|
.brand {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
width: fit-content;
|
|
gap: 12px;
|
|
color: var(--paper);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.brand__mark-frame {
|
|
width: 38px;
|
|
height: 38px;
|
|
flex: 0 0 auto;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(255, 255, 255, .12);
|
|
background: rgba(231, 225, 210, .92);
|
|
}
|
|
|
|
.brand__mark {
|
|
display: block;
|
|
width: 165%;
|
|
height: 165%;
|
|
max-width: none;
|
|
object-fit: contain;
|
|
transform: translate(-19.5%, -15.5%);
|
|
}
|
|
|
|
.brand__text strong {
|
|
display: block;
|
|
font-family: "STKaiti", "KaiTi", serif;
|
|
font-size: 17px;
|
|
font-weight: 600;
|
|
letter-spacing: .28em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.brand__text small,
|
|
.header__chapter {
|
|
display: block;
|
|
margin-top: 4px;
|
|
color: var(--muted);
|
|
font-size: 8px;
|
|
letter-spacing: .22em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.header__chapter {
|
|
margin: 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.sound-button {
|
|
justify-self: end;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 11px;
|
|
min-height: 38px;
|
|
padding: 0 15px;
|
|
border: 1px solid rgba(255, 255, 255, .19);
|
|
border-radius: 999px;
|
|
background: rgba(8, 8, 7, .38);
|
|
backdrop-filter: blur(14px);
|
|
cursor: pointer;
|
|
transition: border-color .25s, background .25s, transform .25s;
|
|
}
|
|
|
|
.sound-button:hover {
|
|
border-color: rgba(185, 139, 86, .68);
|
|
background: rgba(185, 139, 86, .1);
|
|
}
|
|
|
|
.sound-button:active {
|
|
transform: scale(.98);
|
|
}
|
|
|
|
.sound-button svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.sound-button span {
|
|
font-size: 9px;
|
|
letter-spacing: .18em;
|
|
}
|
|
|
|
.sound-button .wave {
|
|
opacity: .35;
|
|
transition: opacity .25s;
|
|
}
|
|
|
|
.sound-button.is-playing .wave {
|
|
opacity: 1;
|
|
}
|
|
|
|
.stage {
|
|
position: absolute;
|
|
z-index: 10;
|
|
inset: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(310px, 36vw) 1fr;
|
|
align-items: center;
|
|
min-height: 100dvh;
|
|
padding: clamp(90px, 9vw, 128px) clamp(24px, 4vw, 70px) clamp(70px, 7vw, 96px);
|
|
}
|
|
|
|
.story-panel {
|
|
position: relative;
|
|
z-index: 30;
|
|
align-self: center;
|
|
max-width: 520px;
|
|
padding-right: clamp(20px, 4vw, 68px);
|
|
transform: translate3d(calc(var(--mx) * -3px), calc(var(--my) * -2px), 70px);
|
|
transition: transform .2s linear;
|
|
}
|
|
|
|
.story-panel::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: -30px;
|
|
left: -24px;
|
|
width: 1px;
|
|
height: 168px;
|
|
background: linear-gradient(var(--bronze), transparent);
|
|
}
|
|
|
|
.story-panel::after {
|
|
content: "";
|
|
position: absolute;
|
|
z-index: 0;
|
|
inset: -52px -145px -55px -38px;
|
|
background: linear-gradient(90deg, rgba(9, 9, 8, .98) 0%, rgba(9, 9, 8, .87) 56%, rgba(9, 9, 8, 0) 100%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.story-content {
|
|
position: relative;
|
|
z-index: 1;
|
|
transition: opacity .18s ease, transform .18s ease, filter .18s ease;
|
|
}
|
|
|
|
.story-content.is-changing {
|
|
opacity: 0;
|
|
filter: blur(5px);
|
|
transform: translate3d(0, 16px, 0);
|
|
}
|
|
|
|
.eyebrow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin: 0 0 22px;
|
|
color: var(--bronze);
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
letter-spacing: .22em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.eyebrow::before {
|
|
content: "";
|
|
width: 35px;
|
|
height: 1px;
|
|
background: currentColor;
|
|
}
|
|
|
|
.battle-title {
|
|
max-width: 10em;
|
|
margin: 0;
|
|
font-family: "STKaiti", "KaiTi", "FangSong", serif;
|
|
font-size: clamp(39px, 4.5vw, 78px);
|
|
font-weight: 500;
|
|
line-height: .98;
|
|
letter-spacing: -.06em;
|
|
text-wrap: balance;
|
|
}
|
|
|
|
.battle-title .accent-dot {
|
|
color: var(--bronze);
|
|
}
|
|
|
|
.battle-meta {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
gap: 9px 18px;
|
|
margin: 28px 0 23px;
|
|
padding: 16px 0;
|
|
border-top: 1px solid var(--line);
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.battle-meta dt {
|
|
color: #77756f;
|
|
font-size: 9px;
|
|
letter-spacing: .18em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.battle-meta dd {
|
|
min-width: 0;
|
|
margin: 0;
|
|
color: #c9c3b6;
|
|
font-size: 11px;
|
|
line-height: 1.55;
|
|
letter-spacing: .05em;
|
|
}
|
|
|
|
.battle-summary {
|
|
max-width: 48ch;
|
|
margin: 0;
|
|
color: #bbb6aa;
|
|
font-family: "FangSong", "STFangsong", serif;
|
|
font-size: clamp(14px, 1.08vw, 17px);
|
|
line-height: 1.9;
|
|
text-align: justify;
|
|
}
|
|
|
|
.story-index {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 7px;
|
|
margin-top: 27px;
|
|
color: #77756f;
|
|
font-size: 10px;
|
|
letter-spacing: .12em;
|
|
}
|
|
|
|
.story-index strong {
|
|
color: var(--paper);
|
|
font-size: 22px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.carousel-viewport {
|
|
position: absolute;
|
|
z-index: 10;
|
|
top: 50%;
|
|
left: 53%;
|
|
width: 100vw;
|
|
height: min(88vh, 1000px);
|
|
perspective: 1500px;
|
|
transform: translate3d(-50%, -50%, 0);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.orbit {
|
|
position: absolute;
|
|
inset: 0;
|
|
transform-style: preserve-3d;
|
|
transform: rotateX(calc(var(--my) * -1.6deg)) rotateY(calc(var(--mx) * 2deg));
|
|
transition: transform .2s linear;
|
|
}
|
|
|
|
.orbit-ring,
|
|
.orbit-ring::before {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: 71%;
|
|
height: 46%;
|
|
border: 1px solid rgba(185, 139, 86, .23);
|
|
border-radius: 50%;
|
|
content: "";
|
|
transform: translate(-50%, -50%) rotateX(72deg);
|
|
box-shadow: 0 0 55px rgba(185, 139, 86, .05);
|
|
}
|
|
|
|
.orbit-ring::before {
|
|
top: 50%;
|
|
left: 50%;
|
|
width: 94%;
|
|
height: 94%;
|
|
border-color: rgba(231, 225, 210, .08);
|
|
transform: translate(-50%, -50%) scale(.86);
|
|
}
|
|
|
|
.orbit-card {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: clamp(650px, 52vw, 1080px);
|
|
aspect-ratio: 5 / 3;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(235, 229, 216, .16);
|
|
background: #151512;
|
|
box-shadow: 0 35px 90px rgba(0, 0, 0, .48);
|
|
transform-style: preserve-3d;
|
|
transform-origin: center;
|
|
will-change: transform, opacity, filter;
|
|
pointer-events: auto;
|
|
cursor: pointer;
|
|
transition: border-color .3s ease;
|
|
}
|
|
|
|
.orbit-card::before,
|
|
.orbit-card::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 2;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.orbit-card::before {
|
|
background:
|
|
linear-gradient(125deg, rgba(255, 255, 255, .14), transparent 18%, transparent 72%, rgba(185, 139, 86, .12)),
|
|
linear-gradient(to top, rgba(0, 0, 0, .8), transparent 45%);
|
|
}
|
|
|
|
.orbit-card::after {
|
|
inset: 12px;
|
|
border: 1px solid rgba(255, 255, 255, .08);
|
|
}
|
|
|
|
.orbit-card.is-active {
|
|
border-color: rgba(185, 139, 86, .72);
|
|
}
|
|
|
|
.orbit-card img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
filter: saturate(.58) contrast(1.04) brightness(.72);
|
|
transform: scale(1.035);
|
|
transition: filter .55s ease, transform 1.1s cubic-bezier(.16, 1, .3, 1);
|
|
}
|
|
|
|
.orbit-card.is-active img {
|
|
filter: saturate(.82) contrast(1.03) brightness(.94);
|
|
transform: scale(1);
|
|
}
|
|
|
|
.card-fallback {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: none;
|
|
place-items: center;
|
|
padding: 36px;
|
|
color: rgba(231, 225, 210, .55);
|
|
background:
|
|
linear-gradient(45deg, transparent 48%, rgba(185, 139, 86, .14) 49%, rgba(185, 139, 86, .14) 51%, transparent 52%),
|
|
#161512;
|
|
font-family: "STKaiti", "KaiTi", serif;
|
|
font-size: 28px;
|
|
text-align: center;
|
|
}
|
|
|
|
.orbit-card.has-error .card-fallback {
|
|
display: grid;
|
|
}
|
|
|
|
.card-caption {
|
|
position: absolute;
|
|
z-index: 3;
|
|
right: 24px;
|
|
bottom: 20px;
|
|
left: 24px;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
opacity: 0;
|
|
transform: translateY(8px);
|
|
transition: opacity .35s, transform .35s;
|
|
}
|
|
|
|
.orbit-card.is-active .card-caption {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.card-caption strong {
|
|
font-family: "STKaiti", "KaiTi", serif;
|
|
font-size: clamp(18px, 1.55vw, 25px);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.card-caption span {
|
|
color: #bcb6a9;
|
|
font-size: 9px;
|
|
letter-spacing: .16em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.depth-marker {
|
|
position: absolute;
|
|
z-index: 1;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: 5px;
|
|
height: 5px;
|
|
border: 1px solid var(--bronze);
|
|
border-radius: 50%;
|
|
box-shadow: 0 0 0 9px rgba(185, 139, 86, .05);
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
.timeline {
|
|
position: absolute;
|
|
z-index: 55;
|
|
top: 50%;
|
|
right: clamp(20px, 3vw, 52px);
|
|
display: grid;
|
|
gap: 12px;
|
|
transform: translateY(-50%);
|
|
}
|
|
|
|
.timeline::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 8px;
|
|
bottom: 8px;
|
|
left: 4px;
|
|
width: 1px;
|
|
background: rgba(255, 255, 255, .14);
|
|
}
|
|
|
|
.timeline button {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
width: 16px;
|
|
height: 16px;
|
|
padding: 0;
|
|
border: 0;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.timeline button::before {
|
|
content: "";
|
|
z-index: 1;
|
|
width: 9px;
|
|
height: 9px;
|
|
border: 1px solid rgba(231, 225, 210, .35);
|
|
border-radius: 50%;
|
|
background: #0b0b0a;
|
|
transition: background .25s, border-color .25s, transform .25s;
|
|
}
|
|
|
|
.timeline button span {
|
|
position: absolute;
|
|
right: 25px;
|
|
width: max-content;
|
|
opacity: 0;
|
|
color: #b8b3a8;
|
|
font-size: 9px;
|
|
letter-spacing: .12em;
|
|
pointer-events: none;
|
|
transform: translateX(5px);
|
|
transition: opacity .25s, transform .25s;
|
|
}
|
|
|
|
.timeline button:hover span,
|
|
.timeline button:focus-visible span,
|
|
.timeline button.is-active span {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.timeline button.is-active::before {
|
|
border-color: var(--bronze);
|
|
background: var(--bronze);
|
|
transform: scale(1.2);
|
|
}
|
|
|
|
.scroll-hint {
|
|
position: absolute;
|
|
z-index: 50;
|
|
bottom: clamp(23px, 3.2vw, 45px);
|
|
left: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
color: #8d897f;
|
|
font-size: 9px;
|
|
letter-spacing: .2em;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.scroll-hint__mouse {
|
|
position: relative;
|
|
width: 16px;
|
|
height: 27px;
|
|
border: 1px solid rgba(231, 225, 210, .38);
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.scroll-hint__mouse::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 5px;
|
|
left: 50%;
|
|
width: 2px;
|
|
height: 5px;
|
|
border-radius: 2px;
|
|
background: var(--bronze);
|
|
transform: translateX(-50%);
|
|
animation: wheel 1.6s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes wheel {
|
|
0% { opacity: 0; transform: translate(-50%, 0); }
|
|
30% { opacity: 1; }
|
|
100% { opacity: 0; transform: translate(-50%, 9px); }
|
|
}
|
|
|
|
.scroll-progress {
|
|
position: absolute;
|
|
z-index: 52;
|
|
right: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
height: 2px;
|
|
background: rgba(255, 255, 255, .07);
|
|
}
|
|
|
|
.scroll-progress span {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--bronze);
|
|
transform: scaleX(0);
|
|
transform-origin: left;
|
|
will-change: transform;
|
|
}
|
|
|
|
.system-status {
|
|
position: absolute;
|
|
z-index: 50;
|
|
right: clamp(24px, 4vw, 70px);
|
|
bottom: clamp(23px, 3.2vw, 45px);
|
|
color: #6d6a63;
|
|
font-size: 8px;
|
|
letter-spacing: .18em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.system-status::before {
|
|
content: "";
|
|
display: inline-block;
|
|
width: 5px;
|
|
height: 5px;
|
|
margin-right: 8px;
|
|
border-radius: 50%;
|
|
background: #758266;
|
|
animation: pulse 2.4s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
50% { opacity: .35; transform: scale(.8); }
|
|
}
|
|
|
|
.scroll-rail {
|
|
position: relative;
|
|
z-index: -1;
|
|
}
|
|
|
|
.snap-point {
|
|
min-height: 100dvh;
|
|
scroll-snap-align: start;
|
|
scroll-snap-stop: always;
|
|
}
|
|
|
|
.empty-state {
|
|
position: absolute;
|
|
z-index: 80;
|
|
inset: 0;
|
|
display: none;
|
|
place-items: center;
|
|
padding: 24px;
|
|
background: #090908;
|
|
color: #aaa79d;
|
|
text-align: center;
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.header {
|
|
grid-template-columns: 1fr auto;
|
|
}
|
|
|
|
.header__chapter {
|
|
display: none;
|
|
}
|
|
|
|
.stage {
|
|
display: block;
|
|
padding: 96px 22px 76px;
|
|
}
|
|
|
|
.carousel-viewport {
|
|
top: 36%;
|
|
left: 50%;
|
|
width: 100vw;
|
|
height: 48vh;
|
|
min-height: 300px;
|
|
transform: translate3d(-50%, -50%, 0);
|
|
}
|
|
|
|
.orbit-card {
|
|
width: min(76vw, 530px);
|
|
aspect-ratio: 16 / 9;
|
|
}
|
|
|
|
.story-panel {
|
|
position: absolute;
|
|
right: 52px;
|
|
bottom: 64px;
|
|
left: 22px;
|
|
max-width: 620px;
|
|
padding: 22px 24px;
|
|
border: 1px solid rgba(255, 255, 255, .1);
|
|
background: rgba(9, 9, 8, .74);
|
|
backdrop-filter: blur(18px);
|
|
transform: none;
|
|
}
|
|
|
|
.story-panel::before {
|
|
display: none;
|
|
}
|
|
|
|
.story-panel::after {
|
|
display: none;
|
|
}
|
|
|
|
.eyebrow {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.battle-title {
|
|
font-size: clamp(30px, 7vw, 48px);
|
|
}
|
|
|
|
.battle-meta {
|
|
margin: 16px 0 13px;
|
|
padding: 10px 0;
|
|
}
|
|
|
|
.battle-meta .meta-location,
|
|
.battle-meta .meta-location + dd {
|
|
display: none;
|
|
}
|
|
|
|
.battle-summary {
|
|
display: -webkit-box;
|
|
overflow: hidden;
|
|
font-size: 13px;
|
|
line-height: 1.7;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 3;
|
|
}
|
|
|
|
.story-index {
|
|
position: absolute;
|
|
top: 17px;
|
|
right: 20px;
|
|
margin: 0;
|
|
}
|
|
|
|
.timeline {
|
|
right: 20px;
|
|
}
|
|
|
|
.timeline button span {
|
|
display: none;
|
|
}
|
|
|
|
.scroll-hint {
|
|
left: 22px;
|
|
transform: none;
|
|
}
|
|
|
|
.system-status {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 560px) {
|
|
.header {
|
|
padding: 18px;
|
|
}
|
|
|
|
.brand__mark-frame {
|
|
width: 32px;
|
|
height: 32px;
|
|
}
|
|
|
|
.brand__text strong {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.brand__text small,
|
|
.sound-button span {
|
|
display: none;
|
|
}
|
|
|
|
.sound-button {
|
|
width: 38px;
|
|
padding: 0;
|
|
justify-content: center;
|
|
}
|
|
|
|
.carousel-viewport {
|
|
top: 31%;
|
|
height: 40vh;
|
|
}
|
|
|
|
.orbit-card {
|
|
width: 78vw;
|
|
}
|
|
|
|
.story-panel {
|
|
right: 38px;
|
|
bottom: 58px;
|
|
left: 14px;
|
|
padding: 18px;
|
|
}
|
|
|
|
.eyebrow {
|
|
font-size: 8px;
|
|
}
|
|
|
|
.battle-title {
|
|
max-width: 8em;
|
|
font-size: clamp(28px, 9vw, 42px);
|
|
}
|
|
|
|
.battle-meta {
|
|
grid-template-columns: 46px 1fr;
|
|
}
|
|
|
|
.battle-meta dd {
|
|
font-size: 10px;
|
|
}
|
|
|
|
.timeline {
|
|
right: 10px;
|
|
}
|
|
|
|
.scroll-hint {
|
|
bottom: 20px;
|
|
left: 16px;
|
|
}
|
|
|
|
.scroll-hint span:last-child {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media (pointer: coarse) {
|
|
.grid-wall,
|
|
.ink-landscape,
|
|
.story-panel,
|
|
.battle-glyphs span,
|
|
.orbit {
|
|
transition: none;
|
|
transform: none;
|
|
}
|
|
|
|
.orbit-card {
|
|
box-shadow: 0 22px 55px rgba(0, 0, 0, .42);
|
|
}
|
|
|
|
.meta-mote__shape {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
html {
|
|
scroll-behavior: auto;
|
|
}
|
|
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
scroll-behavior: auto !important;
|
|
animation-duration: .001ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: .001ms !important;
|
|
}
|
|
|
|
#particleCanvas {
|
|
display: none;
|
|
}
|
|
|
|
.meta-mote__shape {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
/* Bright archive edition · 4K glass ding + vermilion etching */
|
|
@font-face {
|
|
font-family: "HengShanMaoXing";
|
|
src: url("HengShanMaoXing/HengShanMaoXing/HengShanMaoXing-1.ttf") format("truetype");
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: "YunFengJingLongXingShu";
|
|
src: url("YunFengJingLongXingShu/YunFengJingLongXingShu/YunFengJingLongXingShu-2.ttf") format("truetype");
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: "ZhengFengMaoBi";
|
|
src: url("ZhengFengMaoBi-Light/ZhengFengMaoBi-Light/MasaFont-Light-2.ttf") format("truetype");
|
|
font-display: swap;
|
|
}
|
|
|
|
:root {
|
|
color-scheme: light;
|
|
--ink: #ececea;
|
|
--paper: #17181b;
|
|
--muted: #66686d;
|
|
--bronze: #b71d2a;
|
|
--bronze-soft: rgba(183, 29, 42, .24);
|
|
--line: rgba(26, 28, 33, .16);
|
|
--vermilion: #b71d2a;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
background: var(--ink);
|
|
color: var(--paper);
|
|
}
|
|
|
|
button:focus-visible,
|
|
a:focus-visible {
|
|
outline-color: var(--vermilion);
|
|
}
|
|
|
|
.loading-screen {
|
|
color: #202126;
|
|
background:
|
|
radial-gradient(circle at 62% 45%, rgba(183, 29, 42, .08), transparent 24%),
|
|
#efefec;
|
|
}
|
|
|
|
.loader__label {
|
|
color: #4c4e53;
|
|
}
|
|
|
|
.loader__track {
|
|
background: rgba(25, 27, 31, .16);
|
|
}
|
|
|
|
.experience {
|
|
background:
|
|
radial-gradient(circle at 57% 47%, rgba(255, 255, 255, .98) 0%, rgba(255, 255, 255, .82) 19%, transparent 39%),
|
|
radial-gradient(circle at 74% 36%, rgba(183, 29, 42, .095), transparent 23%),
|
|
linear-gradient(116deg, #f6f6f3 0%, #e9e9e7 47%, #d7d8da 100%);
|
|
}
|
|
|
|
.experience::before {
|
|
content: none;
|
|
}
|
|
|
|
.experience::after {
|
|
content: "赤 · 魏 · 蜀 · 吴 / 228—264";
|
|
position: absolute;
|
|
z-index: 4;
|
|
top: 23%;
|
|
right: 4.8vw;
|
|
color: rgba(168, 15, 29, .48);
|
|
font-size: 8px;
|
|
font-weight: 700;
|
|
letter-spacing: .34em;
|
|
writing-mode: vertical-rl;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.ink-landscape {
|
|
opacity: .09;
|
|
filter: grayscale(1) contrast(1.35) brightness(.64);
|
|
mix-blend-mode: multiply;
|
|
}
|
|
|
|
.grid-wall {
|
|
opacity: .52;
|
|
background-image:
|
|
linear-gradient(rgba(25, 27, 32, .13) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(25, 27, 32, .13) 1px, transparent 1px);
|
|
mask-image: linear-gradient(to bottom, #000 0%, #000 62%, transparent 100%);
|
|
}
|
|
|
|
.grid-floor {
|
|
opacity: .36;
|
|
background-image:
|
|
linear-gradient(rgba(25, 27, 32, .16) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(25, 27, 32, .16) 1px, transparent 1px);
|
|
}
|
|
|
|
.vignette {
|
|
background:
|
|
linear-gradient(90deg, rgba(246, 246, 243, .98) 0%, rgba(242, 242, 239, .87) 23%, rgba(241, 241, 238, .32) 43%, transparent 65%),
|
|
radial-gradient(ellipse at center, transparent 43%, rgba(39, 41, 47, .16) 118%);
|
|
}
|
|
|
|
.grain {
|
|
opacity: .12;
|
|
mix-blend-mode: multiply;
|
|
}
|
|
|
|
.battle-glyphs {
|
|
color: rgba(25, 27, 32, .065);
|
|
font-family: "HengShanMaoXing", "STKaiti", "KaiTi", serif;
|
|
}
|
|
|
|
.battle-glyphs span:nth-child(2) {
|
|
color: rgba(183, 29, 42, .075);
|
|
}
|
|
|
|
#particleCanvas {
|
|
opacity: .48;
|
|
mix-blend-mode: multiply;
|
|
}
|
|
|
|
.brand {
|
|
color: #1c1d21;
|
|
}
|
|
|
|
.brand__mark-frame {
|
|
border-color: rgba(28, 29, 33, .18);
|
|
background: #f8f8f5;
|
|
box-shadow: 0 10px 28px rgba(31, 33, 38, .12);
|
|
}
|
|
|
|
.brand__text strong {
|
|
font-family: "HengShanMaoXing", "STKaiti", "KaiTi", serif;
|
|
font-size: 19px;
|
|
}
|
|
|
|
.brand__text small,
|
|
.header__chapter {
|
|
color: #74767c;
|
|
}
|
|
|
|
.header__chapter::before {
|
|
content: "";
|
|
display: inline-block;
|
|
width: 34px;
|
|
height: 1px;
|
|
margin: 0 12px 2px 0;
|
|
background: var(--vermilion);
|
|
}
|
|
|
|
.sound-button {
|
|
border-color: rgba(31, 33, 38, .22);
|
|
background: rgba(255, 255, 255, .55);
|
|
box-shadow: 0 12px 34px rgba(31, 33, 38, .09);
|
|
}
|
|
|
|
.sound-button:hover {
|
|
border-color: rgba(183, 29, 42, .65);
|
|
background: rgba(183, 29, 42, .08);
|
|
}
|
|
|
|
.story-panel::before {
|
|
width: 2px;
|
|
background: linear-gradient(var(--vermilion), rgba(183, 29, 42, 0));
|
|
}
|
|
|
|
.story-panel::after {
|
|
inset: -58px -165px -60px -40px;
|
|
border-left: 1px solid rgba(183, 29, 42, .16);
|
|
background: linear-gradient(90deg, rgba(249, 249, 246, .99) 0%, rgba(247, 247, 244, .94) 58%, rgba(247, 247, 244, .28) 84%, transparent 100%);
|
|
box-shadow: -30px 30px 80px rgba(46, 48, 54, .05);
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
.battle-title {
|
|
font-family: "HengShanMaoXing", "YunFengJingLongXingShu", "ZhengFengMaoBi", "STKaiti", "KaiTi", serif;
|
|
color: #17181b;
|
|
text-shadow: 0 16px 34px rgba(31, 33, 38, .08);
|
|
}
|
|
|
|
.battle-meta dt,
|
|
.story-index {
|
|
color: #77797f;
|
|
}
|
|
|
|
.battle-meta dd,
|
|
.battle-summary {
|
|
color: #42444a;
|
|
}
|
|
|
|
.story-index strong {
|
|
color: #17181b;
|
|
}
|
|
|
|
.orbit-ring,
|
|
.orbit-ring::before {
|
|
border-color: rgba(183, 29, 42, .34);
|
|
box-shadow: 0 0 75px rgba(183, 29, 42, .07);
|
|
}
|
|
|
|
.orbit-ring::before {
|
|
border-color: rgba(23, 24, 28, .14);
|
|
}
|
|
|
|
.orbit-card {
|
|
border-color: rgba(27, 29, 34, .18);
|
|
background: #d4d5d6;
|
|
box-shadow: 0 42px 100px rgba(34, 36, 42, .28), 0 1px 0 rgba(255, 255, 255, .82) inset;
|
|
}
|
|
|
|
.orbit-card::before {
|
|
background:
|
|
linear-gradient(125deg, rgba(255, 255, 255, .26), transparent 20%, transparent 73%, rgba(183, 29, 42, .16)),
|
|
linear-gradient(to top, rgba(12, 13, 16, .68), transparent 42%);
|
|
}
|
|
|
|
.orbit-card::after {
|
|
border-color: rgba(255, 255, 255, .3);
|
|
}
|
|
|
|
.orbit-card.is-active {
|
|
border: 2px solid rgba(183, 29, 42, .96);
|
|
box-shadow: 0 46px 120px rgba(41, 43, 49, .3), 0 0 0 8px rgba(183, 29, 42, .06);
|
|
}
|
|
|
|
.orbit-card img {
|
|
filter: saturate(.5) contrast(.95) brightness(.96);
|
|
}
|
|
|
|
.orbit-card.is-active img {
|
|
filter: saturate(.78) contrast(1.01) brightness(1.05);
|
|
}
|
|
|
|
.card-fallback {
|
|
color: rgba(31, 33, 38, .55);
|
|
background:
|
|
linear-gradient(45deg, transparent 48%, rgba(183, 29, 42, .14) 49%, rgba(183, 29, 42, .14) 51%, transparent 52%),
|
|
#e0e0de;
|
|
}
|
|
|
|
.card-caption strong {
|
|
font-family: "HengShanMaoXing", "YunFengJingLongXingShu", "ZhengFengMaoBi", "STKaiti", "KaiTi", serif;
|
|
}
|
|
|
|
.card-caption span {
|
|
color: #e8e8e5;
|
|
}
|
|
|
|
.depth-marker {
|
|
border-color: var(--vermilion);
|
|
background: rgba(183, 29, 42, .28);
|
|
box-shadow: 0 0 0 9px rgba(183, 29, 42, .08), 0 0 28px rgba(183, 29, 42, .4);
|
|
}
|
|
|
|
.timeline::before {
|
|
background: rgba(28, 30, 35, .18);
|
|
}
|
|
|
|
.timeline button::before {
|
|
border-color: rgba(31, 33, 38, .38);
|
|
background: #ededeb;
|
|
}
|
|
|
|
.timeline button span {
|
|
color: #3f4147;
|
|
text-shadow: 0 1px 12px rgba(255, 255, 255, .9);
|
|
}
|
|
|
|
.timeline button.is-active::before {
|
|
border-color: var(--vermilion);
|
|
background: var(--vermilion);
|
|
box-shadow: 0 0 0 5px rgba(183, 29, 42, .12);
|
|
}
|
|
|
|
.scroll-hint,
|
|
.system-status {
|
|
color: #55575d;
|
|
}
|
|
|
|
.scroll-hint__mouse {
|
|
border-color: rgba(31, 33, 38, .42);
|
|
}
|
|
|
|
.scroll-progress {
|
|
background: rgba(31, 33, 38, .1);
|
|
}
|
|
|
|
.system-status::before {
|
|
background: var(--vermilion);
|
|
box-shadow: 0 0 12px rgba(183, 29, 42, .55);
|
|
}
|
|
|
|
.empty-state {
|
|
color: #66686d;
|
|
background: #ececea;
|
|
}
|
|
|
|
.ding-scene {
|
|
position: absolute;
|
|
z-index: 22;
|
|
top: 20.8%;
|
|
left: 10.7%;
|
|
width: clamp(150px, 9vw, 190px);
|
|
height: clamp(160px, 19vh, 210px);
|
|
pointer-events: none;
|
|
transform: translate(-50%, -50%);
|
|
filter: drop-shadow(0 16px 22px rgba(30, 32, 38, .16));
|
|
}
|
|
|
|
.ding-scene::after {
|
|
content: "";
|
|
position: absolute;
|
|
z-index: -1;
|
|
left: 50%;
|
|
pointer-events: none;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.ding-scene::after {
|
|
bottom: 7%;
|
|
width: 66%;
|
|
height: 8%;
|
|
border-radius: 50%;
|
|
background: radial-gradient(ellipse, rgba(29, 31, 37, .24) 0%, rgba(183, 29, 42, .06) 35%, transparent 72%);
|
|
filter: blur(5px);
|
|
}
|
|
|
|
#dingCanvas {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
opacity: 0;
|
|
transition: opacity 1.1s cubic-bezier(.16, 1, .3, 1), filter 1.1s ease;
|
|
filter: blur(8px);
|
|
}
|
|
|
|
.ding-scene.is-ready #dingCanvas {
|
|
opacity: 1;
|
|
filter: blur(0);
|
|
}
|
|
|
|
.ding-loading {
|
|
display: none;
|
|
}
|
|
|
|
.ding-loading::before {
|
|
content: "";
|
|
width: 25px;
|
|
height: 1px;
|
|
background: var(--vermilion);
|
|
}
|
|
|
|
.ding-scene.is-ready .ding-loading {
|
|
opacity: .5;
|
|
}
|
|
|
|
.ding-fallback {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
color: rgba(183, 29, 42, .1);
|
|
font-family: "HengShanMaoXing", "STKaiti", "KaiTi", serif;
|
|
font-size: min(12vw, 170px);
|
|
line-height: 1;
|
|
transform: translate(-50%, -52%);
|
|
transition: opacity .8s ease;
|
|
}
|
|
|
|
.ding-scene.is-ready .ding-fallback {
|
|
opacity: 0;
|
|
}
|
|
|
|
.ding-scene.is-fallback .ding-fallback {
|
|
opacity: 1;
|
|
color: rgba(183, 29, 42, .22);
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.story-panel {
|
|
border-color: rgba(183, 29, 42, .18);
|
|
background: rgba(247, 247, 244, .82);
|
|
box-shadow: 0 26px 70px rgba(39, 41, 47, .13);
|
|
}
|
|
|
|
.ding-scene {
|
|
top: 18%;
|
|
left: 19%;
|
|
width: clamp(130px, 18vw, 180px);
|
|
height: clamp(140px, 22vh, 190px);
|
|
}
|
|
|
|
}
|
|
|
|
@media (max-width: 560px) {
|
|
.experience::after {
|
|
display: none;
|
|
}
|
|
|
|
.ding-scene {
|
|
top: 15%;
|
|
left: 27%;
|
|
width: 112px;
|
|
height: 130px;
|
|
}
|
|
|
|
.ding-loading {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
html.story-route-lock {
|
|
overflow: hidden;
|
|
scroll-snap-type: none;
|
|
}
|
|
|
|
.story-route-transition {
|
|
position: fixed;
|
|
z-index: 300;
|
|
inset: 0;
|
|
overflow: hidden;
|
|
pointer-events: none;
|
|
contain: layout style paint;
|
|
}
|
|
|
|
.story-route-transition__image {
|
|
position: fixed;
|
|
z-index: 1;
|
|
display: block;
|
|
object-fit: cover;
|
|
transform: translate3d(0, 0, 0) scale(1);
|
|
transform-origin: 50% 50%;
|
|
transition:
|
|
transform 420ms cubic-bezier(.16, 1, .3, 1),
|
|
border-radius 320ms ease,
|
|
filter 340ms ease;
|
|
will-change: transform, filter;
|
|
}
|
|
|
|
.story-route-transition__wash {
|
|
position: absolute;
|
|
z-index: 2;
|
|
inset: 0;
|
|
background:
|
|
radial-gradient(circle at 50% 46%, transparent 16%, rgba(9, 9, 8, .34) 72%),
|
|
rgba(9, 9, 8, .42);
|
|
opacity: 0;
|
|
transition: opacity 210ms cubic-bezier(.45, 0, .2, 1) 90ms;
|
|
}
|
|
|
|
.story-route-transition__caption {
|
|
position: absolute;
|
|
z-index: 3;
|
|
right: clamp(1.5rem, 5vw, 5rem);
|
|
bottom: clamp(1.5rem, 6vh, 5rem);
|
|
color: rgba(238, 232, 220, .9);
|
|
font-family: "ZhengFengMaoBi", "STKaiti", "KaiTi", serif;
|
|
font-size: clamp(.72rem, 1vw, .95rem);
|
|
letter-spacing: .24em;
|
|
opacity: 0;
|
|
transform: translate3d(0, .7rem, 0);
|
|
transition: opacity 200ms ease 90ms, transform 300ms cubic-bezier(.16, 1, .3, 1) 60ms;
|
|
}
|
|
|
|
.story-route-transition.is-expanding .story-route-transition__image {
|
|
border-radius: 0 !important;
|
|
filter: brightness(.72) saturate(.84);
|
|
transform:
|
|
translate3d(var(--story-shift-x), var(--story-shift-y), 0)
|
|
scale(var(--story-cover-scale));
|
|
}
|
|
|
|
.story-route-transition.is-expanding .story-route-transition__wash {
|
|
opacity: 1;
|
|
}
|
|
|
|
.story-route-transition.is-expanding .story-route-transition__caption {
|
|
opacity: .88;
|
|
transform: translate3d(0, 0, 0);
|
|
}
|
|
|
|
.story-route-transition.is-resetting {
|
|
opacity: 0;
|
|
transition: opacity 220ms ease;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.story-route-transition__image,
|
|
.story-route-transition__wash,
|
|
.story-route-transition__caption,
|
|
.story-route-transition.is-resetting {
|
|
transition-duration: 180ms;
|
|
transition-delay: 0ms;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="loading-screen" id="loadingScreen" role="status" aria-live="polite">
|
|
<div class="loader">
|
|
<div class="loader__label"><span>正在展开战场档案</span><span id="loadCount">00 / 06</span></div>
|
|
<div class="loader__track"><div class="loader__bar" id="loaderBar"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="experience" id="experience">
|
|
<img class="ink-landscape" src="three-kingdoms-parallax/layer-back-2560x1440.webp" alt="" aria-hidden="true">
|
|
<div class="atmosphere" aria-hidden="true">
|
|
<div class="grid-wall"></div>
|
|
<div class="grid-floor"></div>
|
|
<div class="battle-glyphs"><span>鼎</span><span>吴</span><span>蜀</span></div>
|
|
<canvas id="particleCanvas"></canvas>
|
|
</div>
|
|
<div class="vignette" aria-hidden="true"></div>
|
|
<div class="meta-field" id="metaField" aria-hidden="true">
|
|
<div class="meta-zone meta-zone--top" data-meta-zone="top"></div>
|
|
<div class="meta-zone meta-zone--bottom" data-meta-zone="bottom"></div>
|
|
<div class="meta-zone meta-zone--left" data-meta-zone="left"></div>
|
|
<div class="meta-zone meta-zone--right" data-meta-zone="right"></div>
|
|
</div>
|
|
<div class="grain" aria-hidden="true"></div>
|
|
|
|
<header class="header">
|
|
<a class="brand" href="#battle-1" aria-label="回到第一场战役">
|
|
<span class="brand__mark-frame"><img class="brand__mark" src="../../assets/Logo/Logo.png" alt="Digital Me 标志"></span>
|
|
<span class="brand__text">
|
|
<strong>三国鼎立</strong>
|
|
<small>Three Kingdoms Archive</small>
|
|
</span>
|
|
</a>
|
|
<p class="header__chapter">卷一 · 鼎势与余响 / 228—264</p>
|
|
<button class="sound-button" id="soundButton" type="button" aria-pressed="false" aria-label="播放背景音乐">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
|
<path d="M5 9v6h4l5 4V5L9 9H5Z"></path>
|
|
<path class="wave" d="M17 9.5a4 4 0 0 1 0 5M19.5 7a7.5 7.5 0 0 1 0 10"></path>
|
|
</svg>
|
|
<span id="soundLabel">开启声场</span>
|
|
</button>
|
|
</header>
|
|
|
|
<main class="stage">
|
|
<section class="story-panel" aria-label="当前战役介绍">
|
|
<div class="story-content" id="storyContent" aria-live="polite">
|
|
<p class="eyebrow" id="battleEra"></p>
|
|
<h1 class="battle-title" id="battleTitle"></h1>
|
|
<dl class="battle-meta">
|
|
<dt>交战</dt><dd id="battleSides"></dd>
|
|
<dt>时间</dt><dd id="battleTime"></dd>
|
|
<dt class="meta-location">地点</dt><dd id="battleLocation"></dd>
|
|
</dl>
|
|
<p class="battle-summary" id="battleSummary"></p>
|
|
<div class="story-index"><strong id="currentIndex">01</strong><span>/</span><span id="totalIndex">06</span></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="carousel-viewport" aria-label="三国战役3D环形轮播">
|
|
<div class="orbit" id="orbit">
|
|
<div class="orbit-ring" aria-hidden="true"></div>
|
|
<div class="depth-marker" aria-hidden="true"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="ding-scene" id="dingScene" aria-label="玻璃材质三足鼎三维模型">
|
|
<canvas id="dingCanvas" role="img" aria-label="带朱红浮雕纹路的透明玻璃三足鼎"></canvas>
|
|
<div class="ding-fallback" id="dingFallback" aria-hidden="true">鼎</div>
|
|
<div class="ding-loading" id="dingLoading" aria-live="polite">4K DING / LOADING</div>
|
|
</div>
|
|
</main>
|
|
|
|
<nav class="timeline" id="timeline" aria-label="战役时间线"></nav>
|
|
|
|
<div class="scroll-hint" aria-hidden="true">
|
|
<span class="scroll-hint__mouse"></span>
|
|
<span>滚动 · 推演下一役</span>
|
|
</div>
|
|
<div class="system-status" id="systemStatus" aria-live="polite">场景运行中</div>
|
|
<div class="scroll-progress" aria-hidden="true"><span id="progressBar"></span></div>
|
|
<div class="empty-state" id="emptyState">未找到可展示的战役档案。</div>
|
|
</div>
|
|
|
|
<div class="scroll-rail" id="scrollRail" aria-hidden="true"></div>
|
|
|
|
<audio id="bgm" loop preload="metadata">
|
|
<source src="../../assets/bgm/VideoPlus__【古琴】天地缓缓.m4a" type="audio/mp4">
|
|
</audio>
|
|
|
|
<script>
|
|
(() => {
|
|
'use strict';
|
|
|
|
const battles = [
|
|
{
|
|
id: 'jieting',
|
|
year: '公元二二八年',
|
|
title: '街亭之战',
|
|
subtitle: '诸葛亮第一次北伐',
|
|
sides: '蜀汉 · 马谡、王平 × 曹魏 · 张郃',
|
|
time: '蜀汉建兴六年春',
|
|
location: '街亭,今甘肃天水秦安一带(具体今址尚有争议)',
|
|
summary: '马谡舍水上山,张郃截断汲道而大破蜀军。街亭失守使诸葛亮第一次北伐失去前进支点,最终拔西县千余家退回汉中。',
|
|
image: '街亭之战·诸葛亮第一次北伐/images/04-drums-hold-the-line.webp',
|
|
alt: '夕阳下,王平率蜀军列鼓自守,远处骑兵停驻山谷。'
|
|
},
|
|
{
|
|
id: 'wuzhang',
|
|
year: '公元二三四年',
|
|
title: '五丈原之战',
|
|
subtitle: '诸葛亮卒于军',
|
|
sides: '蜀汉 · 诸葛亮 × 曹魏 · 司马懿',
|
|
time: '蜀汉建兴十二年春至八月',
|
|
location: '武功五丈原、渭水南岸',
|
|
summary: '诸葛亮据五丈原屯田久驻,司马懿奉诏坚壁拒守。两军相持百余日后诸葛亮病卒军中,蜀军整阵退还,北伐止于渭水南岸。',
|
|
image: '五丈原之战(诸葛亮卒于军)/images/02-farming-wei-bank.webp',
|
|
alt: '渭水岸边,军民在五丈原屯田,远处营垒沿高地展开。'
|
|
},
|
|
{
|
|
id: 'gaopingling',
|
|
year: '公元二四九年',
|
|
title: '高平陵之变',
|
|
subtitle: '曹魏权柄易手',
|
|
sides: '司马懿集团 × 曹爽集团',
|
|
time: '正始十年正月甲午至戊戌',
|
|
location: '洛阳、高平陵、伊水南与洛水浮桥',
|
|
summary: '司马懿趁魏帝曹芳与曹爽兄弟出城谒陵控制洛阳,迫使曹爽交权,随后诛灭曹爽集团。高平陵没有大战,曹魏实权却由此转入司马氏之手。',
|
|
image: '高平陵之变/images/04-cao-shuang-drops-dao.webp',
|
|
alt: '阴沉军帐内,曹爽面对桓范投刀交权,帐外军阵沉默。'
|
|
},
|
|
{
|
|
id: 'caomao',
|
|
year: '公元二六〇年',
|
|
title: '曹髦之死',
|
|
subtitle: '高贵乡公遇弑',
|
|
sides: '魏帝曹髦 × 司马昭集团',
|
|
time: '曹魏甘露五年五月己丑',
|
|
location: '洛阳宫城云龙门至南阙一带',
|
|
summary: '二十岁的魏帝曹髦不愿坐受废辱,率少数宿卫出宫讨司马昭,行至南阙被贾充所部阻截并遇弑;宫门血变之后,司马昭仍掌朝政。',
|
|
image: '高贵乡公曹髦之死/images/05-only-one-line-remains.webp',
|
|
alt: '暮色宫门外,空下来的帝车停在雨后石道,远处葬仪离去。'
|
|
},
|
|
{
|
|
id: 'wei-shu',
|
|
year: '公元二六三年',
|
|
title: '魏灭蜀之战',
|
|
subtitle: '邓艾钟会伐蜀 · 刘禅出降',
|
|
sides: '曹魏 · 邓艾、钟会 × 蜀汉 · 姜维、诸葛瞻',
|
|
time: '曹魏景元四年秋至冬',
|
|
location: '汉中、剑阁、阴平、江油、绵竹、成都',
|
|
summary: '钟会主力进入汉中却受阻剑阁,邓艾由阴平险道迂回江油、击破绵竹。刘禅随后在成都出降,延续四十二年的蜀汉政权覆亡。',
|
|
image: '魏灭蜀之战(邓艾钟会伐蜀·刘禅出降)/images/01-三路入汉中.webp',
|
|
alt: '阴云笼罩秦岭谷道,魏军沿山路向汉中推进。'
|
|
},
|
|
{
|
|
id: 'zhonghui',
|
|
year: '公元二六四年',
|
|
title: '钟会之乱',
|
|
subtitle: '邓艾、钟会之死',
|
|
sides: '钟会、姜维 × 魏军诸将',
|
|
time: '曹魏景元五年正月',
|
|
location: '益州成都、绵竹西',
|
|
summary: '灭蜀后,钟会先促成邓艾被捕,继而在成都起兵失败。钟会与姜维死于兵变,刚被旧部救出的邓艾也与子邓忠在绵竹西被杀。',
|
|
image: '钟会之乱(邓艾、钟会之死)/images/05-death-west-mianzhu.webp',
|
|
alt: '绵竹西的冬路上,邓艾父子在槛车旁被魏军包围。'
|
|
}
|
|
];
|
|
|
|
const refs = {
|
|
orbit: document.getElementById('orbit'),
|
|
timeline: document.getElementById('timeline'),
|
|
rail: document.getElementById('scrollRail'),
|
|
story: document.getElementById('storyContent'),
|
|
era: document.getElementById('battleEra'),
|
|
title: document.getElementById('battleTitle'),
|
|
sides: document.getElementById('battleSides'),
|
|
time: document.getElementById('battleTime'),
|
|
location: document.getElementById('battleLocation'),
|
|
summary: document.getElementById('battleSummary'),
|
|
current: document.getElementById('currentIndex'),
|
|
total: document.getElementById('totalIndex'),
|
|
progress: document.getElementById('progressBar'),
|
|
loading: document.getElementById('loadingScreen'),
|
|
loaderBar: document.getElementById('loaderBar'),
|
|
loadCount: document.getElementById('loadCount'),
|
|
empty: document.getElementById('emptyState'),
|
|
sound: document.getElementById('soundButton'),
|
|
soundLabel: document.getElementById('soundLabel'),
|
|
audio: document.getElementById('bgm'),
|
|
status: document.getElementById('systemStatus'),
|
|
canvas: document.getElementById('particleCanvas'),
|
|
metaField: document.getElementById('metaField')
|
|
};
|
|
|
|
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
const coarsePointer = window.matchMedia('(pointer: coarse)').matches;
|
|
let activeIndex = -1;
|
|
let progress = 0;
|
|
let frameRequested = false;
|
|
let storyTimer = 0;
|
|
let snapTimer = 0;
|
|
let loaded = 0;
|
|
let loadFinished = false;
|
|
const metaShapes = [
|
|
'../../assets/meta_PIC/十字-01.svg',
|
|
'../../assets/meta_PIC/形状-椭圆形.svg',
|
|
'../../assets/meta_PIC/tx-正方形.svg'
|
|
];
|
|
const metaMotes = [];
|
|
let metaFrame = 0;
|
|
let metaResetTimer = 0;
|
|
let previousPointerX = window.innerWidth * .5;
|
|
let previousPointerY = window.innerHeight * .5;
|
|
let previousPointerTime = performance.now();
|
|
let storyTransitioning = false;
|
|
let storyTransitionOverlay = null;
|
|
let storyTransitionSourceImage = null;
|
|
const standaloneStoryEntries = {
|
|
jieting: '故事集/stories/jieting-battle/index.html',
|
|
wuzhang: '故事集/stories/wuzhang-plains/index.html',
|
|
gaopingling: '故事集/stories/gaopingling-coup/index.html',
|
|
caomao: '故事集/stories/cao-mao-death/index.html',
|
|
'wei-shu': '故事集/stories/wei-conquest-shu/index.html',
|
|
zhonghui: '故事集/stories/zhong-hui-rebellion/index.html'
|
|
};
|
|
|
|
function resetStoryTransition() {
|
|
if (!storyTransitionOverlay) return;
|
|
storyTransitionOverlay.classList.add('is-resetting');
|
|
document.documentElement.classList.remove('story-route-lock');
|
|
if (storyTransitionSourceImage) storyTransitionSourceImage.style.visibility = '';
|
|
const retiringOverlay = storyTransitionOverlay;
|
|
storyTransitionOverlay = null;
|
|
storyTransitionSourceImage = null;
|
|
storyTransitioning = false;
|
|
window.setTimeout(() => retiringOverlay.remove(), reduceMotion ? 200 : 440);
|
|
}
|
|
|
|
function enterStory(battle, card, image) {
|
|
if (storyTransitioning || !battle || !image) return;
|
|
const rect = image.getBoundingClientRect();
|
|
if (rect.width < 2 || rect.height < 2) return;
|
|
storyTransitioning = true;
|
|
|
|
const overlay = document.createElement('div');
|
|
const expandedImage = image.cloneNode(true);
|
|
const wash = document.createElement('span');
|
|
const caption = document.createElement('span');
|
|
const viewportCenterX = window.innerWidth / 2;
|
|
const viewportCenterY = window.innerHeight / 2;
|
|
const imageCenterX = rect.left + rect.width / 2;
|
|
const imageCenterY = rect.top + rect.height / 2;
|
|
const coverScale = Math.max(window.innerWidth / rect.width, window.innerHeight / rect.height) * 1.015;
|
|
|
|
overlay.className = 'story-route-transition';
|
|
overlay.setAttribute('aria-hidden', 'true');
|
|
overlay.style.setProperty('--story-shift-x', `${(viewportCenterX - imageCenterX).toFixed(2)}px`);
|
|
overlay.style.setProperty('--story-shift-y', `${(viewportCenterY - imageCenterY).toFixed(2)}px`);
|
|
overlay.style.setProperty('--story-cover-scale', coverScale.toFixed(4));
|
|
expandedImage.className = 'story-route-transition__image';
|
|
expandedImage.removeAttribute('loading');
|
|
expandedImage.style.left = `${rect.left.toFixed(2)}px`;
|
|
expandedImage.style.top = `${rect.top.toFixed(2)}px`;
|
|
expandedImage.style.width = `${rect.width.toFixed(2)}px`;
|
|
expandedImage.style.height = `${rect.height.toFixed(2)}px`;
|
|
expandedImage.style.borderRadius = getComputedStyle(image).borderRadius;
|
|
wash.className = 'story-route-transition__wash';
|
|
caption.className = 'story-route-transition__caption';
|
|
caption.textContent = `${battle.year} · ${battle.title}`;
|
|
overlay.append(expandedImage, wash, caption);
|
|
document.body.appendChild(overlay);
|
|
document.documentElement.classList.add('story-route-lock');
|
|
image.style.visibility = 'hidden';
|
|
storyTransitionOverlay = overlay;
|
|
storyTransitionSourceImage = image;
|
|
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage({
|
|
type: 'three-kingdoms-story-prefetch',
|
|
storyId: battle.id
|
|
}, window.location.origin);
|
|
}
|
|
|
|
requestAnimationFrame(() => requestAnimationFrame(() => overlay.classList.add('is-expanding')));
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage({
|
|
type: 'three-kingdoms-story-enter',
|
|
storyId: battle.id
|
|
}, window.location.origin);
|
|
} else if (standaloneStoryEntries[battle.id]) {
|
|
window.location.href = standaloneStoryEntries[battle.id];
|
|
} else {
|
|
resetStoryTransition();
|
|
}
|
|
}
|
|
|
|
window.addEventListener('message', event => {
|
|
if (event.origin !== window.location.origin) return;
|
|
if (event.data?.type === 'three-kingdoms-story-reset') resetStoryTransition();
|
|
});
|
|
|
|
function randomBetween(min, max) {
|
|
return min + Math.random() * (max - min);
|
|
}
|
|
|
|
function clamp(value, min, max) {
|
|
return Math.min(max, Math.max(min, value));
|
|
}
|
|
|
|
function updateMetaCenters() {
|
|
const zoneRects = new Map();
|
|
metaMotes.forEach(mote => {
|
|
if (!zoneRects.has(mote.zone)) zoneRects.set(mote.zone, mote.zone.getBoundingClientRect());
|
|
const rect = zoneRects.get(mote.zone);
|
|
mote.centerX = rect.left + mote.left * rect.width;
|
|
mote.centerY = rect.top + mote.top * rect.height;
|
|
});
|
|
}
|
|
|
|
function createMetaField() {
|
|
if (!refs.metaField) return;
|
|
|
|
const count = coarsePointer ? 9 : 18;
|
|
const zones = Object.fromEntries(
|
|
Array.from(refs.metaField.querySelectorAll('[data-meta-zone]')).map(zone => [zone.dataset.metaZone, zone])
|
|
);
|
|
const zoneOrder = coarsePointer
|
|
? ['top', 'top', 'top', 'top', 'top', 'bottom', 'bottom', 'bottom', 'bottom']
|
|
: ['top', 'top', 'top', 'top', 'top', 'top', 'top', 'top', 'top', 'top', 'bottom', 'bottom', 'bottom', 'bottom', 'bottom', 'bottom', 'bottom', 'bottom'];
|
|
|
|
for (let index = 0; index < count; index += 1) {
|
|
const mote = document.createElement('span');
|
|
const shape = document.createElement('img');
|
|
const zone = zones[zoneOrder[index]];
|
|
const left = randomBetween(.08, .9);
|
|
const top = randomBetween(.14, .82);
|
|
const driftX = randomBetween(-22, 22);
|
|
const driftY = randomBetween(-18, 18);
|
|
const driftRotation = randomBetween(-24, 24);
|
|
|
|
mote.className = 'meta-mote';
|
|
shape.className = 'meta-mote__shape';
|
|
shape.src = metaShapes[index % metaShapes.length];
|
|
shape.alt = '';
|
|
shape.decoding = 'async';
|
|
shape.draggable = false;
|
|
if (index % 3 === 0 || Math.random() < .24) mote.classList.add('is-vermilion');
|
|
mote.style.left = `${(left * 100).toFixed(2)}%`;
|
|
mote.style.top = `${(top * 100).toFixed(2)}%`;
|
|
mote.style.setProperty('--meta-scale', randomBetween(.6, 1.6).toFixed(3));
|
|
mote.style.setProperty('--meta-rotation', `${randomBetween(-180, 180).toFixed(2)}deg`);
|
|
mote.style.setProperty('--meta-opacity', randomBetween(.28, .58).toFixed(3));
|
|
mote.style.setProperty('--drift-x', `${driftX.toFixed(2)}px`);
|
|
mote.style.setProperty('--drift-y', `${driftY.toFixed(2)}px`);
|
|
mote.style.setProperty('--drift-rotation', `${driftRotation.toFixed(2)}deg`);
|
|
mote.style.setProperty('--drift-x-start', `${(-driftX * .45).toFixed(2)}px`);
|
|
mote.style.setProperty('--drift-y-start', `${(-driftY * .45).toFixed(2)}px`);
|
|
mote.style.setProperty('--drift-rotation-start', `${(-driftRotation * .35).toFixed(2)}deg`);
|
|
mote.style.setProperty('--drift-x-end', `${(-driftX * .2).toFixed(2)}px`);
|
|
mote.style.setProperty('--drift-y-end', `${(driftY * .7).toFixed(2)}px`);
|
|
mote.style.setProperty('--drift-rotation-end', `${(-driftRotation * .65).toFixed(2)}deg`);
|
|
mote.style.setProperty('--drift-duration', `${randomBetween(13, 27).toFixed(2)}s`);
|
|
mote.style.setProperty('--drift-delay', `${randomBetween(-24, 0).toFixed(2)}s`);
|
|
mote.appendChild(shape);
|
|
zone.appendChild(mote);
|
|
|
|
metaMotes.push({
|
|
element: mote,
|
|
zone,
|
|
left,
|
|
top,
|
|
centerX: 0,
|
|
centerY: 0,
|
|
x: 0,
|
|
y: 0,
|
|
rotation: 0,
|
|
velocityX: 0,
|
|
velocityY: 0,
|
|
velocityRotation: 0,
|
|
targetX: 0,
|
|
targetY: 0,
|
|
targetRotation: 0
|
|
});
|
|
}
|
|
|
|
updateMetaCenters();
|
|
}
|
|
|
|
function animateMetaField() {
|
|
let isMoving = false;
|
|
|
|
metaMotes.forEach(mote => {
|
|
mote.velocityX = (mote.velocityX + (mote.targetX - mote.x) * .075) * .76;
|
|
mote.velocityY = (mote.velocityY + (mote.targetY - mote.y) * .075) * .76;
|
|
mote.velocityRotation = (mote.velocityRotation + (mote.targetRotation - mote.rotation) * .065) * .74;
|
|
mote.x += mote.velocityX;
|
|
mote.y += mote.velocityY;
|
|
mote.rotation += mote.velocityRotation;
|
|
|
|
mote.element.style.setProperty('--fluid-x', `${mote.x.toFixed(2)}px`);
|
|
mote.element.style.setProperty('--fluid-y', `${mote.y.toFixed(2)}px`);
|
|
mote.element.style.setProperty('--fluid-rotation', `${mote.rotation.toFixed(2)}deg`);
|
|
|
|
if (
|
|
Math.abs(mote.x) + Math.abs(mote.y) + Math.abs(mote.rotation) +
|
|
Math.abs(mote.velocityX) + Math.abs(mote.velocityY) + Math.abs(mote.velocityRotation) > .08
|
|
) {
|
|
isMoving = true;
|
|
}
|
|
});
|
|
|
|
if (isMoving) {
|
|
metaFrame = window.requestAnimationFrame(animateMetaField);
|
|
} else {
|
|
metaFrame = 0;
|
|
refs.metaField.classList.remove('is-disturbed');
|
|
}
|
|
}
|
|
|
|
function startMetaAnimation() {
|
|
refs.metaField.classList.add('is-disturbed');
|
|
if (!metaFrame) metaFrame = window.requestAnimationFrame(animateMetaField);
|
|
}
|
|
|
|
function settleMetaField() {
|
|
metaMotes.forEach(mote => {
|
|
mote.targetX = 0;
|
|
mote.targetY = 0;
|
|
mote.targetRotation = 0;
|
|
});
|
|
startMetaAnimation();
|
|
}
|
|
|
|
function disturbMetaField(event) {
|
|
const now = performance.now();
|
|
const elapsed = Math.max(8, now - previousPointerTime);
|
|
const pointerVelocityX = clamp((event.clientX - previousPointerX) / elapsed, -2.4, 2.4);
|
|
const pointerVelocityY = clamp((event.clientY - previousPointerY) / elapsed, -2.4, 2.4);
|
|
const radius = clamp(window.innerWidth * .15, 190, 290);
|
|
|
|
previousPointerX = event.clientX;
|
|
previousPointerY = event.clientY;
|
|
previousPointerTime = now;
|
|
|
|
metaMotes.forEach(mote => {
|
|
const deltaX = mote.centerX - event.clientX;
|
|
const deltaY = mote.centerY - event.clientY;
|
|
const distance = Math.hypot(deltaX, deltaY);
|
|
|
|
if (distance >= radius) {
|
|
mote.targetX = 0;
|
|
mote.targetY = 0;
|
|
mote.targetRotation = 0;
|
|
return;
|
|
}
|
|
|
|
const safeDistance = Math.max(1, distance);
|
|
const strength = Math.pow(1 - distance / radius, 1.7);
|
|
const normalX = deltaX / safeDistance;
|
|
const normalY = deltaY / safeDistance;
|
|
const pointerSpeed = Math.hypot(pointerVelocityX, pointerVelocityY);
|
|
const radialForce = 46 * strength;
|
|
const swirlForce = (12 + pointerSpeed * 16) * strength;
|
|
|
|
mote.targetX = normalX * radialForce - normalY * swirlForce + pointerVelocityX * 9 * strength;
|
|
mote.targetY = normalY * radialForce + normalX * swirlForce + pointerVelocityY * 9 * strength;
|
|
mote.targetRotation = clamp((pointerVelocityX * normalY - pointerVelocityY * normalX) * 38 * strength, -32, 32);
|
|
});
|
|
|
|
window.clearTimeout(metaResetTimer);
|
|
metaResetTimer = window.setTimeout(settleMetaField, 90);
|
|
startMetaAnimation();
|
|
}
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
refs.metaField.classList.toggle('is-paused', document.hidden);
|
|
if (document.hidden && metaFrame) {
|
|
window.cancelAnimationFrame(metaFrame);
|
|
metaFrame = 0;
|
|
refs.metaField.classList.remove('is-disturbed');
|
|
} else if (!document.hidden && !reduceMotion && !coarsePointer) {
|
|
settleMetaField();
|
|
}
|
|
});
|
|
|
|
if (!battles.length) {
|
|
refs.empty.style.display = 'grid';
|
|
refs.loading.classList.add('is-ready');
|
|
return;
|
|
}
|
|
|
|
refs.total.textContent = String(battles.length).padStart(2, '0');
|
|
|
|
const cards = battles.map((battle, index) => {
|
|
const card = document.createElement('article');
|
|
card.className = 'orbit-card';
|
|
card.dataset.index = index;
|
|
card.tabIndex = 0;
|
|
card.setAttribute('role', 'button');
|
|
card.setAttribute('aria-label', `查看${battle.year}${battle.title}`);
|
|
|
|
const image = document.createElement('img');
|
|
image.src = battle.image;
|
|
image.alt = battle.alt;
|
|
image.decoding = 'async';
|
|
image.loading = index < 2 ? 'eager' : 'lazy';
|
|
|
|
const fallback = document.createElement('div');
|
|
fallback.className = 'card-fallback';
|
|
fallback.textContent = battle.title;
|
|
|
|
const caption = document.createElement('div');
|
|
caption.className = 'card-caption';
|
|
caption.innerHTML = `<strong>${battle.title}</strong><span>${battle.year}</span>`;
|
|
|
|
image.addEventListener('load', markLoaded, { once: true });
|
|
image.addEventListener('error', () => {
|
|
card.classList.add('has-error');
|
|
refs.status.textContent = '部分图像未载入';
|
|
markLoaded();
|
|
}, { once: true });
|
|
|
|
const activate = () => enterStory(battle, card, image);
|
|
card.addEventListener('click', activate);
|
|
card.addEventListener('keydown', event => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
activate();
|
|
}
|
|
});
|
|
|
|
card.append(image, fallback, caption);
|
|
refs.orbit.appendChild(card);
|
|
return card;
|
|
});
|
|
|
|
battles.forEach((battle, index) => {
|
|
const section = document.createElement('section');
|
|
section.className = 'snap-point';
|
|
section.id = `battle-${index + 1}`;
|
|
refs.rail.appendChild(section);
|
|
|
|
const dot = document.createElement('button');
|
|
dot.type = 'button';
|
|
dot.setAttribute('aria-label', `前往${battle.year}${battle.title}`);
|
|
dot.innerHTML = `<span>${battle.year.slice(2)} · ${battle.title}</span>`;
|
|
dot.addEventListener('click', () => goTo(index));
|
|
refs.timeline.appendChild(dot);
|
|
});
|
|
|
|
const dots = Array.from(refs.timeline.querySelectorAll('button'));
|
|
|
|
function markLoaded() {
|
|
loaded += 1;
|
|
refs.loadCount.textContent = `${String(loaded).padStart(2, '0')} / ${String(battles.length).padStart(2, '0')}`;
|
|
refs.loaderBar.style.width = `${(loaded / battles.length) * 100}%`;
|
|
if (loaded >= battles.length) finishLoading();
|
|
}
|
|
|
|
function finishLoading() {
|
|
if (loadFinished) return;
|
|
loadFinished = true;
|
|
refs.loaderBar.style.width = '100%';
|
|
window.setTimeout(() => refs.loading.classList.add('is-ready'), 260);
|
|
}
|
|
|
|
window.setTimeout(finishLoading, 3800);
|
|
|
|
function goTo(index) {
|
|
const safeIndex = Math.max(0, Math.min(battles.length - 1, index));
|
|
window.scrollTo({
|
|
top: safeIndex * window.innerHeight,
|
|
behavior: reduceMotion ? 'auto' : 'smooth'
|
|
});
|
|
}
|
|
|
|
function circularDistance(index, value) {
|
|
let distance = index - value;
|
|
const half = battles.length / 2;
|
|
while (distance > half) distance -= battles.length;
|
|
while (distance < -half) distance += battles.length;
|
|
return distance;
|
|
}
|
|
|
|
function renderOrbit() {
|
|
const width = window.innerWidth;
|
|
const mobile = width <= 960;
|
|
const radiusX = mobile ? Math.min(width * .48, 320) : Math.min(width * .31, 620);
|
|
const radiusZ = mobile ? 260 : 420;
|
|
const ySpread = mobile ? 18 : 34;
|
|
|
|
cards.forEach((card, index) => {
|
|
const distance = circularDistance(index, progress);
|
|
const angle = distance * (Math.PI * 2 / battles.length);
|
|
const x = Math.sin(angle) * radiusX;
|
|
const z = Math.cos(angle) * radiusZ;
|
|
const y = Math.abs(Math.sin(angle)) * ySpread + (1 - Math.cos(angle)) * 18;
|
|
const depth = (z + radiusZ) / (radiusZ * 2);
|
|
const scale = mobile ? .7 + depth * .3 : .28 + Math.pow(depth, 3) * .72;
|
|
const opacity = mobile ? .16 + depth * .84 : .08 + Math.pow(depth, 3) * .92;
|
|
const blur = Math.max(0, (1 - depth) * 3.5);
|
|
const rotateY = -Math.sin(angle) * 15;
|
|
card.style.transform = `translate3d(calc(-50% + ${x}px), calc(-50% + ${y}px), ${z}px) rotateY(${rotateY}deg) scale(${scale})`;
|
|
card.style.opacity = opacity.toFixed(3);
|
|
card.style.filter = `blur(${blur.toFixed(2)}px)`;
|
|
card.style.zIndex = String(Math.round(depth * 100));
|
|
card.style.pointerEvents = depth > .72 ? 'auto' : 'none';
|
|
});
|
|
}
|
|
|
|
function updateStory(nextIndex, immediate = false) {
|
|
if (nextIndex === activeIndex) return;
|
|
activeIndex = nextIndex;
|
|
const battle = battles[nextIndex];
|
|
window.clearTimeout(storyTimer);
|
|
|
|
const applyContent = () => {
|
|
refs.era.textContent = `${battle.year} · ${battle.subtitle}`;
|
|
refs.title.innerHTML = `${battle.title}<span class="accent-dot">。</span>`;
|
|
refs.sides.textContent = battle.sides;
|
|
refs.time.textContent = battle.time;
|
|
refs.location.textContent = battle.location;
|
|
refs.summary.textContent = battle.summary;
|
|
refs.current.textContent = String(nextIndex + 1).padStart(2, '0');
|
|
cards.forEach((card, index) => card.classList.toggle('is-active', index === nextIndex));
|
|
dots.forEach((dot, index) => {
|
|
const isActive = index === nextIndex;
|
|
dot.classList.toggle('is-active', isActive);
|
|
dot.setAttribute('aria-current', isActive ? 'true' : 'false');
|
|
});
|
|
document.title = `${battle.title}|三国鼎立`;
|
|
refs.status.textContent = `档案 ${String(nextIndex + 1).padStart(2, '0')} 已定位`;
|
|
refs.story.classList.remove('is-changing');
|
|
};
|
|
|
|
if (immediate || reduceMotion) {
|
|
applyContent();
|
|
return;
|
|
}
|
|
|
|
refs.story.classList.add('is-changing');
|
|
storyTimer = window.setTimeout(applyContent, 175);
|
|
}
|
|
|
|
function readScroll() {
|
|
frameRequested = false;
|
|
const unit = Math.max(1, window.innerHeight);
|
|
progress = Math.max(0, Math.min(battles.length - 1, window.scrollY / unit));
|
|
const nearest = Math.round(progress);
|
|
renderOrbit();
|
|
updateStory(nearest);
|
|
refs.progress.style.transform = `scaleX(${progress / (battles.length - 1)})`;
|
|
window.dispatchEvent(new CustomEvent('three-ding-progress', {
|
|
detail: { progress: progress / Math.max(1, battles.length - 1) }
|
|
}));
|
|
}
|
|
|
|
function scheduleScrollRead() {
|
|
if (!frameRequested) {
|
|
frameRequested = true;
|
|
requestAnimationFrame(readScroll);
|
|
}
|
|
window.clearTimeout(snapTimer);
|
|
snapTimer = window.setTimeout(() => {
|
|
const nearest = Math.round(window.scrollY / Math.max(1, window.innerHeight));
|
|
const target = nearest * window.innerHeight;
|
|
if (Math.abs(window.scrollY - target) > 2) goTo(nearest);
|
|
}, 150);
|
|
}
|
|
|
|
window.addEventListener('scroll', scheduleScrollRead, { passive: true });
|
|
window.addEventListener('resize', () => {
|
|
document.documentElement.style.setProperty('--vh', `${window.innerHeight * .01}px`);
|
|
readScroll();
|
|
resizeCanvas();
|
|
updateMetaCenters();
|
|
});
|
|
|
|
window.addEventListener('keydown', event => {
|
|
const current = Math.round(window.scrollY / Math.max(1, window.innerHeight));
|
|
if (event.key === 'ArrowDown' || event.key === 'PageDown') {
|
|
event.preventDefault();
|
|
goTo(current + 1);
|
|
} else if (event.key === 'ArrowUp' || event.key === 'PageUp') {
|
|
event.preventDefault();
|
|
goTo(current - 1);
|
|
} else if (event.key === 'Home') {
|
|
event.preventDefault();
|
|
goTo(0);
|
|
} else if (event.key === 'End') {
|
|
event.preventDefault();
|
|
goTo(battles.length - 1);
|
|
}
|
|
});
|
|
|
|
if (!coarsePointer && !reduceMotion) {
|
|
window.addEventListener('pointermove', event => {
|
|
const x = (event.clientX / window.innerWidth - .5) * 2;
|
|
const y = (event.clientY / window.innerHeight - .5) * 2;
|
|
document.documentElement.style.setProperty('--mx', x.toFixed(3));
|
|
document.documentElement.style.setProperty('--my', y.toFixed(3));
|
|
}, { passive: true });
|
|
}
|
|
|
|
refs.sound.addEventListener('click', async () => {
|
|
try {
|
|
if (refs.audio.paused) {
|
|
refs.audio.volume = .32;
|
|
await refs.audio.play();
|
|
refs.sound.classList.add('is-playing');
|
|
refs.sound.setAttribute('aria-pressed', 'true');
|
|
refs.sound.setAttribute('aria-label', '暂停背景音乐');
|
|
refs.soundLabel.textContent = '关闭声场';
|
|
refs.status.textContent = '声场已开启';
|
|
} else {
|
|
refs.audio.pause();
|
|
refs.sound.classList.remove('is-playing');
|
|
refs.sound.setAttribute('aria-pressed', 'false');
|
|
refs.sound.setAttribute('aria-label', '播放背景音乐');
|
|
refs.soundLabel.textContent = '开启声场';
|
|
refs.status.textContent = '声场已关闭';
|
|
}
|
|
} catch (error) {
|
|
refs.status.textContent = '浏览器未允许播放音频';
|
|
}
|
|
});
|
|
|
|
const ctx = refs.canvas.getContext('2d');
|
|
let particles = [];
|
|
let canvasFrame = 0;
|
|
|
|
function resizeCanvas() {
|
|
if (!ctx) return;
|
|
const ratio = Math.min(window.devicePixelRatio || 1, 2);
|
|
refs.canvas.width = Math.floor(window.innerWidth * ratio);
|
|
refs.canvas.height = Math.floor(window.innerHeight * ratio);
|
|
refs.canvas.style.width = `${window.innerWidth}px`;
|
|
refs.canvas.style.height = `${window.innerHeight}px`;
|
|
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
const count = window.innerWidth < 600 ? 42 : window.innerWidth < 1000 ? 85 : 150;
|
|
particles = Array.from({ length: count }, (_, index) => ({
|
|
x: Math.random() * window.innerWidth,
|
|
y: Math.random() * window.innerHeight,
|
|
r: index % 13 === 0 ? 1.3 : Math.random() * .8 + .2,
|
|
speed: Math.random() * .13 + .035,
|
|
drift: (Math.random() - .5) * .08,
|
|
alpha: Math.random() * .34 + .08,
|
|
cross: index % 19 === 0
|
|
}));
|
|
}
|
|
|
|
function drawParticles() {
|
|
if (!ctx || reduceMotion) return;
|
|
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
|
particles.forEach(particle => {
|
|
particle.y -= particle.speed;
|
|
particle.x += particle.drift;
|
|
if (particle.y < -8) particle.y = window.innerHeight + 8;
|
|
if (particle.x < -8) particle.x = window.innerWidth + 8;
|
|
if (particle.x > window.innerWidth + 8) particle.x = -8;
|
|
ctx.strokeStyle = `rgba(36, 38, 44, ${particle.alpha})`;
|
|
ctx.fillStyle = ctx.strokeStyle;
|
|
if (particle.cross) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(particle.x - 3, particle.y);
|
|
ctx.lineTo(particle.x + 3, particle.y);
|
|
ctx.moveTo(particle.x, particle.y - 3);
|
|
ctx.lineTo(particle.x, particle.y + 3);
|
|
ctx.stroke();
|
|
} else {
|
|
ctx.beginPath();
|
|
ctx.arc(particle.x, particle.y, particle.r, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
});
|
|
canvasFrame = requestAnimationFrame(drawParticles);
|
|
}
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) {
|
|
cancelAnimationFrame(canvasFrame);
|
|
} else if (!reduceMotion) {
|
|
drawParticles();
|
|
}
|
|
});
|
|
|
|
document.documentElement.style.setProperty('--vh', `${window.innerHeight * .01}px`);
|
|
resizeCanvas();
|
|
updateStory(0, true);
|
|
readScroll();
|
|
if (!reduceMotion) drawParticles();
|
|
})();
|
|
</script>
|
|
<script src="三国鼎立-scene.bundle.js"></script>
|
|
</body>
|
|
</html>
|