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.
- package/dist/anime-cursor.esm.js +22 -58
- package/dist/anime-cursor.umd.js +22 -58
- package/dist/anime-cursor.umd.min.js +1 -1
- package/package.json +1 -1
package/dist/anime-cursor.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// AnimeCursor by github@ShuninYu
|
|
2
|
-
// v2.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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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 =
|
|
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;
|
package/dist/anime-cursor.umd.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
})(this, (function () { 'use strict';
|
|
6
6
|
|
|
7
7
|
// AnimeCursor by github@ShuninYu
|
|
8
|
-
// v2.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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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 =
|
|
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`)}
|
|
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"))}}});
|