@aicupa/plugin-bg-music 1.0.3 → 1.0.4
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/inject.js +247 -146
- package/package.json +1 -1
package/inject.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
(function () {
|
|
2
|
-
// 防止重复注入
|
|
3
2
|
if (window.__AICUPA_BG_MUSIC_LOADED__) return;
|
|
4
3
|
window.__AICUPA_BG_MUSIC_LOADED__ = true;
|
|
5
4
|
|
|
6
|
-
// ==========================================
|
|
7
5
|
// 1. 动态引入 Tone.js
|
|
8
|
-
// ==========================================
|
|
9
6
|
const script = document.createElement("script");
|
|
10
7
|
script.src = "https://unpkg.com/tone";
|
|
11
8
|
script.async = true;
|
|
@@ -16,9 +13,7 @@
|
|
|
16
13
|
};
|
|
17
14
|
|
|
18
15
|
function initPlugin() {
|
|
19
|
-
//
|
|
20
|
-
// 2. 注入 CSS 霓虹深色样式
|
|
21
|
-
// ==========================================
|
|
16
|
+
// 2. 注入全局样式 (包含曲目列表的抽屉动画)
|
|
22
17
|
const style = document.createElement("style");
|
|
23
18
|
style.textContent = `
|
|
24
19
|
.music-floating-container {
|
|
@@ -28,7 +23,7 @@
|
|
|
28
23
|
display: flex;
|
|
29
24
|
align-items: center;
|
|
30
25
|
justify-content: flex-end;
|
|
31
|
-
z-index: 999999;
|
|
26
|
+
z-index: 999999;
|
|
32
27
|
touch-action: none;
|
|
33
28
|
user-select: none;
|
|
34
29
|
}
|
|
@@ -47,119 +42,145 @@
|
|
|
47
42
|
position: relative;
|
|
48
43
|
z-index: 2;
|
|
49
44
|
}
|
|
50
|
-
.music-icon-wrapper:active {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.music-icon-wrapper svg {
|
|
55
|
-
fill: #00e5ff;
|
|
56
|
-
width: 20px;
|
|
57
|
-
height: 20px;
|
|
58
|
-
pointer-events: none;
|
|
59
|
-
}
|
|
60
|
-
.music-icon-wrapper.playing svg {
|
|
61
|
-
animation: musicPulse 1.5s infinite alternate;
|
|
62
|
-
}
|
|
45
|
+
.music-icon-wrapper:active { cursor: grabbing; transform: scale(0.95); }
|
|
46
|
+
.music-icon-wrapper svg { fill: #00e5ff; width: 20px; height: 20px; pointer-events: none; }
|
|
47
|
+
.music-icon-wrapper.playing svg { animation: musicPulse 1.5s infinite alternate; }
|
|
48
|
+
|
|
63
49
|
@keyframes musicPulse {
|
|
64
50
|
0% { transform: scale(1); filter: drop-shadow(0 0 2px #00e5ff); }
|
|
65
51
|
100% { transform: scale(1.12); filter: drop-shadow(0 0 8px #00e5ff); }
|
|
66
52
|
}
|
|
53
|
+
|
|
54
|
+
/* 控制面板基础样式 */
|
|
67
55
|
.music-control-panel {
|
|
68
56
|
position: absolute;
|
|
69
57
|
right: 15px;
|
|
70
58
|
background: #0f172a;
|
|
71
59
|
border: 1px solid #1e293b;
|
|
72
|
-
padding:
|
|
73
|
-
border-radius:
|
|
60
|
+
padding: 12px 45px 12px 15px;
|
|
61
|
+
border-radius: 16px 0 0 16px;
|
|
74
62
|
display: flex;
|
|
75
63
|
flex-direction: column;
|
|
76
|
-
gap:
|
|
77
|
-
width:
|
|
64
|
+
gap: 8px;
|
|
65
|
+
width: 200px;
|
|
78
66
|
opacity: 0;
|
|
79
67
|
transform: translateX(20px);
|
|
80
|
-
transition: opacity 0.3s, transform 0.3s;
|
|
68
|
+
transition: opacity 0.3s, transform 0.3s, height 0.3s;
|
|
81
69
|
pointer-events: none;
|
|
82
70
|
box-shadow: -5px 5px 15px rgba(0,0,0,0.5);
|
|
71
|
+
overflow: hidden;
|
|
83
72
|
}
|
|
73
|
+
|
|
74
|
+
/* Hover 激活主面板 */
|
|
84
75
|
.music-floating-container:not(.dragging):hover .music-control-panel {
|
|
85
76
|
opacity: 1;
|
|
86
77
|
transform: translateX(0);
|
|
87
78
|
pointer-events: auto;
|
|
88
79
|
}
|
|
80
|
+
|
|
81
|
+
/* 左靠齐适配 */
|
|
89
82
|
.music-floating-container.align-left .music-control-panel {
|
|
90
83
|
right: auto;
|
|
91
84
|
left: 15px;
|
|
92
|
-
border-radius: 0
|
|
93
|
-
padding:
|
|
85
|
+
border-radius: 0 16px 16px 0;
|
|
86
|
+
padding: 12px 15px 12px 45px;
|
|
94
87
|
transform: translateX(-20px);
|
|
95
88
|
box-shadow: 5px 5px 15px rgba(0,0,0,0.5);
|
|
96
89
|
}
|
|
97
90
|
.music-floating-container.align-left:not(.dragging):hover .music-control-panel {
|
|
98
91
|
transform: translateX(0);
|
|
99
92
|
}
|
|
93
|
+
|
|
100
94
|
.music-floating-container:hover .music-icon-wrapper {
|
|
101
95
|
border-color: #ffffff;
|
|
102
96
|
box-shadow: 0 0 15px rgba(255, 255, 255, 0.2);
|
|
103
97
|
}
|
|
98
|
+
|
|
104
99
|
.music-title {
|
|
105
100
|
color: #00e5ff;
|
|
106
101
|
font-size: 11px;
|
|
107
102
|
white-space: nowrap;
|
|
108
103
|
overflow: hidden;
|
|
109
104
|
text-overflow: ellipsis;
|
|
105
|
+
font-weight: bold;
|
|
110
106
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
justify-content: space-between;
|
|
115
|
-
gap: 8px;
|
|
116
|
-
}
|
|
107
|
+
|
|
108
|
+
.panel-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
109
|
+
|
|
117
110
|
.music-ctrl-btn {
|
|
118
111
|
background: none;
|
|
119
112
|
border: 1px solid #334155;
|
|
120
113
|
color: #e2e8f0;
|
|
121
114
|
font-size: 10px;
|
|
122
|
-
padding:
|
|
115
|
+
padding: 3px 8px;
|
|
123
116
|
cursor: pointer;
|
|
124
|
-
border-radius:
|
|
117
|
+
border-radius: 4px;
|
|
118
|
+
transition: all 0.2s;
|
|
125
119
|
}
|
|
126
|
-
.music-ctrl-btn:hover {
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
.music-ctrl-btn:hover { border-color: #00e5ff; color: #00e5ff; }
|
|
121
|
+
|
|
122
|
+
.music-progress-container { width: 100%; height: 4px; background: #334155; border-radius: 2px; position: relative; overflow: hidden; }
|
|
123
|
+
.music-progress-bar { height: 100%; width: 0%; background: #00e5ff; transition: width 0.5s linear; }
|
|
124
|
+
|
|
125
|
+
/* === 新增:曲库列表样式 === */
|
|
126
|
+
.music-playlist-drawer {
|
|
127
|
+
border-top: 1px solid #1e293b;
|
|
128
|
+
margin-top: 4px;
|
|
129
|
+
padding-top: 8px;
|
|
130
|
+
display: none; /* 默认折叠 */
|
|
131
|
+
flex-direction: column;
|
|
132
|
+
gap: 4px;
|
|
133
|
+
max-height: 150px;
|
|
134
|
+
overflow-y: auto;
|
|
129
135
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
/* 自定义滚动条 */
|
|
137
|
+
.music-playlist-drawer::-webkit-scrollbar { width: 4px; }
|
|
138
|
+
.music-playlist-drawer::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 2px; }
|
|
139
|
+
|
|
140
|
+
/* 控制面板展开状态样式触发 */
|
|
141
|
+
.music-control-panel.list-expanded .music-playlist-drawer {
|
|
142
|
+
display: flex;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.playlist-item {
|
|
146
|
+
font-size: 10px;
|
|
147
|
+
color: #94a3b8;
|
|
148
|
+
padding: 5px 6px;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
border-radius: 4px;
|
|
151
|
+
white-space: nowrap;
|
|
136
152
|
overflow: hidden;
|
|
153
|
+
text-overflow: ellipsis;
|
|
154
|
+
transition: all 0.2s;
|
|
137
155
|
}
|
|
138
|
-
.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
.playlist-item:hover {
|
|
157
|
+
background: rgba(0, 229, 255, 0.1);
|
|
158
|
+
color: #ffffff;
|
|
159
|
+
}
|
|
160
|
+
.playlist-item.active {
|
|
161
|
+
color: #00e5ff;
|
|
162
|
+
background: rgba(0, 229, 255, 0.15);
|
|
163
|
+
font-weight: bold;
|
|
164
|
+
border-left: 2px solid #00e5ff;
|
|
143
165
|
}
|
|
144
166
|
`;
|
|
145
167
|
document.head.appendChild(style);
|
|
146
168
|
|
|
147
|
-
//
|
|
148
|
-
// 3. 动态插入 DOM 结构
|
|
149
|
-
// ==========================================
|
|
169
|
+
// 3. 动态插入 DOM (加入了 LIST 列表切换按钮和列表容器)
|
|
150
170
|
const container = document.createElement("div");
|
|
151
171
|
container.className = "music-floating-container";
|
|
152
172
|
container.id = "music-container";
|
|
153
173
|
container.innerHTML = `
|
|
154
|
-
<div class="music-control-panel">
|
|
155
|
-
<div class="music-title" id="track-name">Crystal Piano</div>
|
|
174
|
+
<div class="music-control-panel" id="control-panel">
|
|
175
|
+
<div class="music-title" id="track-name">1. Crystal Piano</div>
|
|
156
176
|
<div class="music-progress-container">
|
|
157
177
|
<div class="music-progress-bar" id="track-progress"></div>
|
|
158
178
|
</div>
|
|
159
179
|
<div class="panel-row">
|
|
160
180
|
<button class="music-ctrl-btn" id="btn-toggle">PLAY</button>
|
|
161
|
-
<button class="music-ctrl-btn" id="btn-
|
|
181
|
+
<button class="music-ctrl-btn" id="btn-list">LIST ☰</button>
|
|
162
182
|
</div>
|
|
183
|
+
<div class="music-playlist-drawer" id="playlist-drawer"></div>
|
|
163
184
|
</div>
|
|
164
185
|
<div class="music-icon-wrapper" id="icon-trigger">
|
|
165
186
|
<svg viewBox="0 0 24 24">
|
|
@@ -169,21 +190,22 @@
|
|
|
169
190
|
`;
|
|
170
191
|
document.body.appendChild(container);
|
|
171
192
|
|
|
172
|
-
//
|
|
173
|
-
// 4. 音频引擎调度逻辑
|
|
174
|
-
// ==========================================
|
|
193
|
+
// 4. 音频核心配置与多曲目矩阵
|
|
175
194
|
let isPlaying = false;
|
|
176
195
|
let currentTrackIndex = 0;
|
|
177
196
|
let progressTimer = null;
|
|
178
197
|
let currentStep = 0;
|
|
179
198
|
|
|
180
199
|
const tracks = [
|
|
181
|
-
{ name: "Crystal Piano", stepsMax: 12 },
|
|
182
|
-
{ name: "Ambient Canon", stepsMax: 16 },
|
|
200
|
+
{ name: "1. Crystal Piano", stepsMax: 12, bpm: 72 },
|
|
201
|
+
{ name: "2. Ambient Canon", stepsMax: 16, bpm: 72 },
|
|
202
|
+
{ name: "3. Always With Me", stepsMax: 16, bpm: 84 },
|
|
203
|
+
{ name: "4. Air on G String", stepsMax: 16, bpm: 60 },
|
|
204
|
+
{ name: "5. Gymnopédie No.1", stepsMax: 8, bpm: 54 },
|
|
183
205
|
];
|
|
184
206
|
|
|
185
207
|
const reverb = new Tone.Reverb({
|
|
186
|
-
decay:
|
|
208
|
+
decay: 7,
|
|
187
209
|
preDelay: 0.15,
|
|
188
210
|
wet: 0.45,
|
|
189
211
|
}).toDestination();
|
|
@@ -193,13 +215,13 @@
|
|
|
193
215
|
wet: 0.2,
|
|
194
216
|
}).connect(reverb);
|
|
195
217
|
const filter = new Tone.Filter({
|
|
196
|
-
frequency:
|
|
218
|
+
frequency: 2000,
|
|
197
219
|
type: "lowpass",
|
|
198
220
|
}).connect(delay);
|
|
199
221
|
|
|
200
222
|
const padSynth = new Tone.PolySynth(Tone.Synth, {
|
|
201
223
|
oscillator: { type: "triangle" },
|
|
202
|
-
envelope: { attack: 0.
|
|
224
|
+
envelope: { attack: 0.3, decay: 2, sustain: 0.5, release: 3.5 },
|
|
203
225
|
}).connect(filter);
|
|
204
226
|
padSynth.volume.value = -18;
|
|
205
227
|
|
|
@@ -209,24 +231,24 @@
|
|
|
209
231
|
}).connect(filter);
|
|
210
232
|
leadSynth.volume.value = -10;
|
|
211
233
|
|
|
212
|
-
|
|
213
|
-
|
|
234
|
+
// 乐理矩阵
|
|
235
|
+
const pChords = [
|
|
236
|
+
["F3", "A3", "C4", "E4"],
|
|
214
237
|
["G3", "B3", "D4", "G4"],
|
|
215
|
-
["C3", "E3", "G3", "B3"
|
|
238
|
+
["C3", "E3", "G3", "B3"],
|
|
216
239
|
];
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
["
|
|
221
|
-
["
|
|
222
|
-
["
|
|
223
|
-
["
|
|
224
|
-
["
|
|
225
|
-
["
|
|
226
|
-
["
|
|
227
|
-
["G2", "B3", "D4"],
|
|
240
|
+
const pScale = ["C5", "D5", "E5", "G5", "A5", "C6", "D6", "E6"];
|
|
241
|
+
const cChords = [
|
|
242
|
+
["C3", "E4"],
|
|
243
|
+
["G3", "D4"],
|
|
244
|
+
["A2", "C4"],
|
|
245
|
+
["E3", "B3"],
|
|
246
|
+
["F2", "A3"],
|
|
247
|
+
["C3", "G3"],
|
|
248
|
+
["F2", "A3"],
|
|
249
|
+
["G2", "B3"],
|
|
228
250
|
];
|
|
229
|
-
const
|
|
251
|
+
const cMelody = [
|
|
230
252
|
["C5", "G5"],
|
|
231
253
|
["B4", "G5"],
|
|
232
254
|
["A4", "E5"],
|
|
@@ -236,138 +258,216 @@
|
|
|
236
258
|
["F4", "C5"],
|
|
237
259
|
["D4", "G4"],
|
|
238
260
|
];
|
|
261
|
+
const ghibliChords = [
|
|
262
|
+
["F3", "A3", "C4"],
|
|
263
|
+
["C3", "E3", "G3"],
|
|
264
|
+
["G3", "B3", "D4"],
|
|
265
|
+
["A2", "C3", "E3"],
|
|
266
|
+
["F3", "A3", "C4"],
|
|
267
|
+
["C3", "E3", "G3"],
|
|
268
|
+
["G3", "B3", "D4"],
|
|
269
|
+
["C3", "E4", "G4"],
|
|
270
|
+
];
|
|
271
|
+
const ghibliMelody = [
|
|
272
|
+
["A4", "C5"],
|
|
273
|
+
["G4", "C5"],
|
|
274
|
+
["F4", "D5"],
|
|
275
|
+
["E4", "C5"],
|
|
276
|
+
["F4", "A4"],
|
|
277
|
+
["E4", "G4"],
|
|
278
|
+
["D4", "F4"],
|
|
279
|
+
["C4", "E5"],
|
|
280
|
+
];
|
|
281
|
+
const bachBass = [
|
|
282
|
+
["C3", "E4"],
|
|
283
|
+
["B2", "D4"],
|
|
284
|
+
["A2", "C4"],
|
|
285
|
+
["G2", "B3"],
|
|
286
|
+
["F2", "A3"],
|
|
287
|
+
["E2", "G3"],
|
|
288
|
+
["D2", "F3"],
|
|
289
|
+
["G2", "F3"],
|
|
290
|
+
];
|
|
291
|
+
const bachMelody = [
|
|
292
|
+
["E5", "G5"],
|
|
293
|
+
["F5", "A5"],
|
|
294
|
+
["C5", "E5"],
|
|
295
|
+
["B4", "D5"],
|
|
296
|
+
["A4", "C5"],
|
|
297
|
+
["G4", "B4"],
|
|
298
|
+
["F4", "A4"],
|
|
299
|
+
["B4", "G5"],
|
|
300
|
+
];
|
|
301
|
+
const satieChords = [
|
|
302
|
+
["G2", "B3", "D4", "F#4"],
|
|
303
|
+
["D2", "F#3", "A3", "C#4"],
|
|
304
|
+
];
|
|
305
|
+
const satieMelody = ["F#5", "A5", "C#6", "B5", "E5", "D5", "A4", "F#4"];
|
|
239
306
|
|
|
240
307
|
function setupTransport() {
|
|
241
308
|
Tone.getTransport().cancel();
|
|
242
|
-
Tone.getTransport().bpm.value =
|
|
309
|
+
Tone.getTransport().bpm.value = tracks[currentTrackIndex].bpm;
|
|
243
310
|
currentStep = 0;
|
|
244
311
|
|
|
245
312
|
Tone.getTransport().scheduleRepeat((time) => {
|
|
313
|
+
const step = currentStep;
|
|
246
314
|
if (currentTrackIndex === 0) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
315
|
+
padSynth.triggerAttackRelease(
|
|
316
|
+
pChords[step % pChords.length],
|
|
317
|
+
"2n",
|
|
318
|
+
time,
|
|
319
|
+
);
|
|
320
|
+
if (Math.random() > 0.3)
|
|
252
321
|
leadSynth.triggerAttackRelease(
|
|
253
|
-
|
|
322
|
+
pScale[Math.floor(Math.random() * pScale.length)],
|
|
254
323
|
"4n",
|
|
255
|
-
time + Math.random() * 0.
|
|
324
|
+
time + Math.random() * 0.1,
|
|
256
325
|
);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
leadSynth.triggerAttackRelease(pool[0], "4n", time);
|
|
263
|
-
if (Math.random() > 0.4) {
|
|
326
|
+
} else if (currentTrackIndex === 1) {
|
|
327
|
+
const idx = step % 8;
|
|
328
|
+
padSynth.triggerAttackRelease(cChords[idx], "2n", time);
|
|
329
|
+
leadSynth.triggerAttackRelease(cMelody[idx][step % 2], "4n", time);
|
|
330
|
+
if (Math.random() > 0.5)
|
|
264
331
|
leadSynth.triggerAttackRelease(
|
|
265
|
-
|
|
332
|
+
cMelody[idx][1],
|
|
266
333
|
"8n",
|
|
267
334
|
time + Tone.Time("4n"),
|
|
268
335
|
);
|
|
269
|
-
|
|
336
|
+
} else if (currentTrackIndex === 2) {
|
|
337
|
+
const idx = step % 8;
|
|
338
|
+
padSynth.triggerAttackRelease(ghibliChords[idx], "2n", time);
|
|
339
|
+
leadSynth.triggerAttackRelease(
|
|
340
|
+
ghibliMelody[idx][Math.floor(Math.random() * 2)],
|
|
341
|
+
"4n",
|
|
342
|
+
time,
|
|
343
|
+
);
|
|
344
|
+
} else if (currentTrackIndex === 3) {
|
|
345
|
+
const idx = step % 8;
|
|
346
|
+
padSynth.triggerAttackRelease(bachBass[idx], "2n", time);
|
|
347
|
+
leadSynth.triggerAttackRelease(
|
|
348
|
+
step % 2 === 0 ? bachMelody[idx][0] : bachMelody[idx][1],
|
|
349
|
+
"4n",
|
|
350
|
+
time,
|
|
351
|
+
);
|
|
352
|
+
} else if (currentTrackIndex === 4) {
|
|
353
|
+
padSynth.triggerAttackRelease(satieChords[step % 2], "1m", time);
|
|
354
|
+
if (step % 2 === 0)
|
|
355
|
+
leadSynth.triggerAttackRelease(
|
|
356
|
+
satieMelody[Math.floor(step / 2) % satieMelody.length],
|
|
357
|
+
"2n",
|
|
358
|
+
time + Tone.Time("4n"),
|
|
359
|
+
);
|
|
270
360
|
}
|
|
271
361
|
currentStep++;
|
|
272
362
|
}, "2n");
|
|
273
363
|
}
|
|
274
364
|
|
|
275
|
-
//
|
|
276
|
-
// 5. 交互绑定与拖拽定位
|
|
277
|
-
// ==========================================
|
|
365
|
+
// 5. DOM 节点引用与渲染逻辑
|
|
278
366
|
const icon = document.getElementById("icon-trigger");
|
|
279
367
|
const btnToggle = document.getElementById("btn-toggle");
|
|
280
|
-
const
|
|
368
|
+
const btnList = document.getElementById("btn-list");
|
|
369
|
+
const panel = document.getElementById("control-panel");
|
|
370
|
+
const drawer = document.getElementById("playlist-drawer");
|
|
281
371
|
const trackName = document.getElementById("track-name");
|
|
282
372
|
const progressBar = document.getElementById("track-progress");
|
|
283
373
|
|
|
284
|
-
|
|
285
|
-
|
|
374
|
+
// 动态渲染曲目列表
|
|
375
|
+
function renderPlaylist() {
|
|
376
|
+
drawer.innerHTML = "";
|
|
377
|
+
tracks.forEach((track, index) => {
|
|
378
|
+
const item = document.createElement("div");
|
|
379
|
+
item.className = `playlist-item ${index === currentTrackIndex ? "active" : ""}`;
|
|
380
|
+
item.innerText = track.name;
|
|
381
|
+
|
|
382
|
+
// 绑定点击曲目切歌事件
|
|
383
|
+
item.addEventListener("click", (e) => {
|
|
384
|
+
e.stopPropagation(); // 阻止冒泡
|
|
385
|
+
selectTrack(index);
|
|
386
|
+
});
|
|
387
|
+
drawer.appendChild(item);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function selectTrack(index) {
|
|
392
|
+
currentTrackIndex = index;
|
|
393
|
+
trackName.innerText = tracks[currentTrackIndex].name;
|
|
394
|
+
currentStep = 0;
|
|
395
|
+
progressBar.style.width = "0%";
|
|
396
|
+
renderPlaylist(); // 刷新高亮状态
|
|
397
|
+
|
|
398
|
+
if (isPlaying) {
|
|
399
|
+
setupTransport();
|
|
400
|
+
} else {
|
|
401
|
+
togglePlay(); // 如果之前没播放,点击列表曲目直接触发播放
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 6. 交互控制
|
|
406
|
+
let isDragging = false,
|
|
407
|
+
hasMoved = false;
|
|
286
408
|
let startX = 0,
|
|
287
|
-
startY = 0
|
|
288
|
-
|
|
409
|
+
startY = 0,
|
|
410
|
+
initialLeft = 0,
|
|
289
411
|
initialTop = 0;
|
|
290
412
|
|
|
291
413
|
function onStart(e) {
|
|
292
414
|
isDragging = true;
|
|
293
415
|
hasMoved = false;
|
|
294
416
|
container.classList.add("dragging");
|
|
295
|
-
|
|
296
417
|
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
297
418
|
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
298
419
|
startX = clientX;
|
|
299
420
|
startY = clientY;
|
|
300
|
-
|
|
301
421
|
const rect = container.getBoundingClientRect();
|
|
302
422
|
initialLeft = rect.left;
|
|
303
423
|
initialTop = rect.top;
|
|
304
|
-
|
|
305
424
|
container.style.right = "auto";
|
|
306
425
|
container.style.bottom = "auto";
|
|
307
426
|
container.style.left = `${initialLeft}px`;
|
|
308
427
|
container.style.top = `${initialTop}px`;
|
|
309
|
-
|
|
310
428
|
document.addEventListener("mousemove", onMove);
|
|
311
429
|
document.addEventListener("mouseup", onEnd);
|
|
312
|
-
document.addEventListener("touchmove", onMove, { passive: false });
|
|
313
|
-
document.addEventListener("touchend", onEnd);
|
|
314
430
|
}
|
|
315
431
|
|
|
316
432
|
function onMove(e) {
|
|
317
433
|
if (!isDragging) return;
|
|
318
434
|
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
319
435
|
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
let newTop = initialTop + deltaY;
|
|
330
|
-
|
|
331
|
-
const padding = 10;
|
|
332
|
-
newLeft = Math.max(padding, Math.min(window.innerWidth - 55, newLeft));
|
|
333
|
-
newTop = Math.max(padding, Math.min(window.innerHeight - 55, newTop));
|
|
334
|
-
|
|
335
|
-
container.style.left = `${newLeft}px`;
|
|
336
|
-
container.style.top = `${newTop}px`;
|
|
436
|
+
const deltaX = clientX - startX,
|
|
437
|
+
deltaY = clientY - startY;
|
|
438
|
+
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) hasMoved = true;
|
|
439
|
+
let nl = initialLeft + deltaX,
|
|
440
|
+
nt = initialTop + deltaY;
|
|
441
|
+
nl = Math.max(10, Math.min(window.innerWidth - 55, nl));
|
|
442
|
+
nt = Math.max(10, Math.min(window.innerHeight - 55, nt));
|
|
443
|
+
container.style.left = `${nl}px`;
|
|
444
|
+
container.style.top = `${nt}px`;
|
|
337
445
|
}
|
|
338
446
|
|
|
339
447
|
function onEnd() {
|
|
340
448
|
if (!isDragging) return;
|
|
341
449
|
isDragging = false;
|
|
342
450
|
container.classList.remove("dragging");
|
|
343
|
-
|
|
344
451
|
document.removeEventListener("mousemove", onMove);
|
|
345
452
|
document.removeEventListener("mouseup", onEnd);
|
|
346
|
-
document.removeEventListener("touchmove", onMove);
|
|
347
|
-
document.removeEventListener("touchend", onEnd);
|
|
348
|
-
|
|
349
453
|
const rect = container.getBoundingClientRect();
|
|
350
|
-
const
|
|
351
|
-
const padding = 20;
|
|
352
|
-
|
|
454
|
+
const sw = window.innerWidth;
|
|
353
455
|
container.style.transition =
|
|
354
456
|
"left 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28)";
|
|
355
|
-
if (rect.left + rect.width / 2 <
|
|
356
|
-
container.style.left =
|
|
457
|
+
if (rect.left + rect.width / 2 < sw / 2) {
|
|
458
|
+
container.style.left = "20px";
|
|
357
459
|
container.classList.add("align-left");
|
|
358
460
|
} else {
|
|
359
|
-
container.style.left = `${
|
|
461
|
+
container.style.left = `${sw - rect.width - 20}px`;
|
|
360
462
|
container.classList.remove("align-left");
|
|
361
463
|
}
|
|
362
|
-
|
|
363
464
|
setTimeout(() => {
|
|
364
465
|
container.style.transition = "";
|
|
365
466
|
}, 300);
|
|
366
467
|
}
|
|
367
468
|
|
|
368
469
|
async function togglePlay() {
|
|
369
|
-
if (hasMoved) return; //
|
|
370
|
-
|
|
470
|
+
if (hasMoved) return; // 拖拽时不执行点击
|
|
371
471
|
await Tone.start();
|
|
372
472
|
await reverb.ready;
|
|
373
473
|
|
|
@@ -390,17 +490,18 @@
|
|
|
390
490
|
}
|
|
391
491
|
}
|
|
392
492
|
|
|
493
|
+
// 事件注册
|
|
393
494
|
icon.addEventListener("mousedown", onStart);
|
|
394
|
-
icon.addEventListener("touchstart", onStart, { passive: true });
|
|
395
495
|
icon.addEventListener("click", togglePlay);
|
|
396
496
|
btnToggle.addEventListener("click", togglePlay);
|
|
397
497
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
progressBar.style.width = "0%";
|
|
403
|
-
if (isPlaying) setupTransport();
|
|
498
|
+
// LIST 按钮:控制列表展开与收起
|
|
499
|
+
btnList.addEventListener("click", (e) => {
|
|
500
|
+
e.stopPropagation();
|
|
501
|
+
panel.classList.toggle("list-expanded");
|
|
404
502
|
});
|
|
503
|
+
|
|
504
|
+
// 首次渲染列表
|
|
505
|
+
renderPlaylist();
|
|
405
506
|
}
|
|
406
507
|
})();
|