当前位置: 首页 > news >正文

水墨风鼠标效果实现

文章目录

      • 从 0 到 1:实现一个顺滑的“墨水圈”光标特效
      • 页面骨架与资源引入
      • 样式与动画:墨圈如何“活起来”
      • 核心 JS:在鼠标处“滴墨”
        • 1) 容器与事件委托
        • 2) 鼠标移动:轻墨圈(批次抽样控制密度)
        • 3) 鼠标按下:浓墨滴
      • 性能与体验优化要点
      • 可配置维度(快速调出不同风格)
      • Vue 版本(结尾附上代码)

从 0 到 1:实现一个顺滑的“墨水圈”光标特效

这篇文章带你基于现有 index.htmlinkCursor.js(另附 Vue 版 App.vue)实现一个轻量的“墨水圈”效果:鼠标移动时淡淡的墨圈扩散,按下鼠标时更浓、更大的墨滴涟漪。
示例

  • 核心思路:用 JS 在鼠标位置动态插入绝对定位的 div,用 CSS @keyframes 做扩散与淡出动画,动画结束后自动移除节点。
  • 两类波纹:移动时的轻微墨圈(ink),按下时的浓重墨滴(inkDrop)。
  • 性能要点:基于“抽样”技术降低插入频率,避免频繁 DOM 操作导致掉帧。

HTML文件(index.html)

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Ink Cursor Demo</title><style>/* Container to capture events and position ink circles */.container {position: relative;height: 100vh;/* cursor: none; */}.ink {position: absolute;pointer-events: none;z-index: 9999;opacity: 0.7;animation: growInk 2500ms;}.inkDrop{position: absolute;pointer-events: none;z-index: 9999;opacity: 0.7;animation: growInkDrop 3000ms;}@keyframes growInk {0% {transform: scale(1);opacity: 1;}100% {transform: scale(3);opacity: 0;}}@keyframes growInkDrop{0% {transform: scale(1);opacity: 1;}100% {transform: scale(6);opacity: 0;}}</style>
</head>
<body><div id="ink-container" class="container"></div><script type="module" src="./inkCursor.js"></script>
</body>
</html>

JS文件(inkCursor.js)

let inkId = 0;const container = document.getElementById('ink-container');
const targetRoot = container ?? document.body;function createInkCircle(x, y) {const batch = 7;inkId++;if (inkId % batch !== 0) return;const initialSize = 7 + Math.random() * 5;const duration = 2400;const el = document.createElement('div');el.className = 'ink';Object.assign(el.style, {left: `${x - initialSize / 2}px`,top: `${y - initialSize / 2}px`,width: `${initialSize}px`,height: `${initialSize}px`,borderRadius: '50%',position: 'absolute',backgroundColor: 'rgba(0, 0, 0, 0.4)'});targetRoot.appendChild(el);setTimeout(() => el.remove(), duration);
}function createInkCircleDown(x, y) {const initialSize = 20 + Math.random() * 10;const duration = 1500;const el = document.createElement('div');el.className = 'inkDrop';Object.assign(el.style, {left: `${x - initialSize / 2}px`,top: `${y - initialSize / 2}px`,width: `${initialSize}px`,height: `${initialSize}px`,borderRadius: '50%',position: 'absolute',backgroundColor: 'rgba(0, 0, 0, 0.8)'});targetRoot.appendChild(el);setTimeout(() => el.remove(), duration);
}function handleMouseMove(event) {createInkCircle(event.clientX, event.clientY);
}function handleMouseDown(event) {createInkCircleDown(event.clientX, event.clientY);
}function init() {const listenTarget = container ?? window;listenTarget.addEventListener('mousemove', handleMouseMove, { passive: true });listenTarget.addEventListener('mousedown', handleMouseDown, { passive: true });
}init();

将两个文件复制并放置在同一级文件下后,便可以打开html查看效果(确保js文件命名正确)


页面骨架与资源引入

容器负责承载墨水圈,与脚本模块化引入:

<body><div id="ink-container" class="container"></div><script type="module" src="./inkCursor.js"></script>
</body>
  • 容器#ink-container 作为波纹的定位上下文(position: relative),也方便只在特定区域显示效果。
  • 模块化type="module" 让我们可以使用现代 JS 书写方式。

样式与动画:墨圈如何“活起来”

两类波纹共性:绝对定位、不可交互(pointer-events: none)、高层级、透明度动画。差异:扩散倍数和持续时间。

.ink {position: absolute;pointer-events: none;z-index: 9999;opacity: 0.7;animation: growInk 2500ms;
}.inkDrop{position: absolute;pointer-events: none;z-index: 9999;opacity: 0.7;animation: growInkDrop 3000ms;
}

动画曲线:从 1 倍缩放到更大,同时透明度从 1 衰减到 0。

@keyframes growInk {0% {transform: scale(1);opacity: 1;}100% {transform: scale(3);opacity: 0;}
}@keyframes growInkDrop{0% {transform: scale(1);opacity: 1;}100% {transform: scale(6);opacity: 0;}
}
  • 设计建议
    • 氛围感scale(3)scale(6) 搭配不同颜色透明度(见下文 JS)拉开层次。
    • 性能:尽量用 transformopacity 做动画,避免引发重排。

