anime-cursor 0.2.0 → 0.3.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 CHANGED
@@ -8,7 +8,7 @@
8
8
  ## [Visit the official website](https://shuninyu.github.io/anime-cursor/) for more informations
9
9
  ## [Read documents](https://shuninyu.github.io/anime-cursor/docs) to get started with AnimeCursor
10
10
 
11
- AnimeCursor is a lightweight JavaScript library for animated custom cursors.
11
+ AnimeCursor is a lightweight JavaScript library for frame by frame animated custom cursors.
12
12
 
13
13
  AnimeCursor has no dependencies on any frameworks, making it suitable for personal websites, creative portfolios, and experimental UI projects.
14
14
 
@@ -41,32 +41,34 @@ import AnimeCursor from 'anime-cursor';
41
41
  new AnimeCursor({...});
42
42
  ```
43
43
 
44
- ### Local storage
44
+ ### Host Yourself
45
45
 
46
46
  ```html
47
47
  <script src="anime-cursor.umd.min.js"></script>
48
48
  ```
49
49
 
50
- ## 🚀 Basic Usage
50
+ ## 🚀 How to use
51
51
 
52
52
  Here is an example of how to use AnimeCursor:
53
53
 
54
- ```html
55
- <script>
54
+ &zwnj;**IMPORTANT**&zwnj;
55
+ - Ensure the initialization code is placed **within** the `<body>` tag of your HTML document.
56
+ - For optimal performance, it is recommended to initialize AnimeCursor **before**​ the DOM has fully loaded, as certain features require execution prior to the completion of DOM loading.
57
+
58
+ ```javascript
56
59
  new AnimeCursor({
57
60
  cursors: {
58
- // each type of cursor needs tags, size and image
61
+ // each type of cursor needs size and image
59
62
  idle: {
60
63
  size: [64,64],
61
- image: './cursor_default.png', // static cursor only needs image
64
+ image: 'https://example.com/cursor_default.png', // static cursor only needs image
62
65
  default: true // set this cursor as default cursor
63
- // only default cursor doesn't needs tags
64
66
  },
65
67
  // sprite animated cursor needs frames and duration
66
68
  pointer: {
67
- tags: ['a', 'button'],
69
+ tags: ['a', 'button'], // if you need certain types of elements to trigger this cursor, set the tags
68
70
  size: [64,64],
69
- image: './cursor_pointer.png',
71
+ image: 'https://example.com/cursor_pointer.png',
70
72
  frames: 3,
71
73
  duration: 0.3,
72
74
  pingpong: true, // enable pingpong loop
@@ -76,12 +78,20 @@ new AnimeCursor({
76
78
  text: {
77
79
  tags: ['p', 'h1', 'h2', 'span'],
78
80
  size: [32, 64],
79
- image: './cursor_text.gif'
81
+ image: 'https://example.com/cursor_text.gif'
82
+ },
83
+ haha: {
84
+ size: [32,32],
85
+ image: 'https://example.com/cursor_haha.png',
86
+ frames: 12,
87
+ duration: 1,
88
+ pixel: true, // if the image is origin size pixel art, set pixel to true
89
+ scale: [2,2] // scale the cursor
80
90
  }
81
91
  }
82
92
  });
83
- </script>
84
93
  ```
94
+ For non-default cursors, if you need a specific element to trigger the cursor, manually add the `data-cursor` attribute to the element. For example: if you want the `<div class="custom-div"></div>` to trigger the `haha` cursor, you need to add `data-cursor="haha"` to it, and the modified code should be as follows: `<div class="custom-div" data-cursor="haha"></div>`. This way, when the cursor hovers over the `custom-div` element, the cursor will switch to the `haha` style.
85
95
 
86
96
  ## ⚙️ Configuration Options
87
97
 
@@ -167,7 +177,7 @@ If `duration` is not set, the cursor will be treated as a **static cursor**, eve
167
177
  ## [访问官网](https://shuninyu.github.io/anime-cursor/)以获取更多信息
168
178
  ## [阅读文档](https://shuninyu.github.io/anime-cursor/docs/zh)快速上手 AnimeCursor
169
179
 
170
- AnimeCursor 是一个轻量级自定义动画光标JavaScript库。
180
+ AnimeCursor 是一个轻量级自定义逐帧动画光标 JavaScript 库。
171
181
 
172
182
  AnimeCursor 无需依赖任何框架,适合个人网站、创意作品集以及实验性 UI 项目。
173
183
 
@@ -209,37 +219,48 @@ new AnimeCursor({...});
209
219
  ## 🚀 基础用法
210
220
 
211
221
  下面是一个 AnimeCursor 使用示例:
