如何使用CSS和D3实现小鱼游动的交互动画
在网页中实现生动的交互动画,常需要将样式控制与数据驱动能力结合。CSS负责流畅的视觉表现,D3.js则提供灵活的数学计算与事件响应。本文将演示如何利用这两者,创建一条在鼠标控制下自由游动的小鱼动画。
核心思路与原理
动画的本质是不断更新元素的位置与形态。在这套方案中,CSS用于定义小鱼的身体、鱼鳍、尾巴等部位的静态样式以及基础的摆动过渡效果。D3.js则承担计算角色,监听鼠标移动事件,实时计算鱼头朝向与身体各部位的偏移量,并通过修改CSS变量或直接更新DOM属性的方式来驱动动画。
关键点在于将小鱼的游动分解为两个层次:
- 轨迹控制:鱼头跟随鼠标指针移动,身体其他部分按照固定比例延迟跟随,形成波浪式游动效果。
- 形态变化:鱼鳍和尾巴通过CSS关键帧动画进行周期性的摆动,模拟真实鱼类的游泳姿态。
第一步:构建HTML结构
我们需要一个容器来承载小鱼,并在内部划分出鱼头、鱼身、鱼尾和鱼鳍等部分。每个部分使用独立的<div>或<svg>元素,便于CSS单独控制样式和D3单独操作属性。
<div id="aquarium">
<div id="fish">
<div class="fish-body">
<div class="head"></div>
<div class="fin top-fin"></div>
<div class="fin bottom-fin"></div>
</div>
<div class="tail"></div>
</div>
</div>为了更精细地控制形状,我们可以在鱼身内部使用内联SVG绘制轮廓,或者直接使用CSS的border-radius组合出鱼形。这里采用纯CSS绘制,以降低依赖。
第二步:使用CSS绘制小鱼样式
鱼身的椭圆形状、鱼头的圆形、鱼鳍的三角形以及尾巴的分叉,都可以通过CSS实现。同时我们利用CSS自定义属性(CSS变量)来存储位置与角度数据,便于D3在运行时动态修改。
#aquarium {
width: 100vw;
height: 100vh;
background: linear-gradient(180deg, #0a2a4a, #0e3a5c);
position: relative;
overflow: hidden;
cursor: none;
}
#fish {
position: absolute;
width: 120px;
height: 60px;
top: var(--fish-y, 50%);
left: var(--fish-x, 50%);
transform: translate(-50%, -50%) rotate(var(--fish-angle, 0deg));
transition: transform 0.15s ease-out;
}
.fish-body {
width: 80px;
height: 40px;
background: radial-gradient(ellipse at 30% 50%, #ffaa44, #cc7722);
border-radius: 50%;
position: relative;
margin-left: 20px;
}
.head {
width: 22px;
height: 22px;
background: #ffcc66;
border-radius: 50%;
position: absolute;
top: 9px;
left: -6px;
box-shadow: 2px 0 4px rgba(0,0,0,0.3);
}
.fin {
width: 20px;
height: 12px;
border-radius: 50% 50% 0 0;
position: absolute;
animation: finWave 0.6s ease-in-out infinite alternate;
}
.top-fin {
background: #ee8822;
top: -10px;
left: 30px;
transform-origin: bottom center;
}
.bottom-fin {
background: #dd7711;
bottom: -8px;
left: 26px;
transform-origin: top center;
}
.tail {
width: 30px;
height: 18px;
background: #cc7722;
border-radius: 0 80% 80% 0 / 0 60% 60% 0;
position: absolute;
right: -18px;
top: 11px;
animation: tailWave 0.4s ease-in-out infinite alternate;
transform-origin: left center;
}
@keyframes finWave {
0% { transform: rotate(-15deg) scaleY(1); }
100% { transform: rotate(15deg) scaleY(0.7); }
}
@keyframes tailWave {
0% { transform: rotate(-10deg); }
100% { transform: rotate(10deg); }
}以上CSS定义了小鱼的基本形态和鱼鳍、尾巴的摆动动画。鱼的整体位置由--fish-x和--fish-y两个CSS变量控制,朝向由--fish-angle变量控制,这些变量将由D3在运行时更新。
第三步:使用D3.js实现交互逻辑
D3负责监听鼠标移动事件,计算鱼头与鼠标之间的夹角,并更新CSS变量。为了让游动更自然,我们加入了平滑跟随算法:鱼不会瞬间跳到鼠标位置,而是以一定的速度逐渐接近。
// 选择容器和小鱼元素
const aquarium = document.getElementById('aquarium');
const fish = document.getElementById('fish');
// 初始化鱼的位置(屏幕中央)
let currentX = window.innerWidth / 2;
let currentY = window.innerHeight / 2;
// 目标位置(鼠标位置)
let targetX = currentX;
let targetY = currentY;
// 跟随系数(0~1之间,越大跟随越快)
const followFactor = 0.08;
// 监听鼠标移动
aquarium.addEventListener('mousemove', function(event) {
targetX = event.clientX;
targetY = event.clientY;
});
// 更新函数,由D3的timer驱动
function updateFish() {
// 计算当前位置与目标位置的差值
const dx = targetX - currentX;
const dy = targetY - currentY;
// 如果距离很小,直接跳到目标点避免抖动
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 1) {
currentX = targetX;
currentY = targetY;
} else {
// 渐进式靠近
currentX += dx * followFactor;
currentY += dy * followFactor;
}
// 计算朝向角度(弧度转角度)
// 注意atan2返回的是弧度,需要转为度,并且CSS的rotate角度以顺时针为正
let angle = Math.atan2(dy, dx) * (180 / Math.PI);
// 更新CSS变量
fish.style.setProperty('--fish-x', currentX + 'px');
fish.style.setProperty('--fish-y', currentY + 'px');
fish.style.setProperty('--fish-angle', angle + 'deg');
// 继续下一帧
requestAnimationFrame(updateFish);
}
// 启动动画循环
updateFish();上面的代码使用原生requestAnimationFrame驱动动画循环,让鱼平滑地跟随鼠标移动,并始终面向鼠标方向。为了更丰富地使用D3特性,我们可以加入比例尺来映射鼠标位置与游动速度之间的关系。
第四步:引入D3的比例尺与插值增强效果
使用D3的scaleLinear创建比例尺,将鼠标距离中心的偏移映射为游动速度,从而影响鱼鳍摆动的频率。同时利用d3.interpolate实现更平滑的颜色或形状过渡。
// 使用D3比例尺将鼠标距离映射为摆动速度倍数
const speedScale = d3.scaleLinear()
.domain([0, 400]) // 鼠标距离范围(像素)
.range([1, 2.5]); // 速度倍数范围
// 监听鼠标移动并更新目标位置
aquarium.addEventListener('mousemove', function(event) {
targetX = event.clientX;
targetY = event.clientY;
// 计算鼠标与屏幕中心的距离
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const dist = Math.sqrt(
Math.pow(event.clientX - centerX, 2) +
Math.pow(event.clientY - centerY, 2)
);
// 根据距离调整鱼鳍摆动动画的速度
const speedFactor = speedScale(dist);
const fins = document.querySelectorAll('.fin');
fins.forEach(function(fin) {
fin.style.animationDuration = (0.6 / speedFactor) + 's';
});
const tail = document.querySelector('.tail');
tail.style.animationDuration = (0.4 / speedFactor) + 's';
});
// 其余更新逻辑与上文相同这样一来,当鼠标离鱼越远,鱼鳍和尾巴摆动越快,模拟出鱼加速游动的视觉效果。
第五步:增加水体背景与气泡粒子
为了让场景更生动,可以使用D3在背景中生成随机气泡粒子,缓慢上升,增加沉浸感。
// 生成气泡粒子
const bubbleCount = 30;
const aquariumEl = d3.select('#aquarium');
for (let i = 0; i < bubbleCount; i++) {
aquariumEl.append('div')
.attr('class', 'bubble')
.style('left', Math.random() * 100 + '%')
.style('bottom', '-20px')
.style('width', (8 + Math.random() * 16) + 'px')
.style('height', function() {
return this.style.width;
})
.style('animation-duration', (4 + Math.random() * 6) + 's')
.style('animation-delay', (Math.random() * 8) + 's');
}.bubble {
position: absolute;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.3);
animation: bubbleRise linear infinite;
pointer-events: none;
}
@keyframes bubbleRise {
0% {
transform: translateY(0) scale(0.8);
opacity: 0.3;
}
50% {
opacity: 0.6;
}
100% {
transform: translateY(-110vh) scale(1.1);
opacity: 0;
}
}效果预览与调试建议
将以上HTML、CSS和JavaScript代码整合到一个文件中,用浏览器打开即可看到一条金黄色的小鱼在深蓝色水族箱中跟随鼠标游动。鼠标移动越快,鱼鳍摆动越急促,尾巴摇摆越频繁,整体游动姿态十分自然。
调试时需要注意以下几点:
- 确保CSS变量在D3更新前已正确定义初始值,避免鱼出现位置异常。
- 跟随系数followFactor建议在0.05到0.15之间调整,数值越大鱼跟随越紧,但会显得生硬。
- 鱼鳍和尾巴的动画时间不宜过短或过长,0.4~0.8秒是一个合理的范围。
结语
通过CSS与D3的配合,我们实现了从视觉呈现到交互逻辑的完整闭环。CSS负责精致的外观与基础动效,D3则承担数据处理与事件驱动的任务,两者相辅相成。这套模式可以扩展至更多生物模拟、游戏角色控制等场景,只需替换对应的样式与运动算法即可。