核心 JS:在鼠标处“滴墨”

1) 容器与事件委托
let inkId = 0;const container = document.getElementById('ink-container');
const targetRoot = container ?? document.body;
  • 定位上下文:优先挂到 #ink-container,否则退回 document.body
  • 标识符:通过自增 inkId 给每个墨圈唯一 id(便于移除/调试)。

事件绑定与初始化

function init() {const listenTarget = container ?? window;listenTarget.addEventListener('mousemove', handleMouseMove, { passive: true });listenTarget.addEventListener('mousedown', handleMouseDown, { passive: true });
}init();
  • passive: true:避免滚动阻塞等潜在性能问题。
  • listenTarget:优先监听容器,必要时监听 window 覆盖全局。
2) 鼠标移动:轻墨圈(批次抽样控制密度)
function createInkCircle(x, y) {const batch = 7;inkId++;if (inkId % batch !== 0) return;const initialSize = 7 + Math.random() * 5;const duration = 2400;const el = document.createElement('div');el.className = 'ink';Object.assign(el.style, {left: `${x - initialSize / 2}px`,top: `${y - initialSize / 2}px`,width: `${initialSize}px`,height: `${initialSize}px`,borderRadius: '50%',position: 'absolute',backgroundColor: 'rgba(0, 0, 0, 0.4)'});targetRoot.appendChild(el);setTimeout(() => el.remove(), duration);
}
  • 抽样控制inkId % 7 仅让每 7 次移动生成一次节点,显著减少 DOM 插入次数。
  • 随机尺寸7~12px 的初始尺寸 + 动画扩散,视觉更自然。
  • 定时清理:与 CSS 动画时长匹配,自动回收节点。

触发器:

function handleMouseMove(event) {createInkCircle(event.clientX, event.clientY);
}function handleMouseDown(event) {createInkCircleDown(event.clientX, event.clientY);
}
3) 鼠标按下:浓墨滴
function createInkCircleDown(x, y) {const initialSize = 20 + Math.random() * 10;const duration = 1500;const el = document.createElement('div');el.className = 'inkDrop';Object.assign(el.style, {left: `${x - initialSize / 2}px`,top: `${y - initialSize / 2}px`,width: `${initialSize}px`,height: `${initialSize}px`,borderRadius: '50%',position: 'absolute',backgroundColor: 'rgba(0, 0, 0, 0.8)'});targetRoot.appendChild(el);setTimeout(() => el.remove(), duration);
}
  • 视觉对比:更大初始尺寸、更高不透明、扩散更剧烈(CSS scale(6))→ 有“按压反馈”的质感。

性能与体验优化要点

  • 抽样节流:移动事件高频触发,基于批次抽样是足够稳定好用的策略;也可切换到 requestAnimationFrame 结合“上一帧是否已生成”标记进一步平滑。
  • 动画时长对齐setTimeout 的时长需与 CSS 动画时长一致(或略长 50ms),避免提前删除或残留。
  • 定位上下文:容器 position: relative 保证绝对定位的 div 正确落点;若要全屏效果可用 body
  • 可访问性:保持 pointer-events: none,确保不影响页面交互。

可配置维度(快速调出不同风格)

  • 颜色与透明度:更柔和可用 rgba(0,0,0,0.25);品牌色则替换为主题色。
  • 密度:调整 batch(比如 5 更密,10 更稀)。
  • 大小initialSize 区间配合 CSS scale 影响整体氛围。
  • 区域限制:仅在指定容器内生效,避免干扰全局。

Vue 版本(结尾附上代码)

如果你在 Vue 项目内,需要响应式地管理“墨圈列表”,App.vue 展示了一个等价实现:

模板层:监听事件并按 v-for 渲染出墨圈节点。

<template><div @mousemove="handleMouseMove" @mousedown="handleMouseDown" class="container"><router-view></router-view><divv-for="(ink, index) in inks":key="ink.id":style="ink.style":class="ink.class"></div> </div>
</template>

逻辑层:和原生 JS 一致,只是把“创建/删除”转为操作 inks 数组。

const createInkCircle = (x: number, y: number) => {const batch = 7;inkId++;if(inkId%batch!=0)return;const initialSize = 7 + Math.random() * 5const duration = 2400const newInk = {style: {left: `${x - initialSize / 2}px`,top: `${y - initialSize / 2}px`,width: `${initialSize}px`,height: `${initialSize}px`,borderRadius: '50%',position: 'absolute',backgroundColor: 'rgba(0, 0, 0, 0.4)',},id: inkId,class: 'ink'}inks.value.push(newInk)setTimeout(() => {inks.value = inks.value.filter((ink) => ink.id !== newInk.id)}, duration)
}

样式层同样沿用 @keyframes,与 index.html 中一致:

