anime-cursor 2.0.0 → 2.0.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.
@@ -1,5 +1,5 @@
1
1
  // AnimeCursor by github@ShuninYu
2
- // v2.0.0
2
+ // v2.0.1
3
3
 
4
4
  let _instance = null;
5
5
 
@@ -82,7 +82,7 @@ class AnimeCursor {
82
82
  return window.matchMedia('(pointer: fine)').matches;
83
83
  }
84
84
 
85
- // 验证配置
85
+ // 验证配置(修改点:默认光标可选)
86
86
  _validateOptions() {
87
87
  if (this.disabled) return;
88
88
 
@@ -99,7 +99,6 @@ class AnimeCursor {
99
99
 
100
100
  // 处理 frames 和 duration 配置
101
101
  if (cfg.frames !== undefined && cfg.duration !== undefined) {
102
- // 检查类型一致性
103
102
  const framesType = typeof cfg.frames;
104
103
  const durationType = typeof cfg.duration;
105
104
  if (framesType !== durationType) {
@@ -107,13 +106,11 @@ class AnimeCursor {
107
106
  delete cfg.frames;
108
107
  delete cfg.duration;
109
108
  } else if (Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) {
110
- // 数组形式:必须长度相等
111
109
  if (cfg.frames.length !== cfg.duration.length) {
112
110
  console.warn(`[AnimeCursor] Cursor "${name}" frames and duration arrays have different lengths, treating as static cursor`);
113
111
  delete cfg.frames;
114
112
  delete cfg.duration;
115
113
  } else {
116
- // 验证数组元素为正整数/正数
117
114
  for (let f of cfg.frames) {
118
115
  if (!Number.isInteger(f) || f <= 0) {
119
116
  console.warn(`[AnimeCursor] Cursor "${name}" frames array contains invalid value, treating as static cursor`);
@@ -132,45 +129,36 @@ class AnimeCursor {
132
129
  }
133
130
  }
134
131
  } else if (typeof cfg.frames === 'number' && typeof cfg.duration === 'number') {
135
- // 数字形式:合法
136
132
  if (cfg.frames <= 0 || cfg.duration <= 0) {
137
133
  console.warn(`[AnimeCursor] Cursor "${name}" frames or duration <= 0, treating as static cursor`);
138
134
  delete cfg.frames;
139
135
  delete cfg.duration;
140
136
  }
141
137
  } else {
142
- // 其他情况(如一个数字一个数组)
143
138
  console.warn(`[AnimeCursor] Cursor "${name}" frames and duration must be both numbers or both arrays, treating as static cursor`);
144
139
  delete cfg.frames;
145
140
  delete cfg.duration;
146
141
  }
147
142
  } else if (cfg.frames !== undefined || cfg.duration !== undefined) {
148
- // 只设置了一个
149
143
  console.warn(`[AnimeCursor] Cursor "${name}" has only frames or duration defined, treating as static cursor`);
150
144
  delete cfg.frames;
151
145
  delete cfg.duration;
152
146
  }
153
147
 
154
- // 检查 tags
155
148
  if (cfg.tags && !Array.isArray(cfg.tags)) {
156
149
  throw new Error(`[AnimeCursor] Cursor "${name}" tags must be an array`);
157
150
  }
158
- // 检查 default
159
151
  if (cfg.default) {
160
152
  if (hasDefault) throw new Error('[AnimeCursor] Only one default cursor allowed');
161
153
  hasDefault = true;
162
154
  }
163
- // 检查 offset
164
155
  if (cfg.offset && (!Array.isArray(cfg.offset) || cfg.offset.length !== 2)) {
165
156
  throw new Error(`[AnimeCursor] Cursor "${name}" offset must be [x, y] array`);
166
157
  }
167
158
  }
168
159
 
169
- if (!hasDefault) {
170
- throw new Error('[AnimeCursor] A default cursor (default: true) must be defined');
171
- }
172
-
173
- this.defaultCursorName = Object.keys(this.cursors).find(name => this.cursors[name].default);
160
+ // 不再强制要求默认光标
161
+ this.defaultCursorName = hasDefault ? Object.keys(this.cursors).find(name => this.cursors[name].default) : null;
174
162
  }
175
163
 
176
164
  // 预加载所有图片
@@ -197,8 +185,7 @@ class AnimeCursor {
197
185
 
198
186
  // 根据配置生成所有帧的 URL 数组
199
187
  _getFrameUrls(cfg) {
200
- // 确定总帧数
201
- let totalFrames = 1; // 默认单帧
188
+ let totalFrames = 1;
202
189
  if (cfg.frames !== undefined) {
203
190
  if (Array.isArray(cfg.frames)) {
204
191
  totalFrames = cfg.frames.reduce((a, b) => a + b, 0);
@@ -210,7 +197,6 @@ class AnimeCursor {
210
197
  const { image } = cfg;
211
198
  if (totalFrames === 1) return [image];
212
199
 
213
- // 解析文件名模板
214
200
  const { prefix, suffix, startNum, numFormat, ext } = this._parseImagePattern(image);
215
201
  const urls = [];
216
202
  for (let i = 0; i < totalFrames; i++) {
@@ -222,15 +208,12 @@ class AnimeCursor {
222
208
  return urls;
223
209
  }
224
210
 
225
- // 解析图片路径,提取数字模板
226
211
  _parseImagePattern(path) {
227
- // 匹配最后一个数字部分(包括可能的前后括号/下划线等)
228
212
  const extMatch = path.match(/\.[^.]+$/);
229
213
  const ext = extMatch ? extMatch[0] : '';
230
214
  const base = path.slice(0, -ext.length);
231
- const numMatch = base.match(/(\d+)(?!.*\d)/); // 最后一个数字串
215
+ const numMatch = base.match(/(\d+)(?!.*\d)/);
232
216
  if (!numMatch) {
233
- // 无数字,则默认在扩展名前加 _%d
234
217
  return {
235
218
  prefix: base + '_',
236
219
  suffix: '',
@@ -241,10 +224,9 @@ class AnimeCursor {
241
224
  }
242
225
  const numStr = numMatch[0];
243
226
  const startNum = parseInt(numStr, 10);
244
- const numFormat = numStr.length; // 数字位数,用于格式化
227
+ const numFormat = numStr.length;
245
228
  const prefix = base.slice(0, numMatch.index);
246
229
  const suffix = base.slice(numMatch.index + numStr.length);
247
- // 判断是否有包裹字符(如括号)
248
230
  return { prefix, suffix, startNum, numFormat, ext };
249
231
  }
250
232
 
@@ -252,7 +234,6 @@ class AnimeCursor {
252
234
  return String(num).padStart(width, '0');
253
235
  }
254
236
 
255
- // 等待 DOM 加载
256
237
  _checkDomLoad() {
257
238
  const init = () => {
258
239
  this._injectStyles();
@@ -266,7 +247,7 @@ class AnimeCursor {
266
247
  }
267
248
  }
268
249
 
269
- // 注入所有 CSS 规则
250
+ // 注入所有 CSS 规则(修改点:只有存在默认光标才生成 * 规则)
270
251
  _injectStyles() {
271
252
  if (this.disabled) return;
272
253
 
@@ -274,11 +255,12 @@ class AnimeCursor {
274
255
  style.id = 'animecursor-styles';
275
256
  let css = '';
276
257
 
277
- // 全局规则:隐藏原生光标并应用默认动画光标
278
- // 注意:默认光标通过 * 应用,但会被后面更具体的规则覆盖
279
- const defaultCfg = this.cursors[this.defaultCursorName];
280
- const defaultCursorDef = this._buildCursorCss(this.defaultCursorName, defaultCfg);
281
- css += `* { ${defaultCursorDef} }\n`;
258
+ // 如果有默认光标,生成全局规则
259
+ if (this.defaultCursorName) {
260
+ const defaultCfg = this.cursors[this.defaultCursorName];
261
+ const defaultCursorDef = this._buildCursorCss(this.defaultCursorName, defaultCfg);
262
+ css += `* { ${defaultCursorDef} }\n`;
263
+ }
282
264
 
283
265
  // 为每个光标生成独立的类和关键帧
284
266
  for (const [name, cfg] of Object.entries(this.cursors)) {
@@ -286,11 +268,9 @@ class AnimeCursor {
286
268
  const offset = cfg.offset || [0, 0];
287
269
  const fallback = cfg.fallback || this.options.fallbackCursor;
288
270
 
289
- // 获取所有帧 URL
290
271
  const frameUrls = this._getFrameUrls(cfg);
291
272
  const frameCount = frameUrls.length;
292
273
 
293
- // 判断是否有动画(有 frames 和 duration 且都有效)
294
274
  const hasAnimation = cfg.frames !== undefined && cfg.duration !== undefined &&
295
275
  ((Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) ||
296
276
  (typeof cfg.frames === 'number' && typeof cfg.duration === 'number'));
@@ -298,8 +278,6 @@ class AnimeCursor {
298
278
  if (hasAnimation && frameCount > 1) {
299
279
  const keyframeName = `ac_anim_${name}`;
300
280
  let keyframesCss = `@keyframes ${keyframeName} {\n`;
301
-
302
- // 构建关键帧列表(百分比和对应图片)
303
281
  const keyframes = this._buildKeyframes(cfg, frameUrls);
304
282
  for (const kf of keyframes) {
305
283
  let percent = (kf.percent * 100).toFixed(5);
@@ -310,30 +288,24 @@ class AnimeCursor {
310
288
  keyframesCss += `}\n`;
311
289
  css += keyframesCss;
312
290
 
313
- // 应用动画的类
314
291
  const totalDuration = Array.isArray(cfg.duration) ? cfg.duration.reduce((a, b) => a + b, 0) : cfg.duration;
315
292
  const animation = `${keyframeName} ${totalDuration}s steps(1) infinite ${cfg.pingpong ? 'alternate' : ''}`;
316
293
  css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; animation: ${animation}; }\n`;
317
294
  } else {
318
- // 静态光标
319
295
  css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; }\n`;
320
296
  }
321
297
 
322
- // 为 tags 和 data-cursor 生成选择器规则
323
298
  if (cfg.tags && cfg.tags.length) {
324
299
  const selector = cfg.tags.join(', ');
325
300
  css += `${selector} { ${this._buildCursorCss(name, cfg)} }\n`;
326
301
  }
327
- // 支持 data-cursor 属性
328
302
  css += `[data-cursor="${name}"] { ${this._buildCursorCss(name, cfg)} }\n`;
329
303
  }
330
304
 
331
- // 排除原生文本光标元素
332
305
  if (this.options.excludeSelectors) {
333
306
  css += `${this.options.excludeSelectors} { cursor: text !important; animation: none !important; }\n`;
334
307
  }
335
308
 
336
- // 全局禁用类
337
309
  css += `body.animecursor-disabled * { cursor: auto !important; animation: none !important; }\n`;
338
310
 
339
311
  style.textContent = css;
@@ -341,20 +313,16 @@ class AnimeCursor {
341
313
  this.styleEl = style;
342
314
  }
343
315
 
344
- // 根据 frames/duration 配置构建关键帧列表(百分比和对应图片 URL)
345
316
  _buildKeyframes(cfg, frameUrls) {
346
317
  let frames = cfg.frames;
347
318
  let durations = cfg.duration;
348
319
  const frameCount = frameUrls.length;
349
320
 
350
- // 统一转换为数组形式,方便处理
351
321
  if (typeof frames === 'number') {
352
- // 均匀分配
353
322
  const perFrameDuration = durations / frames;
354
323
  frames = new Array(frames).fill(1);
355
324
  durations = new Array(frames.length).fill(perFrameDuration);
356
325
  }
357
- // 此时 frames 和 durations 都是等长数组
358
326
 
359
327
  const keyframes = [];
360
328
  let totalTime = durations.reduce((a, b) => a + b, 0);
@@ -363,7 +331,7 @@ class AnimeCursor {
363
331
  for (let seg = 0; seg < frames.length; seg++) {
364
332
  const segFrames = frames[seg];
365
333
  const segDuration = durations[seg];
366
- const stepTime = segDuration / segFrames; // 每帧时长
334
+ const stepTime = segDuration / segFrames;
367
335
  for (let f = 0; f < segFrames; f++) {
368
336
  const percent = currentTime / totalTime;
369
337
  keyframes.push({
@@ -374,7 +342,6 @@ class AnimeCursor {
374
342
  frameIdx++;
375
343
  }
376
344
  }
377
- // 确保最后一帧在 100%
378
345
  keyframes.push({
379
346
  percent: 1.0,
380
347
  url: frameUrls[frameCount - 1]
@@ -382,13 +349,11 @@ class AnimeCursor {
382
349
  return keyframes;
383
350
  }
384
351
 
385
- // 生成单个光标的 CSS 声明(不包含动画,用于选择器规则)
386
352
  _buildCursorCss(name, cfg) {
387
353
  const frameUrls = this._getFrameUrls(cfg);
388
354
  const offset = cfg.offset || [0, 0];
389
355
  const fallback = cfg.fallback || this.options.fallbackCursor;
390
356
  let css = `cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback};`;
391
- // 如果有动画,附加动画属性
392
357
  const hasAnimation = cfg.frames !== undefined && cfg.duration !== undefined &&
393
358
  ((Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) ||
394
359
  (typeof cfg.frames === 'number' && typeof cfg.duration === 'number'));
@@ -399,7 +364,6 @@ class AnimeCursor {
399
364
  return css;
400
365
  }
401
366
 
402
- // 调试模式:显示当前光标类型和坐标
403
367
  _initDebug() {
404
368
  const debugDiv = document.createElement('div');
405
369
  debugDiv.className = 'animecursor-debug';
@@ -422,12 +386,11 @@ class AnimeCursor {
422
386
  let lastCursor = '';
423
387
  this._onMouseMove = (e) => {
424
388
  const target = document.elementFromPoint(e.clientX, e.clientY);
425
- let cursorType = this.defaultCursorName;
389
+ let cursorType = null;
426
390
  if (target) {
427
391
  if (target.dataset.cursor && this.cursors[target.dataset.cursor]) {
428
392
  cursorType = target.dataset.cursor;
429
393
  } else {
430
- // 检查匹配 tags
431
394
  for (const [name, cfg] of Object.entries(this.cursors)) {
432
395
  if (cfg.tags && cfg.tags.some(tag => target.matches(tag))) {
433
396
  cursorType = name;
@@ -436,6 +399,12 @@ class AnimeCursor {
436
399
  }
437
400
  }
438
401
  }
402
+ // 如果没有匹配到任何自定义光标,且没有默认光标,则显示 "native"
403
+ if (!cursorType && !this.defaultCursorName) {
404
+ cursorType = 'native';
405
+ } else if (!cursorType && this.defaultCursorName) {
406
+ cursorType = this.defaultCursorName;
407
+ }
439
408
  if (cursorType !== lastCursor) {
440
409
  lastCursor = cursorType;
441
410
  debugDiv.textContent = `🎯 ${cursorType} @ (${e.clientX}, ${e.clientY})`;
@@ -446,7 +415,6 @@ class AnimeCursor {
446
415
  document.addEventListener('mousemove', this._onMouseMove);
447
416
  }
448
417
 
449
- // 刷新:重新注入样式(用于动态添加新光标等场景)
450
418
  refresh() {
451
419
  if (this.disabled) return;
452
420
  if (this.styleEl) this.styleEl.remove();
@@ -458,7 +426,6 @@ class AnimeCursor {
458
426
  console.log('[AnimeCursor] Refresh complete');
459
427
  }
460
428
 
461
- // 销毁实例
462
429
  destroy() {
463
430
  if (this.disabled) return;
464
431
  if (this.styleEl) this.styleEl.remove();
@@ -466,13 +433,11 @@ class AnimeCursor {
466
433
  if (this._onMouseMove) {
467
434
  document.removeEventListener('mousemove', this._onMouseMove);
468
435
  }
469
- // 清除全局禁用类
470
436
  document.body.classList.remove('animecursor-disabled');
471
437
  _instance = null;
472
438
  console.log('[AnimeCursor] Destroyed');
473
439
  }
474
440
 
475
- // 禁用光标动画
476
441
  disable() {
477
442
  if (this.disabled) return;
478
443
  this.disabled = true;
@@ -480,7 +445,6 @@ class AnimeCursor {
480
445
  if (this.options.debug) console.log('[AnimeCursor] Disabled');
481
446
  }
482
447
 
483
- // 启用光标动画
484
448
  enable() {
485
449
  if (!this.disabled) return;
486
450
  this.disabled = false;
@@ -5,7 +5,7 @@
5
5
  })(this, (function () { 'use strict';
6
6
 
7
7
  // AnimeCursor by github@ShuninYu
8
- // v2.0.0
8
+ // v2.0.1
9
9
 
10
10
  let _instance = null;
11
11
 
@@ -88,7 +88,7 @@
88
88
  return window.matchMedia('(pointer: fine)').matches;
89
89
  }
90
90
 
91
- // 验证配置
91
+ // 验证配置(修改点:默认光标可选)
92
92
  _validateOptions() {
93
93
  if (this.disabled) return;
94
94
 
@@ -105,7 +105,6 @@
105
105
 
106
106
  // 处理 frames 和 duration 配置
107
107
  if (cfg.frames !== undefined && cfg.duration !== undefined) {
108
- // 检查类型一致性
109
108
  const framesType = typeof cfg.frames;
110
109
  const durationType = typeof cfg.duration;
111
110
  if (framesType !== durationType) {
@@ -113,13 +112,11 @@
113
112
  delete cfg.frames;
114
113
  delete cfg.duration;
115
114
  } else if (Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) {
116
- // 数组形式:必须长度相等
117
115
  if (cfg.frames.length !== cfg.duration.length) {
118
116
  console.warn(`[AnimeCursor] Cursor "${name}" frames and duration arrays have different lengths, treating as static cursor`);
119
117
  delete cfg.frames;
120
118
  delete cfg.duration;
121
119
  } else {
122
- // 验证数组元素为正整数/正数
123
120
  for (let f of cfg.frames) {
124
121
  if (!Number.isInteger(f) || f <= 0) {
125
122
  console.warn(`[AnimeCursor] Cursor "${name}" frames array contains invalid value, treating as static cursor`);
@@ -138,45 +135,36 @@
138
135
  }
139
136
  }
140
137
  } else if (typeof cfg.frames === 'number' && typeof cfg.duration === 'number') {
141
- // 数字形式:合法
142
138
  if (cfg.frames <= 0 || cfg.duration <= 0) {
143
139
  console.warn(`[AnimeCursor] Cursor "${name}" frames or duration <= 0, treating as static cursor`);
144
140
  delete cfg.frames;
145
141
  delete cfg.duration;
146
142
  }
147
143
  } else {
148
- // 其他情况(如一个数字一个数组)
149
144
  console.warn(`[AnimeCursor] Cursor "${name}" frames and duration must be both numbers or both arrays, treating as static cursor`);
150
145
  delete cfg.frames;
151
146
  delete cfg.duration;
152
147
  }
153
148
  } else if (cfg.frames !== undefined || cfg.duration !== undefined) {
154
- // 只设置了一个
155
149
  console.warn(`[AnimeCursor] Cursor "${name}" has only frames or duration defined, treating as static cursor`);
156
150
  delete cfg.frames;
157
151
  delete cfg.duration;
158
152
  }
159
153
 
160
- // 检查 tags
161
154
  if (cfg.tags && !Array.isArray(cfg.tags)) {
162
155
  throw new Error(`[AnimeCursor] Cursor "${name}" tags must be an array`);
163
156
  }
164
- // 检查 default
165
157
  if (cfg.default) {
166
158
  if (hasDefault) throw new Error('[AnimeCursor] Only one default cursor allowed');
167
159
  hasDefault = true;
168
160
  }
169
- // 检查 offset
170
161
  if (cfg.offset && (!Array.isArray(cfg.offset) || cfg.offset.length !== 2)) {
171
162
  throw new Error(`[AnimeCursor] Cursor "${name}" offset must be [x, y] array`);
172
163
  }
173
164
  }
174
165
 
175
- if (!hasDefault) {
176
- throw new Error('[AnimeCursor] A default cursor (default: true) must be defined');
177
- }
178
-
179
- this.defaultCursorName = Object.keys(this.cursors).find(name => this.cursors[name].default);
166
+ // 不再强制要求默认光标
167
+ this.defaultCursorName = hasDefault ? Object.keys(this.cursors).find(name => this.cursors[name].default) : null;
180
168
  }
181
169
 
182
170
  // 预加载所有图片
@@ -203,8 +191,7 @@
203
191
 
204
192
  // 根据配置生成所有帧的 URL 数组
205
193
  _getFrameUrls(cfg) {
206
- // 确定总帧数
207
- let totalFrames = 1; // 默认单帧
194
+ let totalFrames = 1;
208
195
  if (cfg.frames !== undefined) {
209
196
  if (Array.isArray(cfg.frames)) {
210
197
  totalFrames = cfg.frames.reduce((a, b) => a + b, 0);
@@ -216,7 +203,6 @@
216
203
  const { image } = cfg;
217
204
  if (totalFrames === 1) return [image];
218
205
 
219
- // 解析文件名模板
220
206
  const { prefix, suffix, startNum, numFormat, ext } = this._parseImagePattern(image);
221
207
  const urls = [];
222
208
  for (let i = 0; i < totalFrames; i++) {
@@ -228,15 +214,12 @@
228
214
  return urls;
229
215
  }
230
216
 
231
- // 解析图片路径,提取数字模板
232
217
  _parseImagePattern(path) {
233
- // 匹配最后一个数字部分(包括可能的前后括号/下划线等)
234
218
  const extMatch = path.match(/\.[^.]+$/);
235
219
  const ext = extMatch ? extMatch[0] : '';
236
220
  const base = path.slice(0, -ext.length);
237
- const numMatch = base.match(/(\d+)(?!.*\d)/); // 最后一个数字串
221
+ const numMatch = base.match(/(\d+)(?!.*\d)/);
238
222
  if (!numMatch) {
239
- // 无数字,则默认在扩展名前加 _%d
240
223
  return {
241
224
  prefix: base + '_',
242
225
  suffix: '',
@@ -247,10 +230,9 @@
247
230
  }
248
231
  const numStr = numMatch[0];
249
232
  const startNum = parseInt(numStr, 10);
250
- const numFormat = numStr.length; // 数字位数,用于格式化
233
+ const numFormat = numStr.length;
251
234
  const prefix = base.slice(0, numMatch.index);
252
235
  const suffix = base.slice(numMatch.index + numStr.length);
253
- // 判断是否有包裹字符(如括号)
254
236
  return { prefix, suffix, startNum, numFormat, ext };
255
237
  }
256
238
 
@@ -258,7 +240,6 @@
258
240
  return String(num).padStart(width, '0');
259
241
  }
260
242
 
261
- // 等待 DOM 加载
262
243
  _checkDomLoad() {
263
244
  const init = () => {
264
245
  this._injectStyles();
@@ -272,7 +253,7 @@
272
253
  }
273
254
  }
274
255
 
275
- // 注入所有 CSS 规则
256
+ // 注入所有 CSS 规则(修改点:只有存在默认光标才生成 * 规则)
276
257
  _injectStyles() {
277
258
  if (this.disabled) return;
278
259
 
@@ -280,11 +261,12 @@
280
261
  style.id = 'animecursor-styles';
281
262
  let css = '';
282
263
 
283
- // 全局规则:隐藏原生光标并应用默认动画光标
284
- // 注意:默认光标通过 * 应用,但会被后面更具体的规则覆盖
285
- const defaultCfg = this.cursors[this.defaultCursorName];
286
- const defaultCursorDef = this._buildCursorCss(this.defaultCursorName, defaultCfg);
287
- css += `* { ${defaultCursorDef} }\n`;
264
+ // 如果有默认光标,生成全局规则
265
+ if (this.defaultCursorName) {
266
+ const defaultCfg = this.cursors[this.defaultCursorName];
267
+ const defaultCursorDef = this._buildCursorCss(this.defaultCursorName, defaultCfg);
268
+ css += `* { ${defaultCursorDef} }\n`;
269
+ }
288
270
 
289
271
  // 为每个光标生成独立的类和关键帧
290
272
  for (const [name, cfg] of Object.entries(this.cursors)) {
@@ -292,11 +274,9 @@
292
274
  const offset = cfg.offset || [0, 0];
293
275
  const fallback = cfg.fallback || this.options.fallbackCursor;
294
276
 
295
- // 获取所有帧 URL
296
277
  const frameUrls = this._getFrameUrls(cfg);
297
278
  const frameCount = frameUrls.length;
298
279
 
299
- // 判断是否有动画(有 frames 和 duration 且都有效)
300
280
  const hasAnimation = cfg.frames !== undefined && cfg.duration !== undefined &&
301
281
  ((Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) ||
302
282
  (typeof cfg.frames === 'number' && typeof cfg.duration === 'number'));
@@ -304,8 +284,6 @@
304
284
  if (hasAnimation && frameCount > 1) {
305
285
  const keyframeName = `ac_anim_${name}`;
306
286
  let keyframesCss = `@keyframes ${keyframeName} {\n`;
307
-
308
- // 构建关键帧列表(百分比和对应图片)
309
287
  const keyframes = this._buildKeyframes(cfg, frameUrls);
310
288
  for (const kf of keyframes) {
311
289
  let percent = (kf.percent * 100).toFixed(5);
@@ -316,30 +294,24 @@
316
294
  keyframesCss += `}\n`;
317
295
  css += keyframesCss;
318
296
 
319
- // 应用动画的类
320
297
  const totalDuration = Array.isArray(cfg.duration) ? cfg.duration.reduce((a, b) => a + b, 0) : cfg.duration;
321
298
  const animation = `${keyframeName} ${totalDuration}s steps(1) infinite ${cfg.pingpong ? 'alternate' : ''}`;
322
299
  css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; animation: ${animation}; }\n`;
323
300
  } else {
324
- // 静态光标
325
301
  css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; }\n`;
326
302
  }
327
303
 
328
- // 为 tags 和 data-cursor 生成选择器规则
329
304
  if (cfg.tags && cfg.tags.length) {
330
305
  const selector = cfg.tags.join(', ');
331
306
  css += `${selector} { ${this._buildCursorCss(name, cfg)} }\n`;
332
307
  }
333
- // 支持 data-cursor 属性
334
308
  css += `[data-cursor="${name}"] { ${this._buildCursorCss(name, cfg)} }\n`;
335
309
  }
336
310
 
337
- // 排除原生文本光标元素
338
311
  if (this.options.excludeSelectors) {
339
312
  css += `${this.options.excludeSelectors} { cursor: text !important; animation: none !important; }\n`;
340
313
  }
341
314
 
342
- // 全局禁用类
343
315
  css += `body.animecursor-disabled * { cursor: auto !important; animation: none !important; }\n`;
344
316
 
345
317
  style.textContent = css;
@@ -347,20 +319,16 @@
347
319
  this.styleEl = style;
348
320
  }
349
321
 
350
- // 根据 frames/duration 配置构建关键帧列表(百分比和对应图片 URL)
351
322
  _buildKeyframes(cfg, frameUrls) {
352
323
  let frames = cfg.frames;
353
324
  let durations = cfg.duration;
354
325
  const frameCount = frameUrls.length;
355
326
 
356
- // 统一转换为数组形式,方便处理
357
327
  if (typeof frames === 'number') {
358
- // 均匀分配
359
328
  const perFrameDuration = durations / frames;
360
329
  frames = new Array(frames).fill(1);
361
330
  durations = new Array(frames.length).fill(perFrameDuration);
362
331
  }
363
- // 此时 frames 和 durations 都是等长数组
364
332
 
365
333
  const keyframes = [];
366
334
  let totalTime = durations.reduce((a, b) => a + b, 0);
@@ -369,7 +337,7 @@
369
337
  for (let seg = 0; seg < frames.length; seg++) {
370
338
  const segFrames = frames[seg];
371
339
  const segDuration = durations[seg];
372
- const stepTime = segDuration / segFrames; // 每帧时长
340
+ const stepTime = segDuration / segFrames;
373
341
  for (let f = 0; f < segFrames; f++) {
374
342
  const percent = currentTime / totalTime;
375
343
  keyframes.push({
@@ -380,7 +348,6 @@
380
348
  frameIdx++;
381
349
  }
382
350
  }
383
- // 确保最后一帧在 100%
384
351
  keyframes.push({
385
352
  percent: 1.0,
386
353
  url: frameUrls[frameCount - 1]
@@ -388,13 +355,11 @@
388
355
  return keyframes;
389
356
  }
390
357
 
391
- // 生成单个光标的 CSS 声明(不包含动画,用于选择器规则)
392
358
  _buildCursorCss(name, cfg) {
393
359
  const frameUrls = this._getFrameUrls(cfg);
394
360
  const offset = cfg.offset || [0, 0];
395
361
  const fallback = cfg.fallback || this.options.fallbackCursor;
396
362
  let css = `cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback};`;
397
- // 如果有动画,附加动画属性
398
363
  const hasAnimation = cfg.frames !== undefined && cfg.duration !== undefined &&
399
364
  ((Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) ||
400
365
  (typeof cfg.frames === 'number' && typeof cfg.duration === 'number'));
@@ -405,7 +370,6 @@
405
370
  return css;
406
371
  }
407
372
 
408
- // 调试模式:显示当前光标类型和坐标
409
373
  _initDebug() {
410
374
  const debugDiv = document.createElement('div');
411
375
  debugDiv.className = 'animecursor-debug';
@@ -428,12 +392,11 @@
428
392
  let lastCursor = '';
429
393
  this._onMouseMove = (e) => {
430
394
  const target = document.elementFromPoint(e.clientX, e.clientY);
431
- let cursorType = this.defaultCursorName;
395
+ let cursorType = null;
432
396
  if (target) {
433
397
  if (target.dataset.cursor && this.cursors[target.dataset.cursor]) {
434
398
  cursorType = target.dataset.cursor;
435
399
  } else {
436
- // 检查匹配 tags
437
400
  for (const [name, cfg] of Object.entries(this.cursors)) {
438
401
  if (cfg.tags && cfg.tags.some(tag => target.matches(tag))) {
439
402
  cursorType = name;
@@ -442,6 +405,12 @@
442
405
  }
443
406
  }
444
407
  }
408
+ // 如果没有匹配到任何自定义光标,且没有默认光标,则显示 "native"
409
+ if (!cursorType && !this.defaultCursorName) {
410
+ cursorType = 'native';
411
+ } else if (!cursorType && this.defaultCursorName) {
412
+ cursorType = this.defaultCursorName;
413
+ }
445
414
  if (cursorType !== lastCursor) {
446
415
  lastCursor = cursorType;
447
416
  debugDiv.textContent = `🎯 ${cursorType} @ (${e.clientX}, ${e.clientY})`;
@@ -452,7 +421,6 @@
452
421
  document.addEventListener('mousemove', this._onMouseMove);
453
422
  }
454
423
 
455
- // 刷新:重新注入样式(用于动态添加新光标等场景)
456
424
  refresh() {
457
425
  if (this.disabled) return;
458
426
  if (this.styleEl) this.styleEl.remove();
@@ -464,7 +432,6 @@
464
432
  console.log('[AnimeCursor] Refresh complete');
465
433
  }
466
434
 
467
- // 销毁实例
468
435
  destroy() {
469
436
  if (this.disabled) return;
470
437
  if (this.styleEl) this.styleEl.remove();
@@ -472,13 +439,11 @@
472
439
  if (this._onMouseMove) {
473
440
  document.removeEventListener('mousemove', this._onMouseMove);
474
441
  }
475
- // 清除全局禁用类
476
442
  document.body.classList.remove('animecursor-disabled');
477
443
  _instance = null;
478
444
  console.log('[AnimeCursor] Destroyed');
479
445
  }
480
446
 
481
- // 禁用光标动画
482
447
  disable() {
483
448
  if (this.disabled) return;
484
449
  this.disabled = true;
@@ -486,7 +451,6 @@
486
451
  if (this.options.debug) console.log('[AnimeCursor] Disabled');
487
452
  }
488
453
 
489
- // 启用光标动画
490
454
  enable() {
491
455
  if (!this.disabled) return;
492
456
  this.disabled = false;
@@ -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`)}if(!e)throw new Error("[AnimeCursor] A default cursor (default: true) must be defined");this.defaultCursorName=Object.keys(this.cursors).find(e=>this.cursors[e].default)}_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:n,numFormat:i,ext:a}=this._parseImagePattern(r),u=[];for(let e=0;e<t;e++){const t=n+e,r=`${s}${i?this._formatNumber(t,i):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 n=o[0],i=parseInt(n,10),a=n.length;return{prefix:s.slice(0,o.index),suffix:s.slice(o.index+n.length),startNum:i,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="";const r=this.cursors[this.defaultCursorName];t+=`* { ${this._buildCursorCss(this.defaultCursorName,r)} }\n`;for(const[e,r]of Object.entries(this.cursors)){const s=`.ac-cursor-${e}`,o=r.offset||[0,0],n=r.fallback||this.options.fallbackCursor,i=this._getFrameUrls(r),a=i.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,i);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]}, ${n};`} }\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("${i[0]}") ${o[0]} ${o[1]}, ${n}; animation: ${d}; }\n`}else t+=`${s} { cursor: url("${i[0]}") ${o[0]} ${o[1]}, ${n}; }\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 n=[];let i=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/i;n.push({percent:e,url:t[u]}),a+=l,u++}}return n.push({percent:1,url:t[o-1]}),n}_buildCursorCss(e,t){const r=this._getFrameUrls(t),s=t.offset||[0,0],o=t.fallback||this.options.fallbackCursor;let n=`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){n+=` 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 n}_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=this.defaultCursorName;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!==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]",...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"))}}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anime-cursor",
3
- "version": "2.0.0",
3
+ "version": "2.0.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",