anime-cursor 0.3.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  // AnimeCursor by github@ShuninYu
2
- // v0.3.0
2
+ // v1.0.0
3
3
 
4
4
  // 静态变量存储唯一实例
5
5
  let _instance = null;
@@ -173,14 +173,14 @@ class AnimeCursor {
173
173
  // ----------------------------
174
174
  _validateOptions() {
175
175
  if (this.disabled) return;
176
-
176
+
177
177
  if (!this.options || !this.options.cursors) {
178
178
  console.error('[AnimeCursor] missing cursors set up');
179
179
  throw new Error('AnimeCursor init failed');
180
180
  }
181
-
181
+
182
182
  this.defaultCursorType = null;
183
-
183
+
184
184
  for (const [name, cfg] of Object.entries(this.options.cursors)) {
185
185
  if (cfg.default === true) {
186
186
  if (this.defaultCursorType) {
@@ -189,7 +189,7 @@ class AnimeCursor {
189
189
  this.defaultCursorType = name;
190
190
  }
191
191
  }
192
-
192
+
193
193
  for (const [name, cfg] of Object.entries(this.options.cursors)) {
194
194
  const required = ['size', 'image'];
195
195
  required.forEach(key => {
@@ -198,14 +198,51 @@ class AnimeCursor {
198
198
  throw new Error('AnimeCursor init failed');
199
199
  }
200
200
  });
201
-
202
- if (cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
203
- console.error(`[AnimeCursor] default cursor "${name}" 's tags must be an array if provided`);
204
- throw new Error('AnimeCursor init failed');
201
+
202
+ // 新增:校验 frames duration 的类型一致性
203
+ if (cfg.frames !== undefined) {
204
+ if (Array.isArray(cfg.frames)) {
205
+ // frames 为数组时,duration 必须为等长数组
206
+ if (!Array.isArray(cfg.duration) || cfg.duration.length !== cfg.frames.length) {
207
+ console.error(`[AnimeCursor] cursor "${name}" has frames as array but duration is not an array of same length`);
208
+ throw new Error('AnimeCursor init failed');
209
+ }
210
+ // 检查 frames 数组元素是否为正整数
211
+ for (let f of cfg.frames) {
212
+ if (!Number.isInteger(f) || f <= 0) {
213
+ console.error(`[AnimeCursor] cursor "${name}" frames array must contain positive integers`);
214
+ throw new Error('AnimeCursor init failed');
215
+ }
216
+ }
217
+ // 检查 duration 数组元素是否为正数
218
+ for (let d of cfg.duration) {
219
+ if (typeof d !== 'number' || d <= 0) {
220
+ console.error(`[AnimeCursor] cursor "${name}" duration array must contain positive numbers`);
221
+ throw new Error('AnimeCursor init failed');
222
+ }
223
+ }
224
+ } else if (typeof cfg.frames === 'number') {
225
+ // frames 为数字时,duration 可选,但不能是数组
226
+ if (Array.isArray(cfg.duration)) {
227
+ console.error(`[AnimeCursor] cursor "${name}" frames is number but duration is array, must be consistent`);
228
+ throw new Error('AnimeCursor init failed');
229
+ }
230
+ if (!Number.isInteger(cfg.frames) || cfg.frames <= 0) {
231
+ console.error(`[AnimeCursor] cursor "${name}" frames must be a positive integer`);
232
+ throw new Error('AnimeCursor init failed');
233
+ }
234
+ if (cfg.duration !== undefined && (typeof cfg.duration !== 'number' || cfg.duration <= 0)) {
235
+ console.error(`[AnimeCursor] cursor "${name}" duration must be a positive number`);
236
+ throw new Error('AnimeCursor init failed');
237
+ }
238
+ } else {
239
+ console.error(`[AnimeCursor] cursor "${name}" frames must be a number or an array`);
240
+ throw new Error('AnimeCursor init failed');
241
+ }
205
242
  }
206
-
207
- if (cfg.duration !== undefined && typeof cfg.duration !== 'number') {
208
- console.error(`[AnimeCursor] cursor "${name}" 's duration must be a number(seconds)`);
243
+
244
+ if (cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
245
+ console.error(`[AnimeCursor] cursor "${name}" 's tags must be an array if provided`);
209
246
  throw new Error('AnimeCursor init failed');
210
247
  }
211
248
  }
@@ -361,57 +398,110 @@ class AnimeCursor {
361
398
 
362
399
  /* 每种光标以及debug生成 CSS */
363
400
  for (const [type, cfg] of Object.entries(this.options.cursors)) {
364
- const className = `.cursor-${type}`;
365
- const size = cfg.size;
366
- const frames = cfg.frames;
367
- const image = cfg.image;
368
- const offset = cfg.offset;
369
- const zIndex = cfg.zIndex;
370
- const scale = cfg.scale;
371
- const isGif = image.toLowerCase().endsWith('.gif');
372
- var pixel;
373
- if (cfg.pixel) {pixel = 'pixelated';}
374
- else {pixel = 'auto';}
375
-
376
- css += `
377
- ${className} {
401
+ const className = `.cursor-${type}`;
402
+ const size = cfg.size;
403
+ const image = cfg.image;
404
+ const offset = cfg.offset;
405
+ const zIndex = cfg.zIndex;
406
+ const scale = cfg.scale;
407
+ const isGif = image.toLowerCase().endsWith('.gif');
408
+ const pixel = cfg.pixel ? 'pixelated' : 'auto';
409
+
410
+ // 基础样式
411
+ css += `
412
+ ${className} {
378
413
  width: ${size[0]}px;
379
414
  height: ${size[1]}px;
380
415
  background-image: url("${image}");
381
416
  image-rendering: ${pixel};
382
417
  ${(scale || offset) ? `transform: ${[scale && `scale(${scale[0]}, ${scale[1]})`, offset && `translate(-${offset[0]}px, -${offset[1]}px)`].filter(Boolean).join(' ')};` : ''}
383
-
384
418
  ${zIndex !== undefined ? `z-index:${zIndex};` : ''}
385
- }`;
419
+ }`;
420
+
421
+ // 动画生成
422
+ const frames = cfg.frames;
423
+ const duration = cfg.duration;
386
424
 
387
- /* 精灵图动画 */
388
- const duration = cfg.duration;
389
- const hasAnimation =
390
- !isGif &&
391
- frames > 1 &&
392
- typeof duration === 'number';
425
+ // 判断是否使用新逻辑(数组形式)
426
+ if (Array.isArray(frames) && Array.isArray(duration) && frames.length === duration.length) {
427
+ // 计算总帧数和总时长
428
+ const totalFrames = frames.reduce((a, b) => a + b, 0);
429
+ const totalDuration = duration.reduce((a, b) => a + b, 0);
430
+ const hasAnimation = !isGif && totalFrames > 1 && totalDuration > 0;
393
431
 
394
432
  if (hasAnimation) {
395
433
  const animName = `animecursor_${type}`;
434
+ // 构建关键帧数组
435
+ const keyframes = [];
436
+ let cumPercent = 0;
437
+ let frameIndex = 0;
438
+
439
+ for (let p = 0; p < frames.length; p++) {
440
+ const segFrames = frames[p];
441
+ const segDuration = duration[p];
442
+ const segPercent = segDuration / totalDuration;
443
+ for (let j = 0; j < segFrames; j++) {
444
+ const startPercent = cumPercent + (j * segPercent) / segFrames;
445
+ keyframes.push({
446
+ percent: startPercent,
447
+ pos: -frameIndex * size[0]
448
+ });
449
+ frameIndex++;
450
+ }
451
+ cumPercent += segPercent;
452
+ }
453
+ // 添加最后一帧的结束点(100%)
454
+ keyframes.push({
455
+ percent: 1.0,
456
+ pos: -(totalFrames - 1) * size[0]
457
+ });
396
458
 
459
+ // 生成动画属性
397
460
  css += `
398
461
  ${className} {
399
- animation: ${animName} steps(${frames}) ${duration}s infinite ${cfg.pingpong ? 'alternate' : ''};
462
+ animation: ${animName} ${totalDuration}s infinite ${cfg.pingpong ? 'alternate' : ''};
463
+ }`;
464
+
465
+ // 生成 @keyframes
466
+ css += `
467
+ @keyframes ${animName} {`;
468
+ for (let i = 0; i < keyframes.length; i++) {
469
+ const kf = keyframes[i];
470
+ let percent = (kf.percent * 100).toFixed(5);
471
+ if (kf.percent === 1.0) percent = '100'; // 确保100%显示为整数
472
+ css += `
473
+ ${percent}% {
474
+ background-position: ${kf.pos}px 0;
475
+ ${i < keyframes.length - 1 ? 'animation-timing-function: steps(1, end);' : ''}
476
+ }`;
477
+ }
478
+ css += `
479
+ }`;
480
+ }
481
+ } else if (typeof frames === 'number' && typeof duration === 'number') {
482
+ // 旧逻辑:均匀动画
483
+ const hasAnimation = !isGif && frames > 1 && duration > 0;
484
+ if (hasAnimation) {
485
+ const animName = `animecursor_${type}`;
486
+ css += `
487
+ ${className} {
488
+ animation: ${animName} steps(${frames}) ${duration}s infinite ${cfg.pingpong ? 'alternate' : ''};
400
489
  }
401
490
 
402
491
  @keyframes ${animName} {
403
- from { background-position: 0 0; }
404
- to { background-position: -${size[0] * frames}px 0; }
405
- }
406
- `;
492
+ from { background-position: 0 0; }
493
+ to { background-position: -${size[0] * frames}px 0; }
494
+ }`;
407
495
  }
408
496
  }
409
-
410
- style.textContent = css;
411
- document.head.appendChild(style);
412
- this.styleEl = style;
497
+ // 若 frames 未定义或为单帧,则不生成动画,仅保留基础样式
413
498
  }
414
499
 
500
+ style.textContent = css;
501
+ document.head.appendChild(style);
502
+ this.styleEl = style;
503
+ }
504
+
415
505
  // ----------------------------
416
506
  // 给元素自动添加 data-cursor
417
507
  // ----------------------------
@@ -5,7 +5,7 @@
5
5
  })(this, (function () { 'use strict';
6
6
 
7
7
  // AnimeCursor by github@ShuninYu
8
- // v0.3.0
8
+ // v1.0.0
9
9
 
10
10
  // 静态变量存储唯一实例
11
11
  let _instance = null;
@@ -179,14 +179,14 @@
179
179
  // ----------------------------
180
180
  _validateOptions() {
181
181
  if (this.disabled) return;
182
-
182
+
183
183
  if (!this.options || !this.options.cursors) {
184
184
  console.error('[AnimeCursor] missing cursors set up');
185
185
  throw new Error('AnimeCursor init failed');
186
186
  }
187
-
187
+
188
188
  this.defaultCursorType = null;
189
-
189
+
190
190
  for (const [name, cfg] of Object.entries(this.options.cursors)) {
191
191
  if (cfg.default === true) {
192
192
  if (this.defaultCursorType) {
@@ -195,7 +195,7 @@
195
195
  this.defaultCursorType = name;
196
196
  }
197
197
  }
198
-
198
+
199
199
  for (const [name, cfg] of Object.entries(this.options.cursors)) {
200
200
  const required = ['size', 'image'];
201
201
  required.forEach(key => {
@@ -204,14 +204,51 @@
204
204
  throw new Error('AnimeCursor init failed');
205
205
  }
206
206
  });
207
-
208
- if (cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
209
- console.error(`[AnimeCursor] default cursor "${name}" 's tags must be an array if provided`);
210
- throw new Error('AnimeCursor init failed');
207
+
208
+ // 新增:校验 frames duration 的类型一致性
209
+ if (cfg.frames !== undefined) {
210
+ if (Array.isArray(cfg.frames)) {
211
+ // frames 为数组时,duration 必须为等长数组
212
+ if (!Array.isArray(cfg.duration) || cfg.duration.length !== cfg.frames.length) {
213
+ console.error(`[AnimeCursor] cursor "${name}" has frames as array but duration is not an array of same length`);
214
+ throw new Error('AnimeCursor init failed');
215
+ }
216
+ // 检查 frames 数组元素是否为正整数
217
+ for (let f of cfg.frames) {
218
+ if (!Number.isInteger(f) || f <= 0) {
219
+ console.error(`[AnimeCursor] cursor "${name}" frames array must contain positive integers`);
220
+ throw new Error('AnimeCursor init failed');
221
+ }
222
+ }
223
+ // 检查 duration 数组元素是否为正数
224
+ for (let d of cfg.duration) {
225
+ if (typeof d !== 'number' || d <= 0) {
226
+ console.error(`[AnimeCursor] cursor "${name}" duration array must contain positive numbers`);
227
+ throw new Error('AnimeCursor init failed');
228
+ }
229
+ }
230
+ } else if (typeof cfg.frames === 'number') {
231
+ // frames 为数字时,duration 可选,但不能是数组
232
+ if (Array.isArray(cfg.duration)) {
233
+ console.error(`[AnimeCursor] cursor "${name}" frames is number but duration is array, must be consistent`);
234
+ throw new Error('AnimeCursor init failed');
235
+ }
236
+ if (!Number.isInteger(cfg.frames) || cfg.frames <= 0) {
237
+ console.error(`[AnimeCursor] cursor "${name}" frames must be a positive integer`);
238
+ throw new Error('AnimeCursor init failed');
239
+ }
240
+ if (cfg.duration !== undefined && (typeof cfg.duration !== 'number' || cfg.duration <= 0)) {
241
+ console.error(`[AnimeCursor] cursor "${name}" duration must be a positive number`);
242
+ throw new Error('AnimeCursor init failed');
243
+ }
244
+ } else {
245
+ console.error(`[AnimeCursor] cursor "${name}" frames must be a number or an array`);
246
+ throw new Error('AnimeCursor init failed');
247
+ }
211
248
  }
212
-
213
- if (cfg.duration !== undefined && typeof cfg.duration !== 'number') {
214
- console.error(`[AnimeCursor] cursor "${name}" 's duration must be a number(seconds)`);
249
+
250
+ if (cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
251
+ console.error(`[AnimeCursor] cursor "${name}" 's tags must be an array if provided`);
215
252
  throw new Error('AnimeCursor init failed');
216
253
  }
217
254
  }
@@ -367,57 +404,110 @@
367
404
 
368
405
  /* 每种光标以及debug生成 CSS */
369
406
  for (const [type, cfg] of Object.entries(this.options.cursors)) {
370
- const className = `.cursor-${type}`;
371
- const size = cfg.size;
372
- const frames = cfg.frames;
373
- const image = cfg.image;
374
- const offset = cfg.offset;
375
- const zIndex = cfg.zIndex;
376
- const scale = cfg.scale;
377
- const isGif = image.toLowerCase().endsWith('.gif');
378
- var pixel;
379
- if (cfg.pixel) {pixel = 'pixelated';}
380
- else {pixel = 'auto';}
381
-
382
- css += `
383
- ${className} {
407
+ const className = `.cursor-${type}`;
408
+ const size = cfg.size;
409
+ const image = cfg.image;
410
+ const offset = cfg.offset;
411
+ const zIndex = cfg.zIndex;
412
+ const scale = cfg.scale;
413
+ const isGif = image.toLowerCase().endsWith('.gif');
414
+ const pixel = cfg.pixel ? 'pixelated' : 'auto';
415
+
416
+ // 基础样式
417
+ css += `
418
+ ${className} {
384
419
  width: ${size[0]}px;
385
420
  height: ${size[1]}px;
386
421
  background-image: url("${image}");
387
422
  image-rendering: ${pixel};
388
423
  ${(scale || offset) ? `transform: ${[scale && `scale(${scale[0]}, ${scale[1]})`, offset && `translate(-${offset[0]}px, -${offset[1]}px)`].filter(Boolean).join(' ')};` : ''}
389
-
390
424
  ${zIndex !== undefined ? `z-index:${zIndex};` : ''}
391
- }`;
425
+ }`;
426
+
427
+ // 动画生成
428
+ const frames = cfg.frames;
429
+ const duration = cfg.duration;
392
430
 
393
- /* 精灵图动画 */
394
- const duration = cfg.duration;
395
- const hasAnimation =
396
- !isGif &&
397
- frames > 1 &&
398
- typeof duration === 'number';
431
+ // 判断是否使用新逻辑(数组形式)
432
+ if (Array.isArray(frames) && Array.isArray(duration) && frames.length === duration.length) {
433
+ // 计算总帧数和总时长
434
+ const totalFrames = frames.reduce((a, b) => a + b, 0);
435
+ const totalDuration = duration.reduce((a, b) => a + b, 0);
436
+ const hasAnimation = !isGif && totalFrames > 1 && totalDuration > 0;
399
437
 
400
438
  if (hasAnimation) {
401
439
  const animName = `animecursor_${type}`;
440
+ // 构建关键帧数组
441
+ const keyframes = [];
442
+ let cumPercent = 0;
443
+ let frameIndex = 0;
444
+
445
+ for (let p = 0; p < frames.length; p++) {
446
+ const segFrames = frames[p];
447
+ const segDuration = duration[p];
448
+ const segPercent = segDuration / totalDuration;
449
+ for (let j = 0; j < segFrames; j++) {
450
+ const startPercent = cumPercent + (j * segPercent) / segFrames;
451
+ keyframes.push({
452
+ percent: startPercent,
453
+ pos: -frameIndex * size[0]
454
+ });
455
+ frameIndex++;
456
+ }
457
+ cumPercent += segPercent;
458
+ }
459
+ // 添加最后一帧的结束点(100%)
460
+ keyframes.push({
461
+ percent: 1.0,
462
+ pos: -(totalFrames - 1) * size[0]
463
+ });
402
464
 
465
+ // 生成动画属性
403
466
  css += `
404
467
  ${className} {
405
- animation: ${animName} steps(${frames}) ${duration}s infinite ${cfg.pingpong ? 'alternate' : ''};
468
+ animation: ${animName} ${totalDuration}s infinite ${cfg.pingpong ? 'alternate' : ''};
469
+ }`;
470
+
471
+ // 生成 @keyframes
472
+ css += `
473
+ @keyframes ${animName} {`;
474
+ for (let i = 0; i < keyframes.length; i++) {
475
+ const kf = keyframes[i];
476
+ let percent = (kf.percent * 100).toFixed(5);
477
+ if (kf.percent === 1.0) percent = '100'; // 确保100%显示为整数
478
+ css += `
479
+ ${percent}% {
480
+ background-position: ${kf.pos}px 0;
481
+ ${i < keyframes.length - 1 ? 'animation-timing-function: steps(1, end);' : ''}
482
+ }`;
483
+ }
484
+ css += `
485
+ }`;
486
+ }
487
+ } else if (typeof frames === 'number' && typeof duration === 'number') {
488
+ // 旧逻辑:均匀动画
489
+ const hasAnimation = !isGif && frames > 1 && duration > 0;
490
+ if (hasAnimation) {
491
+ const animName = `animecursor_${type}`;
492
+ css += `
493
+ ${className} {
494
+ animation: ${animName} steps(${frames}) ${duration}s infinite ${cfg.pingpong ? 'alternate' : ''};
406
495
  }
407
496
 
408
497
  @keyframes ${animName} {
409
- from { background-position: 0 0; }
410
- to { background-position: -${size[0] * frames}px 0; }
411
- }
412
- `;
498
+ from { background-position: 0 0; }
499
+ to { background-position: -${size[0] * frames}px 0; }
500
+ }`;
413
501
  }
414
502
  }
415
-
416
- style.textContent = css;
417
- document.head.appendChild(style);
418
- this.styleEl = style;
503
+ // 若 frames 未定义或为单帧,则不生成动画,仅保留基础样式
419
504
  }
420
505
 
506
+ style.textContent = css;
507
+ document.head.appendChild(style);
508
+ this.styleEl = style;
509
+ }
510
+
421
511
  // ----------------------------
422
512
  // 给元素自动添加 data-cursor
423
513
  // ----------------------------
@@ -1 +1 @@
1
- !function(e,s){"object"==typeof exports&&"undefined"!=typeof module?module.exports=s():"function"==typeof define&&define.amd?define(s):(e="undefined"!=typeof globalThis?globalThis:e||self).AnimeCursor=s()}(this,function(){"use strict";let e=null;return class{static get instance(){return e}static destroy(){return!!e&&(e.destroy(),!0)}static refresh(){return!!e&&(e.refresh(),!0)}static disable(){return!!e&&(e.disable(),!0)}static enable(){return!!e&&(e.enable(),!0)}constructor(s={}){return e?(console.warn("[AnimeCursor] AnimeCursor already exists."),e):(this.options={displayOnLoad:!1,enableTouch:!1,debug:!1,...s},this.disabled=!1,this.options.enableTouch||this.isMouseLikeDevice()?(this.cursorEl=null,this.lastCursorType=null,this.debugEl=null,this.styleEl=null,this._onMouseMove=null,this._validateOptions(),this._injectPreload(),this._checkDomLoad(),void(e=this)):(this.disabled=!0,void(this.options.debug&&console.warn("[AnimeCursor] Touch device detected, cursor disabled."))))}isMouseLikeDevice(){if(!this.disabled)return window.matchMedia("(pointer: fine)").matches}refresh(){this.disabled||(this.options.debug&&console.info("[AnimeCursor] starting refresh..."),this._bindElements(!0))}destroy(){if(!this.disabled){this._onMouseMove&&(document.removeEventListener("mousemove",this._onMouseMove),this._onMouseMove=null),this.cursorEl&&(this.cursorEl.remove(),this.cursorEl=null),this.debugEl&&(this.debugEl.remove(),this.debugEl=null),this.styleEl&&(this.styleEl.remove(),this.styleEl=null);for(const e of Object.values(this.options.cursors))e.tags&&Array.isArray(e.tags)&&e.tags.forEach(e=>{document.querySelectorAll(e).forEach(e=>{e.dataset.cursorBound&&(delete e.dataset.cursor,delete e.dataset.cursorBound)})});this.lastCursorType=null,e===this&&(e=null)}}disable(){this.disabled||(this.disabled=!0,this.cursorEl&&(this.cursorEl.style.display="none",this.styleEl.innerHTML=this.styleEl.innerHTML.replace("* {cursor: none !important;}",""),console.log("[AnimeCursor] AnimeCursor disabled!")))}enable(){this.disabled&&(this.disabled=!1,this.cursorEl&&(this.cursorEl.style.display="",this.styleEl.innerHTML+="* {cursor: none; !important;}",console.log("[AnimeCursor] AnimeCursor enabled!")))}_validateOptions(){if(!this.disabled){if(!this.options||!this.options.cursors)throw console.error("[AnimeCursor] missing cursors set up"),new Error("AnimeCursor init failed");this.defaultCursorType=null;for(const[e,s]of Object.entries(this.options.cursors))if(!0===s.default){if(this.defaultCursorType)throw new Error("[AnimeCursor] There can only be one default cursor");this.defaultCursorType=e}for(const[e,s]of Object.entries(this.options.cursors)){if(["size","image"].forEach(t=>{if(void 0===s[t])throw console.error(`[AnimeCursor] cursor "${e}" missing required setting: ${t}`),new Error("AnimeCursor init failed")}),void 0!==s.tags&&!Array.isArray(s.tags))throw console.error(`[AnimeCursor] default cursor "${e}" 's tags must be an array if provided`),new Error("AnimeCursor init failed");if(void 0!==s.duration&&"number"!=typeof s.duration)throw console.error(`[AnimeCursor] cursor "${e}" 's duration must be a number(seconds)`),new Error("AnimeCursor init failed")}}}_checkDomLoad(){const e=()=>{this._injectHTML(),this._injectCSS(),this._bindElements(),this._bindMouse()};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",e):e()}_injectPreload(){if(this.disabled)return;const e=new Set;for(const s of Object.values(this.options.cursors))s.image&&e.add(s.image);e.forEach(e=>{const s=document.createElement("link");s.rel="preload",s.as="image",s.href=e,e.startsWith("http")&&!e.startsWith(window.location.origin)&&(s.crossOrigin="anonymous"),document.head.appendChild(s),this.options.debug&&console.info(`[AnimeCursor] Preloading image: ${e}`)}),this.options.debug&&e.size>0&&console.info(`[AnimeCursor] Preloaded ${e.size} cursor image(s)`)}_injectHTML(){if(this.disabled)return;const e=document.createElement("div");if(e.id="anime-cursor",this.options.debug){e.className="cursor-default cursor-debugmode";const s=document.createElement("div");s.className="anime-cursor-debug",document.body.appendChild(s),this.debugEl=s}else e.className="cursor-default";this.options.displayOnLoad?e.style.display="block":(e.style.display="none",e.dataset.animecursorHide="true"),document.body.appendChild(e),this.cursorEl=e}_injectCSS(){if(this.disabled)return;const e=document.createElement("style");e.id="animecursor-styles";let s="";s+=`\n * {cursor: none !important;}\n #anime-cursor {\n position: fixed;\n top: 0;\n left: 0;\n pointer-events: none;\n background-repeat: no-repeat;\n transform-origin: 0 0;\n transform-style: preserve-3d;\n z-index: ${this._getMaxZIndex()};\n }\n .cursor-debugmode {\n border: 1px solid green;\n }\n .anime-cursor-debug {\n position: fixed;\n top: 0;\n left: 0;\n width: fit-content;\n height: fit-content;\n padding: 5px;\n font-size: 16px;\n text-wrap: nowrap;\n color: red;\n pointer-events: none;\n overflow: visible;\n z-index: 2147483647;\n }\n .anime-cursor-debug::before {\n position: absolute;\n content: "";\n top: 0;\n left: 0;\n width: 100vw;\n height: 1px;\n background-color: red;\n }\n .anime-cursor-debug::after {\n position: absolute;\n content: "";\n top: 0;\n left: 0;\n width: 1px;\n height: 100vh;\n background-color: red;\n }\n `;for(const[e,o]of Object.entries(this.options.cursors)){const n=`.cursor-${e}`,r=o.size,i=o.frames,l=o.image,d=o.offset,a=o.zIndex,u=o.scale,c=l.toLowerCase().endsWith(".gif");var t;t=o.pixel?"pixelated":"auto",s+=`\n ${n} {\n width: ${r[0]}px;\n height: ${r[1]}px;\n background-image: url("${l}");\n image-rendering: ${t};\n ${u||d?`transform: ${[u&&`scale(${u[0]}, ${u[1]})`,d&&`translate(-${d[0]}px, -${d[1]}px)`].filter(Boolean).join(" ")};`:""}\n \n ${void 0!==a?`z-index:${a};`:""}\n }`;const h=o.duration;if(!c&&i>1&&"number"==typeof h){const t=`animecursor_${e}`;s+=`\n ${n} {\n animation: ${t} steps(${i}) ${h}s infinite ${o.pingpong?"alternate":""};\n }\n\n @keyframes ${t} {\n from { background-position: 0 0; }\n to { background-position: -${r[0]*i}px 0; }\n }\n `}}e.textContent=s,document.head.appendChild(e),this.styleEl=e}_bindElements(e){if(!this.disabled){for(const[e,s]of Object.entries(this.options.cursors))s.tags&&Array.isArray(s.tags)&&0!==s.tags.length&&s.tags.forEach(s=>{const t=s.toUpperCase();document.querySelectorAll(t).forEach(s=>{s.dataset.cursor||(s.dataset.cursor=e,s.dataset.cursorBound="true")})});e&&console.info("[AnimeCursor] refresh done")}}_bindMouse(){this.disabled||(this._onMouseMove=e=>{if(this.disabled)return;const s=e.clientX,t=e.clientY;this.cursorEl.style.left=s+"px",this.cursorEl.style.top=t+"px",this.cursorEl.dataset.animecursorHide&&(this.cursorEl.style.display="block"),this.debugEl&&(this.debugEl.style.left=s+"px",this.debugEl.style.top=t+"px");let o=null;const n=document.elementFromPoint(s,t);n&&n.dataset&&n.dataset.cursor?o=n.dataset.cursor:this.defaultCursorType&&(o=this.defaultCursorType),o&&(this.debugEl&&(this.debugEl.textContent=`(${s}px , ${t}px) ${o}`),o!==this.lastCursorType&&(this.debugEl?this.cursorEl.className=`cursor-${o} cursor-debugmode`:this.cursorEl.className=`cursor-${o}`,this.lastCursorType=o))},document.addEventListener("mousemove",this._onMouseMove),console.log("[AnimeCursor] AnimeCursor setted up."))}_getMaxZIndex(){if(!this.disabled)return 2147483646}}});
1
+ !function(e,r){"object"==typeof exports&&"undefined"!=typeof module?module.exports=r():"function"==typeof define&&define.amd?define(r):(e="undefined"!=typeof globalThis?globalThis:e||self).AnimeCursor=r()}(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(r={}){return e?(console.warn("[AnimeCursor] AnimeCursor already exists."),e):(this.options={displayOnLoad:!1,enableTouch:!1,debug:!1,...r},this.disabled=!1,this.options.enableTouch||this.isMouseLikeDevice()?(this.cursorEl=null,this.lastCursorType=null,this.debugEl=null,this.styleEl=null,this._onMouseMove=null,this._validateOptions(),this._injectPreload(),this._checkDomLoad(),void(e=this)):(this.disabled=!0,void(this.options.debug&&console.warn("[AnimeCursor] Touch device detected, cursor disabled."))))}isMouseLikeDevice(){if(!this.disabled)return window.matchMedia("(pointer: fine)").matches}refresh(){this.disabled||(this.options.debug&&console.info("[AnimeCursor] starting refresh..."),this._bindElements(!0))}destroy(){if(!this.disabled){this._onMouseMove&&(document.removeEventListener("mousemove",this._onMouseMove),this._onMouseMove=null),this.cursorEl&&(this.cursorEl.remove(),this.cursorEl=null),this.debugEl&&(this.debugEl.remove(),this.debugEl=null),this.styleEl&&(this.styleEl.remove(),this.styleEl=null);for(const e of Object.values(this.options.cursors))e.tags&&Array.isArray(e.tags)&&e.tags.forEach(e=>{document.querySelectorAll(e).forEach(e=>{e.dataset.cursorBound&&(delete e.dataset.cursor,delete e.dataset.cursorBound)})});this.lastCursorType=null,e===this&&(e=null)}}disable(){this.disabled||(this.disabled=!0,this.cursorEl&&(this.cursorEl.style.display="none",this.styleEl.innerHTML=this.styleEl.innerHTML.replace("* {cursor: none !important;}",""),console.log("[AnimeCursor] AnimeCursor disabled!")))}enable(){this.disabled&&(this.disabled=!1,this.cursorEl&&(this.cursorEl.style.display="",this.styleEl.innerHTML+="* {cursor: none; !important;}",console.log("[AnimeCursor] AnimeCursor enabled!")))}_validateOptions(){if(!this.disabled){if(!this.options||!this.options.cursors)throw console.error("[AnimeCursor] missing cursors set up"),new Error("AnimeCursor init failed");this.defaultCursorType=null;for(const[e,r]of Object.entries(this.options.cursors))if(!0===r.default){if(this.defaultCursorType)throw new Error("[AnimeCursor] There can only be one default cursor");this.defaultCursorType=e}for(const[e,r]of Object.entries(this.options.cursors)){if(["size","image"].forEach(s=>{if(void 0===r[s])throw console.error(`[AnimeCursor] cursor "${e}" missing required setting: ${s}`),new Error("AnimeCursor init failed")}),void 0!==r.frames)if(Array.isArray(r.frames)){if(!Array.isArray(r.duration)||r.duration.length!==r.frames.length)throw console.error(`[AnimeCursor] cursor "${e}" has frames as array but duration is not an array of same length`),new Error("AnimeCursor init failed");for(let s of r.frames)if(!Number.isInteger(s)||s<=0)throw console.error(`[AnimeCursor] cursor "${e}" frames array must contain positive integers`),new Error("AnimeCursor init failed");for(let s of r.duration)if("number"!=typeof s||s<=0)throw console.error(`[AnimeCursor] cursor "${e}" duration array must contain positive numbers`),new Error("AnimeCursor init failed")}else{if("number"!=typeof r.frames)throw console.error(`[AnimeCursor] cursor "${e}" frames must be a number or an array`),new Error("AnimeCursor init failed");if(Array.isArray(r.duration))throw console.error(`[AnimeCursor] cursor "${e}" frames is number but duration is array, must be consistent`),new Error("AnimeCursor init failed");if(!Number.isInteger(r.frames)||r.frames<=0)throw console.error(`[AnimeCursor] cursor "${e}" frames must be a positive integer`),new Error("AnimeCursor init failed");if(void 0!==r.duration&&("number"!=typeof r.duration||r.duration<=0))throw console.error(`[AnimeCursor] cursor "${e}" duration must be a positive number`),new Error("AnimeCursor init failed")}if(void 0!==r.tags&&!Array.isArray(r.tags))throw console.error(`[AnimeCursor] cursor "${e}" 's tags must be an array if provided`),new Error("AnimeCursor init failed")}}}_checkDomLoad(){const e=()=>{this._injectHTML(),this._injectCSS(),this._bindElements(),this._bindMouse()};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",e):e()}_injectPreload(){if(this.disabled)return;const e=new Set;for(const r of Object.values(this.options.cursors))r.image&&e.add(r.image);e.forEach(e=>{const r=document.createElement("link");r.rel="preload",r.as="image",r.href=e,e.startsWith("http")&&!e.startsWith(window.location.origin)&&(r.crossOrigin="anonymous"),document.head.appendChild(r),this.options.debug&&console.info(`[AnimeCursor] Preloading image: ${e}`)}),this.options.debug&&e.size>0&&console.info(`[AnimeCursor] Preloaded ${e.size} cursor image(s)`)}_injectHTML(){if(this.disabled)return;const e=document.createElement("div");if(e.id="anime-cursor",this.options.debug){e.className="cursor-default cursor-debugmode";const r=document.createElement("div");r.className="anime-cursor-debug",document.body.appendChild(r),this.debugEl=r}else e.className="cursor-default";this.options.displayOnLoad?e.style.display="block":(e.style.display="none",e.dataset.animecursorHide="true"),document.body.appendChild(e),this.cursorEl=e}_injectCSS(){if(this.disabled)return;const e=document.createElement("style");e.id="animecursor-styles";let r="";r+=`\n * {cursor: none !important;}\n #anime-cursor {\n position: fixed;\n top: 0;\n left: 0;\n pointer-events: none;\n background-repeat: no-repeat;\n transform-origin: 0 0;\n transform-style: preserve-3d;\n z-index: ${this._getMaxZIndex()};\n }\n .cursor-debugmode {\n border: 1px solid green;\n }\n .anime-cursor-debug {\n position: fixed;\n top: 0;\n left: 0;\n width: fit-content;\n height: fit-content;\n padding: 5px;\n font-size: 16px;\n text-wrap: nowrap;\n color: red;\n pointer-events: none;\n overflow: visible;\n z-index: 2147483647;\n }\n .anime-cursor-debug::before {\n position: absolute;\n content: "";\n top: 0;\n left: 0;\n width: 100vw;\n height: 1px;\n background-color: red;\n }\n .anime-cursor-debug::after {\n position: absolute;\n content: "";\n top: 0;\n left: 0;\n width: 1px;\n height: 100vh;\n background-color: red;\n }\n `;for(const[e,s]of Object.entries(this.options.cursors)){const t=`.cursor-${e}`,o=s.size,n=s.image,i=s.offset,a=s.zIndex,u=s.scale,l=n.toLowerCase().endsWith(".gif"),d=s.pixel?"pixelated":"auto";r+=`\n ${t} {\n width: ${o[0]}px;\n height: ${o[1]}px;\n background-image: url("${n}");\n image-rendering: ${d};\n ${u||i?`transform: ${[u&&`scale(${u[0]}, ${u[1]})`,i&&`translate(-${i[0]}px, -${i[1]}px)`].filter(Boolean).join(" ")};`:""}\n ${void 0!==a?`z-index:${a};`:""}\n }`;const c=s.frames,h=s.duration;if(Array.isArray(c)&&Array.isArray(h)&&c.length===h.length){const n=c.reduce((e,r)=>e+r,0),i=h.reduce((e,r)=>e+r,0);if(!l&&n>1&&i>0){const a=`animecursor_${e}`,u=[];let l=0,d=0;for(let e=0;e<c.length;e++){const r=c[e],s=h[e]/i;for(let e=0;e<r;e++){const t=l+e*s/r;u.push({percent:t,pos:-d*o[0]}),d++}l+=s}u.push({percent:1,pos:-(n-1)*o[0]}),r+=`\n ${t} {\n animation: ${a} ${i}s infinite ${s.pingpong?"alternate":""};\n }`,r+=`\n @keyframes ${a} {`;for(let e=0;e<u.length;e++){const s=u[e];let t=(100*s.percent).toFixed(5);1===s.percent&&(t="100"),r+=`\n ${t}% {\n background-position: ${s.pos}px 0;\n ${e<u.length-1?"animation-timing-function: steps(1, end);":""}\n }`}r+="\n }"}}else if("number"==typeof c&&"number"==typeof h){if(!l&&c>1&&h>0){const n=`animecursor_${e}`;r+=`\n ${t} {\n animation: ${n} steps(${c}) ${h}s infinite ${s.pingpong?"alternate":""};\n }\n\n @keyframes ${n} {\n from { background-position: 0 0; }\n to { background-position: -${o[0]*c}px 0; }\n }`}}}e.textContent=r,document.head.appendChild(e),this.styleEl=e}_bindElements(e){if(!this.disabled){for(const[e,r]of Object.entries(this.options.cursors))r.tags&&Array.isArray(r.tags)&&0!==r.tags.length&&r.tags.forEach(r=>{const s=r.toUpperCase();document.querySelectorAll(s).forEach(r=>{r.dataset.cursor||(r.dataset.cursor=e,r.dataset.cursorBound="true")})});e&&console.info("[AnimeCursor] refresh done")}}_bindMouse(){this.disabled||(this._onMouseMove=e=>{if(this.disabled)return;const r=e.clientX,s=e.clientY;this.cursorEl.style.left=r+"px",this.cursorEl.style.top=s+"px",this.cursorEl.dataset.animecursorHide&&(this.cursorEl.style.display="block"),this.debugEl&&(this.debugEl.style.left=r+"px",this.debugEl.style.top=s+"px");let t=null;const o=document.elementFromPoint(r,s);o&&o.dataset&&o.dataset.cursor?t=o.dataset.cursor:this.defaultCursorType&&(t=this.defaultCursorType),t&&(this.debugEl&&(this.debugEl.textContent=`(${r}px , ${s}px) ${t}`),t!==this.lastCursorType&&(this.debugEl?this.cursorEl.className=`cursor-${t} cursor-debugmode`:this.cursorEl.className=`cursor-${t}`,this.lastCursorType=t))},document.addEventListener("mousemove",this._onMouseMove),console.log("[AnimeCursor] AnimeCursor setted up."))}_getMaxZIndex(){if(!this.disabled)return 2147483646}}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anime-cursor",
3
- "version": "0.3.1",
3
+ "version": "1.0.0",
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",