anime-cursor 0.1.2 → 0.2.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
@@ -1,12 +1,14 @@
1
1
  # AnimeCursor
2
2
 
3
3
  <div align="center">
4
- <img src="https://cdn.jsdelivr.net/gh/shuninyu/anime-cursor@main/title.gif" width="60%" alt="AnimeCursor"/>
4
+ <img src="https://cdn.jsdelivr.net/gh/shuninyu/anime-cursor@main/title.gif" width="384px" alt="AnimeCursor"/>
5
5
  </div>
6
6
 
7
7
  [[简体中文]](#animecursorsc)
8
+ ## [Visit the official website](https://shuninyu.github.io/anime-cursor/) for more informations
9
+ ## [Read documents](https://shuninyu.github.io/anime-cursor/docs) to get started with AnimeCursor
8
10
 
9
- AnimeCursor is a lightweight animated cursor JS library that enables dynamic mouse pointers for websites.
11
+ AnimeCursor is a lightweight JavaScript library for animated custom cursors.
10
12
 
11
13
  AnimeCursor has no dependencies on any frameworks, making it suitable for personal websites, creative portfolios, and experimental UI projects.
12
14
 
@@ -14,11 +16,11 @@ AnimeCursor has no dependencies on any frameworks, making it suitable for person
14
16
 
15
17
  ## ✨ Features
16
18
 
17
- * Supports PNG sprite sheet frame-by-frame animation
19
+ * Supports sprite sheet frame-by-frame animation
18
20
  * Supports GIF (animated GIFs, not static GIFs used by native cursors)
19
21
  * Customizable cursor types, automatically switched by AnimeCursor
20
22
  * CSS-based animation implementation, high performance
21
- * Prepare PNG sprite sheets in the correct format, and AnimeCursor will automatically generate cursor animations based on your settings
23
+ * Prepare sprite sheets in the correct format, and AnimeCursor will automatically generate cursor animations based on your settings
22
24
  * Built with native JavaScript, no third-party dependencies
23
25
 
24
26
  ## 📦 Installation
@@ -54,12 +56,13 @@ Here is an example of how to use AnimeCursor:
54
56
  new AnimeCursor({
55
57
  cursors: {
56
58
  // each type of cursor needs tags, size and image
57
- default: {
58
- tags: ['body'], // default cursor recommended setting
59
+ idle: {
59
60
  size: [64,64],
60
- image: './cursor_default.png' // static cursor only needs image
61
+ image: './cursor_default.png', // static cursor only needs image
62
+ default: true // set this cursor as default cursor
63
+ // only default cursor doesn't needs tags
61
64
  },
62
- // png sprite animated cursor needs frames and duration
65
+ // sprite animated cursor needs frames and duration
63
66
  pointer: {
64
67
  tags: ['a', 'button'],
65
68
  size: [64,64],
@@ -92,18 +95,7 @@ Each key represents a cursor type (the name can be freely defined).
92
95
 
93
96
  For each key, the following parameters can be set. Missing required parameters will cause an error.
94
97
 
95
- | Parameter | Type | Required | Description |
96
- |-|-|-|-|
97
- | `tags` | `string[]` | ✅ | HTML tags that should use this cursor |
98
- | `size` | `number` | ✅ | Cursor dimensions [width, height] in pixels |
99
- | `image` | `string` | ✅ | Image path (PNG / GIF) |
100
- | `frames` | `number` || Number of frames for PNG sprites (set to `1` for static images) |
101
- | `duration` | `number` | | Animation loop duration (seconds) ⚠️ PNG sprite animations *only* work when this parameter is set |
102
- | `pingpong` | `boolean` | | Enable ping-pong (back-and-forth) looping (for PNG sprite animations only) |
103
- | `offset` | `[number, number]` | | Cursor alignment offset [ x , y ] ⚠️ This parameter is not affected by `scale`|
104
- | `scale` | `[number, number]` | | Cursor scale factor based on `size` [ x , y ] ⚠️ Only supported for GIF cursors. Do not set for PNG sprite cursors, as it will break the animation. |
105
- | `pixel` | `boolean` | | Enable pixelated rendering |
106
- | `zIndex` | `number` | | Cursor z-index layer (not recommended to modify) |
98
+ Check the [DOCUMENTATION](https://shuninyu.github.io/anime-cursor/docs/configuration#options) for details.
107
99
 
108
100
  ### `debug` (Optional)
109
101
 
@@ -122,10 +114,10 @@ If you want animated cursors to be displayed on these devices, add `enableTouch:
122
114
 
123
115
  ### 📁 Files
124
116
 
125
- * **For any PNG sprite animation cursor, its PNG sprite sheet should be arranged in a single horizontal row.** AnimeCursor will automatically generate the PNG sprite animation.
117
+ * **For any sprite animation cursor, its sprite sheet should be arranged in a single horizontal row.** AnimeCursor will automatically generate the PNG sprite animation.
126
118
  For example, if you set the `size` (width, height) for a `pointer` cursor to `[64px , 64px]` and `frames` to `3`, the prepared sprite sheet dimensions (width, height) should be: `[192px , 64px]`.
127
119
 
128
- * For pixel art with a large number of frames, you can use the original image (whether GIF or PNG-sprite-sheet) to save storage space or bandwidth. Then, use the `scale` parameter in the configuration to resize the cursor and set `pixel` to `true` to avoid blurry scaling.
120
+ * For pixel art with a large number of frames, you can use the original image (whether GIF or sprite-sheet) to save storage space or bandwidth. Then, use the `scale` parameter in the configuration to resize the cursor and set `pixel` to `true` to avoid blurry scaling.
129
121
 
130
122
  ### 🧩 Tagging Mechanism
131
123
 
@@ -145,11 +137,10 @@ Therefore, to assign a specific animated cursor to a particular element, simply
145
137
 
146
138
  ### 🎞️ Animation Rules
147
139
 
148
- #### PNG Cursors
140
+ #### Sprite Sheets Animation Cursors
149
141
 
150
142
  Animation is generated **only when all of the following conditions are met**:
151
143
 
152
- * The image is a PNG
153
144
  * `frames` is set and `frames > 1`
154
145
  * `duration` is set
155
146
 
@@ -170,10 +161,13 @@ If `duration` is not set, the cursor will be treated as a **static cursor**, eve
170
161
  # AnimeCursor(SC)
171
162
 
172
163
  <div align="center">
173
- <img src="https://cdn.jsdelivr.net/gh/shuninyu/anime-cursor@main/title.gif" width="60%" alt="AnimeCursor"/>
164
+ <img src="https://cdn.jsdelivr.net/gh/shuninyu/anime-cursor@main/title.gif" width="384px" alt="AnimeCursor"/>
174
165
  </div>
175
166
 
176
- AnimeCursor 是一个轻量级动画光标JS,能够让网站拥有动态的鼠标指针。
167
+ ## [访问官网](https://shuninyu.github.io/anime-cursor/)以获取更多信息
168
+ ## [阅读文档](https://shuninyu.github.io/anime-cursor/docs/zh)快速上手 AnimeCursor
169
+
170
+ AnimeCursor 是一个轻量级自定义动画光标JavaScript库。
177
171
 
178
172
  AnimeCursor 无需依赖任何框架,适合个人网站、创意作品集以及实验性 UI 项目。
179
173
 
@@ -181,11 +175,11 @@ AnimeCursor 无需依赖任何框架,适合个人网站、创意作品集以
181
175
 
182
176
  ## ✨ 特性
183
177
 
184
- * 支持 PNG 精灵图逐帧动画
178
+ * 支持精灵图逐帧动画
185
179
  * 支持 GIF(动态gif,而不是原生光标的静止gif)
186
180
  * 自定义光标类型,由 AnimeCursor 自动切换
187
181
  * 基于 CSS 的动画实现,高性能
188
- * 按照格式准备好 PNG 精灵图表,AnimeCursor 将基于你的设置自动生成光标动画
182
+ * 按照格式准备好精灵图表,AnimeCursor 将基于你的设置自动生成光标动画
189
183
  * 基于原生JavaScript,无任何第三方依赖
190
184
 
191
185
  ## 📦 部署方法
@@ -221,11 +215,12 @@ new AnimeCursor({
221
215
  cursors: {
222
216
  // 每种光标都需要 tags size 和 image
223
217
  default: {
224
- tags: ['body'], // 默认光标推荐照此设置
225
218
  size: [64,64],
226
- image: './cursor_default.png' // 静态光标只需要图片链接
219
+ image: './cursor_default.png', // 静态光标只需要图片链接
220
+ default: true // 将此光标设为默认光标
221
+ // 默认光标不需要 tags
227
222
  },
228
- // png 精灵图动画光标还需要 frames 和 duration
223
+ // 精灵图动画光标还需要 frames 和 duration
229
224
  pointer: {
230
225
  tags: ['a', 'button'],
231
226
  size: [64,64],
@@ -258,18 +253,7 @@ new AnimeCursor({
258
253
 
259
254
  对于每个key,有以下参数可以设置,其中必填项如果缺失则会报错。
260
255
 
261
- |参数|类型|必填|说明|
262
- |-|-|-|-|
263
- |`tags`|`string[]`|✅|使用该光标的 HTML 标签|
264
- |`size`|`number`|✅|光标尺寸(宽高,像素)|
265
- |`image`|`string`|✅|图片路径(PNG / GIF)|
266
- |`frames`|`number`||PNG 帧数(静态图片请设置为 `1` )|
267
- |`duration`|`number`||动画循环时长(秒)⚠️PNG精灵图动画只有设置该参数才会生效|
268
- |`pingpong`|`boolean`||是否启用乒乓循环(仅PNG精灵图动画)|
269
- |`offset`|`[number, number]`||光标对齐偏移量 [ x , y ]⚠️此参数不受 `scale` 影响|
270
- |`scale`|`[number, number]`||基于size的光标缩放 [ x , y ]⚠️仅支持GIF光标,PNG精灵图动画光标请勿设置,否则会使动画失效|
271
- |`pixel`|`boolean`||是否启用像素化渲染|
272
- |`zIndex`|`number`||光标层级(不建议添加此项设置)|
256
+ 查看 [官方文档](https://shuninyu.github.io/anime-cursor/docs/zh/configuration#options) 查看详细可用参数。
273
257
 
274
258
  ### `debug`(选填)
275
259
 
@@ -288,10 +272,10 @@ AnimeCursor 会自动识别移动触屏设备(比如手机、平板电脑)
288
272
 
289
273
  ### 📁 文件
290
274
 
291
- * **对于任何 PNG 精灵图动画光标,它的 PNG 精灵图表都应该为单行横向布局,** AnimeCursor 会自动生成 PNG 精灵图动画。
275
+ * **对于任何精灵图动画光标,它的精灵图表都应该为单行横向布局,** AnimeCursor 会自动生成 PNG 精灵图动画。
292
276
  例如,你为 `pointer` 光标设置的`size`(长,宽)为 `[64px , 64px]` ,帧数为 `3` ,那么你准备的精灵图表尺寸(长,宽)应该为: `[192px , 64px]` 。
293
277
 
294
- * 对于帧数特别多的像素图,你可以使用原尺寸图片(无论是gif还是png精灵图表)以节省存储空间或带宽,并在参数中设置 `scale` 来缩放光标,并将 `pixel` 设置为 `true` 来避免缩放模糊。
278
+ * 对于帧数特别多的像素图,你可以使用原尺寸图片(无论是gif还是精灵图表)以节省存储空间或带宽,并在参数中设置 `scale` 来缩放光标,并将 `pixel` 设置为 `true` 来避免缩放模糊。
295
279
 
296
280
  ### 🧩 标记机制
297
281
 
@@ -311,11 +295,10 @@ AnimeCursor 会根据配置自动为页面元素添加 `data-cursor`:
311
295
 
312
296
  ### 🎞️ 动画判定
313
297
 
314
- #### PNG 光标
298
+ #### 精灵图表动画光标
315
299
 
316
300
  只有在 **同时满足以下条件** 时,才会生成动画:
317
301
 
318
- * 图片为 PNG
319
302
  * 设置了 `frames` 且 `frames > 1`
320
303
  * 设置了 `duration`
321
304
 
@@ -1,5 +1,5 @@
1
1
  // AnimeCursor by github@ShuninYu
2
- // v0.1.2
2
+ // v0.2.0
3
3
 
4
4
  class AnimeCursor {
5
5
 
@@ -32,38 +32,126 @@ class AnimeCursor {
32
32
  }
33
33
 
34
34
  isMouseLikeDevice() {
35
+ if (this.disabled) return;
36
+
35
37
  return window.matchMedia('(pointer: fine)').matches;
36
38
  }
37
39
 
38
- destroy() {
40
+ // ----------------------------
41
+ // 刷新 清理 关闭 开启
42
+ // ----------------------------
43
+ refresh() {
39
44
  if (this.disabled) return;
45
+
46
+ if (this.options.debug) {
47
+ console.info('[AnimeCursor] starting refresh...');
48
+ }
49
+
50
+ this._bindElements(true);
40
51
  }
52
+ destroy() {
53
+ if (this.disabled) return;
41
54
 
55
+ // 1 移除事件监听
56
+ if (this._onMouseMove) {
57
+ document.removeEventListener('mousemove', this._onMouseMove);
58
+ this._onMouseMove = null;
59
+ }
60
+
61
+ // 2 移除 cursor DOM
62
+ if (this.cursorEl) {
63
+ this.cursorEl.remove();
64
+ this.cursorEl = null;
65
+ }
66
+
67
+ if (this.debugEl) {
68
+ this.debugEl.remove();
69
+ this.debugEl = null;
70
+ }
71
+
72
+ // 3 移除注入的 CSS
73
+ if (this.styleEl) {
74
+ this.styleEl.remove();
75
+ this.styleEl = null;
76
+ }
77
+
78
+ // 4 清理 data-cursor(只清理自己加的)
79
+ 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
+ }
85
+ });
86
+ });
87
+ }
88
+
89
+ // 5 重置状态
90
+ this.lastCursorType = null;
91
+ }
92
+ disable() {
93
+ if (this.disabled) return;
94
+ this.disabled = true;
95
+
96
+ if (this.cursorEl) {
97
+ this.cursorEl.style.display = 'none';
98
+ console.log('[AnimeCursor] AnimeCursor disabled!');
99
+ }
100
+ }
101
+ enable() {
102
+ if (!this.disabled) return;
103
+ this.disabled = false;
104
+
105
+ if (this.cursorEl) {
106
+ this.cursorEl.style.display = '';
107
+ console.log('[AnimeCursor] AnimeCursor enabled!');
108
+ }
109
+ }
110
+
42
111
  // ----------------------------
43
112
  // 配置校验(必填项)
44
113
  // ----------------------------
45
114
  _validateOptions() {
115
+ if (this.disabled) return;
116
+
46
117
  if (!this.options || !this.options.cursors) {
47
- console.error('[AnimeCursor] 缺少 cursors 配置');
118
+ console.error('[AnimeCursor] missing cursors set up');
48
119
  throw new Error('AnimeCursor init failed');
49
120
  }
50
121
 
122
+ this.defaultCursorType = null;
123
+
124
+ for (const [name, cfg] of Object.entries(this.options.cursors)) {
125
+ if (cfg.default === true) {
126
+ if (this.defaultCursorType) {
127
+ throw new Error('[AnimeCursor] 只能有一个 default 光标');
128
+ }
129
+ this.defaultCursorType = name;
130
+ }
131
+ }
132
+
51
133
  for (const [name, cfg] of Object.entries(this.options.cursors)) {
52
- const required = ['tags', 'size', 'image'];
134
+ const required = ['size', 'image'];
53
135
  required.forEach(key => {
54
136
  if (cfg[key] === undefined) {
55
- console.error(`[AnimeCursor] 光标 "${name}" 缺少必填项:${key}`);
137
+ console.error(`[AnimeCursor] cursor "${name}" missing required setting: ${key}`);
56
138
  throw new Error('AnimeCursor init failed');
57
139
  }
58
140
  });
59
141
 
60
- if (!Array.isArray(cfg.tags)) {
61
- console.error(`[AnimeCursor] 光标 "${name}" tags 必须是数组`);
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)) {
149
+ console.error(`[AnimeCursor] default cursor "${name}" 's tags must be an array if provided`);
62
150
  throw new Error('AnimeCursor init failed');
63
151
  }
64
152
 
65
153
  if (cfg.duration !== undefined && typeof cfg.duration !== 'number') {
66
- console.error(`[AnimeCursor] 光标 "${name}" duration 必须是数字(秒)`);
154
+ console.error(`[AnimeCursor] cursor "${name}" 's duration must be a number(seconds)`);
67
155
  throw new Error('AnimeCursor init failed');
68
156
  }
69
157
  }
@@ -73,6 +161,8 @@ class AnimeCursor {
73
161
  // 插入光标元素 HTML
74
162
  // ----------------------------
75
163
  _injectHTML() {
164
+ if (this.disabled) return;
165
+
76
166
  const cursor = document.createElement('div');
77
167
  cursor.id = 'anime-cursor';
78
168
 
@@ -93,60 +183,62 @@ class AnimeCursor {
93
183
  // 插入样式 CSS
94
184
  // ----------------------------
95
185
  _injectCSS() {
186
+ if (this.disabled) return;
187
+
96
188
  const style = document.createElement('style');
97
189
  let css = '';
98
190
 
99
191
  /* 通用样式 */
100
192
  css += `
101
- * {
102
- cursor: none !important;
103
- }
104
- #anime-cursor {
105
- position: fixed;
106
- top: 0;
107
- left: 0;
108
- pointer-events: none;
109
- background-repeat: no-repeat;
110
- transform-origin: 0 0;
111
- transform-style: preserve-3d;
112
- z-index: ${this._getMaxZIndex()};
113
- }
114
- .cursor-debugmode {
115
- border: 1px solid green;
116
- }
117
- .anime-cursor-debug {
118
- position: fixed;
119
- top: 0;
120
- left: 0;
121
- width: fit-content;
122
- height: fit-content;
123
- padding: 5px;
124
- font-size: 16px;
125
- text-wrap: nowrap;
126
- color: red;
127
- pointer-events: none;
128
- overflow: visible;
129
- z-index: 2147483647;
130
- }
131
- .anime-cursor-debug::before {
132
- position: absolute;
133
- content: "";
134
- top: 0;
135
- left: 0;
136
- width: 100vw;
137
- height: 1px;
138
- background-color: red;
139
- }
140
- .anime-cursor-debug::after {
141
- position: absolute;
142
- content: "";
143
- top: 0;
144
- left: 0;
145
- width: 1px;
146
- height: 100vh;
147
- background-color: red;
148
- }
149
- `;
193
+ * {
194
+ cursor: none !important;
195
+ }
196
+ #anime-cursor {
197
+ position: fixed;
198
+ top: 0;
199
+ left: 0;
200
+ pointer-events: none;
201
+ background-repeat: no-repeat;
202
+ transform-origin: 0 0;
203
+ transform-style: preserve-3d;
204
+ z-index: ${this._getMaxZIndex()};
205
+ }
206
+ .cursor-debugmode {
207
+ border: 1px solid green;
208
+ }
209
+ .anime-cursor-debug {
210
+ position: fixed;
211
+ top: 0;
212
+ left: 0;
213
+ width: fit-content;
214
+ height: fit-content;
215
+ padding: 5px;
216
+ font-size: 16px;
217
+ text-wrap: nowrap;
218
+ color: red;
219
+ pointer-events: none;
220
+ overflow: visible;
221
+ z-index: 2147483647;
222
+ }
223
+ .anime-cursor-debug::before {
224
+ position: absolute;
225
+ content: "";
226
+ top: 0;
227
+ left: 0;
228
+ width: 100vw;
229
+ height: 1px;
230
+ background-color: red;
231
+ }
232
+ .anime-cursor-debug::after {
233
+ position: absolute;
234
+ content: "";
235
+ top: 0;
236
+ left: 0;
237
+ width: 1px;
238
+ height: 100vh;
239
+ background-color: red;
240
+ }
241
+ `;
150
242
 
151
243
  /* 每种光标以及debug生成 CSS */
152
244
  for (const [type, cfg] of Object.entries(this.options.cursors)) {
@@ -163,17 +255,17 @@ z-index: ${this._getMaxZIndex()};
163
255
  else {pixel = 'auto';}
164
256
 
165
257
  css += `
166
- ${className} {
167
- width: ${size[0]}px;
168
- height: ${size[1]}px;
169
- background-image: url("${image}");
170
- image-rendering: ${pixel};
171
- ${(scale || offset) ? `transform: ${[scale && `scale(${scale[0]}, ${scale[1]})`, offset && `translate(-${offset[0]}px, -${offset[1]}px)`].filter(Boolean).join(' ')};` : ''}
172
-
173
- ${zIndex !== undefined ? `z-index:${zIndex};` : ''}
174
- }`;
175
-
176
- /* PNG 精灵图动画 */
258
+ ${className} {
259
+ width: ${size[0]}px;
260
+ height: ${size[1]}px;
261
+ background-image: url("${image}");
262
+ image-rendering: ${pixel};
263
+ ${(scale || offset) ? `transform: ${[scale && `scale(${scale[0]}, ${scale[1]})`, offset && `translate(-${offset[0]}px, -${offset[1]}px)`].filter(Boolean).join(' ')};` : ''}
264
+
265
+ ${zIndex !== undefined ? `z-index:${zIndex};` : ''}
266
+ }`;
267
+
268
+ /* 精灵图动画 */
177
269
  const duration = cfg.duration;
178
270
  const hasAnimation =
179
271
  !isGif &&
@@ -184,43 +276,54 @@ ${zIndex !== undefined ? `z-index:${zIndex};` : ''}
184
276
  const animName = `animecursor_${type}`;
185
277
 
186
278
  css += `
187
- ${className} {
188
- animation: ${animName} steps(${frames}) ${duration}s infinite ${cfg.pingpong ? 'alternate' : ''};
189
- }
190
-
191
- @keyframes ${animName} {
192
- from { background-position: 0 0; }
193
- to { background-position: -${size[0] * frames}px 0; }
194
- }
195
- `;
279
+ ${className} {
280
+ animation: ${animName} steps(${frames}) ${duration}s infinite ${cfg.pingpong ? 'alternate' : ''};
281
+ }
282
+
283
+ @keyframes ${animName} {
284
+ from { background-position: 0 0; }
285
+ to { background-position: -${size[0] * frames}px 0; }
286
+ }
287
+ `;
196
288
  }
197
289
  }
198
290
 
199
291
  style.textContent = css;
200
292
  document.head.appendChild(style);
293
+ this.styleEl = style;
201
294
  }
202
295
 
203
296
  // ----------------------------
204
297
  // 给元素自动添加 data-cursor
205
298
  // ----------------------------
206
- _bindElements() {
299
+ _bindElements(refresh) {
300
+ if (this.disabled) return;
301
+
207
302
  for (const [type, cfg] of Object.entries(this.options.cursors)) {
303
+ if (!cfg.tags || cfg.tags.length === 0) continue;
304
+
208
305
  cfg.tags.forEach(tag => {
209
306
  const tagName = tag.toUpperCase();
210
307
  document.querySelectorAll(tagName).forEach(el => {
211
308
  if (!el.dataset.cursor) {
212
309
  el.dataset.cursor = type;
310
+ el.dataset.cursorBound = 'true';
213
311
  }
214
312
  });
215
313
  });
216
314
  }
315
+ if (refresh) {
316
+ console.info('[AnimeCursor] refresh done!');
317
+ }
217
318
  }
218
319
 
219
320
  // ----------------------------
220
321
  // 鼠标跟随 & 光标切换
221
322
  // ----------------------------
222
323
  _bindMouse() {
223
- document.addEventListener('mousemove', e => {
324
+ if (this.disabled) return;
325
+
326
+ this._onMouseMove = (e) => {
224
327
  const x = e.clientX;
225
328
  const y = e.clientY;
226
329
 
@@ -232,24 +335,42 @@ to { background-position: -${size[0] * frames}px 0; }
232
335
  this.debugEl.style.top = y + 'px';
233
336
  }
234
337
 
235
- const target = document.elementFromPoint(x, y);
236
- if (!target) return;
338
+ let nextCursorType = null;
237
339
 
238
- const cursorType = target.dataset.cursor || 'default';
239
- if (this.debugEl) {this.debugEl.textContent = `(${x}px , ${y}px) ${cursorType}`;}
340
+ // 获取命中的元素
341
+ const target = document.elementFromPoint(x, y);
240
342
 
241
- if (cursorType !== this.lastCursorType) {
242
- if (this.debugEl) {this.cursorEl.className = `cursor-${cursorType}` + ' cursor-debugmode';}
243
- else {this.cursorEl.className = `cursor-${cursorType}`;}
244
- this.lastCursorType = cursorType;
343
+ // 优先使用元素自身的 data-cursor
344
+ if (target && target.dataset && target.dataset.cursor) {
345
+ nextCursorType = target.dataset.cursor;
346
+ }
347
+ // 否则 尝试使用 default 光标
348
+ else if (this.defaultCursorType) {
349
+ nextCursorType = this.defaultCursorType;
350
+ }
351
+
352
+ // 如果两者都没有 - 保持当前状态
353
+ if (!nextCursorType) return;
354
+ if (this.debugEl) {this.debugEl.textContent = `(${x}px , ${y}px) ${nextCursorType}`;}
355
+
356
+ // 状态变化才切换 class
357
+ if (nextCursorType !== this.lastCursorType) {
358
+ if (this.debugEl) {this.cursorEl.className = `cursor-${nextCursorType}` + ' cursor-debugmode';}
359
+ else {this.cursorEl.className = `cursor-${nextCursorType}`;}
360
+ this.lastCursorType = nextCursorType;
245
361
  }
246
- });
362
+ };
363
+
364
+ document.addEventListener('mousemove', this._onMouseMove);
365
+ console.log('[AnimeCursor] AnimeCursor setted up.');
247
366
  }
248
367
 
249
368
  // ----------------------------
250
369
  // 获取可用最大 z-index
251
370
  // ----------------------------
252
371
  _getMaxZIndex() {
372
+ if (this.disabled) return;
373
+
253
374
  return 2147483646; // 浏览器安全最大值 2147483647 减一为留给debug覆盖
254
375
  }
255
376
  }
@@ -1,265 +1,386 @@
1
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());
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
5
  })(this, (function () { 'use strict';
6
6
 
7
- // AnimeCursor by github@ShuninYu
8
- // v0.1.2
9
-
10
- class AnimeCursor {
11
-
12
- constructor(options = {}) {
13
- this.options = {
14
- enableTouch: false,
15
- debug: false,
16
- ...options
17
- };
18
- this.disabled = false;
19
-
20
- if (!this.options.enableTouch && !this.isMouseLikeDevice()) {
21
- this.disabled = true;
22
-
23
- if (this.options.debug) {
24
- console.warn('[AnimeCursor] Touch device detected, cursor disabled.');
25
- }
26
- return;
27
- }
28
-
29
- this.cursorEl = null;
30
- this.lastCursorType = null;
31
- this.debugEl = null;
32
-
33
- this._validateOptions();
34
- this._injectHTML();
35
- this._injectCSS();
36
- this._bindElements();
37
- this._bindMouse();
38
- }
39
-
40
- isMouseLikeDevice() {
41
- return window.matchMedia('(pointer: fine)').matches;
42
- }
43
-
44
- destroy() {
45
- if (this.disabled) return;
46
- }
47
-
48
- // ----------------------------
49
- // 配置校验(必填项)
50
- // ----------------------------
51
- _validateOptions() {
52
- if (!this.options || !this.options.cursors) {
53
- console.error('[AnimeCursor] 缺少 cursors 配置');
54
- throw new Error('AnimeCursor init failed');
55
- }
56
-
57
- for (const [name, cfg] of Object.entries(this.options.cursors)) {
58
- const required = ['tags', 'size', 'image'];
59
- required.forEach(key => {
60
- if (cfg[key] === undefined) {
61
- console.error(`[AnimeCursor] 光标 "${name}" 缺少必填项:${key}`);
62
- throw new Error('AnimeCursor init failed');
63
- }
64
- });
65
-
66
- if (!Array.isArray(cfg.tags)) {
67
- console.error(`[AnimeCursor] 光标 "${name}" tags 必须是数组`);
68
- throw new Error('AnimeCursor init failed');
69
- }
70
-
71
- if (cfg.duration !== undefined && typeof cfg.duration !== 'number') {
72
- console.error(`[AnimeCursor] 光标 "${name}" 的 duration 必须是数字(秒)`);
73
- throw new Error('AnimeCursor init failed');
74
- }
75
- }
76
- }
77
-
78
- // ----------------------------
79
- // 插入光标元素 HTML
80
- // ----------------------------
81
- _injectHTML() {
82
- const cursor = document.createElement('div');
83
- cursor.id = 'anime-cursor';
84
-
85
- // 如果debug选项存在,则添加debug子元素
86
- if (this.options.debug) {
87
- cursor.className = 'cursor-default cursor-debugmode';
88
- const debuger = document.createElement('div');
89
- debuger.className = 'anime-cursor-debug';
90
- document.body.appendChild(debuger);
91
- this.debugEl = debuger;
92
- }
93
- else {cursor.className = 'cursor-default';}
94
- document.body.appendChild(cursor);
95
- this.cursorEl = cursor;
96
- }
97
-
98
- // ----------------------------
99
- // 插入样式 CSS
100
- // ----------------------------
101
- _injectCSS() {
102
- const style = document.createElement('style');
103
- let css = '';
104
-
105
- /* 通用样式 */
106
- css += `
107
- * {
108
- cursor: none !important;
109
- }
110
- #anime-cursor {
111
- position: fixed;
112
- top: 0;
113
- left: 0;
114
- pointer-events: none;
115
- background-repeat: no-repeat;
116
- transform-origin: 0 0;
117
- transform-style: preserve-3d;
118
- z-index: ${this._getMaxZIndex()};
119
- }
120
- .cursor-debugmode {
121
- border: 1px solid green;
122
- }
123
- .anime-cursor-debug {
124
- position: fixed;
125
- top: 0;
126
- left: 0;
127
- width: fit-content;
128
- height: fit-content;
129
- padding: 5px;
130
- font-size: 16px;
131
- text-wrap: nowrap;
132
- color: red;
133
- pointer-events: none;
134
- overflow: visible;
135
- z-index: 2147483647;
136
- }
137
- .anime-cursor-debug::before {
138
- position: absolute;
139
- content: "";
140
- top: 0;
141
- left: 0;
142
- width: 100vw;
143
- height: 1px;
144
- background-color: red;
145
- }
146
- .anime-cursor-debug::after {
147
- position: absolute;
148
- content: "";
149
- top: 0;
150
- left: 0;
151
- width: 1px;
152
- height: 100vh;
153
- background-color: red;
154
- }
155
- `;
156
-
157
- /* 每种光标以及debug生成 CSS */
158
- for (const [type, cfg] of Object.entries(this.options.cursors)) {
159
- const className = `.cursor-${type}`;
160
- const size = cfg.size;
161
- const frames = cfg.frames;
162
- const image = cfg.image;
163
- const offset = cfg.offset;
164
- const zIndex = cfg.zIndex;
165
- const scale = cfg.scale;
166
- const isGif = image.toLowerCase().endsWith('.gif');
167
- var pixel;
168
- if (cfg.pixel) {pixel = 'pixelated';}
169
- else {pixel = 'auto';}
170
-
171
- css += `
172
- ${className} {
173
- width: ${size[0]}px;
174
- height: ${size[1]}px;
175
- background-image: url("${image}");
176
- image-rendering: ${pixel};
177
- ${(scale || offset) ? `transform: ${[scale && `scale(${scale[0]}, ${scale[1]})`, offset && `translate(-${offset[0]}px, -${offset[1]}px)`].filter(Boolean).join(' ')};` : ''}
178
-
179
- ${zIndex !== undefined ? `z-index:${zIndex};` : ''}
180
- }`;
181
-
182
- /* PNG 精灵图动画 */
183
- const duration = cfg.duration;
184
- const hasAnimation =
185
- !isGif &&
186
- frames > 1 &&
187
- typeof duration === 'number';
188
-
189
- if (hasAnimation) {
190
- const animName = `animecursor_${type}`;
191
-
192
- css += `
193
- ${className} {
194
- animation: ${animName} steps(${frames}) ${duration}s infinite ${cfg.pingpong ? 'alternate' : ''};
195
- }
196
-
197
- @keyframes ${animName} {
198
- from { background-position: 0 0; }
199
- to { background-position: -${size[0] * frames}px 0; }
200
- }
201
- `;
202
- }
203
- }
204
-
205
- style.textContent = css;
206
- document.head.appendChild(style);
207
- }
208
-
209
- // ----------------------------
210
- // 给元素自动添加 data-cursor
211
- // ----------------------------
212
- _bindElements() {
213
- for (const [type, cfg] of Object.entries(this.options.cursors)) {
214
- cfg.tags.forEach(tag => {
215
- const tagName = tag.toUpperCase();
216
- document.querySelectorAll(tagName).forEach(el => {
217
- if (!el.dataset.cursor) {
218
- el.dataset.cursor = type;
219
- }
220
- });
221
- });
222
- }
223
- }
224
-
225
- // ----------------------------
226
- // 鼠标跟随 & 光标切换
227
- // ----------------------------
228
- _bindMouse() {
229
- document.addEventListener('mousemove', e => {
230
- const x = e.clientX;
231
- const y = e.clientY;
232
-
233
- this.cursorEl.style.left = x + 'px';
234
- this.cursorEl.style.top = y + 'px';
235
-
236
- if (this.debugEl) {
237
- this.debugEl.style.left = x + 'px';
238
- this.debugEl.style.top = y + 'px';
239
- }
240
-
241
- const target = document.elementFromPoint(x, y);
242
- if (!target) return;
243
-
244
- const cursorType = target.dataset.cursor || 'default';
245
- if (this.debugEl) {this.debugEl.textContent = `(${x}px , ${y}px) ${cursorType}`;}
246
-
247
- if (cursorType !== this.lastCursorType) {
248
- if (this.debugEl) {this.cursorEl.className = `cursor-${cursorType}` + ' cursor-debugmode';}
249
- else {this.cursorEl.className = `cursor-${cursorType}`;}
250
- this.lastCursorType = cursorType;
251
- }
252
- });
253
- }
254
-
255
- // ----------------------------
256
- // 获取可用最大 z-index
257
- // ----------------------------
258
- _getMaxZIndex() {
259
- return 2147483646; // 浏览器安全最大值 2147483647 减一为留给debug覆盖
260
- }
261
- }
7
+ // AnimeCursor by github@ShuninYu
8
+ // v0.2.0
9
+
10
+ class AnimeCursor {
11
+
12
+ constructor(options = {}) {
13
+ this.options = {
14
+ enableTouch: false,
15
+ debug: false,
16
+ ...options
17
+ };
18
+ this.disabled = false;
19
+
20
+ if (!this.options.enableTouch && !this.isMouseLikeDevice()) {
21
+ this.disabled = true;
22
+
23
+ if (this.options.debug) {
24
+ console.warn('[AnimeCursor] Touch device detected, cursor disabled.');
25
+ }
26
+ return;
27
+ }
28
+
29
+ this.cursorEl = null;
30
+ this.lastCursorType = null;
31
+ this.debugEl = null;
32
+
33
+ this._validateOptions();
34
+ this._injectHTML();
35
+ this._injectCSS();
36
+ this._bindElements();
37
+ this._bindMouse();
38
+ }
39
+
40
+ isMouseLikeDevice() {
41
+ if (this.disabled) return;
42
+
43
+ return window.matchMedia('(pointer: fine)').matches;
44
+ }
45
+
46
+ // ----------------------------
47
+ // 刷新 清理 关闭 开启
48
+ // ----------------------------
49
+ refresh() {
50
+ if (this.disabled) return;
51
+
52
+ if (this.options.debug) {
53
+ console.info('[AnimeCursor] starting refresh...');
54
+ }
55
+
56
+ this._bindElements(true);
57
+ }
58
+ destroy() {
59
+ if (this.disabled) return;
60
+
61
+ // 1 移除事件监听
62
+ if (this._onMouseMove) {
63
+ document.removeEventListener('mousemove', this._onMouseMove);
64
+ this._onMouseMove = null;
65
+ }
66
+
67
+ // 2 移除 cursor DOM
68
+ if (this.cursorEl) {
69
+ this.cursorEl.remove();
70
+ this.cursorEl = null;
71
+ }
72
+
73
+ if (this.debugEl) {
74
+ this.debugEl.remove();
75
+ this.debugEl = null;
76
+ }
77
+
78
+ // 3 移除注入的 CSS
79
+ if (this.styleEl) {
80
+ this.styleEl.remove();
81
+ this.styleEl = null;
82
+ }
83
+
84
+ // 4 清理 data-cursor(只清理自己加的)
85
+ 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
+ }
91
+ });
92
+ });
93
+ }
94
+
95
+ // 5 重置状态
96
+ this.lastCursorType = null;
97
+ }
98
+ disable() {
99
+ if (this.disabled) return;
100
+ this.disabled = true;
101
+
102
+ if (this.cursorEl) {
103
+ this.cursorEl.style.display = 'none';
104
+ console.log('[AnimeCursor] AnimeCursor disabled!');
105
+ }
106
+ }
107
+ enable() {
108
+ if (!this.disabled) return;
109
+ this.disabled = false;
110
+
111
+ if (this.cursorEl) {
112
+ this.cursorEl.style.display = '';
113
+ console.log('[AnimeCursor] AnimeCursor enabled!');
114
+ }
115
+ }
116
+
117
+ // ----------------------------
118
+ // 配置校验(必填项)
119
+ // ----------------------------
120
+ _validateOptions() {
121
+ if (this.disabled) return;
122
+
123
+ if (!this.options || !this.options.cursors) {
124
+ console.error('[AnimeCursor] missing cursors set up');
125
+ throw new Error('AnimeCursor init failed');
126
+ }
127
+
128
+ this.defaultCursorType = null;
129
+
130
+ for (const [name, cfg] of Object.entries(this.options.cursors)) {
131
+ if (cfg.default === true) {
132
+ if (this.defaultCursorType) {
133
+ throw new Error('[AnimeCursor] 只能有一个 default 光标');
134
+ }
135
+ this.defaultCursorType = name;
136
+ }
137
+ }
138
+
139
+ for (const [name, cfg] of Object.entries(this.options.cursors)) {
140
+ const required = ['size', 'image'];
141
+ required.forEach(key => {
142
+ if (cfg[key] === undefined) {
143
+ console.error(`[AnimeCursor] cursor "${name}" missing required setting: ${key}`);
144
+ throw new Error('AnimeCursor init failed');
145
+ }
146
+ });
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)) {
155
+ console.error(`[AnimeCursor] default cursor "${name}" 's tags must be an array if provided`);
156
+ throw new Error('AnimeCursor init failed');
157
+ }
158
+
159
+ if (cfg.duration !== undefined && typeof cfg.duration !== 'number') {
160
+ console.error(`[AnimeCursor] cursor "${name}" 's duration must be a number(seconds)`);
161
+ throw new Error('AnimeCursor init failed');
162
+ }
163
+ }
164
+ }
165
+
166
+ // ----------------------------
167
+ // 插入光标元素 HTML
168
+ // ----------------------------
169
+ _injectHTML() {
170
+ if (this.disabled) return;
171
+
172
+ const cursor = document.createElement('div');
173
+ cursor.id = 'anime-cursor';
174
+
175
+ // 如果debug选项存在,则添加debug子元素
176
+ if (this.options.debug) {
177
+ cursor.className = 'cursor-default cursor-debugmode';
178
+ const debuger = document.createElement('div');
179
+ debuger.className = 'anime-cursor-debug';
180
+ document.body.appendChild(debuger);
181
+ this.debugEl = debuger;
182
+ }
183
+ else {cursor.className = 'cursor-default';}
184
+ document.body.appendChild(cursor);
185
+ this.cursorEl = cursor;
186
+ }
187
+
188
+ // ----------------------------
189
+ // 插入样式 CSS
190
+ // ----------------------------
191
+ _injectCSS() {
192
+ if (this.disabled) return;
193
+
194
+ const style = document.createElement('style');
195
+ let css = '';
196
+
197
+ /* 通用样式 */
198
+ css += `
199
+ * {
200
+ cursor: none !important;
201
+ }
202
+ #anime-cursor {
203
+ position: fixed;
204
+ top: 0;
205
+ left: 0;
206
+ pointer-events: none;
207
+ background-repeat: no-repeat;
208
+ transform-origin: 0 0;
209
+ transform-style: preserve-3d;
210
+ z-index: ${this._getMaxZIndex()};
211
+ }
212
+ .cursor-debugmode {
213
+ border: 1px solid green;
214
+ }
215
+ .anime-cursor-debug {
216
+ position: fixed;
217
+ top: 0;
218
+ left: 0;
219
+ width: fit-content;
220
+ height: fit-content;
221
+ padding: 5px;
222
+ font-size: 16px;
223
+ text-wrap: nowrap;
224
+ color: red;
225
+ pointer-events: none;
226
+ overflow: visible;
227
+ z-index: 2147483647;
228
+ }
229
+ .anime-cursor-debug::before {
230
+ position: absolute;
231
+ content: "";
232
+ top: 0;
233
+ left: 0;
234
+ width: 100vw;
235
+ height: 1px;
236
+ background-color: red;
237
+ }
238
+ .anime-cursor-debug::after {
239
+ position: absolute;
240
+ content: "";
241
+ top: 0;
242
+ left: 0;
243
+ width: 1px;
244
+ height: 100vh;
245
+ background-color: red;
246
+ }
247
+ `;
248
+
249
+ /* 每种光标以及debug生成 CSS */
250
+ for (const [type, cfg] of Object.entries(this.options.cursors)) {
251
+ const className = `.cursor-${type}`;
252
+ const size = cfg.size;
253
+ const frames = cfg.frames;
254
+ const image = cfg.image;
255
+ const offset = cfg.offset;
256
+ const zIndex = cfg.zIndex;
257
+ const scale = cfg.scale;
258
+ const isGif = image.toLowerCase().endsWith('.gif');
259
+ var pixel;
260
+ if (cfg.pixel) {pixel = 'pixelated';}
261
+ else {pixel = 'auto';}
262
+
263
+ css += `
264
+ ${className} {
265
+ width: ${size[0]}px;
266
+ height: ${size[1]}px;
267
+ background-image: url("${image}");
268
+ image-rendering: ${pixel};
269
+ ${(scale || offset) ? `transform: ${[scale && `scale(${scale[0]}, ${scale[1]})`, offset && `translate(-${offset[0]}px, -${offset[1]}px)`].filter(Boolean).join(' ')};` : ''}
270
+
271
+ ${zIndex !== undefined ? `z-index:${zIndex};` : ''}
272
+ }`;
273
+
274
+ /* 精灵图动画 */
275
+ const duration = cfg.duration;
276
+ const hasAnimation =
277
+ !isGif &&
278
+ frames > 1 &&
279
+ typeof duration === 'number';
280
+
281
+ if (hasAnimation) {
282
+ const animName = `animecursor_${type}`;
283
+
284
+ css += `
285
+ ${className} {
286
+ animation: ${animName} steps(${frames}) ${duration}s infinite ${cfg.pingpong ? 'alternate' : ''};
287
+ }
288
+
289
+ @keyframes ${animName} {
290
+ from { background-position: 0 0; }
291
+ to { background-position: -${size[0] * frames}px 0; }
292
+ }
293
+ `;
294
+ }
295
+ }
296
+
297
+ style.textContent = css;
298
+ document.head.appendChild(style);
299
+ this.styleEl = style;
300
+ }
301
+
302
+ // ----------------------------
303
+ // 给元素自动添加 data-cursor
304
+ // ----------------------------
305
+ _bindElements(refresh) {
306
+ if (this.disabled) return;
307
+
308
+ for (const [type, cfg] of Object.entries(this.options.cursors)) {
309
+ if (!cfg.tags || cfg.tags.length === 0) continue;
310
+
311
+ cfg.tags.forEach(tag => {
312
+ const tagName = tag.toUpperCase();
313
+ document.querySelectorAll(tagName).forEach(el => {
314
+ if (!el.dataset.cursor) {
315
+ el.dataset.cursor = type;
316
+ el.dataset.cursorBound = 'true';
317
+ }
318
+ });
319
+ });
320
+ }
321
+ if (refresh) {
322
+ console.info('[AnimeCursor] refresh done!');
323
+ }
324
+ }
325
+
326
+ // ----------------------------
327
+ // 鼠标跟随 & 光标切换
328
+ // ----------------------------
329
+ _bindMouse() {
330
+ if (this.disabled) return;
331
+
332
+ this._onMouseMove = (e) => {
333
+ const x = e.clientX;
334
+ const y = e.clientY;
335
+
336
+ this.cursorEl.style.left = x + 'px';
337
+ this.cursorEl.style.top = y + 'px';
338
+
339
+ if (this.debugEl) {
340
+ this.debugEl.style.left = x + 'px';
341
+ this.debugEl.style.top = y + 'px';
342
+ }
343
+
344
+ let nextCursorType = null;
345
+
346
+ // 获取命中的元素
347
+ const target = document.elementFromPoint(x, y);
348
+
349
+ // 优先使用元素自身的 data-cursor
350
+ if (target && target.dataset && target.dataset.cursor) {
351
+ nextCursorType = target.dataset.cursor;
352
+ }
353
+ // 否则 尝试使用 default 光标
354
+ else if (this.defaultCursorType) {
355
+ nextCursorType = this.defaultCursorType;
356
+ }
357
+
358
+ // 如果两者都没有 - 保持当前状态
359
+ if (!nextCursorType) return;
360
+ if (this.debugEl) {this.debugEl.textContent = `(${x}px , ${y}px) ${nextCursorType}`;}
361
+
362
+ // 状态变化才切换 class
363
+ if (nextCursorType !== this.lastCursorType) {
364
+ if (this.debugEl) {this.cursorEl.className = `cursor-${nextCursorType}` + ' cursor-debugmode';}
365
+ else {this.cursorEl.className = `cursor-${nextCursorType}`;}
366
+ this.lastCursorType = nextCursorType;
367
+ }
368
+ };
369
+
370
+ document.addEventListener('mousemove', this._onMouseMove);
371
+ console.log('[AnimeCursor] AnimeCursor setted up.');
372
+ }
373
+
374
+ // ----------------------------
375
+ // 获取可用最大 z-index
376
+ // ----------------------------
377
+ _getMaxZIndex() {
378
+ if (this.disabled) return;
379
+
380
+ return 2147483646; // 浏览器安全最大值 2147483647 减一为留给debug覆盖
381
+ }
382
+ }
262
383
 
263
- return AnimeCursor;
384
+ return AnimeCursor;
264
385
 
265
386
  }));
