anime-cursor 2.0.1 → 2.1.0
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 +34 -2
- package/dist/anime-cursor.esm.js +64 -12
- package/dist/anime-cursor.umd.js +64 -12
- package/dist/anime-cursor.umd.min.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ AnimeCursor has no dependencies on any frameworks, making it suitable for person
|
|
|
33
33
|
### CDN
|
|
34
34
|
|
|
35
35
|
```html
|
|
36
|
-
<script src="https://cdn.jsdelivr.net/npm/anime-cursor@v2.
|
|
36
|
+
<script src="https://cdn.jsdelivr.net/npm/anime-cursor@v2.1.0/dist/anime-cursor.umd.min.js"></script>
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
### npm
|
|
@@ -125,6 +125,7 @@ An object that defines all cursor types. Each key is a cursor name (any string).
|
|
|
125
125
|
|
|
126
126
|
| Option | Type | Default | Description |
|
|
127
127
|
| ------------------ | ------- | -------------------------------------- | ------------------------------------------------------------ |
|
|
128
|
+
| `combineAnimations` | boolean | `false` | Automatically merges cursor animations with element‑defined animations (via `data-ac-animation`). When enabled, any element with the `data-ac-animation` attribute will have its own CSS animation combined with the cursor animation, allowing both to run simultaneously without overriding each other. |
|
|
128
129
|
| `debug` | boolean | `false` | Enables a debug overlay showing current cursor type and coordinates. |
|
|
129
130
|
| `enableTouch` | boolean | `false` | Allow animated cursors on touch devices (detected automatically, disabled by default). |
|
|
130
131
|
| `fallbackCursor` | string | `'auto'` | Global fallback cursor for all animated cursors (e.g., `'auto'`, `'pointer'`). |
|
|
@@ -149,6 +150,21 @@ An object that defines all cursor types. Each key is a cursor name (any string).
|
|
|
149
150
|
- **Variable Speed Animation**: `frames` and `duration` are arrays of equal length. Each segment defines a number of frames and the time to play them. The frames are evenly distributed within each segment.
|
|
150
151
|
- **Static Cursor**: If `frames` or `duration` is missing, or if they are invalid (e.g., non-positive numbers, mismatched array lengths), the cursor is treated as static (only the first image is used). A warning will be logged in the console.
|
|
151
152
|
|
|
153
|
+
### 🎞️ Combining Animations
|
|
154
|
+
|
|
155
|
+
If your elements already have their own CSS animations (e.g., `animation: spin 2s infinite`), they may override AnimeCursor's cursor animations. To make both play together, enable the `combineAnimations` global option and add a `data-ac-animation` attribute to the element with your animation definition(s). AnimeCursor will then automatically combine them.
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
|
|
159
|
+
```html
|
|
160
|
+
<button data-ac-animation="mySpin 2s linear infinite">Click Me</button>
|
|
161
|
+
new AnimeCursor({
|
|
162
|
+
combineAnimations: true,
|
|
163
|
+
cursors: { ... }
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
Multiple animations can be listed, separated by commas.
|
|
167
|
+
|
|
152
168
|
### 🧩 Tagging Mechanism
|
|
153
169
|
|
|
154
170
|
AnimeCursor automatically adds the appropriate cursor style to elements based on `tags` and `data-cursor` attributes:
|
|
@@ -202,7 +218,7 @@ AnimeCursor 无需依赖任何框架,适合个人网站、创意作品集以
|
|
|
202
218
|
### CDN
|
|
203
219
|
|
|
204
220
|
```html
|
|
205
|
-
<script src="https://cdn.jsdelivr.net/npm/anime-cursor@v2.
|
|
221
|
+
<script src="https://cdn.jsdelivr.net/npm/anime-cursor@v2.1.0/dist/anime-cursor.umd.min.js"></script>
|
|
206
222
|
```
|
|
207
223
|
|
|
208
224
|
### npm
|
|
@@ -294,6 +310,7 @@ new AnimeCursor({
|
|
|
294
310
|
|
|
295
311
|
| 选项 | 类型 | 默认值 | 描述 |
|
|
296
312
|
| ------------------ | ------- | -------------------------------------- | ------------------------------------------------------------ |
|
|
313
|
+
| `combineAnimations` | boolean | `false` | 自动将光标动画与元素自身定义的动画(通过 `data-ac-animation`)合并。开启后,任何带有 `data-ac-animation` 属性的元素,其光标动画与用户动画会同时播放,互不覆盖。 |
|
|
297
314
|
| `debug` | boolean | `false` | 启用调试浮层,显示当前光标类型和坐标。 |
|
|
298
315
|
| `enableTouch` | boolean | `false` | 允许在触屏设备上显示动画光标(默认自动检测并禁用)。 |
|
|
299
316
|
| `fallbackCursor` | string | `'auto'` | 全局备用光标类型,用于所有动画光标(如 `'auto'`、`'pointer'`)。 |
|
|
@@ -318,6 +335,21 @@ new AnimeCursor({
|
|
|
318
335
|
- **变速动画**:`frames` 和 `duration` 均为等长数组。每个片段指定帧数和该片段的时长,片段内的帧均匀分布。
|
|
319
336
|
- **静态光标**:如果缺少 `frames` 或 `duration`,或者设置无效(如非正数、数组长度不匹配),则光标被视为静态(仅使用第一帧图片)。控制台会输出警告。
|
|
320
337
|
|
|
338
|
+
### 🎞️ 组合动画
|
|
339
|
+
|
|
340
|
+
如果元素自身已经拥有 CSS 动画(例如 `animation: spin 2s infinite`),这些动画可能会覆盖 AnimeCursor 的光标动画。要让两者同时播放,请启用 `combineAnimations` 全局选项,并为元素添加 `data-ac-animation` 属性,将你的动画定义写入其中。AnimeCursor 会自动将两者组合。
|
|
341
|
+
|
|
342
|
+
示例:
|
|
343
|
+
|
|
344
|
+
```html
|
|
345
|
+
<button data-ac-animation="mySpin 2s linear infinite">点我</button>
|
|
346
|
+
new AnimeCursor({
|
|
347
|
+
combineAnimations: true,
|
|
348
|
+
cursors: { ... }
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
多个动画可用逗号分隔。
|
|
352
|
+
|
|
321
353
|
### 🧩 标记机制
|
|
322
354
|
|
|
323
355
|
AnimeCursor 会根据 `tags` 和 `data-cursor` 自动应用光标样式:
|
package/dist/anime-cursor.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// AnimeCursor by github@ShuninYu
|
|
2
|
-
// v2.0
|
|
2
|
+
// v2.1.0
|
|
3
3
|
|
|
4
4
|
let _instance = null;
|
|
5
5
|
|
|
@@ -49,13 +49,16 @@ class AnimeCursor {
|
|
|
49
49
|
this.options = {
|
|
50
50
|
debug: false,
|
|
51
51
|
enableTouch: false,
|
|
52
|
-
fallbackCursor: 'auto',
|
|
53
|
-
excludeSelectors: 'input, textarea, [contenteditable]',
|
|
52
|
+
fallbackCursor: 'auto',
|
|
53
|
+
excludeSelectors: 'input, textarea, [contenteditable]',
|
|
54
|
+
combineAnimations: false, // 是否自动组合用户动画
|
|
54
55
|
...options
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
this.disabled = false;
|
|
58
59
|
this.cursors = this.options.cursors || {};
|
|
60
|
+
this.cursorAnimationStrings = {}; // 存储每个光标类型的动画字符串
|
|
61
|
+
this.combinedRules = new Map(); // 存储已生成的组合类名
|
|
59
62
|
|
|
60
63
|
// 检查是否应启用(触摸设备且未强制启用则禁用)
|
|
61
64
|
if (!this.options.enableTouch && !this.isMouseLikeDevice()) {
|
|
@@ -77,12 +80,10 @@ class AnimeCursor {
|
|
|
77
80
|
_instance = this;
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
// 判断是否鼠标设备
|
|
81
83
|
isMouseLikeDevice() {
|
|
82
84
|
return window.matchMedia('(pointer: fine)').matches;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
// 验证配置(修改点:默认光标可选)
|
|
86
87
|
_validateOptions() {
|
|
87
88
|
if (this.disabled) return;
|
|
88
89
|
|
|
@@ -92,12 +93,11 @@ class AnimeCursor {
|
|
|
92
93
|
|
|
93
94
|
let hasDefault = false;
|
|
94
95
|
for (const [name, cfg] of Object.entries(this.cursors)) {
|
|
95
|
-
// 检查必填项
|
|
96
96
|
if (!cfg.image) {
|
|
97
97
|
throw new Error(`[AnimeCursor] Cursor "${name}" missing required setting: image`);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
// 处理 frames 和 duration
|
|
100
|
+
// 处理 frames 和 duration
|
|
101
101
|
if (cfg.frames !== undefined && cfg.duration !== undefined) {
|
|
102
102
|
const framesType = typeof cfg.frames;
|
|
103
103
|
const durationType = typeof cfg.duration;
|
|
@@ -157,11 +157,9 @@ class AnimeCursor {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
// 不再强制要求默认光标
|
|
161
160
|
this.defaultCursorName = hasDefault ? Object.keys(this.cursors).find(name => this.cursors[name].default) : null;
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
// 预加载所有图片
|
|
165
163
|
_preloadImages() {
|
|
166
164
|
const images = new Set();
|
|
167
165
|
for (const cfg of Object.values(this.cursors)) {
|
|
@@ -183,7 +181,6 @@ class AnimeCursor {
|
|
|
183
181
|
}
|
|
184
182
|
}
|
|
185
183
|
|
|
186
|
-
// 根据配置生成所有帧的 URL 数组
|
|
187
184
|
_getFrameUrls(cfg) {
|
|
188
185
|
let totalFrames = 1;
|
|
189
186
|
if (cfg.frames !== undefined) {
|
|
@@ -247,7 +244,7 @@ class AnimeCursor {
|
|
|
247
244
|
}
|
|
248
245
|
}
|
|
249
246
|
|
|
250
|
-
//
|
|
247
|
+
// 核心注入样式
|
|
251
248
|
_injectStyles() {
|
|
252
249
|
if (this.disabled) return;
|
|
253
250
|
|
|
@@ -275,6 +272,7 @@ class AnimeCursor {
|
|
|
275
272
|
((Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) ||
|
|
276
273
|
(typeof cfg.frames === 'number' && typeof cfg.duration === 'number'));
|
|
277
274
|
|
|
275
|
+
let cursorAnimation = '';
|
|
278
276
|
if (hasAnimation && frameCount > 1) {
|
|
279
277
|
const keyframeName = `ac_anim_${name}`;
|
|
280
278
|
let keyframesCss = `@keyframes ${keyframeName} {\n`;
|
|
@@ -290,11 +288,15 @@ class AnimeCursor {
|
|
|
290
288
|
|
|
291
289
|
const totalDuration = Array.isArray(cfg.duration) ? cfg.duration.reduce((a, b) => a + b, 0) : cfg.duration;
|
|
292
290
|
const animation = `${keyframeName} ${totalDuration}s steps(1) infinite ${cfg.pingpong ? 'alternate' : ''}`;
|
|
291
|
+
cursorAnimation = animation;
|
|
293
292
|
css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; animation: ${animation}; }\n`;
|
|
294
293
|
} else {
|
|
295
294
|
css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; }\n`;
|
|
296
295
|
}
|
|
297
296
|
|
|
297
|
+
this.cursorAnimationStrings[name] = cursorAnimation;
|
|
298
|
+
|
|
299
|
+
// 标签和 data-cursor 规则
|
|
298
300
|
if (cfg.tags && cfg.tags.length) {
|
|
299
301
|
const selector = cfg.tags.join(', ');
|
|
300
302
|
css += `${selector} { ${this._buildCursorCss(name, cfg)} }\n`;
|
|
@@ -306,6 +308,33 @@ class AnimeCursor {
|
|
|
306
308
|
css += `${this.options.excludeSelectors} { cursor: text !important; animation: none !important; }\n`;
|
|
307
309
|
}
|
|
308
310
|
|
|
311
|
+
// 自动组合动画(新功能)
|
|
312
|
+
if (this.options.combineAnimations) {
|
|
313
|
+
const elements = document.querySelectorAll('[data-ac-animation]');
|
|
314
|
+
for (const el of elements) {
|
|
315
|
+
const userAnim = el.getAttribute('data-ac-animation');
|
|
316
|
+
if (!userAnim) continue;
|
|
317
|
+
|
|
318
|
+
// 确定该元素应该使用哪个光标
|
|
319
|
+
let cursorName = this._getCursorTypeForElement(el);
|
|
320
|
+
if (!cursorName) continue;
|
|
321
|
+
|
|
322
|
+
const cursorAnim = this.cursorAnimationStrings[cursorName];
|
|
323
|
+
if (!cursorAnim) continue;
|
|
324
|
+
|
|
325
|
+
// 生成唯一标识
|
|
326
|
+
const key = `${cursorName}:${userAnim}`;
|
|
327
|
+
if (!this.combinedRules.has(key)) {
|
|
328
|
+
const hash = this._simpleHash(key);
|
|
329
|
+
const combinedClass = `ac-combined-${hash}`;
|
|
330
|
+
css += `.${combinedClass} { animation: ${cursorAnim}, ${userAnim}; }\n`;
|
|
331
|
+
this.combinedRules.set(key, combinedClass);
|
|
332
|
+
}
|
|
333
|
+
const combinedClass = this.combinedRules.get(key);
|
|
334
|
+
el.classList.add(combinedClass);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
309
338
|
css += `body.animecursor-disabled * { cursor: auto !important; animation: none !important; }\n`;
|
|
310
339
|
|
|
311
340
|
style.textContent = css;
|
|
@@ -313,6 +342,19 @@ class AnimeCursor {
|
|
|
313
342
|
this.styleEl = style;
|
|
314
343
|
}
|
|
315
344
|
|
|
345
|
+
// 获取元素对应的光标类型(复用 debug 逻辑)
|
|
346
|
+
_getCursorTypeForElement(el) {
|
|
347
|
+
if (el.dataset.cursor && this.cursors[el.dataset.cursor]) {
|
|
348
|
+
return el.dataset.cursor;
|
|
349
|
+
}
|
|
350
|
+
for (const [name, cfg] of Object.entries(this.cursors)) {
|
|
351
|
+
if (cfg.tags && cfg.tags.some(tag => el.matches(tag))) {
|
|
352
|
+
return name;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return this.defaultCursorName; // 可能为 null
|
|
356
|
+
}
|
|
357
|
+
|
|
316
358
|
_buildKeyframes(cfg, frameUrls) {
|
|
317
359
|
let frames = cfg.frames;
|
|
318
360
|
let durations = cfg.duration;
|
|
@@ -364,6 +406,16 @@ class AnimeCursor {
|
|
|
364
406
|
return css;
|
|
365
407
|
}
|
|
366
408
|
|
|
409
|
+
_simpleHash(str) {
|
|
410
|
+
let hash = 0;
|
|
411
|
+
for (let i = 0; i < str.length; i++) {
|
|
412
|
+
const char = str.charCodeAt(i);
|
|
413
|
+
hash = ((hash << 5) - hash) + char;
|
|
414
|
+
hash |= 0;
|
|
415
|
+
}
|
|
416
|
+
return Math.abs(hash).toString(36);
|
|
417
|
+
}
|
|
418
|
+
|
|
367
419
|
_initDebug() {
|
|
368
420
|
const debugDiv = document.createElement('div');
|
|
369
421
|
debugDiv.className = 'animecursor-debug';
|
|
@@ -399,7 +451,6 @@ class AnimeCursor {
|
|
|
399
451
|
}
|
|
400
452
|
}
|
|
401
453
|
}
|
|
402
|
-
// 如果没有匹配到任何自定义光标,且没有默认光标,则显示 "native"
|
|
403
454
|
if (!cursorType && !this.defaultCursorName) {
|
|
404
455
|
cursorType = 'native';
|
|
405
456
|
} else if (!cursorType && this.defaultCursorName) {
|
|
@@ -418,6 +469,7 @@ class AnimeCursor {
|
|
|
418
469
|
refresh() {
|
|
419
470
|
if (this.disabled) return;
|
|
420
471
|
if (this.styleEl) this.styleEl.remove();
|
|
472
|
+
this.combinedRules.clear();
|
|
421
473
|
this._injectStyles();
|
|
422
474
|
if (this.options.debug) {
|
|
423
475
|
if (this.debugEl) this.debugEl.remove();
|
package/dist/anime-cursor.umd.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
})(this, (function () { 'use strict';
|
|
6
6
|
|
|
7
7
|
// AnimeCursor by github@ShuninYu
|
|
8
|
-
// v2.0
|
|
8
|
+
// v2.1.0
|
|
9
9
|
|
|
10
10
|
let _instance = null;
|
|
11
11
|
|
|
@@ -55,13 +55,16 @@
|
|
|
55
55
|
this.options = {
|
|
56
56
|
debug: false,
|
|
57
57
|
enableTouch: false,
|
|
58
|
-
fallbackCursor: 'auto',
|
|
59
|
-
excludeSelectors: 'input, textarea, [contenteditable]',
|
|
58
|
+
fallbackCursor: 'auto',
|
|
59
|
+
excludeSelectors: 'input, textarea, [contenteditable]',
|
|
60
|
+
combineAnimations: false, // 是否自动组合用户动画
|
|
60
61
|
...options
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
this.disabled = false;
|
|
64
65
|
this.cursors = this.options.cursors || {};
|
|
66
|
+
this.cursorAnimationStrings = {}; // 存储每个光标类型的动画字符串
|
|
67
|
+
this.combinedRules = new Map(); // 存储已生成的组合类名
|
|
65
68
|
|
|
66
69
|
// 检查是否应启用(触摸设备且未强制启用则禁用)
|
|
67
70
|
if (!this.options.enableTouch && !this.isMouseLikeDevice()) {
|
|
@@ -83,12 +86,10 @@
|
|
|
83
86
|
_instance = this;
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
// 判断是否鼠标设备
|
|
87
89
|
isMouseLikeDevice() {
|
|
88
90
|
return window.matchMedia('(pointer: fine)').matches;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
// 验证配置(修改点:默认光标可选)
|
|
92
93
|
_validateOptions() {
|
|
93
94
|
if (this.disabled) return;
|
|
94
95
|
|
|
@@ -98,12 +99,11 @@
|
|
|
98
99
|
|
|
99
100
|
let hasDefault = false;
|
|
100
101
|
for (const [name, cfg] of Object.entries(this.cursors)) {
|
|
101
|
-
// 检查必填项
|
|
102
102
|
if (!cfg.image) {
|
|
103
103
|
throw new Error(`[AnimeCursor] Cursor "${name}" missing required setting: image`);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// 处理 frames 和 duration
|
|
106
|
+
// 处理 frames 和 duration
|
|
107
107
|
if (cfg.frames !== undefined && cfg.duration !== undefined) {
|
|
108
108
|
const framesType = typeof cfg.frames;
|
|
109
109
|
const durationType = typeof cfg.duration;
|
|
@@ -163,11 +163,9 @@
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
// 不再强制要求默认光标
|
|
167
166
|
this.defaultCursorName = hasDefault ? Object.keys(this.cursors).find(name => this.cursors[name].default) : null;
|
|
168
167
|
}
|
|
169
168
|
|
|
170
|
-
// 预加载所有图片
|
|
171
169
|
_preloadImages() {
|
|
172
170
|
const images = new Set();
|
|
173
171
|
for (const cfg of Object.values(this.cursors)) {
|
|
@@ -189,7 +187,6 @@
|
|
|
189
187
|
}
|
|
190
188
|
}
|
|
191
189
|
|
|
192
|
-
// 根据配置生成所有帧的 URL 数组
|
|
193
190
|
_getFrameUrls(cfg) {
|
|
194
191
|
let totalFrames = 1;
|
|
195
192
|
if (cfg.frames !== undefined) {
|
|
@@ -253,7 +250,7 @@
|
|
|
253
250
|
}
|
|
254
251
|
}
|
|
255
252
|
|
|
256
|
-
//
|
|
253
|
+
// 核心注入样式
|
|
257
254
|
_injectStyles() {
|
|
258
255
|
if (this.disabled) return;
|
|
259
256
|
|
|
@@ -281,6 +278,7 @@
|
|
|
281
278
|
((Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) ||
|
|
282
279
|
(typeof cfg.frames === 'number' && typeof cfg.duration === 'number'));
|
|
283
280
|
|
|
281
|
+
let cursorAnimation = '';
|
|
284
282
|
if (hasAnimation && frameCount > 1) {
|
|
285
283
|
const keyframeName = `ac_anim_${name}`;
|
|
286
284
|
let keyframesCss = `@keyframes ${keyframeName} {\n`;
|
|
@@ -296,11 +294,15 @@
|
|
|
296
294
|
|
|
297
295
|
const totalDuration = Array.isArray(cfg.duration) ? cfg.duration.reduce((a, b) => a + b, 0) : cfg.duration;
|
|
298
296
|
const animation = `${keyframeName} ${totalDuration}s steps(1) infinite ${cfg.pingpong ? 'alternate' : ''}`;
|
|
297
|
+
cursorAnimation = animation;
|
|
299
298
|
css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; animation: ${animation}; }\n`;
|
|
300
299
|
} else {
|
|
301
300
|
css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; }\n`;
|
|
302
301
|
}
|
|
303
302
|
|
|
303
|
+
this.cursorAnimationStrings[name] = cursorAnimation;
|
|
304
|
+
|
|
305
|
+
// 标签和 data-cursor 规则
|
|
304
306
|
if (cfg.tags && cfg.tags.length) {
|
|
305
307
|
const selector = cfg.tags.join(', ');
|
|
306
308
|
css += `${selector} { ${this._buildCursorCss(name, cfg)} }\n`;
|
|
@@ -312,6 +314,33 @@
|
|
|
312
314
|
css += `${this.options.excludeSelectors} { cursor: text !important; animation: none !important; }\n`;
|
|
313
315
|
}
|
|
314
316
|
|
|
317
|
+
// 自动组合动画(新功能)
|
|
318
|
+
if (this.options.combineAnimations) {
|
|
319
|
+
const elements = document.querySelectorAll('[data-ac-animation]');
|
|
320
|
+
for (const el of elements) {
|
|
321
|
+
const userAnim = el.getAttribute('data-ac-animation');
|
|
322
|
+
if (!userAnim) continue;
|
|
323
|
+
|
|
324
|
+
// 确定该元素应该使用哪个光标
|
|
325
|
+
let cursorName = this._getCursorTypeForElement(el);
|
|
326
|
+
if (!cursorName) continue;
|
|
327
|
+
|
|
328
|
+
const cursorAnim = this.cursorAnimationStrings[cursorName];
|
|
329
|
+
if (!cursorAnim) continue;
|
|
330
|
+
|
|
331
|
+
// 生成唯一标识
|
|
332
|
+
const key = `${cursorName}:${userAnim}`;
|
|
333
|
+
if (!this.combinedRules.has(key)) {
|
|
334
|
+
const hash = this._simpleHash(key);
|
|
335
|
+
const combinedClass = `ac-combined-${hash}`;
|
|
336
|
+
css += `.${combinedClass} { animation: ${cursorAnim}, ${userAnim}; }\n`;
|
|
337
|
+
this.combinedRules.set(key, combinedClass);
|
|
338
|
+
}
|
|
339
|
+
const combinedClass = this.combinedRules.get(key);
|
|
340
|
+
el.classList.add(combinedClass);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
315
344
|
css += `body.animecursor-disabled * { cursor: auto !important; animation: none !important; }\n`;
|
|
316
345
|
|
|
317
346
|
style.textContent = css;
|
|
@@ -319,6 +348,19 @@
|
|
|
319
348
|
this.styleEl = style;
|
|
320
349
|
}
|
|
321
350
|
|
|
351
|
+
// 获取元素对应的光标类型(复用 debug 逻辑)
|
|
352
|
+
_getCursorTypeForElement(el) {
|
|
353
|
+
if (el.dataset.cursor && this.cursors[el.dataset.cursor]) {
|
|
354
|
+
return el.dataset.cursor;
|
|
355
|
+
}
|
|
356
|
+
for (const [name, cfg] of Object.entries(this.cursors)) {
|
|
357
|
+
if (cfg.tags && cfg.tags.some(tag => el.matches(tag))) {
|
|
358
|
+
return name;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return this.defaultCursorName; // 可能为 null
|
|
362
|
+
}
|
|
363
|
+
|
|
322
364
|
_buildKeyframes(cfg, frameUrls) {
|
|
323
365
|
let frames = cfg.frames;
|
|
324
366
|
let durations = cfg.duration;
|
|
@@ -370,6 +412,16 @@
|
|
|
370
412
|
return css;
|
|
371
413
|
}
|
|
372
414
|
|
|
415
|
+
_simpleHash(str) {
|
|
416
|
+
let hash = 0;
|
|
417
|
+
for (let i = 0; i < str.length; i++) {
|
|
418
|
+
const char = str.charCodeAt(i);
|
|
419
|
+
hash = ((hash << 5) - hash) + char;
|
|
420
|
+
hash |= 0;
|
|
421
|
+
}
|
|
422
|
+
return Math.abs(hash).toString(36);
|
|
423
|
+
}
|
|
424
|
+
|
|
373
425
|
_initDebug() {
|
|
374
426
|
const debugDiv = document.createElement('div');
|
|
375
427
|
debugDiv.className = 'animecursor-debug';
|
|
@@ -405,7 +457,6 @@
|
|
|
405
457
|
}
|
|
406
458
|
}
|
|
407
459
|
}
|
|
408
|
-
// 如果没有匹配到任何自定义光标,且没有默认光标,则显示 "native"
|
|
409
460
|
if (!cursorType && !this.defaultCursorName) {
|
|
410
461
|
cursorType = 'native';
|
|
411
462
|
} else if (!cursorType && this.defaultCursorName) {
|
|
@@ -424,6 +475,7 @@
|
|
|
424
475
|
refresh() {
|
|
425
476
|
if (this.disabled) return;
|
|
426
477
|
if (this.styleEl) this.styleEl.remove();
|
|
478
|
+
this.combinedRules.clear();
|
|
427
479
|
this._injectStyles();
|
|
428
480
|
if (this.options.debug) {
|
|
429
481
|
if (this.debugEl) this.debugEl.remove();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).AnimeCursor=t()}(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(t={}){return e?(console.warn("[AnimeCursor] Instance already exists, returning existing one"),e):(this.options={debug:!1,enableTouch:!1,fallbackCursor:"auto",excludeSelectors:"input, textarea, [contenteditable]",...t},this.disabled=!1,this.cursors=this.options.cursors||{},this.options.enableTouch||this.isMouseLikeDevice()?(this.styleEl=null,this.debugEl=null,this._onMouseMove=null,this._validateOptions(),this._preloadImages(),this._checkDomLoad(),void(e=this)):(this.disabled=!0,void(this.options.debug&&console.warn("[AnimeCursor] Touch device detected, cursor animations disabled"))))}isMouseLikeDevice(){return window.matchMedia("(pointer: fine)").matches}_validateOptions(){if(this.disabled)return;if(!this.cursors||0===Object.keys(this.cursors).length)throw new Error("[AnimeCursor] At least one cursor must be defined");let e=!1;for(const[t,r]of Object.entries(this.cursors)){if(!r.image)throw new Error(`[AnimeCursor] Cursor "${t}" missing required setting: image`);if(void 0!==r.frames&&void 0!==r.duration){if(typeof r.frames!==typeof r.duration)console.warn(`[AnimeCursor] Cursor "${t}" has mismatched types for frames and duration, treating as static cursor`),delete r.frames,delete r.duration;else if(Array.isArray(r.frames)&&Array.isArray(r.duration))if(r.frames.length!==r.duration.length)console.warn(`[AnimeCursor] Cursor "${t}" frames and duration arrays have different lengths, treating as static cursor`),delete r.frames,delete r.duration;else{for(let e of r.frames)if(!Number.isInteger(e)||e<=0){console.warn(`[AnimeCursor] Cursor "${t}" frames array contains invalid value, treating as static cursor`),delete r.frames,delete r.duration;break}for(let e of r.duration)if("number"!=typeof e||e<=0){console.warn(`[AnimeCursor] Cursor "${t}" duration array contains invalid value, treating as static cursor`),delete r.frames,delete r.duration;break}}else"number"==typeof r.frames&&"number"==typeof r.duration?(r.frames<=0||r.duration<=0)&&(console.warn(`[AnimeCursor] Cursor "${t}" frames or duration <= 0, treating as static cursor`),delete r.frames,delete r.duration):(console.warn(`[AnimeCursor] Cursor "${t}" frames and duration must be both numbers or both arrays, treating as static cursor`),delete r.frames,delete r.duration)}else void 0===r.frames&&void 0===r.duration||(console.warn(`[AnimeCursor] Cursor "${t}" has only frames or duration defined, treating as static cursor`),delete r.frames,delete r.duration);if(r.tags&&!Array.isArray(r.tags))throw new Error(`[AnimeCursor] Cursor "${t}" tags must be an array`);if(r.default){if(e)throw new Error("[AnimeCursor] Only one default cursor allowed");e=!0}if(r.offset&&(!Array.isArray(r.offset)||2!==r.offset.length))throw new Error(`[AnimeCursor] Cursor "${t}" offset must be [x, y] array`)}this.defaultCursorName=e?Object.keys(this.cursors).find(e=>this.cursors[e].default):null}_preloadImages(){const e=new Set;for(const t of Object.values(this.cursors)){this._getFrameUrls(t).forEach(t=>e.add(t))}e.forEach(e=>{const t=document.createElement("link");t.rel="preload",t.as="image",t.href=e,e.startsWith("http")&&!e.startsWith(window.location.origin)&&(t.crossOrigin="anonymous"),document.head.appendChild(t)}),this.options.debug&&e.size&&console.info(`[AnimeCursor] Preloaded ${e.size} cursor images`)}_getFrameUrls(e){let t=1;void 0!==e.frames&&(Array.isArray(e.frames)?t=e.frames.reduce((e,t)=>e+t,0):"number"==typeof e.frames&&(t=e.frames));const{image:r}=e;if(1===t)return[r];const{prefix:s,suffix:o,startNum:i,numFormat:n,ext:a}=this._parseImagePattern(r),u=[];for(let e=0;e<t;e++){const t=i+e,r=`${s}${n?this._formatNumber(t,n):t}${o}${a}`;u.push(r)}return u}_parseImagePattern(e){const t=e.match(/\.[^.]+$/),r=t?t[0]:"",s=e.slice(0,-r.length),o=s.match(/(\d+)(?!.*\d)/);if(!o)return{prefix:s+"_",suffix:"",startNum:1,numFormat:null,ext:r};const i=o[0],n=parseInt(i,10),a=i.length;return{prefix:s.slice(0,o.index),suffix:s.slice(o.index+i.length),startNum:n,numFormat:a,ext:r}}_formatNumber(e,t){return String(e).padStart(t,"0")}_checkDomLoad(){const e=()=>{this._injectStyles(),this.options.debug&&this._initDebug(),console.log("[AnimeCursor] Initialization complete")};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",e):e()}_injectStyles(){if(this.disabled)return;const e=document.createElement("style");e.id="animecursor-styles";let t="";if(this.defaultCursorName){const e=this.cursors[this.defaultCursorName];t+=`* { ${this._buildCursorCss(this.defaultCursorName,e)} }\n`}for(const[e,r]of Object.entries(this.cursors)){const s=`.ac-cursor-${e}`,o=r.offset||[0,0],i=r.fallback||this.options.fallbackCursor,n=this._getFrameUrls(r),a=n.length;if(void 0!==r.frames&&void 0!==r.duration&&(Array.isArray(r.frames)&&Array.isArray(r.duration)||"number"==typeof r.frames&&"number"==typeof r.duration)&&a>1){const a=`ac_anim_${e}`;let
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).AnimeCursor=t()}(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(t={}){return e?(console.warn("[AnimeCursor] Instance already exists, returning existing one"),e):(this.options={debug:!1,enableTouch:!1,fallbackCursor:"auto",excludeSelectors:"input, textarea, [contenteditable]",combineAnimations:!1,...t},this.disabled=!1,this.cursors=this.options.cursors||{},this.cursorAnimationStrings={},this.combinedRules=new Map,this.options.enableTouch||this.isMouseLikeDevice()?(this.styleEl=null,this.debugEl=null,this._onMouseMove=null,this._validateOptions(),this._preloadImages(),this._checkDomLoad(),void(e=this)):(this.disabled=!0,void(this.options.debug&&console.warn("[AnimeCursor] Touch device detected, cursor animations disabled"))))}isMouseLikeDevice(){return window.matchMedia("(pointer: fine)").matches}_validateOptions(){if(this.disabled)return;if(!this.cursors||0===Object.keys(this.cursors).length)throw new Error("[AnimeCursor] At least one cursor must be defined");let e=!1;for(const[t,r]of Object.entries(this.cursors)){if(!r.image)throw new Error(`[AnimeCursor] Cursor "${t}" missing required setting: image`);if(void 0!==r.frames&&void 0!==r.duration){if(typeof r.frames!==typeof r.duration)console.warn(`[AnimeCursor] Cursor "${t}" has mismatched types for frames and duration, treating as static cursor`),delete r.frames,delete r.duration;else if(Array.isArray(r.frames)&&Array.isArray(r.duration))if(r.frames.length!==r.duration.length)console.warn(`[AnimeCursor] Cursor "${t}" frames and duration arrays have different lengths, treating as static cursor`),delete r.frames,delete r.duration;else{for(let e of r.frames)if(!Number.isInteger(e)||e<=0){console.warn(`[AnimeCursor] Cursor "${t}" frames array contains invalid value, treating as static cursor`),delete r.frames,delete r.duration;break}for(let e of r.duration)if("number"!=typeof e||e<=0){console.warn(`[AnimeCursor] Cursor "${t}" duration array contains invalid value, treating as static cursor`),delete r.frames,delete r.duration;break}}else"number"==typeof r.frames&&"number"==typeof r.duration?(r.frames<=0||r.duration<=0)&&(console.warn(`[AnimeCursor] Cursor "${t}" frames or duration <= 0, treating as static cursor`),delete r.frames,delete r.duration):(console.warn(`[AnimeCursor] Cursor "${t}" frames and duration must be both numbers or both arrays, treating as static cursor`),delete r.frames,delete r.duration)}else void 0===r.frames&&void 0===r.duration||(console.warn(`[AnimeCursor] Cursor "${t}" has only frames or duration defined, treating as static cursor`),delete r.frames,delete r.duration);if(r.tags&&!Array.isArray(r.tags))throw new Error(`[AnimeCursor] Cursor "${t}" tags must be an array`);if(r.default){if(e)throw new Error("[AnimeCursor] Only one default cursor allowed");e=!0}if(r.offset&&(!Array.isArray(r.offset)||2!==r.offset.length))throw new Error(`[AnimeCursor] Cursor "${t}" offset must be [x, y] array`)}this.defaultCursorName=e?Object.keys(this.cursors).find(e=>this.cursors[e].default):null}_preloadImages(){const e=new Set;for(const t of Object.values(this.cursors)){this._getFrameUrls(t).forEach(t=>e.add(t))}e.forEach(e=>{const t=document.createElement("link");t.rel="preload",t.as="image",t.href=e,e.startsWith("http")&&!e.startsWith(window.location.origin)&&(t.crossOrigin="anonymous"),document.head.appendChild(t)}),this.options.debug&&e.size&&console.info(`[AnimeCursor] Preloaded ${e.size} cursor images`)}_getFrameUrls(e){let t=1;void 0!==e.frames&&(Array.isArray(e.frames)?t=e.frames.reduce((e,t)=>e+t,0):"number"==typeof e.frames&&(t=e.frames));const{image:r}=e;if(1===t)return[r];const{prefix:s,suffix:o,startNum:i,numFormat:n,ext:a}=this._parseImagePattern(r),u=[];for(let e=0;e<t;e++){const t=i+e,r=`${s}${n?this._formatNumber(t,n):t}${o}${a}`;u.push(r)}return u}_parseImagePattern(e){const t=e.match(/\.[^.]+$/),r=t?t[0]:"",s=e.slice(0,-r.length),o=s.match(/(\d+)(?!.*\d)/);if(!o)return{prefix:s+"_",suffix:"",startNum:1,numFormat:null,ext:r};const i=o[0],n=parseInt(i,10),a=i.length;return{prefix:s.slice(0,o.index),suffix:s.slice(o.index+i.length),startNum:n,numFormat:a,ext:r}}_formatNumber(e,t){return String(e).padStart(t,"0")}_checkDomLoad(){const e=()=>{this._injectStyles(),this.options.debug&&this._initDebug(),console.log("[AnimeCursor] Initialization complete")};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",e):e()}_injectStyles(){if(this.disabled)return;const e=document.createElement("style");e.id="animecursor-styles";let t="";if(this.defaultCursorName){const e=this.cursors[this.defaultCursorName];t+=`* { ${this._buildCursorCss(this.defaultCursorName,e)} }\n`}for(const[e,r]of Object.entries(this.cursors)){const s=`.ac-cursor-${e}`,o=r.offset||[0,0],i=r.fallback||this.options.fallbackCursor,n=this._getFrameUrls(r),a=n.length;let u="";if(void 0!==r.frames&&void 0!==r.duration&&(Array.isArray(r.frames)&&Array.isArray(r.duration)||"number"==typeof r.frames&&"number"==typeof r.duration)&&a>1){const a=`ac_anim_${e}`;let l=`@keyframes ${a} {\n`;const d=this._buildKeyframes(r,n);for(const e of d){let t=(100*e.percent).toFixed(5);1===e.percent&&(t="100");l+=` ${t}% { ${`cursor: url("${e.url}") ${o[0]} ${o[1]}, ${i};`} }\n`}l+="}\n",t+=l;const c=`${a} ${Array.isArray(r.duration)?r.duration.reduce((e,t)=>e+t,0):r.duration}s steps(1) infinite ${r.pingpong?"alternate":""}`;u=c,t+=`${s} { cursor: url("${n[0]}") ${o[0]} ${o[1]}, ${i}; animation: ${c}; }\n`}else t+=`${s} { cursor: url("${n[0]}") ${o[0]} ${o[1]}, ${i}; }\n`;if(this.cursorAnimationStrings[e]=u,r.tags&&r.tags.length){t+=`${r.tags.join(", ")} { ${this._buildCursorCss(e,r)} }\n`}t+=`[data-cursor="${e}"] { ${this._buildCursorCss(e,r)} }\n`}if(this.options.excludeSelectors&&(t+=`${this.options.excludeSelectors} { cursor: text !important; animation: none !important; }\n`),this.options.combineAnimations){const e=document.querySelectorAll("[data-ac-animation]");for(const r of e){const e=r.getAttribute("data-ac-animation");if(!e)continue;let s=this._getCursorTypeForElement(r);if(!s)continue;const o=this.cursorAnimationStrings[s];if(!o)continue;const i=`${s}:${e}`;if(!this.combinedRules.has(i)){const r=`ac-combined-${this._simpleHash(i)}`;t+=`.${r} { animation: ${o}, ${e}; }\n`,this.combinedRules.set(i,r)}const n=this.combinedRules.get(i);r.classList.add(n)}}t+="body.animecursor-disabled * { cursor: auto !important; animation: none !important; }\n",e.textContent=t,document.head.appendChild(e),this.styleEl=e}_getCursorTypeForElement(e){if(e.dataset.cursor&&this.cursors[e.dataset.cursor])return e.dataset.cursor;for(const[t,r]of Object.entries(this.cursors))if(r.tags&&r.tags.some(t=>e.matches(t)))return t;return this.defaultCursorName}_buildKeyframes(e,t){let r=e.frames,s=e.duration;const o=t.length;if("number"==typeof r){const e=s/r;r=new Array(r).fill(1),s=new Array(r.length).fill(e)}const i=[];let n=s.reduce((e,t)=>e+t,0),a=0,u=0;for(let e=0;e<r.length;e++){const o=r[e],l=s[e]/o;for(let e=0;e<o;e++){const e=a/n;i.push({percent:e,url:t[u]}),a+=l,u++}}return i.push({percent:1,url:t[o-1]}),i}_buildCursorCss(e,t){const r=this._getFrameUrls(t),s=t.offset||[0,0],o=t.fallback||this.options.fallbackCursor;let i=`cursor: url("${r[0]}") ${s[0]} ${s[1]}, ${o};`;if(void 0!==t.frames&&void 0!==t.duration&&(Array.isArray(t.frames)&&Array.isArray(t.duration)||"number"==typeof t.frames&&"number"==typeof t.duration)&&r.length>1){i+=` animation: ac_anim_${e} ${Array.isArray(t.duration)?t.duration.reduce((e,t)=>e+t,0):t.duration}s steps(1) infinite ${t.pingpong?"alternate":""};`}return i}_simpleHash(e){let t=0;for(let r=0;r<e.length;r++){t=(t<<5)-t+e.charCodeAt(r),t|=0}return Math.abs(t).toString(36)}_initDebug(){const e=document.createElement("div");e.className="animecursor-debug",e.style.cssText="\n position: fixed;\n top: 0;\n left: 0;\n background: rgba(0,0,0,0.7);\n color: #0f0;\n padding: 4px 8px;\n font-family: monospace;\n font-size: 12px;\n z-index: 2147483647;\n pointer-events: none;\n white-space: nowrap;\n ",document.body.appendChild(e),this.debugEl=e;let t="";this._onMouseMove=r=>{const s=document.elementFromPoint(r.clientX,r.clientY);let o=null;if(s)if(s.dataset.cursor&&this.cursors[s.dataset.cursor])o=s.dataset.cursor;else for(const[e,t]of Object.entries(this.cursors))if(t.tags&&t.tags.some(e=>s.matches(e))){o=e;break}o||this.defaultCursorName?!o&&this.defaultCursorName&&(o=this.defaultCursorName):o="native",o!==t?(t=o,e.textContent=`🎯 ${o} @ (${r.clientX}, ${r.clientY})`):e.textContent=`🎯 ${o} @ (${r.clientX}, ${r.clientY})`},document.addEventListener("mousemove",this._onMouseMove)}refresh(){this.disabled||(this.styleEl&&this.styleEl.remove(),this.combinedRules.clear(),this._injectStyles(),this.options.debug&&(this.debugEl&&this.debugEl.remove(),this._initDebug()),console.log("[AnimeCursor] Refresh complete"))}destroy(){this.disabled||(this.styleEl&&this.styleEl.remove(),this.debugEl&&this.debugEl.remove(),this._onMouseMove&&document.removeEventListener("mousemove",this._onMouseMove),document.body.classList.remove("animecursor-disabled"),e=null,console.log("[AnimeCursor] Destroyed"))}disable(){this.disabled||(this.disabled=!0,document.body.classList.add("animecursor-disabled"),this.options.debug&&console.log("[AnimeCursor] Disabled"))}enable(){this.disabled&&(this.disabled=!1,document.body.classList.remove("animecursor-disabled"),this.options.debug&&console.log("[AnimeCursor] Enabled"))}}});
|