首页 profile 底部的海面由 JS 逐帧驱动:requestAnimationFrame 每帧重建 SVG 路径的 d 属性,波峰随相位真实流动,不是 CSS 平移的假动画。记录实现参数和两个坑。

波浪合成

海面三层(远/中/近),每层的波形是双频率正弦叠加:

shape=0.8sin(tspeed+ik1+phase)+0.2sin ⁣(tspeedk2k1+ik2+phase2)\text{shape} = 0.8 \sin(t \cdot \text{speed} + i \cdot k_1 + \text{phase}) + 0.2 \sin\!\left(t \cdot \text{speed} \cdot \frac{k_2}{k_1} + i \cdot k_2 + \text{phase}_2\right)
  • k1 是主波空间频率(每段弧度),speed 是时间频率,相位速度 = speed / k1(段/秒)
  • 次频是空间频率 k2,约主波的 1/3,用来打破”一模一样”的波浪
  • 次频的时间系数必须是 speed * (k2 / k1):让次波和主波同速前进,波形作为整体平移。如果随手写 speed * 1.23,次波会在主波上快速滑过,看起来像高频抖动

三层参数刻意拉开,避免”几乎同频”的复制感:

baseampspeed (rad/s)k1波长
18080.30.20~898 单位
160150.850.34~528 单位
126302.40.70~257 单位

大浪(swell)

每层有独立的涨落周期:远 14s、中 11s、近 8.5s,幅度 5/8/11。最初三层共用同一个 10s 大浪,但远中层的自身波幅只有 5-9 单位,被 7 单位幅度的共享大浪盖过——三层看起来就是跟着同一个节奏上下抖,于是改成各自独立。

主题色与波峰亮线

水色跟随每款主题的 --link

css
.home-island {
color: color-mix(in srgb, var(--link) 30%, transparent);
}
[data-scheme="dark"] .home-island {
color: color-mix(in srgb, var(--link) 38%, transparent);
}

近层浪尖有一条 1.2px 亮线,跟随波浪:

css
.home-island path.home-island-foam {
fill: none; /* 必须提优先级 */
stroke: color-mix(in srgb, var(--link) 48%, transparent);
stroke-width: 1.2px;
vector-effect: non-scaling-stroke;
}

两个注意点:SVG 被 preserveAspectRatio="none" 拉伸,描边要加 vector-effect: non-scaling-stroke 才不会变形;.home-island path { fill: url(...) } 会覆盖 fill="none" 属性,选择器要提到 path.home-island-foam 才不把开放折线填充成三角色块。

坑:视图切换后动画失效

首页 → 文章 → 返回首页,海面不动了。原因是 astro:before-swap 由 Astro 以 bubbles: false 派发在 document 上,监听 window 永远收不到。循环没被取消、初始化状态没被重置,返回后 astro:page-load 想重新初始化又被状态挡住。改成挂 document 即可,和项目里其他脚本一致。

另外初始化加了守卫,astro:page-load 和模块立即执行可能双触发,只初始化一次;inited 要在所有前置检查通过后才置位,否则视图切换时脚本先于 DOM 换入执行,找不到目标元素就提前返回,状态却已锁死。

减动

prefers-reduced-motion: reduce 下不跑循环,只渲染一帧静态波形。所有动效都有这个兜底。