实现一个超萌的柯基交互输入框

0 阅读6分钟

说在前面

看腻了千篇一律的常规输入框,是时候给输入框加点趣味性了,今天我们来实现一个会和用户互动的柯基输入框:鼠标移动时柯基会扭头盯着你看,点击页面会眨眼,输入文字时还会弹动身体、弹出气泡反馈。

  • 视线跟随:鼠标在页面移动,柯基的瞳孔会精准跟着鼠标转,聚焦输入框时还会盯着输入框看;
  • 趣味眨眼:点击页面任意位置,柯基会俏皮眨眼,动画超自然;
  • 打字反馈:在输入框打字时,柯基会轻轻弹动身体,还会根据输入字数弹出不同的气泡文案(比如 “在写啥呢...”“写了好多呀~”);
  • 3D 视角:鼠标移动时柯基整体会有轻微 3D 旋转,视觉上更立体生动。

在线体验

在线预览

jyeontu.xyz/htmlDemo/柯基…

codePen

codepen.io/yongtaozhen…

码上掘金

code.juejin.cn/pen/7608923…

关键代码实现

1.用 SVG 画一只的柯基

前面有一篇文章介绍了怎么将图片转为 SVG,这里可以直接在找一张柯基的图片来将其转为 SVG,转换工具地址如下:

jyeontu.xyz/htmlDemo/图片…

对转换工具实现感兴趣的同学可以查看这篇文章:mp.weixin.qq.com/s/c6qVOu_hT…

2.CSS 实现基础动画

转换成 SVG 之后我们需要将柯基的眼睛单独设为带 ID 的元素,方便做眨眼动画

  • 眨眼动画:用@keyframes做垂直缩放(scaleY),模拟闭眼效果;
@keyframes wink-animation {
  0%, 100% { transform: scaleY(1); } /* 正常状态 */
  50% { transform: scaleY(0.2); } /* 闭眼状态:垂直缩放到20% */
}
.wink { animation: wink-animation 0.3s ease-in-out; }
  • 打字弹动:给柯基容器加弹跳动画,输入文字时触发,增强互动感;
@keyframes bounce-typing {
  0%, 100% { transform: translateY(0) scale(1); }
  30% { transform: translateY(-6px) scale(1.05); } /* 向上弹+轻微放大 */
  60% { transform: translateY(2px) scale(0.98); } /* 轻微回落+缩小 */
}
.dog-container.bounce { animation: bounce-typing 0.4s ease-out; }
  • 气泡提示:用绝对定位做气泡样式,通过opacity控制显隐,加小三角伪元素模拟气泡尾巴。
.thought-bubble {
  position: absolute;
  opacity: 0; /* 默认隐藏 */
  transform: translate(-50%, -100%) scale(0.8); /* 初始缩小+位移 */
  transition: opacity 0.2s, transform 0.2s; /* 过渡动画 */
}
.thought-bubble.show {
  opacity: 1; /* 显示 */
  transform: translate(-50%, -100%) scale(1); /* 恢复正常大小 */
}
.thought-bubble::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -8px;
  margin-left: -6px;
  border: 6px solid transparent;
  border-top-color: #fff; /* 气泡尾巴的核心:透明边框+顶部白色 */
  border-bottom: none;
}

3.JS 实现交互逻辑

(1)3D 视角旋转(柯基扭头)

// 计算鼠标偏离页面中心的比例,转换为旋转角度
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
// 旋转角度:鼠标在中心上方→rotateX为正(柯基低头),右侧→rotateY为正(柯基左转)
const rotateX = ((mouseY - centerY) / centerY) * -10;
const rotateY = ((mouseX - centerX) / centerX) * 10;
// 应用3D旋转
dogContainer.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;

(2)确定瞳孔的目标位置

// 确定瞳孔要看向的目标点
let pointInSvg;
if (lookAtInput) {
  // 输入框聚焦时,看向预设的输入框位置
  pointInSvg = lookAtInputPoint;
} else {
  // 否则,把鼠标的屏幕坐标转换为SVG内部坐标
  const svgPoint = dogSvg.createSVGPoint();
  svgPoint.x = mouseX;
  svgPoint.y = mouseY;
  // 坐标转换:屏幕坐标 → SVG内部坐标(解决SVG嵌套/缩放导致的错位)
  pointInSvg = svgPoint.matrixTransform(dogSvg.getScreenCTM().inverse());
}

(3)计算瞳孔的目标偏移(以左眼为例)