212
- ```html
213
- <script>
222
+
223
+ **重要提示**
224
+ - 请务必将初始化代码置于HTML文档的 **`<body>`** 标签内部。
225
+ - 为获得最佳性能,建议在DOM完全加载**之前**初始化AnimeCursor,因其部分功能需在DOM加载完成前执行。
226
+
227
+ ```javascript
214
228
  new AnimeCursor({
215
229
  cursors: {
216
- // 每种光标都需要 tags size 和 image
230
+ // 每种光标都需要 size 和 image
217
231
  default: {
218
232
  size: [64,64],
219
- image: './cursor_default.png', // 静态光标只需要图片链接
233
+ image: 'https://example.com/cursor_default.png', // 静态光标只需要图片链接
220
234
  default: true // 将此光标设为默认光标
221
- // 默认光标不需要 tags
222
235
  },
223
236
  // 精灵图动画光标还需要 frames 和 duration
224
237
  pointer: {
225
- tags: ['a', 'button'],
238
+ tags: ['a', 'button'], // 如果需要某类元素触发该光标,设置 tags
226
239
  size: [64,64],
227
- image: './cursor_pointer.png',
240
+ image: 'https://example.com/cursor_pointer.png',
228
241
  frames: 3,
229
242
  duration: 0.3,
230
- pingpong: true, // enable pingpong loop
231
- offset: [10, 4] // if the pointing spot is not at the top left of the image, set offset
243
+ pingpong: true, // 启用乒乓循环
244
+ offset: [10,4] // 如果指针位置不在左上角,设置 offset
232
245
  },
233
246
  // gif 光标不需要 frames 或 duration
234
247
  text: {
235
248
  tags: ['p', 'h1', 'h2', 'span'],
236
- size: [32, 64],
237
- image: './cursor_text.gif'
249
+ size: [32,64],
250
+ image: 'https://example.com/cursor_text.gif'
251
+ },
252
+ haha: {
253
+ size: [32,32],
254
+ image: 'https://example.com/cursor_haha.png',
255
+ frames: 12,
256
+ duration: 1,
257
+ pixel: true, // 如果是原尺寸像素图,启用像素化渲染
258
+ scale: [2,2] // 缩放光标
238
259
  }
239
260
  }
240
261
  });
241
- </script>
242
262
  ```
263
+ 对于非默认光标,如果需要某元素触发该光标,请手动为该元素添加 `data-cursor`。例如:如果你想让 `<div class="custom-div"></div>` 触发 `haha` 光标,那么就要为其添加 `data-cursor="haha"`,修改完后应该是这样:`<div class="custom-div" data-cursor="haha"></div>`。这样当光标指向 `custom-div` 元素时,光标就会切换到 `haha`。
243
264
 
244
265
  ## ⚙️ 配置项说明
245
266
 
@@ -1,10 +1,56 @@
1
1
  // AnimeCursor by github@ShuninYu
2
- // v0.2.0
2
+ // v0.3.0
3
+
4
+ // 静态变量存储唯一实例
5
+ let _instance = null;
3
6
 
4
7
  class AnimeCursor {
5
8
 
9
+ static get instance() {
10
+ return _instance;
11
+ }
12
+
13
+ static destroy() {
14
+ if (_instance) {
15
+ _instance.destroy();
16
+ return true;
17
+ }
18
+ return false;
19
+ }
20
+
21
+ static refresh() {
22
+ if (_instance) {
23
+ _instance.refresh();
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+
29
+ static disable() {
30
+ if (_instance) {
31
+ _instance.disable();
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ static enable() {
38
+ if (_instance) {
39
+ _instance.enable();
40
+ return true;
41
+ }
42
+ return false;
43
+ }
44
+
6
45
  constructor(options = {}) {
46
+ // 如果已有实例,直接返回它
47
+ if (_instance) {
48
+ console.warn('[AnimeCursor] AnimeCursor already exists.');
49
+ return _instance;
50
+ }
51
+
7
52
  this.options = {
53
+ displayOnLoad: false,
8
54
  enableTouch: false,
9
55
  debug: false,
10
56
  ...options
@@ -23,12 +69,15 @@ class AnimeCursor {
23
69
  this.cursorEl = null;
24
70
  this.lastCursorType = null;
25
71
  this.debugEl = null;
72
+ this.styleEl = null;
73
+ this._onMouseMove = null;
26
74
 
27
75
  this._validateOptions();
28
- this._injectHTML();
29
- this._injectCSS();
30
- this._bindElements();
31
- this._bindMouse();
76
+ this._injectPreload();
77
+ this._checkDomLoad();
78
+
79
+ // 保存实例引用
80
+ _instance = this;
32
81
  }
33
82
 
34
83
  isMouseLikeDevice() {
@@ -75,19 +124,28 @@ class AnimeCursor {
75
124
  this.styleEl = null;
76
125
  }
77
126
 
78
- // 4 清理 data-cursor(只清理自己加的)
127
+ // 4 清理 data-cursor(只清理由 AnimeCursor 添加的)
79
128
  for (const cfg of Object.values(this.options.cursors)) {
80
- cfg.tags.forEach(tag => {
81
- document.querySelectorAll(tag).forEach(el => {
82
- if (el.dataset.cursor) {
83
- delete el.dataset.cursor;
84
- }
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
+ });
85
138
  });
86
- });
139
+ }
87
140
  }
88
141
 
89
142
  // 5 重置状态
90
143
  this.lastCursorType = null;
144
+
145
+ // 清除静态引用
146
+ if (_instance === this) {
147
+ _instance = null;
148
+ }
91
149
  }
92
150
  disable() {
93
151
  if (this.disabled) return;
@@ -95,6 +153,7 @@ class AnimeCursor {
95
153
 
96
154
  if (this.cursorEl) {
97
155
  this.cursorEl.style.display = 'none';
156
+ this.styleEl.innerHTML = this.styleEl.innerHTML.replace('* {cursor: none !important;}', '');
98
157
  console.log('[AnimeCursor] AnimeCursor disabled!');
99
158
  }
100
159
  }
@@ -104,6 +163,7 @@ class AnimeCursor {
104
163
 
105
164
  if (this.cursorEl) {
106
165
  this.cursorEl.style.display = '';
166
+ this.styleEl.innerHTML += '* {cursor: none; !important;}';
107
167
  console.log('[AnimeCursor] AnimeCursor enabled!');
108
168
  }
109
169
  }
@@ -124,7 +184,7 @@ class AnimeCursor {
124
184
  for (const [name, cfg] of Object.entries(this.options.cursors)) {
125
185
  if (cfg.default === true) {
126
186
  if (this.defaultCursorType) {
127
- throw new Error('[AnimeCursor] 只能有一个 default 光标');
187
+ throw new Error('[AnimeCursor] There can only be one default cursor');
128
188
  }
129
189
  this.defaultCursorType = name;
130
190
  }
@@ -138,14 +198,8 @@ class AnimeCursor {
138
198
  throw new Error('AnimeCursor init failed');
139
199
  }
140
200
  });
141
-
142
- if (!cfg.default) {
143
- if (!Array.isArray(cfg.tags) || cfg.tags.length === 0) {
144
- console.error(`[AnimeCursor] non-default cursor "${name}" must define at least one tag`);
145
- throw new Error('AnimeCursor init failed');
146
- }
147
- }
148
- if (cfg.default && cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
201
+
202
+ if (cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
149
203
  console.error(`[AnimeCursor] default cursor "${name}" 's tags must be an array if provided`);
