anime-cursor 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -25
- package/dist/anime-cursor.esm.js +152 -27
- package/dist/anime-cursor.umd.js +152 -27
- package/dist/anime-cursor.umd.min.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
## [Visit the official website](https://shuninyu.github.io/anime-cursor/) for more informations
|
|
9
9
|
## [Read documents](https://shuninyu.github.io/anime-cursor/docs) to get started with AnimeCursor
|
|
10
10
|
|
|
11
|
-
AnimeCursor is a lightweight JavaScript library for animated custom cursors.
|
|
11
|
+
AnimeCursor is a lightweight JavaScript library for frame by frame animated custom cursors.
|
|
12
12
|
|
|
13
13
|
AnimeCursor has no dependencies on any frameworks, making it suitable for personal websites, creative portfolios, and experimental UI projects.
|
|
14
14
|
|
|
@@ -41,32 +41,34 @@ import AnimeCursor from 'anime-cursor';
|
|
|
41
41
|
new AnimeCursor({...});
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
###
|
|
44
|
+
### Host Yourself
|
|
45
45
|
|
|
46
46
|
```html
|
|
47
47
|
<script src="anime-cursor.umd.min.js"></script>
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
## 🚀
|
|
50
|
+
## 🚀 How to use
|
|
51
51
|
|
|
52
52
|
Here is an example of how to use AnimeCursor:
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
‌**IMPORTANT**‌
|
|
55
|
+
- Ensure the initialization code is placed **within** the `<body>` tag of your HTML document.
|
|
56
|
+
- For optimal performance, it is recommended to initialize AnimeCursor **before** the DOM has fully loaded, as certain features require execution prior to the completion of DOM loading.
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
56
59
|
new AnimeCursor({
|
|
57
60
|
cursors: {
|
|
58
|
-
// each type of cursor needs
|
|
61
|
+
// each type of cursor needs size and image
|
|
59
62
|
idle: {
|
|
60
63
|
size: [64,64],
|
|
61
|
-
image: '
|
|
64
|
+
image: 'https://example.com/cursor_default.png', // static cursor only needs image
|
|
62
65
|
default: true // set this cursor as default cursor
|
|
63
|
-
// only default cursor doesn't needs tags
|
|
64
66
|
},
|
|
65
67
|
// sprite animated cursor needs frames and duration
|
|
66
68
|
pointer: {
|
|
67
|
-
tags: ['a', 'button'],
|
|
69
|
+
tags: ['a', 'button'], // if you need certain types of elements to trigger this cursor, set the tags
|
|
68
70
|
size: [64,64],
|
|
69
|
-
image: '
|
|
71
|
+
image: 'https://example.com/cursor_pointer.png',
|
|
70
72
|
frames: 3,
|
|
71
73
|
duration: 0.3,
|
|
72
74
|
pingpong: true, // enable pingpong loop
|
|
@@ -76,12 +78,20 @@ new AnimeCursor({
|
|
|
76
78
|
text: {
|
|
77
79
|
tags: ['p', 'h1', 'h2', 'span'],
|
|
78
80
|
size: [32, 64],
|
|
79
|
-
image: '
|
|
81
|
+
image: 'https://example.com/cursor_text.gif'
|
|
82
|
+
},
|
|
83
|
+
haha: {
|
|
84
|
+
size: [32,32],
|
|
85
|
+
image: 'https://example.com/cursor_haha.png',
|
|
86
|
+
frames: 12,
|
|
87
|
+
duration: 1,
|
|
88
|
+
pixel: true, // if the image is origin size pixel art, set pixel to true
|
|
89
|
+
scale: [2,2] // scale the cursor
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
92
|
});
|
|
83
|
-
</script>
|
|
84
93
|
```
|
|
94
|
+
For non-default cursors, if you need a specific element to trigger the cursor, manually add the `data-cursor` attribute to the element. For example: if you want the `<div class="custom-div"></div>` to trigger the `haha` cursor, you need to add `data-cursor="haha"` to it, and the modified code should be as follows: `<div class="custom-div" data-cursor="haha"></div>`. This way, when the cursor hovers over the `custom-div` element, the cursor will switch to the `haha` style.
|
|
85
95
|
|
|
86
96
|
## ⚙️ Configuration Options
|
|
87
97
|
|
|
@@ -167,7 +177,7 @@ If `duration` is not set, the cursor will be treated as a **static cursor**, eve
|
|
|
167
177
|
## [访问官网](https://shuninyu.github.io/anime-cursor/)以获取更多信息
|
|
168
178
|
## [阅读文档](https://shuninyu.github.io/anime-cursor/docs/zh)快速上手 AnimeCursor
|
|
169
179
|
|
|
170
|
-
AnimeCursor
|
|
180
|
+
AnimeCursor 是一个轻量级自定义逐帧动画光标 JavaScript 库。
|
|
171
181
|
|
|
172
182
|
AnimeCursor 无需依赖任何框架,适合个人网站、创意作品集以及实验性 UI 项目。
|
|
173
183
|
|
|
@@ -209,37 +219,48 @@ new AnimeCursor({...});
|
|
|
209
219
|
## 🚀 基础用法
|
|
210
220
|
|
|
211
221
|
下面是一个 AnimeCursor 使用示例:
|
|
212
|
-
|
|
213
|
-
|
|
222
|
+
|
|
223
|
+
**重要提示**
|
|
224
|
+
- 请务必将初始化代码置于HTML文档的 **`<body>`** 标签内部。
|
|
225
|
+
- 为获得最佳性能,建议在DOM完全加载**之前**初始化AnimeCursor,因其部分功能需在DOM加载完成前执行。
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
214
228
|
new AnimeCursor({
|
|
215
229
|
cursors: {
|
|
216
|
-
// 每种光标都需要
|
|
230
|
+
// 每种光标都需要 size 和 image
|
|
217
231
|
default: {
|
|
218
232
|
size: [64,64],
|
|
219
|
-
image: '
|
|
233
|
+
image: 'https://example.com/cursor_default.png', // 静态光标只需要图片链接
|
|
220
234
|
default: true // 将此光标设为默认光标
|
|
221
|
-
// 默认光标不需要 tags
|
|
222
235
|
},
|
|
223
236
|
// 精灵图动画光标还需要 frames 和 duration
|
|
224
237
|
pointer: {
|
|
225
|
-
tags: ['a', 'button'],
|
|
238
|
+
tags: ['a', 'button'], // 如果需要某类元素触发该光标,设置 tags
|
|
226
239
|
size: [64,64],
|
|
227
|
-
image: '
|
|
240
|
+
image: 'https://example.com/cursor_pointer.png',
|
|
228
241
|
frames: 3,
|
|
229
242
|
duration: 0.3,
|
|
230
|
-
pingpong: true, //
|
|
231
|
-
offset: [10,
|
|
243
|
+
pingpong: true, // 启用乒乓循环
|
|
244
|
+
offset: [10,4] // 如果指针位置不在左上角,设置 offset
|
|
232
245
|
},
|
|
233
246
|
// gif 光标不需要 frames 或 duration
|
|
234
247
|
text: {
|
|
235
248
|
tags: ['p', 'h1', 'h2', 'span'],
|
|
236
|
-
size: [32,
|
|
237
|
-
image: '
|
|
249
|
+
size: [32,64],
|
|
250
|
+
image: 'https://example.com/cursor_text.gif'
|
|
251
|
+
},
|
|
252
|
+
haha: {
|
|
253
|
+
size: [32,32],
|
|
254
|
+
image: 'https://example.com/cursor_haha.png',
|
|
255
|
+
frames: 12,
|
|
256
|
+
duration: 1,
|
|
257
|
+
pixel: true, // 如果是原尺寸像素图,启用像素化渲染
|
|
258
|
+
scale: [2,2] // 缩放光标
|
|
238
259
|
}
|
|
239
260
|
}
|
|
240
261
|
});
|
|
241
|
-
</script>
|
|
242
262
|
```
|
|
263
|
+
对于非默认光标,如果需要某元素触发该光标,请手动为该元素添加 `data-cursor`。例如:如果你想让 `<div class="custom-div"></div>` 触发 `haha` 光标,那么就要为其添加 `data-cursor="haha"`,修改完后应该是这样:`<div class="custom-div" data-cursor="haha"></div>`。这样当光标指向 `custom-div` 元素时,光标就会切换到 `haha`。
|
|
243
264
|
|
|
244
265
|
## ⚙️ 配置项说明
|
|
245
266
|
|
package/dist/anime-cursor.esm.js
CHANGED
|
@@ -1,10 +1,56 @@
|
|
|
1
1
|
// AnimeCursor by github@ShuninYu
|
|
2
|
-
// v0.
|
|
2
|
+
// v0.3.0
|
|
3
|
+
|
|
4
|
+
// 静态变量存储唯一实例
|
|
5
|
+
let _instance = null;
|
|
3
6
|
|
|
4
7
|
class AnimeCursor {
|
|
5
8
|
|
|
9
|
+
static get instance() {
|
|
10
|
+
return _instance;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static destroy() {
|
|
14
|
+
if (_instance) {
|
|
15
|
+
_instance.destroy();
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static refresh() {
|
|
22
|
+
if (_instance) {
|
|
23
|
+
_instance.refresh();
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static disable() {
|
|
30
|
+
if (_instance) {
|
|
31
|
+
_instance.disable();
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static enable() {
|
|
38
|
+
if (_instance) {
|
|
39
|
+
_instance.enable();
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
6
45
|
constructor(options = {}) {
|
|
46
|
+
// 如果已有实例,直接返回它
|
|
47
|
+
if (_instance) {
|
|
48
|
+
console.warn('[AnimeCursor] AnimeCursor already exists.');
|
|
49
|
+
return _instance;
|
|
50
|
+
}
|
|
51
|
+
|
|
7
52
|
this.options = {
|
|
53
|
+
displayOnLoad: false,
|
|
8
54
|
enableTouch: false,
|
|
9
55
|
debug: false,
|
|
10
56
|
...options
|
|
@@ -23,12 +69,15 @@ class AnimeCursor {
|
|
|
23
69
|
this.cursorEl = null;
|
|
24
70
|
this.lastCursorType = null;
|
|
25
71
|
this.debugEl = null;
|
|
72
|
+
this.styleEl = null;
|
|
73
|
+
this._onMouseMove = null;
|
|
26
74
|
|
|
27
75
|
this._validateOptions();
|
|
28
|
-
this.
|
|
29
|
-
this.
|
|
30
|
-
|
|
31
|
-
|
|
76
|
+
this._injectPreload();
|
|
77
|
+
this._checkDomLoad();
|
|
78
|
+
|
|
79
|
+
// 保存实例引用
|
|
80
|
+
_instance = this;
|
|
32
81
|
}
|
|
33
82
|
|
|
34
83
|
isMouseLikeDevice() {
|
|
@@ -75,19 +124,28 @@ class AnimeCursor {
|
|
|
75
124
|
this.styleEl = null;
|
|
76
125
|
}
|
|
77
126
|
|
|
78
|
-
// 4 清理 data-cursor
|
|
127
|
+
// 4 清理 data-cursor(只清理由 AnimeCursor 添加的)
|
|
79
128
|
for (const cfg of Object.values(this.options.cursors)) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
129
|
+
// v0.2.1 添加检查:只有存在且为数组的 tags 才进行处理
|
|
130
|
+
if (cfg.tags && Array.isArray(cfg.tags)) {
|
|
131
|
+
cfg.tags.forEach(tag => {
|
|
132
|
+
document.querySelectorAll(tag).forEach(el => {
|
|
133
|
+
if (el.dataset.cursorBound) {
|
|
134
|
+
delete el.dataset.cursor;
|
|
135
|
+
delete el.dataset.cursorBound;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
85
138
|
});
|
|
86
|
-
}
|
|
139
|
+
}
|
|
87
140
|
}
|
|
88
141
|
|
|
89
142
|
// 5 重置状态
|
|
90
143
|
this.lastCursorType = null;
|
|
144
|
+
|
|
145
|
+
// 清除静态引用
|
|
146
|
+
if (_instance === this) {
|
|
147
|
+
_instance = null;
|
|
148
|
+
}
|
|
91
149
|
}
|
|
92
150
|
disable() {
|
|
93
151
|
if (this.disabled) return;
|
|
@@ -95,6 +153,7 @@ class AnimeCursor {
|
|
|
95
153
|
|
|
96
154
|
if (this.cursorEl) {
|
|
97
155
|
this.cursorEl.style.display = 'none';
|
|
156
|
+
this.styleEl.innerHTML = this.styleEl.innerHTML.replace('* {cursor: none !important;}', '');
|
|
98
157
|
console.log('[AnimeCursor] AnimeCursor disabled!');
|
|
99
158
|
}
|
|
100
159
|
}
|
|
@@ -104,6 +163,7 @@ class AnimeCursor {
|
|
|
104
163
|
|
|
105
164
|
if (this.cursorEl) {
|
|
106
165
|
this.cursorEl.style.display = '';
|
|
166
|
+
this.styleEl.innerHTML += '* {cursor: none; !important;}';
|
|
107
167
|
console.log('[AnimeCursor] AnimeCursor enabled!');
|
|
108
168
|
}
|
|
109
169
|
}
|
|
@@ -124,7 +184,7 @@ class AnimeCursor {
|
|
|
124
184
|
for (const [name, cfg] of Object.entries(this.options.cursors)) {
|
|
125
185
|
if (cfg.default === true) {
|
|
126
186
|
if (this.defaultCursorType) {
|
|
127
|
-
throw new Error('[AnimeCursor]
|
|
187
|
+
throw new Error('[AnimeCursor] There can only be one default cursor');
|
|
128
188
|
}
|
|
129
189
|
this.defaultCursorType = name;
|
|
130
190
|
}
|
|
@@ -138,14 +198,8 @@ class AnimeCursor {
|
|
|
138
198
|
throw new Error('AnimeCursor init failed');
|
|
139
199
|
}
|
|
140
200
|
});
|
|
141
|
-
|
|
142
|
-
if (!cfg.
|
|
143
|
-
if (!Array.isArray(cfg.tags) || cfg.tags.length === 0) {
|
|
144
|
-
console.error(`[AnimeCursor] non-default cursor "${name}" must define at least one tag`);
|
|
145
|
-
throw new Error('AnimeCursor init failed');
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (cfg.default && cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
|
|
201
|
+
|
|
202
|
+
if (cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
|
|
149
203
|
console.error(`[AnimeCursor] default cursor "${name}" 's tags must be an array if provided`);
|
|
150
204
|
throw new Error('AnimeCursor init failed');
|
|
151
205
|
}
|
|
@@ -157,6 +211,64 @@ class AnimeCursor {
|
|
|
157
211
|
}
|
|
158
212
|
}
|
|
159
213
|
|
|
214
|
+
// ----------------------------
|
|
215
|
+
// 等待 DOM 加载完成
|
|
216
|
+
// ----------------------------
|
|
217
|
+
_checkDomLoad() {
|
|
218
|
+
const init = () => {
|
|
219
|
+
this._injectHTML();
|
|
220
|
+
this._injectCSS();
|
|
221
|
+
this._bindElements();
|
|
222
|
+
this._bindMouse();
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (document.readyState === 'loading') {
|
|
226
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
227
|
+
} else {
|
|
228
|
+
init();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ----------------------------
|
|
233
|
+
// 插入光标图片预加载()
|
|
234
|
+
// ----------------------------
|
|
235
|
+
_injectPreload() {
|
|
236
|
+
if (this.disabled) return;
|
|
237
|
+
|
|
238
|
+
// 收集所有需要预加载的图片URL
|
|
239
|
+
const imageUrls = new Set();
|
|
240
|
+
|
|
241
|
+
// 遍历所有光标配置,提取图片URL
|
|
242
|
+
for (const cfg of Object.values(this.options.cursors)) {
|
|
243
|
+
if (cfg.image) {
|
|
244
|
+
imageUrls.add(cfg.image);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 为每个图片URL创建预加载标签
|
|
249
|
+
imageUrls.forEach(url => {
|
|
250
|
+
const link = document.createElement('link');
|
|
251
|
+
link.rel = 'preload';
|
|
252
|
+
link.as = 'image';
|
|
253
|
+
link.href = url;
|
|
254
|
+
|
|
255
|
+
// 可选:添加跨域处理(如果图片来自不同域名)
|
|
256
|
+
if (url.startsWith('http') && !url.startsWith(window.location.origin)) {
|
|
257
|
+
link.crossOrigin = 'anonymous';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
document.head.appendChild(link);
|
|
261
|
+
|
|
262
|
+
if (this.options.debug) {
|
|
263
|
+
console.info(`[AnimeCursor] Preloading image: ${url}`);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (this.options.debug && imageUrls.size > 0) {
|
|
268
|
+
console.info(`[AnimeCursor] Preloaded ${imageUrls.size} cursor image(s)`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
160
272
|
// ----------------------------
|
|
161
273
|
// 插入光标元素 HTML
|
|
162
274
|
// ----------------------------
|
|
@@ -166,7 +278,7 @@ class AnimeCursor {
|
|
|
166
278
|
const cursor = document.createElement('div');
|
|
167
279
|
cursor.id = 'anime-cursor';
|
|
168
280
|
|
|
169
|
-
// 如果debug选项存在,则添加debug
|
|
281
|
+
// 如果debug选项存在,则添加debug元素
|
|
170
282
|
if (this.options.debug) {
|
|
171
283
|
cursor.className = 'cursor-default cursor-debugmode';
|
|
172
284
|
const debuger = document.createElement('div');
|
|
@@ -175,6 +287,14 @@ class AnimeCursor {
|
|
|
175
287
|
this.debugEl = debuger;
|
|
176
288
|
}
|
|
177
289
|
else {cursor.className = 'cursor-default';}
|
|
290
|
+
|
|
291
|
+
// 检查是否设置初始化时显示光标
|
|
292
|
+
if (this.options.displayOnLoad) {
|
|
293
|
+
cursor.style.display = 'block';
|
|
294
|
+
} else {
|
|
295
|
+
cursor.style.display = 'none';
|
|
296
|
+
cursor.dataset.animecursorHide = 'true';
|
|
297
|
+
}
|
|
178
298
|
document.body.appendChild(cursor);
|
|
179
299
|
this.cursorEl = cursor;
|
|
180
300
|
}
|
|
@@ -186,13 +306,12 @@ class AnimeCursor {
|
|
|
186
306
|
if (this.disabled) return;
|
|
187
307
|
|
|
188
308
|
const style = document.createElement('style');
|
|
309
|
+
style.id = 'animecursor-styles';
|
|
189
310
|
let css = '';
|
|
190
311
|
|
|
191
312
|
/* 通用样式 */
|
|
192
313
|
css += `
|
|
193
|
-
* {
|
|
194
|
-
cursor: none !important;
|
|
195
|
-
}
|
|
314
|
+
* {cursor: none !important;}
|
|
196
315
|
#anime-cursor {
|
|
197
316
|
position: fixed;
|
|
198
317
|
top: 0;
|
|
@@ -300,7 +419,7 @@ class AnimeCursor {
|
|
|
300
419
|
if (this.disabled) return;
|
|
301
420
|
|
|
302
421
|
for (const [type, cfg] of Object.entries(this.options.cursors)) {
|
|
303
|
-
if (!cfg.tags || cfg.tags.length === 0) continue;
|
|
422
|
+
if (!cfg.tags || !Array.isArray(cfg.tags) || cfg.tags.length === 0) continue;
|
|
304
423
|
|
|
305
424
|
cfg.tags.forEach(tag => {
|
|
306
425
|
const tagName = tag.toUpperCase();
|
|
@@ -313,7 +432,7 @@ class AnimeCursor {
|
|
|
313
432
|
});
|
|
314
433
|
}
|
|
315
434
|
if (refresh) {
|
|
316
|
-
console.info('[AnimeCursor] refresh done
|
|
435
|
+
console.info('[AnimeCursor] refresh done');
|
|
317
436
|
}
|
|
318
437
|
}
|
|
319
438
|
|
|
@@ -324,12 +443,18 @@ class AnimeCursor {
|
|
|
324
443
|
if (this.disabled) return;
|
|
325
444
|
|
|
326
445
|
this._onMouseMove = (e) => {
|
|
446
|
+
if (this.disabled) return;
|
|
447
|
+
|
|
327
448
|
const x = e.clientX;
|
|
328
449
|
const y = e.clientY;
|
|
329
450
|
|
|
330
451
|
this.cursorEl.style.left = x + 'px';
|
|
331
452
|
this.cursorEl.style.top = y + 'px';
|
|
332
453
|
|
|
454
|
+
if (this.cursorEl.dataset.animecursorHide) {
|
|
455
|
+
this.cursorEl.style.display = 'block';
|
|
456
|
+
}
|
|
457
|
+
|
|
333
458
|
if (this.debugEl) {
|
|
334
459
|
this.debugEl.style.left = x + 'px';
|
|
335
460
|
this.debugEl.style.top = y + 'px';
|
package/dist/anime-cursor.umd.js
CHANGED
|
@@ -5,12 +5,58 @@
|
|
|
5
5
|
})(this, (function () { 'use strict';
|
|
6
6
|
|
|
7
7
|
// AnimeCursor by github@ShuninYu
|
|
8
|
-
// v0.
|
|
8
|
+
// v0.3.0
|
|
9
|
+
|
|
10
|
+
// 静态变量存储唯一实例
|
|
11
|
+
let _instance = null;
|
|
9
12
|
|
|
10
13
|
class AnimeCursor {
|
|
11
14
|
|
|
15
|
+
static get instance() {
|
|
16
|
+
return _instance;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static destroy() {
|
|
20
|
+
if (_instance) {
|
|
21
|
+
_instance.destroy();
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static refresh() {
|
|
28
|
+
if (_instance) {
|
|
29
|
+
_instance.refresh();
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static disable() {
|
|
36
|
+
if (_instance) {
|
|
37
|
+
_instance.disable();
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static enable() {
|
|
44
|
+
if (_instance) {
|
|
45
|
+
_instance.enable();
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
12
51
|
constructor(options = {}) {
|
|
52
|
+
// 如果已有实例,直接返回它
|
|
53
|
+
if (_instance) {
|
|
54
|
+
console.warn('[AnimeCursor] AnimeCursor already exists.');
|
|
55
|
+
return _instance;
|
|
56
|
+
}
|
|
57
|
+
|
|
13
58
|
this.options = {
|
|
59
|
+
displayOnLoad: false,
|
|
14
60
|
enableTouch: false,
|
|
15
61
|
debug: false,
|
|
16
62
|
...options
|
|
@@ -29,12 +75,15 @@
|
|
|
29
75
|
this.cursorEl = null;
|
|
30
76
|
this.lastCursorType = null;
|
|
31
77
|
this.debugEl = null;
|
|
78
|
+
this.styleEl = null;
|
|
79
|
+
this._onMouseMove = null;
|
|
32
80
|
|
|
33
81
|
this._validateOptions();
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
|
|
82
|
+
this._injectPreload();
|
|
83
|
+
this._checkDomLoad();
|
|
84
|
+
|
|
85
|
+
// 保存实例引用
|
|
86
|
+
_instance = this;
|
|
38
87
|
}
|
|
39
88
|
|
|
40
89
|
isMouseLikeDevice() {
|
|
@@ -81,19 +130,28 @@
|
|
|
81
130
|
this.styleEl = null;
|
|
82
131
|
}
|
|
83
132
|
|
|
84
|
-
// 4 清理 data-cursor
|
|
133
|
+
// 4 清理 data-cursor(只清理由 AnimeCursor 添加的)
|
|
85
134
|
for (const cfg of Object.values(this.options.cursors)) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
135
|
+
// v0.2.1 添加检查:只有存在且为数组的 tags 才进行处理
|
|
136
|
+
if (cfg.tags && Array.isArray(cfg.tags)) {
|
|
137
|
+
cfg.tags.forEach(tag => {
|
|
138
|
+
document.querySelectorAll(tag).forEach(el => {
|
|
139
|
+
if (el.dataset.cursorBound) {
|
|
140
|
+
delete el.dataset.cursor;
|
|
141
|
+
delete el.dataset.cursorBound;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
91
144
|
});
|
|
92
|
-
}
|
|
145
|
+
}
|
|
93
146
|
}
|
|
94
147
|
|
|
95
148
|
// 5 重置状态
|
|
96
149
|
this.lastCursorType = null;
|
|
150
|
+
|
|
151
|
+
// 清除静态引用
|
|
152
|
+
if (_instance === this) {
|
|
153
|
+
_instance = null;
|
|
154
|
+
}
|
|
97
155
|
}
|
|
98
156
|
disable() {
|
|
99
157
|
if (this.disabled) return;
|
|
@@ -101,6 +159,7 @@
|
|
|
101
159
|
|
|
102
160
|
if (this.cursorEl) {
|
|
103
161
|
this.cursorEl.style.display = 'none';
|
|
162
|
+
this.styleEl.innerHTML = this.styleEl.innerHTML.replace('* {cursor: none !important;}', '');
|
|
104
163
|
console.log('[AnimeCursor] AnimeCursor disabled!');
|
|
105
164
|
}
|
|
106
165
|
}
|
|
@@ -110,6 +169,7 @@
|
|
|
110
169
|
|
|
111
170
|
if (this.cursorEl) {
|
|
112
171
|
this.cursorEl.style.display = '';
|
|
172
|
+
this.styleEl.innerHTML += '* {cursor: none; !important;}';
|
|
113
173
|
console.log('[AnimeCursor] AnimeCursor enabled!');
|
|
114
174
|
}
|
|
115
175
|
}
|
|
@@ -130,7 +190,7 @@
|
|
|
130
190
|
for (const [name, cfg] of Object.entries(this.options.cursors)) {
|
|
131
191
|
if (cfg.default === true) {
|
|
132
192
|
if (this.defaultCursorType) {
|
|
133
|
-
throw new Error('[AnimeCursor]
|
|
193
|
+
throw new Error('[AnimeCursor] There can only be one default cursor');
|
|
134
194
|
}
|
|
135
195
|
this.defaultCursorType = name;
|
|
136
196
|
}
|
|
@@ -144,14 +204,8 @@
|
|
|
144
204
|
throw new Error('AnimeCursor init failed');
|
|
145
205
|
}
|
|
146
206
|
});
|
|
147
|
-
|
|
148
|
-
if (!cfg.
|
|
149
|
-
if (!Array.isArray(cfg.tags) || cfg.tags.length === 0) {
|
|
150
|
-
console.error(`[AnimeCursor] non-default cursor "${name}" must define at least one tag`);
|
|
151
|
-
throw new Error('AnimeCursor init failed');
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (cfg.default && cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
|
|
207
|
+
|
|
208
|
+
if (cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
|
|
155
209
|
console.error(`[AnimeCursor] default cursor "${name}" 's tags must be an array if provided`);
|
|
156
210
|
throw new Error('AnimeCursor init failed');
|
|
157
211
|
}
|
|
@@ -163,6 +217,64 @@
|
|
|
163
217
|
}
|
|
164
218
|
}
|
|
165
219
|
|
|
220
|
+
// ----------------------------
|
|
221
|
+
// 等待 DOM 加载完成
|
|
222
|
+
// ----------------------------
|
|
223
|
+
_checkDomLoad() {
|
|
224
|
+
const init = () => {
|
|
225
|
+
this._injectHTML();
|
|
226
|
+
this._injectCSS();
|
|
227
|
+
this._bindElements();
|
|
228
|
+
this._bindMouse();
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (document.readyState === 'loading') {
|
|
232
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
233
|
+
} else {
|
|
234
|
+
init();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ----------------------------
|
|
239
|
+
// 插入光标图片预加载()
|
|
240
|
+
// ----------------------------
|
|
241
|
+
_injectPreload() {
|
|
242
|
+
if (this.disabled) return;
|
|
243
|
+
|
|
244
|
+
// 收集所有需要预加载的图片URL
|
|
245
|
+
const imageUrls = new Set();
|
|
246
|
+
|
|
247
|
+
// 遍历所有光标配置,提取图片URL
|
|
248
|
+
for (const cfg of Object.values(this.options.cursors)) {
|
|
249
|
+
if (cfg.image) {
|
|
250
|
+
imageUrls.add(cfg.image);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 为每个图片URL创建预加载标签
|
|
255
|
+
imageUrls.forEach(url => {
|
|
256
|
+
const link = document.createElement('link');
|
|
257
|
+
link.rel = 'preload';
|
|
258
|
+
link.as = 'image';
|
|
259
|
+
link.href = url;
|
|
260
|
+
|
|
261
|
+
// 可选:添加跨域处理(如果图片来自不同域名)
|
|
262
|
+
if (url.startsWith('http') && !url.startsWith(window.location.origin)) {
|
|
263
|
+
link.crossOrigin = 'anonymous';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
document.head.appendChild(link);
|
|
267
|
+
|
|
268
|
+
if (this.options.debug) {
|
|
269
|
+
console.info(`[AnimeCursor] Preloading image: ${url}`);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (this.options.debug && imageUrls.size > 0) {
|
|
274
|
+
console.info(`[AnimeCursor] Preloaded ${imageUrls.size} cursor image(s)`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
166
278
|
// ----------------------------
|
|
167
279
|
// 插入光标元素 HTML
|
|
168
280
|
// ----------------------------
|
|
@@ -172,7 +284,7 @@
|
|
|
172
284
|
const cursor = document.createElement('div');
|
|
173
285
|
cursor.id = 'anime-cursor';
|
|
174
286
|
|
|
175
|
-
// 如果debug选项存在,则添加debug
|
|
287
|
+
// 如果debug选项存在,则添加debug元素
|
|
176
288
|
if (this.options.debug) {
|
|
177
289
|
cursor.className = 'cursor-default cursor-debugmode';
|
|
178
290
|
const debuger = document.createElement('div');
|
|
@@ -181,6 +293,14 @@
|
|
|
181
293
|
this.debugEl = debuger;
|
|
182
294
|
}
|
|
183
295
|
else {cursor.className = 'cursor-default';}
|
|
296
|
+
|
|
297
|
+
// 检查是否设置初始化时显示光标
|
|
298
|
+
if (this.options.displayOnLoad) {
|
|
299
|
+
cursor.style.display = 'block';
|
|
300
|
+
} else {
|
|
301
|
+
cursor.style.display = 'none';
|
|
302
|
+
cursor.dataset.animecursorHide = 'true';
|
|
303
|
+
}
|
|
184
304
|
document.body.appendChild(cursor);
|
|
185
305
|
this.cursorEl = cursor;
|
|
186
306
|
}
|
|
@@ -192,13 +312,12 @@
|
|
|
192
312
|
if (this.disabled) return;
|
|
193
313
|
|
|
194
314
|
const style = document.createElement('style');
|
|
315
|
+
style.id = 'animecursor-styles';
|
|
195
316
|
let css = '';
|
|
196
317
|
|
|
197
318
|
/* 通用样式 */
|
|
198
319
|
css += `
|
|
199
|
-
* {
|
|
200
|
-
cursor: none !important;
|
|
201
|
-
}
|
|
320
|
+
* {cursor: none !important;}
|
|
202
321
|
#anime-cursor {
|
|
203
322
|
position: fixed;
|
|
204
323
|
top: 0;
|
|
@@ -306,7 +425,7 @@
|
|
|
306
425
|
if (this.disabled) return;
|
|
307
426
|
|
|
308
427
|
for (const [type, cfg] of Object.entries(this.options.cursors)) {
|
|
309
|
-
if (!cfg.tags || cfg.tags.length === 0) continue;
|
|
428
|
+
if (!cfg.tags || !Array.isArray(cfg.tags) || cfg.tags.length === 0) continue;
|
|
310
429
|
|
|
311
430
|
cfg.tags.forEach(tag => {
|
|
312
431
|
const tagName = tag.toUpperCase();
|
|
@@ -319,7 +438,7 @@
|
|
|
319
438
|
});
|
|
320
439
|
}
|
|
321
440
|
if (refresh) {
|
|
322
|
-
console.info('[AnimeCursor] refresh done
|
|
441
|
+
console.info('[AnimeCursor] refresh done');
|
|
323
442
|
}
|
|
324
443
|
}
|
|
325
444
|
|
|
@@ -330,12 +449,18 @@
|
|
|
330
449
|
if (this.disabled) return;
|
|
331
450
|
|
|
332
451
|
this._onMouseMove = (e) => {
|
|
452
|
+
if (this.disabled) return;
|
|
453
|
+
|
|
333
454
|
const x = e.clientX;
|
|
334
455
|
const y = e.clientY;
|
|
335
456
|
|
|
336
457
|
this.cursorEl.style.left = x + 'px';
|
|
337
458
|
this.cursorEl.style.top = y + 'px';
|
|
338
459
|
|
|
460
|
+
if (this.cursorEl.dataset.animecursorHide) {
|
|
461
|
+
this.cursorEl.style.display = 'block';
|
|
462
|
+
}
|
|
463
|
+
|
|
339
464
|
if (this.debugEl) {
|
|
340
465
|
this.debugEl.style.left = x + 'px';
|
|
341
466
|
this.debugEl.style.top = y + 'px';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(e,s){"object"==typeof exports&&"undefined"!=typeof module?module.exports=s():"function"==typeof define&&define.amd?define(s):(e="undefined"!=typeof globalThis?globalThis:e||self).AnimeCursor=s()}(this,function(){"use strict";return class{constructor(
|
|
1
|
+
!function(e,s){"object"==typeof exports&&"undefined"!=typeof module?module.exports=s():"function"==typeof define&&define.amd?define(s):(e="undefined"!=typeof globalThis?globalThis:e||self).AnimeCursor=s()}(this,function(){"use strict";let e=null;return class{static get instance(){return e}static destroy(){return!!e&&(e.destroy(),!0)}static refresh(){return!!e&&(e.refresh(),!0)}static disable(){return!!e&&(e.disable(),!0)}static enable(){return!!e&&(e.enable(),!0)}constructor(s={}){return e?(console.warn("[AnimeCursor] AnimeCursor already exists."),e):(this.options={displayOnLoad:!1,enableTouch:!1,debug:!1,...s},this.disabled=!1,this.options.enableTouch||this.isMouseLikeDevice()?(this.cursorEl=null,this.lastCursorType=null,this.debugEl=null,this.styleEl=null,this._onMouseMove=null,this._validateOptions(),this._injectPreload(),this._checkDomLoad(),void(e=this)):(this.disabled=!0,void(this.options.debug&&console.warn("[AnimeCursor] Touch device detected, cursor disabled."))))}isMouseLikeDevice(){if(!this.disabled)return window.matchMedia("(pointer: fine)").matches}refresh(){this.disabled||(this.options.debug&&console.info("[AnimeCursor] starting refresh..."),this._bindElements(!0))}destroy(){if(!this.disabled){this._onMouseMove&&(document.removeEventListener("mousemove",this._onMouseMove),this._onMouseMove=null),this.cursorEl&&(this.cursorEl.remove(),this.cursorEl=null),this.debugEl&&(this.debugEl.remove(),this.debugEl=null),this.styleEl&&(this.styleEl.remove(),this.styleEl=null);for(const e of Object.values(this.options.cursors))e.tags&&Array.isArray(e.tags)&&e.tags.forEach(e=>{document.querySelectorAll(e).forEach(e=>{e.dataset.cursorBound&&(delete e.dataset.cursor,delete e.dataset.cursorBound)})});this.lastCursorType=null,e===this&&(e=null)}}disable(){this.disabled||(this.disabled=!0,this.cursorEl&&(this.cursorEl.style.display="none",this.styleEl.innerHTML=this.styleEl.innerHTML.replace("* {cursor: none !important;}",""),console.log("[AnimeCursor] AnimeCursor disabled!")))}enable(){this.disabled&&(this.disabled=!1,this.cursorEl&&(this.cursorEl.style.display="",this.styleEl.innerHTML+="* {cursor: none; !important;}",console.log("[AnimeCursor] AnimeCursor enabled!")))}_validateOptions(){if(!this.disabled){if(!this.options||!this.options.cursors)throw console.error("[AnimeCursor] missing cursors set up"),new Error("AnimeCursor init failed");this.defaultCursorType=null;for(const[e,s]of Object.entries(this.options.cursors))if(!0===s.default){if(this.defaultCursorType)throw new Error("[AnimeCursor] There can only be one default cursor");this.defaultCursorType=e}for(const[e,s]of Object.entries(this.options.cursors)){if(["size","image"].forEach(t=>{if(void 0===s[t])throw console.error(`[AnimeCursor] cursor "${e}" missing required setting: ${t}`),new Error("AnimeCursor init failed")}),void 0!==s.tags&&!Array.isArray(s.tags))throw console.error(`[AnimeCursor] default cursor "${e}" 's tags must be an array if provided`),new Error("AnimeCursor init failed");if(void 0!==s.duration&&"number"!=typeof s.duration)throw console.error(`[AnimeCursor] cursor "${e}" 's duration must be a number(seconds)`),new Error("AnimeCursor init failed")}}}_checkDomLoad(){const e=()=>{this._injectHTML(),this._injectCSS(),this._bindElements(),this._bindMouse()};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",e):e()}_injectPreload(){if(this.disabled)return;const e=new Set;for(const s of Object.values(this.options.cursors))s.image&&e.add(s.image);e.forEach(e=>{const s=document.createElement("link");s.rel="preload",s.as="image",s.href=e,e.startsWith("http")&&!e.startsWith(window.location.origin)&&(s.crossOrigin="anonymous"),document.head.appendChild(s),this.options.debug&&console.info(`[AnimeCursor] Preloading image: ${e}`)}),this.options.debug&&e.size>0&&console.info(`[AnimeCursor] Preloaded ${e.size} cursor image(s)`)}_injectHTML(){if(this.disabled)return;const e=document.createElement("div");if(e.id="anime-cursor",this.options.debug){e.className="cursor-default cursor-debugmode";const s=document.createElement("div");s.className="anime-cursor-debug",document.body.appendChild(s),this.debugEl=s}else e.className="cursor-default";this.options.displayOnLoad?e.style.display="block":(e.style.display="none",e.dataset.animecursorHide="true"),document.body.appendChild(e),this.cursorEl=e}_injectCSS(){if(this.disabled)return;const e=document.createElement("style");e.id="animecursor-styles";let s="";s+=`\n * {cursor: none !important;}\n #anime-cursor {\n position: fixed;\n top: 0;\n left: 0;\n pointer-events: none;\n background-repeat: no-repeat;\n transform-origin: 0 0;\n transform-style: preserve-3d;\n z-index: ${this._getMaxZIndex()};\n }\n .cursor-debugmode {\n border: 1px solid green;\n }\n .anime-cursor-debug {\n position: fixed;\n top: 0;\n left: 0;\n width: fit-content;\n height: fit-content;\n padding: 5px;\n font-size: 16px;\n text-wrap: nowrap;\n color: red;\n pointer-events: none;\n overflow: visible;\n z-index: 2147483647;\n }\n .anime-cursor-debug::before {\n position: absolute;\n content: "";\n top: 0;\n left: 0;\n width: 100vw;\n height: 1px;\n background-color: red;\n }\n .anime-cursor-debug::after {\n position: absolute;\n content: "";\n top: 0;\n left: 0;\n width: 1px;\n height: 100vh;\n background-color: red;\n }\n `;for(const[e,o]of Object.entries(this.options.cursors)){const n=`.cursor-${e}`,r=o.size,i=o.frames,l=o.image,d=o.offset,a=o.zIndex,u=o.scale,c=l.toLowerCase().endsWith(".gif");var t;t=o.pixel?"pixelated":"auto",s+=`\n ${n} {\n width: ${r[0]}px;\n height: ${r[1]}px;\n background-image: url("${l}");\n image-rendering: ${t};\n ${u||d?`transform: ${[u&&`scale(${u[0]}, ${u[1]})`,d&&`translate(-${d[0]}px, -${d[1]}px)`].filter(Boolean).join(" ")};`:""}\n \n ${void 0!==a?`z-index:${a};`:""}\n }`;const h=o.duration;if(!c&&i>1&&"number"==typeof h){const t=`animecursor_${e}`;s+=`\n ${n} {\n animation: ${t} steps(${i}) ${h}s infinite ${o.pingpong?"alternate":""};\n }\n\n @keyframes ${t} {\n from { background-position: 0 0; }\n to { background-position: -${r[0]*i}px 0; }\n }\n `}}e.textContent=s,document.head.appendChild(e),this.styleEl=e}_bindElements(e){if(!this.disabled){for(const[e,s]of Object.entries(this.options.cursors))s.tags&&Array.isArray(s.tags)&&0!==s.tags.length&&s.tags.forEach(s=>{const t=s.toUpperCase();document.querySelectorAll(t).forEach(s=>{s.dataset.cursor||(s.dataset.cursor=e,s.dataset.cursorBound="true")})});e&&console.info("[AnimeCursor] refresh done")}}_bindMouse(){this.disabled||(this._onMouseMove=e=>{if(this.disabled)return;const s=e.clientX,t=e.clientY;this.cursorEl.style.left=s+"px",this.cursorEl.style.top=t+"px",this.cursorEl.dataset.animecursorHide&&(this.cursorEl.style.display="block"),this.debugEl&&(this.debugEl.style.left=s+"px",this.debugEl.style.top=t+"px");let o=null;const n=document.elementFromPoint(s,t);n&&n.dataset&&n.dataset.cursor?o=n.dataset.cursor:this.defaultCursorType&&(o=this.defaultCursorType),o&&(this.debugEl&&(this.debugEl.textContent=`(${s}px , ${t}px) ${o}`),o!==this.lastCursorType&&(this.debugEl?this.cursorEl.className=`cursor-${o} cursor-debugmode`:this.cursorEl.className=`cursor-${o}`,this.lastCursorType=o))},document.addEventListener("mousemove",this._onMouseMove),console.log("[AnimeCursor] AnimeCursor setted up."))}_getMaxZIndex(){if(!this.disabled)return 2147483646}}});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anime-cursor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A lightweight JavaScript library for animated custom cursors",
|
|
5
5
|
"main": "dist/anime-cursor.umd.js",
|
|
6
6
|
"module": "dist/anime-cursor.esm.js",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"author": "ShuninYu",
|
|
24
24
|
"license": "MIT",
|
|
25
|
+
"homepage": "https://animecursor.js.org",
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@rollup/plugin-terser": "^0.4.4",
|
|
27
28
|
"rollup": "^4.56.0"
|