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 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.0.0/dist/anime-cursor.umd.min.js"></script>
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.0.0/dist/anime-cursor.umd.min.js"></script>
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` 自动应用光标样式:
@@ -1,5 +1,5 @@
1
1
  // AnimeCursor by github@ShuninYu
2
- // v2.0.1
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', // Fallback cursor type (auto, pointer, etc.)
53
- excludeSelectors: 'input, textarea, [contenteditable]', // Exclude native cursor elements
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
- // 注入所有 CSS 规则(修改点:只有存在默认光标才生成 * 规则)
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
- document.body.classList.remove('animecursor-disabled');
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
  }
@@ -5,7 +5,7 @@
5
5
  })(this, (function () { 'use strict';
6
6
 
7
7
  // AnimeCursor by github@ShuninYu
8
- // v2.0.1
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', // Fallback cursor type (auto, pointer, etc.)
59
- excludeSelectors: 'input, textarea, [contenteditable]', // Exclude native cursor elements
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
- // 注入所有 CSS 规则(修改点:只有存在默认光标才生成 * 规则)
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
- document.body.classList.remove('animecursor-disabled');
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"))}}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anime-cursor",
3
- "version": "2.0.1",
3
+ "version": "2.1.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",