150
204
  throw new Error('AnimeCursor init failed');
151
205
  }
@@ -157,6 +211,64 @@ class AnimeCursor {
157
211
  }
158
212
  }
159
213
 
214
+ // ----------------------------
215
+ // 等待 DOM 加载完成
216
+ // ----------------------------
217
+ _checkDomLoad() {
218
+ const init = () => {
219
+ this._injectHTML();
220
+ this._injectCSS();
221
+ this._bindElements();
222
+ this._bindMouse();
223
+ };
224
+
225
+ if (document.readyState === 'loading') {
226
+ document.addEventListener('DOMContentLoaded', init);
227
+ } else {
228
+ init();
229
+ }
230
+ }
231
+
232
+ // ----------------------------
233
+ // 插入光标图片预加载()
234
+ // ----------------------------
235
+ _injectPreload() {
236
+ if (this.disabled) return;
237
+
238
+ // 收集所有需要预加载的图片URL
239
+ const imageUrls = new Set();
240
+
241
+ // 遍历所有光标配置,提取图片URL
242
+ for (const cfg of Object.values(this.options.cursors)) {
243
+ if (cfg.image) {
244
+ imageUrls.add(cfg.image);
245
+ }
246
+ }
247
+
248
+ // 为每个图片URL创建预加载标签
249
+ imageUrls.forEach(url => {
250
+ const link = document.createElement('link');
251
+ link.rel = 'preload';
252
+ link.as = 'image';
253
+ link.href = url;
254
+
255
+ // 可选:添加跨域处理(如果图片来自不同域名)
256
+ if (url.startsWith('http') && !url.startsWith(window.location.origin)) {
257
+ link.crossOrigin = 'anonymous';
258
+ }
259
+
260
+ document.head.appendChild(link);
261
+
262
+ if (this.options.debug) {
263
+ console.info(`[AnimeCursor] Preloading image: ${url}`);
264
+ }
265
+ });
266
+
267
+ if (this.options.debug && imageUrls.size > 0) {
268
+ console.info(`[AnimeCursor] Preloaded ${imageUrls.size} cursor image(s)`);
269
+ }
270
+ }
271
+
160
272
  // ----------------------------
161
273
  // 插入光标元素 HTML
162
274
  // ----------------------------
