anime-cursor 2.1.0 → 2.1.2

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.1.0/dist/anime-cursor.umd.min.js"></script>
36
+ <script src="https://cdn.jsdelivr.net/npm/anime-cursor@v2.1.2/dist/anime-cursor.umd.min.js"></script>
37
37
  ```
38
38
 
39
39
  ### npm
@@ -218,7 +218,7 @@ AnimeCursor 无需依赖任何框架,适合个人网站、创意作品集以
218
218
  ### CDN
219
219
 
220
220
  ```html
221
- <script src="https://cdn.jsdelivr.net/npm/anime-cursor@v2.1.0/dist/anime-cursor.umd.min.js"></script>
221
+ <script src="https://cdn.jsdelivr.net/npm/anime-cursor@v2.1.2/dist/anime-cursor.umd.min.js"></script>
222
222
  ```
223
223
 
224
224
  ### npm
@@ -1,5 +1,5 @@
1
1
  // AnimeCursor by github@ShuninYu
2
- // v2.1.0
2
+ // v2.1.2
3
3
 
4
4
  let _instance = null;
5
5
 
@@ -247,6 +247,7 @@ class AnimeCursor {
247
247
  // 核心注入样式
248
248
  _injectStyles() {
249
249
  if (this.disabled) return;
250
+ this.combinedRules.clear();
250
251
 
251
252
  const style = document.createElement('style');
252
253
  style.id = 'animecursor-styles';
@@ -291,7 +292,14 @@ class AnimeCursor {
291
292
  cursorAnimation = animation;
292
293
  css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; animation: ${animation}; }\n`;
293
294
  } else {
294
- css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; }\n`;
295
+ // 静态光标:生成一帧动画,只播放一次,结束后保持最后一帧
296
+ const staticKeyframeName = `ac_anim_${name}_static`;
297
+ css += `@keyframes ${staticKeyframeName} {\n`;
298
+ css += ` 0%, 100% { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; }\n`;
299
+ css += `}\n`;
300
+ const staticAnimation = `${staticKeyframeName} 0.001s forwards steps(1)`;
301
+ cursorAnimation = staticAnimation;
302
+ css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; animation: ${staticAnimation}; }\n`;
295
303
  }
296
304
 
297
305
  this.cursorAnimationStrings[name] = cursorAnimation;
@@ -308,21 +316,19 @@ class AnimeCursor {
308
316
  css += `${this.options.excludeSelectors} { cursor: text !important; animation: none !important; }\n`;
309
317
  }
310
318
 
