anime-cursor 2.0.1 → 2.1.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 +34 -2
- package/dist/anime-cursor.esm.js +85 -14
- package/dist/anime-cursor.umd.js +85 -14
- 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.1/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.1/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,9 +244,10 @@ class AnimeCursor {
|
|
|
247
244
|
}
|
|
248
245
|
}
|
|
249
246
|
|
|
250
|
-
//
|
|
247
|
+
// 核心注入样式
|
|
251
248
|
_injectStyles() {
|
|
252
249
|
if (this.disabled) return;
|
|
250
|
+
this.combinedRules.clear(); // 清空旧组合规则
|
|
253
251
|
|
|
254
252
|
const style = document.createElement('style');
|
|
255
253
|
style.id = 'animecursor-styles';
|
|
@@ -275,6 +273,7 @@ class AnimeCursor {
|
|
|
275
273
|
((Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) ||
|
|
276
274
|
(typeof cfg.frames === 'number' && typeof cfg.duration === 'number'));
|
|
277
275
|
|
|
276
|
+
let cursorAnimation = '';
|
|
278
277
|
if (hasAnimation && frameCount > 1) {
|
|
279
278
|
const keyframeName = `ac_anim_${name}`;
|
|
280
279
|
let keyframesCss = `@keyframes ${keyframeName} {\n`;
|
|
@@ -290,11 +289,15 @@ class AnimeCursor {
|
|
|
290
289
|
|
|
291
290
|
const totalDuration = Array.isArray(cfg.duration) ? cfg.duration.reduce((a, b) => a + b, 0) : cfg.duration;
|
|
292
291
|
const animation = `${keyframeName} ${totalDuration}s steps(1) infinite ${cfg.pingpong ? 'alternate' : ''}`;
|
|
292
|
+
cursorAnimation = animation;
|
|
293
293
|
css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; animation: ${animation}; }\n`;
|
|
294
294
|
} else {
|
|
295
295
|
css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; }\n`;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
this.cursorAnimationStrings[name] = cursorAnimation;
|
|
299
|
+
|
|
300
|
+
// 标签和 data-cursor 规则
|
|
298
301
|
if (cfg.tags && cfg.tags.length) {
|
|
299
302
|
const selector = cfg.tags.join(', ');
|
|
300
303
|
css += `${selector} { ${this._buildCursorCss(name, cfg)} }\n`;
|
|
@@ -306,6 +309,33 @@ class AnimeCursor {
|
|
|
306
309
|
css += `${this.options.excludeSelectors} { cursor: text !important; animation: none !important; }\n`;
|
|
307
310
|
}
|
|
308
311
|
|
|
312
|
+
// 自动组合动画(新功能)
|
|
313
|
+
if (this.options.combineAnimations) {
|
|
314
|
+
const elements = document.querySelectorAll('[data-ac-animation]');
|
|
315
|
+
for (const el of elements) {
|
|
316
|
+
const userAnim = el.getAttribute('data-ac-animation');
|
|
317
|
+
if (!userAnim) continue;
|
|
318
|
+
|
|
319
|
+
// 确定该元素应该使用哪个光标
|
|
320
|
+
let cursorName = this._getCursorTypeForElement(el);
|
|
321
|
+
if (!cursorName) continue;
|
|
322
|
+
|
|
323
|
+
const cursorAnim = this.cursorAnimationStrings[cursorName];
|
|
324
|
+
if (!cursorAnim) continue;
|
|
325
|
+
|
|
326
|
+
// 生成唯一标识
|
|
327
|
+
const key = `${cursorName}:${userAnim}`;
|
|
328
|
+
if (!this.combinedRules.has(key)) {
|
|
329
|
+
const hash = this._simpleHash(key);
|
|
330
|
+
const combinedClass = `ac-combined-${hash}`;
|
|
331
|
+
css += `.${combinedClass} { animation: ${cursorAnim}, ${userAnim}; }\n`;
|
|
332
|
+
this.combinedRules.set(key, combinedClass);
|
|
333
|
+
}
|
|
334
|
+
const combinedClass = this.combinedRules.get(key);
|
|
335
|
+
el.classList.add(combinedClass);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
309
339
|
css += `body.animecursor-disabled * { cursor: auto !important; animation: none !important; }\n`;
|
|
310
340
|
|
|
311
341
|
style.textContent = css;
|
|
@@ -313,6 +343,19 @@ class AnimeCursor {
|
|
|
313
343
|
this.styleEl = style;
|
|
314
344
|
}
|
|
315
345
|
|
|
346
|
+
// 获取元素对应的光标类型(复用 debug 逻辑)
|
|
347
|
+
_getCursorTypeForElement(el) {
|
|
348
|
+
if (el.dataset.cursor && this.cursors[el.dataset.cursor]) {
|
|
349
|
+
return el.dataset.cursor;
|
|
350
|
+
}
|
|
351
|
+
for (const [name, cfg] of Object.entries(this.cursors)) {
|
|
352
|
+
if (cfg.tags && cfg.tags.some(tag => el.matches(tag))) {
|
|
353
|
+
return name;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return this.defaultCursorName; // 可能为 null
|
|
357
|
+
}
|
|
358
|
+
|
|
316
359
|
_buildKeyframes(cfg, frameUrls) {
|
|
317
360
|
let frames = cfg.frames;
|
|
318
361
|
let durations = cfg.duration;
|
|
@@ -364,6 +407,16 @@ class AnimeCursor {
|
|
|
364
407
|
return css;
|
|
365
408
|
}
|
|
366
409
|
|
|
410
|
+
_simpleHash(str) {
|
|
411
|
+
let hash = 0;
|
|
412
|
+
for (let i = 0; i < str.length; i++) {
|
|
413
|
+
const char = str.charCodeAt(i);
|
|
414
|
+
hash = ((hash << 5) - hash) + char;
|
|
415
|
+
hash |= 0;
|
|
416
|
+
}
|
|
417
|
+
return Math.abs(hash).toString(36);
|
|
418
|
+
}
|
|
419
|
+
|
|
367
420
|
_initDebug() {
|
|
368
421
|
const debugDiv = document.createElement('div');
|
|
369
422
|
debugDiv.className = 'animecursor-debug';
|
|
@@ -399,7 +452,6 @@ class AnimeCursor {
|
|
|
399
452
|
}
|
|
400
453
|
}
|
|
401
454
|
}
|
|
402
|
-
// 如果没有匹配到任何自定义光标,且没有默认光标,则显示 "native"
|
|
403
455
|
if (!cursorType && !this.defaultCursorName) {
|
|
404
456
|
cursorType = 'native';
|
|
405
457
|
} else if (!cursorType && this.defaultCursorName) {
|
|
@@ -418,6 +470,7 @@ class AnimeCursor {
|
|
|
418
470
|
refresh() {
|
|
419
471
|
if (this.disabled) return;
|
|
420
472
|
if (this.styleEl) this.styleEl.remove();
|
|
473
|
+
this.combinedRules.clear();
|
|
421
474
|
this._injectStyles();
|
|
422
475
|
if (this.options.debug) {
|
|
423
476
|
if (this.debugEl) this.debugEl.remove();
|
|
@@ -440,15 +493,33 @@ class AnimeCursor {
|
|
|
440
493
|
|
|
441
494
|
disable() {
|
|
442
495
|
if (this.disabled) return;
|
|
496
|
+
// 移除样式表
|
|
497
|
+
if (this.styleEl) {
|
|
498
|
+
this.styleEl.remove();
|
|
499
|
+
this.styleEl = null;
|
|
500
|
+
}
|
|
501
|
+
// 移除 debug 相关
|
|
502
|
+
if (this.debugEl) {
|
|
503
|
+
this.debugEl.remove();
|
|
504
|
+
this.debugEl = null;
|
|
505
|
+
}
|
|
506
|
+
if (this._onMouseMove) {
|
|
507
|
+
document.removeEventListener('mousemove', this._onMouseMove);
|
|
508
|
+
this._onMouseMove = null;
|
|
509
|
+
}
|
|
443
510
|
this.disabled = true;
|
|
444
|
-
document.body.classList.add('animecursor-disabled');
|
|
445
511
|
if (this.options.debug) console.log('[AnimeCursor] Disabled');
|
|
446
512
|
}
|
|
447
513
|
|
|
448
514
|
enable() {
|
|
449
515
|
if (!this.disabled) return;
|
|
450
516
|
this.disabled = false;
|
|
451
|
-
|
|
517
|
+
// 重新注入样式
|
|
518
|
+
this._injectStyles();
|
|
519
|
+
// 如果 debug 模式开启,重新初始化 debug
|
|
520
|
+
if (this.options.debug) {
|
|
521
|
+
this._initDebug();
|
|
522
|
+
}
|
|
452
523
|
if (this.options.debug) console.log('[AnimeCursor] Enabled');
|
|
453
524
|
}
|
|
454
525
|
}
|
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,9 +250,10 @@
|
|
|
253
250
|
}
|
|
254
251
|
}
|
|
255
252
|
|
|
256
|
-
//
|
|
253
|
+
// 核心注入样式
|
|
257
254
|
_injectStyles() {
|
|
258
255
|
if (this.disabled) return;
|
|
256
|
+
this.combinedRules.clear(); // 清空旧组合规则
|
|
259
257
|
|
|
260
258
|
const style = document.createElement('style');
|
|
261
259
|
style.id = 'animecursor-styles';
|
|
@@ -281,6 +279,7 @@
|
|
|
281
279
|
((Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) ||
|
|
282
280
|
(typeof cfg.frames === 'number' && typeof cfg.duration === 'number'));
|
|
283
281
|
|
|
282
|
+
let cursorAnimation = '';
|
|
284
283
|
if (hasAnimation && frameCount > 1) {
|
|
285
284
|
const keyframeName = `ac_anim_${name}`;
|
|
286
285
|
let keyframesCss = `@keyframes ${keyframeName} {\n`;
|
|
@@ -296,11 +295,15 @@
|
|
|
296
295
|
|
|
297
296
|
const totalDuration = Array.isArray(cfg.duration) ? cfg.duration.reduce((a, b) => a + b, 0) : cfg.duration;
|
|
298
297
|
const animation = `${keyframeName} ${totalDuration}s steps(1) infinite ${cfg.pingpong ? 'alternate' : ''}`;
|
|
298
|
+
cursorAnimation = animation;
|
|
299
299
|
css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; animation: ${animation}; }\n`;
|
|
300
300
|
} else {
|
|
301
301
|
css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; }\n`;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
this.cursorAnimationStrings[name] = cursorAnimation;
|
|
305
|
+
|
|
306
|
+
// 标签和 data-cursor 规则
|
|
304
307
|
if (cfg.tags && cfg.tags.length) {
|
|
305
308
|
const selector = cfg.tags.join(', ');
|
|
306
309
|
css += `${selector} { ${this._buildCursorCss(name, cfg)} }\n`;
|
|
@@ -312,6 +315,33 @@
|
|
|
312
315
|
css += `${this.options.excludeSelectors} { cursor: text !important; animation: none !important; }\n`;
|
|
313
316
|
}
|
|
314
317
|
|
|
318
|
+
// 自动组合动画(新功能)
|
|
319
|
+
if (this.options.combineAnimations) {
|
|
320
|
+
const elements = document.querySelectorAll('[data-ac-animation]');
|
|
321
|
+
for (const el of elements) {
|
|
322
|
+
const userAnim = el.getAttribute('data-ac-animation');
|
|
323
|
+
if (!userAnim) continue;
|
|
324
|
+
|
|
325
|
+
// 确定该元素应该使用哪个光标
|
|
326
|
+
let cursorName = this._getCursorTypeForElement(el);
|
|
327
|
+
if (!cursorName) continue;
|
|
328
|
+
|
|
329
|
+
const cursorAnim = this.cursorAnimationStrings[cursorName];
|
|
330
|
+
if (!cursorAnim) continue;
|
|
331
|
+
|
|
332
|
+
// 生成唯一标识
|
|
333
|
+
const key = `${cursorName}:${userAnim}`;
|
|
334
|
+
if (!this.combinedRules.has(key)) {
|
|
335
|
+
const hash = this._simpleHash(key);
|
|
336
|
+
const combinedClass = `ac-combined-${hash}`;
|
|
337
|
+
css += `.${combinedClass} { animation: ${cursorAnim}, ${userAnim}; }\n`;
|
|
338
|
+
this.combinedRules.set(key, combinedClass);
|
|
339
|
+
}
|
|
340
|
+
const combinedClass = this.combinedRules.get(key);
|
|
341
|
+
el.classList.add(combinedClass);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
315
345
|
css += `body.animecursor-disabled * { cursor: auto !important; animation: none !important; }\n`;
|
|
316
346
|
|
|
317
347
|
style.textContent = css;
|
|
@@ -319,6 +349,19 @@
|
|
|
319
349
|
this.styleEl = style;
|
|
320
350
|
}
|
|
321
351
|
|
|
352
|
+
// 获取元素对应的光标类型(复用 debug 逻辑)
|
|
353
|
+
_getCursorTypeForElement(el) {
|
|
354
|
+
if (el.dataset.cursor && this.cursors[el.dataset.cursor]) {
|
|
355
|
+
return el.dataset.cursor;
|
|
356
|
+
}
|
|
357
|
+
for (const [name, cfg] of Object.entries(this.cursors)) {
|
|
358
|
+
if (cfg.tags && cfg.tags.some(tag => el.matches(tag))) {
|
|
359
|
+
return name;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return this.defaultCursorName; // 可能为 null
|
|
363
|
+
}
|
|
364
|
+
|
|
322
365
|
_buildKeyframes(cfg, frameUrls) {
|
|
323
366
|
let frames = cfg.frames;
|
|
324
367
|
let durations = cfg.duration;
|
|
@@ -370,6 +413,16 @@
|
|
|
370
413
|
return css;
|
|
371
414
|
}
|
|
372
415
|
|
|
416
|
+
_simpleHash(str) {
|
|
417
|
+
let hash = 0;
|
|
418
|
+
for (let i = 0; i < str.length; i++) {
|
|
419
|
+
const char = str.charCodeAt(i);
|
|
420
|
+
hash = ((hash << 5) - hash) + char;
|
|
421
|
+
hash |= 0;
|
|
422
|
+
}
|
|
423
|
+
return Math.abs(hash).toString(36);
|
|
424
|
+
}
|
|
425
|
+
|
|
373
426
|
_initDebug() {
|
|
374
427
|
const debugDiv = document.createElement('div');
|
|
375
428
|
debugDiv.className = 'animecursor-debug';
|
|
@@ -405,7 +458,6 @@
|
|
|
405
458
|
}
|
|
406
459
|
}
|
|
407
460
|
}
|
|
408
|
-
// 如果没有匹配到任何自定义光标,且没有默认光标,则显示 "native"
|
|
409
461
|
if (!cursorType && !this.defaultCursorName) {
|
|
410
462
|
cursorType = 'native';
|
|
411
463
|
} else if (!cursorType && this.defaultCursorName) {
|
|
@@ -424,6 +476,7 @@
|
|
|
424
476
|
refresh() {
|
|
425
477
|
if (this.disabled) return;
|
|
426
478
|
if (this.styleEl) this.styleEl.remove();
|
|
479
|
+
this.combinedRules.clear();
|
|
427
480
|
this._injectStyles();
|
|
428
481
|
if (this.options.debug) {
|
|
429
482
|
if (this.debugEl) this.debugEl.remove();
|
|
@@ -446,15 +499,33 @@
|
|
|
446
499
|
|
|
447
500
|
disable() {
|
|
448
501
|
if (this.disabled) return;
|
|
502
|
+
// 移除样式表
|
|
503
|
+
if (this.styleEl) {
|
|
504
|
+
this.styleEl.remove();
|
|
505
|
+
this.styleEl = null;
|
|
506
|
+
}
|
|
507
|
+
// 移除 debug 相关
|
|
508
|
+
if (this.debugEl) {
|
|
509
|
+
this.debugEl.remove();
|
|
510
|
+
this.debugEl = null;
|
|
511
|
+
}
|
|
512
|
+
if (this._onMouseMove) {
|
|
513
|
+
document.removeEventListener('mousemove', this._onMouseMove);
|
|
514
|
+
this._onMouseMove = null;
|
|
515
|
+
}
|
|
449
516
|
this.disabled = true;
|
|
450
|
-
document.body.classList.add('animecursor-disabled');
|
|
451
517
|
if (this.options.debug) console.log('[AnimeCursor] Disabled');
|
|
452
518
|
}
|
|
453
519
|
|
|
454
520
|
enable() {
|
|
455
521
|
if (!this.disabled) return;
|
|
456
522
|
this.disabled = false;
|
|
457
|
-
|
|
523
|
+
// 重新注入样式
|
|
524
|
+
this._injectStyles();
|
|
525
|
+
// 如果 debug 模式开启,重新初始化 debug
|
|
526
|
+
if (this.options.debug) {
|
|
527
|
+
this._initDebug();
|
|
528
|
+
}
|
|
458
529
|
if (this.options.debug) console.log('[AnimeCursor] Enabled');
|
|
459
530
|
}
|
|
460
531
|
}
|
|
@@ -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 u=`@keyframes ${a} {\n`;const l=this._buildKeyframes(r,n);for(const e of l){let t=(100*e.percent).toFixed(5);1===e.percent&&(t="100");u+=` ${t}% { ${`cursor: url("${e.url}") ${o[0]} ${o[1]}, ${i};`} }\n`}u+="}\n",t+=u;const d=`${a} ${Array.isArray(r.duration)?r.duration.reduce((e,t)=>e+t,0):r.duration}s steps(1) infinite ${r.pingpong?"alternate":""}`;t+=`${s} { cursor: url("${n[0]}") ${o[0]} ${o[1]}, ${i}; animation: ${d}; }\n`}else t+=`${s} { cursor: url("${n[0]}") ${o[0]} ${o[1]}, ${i}; }\n`;if(r.tags&&r.tags.length){t+=`${r.tags.join(", ")} { ${this._buildCursorCss(e,r)} }\n`}t+=`[data-cursor="${e}"] { ${this._buildCursorCss(e,r)} }\n`}this.options.excludeSelectors&&(t+=`${this.options.excludeSelectors} { cursor: text !important; animation: none !important; }\n`),t+="body.animecursor-disabled * { cursor: auto !important; animation: none !important; }\n",e.textContent=t,document.head.appendChild(e),this.styleEl=e}_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}_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._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"))}}});
|
|
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,s]of Object.entries(this.cursors)){if(!s.image)throw new Error(`[AnimeCursor] Cursor "${t}" missing required setting: image`);if(void 0!==s.frames&&void 0!==s.duration){if(typeof s.frames!==typeof s.duration)console.warn(`[AnimeCursor] Cursor "${t}" has mismatched types for frames and duration, treating as static cursor`),delete s.frames,delete s.duration;else if(Array.isArray(s.frames)&&Array.isArray(s.duration))if(s.frames.length!==s.duration.length)console.warn(`[AnimeCursor] Cursor "${t}" frames and duration arrays have different lengths, treating as static cursor`),delete s.frames,delete s.duration;else{for(let e of s.frames)if(!Number.isInteger(e)||e<=0){console.warn(`[AnimeCursor] Cursor "${t}" frames array contains invalid value, treating as static cursor`),delete s.frames,delete s.duration;break}for(let e of s.duration)if("number"!=typeof e||e<=0){console.warn(`[AnimeCursor] Cursor "${t}" duration array contains invalid value, treating as static cursor`),delete s.frames,delete s.duration;break}}else"number"==typeof s.frames&&"number"==typeof s.duration?(s.frames<=0||s.duration<=0)&&(console.warn(`[AnimeCursor] Cursor "${t}" frames or duration <= 0, treating as static cursor`),delete s.frames,delete s.duration):(console.warn(`[AnimeCursor] Cursor "${t}" frames and duration must be both numbers or both arrays, treating as static cursor`),delete s.frames,delete s.duration)}else void 0===s.frames&&void 0===s.duration||(console.warn(`[AnimeCursor] Cursor "${t}" has only frames or duration defined, treating as static cursor`),delete s.frames,delete s.duration);if(s.tags&&!Array.isArray(s.tags))throw new Error(`[AnimeCursor] Cursor "${t}" tags must be an array`);if(s.default){if(e)throw new Error("[AnimeCursor] Only one default cursor allowed");e=!0}if(s.offset&&(!Array.isArray(s.offset)||2!==s.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:s}=e;if(1===t)return[s];const{prefix:r,suffix:o,startNum:i,numFormat:n,ext:a}=this._parseImagePattern(s),u=[];for(let e=0;e<t;e++){const t=i+e,s=`${r}${n?this._formatNumber(t,n):t}${o}${a}`;u.push(s)}return u}_parseImagePattern(e){const t=e.match(/\.[^.]+$/),s=t?t[0]:"",r=e.slice(0,-s.length),o=r.match(/(\d+)(?!.*\d)/);if(!o)return{prefix:r+"_",suffix:"",startNum:1,numFormat:null,ext:s};const i=o[0],n=parseInt(i,10),a=i.length;return{prefix:r.slice(0,o.index),suffix:r.slice(o.index+i.length),startNum:n,numFormat:a,ext:s}}_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;this.combinedRules.clear();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,s]of Object.entries(this.cursors)){const r=`.ac-cursor-${e}`,o=s.offset||[0,0],i=s.fallback||this.options.fallbackCursor,n=this._getFrameUrls(s),a=n.length;let u="";if(void 0!==s.frames&&void 0!==s.duration&&(Array.isArray(s.frames)&&Array.isArray(s.duration)||"number"==typeof s.frames&&"number"==typeof s.duration)&&a>1){const a=`ac_anim_${e}`;let l=`@keyframes ${a} {\n`;const d=this._buildKeyframes(s,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(s.duration)?s.duration.reduce((e,t)=>e+t,0):s.duration}s steps(1) infinite ${s.pingpong?"alternate":""}`;u=c,t+=`${r} { cursor: url("${n[0]}") ${o[0]} ${o[1]}, ${i}; animation: ${c}; }\n`}else t+=`${r} { cursor: url("${n[0]}") ${o[0]} ${o[1]}, ${i}; }\n`;if(this.cursorAnimationStrings[e]=u,s.tags&&s.tags.length){t+=`${s.tags.join(", ")} { ${this._buildCursorCss(e,s)} }\n`}t+=`[data-cursor="${e}"] { ${this._buildCursorCss(e,s)} }\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 s of e){const e=s.getAttribute("data-ac-animation");if(!e)continue;let r=this._getCursorTypeForElement(s);if(!r)continue;const o=this.cursorAnimationStrings[r];if(!o)continue;const i=`${r}:${e}`;if(!this.combinedRules.has(i)){const s=`ac-combined-${this._simpleHash(i)}`;t+=`.${s} { animation: ${o}, ${e}; }\n`,this.combinedRules.set(i,s)}const n=this.combinedRules.get(i);s.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,s]of Object.entries(this.cursors))if(s.tags&&s.tags.some(t=>e.matches(t)))return t;return this.defaultCursorName}_buildKeyframes(e,t){let s=e.frames,r=e.duration;const o=t.length;if("number"==typeof s){const e=r/s;s=new Array(s).fill(1),r=new Array(s.length).fill(e)}const i=[];let n=r.reduce((e,t)=>e+t,0),a=0,u=0;for(let e=0;e<s.length;e++){const o=s[e],l=r[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 s=this._getFrameUrls(t),r=t.offset||[0,0],o=t.fallback||this.options.fallbackCursor;let i=`cursor: url("${s[0]}") ${r[0]} ${r[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)&&s.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 s=0;s<e.length;s++){t=(t<<5)-t+e.charCodeAt(s),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=s=>{const r=document.elementFromPoint(s.clientX,s.clientY);let o=null;if(r)if(r.dataset.cursor&&this.cursors[r.dataset.cursor])o=r.dataset.cursor;else for(const[e,t]of Object.entries(this.cursors))if(t.tags&&t.tags.some(e=>r.matches(e))){o=e;break}o||this.defaultCursorName?!o&&this.defaultCursorName&&(o=this.defaultCursorName):o="native",o!==t?(t=o,e.textContent=`🎯 ${o} @ (${s.clientX}, ${s.clientY})`):e.textContent=`🎯 ${o} @ (${s.clientX}, ${s.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.styleEl&&(this.styleEl.remove(),this.styleEl=null),this.debugEl&&(this.debugEl.remove(),this.debugEl=null),this._onMouseMove&&(document.removeEventListener("mousemove",this._onMouseMove),this._onMouseMove=null),this.disabled=!0,this.options.debug&&console.log("[AnimeCursor] Disabled"))}enable(){this.disabled&&(this.disabled=!1,this._injectStyles(),this.options.debug&&this._initDebug(),this.options.debug&&console.log("[AnimeCursor] Enabled"))}}});
|