@@ -166,7 +278,7 @@ class AnimeCursor {
166
278
  const cursor = document.createElement('div');
167
279
  cursor.id = 'anime-cursor';
168
280
 
169
- // 如果debug选项存在,则添加debug子元素
281
+ // 如果debug选项存在,则添加debug元素
170
282
  if (this.options.debug) {
171
283
  cursor.className = 'cursor-default cursor-debugmode';
172
284
  const debuger = document.createElement('div');
@@ -175,6 +287,14 @@ class AnimeCursor {
175
287
  this.debugEl = debuger;
176
288
  }
177
289
  else {cursor.className = 'cursor-default';}
290
+
291
+ // 检查是否设置初始化时显示光标
292
+ if (this.options.displayOnLoad) {
293
+ cursor.style.display = 'block';
294
+ } else {
295
+ cursor.style.display = 'none';
296
+ cursor.dataset.animecursorHide = 'true';
297
+ }
178
298
  document.body.appendChild(cursor);
179
299
  this.cursorEl = cursor;
180
300
  }
@@ -186,13 +306,12 @@ class AnimeCursor {
186
306
  if (this.disabled) return;
187
307
 
188
308
  const style = document.createElement('style');
309
+ style.id = 'animecursor-styles';
189
310
  let css = '';
190
311
 
191
312
  /* 通用样式 */
192
313
  css += `
193
- * {
194
- cursor: none !important;
195
- }
314
+ * {cursor: none !important;}
196
315
  #anime-cursor {
197
316
  position: fixed;
198
317
  top: 0;
@@ -300,7 +419,7 @@ class AnimeCursor {
300
419
  if (this.disabled) return;
301
420
 
302
421
  for (const [type, cfg] of Object.entries(this.options.cursors)) {
303
- if (!cfg.tags || cfg.tags.length === 0) continue;
422
+ if (!cfg.tags || !Array.isArray(cfg.tags) || cfg.tags.length === 0) continue;
304
423
 
305
424
  cfg.tags.forEach(tag => {
306
425
  const tagName = tag.toUpperCase();
@@ -313,7 +432,7 @@ class AnimeCursor {
313
432
  });
314
433
  }
315
434
  if (refresh) {
316
- console.info('[AnimeCursor] refresh done!');
435
+ console.info('[AnimeCursor] refresh done');
317
436
  }
318
437
  }
319
438
 
@@ -324,12 +443,18 @@ class AnimeCursor {
324
443
  if (this.disabled) return;
325
444
 
326
445
  this._onMouseMove = (e) => {
446
+ if (this.disabled) return;
447
+
327
448
  const x = e.clientX;
328
449
  const y = e.clientY;
329
450
 
330
451
  this.cursorEl.style.left = x + 'px';
331
452
  this.cursorEl.style.top = y + 'px';
332
453
 
454
+ if (this.cursorEl.dataset.animecursorHide) {
455
+ this.cursorEl.style.display = 'block';
456
+ }
457
+
333
458
  if (this.debugEl) {
334
459
  this.debugEl.style.left = x + 'px';
335
460
  this.debugEl.style.top = y + 'px';
@@ -5,12 +5,58 @@
5
5
  })(this, (function () { 'use strict';
6
6
 
7
7
  // AnimeCursor by github@ShuninYu
8
- // v0.2.0
8
+ // v0.3.0
9
+
10
+ // 静态变量存储唯一实例
11
+ let _instance = null;
9
12
 
10
13
  class AnimeCursor {
11
14
 
15
+ static get instance() {
16
+ return _instance;
17
+ }
18
+
19
+ static destroy() {
20
+ if (_instance) {
21
+ _instance.destroy();
22
+ return true;
23
+ }
24
+ return false;
25
+ }
26
+
27
+ static refresh() {
28
+ if (_instance) {
29
+ _instance.refresh();
30
+ return true;
31
+ }
32
+ return false;
33
+ }
34
+
35
+ static disable() {
36
+ if (_instance) {
37
+ _instance.disable();
38
+ return true;
39
+ }
40
+ return false;
41
+ }
42
+
43
+ static enable() {
44
+ if (_instance) {
45
+ _instance.enable();
46
+ return true;
47
+ }
48
+ return false;
49
+ }
50
+
12
51
  constructor(options = {}) {
52
+ // 如果已有实例,直接返回它
53
+ if (_instance) {
54
+ console.warn('[AnimeCursor] AnimeCursor already exists.');
55
+ return _instance;
56
+ }
57
+
13
58
  this.options = {
59
+ displayOnLoad: false,
14
60
  enableTouch: false,
15
61
  debug: false,
16
62
  ...options
@@ -29,12 +75,15 @@
29
75
  this.cursorEl = null;
30
76
  this.lastCursorType = null;
31
77
  this.debugEl = null;
78
+ this.styleEl = null;
79
+ this._onMouseMove = null;
32
80
 
33
81
  this._validateOptions();
34
- this._injectHTML();
35
- this._injectCSS();
36
- this._bindElements();
37
- this._bindMouse();
82
+ this._injectPreload();
83
+ this._checkDomLoad();
84
+
85
+ // 保存实例引用
86
+ _instance = this;
38
87
  }
39
88
 
40
89
  isMouseLikeDevice() {
@@ -81,19 +130,28 @@
81
130
  this.styleEl = null;
82
131
  }
83
132
 
84
- // 4 清理 data-cursor(只清理自己加的)
133
+ // 4 清理 data-cursor(只清理由 AnimeCursor 添加的)
85
134
  for (const cfg of Object.values(this.options.cursors)) {
86
- cfg.tags.forEach(tag => {
87
- document.querySelectorAll(tag).forEach(el => {
88
- if (el.dataset.cursor) {
89
- delete el.dataset.cursor;
90
- }
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
+ });
91
144
  });
92
- });
145
+ }
93
146
  }
94
147
 
95
148
  // 5 重置状态
96
149
  this.lastCursorType = null;
150
+
151
+ // 清除静态引用
152
+ if (_instance === this) {
153
+ _instance = null;
154
+ }
97
155
  }
98
156
  disable() {
99
157
  if (this.disabled) return;
@@ -101,6 +159,7 @@
101
159
 
102
160
  if (this.cursorEl) {
103
161
  this.cursorEl.style.display = 'none';
162
+ this.styleEl.innerHTML = this.styleEl.innerHTML.replace('* {cursor: none !important;}', '');
104
163
  console.log('[AnimeCursor] AnimeCursor disabled!');
105
164
  }
106
165
  }
@@ -110,6 +169,7 @@
110
169
 
111
170
  if (this.cursorEl) {
112
171
  this.cursorEl.style.display = '';
172
+ this.styleEl.innerHTML += '* {cursor: none; !important;}';
113
173
  console.log('[AnimeCursor] AnimeCursor enabled!');
114
174
  }
115
175
  }
@@ -130,7 +190,7 @@
130
190
  for (const [name, cfg] of Object.entries(this.options.cursors)) {
131
191
  if (cfg.default === true) {
132
192
  if (this.defaultCursorType) {
133
- throw new Error('[AnimeCursor] 只能有一个 default 光标');
193
+ throw new Error('[AnimeCursor] There can only be one default cursor');
134
194
  }
135
195
  this.defaultCursorType = name;
136
196
  }
@@ -144,14 +204,8 @@
144
204
  throw new Error('AnimeCursor init failed');
145
205
  }
146
206
  });
147
-
148
- if (!cfg.default) {
149
- if (!Array.isArray(cfg.tags) || cfg.tags.length === 0) {
150
- console.error(`[AnimeCursor] non-default cursor "${name}" must define at least one tag`);
151
- throw new Error('AnimeCursor init failed');
152
- }
153
- }
154
- if (cfg.default && cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
207
+
208
+ if (cfg.tags !== undefined && !Array.isArray(cfg.tags)) {
155
209
  console.error(`[AnimeCursor] default cursor "${name}" 's tags must be an array if provided`);
156
210
  throw new Error('AnimeCursor init failed');
157
211
  }
@@ -163,6 +217,64 @@
163
217
  }