311
- // 自动组合动画(新功能)
319
+ // 自动组合动画
312
320
  if (this.options.combineAnimations) {
313
321
  const elements = document.querySelectorAll('[data-ac-animation]');
314
322
  for (const el of elements) {
315
323
  const userAnim = el.getAttribute('data-ac-animation');
316
324
  if (!userAnim) continue;
317
325
 
318
- // 确定该元素应该使用哪个光标
319
326
  let cursorName = this._getCursorTypeForElement(el);
320
327
  if (!cursorName) continue;
321
328
 
322
329
  const cursorAnim = this.cursorAnimationStrings[cursorName];
323
330
  if (!cursorAnim) continue;
324
331
 
325
- // 生成唯一标识
326
332
  const key = `${cursorName}:${userAnim}`;
327
333
  if (!this.combinedRules.has(key)) {
328
334
  const hash = this._simpleHash(key);
@@ -396,12 +402,17 @@ class AnimeCursor {
396
402
  const offset = cfg.offset || [0, 0];
397
403
  const fallback = cfg.fallback || this.options.fallbackCursor;
398
404
  let css = `cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback};`;
405
+
399
406
  const hasAnimation = cfg.frames !== undefined && cfg.duration !== undefined &&
400
407
  ((Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) ||
401
408
  (typeof cfg.frames === 'number' && typeof cfg.duration === 'number'));
409
+
402
410
  if (hasAnimation && frameUrls.length > 1) {
403
411
  const totalDuration = Array.isArray(cfg.duration) ? cfg.duration.reduce((a, b) => a + b, 0) : cfg.duration;
404
412
  css += ` animation: ac_anim_${name} ${totalDuration}s steps(1) infinite ${cfg.pingpong ? 'alternate' : ''};`;
413
+ } else {
414
+ // 静态光标:一帧动画,只播放一次,结束后保持
415
+ css += ` animation: ac_anim_${name}_static 0.001s forwards steps(1);`;
405
416
  }
406
417
  return css;
407
418
  }
@@ -492,15 +503,33 @@ class AnimeCursor {
492
503
 
493
504
  disable() {
494
505
  if (this.disabled) return;
506
+ // 移除样式表
507
+ if (this.styleEl) {
508
+ this.styleEl.remove();
509
+ this.styleEl = null;
510
+ }
511
+ // 移除 debug 相关
512
+ if (this.debugEl) {
513
+ this.debugEl.remove();
514
+ this.debugEl = null;
515
+ }
516
+ if (this._onMouseMove) {
517
+ document.removeEventListener('mousemove', this._onMouseMove);
518
+ this._onMouseMove = null;
519
+ }
495
520
  this.disabled = true;
496
- document.body.classList.add('animecursor-disabled');
497
521
  if (this.options.debug) console.log('[AnimeCursor] Disabled');
498
522
  }
499
523
 
500
524
  enable() {
501
525
  if (!this.disabled) return;
502
526
  this.disabled = false;
503
- document.body.classList.remove('animecursor-disabled');
527
+ // 重新注入样式
528
+ this._injectStyles();
529
+ // 如果 debug 模式开启,重新初始化 debug
530
+ if (this.options.debug) {
531
+ this._initDebug();
532
+ }
504
533
  if (this.options.debug) console.log('[AnimeCursor] Enabled');
505
534
  }
506
535
  }
@@ -5,7 +5,7 @@
5
5
  })(this, (function () { 'use strict';
6
6
 
7
7
  // AnimeCursor by github@ShuninYu
8
- // v2.1.0
8
+ // v2.1.2
9
9
 
10
10
  let _instance = null;
11
11
 
@@ -253,6 +253,7 @@
253
253
  // 核心注入样式
254
254
  _injectStyles() {
255
255
  if (this.disabled) return;
256
+ this.combinedRules.clear();
256
257
 
257
258
  const style = document.createElement('style');
258
259
  style.id = 'animecursor-styles';
@@ -297,7 +298,14 @@
297
298
  cursorAnimation = animation;
298
299
  css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; animation: ${animation}; }\n`;
299
300
  } else {
300
- css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; }\n`;
301
+ // 静态光标:生成一帧动画,只播放一次,结束后保持最后一帧
302
+ const staticKeyframeName = `ac_anim_${name}_static`;
303
+ css += `@keyframes ${staticKeyframeName} {\n`;
304
+ css += ` 0%, 100% { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; }\n`;
305
+ css += `}\n`;
306
+ const staticAnimation = `${staticKeyframeName} 0.001s forwards steps(1)`;
307
+ cursorAnimation = staticAnimation;
308
+ css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; animation: ${staticAnimation}; }\n`;
301
309
  }
302
310
 
303
311
  this.cursorAnimationStrings[name] = cursorAnimation;
@@ -314,21 +322,19 @@
314
322
  css += `${this.options.excludeSelectors} { cursor: text !important; animation: none !important; }\n`;
315
323
  }
316
324
 
317
- // 自动组合动画(新功能)
325
+ // 自动组合动画
318
326
  if (this.options.combineAnimations) {
319
327
  const elements = document.querySelectorAll('[data-ac-animation]');
320
328
  for (const el of elements) {
321
329
  const userAnim = el.getAttribute('data-ac-animation');
322
330
  if (!userAnim) continue;
323
331
 
324
- // 确定该元素应该使用哪个光标
325
332
  let cursorName = this._getCursorTypeForElement(el);
326
333
  if (!cursorName) continue;
327
334
 
328
335
  const cursorAnim = this.cursorAnimationStrings[cursorName];
329
336
  if (!cursorAnim) continue;
330
337
 
331
- // 生成唯一标识
332
338
  const key = `${cursorName}:${userAnim}`;
333
339
  if (!this.combinedRules.has(key)) {
334
340
  const hash = this._simpleHash(key);
@@ -402,12 +408,17 @@
402
408
  const offset = cfg.offset || [0, 0];
403
409
  const fallback = cfg.fallback || this.options.fallbackCursor;
404
410
  let css = `cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback};`;
411
+
405
412
  const hasAnimation = cfg.frames !== undefined && cfg.duration !== undefined &&
406
413
  ((Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) ||
407
414
  (typeof cfg.frames === 'number' && typeof cfg.duration === 'number'));
415
+
408
416
  if (hasAnimation && frameUrls.length > 1) {
409
417
  const totalDuration = Array.isArray(cfg.duration) ? cfg.duration.reduce((a, b) => a + b, 0) : cfg.duration;
410
418
  css += ` animation: ac_anim_${name} ${totalDuration}s steps(1) infinite ${cfg.pingpong ? 'alternate' : ''};`;
419
+ } else {
420
+ // 静态光标:一帧动画,只播放一次,结束后保持
421
+ css += ` animation: ac_anim_${name}_static 0.001s forwards steps(1);`;
411
422
  }
412
423
  return css;
413
424
  }
@@ -498,15 +509,33 @@
498
509
 
499
510
  disable() {
500
511
  if (this.disabled) return;
512
+ // 移除样式表
513
+ if (this.styleEl) {
514
+ this.styleEl.remove();
515
+ this.styleEl = null;
516
+ }
517
+ // 移除 debug 相关
518
+ if (this.debugEl) {
519
+ this.debugEl.remove();
520
+ this.debugEl = null;
521
+ }
522
+ if (this._onMouseMove) {
523
+ document.removeEventListener('mousemove', this._onMouseMove);
524
+ this._onMouseMove = null;
525
+ }
501
526
  this.disabled = true;
502
- document.body.classList.add('animecursor-disabled');
503
527
  if (this.options.debug) console.log('[AnimeCursor] Disabled');
504
528
  }
505
529
 
506
530
  enable() {
507
531
  if (!this.disabled) return;
508
532
  this.disabled = false;
509
- document.body.classList.remove('animecursor-disabled');
533
+ // 重新注入样式
534
+ this._injectStyles();
535
+ // 如果 debug 模式开启,重新初始化 debug
536
+ if (this.options.debug) {
537
+ this._initDebug();
538
+ }
510
539
  if (this.options.debug) console.log('[AnimeCursor] Enabled');
511
540
  }
512
541
  }
@@ -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]",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"))}}});
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{const s=`ac_anim_${e}_static`;t+=`@keyframes ${s} {\n`,t+=` 0%, 100% { cursor: url("${n[0]}") ${o[0]} ${o[1]}, ${i}; }\n`,t+="}\n";const a=`${s} 0.001s forwards steps(1)`;u=a,t+=`${r} { cursor: url("${n[0]}") ${o[0]} ${o[1]}, ${i}; animation: ${a}; }\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":""};`}else i+=` animation: ac_anim_${e}_static 0.001s forwards steps(1);`;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.1.0",
3
+ "version": "2.1.2",
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",