anime-cursor 1.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,11 +1,9 @@
1
1
  // AnimeCursor by github@ShuninYu
2
- // v1.0.0
2
+ // v2.0.1
3
3
 
4
- // 静态变量存储唯一实例
5
4
  let _instance = null;
6
5
 
7
6
  class AnimeCursor {
8
-
9
7
  static get instance() {
10
8
  return _instance;
11
9
  }
@@ -43,551 +41,416 @@ class AnimeCursor {
43
41
  }
44
42
 
45
43
  constructor(options = {}) {
46
- // 如果已有实例,直接返回它
47
44
  if (_instance) {
48
- console.warn('[AnimeCursor] AnimeCursor already exists.');
45
+ console.warn('[AnimeCursor] Instance already exists, returning existing one');
49
46
  return _instance;
50
47
  }
51
48
 
52
49
  this.options = {
53
- displayOnLoad: false,
54
- enableTouch: false,
55
50
  debug: false,
51
+ enableTouch: false,
52
+ fallbackCursor: 'auto', // Fallback cursor type (auto, pointer, etc.)
53
+ excludeSelectors: 'input, textarea, [contenteditable]', // Exclude native cursor elements
56
54
  ...options
57
55
  };
56
+
58
57
  this.disabled = false;
58
+ this.cursors = this.options.cursors || {};
59
59
 
60
+ // 检查是否应启用(触摸设备且未强制启用则禁用)
60
61
  if (!this.options.enableTouch && !this.isMouseLikeDevice()) {
61
62
  this.disabled = true;
62
-
63
63
  if (this.options.debug) {
64
- console.warn('[AnimeCursor] Touch device detected, cursor disabled.');
64
+ console.warn('[AnimeCursor] Touch device detected, cursor animations disabled');
65
65
  }
66
66
  return;
67
67
  }
68
68
 
69
- this.cursorEl = null;
70
- this.lastCursorType = null;
71
- this.debugEl = null;
72
69
  this.styleEl = null;
70
+ this.debugEl = null;
73
71
  this._onMouseMove = null;
74
72
 
75
73
  this._validateOptions();
76
- this._injectPreload();
74
+ this._preloadImages();
77
75
  this._checkDomLoad();
78
76
 
79
- // 保存实例引用
80
77
  _instance = this;
81
78
  }
82
-
83
- isMouseLikeDevice() {
84
- if (this.disabled) return;
85
79
 
80
+ // 判断是否鼠标设备
81
+ isMouseLikeDevice() {
86
82
  return window.matchMedia('(pointer: fine)').matches;
87
83
  }
88
-
89
- // ----------------------------
90
- // 刷新 清理 关闭 开启
91
- // ----------------------------
92
- refresh() {
93
- if (this.disabled) return;
94
-
95
- if (this.options.debug) {
96
- console.info('[AnimeCursor] starting refresh...');
97
- }
98
-
99
- this._bindElements(true);
100
- }
101
- destroy() {
102
- if (this.disabled) return;
103
84
 
104
- // 1 移除事件监听
105
- if (this._onMouseMove) {
106
- document.removeEventListener('mousemove', this._onMouseMove);
107
- this._onMouseMove = null;
108
- }
109
-
110
- // 2 移除 cursor DOM
111
- if (this.cursorEl) {
112
- this.cursorEl.remove();
113
- this.cursorEl = null;
114
- }
115
-
116
- if (this.debugEl) {
117
- this.debugEl.remove();
118
- this.debugEl = null;
119
- }
120
-
121
- // 3 移除注入的 CSS
122
- if (this.styleEl) {
123
- this.styleEl.remove();
124
- this.styleEl = null;
125
- }
126
-
127
- // 4 清理 data-cursor(只清理由 AnimeCursor 添加的)
128
- for (const cfg of Object.values(this.options.cursors)) {
129
- // v0.2.1 添加检查:只有存在且为数组的 tags 才进行处理
130
- if (cfg.tags && Array.isArray(cfg.tags)) {
131
- cfg.tags.forEach(tag => {
132
- document.querySelectorAll(tag).forEach(el => {
133
- if (el.dataset.cursorBound) {
134
- delete el.dataset.cursor;
135
- delete el.dataset.cursorBound;
136
- }
137
- });
138
- });
139
- }
140
- }
141
-
142
- // 5 重置状态
143
- this.lastCursorType = null;
144
-
145
- // 清除静态引用
146
- if (_instance === this) {
147
- _instance = null;
148
- }
149
- }
150
- disable() {
151
- if (this.disabled) return;
152
- this.disabled = true;
153
-
154
- if (this.cursorEl) {
155
- this.cursorEl.style.display = 'none';
156
- this.styleEl.innerHTML = this.styleEl.innerHTML.replace('* {cursor: none !important;}', '');
157
- console.log('[AnimeCursor] AnimeCursor disabled!');
158
- }
159
- }
160
- enable() {
161
- if (!this.disabled) return;
162
- this.disabled = false;
163
-
164
- if (this.cursorEl) {
165
- this.cursorEl.style.display = '';
166
- this.styleEl.innerHTML += '* {cursor: none; !important;}';
167
- console.log('[AnimeCursor] AnimeCursor enabled!');
168
- }
169
- }
170
-
171
- // ----------------------------
172
- // 配置校验(必填项)
173
- // ----------------------------
85
+ // 验证配置(修改点:默认光标可选)
174
86
  _validateOptions() {
175
87
  if (this.disabled) return;
176
-
177
- if (!this.options || !this.options.cursors) {
178
- console.error('[AnimeCursor] missing cursors set up');
179
- throw new Error('AnimeCursor init failed');
88
+
89
+ if (!this.cursors || Object.keys(this.cursors).length === 0) {
90
+ throw new Error('[AnimeCursor] At least one cursor must be defined');
180
91
  }
181
-
182
- this.defaultCursorType = null;
183
-
184
- for (const [name, cfg] of Object.entries(this.options.cursors)) {
185
- if (cfg.default === true) {
186
- if (this.defaultCursorType) {
187
- throw new Error('[AnimeCursor] There can only be one default cursor');
188
- }
189
- this.defaultCursorType = name;
92
+
93
+ let hasDefault = false;
94
+ for (const [name, cfg] of Object.entries(this.cursors)) {
95
+ // 检查必填项
96
+ if (!cfg.image) {
97
+ throw new Error(`[AnimeCursor] Cursor "${name}" missing required setting: image`);
190
98
  }
191
- }
192
-
193
- for (const [name, cfg] of Object.entries(this.options.cursors)) {
194
- const required = ['size', 'image'];
195
- required.forEach(key => {
196
- if (cfg[key] === undefined) {
197
- console.error(`[AnimeCursor] cursor "${name}" missing required setting: ${key}`);
198
- throw new Error('AnimeCursor init failed');
199
- }
200
- });
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');
99
+
100
+ // 处理 frames 和 duration 配置
101
+ if (cfg.frames !== undefined && cfg.duration !== undefined) {
102
+ const framesType = typeof cfg.frames;
103
+ const durationType = typeof cfg.duration;
104
+ if (framesType !== durationType) {
105
+ console.warn(`[AnimeCursor] Cursor "${name}" has mismatched types for frames and duration, treating as static cursor`);
106
+ delete cfg.frames;
107
+ delete cfg.duration;
108
+ } else if (Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) {
109
+ if (cfg.frames.length !== cfg.duration.length) {
110
+ console.warn(`[AnimeCursor] Cursor "${name}" frames and duration arrays have different lengths, treating as static cursor`);
111
+ delete cfg.frames;
112
+ delete cfg.duration;
113
+ } else {
114
+ for (let f of cfg.frames) {
115
+ if (!Number.isInteger(f) || f <= 0) {
116
+ console.warn(`[AnimeCursor] Cursor "${name}" frames array contains invalid value, treating as static cursor`);
117
+ delete cfg.frames;
118
+ delete cfg.duration;
119
+ break;
120
+ }
215
121
  }
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');
122
+ for (let d of cfg.duration) {
123
+ if (typeof d !== 'number' || d <= 0) {
124
+ console.warn(`[AnimeCursor] Cursor "${name}" duration array contains invalid value, treating as static cursor`);
125
+ delete cfg.frames;
126
+ delete cfg.duration;
127
+ break;
128
+ }
222
129
  }
223
130
  }
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');
131
+ } else if (typeof cfg.frames === 'number' && typeof cfg.duration === 'number') {
132
+ if (cfg.frames <= 0 || cfg.duration <= 0) {
133
+ console.warn(`[AnimeCursor] Cursor "${name}" frames or duration <= 0, treating as static cursor`);
134
+ delete cfg.frames;
135
+ delete cfg.duration;
237
136
  }
238
137
  } else {
239
- console.error(`[AnimeCursor] cursor "${name}" frames must be a number or an array`);
240
- throw new Error('AnimeCursor init failed');
138
+ console.warn(`[AnimeCursor] Cursor "${name}" frames and duration must be both numbers or both arrays, treating as static cursor`);
139
+ delete cfg.frames;
140
+ delete cfg.duration;
241
141
  }
142
+ } else if (cfg.frames !== undefined || cfg.duration !== undefined) {
143
+ console.warn(`[AnimeCursor] Cursor "${name}" has only frames or duration defined, treating as static cursor`);
144
+ delete cfg.frames;
145
+ delete cfg.duration;
242
146
  }
243
-
244
- if (cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
245
- console.error(`[AnimeCursor] cursor "${name}" 's tags must be an array if provided`);
246
- throw new Error('AnimeCursor init failed');
147
+
148
+ if (cfg.tags && !Array.isArray(cfg.tags)) {
149
+ throw new Error(`[AnimeCursor] Cursor "${name}" tags must be an array`);
150
+ }
151
+ if (cfg.default) {
152
+ if (hasDefault) throw new Error('[AnimeCursor] Only one default cursor allowed');
153
+ hasDefault = true;
154
+ }
155
+ if (cfg.offset && (!Array.isArray(cfg.offset) || cfg.offset.length !== 2)) {
156
+ throw new Error(`[AnimeCursor] Cursor "${name}" offset must be [x, y] array`);
247
157
  }
248
158
  }
249
- }
250
159
 
251
- // ----------------------------
252
- // 等待 DOM 加载完成
253
- // ----------------------------
254
- _checkDomLoad() {
255
- const init = () => {
256
- this._injectHTML();
257
- this._injectCSS();
258
- this._bindElements();
259
- this._bindMouse();
260
- };
261
-
262
- if (document.readyState === 'loading') {
263
- document.addEventListener('DOMContentLoaded', init);
264
- } else {
265
- init();
266
- }
160
+ // 不再强制要求默认光标
161
+ this.defaultCursorName = hasDefault ? Object.keys(this.cursors).find(name => this.cursors[name].default) : null;
267
162
  }
268
163
 
269
- // ----------------------------
270
- // 插入光标图片预加载()
271
- // ----------------------------
272
- _injectPreload() {
273
- if (this.disabled) return;
274
-
275
- // 收集所有需要预加载的图片URL
276
- const imageUrls = new Set();
277
-
278
- // 遍历所有光标配置,提取图片URL
279
- for (const cfg of Object.values(this.options.cursors)) {
280
- if (cfg.image) {
281
- imageUrls.add(cfg.image);
282
- }
164
+ // 预加载所有图片
165
+ _preloadImages() {
166
+ const images = new Set();
167
+ for (const cfg of Object.values(this.cursors)) {
168
+ const frameUrls = this._getFrameUrls(cfg);
169
+ frameUrls.forEach(url => images.add(url));
283
170
  }
284
-
285
- // 为每个图片URL创建预加载标签
286
- imageUrls.forEach(url => {
171
+ images.forEach(url => {
287
172
  const link = document.createElement('link');
288
173
  link.rel = 'preload';
289
174
  link.as = 'image';
290
175
  link.href = url;
291
-
292
- // 可选:添加跨域处理(如果图片来自不同域名)
293
176
  if (url.startsWith('http') && !url.startsWith(window.location.origin)) {
294
177
  link.crossOrigin = 'anonymous';
295
178
  }
296
-
297
179
  document.head.appendChild(link);
298
-
299
- if (this.options.debug) {
300
- console.info(`[AnimeCursor] Preloading image: ${url}`);
301
- }
302
180
  });
303
-
304
- if (this.options.debug && imageUrls.size > 0) {
305
- console.info(`[AnimeCursor] Preloaded ${imageUrls.size} cursor image(s)`);
181
+ if (this.options.debug && images.size) {
182
+ console.info(`[AnimeCursor] Preloaded ${images.size} cursor images`);
306
183
  }
307
184
  }
308
185
 
309
- // ----------------------------
310
- // 插入光标元素 HTML
311
- // ----------------------------
312
- _injectHTML() {
313
- if (this.disabled) return;
186
+ // 根据配置生成所有帧的 URL 数组
187
+ _getFrameUrls(cfg) {
188
+ let totalFrames = 1;
189
+ if (cfg.frames !== undefined) {
190
+ if (Array.isArray(cfg.frames)) {
191
+ totalFrames = cfg.frames.reduce((a, b) => a + b, 0);
192
+ } else if (typeof cfg.frames === 'number') {
193
+ totalFrames = cfg.frames;
194
+ }
195
+ }
314
196
 
315
- const cursor = document.createElement('div');
316
- cursor.id = 'anime-cursor';
317
-
318
- // 如果debug选项存在,则添加debug元素
319
- if (this.options.debug) {
320
- cursor.className = 'cursor-default cursor-debugmode';
321
- const debuger = document.createElement('div');
322
- debuger.className = 'anime-cursor-debug';
323
- document.body.appendChild(debuger);
324
- this.debugEl = debuger;
197
+ const { image } = cfg;
198
+ if (totalFrames === 1) return [image];
199
+
200
+ const { prefix, suffix, startNum, numFormat, ext } = this._parseImagePattern(image);
201
+ const urls = [];
202
+ for (let i = 0; i < totalFrames; i++) {
203
+ const frameNum = startNum + i;
204
+ const numStr = numFormat ? this._formatNumber(frameNum, numFormat) : frameNum;
205
+ const url = `${prefix}${numStr}${suffix}${ext}`;
206
+ urls.push(url);
207
+ }
208
+ return urls;
209
+ }
210
+
211
+ _parseImagePattern(path) {
212
+ const extMatch = path.match(/\.[^.]+$/);
213
+ const ext = extMatch ? extMatch[0] : '';
214
+ const base = path.slice(0, -ext.length);
215
+ const numMatch = base.match(/(\d+)(?!.*\d)/);
216
+ if (!numMatch) {
217
+ return {
218
+ prefix: base + '_',
219
+ suffix: '',
220
+ startNum: 1,
221
+ numFormat: null,
222
+ ext
223
+ };
325
224
  }
326
- else {cursor.className = 'cursor-default';}
327
-
328
- // 检查是否设置初始化时显示光标
329
- if (this.options.displayOnLoad) {
330
- cursor.style.display = 'block';
225
+ const numStr = numMatch[0];
226
+ const startNum = parseInt(numStr, 10);
227
+ const numFormat = numStr.length;
228
+ const prefix = base.slice(0, numMatch.index);
229
+ const suffix = base.slice(numMatch.index + numStr.length);
230
+ return { prefix, suffix, startNum, numFormat, ext };
231
+ }
232
+
233
+ _formatNumber(num, width) {
234
+ return String(num).padStart(width, '0');
235
+ }
236
+
237
+ _checkDomLoad() {
238
+ const init = () => {
239
+ this._injectStyles();
240
+ if (this.options.debug) this._initDebug();
241
+ console.log('[AnimeCursor] Initialization complete');
242
+ };
243
+ if (document.readyState === 'loading') {
244
+ document.addEventListener('DOMContentLoaded', init);
331
245
  } else {
332
- cursor.style.display = 'none';
333
- cursor.dataset.animecursorHide = 'true';
246
+ init();
334
247
  }
335
- document.body.appendChild(cursor);
336
- this.cursorEl = cursor;
337
248
  }
338
249
 
339
- // ----------------------------
340
- // 插入样式 CSS
341
- // ----------------------------
342
- _injectCSS() {
250
+ // 注入所有 CSS 规则(修改点:只有存在默认光标才生成 * 规则)
251
+ _injectStyles() {
343
252
  if (this.disabled) return;
344
253
 
345
254
  const style = document.createElement('style');
346
255
  style.id = 'animecursor-styles';
347
256
  let css = '';
348
257
 
349
- /* 通用样式 */
350
- css += `
351
- * {cursor: none !important;}
352
- #anime-cursor {
353
- position: fixed;
354
- top: 0;
355
- left: 0;
356
- pointer-events: none;
357
- background-repeat: no-repeat;
358
- transform-origin: 0 0;
359
- transform-style: preserve-3d;
360
- z-index: ${this._getMaxZIndex()};
361
- }
362
- .cursor-debugmode {
363
- border: 1px solid green;
364
- }
365
- .anime-cursor-debug {
366
- position: fixed;
367
- top: 0;
368
- left: 0;
369
- width: fit-content;
370
- height: fit-content;
371
- padding: 5px;
372
- font-size: 16px;
373
- text-wrap: nowrap;
374
- color: red;
375
- pointer-events: none;
376
- overflow: visible;
377
- z-index: 2147483647;
378
- }
379
- .anime-cursor-debug::before {
380
- position: absolute;
381
- content: "";
382
- top: 0;
383
- left: 0;
384
- width: 100vw;
385
- height: 1px;
386
- background-color: red;
387
- }
388
- .anime-cursor-debug::after {
389
- position: absolute;
390
- content: "";
391
- top: 0;
392
- left: 0;
393
- width: 1px;
394
- height: 100vh;
395
- background-color: red;
258
+ // 如果有默认光标,生成全局规则
259
+ if (this.defaultCursorName) {
260
+ const defaultCfg = this.cursors[this.defaultCursorName];
261
+ const defaultCursorDef = this._buildCursorCss(this.defaultCursorName, defaultCfg);
262
+ css += `* { ${defaultCursorDef} }\n`;
396
263
  }
397
- `;
398
-
399
- /* 每种光标以及debug生成 CSS */
400
- for (const [type, cfg] of Object.entries(this.options.cursors)) {
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} {
413
- width: ${size[0]}px;
414
- height: ${size[1]}px;
415
- background-image: url("${image}");
416
- image-rendering: ${pixel};
417
- ${(scale || offset) ? `transform: ${[scale && `scale(${scale[0]}, ${scale[1]})`, offset && `translate(-${offset[0]}px, -${offset[1]}px)`].filter(Boolean).join(' ')};` : ''}
418
- ${zIndex !== undefined ? `z-index:${zIndex};` : ''}
419
- }`;
420
-
421
- // 动画生成
422
- const frames = cfg.frames;
423
- const duration = cfg.duration;
424
-
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;
431
-
432
- if (hasAnimation) {
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
- });
458
264
 
459
- // 生成动画属性
460
- css += `
461
- ${className} {
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];
265
+ // 为每个光标生成独立的类和关键帧
266
+ for (const [name, cfg] of Object.entries(this.cursors)) {
267
+ const className = `.ac-cursor-${name}`;
268
+ const offset = cfg.offset || [0, 0];
269
+ const fallback = cfg.fallback || this.options.fallbackCursor;
270
+
271
+ const frameUrls = this._getFrameUrls(cfg);
272
+ const frameCount = frameUrls.length;
273
+
274
+ const hasAnimation = cfg.frames !== undefined && cfg.duration !== undefined &&
275
+ ((Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) ||
276
+ (typeof cfg.frames === 'number' && typeof cfg.duration === 'number'));
277
+
278
+ if (hasAnimation && frameCount > 1) {
279
+ const keyframeName = `ac_anim_${name}`;
280
+ let keyframesCss = `@keyframes ${keyframeName} {\n`;
281
+ const keyframes = this._buildKeyframes(cfg, frameUrls);
282
+ for (const kf of keyframes) {
470
283
  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
- }`;
284
+ if (kf.percent === 1.0) percent = '100';
285
+ const cursorRule = `cursor: url("${kf.url}") ${offset[0]} ${offset[1]}, ${fallback};`;
286
+ keyframesCss += ` ${percent}% { ${cursorRule} }\n`;
477
287
  }
478
- css += `
479
- }`;
288
+ keyframesCss += `}\n`;
289
+ css += keyframesCss;
290
+
291
+ const totalDuration = Array.isArray(cfg.duration) ? cfg.duration.reduce((a, b) => a + b, 0) : cfg.duration;
292
+ const animation = `${keyframeName} ${totalDuration}s steps(1) infinite ${cfg.pingpong ? 'alternate' : ''}`;
293
+ css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; animation: ${animation}; }\n`;
294
+ } else {
295
+ css += `${className} { cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback}; }\n`;
480
296
  }
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' : ''};
489
- }
490
297
 
491
- @keyframes ${animName} {
492
- from { background-position: 0 0; }
493
- to { background-position: -${size[0] * frames}px 0; }
494
- }`;
298
+ if (cfg.tags && cfg.tags.length) {
299
+ const selector = cfg.tags.join(', ');
300
+ css += `${selector} { ${this._buildCursorCss(name, cfg)} }\n`;
495
301
  }
302
+ css += `[data-cursor="${name}"] { ${this._buildCursorCss(name, cfg)} }\n`;
496
303
  }
497
- // 若 frames 未定义或为单帧,则不生成动画,仅保留基础样式
498
- }
499
-
500
- style.textContent = css;
501
- document.head.appendChild(style);
502
- this.styleEl = style;
503
- }
504
-
505
- // ----------------------------
506
- // 给元素自动添加 data-cursor
507
- // ----------------------------
508
- _bindElements(refresh) {
509
- if (this.disabled) return;
510
304
 
511
- for (const [type, cfg] of Object.entries(this.options.cursors)) {
512
- if (!cfg.tags || !Array.isArray(cfg.tags) || cfg.tags.length === 0) continue;
513
-
514
- cfg.tags.forEach(tag => {
515
- const tagName = tag.toUpperCase();
516
- document.querySelectorAll(tagName).forEach(el => {
517
- if (!el.dataset.cursor) {
518
- el.dataset.cursor = type;
519
- el.dataset.cursorBound = 'true';
520
- }
521
- });
522
- });
523
- }
524
- if (refresh) {
525
- console.info('[AnimeCursor] refresh done');
305
+ if (this.options.excludeSelectors) {
306
+ css += `${this.options.excludeSelectors} { cursor: text !important; animation: none !important; }\n`;
526
307
  }
527
- }
528
308
 
529
- // ----------------------------
530
- // 鼠标跟随 & 光标切换
531
- // ----------------------------
532
- _bindMouse() {
533
- if (this.disabled) return;
309
+ css += `body.animecursor-disabled * { cursor: auto !important; animation: none !important; }\n`;
534
310
 
535
- this._onMouseMove = (e) => {
536
- if (this.disabled) return;
537
-
538
- const x = e.clientX;
539
- const y = e.clientY;
311
+ style.textContent = css;
312
+ document.head.appendChild(style);
313
+ this.styleEl = style;
314
+ }
540
315
 
541
- this.cursorEl.style.left = x + 'px';
542
- this.cursorEl.style.top = y + 'px';
316
+ _buildKeyframes(cfg, frameUrls) {
317
+ let frames = cfg.frames;
318
+ let durations = cfg.duration;
319
+ const frameCount = frameUrls.length;
543
320
 
544
- if (this.cursorEl.dataset.animecursorHide) {
545
- this.cursorEl.style.display = 'block';
546
- }
321
+ if (typeof frames === 'number') {
322
+ const perFrameDuration = durations / frames;
323
+ frames = new Array(frames).fill(1);
324
+ durations = new Array(frames.length).fill(perFrameDuration);
325
+ }
547
326
 
548
- if (this.debugEl) {
549
- this.debugEl.style.left = x + 'px';
550
- this.debugEl.style.top = y + 'px';
327
+ const keyframes = [];
328
+ let totalTime = durations.reduce((a, b) => a + b, 0);
329
+ let currentTime = 0;
330
+ let frameIdx = 0;
331
+ for (let seg = 0; seg < frames.length; seg++) {
332
+ const segFrames = frames[seg];
333
+ const segDuration = durations[seg];
334
+ const stepTime = segDuration / segFrames;
335
+ for (let f = 0; f < segFrames; f++) {
336
+ const percent = currentTime / totalTime;
337
+ keyframes.push({
338
+ percent: percent,
339
+ url: frameUrls[frameIdx]
340
+ });
341
+ currentTime += stepTime;
342
+ frameIdx++;
551
343
  }
344
+ }
345
+ keyframes.push({
346
+ percent: 1.0,
347
+ url: frameUrls[frameCount - 1]
348
+ });
349
+ return keyframes;
350
+ }
552
351
 
553
- let nextCursorType = null;
554
-
555
- // 获取命中的元素
556
- const target = document.elementFromPoint(x, y);
352
+ _buildCursorCss(name, cfg) {
353
+ const frameUrls = this._getFrameUrls(cfg);
354
+ const offset = cfg.offset || [0, 0];
355
+ const fallback = cfg.fallback || this.options.fallbackCursor;
356
+ let css = `cursor: url("${frameUrls[0]}") ${offset[0]} ${offset[1]}, ${fallback};`;
357
+ const hasAnimation = cfg.frames !== undefined && cfg.duration !== undefined &&
358
+ ((Array.isArray(cfg.frames) && Array.isArray(cfg.duration)) ||
359
+ (typeof cfg.frames === 'number' && typeof cfg.duration === 'number'));
360
+ if (hasAnimation && frameUrls.length > 1) {
361
+ const totalDuration = Array.isArray(cfg.duration) ? cfg.duration.reduce((a, b) => a + b, 0) : cfg.duration;
362
+ css += ` animation: ac_anim_${name} ${totalDuration}s steps(1) infinite ${cfg.pingpong ? 'alternate' : ''};`;
363
+ }
364
+ return css;
365
+ }
557
366
 
558
- // 优先使用元素自身的 data-cursor
559
- if (target && target.dataset && target.dataset.cursor) {
560
- nextCursorType = target.dataset.cursor;
367
+ _initDebug() {
368
+ const debugDiv = document.createElement('div');
369
+ debugDiv.className = 'animecursor-debug';
370
+ debugDiv.style.cssText = `
371
+ position: fixed;
372
+ top: 0;
373
+ left: 0;
374
+ background: rgba(0,0,0,0.7);
375
+ color: #0f0;
376
+ padding: 4px 8px;
377
+ font-family: monospace;
378
+ font-size: 12px;
379
+ z-index: 2147483647;
380
+ pointer-events: none;
381
+ white-space: nowrap;
382
+ `;
383
+ document.body.appendChild(debugDiv);
384
+ this.debugEl = debugDiv;
385
+
386
+ let lastCursor = '';
387
+ this._onMouseMove = (e) => {
388
+ const target = document.elementFromPoint(e.clientX, e.clientY);
389
+ let cursorType = null;
390
+ if (target) {
391
+ if (target.dataset.cursor && this.cursors[target.dataset.cursor]) {
392
+ cursorType = target.dataset.cursor;
393
+ } else {
394
+ for (const [name, cfg] of Object.entries(this.cursors)) {
395
+ if (cfg.tags && cfg.tags.some(tag => target.matches(tag))) {
396
+ cursorType = name;
397
+ break;
398
+ }
399
+ }
400
+ }
561
401
  }
562
- // 否则 尝试使用 default 光标
563
- else if (this.defaultCursorType) {
564
- nextCursorType = this.defaultCursorType;
402
+ // 如果没有匹配到任何自定义光标,且没有默认光标,则显示 "native"
403
+ if (!cursorType && !this.defaultCursorName) {
404
+ cursorType = 'native';
405
+ } else if (!cursorType && this.defaultCursorName) {
406
+ cursorType = this.defaultCursorName;
565
407
  }
566
-
567
- // 如果两者都没有 - 保持当前状态
568
- if (!nextCursorType) return;
569
- if (this.debugEl) {this.debugEl.textContent = `(${x}px , ${y}px) ${nextCursorType}`;}
570
-
571
- // 状态变化才切换 class
572
- if (nextCursorType !== this.lastCursorType) {
573
- if (this.debugEl) {this.cursorEl.className = `cursor-${nextCursorType}` + ' cursor-debugmode';}
574
- else {this.cursorEl.className = `cursor-${nextCursorType}`;}
575
- this.lastCursorType = nextCursorType;
408
+ if (cursorType !== lastCursor) {
409
+ lastCursor = cursorType;
410
+ debugDiv.textContent = `🎯 ${cursorType} @ (${e.clientX}, ${e.clientY})`;
411
+ } else {
412
+ debugDiv.textContent = `🎯 ${cursorType} @ (${e.clientX}, ${e.clientY})`;
576
413
  }
577
414
  };
578
-
579
415
  document.addEventListener('mousemove', this._onMouseMove);
580
- console.log('[AnimeCursor] AnimeCursor setted up.');
581
416
  }
582
417
 
583
- // ----------------------------
584
- // 获取可用最大 z-index
585
- // ----------------------------
586
- _getMaxZIndex() {
418
+ refresh() {
419
+ if (this.disabled) return;
420
+ if (this.styleEl) this.styleEl.remove();
421
+ this._injectStyles();
422
+ if (this.options.debug) {
423
+ if (this.debugEl) this.debugEl.remove();
424
+ this._initDebug();
425
+ }
426
+ console.log('[AnimeCursor] Refresh complete');
427
+ }
428
+
429
+ destroy() {
430
+ if (this.disabled) return;
431
+ if (this.styleEl) this.styleEl.remove();
432
+ if (this.debugEl) this.debugEl.remove();
433
+ if (this._onMouseMove) {
434
+ document.removeEventListener('mousemove', this._onMouseMove);
435
+ }
436
+ document.body.classList.remove('animecursor-disabled');
437
+ _instance = null;
438
+ console.log('[AnimeCursor] Destroyed');
439
+ }
440
+
441
+ disable() {
587
442
  if (this.disabled) return;
443
+ this.disabled = true;
444
+ document.body.classList.add('animecursor-disabled');
445
+ if (this.options.debug) console.log('[AnimeCursor] Disabled');
446
+ }
588
447
 
589
- return 2147483646; // 浏览器安全最大值 2147483647 减一为留给debug覆盖
448
+ enable() {
449
+ if (!this.disabled) return;
450
+ this.disabled = false;
451
+ document.body.classList.remove('animecursor-disabled');
452
+ if (this.options.debug) console.log('[AnimeCursor] Enabled');
590
453
  }
591
- }
592
-
593
- export { AnimeCursor as default };
454
+ }
455
+
456
+ export { AnimeCursor as default };