164
218
  }
165
219
 
220
+ // ----------------------------
221
+ // 等待 DOM 加载完成
222
+ // ----------------------------
223
+ _checkDomLoad() {
224
+ const init = () => {
225
+ this._injectHTML();
226
+ this._injectCSS();
227
+ this._bindElements();
228
+ this._bindMouse();
229
+ };
230
+
231
+ if (document.readyState === 'loading') {
232
+ document.addEventListener('DOMContentLoaded', init);
233
+ } else {
234
+ init();
235
+ }
236
+ }
237
+
238
+ // ----------------------------
239
+ // 插入光标图片预加载()
240
+ // ----------------------------
241
+ _injectPreload() {
242
+ if (this.disabled) return;
243
+
244
+ // 收集所有需要预加载的图片URL
245
+ const imageUrls = new Set();
246
+
247
+ // 遍历所有光标配置,提取图片URL
248
+ for (const cfg of Object.values(this.options.cursors)) {
249
+ if (cfg.image) {
250
+ imageUrls.add(cfg.image);
251
+ }
252
+ }
253
+
254
+ // 为每个图片URL创建预加载标签
255
+ imageUrls.forEach(url => {
256
+ const link = document.createElement('link');
257
+ link.rel = 'preload';
258
+ link.as = 'image';
259
+ link.href = url;
260
+
261
+ // 可选:添加跨域处理(如果图片来自不同域名)
262
+ if (url.startsWith('http') && !url.startsWith(window.location.origin)) {
263
+ link.crossOrigin = 'anonymous';
264
+ }
265
+
266
+ document.head.appendChild(link);
267
+
268
+ if (this.options.debug) {
269
+ console.info(`[AnimeCursor] Preloading image: ${url}`);
270
+ }
271
+ });
272
+
273
+ if (this.options.debug && imageUrls.size > 0) {
274
+ console.info(`[AnimeCursor] Preloaded ${imageUrls.size} cursor image(s)`);
275
+ }
276
+ }
277
+
166
278
  // ----------------------------
167
279
  // 插入光标元素 HTML
168
280
  // ----------------------------
@@ -172,7 +284,7 @@
172
284
  const cursor = document.createElement('div');
173
285
  cursor.id = 'anime-cursor';
174
286
 
175
- // 如果debug选项存在,则添加debug子元素
287
+ // 如果debug选项存在,则添加debug元素
176
288
  if (this.options.debug) {
177
289
  cursor.className = 'cursor-default cursor-debugmode';
178
290
  const debuger = document.createElement('div');
@@ -181,6 +293,14 @@
181
293
  this.debugEl = debuger;
182
294
  }
183
295
  else {cursor.className = 'cursor-default';}
296
+
297
+ // 检查是否设置初始化时显示光标
298
+ if (this.options.displayOnLoad) {
299
+ cursor.style.display = 'block';
300
+ } else {
301
+ cursor.style.display = 'none';
302
+ cursor.dataset.animecursorHide = 'true';
303
+ }
184
304
  document.body.appendChild(cursor);
185
305
  this.cursorEl = cursor;
186
306
  }
@@ -192,13 +312,12 @@
192
312
  if (this.disabled) return;
193
313
 
194
314
  const style = document.createElement('style');
315
+ style.id = 'animecursor-styles';
195
316
  let css = '';
196
317
 
197
318
  /* 通用样式 */
