780 lines
26 KiB
HTML
780 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Hero 动画 — 精选方案</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body { background: #0a0a0a; color: #fff; overflow-x: hidden; }
|
|
|
|
.nav {
|
|
position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
|
|
display: flex; gap: 6px; padding: 14px 20px;
|
|
background: rgba(0,0,0,0.85); backdrop-filter: blur(20px);
|
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
}
|
|
.nav button {
|
|
padding: 8px 18px; border: 1px solid rgba(255,255,255,0.12); border-radius: 6px;
|
|
background: transparent; color: rgba(255,255,255,0.6); cursor: pointer;
|
|
font-size: 13px; font-weight: 500; transition: all 0.3s; letter-spacing: 0.02em;
|
|
}
|
|
.nav button:hover { background: rgba(255,255,255,0.06); color: #fff; }
|
|
.nav button.active { background: #C41E3A; border-color: #C41E3A; color: #fff; }
|
|
|
|
.scene { display: none; position: relative; width: 100vw; height: 100vh; overflow: hidden; }
|
|
.scene.active { display: block; }
|
|
|
|
.replay-btn {
|
|
position: fixed; bottom: 20px; right: 20px; z-index: 1000;
|
|
padding: 10px 18px; background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.5);
|
|
border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; cursor: pointer;
|
|
font-size: 12px; backdrop-filter: blur(12px); transition: all 0.3s;
|
|
}
|
|
.replay-btn:hover { background: rgba(255,255,255,0.15); color: #fff; }
|
|
|
|
.hint {
|
|
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 1000;
|
|
padding: 8px 20px; background: rgba(0,0,0,0.6); backdrop-filter: blur(12px);
|
|
border-radius: 999px; font-size: 12px; color: rgba(255,255,255,0.4);
|
|
}
|
|
|
|
/* ============================================ */
|
|
/* Scene 1: 墨韵流光 — Cinematic Ink Flow */
|
|
/* ============================================ */
|
|
#s1 { background: #080808; }
|
|
#s1 canvas { position: absolute; inset: 0; }
|
|
|
|
#s1 .content {
|
|
position: absolute; inset: 0; z-index: 10;
|
|
display: flex; flex-direction: column; justify-content: center;
|
|
padding: 0 8vw;
|
|
}
|
|
#s1 .tag {
|
|
display: inline-flex; align-items: center; gap: 8px;
|
|
padding: 6px 16px; border-radius: 999px;
|
|
font-size: 13px; font-weight: 500; letter-spacing: 0.08em;
|
|
background: rgba(196,30,58,0.12); color: #e85d6f;
|
|
border: 1px solid rgba(196,30,58,0.2);
|
|
opacity: 0; transform: translateY(16px);
|
|
}
|
|
#s1 h1 {
|
|
font-family: 'Noto Serif SC', serif; font-size: clamp(48px, 7vw, 88px);
|
|
font-weight: 300; letter-spacing: 0.12em; line-height: 1.15;
|
|
color: #F5F0E8; margin: 20px 0 16px;
|
|
opacity: 0; transform: translateY(20px);
|
|
}
|
|
#s1 .accent { color: #C41E3A; font-weight: 500; }
|
|
#s1 .sub {
|
|
font-size: clamp(16px, 2vw, 22px); color: rgba(245,240,232,0.45);
|
|
font-weight: 300; letter-spacing: 0.04em; line-height: 1.6;
|
|
max-width: 520px; margin-bottom: 36px;
|
|
opacity: 0; transform: translateY(16px);
|
|
}
|
|
#s1 .btns { display: flex; gap: 14px; opacity: 0; transform: translateY(16px); }
|
|
#s1 .btn-p {
|
|
padding: 13px 28px; background: #C41E3A; color: #fff; border: none;
|
|
border-radius: 6px; font-size: 15px; font-weight: 500; cursor: pointer;
|
|
letter-spacing: 0.04em; transition: all 0.3s;
|
|
}
|
|
#s1 .btn-p:hover { background: #d4213f; transform: translateY(-1px); box-shadow: 0 8px 24px rgba(196,30,58,0.3); }
|
|
#s1 .btn-o {
|
|
padding: 13px 28px; background: transparent; color: rgba(245,240,232,0.7);
|
|
border: 1px solid rgba(245,240,232,0.15); border-radius: 6px;
|
|
font-size: 15px; cursor: pointer; letter-spacing: 0.04em; transition: all 0.3s;
|
|
}
|
|
#s1 .btn-o:hover { border-color: rgba(245,240,232,0.3); color: #F5F0E8; }
|
|
|
|
#s1 .seal {
|
|
position: absolute; top: 18%; right: 12%; z-index: 10;
|
|
width: 64px; height: 64px; border: 2px solid rgba(196,30,58,0.7);
|
|
border-radius: 4px; display: flex; align-items: center; justify-content: center;
|
|
font-family: 'Noto Serif SC', serif; font-size: 24px; color: rgba(196,30,58,0.7);
|
|
opacity: 0; transform: scale(2) rotate(-15deg);
|
|
}
|
|
|
|
/* ============================================ */
|
|
/* Scene 2: 山水意境 — Layered Mountain Mist */
|
|
/* ============================================ */
|
|
#s2 { background: linear-gradient(180deg, #1a1a2e 0%, #16213e 30%, #0f3460 60%, #1a1a2e 100%); }
|
|
#s2 canvas { position: absolute; inset: 0; }
|
|
|
|
#s2 .content {
|
|
position: absolute; inset: 0; z-index: 10;
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
text-align: center; padding: 0 6vw;
|
|
}
|
|
#s2 .tag {
|
|
display: inline-flex; padding: 6px 16px; border-radius: 999px;
|
|
font-size: 13px; font-weight: 500; letter-spacing: 0.08em;
|
|
background: rgba(196,30,58,0.15); color: #ff8a95;
|
|
border: 1px solid rgba(196,30,58,0.25);
|
|
opacity: 0; transform: translateY(16px);
|
|
}
|
|
#s2 h1 {
|
|
font-family: 'Noto Serif SC', serif; font-size: clamp(52px, 8vw, 96px);
|
|
font-weight: 300; letter-spacing: 0.15em; line-height: 1.1;
|
|
color: #F5F0E8; margin: 24px 0 16px;
|
|
opacity: 0; transform: translateY(20px);
|
|
}
|
|
#s2 .accent { color: #C41E3A; }
|
|
#s2 .sub {
|
|
font-size: clamp(15px, 1.8vw, 20px); color: rgba(245,240,232,0.4);
|
|
font-weight: 300; letter-spacing: 0.06em; line-height: 1.7;
|
|
max-width: 480px; margin-bottom: 36px;
|
|
opacity: 0; transform: translateY(16px);
|
|
}
|
|
#s2 .btns { display: flex; gap: 14px; opacity: 0; transform: translateY(16px); }
|
|
#s2 .btn-p {
|
|
padding: 13px 28px; background: #C41E3A; color: #fff; border: none;
|
|
border-radius: 6px; font-size: 15px; font-weight: 500; cursor: pointer;
|
|
letter-spacing: 0.04em; transition: all 0.3s;
|
|
}
|
|
#s2 .btn-p:hover { background: #d4213f; transform: translateY(-1px); box-shadow: 0 8px 24px rgba(196,30,58,0.3); }
|
|
#s2 .btn-o {
|
|
padding: 13px 28px; background: transparent; color: rgba(245,240,232,0.6);
|
|
border: 1px solid rgba(245,240,232,0.15); border-radius: 6px;
|
|
font-size: 15px; cursor: pointer; transition: all 0.3s;
|
|
}
|
|
#s2 .btn-o:hover { border-color: rgba(245,240,232,0.3); color: #F5F0E8; }
|
|
|
|
/* ============================================ */
|
|
/* Scene 3: 数字水墨 — Ink-to-Data Morphing */
|
|
/* ============================================ */
|
|
#s3 { background: #FAFAF5; }
|
|
#s3 canvas { position: absolute; inset: 0; }
|
|
|
|
#s3 .content {
|
|
position: absolute; inset: 0; z-index: 10;
|
|
display: flex; flex-direction: column; justify-content: center;
|
|
padding: 0 8vw;
|
|
}
|
|
#s3 .tag {
|
|
display: inline-flex; padding: 6px 16px; border-radius: 999px;
|
|
font-size: 13px; font-weight: 500; letter-spacing: 0.06em;
|
|
background: #FEF2F4; color: #C41E3A; border: 1px solid rgba(196,30,58,0.1);
|
|
opacity: 0; transform: translateY(16px);
|
|
}
|
|
#s3 h1 {
|
|
font-family: 'Inter', -apple-system, sans-serif;
|
|
font-size: clamp(44px, 6.5vw, 80px); font-weight: 600;
|
|
letter-spacing: -0.03em; line-height: 1.05;
|
|
color: #1C1C1C; margin: 20px 0 12px;
|
|
opacity: 0; transform: translateY(20px);
|
|
}
|
|
#s3 .accent { color: #C41E3A; font-weight: 600; }
|
|
#s3 .sub {
|
|
font-size: clamp(16px, 1.8vw, 20px); color: rgba(28,28,28,0.5);
|
|
font-weight: 400; line-height: 1.7; max-width: 520px; margin-bottom: 36px;
|
|
opacity: 0; transform: translateY(16px);
|
|
}
|
|
#s3 .btns { display: flex; gap: 14px; opacity: 0; transform: translateY(16px); }
|
|
#s3 .btn-p {
|
|
padding: 13px 28px; background: #C41E3A; color: #fff; border: none;
|
|
border-radius: 8px; font-size: 15px; font-weight: 500; cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
#s3 .btn-p:hover { background: #d4213f; transform: translateY(-1px); box-shadow: 0 8px 24px rgba(196,30,58,0.2); }
|
|
#s3 .btn-o {
|
|
padding: 13px 28px; background: transparent; color: #1C1C1C;
|
|
border: 1px solid rgba(28,28,28,0.15); border-radius: 8px;
|
|
font-size: 15px; cursor: pointer; transition: all 0.3s;
|
|
}
|
|
#s3 .btn-o:hover { border-color: rgba(28,28,28,0.3); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="nav">
|
|
<button class="active" onclick="go('s1')">壹 · 墨韵流光</button>
|
|
<button onclick="go('s2')">贰 · 山水意境</button>
|
|
<button onclick="go('s3')">叁 · 数字水墨</button>
|
|
</div>
|
|
|
|
<!-- Scene 1: 墨韵流光 -->
|
|
<div id="s1" class="scene active">
|
|
<canvas id="c1"></canvas>
|
|
<div class="seal">睿</div>
|
|
<div class="content">
|
|
<div class="tag">✦ 智连未来,成长伙伴</div>
|
|
<h1>睿新<span class="accent">致遠</span></h1>
|
|
<p class="sub">以智慧连接数字趋势,以伙伴身份陪您成长<br/>——您的数字化转型同行者</p>
|
|
<div class="btns">
|
|
<button class="btn-p">立即咨询 →</button>
|
|
<button class="btn-o">探索产品</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scene 2: 山水意境 -->
|
|
<div id="s2" class="scene">
|
|
<canvas id="c2"></canvas>
|
|
<div class="content">
|
|
<div class="tag">✦ 智连未来,成长伙伴</div>
|
|
<h1>睿新<span class="accent">致遠</span></h1>
|
|
<p class="sub">以智慧连接数字趋势<br/>以伙伴身份陪您成长</p>
|
|
<div class="btns">
|
|
<button class="btn-p">立即咨询 →</button>
|
|
<button class="btn-o">探索产品</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scene 3: 数字水墨 -->
|
|
<div id="s3" class="scene">
|
|
<canvas id="c3"></canvas>
|
|
<div class="content">
|
|
<div class="tag">✦ 智连未来,成长伙伴</div>
|
|
<h1>睿新<span class="accent">致遠</span></h1>
|
|
<p class="sub">以智慧连接数字趋势,以伙伴身份陪您成长——您的数字化转型同行者</p>
|
|
<div class="btns">
|
|
<button class="btn-p">立即咨询 →</button>
|
|
<button class="btn-o">探索产品</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="hint" id="hint">壹 · 墨韵流光 — 流动水墨 + 光晕 + 印章</div>
|
|
<button class="replay-btn" onclick="replay()">↻ 重播</button>
|
|
|
|
<script>
|
|
const hints = {
|
|
s1: '壹 · 墨韵流光 — 流动水墨 + 光晕 + 印章',
|
|
s2: '贰 · 山水意境 — 层叠山峦 + 云雾 + 星点',
|
|
s3: '叁 · 数字水墨 — 墨粒扩散 + 数据化消融',
|
|
};
|
|
let current = 's1';
|
|
let rafs = {};
|
|
|
|
function go(id) {
|
|
current = id;
|
|
document.querySelectorAll('.scene').forEach(s => s.classList.remove('active'));
|
|
document.getElementById(id).classList.add('active');
|
|
document.querySelectorAll('.nav button').forEach((b, i) => {
|
|
b.classList.toggle('active', ['s1','s2','s3'][i] === id);
|
|
});
|
|
document.getElementById('hint').textContent = hints[id];
|
|
replay();
|
|
}
|
|
|
|
function replay() {
|
|
Object.values(rafs).forEach(id => cancelAnimationFrame(id));
|
|
rafs = {};
|
|
|
|
const scene = document.getElementById(current);
|
|
const clone = scene.cloneNode(true);
|
|
scene.parentNode.replaceChild(clone, scene);
|
|
|
|
if (current === 's1') initS1();
|
|
else if (current === 's2') initS2();
|
|
else if (current === 's3') initS3();
|
|
|
|
animateContent(current);
|
|
}
|
|
|
|
function animateContent(id) {
|
|
const scene = document.getElementById(id);
|
|
const els = scene.querySelectorAll('.tag, h1, .sub, .btns, .seal');
|
|
const delays = [1.6, 1.9, 2.2, 2.5, 2.8];
|
|
|
|
els.forEach((el, i) => {
|
|
if (i >= delays.length) return;
|
|
setTimeout(() => {
|
|
el.style.transition = 'all 0.8s cubic-bezier(0.16, 1, 0.3, 1)';
|
|
el.style.opacity = '1';
|
|
el.style.transform = el.classList.contains('seal') ? 'scale(1) rotate(-5deg)' : 'translateY(0)';
|
|
}, delays[i] * 1000);
|
|
});
|
|
}
|
|
|
|
/* ============================================= */
|
|
/* Scene 1: 墨韵流光 — Flowing Ink Streams */
|
|
/* ============================================= */
|
|
function initS1() {
|
|
const canvas = document.getElementById('c1');
|
|
const ctx = canvas.getContext('2d');
|
|
canvas.width = window.innerWidth * 2;
|
|
canvas.height = window.innerHeight * 2;
|
|
canvas.style.width = '100%';
|
|
canvas.style.height = '100%';
|
|
ctx.scale(2, 2);
|
|
|
|
const W = window.innerWidth;
|
|
const H = window.innerHeight;
|
|
|
|
// Ink streams
|
|
class InkStream {
|
|
constructor() {
|
|
this.reset();
|
|
}
|
|
reset() {
|
|
this.x = W * (0.5 + Math.random() * 0.5);
|
|
this.y = -50;
|
|
this.vx = (Math.random() - 0.6) * 0.8;
|
|
this.vy = 0.5 + Math.random() * 1.5;
|
|
this.width = 1 + Math.random() * 3;
|
|
this.opacity = 0;
|
|
this.targetOpacity = 0.08 + Math.random() * 0.15;
|
|
this.trail = [];
|
|
this.maxTrail = 80 + Math.floor(Math.random() * 60);
|
|
this.hue = Math.random() > 0.7 ? 0 : 0; // black or red tint
|
|
this.sat = Math.random() > 0.7 ? 60 : 0;
|
|
this.light = this.sat > 0 ? 25 : 10 + Math.random() * 8;
|
|
this.delay = Math.random() * 120;
|
|
this.age = 0;
|
|
this.wobblePhase = Math.random() * Math.PI * 2;
|
|
this.wobbleSpeed = 0.01 + Math.random() * 0.02;
|
|
this.wobbleAmp = 0.3 + Math.random() * 0.5;
|
|
}
|
|
update() {
|
|
this.age++;
|
|
if (this.age < this.delay) return;
|
|
|
|
this.wobblePhase += this.wobbleSpeed;
|
|
this.vx += Math.sin(this.wobblePhase) * this.wobbleAmp * 0.02;
|
|
this.vy += 0.01;
|
|
|
|
this.x += this.vx;
|
|
this.y += this.vy;
|
|
|
|
this.trail.push({ x: this.x, y: this.y, w: this.width });
|
|
if (this.trail.length > this.maxTrail) this.trail.shift();
|
|
|
|
this.opacity = Math.min(this.targetOpacity, this.opacity + 0.002);
|
|
|
|
if (this.y > H + 100) this.reset();
|
|
}
|
|
draw(ctx) {
|
|
if (this.age < this.delay || this.trail.length < 3) return;
|
|
|
|
ctx.save();
|
|
ctx.globalAlpha = this.opacity;
|
|
ctx.lineCap = 'round';
|
|
ctx.lineJoin = 'round';
|
|
|
|
for (let i = 1; i < this.trail.length; i++) {
|
|
const p0 = this.trail[i - 1];
|
|
const p1 = this.trail[i];
|
|
const t = i / this.trail.length;
|
|
const alpha = t * this.opacity;
|
|
const w = p1.w * (0.3 + t * 0.7);
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(p0.x, p0.y);
|
|
ctx.lineTo(p1.x, p1.y);
|
|
ctx.strokeStyle = `hsla(${this.hue}, ${this.sat}%, ${this.light}%, ${alpha})`;
|
|
ctx.lineWidth = w;
|
|
ctx.stroke();
|
|
|
|
// Ink bleed effect
|
|
if (Math.random() < 0.03 && t > 0.3) {
|
|
ctx.beginPath();
|
|
ctx.arc(p1.x, p1.y, w * 3 + Math.random() * 8, 0, Math.PI * 2);
|
|
ctx.fillStyle = `hsla(${this.hue}, ${this.sat}%, ${this.light}%, ${alpha * 0.3})`;
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
// Glow orbs
|
|
class GlowOrb {
|
|
constructor() {
|
|
this.x = W * (0.3 + Math.random() * 0.5);
|
|
this.y = H * (0.2 + Math.random() * 0.4);
|
|
this.radius = 150 + Math.random() * 200;
|
|
this.vx = (Math.random() - 0.5) * 0.3;
|
|
this.vy = (Math.random() - 0.5) * 0.2;
|
|
this.isRed = Math.random() > 0.6;
|
|
this.opacity = 0;
|
|
this.targetOpacity = this.isRed ? 0.06 : 0.04;
|
|
this.phase = Math.random() * Math.PI * 2;
|
|
}
|
|
update() {
|
|
this.phase += 0.005;
|
|
this.x += this.vx + Math.sin(this.phase) * 0.2;
|
|
this.y += this.vy + Math.cos(this.phase * 0.7) * 0.15;
|
|
this.opacity = Math.min(this.targetOpacity, this.opacity + 0.0005);
|
|
if (this.x < -200 || this.x > W + 200) this.vx *= -1;
|
|
if (this.y < -200 || this.y > H + 200) this.vy *= -1;
|
|
}
|
|
draw(ctx) {
|
|
const grad = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.radius);
|
|
const color = this.isRed ? '196, 30, 58' : '180, 160, 140';
|
|
grad.addColorStop(0, `rgba(${color}, ${this.opacity})`);
|
|
grad.addColorStop(0.5, `rgba(${color}, ${this.opacity * 0.4})`);
|
|
grad.addColorStop(1, `rgba(${color}, 0)`);
|
|
ctx.fillStyle = grad;
|
|
ctx.fillRect(this.x - this.radius, this.y - this.radius, this.radius * 2, this.radius * 2);
|
|
}
|
|
}
|
|
|
|
const streams = Array.from({ length: 8 }, () => new InkStream());
|
|
const orbs = Array.from({ length: 4 }, () => new GlowOrb());
|
|
|
|
// Initial big ink splash
|
|
let splashProgress = 0;
|
|
const splashCenter = { x: W * 0.7, y: H * 0.35 };
|
|
|
|
function drawSplash(ctx) {
|
|
if (splashProgress >= 1) return;
|
|
splashProgress = Math.min(1, splashProgress + 0.008);
|
|
|
|
const maxR = 280;
|
|
const r = maxR * easeOutQuart(splashProgress);
|
|
const alpha = 0.12 * (1 - splashProgress * 0.5);
|
|
|
|
ctx.save();
|
|
ctx.globalAlpha = alpha;
|
|
|
|
// Main splash
|
|
const grad = ctx.createRadialGradient(splashCenter.x, splashCenter.y, 0, splashCenter.x, splashCenter.y, r);
|
|
grad.addColorStop(0, 'rgba(28, 28, 28, 0.6)');
|
|
grad.addColorStop(0.4, 'rgba(28, 28, 28, 0.3)');
|
|
grad.addColorStop(0.7, 'rgba(28, 28, 28, 0.1)');
|
|
grad.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = grad;
|
|
ctx.beginPath();
|
|
ctx.arc(splashCenter.x, splashCenter.y, r, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Red accent splash
|
|
const redR = r * 0.4;
|
|
const redGrad = ctx.createRadialGradient(splashCenter.x + 20, splashCenter.y - 10, 0, splashCenter.x + 20, splashCenter.y - 10, redR);
|
|
redGrad.addColorStop(0, 'rgba(196, 30, 58, 0.4)');
|
|
redGrad.addColorStop(0.5, 'rgba(196, 30, 58, 0.15)');
|
|
redGrad.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = redGrad;
|
|
ctx.beginPath();
|
|
ctx.arc(splashCenter.x + 20, splashCenter.y - 10, redR, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
function animate() {
|
|
ctx.fillStyle = 'rgba(8, 8, 8, 0.04)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
drawSplash(ctx);
|
|
|
|
for (const orb of orbs) { orb.update(); orb.draw(ctx); }
|
|
for (const stream of streams) { stream.update(); stream.draw(ctx); }
|
|
|
|
rafs['s1'] = requestAnimationFrame(animate);
|
|
}
|
|
|
|
ctx.fillStyle = '#080808';
|
|
ctx.fillRect(0, 0, W, H);
|
|
animate();
|
|
}
|
|
|
|
/* ============================================= */
|
|
/* Scene 2: 山水意境 — Layered Mountains + Mist */
|
|
/* ============================================= */
|
|
function initS2() {
|
|
const canvas = document.getElementById('c2');
|
|
const ctx = canvas.getContext('2d');
|
|
canvas.width = window.innerWidth * 2;
|
|
canvas.height = window.innerHeight * 2;
|
|
canvas.style.width = '100%';
|
|
canvas.style.height = '100%';
|
|
ctx.scale(2, 2);
|
|
|
|
const W = window.innerWidth;
|
|
const H = window.innerHeight;
|
|
|
|
// Mountain layers
|
|
function drawMountain(ctx, baseY, height, color, offset, segments) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(-10, H);
|
|
for (let i = 0; i <= segments; i++) {
|
|
const x = (W / segments) * i;
|
|
const noise = Math.sin(i * 0.3 + offset) * height * 0.4
|
|
+ Math.sin(i * 0.7 + offset * 1.5) * height * 0.3
|
|
+ Math.sin(i * 1.3 + offset * 0.8) * height * 0.15;
|
|
const y = baseY - Math.abs(noise);
|
|
ctx.lineTo(x, y);
|
|
}
|
|
ctx.lineTo(W + 10, H);
|
|
ctx.closePath();
|
|
ctx.fillStyle = color;
|
|
ctx.fill();
|
|
}
|
|
|
|
// Stars
|
|
const stars = Array.from({ length: 80 }, () => ({
|
|
x: Math.random() * W,
|
|
y: Math.random() * H * 0.5,
|
|
r: 0.5 + Math.random() * 1.5,
|
|
twinkle: Math.random() * Math.PI * 2,
|
|
speed: 0.02 + Math.random() * 0.03,
|
|
}));
|
|
|
|
// Mist particles
|
|
const mist = Array.from({ length: 20 }, () => ({
|
|
x: Math.random() * W,
|
|
y: H * (0.4 + Math.random() * 0.3),
|
|
w: 200 + Math.random() * 400,
|
|
h: 30 + Math.random() * 60,
|
|
speed: 0.15 + Math.random() * 0.3,
|
|
opacity: 0.02 + Math.random() * 0.04,
|
|
}));
|
|
|
|
// Red accent light
|
|
let redGlowProgress = 0;
|
|
|
|
let time = 0;
|
|
|
|
function animate() {
|
|
time += 0.003;
|
|
ctx.clearRect(0, 0, W, H);
|
|
|
|
// Sky gradient
|
|
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
|
|
skyGrad.addColorStop(0, '#0a0a1a');
|
|
skyGrad.addColorStop(0.4, '#16213e');
|
|
skyGrad.addColorStop(0.7, '#1a1a3e');
|
|
skyGrad.addColorStop(1, '#0a0a1a');
|
|
ctx.fillStyle = skyGrad;
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
// Stars
|
|
for (const s of stars) {
|
|
s.twinkle += s.speed;
|
|
const alpha = 0.3 + Math.sin(s.twinkle) * 0.3;
|
|
ctx.beginPath();
|
|
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
|
|
ctx.fillStyle = `rgba(245, 240, 232, ${alpha})`;
|
|
ctx.fill();
|
|
}
|
|
|
|
// Red glow behind mountains
|
|
redGlowProgress = Math.min(1, redGlowProgress + 0.005);
|
|
const glowAlpha = 0.08 * redGlowProgress;
|
|
const glowGrad = ctx.createRadialGradient(W * 0.5, H * 0.35, 0, W * 0.5, H * 0.35, 400);
|
|
glowGrad.addColorStop(0, `rgba(196, 30, 58, ${glowAlpha})`);
|
|
glowGrad.addColorStop(0.5, `rgba(196, 30, 58, ${glowAlpha * 0.3})`);
|
|
glowGrad.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = glowGrad;
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
// Mountains (back to front)
|
|
drawMountain(ctx, H * 0.55, 120, 'rgba(15, 20, 40, 0.9)', time * 0.5, 30);
|
|
drawMountain(ctx, H * 0.62, 100, 'rgba(20, 25, 50, 0.85)', time * 0.8 + 2, 25);
|
|
drawMountain(ctx, H * 0.7, 80, 'rgba(25, 30, 55, 0.8)', time * 1.2 + 4, 20);
|
|
drawMountain(ctx, H * 0.78, 60, 'rgba(30, 35, 60, 0.75)', time * 1.5 + 6, 18);
|
|
|
|
// Mist
|
|
for (const m of mist) {
|
|
m.x += m.speed;
|
|
if (m.x > W + m.w) m.x = -m.w;
|
|
ctx.save();
|
|
ctx.globalAlpha = m.opacity;
|
|
const mistGrad = ctx.createRadialGradient(m.x, m.y, 0, m.x, m.y, m.w * 0.5);
|
|
mistGrad.addColorStop(0, 'rgba(200, 210, 230, 0.3)');
|
|
mistGrad.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = mistGrad;
|
|
ctx.fillRect(m.x - m.w * 0.5, m.y - m.h, m.w, m.h * 2);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Foreground mountain
|
|
drawMountain(ctx, H * 0.88, 40, 'rgba(10, 12, 25, 0.95)', time * 2 + 8, 15);
|
|
|
|
rafs['s2'] = requestAnimationFrame(animate);
|
|
}
|
|
|
|
animate();
|
|
}
|
|
|
|
/* ============================================= */
|
|
/* Scene 3: 数字水墨 — Ink Particles → Data */
|
|
/* ============================================= */
|
|
function initS3() {
|
|
const canvas = document.getElementById('c3');
|
|
const ctx = canvas.getContext('2d');
|
|
canvas.width = window.innerWidth * 2;
|
|
canvas.height = window.innerHeight * 2;
|
|
canvas.style.width = '100%';
|
|
canvas.style.height = '100%';
|
|
ctx.scale(2, 2);
|
|
|
|
const W = window.innerWidth;
|
|
const H = window.innerHeight;
|
|
|
|
// Ink particles that spread and then morph into data dots
|
|
class InkParticle {
|
|
constructor(cx, cy) {
|
|
this.cx = cx;
|
|
this.cy = cy;
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = Math.random() * 10;
|
|
this.x = cx + Math.cos(angle) * dist;
|
|
this.y = cy + Math.sin(angle) * dist;
|
|
|
|
const speed = 0.3 + Math.random() * 1.5;
|
|
this.vx = Math.cos(angle) * speed;
|
|
this.vy = Math.sin(angle) * speed;
|
|
|
|
this.radius = 1.5 + Math.random() * 4;
|
|
this.targetRadius = this.radius;
|
|
this.opacity = 0.4 + Math.random() * 0.4;
|
|
this.isRed = Math.random() > 0.75;
|
|
|
|
this.phase = 'spreading'; // spreading -> settling -> morphing
|
|
this.spreadTime = 0;
|
|
this.maxSpreadTime = 60 + Math.random() * 80;
|
|
this.settleTime = 0;
|
|
this.morphProgress = 0;
|
|
|
|
// Target position for data morph
|
|
this.targetX = W * (0.55 + Math.random() * 0.35);
|
|
this.targetY = H * (0.15 + Math.random() * 0.6);
|
|
this.dataRadius = 1 + Math.random() * 2;
|
|
}
|
|
|
|
update() {
|
|
if (this.phase === 'spreading') {
|
|
this.x += this.vx;
|
|
this.y += this.vy;
|
|
this.vx *= 0.98;
|
|
this.vy *= 0.98;
|
|
this.spreadTime++;
|
|
if (this.spreadTime >= this.maxSpreadTime) {
|
|
this.phase = 'settling';
|
|
}
|
|
} else if (this.phase === 'settling') {
|
|
this.settleTime++;
|
|
this.opacity = Math.max(0.15, this.opacity - 0.003);
|
|
if (this.settleTime > 60) {
|
|
this.phase = 'morphing';
|
|
}
|
|
} else if (this.phase === 'morphing') {
|
|
this.morphProgress = Math.min(1, this.morphProgress + 0.008);
|
|
const t = easeInOutCubic(this.morphProgress);
|
|
this.x = lerp(this.x, this.targetX, t * 0.02);
|
|
this.y = lerp(this.y, this.targetY, t * 0.02);
|
|
this.radius = lerp(this.radius, this.dataRadius, t * 0.02);
|
|
this.opacity = lerp(this.opacity, 0.2 + Math.random() * 0.1, t * 0.01);
|
|
}
|
|
}
|
|
|
|
draw(ctx) {
|
|
ctx.beginPath();
|
|
ctx.arc(this.x, this.y, Math.max(0.5, this.radius), 0, Math.PI * 2);
|
|
if (this.isRed) {
|
|
ctx.fillStyle = `rgba(196, 30, 58, ${this.opacity})`;
|
|
} else {
|
|
ctx.fillStyle = `rgba(28, 28, 28, ${this.opacity})`;
|
|
}
|
|
ctx.fill();
|
|
|
|
// Glow for larger particles
|
|
if (this.radius > 3 && this.phase === 'spreading') {
|
|
ctx.beginPath();
|
|
ctx.arc(this.x, this.y, this.radius * 2.5, 0, Math.PI * 2);
|
|
ctx.fillStyle = this.isRed
|
|
? `rgba(196, 30, 58, ${this.opacity * 0.1})`
|
|
: `rgba(28, 28, 28, ${this.opacity * 0.08})`;
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Connection lines between nearby data particles
|
|
function drawConnections(ctx, particles) {
|
|
const morphed = particles.filter(p => p.phase === 'morphing' && p.morphProgress > 0.3);
|
|
ctx.save();
|
|
for (let i = 0; i < morphed.length; i++) {
|
|
for (let j = i + 1; j < morphed.length; j++) {
|
|
const dx = morphed[i].x - morphed[j].x;
|
|
const dy = morphed[i].y - morphed[j].y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
if (dist < 80) {
|
|
const alpha = (1 - dist / 80) * 0.08 * Math.min(morphed[i].morphProgress, morphed[j].morphProgress);
|
|
ctx.beginPath();
|
|
ctx.moveTo(morphed[i].x, morphed[i].y);
|
|
ctx.lineTo(morphed[j].x, morphed[j].y);
|
|
ctx.strokeStyle = `rgba(196, 30, 58, ${alpha})`;
|
|
ctx.lineWidth = 0.5;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
// Create particles in batches
|
|
const particles = [];
|
|
const center1 = { x: W * 0.7, y: H * 0.35 };
|
|
const center2 = { x: W * 0.25, y: H * 0.65 };
|
|
|
|
for (let i = 0; i < 120; i++) {
|
|
particles.push(new InkParticle(center1.x, center1.y));
|
|
}
|
|
setTimeout(() => {
|
|
for (let i = 0; i < 60; i++) {
|
|
particles.push(new InkParticle(center2.x, center2.y));
|
|
}
|
|
}, 500);
|
|
|
|
// Subtle background gradient orbs
|
|
function drawBgOrbs(ctx) {
|
|
const orbData = [
|
|
{ x: W * 0.65, y: H * 0.3, r: 300, color: 'rgba(196, 30, 58, 0.03)' },
|
|
{ x: W * 0.3, y: H * 0.7, r: 250, color: 'rgba(28, 28, 28, 0.02)' },
|
|
];
|
|
for (const o of orbData) {
|
|
const grad = ctx.createRadialGradient(o.x, o.y, 0, o.x, o.y, o.r);
|
|
grad.addColorStop(0, o.color);
|
|
grad.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = grad;
|
|
ctx.fillRect(o.x - o.r, o.y - o.r, o.r * 2, o.r * 2);
|
|
}
|
|
}
|
|
|
|
function animate() {
|
|
ctx.clearRect(0, 0, W, H);
|
|
ctx.fillStyle = '#FAFAF5';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
drawBgOrbs(ctx);
|
|
|
|
for (const p of particles) {
|
|
p.update();
|
|
p.draw(ctx);
|
|
}
|
|
|
|
drawConnections(ctx, particles);
|
|
|
|
rafs['s3'] = requestAnimationFrame(animate);
|
|
}
|
|
|
|
animate();
|
|
}
|
|
|
|
/* ============================================= */
|
|
/* Utilities */
|
|
/* ============================================= */
|
|
function easeOutQuart(t) { return 1 - Math.pow(1 - t, 4); }
|
|
function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
|
|
function lerp(a, b, t) { return a + (b - a) * t; }
|
|
|
|
// Init first scene
|
|
initS1();
|
|
animateContent('s1');
|
|
|
|
window.addEventListener('resize', () => {
|
|
if (current === 's1') initS1();
|
|
else if (current === 's2') initS2();
|
|
else if (current === 's3') initS3();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|