叁拾玖2026-09-28 16:33

我把博客的鼠标做成了一朵樱花(附完整代码)

8 3

前段时间给博客换樱花主题,越看那个系统默认的小箭头越不顺眼,索性把鼠标也重做了一遍。做完发现效果意外地好,来串门的朋友第一句话都从"文章不错"变成了"你这鼠标哪弄的"。

那就开源吧,代码已经放在 [Gitee 仓库] ,先看效果:

樱花鼠标效果演示 #center

一套完整的光标语言

不只是换个图标,整套东西有六种状态,互相衔接:

场景 效果
平时移动 一朵带花杆的小樱花,杆尖就是点击点
悬停可点击处 花朵正脸盛开
按下 花缩小一号,像按进花心
输入框 竖着的一朵迷你樱花,替代 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)
})()

换个颜色,就是你的了。

全文完