198
319
  css += `
199
- * {
200
- cursor: none !important;
201
- }
320
+ * {cursor: none !important;}
202
321
  #anime-cursor {
203
322
  position: fixed;
204
323
  top: 0;
@@ -306,7 +425,7 @@
306
425
  if (this.disabled) return;
307
426
 
308
427
  for (const [type, cfg] of Object.entries(this.options.cursors)) {
309
- if (!cfg.tags || cfg.tags.length === 0) continue;
428
+ if (!cfg.tags || !Array.isArray(cfg.tags) || cfg.tags.length === 0) continue;
310
429
 
311
430
  cfg.tags.forEach(tag => {
312
431
  const tagName = tag.toUpperCase();
@@ -319,7 +438,7 @@
319
438
  });
320
439
  }
321
440
  if (refresh) {
322
- console.info('[AnimeCursor] refresh done!');
441
+ console.info('[AnimeCursor] refresh done');
323
442
  }
324
443
  }
325
444
 
@@ -330,12 +449,18 @@
330
449
  if (this.disabled) return;
331
450
 
332
451
  this._onMouseMove = (e) => {
452
+ if (this.disabled) return;
453
+
333
454
  const x = e.clientX;
334
455
  const y = e.clientY;
335
456
 
336
457
  this.cursorEl.style.left = x + 'px';
337
458
  this.cursorEl.style.top = y + 'px';
338
459
 
460
+ if (this.cursorEl.dataset.animecursorHide) {
461
+ this.cursorEl.style.display = 'block';
462
+ }
463
+
339
464
  if (this.debugEl) {
340
465
  this.debugEl.style.left = x + 'px';
341
466
  this.debugEl.style.top = y + 'px';
@@ -1 +1 @@
1
- !function(e,s){"object"==typeof exports&&"undefined"!=typeof module?module.exports=s():"function"==typeof define&&define.amd?define(s):(e="undefined"!=typeof globalThis?globalThis:e||self).AnimeCursor=s()}(this,function(){"use strict";return class{constructor(e={}){if(this.options={enableTouch:!1,debug:!1,...e},this.disabled=!1,!this.options.enableTouch&&!this.isMouseLikeDevice())return this.disabled=!0,void(this.options.debug&&console.warn("[AnimeCursor] Touch device detected, cursor disabled."));this.cursorEl=null,this.lastCursorType=null,this.debugEl=null,this._validateOptions(),this._injectHTML(),this._injectCSS(),this._bindElements(),this._bindMouse()}isMouseLikeDevice(){if(!this.disabled)return window.matchMedia("(pointer: fine)").matches}refresh(){this.disabled||(this.options.debug&&console.info("[AnimeCursor] starting refresh..."),this._bindElements(!0))}destroy(){if(!this.disabled){this._onMouseMove&&(document.removeEventListener("mousemove",this._onMouseMove),this._onMouseMove=null),this.cursorEl&&(this.cursorEl.remove(),this.cursorEl=null),this.debugEl&&(this.debugEl.remove(),this.debugEl=null),this.styleEl&&(this.styleEl.remove(),this.styleEl=null);for(const e of Object.values(this.options.cursors))e.tags.forEach(e=>{document.querySelectorAll(e).forEach(e=>{e.dataset.cursor&&delete e.dataset.cursor})});this.lastCursorType=null}}disable(){this.disabled||(this.disabled=!0,this.cursorEl&&(this.cursorEl.style.display="none",console.log("[AnimeCursor] AnimeCursor disabled!")))}enable(){this.disabled&&(this.disabled=!1,this.cursorEl&&(this.cursorEl.style.display="",console.log("[AnimeCursor] AnimeCursor enabled!")))}_validateOptions(){if(!this.disabled){if(!this.options||!this.options.cursors)throw console.error("[AnimeCursor] missing cursors set up"),new Error("AnimeCursor init failed");this.defaultCursorType=null;for(const[e,s]of Object.entries(this.options.cursors))if(!0===s.default){if(this.defaultCursorType)throw new Error("[AnimeCursor] 只能有一个 default 光标");this.defaultCursorType=e}for(const[e,s]of Object.entries(this.options.cursors)){if(["size","image"].forEach(t=>{if(void 0===s[t])throw console.error(`[AnimeCursor] cursor "${e}" missing required setting: ${t}`),new Error("AnimeCursor init failed")}),!(s.default||Array.isArray(s.tags)&&0!==s.tags.length))throw console.error(`[AnimeCursor] non-default cursor "${e}" must define at least one tag`),new Error("AnimeCursor init failed");if(s.default&&void 0!==s.tags&&!Array.isArray(s.tags))throw console.error(`[AnimeCursor] default cursor "${e}" 's tags must be an array if provided`),new Error("AnimeCursor init failed");if(void 0!==s.duration&&"number"!=typeof s.duration)throw console.error(`[AnimeCursor] cursor "${e}" 's duration must be a number(seconds)`),new Error("AnimeCursor init failed")}}}_injectHTML(){if(this.disabled)return;const e=document.createElement("div");if(e.id="anime-cursor",this.options.debug){e.className="cursor-default cursor-debugmode";const s=document.createElement("div");s.className="anime-cursor-debug",document.body.appendChild(s),this.debugEl=s}else e.className="cursor-default";document.body.appendChild(e),this.cursorEl=e}_injectCSS(){if(this.disabled)return;const e=document.createElement("style");let s="";s+=`\n * {\n cursor: none !important;\n }\n #anime-cursor {\n position: fixed;\n top: 0;\n left: 0;\n pointer-events: none;\n background-repeat: no-repeat;\n transform-origin: 0 0;\n transform-style: preserve-3d;\n z-index: ${this._getMaxZIndex()};\n }\n .cursor-debugmode {\n border: 1px solid green;\n }\n .anime-cursor-debug {\n position: fixed;\n top: 0;\n left: 0;\n width: fit-content;\n height: fit-content;\n padding: 5px;\n font-size: 16px;\n text-wrap: nowrap;\n color: red;\n pointer-events: none;\n overflow: visible;\n z-index: 2147483647;\n }\n .anime-cursor-debug::before {\n position: absolute;\n content: "";\n top: 0;\n left: 0;\n width: 100vw;\n height: 1px;\n background-color: red;\n }\n .anime-cursor-debug::after {\n position: absolute;\n content: "";\n top: 0;\n left: 0;\n width: 1px;\n height: 100vh;\n background-color: red;\n }\n `;for(const[e,o]of Object.entries(this.options.cursors)){const n=`.cursor-${e}`,r=o.size,i=o.frames,u=o.image,d=o.offset,l=o.zIndex,a=o.scale,c=u.toLowerCase().endsWith(".gif");var t;t=o.pixel?"pixelated":"auto",s+=`\n ${n} {\n width: ${r[0]}px;\n height: ${r[1]}px;\n background-image: url("${u}");\n image-rendering: ${t};\n ${a||d?`transform: ${[a&&`scale(${a[0]}, ${a[1]})`,d&&`translate(-${d[0]}px, -${d[1]}px)`].filter(Boolean).join(" ")};`:""}\n \n ${void 0!==l?`z-index:${l};`:""}\n }`;const h=o.duration;if(!c&&i>1&&"number"==typeof h){const t=`animecursor_${e}`;s+=`\n ${n} {\n animation: ${t} steps(${i}) ${h}s infinite ${o.pingpong?"alternate":""};\n }\n\n @keyframes ${t} {\n from { background-position: 0 0; }\n to { background-position: -${r[0]*i}px 0; }\n }\n `}}e.textContent=s,document.head.appendChild(e),this.styleEl=e}_bindElements(e){if(!this.disabled){for(const[e,s]of Object.entries(this.options.cursors))s.tags&&0!==s.tags.length&&s.tags.forEach(s=>{const t=s.toUpperCase();document.querySelectorAll(t).forEach(s=>{s.dataset.cursor||(s.dataset.cursor=e,s.dataset.cursorBound="true")})});e&&console.info("[AnimeCursor] refresh done!")}}_bindMouse(){this.disabled||(this._onMouseMove=e=>{const s=e.clientX,t=e.clientY;this.cursorEl.style.left=s+"px",this.cursorEl.style.top=t+"px",this.debugEl&&(this.debugEl.style.left=s+"px",this.debugEl.style.top=t+"px");let o=null;const n=document.elementFromPoint(s,t);n&&n.dataset&&n.dataset.cursor?o=n.dataset.cursor:this.defaultCursorType&&(o=this.defaultCursorType),o&&(this.debugEl&&(this.debugEl.textContent=`(${s}px , ${t}px) ${o}`),o!==this.lastCursorType&&(this.debugEl?this.cursorEl.className=`cursor-${o} cursor-debugmode`:this.cursorEl.className=`cursor-${o}`,this.lastCursorType=o))},document.addEventListener("mousemove",this._onMouseMove),console.log("[AnimeCursor] AnimeCursor setted up."))}_getMaxZIndex(){if(!this.disabled)return 2147483646}}});
1
+ !function(e,s){"object"==typeof exports&&"undefined"!=typeof module?module.exports=s():"function"==typeof define&&define.amd?define(s):(e="undefined"!=typeof globalThis?globalThis:e||self).AnimeCursor=s()}(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(s={}){return e?(console.warn("[AnimeCursor] AnimeCursor already exists."),e):(this.options={displayOnLoad:!1,enableTouch:!1,debug:!1,...s},this.disabled=!1,this.options.enableTouch||this.isMouseLikeDevice()?(this.cursorEl=null,this.lastCursorType=null,this.debugEl=null,this.styleEl=null,this._onMouseMove=null,this._validateOptions(),this._injectPreload(),this._checkDomLoad(),void(e=this)):(this.disabled=!0,void(this.options.debug&&console.warn("[AnimeCursor] Touch device detected, cursor disabled."))))}isMouseLikeDevice(){if(!this.disabled)return window.matchMedia("(pointer: fine)").matches}refresh(){this.disabled||(this.options.debug&&console.info("[AnimeCursor] starting refresh..."),this._bindElements(!0))}destroy(){if(!this.disabled){this._onMouseMove&&(document.removeEventListener("mousemove",this._onMouseMove),this._onMouseMove=null),this.cursorEl&&(this.cursorEl.remove(),this.cursorEl=null),this.debugEl&&(this.debugEl.remove(),this.debugEl=null),this.styleEl&&(this.styleEl.remove(),this.styleEl=null);for(const e of Object.values(this.options.cursors))e.tags&&Array.isArray(e.tags)&&e.tags.forEach(e=>{document.querySelectorAll(e).forEach(e=>{e.dataset.cursorBound&&(delete e.dataset.cursor,delete e.dataset.cursorBound)})});this.lastCursorType=null,e===this&&(e=null)}}disable(){this.disabled||(this.disabled=!0,this.cursorEl&&(this.cursorEl.style.display="none",this.styleEl.innerHTML=this.styleEl.innerHTML.replace("* {cursor: none !important;}",""),console.log("[AnimeCursor] AnimeCursor disabled!")))}enable(){this.disabled&&(this.disabled=!1,this.cursorEl&&(this.cursorEl.style.display="",this.styleEl.innerHTML+="* {cursor: none; !important;}",console.log("[AnimeCursor] AnimeCursor enabled!")))}_validateOptions(){if(!this.disabled){if(!this.options||!this.options.cursors)throw console.error("[AnimeCursor] missing cursors set up"),new Error("AnimeCursor init failed");this.defaultCursorType=null;for(const[e,s]of Object.entries(this.options.cursors))if(!0===s.default){if(this.defaultCursorType)throw new Error("[AnimeCursor] There can only be one default cursor");this.defaultCursorType=e}for(const[e,s]of Object.entries(this.options.cursors)){if(["size","image"].forEach(t=>{if(void 0===s[t])throw console.error(`[AnimeCursor] cursor "${e}" missing required setting: ${t}`),new Error("AnimeCursor init failed")}),void 0!==s.tags&&!Array.isArray(s.tags))throw console.error(`[AnimeCursor] default cursor "${e}" 's tags must be an array if provided`),new Error("AnimeCursor init failed");if(void 0!==s.duration&&"number"!=typeof s.duration)throw console.error(`[AnimeCursor] cursor "${e}" 's duration must be a number(seconds)`),new Error("AnimeCursor init failed")}}}_checkDomLoad(){const e=()=>{this._injectHTML(),this._injectCSS(),this._bindElements(),this._bindMouse()};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",e):e()}_injectPreload(){if(this.disabled)return;const e=new Set;for(const s of Object.values(this.options.cursors))s.image&&e.add(s.image);e.forEach(e=>{const s=document.createElement("link");s.rel="preload",s.as="image",s.href=e,e.startsWith("http")&&!e.startsWith(window.location.origin)&&(s.crossOrigin="anonymous"),document.head.appendChild(s),this.options.debug&&console.info(`[AnimeCursor] Preloading image: ${e}`)}),this.options.debug&&e.size>0&&console.info(`[AnimeCursor] Preloaded ${e.size} cursor image(s)`)}_injectHTML(){if(this.disabled)return;const e=document.createElement("div");if(e.id="anime-cursor",this.options.debug){e.className="cursor-default cursor-debugmode";const s=document.createElement("div");s.className="anime-cursor-debug",document.body.appendChild(s),this.debugEl=s}else e.className="cursor-default";this.options.displayOnLoad?e.style.display="block":(e.style.display="none",e.dataset.animecursorHide="true"),document.body.appendChild(e),this.cursorEl=e}_injectCSS(){if(this.disabled)return;const e=document.createElement("style");e.id="animecursor-styles";let s="";s+=`\n * {cursor: none !important;}\n #anime-cursor {\n position: fixed;\n top: 0;\n left: 0;\n pointer-events: none;\n background-repeat: no-repeat;\n transform-origin: 0 0;\n transform-style: preserve-3d;\n z-index: ${this._getMaxZIndex()};\n }\n .cursor-debugmode {\n border: 1px solid green;\n }\n .anime-cursor-debug {\n position: fixed;\n top: 0;\n left: 0;\n width: fit-content;\n height: fit-content;\n padding: 5px;\n font-size: 16px;\n text-wrap: nowrap;\n color: red;\n pointer-events: none;\n overflow: visible;\n z-index: 2147483647;\n }\n .anime-cursor-debug::before {\n position: absolute;\n content: "";\n top: 0;\n left: 0;\n width: 100vw;\n height: 1px;\n background-color: red;\n }\n .anime-cursor-debug::after {\n position: absolute;\n content: "";\n top: 0;\n left: 0;\n width: 1px;\n height: 100vh;\n background-color: red;\n }\n `;for(const[e,o]of Object.entries(this.options.cursors)){const n=`.cursor-${e}`,r=o.size,i=o.frames,l=o.image,d=o.offset,a=o.zIndex,u=o.scale,c=l.toLowerCase().endsWith(".gif");var t;t=o.pixel?"pixelated":"auto",s+=`\n ${n} {\n width: ${r[0]}px;\n height: ${r[1]}px;\n background-image: url("${l}");\n image-rendering: ${t};\n ${u||d?`transform: ${[u&&`scale(${u[0]}, ${u[1]})`,d&&`translate(-${d[0]}px, -${d[1]}px)`].filter(Boolean).join(" ")};`:""}\n \n ${void 0!==a?`z-index:${a};`:""}\n }`;const h=o.duration;if(!c&&i>1&&"number"==typeof h){const t=`animecursor_${e}`;s+=`\n ${n} {\n animation: ${t} steps(${i}) ${h}s infinite ${o.pingpong?"alternate":""};\n }\n\n @keyframes ${t} {\n from { background-position: 0 0; }\n to { background-position: -${r[0]*i}px 0; }\n }\n `}}e.textContent=s,document.head.appendChild(e),this.styleEl=e}_bindElements(e){if(!this.disabled){for(const[e,s]of Object.entries(this.options.cursors))s.tags&&Array.isArray(s.tags)&&0!==s.tags.length&&s.tags.forEach(s=>{const t=s.toUpperCase();document.querySelectorAll(t).forEach(s=>{s.dataset.cursor||(s.dataset.cursor=e,s.dataset.cursorBound="true")})});e&&console.info("[AnimeCursor] refresh done")}}_bindMouse(){this.disabled||(this._onMouseMove=e=>{if(this.disabled)return;const s=e.clientX,t=e.clientY;this.cursorEl.style.left=s+"px",this.cursorEl.style.top=t+"px",this.cursorEl.dataset.animecursorHide&&(this.cursorEl.style.display="block"),this.debugEl&&(this.debugEl.style.left=s+"px",this.debugEl.style.top=t+"px");let o=null;const n=document.elementFromPoint(s,t);n&&n.dataset&&n.dataset.cursor?o=n.dataset.cursor:this.defaultCursorType&&(o=this.defaultCursorType),o&&(this.debugEl&&(this.debugEl.textContent=`(${s}px , ${t}px) ${o}`),o!==this.lastCursorType&&(this.debugEl?this.cursorEl.className=`cursor-${o} cursor-debugmode`:this.cursorEl.className=`cursor-${o}`,this.lastCursorType=o))},document.addEventListener("mousemove",this._onMouseMove),console.log("[AnimeCursor] AnimeCursor setted up."))}_getMaxZIndex(){if(!this.disabled)return 2147483646}}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anime-cursor",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "A lightweight JavaScript library for animated custom cursors",
5
5
  "main": "dist/anime-cursor.umd.js",
6
6
  "module": "dist/anime-cursor.esm.js",
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "author": "ShuninYu",
24
24
  "license": "MIT",
25
+ "homepage": "https://animecursor.js.org",
25
26
  "devDependencies": {
26
27
  "@rollup/plugin-terser": "^0.4.4",
27
28
  "rollup": "^4.56.0"