一个前端测试,哪位佬友可以在faber和Kimi k3 上试试吗

铁锤.王 2026-07-29 14:43 1

利用animate js,文字组成大海,模拟文字组成的海浪被风吹动,风吹过大海,形成海浪,一个文字组成的小船在波浪上前行 。真实,有趣 。

这是GLM5.2的效果:


最新回复 (6)
  • 行思渐远 07-29 14:51
    1

    k3 code客户端写的,不是网页上


    text-sea.zip (4.1 KB)


  • 魔教少主-黑小虎 07-29 14:59
    2
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>字海 — Sea of Words</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.2/anime.min.js"></script>
    <style>
    :root{
    --ink:#dfe9f5;
    --dim:#5b7492;
    --deep:#020810;
    --accent:#6ec8ff;
    }
    *{margin:0;padding:0;box-sizing:border-box}
    html,body{height:100%;overflow:hidden;background:var(--deep);font-family:"Songti SC","Noto Serif SC",serif}
    canvas{position:fixed;inset:0;display:block}

    .frame{position:fixed;inset:0;pointer-events:none;z-index:10}
    .title{position:absolute;top:6vh;left:6vw;color:var(--ink)}
    .title h1{
    font-size:clamp(3rem,8vw,7rem);
    font-weight:900;letter-spacing:.35em;line-height:1;
    writing-mode:vertical-rl;
    text-shadow:0 0 40px rgba(110,200,255,.35);
    opacity:0;
    }
    .title h1 span{display:inline-block}
    .sub{
    position:absolute;top:7vh;left:calc(6vw + clamp(4rem,9vw,8rem));
    color:var(--dim);font-size:.72rem;letter-spacing:.55em;
    writing-mode:vertical-rl;text-transform:uppercase;
    font-family:"Helvetica Neue",sans-serif;opacity:0;
    }
    .meta{
    position:absolute;bottom:5vh;right:6vw;text-align:right;color:var(--dim);
    font-family:"Helvetica Neue",sans-serif;font-size:.68rem;letter-spacing:.32em;
    line-height:2.1;opacity:0;
    }
    .meta b{color:var(--ink);font-weight:500}
    .wind-hud{
    position:absolute;bottom:5vh;left:6vw;display:flex;align-items:center;gap:.7rem;
    color:var(--dim);font-family:"Helvetica Neue",sans-serif;
    font-size:.68rem;letter-spacing:.32em;opacity:0;
    }
    .wind-hud svg{width:16px;height:16px;stroke:var(--accent)}
    .wind-bar{width:120px;height:1px;background:rgba(110,200,255,.18);position:relative}
    .wind-bar i{position:absolute;left:0;top:-1px;height:3px;width:20%;background:var(--accent);
    box-shadow:0 0 12px var(--accent)}
    .hint{
    position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
    color:var(--dim);font-size:.7rem;letter-spacing:.6em;
    font-family:"Helvetica Neue",sans-serif;opacity:0;
    }
    .vignette{position:fixed;inset:0;z-index:5;pointer-events:none;
    background:radial-gradient(ellipse at 50% 60%,transparent 55%,rgba(0,4,10,.75) 100%)}
    </style>
    </head>
    <body>
    <canvas id="sea"></canvas>
    <div class="vignette"></div>

    <div class="frame">
    <div class="title"><h1 id="h1"></h1></div>
    <div class="sub" id="sub">A SEA COMPOSED OF CHARACTERS</div>
    <div class="wind-hud" id="hud">
    <!-- Lucide: wind -->
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.8 19.6A2 2 0 1 0 14 16H2"/><path d="M17.5 8a2.5 2.5 0 1 1 2 4H2"/><path d="M9.8 4.4A2 2 0 1 1 11 8H2"/></svg>
    <span>风力</span>
    <div class="wind-bar"><i id="windbar"></i></div>
    </div>
    <div class="meta" id="meta">
    <b>风起于青萍之末</b><br>
    浪成于微澜之间<br>
    LAT 30.66 · LON 122.51
    </div>
    <div class="hint" id="hint">移动光标 唤风</div>
    </div>

    <script>
    "use strict";
    const cvs = document.getElementById('sea');
    const ctx = cvs.getContext('2d', {alpha:false});
    let W, H;

    /* ============================================================
    PERFORMANCE STRATEGY
    - 所有海面文字预渲染进精灵图集(sprite atlas):
    每个字 × 5 个亮度档,一次性画好,之后全部用 drawImage。
    彻底消灭每帧上千次 fillText / font 切换 / shadowBlur。
    - DPR 锁定 1,网格加大,波形函数用查表 sin。
    ============================================================ */

    // ---- fast sin lookup ----
    const SIN_N = 2048, SIN_TAB = new Float32Array(SIN_N);
    for(let i=0;i<SIN_N;i++) SIN_TAB[i]=Math.sin(i/SIN_N*Math.PI*2);
    const K = SIN_N/(Math.PI*2);
    function fsin(x){ return SIN_TAB[((x*K)|0)&(SIN_N-1)]; }

    // ---- atlas ----
    const SEA_TEXT = "浪潮汐波涛海流沫湾澜漪沧涟".split('');
    const TIERS = 5; // brightness levels
    const GS = 40; // glyph cell px in atlas
    const atlas = document.createElement('canvas');
    const TIER_COLORS = [
    "rgba(50,90,140,1)",
    "rgba(70,120,175,1)",
    "rgba(100,155,205,1)",
    "rgba(150,200,240,1)",
    "rgba(225,242,255,1)"
    ];
    function buildAtlas(){
    atlas.width = GS*SEA_TEXT.length; atlas.height = GS*TIERS;
    const a = atlas.getContext('2d');
    a.textAlign="center"; a.textBaseline="middle";
    for(let t=0;t<TIERS;t++){
    a.font = "28px 'Songti SC','Noto Serif SC',serif";
    a.fillStyle = TIER_COLORS[t];
    if(t===TIERS-1){ a.shadowColor="rgba(140,205,255,.9)"; a.shadowBlur=10; }
    else a.shadowBlur=0;
    for(let i=0;i<SEA_TEXT.length;i++)
    a.fillText(SEA_TEXT[i], i*GS+GS/2, t*GS+GS/2);
    }
    }
    buildAtlas();

    // foam glyphs pre-rendered too
    const FOAM_CH = ["ᐟ","ˊ","~","ᐠ"];
    const foamAtlas = document.createElement('canvas');
    (function(){
    foamAtlas.width=16*FOAM_CH.length; foamAtlas.height=16;
    const a=foamAtlas.getContext('2d');
    a.textAlign="center"; a.textBaseline="middle";
    a.font="12px serif"; a.fillStyle="rgba(200,230,255,1)";
    FOAM_CH.forEach((c,i)=>a.fillText(c,i*16+8,8));
    })();

    // wind glyph
    const windGlyph = document.createElement('canvas');
    (function(){
    windGlyph.width=20; windGlyph.height=20;
    const a=windGlyph.getContext('2d');
    a.textAlign="center"; a.textBaseline="middle";
    a.font="14px serif"; a.fillStyle="rgba(150,210,255,1)";
    a.fillText("风",10,10);
    })();

    // ---- grid ----
    const CELL = 30;
    let cols, rows, grid;
    function resize(){
    W = innerWidth; H = innerHeight;
    cvs.width = W; cvs.height = H; // DPR=1 on purpose
    ctx.imageSmoothingEnabled = true;
    buildGrid();
    buildSky();
    }
    function buildGrid(){
    grid = [];
    cols = Math.ceil(W/CELL)+2;
    const seaTop = H*0.42;
    rows = Math.ceil((H-seaTop)/CELL)+1;
    for(let r=0;r<rows;r++){
    const depth = r/rows;
    for(let c=0;c<cols;c++){
    grid.push({
    x:c*CELL + (r%2)*CELL*0.5,
    baseY:seaTop + r*CELL,
    gi:(Math.random()*SEA_TEXT.length)|0,
    depth,
    seed:Math.random()*6.28
    });
    }
    }
    }

    // pre-rendered sky (gradient + moon glow) — static, blit each frame
    const sky = document.createElement('canvas');
    function buildSky(){
    sky.width=W; sky.height=H;
    const s = sky.getContext('2d');
    const bg = s.createLinearGradient(0,0,0,H);
    bg.addColorStop(0,"#04101f"); bg.addColorStop(0.45,"#061627"); bg.addColorStop(1,"#02070e");
    s.fillStyle=bg; s.fillRect(0,0,W,H);
    const mg = s.createRadialGradient(W*0.78,H*0.16,0,W*0.78,H*0.16,H*0.3);
    mg.addColorStop(0,"rgba(180,215,255,.14)"); mg.addColorStop(1,"transparent");
    s.fillStyle=mg; s.fillRect(0,0,W,H);
    }
    addEventListener('resize', resize);

    // ---- wind ----
    const wind = {base:0.6, gust:0, gustX:-9999, target:0.6};
    const mouse = {vx:0, px:-9999};
    addEventListener('pointermove', e=>{
    if(mouse.px>-9998) mouse.vx = mouse.vx*0.8 + (e.clientX-mouse.px)*0.2;
    mouse.px = e.clientX;
    });
    function spawnGust(){
    wind.gustX = -W*0.3;
    anime({
    targets: wind,
    gustX: W*1.4,
    gust: [{value:1,duration:900,easing:'easeOutQuad'},{value:0,duration:2600,easing:'easeInOutSine'}],
    duration: 3500 + Math.random()*2000,
    easing:'linear',
    complete: ()=> setTimeout(spawnGust, 1500+Math.random()*3500)
    });
    }

    // ---- wave model (gust term computed per-column, cached) ----
    let gustCol = new Float32Array(0);
    function precomputeGust(){
    if(gustCol.length!==cols) gustCol = new Float32Array(cols);
    const inv = 1/(W*0.18);
    for(let c=0;c<cols;c++){
    const d = (c*CELL - wind.gustX)*inv;
    gustCol[c] = Math.exp(-d*d)*wind.gust;
    }
    }
    function waveY(x, t, depthMul, g){
    const amp = (14 + 26*wind.base) * (1-depthMul*0.75) * (1+g*1.6);
    return fsin(x*0.012 + t*1.1) * amp*0.55
    + fsin(x*0.021 - t*1.7 + 2) * amp*0.30
    + fsin(x*0.006 + t*0.55) * amp*0.45
    + fsin(x*0.045 - t*2.6) * amp*0.12*(1+g*2);
    }
    function slopeAt(x,t){
    const e=6; return (waveY(x+e,t,0,0)-waveY(x-e,t,0,0))/(2*e);
    }

    // ---- boat ----
    const boat = {x:-140, speed:0.55};
    const BOAT_ROWS = [
    {txt:"帆", dx:6, dy:-46, size:26, col:"#eaf4ff", sail:true},
    {txt:"│", dx:8, dy:-26, size:18, col:"#9db8d4", sail:true},
    {txt:"一叶轻舟", dx:0, dy:-4, size:17, col:"#eaf4ff"},
    {txt:"╰────╯", dx:0, dy:10, size:15, col:"#7d9cbd"}
    ];

    // ---- spray ----
    const MAX_SPRAY = 160;
    const sprays = [];
    function addSpray(x,y,pow){
    if(sprays.length>=MAX_SPRAY) return;
    sprays.push({x,y,vx:(Math.random()-0.3)*2*pow,vy:-Math.random()*2.4*pow-0.6,
    gi:(Math.random()*FOAM_CH.length)|0, life:1});
    }

    // ---- adaptive quality: drop deepest rows if slow ----
    let quality = 1; // 1 = full, 0.6 = skip deep rows
    let fpsAcc=0, fpsN=0, fpsT=0;

    // ---- render ----
    const windbar = document.getElementById('windbar');
    let T=0, last=0, hudTick=0;
    function frame(ts){
    const dt = Math.min((ts-last)/1000, 0.05); last = ts;
    // fps watch → adapt
    fpsAcc+=dt; fpsN++; fpsT+=dt;
    if(fpsT>2){ const f=fpsN/fpsAcc;
    if(f<40 && quality>0.55) quality-=0.15;
    else if(f>55 && quality<1) quality+=0.05;
    fpsAcc=0;fpsN=0;fpsT=0; }

    wind.target = 0.45 + Math.min(Math.abs(mouse.vx)*0.02, 0.9);
    wind.base += (wind.target - wind.base)*0.02;
    mouse.vx *= 0.95;
    T += dt * (0.9 + wind.base*0.8);

    if((hudTick++&7)===0)
    windbar.style.width = Math.min(100,(wind.base+wind.gust)*62)+"%";

    ctx.drawImage(sky,0,0);
    precomputeGust();

    // ---- sea: pure drawImage from atlas ----
    const maxDepth = quality; // skip deepest rows when adapting
    const n = grid.length;
    for(let i=0;i<n;i++){
    const p = grid[i];
    if(p.depth > maxDepth) continue;
    const c = (p.x/CELL)|0;
    const g = gustCol[c<cols?c:cols-1];
    const y = p.baseY + waveY(p.x, T + p.depth*0.7, p.depth, g);
    const lift = p.baseY - y;
    const bright = lift>0 ? lift/34 : 0;
    // tier 0..4
    let tier = ((0.4 + (1-p.depth)*1.4 + bright*2.2 + g*1.2)) | 0;
    if(tier>4) tier=4;
    const size = CELL*(0.62 + (1-p.depth)*0.3 + bright*0.25);
    const sway = fsin(T*2+p.seed)*2*(1-p.depth) + g*8;
    ctx.globalAlpha = Math.min(1, 0.14 + (1-p.depth)*0.5 + bright*0.5 + g*0.25);
    ctx.drawImage(atlas, p.gi*GS, tier*GS, GS, GS,
    p.x+sway-size*0.5, y-size*0.5, size, size);
    if(tier===4 && p.depth<0.12 && bright>1.1 && Math.random()<0.05)
    addSpray(p.x, y-8, 1+g);
    }
    ctx.globalAlpha = 1;

    // ---- wind streaks ----
    if(wind.gust>0.05){
    for(let i=0;i<10;i++){
    const wx = wind.gustX + (i-5)*46 + fsin(T*3+i)*10;
    const wy = H*0.42 - 60 - (i%4)*26 + fsin(T*5+i*2)*6;
    ctx.globalAlpha = 0.35*wind.gust*(1-Math.abs(i-5)/6);
    ctx.drawImage(windGlyph, wx, wy);
    }
    ctx.globalAlpha=1;
    }

    // ---- boat (few fillText calls — cheap) ----
    boat.x += (boat.speed + wind.base*0.9 + wind.gust*2.2) * dt * 36;
    if(boat.x > W+160) boat.x = -160;
    const by = H*0.42 + waveY(boat.x, T, 0, 0) - 6;
    const tilt = Math.atan(slopeAt(boat.x, T))*0.85 + fsin(T*1.4)*0.03;
    ctx.save();
    ctx.translate(boat.x, by); ctx.rotate(tilt);
    ctx.textAlign="center"; ctx.textBaseline="middle";
    for(const r of BOAT_ROWS){
    ctx.font = r.size+"px 'Songti SC','Noto Serif SC',serif";
    ctx.fillStyle = r.col;
    const lean = r.sail ? (wind.base+wind.gust)*6 : 0;
    ctx.fillText(r.txt, r.dx+lean, r.dy);
    }
    ctx.restore();
    if(Math.random()<0.18+wind.gust*0.4) addSpray(boat.x+30, by+8, 0.9+wind.gust);

    // ---- spray ----
    for(let i=sprays.length-1;i>=0;i--){
    const s=sprays[i];
    s.x+=s.vx; s.y+=s.vy; s.vy+=0.09; s.life-=0.028;
    if(s.life<=0){ sprays[i]=sprays[sprays.length-1]; sprays.pop(); continue; }
    ctx.globalAlpha = s.life*0.8;
    ctx.drawImage(foamAtlas, s.gi*16,0,16,16, s.x,s.y,16,16);
    }
    ctx.globalAlpha=1;

    requestAnimationFrame(frame);
    }

    // ---- boot ----
    resize();
    requestAnimationFrame(frame);
    spawnGust();

    const h1 = document.getElementById('h1');
    "字海".split('').forEach(c=>{const s=document.createElement('span');s.textContent=c;h1.appendChild(s);});
    anime.timeline()
    .add({targets:'#h1', opacity:[0,1], duration:400, easing:'linear'})
    .add({targets:'#h1 span', translateY:[60,0], opacity:[0,1], delay:anime.stagger(260),
    duration:1400, easing:'easeOutExpo'}, 0)
    .add({targets:'#sub', opacity:[0,1], translateY:[20,0], duration:1200, easing:'easeOutExpo'}, 800)
    .add({targets:['#meta','#hud'], opacity:[0,1], translateY:[14,0], delay:anime.stagger(150),
    duration:1000, easing:'easeOutExpo'}, 1300)
    .add({targets:'#hint', opacity:[0,0.85], duration:900, easing:'linear'}, 2100)
    .add({targets:'#hint', opacity:0, duration:1200, easing:'linear', delay:2600});
    </script>
    </body>
    </html>

  • 魔教少主-黑小虎 07-29 15:03
    3

    fable-max


    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>文字沧海 · 长风破浪</title>
    <style>
    :root{
    --ink:#1d4e79;
    }
    *{ margin:0; padding:0; box-sizing:border-box; }
    html,body{ width:100%; height:100%; overflow:hidden; }
    body{
    position:relative;
    background:linear-gradient(180deg,#79b4d8 0%, #a7d0e6 34%, #cfe6ef 52%, #f2e3bd 100%);
    font-family:"Kaiti SC","STKaiti","KaiTi","TW-Kai","Noto Serif SC","Songti SC","SimSun",serif;
    -webkit-user-select:none; user-select:none;
    -webkit-tap-highlight-color:transparent;
    cursor:default;
    }

    /* ---------- 天空 ---------- */
    #sun{ position:absolute; right:11%; top:11%; width:120px; height:120px; z-index:2; }
    #sunCore{
    position:absolute; left:50%; top:50%; width:74px; height:74px; margin:-37px 0 0 -37px;
    border-radius:50%;
    background:radial-gradient(circle at 42% 38%, #fffbe8 0%, #ffe9a8 46%, #ffd670 72%, rgba(255,214,112,0) 100%);
    box-shadow:0 0 46px 18px rgba(255,224,140,.55);
    display:flex; align-items:center; justify-content:center;
    color:#e09f35; font-size:30px;
    }
    #sunRing{ position:absolute; left:50%; top:50%; width:0; height:0; }
    #sunRing span{
    position:absolute; left:0; top:0; color:#f4c96b; font-size:14px; opacity:.85;
    transform-origin:0 0;
    text-shadow:0 0 8px rgba(255,220,130,.8);
    }

    .cloud{ position:absolute; z-index:3; transition:filter 2.5s; }
    .cloud span{
    position:absolute; color:#ffffff; text-shadow:0 2px 10px rgba(255,255,255,.9);
    }
    body.storm .cloud{ filter:brightness(.58) saturate(.7); }

    #birds{ position:absolute; inset:0; z-index:4; pointer-events:none; }
    .birdGroup{ position:absolute; left:0; top:0; }
    .birdGroup i{
    position:absolute; font-style:normal; color:#33566e; opacity:.75;
    transform:rotate(180deg); display:inline-block;
    }

    /* ---------- 标题(竖排) ---------- */
    #titleBox{
    position:absolute; left:4.2%; top:6%; z-index:6;
    display:flex; flex-direction:row-reverse; gap:14px;
    pointer-events:none;
    }
    #titleMain{
    writing-mode:vertical-rl; font-size:34px; letter-spacing:10px;
    color:var(--ink); text-shadow:0 1px 0 rgba(255,255,255,.5);
    }
    #titleMain span{ display:inline-block; }
    #seal{
    width:24px; height:24px; margin-top:14px; border-radius:4px;
    background:#c33d2e; color:#fff; font-size:15px; line-height:24px; text-align:center;
    box-shadow:0 1px 4px rgba(120,30,20,.4); transform:rotate(-3deg);
    }
    #titleSub{
    writing-mode:vertical-rl; font-size:13px; letter-spacing:5px;
    color:#41708f; opacity:.9; padding-top:6px;
    }
    #titleSub em{ font-style:normal; font-size:11px; opacity:.75; margin-top:10px; }

    /* ---------- 风带 ---------- */
    #streaks{ position:absolute; inset:0; z-index:24; pointer-events:none; }
    .streak{
    position:absolute; white-space:nowrap; color:#ffffff;
    text-shadow:0 0 10px rgba(255,255,255,.75); opacity:0; will-change:transform;
    }
    .streak .tail{
    letter-spacing:3px; opacity:.8;
    -webkit-mask-image:linear-gradient(to left,#000 30%,transparent 100%);
    mask-image:linear-gradient(to left,#000 30%,transparent 100%);
    }
    .streak .head{ font-size:1.25em; }

    /* ---------- 海 ---------- */
    #sea{
    position:absolute; left:0; right:0; bottom:0; height:46%;
    overflow:hidden; z-index:9;
    background:linear-gradient(180deg,#8fb6cc 0%, #5d92b4 13%, #35719b 36%, #1d537f 70%, #123c5f 100%);
    transition:filter 2.5s;
    }
    body.storm #sea{ filter:brightness(.76) saturate(.82); }
    #sea::before{ /* 海平线雾光 */
    content:""; position:absolute; left:0; right:0; top:0; height:16px; z-index:18;
    background:linear-gradient(180deg,rgba(255,246,220,.55),rgba(255,255,255,0));
    pointer-events:none;
    }
    #glint{
    position:absolute; top:0; height:64%; width:30vw; z-index:17;
    background:radial-gradient(ellipse 50% 60% at 50% 0%, rgba(255,228,150,.38), rgba(255,228,150,0) 70%);
    pointer-events:none; mix-blend-mode:soft-light; transition:opacity 2.5s;
    }
    body.storm #glint{ opacity:.15; }
    #haze{
    position:absolute; left:0; right:0; top:0; height:34%; z-index:12;
    background:linear-gradient(180deg,rgba(214,232,240,.5),rgba(214,232,240,0));
    pointer-events:none;
    }
    .seaRow{ position:absolute; left:0; top:0; white-space:nowrap; will-change:contents; }
    .seaRow span{
    display:inline-block; transform-origin:50% 100%;
    will-change:transform; opacity:0;
    }

    /* ---------- 小船 ---------- */
    #boat{ position:absolute; left:0; top:0; z-index:14; will-change:transform; pointer-events:none; transform-origin:0 0; }
    #boatInner{ position:relative; transform-origin:50% 88%; }
    #boatInner::before{ /* 柔光,把船从文字背景里衬出来 */
    content:""; position:absolute; left:-24px; right:-24px; top:-30px; bottom:-12px;
    background:radial-gradient(ellipse 55% 52% at 48% 62%, rgba(255,248,228,.30), rgba(255,248,228,0) 72%);
    pointer-events:none;
    }
    #rig{ position:relative; margin-left:30px; width:92px; height:82px; }
    #pennant{
    position:absolute; left:-5px; top:-31px; color:#d94f30; font-size:19px;
    transform-origin:0 70%;
    text-shadow:0 1px 2px rgba(80,20,10,.3), 0 0 6px rgba(255,255,255,.5);
    }
    #mast{
    position:absolute; left:3px; top:-18px; height:102px; width:3px;
    background:linear-gradient(#8a5e3a,#4a2f1b); border-radius:2px;
    box-shadow:1px 0 1px rgba(0,0,0,.15);
    }
    #sail{
    color:#f9f4e4; font-size:20px; line-height:1; padding-left:8px;
    text-shadow:1px 1px 0 rgba(90,70,40,.28);
    transform-origin:0 100%;
    }
    #sail div{ height:20px; }
    #hull{
    color:#5d3820; font-size:22px; font-weight:bold;
    text-shadow:0 1px 0 rgba(255,240,210,.3), 0 2px 4px rgba(15,25,45,.4);
    margin-top:0px;
    }

    /* ---------- 粒子层 ---------- */
    #fxBack{ position:absolute; inset:0; z-index:13; pointer-events:none; }
    #fxMid{ position:absolute; inset:0; z-index:15; pointer-events:none; }
    #fxFront{ position:absolute; inset:0; z-index:22; pointer-events:none; }
    #rainLayer{ position:absolute; inset:0; z-index:27; pointer-events:none; }
    .pt{ position:absolute; will-change:transform,opacity; }

    /* ---------- 风暴罩 / 闪电 ---------- */
    #veil{
    position:absolute; inset:0; z-index:25; pointer-events:none; opacity:0;
    background:linear-gradient(180deg,rgba(24,34,52,.72),rgba(28,40,58,.5) 55%,rgba(16,26,44,.62));
    }
    #flash{
    position:absolute; inset:0; z-index:26; pointer-events:none; opacity:0;
    background:radial-gradient(ellipse at 30% 0%, rgba(255,255,255,.95), rgba(220,235,255,.5) 55%, rgba(200,220,255,.25));
    }

    /* ---------- UI ---------- */
    #ui{
    position:absolute; left:18px; bottom:16px; z-index:40;
    display:flex; align-items:center; gap:8px;
    font-family:"PingFang SC","Microsoft YaHei","Noto Sans SC",sans-serif;
    }
    #ui .lab{ color:rgba(255,255,255,.92); font-size:13px; letter-spacing:2px; text-shadow:0 1px 3px rgba(10,30,50,.5); margin-right:2px; }
    .chip{
    appearance:none; -webkit-appearance:none; outline:none;
    font-family:inherit;
    padding:6px 15px; border-radius:999px; font-size:13px; letter-spacing:1px;
    color:var(--ink); background:rgba(255,255,255,.55);
    border:1px solid rgba(255,255,255,.7);
    backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
    cursor:pointer; transition:all .35s;
    }
    .chip:hover{ background:rgba(255,255,255,.8); }
    .chip.on{ background:var(--ink); color:#fff; border-color:rgba(255,255,255,.3); }
    #hint{
    position:absolute; left:50%; bottom:18px; transform:translateX(-50%); z-index:40;
    color:rgba(255,255,255,.9); font-size:13px; letter-spacing:3px;
    text-shadow:0 1px 4px rgba(10,30,50,.6); pointer-events:none;
    font-family:"PingFang SC","Microsoft YaHei","Noto Sans SC",sans-serif;
    }
    </style>
    </head>
    <body>

    <!-- 天空 -->
    <div id="sun"><div id="sunCore">日</div><div id="sunRing"></div></div>
    <div id="birds"></div>
    <div id="titleBox">
    <div style="display:flex;flex-direction:column;align-items:center;gap:0;">
    <div id="titleMain"></div>
    <div id="seal">舟</div>
    </div>
    <div id="titleSub">长风破浪会有时,直挂云帆济沧海<em>—— 李白 · 行路难</em></div>
    </div>
    <div id="streaks"></div>

    <!-- 海 -->
    <div id="sea"><div id="glint"></div><div id="haze"></div></div>

    <!-- 小船 -->
    <div id="boat">
    <div id="boatInner">
    <div id="rig">
    <div id="pennant">风</div>
    <div id="mast"></div>
    <div id="sail">
    <div>帆</div>
    <div>帆帆</div>
    <div>帆帆帆</div>
    <div>帆帆帆帆</div>
    </div>
    </div>
    <div id="hull">\乘风破浪/</div>
    </div>
    </div>

    <div id="fxBack"></div>
    <div id="fxMid"></div>
    <div id="fxFront"></div>
    <div id="rainLayer"></div>
    <div id="veil"></div>
    <div id="flash"></div>

    <div id="ui">
    <span class="lab">风力</span>
    <button class="chip" data-w="0">微风</button>
    <button class="chip on" data-w="1">清风</button>
    <button class="chip" data-w="2">狂风</button>
    </div>
    <div id="hint">点击海面 · 唤一阵风</div>

    <script>/* anime.js v3.2.2 (MIT License) © Julian Garnier — animejs.com,已内联以便离线使用 */
    /*
    * anime.js v3.2.2
    * (c) 2023 Julian Garnier
    * Released under the MIT license
    * animejs.com
    */

    !function(n,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):n.anime=e()}(this,function(){"use strict";var i={update:null,begin:null,loopBegin:null,changeBegin:null,change:null,changeComplete:null,loopComplete:null,complete:null,loop:1,direction:"normal",autoplay:!0,timelineOffset:0},M={duration:1e3,delay:0,endDelay:0,easing:"easeOutElastic(1, .5)",round:0},j=["translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY","perspective","matrix","matrix3d"],l={CSS:{},springs:{}};function C(n,e,t){return Math.min(Math.max(n,e),t)}function u(n,e){return-1<n.indexOf(e)}function o(n,e){return n.apply(null,e)}var w={arr:function(n){return Array.isArray(n)},obj:function(n){return u(Object.prototype.toString.call(n),"Object")},pth:function(n){return w.obj(n)&&n.hasOwnProperty("totalLength")},svg:function(n){return n instanceof SVGElement},inp:function(n){return n instanceof HTMLInputElement},dom:function(n){return n.nodeType||w.svg(n)},str:function(n){return"string"==typeof n},fnc:function(n){return"function"==typeof n},und:function(n){return void 0===n},nil:function(n){return w.und(n)||null===n},hex:function(n){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(n)},rgb:function(n){return/^rgb/.test(n)},hsl:function(n){return/^hsl/.test(n)},col:function(n){return w.hex(n)||w.rgb(n)||w.hsl(n)},key:function(n){return!i.hasOwnProperty(n)&&!M.hasOwnProperty(n)&&"targets"!==n&&"keyframes"!==n}};function d(n){n=/\(([^)]+)\)/.exec(n);return n?n[1].split(",").map(function(n){return parseFloat(n)}):[]}function c(r,t){var n=d(r),e=C(w.und(n[0])?1:n[0],.1,100),a=C(w.und(n[1])?100:n[1],.1,100),o=C(w.und(n[2])?10:n[2],.1,100),n=C(w.und(n[3])?0:n[3],.1,100),u=Math.sqrt(a/e),i=o/(2*Math.sqrt(a*e)),c=i<1?u*Math.sqrt(1-i*i):0,s=i<1?(i*u-n)/c:-n+u;function f(n){var e=t?t*n/1e3:n,e=i<1?Math.exp(-e*i*u)*(+Math.cos(c*e)+s*Math.sin(c*e)):(1+s*e)*Math.exp(-e*u);return 0===n||1===n?n:1-e}return t?f:function(){var n=l.springs[r];if(n)return n;for(var e=0,t=0;;)if(1===f(e+=1/6)){if(16<=++t)break}else t=0;return n=e*(1/6)*1e3,l.springs[r]=n}}function q(e){return void 0===e&&(e=10),function(n){return Math.ceil(C(n,1e-6,1)*e)*(1/e)}}var H=function(b,e,M,t){if(0<=b&&b<=1&&0<=M&&M<=1){var x=new Float32Array(11);if(b!==e||M!==t)for(var n=0;n<11;++n)x[n]=k(.1*n,b,M);return function(n){return b===e&&M===t||0===n||1===n?n:k(r(n),e,t)}}function r(n){for(var e=0,t=1;10!==t&&x[t]<=n;++t)e+=.1;var r=e+.1*((n-x[--t])/(x[t+1]-x[t])),a=O(r,b,M);if(.001<=a){for(var o=n,u=r,i=b,c=M,s=0;s<4;++s){var f=O(u,i,c);if(0===f)return u;u-=(k(u,i,c)-o)/f}return u}if(0===a)return r;for(var l,d,p=n,h=e,g=e+.1,m=b,v=M,y=0;0<(l=k(d=h+(g-h)/2,m,v)-p)?g=d:h=d,1e-7<Math.abs(l)&&++y<10;);return d}};function r(n,e){return 1-3*e+3*n}function k(n,e,t){return((r(e,t)*n+(3*t-6*e))*n+3*e)*n}function O(n,e,t){return 3*r(e,t)*n*n+2*(3*t-6*e)*n+3*e}e={linear:function(){return function(n){return n}}},t={Sine:function(){return function(n){return 1-Math.cos(n*Math.PI/2)}},Expo:function(){return function(n){return n?Math.pow(2,10*n-10):0}},Circ:function(){return function(n){return 1-Math.sqrt(1-n*n)}},Back:function(){return function(n){return n*n*(3*n-2)}},Bounce:function(){return function(n){for(var e,t=4;n<((e=Math.pow(2,--t))-1)/11;);return 1/Math.pow(4,3-t)-7.5625*Math.pow((3*e-2)/22-n,2)}},Elastic:function(n,e){void 0===e&&(e=.5);var t=C(n=void 0===n?1:n,1,10),r=C(e,.1,2);return function(n){return 0===n||1===n?n:-t*Math.pow(2,10*(n-1))*Math.sin((n-1-r/(2*Math.PI)*Math.asin(1/t))*(2*Math.PI)/r)}}},["Quad","Cubic","Quart","Quint"].forEach(function(n,e){t[n]=function(){return function(n){return Math.pow(n,e+2)}}}),Object.keys(t).forEach(function(n){var r=t[n];e["easeIn"+n]=r,e["easeOut"+n]=function(e,t){return function(n){return 1-r(e,t)(1-n)}},e["easeInOut"+n]=function(e,t){return function(n){return n<.5?r(e,t)(2*n)/2:1-r(e,t)(-2*n+2)/2}},e["easeOutIn"+n]=function(e,t){return function(n){return n<.5?(1-r(e,t)(1-2*n))/2:(r(e,t)(2*n-1)+1)/2}}});var e,t,s=e;function P(n,e){if(w.fnc(n))return n;var t=n.split("(")[0],r=s[t],a=d(n);switch(t){case"spring":return c(n,e);case"cubicBezier":return o(H,a);case"steps":return o(q,a);default:return o(r,a)}}function a(n){try{return document.querySelectorAll(n)}catch(n){}}function I(n,e){for(var t,r=n.length,a=2<=arguments.length?e:void 0,o=[],u=0;u<r;u++)u in n&&(t=n[u],e.call(a,t,u,n))&&o.push(t);return o}function f(n){return n.reduce(function(n,e){return n.concat(w.arr(e)?f(e):e)},[])}function p(n){return w.arr(n)?n:(n=w.str(n)?a(n)||n:n)instanceof NodeList||n instanceof HTMLCollection?[].slice.call(n):[n]}function h(n,e){return n.some(function(n){return n===e})}function g(n){var e,t={};for(e in n)t[e]=n[e];return t}function x(n,e){var t,r=g(n);for(t in n)r[t]=(e.hasOwnProperty(t)?e:n)[t];return r}function D(n,e){var t,r=g(n);for(t in e)r[t]=(w.und(n[t])?e:n)[t];return r}function V(n){var e,t,r,a,o,u,i;return w.rgb(n)?(e=/rgb\((\d+,\s*[\d]+,\s*[\d]+)\)/g.exec(t=n))?"rgba("+e[1]+",1)":t:w.hex(n)?(e=(e=n).replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i,function(n,e,t,r){return e+e+t+t+r+r}),e=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e),"rgba("+parseInt(e[1],16)+","+parseInt(e[2],16)+","+parseInt(e[3],16)+",1)"):w.hsl(n)?(t=/hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(t=n)||/hsla\((\d+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)/g.exec(t),n=parseInt(t[1],10)/360,u=parseInt(t[2],10)/100,i=parseInt(t[3],10)/100,t=t[4]||1,0==u?r=a=o=i:(r=c(u=2*i-(i=i<.5?i*(1+u):i+u-i*u),i,n+1/3),a=c(u,i,n),o=c(u,i,n-1/3)),"rgba("+255*r+","+255*a+","+255*o+","+t+")"):void 0;function c(n,e,t){return t<0&&(t+=1),1<t&&--t,t<1/6?n+6*(e-n)*t:t<.5?e:t<2/3?n+(e-n)*(2/3-t)*6:n}}function B(n){n=/[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(n);if(n)return n[1]}function m(n,e){return w.fnc(n)?n(e.target,e.id,e.total):n}function v(n,e){return n.getAttribute(e)}function y(n,e,t){var r,a,o;return h([t,"deg","rad","turn"],B(e))?e:(r=l.CSS[e+t],w.und(r)?(a=document.createElement(n.tagName),(n=n.parentNode&&n.parentNode!==document?n.parentNode:document.body).appendChild(a),a.style.position="absolute",a.style.width=100+t,o=100/a.offsetWidth,n.removeChild(a),n=o*parseFloat(e),l.CSS[e+t]=n):r)}function $(n,e,t){var r;if(e in n.style)return r=e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),e=n.style[e]||getComputedStyle(n).getPropertyValue(r)||"0",t?y(n,e,t):e}function b(n,e){return w.dom(n)&&!w.inp(n)&&(!w.nil(v(n,e))||w.svg(n)&&n[e])?"attribute":w.dom(n)&&h(j,e)?"transform":w.dom(n)&&"transform"!==e&&$(n,e)?"css":null!=n[e]?"object":void 0}function W(n){if(w.dom(n)){for(var e,t=n.style.transform||"",r=/(\w+)\(([^)]*)\)/g,a=new Map;e=r.exec(t);)a.set(e[1],e[2]);return a}}function X(n,e,t,r){var a=u(e,"scale")?1:0+(u(a=e,"translate")||"perspective"===a?"px":u(a,"rotate")||u(a,"skew")?"deg":void 0),o=W(n).get(e)||a;return t&&(t.transforms.list.set(e,o),t.transforms.last=e),r?y(n,o,r):o}function T(n,e,t,r){switch(b(n,e)){case"transform":return X(n,e,r,t);case"css":return $(n,e,t);case"attribute":return v(n,e);default:return n[e]||0}}function E(n,e){var t=/^(\*=|\+=|-=)/.exec(n);if(!t)return n;var r=B(n)||0,a=parseFloat(e),o=parseFloat(n.replace(t[0],""));switch(t[0][0]){case"+":return a+o+r;case"-":return a-o+r;case"*":return a*o+r}}function Y(n,e){var t;return w.col(n)?V(n):/\s/g.test(n)?n:(t=(t=B(n))?n.substr(0,n.length-t.length):n,e?t+e:t)}function F(n,e){return Math.sqrt(Math.pow(e.x-n.x,2)+Math.pow(e.y-n.y,2))}function Z(n){for(var e,t=n.points,r=0,a=0;a<t.numberOfItems;a++){var o=t.getItem(a);0<a&&(r+=F(e,o)),e=o}return r}function G(n){if(n.getTotalLength)return n.getTotalLength();switch(n.tagName.toLowerCase()){case"circle":return 2*Math.PI*v(n,"r");case"rect":return 2*v(t=n,"width")+2*v(t,"height");case"line":return F({x:v(t=n,"x1"),y:v(t,"y1")},{x:v(t,"x2"),y:v(t,"y2")});case"polyline":return Z(n);case"polygon":return e=n.points,Z(n)+F(e.getItem(e.numberOfItems-1),e.getItem(0))}var e,t}function Q(n,e){var e=e||{},n=e.el||function(n){for(var e=n.parentNode;w.svg(e)&&w.svg(e.parentNode);)e=e.parentNode;return e}(n),t=n.getBoundingClientRect(),r=v(n,"viewBox"),a=t.width,t=t.height,e=e.viewBox||(r?r.split(" "):[0,0,a,t]);return{el:n,viewBox:e,x:+e[0],y:+e[1],w:a,h:t,vW:e[2],vH:e[3]}}function z(n,e){var t=/[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g,r=Y(w.pth(n)?n.totalLength:n,e)+"";return{original:r,numbers:r.match(t)?r.match(t).map(Number):[0],strings:w.str(n)||e?r.split(t):[]}}function A(n){return I(n?f(w.arr(n)?n.map(p):p(n)):[],function(n,e,t){return t.indexOf(n)===e})}function _(n){var t=A(n);return t.map(function(n,e){return{target:n,id:e,total:t.length,transforms:{list:W(n)}}})}function R(e){for(var t=I(f(e.map(function(n){return Object.keys(n)})),function(n){return w.key(n)}).reduce(function(n,e){return n.indexOf(e)<0&&n.push(e),n},[]),a={},n=0;n<t.length;n++)!function(n){var r=t[n];a[r]=e.map(function(n){var e,t={};for(e in n)w.key(e)?e==r&&(t.value=n[e]):t[e]=n[e];return t})}(n);return a}function J(n,e){var t,r=[],a=e.keyframes;for(t in e=a?D(R(a),e):e)w.key(t)&&r.push({name:t,tweens:function(n,t){var e,r=g(t),a=(/^spring/.test(r.easing)&&(r.duration=c(r.easing)),w.arr(n)&&(2===(e=n.length)&&!w.obj(n[0])?n={value:n}:w.fnc(t.duration)||(r.duration=t.duration/e)),w.arr(n)?n:[n]);return a.map(function(n,e){n=w.obj(n)&&!w.pth(n)?n:{value:n};return w.und(n.delay)&&(n.delay=e?0:t.delay),w.und(n.endDelay)&&(n.endDelay=e===a.length-1?t.endDelay:0),n}).map(function(n){return D(n,r)})}(e[t],n)});return r}function K(i,c){var s;return i.tweens.map(function(n){var n=function(n,e){var t,r={};for(t in n){var a=m(n[t],e);w.arr(a)&&1===(a=a.map(function(n){return m(n,e)})).length&&(a=a[0]),r[t]=a}return r.duration=parseFloat(r.duration),r.delay=parseFloat(r.delay),r}(n,c),e=n.value,t=w.arr(e)?e[1]:e,r=B(t),a=T(c.target,i.name,r,c),o=s?s.to.original:a,u=w.arr(e)?e[0]:o,a=B(u)||B(a),r=r||a;return w.und(t)&&(t=o),n.from=z(u,r),n.to=z(E(t,u),r),n.start=s?s.end:0,n.end=n.start+n.delay+n.duration+n.endDelay,n.easing=P(n.easing,n.duration),n.isPath=w.pth(e),n.isPathTargetInsideSVG=n.isPath&&w.svg(c.target),n.isColor=w.col(n.from.original),n.isColor&&(n.round=1),s=n})}var U={css:function(n,e,t){return n.style[e]=t},attribute:function(n,e,t){return n.setAttribute(e,t)},object:function(n,e,t){return n[e]=t},transform:function(n,e,t,r,a){var o;r.list.set(e,t),e!==r.last&&!a||(o="",r.list.forEach(function(n,e){o+=e+"("+n+") "}),n.style.transform=o)}};function nn(n,u){_(n).forEach(function(n){for(var e in u){var t=m(u[e],n),r=n.target,a=B(t),o=T(r,e,a,n),t=E(Y(t,a||B(o)),o),a=b(r,e);U[a](r,e,t,n.transforms,!0)}})}function en(n,e){return I(f(n.map(function(o){return e.map(function(n){var e,t,r=o,a=b(r.target,n.name);if(a)return t=(e=K(n,r))[e.length-1],{type:a,property:n.name,animatable:r,tweens:e,duration:t.end,delay:e[0].delay,endDelay:t.endDelay}})})),function(n){return!w.und(n)})}function tn(n,e){function t(n){return n.timelineOffset||0}var r=n.length,a={};return a.duration=r?Math.max.apply(Math,n.map(function(n){return t(n)+n.duration})):e.duration,a.delay=r?Math.min.apply(Math,n.map(function(n){return t(n)+n.delay})):e.delay,a.endDelay=r?a.duration-Math.max.apply(Math,n.map(function(n){return t(n)+n.duration-n.endDelay})):e.endDelay,a}var rn=0;var N,S=[],an=("undefined"!=typeof document&&document.addEventListener("visibilitychange",function(){L.suspendWhenDocumentHidden&&(n()?N=cancelAnimationFrame(N):(S.forEach(function(n){return n._onDocumentVisibility()}),an()))}),function(){!(N||n()&&L.suspendWhenDocumentHidden)&&0<S.length&&(N=requestAnimationFrame(on))});function on(n){for(var e=S.length,t=0;t<e;){var r=S[t];r.paused?(S.splice(t,1),e--):(r.tick(n),t++)}N=0<t?requestAnimationFrame(on):void 0}function n(){return document&&document.hidden}function L(n){var c,s=0,f=0,l=0,d=0,p=null;function h(n){var e=window.Promise&&new Promise(function(n){return p=n});return n.finished=e}e=x(i,n=n=void 0===n?{}:n),t=J(r=x(M,n),n),n=_(n.targets),r=tn(t=en(n,t),r),a=rn,rn++;var e,t,r,a,k=D(e,{id:a,children:[],animatables:n,animations:t,duration:r.duration,delay:r.delay,endDelay:r.endDelay});h(k);function g(){var n=k.direction;"alternate"!==n&&(k.direction="normal"!==n?"normal":"reverse"),k.reversed=!k.reversed,c.forEach(function(n){return n.reversed=k.reversed})}function m(n){return k.reversed?k.duration-n:n}function o(){s=0,f=m(k.currentTime)*(1/L.speed)}function v(n,e){e&&e.seek(n-e.timelineOffset)}function y(e){for(var n=0,t=k.animations,r=t.length;n<r;){for(var a=t[n],o=a.animatable,u=a.tweens,i=u.length-1,c=u[i],i=(i&&(c=I(u,function(n){return e<n.end})[0]||c),C(e-c.start-c.delay,0,c.duration)/c.duration),s=isNaN(i)?1:c.easing(i),f=c.to.strings,l=c.round,d=[],p=c.to.numbers.length,h=void 0,g=0;g<p;g++){var m=void 0,v=c.to.numbers[g],y=c.from.numbers[g]||0,m=c.isPath?function(e,t,n){function r(n){return e.el.getPointAtLength(1<=t+(n=void 0===n?0:n)?t+n:0)}var a=Q(e.el,e.svg),o=r(),u=r(-1),i=r(1),c=n?1:a.w/a.vW,s=n?1:a.h/a.vH;switch(e.property){case"x":return(o.x-a.x)*c;case"y":return(o.y-a.y)*s;case"angle":return 180*Math.atan2(i.y-u.y,i.x-u.x)/Math.PI}}(c.value,s*v,c.isPathTargetInsideSVG):y+s*(v-y);!l||c.isColor&&2<g||(m=Math.round(m*l)/l),d.push(m)}var b=f.length;if(b)for(var h=f[0],M=0;M<b;M++){f[M];var x=f[M+1],w=d[M];isNaN(w)||(h+=x?w+x:w+" ")}else h=d[0];U[a.type](o.target,a.property,h,o.transforms),a.currentValue=h,n++}}function b(n){k[n]&&!k.passThrough&&k[n](k)}function u(n){var e=k.duration,t=k.delay,r=e-k.endDelay,a=m(n);if(k.progress=C(a/e*100,0,100),k.reversePlayback=a<k.currentTime,c){var o=a;if(k.reversePlayback)for(var u=d;u--;)v(o,c[u]);else for(var i=0;i<d;i++)v(o,c[i])}!k.began&&0<k.currentTime&&(k.began=!0,b("begin")),!k.loopBegan&&0<k.currentTime&&(k.loopBegan=!0,b("loopBegin")),a<=t&&0!==k.currentTime&&y(0),(r<=a&&k.currentTime!==e||!e)&&y(e),t<a&&a<r?(k.changeBegan||(k.changeBegan=!0,k.changeCompleted=!1,b("changeBegin")),b("change"),y(a)):k.changeBegan&&(k.changeCompleted=!0,k.changeBegan=!1,b("changeComplete")),k.currentTime=C(a,0,e),k.began&&b("update"),e<=n&&(f=0,k.remaining&&!0!==k.remaining&&k.remaining--,k.remaining?(s=l,b("loopComplete"),k.loopBegan=!1,"alternate"===k.direction&&g()):(k.paused=!0,k.completed||(k.completed=!0,b("loopComplete"),b("complete"),!k.passThrough&&"Promise"in window&&(p(),h(k)))))}return k.reset=function(){var n=k.direction;k.passThrough=!1,k.currentTime=0,k.progress=0,k.paused=!0,k.began=!1,k.loopBegan=!1,k.changeBegan=!1,k.completed=!1,k.changeCompleted=!1,k.reversePlayback=!1,k.reversed="reverse"===n,k.remaining=k.loop,c=k.children;for(var e=d=c.length;e--;)k.children[e].reset();(k.reversed&&!0!==k.loop||"alternate"===n&&1===k.loop)&&k.remaining++,y(k.reversed?k.duration:0)},k._onDocumentVisibility=o,k.set=function(n,e){return nn(n,e),k},k.tick=function(n){u(((l=n)+(f-(s=s||l)))*L.speed)},k.seek=function(n){u(m(n))},k.pause=function(){k.paused=!0,o()},k.play=function(){k.paused&&(k.completed&&k.reset(),k.paused=!1,S.push(k),o(),an())},k.reverse=function(){g(),k.completed=!k.reversed,o()},k.restart=function(){k.reset(),k.play()},k.remove=function(n){cn(A(n),k)},k.reset(),k.autoplay&&k.play(),k}function un(n,e){for(var t=e.length;t--;)h(n,e[t].animatable.target)&&e.splice(t,1)}function cn(n,e){var t=e.animations,r=e.children;un(n,t);for(var a=r.length;a--;){var o=r[a],u=o.animations;un(n,u),u.length||o.children.length||r.splice(a,1)}t.length||r.length||e.pause()}return L.version="3.2.1",L.speed=1,L.suspendWhenDocumentHidden=!0,L.running=S,L.remove=function(n){for(var e=A(n),t=S.length;t--;)cn(e,S[t])},L.get=T,L.set=nn,L.convertPx=y,L.path=function(n,e){var t=w.str(n)?a(n)[0]:n,r=e||100;return function(n){return{property:n,el:t,svg:Q(t),totalLength:G(t)*(r/100)}}},L.setDashoffset=function(n){var e=G(n);return n.setAttribute("stroke-dasharray",e),e},L.stagger=function(n,e){var i=(e=void 0===e?{}:e).direction||"normal",c=e.easing?P(e.easing):null,s=e.grid,f=e.axis,l=e.from||0,d="first"===l,p="center"===l,h="last"===l,g=w.arr(n),m=g?parseFloat(n[0]):parseFloat(n),v=g?parseFloat(n[1]):0,y=B(g?n[1]:n)||0,b=e.start||0+(g?m:0),M=[],x=0;return function(n,e,t){if(d&&(l=0),p&&(l=(t-1)/2),h&&(l=t-1),!M.length){for(var r,a,o,u=0;u<t;u++)s?(r=p?(s[0]-1)/2:l%s[0],a=p?(s[1]-1)/2:Math.floor(l/s[0]),r=r-u%s[0],a=a-Math.floor(u/s[0]),o=Math.sqrt(r*r+a*a),"x"===f&&(o=-r),M.push(o="y"===f?-a:o)):M.push(Math.abs(l-u)),x=Math.max.apply(Math,M);c&&(M=M.map(function(n){return c(n/x)*x})),"reverse"===i&&(M=M.map(function(n){return f?n<0?-1*n:-n:Math.abs(x-n)}))}return b+(g?(v-m)/x:m)*(Math.round(100*M[e])/100)+y}},L.timeline=function(u){var i=L(u=void 0===u?{}:u);return i.duration=0,i.add=function(n,e){var t=S.indexOf(i),r=i.children;function a(n){n.passThrough=!0}-1<t&&S.splice(t,1);for(var o=0;o<r.length;o++)a(r[o]);t=D(n,x(M,u)),t.targets=t.targets||u.targets,n=i.duration,t.autoplay=!1,t.direction=i.direction,t.timelineOffset=w.und(e)?n:E(e,n),a(i),i.seek(t.timelineOffset),e=L(t),a(e),r.push(e),n=tn(r,u);return i.delay=n.delay,i.endDelay=n.endDelay,i.duration=n.duration,i.seek(0),i.reset(),i.autoplay&&i.play(),i},i},L.easing=P,L.penner=s,L.random=function(n,e){return Math.floor(Math.random()*(e-n+1))+n},L});
    </script>
    <script>
    /* =========================================================
    文字沧海 —— 用文字组成大海:风吹过海面,掀起文字的浪,
    一叶文字小船乘风破浪。动画引擎:anime.js
    ========================================================= */
    const TAU = Math.PI * 2;
    const $ = s => document.querySelector(s);
    const rnd = (a,b) => a + Math.random()*(b-a);
    const clamp = (v,a,b) => v<a?a:(v>b?b:v);

    let W = innerWidth, H = innerHeight;
    let sF = clamp(W/1150, .68, 1.05); // 全局缩放(适配小屏)

    /* ---------- 风力档位 ---------- */
    const PRESETS = [
    { name:'微风', amp: 7, spd:.85, gust:.75, int:[6.5,11], storm:0 },
    { name:'清风', amp: 14, spd:1.1, gust:1.0, int:[4.5,8], storm:0 },
    { name:'狂风', amp: 26, spd:1.7, gust:1.5, int:[2.2,4.2], storm:1 },
    ];
    let presetIdx = 1;
    const dyn = { amp:PRESETS[1].amp, spd:PRESETS[1].spd, storm:0 }; // 由 anime 平滑过渡
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) PRESETS.forEach(p=>p.amp*=.55);

    /* ---------- 海面文字(诗句) ---------- */
    const PHRASES = [
    '长风破浪会有时直挂云帆济沧海',
    '春江潮水连海平海上明月共潮生',
    ];

    /* ---------- 海的构建 ---------- */
    const seaEl = $('#sea');
    const rows = [];
    let seaTop = 0, seaH = 0;

    function buildSea(){
    rows.forEach(r => r.el.remove());
    rows.length = 0;
    W = innerWidth; H = innerHeight;
    sF = clamp(W/1150, .68, 1.05);
    seaH = seaEl.clientHeight; seaTop = H - seaH;

    const N = 6;
    const tops = [.045,.14,.27,.42,.60,.80];
    const zIdx = [10,11,12,13,16,17]; // 船在 14,中景水花 15
    const colorsFix = ['#d9ecf5','#b7d7e7','#93c2da','#5b9ec6','#4a94c2','#3f8ab8'];
    for (let i=0;i<N;i++){
    const d = i/(N-1);
    const depth = .35 + .65*Math.pow(d,1.25); // 波幅深度系数
    const fs = Math.round((13 + 17*Math.pow(d,1.15)) * sF);
    const ls = Math.round(5*(1-d));
    const el = document.createElement('div');
    el.className = 'seaRow';
    el.style.cssText = `top:${tops[i]*seaH}px; font-size:${fs}px; letter-spacing:${ls}px; z-index:${zIdx[i]};`;
    seaEl.appendChild(el);

    const phrase = PHRASES[(i+1)%2];
    const charW = fs + ls;
    const count = Math.ceil(W/charW) + 2;
    const els=[], us=[], cols=[];
    const base = hexToHsl(colorsFix[i]);
    for (let c=0;c<count;c++){
    const sp = document.createElement('span');
    sp.textContent = phrase[(c + i*3) % phrase.length];
    const L = clamp(base.l + rnd(-6,6), 8, 96); // 每字亮度微抖 → 水光粼粼
    const col = `hsl(${base.h},${base.s}%,${L}%)`;
    sp.style.color = col;
    sp.dataset.op = (i<2? .85 : .95);
    el.appendChild(sp);
    els.push(sp); us.push((c*charW + charW*.5)/W); cols.push(col);
    }
    rows.push({ el, els, us, cols, d, depth, fs, charW,
    ph: i*1.7 + i*i*.31,
    y:new Float32Array(count), foam:new Uint8Array(count),
    rowTop: tops[i]*seaH });
    }
    }
    function hexToHsl(hex){
    const n = parseInt(hex.slice(1),16);
    let r=(n>>16&255)/255, g=(n>>8&255)/255, b=(n&255)/255;
    const mx=Math.max(r,g,b), mn=Math.min(r,g,b); let h,s,l=(mx+mn)/2;
    if(mx===mn){h=s=0;}else{
    const dd=mx-mn; s=l>.5?dd/(2-mx-mn):dd/(mx+mn);
    switch(mx){case r:h=(g-b)/dd+(g<b?6:0);break;case g:h=(b-r)/dd+2;break;default:h=(r-g)/dd+4;}
    h/=6;
    }
    return {h:Math.round(h*360), s:Math.round(s*100), l:Math.round(l*100)};
    }

    /* ---------- 阵风 ---------- */
    const gusts = [];
    function spawnGust(strength, fromU, fast){
    const g = { u: fromU!=null? fromU-.22 : -.28, pw:0, sig:rnd(.09,.14), str:strength };
    g.inv2 = 1/(2*g.sig*g.sig);
    gusts.push(g);
    const dur = (fast?1900:3000)/(0.75 + dyn.spd*.35) * rnd(.9,1.15);
    anime({ targets:g, u:1.3, duration:dur, easing:'easeInOutSine' });
    anime({ targets:g, keyframes:[
    { pw: strength*dyn.amp*1.25, duration:dur*.3, easing:'easeOutQuad' },
    { pw: strength*dyn.amp*.85, duration:dur*.32, easing:'linear' },
    { pw: 0, duration:dur*.38, easing:'easeInQuad' },
    ],
    complete:()=>{ const k=gusts.indexOf(g); if(k>-1) gusts.splice(k,1); } });
    windStreak(dur, fromU);
    return g;
    }

    /* 风带:一行「~~~风」贴着海面掠过 */
    function windStreak(dur, fromU){
    const n = dyn.storm>.5 ? 3 : 2;
    for(let i=0;i<n;i++){
    const el = document.createElement('div');
    el.className = 'streak';
    const fs = rnd(15,22)*sF;
    el.style.fontSize = fs+'px';
    /* 第一条贴近海平线(风扫过海面),其余在高空 */
    el.style.top = (i===0 ? rnd(.42,.53) : rnd(.12,.34))*H + 'px';
    el.innerHTML = `<span class="tail">${'~'.repeat(Math.round(rnd(8,14)))}</span><span class="head">风</span>`;
    $('#streaks').appendChild(el);
    const startX = (fromU!=null? fromU*W - W*.45 : -W*.42);
    anime({ targets:el,
    translateX:[startX, W*1.18], translateY:rnd(-26,26), rotate:rnd(-3,1),
    opacity:[{value:.9,duration:dur*.25},{value:.75,duration:dur*.4},{value:0,duration:dur*.35}],
    duration:dur*rnd(.95,1.1), delay:i*170, easing:'easeInOutSine',
    complete:()=>el.remove() });
    }
    }

    /* ---------- 粒子 ---------- */
    function particle(layer, txt, x, y, style){
    const sp = document.createElement('span');
    sp.className='pt'; sp.textContent=txt;
    sp.style.cssText = `left:${x}px; top:${y}px; ${style||''}`;
    layer.appendChild(sp);
    return sp;
    }
    const SPRAY_CH = ['°','∘','。','·','°','∘','沫','花'];
    function spray(x, y, n, big, layer){
    for(let i=0;i<n;i++){
    const ch = SPRAY_CH[Math.floor(Math.random()*SPRAY_CH.length)];
    const sp = particle(layer||$('#fxBack'), ch, x, y,
    `color:#eef8ff; font-size:${rnd(9,big?16:12)*sF}px; text-shadow:0 0 6px rgba(255,255,255,.7); opacity:0;`);
    const dx = rnd(-1,1)*(big?46:26)*sF, up = rnd(14, big?52:34)*sF;
    anime({ targets:sp,
    keyframes:[
    { translateY:-up, translateX:dx*.6, opacity:.95, duration:rnd(220,340), easing:'easeOutQuad' },
    { translateY:up*.7, translateX:dx, opacity:0, duration:rnd(380,520), easing:'easeInQuad' },
    ],
    rotate:rnd(-90,90),
    complete:()=>sp.remove() });
    }
    }

    /* ---------- 小船 ---------- */
    const boatEl = $('#boat'), boatInner = $('#boatInner');
    const boat = { u:.18, y:0, a:0, surge:0, lastSlam:0, h:112 };
    const BOAT_D = .72; // 船所在浪层深度
    function boatWaterline(){ return seaTop + rows[4].rowTop + rows[4].fs*.1; }

    let pennantAnim=null, sailFlutter=null;
    function boatIdleAnims(){
    if(pennantAnim) pennantAnim.pause();
    if(sailFlutter) sailFlutter.pause();
    const speed = 900 - dyn.amp*18;
    pennantAnim = anime({ targets:'#pennant',
    keyframes:[{skewY:-10,rotate:-6},{skewY:8,rotate:3},{skewY:-6,rotate:-2},{skewY:10,rotate:5}],
    duration:Math.max(380,speed), loop:true, easing:'easeInOutSine' });
    sailFlutter = anime({ targets:'#rig',
    rotate:[{value:-1.1},{value:1.1}], duration:Math.max(500,speed*1.4),
    direction:'alternate', loop:true, easing:'easeInOutSine' });
    }

    /* ---------- 波形函数 ---------- */
    function waveEnv(u){ // 阵风局部影响:附加波幅 + 风倾
    let gAmp=0;
    for(const g of gusts){
    const du=u-g.u, e=Math.exp(-du*du*g.inv2);
    gAmp += g.pw*e;
    }
    return {gAmp, lean:gAmp/22};
    }
    function waveY(u, t, ph, depth){
    const A = dyn.amp * depth * sF * (1 + .16*Math.sin(TAU*t/23)); // 海的呼吸
    const y1 = .58*Math.sin(TAU*1.35*u - TAU*.30*dyn.spd*t + ph);
    const y2 = .30*Math.sin(TAU*2.90*u - TAU*.52*dyn.spd*t + ph*1.7 + 1.3);
    const y3 = .18*Math.sin(TAU*5.80*u - TAU*.83*dyn.spd*t + ph*2.4);
    const env = waveEnv(u);
    const yg = env.gAmp * depth * Math.sin(TAU*6.2*u - TAU*.9*t);
    return { y: A*(y1+y2+y3) + yg,
    dx: A*.42*Math.cos(TAU*1.35*u - TAU*.30*dyn.spd*t + ph),
    A: A*1.06 + env.gAmp*depth,
    lean: env.lean };
    }

    /* ---------- 主渲染循环(由 anime 引擎驱动) ---------- */
    const FOAM_SHADOW = '0 0 7px rgba(255,255,255,.95)';
    let prevT = 0;
    function render(t){
    const dt = clamp(t - prevT, 0, .05); prevT = t;

    /* --- 海面文字 --- */
    for(const r of rows){
    const n = r.els.length;
    for(let i=0;i<n;i++){
    const w = waveY(r.us[i], t, r.ph, r.depth);
    r.y[i] = w.y;
    r._lean = w.lean; // 行内近似即可
    r._A = w.A;
    const el = r.els[i];
    const crest = -w.y / (w.A+3);
    const wasFoam = r.foam[i];
    if(!wasFoam && crest > .68){ r.foam[i]=1; el.style.color='#f4fbff'; el.style.textShadow=FOAM_SHADOW; }
    else if(wasFoam && crest < .54){ r.foam[i]=0; el.style.color=r.cols[i]; el.style.textShadow='none'; }
    el._dx = w.dx; el._lean = w.lean;
    }
    for(let i=0;i<n;i++){
    const el = r.els[i];
    const yl = r.y[i>0?i-1:i], yr = r.y[i<n-1?i+1:i];
    const rot = r.d>.5 ? clamp((yr-yl)/(2*r.charW)*34, -11, 11) : 0;
    const sk = clamp(-el._lean*13, -16, 16);
    el.style.transform =
    `translate3d(${el._dx.toFixed(1)}px,${r.y[i].toFixed(1)}px,0) rotate(${rot.toFixed(1)}deg) skewX(${sk.toFixed(1)}deg)${r.foam[i]?' scale(1.07)':''}`;
    }
    }

    /* --- 小船 --- */
    boat.u += dt*( .0135 + dyn.amp*.00042 + boat.surge );
    if(boat.u > 1.15) boat.u = -.15;
    const bw = waveY(boat.u, t, rows[3].ph, BOAT_D);
    const e = .012;
    const slope = (waveY(boat.u+e,t,rows[3].ph,BOAT_D).y - waveY(boat.u-e,t,rows[3].ph,BOAT_D).y) / (2*e*W);
    const targetY = bw.y, targetA = clamp(Math.atan(slope)*57.3*.85, -16, 16) + bw.lean*7;

    /* 阵风推船:涌浪 + 加速 */
    if(bw.lean > .25 && boat.surge < .004) {
    anime({ targets:boat, surge:.0085, duration:500, easing:'easeOutQuad',
    complete:()=>anime({targets:boat, surge:0, duration:1600, easing:'easeOutQuad'}) });
    }
    boat.y += (targetY - boat.y)*Math.min(1, dt*5.5);
    boat.a += (targetA - boat.a)*Math.min(1, dt*4.2);

    /* 拍浪水花 */
    if(targetY - boat.y > 9*sF && t-boat.lastSlam > 1.1){
    boat.lastSlam = t;
    spray(boat._x + rnd(30,90)*sF, boatWaterline()+boat.y, 5+Math.round(dyn.amp*.2), dyn.amp>18, $('#fxMid'));
    }

    const bx = boat.u*(W+260*sF) - 130*sF;
    boat._x = bx;
    boatEl.style.transform = `translate3d(${bx.toFixed(1)}px,${(boatWaterline()+boat.y-(boat.h-7)*sF).toFixed(1)}px,0) scale(${sF})`;
    boatInner.style.transform = `rotate(${boat.a.toFixed(1)}deg)`;
    $('#sail').style.transform = `skewX(${(-7 - dyn.amp*.28 - bw.lean*12).toFixed(1)}deg) scaleX(${(1+dyn.amp*.006).toFixed(3)})`;

    /* 尾迹 */
    if(t - (render._wk||0) > .22){
    render._wk = t;
    const wy = boatWaterline()+boat.y+rnd(-2,5);
    const sp = particle($('#fxBack'), ['∘','。','~','°'][Math.floor(rnd(0,4))], bx+rnd(4,22)*sF, wy,
    `color:#e6f4fc; font-size:${rnd(10,15)*sF}px; opacity:.9; text-shadow:0 0 4px rgba(255,255,255,.4);`);
    anime({ targets:sp, translateX:-rnd(30,80)*sF, translateY:rnd(0,8), opacity:0, scale:rnd(.9,1.5),
    duration:rnd(1000,1600), easing:'easeOutQuad', complete:()=>sp.remove() });
    }

    /* --- 风暴罩 --- */
    $('#veil').style.opacity = (dyn.storm*.55).toFixed(2);
    }

    /* anime 作为总时钟 */
    const clock = { t:0 };
    anime({ targets:clock, t:36000, duration:36000000, easing:'linear', loop:true,
    update: () => render(clock.t) });

    /* ---------- 自动阵风 ---------- */
    (function autoGust(){
    const p = PRESETS[presetIdx];
    setTimeout(()=>{ spawnGust(rnd(.7,1.15)*p.gust); autoGust(); }, rnd(p.int[0],p.int[1])*1000);
    })();

    /* ---------- 鱼跃 ---------- */
    (function fishLoop(first){
    setTimeout(()=>{
    if(dyn.storm < .4){
    const x0 = rnd(.15,.8)*W;
    const wl = seaTop + rows[4].rowTop + rows[4].fs*.2;
    const fish = particle($('#fxBack'), '鱼', x0, wl,
    `color:#9fc9de; font-size:${19*sF}px; text-shadow:0 1px 3px rgba(20,50,80,.4);`);
    spray(x0+6, wl+4, 4);
    anime({ targets:fish,
    translateX: 95*sF,
    keyframes:[
    { translateY:-78*sF, duration:430, easing:'easeOutQuad' },
    { translateY: 16*sF, duration:430, easing:'easeInQuad' },
    ],
    rotate:[-52, 78],
    duration:860, easing:'linear',
    complete:()=>{ spray(x0+95*sF, wl+6, 5); fish.remove(); } });
    }
    fishLoop(false);
    }, (first? rnd(5,8) : rnd(9,16))*1000);
    })(true);

    /* ---------- 海鸟 ---------- */
    let birdCount = 0;
    (function birdLoop(first){
    setTimeout(()=>{
    if(dyn.storm < .4 && birdCount < 2){
    birdCount++;
    const gEl = document.createElement('div');
    gEl.className='birdGroup';
    const n = Math.round(rnd(1,3));
    for(let i=0;i<n;i++){
    const b=document.createElement('i'); b.textContent='人';
    const fs = rnd(9,13)*sF;
    b.style.cssText=`left:${i*rnd(16,26)}px; top:${rnd(-8,8)}px; font-size:${fs}px;`;
    gEl.appendChild(b);
    anime({ targets:b, scaleY:[1,.3], duration:rnd(240,320), direction:'alternate',
    loop:true, easing:'easeInOutSine', delay:i*90 });
    }
    $('#birds').appendChild(gEl);
    const y0 = rnd(.08,.3)*H, ltr = Math.random()<.7;
    const dur = rnd(13000,19000);
    /* 单实例:X 匀速穿越,Y 分段起伏(滑翔) */
    const seg = 6, yKeys = [];
    let yy = y0;
    for(let k=0;k<seg;k++){ yy += rnd(-26,26); yKeys.push({ value:yy, duration:dur/seg, easing:'easeInOutSine' }); }
    gEl.style.transform = `translate(${ltr?-80:W+80}px, ${y0}px)`;
    anime({ targets:gEl,
    translateX: ltr? [-80, W+80] : [W+80, -80],
    translateY: yKeys,
    duration:dur, easing:'linear',
    complete:()=>{ gEl.remove(); birdCount--; } });
    }
    birdLoop(false);
    }, (first? rnd(2.5,4) : rnd(9,20))*1000);
    })(true);

    /* ---------- 暴风雨:文字雨 + 闪电 ---------- */
    let rainN = 0;
    setInterval(()=>{
    if(dyn.storm > .45 && rainN < 30){
    rainN++;
    const sp = particle($('#rainLayer'), '丶', rnd(-.25,1)*W, -20,
    `color:rgba(218,238,255,.85); font-size:${rnd(12,17)}px;`);
    anime({ targets:sp, translateY:H*.98, translateX:W*.22, opacity:[.85,.4],
    duration:rnd(520,720), easing:'linear', complete:()=>{ sp.remove(); rainN--; } });
    }
    }, 75);
    function bolt(){
    anime({ targets:'#flash', opacity:[0,.42,.06,.3,0], duration:640, easing:'linear' });
    }
    (function boltLoop(){
    setTimeout(()=>{ if(dyn.storm > .6) bolt(); boltLoop(); }, rnd(8,15)*1000);
    })();

    /* ---------- 太阳 / 云 ---------- */
    (function buildSun(){
    const ring = $('#sunRing'), R = 62;
    for(let i=0;i<10;i++){
    const s=document.createElement('span'); s.textContent='光';
    s.style.transform = `rotate(${i*36}deg) translateY(${-R}px) rotate(${-i*36}deg)`;
    ring.appendChild(s);
    }
    anime({ targets:ring, rotate:'1turn', duration:46000, easing:'linear', loop:true });
    anime({ targets:'#sunCore', scale:[1,1.06], duration:3800, direction:'alternate', loop:true, easing:'easeInOutSine' });
    })();

    function makeCloud(x, y, s){
    const c = document.createElement('div');
    c.className='cloud';
    c.style.cssText = `left:${x}px; top:${y}px; font-size:${Math.round(17*s*sF)}px;`;
    const pos = [[0,10],[14,4],[30,8],[46,3],[62,9],[76,13],[8,-6],[24,-10],[40,-12],[56,-7],[16,18],[34,21],[52,19],[68,15],[44,-2]];
    pos.forEach(p=>{
    const sp=document.createElement('span');
    sp.textContent='云';
    sp.style.cssText=`left:${p[0]*s*sF}px; top:${p[1]*s*sF}px; opacity:${rnd(.45,.9)};`;
    c.appendChild(sp);
    });
    document.body.appendChild(c);
    (function drift(first){
    const startX = first? 0 : -(90*s*sF + 120);
    const dist = W + 240;
    anime({ targets:c, translateX:[startX, dist], duration:(dist-startX)*rnd(700,950)/10,
    easing:'linear', complete:()=>drift(false) });
    })(true);
    return c;
    }

    /* ---------- 交互 ---------- */
    document.addEventListener('pointerdown', ev=>{
    if(ev.target.closest('#ui')) return;
    if(ev.clientY < H*.42) return;
    const u = ev.clientX/W;
    spawnGust(rnd(.9,1.25)*PRESETS[presetIdx].gust, u, true);
    /* 点击处「风」字绽开 */
    const sp = particle($('#fxFront'), '风', ev.clientX-10, ev.clientY-12,
    `color:rgba(255,255,255,.95); font-size:20px; text-shadow:0 0 10px rgba(255,255,255,.9);`);
    anime({ targets:sp, scale:[.6,2.4], opacity:[.95,0], rotate:15, duration:900,
    easing:'easeOutQuad', complete:()=>sp.remove() });
    spray(ev.clientX, ev.clientY, 6, false, $('#fxFront'));
    anime({ targets:'#hint', opacity:.35, duration:800, easing:'linear' });
    });

    document.querySelectorAll('.chip').forEach(btn=>{
    btn.addEventListener('click', ()=>{
    document.querySelectorAll('.chip').forEach(b=>b.classList.remove('on'));
    btn.classList.add('on');
    presetIdx = +btn.dataset.w;
    const p = PRESETS[presetIdx];
    anime({ targets:dyn, amp:p.amp, spd:p.spd, storm:p.storm,
    duration:2000, easing:'easeInOutQuad', complete:boatIdleAnims });
    document.body.classList.toggle('storm', p.storm>0);
    if(p.storm){ spawnGust(1.4); setTimeout(()=>{ if(dyn.storm>.3) bolt(); }, rnd(1800,3200)); }
    });
    });

    /* ---------- 启动 ---------- */
    function intro(){
    /* 海字逐浪浮现 */
    rows.forEach((r,ri)=>{
    anime({ targets:r.els, opacity: el=>+el.dataset.op,
    duration:900, delay:anime.stagger(6,{start:ri*130}), easing:'easeOutCubic' });
    });
    /* 标题落墨 */
    const tm = $('#titleMain');
    '文字沧海'.split('').forEach(ch=>{ const s=document.createElement('span'); s.textContent=ch; tm.appendChild(s); });
    anime({ targets:'#titleMain span', opacity:[0,1], translateY:[-14,0],
    delay:anime.stagger(160,{start:250}), duration:700, easing:'easeOutCubic' });
    anime({ targets:'#seal', opacity:[0,1], scale:[.3,1], delay:1100, duration:600, easing:'easeOutBack' });
    anime({ targets:'#titleSub', opacity:[0,.9], delay:1300, duration:900, easing:'linear' });
    anime({ targets:'#boat', opacity:[0,1], duration:1400, delay:300, easing:'linear' });
    anime({ targets:'#hint', opacity:[0,.9], delay:2200, duration:1200, easing:'linear' });
    setTimeout(()=>spawnGust(1.0), 1500);
    }

    let glintX;
    function layout(){
    glintX = W*.89 - W*.15;
    $('#glint').style.left = (W*.89 - W*.15)+'px';
    }

    buildSea(); layout(); boatIdleAnims();
    makeCloud(W*.16, H*.10, 1); makeCloud(W*.42, H*.17, .62); makeCloud(W*.58, H*.24, .8); makeCloud(W*.8, H*.06, 1.15);
    intro();
    setTimeout(()=>{ boat.h = boatEl.offsetHeight || 110; }, 400);

    let rsT;
    addEventListener('resize', ()=>{
    clearTimeout(rsT);
    rsT = setTimeout(()=>{ buildSea(); layout();
    boat.h = boatEl.offsetHeight || 110;
    rows.forEach(r=>r.els.forEach(el=>el.style.opacity=el.dataset.op)); }, 350);
    });
    </script>
    </body>
    </html>
  • starpipi 07-29 15:44
    4

    kimi-k3的感觉还行,居然做了个鱼跳出来的效果


    <!DOCTYPE html>
    <html lang="zh">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文字之海 · Text Sea</title>
    <style>
    html, body {
    margin: 0; height: 100%; overflow: hidden;
    background: linear-gradient(#04091a 0%, #0a1c38 48%, #07101f 100%);
    font-family: "Songti SC", "STSong", "Noto Serif SC", serif;
    cursor: default;
    }
    #sky { position: absolute; inset: 0; }
    .star { position: absolute; color: #9db8e8; opacity: .4; font-size: 12px; user-select: none; }
    #moon {
    position: absolute; top: 7%; right: 9%; font-size: 46px; color: #ffe9b8;
    text-shadow: 0 0 26px rgba(255,233,184,.85), 0 0 90px rgba(255,233,184,.35);
    user-select: none;
    }
    .cloud { position: absolute; color: rgba(190,215,255,.30); font-size: 22px; letter-spacing: 4px; user-select: none; }
    .streak { position: absolute; color: rgba(170,215,255,0); font-size: 16px; user-select: none; }
    #sea { position: absolute; left: 0; right: 0; bottom: 0; }
    .sea-char {
    position: absolute; width: 16px; text-align: center; font-size: 15px;
    color: #2e7fb8; user-select: none; will-change: transform;
    }
    #boat { position: absolute; left: 0; top: 0; will-change: transform; user-select: none; }
    .boat-char { position: absolute; width: 16px; text-align: center; font-size: 15px; line-height: 18px; }
    #fish {
    position: absolute; font-size: 15px; color: #8fe3ff; opacity: 0;
    user-select: none; text-shadow: 0 0 8px rgba(143,227,255,.6); white-space: nowrap;
    }
    .splash { position: absolute; font-size: 13px; color: #cfeaff; user-select: none; }
    #title {
    position: absolute; top: 6%; left: 5%; writing-mode: vertical-rl;
    color: rgba(215,232,255,.85); font-size: 20px; letter-spacing: 8px;
    text-shadow: 0 0 14px rgba(120,180,255,.35); user-select: none; opacity: 0;
    }
    #hint {
    position: absolute; bottom: 12px; right: 16px; color: rgba(160,190,230,.35);
    font-size: 12px; letter-spacing: 2px; user-select: none;
    }
    </style>
    </head>
    <body>
    <div id="sky"></div>
    <div id="moon">月</div>
    <div id="sea"></div>
    <div id="boat"></div>
    <div id="fish">&gt;&lt;(((°&gt;</div>
    <div id="title">風起時 · 文字成海</div>
    <div id="hint">点击海面 唤起一阵风</div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
    <script>
    (() => {
    const W = () => window.innerWidth;
    const H = () => window.innerHeight;
    const CW = 16, RH = 18; // 字符格子尺寸
    const SEA_CHARS = ['~','~','~','≈','∽','~','~'];
    const seaEl = document.getElementById('sea');
    const boatEl = document.getElementById('boat');
    const fishEl = document.getElementById('fish');
    const skyEl = document.getElementById('sky');

    const seaTop = () => H() * 0.55;

    // ---------- 海面:字符网格 ----------
    let cols = 0, rows = 0, cells = [];
    const SURFACE_TONES = ['#2e7fb8','#3f97cf','#5cb2e2','#86cdf2','#c9ecff'];
    const NEAR_TONES = ['#1f6395','#2a74ab','#3786c1','#4a9bd4','#66b3e6'];

    function buildSea() {
    seaEl.innerHTML = '';
    cells = [];
    cols = Math.ceil(W() / CW) + 1;
    rows = Math.floor(H() * 0.46 / RH);
    seaEl.style.top = seaTop() + 'px';
    const frag = document.createDocumentFragment();
    for (let r = 0; r < rows; r++) {
    const depthAtt = Math.pow(0.86, r); // 越深越平静
    const baseColor = r === 0 ? SURFACE_TONES[0]
    : r < 4 ? NEAR_TONES[0]
    : `rgba(27,90,138,${Math.max(0.12, 0.9 - r * 0.07)})`;
    for (let c = 0; c < cols; c++) {
    const el = document.createElement('span');
    el.className = 'sea-char';
    el.textContent = SEA_CHARS[(Math.random() * SEA_CHARS.length) | 0];
    el.style.left = c * CW + 'px';
    el.style.top = r * RH + 'px';
    el.style.color = baseColor;
    frag.appendChild(el);
    cells.push({ el, x: c * CW, row: r, depthAtt, lastLevel: 0 });
    }
    }
    seaEl.appendChild(frag);
    }

    // ---------- 风与浪 ----------
    const wind = { strength: 1 }; // 全局风力(anime 驱动)
    const gusts = []; // 阵风(移动的浪峰包络)

    function waveAt(x, t) {
    const s = wind.strength;
    let y = Math.sin(x * 0.012 + t * 1.15) * 9 * s
    + Math.sin(x * 0.024 - t * 1.8 + 2.0) * 5.5 * s
    + Math.sin(x * 0.045 + t * 2.9 + 5.2) * 2.5 * s;
    for (const g of gusts) {
    const d = (x - g.x) / g.w;
    if (d > -3 && d < 3) y += Math.exp(-d * d) * g.amp * Math.sin(x * 0.05 - t * 5 + g.phase);
    }
    return y;
    }

    // 全局风:呼吸般强弱变化
    function breeze() {
    anime({
    targets: wind,
    strength: 0.6 + Math.random() * 1.15,
    duration: 3000 + Math.random() * 3500,
    easing: 'easeInOutSine',
    complete: breeze
    });
    }

    // 阵风:一个高斯包络从左向右扫过海面
    function spawnGust(fromClick) {
    const g = {
    x: -300,
    w: 120 + Math.random() * 170,
    amp: (6 + Math.random() * 9) * (fromClick ? 1.5 : 1),
    phase: Math.random() * 6
    };
    gusts.push(g);
    anime({
    targets: g,
    x: W() + 300,
    duration: 3200 + Math.random() * 2600,
    easing: 'linear',
    complete() {
    gusts.splice(gusts.indexOf(g), 1);
    if (!fromClick) setTimeout(() => spawnGust(false), 900 + Math.random() * 3200);
    }
    });
    windStreaks();
    }

    // 风的痕迹:空中掠过的「〜」
    const streaks = [];
    function windStreaks() {
    for (let i = 0; i < 3; i++) {
    const el = streaks[(Math.random() * streaks.length) | 0];
    if (!el) return;
    el.style.top = (H() * (0.12 + Math.random() * 0.3)) + 'px';
    el.style.left = '0px';
    anime.remove(el);
    anime({
    targets: el,
    translateX: [-120, W() + 120],
    opacity: [{ value: 0.45, duration: 500 }, { value: 0, duration: 1600 }],
    delay: i * 260 + Math.random() * 300,
    duration: 2600 + Math.random() * 1200,
    easing: 'linear'
    });
    }
    }

    // ---------- 小船 ----------
    const ART = [
    " 丨☆ ",
    " 丨帆 ",
    " 丨帆帆 ",
    " 丨帆帆帆 ",
    " 丨帆帆帆帆",
    "\船船船船船船/"
    ];
    const CHAR_STYLE = {
    '帆': { color: '#f3ede0', glow: 'rgba(243,237,224,.35)' },
    '丨': { color: '#b08954', glow: 'rgba(176,137,84,.3)' },
    '船': { color: '#d29a63', glow: 'rgba(210,154,99,.4)' },
    '\': { color: '#d29a63', glow: 'rgba(210,154,99,.4)' },
    '/': { color: '#d29a63', glow: 'rgba(210,154,99,.4)' },
    '☆': { color: '#ff6b6b', glow: 'rgba(255,107,107,.7)' }
    };
    const BOAT_W = 10 * CW, BOAT_H = ART.length * RH;
    const boat = { x: -300, rot: 0 };

    function buildBoat() {
    boatEl.innerHTML = '';
    ART.forEach((line, r) => {
    [...line].forEach((ch, c) => {
    if (ch === ' ') return;
    const st = CHAR_STYLE[ch];
    const el = document.createElement('span');
    el.className = 'boat-char';
    el.textContent = ch;
    el.style.left = c * CW + 'px';
    el.style.top = r * RH + 'px';
    el.style.color = st.color;
    el.style.textShadow = `0 0 8px ${st.glow}`;
    boatEl.appendChild(el);
    });
    });
    }

    // ---------- 船尾航迹 ----------
    const wake = []; // {x, life}
    let wakeTick = 0;

    // ---------- 主循环 ----------
    let last = performance.now();
    const t0 = last;

    function frame(now) {
    const t = (now - t0) / 1000;
    const dt = Math.min(0.05, (now - last) / 1000);
    last = now;

    // 船前行(顺风更快)
    boat.x += 26 * (0.6 + wind.strength * 0.45) * dt;
    if (boat.x > W() + 120) boat.x = -BOAT_W - 160;
    const cx = boat.x + BOAT_W / 2;
    const by = waveAt(cx, t);
    const slope = (waveAt(cx + 10, t) - waveAt(cx - 10, t)) / 20;
    const targetRot = Math.atan(slope) * 0.9 + Math.sin(t * 2.1) * 0.02;
    boat.rot += (targetRot - boat.rot) * 0.08; // 惯性:转向略滞后,更真实
    boatEl.style.transform =
    `translate(${boat.x}px, ${seaTop() + by - BOAT_H + 10}px) rotate(${boat.rot}rad)`;

    // 记录航迹
    if (++wakeTick % 6 === 0) wake.push({ x: cx - BOAT_W / 2, life: 1 });
    for (let i = wake.length - 1; i >= 0; i--) {
    wake[i].life -= dt * 0.55;
    if (wake[i].life <= 0) wake.splice(i, 1);
    }

    // 海面字符
    for (const c of cells) {
    const wy = waveAt(c.x, t - c.row * 0.10) * c.depthAtt;
    const drift = Math.sin(t * 0.5 + c.row * 0.7 + c.x * 0.002) * c.row * 0.3;
    c.el.style.transform = `translate(${drift.toFixed(1)}px, ${wy.toFixed(1)}px)`;

    if (c.row < 4) { // 表层:浪峰变亮 + 航迹泛白
    let boost = Math.max(0, -wy / (10 * wind.strength + 5));
    if (c.row < 2) {
    for (const p of wake) {
    const d = (c.x - p.x) / 34;
    if (d > -3 && d < 3) boost += Math.exp(-d * d) * p.life * 0.9;
    }
    }
    const level = Math.min(4, (boost * 5) | 0);
    if (level !== c.lastLevel) {
    c.lastLevel = level;
    c.el.style.color = (c.row === 0 ? SURFACE_TONES : NEAR_TONES)[level];
    }
    }
    }
    requestAnimationFrame(frame);
    }

    // ---------- 鱼跃 ----------
    function splash(x, y) {
    for (let i = 0; i < 4; i++) {
    const s = document.createElement('span');
    s.className = 'splash';
    s.textContent = ['。','˚','・','。'][i];
    s.style.left = x + 'px';
    s.style.top = y + 'px';
    document.body.appendChild(s);
    anime({
    targets: s,
    translateX: (Math.random() - 0.5) * 70,
    translateY: -20 - Math.random() * 30,
    opacity: [1, 0],
    duration: 700 + Math.random() * 400,
    easing: 'easeOutQuad',
    complete: () => s.remove()
    });
    }
    }

    function fishJump() {
    const x = 120 + Math.random() * (W() - 320);
    const y = seaTop() + waveAt(x, (performance.now() - t0) / 1000) - 6;
    fishEl.style.left = x + 'px';
    fishEl.style.top = y + 'px';
    splash(x, y);
    anime.remove(fishEl);
    anime({
    targets: fishEl,
    opacity: [{ value: 1, duration: 150 }, { value: 1, duration: 900 }, { value: 0, duration: 250 }],
    translateX: [{ value: 60, duration: 550, easing: 'easeOutQuad' }, { value: 140, duration: 750, easing: 'easeInQuad' }],
    translateY: [{ value: -95, duration: 550, easing: 'easeOutQuad' }, { value: 14, duration: 750, easing: 'easeInQuad' }],
    rotate: [{ value: -25, duration: 400 }, { value: 40, duration: 900 }],
    duration: 1300,
    complete() {
    splash(x + 140, y + 14);
    setTimeout(fishJump, 6000 + Math.random() * 9000);
    }
    });
    }

    // ---------- 天空 ----------
    function buildSky() {
    skyEl.innerHTML = '';
    const starChars = ['·','+','˚','∙'];
    for (let i = 0; i < 45; i++) {
    const s = document.createElement('span');
    s.className = 'star';
    s.textContent = starChars[(Math.random() * starChars.length) | 0];
    s.style.left = Math.random() * 100 + '%';
    s.style.top = Math.random() * seaTop() * 0.75 + 'px';
    skyEl.appendChild(s);
    anime({
    targets: s,
    opacity: [0.1, 0.85],
    direction: 'alternate',
    loop: true,
    duration: 1200 + Math.random() * 2200,
    delay: Math.random() * 2000,
    easing: 'easeInOutSine'
    });
    }
    for (let i = 0; i < 4; i++) {
    const c = document.createElement('span');
    c.className = 'cloud';
    c.textContent = ['云 云','云','云 云 云','云 云'][i];
    c.style.top = (H() * (0.08 + i * 0.09)) + 'px';
    skyEl.appendChild(c);
    anime({
    targets: c,
    translateX: [-220, W() + 220],
    duration: 70000 + Math.random() * 60000,
    easing: 'linear',
    loop: true
    });
    }
    for (let i = 0; i < 6; i++) {
    const st = document.createElement('span');
    st.className = 'streak';
    st.textContent = '〜';
    skyEl.appendChild(st);
    streaks.push(st);
    }
    anime({ targets: '#moon', scale: [1, 1.05], direction: 'alternate', loop: true, duration: 4000, easing: 'easeInOutSine' });
    anime({ targets: '#title', opacity: [0, 1], translateY: [-12, 0], duration: 2400, easing: 'easeOutQuad', delay: 600 });
    }

    // ---------- 交互:点击唤风 ----------
    window.addEventListener('pointerdown', () => spawnGust(true));

    let resizeTimer;
    window.addEventListener('resize', () => {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(() => { buildSea(); buildSky(); }, 200);
    });

    // ---------- 启动 ----------
    buildSea();
    buildBoat();
    buildSky();
    breeze();
    setTimeout(() => spawnGust(false), 1200);
    setTimeout(fishJump, 4000);
    requestAnimationFrame(frame);
    })();
    </script>
    </body>
    </html>

  • Haskell 07-29 15:45
    5

    楼上的大伙,放个效果图吧


    懒得下本地看了哈哈

    @here

  • scp3500 07-29 16:03
    6
    <!DOCTYPE html>
    <html lang="zh">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文字之海 · 风吹过文字的海</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
    <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; overflow: hidden; background: #04101f; }
    body {
    font-family: "SimSun", "NSimSun", "Courier New", monospace;
    background: linear-gradient(to bottom,
    #02060f 0%, #061428 30%, #0a2440 48%,
    #0d3054 55%, #082647 100%);
    user-select: none;
    cursor: default;
    }

    /* ---------- 天空 ---------- */
    #sky { position: absolute; inset: 0; pointer-events: none; }
    .star { position: absolute; color: #cfe4ff; font-size: 12px; opacity: .6; }
    #moon {
    position: absolute; top: 7%; right: 10%;
    font-size: 44px; color: #f5e9c8;
    text-shadow: 0 0 18px rgba(245,233,200,.85), 0 0 46px rgba(245,233,200,.45);
    }
    .cloud { position: absolute; color: rgba(190,215,240,.28); font-size: 20px; letter-spacing: 2px; white-space: nowrap; }
    .gull { position: absolute; color: rgba(220,235,250,.75); font-size: 16px; }

    /* ---------- 标题 ---------- */
    #title {
    position: absolute; top: 10%; width: 100%; text-align: center;
    color: #dceeff; font-size: 30px; letter-spacing: 14px;
    text-shadow: 0 0 14px rgba(120,190,255,.6);
    pointer-events: none; z-index: 5;
    }
    #title span { display: inline-block; opacity: 0; }
    #subtitle {
    position: absolute; top: calc(10% + 46px); width: 100%; text-align: center;
    color: rgba(170,205,240,.55); font-size: 13px; letter-spacing: 6px;
    pointer-events: none; z-index: 5; opacity: 0;
    }

    /* ---------- 风力计 ---------- */
    #windvane {
    position: absolute; top: 20px; left: 24px; z-index: 6;
    color: rgba(190,220,250,.8); font-size: 14px; letter-spacing: 2px;
    }
    #windArrows { color: #9fd4ff; }

    /* ---------- 风痕 ---------- */
    .streak {
    position: absolute; color: rgba(200,230,255,.0); font-size: 15px;
    white-space: nowrap; pointer-events: none; z-index: 3;
    }

    /* ---------- 文字海洋 ---------- */
    #ocean { position: absolute; inset: 0; overflow: hidden; }
    .cell { position: absolute; left: 0; top: 0; will-change: transform; font-size: 18px; }

    /* ---------- 小船 ---------- */
    #boat {
    position: absolute; left: 0; top: 0; z-index: 4;
    font-size: 22px; line-height: 1.06;
    transform-origin: 50% 100%;
    will-change: transform;
    filter: drop-shadow(0 4px 8px rgba(0,0,0,.5));
    }
    #boat pre { font-family: inherit; }
    .c-mast { color: #d8c49a; }
    .c-sail { color: #eef6ff; text-shadow: 0 0 8px rgba(220,240,255,.45); }
    .c-hull { color: #c98d4e; text-shadow: 0 0 6px rgba(160,100,40,.6); }
    .c-keel { color: #8a5a2e; }

    /* ---------- 浪花 / 鱼 ---------- */
    .splash { position: absolute; color: #dff2ff; font-size: 14px; z-index: 4; pointer-events: none; }
    .fish { position: absolute; color: #ffd88a; font-size: 15px; z-index: 4; pointer-events: none; white-space: nowrap; }
    </style>
    </head>
    <body>

    <div id="sky">
    <div id="moon">月</div>
    </div>

    <div id="title"></div>
    <div id="subtitle">风 吹 过 文 字 的 海</div>

    <div id="windvane">风力 <span id="windArrows">→</span></div>

    <div id="ocean"></div>

    <div id="boat">
    <pre id="boatArt"><span class="c-mast"> 丨</span>
    <span class="c-sail"> 帆帆丨</span>
    <span class="c-sail"> 帆帆帆帆丨</span>
    <span class="c-sail">帆帆帆帆帆帆丨</span>
    <span class="c-hull">舟舟舟舟舟舟舟</span>
    <span class="c-keel">  ̄ ̄ ̄ ̄ ̄</span></pre>
    </div>

    <script>
    /* =========================================================
    文字之海 —— 风、浪与一只文字小船
    海浪场:多组正弦波叠加 + 阵风行波(anime.js 驱动风力起伏)
    ========================================================= */

    const W = () => window.innerWidth;
    const H = () => window.innerHeight;

    const ocean = document.getElementById('ocean');
    const boat = document.getElementById('boat');

    /* ---------- 全局风(anime.js tween 的目标对象) ---------- */
    const wind = { amp: 6, phase: 0, dir: 1 }; // amp: 风力(浪高加成) phase: 阵风行波相位

    function gustLoop() {
    anime({
    targets: wind,
    amp: anime.random(2, 26),
    duration: anime.random(2200, 5200),
    easing: 'easeInOutSine',
    complete: gustLoop
    });
    }
    gustLoop();

    /* ---------- 波浪函数:基础浪 + 阵风推起来的行波 ---------- */
    function waveY(x, t) {
    const base = 13 * Math.sin(x * 0.011 + t * 0.9)
    + 8 * Math.sin(x * 0.023 - t * 1.35)
    + 4 * Math.sin(x * 0.047 + t * 2.1);
    const gust = wind.amp * Math.sin(x * 0.028 - wind.phase);
    return base + gust;
    }
    const MAX_AMP = 13 + 8 + 4 + 26; // 用于浪尖归一化

    /* ---------- 字符池 ---------- */
    const CHARS = {
    foam: ['≋','浪','°','˚','≈','花','沫'],
    body: ['~','≈','〜','波','水','∽','浪'],
    deep: ['~','·','。','水','流','丶','~']
    };
    const pick = arr => arr[(Math.random() * arr.length) | 0];

    /* ---------- 构建海洋网格 ---------- */
    const SP_X = 22, ROW_GAP = 27, ROWS = 10, LAG = 0.14;
    let cells = [], oceanTop = 0;

    function buildOcean() {
    ocean.innerHTML = '';
    cells = [];
    oceanTop = H() * 0.48;
    const cols = Math.ceil(W() / SP_X) + 2;
    for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < cols; c++) {
    const s = document.createElement('span');
    s.className = 'cell';
    const cell = {
    el: s, x: c * SP_X, row: r,
    state: '', // 'foam' | 'body' | 'deep'
    ch: r < 3 ? pick(CHARS.body) : pick(CHARS.deep)
    };
    s.textContent = cell.ch;
    ocean.appendChild(s);
    cells.push(cell);
    }
    }
    }

    function setCellState(cell, state) {
    if (cell.state === state) return;
    cell.state = state;
    cell.ch = pick(CHARS[state]);
    cell.el.textContent = cell.ch;
    }

    /* ---------- 主循环 ---------- */
    let t = 0, last = performance.now();
    let boatX = -200;
    const BOAT_W = 7 * 22, BOAT_H = 6 * 23.5;

    function tick(now) {
    const dt = Math.min(0.05, (now - last) / 1000);
    last = now;
    t += dt;
    wind.phase += dt * (1.0 + wind.amp * 0.16) * wind.dir;

    /* --- 海面每个字符的位置与形态 --- */
    for (let i = 0; i < cells.length; i++) {
    const cell = cells[i];
    const r = cell.row;
    // 深层海水:相位滞后 + 振幅衰减,制造"涌"的层次
    const v = waveY(cell.x, t - r * LAG) * (1 - r * 0.055);
    const y = oceanTop + r * ROW_GAP + v;
    cell.el.style.transform = `translate3d(${cell.x}px, ${y}px, 0)`;

    const crest = -v / MAX_AMP; // 越靠近波峰越大
    if (r <= 1 && crest > 0.52) {
    setCellState(cell, 'foam');
    const L = 78 + crest * 20;
    cell.el.style.color = `hsl(200, 85%, ${L}%)`;
    } else if (r < 4) {
    setCellState(cell, 'body');
    const L = 46 + crest * 26 - r * 3;
    cell.el.style.color = `hsl(${208 + crest * 12}, 78%, ${L}%)`;
    } else {
    setCellState(cell, 'deep');
    const L = 34 - r * 1.6 + crest * 12;
    cell.el.style.color = `hsl(212, 70%, ${L}%)`;
    }
    }

    /* --- 小船:随浪起伏、按坡度倾斜、被风推着走 --- */
    boatX += dt * (14 + wind.amp * 2.4) * wind.dir;
    if (boatX > W() + 60) boatX = -BOAT_W - 60;

    const cx = boatX + BOAT_W / 2;
    const wv = waveY(cx, t);
    const slope = (waveY(cx + 34, t) - waveY(cx - 34, t)) / 68;
    let rot = Math.atan(slope) * 57.3 * 0.85;
    rot = Math.max(-16, Math.min(16, rot));

    boat.style.transform =
    `translate3d(${boatX}px, ${oceanTop + wv - BOAT_H + 14}px, 0) rotate(${rot}deg)`;

    /* --- 船头浪花:浪陡或风大时喷溅字符 --- */
    const steep = Math.abs(slope);
    if ((steep > 0.35 || wind.amp > 17) && Math.random() < 0.3) spawnSplash(boatX + BOAT_W - 8, oceanTop + wv + 2);

    /* --- 风痕 --- */
    updateStreaks(dt);

    /* --- 风力计 --- */
    windArrowsEl.textContent = '→'.repeat(1 + Math.round(wind.amp / 5));

    requestAnimationFrame(tick);
    }

    /* ---------- 船头浪花 ---------- */
    function spawnSplash(x, y) {
    const s = document.createElement('span');
    s.className = 'splash';
    s.textContent = pick(['·','˚','°','。','′','水']);
    s.style.left = x + 'px';
    s.style.top = y + 'px';
    document.body.appendChild(s);
    anime({
    targets: s,
    translateX: anime.random(6, 30),
    translateY: [-anime.random(14, 42), anime.random(8, 20)],
    opacity: [0.95, 0],
    duration: anime.random(500, 900),
    easing: 'easeOutQuad',
    complete: () => s.remove()
    });
    }

    /* ---------- 风痕:阵风大时可见的气流线 ---------- */
    const streaks = [];
    function buildStreaks() {
    for (let i = 0; i < 14; i++) {
    const s = document.createElement('span');
    s.className = 'streak';
    s.textContent = ['〜〜〜','———','~~~~'][(Math.random()*3)|0];
    s._x = Math.random() * W();
    s._y = H() * 0.15 + Math.random() * H() * 0.5;
    s._speed = 1 + Math.random();
    document.body.appendChild(s);
    streaks.push(s);
    }
    }
    function updateStreaks(dt) {
    const vis = Math.max(0, (wind.amp - 10) / 18); // 风力 >10 才开始显形
    for (const s of streaks) {
    s._x += dt * (60 + wind.amp * 26) * s._speed * wind.dir;
    if (s._x > W() + 80) { s._x = -80; s._y = H() * 0.12 + Math.random() * H() * 0.55; }
    s.style.transform = `translate3d(${s._x}px, ${s._y}px, 0)`;
    s.style.color = `rgba(200,230,255,${(vis * 0.5 * s._speed / 2).toFixed(3)})`;
    }
    }

    /* ---------- 跳鱼 ---------- */
    function jumpFish() {
    const f = document.createElement('span');
    f.className = 'fish';
    f.textContent = pick(['><(((°>', '><> ', '鱼', '><(((°>']);
    const x = 100 + Math.random() * (W() - 200);
    f.style.left = x + 'px';
    f.style.top = (oceanTop + waveY(x, t) - 6) + 'px';
    document.body.appendChild(f);
    anime({
    targets: f,
    translateY: [{ value: -anime.random(45, 80), duration: 480, easing: 'easeOutQuad' },
    { value: 12, duration: 560, easing: 'easeInQuad' }],
    rotate: [{ value: anime.random(-40, 40), duration: 1040 }],
    opacity: [{ value: 1, duration: 300 }, { value: 0, duration: 740 }],
    complete: () => f.remove()
    });
    setTimeout(jumpFish, anime.random(3000, 9000));
    }

    /* ---------- 天空:星星、云、海鸥 ---------- */
    function buildSky() {
    const sky = document.getElementById('sky');
    for (let i = 0; i < 42; i++) {
    const s = document.createElement('span');
    s.className = 'star';
    s.textContent = pick(['·', '˙', '*', '∘', '.']);
    s.style.left = Math.random() * 100 + '%';
    s.style.top = Math.random() * 34 + '%';
    sky.appendChild(s);
    anime({
    targets: s,
    opacity: [0.15, 0.85],
    duration: anime.random(1200, 3200),
    direction: 'alternate',
    loop: true,
    easing: 'easeInOutSine',
    delay: anime.random(0, 2000)
    });
    }
    // 云
    for (let i = 0; i < 3; i++) {
    const c = document.createElement('div');
    c.className = 'cloud';
    c.textContent = pick(['云 云 云', '云 云 云 云', '云 云']);
    c.style.top = (8 + i * 9 + Math.random() * 4) + '%';
    c.style.left = '-30%';
    sky.appendChild(c);
    anime({
    targets: c,
    left: '110%',
    duration: anime.random(90000, 160000),
    loop: true,
    easing: 'linear',
    delay: -anime.random(0, 90000)
    });
    }
    // 海鸥
    for (let i = 0; i < 3; i++) {
    const g = document.createElement('span');
    g.className = 'gull';
    g.textContent = pick(['︵', '⌒', '︿']);
    g.style.top = (18 + Math.random() * 16) + '%';
    sky.appendChild(g);
    anime({
    targets: g,
    left: ['-5%', '105%'],
    translateY: [{ value: -14, duration: 900, direction: 'alternate', loop: 999, easing: 'easeInOutSine' }],
    duration: anime.random(26000, 42000),
    loop: true,
    easing: 'linear',
    delay: -anime.random(0, 20000)
    });
    }
    // 月亮呼吸
    anime({
    targets: '#moon',
    textShadow: [
    '0 0 18px rgba(245,233,200,.85), 0 0 46px rgba(245,233,200,.45)',
    '0 0 26px rgba(245,233,200,.95), 0 0 70px rgba(245,233,200,.6)'
    ],
    direction: 'alternate', loop: true, duration: 3600, easing: 'easeInOutSine'
    });
    }

    /* ---------- 标题入场 ---------- */
    function introTitle() {
    const title = document.getElementById('title');
    '文字之海'.split('').forEach(ch => {
    const s = document.createElement('span');
    s.textContent = ch;
    title.appendChild(s);
    });
    anime({
    targets: '#title span',
    opacity: [0, 1],
    translateY: [-46, 0],
    rotate: [() => anime.random(-30, 30), 0],
    delay: anime.stagger(140),
    duration: 1400,
    easing: 'easeOutElastic(1, .6)'
    });
    anime({
    targets: '#subtitle',
    opacity: [0, 1],
    translateY: [14, 0],
    delay: 1200,
    duration: 1600,
    easing: 'easeOutQuad'
    });
    // 标题随后缓慢呼吸,不抢戏
    anime({
    targets: '#title',
    opacity: [1, 0.55],
    delay: 4200,
    duration: 3000,
    direction: 'alternate',
    loop: true,
    easing: 'easeInOutSine'
    });
    }

    /* ---------- 船帆抖动(被风吹得扑簌簌) ---------- */
    function sailFlutter() {
    anime({
    targets: '#boatArt .c-sail',
    skewX: [0, () => anime.random(-6, 6)],
    duration: 700,
    direction: 'alternate',
    loop: true,
    easing: 'easeInOutSine',
    delay: anime.stagger(90)
    });
    }

    /* ---------- 自适应 ---------- */
    let resizeTimer;
    window.addEventListener('resize', () => {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(buildOcean, 200);
    });

    /* ---------- 启动 ---------- */
    const windArrowsEl = document.getElementById('windArrows');
    buildOcean();
    buildStreaks();
    buildSky();
    introTitle();
    sailFlutter();
    setTimeout(jumpFish, 2500);
    requestAnimationFrame(tick);
    </script>
    </body>
    </html>

    k3

* 帖子来源Linux.do
返回