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