From 16b0ad38f383ecb0673f8dad132670dac99b6e71 Mon Sep 17 00:00:00 2001 From: Yaosanqi137 Date: Sun, 1 Mar 2026 23:39:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E7=89=88=E9=A6=96=E9=A1=B5=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/about-hero.css | 358 +++++++++++++ assets/css/front-page.css | 585 +++++++++++---------- assets/css/intro.css | 34 ++ assets/js/landing-hero-canvas.js | 852 +++++++++++++++++++++++++++++++ functions.php | 14 +- header.php | 15 +- index.php | 167 +++--- page-about.php | 358 +++++++------ 8 files changed, 1846 insertions(+), 537 deletions(-) create mode 100644 assets/css/about-hero.css create mode 100644 assets/js/landing-hero-canvas.js diff --git a/assets/css/about-hero.css b/assets/css/about-hero.css new file mode 100644 index 0000000..82d76f6 --- /dev/null +++ b/assets/css/about-hero.css @@ -0,0 +1,358 @@ +/* ========================================= + 工作室介绍页 - 旧首页视觉 + ========================================= */ + +.site-main { + position: relative; +} + +.hero-section { + padding: 40px 0; + background-color: var(--bg-body); + position: relative; + overflow: visible; + background-image: none; + min-height: calc(100vh - var(--header-height)); + display: flex; + align-items: center; + isolation: isolate; + z-index: 1; +} + +.hero-scroll-indicator { + position: absolute; + left: 50%; + bottom: 28px; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + z-index: 4; + color: var(--text-secondary); + font-family: var(--font-mono); + font-size: 0.9rem; + letter-spacing: 0.12em; + text-transform: uppercase; + opacity: 0; + pointer-events: none; + transition: opacity 0.45s var(--ease-in-out), transform 0.45s var(--ease-in-out); +} + +.hero-scroll-indicator .scroll-arrow { + width: 18px; + height: 18px; + border-right: 2px solid currentColor; + border-bottom: 2px solid currentColor; + transform: rotate(45deg); + animation: hero-scroll-bounce 1.6s ease-in-out infinite; +} + +.hero-scroll-indicator .scroll-text { + font-size: 0.7rem; + letter-spacing: 0.3em; +} + +@keyframes hero-scroll-bounce { + 0%, 100% { transform: translateY(0) rotate(45deg); opacity: 0.5; } + 50% { transform: translateY(6px) rotate(45deg); opacity: 1; } +} + +@media (min-width: 900px) { + .home-hero-initial .site-header { + transform: translateY(-100%); + } + + .home-hero-initial .site-header, + .home-hero-scrolled .site-header { + position: fixed; + left: 0; + right: 0; + width: 100%; + } + + .home-hero-initial .hero-scroll-indicator { + opacity: 1; + transform: translateX(-50%) translateY(0); + } + + .home-hero-scrolled .hero-scroll-indicator { + opacity: 0; + transform: translateX(-50%) translateY(10px); + } +} + +.hero-waves { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 140vh; + opacity: 0.5; + pointer-events: none; + z-index: 2; + mix-blend-mode: screen; + filter: saturate(1.08); + display: block; + -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.9) 55%, rgba(0, 0, 0, 0.35) 75%, rgba(0, 0, 0, 0) 100%); + mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.9) 55%, rgba(0, 0, 0, 0.35) 75%, rgba(0, 0, 0, 0) 100%); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; +} + +html:not([data-theme="dark"]) .hero-waves { + opacity: 0.55; + mix-blend-mode: multiply; + filter: saturate(1.2); +} + +[data-theme="dark"] .hero-waves { + opacity: 0.5; + mix-blend-mode: screen; + filter: saturate(1.08); +} + +.hero-section::after { + display: none; +} + +.hero-content { + max-width: 800px; + margin: 0 auto; + text-align: center; + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; +} + +.hero-title { + display: flex; + justify-content: center; + margin: 0 0 1.2rem; + overflow: visible; + --hero-title-gradient: linear-gradient(135deg, var(--text-primary) 0%, var(--color-primary) 100%); + width: 100%; +} + +.hero-title-svg { + display: block; + width: min(88vw, 900px); + margin: 0 auto; + aspect-ratio: 3 / 2; + background-image: var(--hero-title-gradient); + -webkit-mask: url("../../resources/title.svg") center / contain no-repeat; + mask: url("../../resources/title.svg") center / contain no-repeat; + filter: + drop-shadow(0 10px 24px rgba(12, 20, 32, 0.25)) + drop-shadow(0 0 18px rgba(120, 190, 255, 0.28)); + position: relative; + transform: translateY(-8px) scale(1.06); + transform-origin: center; +} + +.hero-title-svg::before { + content: ""; + position: absolute; + inset: -6% -4%; + background: + radial-gradient(circle at 30% 35%, rgba(255, 255, 255, 0.35), transparent 55%), + radial-gradient(circle at 70% 45%, rgba(140, 205, 255, 0.28), transparent 60%), + linear-gradient(180deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0)); + -webkit-mask: url("../../resources/title.svg") center / contain no-repeat; + mask: url("../../resources/title.svg") center / contain no-repeat; + opacity: 0.22; + filter: blur(6px); + mix-blend-mode: screen; + pointer-events: none; +} + +html:not([data-theme="dark"]) .hero-title-svg::before { + opacity: 0; +} + +.hero-title-svg::after { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 100%; + height: 55%; + background-image: var(--hero-title-gradient); + -webkit-mask: url("../../resources/title.svg") center / contain no-repeat; + mask: url("../../resources/title.svg") center / contain no-repeat; + transform: scaleY(-1); + opacity: 0.18; + filter: blur(2px); +} + +.hero-title-text { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.hero-description { + font-size: 1.5rem; + color: var(--text-secondary); + margin-bottom: 3rem; + font-family: var(--font-mono); + min-height: 3.6em; +} + +.stream-char { + transition: opacity 0.4s ease; +} + +.services-provided { + margin-top: 0; + position: relative; + z-index: 2; +} + +.services-section { + padding: 80px 0; + background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--bg-body) 55%); + position: relative; + z-index: 3; +} + +.services-provided h2 { + font-size: 1.8rem; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: none; + color: var(--text-primary); + display: flex; + align-items: center; +} + +.services-provided h2::before { + content: "$"; + color: var(--color-primary); + margin-right: 10px; + font-family: var(--font-mono); + font-weight: 800; +} + +.services-grid-box { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24px; + padding: 0 10px; +} + +.service-item { + background: transparent; + border: none; + border-radius: var(--radius-md); + padding: 32px 24px; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + transition: all var(--duration-fast); + text-decoration: none; +} + +.service-item:hover { + transform: translateY(-4px); + box-shadow: none; +} + +.service-icon { + margin-bottom: 1.5rem; + color: var(--color-primary); +} + +.service-icon svg { + width: 90px; + height: 90px; + display: block; +} + +.service-item span { + font-weight: 600; + color: var(--text-primary); + font-size: 1.25rem; +} + +@media (max-width: 1100px) { + .services-grid-box { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .hero-title { + margin-bottom: 1rem; + } + + .hero-title-svg { + width: min(92vw, 560px); + transform: translateY(-6px) scale(1.03); + } + + .hero-description { + font-size: 1rem; + } + + .services-section { + padding: 28px 0; + } + + .hero-section { + min-height: auto; + padding: 16px 0 16px; + } + + .hero-waves { + display: none; + } + + .hero-scroll-indicator { + display: none; + } +} + +@media (max-width: 600px) { + .services-grid-box { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + } + + .service-item { + padding: 12px 8px; + border: none; + background: transparent; + box-shadow: none; + } + + .service-item:hover { + transform: none; + border-color: transparent; + box-shadow: none; + } + + .service-icon { + margin-bottom: 0.75rem; + } + + .service-icon svg { + width: 64px; + height: 64px; + } + + .service-item span { + font-size: 1rem; + } +} diff --git a/assets/css/front-page.css b/assets/css/front-page.css index 7c964fb..83ea406 100644 --- a/assets/css/front-page.css +++ b/assets/css/front-page.css @@ -1,368 +1,365 @@ /* ========================================= - 首页独有样式 (Front Page / Hero) + Front Page - Minimal Landing Layout ========================================= */ -/* --- Hero 区域 --- */ -.site-main { - position: relative; -} - -.hero-section { - padding: 40px 0; +.home-landing { background-color: var(--bg-body); +} + +.landing-hero { position: relative; - overflow: visible; - background-image: none; - min-height: calc(100vh - var(--header-height)); + min-height: clamp(420px, 68vh, 700px); display: flex; align-items: center; - isolation: isolate; - z-index: 1; -} - -.hero-scroll-indicator { - position: absolute; - left: 50%; - bottom: 28px; - transform: translateX(-50%); - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - z-index: 4; - color: var(--text-secondary); - font-family: var(--font-mono); - font-size: 0.9rem; - letter-spacing: 0.12em; - text-transform: uppercase; - opacity: 0; - pointer-events: none; - transition: opacity 0.45s var(--ease-in-out), transform 0.45s var(--ease-in-out); -} - -.hero-scroll-indicator .scroll-arrow { - width: 18px; - height: 18px; - border-right: 2px solid currentColor; - border-bottom: 2px solid currentColor; - transform: rotate(45deg); - animation: hero-scroll-bounce 1.6s ease-in-out infinite; -} - -.hero-scroll-indicator .scroll-text { - font-size: 0.7rem; - letter-spacing: 0.3em; -} - -@keyframes hero-scroll-bounce { - 0%, 100% { transform: translateY(0) rotate(45deg); opacity: 0.5; } - 50% { transform: translateY(6px) rotate(45deg); opacity: 1; } -} - -@media (min-width: 900px) { - .home-hero-initial .site-header { - transform: translateY(-100%); - } - - .home-hero-initial .site-header, - .home-hero-scrolled .site-header { - position: fixed; - left: 0; - right: 0; - width: 100%; - } - - .home-hero-initial .hero-scroll-indicator { - opacity: 1; - transform: translateX(-50%) translateY(0); - } - - .home-hero-scrolled .hero-scroll-indicator { - opacity: 0; - transform: translateX(-50%) translateY(10px); - } -} - -.hero-waves { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 140vh; - opacity: 0.5; - pointer-events: none; - z-index: 2; - mix-blend-mode: screen; - filter: saturate(1.08); - display: block; - -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.9) 55%, rgba(0, 0, 0, 0.35) 75%, rgba(0, 0, 0, 0) 100%); - mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.9) 55%, rgba(0, 0, 0, 0.35) 75%, rgba(0, 0, 0, 0) 100%); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: 100% 100%; - mask-size: 100% 100%; -} - -html:not([data-theme="dark"]) .hero-waves { - opacity: 0.55; - mix-blend-mode: multiply; - filter: saturate(1.2); -} - -[data-theme="dark"] .hero-waves { - opacity: 0.5; - mix-blend-mode: screen; - filter: saturate(1.08); -} - -/* 底部渐变遮罩 */ -.hero-section::after { - display: none; -} - -.hero-content { - max-width: 800px; - margin: 0 auto; text-align: center; - position: relative; - z-index: 1; - display: flex; - flex-direction: column; - align-items: center; -} - -/* 大标题 */ -.hero-title { - display: flex; - justify-content: center; - margin: 0 0 1.2rem; - overflow: visible; - --hero-title-gradient: linear-gradient(135deg, var(--text-primary) 0%, var(--color-primary) 100%); - width: 100%; -} - -.hero-title-svg { - display: block; - width: min(88vw, 900px); - margin: 0 auto; - aspect-ratio: 3 / 2; - background-image: var(--hero-title-gradient); - -webkit-mask: url("../../resources/title.svg") center / contain no-repeat; - mask: url("../../resources/title.svg") center / contain no-repeat; - filter: - drop-shadow(0 10px 24px rgba(12, 20, 32, 0.25)) - drop-shadow(0 0 18px rgba(120, 190, 255, 0.28)); - position: relative; - transform: translateY(-8px) scale(1.06); - transform-origin: center; -} - -.hero-title-svg::before { - content: ""; - position: absolute; - inset: -6% -4%; - background: - radial-gradient(circle at 30% 35%, rgba(255, 255, 255, 0.35), transparent 55%), - radial-gradient(circle at 70% 45%, rgba(140, 205, 255, 0.28), transparent 60%), - linear-gradient(180deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0)); - -webkit-mask: url("../../resources/title.svg") center / contain no-repeat; - mask: url("../../resources/title.svg") center / contain no-repeat; - opacity: 0.22; - filter: blur(6px); - mix-blend-mode: screen; - pointer-events: none; -} - -html:not([data-theme="dark"]) .hero-title-svg::before { - opacity: 0; -} - -.hero-title-svg::after { - content: ""; - position: absolute; - left: 0; - right: 0; - top: 100%; - height: 55%; - background-image: var(--hero-title-gradient); - -webkit-mask: url("../../resources/title.svg") center / contain no-repeat; - mask: url("../../resources/title.svg") center / contain no-repeat; - transform: scaleY(-1); - opacity: 0.18; - filter: blur(2px); -} - -.hero-title-text { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; + color: var(--hero-fg); + isolation: isolate; overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; + --hero-fg: #1c2430; + --hero-subtitle: rgba(28, 36, 48, 0.86); + --hero-title-shadow: rgba(24, 36, 58, 0.14); + --hero-btn-border: rgba(30, 42, 59, 0.52); + --hero-btn-bg: rgba(255, 255, 255, 0.42); + --hero-btn-hover-bg: rgba(255, 255, 255, 0.65); + --hero-overlay-highlight: rgba(255, 255, 255, 0.45); + --hero-overlay-top: rgba(248, 251, 255, 0.24); + --hero-overlay-bottom: rgba(206, 220, 243, 0.2); } -/* 描述文本 (代码风格) */ -.hero-description { - font-size: 1.5rem; /* 增大 Hero 描述 (原1.25rem) */ - color: var(--text-secondary); - margin-bottom: 3rem; - font-family: var(--font-mono); - min-height: 3.6em; /* 预留高度防止抖动 */ +[data-theme="dark"] .landing-hero { + --hero-fg: #ffffff; + --hero-subtitle: rgba(255, 255, 255, 0.92); + --hero-title-shadow: rgba(0, 0, 0, 0.34); + --hero-btn-border: rgba(255, 255, 255, 0.86); + --hero-btn-bg: rgba(255, 255, 255, 0.08); + --hero-btn-hover-bg: rgba(255, 255, 255, 0.23); + --hero-overlay-highlight: rgba(255, 255, 255, 0.16); + --hero-overlay-top: rgba(13, 18, 27, 0.14); + --hero-overlay-bottom: rgba(12, 18, 28, 0.52); } -/* --- 流式输出字符样式 --- */ -.stream-char { - transition: opacity 0.4s ease; /* 平滑淡入 */ +.landing-hero-canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + display: block; + z-index: 0; } -/* --- 服务展示区 (Services) --- */ -.services-provided { - margin-top: 0; +.landing-hero::before { + content: ""; + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; + background: + radial-gradient(circle at 72% 18%, var(--hero-overlay-highlight), rgba(255, 255, 255, 0) 36%), + linear-gradient(180deg, var(--hero-overlay-top), var(--hero-overlay-bottom)); +} + +.landing-hero .container { position: relative; z-index: 2; } +.landing-hero-content { + max-width: 880px; + margin: 0 auto; + padding: 56px 0; +} + +.landing-hero-title { + margin: 0 0 18px; + font-size: clamp(2.4rem, 7vw, 4.4rem); + line-height: 1.12; + letter-spacing: 0.02em; + font-weight: 700; + text-shadow: 0 4px 18px var(--hero-title-shadow); +} + +.landing-hero-subtitle { + margin: 0; + font-size: clamp(1.1rem, 2.6vw, 1.9rem); + color: var(--hero-subtitle); +} + +.landing-hero-btn { + display: inline-flex; + align-items: center; + justify-content: center; + margin-top: 30px; + padding: 0 30px; + min-height: 48px; + border: 1px solid var(--hero-btn-border); + border-radius: var(--radius-sm); + color: var(--hero-fg); + background-color: var(--hero-btn-bg); + backdrop-filter: blur(1px); + font-size: 1.05rem; + font-weight: 600; + transition: all var(--duration-fast); +} + +.landing-hero-btn:hover { + background-color: var(--hero-btn-hover-bg); + border-color: var(--hero-fg); +} + .services-section { - padding: 80px 0; - background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--bg-body) 55%); - position: relative; - z-index: 3; + padding: 24px 0 14px; } -/* 标题带有命令提示符风格 */ -.services-provided h2 { - font-size: 1.8rem; /* 增大服务标题 (原1.5rem) */ - margin-bottom: 1.5rem; - padding-bottom: 1rem; - border-bottom: none; +.services-provided h2, +.landing-feed-head h2 { + margin: 0 0 12px; + font-size: 1.15rem; color: var(--text-primary); - display: flex; /* 改为 flex 以支持全宽下划线 */ + display: flex; align-items: center; } -.services-provided h2::before { - content: "$"; - color: var(--color-primary); /* 统一使用蓝色,保持与 blog h2 风格一致 */ - margin-right: 10px; /* 保持与 blog h2 一致的间距 */ +.services-provided h2::before, +.landing-feed-head h2::before { + content: "#"; + margin-right: 8px; + color: var(--color-primary); font-family: var(--font-mono); - font-weight: 800; + font-weight: 700; } -/* 服务卡片网格 */ .services-grid-box { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 24px; - padding: 0 10px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 18px; } .service-item { - background: transparent; - border: none; - border-radius: var(--radius-md); - padding: 32px 24px; + min-height: 170px; display: flex; flex-direction: column; align-items: center; + justify-content: center; text-align: center; - transition: all var(--duration-fast); - text-decoration: none; -} - -.service-item:hover { - transform: translateY(-4px); - box-shadow: none; + padding: 20px 14px; + border: none; + border-radius: 0; + background-color: transparent; + color: var(--text-primary); + transition: none; } .service-icon { - margin-bottom: 1.5rem; + margin-bottom: 14px; color: var(--color-primary); - /* 确保 SVG 继承颜色 */ } .service-icon svg { - width: 90px; /* 增大图标 (原80px) */ - height: 90px; + width: 78px; + height: 78px; display: block; } .service-item span { + display: block; + font-size: 1.08rem; font-weight: 600; color: var(--text-primary); - font-size: 1.25rem; /* 增大服务项文字 (原1.1rem) */ } -/* --- 响应式适配 --- */ -@media (max-width: 1100px) { - .services-grid-box { - grid-template-columns: repeat(2, 1fr); /* 平板/小屏变 2 列 */ - } +.landing-updates { + padding: 16px 0 58px; } -@media (max-width: 768px) { - .hero-title { - margin-bottom: 1rem; - } - - .hero-title-svg { - width: min(92vw, 560px); - transform: translateY(-6px) scale(1.03); - } - - .hero-description { - font-size: 1rem; - } - - .services-section { - padding: 28px 0; - } - - .hero-section { - min-height: auto; - padding: 16px 0 16px; - } - - .hero-waves { - display: none; - } - - .hero-scroll-indicator { - display: none; - } +.landing-updates-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; } -@media (max-width: 600px) { +.landing-feed-box { + border: none; + border-radius: 0; + background-color: transparent; + overflow: visible; +} + +.landing-feed-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 0 0 12px; +} + +.landing-feed-head h2 { + margin: 0; +} + +.landing-feed-head a { + font-size: 0.94rem; + font-weight: 600; + color: var(--text-secondary); + white-space: nowrap; +} + +.landing-feed-head a::after { + content: " ->"; + font-family: var(--font-mono); +} + +.landing-feed-head a:hover { + color: var(--color-primary); +} + +.landing-feed { + margin: 0; + padding: 0; + list-style: none; +} + +.landing-feed-item { + padding: 14px 0 16px; + border-bottom: 1px solid var(--border-muted); +} + +.landing-feed-item:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.landing-feed-title { + margin: 0; + display: flex; + align-items: flex-start; + gap: 10px; +} + +.landing-feed-title::before { + content: ""; + width: 8px; + height: 8px; + margin-top: 0.54em; + border-radius: 2px; + background: var(--color-primary); + flex-shrink: 0; + opacity: 0.9; +} + +.landing-feed-link { + display: block; + min-width: 0; + font-size: clamp(1.1rem, 1.2vw + 0.82rem, 1.45rem); + line-height: 1.36; + font-weight: 600; + color: var(--text-primary); + overflow-wrap: anywhere; +} + +.landing-feed-link:hover { + color: var(--color-primary); +} + +.landing-feed-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin: 8px 0 0 18px; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.landing-feed-author { + font-weight: 500; +} + +.landing-feed-meta time { + font-family: var(--font-mono); + font-size: 0.84rem; + color: var(--text-secondary); +} + +.landing-feed-meta time::before { + content: "/"; + margin-right: 8px; + color: var(--border-default); +} + +.landing-feed-excerpt { + margin: 8px 0 0 18px; + font-size: 0.98rem; + line-height: 1.66; + color: var(--text-secondary); + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; +} + +.landing-feed-empty { + padding: 14px 0; + color: var(--text-secondary); +} + +@media (max-width: 1060px) { .services-grid-box { grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 16px; + } +} + +@media (max-width: 820px) { + .landing-updates-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 680px) { + .landing-hero { + min-height: 370px; + } + + .landing-hero-content { + padding: 44px 0; + } + + .landing-hero-btn { + min-height: 44px; + padding: 0 24px; } .service-item { + min-height: 132px; padding: 12px 8px; - border: none; - background: transparent; - box-shadow: none; - } - - .service-item:hover { - transform: none; - border-color: transparent; - box-shadow: none; } .service-icon { - margin-bottom: 0.75rem; + margin-bottom: 8px; } .service-icon svg { - width: 64px; - height: 64px; + width: 58px; + height: 58px; } .service-item span { - font-size: 1rem; + font-size: 0.95rem; + } + + .landing-feed-head { + padding-bottom: 10px; + } + + .landing-feed-link { + font-size: 1.08rem; + } + + .landing-feed-meta, + .landing-feed-excerpt { + margin-left: 14px; + } + + .landing-feed-excerpt { + -webkit-line-clamp: 2; } } diff --git a/assets/css/intro.css b/assets/css/intro.css index f1f09ae..5de7c5b 100644 --- a/assets/css/intro.css +++ b/assets/css/intro.css @@ -71,6 +71,36 @@ html:not([data-theme="dark"]) .intro-about .site-header { gap: 14px; } +.intro-brand-mark { + position: relative; + width: min(420px, 100%); + margin-bottom: 2px; +} + +.intro-brand-svg { + display: block; + width: min(76vw, 360px); + aspect-ratio: 3 / 2; + background-image: linear-gradient(135deg, var(--text-primary), var(--color-primary)); + -webkit-mask: url("../../resources/title.svg") center / contain no-repeat; + mask: url("../../resources/title.svg") center / contain no-repeat; + filter: + drop-shadow(0 10px 24px rgba(12, 20, 32, 0.24)) + drop-shadow(0 0 16px rgba(120, 190, 255, 0.24)); +} + +.intro-brand-text { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + .intro-title { font-size: clamp(2.6rem, 4vw, 4rem); font-weight: 800; @@ -357,6 +387,10 @@ body.has-intro-animations .intro-step.is-active .intro-animate { padding-top: 110px; } + .intro-brand-svg { + width: min(86vw, 320px); + } + .intro-title { font-size: 2.2rem; } diff --git a/assets/js/landing-hero-canvas.js b/assets/js/landing-hero-canvas.js new file mode 100644 index 0000000..676f726 --- /dev/null +++ b/assets/js/landing-hero-canvas.js @@ -0,0 +1,852 @@ +(() => { + const hero = document.querySelector('.landing-hero'); + const canvas = document.querySelector('.landing-hero-canvas'); + if (!hero || !canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + + let width = 0; + let height = 0; + let dpr = 1; + let rafId = null; + let startAt = performance.now(); + + const EMPTY_SET = new Set(); + const WORD_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; + const NUMBER_RE = /^\d+(?:_\d+)*(?:\.\d+)?$/; + const STRING_RE = /^("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)$/; + const CONSTANT_RE = /^[A-Z][A-Z0-9_]{2,}$/; + const TYPE_RE = /^[A-Z][A-Za-z0-9_]*$/; + const OPERATOR_RE = /^(::|->|=>|==|!=|<=|>=|\+=|-=|\*=|\/=|&&|\|\||[+\-*/%=&|^!<>?:.#])$/; + const TOKEN_RE = /(\/\/.*$|#\[.*$|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:_\d+)*(?:\.\d+)?\b|[A-Za-z_][A-Za-z0-9_]*|::|->|=>|==|!=|<=|>=|\+=|-=|\*=|\/=|&&|\|\||[+\-*/%=&|^!<>?:.,;()[\]{}#])/g; + + const LANGUAGE_KEYWORDS = { + springboot: new Set([ + 'package', 'import', 'public', 'private', 'protected', 'class', 'interface', + 'static', 'final', 'new', 'return', 'if', 'else', 'try', 'catch', 'finally', + 'throw', 'throws', 'void', 'long', 'int', 'boolean', 'null', 'true', 'false', + 'extends', 'implements', 'this', + ]), + rust: new Set([ + 'use', 'pub', 'struct', 'enum', 'impl', 'async', 'await', 'fn', 'let', 'mut', + 'loop', 'match', 'if', 'else', 'return', 'while', 'for', 'in', 'break', 'continue', + 'crate', 'self', 'super', 'mod', 'where', 'Result', 'Ok', 'Err', + ]), + cpp: new Set([ + 'include', 'template', 'typename', 'concept', 'requires', 'struct', 'class', + 'public', 'private', 'protected', 'explicit', 'void', 'auto', 'const', 'constexpr', + 'if', 'else', 'while', 'for', 'return', 'using', 'namespace', 'std', 'nullptr', + 'true', 'false', + ]), + python: new Set([ + 'import', 'from', 'as', 'class', 'def', 'return', 'if', 'elif', 'else', 'for', + 'while', 'in', 'and', 'or', 'not', 'await', 'async', 'with', 'try', 'except', + 'finally', 'lambda', 'None', 'True', 'False', 'yield', + ]), + javascript: new Set([ + 'class', 'constructor', 'return', 'if', 'else', 'while', 'for', 'const', 'let', + 'var', 'new', 'async', 'await', 'try', 'catch', 'finally', 'throw', 'export', + 'import', 'from', 'this', 'null', 'true', 'false', 'yield', 'function', + ]), + nim: new Set([ + 'import', 'type', 'enum', 'object', 'ref', 'proc', 'result', 'let', 'var', + 'if', 'elif', 'else', 'for', 'in', 'while', 'return', 'await', 'defer', + 'mod', 'echo', 'discard', + ]), + }; + + const classifyToken = (token, language, nextToken, prevToken, atLineStart) => { + const keywords = LANGUAGE_KEYWORDS[language] || EMPTY_SET; + + if (token.startsWith('//')) return 'comment'; + if (token.startsWith('#[') || token.startsWith('@')) return 'annotation'; + if (STRING_RE.test(token)) return 'string'; + if (NUMBER_RE.test(token)) return 'number'; + if (OPERATOR_RE.test(token)) return 'operator'; + + if (!WORD_RE.test(token)) { + return 'plain'; + } + + if (keywords.has(token)) return 'keyword'; + if (language === 'cpp' && atLineStart && prevToken === '#' && token === 'include') return 'keyword'; + if (CONSTANT_RE.test(token)) return 'constant'; + if (nextToken === '(') return 'method'; + if (TYPE_RE.test(token)) return 'type'; + return 'field'; + }; + + const tokenizeLine = (line, language) => { + if (line.length === 0) return [['', 'plain']]; + + const matches = [...line.matchAll(TOKEN_RE)]; + if (matches.length === 0) return [[line, 'plain']]; + + const tokens = []; + let cursor = 0; + + for (let i = 0; i < matches.length; i += 1) { + const match = matches[i]; + const start = match.index ?? 0; + const raw = match[0]; + const nextToken = i + 1 < matches.length ? matches[i + 1][0] : ''; + const prevToken = i > 0 ? matches[i - 1][0] : ''; + const atLineStart = line.slice(0, start).trim().length === 0; + + if (start > cursor) { + tokens.push([line.slice(cursor, start), 'plain']); + } + + if ((language === 'python' || language === 'nim') && raw === '#') { + tokens.push([line.slice(start), 'comment']); + cursor = line.length; + break; + } + + tokens.push([raw, classifyToken(raw, language, nextToken, prevToken, atLineStart)]); + cursor = start + raw.length; + } + + if (cursor < line.length) { + tokens.push([line.slice(cursor), 'plain']); + } + + if (tokens.length === 0) { + tokens.push(['', 'plain']); + } + return tokens; + }; + + const tokenizeSnippet = (lines, language) => lines.map((line) => tokenizeLine(line, language)); + + const rawSnippetCatalog = [ + { + name: 'springboot', + repeat: 4, + lines: [ + 'package cn.edu.ouc.itstudio.payment.workflow;', + '', + 'import org.springframework.stereotype.Service;', + 'import org.springframework.transaction.annotation.Transactional;', + 'import org.springframework.transaction.annotation.Propagation;', + 'import org.springframework.transaction.annotation.Isolation;', + 'import org.springframework.retry.support.RetryTemplate;', + 'import java.math.BigDecimal;', + 'import java.time.Duration;', + 'import java.util.Optional;', + 'import java.util.concurrent.CompletableFuture;', + 'import java.util.concurrent.ConcurrentHashMap;', + 'import java.util.concurrent.ConcurrentMap;', + 'import java.util.concurrent.atomic.AtomicLong;', + '', + '@Service', + '@RequiredArgsConstructor', + 'public class PaymentWorkflowOrchestrator {', + ' private static final String IDEMPOTENCY_PREFIX = "pay:idem:";', + ' private static final BigDecimal RISK_THRESHOLD = new BigDecimal("9999.99");', + ' private static final Duration LOCK_TTL = Duration.ofSeconds(45);', + '', + ' private final PaymentRepository paymentRepository;', + ' private final RiskEngine riskEngine;', + ' private final RetryTemplate retryTemplate;', + ' private final DistributedLockManager lockManager;', + ' private final IdempotencyGateway idempotencyGateway;', + ' private final OutboxPublisher outboxPublisher;', + ' private final CompensationService compensator;', + ' private final GatewayClient gateway;', + ' private final Clock clock;', + ' private final TraceContext traceContext;', + ' private final ConcurrentMap inMemorySeq = new ConcurrentHashMap<>();', + '', + ' @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)', + ' public PaymentResult orchestrate(PaymentCommand command, String idemToken) {', + ' String idemKey = IDEMPOTENCY_PREFIX + Digest.sha256Hex(idemToken + ":" + command.getOrderNo());', + ' Optional cached = idempotencyGateway.read(idemKey, PaymentResult.class);', + ' if (cached.isPresent()) { return cached.get(); }', + ' DistributedLock lock = lockManager.tryLock("lock:order:" + command.getOrderNo(), LOCK_TTL);', + ' if (lock == null) { throw new BusyException("order is processing by another worker"); }', + ' try {', + ' validateCommand(command);', + ' long seq = inMemorySeq.computeIfAbsent(command.getTenantId(), k -> new AtomicLong(0)).incrementAndGet();', + ' RiskSnapshot risk = riskEngine.evaluate(command, seq);', + ' if (risk.getScore().compareTo(RISK_THRESHOLD) > 0 && !command.isForceSubmit()) {', + ' throw new RiskBlockedException("risk score exceeded threshold");', + ' }', + ' PaymentAggregate aggregate = aggregateFactory.rehydrateOrCreate(command.getOrderNo(), command.getTenantId());', + ' aggregate.apply(command.toEvent(clock.instant(), traceContext.currentTraceId()));', + ' paymentRepository.save(aggregate);', + ' PaymentResult result = retryTemplate.execute(ctx -> gateway.charge(aggregate.buildChargeRequest(ctx.getRetryCount())));', + ' outboxPublisher.publish(new PaymentSucceededEvent(aggregate.getOrderNo(), result.getTransactionId(), clock.instant(), traceContext.currentTraceId()));', + ' idempotencyGateway.write(idemKey, result, Duration.ofHours(6));', + ' return result;', + ' } catch (Exception ex) {', + ' compensator.compensate(command.getOrderNo(), ex.getMessage(), clock.instant());', + ' throw ex;', + ' } finally {', + ' lock.unlock();', + ' }', + ' }', + '}', + ], + }, + { + name: 'rust', + repeat: 5, + lines: [ + 'use std::collections::{BTreeMap, HashMap};', + 'use std::sync::Arc;', + 'use tokio::sync::{Mutex, RwLock};', + 'use tokio::time::{sleep, Duration, Instant};', + 'use serde::{Deserialize, Serialize};', + '', + '#[derive(Debug, Clone, Serialize, Deserialize)]', + 'struct Command { tenant: String, key: String, amount: i64, deadline_ms: u64 }', + '', + '#[derive(Debug, Clone, Serialize, Deserialize)]', + 'enum Event { Reserved { id: String, cents: i64 }, Committed { id: String }, Failed { id: String, reason: String } }', + '', + '#[derive(Default)]', + 'struct Snapshot {', + ' ledger: HashMap,', + ' inbox: BTreeMap,', + ' watermark: u64,', + '}', + '', + 'struct Orchestrator {', + ' state: Arc>,', + ' idempotency: Arc>>,', + '}', + '', + 'impl Orchestrator {', + ' async fn apply(&self, cmd: Command) -> Result {', + ' let idem = format!("{}:{}:{}", cmd.tenant, cmd.key, cmd.amount);', + ' if let Some(hit) = self.idempotency.lock().await.get(&idem).cloned() {', + ' return Ok(hit);', + ' }', + ' let start = Instant::now();', + ' let mut retries = 0u8;', + ' loop {', + ' retries += 1;', + ' match self.try_once(&cmd).await {', + ' Ok(evt) => {', + ' self.idempotency.lock().await.insert(idem.clone(), evt.clone());', + ' return Ok(evt);', + ' }', + ' Err(e) if retries < 5 && start.elapsed() < Duration::from_millis(cmd.deadline_ms) => {', + ' sleep(Duration::from_millis((retries as u64) * 17)).await;', + ' continue;', + ' }', + ' Err(e) => return Err(format!("orchestration failed after retries: {}", e)),', + ' }', + ' }', + ' }', + '', + ' async fn try_once(&self, cmd: &Command) -> Result {', + ' let mut guard = self.state.write().await;', + ' let balance = guard.ledger.get(&cmd.key).copied().unwrap_or_default();', + ' if balance < cmd.amount {', + ' let evt = Event::Failed { id: cmd.key.clone(), reason: "insufficient_funds".to_string() };', + ' guard.watermark += 1;', + ' guard.inbox.insert(guard.watermark, evt.clone());', + ' return Ok(evt);', + ' }', + ' guard.ledger.insert(cmd.key.clone(), balance - cmd.amount);', + ' guard.watermark += 1;', + ' let evt = Event::Committed { id: cmd.key.clone() };', + ' guard.inbox.insert(guard.watermark, evt.clone());', + ' Ok(evt)', + ' }', + '}', + ], + }, + { + name: 'cpp', + repeat: 5, + lines: [ + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '#include ', + '', + 'template ', + 'concept Hashable = requires(T v) { std::hash{}(v); };', + '', + 'struct Job {', + ' std::string id;', + ' std::vector deps;', + ' std::function()> run;', + '};', + '', + 'class DagExecutor {', + 'public:', + ' explicit DagExecutor(std::size_t maxConcurrency) : maxConcurrency_(maxConcurrency) {}', + '', + ' void add(Job job) {', + ' std::unique_lock lk(mu_);', + ' graph_[job.id] = std::move(job);', + ' }', + '', + ' std::map> execute() {', + ' auto order = topoSort();', + ' std::map> out;', + ' std::vector> inflight;', + ' std::atomic_size_t cursor{0};', + '', + ' auto worker = [&]() {', + ' while (true) {', + ' auto idx = cursor.fetch_add(1);', + ' if (idx >= order.size()) { break; }', + ' const auto& id = order[idx];', + ' auto result = graph_.at(id).run();', + ' {', + ' std::unique_lock lk(outMu_);', + ' out[id] = std::move(result);', + ' }', + ' }', + ' };', + '', + ' for (std::size_t i = 0; i < maxConcurrency_; ++i) {', + ' inflight.emplace_back(std::async(std::launch::async, worker));', + ' }', + ' for (auto& f : inflight) { f.get(); }', + ' return out;', + ' }', + '', + 'private:', + ' std::vector topoSort();', + ' std::size_t maxConcurrency_;', + ' std::unordered_map graph_;', + ' std::shared_mutex mu_;', + ' std::mutex outMu_;', + '};', + ], + }, + { + name: 'python', + repeat: 5, + lines: [ + 'import asyncio', + 'import contextvars', + 'import dataclasses', + 'import hashlib', + 'import random', + 'from collections import defaultdict', + 'from typing import Any, Callable, Dict, Iterable, List', + '', + 'trace_id_var = contextvars.ContextVar("trace_id", default="n/a")', + '', + '@dataclasses.dataclass', + 'class TaskSpec:', + ' key: str', + ' deps: List[str]', + ' timeout: float', + ' fn: Callable[[Dict[str, Any]], "asyncio.Future[Any]"]', + '', + '@dataclasses.dataclass', + 'class EngineState:', + ' done: Dict[str, Any]', + ' failed: Dict[str, str]', + ' latency_ms: Dict[str, float]', + '', + 'class AsyncDagEngine:', + ' def __init__(self, specs: Iterable[TaskSpec], concurrency: int = 8) -> None:', + ' self.specs = {s.key: s for s in specs}', + ' self.children = defaultdict(list)', + ' self.indegree = defaultdict(int)', + ' for spec in self.specs.values():', + ' self.indegree[spec.key] = len(spec.deps)', + ' for dep in spec.deps:', + ' self.children[dep].append(spec.key)', + ' self.sem = asyncio.Semaphore(concurrency)', + '', + ' async def run(self, seed: Dict[str, Any]) -> EngineState:', + ' state = EngineState(done=dict(seed), failed={}, latency_ms={})', + ' ready = asyncio.Queue()', + ' for k, v in self.indegree.items():', + ' if v == 0:', + ' ready.put_nowait(k)', + '', + ' async def worker() -> None:', + ' while True:', + ' key = await ready.get()', + ' if key is None:', + ' return', + ' spec = self.specs[key]', + ' token = trace_id_var.set(hashlib.sha1(key.encode()).hexdigest()[:12])', + ' started = asyncio.get_running_loop().time()', + ' try:', + ' async with self.sem:', + ' result = await asyncio.wait_for(spec.fn(state.done), timeout=spec.timeout)', + ' state.done[key] = result', + ' except Exception as exc:', + ' state.failed[key] = f"{type(exc).__name__}:{exc}"', + ' finally:', + ' elapsed = (asyncio.get_running_loop().time() - started) * 1000', + ' state.latency_ms[key] = elapsed', + ' trace_id_var.reset(token)', + '', + ' for child in self.children[key]:', + ' self.indegree[child] -= 1', + ' if self.indegree[child] == 0 and child not in state.failed:', + ' ready.put_nowait(child)', + '', + ' workers = [asyncio.create_task(worker()) for _ in range(6)]', + ' await ready.join()', + ' for _ in workers: ready.put_nowait(None)', + ' await asyncio.gather(*workers)', + ' return state', + ], + }, + { + name: 'javascript', + repeat: 5, + lines: [ + 'class CircuitBreaker {', + ' constructor({ threshold = 0.35, minRequests = 20, coolDownMs = 2500 } = {}) {', + ' this.threshold = threshold;', + ' this.minRequests = minRequests;', + ' this.coolDownMs = coolDownMs;', + ' this.state = "closed";', + ' this.window = [];', + ' this.openUntil = 0;', + ' }', + '', + ' record(ok) {', + ' this.window.push(ok ? 1 : 0);', + ' if (this.window.length > 120) this.window.shift();', + ' const failRatio = 1 - this.window.reduce((a, b) => a + b, 0) / this.window.length;', + ' if (this.window.length >= this.minRequests && failRatio >= this.threshold) {', + ' this.state = "open";', + ' this.openUntil = Date.now() + this.coolDownMs;', + ' }', + ' }', + '', + ' canPass() {', + ' if (this.state === "closed") return true;', + ' if (Date.now() >= this.openUntil) {', + ' this.state = "half-open";', + ' return true;', + ' }', + ' return false;', + ' }', + '}', + '', + 'export class StreamOrchestrator {', + ' constructor(fetchers, { concurrency = 6 } = {}) {', + ' this.fetchers = fetchers;', + ' this.concurrency = concurrency;', + ' this.cache = new Map();', + ' this.breakers = new Map();', + ' }', + '', + ' async *collect(keys, signal) {', + ' const queue = [...keys];', + ' const inflight = new Set();', + ' const next = async () => {', + ' if (!queue.length) return;', + ' const key = queue.shift();', + ' const fn = this.fetchers.get(key);', + ' if (!fn) return;', + ' const breaker = this.breakers.get(key) ?? new CircuitBreaker();', + ' this.breakers.set(key, breaker);', + ' if (!breaker.canPass()) return;', + '', + ' const p = fn({ signal })', + ' .then((value) => { breaker.record(true); return { key, value, ok: true }; })', + ' .catch((err) => { breaker.record(false); return { key, error: err, ok: false }; })', + ' .finally(() => inflight.delete(p));', + ' inflight.add(p);', + ' };', + '', + ' while (queue.length || inflight.size) {', + ' while (inflight.size < this.concurrency && queue.length) await next();', + ' if (!inflight.size) continue;', + ' const settled = await Promise.race([...inflight]);', + ' if (settled.ok) this.cache.set(settled.key, settled.value);', + ' yield settled;', + ' }', + ' }', + '}', + ], + }, + { + name: 'nim', + repeat: 5, + lines: [ + 'import asyncdispatch, options, tables, sequtils, algorithm, strformat, times, hashes, locks', + '', + 'type', + ' EventKind = enum', + ' ekWrite, ekCompact, ekSnapshot, ekReplicate', + '', + ' Event = object', + ' tenantId: string', + ' streamId: string', + ' offset: int64', + ' payload: string', + ' kind: EventKind', + ' ts: DateTime', + '', + ' ShardState = ref object', + ' watermark: int64', + ' pending: Table[int64, Future[void]]', + ' snapshotEvery: int', + ' lock: Lock', + '', + 'proc routeShard(tenantId, streamId: string; shardCount: int): int =', + ' result = abs(hash(tenantId & ":" & streamId)) mod shardCount', + '', + 'proc applyEvent(state: ShardState; ev: Event): Future[void] {.async.} =', + ' acquire(state.lock)', + ' defer: release(state.lock)', + ' if ev.offset <= state.watermark:', + ' return', + '', + ' let delayMs = (ev.payload.len mod 13) + 3', + ' await sleepAsync(delayMs)', + ' state.watermark = ev.offset', + '', + ' if state.watermark mod int64(state.snapshotEvery) == 0:', + ' let sid = state.watermark', + ' state.pending[sid] = asyncCheck proc() {.async.} =', + ' await sleepAsync(20)', + ' echo &"[snapshot] stream={ev.streamId} offset={sid}"', + '', + 'proc mergeOrdered(buffers: seq[seq[Event]]): seq[Event] =', + ' result = @[]', + ' for b in buffers: result.add(b)', + ' result.sort(proc(a, b: Event): int = cmp(a.offset, b.offset))', + '', + 'proc replay(shards: seq[ShardState]; buffers: seq[seq[Event]]) {.async.} =', + ' for ev in mergeOrdered(buffers):', + ' let idx = routeShard(ev.tenantId, ev.streamId, shards.len)', + ' await shards[idx].applyEvent(ev)', + ], + }, + ]; + + const snippetCatalog = rawSnippetCatalog.map((entry) => ({ + ...entry, + lines: tokenizeSnippet(entry.lines, entry.name), + })); + + const selectedSnippet = snippetCatalog[Math.floor(Math.random() * snippetCatalog.length)]; + const snippetLines = selectedSnippet.lines; + const repeatCount = selectedSnippet.repeat ?? 6; + + const codeLines = []; + for (let i = 0; i < repeatCount; i += 1) { + snippetLines.forEach((line) => codeLines.push(line)); + codeLines.push([['', 'plain']]); + } + + const themes = { + light: { + background: '#f7f9fd', + editor: '#fbfcff', + gutter: '#f1f4fa', + divider: '#d5dde9', + guide: 'rgba(115, 138, 170, 0.14)', + currentLine: 'rgba(46, 118, 245, 0.13)', + lineNo: '#8a93a4', + lineNoActive: '#4f5e77', + caret: '#2e76f5', + markerOk: '#238551', + markerWarn: '#bb7a15', + fadeTop: 'rgba(0, 0, 0, 0)', + fadeBottom: 'rgba(0, 0, 0, 0)', + code: { + plain: '#3e4553', + keyword: '#0f59c5', + type: '#136a4d', + method: '#7c3ec1', + annotation: '#9a5f0a', + comment: '#758191', + string: '#1c7f3f', + number: '#8f437d', + field: '#4c607f', + operator: '#4a607e', + constant: '#3f67b0', + }, + }, + dark: { + background: '#1d1f23', + editor: '#1f2126', + gutter: '#1a1b1f', + divider: '#333842', + guide: 'rgba(103, 122, 157, 0.2)', + currentLine: 'rgba(76, 149, 255, 0.17)', + lineNo: '#5f6674', + lineNoActive: '#8da1bf', + caret: '#4c95ff', + markerOk: '#53d779', + markerWarn: '#e8b04a', + fadeTop: 'rgba(24, 26, 32, 0.5)', + fadeBottom: 'rgba(20, 22, 28, 0.38)', + code: { + plain: '#ccd3dd', + keyword: '#cc7832', + type: '#4ea1f2', + method: '#d8c66c', + annotation: '#bbb529', + comment: '#6a7484', + string: '#6aab73', + number: '#8a6db6', + field: '#c4cbd6', + operator: '#909ba9', + constant: '#9876aa', + }, + }, + }; + + const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); + const getTheme = () => (document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'); + const tokenCount = (tokens) => tokens.reduce((sum, token) => sum + token[0].length, 0); + + const resizeCanvas = () => { + const rect = hero.getBoundingClientRect(); + width = Math.max(1, Math.round(rect.width)); + height = Math.max(1, Math.round(rect.height)); + dpr = window.devicePixelRatio || 1; + canvas.width = Math.floor(width * dpr); + canvas.height = Math.floor(height * dpr); + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + }; + + const drawTokenLine = (tokens, x, y, charBudget, palette) => { + let cursorX = x; + const limit = Number.isFinite(charBudget); + let remaining = limit ? charBudget : 0; + + for (const token of tokens) { + if (limit && remaining <= 0) break; + + const text = token[0]; + if (!text) continue; + + const visibleText = limit ? text.slice(0, remaining) : text; + ctx.fillStyle = palette.code[token[1]] || palette.code.plain; + ctx.fillText(visibleText, cursorX, y); + cursorX += ctx.measureText(visibleText).width; + + if (limit) { + remaining -= visibleText.length; + } + } + + return cursorX - x; + }; + + const drawCodeEditor = (palette, elapsed, isStatic) => { + const gutterWidth = clamp(Math.round(width * 0.08), 56, 92); + const fontSize = clamp(Math.round(width / 98), 14, 20); + const lineHeight = Math.round(fontSize * 1.72); + const topPadding = Math.round(lineHeight * 1.24); + const codeStartX = gutterWidth + 18; + const visibleLines = Math.max(1, Math.ceil((height - topPadding * 2) / lineHeight) + 2); + const baseLineNo = 1; + const scrollLeadLines = 2; + + const lineCharTotals = codeLines.map((line) => tokenCount(line)); + const lineDurations = lineCharTotals.map((chars) => Math.max(150, chars * 8)); + const totalTypingDuration = lineDurations.reduce((sum, duration) => sum + duration, 0); + const holdDuration = 500; + const cycleDuration = totalTypingDuration + holdDuration; + + ctx.fillStyle = palette.background; + ctx.fillRect(0, 0, width, height); + ctx.fillStyle = palette.editor; + ctx.fillRect(gutterWidth, 0, width - gutterWidth, height); + ctx.fillStyle = palette.gutter; + ctx.fillRect(0, 0, gutterWidth, height); + ctx.fillStyle = palette.divider; + ctx.fillRect(gutterWidth, 0, 1, height); + + ctx.strokeStyle = palette.guide; + ctx.lineWidth = 1; + for (let x = codeStartX + 110; x < width; x += 120) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, height); + ctx.stroke(); + } + + const cyclePos = isStatic ? totalTypingDuration + holdDuration : elapsed % cycleDuration; + const writingPhase = cyclePos < totalTypingDuration; + + let completedLines = codeLines.length; + let activeLine = codeLines.length - 1; + let activeLineProgress = 1; + + if (writingPhase && !isStatic) { + let remaining = cyclePos; + completedLines = 0; + activeLine = 0; + activeLineProgress = 0; + + for (let i = 0; i < lineDurations.length; i += 1) { + const duration = lineDurations[i]; + if (remaining >= duration) { + remaining -= duration; + completedLines += 1; + continue; + } + activeLine = i; + activeLineProgress = duration > 0 ? remaining / duration : 1; + break; + } + } + + const startLine = isStatic + ? Math.max(0, codeLines.length - visibleLines) + : Math.max(0, completedLines - Math.max(1, visibleLines - 1 - scrollLeadLines)); + + ctx.font = `500 ${fontSize}px "JetBrains Mono", "Fira Code", "SFMono-Regular", Consolas, monospace`; + ctx.textBaseline = 'alphabetic'; + + let caret = null; + for (let i = 0; i < visibleLines; i += 1) { + const lineIndex = startLine + i; + const tokens = codeLines[lineIndex]; + if (!tokens) break; + + const y = topPadding + i * lineHeight; + if (y < -lineHeight || y > height + lineHeight) continue; + + const charsInLine = lineCharTotals[lineIndex]; + const lineCompleted = isStatic || lineIndex < completedLines || !writingPhase; + const isActive = writingPhase && lineIndex === activeLine; + + let charBudget = 0; + if (lineCompleted) { + charBudget = Infinity; + } else if (isActive) { + charBudget = charsInLine > 0 ? Math.floor(charsInLine * clamp(activeLineProgress, 0, 1)) : 0; + if (charsInLine > 0) { + charBudget = Math.max(1, charBudget); + } + } + + const hasStarted = lineCompleted || isActive; + + if (isActive) { + ctx.fillStyle = palette.currentLine; + ctx.fillRect(gutterWidth + 1, y - lineHeight * 0.76, width - gutterWidth - 1, lineHeight); + } + + if (hasStarted) { + const lineNumber = String(baseLineNo + lineIndex); + ctx.fillStyle = isActive ? palette.lineNoActive : palette.lineNo; + ctx.fillText(lineNumber, gutterWidth - 10 - ctx.measureText(lineNumber).width, y); + } + + let drawnWidth = 0; + if (hasStarted && (charBudget > 0 || charBudget === Infinity)) { + drawnWidth = drawTokenLine(tokens, codeStartX, y, charBudget, palette); + } + + if (isActive && writingPhase && Math.floor(elapsed / 430) % 2 === 0) { + caret = { x: codeStartX + drawnWidth + 1, y }; + } + + if (!hasStarted) continue; + + if ((lineIndex + 7) % 19 === 0) { + ctx.beginPath(); + ctx.fillStyle = palette.markerOk; + ctx.arc(12, y - lineHeight * 0.38, 3.2, 0, Math.PI * 2); + ctx.fill(); + } else if ((lineIndex + 3) % 23 === 0) { + ctx.beginPath(); + ctx.fillStyle = palette.markerWarn; + ctx.arc(12, y - lineHeight * 0.38, 3.2, 0, Math.PI * 2); + ctx.fill(); + } + } + + if (caret) { + ctx.fillStyle = palette.caret; + ctx.fillRect(caret.x, caret.y - fontSize, 1.8, Math.max(12, fontSize + 1)); + } + }; + + const render = (timestamp) => { + const elapsed = timestamp - startAt; + const palette = themes[getTheme()]; + ctx.clearRect(0, 0, width, height); + drawCodeEditor(palette, elapsed, false); + rafId = requestAnimationFrame(render); + }; + + const drawStatic = () => { + const palette = themes[getTheme()]; + ctx.clearRect(0, 0, width, height); + drawCodeEditor(palette, 0, true); + }; + + const stop = () => { + if (!rafId) return; + cancelAnimationFrame(rafId); + rafId = null; + }; + + const start = () => { + stop(); + resizeCanvas(); + startAt = performance.now(); + if (reducedMotionQuery.matches) { + drawStatic(); + return; + } + rafId = requestAnimationFrame(render); + }; + + const onResize = () => { + resizeCanvas(); + if (reducedMotionQuery.matches) { + drawStatic(); + } + }; + + const themeObserver = new MutationObserver(() => { + if (reducedMotionQuery.matches) { + drawStatic(); + } + }); + + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'], + }); + + if (typeof ResizeObserver !== 'undefined') { + const resizeObserver = new ResizeObserver(onResize); + resizeObserver.observe(hero); + } + + if (reducedMotionQuery.addEventListener) { + reducedMotionQuery.addEventListener('change', start); + } else if (reducedMotionQuery.addListener) { + reducedMotionQuery.addListener(start); + } + + window.addEventListener('resize', onResize); + window.addEventListener('load', onResize); + + start(); +})(); diff --git a/functions.php b/functions.php index f7584b4..2947731 100644 --- a/functions.php +++ b/functions.php @@ -28,11 +28,10 @@ function itstudio_enqueue_scripts() { wp_enqueue_style('itstudio-footer', get_template_directory_uri() . '/assets/css/footer.css', array('itstudio-style'), '2.1.2'); wp_enqueue_style('itstudio-content', get_template_directory_uri() . '/assets/css/content.css', array('itstudio-style'), '2.1.2'); - // 仅在首页加载 Hero 样式 + // 仅在首页加载首页样式 if (is_front_page() || is_home()) { wp_enqueue_style('itstudio-front-page', get_template_directory_uri() . '/assets/css/front-page.css', array('itstudio-style'), '2.1.2'); - wp_enqueue_script('itstudio-hero-waves', get_template_directory_uri() . '/assets/js/hero-waves.js', array(), '1.0.0', true); - wp_enqueue_script('itstudio-home-hero', get_template_directory_uri() . '/assets/js/home-hero.js', array(), '1.0.0', true); + wp_enqueue_script('itstudio-landing-hero-canvas', get_template_directory_uri() . '/assets/js/landing-hero-canvas.js', array(), '1.0.0', true); } // 仅在工作室介绍页加载(包含 /about fallback) @@ -44,17 +43,14 @@ function itstudio_enqueue_scripts() { } if ($is_about) { - wp_enqueue_style('itstudio-intro', get_template_directory_uri() . '/assets/css/intro.css', array('itstudio-content'), '2.1.2'); - wp_enqueue_script('itstudio-intro-scroll', get_template_directory_uri() . '/assets/js/intro-scroll.js', array(), '1.0.0', true); + wp_enqueue_style('itstudio-about-hero', get_template_directory_uri() . '/assets/css/about-hero.css', array('itstudio-content'), '2.1.2'); + wp_enqueue_script('itstudio-about-hero-waves', get_template_directory_uri() . '/assets/js/hero-waves.js', array(), '1.0.0', true); + wp_enqueue_script('itstudio-about-hero', get_template_directory_uri() . '/assets/js/home-hero.js', array(), '1.0.0', true); } // Scripts wp_enqueue_script('itstudio-theme-toggle', get_template_directory_uri() . '/assets/js/theme-toggle.js', array(), '1.0.0', true); wp_enqueue_script('itstudio-lang-toggle', get_template_directory_uri() . '/assets/js/lang-toggle.js', array(), '1.0.0', true); - // 注册并加载打字机效果脚本 - 仅在工作室介绍页 - if ($is_about) { - wp_enqueue_script('itstudio-stream', get_template_directory_uri() . '/assets/js/stream.js', array(), '1.0.0', true); - } wp_enqueue_script('itstudio-main', get_template_directory_uri() . '/assets/js/main.js', array(), '1.0.0', true); } add_action('wp_enqueue_scripts', 'itstudio_enqueue_scripts'); diff --git a/header.php b/header.php index bcf903a..0f8c309 100644 --- a/header.php +++ b/header.php @@ -74,11 +74,22 @@ + + -
-

-
+
+
+

+ +
+
    'post', + 'post_status' => 'publish', 'posts_per_page' => 5, 'orderby' => 'date', - 'order' => 'DESC' + 'order' => 'DESC', + 'ignore_sticky_posts' => true, + 'no_found_rows' => true )); - if ($blogs->have_posts()) : while ($blogs->have_posts()) : $blogs->the_post(); + $blog_excerpt = get_the_excerpt(); + if ('' === trim($blog_excerpt)) { + $blog_excerpt = wp_trim_words(wp_strip_all_tags(get_the_content()), 40, '...'); + } ?> -
    -
    -

    -
    - -
    - +
  • +

    + +

    +
    + +
    -
  • +

    + -

    +
  • -
