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.
- package/README.md +170 -170
- package/dist/anime-cursor.esm.js +316 -453
- package/dist/anime-cursor.umd.js +325 -462
- package/dist/anime-cursor.umd.min.js +1 -1
- package/package.json +1 -1
package/dist/anime-cursor.esm.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// AnimeCursor by github@ShuninYu
|
|
2
|
-
//
|
|
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]
|
|
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.
|
|
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
|
-
|
|
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.
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (cfg.
|
|
186
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
console.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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.
|
|
240
|
-
|
|
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
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
cursor.dataset.animecursorHide = 'true';
|
|
246
|
+
init();
|
|
334
247
|
}
|
|
335
|
-
document.body.appendChild(cursor);
|
|
336
|
-
this.cursorEl = cursor;
|
|
337
248
|
}
|
|
338
249
|
|
|
339
|
-
//
|
|
340
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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';
|
|
472
|
-
|
|
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
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
512
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const y = e.clientY;
|
|
311
|
+
style.textContent = css;
|
|
312
|
+
document.head.appendChild(style);
|
|
313
|
+
this.styleEl = style;
|
|
314
|
+
}
|
|
540
315
|
|
|
541
|
-
|
|
542
|
-
|
|
316
|
+
_buildKeyframes(cfg, frameUrls) {
|
|
317
|
+
let frames = cfg.frames;
|
|
318
|
+
let durations = cfg.duration;
|
|
319
|
+
const frameCount = frameUrls.length;
|
|
543
320
|
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
569
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
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 };
|