.ink {position: absolute;pointer-events: none;z-index: 9999;opacity: 0.7;animation: growInk 2500ms;
}.inkDrop{position: absolute;pointer-events: none;z-index: 9999;opacity: 0.7;animation: growInkDrop 3000ms;
}

最后附上Vue版本源码

<template><div @mousemove="handleMouseMove" @mousedown="handleMouseDown" class="container"><router-view></router-view><divv-for="(ink, index) in inks":key="ink.id":style="ink.style":class="ink.class"></div> </div>
</template><script lang="ts" setup>
import { ref, onMounted } from 'vue'// 墨水圈数据
const inks = ref<{ style: Record<string, string | number>; id: number ;class:string}[]>([])let inkId = 0 // 用来区分每个墨水圈的唯一标识符const handleMouseMove = (event: MouseEvent) => {createInkCircle(event.clientX, event.clientY)
}const handleMouseDown = (event: MouseEvent) => {createInkCircleDown(event.clientX, event.clientY)
}const createInkCircle = (x: number, y: number) => {const batch = 7;inkId++;if(inkId%batch!=0)return;// 墨水圈初始大小const initialSize = 7 + Math.random() * 5const duration = 2400 // 动画时长,单位:毫秒const newInk = {style: {left: `${x - initialSize / 2}px`,top: `${y - initialSize / 2}px`,width: `${initialSize}px`,height: `${initialSize}px`,borderRadius: '50%',position: 'absolute',backgroundColor: 'rgba(0, 0, 0, 0.4)', // 墨水圈颜色//   animation: `growInk 700ms ease-out`, // 使用新的growInk动画},id: inkId, // 增加唯一标识符class: 'ink'}// 增加新的墨水圈inks.value.push(newInk)// 墨水圈动画结束后移除setTimeout(() => {inks.value = inks.value.filter((ink) => ink.id !== newInk.id)}, duration)
}const createInkCircleDown = (x: number, y: number) => {// 墨水圈初始大小const initialSize = 20 + Math.random() * 10const duration = 1500 // 动画时长,单位:毫秒const newInk = {style: {left: `${x - initialSize / 2}px`,top: `${y - initialSize / 2}px`,width: `${initialSize}px`,height: `${initialSize}px`,borderRadius: '50%',position: 'absolute',backgroundColor: 'rgba(0, 0, 0, 0.8)', // 墨水圈颜色},id: inkId++, // 增加唯一标识符class: 'inkDrop'}// 增加新的墨水圈inks.value.push(newInk)// 墨水圈动画结束后移除setTimeout(() => {inks.value = inks.value.filter((ink) => ink.id !== newInk.id)}, duration)
}onMounted(() => {// 页面加载后清除所有墨水圈inks.value = []
})</script><style scoped>
.container {position: relative;height: 100vh;/* cursor: none; 隐藏默认鼠标 */
}.ink {position: absolute;pointer-events: none;z-index: 9999;opacity: 0.7;animation: growInk 2500ms;
}.inkDrop{position: absolute;pointer-events: none;z-index: 9999;opacity: 0.7;animation: growInkDrop 3000ms;
}/* 定义墨水圈的动画 */
@keyframes growInk {0% {transform: scale(1);opacity: 1;}100% {transform: scale(3);opacity: 0;}
}@keyframes growInkDrop{0% {transform: scale(1);opacity: 1;}100% {transform: scale(6);opacity: 0;}
}
</style>
http://www.dtcms.com/a/452972.html

相关文章:

  • AI时代:IT从业者会被取代吗?
  • Python跨端Django+Vue3全栈开发:智慧社区小程序构建
  • 池州网站网站建设如何介绍自己的设计方案
  • Vue内置组件KeepAlive——缓存组件实例
  • 品牌网站建设小h蝌蚪机械电子工程网
  • 【高并发服务器】三、正则表达式的使用
  • 网站建设好公司好深圳好的品牌策划公司
  • Java的`volatile`关键字 笔记251007
  • 【文件读写】图片木马
  • 如何避免消息丢失
  • 设备管理平台项目部署
  • 最小二乘法(Least Squares Method):原理、应用与扩展
  • 13. Pandas 透视表与交叉表分析
  • Edu161 D、E 模拟+位运算构造
  • 临床研究三千问——如何选择合适的研究类型(12)
  • 电销做网站的话术响应式网站是
  • Channel 的核心特点 (Channel vs SharedFlow 选择对比)
  • 什么网站权重高wordpress置顶代码
  • 厦门app网站设计青岛队建网站
  • 【Linux】Linux进程信号(下)
  • C++基础:(九)string类的使用与模拟实现
  • C++网络编程(二)字节序与IP地址转换
  • 从零开始XR开发:Three.js实现交互式3D积木搭建器
  • 如何解决网站只收录首页的一些办法wordpress多站点内容聚合
  • 个人备忘录的设计与实现
  • 删除cad无关线条 的ppo 随手记
  • Python AI编程在微创手术通过数据分析改善恢复的路径分析(下)
  • 深度学习之神经网络1(Neural Network)
  • pycharm下创建flask项目,配置端口问题
  • 计算机科学中的核心思想与理论