@chocodrop/mcp 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +19 -0
- package/bin/chocodrop-mcp.js +13 -0
- package/package.json +42 -0
- package/runtime/.chocodrop-mcp-runtime +1 -0
- package/runtime/asset-bridge.js +460 -0
- package/runtime/mcp-server.js +107 -0
- package/runtime/site/examples/basic/README.md +38 -0
- package/runtime/site/examples/basic/index.html +1730 -0
- package/runtime/site/examples/bookmarklet-v2.html +141 -0
- package/runtime/site/examples/bookmarklet.html +126 -0
- package/runtime/site/examples/lofi-room/index.html +2570 -0
- package/runtime/site/examples/lofi-room/sandbox-lite.html +124 -0
- package/runtime/site/examples/music-garden/index.html +2150 -0
- package/runtime/site/examples/pixel-ocean/index.html +1780 -0
- package/runtime/site/examples/react-three-fiber/AdvancedExample.jsx +338 -0
- package/runtime/site/examples/react-three-fiber/App.jsx +97 -0
- package/runtime/site/examples/react-three-fiber/README.md +294 -0
- package/runtime/site/examples/react-three-fiber/package.json +25 -0
- package/runtime/site/examples/sdk-test/README.md +70 -0
- package/runtime/site/examples/space/index.html +1975 -0
- package/runtime/site/examples/toy-city/index.html +2288 -0
- package/runtime/site/examples/wabi-sabi/index.html +2515 -0
- package/runtime/site/getting-started.html +84 -0
- package/runtime/site/index.html +1370 -0
- package/runtime/site/public/CommandUI.js +6341 -0
- package/runtime/site/public/LiveCommandClient.js +432 -0
- package/runtime/site/public/SceneManager.js +5493 -0
- package/runtime/site/public/bookmarklet.js +206 -0
- package/runtime/site/public/bootstrap.js +50 -0
- package/runtime/site/public/chocodrop-demo.umd.js +24020 -0
- package/runtime/site/public/chocodrop-demo.umd.min.js +24020 -0
- package/runtime/site/public/chocodrop.esm.js +2 -0
- package/runtime/site/public/html-sandbox/frame.js +865 -0
- package/runtime/site/public/icons/icon-192.png +0 -0
- package/runtime/site/public/icons/icon-512.png +0 -0
- package/runtime/site/public/icons/icon-maskable-512.png +0 -0
- package/runtime/site/public/immersive-launcher.js +175 -0
- package/runtime/site/public/immersive.html +165 -0
- package/runtime/site/public/index.html +1011 -0
- package/runtime/site/public/index.js +9 -0
- package/runtime/site/public/load-chocodrop.js +25 -0
- package/runtime/site/public/load-three.js +26 -0
- package/runtime/site/public/manifest.webmanifest +59 -0
- package/runtime/site/public/pwa-bootstrap.js +128 -0
- package/runtime/site/public/sample-card.svg +15 -0
- package/runtime/site/public/service-worker.js +117 -0
- package/runtime/site/public/translation-dictionary.js +257 -0
- package/runtime/site/public/xr/xr-ui.css +338 -0
- package/runtime/site/public/xr-viewer.html +263 -0
- package/runtime/site/src/client/local-bridge.js +83 -0
|
@@ -0,0 +1,2150 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ja">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>🌸 音楽の花園 - ChocoDrop Demo</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{
|
|
9
|
+
"imports": {
|
|
10
|
+
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
|
|
11
|
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
15
|
+
<style>
|
|
16
|
+
body {
|
|
17
|
+
margin: 0;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
font-family: 'Yu Gothic', 'Hiragino Sans', sans-serif;
|
|
20
|
+
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#info {
|
|
24
|
+
position: fixed;
|
|
25
|
+
top: 16px;
|
|
26
|
+
left: 16px;
|
|
27
|
+
color: rgba(255, 255, 255, 0.95);
|
|
28
|
+
background: rgba(255, 255, 255, 0.12);
|
|
29
|
+
backdrop-filter: blur(20px) saturate(180%);
|
|
30
|
+
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
|
31
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
32
|
+
padding: 14px 18px;
|
|
33
|
+
border-radius: 12px;
|
|
34
|
+
font-size: 12px;
|
|
35
|
+
font-weight: 400;
|
|
36
|
+
z-index: 2000;
|
|
37
|
+
max-width: 280px;
|
|
38
|
+
line-height: 1.5;
|
|
39
|
+
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.15),
|
|
40
|
+
inset 0 1px 2px rgba(255, 255, 255, 0.3);
|
|
41
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
42
|
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#info.hidden {
|
|
46
|
+
opacity: 0;
|
|
47
|
+
transform: translateY(-10px);
|
|
48
|
+
pointer-events: none;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.close-btn {
|
|
52
|
+
position: absolute;
|
|
53
|
+
top: 8px;
|
|
54
|
+
right: 8px;
|
|
55
|
+
background: rgba(255, 255, 255, 0.1);
|
|
56
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
57
|
+
font-size: 16px;
|
|
58
|
+
color: rgba(255, 255, 255, 0.9);
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
width: 22px;
|
|
61
|
+
height: 22px;
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: center;
|
|
65
|
+
border-radius: 50%;
|
|
66
|
+
transition: all 0.2s ease;
|
|
67
|
+
backdrop-filter: blur(10px);
|
|
68
|
+
-webkit-backdrop-filter: blur(10px);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.close-btn:hover {
|
|
72
|
+
background-color: rgba(255, 255, 255, 0.25);
|
|
73
|
+
border-color: rgba(255, 255, 255, 0.4);
|
|
74
|
+
transform: scale(1.1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#hint-toggle {
|
|
78
|
+
position: fixed;
|
|
79
|
+
top: 16px;
|
|
80
|
+
left: 16px;
|
|
81
|
+
background: rgba(255, 255, 255, 0.15);
|
|
82
|
+
backdrop-filter: blur(15px) saturate(180%);
|
|
83
|
+
-webkit-backdrop-filter: blur(15px) saturate(180%);
|
|
84
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
85
|
+
color: rgba(255, 255, 255, 0.95);
|
|
86
|
+
padding: 6px 10px;
|
|
87
|
+
border-radius: 8px;
|
|
88
|
+
font-size: 12px;
|
|
89
|
+
font-weight: 500;
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
z-index: 99;
|
|
92
|
+
display: none;
|
|
93
|
+
transition: all 0.2s ease;
|
|
94
|
+
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.15),
|
|
95
|
+
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
|
96
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
97
|
+
animation: hint-sparkle 2.5s ease-in-out infinite;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#hint-toggle:hover {
|
|
101
|
+
background: rgba(255, 255, 255, 0.25);
|
|
102
|
+
border-color: rgba(255, 255, 255, 0.4);
|
|
103
|
+
transform: translateY(-1px) scale(1.05);
|
|
104
|
+
animation: hint-sparkle-fast 1s ease-in-out infinite;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@keyframes hint-sparkle {
|
|
108
|
+
0%, 100% {
|
|
109
|
+
filter: brightness(1) drop-shadow(0 0 3px rgba(139, 92, 246, 0.3));
|
|
110
|
+
transform: scale(1) rotate(0deg);
|
|
111
|
+
}
|
|
112
|
+
25% {
|
|
113
|
+
filter: brightness(1.1) drop-shadow(0 0 6px rgba(139, 92, 246, 0.5));
|
|
114
|
+
transform: scale(1.02) rotate(2deg);
|
|
115
|
+
}
|
|
116
|
+
50% {
|
|
117
|
+
filter: brightness(1.2) drop-shadow(0 0 8px rgba(139, 92, 246, 0.7));
|
|
118
|
+
transform: scale(1.05) rotate(0deg);
|
|
119
|
+
}
|
|
120
|
+
75% {
|
|
121
|
+
filter: brightness(1.1) drop-shadow(0 0 6px rgba(139, 92, 246, 0.5));
|
|
122
|
+
transform: scale(1.02) rotate(-2deg);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@keyframes hint-sparkle-fast {
|
|
127
|
+
0%, 100% {
|
|
128
|
+
filter: brightness(1.1) drop-shadow(0 0 5px rgba(139, 92, 246, 0.6));
|
|
129
|
+
transform: scale(1.05) rotate(0deg);
|
|
130
|
+
}
|
|
131
|
+
50% {
|
|
132
|
+
filter: brightness(1.3) drop-shadow(0 0 10px rgba(139, 92, 246, 0.8));
|
|
133
|
+
transform: scale(1.1) rotate(5deg);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.close-btn {
|
|
138
|
+
position: absolute;
|
|
139
|
+
top: 8px;
|
|
140
|
+
right: 8px;
|
|
141
|
+
background: none;
|
|
142
|
+
border: none;
|
|
143
|
+
font-size: 16px;
|
|
144
|
+
color: #cbd5e0;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
width: 20px;
|
|
147
|
+
height: 20px;
|
|
148
|
+
display: flex;
|
|
149
|
+
align-items: center;
|
|
150
|
+
justify-content: center;
|
|
151
|
+
border-radius: 50%;
|
|
152
|
+
transition: background-color 0.2s ease;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.close-btn:hover {
|
|
156
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#hint-toggle {
|
|
160
|
+
position: fixed;
|
|
161
|
+
top: 16px;
|
|
162
|
+
left: 16px;
|
|
163
|
+
background: rgba(30, 40, 60, 0.9);
|
|
164
|
+
backdrop-filter: blur(10px);
|
|
165
|
+
-webkit-backdrop-filter: blur(10px);
|
|
166
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
167
|
+
color: #f7fafc;
|
|
168
|
+
padding: 8px 12px;
|
|
169
|
+
border-radius: 6px;
|
|
170
|
+
font-size: 12px;
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
z-index: 99;
|
|
173
|
+
display: none;
|
|
174
|
+
transition: all 0.2s ease;
|
|
175
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#hint-toggle:hover {
|
|
179
|
+
background: rgba(30, 40, 60, 1);
|
|
180
|
+
transform: translateY(-1px);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.navigation {
|
|
184
|
+
position: fixed;
|
|
185
|
+
top: 16px;
|
|
186
|
+
right: 16px;
|
|
187
|
+
z-index: 100;
|
|
188
|
+
display: flex;
|
|
189
|
+
gap: 8px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.nav-link {
|
|
193
|
+
background: rgba(30, 40, 60, 0.9);
|
|
194
|
+
backdrop-filter: blur(10px);
|
|
195
|
+
-webkit-backdrop-filter: blur(10px);
|
|
196
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
197
|
+
color: #f7fafc;
|
|
198
|
+
text-decoration: none;
|
|
199
|
+
padding: 8px 12px;
|
|
200
|
+
border-radius: 6px;
|
|
201
|
+
font-size: 12px;
|
|
202
|
+
transition: all 0.2s ease;
|
|
203
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.nav-link:hover {
|
|
207
|
+
background: rgba(30, 40, 60, 1);
|
|
208
|
+
transform: translateY(-1px);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.title {
|
|
212
|
+
font-size: 20px;
|
|
213
|
+
font-weight: bold;
|
|
214
|
+
margin-bottom: 10px;
|
|
215
|
+
background: linear-gradient(45deg, #ff6b9d, #c44569, #6c7b95);
|
|
216
|
+
background-clip: text;
|
|
217
|
+
-webkit-background-clip: text;
|
|
218
|
+
-webkit-text-fill-color: transparent;
|
|
219
|
+
text-align: center;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.description {
|
|
223
|
+
font-size: 14px;
|
|
224
|
+
line-height: 1.6;
|
|
225
|
+
margin-bottom: 15px;
|
|
226
|
+
opacity: 0.9;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.controls {
|
|
230
|
+
text-align: center;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.control-item {
|
|
234
|
+
margin: 8px 0;
|
|
235
|
+
font-size: 13px;
|
|
236
|
+
opacity: 0.8;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.emoji {
|
|
240
|
+
display: inline-block;
|
|
241
|
+
margin-right: 8px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.highlight {
|
|
245
|
+
color: #ff6b9d;
|
|
246
|
+
font-weight: bold;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.navigation {
|
|
250
|
+
position: fixed;
|
|
251
|
+
top: 16px;
|
|
252
|
+
right: 20px;
|
|
253
|
+
z-index: 100;
|
|
254
|
+
display: flex;
|
|
255
|
+
gap: 8px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.nav-link {
|
|
259
|
+
background: rgba(30, 40, 60, 0.9);
|
|
260
|
+
backdrop-filter: blur(10px);
|
|
261
|
+
-webkit-backdrop-filter: blur(10px);
|
|
262
|
+
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
263
|
+
color: #f7fafc;
|
|
264
|
+
text-decoration: none;
|
|
265
|
+
padding: 8px 12px;
|
|
266
|
+
border-radius: 6px;
|
|
267
|
+
font-size: 12px;
|
|
268
|
+
transition: all 0.2s ease;
|
|
269
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.nav-link:hover {
|
|
273
|
+
background: rgba(139, 92, 246, 0.2);
|
|
274
|
+
transform: translateY(-1px);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.demo-links {
|
|
278
|
+
position: fixed;
|
|
279
|
+
bottom: 20px;
|
|
280
|
+
right: 100px;
|
|
281
|
+
z-index: 100;
|
|
282
|
+
display: flex;
|
|
283
|
+
gap: 8px;
|
|
284
|
+
flex-wrap: wrap;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.demo-link {
|
|
288
|
+
padding: 8px 14px;
|
|
289
|
+
background: rgba(255, 255, 255, 0.15);
|
|
290
|
+
color: rgba(255, 255, 255, 0.95);
|
|
291
|
+
text-decoration: none;
|
|
292
|
+
border-radius: 12px;
|
|
293
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
294
|
+
font-size: 11px;
|
|
295
|
+
font-weight: 500;
|
|
296
|
+
transition: all 0.3s ease;
|
|
297
|
+
backdrop-filter: blur(15px) saturate(180%);
|
|
298
|
+
-webkit-backdrop-filter: blur(15px) saturate(180%);
|
|
299
|
+
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.15),
|
|
300
|
+
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
|
301
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.demo-link:hover {
|
|
305
|
+
background: rgba(255, 255, 255, 0.25);
|
|
306
|
+
border-color: rgba(255, 255, 255, 0.4);
|
|
307
|
+
transform: translateY(-2px);
|
|
308
|
+
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.25),
|
|
309
|
+
inset 0 1px 2px rgba(255, 255, 255, 0.4);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
#audio-status {
|
|
313
|
+
position: fixed;
|
|
314
|
+
top: 70px;
|
|
315
|
+
right: 20px;
|
|
316
|
+
z-index: 100;
|
|
317
|
+
background: rgba(0, 0, 0, 0.7);
|
|
318
|
+
padding: 10px 15px;
|
|
319
|
+
border-radius: 20px;
|
|
320
|
+
font-size: 12px;
|
|
321
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.audio-active {
|
|
325
|
+
color: #4CAF50 !important;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.audio-inactive {
|
|
329
|
+
color: #ff6b9d !important;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.music-icon {
|
|
333
|
+
display: inline-block;
|
|
334
|
+
animation: music-pulse 2.5s ease-in-out infinite;
|
|
335
|
+
cursor: pointer;
|
|
336
|
+
color: #ff6b9d;
|
|
337
|
+
text-shadow: 0 0 10px currentColor;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.music-icon:hover {
|
|
341
|
+
transform: scale(1.3);
|
|
342
|
+
animation: music-dance 0.8s ease-in-out infinite;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
@keyframes music-pulse {
|
|
346
|
+
0%, 100% {
|
|
347
|
+
transform: scale(1);
|
|
348
|
+
filter: brightness(1);
|
|
349
|
+
}
|
|
350
|
+
50% {
|
|
351
|
+
transform: scale(1.1);
|
|
352
|
+
filter: brightness(1.3);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@keyframes music-dance {
|
|
357
|
+
0%, 100% {
|
|
358
|
+
transform: scale(1.3) rotate(0deg);
|
|
359
|
+
}
|
|
360
|
+
25% {
|
|
361
|
+
transform: scale(1.4) rotate(-5deg);
|
|
362
|
+
}
|
|
363
|
+
75% {
|
|
364
|
+
transform: scale(1.4) rotate(5deg);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#info strong {
|
|
369
|
+
font-size: 13px;
|
|
370
|
+
font-weight: 600;
|
|
371
|
+
color: rgba(255, 255, 255, 0.98);
|
|
372
|
+
display: block;
|
|
373
|
+
margin-bottom: 10px;
|
|
374
|
+
text-align: center;
|
|
375
|
+
letter-spacing: 0.5px;
|
|
376
|
+
text-shadow: 0 2px 4px rgba(139, 92, 246, 0.3);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#info code {
|
|
380
|
+
background: rgba(255, 255, 255, 0.15);
|
|
381
|
+
color: rgba(255, 255, 255, 0.95);
|
|
382
|
+
padding: 3px 6px;
|
|
383
|
+
border-radius: 4px;
|
|
384
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
385
|
+
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
|
386
|
+
font-size: 11px;
|
|
387
|
+
font-weight: 500;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.controls {
|
|
391
|
+
margin-top: 10px;
|
|
392
|
+
padding-top: 10px;
|
|
393
|
+
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
|
394
|
+
font-size: 11px;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.control-item {
|
|
398
|
+
margin: 5px 0;
|
|
399
|
+
display: flex;
|
|
400
|
+
align-items: center;
|
|
401
|
+
gap: 8px;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.key {
|
|
405
|
+
background: rgba(255, 255, 255, 0.2);
|
|
406
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
407
|
+
padding: 3px 8px;
|
|
408
|
+
border-radius: 5px;
|
|
409
|
+
font-weight: 500;
|
|
410
|
+
min-width: fit-content;
|
|
411
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1),
|
|
412
|
+
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* 統合メニュー */
|
|
416
|
+
.unified-menu {
|
|
417
|
+
position: fixed;
|
|
418
|
+
top: 16px;
|
|
419
|
+
right: 20px;
|
|
420
|
+
z-index: 1000;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.menu-toggle {
|
|
424
|
+
background: rgba(255, 255, 255, 0.15);
|
|
425
|
+
backdrop-filter: blur(16px);
|
|
426
|
+
-webkit-backdrop-filter: blur(16px);
|
|
427
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
428
|
+
color: #2d3748;
|
|
429
|
+
padding: 10px 14px;
|
|
430
|
+
border-radius: 8px;
|
|
431
|
+
font-size: 18px;
|
|
432
|
+
cursor: pointer;
|
|
433
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
434
|
+
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.2), 0 2px 6px rgba(0, 0, 0, 0.05);
|
|
435
|
+
width: 44px;
|
|
436
|
+
height: 44px;
|
|
437
|
+
display: flex;
|
|
438
|
+
align-items: center;
|
|
439
|
+
justify-content: center;
|
|
440
|
+
opacity: 0.8;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.menu-toggle:hover {
|
|
444
|
+
background: rgba(255, 255, 255, 0.25);
|
|
445
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
446
|
+
transform: scale(1.1) translateY(-2px);
|
|
447
|
+
box-shadow: 0 6px 16px rgba(139, 92, 246, 0.3), 0 3px 8px rgba(0, 0, 0, 0.1);
|
|
448
|
+
opacity: 1;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.menu-dropdown {
|
|
452
|
+
position: absolute;
|
|
453
|
+
top: 56px;
|
|
454
|
+
right: 0;
|
|
455
|
+
min-width: 220px;
|
|
456
|
+
background: rgba(255, 255, 255, 0.85);
|
|
457
|
+
backdrop-filter: blur(16px) saturate(180%);
|
|
458
|
+
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
459
|
+
border: 1px solid rgba(139, 92, 246, 0.2);
|
|
460
|
+
border-radius: 12px;
|
|
461
|
+
box-shadow: 0 8px 24px rgba(139, 92, 246, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
462
|
+
opacity: 0;
|
|
463
|
+
visibility: hidden;
|
|
464
|
+
transform: translateY(-10px);
|
|
465
|
+
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
466
|
+
overflow: hidden;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.menu-dropdown.active {
|
|
470
|
+
opacity: 1;
|
|
471
|
+
visibility: visible;
|
|
472
|
+
transform: translateY(0);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.menu-item {
|
|
476
|
+
display: block;
|
|
477
|
+
padding: 12px 16px;
|
|
478
|
+
color: #2d3748;
|
|
479
|
+
text-decoration: none;
|
|
480
|
+
font-size: 13px;
|
|
481
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
482
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
483
|
+
cursor: pointer;
|
|
484
|
+
background: none;
|
|
485
|
+
border-left: none;
|
|
486
|
+
border-right: none;
|
|
487
|
+
border-top: none;
|
|
488
|
+
width: 100%;
|
|
489
|
+
text-align: left;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.menu-item:last-child {
|
|
493
|
+
border-bottom: none;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.menu-item:hover {
|
|
497
|
+
background: rgba(139, 92, 246, 0.08);
|
|
498
|
+
padding-left: 20px;
|
|
499
|
+
border-left: 2px solid rgba(139, 92, 246, 0.4);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.menu-item.has-submenu {
|
|
503
|
+
position: relative;
|
|
504
|
+
padding: 0;
|
|
505
|
+
border: none;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.submenu-toggle {
|
|
509
|
+
display: block;
|
|
510
|
+
padding: 12px 16px;
|
|
511
|
+
color: #2d3748;
|
|
512
|
+
font-size: 13px;
|
|
513
|
+
background: none;
|
|
514
|
+
border: none;
|
|
515
|
+
width: 100%;
|
|
516
|
+
text-align: left;
|
|
517
|
+
cursor: pointer;
|
|
518
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
519
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.submenu-toggle:hover {
|
|
523
|
+
background: rgba(139, 92, 246, 0.08);
|
|
524
|
+
padding-left: 20px;
|
|
525
|
+
border-left: 2px solid rgba(139, 92, 246, 0.4);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.submenu {
|
|
529
|
+
display: none;
|
|
530
|
+
background: rgba(139, 92, 246, 0.03);
|
|
531
|
+
border-top: 1px solid rgba(139, 92, 246, 0.1);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.submenu.active {
|
|
535
|
+
display: block;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.submenu .menu-item {
|
|
539
|
+
padding-left: 32px;
|
|
540
|
+
font-size: 12px;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.submenu .menu-item:hover {
|
|
544
|
+
padding-left: 36px;
|
|
545
|
+
background: rgba(139, 92, 246, 0.12);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* メニューアイコンのバウンスアニメーション */
|
|
549
|
+
.menu-icon {
|
|
550
|
+
display: inline-block;
|
|
551
|
+
animation: menu-bounce 2s ease-in-out infinite;
|
|
552
|
+
cursor: pointer;
|
|
553
|
+
transition: transform 0.2s ease;
|
|
554
|
+
text-shadow: 0 0 10px currentColor;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.menu-icon:hover {
|
|
558
|
+
transform: scale(1.4);
|
|
559
|
+
animation: menu-bounce-fast 0.8s ease-in-out infinite;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
@keyframes menu-bounce {
|
|
563
|
+
0%, 100% {
|
|
564
|
+
filter: brightness(1) drop-shadow(0 0 8px currentColor);
|
|
565
|
+
transform: scale(1) translateY(0px);
|
|
566
|
+
}
|
|
567
|
+
25% {
|
|
568
|
+
filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
|
|
569
|
+
transform: scale(1.1) translateY(-4px);
|
|
570
|
+
}
|
|
571
|
+
50% {
|
|
572
|
+
filter: brightness(1.5) drop-shadow(0 0 20px currentColor);
|
|
573
|
+
transform: scale(1.15) translateY(-8px);
|
|
574
|
+
}
|
|
575
|
+
75% {
|
|
576
|
+
filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
|
|
577
|
+
transform: scale(1.1) translateY(-4px);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
@keyframes menu-bounce-fast {
|
|
582
|
+
0%, 100% {
|
|
583
|
+
filter: brightness(1.4) drop-shadow(0 0 15px currentColor);
|
|
584
|
+
transform: translateY(0px);
|
|
585
|
+
}
|
|
586
|
+
50% {
|
|
587
|
+
filter: brightness(1.8) drop-shadow(0 0 25px currentColor);
|
|
588
|
+
transform: translateY(-12px);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/* ChocoDrop 動画要素が花に埋もれないようにする */
|
|
593
|
+
video {
|
|
594
|
+
z-index: 20 !important;
|
|
595
|
+
position: relative !important;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/* ChocoDrop UI要素のz-index */
|
|
599
|
+
.chocodrop-ui, .chocodrop-overlay {
|
|
600
|
+
z-index: 100 !important;
|
|
601
|
+
}
|
|
602
|
+
</style>
|
|
603
|
+
</head>
|
|
604
|
+
<body>
|
|
605
|
+
<!-- 統合メニュー -->
|
|
606
|
+
<div class="unified-menu">
|
|
607
|
+
<button class="menu-toggle" id="unifiedMenuToggle" aria-label="Menu">☰</button>
|
|
608
|
+
<div class="menu-dropdown" id="unifiedMenuDropdown">
|
|
609
|
+
<button class="menu-item" id="showInfoBtn"><span class="menu-icon">📖</span> このワールドについて</button>
|
|
610
|
+
|
|
611
|
+
<div class="menu-item has-submenu">
|
|
612
|
+
<button class="submenu-toggle" id="worldsMenuToggle"><span class="menu-icon">🌍</span> 世界選択 <span style="font-size: 10px;">▼</span></button>
|
|
613
|
+
<div class="submenu" id="worldsSubmenu">
|
|
614
|
+
<a href="../basic/index.html" class="menu-item"><span class="menu-icon">🍫</span> はじまりの国</a>
|
|
615
|
+
<a href="../pixel-ocean/index.html" class="menu-item"><span class="menu-icon">🌊</span> 海底世界</a>
|
|
616
|
+
<a href="../wabi-sabi/index.html" class="menu-item"><span class="menu-icon">🖤</span> 侘び寂びの世界</a>
|
|
617
|
+
<a href="../space/index.html" class="menu-item"><span class="menu-icon">🌌</span> 宇宙空間</a>
|
|
618
|
+
<a href="../toy-city/index.html" class="menu-item"><span class="menu-icon">🎡</span> AI遊園地</a>
|
|
619
|
+
<a href="index.html" class="menu-item"><span class="menu-icon">🌸</span> 硝子の花(現在地)</a>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
<a href="../../index.html" class="menu-item"><span class="menu-icon">🏠</span> Home</a>
|
|
624
|
+
<a href="https://github.com/nyukicorn/chocodrop" class="menu-item" target="_blank"><span class="menu-icon">🔗</span> GitHub</a>
|
|
625
|
+
|
|
626
|
+
<button class="menu-item" id="vrMenuButton"><span class="menu-icon">🥽</span> VR</button>
|
|
627
|
+
<button class="menu-item" id="arMenuButton"><span class="menu-icon">🥽</span> AR</button>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<!-- Navigation Links (hidden - replaced by unified menu) -->
|
|
632
|
+
<div class="navigation" style="display: none;">
|
|
633
|
+
<a href="../../index.html" class="nav-link">🏠 Home</a>
|
|
634
|
+
<a href="https://github.com/nyukicorn/chocodrop" class="nav-link" target="_blank">📦 GitHub</a>
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
<!-- Hint Toggle Button (initially hidden) -->
|
|
638
|
+
<button id="hint-toggle">💡</button>
|
|
639
|
+
|
|
640
|
+
<!-- Main Info Panel -->
|
|
641
|
+
<div id="info">
|
|
642
|
+
<button class="close-btn" onclick="hideInfoPanel()">×</button>
|
|
643
|
+
<strong>🌸 硝子のかけらの花へようこそ</strong>
|
|
644
|
+
|
|
645
|
+
<div style="font-size: 11px; opacity: 0.9; margin-bottom: 12px; line-height: 1.6; text-align: center; font-style: italic;">
|
|
646
|
+
散らばった硝子のかけらたちが集まり寄り添い、新たな花をそっと咲かせる世界
|
|
647
|
+
</div>
|
|
648
|
+
|
|
649
|
+
💡 基本操作:<br>
|
|
650
|
+
1. 動画や画像をインポートしてみて!<br>
|
|
651
|
+
2. ✨ 音楽を鳴らすと硝子の花たちがキラキラと呼吸を始めます<br>
|
|
652
|
+
3. 視点操作: ChocoDrop を最小化してドラッグ<br>
|
|
653
|
+
4. オブジェクトをクリック → WASDキーで移動<br>
|
|
654
|
+
|
|
655
|
+
<div class="controls">
|
|
656
|
+
<div class="control-item">
|
|
657
|
+
<span class="key">@</span>
|
|
658
|
+
<span>ChocoDrop起動</span>
|
|
659
|
+
</div>
|
|
660
|
+
<div class="control-item">
|
|
661
|
+
<span class="key">🍫</span>
|
|
662
|
+
<span>右下ボタンからも起動可</span>
|
|
663
|
+
</div>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<div id="audio-status" class="audio-inactive">🎤 音声待機中...</div>
|
|
668
|
+
|
|
669
|
+
<!-- Demo Links (hidden - replaced by unified menu) -->
|
|
670
|
+
<div class="demo-links" style="display: none;">
|
|
671
|
+
<a href="../basic/index.html" class="demo-link">🍫 はじまりの国</a>
|
|
672
|
+
<a href="../pixel-ocean/index.html" class="demo-link">🌊 海底世界</a>
|
|
673
|
+
<a href="../wabi-sabi/index.html" class="demo-link">🖤 侘び寂び</a>
|
|
674
|
+
<a href="../space/index.html" class="demo-link">🌌 宇宙空間</a>
|
|
675
|
+
<a href="../toy-city/index.html" class="demo-link">🎡 AI遊園地</a>
|
|
676
|
+
</div>
|
|
677
|
+
|
|
678
|
+
<!-- Three.js + ChocoDrop (module preload) -->
|
|
679
|
+
<script type="module">
|
|
680
|
+
import ensureChocoDrop from '../../public/load-chocodrop.js';
|
|
681
|
+
|
|
682
|
+
// 🎯 情報パネル制御を先行初期化してUXを安定化
|
|
683
|
+
let autoHideTimer = null;
|
|
684
|
+
|
|
685
|
+
const getInfoPanel = () => document.getElementById('info');
|
|
686
|
+
const getHintToggle = () => document.getElementById('hint-toggle');
|
|
687
|
+
|
|
688
|
+
const clearInfoPanelAutoHide = () => {
|
|
689
|
+
if (autoHideTimer) {
|
|
690
|
+
clearTimeout(autoHideTimer);
|
|
691
|
+
autoHideTimer = null;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
const scheduleInfoPanelAutoHide = () => {
|
|
696
|
+
clearInfoPanelAutoHide();
|
|
697
|
+
autoHideTimer = window.setTimeout(() => {
|
|
698
|
+
if (typeof window.hideInfoPanel === 'function') {
|
|
699
|
+
window.hideInfoPanel();
|
|
700
|
+
}
|
|
701
|
+
}, 30000);
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
window.hideInfoPanel = function hideInfoPanel() {
|
|
705
|
+
const infoPanel = getInfoPanel();
|
|
706
|
+
if (!infoPanel) {
|
|
707
|
+
clearInfoPanelAutoHide();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
infoPanel.classList.add('hidden');
|
|
712
|
+
window.setTimeout(() => {
|
|
713
|
+
if (infoPanel.classList.contains('hidden')) {
|
|
714
|
+
infoPanel.style.display = 'none';
|
|
715
|
+
const hintToggle = getHintToggle();
|
|
716
|
+
if (hintToggle) {
|
|
717
|
+
hintToggle.style.display = 'block';
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}, 300);
|
|
721
|
+
|
|
722
|
+
clearInfoPanelAutoHide();
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
window.showInfoPanel = function showInfoPanel() {
|
|
726
|
+
const infoPanel = getInfoPanel();
|
|
727
|
+
if (!infoPanel) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const hintToggle = getHintToggle();
|
|
732
|
+
if (hintToggle) {
|
|
733
|
+
hintToggle.style.display = 'none';
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
infoPanel.style.display = 'block';
|
|
737
|
+
window.requestAnimationFrame(() => {
|
|
738
|
+
infoPanel.classList.remove('hidden');
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
scheduleInfoPanelAutoHide();
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const initialHintToggle = getHintToggle();
|
|
745
|
+
if (initialHintToggle) {
|
|
746
|
+
initialHintToggle.addEventListener('click', window.showInfoPanel);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
(async () => {
|
|
750
|
+
await ensureChocoDrop();
|
|
751
|
+
|
|
752
|
+
const THREE = window.THREE;
|
|
753
|
+
const ChocoDrop = window.ChocoDrop;
|
|
754
|
+
|
|
755
|
+
// 統合メニューの制御
|
|
756
|
+
|
|
757
|
+
(function() {
|
|
758
|
+
const menuToggle = document.getElementById('unifiedMenuToggle');
|
|
759
|
+
const menuDropdown = document.getElementById('unifiedMenuDropdown');
|
|
760
|
+
const worldsMenuToggle = document.getElementById('worldsMenuToggle');
|
|
761
|
+
const worldsSubmenu = document.getElementById('worldsSubmenu');
|
|
762
|
+
const showInfoBtn = document.getElementById('showInfoBtn');
|
|
763
|
+
const infoPanel = document.getElementById('info');
|
|
764
|
+
|
|
765
|
+
// 初回訪問チェック - localStorage使用
|
|
766
|
+
const FIRST_VISIT_KEY = 'music_garden_visited';
|
|
767
|
+
const hasVisited = localStorage.getItem(FIRST_VISIT_KEY);
|
|
768
|
+
|
|
769
|
+
if (!hasVisited) {
|
|
770
|
+
// 初回訪問時は情報パネルを表示
|
|
771
|
+
if (infoPanel) {
|
|
772
|
+
infoPanel.style.display = 'block';
|
|
773
|
+
}
|
|
774
|
+
localStorage.setItem(FIRST_VISIT_KEY, 'true');
|
|
775
|
+
} else {
|
|
776
|
+
// 2回目以降は非表示
|
|
777
|
+
if (infoPanel) {
|
|
778
|
+
infoPanel.style.display = 'none';
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// 統合メニューのトグル
|
|
783
|
+
if (menuToggle && menuDropdown) {
|
|
784
|
+
menuToggle.addEventListener('click', (e) => {
|
|
785
|
+
e.stopPropagation();
|
|
786
|
+
menuDropdown.classList.toggle('active');
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// 外側クリックで閉じる
|
|
790
|
+
document.addEventListener('click', (e) => {
|
|
791
|
+
if (!menuToggle.contains(e.target) && !menuDropdown.contains(e.target)) {
|
|
792
|
+
menuDropdown.classList.remove('active');
|
|
793
|
+
// サブメニューも閉じる
|
|
794
|
+
if (worldsSubmenu) {
|
|
795
|
+
worldsSubmenu.classList.remove('active');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// Escキーで閉じる
|
|
801
|
+
document.addEventListener('keydown', (e) => {
|
|
802
|
+
if (e.key === 'Escape') {
|
|
803
|
+
menuDropdown.classList.remove('active');
|
|
804
|
+
if (worldsSubmenu) {
|
|
805
|
+
worldsSubmenu.classList.remove('active');
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// 世界選択サブメニューのトグル
|
|
812
|
+
if (worldsMenuToggle && worldsSubmenu) {
|
|
813
|
+
worldsMenuToggle.addEventListener('click', (e) => {
|
|
814
|
+
e.stopPropagation();
|
|
815
|
+
worldsSubmenu.classList.toggle('active');
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// 情報パネル表示ボタン
|
|
820
|
+
if (showInfoBtn) {
|
|
821
|
+
showInfoBtn.addEventListener('click', () => {
|
|
822
|
+
// グローバルに定義された showInfoPanel() を呼び出す
|
|
823
|
+
if (typeof showInfoPanel === 'function') {
|
|
824
|
+
showInfoPanel();
|
|
825
|
+
} else {
|
|
826
|
+
// フォールバック: 直接表示
|
|
827
|
+
const panel = document.getElementById('info');
|
|
828
|
+
if (panel) {
|
|
829
|
+
panel.style.display = 'block';
|
|
830
|
+
panel.classList.remove('hidden');
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
menuDropdown.classList.remove('active');
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// VR/ARボタンは後でThree.jsのrendererと連携
|
|
838
|
+
// グローバル変数として保存
|
|
839
|
+
window.unifiedMenuVRButton = document.getElementById('vrMenuButton');
|
|
840
|
+
window.unifiedMenuARButton = document.getElementById('arMenuButton');
|
|
841
|
+
})();
|
|
842
|
+
|
|
843
|
+
// VR/AR Button inline implementation
|
|
844
|
+
// VRButton implementation
|
|
845
|
+
const VRButton = {
|
|
846
|
+
createButton: function(renderer) {
|
|
847
|
+
const button = document.createElement('button');
|
|
848
|
+
|
|
849
|
+
function showEnterVR() {
|
|
850
|
+
button.style.display = '';
|
|
851
|
+
button.style.cursor = 'pointer';
|
|
852
|
+
button.style.left = 'calc(50% - 50px)';
|
|
853
|
+
button.style.width = '100px';
|
|
854
|
+
button.textContent = 'ENTER VR';
|
|
855
|
+
button.onmouseenter = function() { button.style.opacity = '1.0'; };
|
|
856
|
+
button.onmouseleave = function() { button.style.opacity = '0.5'; };
|
|
857
|
+
button.onclick = function() {
|
|
858
|
+
const session = renderer.xr.getSession();
|
|
859
|
+
if (session) {
|
|
860
|
+
session.end();
|
|
861
|
+
} else {
|
|
862
|
+
navigator.xr.requestSession('immersive-vr', {
|
|
863
|
+
optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking']
|
|
864
|
+
}).then(session => {
|
|
865
|
+
renderer.xr.setSession(session);
|
|
866
|
+
button.textContent = 'EXIT VR';
|
|
867
|
+
|
|
868
|
+
session.addEventListener('end', () => {
|
|
869
|
+
button.textContent = 'ENTER VR';
|
|
870
|
+
});
|
|
871
|
+
}).catch(err => {
|
|
872
|
+
console.error('VR session failed:', err);
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function showWebXRNotFound() {
|
|
879
|
+
button.style.display = '';
|
|
880
|
+
button.style.cursor = 'auto';
|
|
881
|
+
button.style.left = 'calc(50% - 75px)';
|
|
882
|
+
button.style.width = '150px';
|
|
883
|
+
button.textContent = 'VR NOT SUPPORTED';
|
|
884
|
+
button.onmouseenter = null;
|
|
885
|
+
button.onmouseleave = null;
|
|
886
|
+
button.onclick = null;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
button.style.position = 'absolute';
|
|
890
|
+
button.style.bottom = '20px';
|
|
891
|
+
button.style.padding = '12px 6px';
|
|
892
|
+
button.style.border = '1px solid #fff';
|
|
893
|
+
button.style.borderRadius = '4px';
|
|
894
|
+
button.style.background = 'rgba(0,0,0,0.1)';
|
|
895
|
+
button.style.color = '#fff';
|
|
896
|
+
button.style.font = 'normal 13px sans-serif';
|
|
897
|
+
button.style.textAlign = 'center';
|
|
898
|
+
button.style.opacity = '0.5';
|
|
899
|
+
button.style.outline = 'none';
|
|
900
|
+
button.style.zIndex = '999';
|
|
901
|
+
|
|
902
|
+
if ('xr' in navigator) {
|
|
903
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
904
|
+
supported ? showEnterVR() : showWebXRNotFound();
|
|
905
|
+
});
|
|
906
|
+
} else {
|
|
907
|
+
showWebXRNotFound();
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return button;
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
// ARButton implementation
|
|
915
|
+
const ARButton = {
|
|
916
|
+
createButton: function(renderer, options) {
|
|
917
|
+
const button = document.createElement('button');
|
|
918
|
+
|
|
919
|
+
function showEnterAR() {
|
|
920
|
+
button.style.display = '';
|
|
921
|
+
button.style.cursor = 'pointer';
|
|
922
|
+
button.style.left = 'calc(50% - 50px)';
|
|
923
|
+
button.style.width = '100px';
|
|
924
|
+
button.textContent = 'ENTER AR';
|
|
925
|
+
button.onmouseenter = function() { button.style.opacity = '1.0'; };
|
|
926
|
+
button.onmouseleave = function() { button.style.opacity = '0.5'; };
|
|
927
|
+
button.onclick = function() {
|
|
928
|
+
const session = renderer.xr.getSession();
|
|
929
|
+
if (session) {
|
|
930
|
+
session.end();
|
|
931
|
+
} else {
|
|
932
|
+
navigator.xr.requestSession('immersive-ar', {
|
|
933
|
+
optionalFeatures: ['hit-test', 'dom-overlay', 'local-floor'],
|
|
934
|
+
domOverlay: options?.domOverlay || { root: document.body }
|
|
935
|
+
}).then(session => {
|
|
936
|
+
renderer.xr.setSession(session);
|
|
937
|
+
button.textContent = 'EXIT AR';
|
|
938
|
+
|
|
939
|
+
session.addEventListener('end', () => {
|
|
940
|
+
button.textContent = 'ENTER AR';
|
|
941
|
+
});
|
|
942
|
+
}).catch(err => {
|
|
943
|
+
console.error('AR session failed:', err);
|
|
944
|
+
alert('AR not supported: ' + err.message);
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function showWebXRNotFound() {
|
|
951
|
+
button.style.display = '';
|
|
952
|
+
button.style.cursor = 'auto';
|
|
953
|
+
button.style.left = 'calc(50% - 75px)';
|
|
954
|
+
button.style.width = '150px';
|
|
955
|
+
button.textContent = 'AR NOT SUPPORTED';
|
|
956
|
+
button.onmouseenter = null;
|
|
957
|
+
button.onmouseleave = null;
|
|
958
|
+
button.onclick = null;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
button.style.position = 'absolute';
|
|
962
|
+
button.style.bottom = '70px';
|
|
963
|
+
button.style.padding = '12px 6px';
|
|
964
|
+
button.style.border = '1px solid #fff';
|
|
965
|
+
button.style.borderRadius = '4px';
|
|
966
|
+
button.style.background = 'rgba(0,0,0,0.1)';
|
|
967
|
+
button.style.color = '#fff';
|
|
968
|
+
button.style.font = 'normal 13px sans-serif';
|
|
969
|
+
button.style.textAlign = 'center';
|
|
970
|
+
button.style.opacity = '0.5';
|
|
971
|
+
button.style.outline = 'none';
|
|
972
|
+
button.style.zIndex = '999';
|
|
973
|
+
|
|
974
|
+
if ('xr' in navigator) {
|
|
975
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
976
|
+
supported ? showEnterAR() : showWebXRNotFound();
|
|
977
|
+
});
|
|
978
|
+
} else {
|
|
979
|
+
showWebXRNotFound();
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return button;
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
console.log('✅ VRButton/ARButton inline implementation loaded');
|
|
987
|
+
|
|
988
|
+
console.log('🌸 Music Garden Loading...');
|
|
989
|
+
console.log('THREE loaded:', typeof THREE !== 'undefined');
|
|
990
|
+
|
|
991
|
+
// Basic Three.js setup
|
|
992
|
+
const scene = new THREE.Scene();
|
|
993
|
+
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
994
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
995
|
+
|
|
996
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
997
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
998
|
+
renderer.setClearColor(0x0a0f1e, 1); // 深い青紫の背景
|
|
999
|
+
renderer.xr.enabled = true; // VR/AR有効化
|
|
1000
|
+
|
|
1001
|
+
// Three.js キャンバスのz-indexを設定(動画より背景に配置)
|
|
1002
|
+
renderer.domElement.style.position = 'fixed';
|
|
1003
|
+
renderer.domElement.style.top = '0';
|
|
1004
|
+
renderer.domElement.style.left = '0';
|
|
1005
|
+
renderer.domElement.style.zIndex = '1';
|
|
1006
|
+
|
|
1007
|
+
document.body.appendChild(renderer.domElement);
|
|
1008
|
+
|
|
1009
|
+
// VR/ARボタンを統合メニューと連携(bodyには追加しない)
|
|
1010
|
+
if (typeof VRButton !== 'undefined' && window.unifiedMenuVRButton) {
|
|
1011
|
+
console.log('✅ VRButton script loaded - connecting to unified menu');
|
|
1012
|
+
|
|
1013
|
+
// VRセッション開始関数を作成
|
|
1014
|
+
function startVRSession() {
|
|
1015
|
+
if ('xr' in navigator) {
|
|
1016
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
1017
|
+
if (supported) {
|
|
1018
|
+
const sessionInit = { optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'layers'] };
|
|
1019
|
+
navigator.xr.requestSession('immersive-vr', sessionInit).then(session => {
|
|
1020
|
+
session.addEventListener('end', () => {
|
|
1021
|
+
renderer.xr.setSession(null);
|
|
1022
|
+
});
|
|
1023
|
+
renderer.xr.setSession(session);
|
|
1024
|
+
});
|
|
1025
|
+
} else {
|
|
1026
|
+
console.log('VR not supported on this device');
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
window.unifiedMenuVRButton.onclick = startVRSession;
|
|
1033
|
+
|
|
1034
|
+
// サポートチェック - 未サポートの場合はボタンを非表示に
|
|
1035
|
+
if ('xr' in navigator) {
|
|
1036
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
1037
|
+
if (!supported) {
|
|
1038
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
} else {
|
|
1042
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
1043
|
+
}
|
|
1044
|
+
} else {
|
|
1045
|
+
console.log('❌ VRButton script not loaded or menu button not found');
|
|
1046
|
+
if (window.unifiedMenuVRButton) {
|
|
1047
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (typeof ARButton !== 'undefined' && window.unifiedMenuARButton) {
|
|
1052
|
+
console.log('✅ ARButton script loaded - connecting to unified menu');
|
|
1053
|
+
|
|
1054
|
+
// ARセッション開始関数を作成
|
|
1055
|
+
function startARSession() {
|
|
1056
|
+
if ('xr' in navigator) {
|
|
1057
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
1058
|
+
if (supported) {
|
|
1059
|
+
const sessionInit = {
|
|
1060
|
+
requiredFeatures: ['hit-test'],
|
|
1061
|
+
optionalFeatures: ['dom-overlay'],
|
|
1062
|
+
domOverlay: { root: document.body }
|
|
1063
|
+
};
|
|
1064
|
+
navigator.xr.requestSession('immersive-ar', sessionInit).then(session => {
|
|
1065
|
+
session.addEventListener('end', () => {
|
|
1066
|
+
renderer.xr.setSession(null);
|
|
1067
|
+
});
|
|
1068
|
+
renderer.xr.setSession(session);
|
|
1069
|
+
});
|
|
1070
|
+
} else {
|
|
1071
|
+
console.log('AR not supported on this device');
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
window.unifiedMenuARButton.onclick = startARSession;
|
|
1078
|
+
|
|
1079
|
+
// サポートチェック - 未サポートの場合はボタンを非表示に
|
|
1080
|
+
if ('xr' in navigator) {
|
|
1081
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
1082
|
+
if (!supported) {
|
|
1083
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
} else {
|
|
1087
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
1088
|
+
}
|
|
1089
|
+
} else {
|
|
1090
|
+
console.log('❌ ARButton script not loaded or menu button not found');
|
|
1091
|
+
if (window.unifiedMenuARButton) {
|
|
1092
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// 🌸 音楽に反応する花を作成(パーティクルで構成された花)
|
|
1097
|
+
function createMusicFlower(x, y, z, scale = 1) {
|
|
1098
|
+
const flowerGroup = new THREE.Group();
|
|
1099
|
+
|
|
1100
|
+
// パーティクルの総数
|
|
1101
|
+
const particleCount = 12000;
|
|
1102
|
+
const positions = new Float32Array(particleCount * 3);
|
|
1103
|
+
const colors = new Float32Array(particleCount * 3);
|
|
1104
|
+
const sizes = new Float32Array(particleCount);
|
|
1105
|
+
|
|
1106
|
+
// ガラスのような透明な花びらの形状を作成
|
|
1107
|
+
const petalCount = 7; // 7枚の花びら
|
|
1108
|
+
|
|
1109
|
+
for (let i = 0; i < particleCount; i++) {
|
|
1110
|
+
// 中心からの距離(0=中心、1=外側)
|
|
1111
|
+
const radiusProgress = Math.pow(Math.random(), 0.4);
|
|
1112
|
+
const maxRadius = 3.5 * scale; // より大きく広げる
|
|
1113
|
+
const baseRadius = radiusProgress * maxRadius;
|
|
1114
|
+
|
|
1115
|
+
// 角度
|
|
1116
|
+
const angle = Math.random() * Math.PI * 2;
|
|
1117
|
+
|
|
1118
|
+
// どの花びらに属するか
|
|
1119
|
+
const petalAngle = (angle / (Math.PI * 2)) * petalCount;
|
|
1120
|
+
const petalIndex = Math.floor(petalAngle);
|
|
1121
|
+
|
|
1122
|
+
// 花びらの中心角度
|
|
1123
|
+
const petalCenterAngle = (petalIndex / petalCount) * Math.PI * 2;
|
|
1124
|
+
const angleFromPetalCenter = angle - petalCenterAngle;
|
|
1125
|
+
|
|
1126
|
+
// 花びらの形状:より広く、波打つ形
|
|
1127
|
+
const normalizedAngle = angleFromPetalCenter / (Math.PI * 2 / petalCount);
|
|
1128
|
+
|
|
1129
|
+
// 花びらの基本幅(より広く)
|
|
1130
|
+
const petalWidthBase = Math.exp(-Math.pow(normalizedAngle * 3.5, 2)) * 0.8;
|
|
1131
|
+
|
|
1132
|
+
// 花びらの先端に向かって広がる
|
|
1133
|
+
const petalExpansion = Math.pow(radiusProgress, 0.7) * 1.2;
|
|
1134
|
+
const petalWidth = petalWidthBase * petalExpansion;
|
|
1135
|
+
|
|
1136
|
+
// 最終的な半径(花びらの形状を適用)
|
|
1137
|
+
const radius = baseRadius * (1 + petalWidth);
|
|
1138
|
+
|
|
1139
|
+
// XY座標
|
|
1140
|
+
let px = Math.cos(angle) * radius;
|
|
1141
|
+
let py = Math.sin(angle) * radius;
|
|
1142
|
+
|
|
1143
|
+
// Z座標:大きく立体的に湾曲(ガラスのような波打つ形)
|
|
1144
|
+
let pz = 0;
|
|
1145
|
+
|
|
1146
|
+
if (radiusProgress < 0.08) {
|
|
1147
|
+
// 中心部:少し盛り上がる
|
|
1148
|
+
pz = (0.08 - radiusProgress) * 2 * scale;
|
|
1149
|
+
} else {
|
|
1150
|
+
// 花びら部分:大きく波打つ
|
|
1151
|
+
const wave = Math.sin(radiusProgress * Math.PI);
|
|
1152
|
+
const petalCurvature = Math.exp(-Math.pow(normalizedAngle * 2.5, 2));
|
|
1153
|
+
|
|
1154
|
+
// 花びらが大きく上下に広がる
|
|
1155
|
+
const verticalSpread = wave * petalCurvature * 2.5 * scale;
|
|
1156
|
+
|
|
1157
|
+
// 花びらの波打ち(ガラスのような不規則な波)
|
|
1158
|
+
const ripple = Math.sin(radiusProgress * Math.PI * 3 + angle * 2) * 0.3;
|
|
1159
|
+
|
|
1160
|
+
// 花びらの先端が優雅に反る
|
|
1161
|
+
const tipCurl = Math.pow(radiusProgress, 2) * 1.2;
|
|
1162
|
+
|
|
1163
|
+
pz = verticalSpread + ripple * scale + tipCurl * scale;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// より大きなノイズで有機的な質感
|
|
1167
|
+
px += (Math.random() - 0.5) * 0.06 * scale;
|
|
1168
|
+
py += (Math.random() - 0.5) * 0.06 * scale;
|
|
1169
|
+
pz += (Math.random() - 0.5) * 0.08 * scale;
|
|
1170
|
+
|
|
1171
|
+
positions[i * 3] = px;
|
|
1172
|
+
positions[i * 3 + 1] = py;
|
|
1173
|
+
positions[i * 3 + 2] = pz;
|
|
1174
|
+
|
|
1175
|
+
// ガラスのような透明感のあるパステルグラデーション
|
|
1176
|
+
let hue, saturation, lightness;
|
|
1177
|
+
|
|
1178
|
+
if (radiusProgress < 0.08) {
|
|
1179
|
+
// 中心:柔らかい黄色
|
|
1180
|
+
const t = radiusProgress / 0.08;
|
|
1181
|
+
hue = 0.12;
|
|
1182
|
+
saturation = 0.6 - t * 0.2;
|
|
1183
|
+
lightness = 0.85 - t * 0.1;
|
|
1184
|
+
} else {
|
|
1185
|
+
// 花びら部分:パステルピンク→ラベンダー→ブルー
|
|
1186
|
+
const t = (radiusProgress - 0.08) / (1.0 - 0.08);
|
|
1187
|
+
|
|
1188
|
+
// 色相を滑らかに変化(パステルトーン)
|
|
1189
|
+
if (t < 0.3) {
|
|
1190
|
+
// パステルピンク
|
|
1191
|
+
const localT = t / 0.3;
|
|
1192
|
+
hue = 0.92 + localT * 0.05;
|
|
1193
|
+
saturation = 0.3 + localT * 0.15;
|
|
1194
|
+
lightness = 0.85 - localT * 0.1;
|
|
1195
|
+
} else if (t < 0.6) {
|
|
1196
|
+
// ピンク→ラベンダー
|
|
1197
|
+
const localT = (t - 0.3) / 0.3;
|
|
1198
|
+
hue = 0.97 - localT * 0.15;
|
|
1199
|
+
saturation = 0.45 + localT * 0.1;
|
|
1200
|
+
lightness = 0.75 - localT * 0.05;
|
|
1201
|
+
} else {
|
|
1202
|
+
// ラベンダー→アイスブルー
|
|
1203
|
+
const localT = (t - 0.6) / 0.4;
|
|
1204
|
+
hue = 0.82 - localT * 0.3;
|
|
1205
|
+
saturation = 0.55 - localT * 0.15;
|
|
1206
|
+
lightness = 0.70 + localT * 0.05;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// 微細なバリエーションで自然な透明感
|
|
1211
|
+
hue += (Math.random() - 0.5) * 0.03;
|
|
1212
|
+
saturation += (Math.random() - 0.5) * 0.08;
|
|
1213
|
+
lightness += (Math.random() - 0.5) * 0.06;
|
|
1214
|
+
|
|
1215
|
+
const color = new THREE.Color().setHSL(
|
|
1216
|
+
(hue + 1) % 1,
|
|
1217
|
+
Math.max(0, Math.min(1, saturation)),
|
|
1218
|
+
Math.max(0.4, Math.min(0.95, lightness)) // より明るく
|
|
1219
|
+
);
|
|
1220
|
+
|
|
1221
|
+
colors[i * 3] = color.r;
|
|
1222
|
+
colors[i * 3 + 1] = color.g;
|
|
1223
|
+
colors[i * 3 + 2] = color.b;
|
|
1224
|
+
|
|
1225
|
+
// パーティクルのサイズ(中心部分は小さく、外側に向かって大きく)
|
|
1226
|
+
const particleSize = 0.08 + radiusProgress * 0.12;
|
|
1227
|
+
sizes[i] = particleSize * scale * (0.9 + Math.random() * 0.2);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const geometry = new THREE.BufferGeometry();
|
|
1231
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
1232
|
+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
1233
|
+
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
|
1234
|
+
|
|
1235
|
+
// 元の色を保存(音楽反応で色を変えた後に戻すため)
|
|
1236
|
+
const originalColors = new Float32Array(colors.length);
|
|
1237
|
+
for (let i = 0; i < colors.length; i++) {
|
|
1238
|
+
originalColors[i] = colors[i];
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// 元の位置を保存(音楽反応で広がった後に戻すため)
|
|
1242
|
+
const originalPositions = new Float32Array(positions.length);
|
|
1243
|
+
for (let i = 0; i < positions.length; i++) {
|
|
1244
|
+
originalPositions[i] = positions[i];
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const material = new THREE.PointsMaterial({
|
|
1248
|
+
size: 0.18 * scale,
|
|
1249
|
+
vertexColors: true,
|
|
1250
|
+
transparent: true,
|
|
1251
|
+
opacity: 0.6, // より透明に
|
|
1252
|
+
blending: THREE.NormalBlending, // ガラスのような自然なブレンド
|
|
1253
|
+
sizeAttenuation: true,
|
|
1254
|
+
depthWrite: false,
|
|
1255
|
+
map: createCircleTexture() // 円形テクスチャで滑らかに
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
// 円形テクスチャを作成(グロー効果)
|
|
1259
|
+
function createCircleTexture() {
|
|
1260
|
+
const canvas = document.createElement('canvas');
|
|
1261
|
+
canvas.width = 64;
|
|
1262
|
+
canvas.height = 64;
|
|
1263
|
+
const ctx = canvas.getContext('2d');
|
|
1264
|
+
|
|
1265
|
+
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
|
|
1266
|
+
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
|
1267
|
+
gradient.addColorStop(0.3, 'rgba(255,255,255,0.8)');
|
|
1268
|
+
gradient.addColorStop(0.6, 'rgba(255,255,255,0.3)');
|
|
1269
|
+
gradient.addColorStop(1, 'rgba(255,255,255,0)');
|
|
1270
|
+
|
|
1271
|
+
ctx.fillStyle = gradient;
|
|
1272
|
+
ctx.fillRect(0, 0, 64, 64);
|
|
1273
|
+
|
|
1274
|
+
const texture = new THREE.Texture(canvas);
|
|
1275
|
+
texture.needsUpdate = true;
|
|
1276
|
+
return texture;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const particles = new THREE.Points(geometry, material);
|
|
1280
|
+
flowerGroup.add(particles);
|
|
1281
|
+
|
|
1282
|
+
const allPetals = [particles];
|
|
1283
|
+
const allBaseColors = [];
|
|
1284
|
+
const centerPetals = [];
|
|
1285
|
+
|
|
1286
|
+
// 追加のキラキラエフェクト用パーティクル
|
|
1287
|
+
const sparkleCount = 500; // 大幅に増やす
|
|
1288
|
+
const sparklePositions = new Float32Array(sparkleCount * 3);
|
|
1289
|
+
const sparkleColors = new Float32Array(sparkleCount * 3);
|
|
1290
|
+
const sparkleSizes = new Float32Array(sparkleCount);
|
|
1291
|
+
const sparklePhases = new Float32Array(sparkleCount); // アニメーション用の位相
|
|
1292
|
+
|
|
1293
|
+
for (let i = 0; i < sparkleCount; i++) {
|
|
1294
|
+
const r = Math.random() * 4.5 * scale; // より広範囲に
|
|
1295
|
+
const theta = Math.random() * Math.PI * 2;
|
|
1296
|
+
const zOffset = (Math.random() - 0.5) * 4 * scale; // より高低差をつける
|
|
1297
|
+
|
|
1298
|
+
sparklePositions[i * 3] = Math.cos(theta) * r;
|
|
1299
|
+
sparklePositions[i * 3 + 1] = Math.sin(theta) * r;
|
|
1300
|
+
sparklePositions[i * 3 + 2] = zOffset;
|
|
1301
|
+
|
|
1302
|
+
// パステルカラーでキラキラ(より明るく)
|
|
1303
|
+
const hue = Math.random() * 0.3 + 0.5; // 青〜ピンク系
|
|
1304
|
+
const color = new THREE.Color().setHSL(hue, 0.6, 0.85); // 明度を上げる
|
|
1305
|
+
sparkleColors[i * 3] = color.r;
|
|
1306
|
+
sparkleColors[i * 3 + 1] = color.g;
|
|
1307
|
+
sparkleColors[i * 3 + 2] = color.b;
|
|
1308
|
+
|
|
1309
|
+
sparkleSizes[i] = (0.04 + Math.random() * 0.08) * scale;
|
|
1310
|
+
sparklePhases[i] = Math.random() * Math.PI * 2; // ランダムな位相
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const sparkleGeometry = new THREE.BufferGeometry();
|
|
1314
|
+
sparkleGeometry.setAttribute('position', new THREE.BufferAttribute(sparklePositions, 3));
|
|
1315
|
+
sparkleGeometry.setAttribute('color', new THREE.BufferAttribute(sparkleColors, 3));
|
|
1316
|
+
|
|
1317
|
+
// キラキラ用の円形テクスチャを作成
|
|
1318
|
+
function createSparkleTexture() {
|
|
1319
|
+
const canvas = document.createElement('canvas');
|
|
1320
|
+
canvas.width = 64;
|
|
1321
|
+
canvas.height = 64;
|
|
1322
|
+
const ctx = canvas.getContext('2d');
|
|
1323
|
+
|
|
1324
|
+
// より明るく光るグラデーション
|
|
1325
|
+
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
|
|
1326
|
+
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
|
1327
|
+
gradient.addColorStop(0.1, 'rgba(255,255,255,1)');
|
|
1328
|
+
gradient.addColorStop(0.3, 'rgba(255,255,255,0.9)');
|
|
1329
|
+
gradient.addColorStop(0.6, 'rgba(255,255,255,0.5)');
|
|
1330
|
+
gradient.addColorStop(1, 'rgba(255,255,255,0)');
|
|
1331
|
+
|
|
1332
|
+
ctx.fillStyle = gradient;
|
|
1333
|
+
ctx.fillRect(0, 0, 64, 64);
|
|
1334
|
+
|
|
1335
|
+
const texture = new THREE.Texture(canvas);
|
|
1336
|
+
texture.needsUpdate = true;
|
|
1337
|
+
return texture;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const sparkleMaterial = new THREE.PointsMaterial({
|
|
1341
|
+
size: 0.12 * scale, // より大きく
|
|
1342
|
+
vertexColors: true,
|
|
1343
|
+
transparent: true,
|
|
1344
|
+
opacity: 0.85, // より明るく
|
|
1345
|
+
blending: THREE.AdditiveBlending,
|
|
1346
|
+
sizeAttenuation: true,
|
|
1347
|
+
depthWrite: false,
|
|
1348
|
+
map: createSparkleTexture() // 円形テクスチャでより美しく
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
const sparkles = new THREE.Points(sparkleGeometry, sparkleMaterial);
|
|
1352
|
+
flowerGroup.add(sparkles);
|
|
1353
|
+
|
|
1354
|
+
// sparkleの位相データと元の位置も保存
|
|
1355
|
+
const originalSparklePositions = new Float32Array(sparklePositions.length);
|
|
1356
|
+
for (let i = 0; i < sparklePositions.length; i++) {
|
|
1357
|
+
originalSparklePositions[i] = sparklePositions[i];
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
sparkles.userData = {
|
|
1361
|
+
phases: sparklePhases,
|
|
1362
|
+
originalPositions: originalSparklePositions
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
flowerGroup.position.set(x, y, z);
|
|
1366
|
+
|
|
1367
|
+
// アニメーション用データ
|
|
1368
|
+
flowerGroup.userData = {
|
|
1369
|
+
originalScale: scale,
|
|
1370
|
+
mainParticles: particles,
|
|
1371
|
+
sparkles: sparkles,
|
|
1372
|
+
pulsePhase: Math.random() * Math.PI * 2,
|
|
1373
|
+
originalColors: originalColors, // 元の色を保存
|
|
1374
|
+
originalPositions: originalPositions // 元の位置を保存
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
return flowerGroup;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// 🎵 Web Audio API セットアップ
|
|
1381
|
+
let audioContext;
|
|
1382
|
+
let analyser;
|
|
1383
|
+
let dataArray;
|
|
1384
|
+
let isAudioActive = false;
|
|
1385
|
+
|
|
1386
|
+
function initAudioContext() {
|
|
1387
|
+
try {
|
|
1388
|
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
1389
|
+
analyser = audioContext.createAnalyser();
|
|
1390
|
+
analyser.fftSize = 256;
|
|
1391
|
+
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
1392
|
+
|
|
1393
|
+
console.log('🎵 Audio context initialized');
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
console.warn('🚫 Audio context not supported:', error);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function connectVideoToAudio(videoElement) {
|
|
1400
|
+
console.log('🔗 Attempting to connect video to audio...');
|
|
1401
|
+
console.log('📺 Video element:', videoElement);
|
|
1402
|
+
|
|
1403
|
+
if (!audioContext || !analyser) {
|
|
1404
|
+
console.warn('🚫 Audio context not initialized, initializing now...');
|
|
1405
|
+
initAudioContext();
|
|
1406
|
+
if (!audioContext || !analyser) {
|
|
1407
|
+
console.error('❌ Failed to initialize audio context');
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
try {
|
|
1413
|
+
console.log('🔊 AudioContext state before connection:', audioContext.state);
|
|
1414
|
+
|
|
1415
|
+
// AudioContextがsuspendedの場合、再開する
|
|
1416
|
+
if (audioContext.state === 'suspended') {
|
|
1417
|
+
console.log('⏸️ AudioContext is suspended, resuming...');
|
|
1418
|
+
audioContext.resume().then(() => {
|
|
1419
|
+
console.log('▶️ AudioContext resumed successfully');
|
|
1420
|
+
}).catch(err => {
|
|
1421
|
+
console.error('❌ Failed to resume AudioContext:', err);
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// 既に接続されている場合はスキップ
|
|
1426
|
+
if (videoElement.audioSource) {
|
|
1427
|
+
console.log('✅ Video already connected to audio');
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
console.log('🎬 Creating MediaElementSource...');
|
|
1432
|
+
const source = audioContext.createMediaElementSource(videoElement);
|
|
1433
|
+
console.log('🎵 Connecting source to analyser...');
|
|
1434
|
+
source.connect(analyser);
|
|
1435
|
+
console.log('🔊 Connecting analyser to destination...');
|
|
1436
|
+
analyser.connect(audioContext.destination);
|
|
1437
|
+
|
|
1438
|
+
// 接続済みマーク
|
|
1439
|
+
videoElement.audioSource = source;
|
|
1440
|
+
|
|
1441
|
+
isAudioActive = true;
|
|
1442
|
+
updateAudioStatus('🎤 音声解析中...', 'audio-active');
|
|
1443
|
+
console.log('✅ Video successfully connected to audio analyser');
|
|
1444
|
+
|
|
1445
|
+
// 音声データのテスト
|
|
1446
|
+
setTimeout(() => {
|
|
1447
|
+
if (analyser && dataArray) {
|
|
1448
|
+
analyser.getByteFrequencyData(dataArray);
|
|
1449
|
+
const testVolume = dataArray.reduce((sum, freq) => sum + freq, 0) / dataArray.length / 255;
|
|
1450
|
+
console.log('🧪 Audio test - Volume level:', testVolume);
|
|
1451
|
+
if (testVolume > 0.01) {
|
|
1452
|
+
console.log('🎵 Audio is being detected!');
|
|
1453
|
+
} else {
|
|
1454
|
+
console.log('🔇 No audio detected. Video might be muted or have no audio track.');
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}, 1000);
|
|
1458
|
+
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
console.error('❌ Failed to connect video to audio:', error);
|
|
1461
|
+
console.log('Error details:', {
|
|
1462
|
+
name: error.name,
|
|
1463
|
+
message: error.message,
|
|
1464
|
+
stack: error.stack
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
if (error.name === 'InvalidStateError') {
|
|
1468
|
+
console.log('⚠️ InvalidStateError - Video already connected to another AudioContext!');
|
|
1469
|
+
console.log('💡 This means ChocoDrop is using its own AudioContext.');
|
|
1470
|
+
console.log('🔧 Trying alternative approach...');
|
|
1471
|
+
|
|
1472
|
+
// 代替案:既存の audio から音声データをコピー
|
|
1473
|
+
tryAlternativeAudioConnection(videoElement);
|
|
1474
|
+
} else if (error.name === 'NotSupportedError') {
|
|
1475
|
+
console.log('❌ NotSupportedError - Audio context creation failed');
|
|
1476
|
+
} else {
|
|
1477
|
+
console.log('❓ Unknown error:', error.name);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function tryAlternativeAudioConnection(videoElement) {
|
|
1483
|
+
console.log('🔄 Trying alternative audio connection method...');
|
|
1484
|
+
|
|
1485
|
+
// 方法1: Web Audio API を使わずに、直接 video の音量を監視
|
|
1486
|
+
const alternativeMonitor = () => {
|
|
1487
|
+
if (videoElement.paused) return;
|
|
1488
|
+
|
|
1489
|
+
// 簡易的な音量検出(完璧ではないが動作する)
|
|
1490
|
+
const currentTime = videoElement.currentTime;
|
|
1491
|
+
const duration = videoElement.duration;
|
|
1492
|
+
const volume = videoElement.volume;
|
|
1493
|
+
|
|
1494
|
+
// 再生時間ベースの簡易パルス生成
|
|
1495
|
+
const timeBasedVolume = Math.sin(currentTime * 10) * 0.3 + 0.7;
|
|
1496
|
+
|
|
1497
|
+
// 音楽が再生中なら花を反応させる
|
|
1498
|
+
if (currentTime > 0 && !videoElement.paused && volume > 0) {
|
|
1499
|
+
// グローバル変数を設定して花のアニメーションを駆動
|
|
1500
|
+
window.alternativeAudioData = {
|
|
1501
|
+
volume: timeBasedVolume * volume,
|
|
1502
|
+
isActive: true,
|
|
1503
|
+
timestamp: Date.now()
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
if (!isAudioActive) {
|
|
1507
|
+
isAudioActive = true;
|
|
1508
|
+
updateAudioStatus('🎵 代替音声監視中...', 'audio-active');
|
|
1509
|
+
console.log('✅ Alternative audio monitoring started');
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
|
|
1514
|
+
// 100ms毎に監視
|
|
1515
|
+
videoElement.alternativeMonitorInterval = setInterval(alternativeMonitor, 100);
|
|
1516
|
+
console.log('🔄 Alternative audio monitoring interval started');
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function updateAudioStatus(text, className) {
|
|
1520
|
+
const status = document.getElementById('audio-status');
|
|
1521
|
+
status.textContent = text;
|
|
1522
|
+
status.className = className;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// 🌸 花畑を作成
|
|
1526
|
+
const flowers = [];
|
|
1527
|
+
const flowerPositions = [
|
|
1528
|
+
[0, 0, 0], // 中央
|
|
1529
|
+
[-8, 2, -5], // 左
|
|
1530
|
+
[8, -1, -3], // 右
|
|
1531
|
+
[-3, -3, -8], // 左奥
|
|
1532
|
+
[5, 3, -6], // 右奥
|
|
1533
|
+
[0, 5, -10], // 上奥
|
|
1534
|
+
[-6, -2, 3], // 左手前
|
|
1535
|
+
[7, 1, 4] // 右手前
|
|
1536
|
+
];
|
|
1537
|
+
|
|
1538
|
+
flowerPositions.forEach(pos => {
|
|
1539
|
+
const flower = createMusicFlower(pos[0], pos[1], pos[2], 0.8 + Math.random() * 0.4);
|
|
1540
|
+
flowers.push(flower);
|
|
1541
|
+
scene.add(flower);
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
// 🎵 音楽反応で散るパーティクルシステム
|
|
1545
|
+
const flyingParticles = [];
|
|
1546
|
+
const maxFlyingParticles = 2000;
|
|
1547
|
+
|
|
1548
|
+
// 飛散パーティクル用のgeometry
|
|
1549
|
+
const flyingGeometry = new THREE.BufferGeometry();
|
|
1550
|
+
const flyingPositions = new Float32Array(maxFlyingParticles * 3);
|
|
1551
|
+
const flyingColors = new Float32Array(maxFlyingParticles * 3);
|
|
1552
|
+
const flyingSizes = new Float32Array(maxFlyingParticles);
|
|
1553
|
+
|
|
1554
|
+
flyingGeometry.setAttribute('position', new THREE.BufferAttribute(flyingPositions, 3));
|
|
1555
|
+
flyingGeometry.setAttribute('color', new THREE.BufferAttribute(flyingColors, 3));
|
|
1556
|
+
flyingGeometry.setAttribute('size', new THREE.BufferAttribute(flyingSizes, 1));
|
|
1557
|
+
|
|
1558
|
+
// 円形テクスチャを作成(グロー効果)
|
|
1559
|
+
function createParticleTexture() {
|
|
1560
|
+
const canvas = document.createElement('canvas');
|
|
1561
|
+
canvas.width = 64;
|
|
1562
|
+
canvas.height = 64;
|
|
1563
|
+
const ctx = canvas.getContext('2d');
|
|
1564
|
+
|
|
1565
|
+
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
|
|
1566
|
+
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
|
1567
|
+
gradient.addColorStop(0.2, 'rgba(255,255,255,0.9)');
|
|
1568
|
+
gradient.addColorStop(0.5, 'rgba(255,255,255,0.5)');
|
|
1569
|
+
gradient.addColorStop(0.8, 'rgba(255,255,255,0.1)');
|
|
1570
|
+
gradient.addColorStop(1, 'rgba(255,255,255,0)');
|
|
1571
|
+
|
|
1572
|
+
ctx.fillStyle = gradient;
|
|
1573
|
+
ctx.fillRect(0, 0, 64, 64);
|
|
1574
|
+
|
|
1575
|
+
const texture = new THREE.Texture(canvas);
|
|
1576
|
+
texture.needsUpdate = true;
|
|
1577
|
+
return texture;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
const flyingMaterial = new THREE.PointsMaterial({
|
|
1581
|
+
size: 0.15,
|
|
1582
|
+
vertexColors: true,
|
|
1583
|
+
transparent: true,
|
|
1584
|
+
opacity: 0.9,
|
|
1585
|
+
blending: THREE.AdditiveBlending,
|
|
1586
|
+
sizeAttenuation: true,
|
|
1587
|
+
depthWrite: false,
|
|
1588
|
+
map: createParticleTexture()
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
const flyingPoints = new THREE.Points(flyingGeometry, flyingMaterial);
|
|
1592
|
+
scene.add(flyingPoints);
|
|
1593
|
+
|
|
1594
|
+
// ✨ 環境光(全体を柔らかく照らす)
|
|
1595
|
+
const ambientLight = new THREE.AmbientLight(0x8090c0, 0.6);
|
|
1596
|
+
scene.add(ambientLight);
|
|
1597
|
+
|
|
1598
|
+
// 🌟 メインのポイントライト(花を美しく照らす)
|
|
1599
|
+
const pointLight = new THREE.PointLight(0xffffff, 1.5, 50);
|
|
1600
|
+
pointLight.position.set(0, 10, 5);
|
|
1601
|
+
scene.add(pointLight);
|
|
1602
|
+
|
|
1603
|
+
// 追加のライト1(パステルピンク)
|
|
1604
|
+
const spotLight1 = new THREE.SpotLight(0xffc0e0, 0.6);
|
|
1605
|
+
spotLight1.position.set(-10, 10, 10);
|
|
1606
|
+
spotLight1.target.position.set(0, 0, 0);
|
|
1607
|
+
scene.add(spotLight1);
|
|
1608
|
+
scene.add(spotLight1.target);
|
|
1609
|
+
|
|
1610
|
+
// 追加のライト2(パステルブルー)
|
|
1611
|
+
const spotLight2 = new THREE.SpotLight(0xb0c5ff, 0.5);
|
|
1612
|
+
spotLight2.position.set(10, 8, -5);
|
|
1613
|
+
spotLight2.target.position.set(0, 0, 0);
|
|
1614
|
+
scene.add(spotLight2);
|
|
1615
|
+
scene.add(spotLight2.target);
|
|
1616
|
+
|
|
1617
|
+
// 📷 カメラ設定(花の美しさが際立つ位置)
|
|
1618
|
+
camera.position.set(-1, 6, 11); // 少し左側から見る
|
|
1619
|
+
camera.lookAt(0, 0.5, 0); // 少し上を見る
|
|
1620
|
+
|
|
1621
|
+
// 🎮 コントロール
|
|
1622
|
+
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
1623
|
+
controls.enableDamping = true;
|
|
1624
|
+
controls.dampingFactor = 0.05;
|
|
1625
|
+
controls.minDistance = 4; // もう少し近づけるように
|
|
1626
|
+
controls.maxDistance = 28;
|
|
1627
|
+
controls.target.set(0, 0.5, 0); // 花畑の少し上を見る
|
|
1628
|
+
|
|
1629
|
+
// 🍫 ChocoDrop初期化
|
|
1630
|
+
console.log('🔍 Pre-ChocoDrop system check:');
|
|
1631
|
+
console.log(' - AudioContext support:', typeof AudioContext !== 'undefined' || typeof webkitAudioContext !== 'undefined');
|
|
1632
|
+
console.log(' - Video elements before ChocoDrop:', document.querySelectorAll('video').length);
|
|
1633
|
+
|
|
1634
|
+
const chocoDrop = ChocoDrop.createChocoDrop(scene, {
|
|
1635
|
+
camera: camera,
|
|
1636
|
+
renderer: renderer,
|
|
1637
|
+
// GitHub Pagesなので生成機能なし、インポートのみ
|
|
1638
|
+
serverUrl: null,
|
|
1639
|
+
enableServerHealthCheck: false, // サーバーレス環境なのでヘルスチェック無効
|
|
1640
|
+
skipServiceDialog: true, // GitHub Pages用:サービス設定ダイアログを無効化
|
|
1641
|
+
sceneOptions: {
|
|
1642
|
+
enableMouseInteraction: true
|
|
1643
|
+
},
|
|
1644
|
+
onControlsToggle: (disabled) => {
|
|
1645
|
+
controls.enabled = !disabled;
|
|
1646
|
+
},
|
|
1647
|
+
onVideoAdded: (videoObject) => {
|
|
1648
|
+
console.log('🎬 Video object added to scene:', videoObject);
|
|
1649
|
+
console.log('🔍 Video object type:', typeof videoObject);
|
|
1650
|
+
console.log('🔍 Video object constructor:', videoObject.constructor.name);
|
|
1651
|
+
|
|
1652
|
+
// 🚨 重要: onVideoAddedが呼ばれていることを確認
|
|
1653
|
+
console.log('🚨 onVideoAdded callback was triggered!');
|
|
1654
|
+
console.log('🔍 DOM video elements at callback time:', document.querySelectorAll('video').length);
|
|
1655
|
+
|
|
1656
|
+
// 🔍 ChocoDrop Three.js オブジェクト詳細調査
|
|
1657
|
+
console.log('🔍 Video object properties:', Object.keys(videoObject));
|
|
1658
|
+
console.log('🔍 Video object material:', videoObject.material);
|
|
1659
|
+
console.log('🔍 Video object material map:', videoObject.material?.map);
|
|
1660
|
+
console.log('🔍 Video object userData:', videoObject.userData);
|
|
1661
|
+
console.log('🔍 Video object children:', videoObject.children);
|
|
1662
|
+
|
|
1663
|
+
// 🔍 Scene内の全オブジェクトを調査
|
|
1664
|
+
console.log('🔍 Scene children count:', scene.children.length);
|
|
1665
|
+
scene.traverse((child) => {
|
|
1666
|
+
if (child.material && child.material.map && child.material.map.image) {
|
|
1667
|
+
console.log('🔍 Found textured object:', child);
|
|
1668
|
+
console.log('🔍 Texture image:', child.material.map.image);
|
|
1669
|
+
console.log('🔍 Image tagName:', child.material.map.image.tagName);
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
// ChocoDrop オブジェクトから video 要素を直接取得する試み
|
|
1674
|
+
if (videoObject && videoObject.userData && videoObject.userData.videoElement) {
|
|
1675
|
+
console.log('✅ Found video element in userData:', videoObject.userData.videoElement);
|
|
1676
|
+
connectVideoToAudio(videoObject.userData.videoElement);
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// MaterialのmapからvideoElementを探す
|
|
1681
|
+
if (videoObject.material && videoObject.material.map && videoObject.material.map.image) {
|
|
1682
|
+
const videoEl = videoObject.material.map.image;
|
|
1683
|
+
if (videoEl.tagName === 'VIDEO') {
|
|
1684
|
+
console.log('✅ Found video element in material.map.image:', videoEl);
|
|
1685
|
+
connectVideoToAudio(videoEl);
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// ChocoDrop は Three.js オブジェクトを返すので、実際のvideo要素を探す
|
|
1691
|
+
console.log('🔍 Searching for video elements in 500ms...');
|
|
1692
|
+
setTimeout(() => {
|
|
1693
|
+
const videoElements = document.querySelectorAll('video');
|
|
1694
|
+
console.log(`📹 Found ${videoElements.length} video elements`);
|
|
1695
|
+
console.log('🔊 AudioContext state:', audioContext ? audioContext.state : 'not initialized');
|
|
1696
|
+
console.log('🎵 Analyser exists:', !!analyser);
|
|
1697
|
+
console.log('📊 Data array exists:', !!dataArray);
|
|
1698
|
+
|
|
1699
|
+
if (videoElements.length === 0) {
|
|
1700
|
+
console.warn('❌ No video elements found! ChocoDrop might not have added the video to DOM yet.');
|
|
1701
|
+
// 再試行
|
|
1702
|
+
setTimeout(() => {
|
|
1703
|
+
console.log('🔄 Retrying video search...');
|
|
1704
|
+
const retryVideos = document.querySelectorAll('video');
|
|
1705
|
+
console.log(`📹 Retry found ${retryVideos.length} video elements`);
|
|
1706
|
+
if (retryVideos.length > 0) {
|
|
1707
|
+
retryVideos.forEach(v => connectVideoToAudio(v));
|
|
1708
|
+
}
|
|
1709
|
+
}, 2000);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
videoElements.forEach((videoElement, index) => {
|
|
1713
|
+
console.log(`🎥 Video ${index}:`);
|
|
1714
|
+
console.log(` - src: ${videoElement.src}`);
|
|
1715
|
+
console.log(` - readyState: ${videoElement.readyState}`);
|
|
1716
|
+
console.log(` - paused: ${videoElement.paused}`);
|
|
1717
|
+
console.log(` - volume: ${videoElement.volume}`);
|
|
1718
|
+
console.log(` - muted: ${videoElement.muted}`);
|
|
1719
|
+
console.log(` - audioTracks: ${videoElement.audioTracks ? videoElement.audioTracks.length : 'not supported'}`);
|
|
1720
|
+
console.log(` - duration: ${videoElement.duration}`);
|
|
1721
|
+
console.log(` - currentTime: ${videoElement.currentTime}`);
|
|
1722
|
+
|
|
1723
|
+
// まだ接続されていない動画要素を処理
|
|
1724
|
+
if (!videoElement.audioSource) {
|
|
1725
|
+
// 動画を前面に配置
|
|
1726
|
+
videoElement.style.zIndex = '10';
|
|
1727
|
+
videoElement.style.position = 'relative';
|
|
1728
|
+
|
|
1729
|
+
// すぐに接続を試みる
|
|
1730
|
+
connectVideoToAudio(videoElement);
|
|
1731
|
+
|
|
1732
|
+
// 動画の準備ができてから再接続
|
|
1733
|
+
videoElement.addEventListener('loadedmetadata', () => {
|
|
1734
|
+
console.log('📹 Video metadata loaded, ensuring audio connection...');
|
|
1735
|
+
if (!videoElement.audioSource) {
|
|
1736
|
+
connectVideoToAudio(videoElement);
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
// 再生開始時にも確実に接続
|
|
1741
|
+
videoElement.addEventListener('play', () => {
|
|
1742
|
+
console.log('▶️ Video playing, checking audio connection...');
|
|
1743
|
+
if (!videoElement.audioSource) {
|
|
1744
|
+
connectVideoToAudio(videoElement);
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
// クリックで再生/一時停止(デバッグ用)
|
|
1749
|
+
videoElement.addEventListener('click', () => {
|
|
1750
|
+
if (videoElement.paused) {
|
|
1751
|
+
videoElement.play();
|
|
1752
|
+
// 再生開始時に強制的に音声アクティブ化
|
|
1753
|
+
setTimeout(() => {
|
|
1754
|
+
if (!isAudioActive) {
|
|
1755
|
+
console.log('🔧 Force activating audio monitoring...');
|
|
1756
|
+
isAudioActive = true;
|
|
1757
|
+
updateAudioStatus('🎵 強制音声監視中...', 'audio-active');
|
|
1758
|
+
|
|
1759
|
+
// 代替監視を開始
|
|
1760
|
+
tryAlternativeAudioConnection(videoElement);
|
|
1761
|
+
}
|
|
1762
|
+
}, 100);
|
|
1763
|
+
} else {
|
|
1764
|
+
videoElement.pause();
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
}, 500); // ChocoDrop が DOM に video 要素を追加するのを待つ
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
// 音声コンテキストを初期化
|
|
1774
|
+
initAudioContext();
|
|
1775
|
+
|
|
1776
|
+
// 🎵 Three.js Scene内のVideoTextureを監視して音声接続
|
|
1777
|
+
function monitorVideoTextures() {
|
|
1778
|
+
let foundVideo = false;
|
|
1779
|
+
|
|
1780
|
+
scene.traverse((child) => {
|
|
1781
|
+
if (child.material && child.material.map && child.material.map.image) {
|
|
1782
|
+
const image = child.material.map.image;
|
|
1783
|
+
|
|
1784
|
+
// HTMLVideoElementを発見
|
|
1785
|
+
if (image instanceof HTMLVideoElement && !image.audioConnected) {
|
|
1786
|
+
console.log('🎵 Found VideoTexture with HTMLVideoElement:', image);
|
|
1787
|
+
console.log(' - src:', image.src);
|
|
1788
|
+
console.log(' - readyState:', image.readyState);
|
|
1789
|
+
console.log(' - paused:', image.paused);
|
|
1790
|
+
|
|
1791
|
+
if (image.readyState >= 2) {
|
|
1792
|
+
connectVideoToAudio(image);
|
|
1793
|
+
image.audioConnected = true;
|
|
1794
|
+
foundVideo = true;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
// デバッグ情報更新
|
|
1801
|
+
if (foundVideo && !window.videoTextureFound) {
|
|
1802
|
+
console.log('✅ VideoTexture audio connection established!');
|
|
1803
|
+
window.videoTextureFound = true;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// 1秒ごとに監視
|
|
1808
|
+
setInterval(monitorVideoTextures, 1000);
|
|
1809
|
+
|
|
1810
|
+
// 初回実行
|
|
1811
|
+
setTimeout(monitorVideoTextures, 2000);
|
|
1812
|
+
|
|
1813
|
+
console.log('🍫 ChocoDrop Music Garden initialized');
|
|
1814
|
+
console.log('Press @ key to activate command interface');
|
|
1815
|
+
console.log('使用例: 🍫アイコンクリックでUI起動、動画を配置して音楽反応!');
|
|
1816
|
+
|
|
1817
|
+
// 🎵 パーティクルを花から放出する関数
|
|
1818
|
+
function emitParticlesFromFlower(flower, volume, time) {
|
|
1819
|
+
const particlesToEmit = Math.floor(volume * 15); // 音量に応じて放出数を調整
|
|
1820
|
+
|
|
1821
|
+
for (let i = 0; i < particlesToEmit; i++) {
|
|
1822
|
+
if (flyingParticles.length >= maxFlyingParticles) {
|
|
1823
|
+
// 古いパーティクルを再利用
|
|
1824
|
+
flyingParticles.shift();
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// 花の位置
|
|
1828
|
+
const flowerPos = flower.position;
|
|
1829
|
+
|
|
1830
|
+
// 花の中心からランダムな位置で放出
|
|
1831
|
+
const angle = Math.random() * Math.PI * 2;
|
|
1832
|
+
const radius = Math.random() * 2.5 * flower.userData.originalScale;
|
|
1833
|
+
const offsetX = Math.cos(angle) * radius;
|
|
1834
|
+
const offsetY = Math.sin(angle) * radius;
|
|
1835
|
+
const offsetZ = Math.random() * 1.5 * flower.userData.originalScale;
|
|
1836
|
+
|
|
1837
|
+
// 放出速度(音量に応じて変化)
|
|
1838
|
+
const speed = (0.5 + volume * 2) * (0.8 + Math.random() * 0.4);
|
|
1839
|
+
const velocityAngle = angle + (Math.random() - 0.5) * 0.5;
|
|
1840
|
+
|
|
1841
|
+
const particle = {
|
|
1842
|
+
position: new THREE.Vector3(
|
|
1843
|
+
flowerPos.x + offsetX,
|
|
1844
|
+
flowerPos.y + offsetY,
|
|
1845
|
+
flowerPos.z + offsetZ
|
|
1846
|
+
),
|
|
1847
|
+
velocity: new THREE.Vector3(
|
|
1848
|
+
Math.cos(velocityAngle) * speed,
|
|
1849
|
+
Math.sin(velocityAngle) * speed,
|
|
1850
|
+
(Math.random() - 0.3) * speed * 1.5
|
|
1851
|
+
),
|
|
1852
|
+
color: new THREE.Color().setHSL(
|
|
1853
|
+
(Math.random() * 0.3 + 0.5 + time * 0.1) % 1,
|
|
1854
|
+
0.8 + Math.random() * 0.2,
|
|
1855
|
+
0.6 + Math.random() * 0.2
|
|
1856
|
+
),
|
|
1857
|
+
size: 0.08 + Math.random() * 0.12,
|
|
1858
|
+
life: 1.0, // 寿命(1.0から0.0に減少)
|
|
1859
|
+
decay: 0.008 + Math.random() * 0.012
|
|
1860
|
+
};
|
|
1861
|
+
|
|
1862
|
+
flyingParticles.push(particle);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// 🎬 アニメーションループ
|
|
1867
|
+
function animate() {
|
|
1868
|
+
requestAnimationFrame(animate);
|
|
1869
|
+
|
|
1870
|
+
const time = Date.now() * 0.001;
|
|
1871
|
+
|
|
1872
|
+
// 音声解析(デバッグ強化版)
|
|
1873
|
+
let volume = 0;
|
|
1874
|
+
let frequencies = new Array(8).fill(0);
|
|
1875
|
+
|
|
1876
|
+
// デバッグ: 5秒ごとに状態をログ出力
|
|
1877
|
+
if (!window.lastDebugLog || Date.now() - window.lastDebugLog > 5000) {
|
|
1878
|
+
console.log('🔍 Audio Debug Status:');
|
|
1879
|
+
console.log(' - isAudioActive:', isAudioActive);
|
|
1880
|
+
console.log(' - analyser exists:', !!analyser);
|
|
1881
|
+
console.log(' - audioContext state:', audioContext ? audioContext.state : 'none');
|
|
1882
|
+
console.log(' - dataArray exists:', !!dataArray);
|
|
1883
|
+
console.log(' - video elements count:', document.querySelectorAll('video').length);
|
|
1884
|
+
console.log(' - alternative audio:', window.alternativeAudioData ? window.alternativeAudioData.isActive : 'none');
|
|
1885
|
+
window.lastDebugLog = Date.now();
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
if (isAudioActive && analyser) {
|
|
1889
|
+
analyser.getByteFrequencyData(dataArray);
|
|
1890
|
+
|
|
1891
|
+
// 全体音量を計算
|
|
1892
|
+
volume = dataArray.reduce((sum, freq) => sum + freq, 0) / dataArray.length / 255;
|
|
1893
|
+
|
|
1894
|
+
// 音楽検出時のステータス更新(ログなし)
|
|
1895
|
+
if (volume > 0.005) {
|
|
1896
|
+
if (!window.audioDetected) {
|
|
1897
|
+
updateAudioStatus('🎵 音楽反応中!', 'audio-active');
|
|
1898
|
+
window.audioDetected = true;
|
|
1899
|
+
}
|
|
1900
|
+
} else if (window.audioDetected) {
|
|
1901
|
+
updateAudioStatus('🔇 音声待機中...', 'audio-waiting');
|
|
1902
|
+
window.audioDetected = false;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// 周波数帯域を分析(低音〜高音)
|
|
1906
|
+
const bands = 8;
|
|
1907
|
+
const bandSize = Math.floor(dataArray.length / bands);
|
|
1908
|
+
for (let i = 0; i < bands; i++) {
|
|
1909
|
+
let sum = 0;
|
|
1910
|
+
for (let j = 0; j < bandSize; j++) {
|
|
1911
|
+
sum += dataArray[i * bandSize + j];
|
|
1912
|
+
}
|
|
1913
|
+
frequencies[i] = sum / bandSize / 255;
|
|
1914
|
+
}
|
|
1915
|
+
} else if (window.alternativeAudioData && window.alternativeAudioData.isActive) {
|
|
1916
|
+
// 代替音声監視データを使用
|
|
1917
|
+
const altData = window.alternativeAudioData;
|
|
1918
|
+
const timeSinceUpdate = Date.now() - altData.timestamp;
|
|
1919
|
+
|
|
1920
|
+
// 最新データ(1秒以内)の場合のみ使用
|
|
1921
|
+
if (timeSinceUpdate < 1000) {
|
|
1922
|
+
volume = altData.volume;
|
|
1923
|
+
// 簡易的な周波数データ生成
|
|
1924
|
+
for (let i = 0; i < 8; i++) {
|
|
1925
|
+
frequencies[i] = Math.sin(time * (i + 1) * 2) * volume * 0.5 + 0.5;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
if (!window.alternativeAudioDetected) {
|
|
1929
|
+
console.log('🔄 Using alternative audio data! Volume:', volume);
|
|
1930
|
+
window.alternativeAudioDetected = true;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// 🎵 音楽に反応して花からパーティクルを放出
|
|
1936
|
+
if (volume > 0.03) {
|
|
1937
|
+
flowers.forEach((flower, index) => {
|
|
1938
|
+
// 音量が大きい時だけ放出(全ての花から少しずつ)
|
|
1939
|
+
if (Math.random() < volume * 0.3) {
|
|
1940
|
+
emitParticlesFromFlower(flower, volume, time);
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// 🌟 飛散パーティクルを更新
|
|
1946
|
+
const activeParticles = [];
|
|
1947
|
+
flyingParticles.forEach((particle, index) => {
|
|
1948
|
+
// 重力と空気抵抗を適用
|
|
1949
|
+
particle.velocity.z -= 0.015; // 重力
|
|
1950
|
+
particle.velocity.x *= 0.98; // 空気抵抗
|
|
1951
|
+
particle.velocity.y *= 0.98;
|
|
1952
|
+
particle.velocity.z *= 0.98;
|
|
1953
|
+
|
|
1954
|
+
// 位置を更新
|
|
1955
|
+
particle.position.add(particle.velocity);
|
|
1956
|
+
|
|
1957
|
+
// 寿命を減らす
|
|
1958
|
+
particle.life -= particle.decay;
|
|
1959
|
+
|
|
1960
|
+
// 生きているパーティクルのみ保持
|
|
1961
|
+
if (particle.life > 0 && particle.position.z > -20) {
|
|
1962
|
+
activeParticles.push(particle);
|
|
1963
|
+
|
|
1964
|
+
// geometryを更新
|
|
1965
|
+
const idx = activeParticles.length - 1;
|
|
1966
|
+
flyingPositions[idx * 3] = particle.position.x;
|
|
1967
|
+
flyingPositions[idx * 3 + 1] = particle.position.y;
|
|
1968
|
+
flyingPositions[idx * 3 + 2] = particle.position.z;
|
|
1969
|
+
|
|
1970
|
+
flyingColors[idx * 3] = particle.color.r;
|
|
1971
|
+
flyingColors[idx * 3 + 1] = particle.color.g;
|
|
1972
|
+
flyingColors[idx * 3 + 2] = particle.color.b;
|
|
1973
|
+
|
|
1974
|
+
flyingSizes[idx] = particle.size * particle.life; // 寿命に応じて小さくなる
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
// 配列を更新
|
|
1979
|
+
flyingParticles.length = 0;
|
|
1980
|
+
flyingParticles.push(...activeParticles);
|
|
1981
|
+
|
|
1982
|
+
// geometryを更新
|
|
1983
|
+
flyingGeometry.setDrawRange(0, activeParticles.length);
|
|
1984
|
+
flyingGeometry.attributes.position.needsUpdate = true;
|
|
1985
|
+
flyingGeometry.attributes.color.needsUpdate = true;
|
|
1986
|
+
flyingGeometry.attributes.size.needsUpdate = true;
|
|
1987
|
+
flyingMaterial.opacity = 0.6 + volume * 0.4;
|
|
1988
|
+
|
|
1989
|
+
// 🌸 パーティクル花のアニメーション
|
|
1990
|
+
flowers.forEach((flower, index) => {
|
|
1991
|
+
const userData = flower.userData;
|
|
1992
|
+
const mainParticles = userData.mainParticles;
|
|
1993
|
+
const sparkles = userData.sparkles;
|
|
1994
|
+
|
|
1995
|
+
// 音楽に反応するスケール
|
|
1996
|
+
if (volume > 0.005) {
|
|
1997
|
+
const basePulse = Math.sin(time + userData.pulsePhase) * 0.1 + 1;
|
|
1998
|
+
const musicScale = 1 + volume * 0.3;
|
|
1999
|
+
flower.scale.setScalar(userData.originalScale * basePulse * musicScale);
|
|
2000
|
+
} else {
|
|
2001
|
+
flower.scale.setScalar(userData.originalScale);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// メインパーティクルのアニメーション
|
|
2005
|
+
if (mainParticles) {
|
|
2006
|
+
// 回転
|
|
2007
|
+
mainParticles.rotation.z += 0.002 + volume * 0.01;
|
|
2008
|
+
|
|
2009
|
+
// 音楽に合わせて色を変化(元の色を保持)
|
|
2010
|
+
const colors = mainParticles.geometry.attributes.color.array;
|
|
2011
|
+
const originalColors = userData.originalColors;
|
|
2012
|
+
const positions = mainParticles.geometry.attributes.position.array;
|
|
2013
|
+
const originalPositions = userData.originalPositions;
|
|
2014
|
+
|
|
2015
|
+
for (let i = 0; i < colors.length; i += 3) {
|
|
2016
|
+
// 元の色を取得
|
|
2017
|
+
const baseColor = new THREE.Color(
|
|
2018
|
+
originalColors[i],
|
|
2019
|
+
originalColors[i + 1],
|
|
2020
|
+
originalColors[i + 2]
|
|
2021
|
+
);
|
|
2022
|
+
|
|
2023
|
+
const hsl = {};
|
|
2024
|
+
baseColor.getHSL(hsl);
|
|
2025
|
+
|
|
2026
|
+
if (volume > 0.005) {
|
|
2027
|
+
// 音楽に合わせて色相をシフト
|
|
2028
|
+
const shift = volume * 0.15 * Math.sin(time * 2 + i * 0.01);
|
|
2029
|
+
const newColor = new THREE.Color().setHSL(
|
|
2030
|
+
(hsl.h + shift + 1) % 1,
|
|
2031
|
+
Math.min(1, hsl.s + volume * 0.1),
|
|
2032
|
+
Math.min(1, hsl.l + volume * 0.1)
|
|
2033
|
+
);
|
|
2034
|
+
colors[i] = newColor.r;
|
|
2035
|
+
colors[i + 1] = newColor.g;
|
|
2036
|
+
colors[i + 2] = newColor.b;
|
|
2037
|
+
|
|
2038
|
+
// 🌸 パーティクルを中心から外側に広げる
|
|
2039
|
+
const particleIndex = i / 3;
|
|
2040
|
+
const px = originalPositions[i];
|
|
2041
|
+
const py = originalPositions[i + 1];
|
|
2042
|
+
const pz = originalPositions[i + 2];
|
|
2043
|
+
|
|
2044
|
+
// 中心からの距離を計算
|
|
2045
|
+
const dist = Math.sqrt(px * px + py * py);
|
|
2046
|
+
|
|
2047
|
+
if (dist > 0.1) {
|
|
2048
|
+
// 外側に広げる量(音量に応じて変化)
|
|
2049
|
+
const expandAmount = 1 + volume * 0.2;
|
|
2050
|
+
const wave = Math.sin(time * 3 + particleIndex * 0.05) * 0.05;
|
|
2051
|
+
const finalExpand = expandAmount + wave;
|
|
2052
|
+
|
|
2053
|
+
positions[i] = px * finalExpand;
|
|
2054
|
+
positions[i + 1] = py * finalExpand;
|
|
2055
|
+
positions[i + 2] = pz + volume * 0.3 * Math.sin(time * 2 + particleIndex * 0.1);
|
|
2056
|
+
}
|
|
2057
|
+
} else {
|
|
2058
|
+
// 音楽がない時は元の色と位置に戻す
|
|
2059
|
+
colors[i] = originalColors[i];
|
|
2060
|
+
colors[i + 1] = originalColors[i + 1];
|
|
2061
|
+
colors[i + 2] = originalColors[i + 2];
|
|
2062
|
+
|
|
2063
|
+
positions[i] = originalPositions[i];
|
|
2064
|
+
positions[i + 1] = originalPositions[i + 1];
|
|
2065
|
+
positions[i + 2] = originalPositions[i + 2];
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
mainParticles.geometry.attributes.color.needsUpdate = true;
|
|
2069
|
+
mainParticles.geometry.attributes.position.needsUpdate = true;
|
|
2070
|
+
|
|
2071
|
+
// 透明度とサイズ(ガラスのような透明感を維持)
|
|
2072
|
+
mainParticles.material.opacity = 0.5 + volume * 0.3;
|
|
2073
|
+
mainParticles.material.size = (0.15 + volume * 0.05) * userData.originalScale;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// キラキラパーティクルのアニメーション
|
|
2077
|
+
if (sparkles) {
|
|
2078
|
+
// ゆっくり回転(音楽があれば速く)
|
|
2079
|
+
sparkles.rotation.z -= 0.002 + volume * 0.02;
|
|
2080
|
+
sparkles.rotation.y += 0.001 + volume * 0.01;
|
|
2081
|
+
|
|
2082
|
+
const sparklePositions = sparkles.geometry.attributes.position.array;
|
|
2083
|
+
const originalSparklePositions = sparkles.userData.originalPositions;
|
|
2084
|
+
const phases = sparkles.userData.phases;
|
|
2085
|
+
|
|
2086
|
+
for (let i = 0; i < sparklePositions.length; i += 3) {
|
|
2087
|
+
const particleIndex = i / 3;
|
|
2088
|
+
const phase = phases[particleIndex];
|
|
2089
|
+
|
|
2090
|
+
// 元の位置を取得
|
|
2091
|
+
const origX = originalSparklePositions[i];
|
|
2092
|
+
const origY = originalSparklePositions[i + 1];
|
|
2093
|
+
const origZ = originalSparklePositions[i + 2];
|
|
2094
|
+
|
|
2095
|
+
// 常に上下に漂う動き
|
|
2096
|
+
const floatSpeed = 0.5 + Math.sin(phase) * 0.3;
|
|
2097
|
+
const floatWave = Math.sin(time * floatSpeed + phase) * 0.3;
|
|
2098
|
+
|
|
2099
|
+
// 音楽に反応して激しく動く
|
|
2100
|
+
const musicWave = Math.sin(time * 4 + phase) * volume * 0.8;
|
|
2101
|
+
|
|
2102
|
+
// 螺旋状に舞う
|
|
2103
|
+
const spiralX = Math.cos(time * 0.3 + phase) * 0.15;
|
|
2104
|
+
const spiralY = Math.sin(time * 0.3 + phase) * 0.15;
|
|
2105
|
+
|
|
2106
|
+
// 元の位置からの相対的な動き
|
|
2107
|
+
sparklePositions[i] = origX + spiralX;
|
|
2108
|
+
sparklePositions[i + 1] = origY + spiralY;
|
|
2109
|
+
sparklePositions[i + 2] = origZ + floatWave + musicWave;
|
|
2110
|
+
}
|
|
2111
|
+
sparkles.geometry.attributes.position.needsUpdate = true;
|
|
2112
|
+
|
|
2113
|
+
// 音楽がある時はより明るく、大きく(常にキラキラ輝く)
|
|
2114
|
+
const twinkle = Math.sin(time * 3) * 0.3; // より激しく明滅
|
|
2115
|
+
sparkles.material.opacity = 0.6 + twinkle + volume * 0.4;
|
|
2116
|
+
sparkles.material.size = (0.12 + Math.sin(time * 4) * 0.04 + volume * 0.15) * userData.originalScale;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// 花全体の回転
|
|
2120
|
+
if (volume > 0.005) {
|
|
2121
|
+
flower.rotation.x = Math.sin(time * 0.3 + index) * 0.08;
|
|
2122
|
+
flower.rotation.y = Math.cos(time * 0.2 + index) * 0.04;
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
|
|
2126
|
+
// ライトも音楽に反応
|
|
2127
|
+
pointLight.intensity = 0.8 + volume * 0.5;
|
|
2128
|
+
pointLight.color.setHSL((time * 0.1) % 1, 0.5, 1);
|
|
2129
|
+
|
|
2130
|
+
controls.update();
|
|
2131
|
+
renderer.render(scene, camera);
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
animate();
|
|
2135
|
+
|
|
2136
|
+
// 📱 レスポンシブ対応
|
|
2137
|
+
window.addEventListener('resize', () => {
|
|
2138
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
2139
|
+
camera.updateProjectionMatrix();
|
|
2140
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
// 30秒後の自動非表示タイマー開始
|
|
2144
|
+
scheduleInfoPanelAutoHide();
|
|
2145
|
+
|
|
2146
|
+
console.log('✅ 音楽の花園が準備完了!🎵 歌ってお花を咲かせましょう');
|
|
2147
|
+
})();
|
|
2148
|
+
</script>
|
|
2149
|
+
</body>
|
|
2150
|
+
</html>
|