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 +30 -47
- package/dist/anime-cursor.esm.js +209 -88
- package/dist/anime-cursor.umd.js +380 -259
- package/dist/anime-cursor.umd.min.js +1 -1
- package/package.json +6 -2
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="
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
####
|
|
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="
|
|
164
|
+
<img src="https://cdn.jsdelivr.net/gh/shuninyu/anime-cursor@main/title.gif" width="384px" alt="AnimeCursor"/>
|
|
174
165
|
</div>
|
|
175
166
|
|
|
176
|
-
|
|
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
|
-
*
|
|
178
|
+
* 支持精灵图逐帧动画
|
|
185
179
|
* 支持 GIF(动态gif,而不是原生光标的静止gif)
|
|
186
180
|
* 自定义光标类型,由 AnimeCursor 自动切换
|
|
187
181
|
* 基于 CSS 的动画实现,高性能
|
|
188
|
-
*
|
|
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
|
-
//
|
|
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
|
-
*
|
|
275
|
+
* **对于任何精灵图动画光标,它的精灵图表都应该为单行横向布局,** AnimeCursor 会自动生成 PNG 精灵图动画。
|
|
292
276
|
例如,你为 `pointer` 光标设置的`size`(长,宽)为 `[64px , 64px]` ,帧数为 `3` ,那么你准备的精灵图表尺寸(长,宽)应该为: `[192px , 64px]` 。
|
|
293
277
|
|
|
294
|
-
* 对于帧数特别多的像素图,你可以使用原尺寸图片(无论是gif
|
|
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
|
-
####
|
|
298
|
+
#### 精灵图表动画光标
|
|
315
299
|
|
|
316
300
|
只有在 **同时满足以下条件** 时,才会生成动画:
|
|
317
301
|
|
|
318
|
-
* 图片为 PNG
|
|
319
302
|
* 设置了 `frames` 且 `frames > 1`
|
|
320
303
|
* 设置了 `duration`
|
|
321
304
|
|
package/dist/anime-cursor.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// AnimeCursor by github@ShuninYu
|
|
2
|
-
// v0.
|
|
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
|
-
|
|
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]
|
|
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 = ['
|
|
134
|
+
const required = ['size', 'image'];
|
|
53
135
|
required.forEach(key => {
|
|
54
136
|
if (cfg[key] === undefined) {
|
|
55
|
-
console.error(`[AnimeCursor]
|
|
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 (!
|
|
61
|
-
|
|
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]
|
|
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
|
-
|
|
116
|
-
}
|
|
117
|
-
.anime-cursor-debug {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
.anime-cursor-debug::before {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
.anime-cursor-debug::after {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
/*
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
if (!target) return;
|
|
338
|
+
let nextCursorType = null;
|
|
237
339
|
|
|
238
|
-
|
|
239
|
-
|
|
340
|
+
// 获取命中的元素
|
|
341
|
+
const target = document.elementFromPoint(x, y);
|
|
240
342
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
}
|
package/dist/anime-cursor.umd.js
CHANGED
|
@@ -1,265 +1,386 @@
|
|
|
1
1
|
(function (global, factory) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
384
|
+
return AnimeCursor;
|
|
264
385
|
|
|
265
386
|
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(e,
|
|
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.
|
|
4
|
-
"description": "A lightweight
|
|
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": {
|