我把博客的鼠标做成了一朵樱花(附完整代码)
前段时间给博客换樱花主题,越看那个系统默认的小箭头越不顺眼,索性把鼠标也重做了一遍。做完发现效果意外地好,来串门的朋友第一句话都从"文章不错"变成了"你这鼠标哪弄的"。
那就开源吧,代码已经放在 [Gitee 仓库] ,先看效果:

一套完整的光标语言
不只是换个图标,整套东西有六种状态,互相衔接:
| 场景 | 效果 |
|---|---|
| 平时移动 | 一朵带花杆的小樱花,杆尖就是点击点 |
| 悬停可点击处 | 花朵正脸盛开 |
| 按下 | 花缩小一号,像按进花心 |
| 输入框 | 竖着的一朵迷你樱花,替代 I 形光标 |
| 移动轨迹 | 划过的地方飘落樱花花瓣 |
| 点击 | 点击处炸开一圈花瓣,中心冒一颗小星星 |
整套东西零依赖:一个 CSS 文件管四种静态光标,一个 JS 文件管拖尾、点击绽放和可点元素接管,加起来不到 200 行。
实现一:四种静态光标,全部内联 SVG
CSS 的 cursor: url() 支持直接塞一段 SVG,所以四种静态光标没有用到任何图片文件,零网络请求。花瓣的渐变、白描边、投影都是矢量画的,还带一个樱花花瓣标志性的尖端小缺口。
先看默认光标这条(完整版在文末):
body {
cursor: url('data:image/svg+xml;utf8,<svg ...>') 5 5, auto;
}
这朵 SVG 里我最想安利的是结构:一片花瓣画一次,rotate(72°) 复制五次。五瓣樱花是旋转对称图形,写五遍几乎相同的贝塞尔曲线太蠢了,所以每片花瓣都是同一条 path,只是套了不同的旋转:
<g transform="translate(11 11) rotate(-45)">
<g transform="rotate(0) translate(0 -7) scale(0.75)"><path d="..." /></g>
<g transform="rotate(72) translate(0 -7) scale(0.75)"><path d="..." /></g>
<g transform="rotate(144) translate(0 -7) scale(0.75)"><path d="..." /></g>
<g transform="rotate(216) translate(0 -7) scale(0.75)"><path d="..." /></g>
<g transform="rotate(288) translate(0 -7) scale(0.75)"><path d="..." /></g>
<circle r="2.6" fill="#ffd76e" stroke="#f0b64a" stroke-width="0.7" />
</g>
外层再补两笔:一根绿色花杆(stroke-linecap="round" 的曲线)加一片叶子,中心一点嫩黄花蕊,"手拿一朵小花"的感觉就出来了。
有个容易忽略的细节是 url(...) 后面跟的两个数字——热点坐标(hotspot),它决定"点击点"落在图案的哪个位置。四种光标各有各的讲究:
| 光标 | SVG 尺寸 | 热点 | 为什么 |
|---|---|---|---|
| 默认(带杆小花) | 40×40 | 5 5 |
杆尖在左上角,指哪点哪 |
| 悬停(盛开花) | 36×36 | 18 18 |
花蕊正中心 |
| 按下(缩小花) | 36×36 | 18 18 |
和悬停一致,按下才有"没跑偏"的感觉 |
| 输入框(竖花) | 14×30 | 7 15 |
花茎中点,对齐文字基线 |
热点给错了,用户会觉得"点了没反应"或者"偏了一点",这是自定义光标最常见的翻车点。
四种状态的选择器大概是这个思路(完整 SVG 见文末):
/* 悬停:a、button、label 等常见可点元素先列上,JS 会自动补漏 */
a, button, [role='button'], summary, label {
cursor: url('data:image/svg+xml;utf8,<svg ...>') 18 18, pointer;
}
/* 按下:同一朵花整体 scale(0.85),热点不变 */
a:active, button:active, [role='button']:active {
cursor: url('data:image/svg+xml;utf8,<svg ...>') 18 18, pointer;
}
/* 输入框:竖着的迷你樱花,按钮型 input 除外(它们走可点击状态) */
input:not([type='button']):not([type='submit']):not([type='reset']),
textarea,
[contenteditable='true'] {
cursor: url('data:image/svg+xml;utf8,<svg ...>') 7 15, text;
}
注意每个 cursor 声明的兜底值:默认光标兜 auto、可点元素兜 pointer、输入框兜 text。Safari 至今不支持 SVG 光标,靠这个兜底机制自动回退系统默认,页面照常用,只是少点花。
实现二:拖尾和点击绽放,一个 canvas 粒子池
动态效果全部画在一张全屏 canvas 上。这张画布 pointer-events: none、z-index 拉满,不拦截任何点击:
var cv = document.createElement('canvas')
cv.style.cssText =
'position:fixed;inset:0;width:100vw;height:100vh;pointer-events:none;z-index:99999'
document.body.appendChild(cv)
var ctx = cv.getContext('2d')
// 高分屏适配,按设备像素比放大画布,最多到 2,够清晰也不浪费
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2)
cv.width = window.innerWidth * dpr
cv.height = window.innerHeight * dpr
}
粒子本体是一个对象池:每个粒子带位置、速度、自转、寿命,寿命尽了从数组里移除:
function spawn(x, y, vx, vy, star) {
parts.push({
x: x, y: y, vx: vx, vy: vy,
rot: Math.random() * Math.PI * 2, // 初始角度随机
vr: (Math.random() - 0.5) * 0.2, // 自转速度随机
size: star ? 3 + Math.random() * 3 : 4.5 + Math.random() * 3.5,
life: 1,
decay: 0.014 + Math.random() * 0.012, // 寿命衰减速度随机
star: !!star
})
// 池子满了丢最老的,快速乱甩也不会堆积
if (parts.length > MAX_PARTS) parts.splice(0, parts.length - MAX_PARTS)
}
拖尾和点击只是两种不同的"出生姿势":
// 拖尾:移动出粒限频,两成概率换成小星星
function onMove(e) {
var now = performance.now()
if (now - lastSpawn < MOVE_GAP) return // 26ms 一片,甩再快也只有一条短尾巴
lastSpawn = now
spawn(e.clientX, e.clientY,
(Math.random() - 0.5) * 1.1, // 水平速度小幅随机
-0.4 + Math.random() * 0.9, // 垂直初速可上可下
Math.random() < 0.2) // 两成概率出星星
}
// 点击:炸开一圈花瓣 + 中心一颗小星星
function onClick(e) {
for (var i = 0; i < 7; i++) {
var a = (Math.PI * 2 * i) / 7 + Math.random() * 0.5 // 七瓣均匀撒开,加随机抖动
var sp = 1.3 + Math.random() * 1.2
spawn(e.clientX, e.clientY, Math.cos(a) * sp, Math.sin(a) * sp - 0.5, false)
}
spawn(e.clientX, e.clientY, 0, -0.3, true)
}
花瓣和星星都是 canvas 路径画的。花瓣两条贝塞尔曲线收出一个带尖端小缺口的形状;星星更取巧——四段二次贝塞尔曲线全过原点,天然凹成四角星:
function drawStar(c, s) {
c.fillStyle = STAR_COLOR
c.beginPath()
c.moveTo(0, -s)
c.quadraticCurveTo(0, 0, s, 0) // 每段曲线的中途都路过圆心
c.quadraticCurveTo(0, 0, 0, s) // 四段拼起来就是一个凹四角星
c.quadraticCurveTo(0, 0, -s, 0)
c.quadraticCurveTo(0, 0, 0, -s)
c.fill()
}
function drawPetal(c, s) {
c.fillStyle = PETAL_COLOR
c.beginPath()
c.moveTo(0, s)
c.bezierCurveTo(-s * 0.9, s * 0.2, -s * 0.55, -s * 0.75, 0, -s * 0.9)
c.bezierCurveTo(s * 0.55, -s * 0.75, s * 0.9, s * 0.2, 0, s)
c.fill()
}
每帧更新就是标准的粒子循环:位移、加重力、自转、扣寿命、画出来:
function frame() {
ctx.clearRect(0, 0, cv.width, cv.height)
for (var i = parts.length - 1; i >= 0; i--) {
var p = parts[i]
p.x += p.vx
p.y += p.vy
p.vy += 0.02 // 一点点重力,花瓣缓缓往下飘
p.rot += p.vr
p.life -= p.decay
if (p.life <= 0) { parts.splice(i, 1); continue }
ctx.save()
ctx.translate(p.x * dpr, p.y * dpr)
ctx.rotate(p.rot)
// 全程不透明,颜色和光标一致,寿命尽了直接消失
if (p.star) drawStar(ctx, p.size * dpr)
else drawPetal(ctx, p.size * dpr)
ctx.restore()
}
requestAnimationFrame(frame)
}
性能上就两个限制:粒子池上限 56 个、移动出粒限频。加上所有事件监听都标了 { passive: true } 告诉浏览器"我不调 preventDefault,你尽管滚动优化",快速甩鼠标也只有一条优雅的短尾巴,不会糊成一片。
实现三:可点元素自动接管,最得意的一招
给链接、按钮换光标,常规做法是列一堆类名清单:a, button, .el-button, .el-switch...。但只要用了组件库,清单永远列不全,总有小图标按钮漏出来显示系统小手,特别出戏。
我的做法是反过来:监听全局 mouseover,凡是计算样式里 cursor 为 pointer 的元素——不管这个 pointer 是浏览器默认给的、组件库设置的、还是自己手写的——当场把它的光标改写成盛开花:
function onOver(e) {
var el = e.target
if (!(el instanceof Element) || el.style.cursor) return // 已经改写过的直接跳过
if (getComputedStyle(el).cursor === 'pointer') el.style.cursor = FLOWER_CURSOR
}
三行代码,等效于一份永远不会漏的清单:
el.style.cursor非空说明改写过(或开发者手动指定过),跳过,几乎零开销;getComputedStyle(el).cursor === 'pointer'是把判定权交给浏览器——它说能点,那就能点;FLOWER_CURSOR就是 CSS 里那朵盛开花的 data URI,JS 里存了一份同样的字符串。
唯一的代价是滑过每个元素时查一次计算样式,实测完全无感。
拿去用
仓库在这里:https://gitee.com/sanshijiu/sakura-cursor,MIT 协议,两步接入:
<link rel="stylesheet" href="sakura-cursor.css">
<script src="sakura-cursor.js"></script>
纯 HTML、WordPress、Hexo、Vue、React 都能直接用。SSR 项目记得把脚本放客户端执行(Vue 套 <ClientOnly>)。JS 开头有四个参数可调:
var PETAL_COLOR = '#ffb7c5' // 花瓣颜色
var STAR_COLOR = '#ffe3ec' // 星星颜色
var MOVE_GAP = 26 // 拖尾间隔毫秒,越小越密
var MAX_PARTS = 56 // 粒子池上限
换整套配色时记得 JS 的 PETAL_COLOR 和 CSS 光标 SVG 里的 %23ffb7c5 一起换(%23 是 # 的 URL 编码),两边一致才和谐——别问我怎么知道的。
完整代码
两个文件全文贴上,复制即用。
sakura-cursor.css(四种静态光标):
/* ============================================================
樱花鼠标 sakura-cursor.css
四种状态:默认带杆小樱花 / 可点击盛开花 / 按下缩小 / 输入框竖花
Safari 不支持 SVG 光标会自动回退系统默认,不影响使用
============================================================ */
/* 默认光标:一朵带花杆的小樱花,杆尖就是点击点 */
body {
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 32 32"><defs><linearGradient id="pg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="%23ffb7c5"/><stop offset="1" stop-color="%23ffb7c5"/></linearGradient><filter id="fs" x="-40%" y="-40%" width="180%" height="180%"><feDropShadow dx="0.8" dy="0.8" stdDeviation="0.9" flood-color="%23e08cab" flood-opacity="0.4"/></filter></defs><path d="M12.5 12.5 C 17.5 17.5, 22 22, 29 29" fill="none" stroke="%237cc4b0" stroke-width="2.4" stroke-linecap="round"/><path d="M16.5 16.5 C 21 15.4, 24 17.2, 24.9 20.8 C 20.5 22, 17.5 20.6, 16.5 16.5 Z" fill="%2393d3bf"/><g transform="translate(11 11) rotate(-45)" filter="url(%23fs)"><g transform="rotate(0) translate(0 -7) scale(0.75)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><g transform="rotate(72) translate(0 -7) scale(0.75)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><g transform="rotate(144) translate(0 -7) scale(0.75)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><g transform="rotate(216) translate(0 -7) scale(0.75)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><g transform="rotate(288) translate(0 -7) scale(0.75)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><circle r="2.6" fill="%23ffd76e" stroke="%23f0b64a" stroke-width="0.7"/></g></svg>') 5 5, auto;
}
/* 可点击元素:盛开的五瓣樱花
(JS 还会自动接管其它 pointer 元素,这里只是先把最常见的列上) */
a,
button,
[role='button'],
summary,
label {
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 32 32"><defs><linearGradient id="pg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="%23ffb7c5"/><stop offset="1" stop-color="%23ffb7c5"/></linearGradient><filter id="fs" x="-40%" y="-40%" width="180%" height="180%"><feDropShadow dx="0.8" dy="0.8" stdDeviation="0.9" flood-color="%23e08cab" flood-opacity="0.4"/></filter></defs><g transform="translate(16 16)" filter="url(%23fs)"><g transform="rotate(0) translate(0 -7.5) scale(0.8)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><g transform="rotate(72) translate(0 -7.5) scale(0.8)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><g transform="rotate(144) translate(0 -7.5) scale(0.8)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><g transform="rotate(216) translate(0 -7.5) scale(0.8)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><g transform="rotate(288) translate(0 -7.5) scale(0.8)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><circle r="2.8" fill="%23ffd76e" stroke="%23f0b64a" stroke-width="0.7"/></g></svg>') 18 18, pointer;
}
/* 按下瞬间:同一朵花缩一号 */
a:active,
button:active,
[role='button']:active {
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 32 32"><defs><linearGradient id="pg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="%23ffb7c5"/><stop offset="1" stop-color="%23ffb7c5"/></linearGradient></defs><g transform="translate(2.4 2.4) scale(0.85)"><g transform="translate(16 16)"><g transform="rotate(0) translate(0 -7.5) scale(0.8)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><g transform="rotate(72) translate(0 -7.5) scale(0.8)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><g transform="rotate(144) translate(0 -7.5) scale(0.8)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><g transform="rotate(216) translate(0 -7.5) scale(0.8)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><g transform="rotate(288) translate(0 -7.5) scale(0.8)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1"/></g><circle r="2.8" fill="%23ffd76e" stroke="%23f0b64a" stroke-width="0.7"/></g></g></svg>') 18 18, pointer;
}
/* 输入框:竖着拿的迷你樱花(按钮型 input 除外,它们走可点击状态) */
input:not([type='button']):not([type='submit']):not([type='reset']):not([type='checkbox']):not([type='radio']),
textarea,
[contenteditable='true'] {
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="14" height="30" viewBox="0 0 14 30"><defs><linearGradient id="pg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="%23ffb7c5"/><stop offset="1" stop-color="%23ffb7c5"/></linearGradient><filter id="fs" x="-40%" y="-40%" width="180%" height="180%"><feDropShadow dx="0.6" dy="0.6" stdDeviation="0.7" flood-color="%23e08cab" flood-opacity="0.4"/></filter></defs><path d="M7 12 C7 17, 7 22, 7 29" fill="none" stroke="%237cc4b0" stroke-width="1.4" stroke-linecap="round"/><path d="M7 19 C9.5 18.4, 11 19.3, 11.4 21.2 C9.3 21.8, 7.6 21, 7 19 Z" fill="%2393d3bf"/><g transform="translate(7 7)" filter="url(%23fs)"><g transform="rotate(0) translate(0 -4.2) scale(0.5)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1.2"/></g><g transform="rotate(72) translate(0 -4.2) scale(0.5)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1.2"/></g><g transform="rotate(144) translate(0 -4.2) scale(0.5)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1.2"/></g><g transform="rotate(216) translate(0 -4.2) scale(0.5)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1.2"/></g><g transform="rotate(288) translate(0 -4.2) scale(0.5)"><path d="M0 5.5 C -3.8 4.2, -4.8 0.6, -3.2 -2.4 C -2.4 -4, -1.6 -4.8, -1 -5 L 0 -3.8 L 1 -5 C 1.6 -4.8, 2.4 -4, 3.2 -2.4 C 4.8 0.6, 3.8 4.2, 0 5.5 Z" fill="url(%23pg)" stroke="%23ffffff" stroke-width="1.2"/></g><circle r="1.8" fill="%23ffd76e" stroke="%23f0b64a" stroke-width="0.5"/></g></svg>') 7 15, text;
}
sakura-cursor.js(拖尾、点击绽放、可点元素接管):
/* ============================================================
樱花鼠标 sakura-cursor.js
三件事:鼠标拖尾飘花瓣 / 点击炸开一圈花印 / 自动把可点元素换成盛开花
无依赖,放到页面任意位置即可
============================================================ */
(function () {
// 触摸设备没有光标,开了也看不见
if (!window.matchMedia('(pointer: fine)').matches) return
// 用户系统开了"减少动态效果"就不玩了
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
// 想换颜色改这里(和 CSS 光标里的色值保持一致才和谐)
var PETAL_COLOR = '#ffb7c5'
var STAR_COLOR = '#ffe3ec'
// 拖尾每隔多少毫秒出一片花瓣,越小越密
var MOVE_GAP = 26
// 粒子池上限,防止快速乱甩糊成一片
var MAX_PARTS = 56
// 盛开花的 SVG,和 CSS 里那段相同,用于接管可点元素
var FLOWER_CURSOR = "url('data:image/svg+xml;utf8,<svg ...>') 18 18, pointer"
// 全屏画布,不拦截任何点击
var cv = document.createElement('canvas')
cv.style.cssText = 'position:fixed;inset:0;width:100vw;height:100vh;pointer-events:none;z-index:99999'
document.body.appendChild(cv)
var ctx = cv.getContext('2d')
var dpr = 1
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2)
cv.width = window.innerWidth * dpr
cv.height = window.innerHeight * dpr
}
resize()
var parts = []
var lastSpawn = 0
function spawn(x, y, vx, vy, star) {
parts.push({
x: x, y: y,
vx: vx, vy: vy,
rot: Math.random() * Math.PI * 2,
vr: (Math.random() - 0.5) * 0.2,
size: star ? 3 + Math.random() * 3 : 4.5 + Math.random() * 3.5,
life: 1,
decay: 0.014 + Math.random() * 0.012,
star: !!star
})
if (parts.length > MAX_PARTS) parts.splice(0, parts.length - MAX_PARTS)
}
// 拖尾:移动时隔一阵出一片花瓣,两成概率换成小星星
function onMove(e) {
var now = performance.now()
if (now - lastSpawn < MOVE_GAP) return
lastSpawn = now
spawn(e.clientX, e.clientY, (Math.random() - 0.5) * 1.1, -0.4 + Math.random() * 0.9, Math.random() < 0.2)
}
// 点击:炸开一圈花瓣,中心一颗小星星
function onClick(e) {
for (var i = 0; i < 7; i++) {
var a = (Math.PI * 2 * i) / 7 + Math.random() * 0.5
var sp = 1.3 + Math.random() * 1.2
spawn(e.clientX, e.clientY, Math.cos(a) * sp, Math.sin(a) * sp - 0.5, false)
}
spawn(e.clientX, e.clientY, 0, -0.3, true)
}
// 可点元素自动换盛开花:滑到任何 cursor:pointer 的元素上就地改写,
// 比写死类名清单靠谱,组件库的、系统默认的、漏网的全都能接住
function onOver(e) {
var el = e.target
if (!(el instanceof Element) || el.style.cursor) return
if (getComputedStyle(el).cursor === 'pointer') el.style.cursor = FLOWER_CURSOR
}
function drawStar(c, s) {
c.fillStyle = STAR_COLOR
c.beginPath()
c.moveTo(0, -s)
c.quadraticCurveTo(0, 0, s, 0)
c.quadraticCurveTo(0, 0, 0, s)
c.quadraticCurveTo(0, 0, -s, 0)
c.quadraticCurveTo(0, 0, 0, -s)
c.fill()
}
function drawPetal(c, s) {
c.fillStyle = PETAL_COLOR
c.beginPath()
c.moveTo(0, s)
c.bezierCurveTo(-s * 0.9, s * 0.2, -s * 0.55, -s * 0.75, 0, -s * 0.9)
c.bezierCurveTo(s * 0.55, -s * 0.75, s * 0.9, s * 0.2, 0, s)
c.fill()
}
function frame() {
ctx.clearRect(0, 0, cv.width, cv.height)
for (var i = parts.length - 1; i >= 0; i--) {
var p = parts[i]
p.x += p.vx
p.y += p.vy
p.vy += 0.02 // 一点点重力,花瓣缓缓往下飘
p.rot += p.vr
p.life -= p.decay
if (p.life <= 0) {
parts.splice(i, 1)
continue
}
ctx.save()
ctx.translate(p.x * dpr, p.y * dpr)
ctx.rotate(p.rot)
// 全程不透明,颜色和光标一致,寿命尽了直接消失
if (p.star) drawStar(ctx, p.size * dpr)
else drawPetal(ctx, p.size * dpr)
ctx.restore()
}
requestAnimationFrame(frame)
}
window.addEventListener('resize', resize)
window.addEventListener('mousemove', onMove, { passive: true })
window.addEventListener('mousedown', onClick, { passive: true })
window.addEventListener('mouseover', onOver, { passive: true })
requestAnimationFrame(frame)
})()
换个颜色,就是你的了。