PageSourceSearch

https://yumetsuki.moe/js/cursor.js

js yumetsuki.moe collected 2026-10-06 12:47:44 UTC 6,855 bytes, 270 lines download raw bytes

1/**
2 * Custom Cursor Module
3 * ES2022 version with performance optimizations
4 */
5
6;(() => {
7  "use strict"
8
9  /**
10   * 线性插值函数
11   */
12  const lerp = (a, b, n) => (1 - n) * a + n * b
13
14  /**
15   * 获取元素计算样式
16   */
17  const getComputedStyleValue = (element, property) => {
18    try {
19      return window.getComputedStyle(element)[property] || ""
20    } catch {
21      return ""
22    }
23  }
24
25  /**
26   * 检查元素是否应该显示 hover 效果
27   */
28  const shouldShowHover = (element) => {
29    if (!element || element === document) {
30      return false
31    }
32
33    // 检查元素或其父元素是否有 pointer cursor
34    let current = element
35    while (current && current !== document.body) {
36      const cursor = getComputedStyleValue(current, "cursor")
37      if (cursor === "pointer") {
38        return true
39      }
40      current = current.parentElement
41    }
42
43    return false
44  }
45
46  /**
47   * 自定义光标类
48   */
49  class CustomCursor {
50    constructor() {
51      this.pos = { curr: null, prev: null }
52      this.cursor = null
53      this.styleElement = null
54      this.animationFrameId = null
55      this.handlers = {}
56
57      this.create()
58      this.bindEvents()
59      this.startRender()
60    }
61
62    /**
63     * 移动光标到指定位置
64     */
65    move(left, top) {
66      if (!this.cursor) return
67
68      this.cursor.style.left = `${left}px`
69      this.cursor.style.top = `${top}px`
70    }
71
72    /**
73     * 创建光标元素和样式
74     */
75    create() {
76      // 创建光标元素
77      if (!this.cursor) {
78        this.cursor = document.createElement("div")
79        this.cursor.id = "cursor"
80        this.cursor.classList.add("hidden")
81        document.body.appendChild(this.cursor)
82      }
83
84      // 创建自定义光标样式
85      if (!this.styleElement) {
86        this.styleElement = document.createElement("style")
87        this.styleElement.innerHTML = `
88                    * {
89                        cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' width='8px' height='8px'><style>circle {fill: white;}</style><circle cx='4' cy='4' r='4' opacity='.5'/></svg>") 4 4, auto !important;
90                    }
91                `
92        document.body.appendChild(this.styleElement)
93      }
94    }
95
96    /**
97     * 绑定事件监听器(修复:Bug #6 - 性能优化,使用事件委托)
98     */
99    bindEvents() {
100      // 修复:不再遍历所有DOM元素,而是使用事件委托动态检查
101      this.handlers.mouseover = (event) => {
102        if (shouldShowHover(event.target)) {
103          this.cursor?.classList.add("hover")
104        }
105      }
106
107      this.handlers.mouseout = (event) => {
108        if (shouldShowHover(event.target)) {
109          this.cursor?.classList.remove("hover")
110        }
111      }
112
113      this.handlers.mousemove = (event) => {
114        const x = event.clientX - 8
115        const y = event.clientY - 8
116
117        // 初始位置
118        if (this.pos.curr === null) {
119          this.move(x, y)
120        }
121
122        this.pos.curr = { x, y }
123        this.cursor?.classList.remove("hidden")
124      }
125
126      this.handlers.mouseenter = () => {
127        this.cursor?.classList.remove("hidden")
128      }
129
130      this.handlers.mouseleave = () => {
131        this.cursor?.classList.add("hidden")
132      }
133
134      this.handlers.mousedown = () => {
135        this.cursor?.classList.add("active")
136      }
137
138      this.handlers.mouseup = () => {
139        this.cursor?.classList.remove("active")
140      }
141
142      // 使用 addEventListener 代替 on* 属性
143      document.addEventListener("mouseover", this.handlers.mouseover, {
144        passive: true
145      })
146      document.addEventListener("mouseout", this.handlers.mouseout, {
147        passive: true
148      })
149      document.addEventListener("mousemove", this.handlers.mousemove, {
150        passive: true
151      })
152      document.addEventListener("mouseenter", this.handlers.mouseenter, {
153        passive: true
154      })
155      document.addEventListener("mouseleave", this.handlers.mouseleave, {
156        passive: true
157      })
158      document.addEventListener("mousedown", this.handlers.mousedown, {
159        passive: true
160      })
161      document.addEventListener("mouseup", this.handlers.mouseup, {
162        passive: true
163      })
164    }
165
166    /**
167     * 移除事件监听器
168     */
169    unbindEvents() {
170      if (this.handlers.mouseover) {
171        document.removeEventListener("mouseover", this.handlers.mouseover)
172        document.removeEventListener("mouseout", this.handlers.mouseout)
173        document.removeEventListener("mousemove", this.handlers.mousemove)
174        document.removeEventListener("mouseenter", this.handlers.mouseenter)
175        document.removeEventListener("mouseleave", this.handlers.mouseleave)
176        document.removeEventListener("mousedown", this.handlers.mousedown)
177        document.removeEventListener("mouseup", this.handlers.mouseup)
178      }
179      this.handlers = {}
180    }
181
182    /**
183     * 开始渲染循环
184     */
185    startRender() {
186      const render = () => {
187        if (this.pos.curr && this.pos.prev) {
188          // 平滑移动
189          this.pos.prev.x = lerp(this.pos.prev.x, this.pos.curr.x, 0.15)
190          this.pos.prev.y = lerp(this.pos.prev.y, this.pos.curr.y, 0.15)
191          this.move(this.pos.prev.x, this.pos.prev.y)
192        } else if (this.pos.curr) {
193          this.pos.prev = { ...this.pos.curr }
194        }
195
196        this.animationFrameId = requestAnimationFrame(render)
197      }
198
199      render()
200    }
201
202    /**
203     * 停止渲染循环
204     */
205    stopRender() {
206      if (this.animationFrameId !== null) {
207        cancelAnimationFrame(this.animationFrameId)
208        this.animationFrameId = null
209      }
210    }
211
212    /**
213     * 刷新光标
214     */
215    refresh() {
216      // 不需要重新遍历DOM,事件委托会自动处理新元素
217      this.cursor?.classList.remove("hover", "active")
218      this.pos = { curr: null, prev: null }
219    }
220
221    /**
222     * 销毁光标
223     */
224    destroy() {
225      this.stopRender()
226      this.unbindEvents()
227
228      if (this.styleElement?.parentNode) {
229        this.styleElement.remove()
230        this.styleElement = null
231      }
232
233      if (this.cursor?.parentNode) {
234        this.cursor.remove()
235        this.cursor = null
236      }
237
238      this.pos = { curr: null, prev: null }
239    }
240  }
241
242  // 初始化自定义光标
243  let cursorInstance = null
244
245  const init = () => {
246    // 清理旧实例
247    if (cursorInstance) {
248      cursorInstance.destroy()
249    }
250
251    // 创建新实例
252    cursorInstance = new CustomCursor()
253
254    // 导出到全局(保持向后兼容)
255    window.CURSOR = cursorInstance
256  }
257
258  const cleanup = () => {
259    if (cursorInstance) {
260      cursorInstance.destroy()
261      cursorInstance = null
262    }
263  }
264
265  lifecycle.once(init)
266  lifecycle.onReady(() => {
267    cursorInstance?.refresh()
268  })
269  lifecycle.onCleanup(cleanup)
270})()

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.