@cut-shade/baspark 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,172 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>BASpark Demo</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
10
+
11
+ #app {
12
+ position: relative;
13
+ width: 100vw;
14
+ height: 100vh;
15
+ overflow: hidden;
16
+ background: #1a1a2e;
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ user-select: none;
21
+ }
22
+
23
+ .content {
24
+ text-align: center;
25
+ color: #fff;
26
+ z-index: 1;
27
+ pointer-events: none;
28
+ }
29
+ .content h1 {
30
+ font-size: clamp(2rem, 6vw, 4rem);
31
+ font-weight: 700;
32
+ background: linear-gradient(135deg, #2dd4ff, #45afff);
33
+ -webkit-background-clip: text;
34
+ -webkit-text-fill-color: transparent;
35
+ margin-bottom: 0.5rem;
36
+ }
37
+ .content p {
38
+ font-size: clamp(0.9rem, 2vw, 1.2rem);
39
+ color: rgba(255,255,255,0.6);
40
+ }
41
+
42
+ .controls {
43
+ position: fixed;
44
+ bottom: 24px;
45
+ left: 50%;
46
+ transform: translateX(-50%);
47
+ display: flex;
48
+ gap: 12px;
49
+ align-items: center;
50
+ flex-wrap: wrap;
51
+ justify-content: center;
52
+ background: rgba(0,0,0,0.6);
53
+ backdrop-filter: blur(12px);
54
+ border: 1px solid rgba(255,255,255,0.1);
55
+ border-radius: 16px;
56
+ padding: 16px 24px;
57
+ z-index: 2;
58
+ pointer-events: auto;
59
+ max-width: 90vw;
60
+ }
61
+ .controls label {
62
+ color: rgba(255,255,255,0.8);
63
+ font-size: 13px;
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 6px;
67
+ }
68
+ .controls input[type="color"] {
69
+ width: 32px;
70
+ height: 32px;
71
+ border: none;
72
+ border-radius: 6px;
73
+ cursor: pointer;
74
+ background: none;
75
+ padding: 0;
76
+ }
77
+ .controls input[type="range"] {
78
+ width: 80px;
79
+ accent-color: #45afff;
80
+ }
81
+ .controls button {
82
+ background: rgba(69,175,255,0.2);
83
+ border: 1px solid rgba(69,175,255,0.4);
84
+ color: #fff;
85
+ padding: 6px 14px;
86
+ border-radius: 8px;
87
+ cursor: pointer;
88
+ font-size: 13px;
89
+ transition: background 0.2s;
90
+ }
91
+ .controls button:hover {
92
+ background: rgba(69,175,255,0.35);
93
+ }
94
+ .badge {
95
+ position: fixed;
96
+ top: 16px;
97
+ right: 16px;
98
+ background: rgba(0,0,0,0.5);
99
+ backdrop-filter: blur(8px);
100
+ border: 1px solid rgba(255,255,255,0.08);
101
+ border-radius: 10px;
102
+ padding: 8px 14px;
103
+ color: rgba(255,255,255,0.5);
104
+ font-size: 12px;
105
+ z-index: 2;
106
+ pointer-events: none;
107
+ }
108
+ </style>
109
+ </head>
110
+ <body>
111
+ <div id="app">
112
+ <div class="content">
113
+ <h1>✦ BASpark</h1>
114
+ <p>Blue Archive 风格粒子特效 · 点击/移动鼠标试试</p>
115
+ </div>
116
+ <div class="badge">v1.6.0 · 移植 Web 版</div>
117
+ <div class="controls">
118
+ <label>颜色 <input type="color" id="colorPicker" value="#2dafff"></label>
119
+ <label>整体 <input type="range" id="scaleSlider" min="0.5" max="3" step="0.1" value="1.5"></label>
120
+ <label>不透明度 <input type="range" id="opacitySlider" min="0.1" max="1" step="0.1" value="1"></label>
121
+ <label>速度 <input type="range" id="speedSlider" min="0.2" max="3" step="0.1" value="1"></label>
122
+ <label>拖尾 <input type="range" id="trailWidthSlider" min="0.25" max="3" step="0.1" value="1"></label>
123
+ <label>迸溅 <input type="range" id="sparkSizeSlider" min="0.25" max="3" step="0.1" value="1"></label>
124
+ <label>波纹 <input type="range" id="clickScaleSlider" min="0.25" max="3" step="0.1" value="1"></label>
125
+ <button id="trailToggle">常驻拖尾: 关</button>
126
+ <button id="boomBtn">✨ 点我</button>
127
+ </div>
128
+ </div>
129
+
130
+ <script src="../dist/index.global.js"></script>
131
+ <script>
132
+ const spark = new BASpark.BASpark('#app', { autoTrack: true, color: '45,175,255' })
133
+
134
+ // ---- 控件绑定 ----
135
+ const colorPicker = document.getElementById('colorPicker')
136
+ const scaleSlider = document.getElementById('scaleSlider')
137
+ const opacitySlider = document.getElementById('opacitySlider')
138
+ const speedSlider = document.getElementById('speedSlider')
139
+ const trailWidthSlider = document.getElementById('trailWidthSlider')
140
+ const sparkSizeSlider = document.getElementById('sparkSizeSlider')
141
+ const clickScaleSlider = document.getElementById('clickScaleSlider')
142
+ const trailBtn = document.getElementById('trailToggle')
143
+ const boomBtn = document.getElementById('boomBtn')
144
+
145
+ colorPicker.addEventListener('input', () => {
146
+ const hex = colorPicker.value
147
+ const rgb = [parseInt(hex.slice(1,3),16), parseInt(hex.slice(3,5),16), parseInt(hex.slice(5,7),16)]
148
+ spark.setColor(rgb.join(','))
149
+ })
150
+
151
+ const val = (el) => parseFloat(el.value)
152
+ scaleSlider.addEventListener('input', () => spark.setScale(val(scaleSlider)))
153
+ opacitySlider.addEventListener('input', () => spark.setOpacity(val(opacitySlider)))
154
+ speedSlider.addEventListener('input', () => spark.setSpeed(val(speedSlider)))
155
+ trailWidthSlider.addEventListener('input', () => spark.setTrailWidth(val(trailWidthSlider)))
156
+ sparkSizeSlider.addEventListener('input', () => spark.setSparkSize(val(sparkSizeSlider)))
157
+ clickScaleSlider.addEventListener('input', () => spark.setClickScale(val(clickScaleSlider)))
158
+
159
+ let alwaysTrail = false
160
+ trailBtn.addEventListener('click', () => {
161
+ alwaysTrail = !alwaysTrail
162
+ spark.setAlwaysTrail(alwaysTrail)
163
+ trailBtn.textContent = '常驻拖尾: ' + (alwaysTrail ? '开' : '关')
164
+ })
165
+
166
+ boomBtn.addEventListener('click', () => {
167
+ // 随机位置触发点击效果 (百分比坐标)
168
+ spark.boom(Math.random() * 0.6 + 0.2, Math.random() * 0.6 + 0.2)
169
+ })
170
+ </script>
171
+ </body>
172
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cut-shade/baspark",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Blue Archive style particle effects — portable web library (Canvas)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -16,7 +16,11 @@
16
16
  "unpkg": "./dist/index.global.js",
17
17
  "jsdelivr": "./dist/index.global.js",
18
18
  "files": [
19
- "dist"
19
+ "dist",
20
+ "src",
21
+ "demo",
22
+ "tsconfig.json",
23
+ "tsup.config.ts"
20
24
  ],
21
25
  "scripts": {
22
26
  "build": "tsup",
package/src/BASpark.ts ADDED
@@ -0,0 +1,194 @@
1
+ import { MouseSpark } from './core/MouseSpark'
2
+ import type { BASparkOptions } from './core/types'
3
+ import { DEFAULT_Z_INDEX } from './core/constants'
4
+
5
+ export class BASpark {
6
+ private canvas: HTMLCanvasElement
7
+ private spark: MouseSpark
8
+ private autoTrack: boolean
9
+ private boundHandlers: {
10
+ mouseDown: (e: MouseEvent) => void
11
+ mouseMove: (e: MouseEvent) => void
12
+ mouseUp: (e: MouseEvent) => void
13
+ touchStart: (e: TouchEvent) => void
14
+ touchMove: (e: TouchEvent) => void
15
+ touchEnd: (e: TouchEvent) => void
16
+ resize: () => void
17
+ }
18
+
19
+ constructor(element: HTMLElement | string, options: BASparkOptions = {}) {
20
+ this.autoTrack = options.autoTrack ?? true
21
+
22
+ const target = typeof element === 'string'
23
+ ? document.querySelector<HTMLElement>(element)
24
+ : element
25
+ if (!target) throw new Error('BASpark: target element not found')
26
+
27
+ this.canvas = document.createElement('canvas')
28
+ this.canvas.id = 'baspark-canvas'
29
+ this.canvas.style.cssText = `
30
+ position: fixed;
31
+ left: 0;
32
+ top: 0;
33
+ width: 100%;
34
+ height: 100%;
35
+ pointer-events: none;
36
+ z-index: ${options.zIndex ?? DEFAULT_Z_INDEX};
37
+ `
38
+ target.appendChild(this.canvas)
39
+
40
+ this.spark = new MouseSpark(this.canvas, {
41
+ color: options.color,
42
+ scale: options.scale,
43
+ opacity: options.opacity,
44
+ trailSpeed: options.trailSpeed,
45
+ clickSpeed: options.clickSpeed,
46
+ trailWidth: options.trailWidth,
47
+ sparkSize: options.sparkSize,
48
+ clickScale: options.clickScale,
49
+ alwaysTrail: options.alwaysTrail,
50
+ })
51
+
52
+ this.boundHandlers = {
53
+ mouseDown: (e: MouseEvent) => {
54
+ this.spark.handleDown(e.clientX, e.clientY)
55
+ },
56
+ mouseMove: (e: MouseEvent) => {
57
+ this.spark.handleMove(e.clientX, e.clientY)
58
+ },
59
+ mouseUp: () => {
60
+ this.spark.handleUp()
61
+ },
62
+ touchStart: (e: TouchEvent) => {
63
+ const t = e.touches[0]
64
+ this.spark.setInputContext('touch', false)
65
+ this.spark.handleDown(t.clientX, t.clientY)
66
+ },
67
+ touchMove: (e: TouchEvent) => {
68
+ const t = e.touches[0]
69
+ this.spark.handleMove(t.clientX, t.clientY)
70
+ },
71
+ touchEnd: () => {
72
+ this.spark.setInputContext('mouse', false)
73
+ this.spark.handleUp()
74
+ },
75
+ resize: () => {
76
+ this.spark.resize()
77
+ },
78
+ }
79
+
80
+ if (this.autoTrack) {
81
+ this.bindEvents()
82
+ }
83
+ }
84
+
85
+ private bindEvents(): void {
86
+ window.addEventListener('mousedown', this.boundHandlers.mouseDown)
87
+ window.addEventListener('mousemove', this.boundHandlers.mouseMove)
88
+ window.addEventListener('mouseup', this.boundHandlers.mouseUp)
89
+ window.addEventListener('touchstart', this.boundHandlers.touchStart, { passive: true })
90
+ window.addEventListener('touchmove', this.boundHandlers.touchMove, { passive: true })
91
+ window.addEventListener('touchend', this.boundHandlers.touchEnd)
92
+ window.addEventListener('resize', this.boundHandlers.resize)
93
+ }
94
+
95
+ private unbindEvents(): void {
96
+ window.removeEventListener('mousedown', this.boundHandlers.mouseDown)
97
+ window.removeEventListener('mousemove', this.boundHandlers.mouseMove)
98
+ window.removeEventListener('mouseup', this.boundHandlers.mouseUp)
99
+ window.removeEventListener('touchstart', this.boundHandlers.touchStart)
100
+ window.removeEventListener('touchmove', this.boundHandlers.touchMove)
101
+ window.removeEventListener('touchend', this.boundHandlers.touchEnd)
102
+ window.removeEventListener('resize', this.boundHandlers.resize)
103
+ }
104
+
105
+ boom(xPercent: number, yPercent: number): void {
106
+ const cx = xPercent * window.innerWidth
107
+ const cy = yPercent * window.innerHeight
108
+ this.spark.handleDown(cx, cy)
109
+ }
110
+
111
+ move(xPercent: number, yPercent: number): void {
112
+ const cx = xPercent * window.innerWidth
113
+ const cy = yPercent * window.innerHeight
114
+ this.spark.handleMove(cx, cy)
115
+ }
116
+
117
+ up(): void {
118
+ this.spark.handleUp()
119
+ }
120
+
121
+ boomAt(x: number, y: number): void {
122
+ this.spark.handleDown(x, y)
123
+ }
124
+
125
+ moveAt(x: number, y: number): void {
126
+ this.spark.handleMove(x, y)
127
+ }
128
+
129
+ setColor(rgb: string): void {
130
+ this.spark.updateColor(rgb)
131
+ }
132
+
133
+ setScale(scale: number): void {
134
+ const s = this.spark
135
+ s.updateEffectSettings(scale, s.opacity, s.trailSpeed, s.clickSpeed)
136
+ }
137
+
138
+ setOpacity(opacity: number): void {
139
+ const s = this.spark
140
+ s.updateEffectSettings(s.scale, opacity, s.trailSpeed, s.clickSpeed)
141
+ }
142
+
143
+ setSpeed(trailSpeed: number, clickSpeed?: number): void {
144
+ this.spark.updateEffectSettings(
145
+ this.spark.scale,
146
+ this.spark.opacity,
147
+ trailSpeed,
148
+ clickSpeed ?? trailSpeed,
149
+ )
150
+ }
151
+
152
+ setAlwaysTrail(enabled: boolean): void {
153
+ this.spark.setInputContext('mouse', enabled)
154
+ }
155
+
156
+ setTrailWidth(v: number): void {
157
+ this.spark.setTrailWidth(v)
158
+ }
159
+
160
+ setSparkSize(v: number): void {
161
+ this.spark.setSparkSize(v)
162
+ }
163
+
164
+ setClickScale(v: number): void {
165
+ this.spark.setClickScale(v)
166
+ }
167
+
168
+ updateOptions(options: BASparkOptions): void {
169
+ if (options.color !== undefined) this.setColor(options.color)
170
+ if (options.scale !== undefined) this.setScale(options.scale)
171
+ if (options.opacity !== undefined) this.setOpacity(options.opacity)
172
+ if (options.trailSpeed !== undefined) {
173
+ this.setSpeed(options.trailSpeed, options.clickSpeed)
174
+ }
175
+ if (options.trailWidth !== undefined) this.setTrailWidth(options.trailWidth)
176
+ if (options.sparkSize !== undefined) this.setSparkSize(options.sparkSize)
177
+ if (options.clickScale !== undefined) this.setClickScale(options.clickScale)
178
+ if (options.alwaysTrail !== undefined) this.setAlwaysTrail(options.alwaysTrail)
179
+ if (options.autoTrack !== undefined && options.autoTrack !== this.autoTrack) {
180
+ this.autoTrack = options.autoTrack
181
+ if (this.autoTrack) {
182
+ this.bindEvents()
183
+ } else {
184
+ this.unbindEvents()
185
+ }
186
+ }
187
+ }
188
+
189
+ destroy(): void {
190
+ this.unbindEvents()
191
+ this.spark.destroy()
192
+ this.canvas.remove()
193
+ }
194
+ }
@@ -0,0 +1,715 @@
1
+ import type { Spark, Wave, TrailPoint, Point, Rect, InputMode } from './types'
2
+ import {
3
+ FILLED_CIRCLE_CFG,
4
+ RINGS_ANIM_CFG,
5
+ CREATE_CLICK_CFG,
6
+ DEFAULT_COLOR,
7
+ DEFAULT_SCALE,
8
+ DEFAULT_OPACITY,
9
+ DEFAULT_SPEED,
10
+ BASE_FRAME_MS,
11
+ MAX_DELTA_MS,
12
+ } from './constants'
13
+
14
+ function ringsEndColorFromRgb(rgbString: string): [number, number, number] {
15
+ const [r, g, b] = rgbString.split(',').map(Number)
16
+ return [
17
+ Math.round((r + 255 * 2) / 3),
18
+ Math.round((g + 255 * 2) / 3),
19
+ Math.round((b + 255 * 2) / 3),
20
+ ]
21
+ }
22
+
23
+ function dist(a: Point, b: Point): number {
24
+ return Math.hypot(a.x - b.x, a.y - b.y)
25
+ }
26
+
27
+ export interface MouseSparkOptions {
28
+ color?: string
29
+ scale?: number
30
+ opacity?: number
31
+ trailSpeed?: number
32
+ clickSpeed?: number
33
+ trailWidth?: number
34
+ sparkSize?: number
35
+ clickScale?: number
36
+ maxTrail?: number
37
+ alwaysTrail?: boolean
38
+ }
39
+
40
+ export class MouseSpark {
41
+ color: string
42
+ scale: number
43
+ opacity: number
44
+ trailSpeed: number
45
+ clickSpeed: number
46
+ trailWidth: number
47
+ sparkSize: number
48
+ clickScale: number
49
+ maxTrail: number
50
+
51
+ private sparksPool: Spark[] = []
52
+ private wavesPool: Wave[] = []
53
+ private waves: Wave[] = []
54
+ private sparks: Spark[] = []
55
+ private trail: TrailPoint[] = []
56
+ isDown = false
57
+ private lastPos: Point | null = null
58
+ private lastFrameTime = performance.now()
59
+ private dpr = 1
60
+ private cssWidth = 1
61
+ private cssHeight = 1
62
+ private previousDirtyRects: Rect[] = []
63
+ private forceFullRedraw = true
64
+
65
+ private ringsStartColor: [number, number, number] = [250, 252, 252]
66
+ private ringsEndColor: [number, number, number]
67
+
68
+ private mainCanvas!: HTMLCanvasElement
69
+ private mainCtx!: CanvasRenderingContext2D
70
+ private bufferCanvas!: HTMLCanvasElement
71
+ private bufferCtx!: CanvasRenderingContext2D
72
+
73
+ private animFrameId = 0
74
+ private inputMode: InputMode = 'mouse'
75
+ private alwaysTrailEnabled = false
76
+ readonly effectiveAlwaysTrail: boolean = false
77
+
78
+ constructor(canvas: HTMLCanvasElement, opts: MouseSparkOptions = {}) {
79
+ this.color = opts.color ?? DEFAULT_COLOR
80
+ this.scale = opts.scale ?? DEFAULT_SCALE
81
+ this.opacity = opts.opacity ?? DEFAULT_OPACITY
82
+ this.trailSpeed = opts.trailSpeed ?? DEFAULT_SPEED
83
+ this.clickSpeed = opts.clickSpeed ?? DEFAULT_SPEED
84
+ this.trailWidth = opts.trailWidth ?? 1
85
+ this.sparkSize = opts.sparkSize ?? 1
86
+ this.clickScale = opts.clickScale ?? 1
87
+ this.maxTrail = opts.maxTrail ?? 16
88
+ this.alwaysTrailEnabled = opts.alwaysTrail ?? false
89
+
90
+ this.ringsEndColor = ringsEndColorFromRgb(this.color)
91
+
92
+ this.initCanvas(canvas)
93
+ this.animFrameId = requestAnimationFrame((now) => this.animationLoops(now))
94
+ }
95
+
96
+ private initCanvas(canvas: HTMLCanvasElement): void {
97
+ this.mainCanvas = canvas
98
+ this.mainCtx = canvas.getContext('2d')!
99
+
100
+ this.bufferCanvas = document.createElement('canvas')
101
+ this.bufferCtx = this.bufferCanvas.getContext('2d')!
102
+
103
+ this.resize()
104
+ }
105
+
106
+ resize(): void {
107
+ const dpr = window.devicePixelRatio || 1
108
+ const cssWidth = Math.max(1, window.innerWidth)
109
+ const cssHeight = Math.max(1, window.innerHeight)
110
+ const w = Math.max(1, Math.floor(cssWidth * dpr))
111
+ const h = Math.max(1, Math.floor(cssHeight * dpr))
112
+
113
+ this.dpr = dpr
114
+ this.cssWidth = cssWidth
115
+ this.cssHeight = cssHeight
116
+ this.mainCanvas.width = w
117
+ this.mainCanvas.height = h
118
+ this.bufferCanvas.width = w
119
+ this.bufferCanvas.height = h
120
+ this.previousDirtyRects = []
121
+ this.forceFullRedraw = true
122
+
123
+ this.bufferCtx.setTransform(dpr, 0, 0, dpr, 0, 0)
124
+ }
125
+
126
+ setInputContext(mode: InputMode, alwaysTrail: boolean): void {
127
+ this.inputMode = mode === 'touch' ? 'touch' : 'mouse'
128
+ this.alwaysTrailEnabled = Boolean(alwaysTrail)
129
+ ;(this as any).effectiveAlwaysTrail = this.inputMode === 'mouse' && this.alwaysTrailEnabled
130
+ }
131
+
132
+ updateColor(rgbString: string): void {
133
+ this.color = rgbString
134
+ this.ringsEndColor = ringsEndColorFromRgb(rgbString)
135
+ }
136
+
137
+ setTrailWidth(v: number): void {
138
+ this.trailWidth = Math.max(0.25, Math.min(4, Number(v) || 1))
139
+ }
140
+
141
+ setSparkSize(v: number): void {
142
+ this.sparkSize = Math.max(0.25, Math.min(4, Number(v) || 1))
143
+ }
144
+
145
+ setClickScale(v: number): void {
146
+ this.clickScale = Math.max(0.25, Math.min(4, Number(v) || 1))
147
+ }
148
+
149
+ updateEffectSettings(scale: number, opacity: number, trailSpeed: number, clickSpeed: number): void {
150
+ this.scale = Math.max(0.5, Math.min(3, Number(scale) || DEFAULT_SCALE))
151
+ this.opacity = Math.max(0.1, Math.min(1, Number(opacity) || DEFAULT_OPACITY))
152
+ let t = Number(trailSpeed)
153
+ if (!Number.isFinite(t)) t = DEFAULT_SPEED
154
+ let c = Number(clickSpeed)
155
+ if (!Number.isFinite(c)) c = t
156
+ this.trailSpeed = Math.max(0.2, Math.min(3, t))
157
+ this.clickSpeed = Math.max(0.2, Math.min(3, c))
158
+ }
159
+
160
+ handleDown(x: number, y: number): void {
161
+ this.isDown = true
162
+ this.lastPos = { x, y }
163
+ this.createEffects(x, y)
164
+ }
165
+
166
+ handleMove(x: number, y: number): void {
167
+ if (!this.isDown && !(this.inputMode === 'mouse' && this.alwaysTrailEnabled)) return
168
+ const prev = this.lastPos
169
+ if (!prev) {
170
+ this.lastPos = { x, y }
171
+ return
172
+ }
173
+ if (dist({ x, y }, prev) > 2) {
174
+ this.trail.push({ x, y, life: 1 })
175
+ if (this.trail.length > this.maxTrail) this.trail.shift()
176
+
177
+ if (Math.random() < 0.3) {
178
+ const a = Math.random() * Math.PI * 2
179
+ const speedAdjust = this.scale / 1.5
180
+ this.sparks.push({
181
+ x: x + Math.cos(a) * 10 * this.scale,
182
+ y: y + Math.sin(a) * 10 * this.scale,
183
+ vx: Math.cos(a) * 1.3 * speedAdjust,
184
+ vy: Math.sin(a) * 1.3 * speedAdjust,
185
+ rot: Math.random() * Math.PI * 2,
186
+ rs: 0.16,
187
+ s: 9 * this.scale * this.sparkSize,
188
+ a: 0.7,
189
+ f: 0.95,
190
+ fromClick: false,
191
+ })
192
+ }
193
+ }
194
+ this.lastPos = { x, y }
195
+ }
196
+
197
+ handleUp(): void {
198
+ this.isDown = false
199
+ }
200
+
201
+ private alpha(value: number): number {
202
+ return Math.max(0, Math.min(1, value * this.opacity))
203
+ }
204
+
205
+ private createEffects(x: number, y: number): void {
206
+ const rc = CREATE_CLICK_CFG.rings
207
+ const sparksCount = CREATE_CLICK_CFG.sparksCount
208
+
209
+ let wave: Wave
210
+ if (this.wavesPool.length > 0) {
211
+ wave = this.wavesPool.pop()!
212
+ } else {
213
+ wave = { ring: { segs: [] } } as unknown as Wave
214
+ }
215
+ if (!wave.ring) wave.ring = { segs: [] } as any
216
+
217
+ wave.x = x
218
+ wave.y = y
219
+ wave.r = 0
220
+ wave.life = 0
221
+ wave.ring.ang = Math.random() * Math.PI * 2
222
+ wave.ring.rs = rc.rsList[Math.floor(Math.random() * rc.rsList.length)]
223
+ wave.ring.segs[0] = {
224
+ off: 0,
225
+ len: rc.len,
226
+ rRoundRate: rc.rRoundRateList[Math.floor(Math.random() * rc.rRoundRateList.length)],
227
+ }
228
+ wave.ring.segs[1] = {
229
+ off: (Math.random() * 3 - 1.5) * Math.PI,
230
+ len: rc.len,
231
+ rRoundRate: rc.rRoundRateList[Math.floor(Math.random() * rc.rRoundRateList.length)],
232
+ }
233
+
234
+ this.waves.push(wave)
235
+
236
+ const speedAdjust = this.scale / 1.5
237
+ for (let i = 0; i < sparksCount; i++) {
238
+ const a = Math.random() * Math.PI * 2
239
+ const speed = (4.8 + Math.random() * 2) * speedAdjust
240
+
241
+ let spark: Spark
242
+ if (this.sparksPool.length > 0) {
243
+ spark = this.sparksPool.pop()!
244
+ } else {
245
+ spark = {} as Spark
246
+ }
247
+
248
+ spark.x = x
249
+ spark.y = y
250
+ spark.vx = Math.cos(a) * speed
251
+ spark.vy = Math.sin(a) * speed
252
+ spark.rot = Math.random() * Math.PI * 2
253
+ spark.rs = (Math.random() - 0.5) * 0.28
254
+ spark.s = (4 + Math.random() * 3) * this.scale * this.sparkSize
255
+ spark.a = 1
256
+ spark.f = 0.9
257
+ spark.fromClick = true
258
+ this.sparks.push(spark)
259
+ }
260
+ }
261
+
262
+ private clearBuffer(rect?: Rect): void {
263
+ const ctx = this.bufferCtx
264
+ if (rect) {
265
+ ctx.clearRect(rect.x, rect.y, rect.w, rect.h)
266
+ } else {
267
+ ctx.clearRect(0, 0, this.cssWidth, this.cssHeight)
268
+ }
269
+ }
270
+
271
+ private clearBufferRects(rects: Rect[]): void {
272
+ if (!rects || rects.length === 0) return
273
+ for (const rect of rects) {
274
+ this.clearBuffer(rect)
275
+ }
276
+ }
277
+
278
+ private updateTrail(frameScale: number): void {
279
+ const ctx = this.bufferCtx
280
+ const n = this.trail.length
281
+ let baseDecay: number
282
+ if (this.inputMode === 'mouse' && this.alwaysTrailEnabled) {
283
+ baseDecay = 0.085 * frameScale
284
+ } else {
285
+ baseDecay = (this.isDown ? 0.085 : 0.18) * frameScale
286
+ }
287
+ const maxStep = 0.42
288
+ for (let i = n - 1; i >= 0; i--) {
289
+ const t = this.trail[i]
290
+ const span = Math.max(1, n - 1)
291
+ const along = n > 1 ? i / span : 1
292
+ const towardCursorBias = 1.25 - 0.55 * along
293
+ let step = baseDecay * towardCursorBias
294
+ if (step > maxStep) step = maxStep
295
+ t.life -= step
296
+ if (t.life <= 0) this.trail.splice(i, 1)
297
+ }
298
+
299
+ const head = this.lastPos
300
+ const pts: TrailPoint[] =
301
+ head && this.trail.length > 0
302
+ ? this.trail.concat([{ x: head.x, y: head.y, life: 1 }])
303
+ : this.trail.slice()
304
+
305
+ if (pts.length < 2) return
306
+
307
+ const gap = Math.hypot(
308
+ pts[pts.length - 1].x - pts[pts.length - 2].x,
309
+ pts[pts.length - 1].y - pts[pts.length - 2].y,
310
+ )
311
+ if (gap < 0.75 && this.trail.length === 1) {
312
+ const fade = Math.max(0, this.trail[0].life)
313
+ ctx.shadowColor = 'transparent'
314
+ const dotSize = (2.5 + 2 * fade) * (this.scale / 1.5) * this.trailWidth
315
+ ctx.beginPath()
316
+ ctx.arc(pts[0].x, pts[0].y, dotSize, 0, Math.PI * 2)
317
+ ctx.fillStyle = `rgba(${this.color}, ${fade * 0.85})`
318
+ ctx.fill()
319
+ return
320
+ }
321
+
322
+ const numPts = pts.length
323
+ const lastIdx = numPts - 1
324
+ const baseWidth = 8 * (this.scale / 1.5) * this.trailWidth
325
+
326
+ const lancetWidth = (progress: number): number => {
327
+ if (progress < 0.65) return Math.pow(progress / 0.65, 0.6)
328
+ return Math.pow((1 - progress) / 0.35, 1.8)
329
+ }
330
+
331
+ // 构建填充多边形的左右边缘点
332
+ const leftEdge: Array<{ x: number; y: number }> = []
333
+ const rightEdge: Array<{ x: number; y: number }> = []
334
+
335
+ for (let i = 0; i < numPts; i++) {
336
+ const progress = i / lastIdx
337
+ const hw = Math.max(0.25, baseWidth * lancetWidth(progress)) / 2
338
+
339
+ let dx: number, dy: number
340
+ if (i === 0) {
341
+ dx = pts[1].x - pts[0].x
342
+ dy = pts[1].y - pts[0].y
343
+ } else if (i === lastIdx) {
344
+ dx = pts[i].x - pts[i - 1].x
345
+ dy = pts[i].y - pts[i - 1].y
346
+ } else {
347
+ dx = pts[i + 1].x - pts[i - 1].x
348
+ dy = pts[i + 1].y - pts[i - 1].y
349
+ }
350
+ let len = Math.hypot(dx, dy)
351
+ if (len < 0.001) {
352
+ dx = 0
353
+ dy = 1
354
+ len = 1
355
+ }
356
+ const nx = -dy / len
357
+ const ny = dx / len
358
+
359
+ leftEdge.push({ x: pts[i].x + nx * hw, y: pts[i].y + ny * hw })
360
+ rightEdge.push({ x: pts[i].x - nx * hw, y: pts[i].y - ny * hw })
361
+ }
362
+
363
+ // 绘制填充多边形
364
+ ctx.shadowColor = `rgba(${this.color}, 0.6)`
365
+ ctx.shadowBlur = 3
366
+ ctx.shadowOffsetX = 0
367
+ ctx.shadowOffsetY = 0
368
+
369
+ ctx.beginPath()
370
+ ctx.moveTo(leftEdge[0].x, leftEdge[0].y)
371
+ for (let i = 1; i < numPts; i++) {
372
+ ctx.lineTo(leftEdge[i].x, leftEdge[i].y)
373
+ }
374
+ for (let i = numPts - 1; i >= 0; i--) {
375
+ ctx.lineTo(rightEdge[i].x, rightEdge[i].y)
376
+ }
377
+ ctx.closePath()
378
+
379
+ const grad = ctx.createLinearGradient(
380
+ pts[0].x, pts[0].y, pts[lastIdx].x, pts[lastIdx].y,
381
+ )
382
+ grad.addColorStop(0, `rgba(${this.color}, 0)`)
383
+ grad.addColorStop(1, `rgba(${this.color}, 1)`)
384
+
385
+ ctx.fillStyle = grad
386
+ ctx.fill()
387
+
388
+ ctx.shadowColor = 'transparent'
389
+ }
390
+
391
+ private strokeRingSegment(
392
+ wx: number, wy: number, radius: number,
393
+ a0: number, a1: number, lineWidth: number, strokeStyle: string,
394
+ ): void {
395
+ const ctx = this.bufferCtx
396
+ ctx.beginPath()
397
+ ctx.arc(wx, wy, radius, a0, a1)
398
+ ctx.lineWidth = lineWidth
399
+ ctx.strokeStyle = strokeStyle
400
+ ctx.stroke()
401
+ }
402
+
403
+ private updateWaves(clickFrameScale: number): void {
404
+ const filled = FILLED_CIRCLE_CFG
405
+ const rings = RINGS_ANIM_CFG
406
+ const ctx = this.bufferCtx
407
+
408
+ for (let i = this.waves.length - 1; i >= 0; i--) {
409
+ const w = this.waves[i]
410
+ const waveProg = Math.min(w.life / filled.maxLife, 1)
411
+ const ringProg = Math.min(w.life / rings.maxLife, 1)
412
+
413
+ // filled circle
414
+ {
415
+ w.life += clickFrameScale
416
+ const ease = 1 - Math.pow(1 - waveProg, 3)
417
+ w.r = filled.rAddRate * this.scale * this.clickScale * ease
418
+ const alpha = Math.max(0, 1 - waveProg)
419
+ if (alpha > 0) {
420
+ ctx.beginPath()
421
+ ctx.arc(w.x, w.y, w.r, 0, Math.PI * 2)
422
+ ctx.fillStyle = `rgba(${this.color},${this.alpha(alpha)})`
423
+ ctx.fill()
424
+ }
425
+ }
426
+
427
+ // rings
428
+ {
429
+ const getWeightProp = (t: number) => Math.min(2 - Math.abs(4 * (t - 0.5)), 1)
430
+
431
+ const getAlpha = (rProg: number) => Math.min(1.1 - 0.3 * rProg, 1)
432
+
433
+ const r = w.ring
434
+ r.ang -= r.rs * clickFrameScale
435
+
436
+ const ringRgbAt = (rProg: number): [number, number, number] => {
437
+ const t = Math.min(1.2 * rProg, 1)
438
+ return [
439
+ Math.round(this.ringsStartColor[0] * (1 - t) + this.ringsEndColor[0] * t),
440
+ Math.round(this.ringsStartColor[1] * (1 - t) + this.ringsEndColor[1] * t),
441
+ Math.round(this.ringsStartColor[2] * (1 - t) + this.ringsEndColor[2] * t),
442
+ ]
443
+ }
444
+
445
+ let start = 0
446
+ let end = 0
447
+ let len = 0
448
+ let seg: { off: number; len: number; rRoundRate: number }
449
+
450
+ for (let j = 0; j < 2; j++) {
451
+ seg = r.segs[j]
452
+ const base = r.ang + seg.off
453
+
454
+ if (ringProg <= rings.lenStopAddPoint) {
455
+ len = seg.len * (ringProg / rings.lenStopAddPoint)
456
+ end = base + seg.len
457
+ start = end - len
458
+ } else if (ringProg > rings.lenStartDimPoint) {
459
+ len = seg.len * (1 - (ringProg - rings.lenStartDimPoint) / (1 - rings.lenStartDimPoint))
460
+ start = base
461
+ end = start + len
462
+ } else {
463
+ len = seg.len
464
+ start = base
465
+ end = start + len
466
+ }
467
+
468
+ const lineWidthMul = Math.min(-0.8 * (ringProg - 0.8) + 1, 1)
469
+ const [rr, gg, bb] = ringRgbAt(ringProg)
470
+ const alphaRing = getAlpha(ringProg)
471
+
472
+ for (let k = 0; k < rings.segNum; k++) {
473
+ const t0 = k / rings.segNum
474
+ const t1 = (k + 1) / rings.segNum
475
+ const a0 = start + (end - start) * t0
476
+ const a1 = start + (end - start) * t1
477
+
478
+ if (Math.abs(a1 - a0) < 0.01) continue
479
+
480
+ const wT = getWeightProp(t0)
481
+ const lw = (rings.minW * (1 - wT) + rings.maxW * wT) * lineWidthMul
482
+ const strokeStyle = `rgba(${rr},${gg},${bb},${alphaRing})`
483
+ const radius = w.r + seg.rRoundRate * this.scale * this.clickScale
484
+ this.strokeRingSegment(w.x, w.y, radius, a0, a1, lw, strokeStyle)
485
+ }
486
+ }
487
+ }
488
+
489
+ if (ringProg >= 1 && waveProg >= 1) {
490
+ this.wavesPool.push(this.waves[i])
491
+ this.waves.splice(i, 1)
492
+ }
493
+ }
494
+ }
495
+
496
+ private updateSparks(clickFrameScale: number, trailFrameScale: number): void {
497
+ const ctx = this.bufferCtx
498
+ for (let i = this.sparks.length - 1; i >= 0; i--) {
499
+ const s = this.sparks[i]
500
+ const fs = s.fromClick ? clickFrameScale : trailFrameScale
501
+ s.x += s.vx * fs
502
+ s.y += s.vy * fs
503
+ s.vx *= Math.pow(s.f, fs)
504
+ s.vy *= Math.pow(s.f, fs)
505
+ s.rot += s.rs * fs
506
+ s.a -= 0.032 * fs
507
+ if (s.a <= 0) {
508
+ this.sparksPool.push(this.sparks[i])
509
+ this.sparks.splice(i, 1)
510
+ continue
511
+ }
512
+
513
+ ctx.save()
514
+ ctx.translate(s.x, s.y)
515
+ ctx.rotate(s.rot)
516
+ ctx.beginPath()
517
+ ctx.moveTo(0, -s.s)
518
+ ctx.lineTo(s.s * 0.6, s.s * 0.6)
519
+ ctx.lineTo(-s.s * 0.6, s.s * 0.6)
520
+ ctx.fillStyle = `rgba(255,255,255,${this.alpha(s.a)})`
521
+ ctx.fill()
522
+ ctx.restore()
523
+ }
524
+ }
525
+
526
+ private canvasRect(): Rect {
527
+ return { x: 0, y: 0, w: this.cssWidth, h: this.cssHeight }
528
+ }
529
+
530
+ private clipRect(rect: Rect): Rect | null {
531
+ if (!rect) return null
532
+ const x0 = Math.max(0, Math.floor(rect.x))
533
+ const y0 = Math.max(0, Math.floor(rect.y))
534
+ const x1 = Math.min(this.cssWidth, Math.ceil(rect.x + rect.w))
535
+ const y1 = Math.min(this.cssHeight, Math.ceil(rect.y + rect.h))
536
+ if (x1 <= x0 || y1 <= y0) return null
537
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }
538
+ }
539
+
540
+ private pointRect(x: number, y: number, padding: number): Rect {
541
+ return { x: x - padding, y: y - padding, w: padding * 2, h: padding * 2 }
542
+ }
543
+
544
+ private segmentRect(a: Point, b: Point, padding: number): Rect {
545
+ const x0 = Math.min(a.x, b.x) - padding
546
+ const y0 = Math.min(a.y, b.y) - padding
547
+ const x1 = Math.max(a.x, b.x) + padding
548
+ const y1 = Math.max(a.y, b.y) + padding
549
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }
550
+ }
551
+
552
+ private intersects(a: Rect, b: Rect): boolean {
553
+ return (
554
+ a.x <= b.x + b.w &&
555
+ a.x + a.w >= b.x &&
556
+ a.y <= b.y + b.h &&
557
+ a.y + a.h >= b.y
558
+ )
559
+ }
560
+
561
+ private unionRect(a: Rect, b: Rect): Rect {
562
+ const x0 = Math.min(a.x, b.x)
563
+ const y0 = Math.min(a.y, b.y)
564
+ const x1 = Math.max(a.x + a.w, b.x + b.w)
565
+ const y1 = Math.max(a.y + a.h, b.y + b.h)
566
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }
567
+ }
568
+
569
+ private mergeRects(rects: Rect[]): Rect[] {
570
+ const merged: Rect[] = []
571
+ for (const raw of rects) {
572
+ let rect = this.clipRect(raw)
573
+ if (!rect) continue
574
+
575
+ for (let i = 0; i < merged.length; i++) {
576
+ if (this.intersects(merged[i], rect)) {
577
+ rect = this.unionRect(merged[i], rect)
578
+ merged.splice(i, 1)
579
+ i = -1
580
+ }
581
+ }
582
+
583
+ merged.push(rect)
584
+ }
585
+ return merged
586
+ }
587
+
588
+ private getEffectRects(): Rect[] {
589
+ const rects: Rect[] = []
590
+ const trailPad = 18 * this.scale * this.trailWidth + 12
591
+
592
+ const trailPoints: TrailPoint[] =
593
+ this.lastPos && this.trail.length > 0
594
+ ? this.trail.concat([{ x: this.lastPos.x, y: this.lastPos.y, life: 1 }])
595
+ : this.trail
596
+
597
+ if (trailPoints.length === 1) {
598
+ rects.push(this.pointRect(trailPoints[0].x, trailPoints[0].y, trailPad))
599
+ } else {
600
+ for (let i = 0; i < trailPoints.length - 1; i++) {
601
+ rects.push(this.segmentRect(trailPoints[i], trailPoints[i + 1], trailPad))
602
+ }
603
+ }
604
+
605
+ const wavePad = 34 * this.scale * this.clickScale + RINGS_ANIM_CFG.maxW + 16
606
+ for (const wave of this.waves) {
607
+ const radius = Math.max(wave.r || 0, FILLED_CIRCLE_CFG.rAddRate * this.scale * this.clickScale) + wavePad
608
+ rects.push(this.pointRect(wave.x, wave.y, radius))
609
+ }
610
+
611
+ const maxFrameScale = MAX_DELTA_MS / BASE_FRAME_MS
612
+ for (const spark of this.sparks) {
613
+ const speed = Math.hypot(spark.vx || 0, spark.vy || 0)
614
+ const speedScale = spark.fromClick ? this.clickSpeed : this.trailSpeed
615
+ const motionPad = speed * maxFrameScale * speedScale
616
+ const sparkPad = Math.max(spark.s || 0, 9 * this.scale * this.sparkSize) * 2 + motionPad + 12
617
+ rects.push(this.pointRect(spark.x, spark.y, sparkPad))
618
+ }
619
+
620
+ return this.mergeRects(rects)
621
+ }
622
+
623
+ private getRenderRects(): Rect[] {
624
+ if (this.forceFullRedraw) {
625
+ return [this.canvasRect()]
626
+ }
627
+ return this.mergeRects(this.previousDirtyRects.concat(this.getEffectRects()))
628
+ }
629
+
630
+ private clipToRects(ctx: CanvasRenderingContext2D, rects: Rect[]): void {
631
+ ctx.beginPath()
632
+ for (const rect of rects) {
633
+ ctx.rect(rect.x, rect.y, rect.w, rect.h)
634
+ }
635
+ ctx.clip()
636
+ }
637
+
638
+ private renderToMain(rects: Rect[]): void {
639
+ const { mainCtx, mainCanvas, bufferCanvas } = this
640
+ if (!rects || rects.length === 0) {
641
+ mainCtx.clearRect(0, 0, mainCanvas.width, mainCanvas.height)
642
+ mainCtx.drawImage(bufferCanvas, 0, 0)
643
+ return
644
+ }
645
+
646
+ const dpr = this.dpr || 1
647
+ for (const rect of rects) {
648
+ const sx = Math.max(0, Math.floor(rect.x * dpr))
649
+ const sy = Math.max(0, Math.floor(rect.y * dpr))
650
+ const sw = Math.min(mainCanvas.width - sx, Math.ceil(rect.w * dpr))
651
+ const sh = Math.min(mainCanvas.height - sy, Math.ceil(rect.h * dpr))
652
+ if (sw <= 0 || sh <= 0) continue
653
+
654
+ mainCtx.clearRect(sx, sy, sw, sh)
655
+ mainCtx.drawImage(bufferCanvas, sx, sy, sw, sh, sx, sy, sw, sh)
656
+ }
657
+ }
658
+
659
+ private animationLoops(now: number): void {
660
+ const hasWork =
661
+ this.waves.length > 0 ||
662
+ this.sparks.length > 0 ||
663
+ this.trail.length > 0
664
+
665
+ if (!hasWork) {
666
+ this.lastFrameTime = now
667
+ if (this.previousDirtyRects.length > 0) {
668
+ this.clearBufferRects(this.previousDirtyRects)
669
+ this.renderToMain(this.previousDirtyRects)
670
+ this.previousDirtyRects = []
671
+ }
672
+ this.animFrameId = requestAnimationFrame((nextNow) => this.animationLoops(nextNow))
673
+ return
674
+ }
675
+
676
+ const deltaMs = Math.min(now - this.lastFrameTime, MAX_DELTA_MS)
677
+ this.lastFrameTime = now
678
+ const baseScale = deltaMs / BASE_FRAME_MS
679
+ const trailFrameScale = baseScale * this.trailSpeed
680
+ const clickFrameScale = baseScale * this.clickSpeed
681
+
682
+ const bctx = this.bufferCtx
683
+ const renderRects = this.getRenderRects()
684
+
685
+ bctx.save()
686
+ this.clipToRects(bctx, renderRects)
687
+ bctx.globalCompositeOperation = 'lighter'
688
+
689
+ this.clearBufferRects(renderRects)
690
+ this.updateTrail(trailFrameScale)
691
+ this.updateWaves(clickFrameScale)
692
+ this.updateSparks(clickFrameScale, trailFrameScale)
693
+
694
+ bctx.globalCompositeOperation = 'source-over'
695
+ bctx.restore()
696
+
697
+ this.renderToMain(renderRects)
698
+ this.previousDirtyRects = this.getEffectRects()
699
+ this.forceFullRedraw = false
700
+
701
+ this.animFrameId = requestAnimationFrame((nextNow) => this.animationLoops(nextNow))
702
+ }
703
+
704
+ destroy(): void {
705
+ cancelAnimationFrame(this.animFrameId)
706
+ this.waves.length = 0
707
+ this.sparks.length = 0
708
+ this.trail.length = 0
709
+ this.sparksPool.length = 0
710
+ this.wavesPool.length = 0
711
+ this.previousDirtyRects = []
712
+ const ctx = this.mainCtx
713
+ ctx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height)
714
+ }
715
+ }
@@ -0,0 +1,35 @@
1
+ import type { FilledCircleConfig, RingsAnimConfig, CreateClickConfig } from './types'
2
+
3
+ export const FILLED_CIRCLE_CFG: FilledCircleConfig = {
4
+ rAddRate: 26,
5
+ maxLife: 16,
6
+ }
7
+
8
+ export const RINGS_ANIM_CFG: RingsAnimConfig = {
9
+ rsList: [0, 0.08, 0.1],
10
+ rRoundRateList: [0, 1, 1.5, 2],
11
+ len: 1.1 * Math.PI,
12
+ maxLife: 23,
13
+ segNum: 10,
14
+ minW: 0.4,
15
+ maxW: 3.3,
16
+ lenStopAddPoint: 0.1,
17
+ lenStartDimPoint: 0.4,
18
+ }
19
+
20
+ export const CREATE_CLICK_CFG: CreateClickConfig = {
21
+ rings: {
22
+ rsList: [0, 0.03, 0.06],
23
+ rRoundRateList: [0, 1, 1.5, 2],
24
+ len: 1.1 * Math.PI,
25
+ },
26
+ sparksCount: 4,
27
+ }
28
+
29
+ export const DEFAULT_COLOR = '45,175,255'
30
+ export const DEFAULT_SCALE = 1.5
31
+ export const DEFAULT_OPACITY = 1.0
32
+ export const DEFAULT_SPEED = 1.0
33
+ export const DEFAULT_Z_INDEX = 2147483647
34
+ export const BASE_FRAME_MS = 1000 / 60
35
+ export const MAX_DELTA_MS = 100
@@ -0,0 +1,92 @@
1
+ export interface BASparkOptions {
2
+ color?: string
3
+ scale?: number
4
+ opacity?: number
5
+ trailSpeed?: number
6
+ clickSpeed?: number
7
+ trailWidth?: number
8
+ sparkSize?: number
9
+ clickScale?: number
10
+ alwaysTrail?: boolean
11
+ autoTrack?: boolean
12
+ zIndex?: number
13
+ }
14
+
15
+ export interface Point {
16
+ x: number
17
+ y: number
18
+ }
19
+
20
+ export interface Rect {
21
+ x: number
22
+ y: number
23
+ w: number
24
+ h: number
25
+ }
26
+
27
+ export interface Spark {
28
+ x: number
29
+ y: number
30
+ vx: number
31
+ vy: number
32
+ rot: number
33
+ rs: number
34
+ s: number
35
+ a: number
36
+ f: number
37
+ fromClick: boolean
38
+ }
39
+
40
+ export interface RingSegment {
41
+ off: number
42
+ len: number
43
+ rRoundRate: number
44
+ }
45
+
46
+ export interface Ring {
47
+ ang: number
48
+ rs: number
49
+ segs: RingSegment[]
50
+ }
51
+
52
+ export interface Wave {
53
+ x: number
54
+ y: number
55
+ r: number
56
+ life: number
57
+ ring: Ring
58
+ }
59
+
60
+ export interface TrailPoint {
61
+ x: number
62
+ y: number
63
+ life: number
64
+ }
65
+
66
+ export interface FilledCircleConfig {
67
+ rAddRate: number
68
+ maxLife: number
69
+ }
70
+
71
+ export interface RingsAnimConfig {
72
+ rsList: number[]
73
+ rRoundRateList: number[]
74
+ len: number
75
+ maxLife: number
76
+ segNum: number
77
+ minW: number
78
+ maxW: number
79
+ lenStopAddPoint: number
80
+ lenStartDimPoint: number
81
+ }
82
+
83
+ export interface CreateClickConfig {
84
+ rings: {
85
+ rsList: number[]
86
+ rRoundRateList: number[]
87
+ len: number
88
+ }
89
+ sparksCount: number
90
+ }
91
+
92
+ export type InputMode = 'mouse' | 'touch'
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { BASpark } from './BASpark'
2
+ export { MouseSpark } from './core/MouseSpark'
3
+ export type { BASparkOptions, InputMode } from './core/types'
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src",
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true
17
+ },
18
+ "include": ["src"]
19
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm', 'cjs', 'iife'],
6
+ globalName: 'BASpark',
7
+ dts: true,
8
+ sourcemap: true,
9
+ clean: true,
10
+ minify: true,
11
+ outExtension({ format }) {
12
+ if (format === 'iife') return { js: '.global.js' }
13
+ if (format === 'cjs') return { js: '.cjs' }
14
+ return { js: '.js' }
15
+ },
16
+ })