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.umd.js
CHANGED
|
@@ -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
|
-
//
|
|
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]
|
|
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.
|
|
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.
|
|
184
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (cfg.
|
|
192
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
console.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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.
|
|
246
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
339
|
-
cursor.dataset.animecursorHide = 'true';
|
|
252
|
+
init();
|
|
340
253
|
}
|
|
341
|
-
document.body.appendChild(cursor);
|
|
342
|
-
this.cursorEl = cursor;
|
|
343
254
|
}
|
|
344
255
|
|
|
345
|
-
//
|
|
346
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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';
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
518
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const y = e.clientY;
|
|
317
|
+
style.textContent = css;
|
|
318
|
+
document.head.appendChild(style);
|
|
319
|
+
this.styleEl = style;
|
|
320
|
+
}
|
|
546
321
|
|
|
547
|
-
|
|
548
|
-
|
|
322
|
+
_buildKeyframes(cfg, frameUrls) {
|
|
323
|
+
let frames = cfg.frames;
|
|
324
|
+
let durations = cfg.duration;
|
|
325
|
+
const frameCount = frameUrls.length;
|
|
549
326
|
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
//
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
575
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}));
|