Files
digital-me-history/digital-me-web/public/stories/three-kingdoms-dingli/index.html
T
admin 0593ff7e62
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s
数字我 Digital Me:全量入库(大文件走 Git LFS)
2026-10-10 16:24:43 +08:00

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>