// 计算左眼瞳孔的目标位置
// 目标点与眼球中心的偏移量
const deltaLeftX = pointInSvg.x - leftEyeCenter.x;
const deltaLeftY = pointInSvg.y - leftEyeCenter.y;
// 计算角度(Math.atan2:根据y/x算弧度,范围-π到π)
const angleLeft = Math.atan2(deltaLeftY, deltaLeftX);
// 计算距离(勾股定理)
const distanceLeft = Math.sqrt(deltaLeftX ** 2 + deltaLeftY ** 2);
// 目标位置:眼球中心 + 沿角度方向的偏移(不超过maxRadius)
const targetLeftX = leftEyeCenter.x + Math.cos(angleLeft) * Math.min(distanceLeft, maxRadius);
const targetLeftY = leftEyeCenter.y + Math.sin(angleLeft) * Math.min(distanceLeft, maxRadius);
  • Math.atan2(dy, dx) :核心函数,返回从 x 轴到点 (dx, dy) 的弧度,用于确定瞳孔的移动方向;
  • Math.min(distanceLeft, maxRadius) :限制偏移距离,避免瞳孔移出眼眶;
  • Math.cos(angle)/Math.sin(angle) :将弧度转换为 x/y 方向的偏移量。

(4)缓动更新瞳孔位置

// 缓动更新瞳孔位置(避免瞬间移动,更自然)
currentPupilPos.left.x += (targetLeftX - currentPupilPos.left.x) * smoothingFactor;
currentPupilPos.left.y += (targetLeftY - currentPupilPos.left.y) * smoothingFactor;
// 右眼同理(代码略)

// 应用位置到SVG元素
leftPupil.setAttribute("transform", `translate(${currentPupilPos.left.x}, ${currentPupilPos.left.y})`);
rightPupil.setAttribute("transform", `translate(${currentPupilPos.right.x}, ${currentPupilPos.right.y})`);

// 循环执行动画(requestAnimationFrame:浏览器刷新频率同步,流畅不卡顿)
requestAnimationFrame(animate);
}
// 启动动画循环
animate();
  • 缓动公式:当前位置 += (目标位置 - 当前位置) × 平滑因子

    每次只移动 “剩余距离的一小部分”,比如剩余 10px,平滑因子 0.08 就移动 0.8px,距离越近移动越慢,最终无限接近目标位置,视觉上就是 “顺滑跟随”;

  • requestAnimationFrame:替代setInterval,让动画和浏览器刷新频率(60 帧 / 秒)同步,避免卡顿。

(5)输入框交互

监听输入框的 focus/blur/input 事件,实现 “聚焦看输入框、打字弹动 + 气泡提示”:

// 聚焦输入框:让柯基看向输入框
inputEl.addEventListener("focus", () => {
  lookAtInput = true;
});
// 失焦:恢复看鼠标,隐藏气泡
inputEl.addEventListener("blur", () => {
  lookAtInput = false;
  thoughtBubble.classList.remove("show");
  thoughtBubble.textContent = "";
});
// 输入文字:弹动+气泡提示
inputEl.addEventListener("input", () => {
  const len = inputEl.value.length;
  if (len > 0) {
    // 根据字数切换文案
    thoughtBubble.textContent = len >= 10 ? "写了好多呀~" : len >= 5 ? "汪!写得好~" : "在写啥呢...";
    thoughtBubble.classList.add("show");
    // 气泡1.8秒后自动隐藏
    clearTimeout(bubbleHideTimer);
    bubbleHideTimer = setTimeout(() => {
      thoughtBubble.classList.remove("show");
    }, 1800);
  } else {
    thoughtBubble.classList.remove("show");
  }
  // 弹动动画(防抖:避免快速输入时重复触发)
  clearTimeout(bounceDebounce);
  dogContainer.classList.add("bounce");
  bounceDebounce = setTimeout(() => {
    dogContainer.classList.remove("bounce");
  }, 420); // 动画时长400ms,留20ms余量
});

(6)点击眨眼

window.addEventListener("click", () => {
  // 避免重复触发(比如快速点击)
  if (leftEyeGroup.classList.contains("wink")) return;
  // 添加眨眼类
  leftEyeGroup.classList.add("wink");
  rightEyeGroup.classList.add("wink");
  // 300ms后移除(和动画时长一致)
  setTimeout(() => {
    leftEyeGroup.classList.remove("wink");
    rightEyeGroup.classList.remove("wink");
  }, 300);
});

源码地址

gitee

gitee.com/zheng_yongt…

github

github.com/yongtaozhen…


  • 🌟 觉得有帮助的可以点个 star~
  • 🖊 有什么问题或错误可以指出,欢迎 pr~
  • 📬 有什么想要实现的功能或想法可以联系我~

公众号

关注公众号『 前端也能这么有趣 』,获取更多有趣内容~

发送 加群 还能加入前端交流群,和大家一起讨论技术、分享经验,偶尔也能摸鱼聊天~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。