-
+ + diff --git a/page-about.php b/page-about.php index 8e780de..73d13d4 100644 --- a/page-about.php +++ b/page-about.php @@ -3,173 +3,221 @@ get_header(); ?> -
-
-
-
-

-

-

-
- - -
-
-
- 2002 - -
-
- 6 - -
-
- 20+ - -
-
+
+ +
+
+
+

+ + +

-
-
-
-
+
+ +
+ +
+
-
-
-
-

-

-
    -
  • -
  • -
  • -
-
-
-
-
-
-
-
-
+
+
+
+
+

+
+ 'announcement', + 'post_status' => 'publish', + 'posts_per_page' => 5, + 'orderby' => 'date', + 'order' => 'DESC', + 'ignore_sticky_posts' => true, + 'no_found_rows' => true + )); -
-
-
-

-

-
-
-

-

-
-
-

-

-
-
-

-

-
-
-

-

-
-
-

-

-
-
-

-

+ if ($announcements->have_posts()) : + while ($announcements->have_posts()) : $announcements->the_post(); + ?> +
+
+

+
+ +
+ +
+
+ +

+
-
-
-
-
-
-
-
-
-
-
-
-

-

-
-
-

-

-
-
-

-

-
-
-

-

+
+

+
+ 'post', + 'post_status' => 'publish', + 'posts_per_page' => 5, + 'orderby' => 'date', + 'order' => 'DESC', + 'ignore_sticky_posts' => true, + 'no_found_rows' => true + )); + + if ($blogs->have_posts()) : + while ($blogs->have_posts()) : $blogs->the_post(); + ?> +
+
+

+
+ +
+ +
+
+ +

+
-
-
-
-
-
-
-
- -
-
-
-

-

-
-
-

-
    -
  • -
  • -
  • -
  • -
-
-
-

-
    -
  • -
  • -
  • -
  • -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-

-

- -
-
-
-
-
-