@@ -1 +1 @@
1
- !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e="undefined"!=typeof globalThis?globalThis:e||self).AnimeCursor=n()}(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(){return window.matchMedia("(pointer: fine)").matches}destroy(){this.disabled}_validateOptions(){if(!this.options||!this.options.cursors)throw console.error("[AnimeCursor] 缺少 cursors 配置"),new Error("AnimeCursor init failed");for(const[e,n]of Object.entries(this.options.cursors)){if(["tags","size","image"].forEach(t=>{if(void 0===n[t])throw console.error(`[AnimeCursor] 光标 "${e}" 缺少必填项:${t}`),new Error("AnimeCursor init failed")}),!Array.isArray(n.tags))throw console.error(`[AnimeCursor] 光标 "${e}" tags 必须是数组`),new Error("AnimeCursor init failed");if(void 0!==n.duration&&"number"!=typeof n.duration)throw console.error(`[AnimeCursor] 光标 "${e}" duration 必须是数字(秒)`),new Error("AnimeCursor init failed")}}_injectHTML(){const e=document.createElement("div");if(e.id="anime-cursor",this.options.debug){e.className="cursor-default cursor-debugmode";const n=document.createElement("div");n.className="anime-cursor-debug",document.body.appendChild(n),this.debugEl=n}else e.className="cursor-default";document.body.appendChild(e),this.cursorEl=e}_injectCSS(){const e=document.createElement("style");let n="";n+=`\n* {\ncursor: none !important;\n}\n#anime-cursor {\nposition: fixed;\ntop: 0;\nleft: 0;\npointer-events: none;\nbackground-repeat: no-repeat;\ntransform-origin: 0 0;\ntransform-style: preserve-3d;\nz-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}`,s=o.size,i=o.frames,d=o.image,u=o.offset,a=o.zIndex,c=o.scale,l=d.toLowerCase().endsWith(".gif");var t;t=o.pixel?"pixelated":"auto",n+=`\n${r} {\nwidth: ${s[0]}px;\nheight: ${s[1]}px;\nbackground-image: url("${d}");\nimage-rendering: ${t};\n${c||u?`transform: ${[c&&`scale(${c[0]}, ${c[1]})`,u&&`translate(-${u[0]}px, -${u[1]}px)`].filter(Boolean).join(" ")};`:""}\n \n${void 0!==a?`z-index:${a};`:""}\n}`;const p=o.duration;if(!l&&i>1&&"number"==typeof p){const t=`animecursor_${e}`;n+=`\n${r} {\nanimation: ${t} steps(${i}) ${p}s infinite ${o.pingpong?"alternate":""};\n}\n\n@keyframes ${t} {\nfrom { background-position: 0 0; }\nto { background-position: -${s[0]*i}px 0; }\n}\n`}}e.textContent=n,document.head.appendChild(e)}_bindElements(){for(const[e,n]of Object.entries(this.options.cursors))n.tags.forEach(n=>{const t=n.toUpperCase();document.querySelectorAll(t).forEach(n=>{n.dataset.cursor||(n.dataset.cursor=e)})})}_bindMouse(){document.addEventListener("mousemove",e=>{const n=e.clientX,t=e.clientY;this.cursorEl.style.left=n+"px",this.cursorEl.style.top=t+"px",this.debugEl&&(this.debugEl.style.left=n+"px",this.debugEl.style.top=t+"px");const o=document.elementFromPoint(n,t);if(!o)return;const r=o.dataset.cursor||"default";this.debugEl&&(this.debugEl.textContent=`(${n}px , ${t}px) ${r}`),r!==this.lastCursorType&&(this.debugEl?this.cursorEl.className=`cursor-${r} cursor-debugmode`:this.cursorEl.className=`cursor-${r}`,this.lastCursorType=r)})}_getMaxZIndex(){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";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}}});
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "anime-cursor",
3
- "version": "0.1.2",
4
- "description": "A lightweight JS for website animated cursors",
3
+ "version": "0.2.0",
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",
7
7
  "files": [
@@ -16,6 +16,10 @@
16
16
  "scripts": {
17
17
  "build": "rollup -c"
18
18
  },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/shuninyu/anime-cursor.git"
22
+ },
19
23
  "author": "ShuninYu",
20
24
  "license": "MIT",
21
25
  "devDependencies": {