anime-cursor 0.2.0 → 0.3.0

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,16 +41,20 @@ 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
+ &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
+
54
58
  ```html
55
59
  <script>
56
60
  new AnimeCursor({
@@ -58,7 +62,7 @@ new AnimeCursor({
58
62
  // each type of cursor needs tags, size and image
59
63
  idle: {
60
64
  size: [64,64],
61
- image: './cursor_default.png', // static cursor only needs image
65
+ image: 'https://example.com/cursor_default.png', // static cursor only needs image
62
66
  default: true // set this cursor as default cursor
63
67
  // only default cursor doesn't needs tags
64
68
  },
@@ -66,7 +70,7 @@ new AnimeCursor({
66
70
  pointer: {
67
71
  tags: ['a', 'button'],
68
72
  size: [64,64],
69
- image: './cursor_pointer.png',
73
+ image: 'https://example.com/cursor_pointer.png',
70
74
  frames: 3,
71
75
  duration: 0.3,
72
76
  pingpong: true, // enable pingpong loop
@@ -76,7 +80,7 @@ new AnimeCursor({
76
80
  text: {
77
81
  tags: ['p', 'h1', 'h2', 'span'],
78
82
  size: [32, 64],
79
- image: './cursor_text.gif'
83
+ image: 'https://example.com/cursor_text.gif'
80
84
  }
81
85
  }
82
86
  });
@@ -167,7 +171,7 @@ If `duration` is not set, the cursor will be treated as a **static cursor**, eve
167
171
  ## [访问官网](https://shuninyu.github.io/anime-cursor/)以获取更多信息
168
172
  ## [阅读文档](https://shuninyu.github.io/anime-cursor/docs/zh)快速上手 AnimeCursor
169
173
 
170
- AnimeCursor 是一个轻量级自定义动画光标JavaScript库。
174
+ AnimeCursor 是一个轻量级自定义逐帧动画光标 JavaScript 库。
171
175
 
172
176
  AnimeCursor 无需依赖任何框架,适合个人网站、创意作品集以及实验性 UI 项目。
173
177
 
@@ -209,6 +213,11 @@ new AnimeCursor({...});
209
213
  ## 🚀 基础用法
210
214
 
211
215
  下面是一个 AnimeCursor 使用示例:
216
+
217
+ **重要提示**
218
+ - 请务必将初始化代码置于HTML文档的 **`<body>`** 标签内部。
219
+ - 为获得最佳性能,建议在DOM完全加载**之前**初始化AnimeCursor,因其部分功能需在DOM加载完成前执行。
220
+
212
221
  ```html
213
222
  <script>
214
223
  new AnimeCursor({
@@ -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
  }
@@ -157,6 +217,64 @@ class AnimeCursor {
157
217
  }
158
218
  }
159
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
+
160
278
  // ----------------------------
161
279
  // 插入光标元素 HTML
162
280
  // ----------------------------
@@ -166,7 +284,7 @@ class AnimeCursor {
166
284
  const cursor = document.createElement('div');
167
285
  cursor.id = 'anime-cursor';
168
286
 
169
- // 如果debug选项存在,则添加debug子元素
287
+ // 如果debug选项存在,则添加debug元素
170
288
  if (this.options.debug) {
171
289
  cursor.className = 'cursor-default cursor-debugmode';
172
290
  const debuger = document.createElement('div');
@@ -175,6 +293,14 @@ class AnimeCursor {
175
293
  this.debugEl = debuger;
176
294
  }
177
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
+ }
178
304
  document.body.appendChild(cursor);
179
305
  this.cursorEl = cursor;
180
306
  }
@@ -186,13 +312,12 @@ class AnimeCursor {
186
312
  if (this.disabled) return;
187
313
 
188
314
  const style = document.createElement('style');
315
+ style.id = 'animecursor-styles';
189
316
  let css = '';
190
317
 
191
318
  /* 通用样式 */
192
319
  css += `
193
- * {
194
- cursor: none !important;
195
- }
320
+ * {cursor: none !important;}
196
321
  #anime-cursor {
197
322
  position: fixed;
198
323
  top: 0;
@@ -313,7 +438,7 @@ class AnimeCursor {
313
438
  });
314
439
  }
315
440
  if (refresh) {
316
- console.info('[AnimeCursor] refresh done!');
441
+ console.info('[AnimeCursor] refresh done');
317
442
  }
318
443
  }
319
444
 
@@ -324,12 +449,18 @@ class AnimeCursor {
324
449
  if (this.disabled) return;
325
450
 
326
451
  this._onMouseMove = (e) => {
452
+ if (this.disabled) return;
453
+
327
454
  const x = e.clientX;
328
455
  const y = e.clientY;
329
456
 
330
457
  this.cursorEl.style.left = x + 'px';
331
458
  this.cursorEl.style.top = y + 'px';
332
459
 
460
+ if (this.cursorEl.dataset.animecursorHide) {
461
+ this.cursorEl.style.display = 'block';
462
+ }
463
+
333
464
  if (this.debugEl) {
334
465
  this.debugEl.style.left = x + 'px';
335
466
  this.debugEl.style.top = y + 'px';
@@ -373,6 +504,6 @@ class AnimeCursor {
373
504
 
374
505
  return 2147483646; // 浏览器安全最大值 2147483647 减一为留给debug覆盖
375
506
  }
376
- }
377
-
378
- export { AnimeCursor as default };
507
+ }
508
+
509
+ export { AnimeCursor as default };
@@ -1,16 +1,62 @@
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
- // 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
  }
@@ -163,6 +223,64 @@
163
223
  }
164
224
  }
165
225
 
226
+ // ----------------------------
227
+ // 等待 DOM 加载完成
228
+ // ----------------------------
229
+ _checkDomLoad() {
230
+ const init = () => {
231
+ this._injectHTML();
232
+ this._injectCSS();
233
+ this._bindElements();
234
+ this._bindMouse();
235
+ };
236
+
237
+ if (document.readyState === 'loading') {
238
+ document.addEventListener('DOMContentLoaded', init);
239
+ } else {
240
+ init();
241
+ }
242
+ }
243
+
244
+ // ----------------------------
245
+ // 插入光标图片预加载()
246
+ // ----------------------------
247
+ _injectPreload() {
248
+ if (this.disabled) return;
249
+
250
+ // 收集所有需要预加载的图片URL
251
+ const imageUrls = new Set();
252
+
253
+ // 遍历所有光标配置,提取图片URL
254
+ for (const cfg of Object.values(this.options.cursors)) {
255
+ if (cfg.image) {
256
+ imageUrls.add(cfg.image);
257
+ }
258
+ }
259
+
260
+ // 为每个图片URL创建预加载标签
261
+ imageUrls.forEach(url => {
262
+ const link = document.createElement('link');
263
+ link.rel = 'preload';
264
+ link.as = 'image';
265
+ link.href = url;
266
+
267
+ // 可选:添加跨域处理(如果图片来自不同域名)
268
+ if (url.startsWith('http') && !url.startsWith(window.location.origin)) {
269
+ link.crossOrigin = 'anonymous';
270
+ }
271
+
272
+ document.head.appendChild(link);
273
+
274
+ if (this.options.debug) {
275
+ console.info(`[AnimeCursor] Preloading image: ${url}`);
276
+ }
277
+ });
278
+
279
+ if (this.options.debug && imageUrls.size > 0) {
280
+ console.info(`[AnimeCursor] Preloaded ${imageUrls.size} cursor image(s)`);
281
+ }
282
+ }
283
+
166
284
  // ----------------------------
167
285
  // 插入光标元素 HTML
168
286
  // ----------------------------
@@ -172,7 +290,7 @@
172
290
  const cursor = document.createElement('div');
173
291
  cursor.id = 'anime-cursor';
174
292
 
175
- // 如果debug选项存在,则添加debug子元素
293
+ // 如果debug选项存在,则添加debug元素
176
294
  if (this.options.debug) {
177
295
  cursor.className = 'cursor-default cursor-debugmode';
178
296
  const debuger = document.createElement('div');
@@ -181,6 +299,14 @@
181
299
  this.debugEl = debuger;
182
300
  }
183
301
  else {cursor.className = 'cursor-default';}
302
+
303
+ // 检查是否设置初始化时显示光标
304
+ if (this.options.displayOnLoad) {
305
+ cursor.style.display = 'block';
306
+ } else {
307
+ cursor.style.display = 'none';
308
+ cursor.dataset.animecursorHide = 'true';
309
+ }
184
310
  document.body.appendChild(cursor);
185
311
  this.cursorEl = cursor;
186
312
  }
@@ -192,13 +318,12 @@
192
318
  if (this.disabled) return;
193
319
 
194
320
  const style = document.createElement('style');
321
+ style.id = 'animecursor-styles';
195
322
  let css = '';
196
323
 
197
324
  /* 通用样式 */
198
325
  css += `
199
- * {
200
- cursor: none !important;
201
- }
326
+ * {cursor: none !important;}
202
327
  #anime-cursor {
203
328
  position: fixed;
204
329
  top: 0;
@@ -319,7 +444,7 @@
319
444
  });
320
445
  }
321
446
  if (refresh) {
322
- console.info('[AnimeCursor] refresh done!');
447
+ console.info('[AnimeCursor] refresh done');
323
448
  }
324
449
  }
325
450
 
@@ -330,12 +455,18 @@
330
455
  if (this.disabled) return;
331
456
 
332
457
  this._onMouseMove = (e) => {
458
+ if (this.disabled) return;
459
+
333
460
  const x = e.clientX;
334
461
  const y = e.clientY;
335
462
 
336
463
  this.cursorEl.style.left = x + 'px';
337
464
  this.cursorEl.style.top = y + 'px';
338
465
 
466
+ if (this.cursorEl.dataset.animecursorHide) {
467
+ this.cursorEl.style.display = 'block';
468
+ }
469
+
339
470
  if (this.debugEl) {
340
471
  this.debugEl.style.left = x + 'px';
341
472
  this.debugEl.style.top = y + 'px';
@@ -379,8 +510,8 @@
379
510
 
380
511
  return 2147483646; // 浏览器安全最大值 2147483647 减一为留给debug覆盖
381
512
  }
382
- }
383
-
384
- return AnimeCursor;
385
-
386
- }));
513
+ }
514
+
515
+ return AnimeCursor;
516
+
517
+ }));
@@ -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")}),!(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")}}}_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 r=`.cursor-${e}`,n=o.size,i=o.frames,l=o.image,a=o.offset,d=o.zIndex,u=o.scale,c=l.toLowerCase().endsWith(".gif");var t;t=o.pixel?"pixelated":"auto",s+=`\n ${r} {\n width: ${n[0]}px;\n height: ${n[1]}px;\n background-image: url("${l}");\n image-rendering: ${t};\n ${u||a?`transform: ${[u&&`scale(${u[0]}, ${u[1]})`,a&&`translate(-${a[0]}px, -${a[1]}px)`].filter(Boolean).join(" ")};`:""}\n \n ${void 0!==d?`z-index:${d};`:""}\n }`;const h=o.duration;if(!c&&i>1&&"number"==typeof h){const t=`animecursor_${e}`;s+=`\n ${r} {\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: -${n[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=>{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 r=document.elementFromPoint(s,t);r&&r.dataset&&r.dataset.cursor?o=r.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.0",
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"