@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/bin/chocodrop-mcp.js +13 -0
  4. package/package.json +42 -0
  5. package/runtime/.chocodrop-mcp-runtime +1 -0
  6. package/runtime/asset-bridge.js +460 -0
  7. package/runtime/mcp-server.js +107 -0
  8. package/runtime/site/examples/basic/README.md +38 -0
  9. package/runtime/site/examples/basic/index.html +1730 -0
  10. package/runtime/site/examples/bookmarklet-v2.html +141 -0
  11. package/runtime/site/examples/bookmarklet.html +126 -0
  12. package/runtime/site/examples/lofi-room/index.html +2570 -0
  13. package/runtime/site/examples/lofi-room/sandbox-lite.html +124 -0
  14. package/runtime/site/examples/music-garden/index.html +2150 -0
  15. package/runtime/site/examples/pixel-ocean/index.html +1780 -0
  16. package/runtime/site/examples/react-three-fiber/AdvancedExample.jsx +338 -0
  17. package/runtime/site/examples/react-three-fiber/App.jsx +97 -0
  18. package/runtime/site/examples/react-three-fiber/README.md +294 -0
  19. package/runtime/site/examples/react-three-fiber/package.json +25 -0
  20. package/runtime/site/examples/sdk-test/README.md +70 -0
  21. package/runtime/site/examples/space/index.html +1975 -0
  22. package/runtime/site/examples/toy-city/index.html +2288 -0
  23. package/runtime/site/examples/wabi-sabi/index.html +2515 -0
  24. package/runtime/site/getting-started.html +84 -0
  25. package/runtime/site/index.html +1370 -0
  26. package/runtime/site/public/CommandUI.js +6341 -0
  27. package/runtime/site/public/LiveCommandClient.js +432 -0
  28. package/runtime/site/public/SceneManager.js +5493 -0
  29. package/runtime/site/public/bookmarklet.js +206 -0
  30. package/runtime/site/public/bootstrap.js +50 -0
  31. package/runtime/site/public/chocodrop-demo.umd.js +24020 -0
  32. package/runtime/site/public/chocodrop-demo.umd.min.js +24020 -0
  33. package/runtime/site/public/chocodrop.esm.js +2 -0
  34. package/runtime/site/public/html-sandbox/frame.js +865 -0
  35. package/runtime/site/public/icons/icon-192.png +0 -0
  36. package/runtime/site/public/icons/icon-512.png +0 -0
  37. package/runtime/site/public/icons/icon-maskable-512.png +0 -0
  38. package/runtime/site/public/immersive-launcher.js +175 -0
  39. package/runtime/site/public/immersive.html +165 -0
  40. package/runtime/site/public/index.html +1011 -0
  41. package/runtime/site/public/index.js +9 -0
  42. package/runtime/site/public/load-chocodrop.js +25 -0
  43. package/runtime/site/public/load-three.js +26 -0
  44. package/runtime/site/public/manifest.webmanifest +59 -0
  45. package/runtime/site/public/pwa-bootstrap.js +128 -0
  46. package/runtime/site/public/sample-card.svg +15 -0
  47. package/runtime/site/public/service-worker.js +117 -0
  48. package/runtime/site/public/translation-dictionary.js +257 -0
  49. package/runtime/site/public/xr/xr-ui.css +338 -0
  50. package/runtime/site/public/xr-viewer.html +263 -0
  51. package/runtime/site/src/client/local-bridge.js +83 -0
@@ -0,0 +1,1975 @@
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 - Cosmic Space</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
+ padding: 0;
19
+ overflow: hidden;
20
+ font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
21
+ background: radial-gradient(ellipse at center, #0a0a1a 0%, #000000 100%);
22
+ }
23
+
24
+ #info {
25
+ position: fixed;
26
+ top: 16px;
27
+ left: 16px;
28
+ color: #ffffff;
29
+ background: linear-gradient(135deg, rgba(0, 17, 82, 0.85) 0%, rgba(0, 0, 0, 0.95) 100%);
30
+ backdrop-filter: blur(20px);
31
+ border: 1px solid rgba(100, 200, 255, 0.3);
32
+ padding: 14px 18px;
33
+ border-radius: 8px;
34
+ font-size: 12px;
35
+ font-weight: 400;
36
+ z-index: 2000;
37
+ max-width: 280px;
38
+ text-align: center;
39
+ line-height: 1.6;
40
+ box-shadow:
41
+ 0 8px 32px rgba(0, 100, 255, 0.15),
42
+ 0 0 40px rgba(100, 200, 255, 0.1),
43
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
44
+ transition: opacity 0.3s ease, transform 0.3s ease;
45
+ }
46
+
47
+ #info.hidden {
48
+ opacity: 0;
49
+ transform: translateY(-10px);
50
+ pointer-events: none;
51
+ }
52
+
53
+ .close-btn {
54
+ position: absolute;
55
+ top: 8px;
56
+ right: 8px;
57
+ background: none;
58
+ border: none;
59
+ font-size: 16px;
60
+ color: #64c8ff;
61
+ cursor: pointer;
62
+ width: 20px;
63
+ height: 20px;
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ border-radius: 50%;
68
+ transition: background-color 0.2s ease;
69
+ }
70
+
71
+ .close-btn:hover {
72
+ background-color: rgba(100, 200, 255, 0.1);
73
+ }
74
+
75
+ /* 統合メニュー */
76
+ .unified-menu {
77
+ position: fixed;
78
+ top: 16px;
79
+ right: 20px;
80
+ z-index: 1000;
81
+ }
82
+
83
+ .menu-toggle {
84
+ background: rgba(255, 255, 255, 0.85);
85
+ backdrop-filter: blur(16px);
86
+ -webkit-backdrop-filter: blur(16px);
87
+ border: 1px solid rgba(255, 255, 255, 0.3);
88
+ color: #2d3748;
89
+ padding: 10px 14px;
90
+ border-radius: 8px;
91
+ font-size: 18px;
92
+ cursor: pointer;
93
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
94
+ box-shadow: 0 4px 12px rgba(100, 200, 255, 0.2), 0 2px 6px rgba(0, 0, 0, 0.05);
95
+ width: 44px;
96
+ height: 44px;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ opacity: 0.8;
101
+ }
102
+
103
+ .menu-toggle:hover {
104
+ background: rgba(255, 255, 255, 0.95);
105
+ border-color: rgba(255, 255, 255, 0.5);
106
+ transform: scale(1.1) translateY(-2px);
107
+ box-shadow: 0 6px 16px rgba(100, 200, 255, 0.3), 0 3px 8px rgba(0, 0, 0, 0.1);
108
+ opacity: 1;
109
+ }
110
+
111
+ .menu-dropdown {
112
+ position: absolute;
113
+ top: 56px;
114
+ right: 0;
115
+ min-width: 220px;
116
+ background: rgba(255, 255, 255, 0.9);
117
+ backdrop-filter: blur(16px) saturate(180%);
118
+ -webkit-backdrop-filter: blur(16px) saturate(180%);
119
+ border: 1px solid rgba(100, 200, 255, 0.3);
120
+ border-radius: 12px;
121
+ box-shadow: 0 8px 24px rgba(100, 200, 255, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08);
122
+ opacity: 0;
123
+ visibility: hidden;
124
+ transform: translateY(-10px);
125
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
126
+ overflow: hidden;
127
+ }
128
+
129
+ .menu-dropdown.active {
130
+ opacity: 1;
131
+ visibility: visible;
132
+ transform: translateY(0);
133
+ }
134
+
135
+ .menu-item {
136
+ display: block;
137
+ padding: 12px 16px;
138
+ color: #2d3748;
139
+ text-decoration: none;
140
+ font-size: 13px;
141
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
142
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06);
143
+ cursor: pointer;
144
+ background: none;
145
+ border-left: none;
146
+ border-right: none;
147
+ border-top: none;
148
+ width: 100%;
149
+ text-align: left;
150
+ }
151
+
152
+ .menu-item:last-child {
153
+ border-bottom: none;
154
+ }
155
+
156
+ .menu-item:hover {
157
+ background: rgba(100, 200, 255, 0.12);
158
+ padding-left: 20px;
159
+ border-left: 2px solid rgba(100, 200, 255, 0.5);
160
+ }
161
+
162
+ .menu-item.has-submenu {
163
+ position: relative;
164
+ padding: 0;
165
+ border: none;
166
+ }
167
+
168
+ .submenu-toggle {
169
+ display: block;
170
+ padding: 12px 16px;
171
+ color: #2d3748;
172
+ font-size: 13px;
173
+ background: none;
174
+ border: none;
175
+ width: 100%;
176
+ text-align: left;
177
+ cursor: pointer;
178
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
179
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06);
180
+ }
181
+
182
+ .submenu-toggle:hover {
183
+ background: rgba(100, 200, 255, 0.12);
184
+ padding-left: 20px;
185
+ border-left: 2px solid rgba(100, 200, 255, 0.5);
186
+ }
187
+
188
+ .submenu {
189
+ display: none;
190
+ background: rgba(100, 200, 255, 0.05);
191
+ border-top: 1px solid rgba(100, 200, 255, 0.15);
192
+ }
193
+
194
+ .submenu.active {
195
+ display: block;
196
+ }
197
+
198
+ .submenu .menu-item {
199
+ padding-left: 32px;
200
+ font-size: 12px;
201
+ }
202
+
203
+ .submenu .menu-item:hover {
204
+ padding-left: 36px;
205
+ background: rgba(100, 200, 255, 0.15);
206
+ }
207
+
208
+ /* 🎵 音響ステータス表示 */
209
+ #audio-status {
210
+ position: fixed;
211
+ top: 70px;
212
+ right: 20px;
213
+ z-index: 100;
214
+ background: linear-gradient(135deg, rgba(0, 17, 82, 0.85) 0%, rgba(0, 0, 0, 0.95) 100%);
215
+ backdrop-filter: blur(20px);
216
+ -webkit-backdrop-filter: blur(20px);
217
+ padding: 10px 15px;
218
+ border-radius: 20px;
219
+ font-size: 12px;
220
+ border: 1px solid rgba(100, 200, 255, 0.3);
221
+ box-shadow: 0 8px 32px rgba(0, 100, 255, 0.15), 0 0 40px rgba(100, 200, 255, 0.1);
222
+ color: #ffffff;
223
+ display: none;
224
+ transition: opacity 0.3s ease;
225
+ }
226
+
227
+ #audio-status.active {
228
+ display: block;
229
+ }
230
+
231
+ #audio-status .status-icon {
232
+ display: inline-block;
233
+ margin-right: 5px;
234
+ animation: pulse 1.5s ease-in-out infinite;
235
+ }
236
+
237
+ @keyframes pulse {
238
+ 0%, 100% { opacity: 0.8; transform: scale(1); }
239
+ 50% { opacity: 1; transform: scale(1.1); }
240
+ }
241
+
242
+ /* メニューアイコンのバウンスアニメーション */
243
+ .menu-icon {
244
+ display: inline-block;
245
+ animation: menu-bounce 2s ease-in-out infinite;
246
+ cursor: pointer;
247
+ transition: transform 0.2s ease;
248
+ text-shadow: 0 0 10px currentColor;
249
+ }
250
+
251
+ .menu-icon:hover {
252
+ transform: scale(1.4);
253
+ animation: menu-bounce-fast 0.8s ease-in-out infinite;
254
+ }
255
+
256
+ @keyframes menu-bounce {
257
+ 0%, 100% {
258
+ filter: brightness(1) drop-shadow(0 0 8px currentColor);
259
+ transform: scale(1) translateY(0px);
260
+ }
261
+ 25% {
262
+ filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
263
+ transform: scale(1.1) translateY(-4px);
264
+ }
265
+ 50% {
266
+ filter: brightness(1.5) drop-shadow(0 0 20px currentColor);
267
+ transform: scale(1.15) translateY(-8px);
268
+ }
269
+ 75% {
270
+ filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
271
+ transform: scale(1.1) translateY(-4px);
272
+ }
273
+ }
274
+
275
+ @keyframes menu-bounce-fast {
276
+ 0%, 100% {
277
+ filter: brightness(1.4) drop-shadow(0 0 15px currentColor);
278
+ transform: translateY(0px);
279
+ }
280
+ 50% {
281
+ filter: brightness(1.8) drop-shadow(0 0 25px currentColor);
282
+ transform: translateY(-12px);
283
+ }
284
+ }
285
+
286
+ #hint-toggle {
287
+ position: fixed;
288
+ top: 16px;
289
+ left: 16px;
290
+ background: rgba(139, 92, 246, 0.15);
291
+ backdrop-filter: blur(10px);
292
+ -webkit-backdrop-filter: blur(10px);
293
+ border: 1px solid rgba(139, 92, 246, 0.3);
294
+ color: #a78bfa;
295
+ padding: 4px 6px;
296
+ border-radius: 4px;
297
+ font-size: 12px;
298
+ cursor: pointer;
299
+ z-index: 99;
300
+ display: none;
301
+ transition: all 0.2s ease;
302
+ box-shadow: 0 1px 4px rgba(139, 92, 246, 0.2);
303
+ animation: hint-sparkle 2.5s ease-in-out infinite;
304
+ }
305
+
306
+ #hint-toggle:hover {
307
+ background: rgba(139, 92, 246, 0.25);
308
+ transform: translateY(-1px) scale(1.05);
309
+ animation: hint-sparkle-fast 1s ease-in-out infinite;
310
+ }
311
+
312
+ @keyframes hint-sparkle {
313
+ 0%, 100% {
314
+ filter: brightness(1) drop-shadow(0 0 3px rgba(139, 92, 246, 0.3));
315
+ transform: scale(1) rotate(0deg);
316
+ }
317
+ 25% {
318
+ filter: brightness(1.1) drop-shadow(0 0 6px rgba(139, 92, 246, 0.5));
319
+ transform: scale(1.02) rotate(2deg);
320
+ }
321
+ 50% {
322
+ filter: brightness(1.2) drop-shadow(0 0 8px rgba(139, 92, 246, 0.7));
323
+ transform: scale(1.05) rotate(0deg);
324
+ }
325
+ 75% {
326
+ filter: brightness(1.1) drop-shadow(0 0 6px rgba(139, 92, 246, 0.5));
327
+ transform: scale(1.02) rotate(-2deg);
328
+ }
329
+ }
330
+
331
+ @keyframes hint-sparkle-fast {
332
+ 0%, 100% {
333
+ filter: brightness(1.1) drop-shadow(0 0 5px rgba(139, 92, 246, 0.6));
334
+ transform: scale(1.05) rotate(0deg);
335
+ }
336
+ 50% {
337
+ filter: brightness(1.3) drop-shadow(0 0 10px rgba(139, 92, 246, 0.8));
338
+ transform: scale(1.1) rotate(5deg);
339
+ }
340
+ }
341
+
342
+ .navigation {
343
+ position: fixed;
344
+ top: 16px;
345
+ right: 20px;
346
+ z-index: 100;
347
+ display: none; /* 統合メニューに移行 */
348
+ gap: 8px;
349
+ }
350
+
351
+ .nav-link {
352
+ background: linear-gradient(135deg, rgba(0, 17, 82, 0.85) 0%, rgba(0, 0, 0, 0.95) 100%);
353
+ backdrop-filter: blur(20px);
354
+ border: 1px solid rgba(139, 92, 246, 0.3);
355
+ color: #ffffff;
356
+ text-decoration: none;
357
+ padding: 8px 12px;
358
+ border-radius: 6px;
359
+ font-size: 12px;
360
+ transition: all 0.2s ease;
361
+ box-shadow: 0 2px 8px rgba(0, 100, 255, 0.15);
362
+ }
363
+
364
+ .nav-link:hover {
365
+ background: rgba(139, 92, 246, 0.2);
366
+ transform: translateY(-1px);
367
+ }
368
+
369
+ .controls {
370
+ margin-top: 8px;
371
+ padding-top: 8px;
372
+ border-top: 1px solid rgba(100, 200, 255, 0.3);
373
+ font-size: 11px;
374
+ }
375
+
376
+ .control-item {
377
+ margin: 4px 0;
378
+ display: flex;
379
+ align-items: center;
380
+ gap: 8px;
381
+ }
382
+
383
+ .key {
384
+ background: rgba(100, 200, 255, 0.15);
385
+ padding: 2px 6px;
386
+ border-radius: 3px;
387
+ font-weight: 500;
388
+ min-width: fit-content;
389
+ }
390
+
391
+ #info strong {
392
+ font-size: 18px;
393
+ font-weight: 700;
394
+ color: #64c8ff;
395
+ display: block;
396
+ margin-bottom: 12px;
397
+ text-align: center;
398
+ text-shadow: 0 0 20px rgba(100, 200, 255, 0.8);
399
+ letter-spacing: 0.5px;
400
+ }
401
+
402
+ #info code {
403
+ background: rgba(100, 200, 255, 0.15);
404
+ color: #a8daff;
405
+ padding: 4px 8px;
406
+ border-radius: 6px;
407
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
408
+ font-size: 13px;
409
+ font-weight: 500;
410
+ border: 1px solid rgba(100, 200, 255, 0.2);
411
+ }
412
+
413
+ .space-icon {
414
+ display: inline-block;
415
+ animation: space-float 3s ease-in-out infinite;
416
+ cursor: pointer;
417
+ transition: all 0.3s ease;
418
+ text-shadow: 0 0 15px currentColor;
419
+ }
420
+
421
+ .space-icon:hover {
422
+ transform: scale(1.4);
423
+ animation: space-pulse 0.8s ease-in-out infinite;
424
+ }
425
+
426
+ .space-title {
427
+ display: inline-block;
428
+ animation: space-glow 4s ease-in-out infinite;
429
+ background: linear-gradient(45deg, #64c8ff, #4facfe, #00f2fe);
430
+ -webkit-background-clip: text;
431
+ -webkit-text-fill-color: transparent;
432
+ background-clip: text;
433
+ }
434
+
435
+ @keyframes space-glow {
436
+ 0%, 100% {
437
+ text-shadow: 0 0 20px rgba(100, 200, 255, 0.8);
438
+ filter: brightness(1);
439
+ }
440
+ 50% {
441
+ text-shadow: 0 0 30px rgba(100, 200, 255, 1), 0 0 50px rgba(79, 172, 254, 0.8);
442
+ filter: brightness(1.2);
443
+ }
444
+ }
445
+
446
+ @keyframes space-float {
447
+ 0%, 100% {
448
+ transform: translateY(0px) rotate(0deg);
449
+ filter: brightness(1) drop-shadow(0 0 10px currentColor);
450
+ }
451
+ 33% {
452
+ transform: translateY(-8px) rotate(5deg);
453
+ filter: brightness(1.3) drop-shadow(0 0 20px currentColor);
454
+ }
455
+ 66% {
456
+ transform: translateY(4px) rotate(-3deg);
457
+ filter: brightness(1.1) drop-shadow(0 0 15px currentColor);
458
+ }
459
+ }
460
+
461
+ @keyframes space-pulse {
462
+ 0%, 100% {
463
+ filter: brightness(1.2) drop-shadow(0 0 15px currentColor);
464
+ transform: rotate(0deg);
465
+ }
466
+ 50% {
467
+ filter: brightness(2) drop-shadow(0 0 30px currentColor) drop-shadow(0 0 20px #fff);
468
+ transform: rotate(10deg);
469
+ }
470
+ }
471
+
472
+ .highlight {
473
+ color: #4facfe;
474
+ font-weight: 600;
475
+ text-shadow: 0 0 10px rgba(79, 172, 254, 0.6);
476
+ }
477
+
478
+ .subtitle {
479
+ color: #a8daff;
480
+ font-size: 13px;
481
+ margin-top: 10px;
482
+ opacity: 0.85;
483
+ font-weight: 300;
484
+ }
485
+
486
+ .demo-links {
487
+ position: fixed;
488
+ bottom: 20px;
489
+ right: 100px;
490
+ display: none; /* 統合メニューに移行 */
491
+ gap: 12px;
492
+ z-index: 100;
493
+ }
494
+
495
+ .demo-link {
496
+ background: linear-gradient(135deg, rgba(0, 17, 82, 0.9) 0%, rgba(0, 0, 0, 0.95) 100%);
497
+ color: white;
498
+ text-decoration: none;
499
+ padding: 12px 18px;
500
+ border-radius: 12px;
501
+ font-size: 12px;
502
+ font-weight: 600;
503
+ border: 1px solid rgba(139, 92, 246, 0.3);
504
+ box-shadow: 0 4px 20px rgba(0, 100, 255, 0.2);
505
+ transition: all 0.3s ease;
506
+ backdrop-filter: blur(20px);
507
+ }
508
+
509
+ .demo-link:hover {
510
+ transform: translateY(-3px);
511
+ background: rgba(139, 92, 246, 0.2);
512
+ border-color: rgba(139, 92, 246, 0.5);
513
+ text-shadow: 0 0 10px currentColor;
514
+ }
515
+
516
+ canvas {
517
+ display: block;
518
+ }
519
+ </style>
520
+ </head>
521
+ <body>
522
+ <!-- 🎵 音響ステータス表示 -->
523
+ <div id="audio-status">
524
+ <span class="status-icon">🎤</span><span id="audio-text">音声待機中...</span>
525
+ </div>
526
+
527
+ <!-- Navigation Links -->
528
+ <div class="navigation">
529
+ <a href="../../index.html" class="nav-link">🏠 Home</a>
530
+ <a href="https://github.com/nyukicorn/chocodrop" class="nav-link" target="_blank">📦 GitHub</a>
531
+ </div>
532
+
533
+ <!-- Hint Toggle Button (initially hidden) -->
534
+ <button id="hint-toggle">💡</button>
535
+
536
+ <!-- Main Info Panel -->
537
+ <div id="info">
538
+ <button class="close-btn" onclick="hideInfoPanel()">×</button>
539
+ <strong>🌌 コズミック空間へようこそ</strong>
540
+
541
+ <div style="font-size: 11px; opacity: 0.9; margin-bottom: 12px; line-height: 1.6; text-align: center; font-style: italic;">
542
+ 銀河が広がり、3つの星雲が煌めく無限の宇宙
543
+ </div>
544
+
545
+ 💡 基本操作:<br>
546
+ 1. 動画や画像をインポートしてみて!<br>
547
+ 2. 🌌宇宙空間配置をやってみて!(「画像を中央に」「右上に配置」)<br>
548
+ 3. 視点操作: ChocoDrop / ちょこドロップを最小化してドラッグ<br>
549
+ 4. ✨スペース世界をやってみて!(銀河の中で創造)<br>
550
+ 5. オブジェクトをクリック → WASDキーで移動<br>
551
+
552
+ <div class="controls">
553
+ <div class="control-item">
554
+ <span class="key">@</span>
555
+ <span>ChocoDrop起動</span>
556
+ </div>
557
+ <div class="control-item">
558
+ <span class="key">🍫</span>
559
+ <span>右下ボタンからも起動可</span>
560
+ </div>
561
+ </div>
562
+ </div>
563
+
564
+ <div class="demo-links">
565
+ <a href="../basic/index.html" class="demo-link">🍫 はじまりの国</a>
566
+ <a href="../pixel-ocean/index.html" class="demo-link">🌊 海底世界</a>
567
+ <a href="../music-garden/index.html" class="demo-link">🌸 硝子の花</a>
568
+ <a href="../wabi-sabi/index.html" class="demo-link">🖤 侘び寂び</a>
569
+ <a href="../toy-city/index.html" class="demo-link">🎡 AI遊園地</a>
570
+ </div>
571
+
572
+ <!-- 統合メニュー -->
573
+ <div class="unified-menu">
574
+ <button class="menu-toggle" id="unifiedMenuToggle" aria-label="Menu">☰</button>
575
+ <div class="menu-dropdown" id="unifiedMenuDropdown">
576
+ <button class="menu-item" id="showInfoBtn"><span class="menu-icon">📖</span> このワールドについて</button>
577
+
578
+ <div class="menu-item has-submenu">
579
+ <button class="submenu-toggle" id="worldsMenuToggle"><span class="menu-icon">🌍</span> 世界選択 <span style="font-size: 10px;">▼</span></button>
580
+ <div class="submenu" id="worldsSubmenu">
581
+ <a href="../basic/index.html" class="menu-item"><span class="menu-icon">🍫</span> はじまりの国</a>
582
+ <a href="../pixel-ocean/index.html" class="menu-item"><span class="menu-icon">🌊</span> 海底世界</a>
583
+ <a href="../music-garden/index.html" class="menu-item"><span class="menu-icon">🌸</span> 硝子の花</a>
584
+ <a href="../wabi-sabi/index.html" class="menu-item"><span class="menu-icon">🖤</span> 侘び寂びの世界</a>
585
+ <a href="../toy-city/index.html" class="menu-item"><span class="menu-icon">🎡</span> AI遊園地</a>
586
+ <a href="index.html" class="menu-item"><span class="menu-icon">🌌</span> 宇宙空間(現在地)</a>
587
+ </div>
588
+ </div>
589
+
590
+ <a href="../../index.html" class="menu-item"><span class="menu-icon">🏠</span> Home</a>
591
+ <a href="https://github.com/nyukicorn/chocodrop" class="menu-item" target="_blank"><span class="menu-icon">🔗</span> GitHub</a>
592
+
593
+ <button class="menu-item" id="vrMenuButton"><span class="menu-icon">🥽</span> VR</button>
594
+ <button class="menu-item" id="arMenuButton"><span class="menu-icon">🥽</span> AR</button>
595
+ </div>
596
+ </div>
597
+
598
+ <!-- Three.js + ChocoDrop (module preload) -->
599
+ <script type="module">
600
+ import ensureChocoDrop from '../../public/load-chocodrop.js';
601
+
602
+ // 🎯 情報パネル制御をローダー前に初期化
603
+ let autoHideTimer = null;
604
+
605
+ const getInfoPanel = () => document.getElementById('info');
606
+ const getHintToggle = () => document.getElementById('hint-toggle');
607
+
608
+ const clearInfoPanelAutoHide = () => {
609
+ if (autoHideTimer) {
610
+ clearTimeout(autoHideTimer);
611
+ autoHideTimer = null;
612
+ }
613
+ };
614
+
615
+ const scheduleInfoPanelAutoHide = () => {
616
+ clearInfoPanelAutoHide();
617
+ autoHideTimer = window.setTimeout(() => {
618
+ if (typeof window.hideInfoPanel === 'function') {
619
+ window.hideInfoPanel();
620
+ }
621
+ }, 30000);
622
+ };
623
+
624
+ window.hideInfoPanel = function hideInfoPanel() {
625
+ const infoPanel = getInfoPanel();
626
+ if (!infoPanel) {
627
+ clearInfoPanelAutoHide();
628
+ return;
629
+ }
630
+
631
+ infoPanel.classList.add('hidden');
632
+ window.setTimeout(() => {
633
+ if (infoPanel.classList.contains('hidden')) {
634
+ infoPanel.style.display = 'none';
635
+ const hintToggle = getHintToggle();
636
+ if (hintToggle) {
637
+ hintToggle.style.display = 'block';
638
+ }
639
+ }
640
+ }, 300);
641
+
642
+ clearInfoPanelAutoHide();
643
+ };
644
+
645
+ window.showInfoPanel = function showInfoPanel() {
646
+ const infoPanel = getInfoPanel();
647
+ if (!infoPanel) {
648
+ return;
649
+ }
650
+
651
+ const hintToggle = getHintToggle();
652
+ if (hintToggle) {
653
+ hintToggle.style.display = 'none';
654
+ }
655
+
656
+ infoPanel.style.display = 'block';
657
+ window.requestAnimationFrame(() => {
658
+ infoPanel.classList.remove('hidden');
659
+ });
660
+
661
+ scheduleInfoPanelAutoHide();
662
+ };
663
+
664
+ const initialHintToggle = getHintToggle();
665
+ if (initialHintToggle) {
666
+ initialHintToggle.addEventListener('click', window.showInfoPanel);
667
+ }
668
+
669
+ (async () => {
670
+ await ensureChocoDrop();
671
+
672
+ const THREE = window.THREE;
673
+ const ChocoDrop = window.ChocoDrop;
674
+ // 統合メニューの制御
675
+ (function() {
676
+ const menuToggle = document.getElementById('unifiedMenuToggle');
677
+ const menuDropdown = document.getElementById('unifiedMenuDropdown');
678
+ const worldsMenuToggle = document.getElementById('worldsMenuToggle');
679
+ const worldsSubmenu = document.getElementById('worldsSubmenu');
680
+ const showInfoBtn = document.getElementById('showInfoBtn');
681
+ const infoPanel = document.getElementById('info');
682
+
683
+ // 初回訪問チェック - localStorage使用
684
+ const FIRST_VISIT_KEY = 'space_visited';
685
+ const hasVisited = localStorage.getItem(FIRST_VISIT_KEY);
686
+
687
+ if (!hasVisited) {
688
+ // 初回訪問時は情報パネルを表示
689
+ if (infoPanel) {
690
+ infoPanel.style.display = 'block';
691
+ }
692
+ localStorage.setItem(FIRST_VISIT_KEY, 'true');
693
+ } else {
694
+ // 2回目以降は非表示
695
+ if (infoPanel) {
696
+ infoPanel.style.display = 'none';
697
+ }
698
+ }
699
+
700
+ // 統合メニューのトグル
701
+ if (menuToggle && menuDropdown) {
702
+ menuToggle.addEventListener('click', (e) => {
703
+ e.stopPropagation();
704
+ menuDropdown.classList.toggle('active');
705
+ });
706
+
707
+ // 外側クリックで閉じる
708
+ document.addEventListener('click', (e) => {
709
+ if (!menuToggle.contains(e.target) && !menuDropdown.contains(e.target)) {
710
+ menuDropdown.classList.remove('active');
711
+ // サブメニューも閉じる
712
+ if (worldsSubmenu) {
713
+ worldsSubmenu.classList.remove('active');
714
+ }
715
+ }
716
+ });
717
+
718
+ // Escキーで閉じる
719
+ document.addEventListener('keydown', (e) => {
720
+ if (e.key === 'Escape') {
721
+ menuDropdown.classList.remove('active');
722
+ if (worldsSubmenu) {
723
+ worldsSubmenu.classList.remove('active');
724
+ }
725
+ }
726
+ });
727
+ }
728
+
729
+ // 世界選択サブメニューのトグル
730
+ if (worldsMenuToggle && worldsSubmenu) {
731
+ worldsMenuToggle.addEventListener('click', (e) => {
732
+ e.stopPropagation();
733
+ worldsSubmenu.classList.toggle('active');
734
+ });
735
+ }
736
+
737
+ // 情報パネル表示ボタン
738
+ if (showInfoBtn) {
739
+ showInfoBtn.addEventListener('click', () => {
740
+ // グローバルに定義された showInfoPanel() を呼び出す
741
+ if (typeof showInfoPanel === 'function') {
742
+ showInfoPanel();
743
+ } else {
744
+ // フォールバック: 直接表示
745
+ const panel = document.getElementById('info');
746
+ if (panel) {
747
+ panel.style.display = 'block';
748
+ panel.classList.remove('hidden');
749
+ }
750
+ }
751
+ menuDropdown.classList.remove('active');
752
+ });
753
+ }
754
+
755
+ // VR/ARボタンは後でThree.jsのrendererと連携
756
+ // グローバル変数として保存
757
+ window.unifiedMenuVRButton = document.getElementById('vrMenuButton');
758
+ window.unifiedMenuARButton = document.getElementById('arMenuButton');
759
+ })();
760
+
761
+ console.log('🌌 Cosmic Space Loading...');
762
+ console.log('THREE loaded:', typeof THREE !== 'undefined');
763
+
764
+ // Basic Three.js setup
765
+ const scene = new THREE.Scene();
766
+ const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
767
+ const renderer = new THREE.WebGLRenderer({
768
+ antialias: true,
769
+ alpha: true,
770
+ powerPreference: "high-performance"
771
+ });
772
+
773
+ renderer.setSize(window.innerWidth, window.innerHeight);
774
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
775
+ renderer.shadowMap.enabled = false; // 軽量化のため影は無効
776
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
777
+ document.body.appendChild(renderer.domElement);
778
+
779
+ // 🌌 深宇宙の環境設定
780
+ scene.background = new THREE.Color(0x000510); // 深い宇宙の色
781
+ scene.fog = new THREE.FogExp2(0x000510, 0.0008); // 遠くのオブジェクトをフェード
782
+
783
+ // 🌌 円形パーティクル用のテクスチャ作成
784
+ function createCircleTexture() {
785
+ const canvas = document.createElement('canvas');
786
+ canvas.width = 64;
787
+ canvas.height = 64;
788
+ const context = canvas.getContext('2d');
789
+
790
+ // グラデーションで円形を描画
791
+ const gradient = context.createRadialGradient(32, 32, 0, 32, 32, 32);
792
+ gradient.addColorStop(0, 'rgba(255,255,255,1)');
793
+ gradient.addColorStop(0.2, 'rgba(255,255,255,1)');
794
+ gradient.addColorStop(0.4, 'rgba(255,255,255,0.8)');
795
+ gradient.addColorStop(0.7, 'rgba(255,255,255,0.4)');
796
+ gradient.addColorStop(1, 'rgba(255,255,255,0)');
797
+
798
+ context.fillStyle = gradient;
799
+ context.fillRect(0, 0, 64, 64);
800
+
801
+ return new THREE.CanvasTexture(canvas);
802
+ }
803
+
804
+ // 🌌 渦巻き銀河作成(美しいグラデーション付き円形パーティクル)
805
+ function createGalaxy() {
806
+ const parameters = {
807
+ count: 8000, // 10000→8000に減らして適度な密度に
808
+ size: 0.02,
809
+ radius: 150, // 50→150に拡大して広々と
810
+ branches: 3,
811
+ spin: 0.5, // 1→0.5で緩やかな螺旋に
812
+ randomness: 0.3, // 0.2→0.3でより自然な散らばり
813
+ randomnessPower: 2, // 3→2で均等な分布に
814
+ insideColor: 0xffa575,
815
+ outsideColor: 0x311599
816
+ };
817
+
818
+ const geometry = new THREE.BufferGeometry();
819
+ const positions = new Float32Array(parameters.count * 3);
820
+ const colors = new Float32Array(parameters.count * 3);
821
+ const sizes = new Float32Array(parameters.count);
822
+
823
+ const colorInside = new THREE.Color(parameters.insideColor);
824
+ const colorOutside = new THREE.Color(parameters.outsideColor);
825
+
826
+ for (let i = 0; i < parameters.count; i++) {
827
+ const i3 = i * 3;
828
+
829
+ // 位置計算
830
+ const radius = Math.random() * parameters.radius;
831
+ const spinAngle = radius * parameters.spin;
832
+ const branchAngle = (i % parameters.branches) / parameters.branches * Math.PI * 2;
833
+
834
+ // ランダム性(中心から離れるほど強く)
835
+ const randomX = Math.pow(Math.random(), parameters.randomnessPower) *
836
+ (Math.random() < 0.5 ? 1 : -1) * parameters.randomness * radius;
837
+ const randomY = Math.pow(Math.random(), parameters.randomnessPower) *
838
+ (Math.random() < 0.5 ? 1 : -1) * parameters.randomness * radius * 0.5;
839
+ const randomZ = Math.pow(Math.random(), parameters.randomnessPower) *
840
+ (Math.random() < 0.5 ? 1 : -1) * parameters.randomness * radius;
841
+
842
+ positions[i3] = Math.cos(branchAngle + spinAngle) * radius + randomX;
843
+ positions[i3 + 1] = randomY;
844
+ positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius + randomZ;
845
+
846
+ // 色のグラデーション(中心は明るく、外側は暗く)
847
+ const mixedColor = colorInside.clone();
848
+ mixedColor.lerp(colorOutside, radius / parameters.radius);
849
+
850
+ colors[i3] = mixedColor.r;
851
+ colors[i3 + 1] = mixedColor.g;
852
+ colors[i3 + 2] = mixedColor.b;
853
+
854
+ // ランダムサイズ(中心は大きく、外側は小さく、でもバラツキあり)
855
+ const baseSize = (1 - radius / parameters.radius) + 0.2;
856
+ const randomFactor = 0.3 + Math.random() * 1.7; // 0.3~2.0倍のランダム
857
+ sizes[i] = baseSize * randomFactor;
858
+ }
859
+
860
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
861
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
862
+ geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
863
+
864
+ // 円形テクスチャを作成
865
+ const circleTexture = createCircleTexture();
866
+
867
+ const material = new THREE.PointsMaterial({
868
+ size: 1.5, // 2→1.5で繊細に
869
+ sizeAttenuation: true,
870
+ vertexColors: true,
871
+ transparent: true,
872
+ opacity: 0.85, // 0.9→0.85で優雅に
873
+ blending: THREE.AdditiveBlending,
874
+ depthWrite: false,
875
+ map: circleTexture // 円形テクスチャを適用
876
+ });
877
+
878
+ const galaxy = new THREE.Points(geometry, material);
879
+ scene.add(galaxy);
880
+
881
+ // 動的回転設定
882
+ galaxy.userData = {
883
+ rotationSpeed: 0.0002, // 時計回りの基本速度
884
+ initialPositions: positions.slice(), // 初期位置保存
885
+ time: 0
886
+ };
887
+
888
+ return galaxy;
889
+ }
890
+
891
+ // 🪐 軽量惑星作成
892
+ function createBeautifulPlanet(radius, color, x, y, z, planetType = 'solid') {
893
+ const planetGroup = new THREE.Group();
894
+
895
+ // 🪐 惑星本体
896
+ const geometry = new THREE.IcosahedronGeometry(radius, 2);
897
+ let material;
898
+
899
+ // 惑星タイプ別のマテリアル
900
+ if (planetType === 'earth') {
901
+ // 🌍 地球テクスチャを作成
902
+ const earthTexture = (function() {
903
+ const canvas = document.createElement('canvas');
904
+ canvas.width = 256;
905
+ canvas.height = 128;
906
+ const ctx = canvas.getContext('2d');
907
+
908
+ // 海のベース(青)
909
+ ctx.fillStyle = '#1e6ba8';
910
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
911
+
912
+ // 大陸を描画(緑と茶色)
913
+ // ユーラシア大陸
914
+ ctx.fillStyle = '#4a8c4a';
915
+ ctx.beginPath();
916
+ ctx.ellipse(180, 40, 40, 20, 0, 0, Math.PI * 2);
917
+ ctx.fill();
918
+
919
+ // アフリカ大陸
920
+ ctx.fillStyle = '#6b8e23';
921
+ ctx.beginPath();
922
+ ctx.ellipse(150, 70, 20, 30, 0, 0, Math.PI * 2);
923
+ ctx.fill();
924
+
925
+ // アメリカ大陸
926
+ ctx.fillStyle = '#5a7c5a';
927
+ ctx.beginPath();
928
+ ctx.ellipse(70, 60, 25, 35, -0.3, 0, Math.PI * 2);
929
+ ctx.fill();
930
+
931
+ // 雲の渦(半透明の白)
932
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
933
+ ctx.lineWidth = 3;
934
+ for (let i = 0; i < 5; i++) {
935
+ ctx.beginPath();
936
+ const x = Math.random() * canvas.width;
937
+ const y = Math.random() * canvas.height;
938
+ ctx.arc(x, y, 10 + Math.random() * 20, 0, Math.PI * 1.5);
939
+ ctx.stroke();
940
+ }
941
+
942
+ // 極地の氷(白)
943
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
944
+ ctx.fillRect(0, 0, canvas.width, 8);
945
+ ctx.fillRect(0, canvas.height - 8, canvas.width, 8);
946
+
947
+ return new THREE.CanvasTexture(canvas);
948
+ })();
949
+
950
+ material = new THREE.MeshPhongMaterial({
951
+ map: earthTexture,
952
+ emissive: 0x112244,
953
+ shininess: 100
954
+ });
955
+ } else if (planetType === 'gas') {
956
+ material = new THREE.MeshLambertMaterial({
957
+ color: color,
958
+ emissive: new THREE.Color(color).multiplyScalar(0.2),
959
+ transparent: true,
960
+ opacity: 0.9
961
+ });
962
+ } else {
963
+ material = new THREE.MeshLambertMaterial({
964
+ color: color,
965
+ emissive: new THREE.Color(color).multiplyScalar(0.15)
966
+ });
967
+ }
968
+
969
+ const planet = new THREE.Mesh(geometry, material);
970
+ planetGroup.add(planet);
971
+
972
+ // ✨ 大気グロー効果
973
+ if (planetType === 'earth' || planetType === 'gas') {
974
+ const atmosphereGeometry = new THREE.IcosahedronGeometry(radius * 1.1, 1);
975
+ const atmosphereMaterial = new THREE.MeshBasicMaterial({
976
+ color: planetType === 'earth' ? 0x87ceeb : new THREE.Color(color).multiplyScalar(0.7),
977
+ transparent: true,
978
+ opacity: 0.15,
979
+ side: THREE.BackSide,
980
+ blending: THREE.AdditiveBlending
981
+ });
982
+
983
+ const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
984
+ planetGroup.add(atmosphere);
985
+ }
986
+
987
+ // 🪐 土星風リング(ガス惑星用)
988
+ if (planetType === 'gas') {
989
+ const ringGeometry = new THREE.RingGeometry(radius * 1.3, radius * 1.8, 32);
990
+ const ringMaterial = new THREE.MeshBasicMaterial({
991
+ color: new THREE.Color(color).multiplyScalar(0.8),
992
+ transparent: true,
993
+ opacity: 0.6,
994
+ side: THREE.DoubleSide,
995
+ blending: THREE.AdditiveBlending
996
+ });
997
+
998
+ const ring = new THREE.Mesh(ringGeometry, ringMaterial);
999
+ ring.rotation.x = Math.PI / 2 + (Math.random() - 0.5) * 0.3; // 少し傾ける
1000
+ planetGroup.add(ring);
1001
+
1002
+ // リング用アニメーションデータ
1003
+ ring.userData = { rotationSpeed: 0.005 };
1004
+ planetGroup.userData.ring = ring;
1005
+ }
1006
+
1007
+ // 🌙 衛星(月型惑星用)
1008
+ if (planetType === 'solid' && radius < 6) { // 小さな固体惑星に衛星
1009
+ const moonGeometry = new THREE.IcosahedronGeometry(radius * 0.3, 1);
1010
+ const moonMaterial = new THREE.MeshLambertMaterial({
1011
+ color: 0xcccccc,
1012
+ emissive: 0x111111
1013
+ });
1014
+
1015
+ const moon = new THREE.Mesh(moonGeometry, moonMaterial);
1016
+ moon.position.set(radius * 2.5, 0, 0);
1017
+ planetGroup.add(moon);
1018
+
1019
+ // 衛星の公転データ
1020
+ moon.userData = {
1021
+ orbitRadius: radius * 2.5,
1022
+ orbitSpeed: 0.02,
1023
+ angle: Math.random() * Math.PI * 2
1024
+ };
1025
+ planetGroup.userData.moon = moon;
1026
+ }
1027
+
1028
+ planetGroup.position.set(x, y, z);
1029
+
1030
+ // 惑星の自転データ
1031
+ planetGroup.userData.rotationSpeed = {
1032
+ x: (Math.random() - 0.5) * 0.01,
1033
+ y: Math.random() * 0.02 + 0.005,
1034
+ z: (Math.random() - 0.5) * 0.005
1035
+ };
1036
+ planetGroup.userData.planetType = planetType;
1037
+
1038
+ scene.add(planetGroup);
1039
+ return planetGroup;
1040
+ }
1041
+
1042
+ // 🌟 軽量ネビュラ(パーティクルクラウド)
1043
+ function createRealNebula(x, y, z, color, intensity = 1.0) {
1044
+ const nebulaGroup = new THREE.Group();
1045
+
1046
+ // ☁️ 雲状スプライト用のキャンバステクスチャ作成
1047
+ function createNebulaTexture(size = 512) {
1048
+ const canvas = document.createElement('canvas');
1049
+ canvas.width = size;
1050
+ canvas.height = size;
1051
+ const context = canvas.getContext('2d');
1052
+
1053
+ // グラデーション背景
1054
+ const gradient = context.createRadialGradient(
1055
+ size/2, size/2, 0,
1056
+ size/2, size/2, size/2
1057
+ );
1058
+ gradient.addColorStop(0, 'rgba(255,255,255,0.8)');
1059
+ gradient.addColorStop(0.3, 'rgba(255,255,255,0.4)');
1060
+ gradient.addColorStop(0.7, 'rgba(255,255,255,0.1)');
1061
+ gradient.addColorStop(1, 'rgba(255,255,255,0)');
1062
+
1063
+ context.fillStyle = gradient;
1064
+ context.fillRect(0, 0, size, size);
1065
+
1066
+ // ノイズで有機的な形状を作る
1067
+ for (let i = 0; i < 50; i++) {
1068
+ const x = Math.random() * size;
1069
+ const y = Math.random() * size;
1070
+ const radius = Math.random() * size * 0.3;
1071
+ const opacity = Math.random() * 0.3;
1072
+
1073
+ const grad = context.createRadialGradient(x, y, 0, x, y, radius);
1074
+ grad.addColorStop(0, `rgba(255,255,255,${opacity})`);
1075
+ grad.addColorStop(1, 'rgba(255,255,255,0)');
1076
+
1077
+ context.fillStyle = grad;
1078
+ context.fillRect(0, 0, size, size);
1079
+ }
1080
+
1081
+ return new THREE.CanvasTexture(canvas);
1082
+ }
1083
+
1084
+ const nebulaTexture = createNebulaTexture();
1085
+
1086
+ // 🌫️ 複数レイヤーの雲状スプライト(軽量化:3レイヤー)
1087
+ for (let layer = 0; layer < 3; layer++) {
1088
+ const layerGroup = new THREE.Group();
1089
+
1090
+ for (let i = 0; i < 8; i++) {
1091
+ const spriteMaterial = new THREE.SpriteMaterial({
1092
+ map: nebulaTexture,
1093
+ color: new THREE.Color(color),
1094
+ transparent: true,
1095
+ opacity: (0.3 - layer * 0.05) * intensity,
1096
+ blending: THREE.AdditiveBlending,
1097
+ depthWrite: false
1098
+ });
1099
+
1100
+ const sprite = new THREE.Sprite(spriteMaterial);
1101
+
1102
+ // 有機的な配置
1103
+ const angle = (i / 8) * Math.PI * 2;
1104
+ const radius = (20 + Math.random() * 30) * (1 + layer * 0.3);
1105
+ const height = (Math.random() - 0.5) * 20;
1106
+
1107
+ sprite.position.set(
1108
+ Math.cos(angle) * radius + (Math.random() - 0.5) * 15,
1109
+ height + (Math.random() - 0.5) * 10,
1110
+ Math.sin(angle) * radius + (Math.random() - 0.5) * 15
1111
+ );
1112
+
1113
+ // サイズをランダムに
1114
+ const scale = (30 + Math.random() * 40) * (1 + layer * 0.2);
1115
+ sprite.scale.set(scale, scale, scale);
1116
+
1117
+ // 回転
1118
+ sprite.material.rotation = Math.random() * Math.PI * 2;
1119
+
1120
+ layerGroup.add(sprite);
1121
+ }
1122
+
1123
+ nebulaGroup.add(layerGroup);
1124
+ }
1125
+
1126
+ // ✨ 中心の明るい核
1127
+ const coreMaterial = new THREE.SpriteMaterial({
1128
+ map: nebulaTexture,
1129
+ color: new THREE.Color(color).multiplyScalar(2),
1130
+ transparent: true,
1131
+ opacity: 0.6 * intensity,
1132
+ blending: THREE.AdditiveBlending
1133
+ });
1134
+
1135
+ const coreSprite = new THREE.Sprite(coreMaterial);
1136
+ coreSprite.scale.set(40, 40, 40);
1137
+ nebulaGroup.add(coreSprite);
1138
+
1139
+ nebulaGroup.position.set(x, y, z);
1140
+
1141
+ // アニメーション用データ
1142
+ nebulaGroup.userData = {
1143
+ rotationSpeed: 0.0001 + Math.random() * 0.0002,
1144
+ pulseSpeed: 0.5 + Math.random() * 0.5,
1145
+ originalIntensity: intensity
1146
+ };
1147
+
1148
+ scene.add(nebulaGroup);
1149
+ return nebulaGroup;
1150
+ }
1151
+
1152
+ // 🛰️ 軽量スペースステーション
1153
+ function createSpaceStation(x, y, z) {
1154
+ const stationGroup = new THREE.Group();
1155
+
1156
+ // 🏗️ 中央コアモジュール(円筒形)
1157
+ const coreModule = new THREE.Mesh(
1158
+ new THREE.CylinderGeometry(3, 3, 12, 16),
1159
+ new THREE.MeshPhongMaterial({
1160
+ color: 0xc0c0c0,
1161
+ emissive: 0x002244,
1162
+ shininess: 100
1163
+ })
1164
+ );
1165
+ coreModule.rotation.z = Math.PI / 2; // 横向きに
1166
+ stationGroup.add(coreModule);
1167
+
1168
+ // 🔗 接続トンネル
1169
+ const tunnelGeometry = new THREE.CylinderGeometry(1, 1, 8, 8);
1170
+ const tunnelMaterial = new THREE.MeshPhongMaterial({
1171
+ color: 0x909090,
1172
+ emissive: 0x001122
1173
+ });
1174
+
1175
+ const tunnel1 = new THREE.Mesh(tunnelGeometry, tunnelMaterial);
1176
+ tunnel1.position.set(8, 0, 0);
1177
+ tunnel1.rotation.z = Math.PI / 2;
1178
+ stationGroup.add(tunnel1);
1179
+
1180
+ const tunnel2 = new THREE.Mesh(tunnelGeometry, tunnelMaterial);
1181
+ tunnel2.position.set(-8, 0, 0);
1182
+ tunnel2.rotation.z = Math.PI / 2;
1183
+ stationGroup.add(tunnel2);
1184
+
1185
+ // 🏠 居住モジュール(左右)
1186
+ const moduleGeometry = new THREE.CylinderGeometry(2.5, 2.5, 6, 12);
1187
+ const moduleMaterial = new THREE.MeshPhongMaterial({
1188
+ color: 0xa0a0a0,
1189
+ emissive: 0x001122
1190
+ });
1191
+
1192
+ const module1 = new THREE.Mesh(moduleGeometry, moduleMaterial);
1193
+ module1.position.set(14, 0, 0);
1194
+ module1.rotation.z = Math.PI / 2;
1195
+ stationGroup.add(module1);
1196
+
1197
+ const module2 = new THREE.Mesh(moduleGeometry, moduleMaterial);
1198
+ module2.position.set(-14, 0, 0);
1199
+ module2.rotation.z = Math.PI / 2;
1200
+ stationGroup.add(module2);
1201
+
1202
+ // ☀️ 巨大ソーラーパネル(4枚)
1203
+ const createSolarPanel = (x, y, z, rotY = 0) => {
1204
+ const panelGroup = new THREE.Group();
1205
+
1206
+ // パネル本体
1207
+ const panel = new THREE.Mesh(
1208
+ new THREE.BoxGeometry(16, 0.2, 8),
1209
+ new THREE.MeshPhongMaterial({
1210
+ color: 0x001a4d,
1211
+ emissive: 0x000033,
1212
+ shininess: 200,
1213
+ transparent: true,
1214
+ opacity: 0.9
1215
+ })
1216
+ );
1217
+
1218
+ // パネル表面のグリッド線
1219
+ const gridGeometry = new THREE.EdgesGeometry(new THREE.BoxGeometry(16, 0.2, 8));
1220
+ const gridMaterial = new THREE.LineBasicMaterial({ color: 0x0066ff });
1221
+ const grid = new THREE.LineSegments(gridGeometry, gridMaterial);
1222
+
1223
+ panelGroup.add(panel);
1224
+ panelGroup.add(grid);
1225
+ panelGroup.position.set(x, y, z);
1226
+ panelGroup.rotation.y = rotY;
1227
+
1228
+ return panelGroup;
1229
+ };
1230
+
1231
+ stationGroup.add(createSolarPanel(0, 12, 0));
1232
+ stationGroup.add(createSolarPanel(0, -12, 0));
1233
+ stationGroup.add(createSolarPanel(0, 0, 12, Math.PI/2));
1234
+ stationGroup.add(createSolarPanel(0, 0, -12, Math.PI/2));
1235
+
1236
+ // 📡 通信アンテナ
1237
+ const createAntenna = (x, y, z) => {
1238
+ const antennaGroup = new THREE.Group();
1239
+
1240
+ // アンテナ支柱
1241
+ const mast = new THREE.Mesh(
1242
+ new THREE.CylinderGeometry(0.1, 0.1, 8),
1243
+ new THREE.MeshPhongMaterial({ color: 0x606060 })
1244
+ );
1245
+
1246
+ // パラボラアンテナ
1247
+ const dish = new THREE.Mesh(
1248
+ new THREE.SphereGeometry(1.5, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2),
1249
+ new THREE.MeshPhongMaterial({
1250
+ color: 0x808080,
1251
+ side: THREE.DoubleSide
1252
+ })
1253
+ );
1254
+ dish.position.y = 4;
1255
+
1256
+ antennaGroup.add(mast);
1257
+ antennaGroup.add(dish);
1258
+ antennaGroup.position.set(x, y, z);
1259
+
1260
+ return antennaGroup;
1261
+ };
1262
+
1263
+ stationGroup.add(createAntenna(3, 6, 3));
1264
+ stationGroup.add(createAntenna(-3, 6, -3));
1265
+
1266
+ // 💡 点滅ライト
1267
+ const createLight = (x, y, z, color) => {
1268
+ const light = new THREE.Mesh(
1269
+ new THREE.SphereGeometry(0.3),
1270
+ new THREE.MeshBasicMaterial({
1271
+ color: color,
1272
+ transparent: true,
1273
+ opacity: 0.8
1274
+ })
1275
+ );
1276
+ light.position.set(x, y, z);
1277
+ return light;
1278
+ };
1279
+
1280
+ const redLight1 = createLight(6, 2, 2, 0xff0000);
1281
+ const redLight2 = createLight(-6, 2, 2, 0xff0000);
1282
+ const greenLight1 = createLight(6, -2, -2, 0x00ff00);
1283
+ const greenLight2 = createLight(-6, -2, -2, 0x00ff00);
1284
+
1285
+ stationGroup.add(redLight1);
1286
+ stationGroup.add(redLight2);
1287
+ stationGroup.add(greenLight1);
1288
+ stationGroup.add(greenLight2);
1289
+
1290
+ // 🎯 ドッキングポート
1291
+ const dockingPort = new THREE.Mesh(
1292
+ new THREE.CylinderGeometry(1.5, 1.2, 2, 8),
1293
+ new THREE.MeshPhongMaterial({
1294
+ color: 0x606060,
1295
+ emissive: 0x001122
1296
+ })
1297
+ );
1298
+ dockingPort.position.set(0, 0, 8);
1299
+ stationGroup.add(dockingPort);
1300
+
1301
+ stationGroup.position.set(x, y, z);
1302
+
1303
+ // アニメーションデータ
1304
+ stationGroup.userData = {
1305
+ rotationSpeed: 0.002,
1306
+ lights: [redLight1, redLight2, greenLight1, greenLight2],
1307
+ blinkTime: 0
1308
+ };
1309
+
1310
+ scene.add(stationGroup);
1311
+ return stationGroup;
1312
+ }
1313
+
1314
+ // 🌌 宇宙環境構築
1315
+ // 美しい渦巻き銀河を作成
1316
+ const galaxy = createGalaxy();
1317
+
1318
+ // 惑星系
1319
+ const planets = [];
1320
+ planets.push(createBeautifulPlanet(8, 0x4169e1, -80, 10, -40, 'earth')); // 地球型 + 大気グロー
1321
+ planets.push(createBeautifulPlanet(6, 0xff6347, 60, -15, -80, 'solid')); // 火星型
1322
+ planets.push(createBeautifulPlanet(12, 0xffd700, -120, 25, 50, 'gas')); // ガス惑星 + リング + 大気
1323
+ planets.push(createBeautifulPlanet(4, 0xc0c0c0, 90, 5, 70, 'solid')); // 月型 + 衛星
1324
+
1325
+ // ネビュラ
1326
+ const nebulas = [];
1327
+ nebulas.push(createRealNebula(-200, 30, -100, 0x9932cc, 1.2)); // パープル
1328
+ nebulas.push(createRealNebula(180, -40, 80, 0x00ced1, 1.0)); // シアン
1329
+ nebulas.push(createRealNebula(-50, 60, 150, 0xff69b4, 1.5)); // ピンク
1330
+
1331
+ // スペースステーション
1332
+ const spaceStation = createSpaceStation(0, 0, -30);
1333
+
1334
+ // ✨ 宇宙ライティング
1335
+ const ambientLight = new THREE.AmbientLight(0x404040, 0.3); // 薄暗い環境光
1336
+ scene.add(ambientLight);
1337
+
1338
+ // 遠方の恒星光
1339
+ const starLight = new THREE.DirectionalLight(0xffffff, 0.8);
1340
+ starLight.position.set(100, 50, 100);
1341
+ scene.add(starLight);
1342
+
1343
+ // ネビュラからの光
1344
+ const nebulaLight = new THREE.PointLight(0x9932cc, 0.5, 200);
1345
+ nebulaLight.position.set(-200, 30, -100);
1346
+ scene.add(nebulaLight);
1347
+
1348
+ // 📷 カメラ位置
1349
+ camera.position.set(0, 30, 100);
1350
+ camera.lookAt(0, 0, 0);
1351
+
1352
+ // 🎮 コントロール
1353
+ const controls = new THREE.OrbitControls(camera, renderer.domElement);
1354
+ controls.enableDamping = true;
1355
+ controls.dampingFactor = 0.05;
1356
+ controls.minDistance = 10;
1357
+ controls.maxDistance = 500;
1358
+
1359
+ // 🎵 高度な音響システムの初期化(wabi-sabi方式)
1360
+ let audioContext = null;
1361
+ let analyser = null;
1362
+ let dataArray = null;
1363
+ let beatDetectionHistory = [];
1364
+ let lastBeatTime = 0;
1365
+ let musicData = {
1366
+ volume: 0,
1367
+ bass: 0,
1368
+ mid: 0,
1369
+ treble: 0,
1370
+ tempo: 0,
1371
+ bpm: 0,
1372
+ beatDetected: false,
1373
+ isPlaying: false
1374
+ };
1375
+ const connectedVideos = new WeakSet(); // 接続済みのvideoを追跡
1376
+ const audioStatusEl = document.getElementById('audio-status');
1377
+ const audioTextEl = document.getElementById('audio-text');
1378
+
1379
+ function setupAudioAnalysis() {
1380
+ if (!audioContext) {
1381
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
1382
+ analyser = audioContext.createAnalyser();
1383
+ analyser.fftSize = 256;
1384
+ dataArray = new Uint8Array(analyser.frequencyBinCount);
1385
+ console.log('🎵 Advanced audio analysis initialized');
1386
+ }
1387
+ }
1388
+
1389
+ // 動画要素を音響分析に接続
1390
+ function connectVideoToAudio(videoElement) {
1391
+ // すでに接続済みの場合はスキップ
1392
+ if (connectedVideos.has(videoElement)) {
1393
+ console.log('🎵 Video already connected, skipping');
1394
+ return;
1395
+ }
1396
+
1397
+ if (!audioContext || !analyser) {
1398
+ setupAudioAnalysis();
1399
+ }
1400
+
1401
+ try {
1402
+ // AudioContextがsuspended状態の場合は再開
1403
+ if (audioContext.state === 'suspended') {
1404
+ audioContext.resume();
1405
+ }
1406
+
1407
+ const source = audioContext.createMediaElementSource(videoElement);
1408
+ source.connect(analyser);
1409
+ analyser.connect(audioContext.destination);
1410
+
1411
+ connectedVideos.add(videoElement); // 接続済みとしてマーク
1412
+
1413
+ musicData.isPlaying = true;
1414
+ if (audioStatusEl) {
1415
+ audioStatusEl.classList.add('active');
1416
+ if (audioTextEl) {
1417
+ audioTextEl.textContent = '音響連動中 ♪ (BPM検出中)';
1418
+ }
1419
+ }
1420
+ console.log('🎵 Video connected to advanced audio analyser');
1421
+ } catch (error) {
1422
+ console.warn('Audio connection error:', error);
1423
+ }
1424
+ }
1425
+
1426
+ // 音楽データの詳細解析
1427
+ function analyzeMusicData() {
1428
+ if (!analyser || !dataArray) return;
1429
+
1430
+ analyser.getByteFrequencyData(dataArray);
1431
+
1432
+ // 音量計算
1433
+ let sum = 0;
1434
+ for (let i = 0; i < dataArray.length; i++) {
1435
+ sum += dataArray[i];
1436
+ }
1437
+ musicData.volume = sum / dataArray.length / 255;
1438
+
1439
+ // 周波数帯域分析
1440
+ const bassRange = Math.floor(dataArray.length * 0.1);
1441
+ const midRange = Math.floor(dataArray.length * 0.5);
1442
+
1443
+ let bassSum = 0, midSum = 0, trebleSum = 0;
1444
+
1445
+ for (let i = 0; i < bassRange; i++) {
1446
+ bassSum += dataArray[i];
1447
+ }
1448
+ for (let i = bassRange; i < midRange; i++) {
1449
+ midSum += dataArray[i];
1450
+ }
1451
+ for (let i = midRange; i < dataArray.length; i++) {
1452
+ trebleSum += dataArray[i];
1453
+ }
1454
+
1455
+ musicData.bass = bassSum / bassRange / 255;
1456
+ musicData.mid = midSum / (midRange - bassRange) / 255;
1457
+ musicData.treble = trebleSum / (dataArray.length - midRange) / 255;
1458
+
1459
+ // 🥁 ビート検出とBPM計算
1460
+ const currentTime = performance.now();
1461
+ const bassThreshold = 0.3;
1462
+ const volumeThreshold = 0.2;
1463
+
1464
+ musicData.beatDetected = false;
1465
+
1466
+ // ビート検出(ベース音量の急激な変化)
1467
+ if (musicData.bass > bassThreshold && musicData.volume > volumeThreshold) {
1468
+ if (currentTime - lastBeatTime > 300) { // 最小300ms間隔
1469
+ musicData.beatDetected = true;
1470
+ lastBeatTime = currentTime;
1471
+
1472
+ // BPM計算用履歴更新
1473
+ beatDetectionHistory.push(currentTime);
1474
+ if (beatDetectionHistory.length > 8) {
1475
+ beatDetectionHistory.shift();
1476
+ }
1477
+
1478
+ // BPM計算
1479
+ if (beatDetectionHistory.length >= 4) {
1480
+ const intervals = [];
1481
+ for (let i = 1; i < beatDetectionHistory.length; i++) {
1482
+ intervals.push(beatDetectionHistory[i] - beatDetectionHistory[i-1]);
1483
+ }
1484
+ const avgInterval = intervals.reduce((a, b) => a + b) / intervals.length;
1485
+ musicData.bpm = Math.round(60000 / avgInterval);
1486
+
1487
+ // ステータス更新
1488
+ if (audioTextEl && musicData.bpm > 0) {
1489
+ audioTextEl.textContent = `音響連動中 ♪ (${musicData.bpm} BPM)`;
1490
+ }
1491
+ }
1492
+ }
1493
+ }
1494
+ }
1495
+
1496
+ // ChocoDrop のビデオ要素を監視して自動接続
1497
+ function observeVideoElements() {
1498
+ const videoObserver = new MutationObserver((mutations) => {
1499
+ mutations.forEach((mutation) => {
1500
+ mutation.addedNodes.forEach((node) => {
1501
+ if (node.nodeName === 'VIDEO') {
1502
+ console.log('🎬 Video element detected, connecting to audio...');
1503
+ connectVideoToAudio(node);
1504
+ }
1505
+ });
1506
+ });
1507
+ });
1508
+
1509
+ videoObserver.observe(document.body, {
1510
+ childList: true,
1511
+ subtree: true
1512
+ });
1513
+
1514
+ // 既存のビデオ要素もチェック
1515
+ const existingVideos = document.querySelectorAll('video');
1516
+ existingVideos.forEach(video => {
1517
+ connectVideoToAudio(video);
1518
+ });
1519
+ }
1520
+
1521
+ // 初期化
1522
+ if (audioStatusEl) {
1523
+ audioStatusEl.classList.add('active');
1524
+ }
1525
+ observeVideoElements();
1526
+
1527
+ // 🍫 ChocoDrop初期化
1528
+ let chocoDrop;
1529
+
1530
+ function initChocoDrop() {
1531
+ try {
1532
+ chocoDrop = ChocoDrop.createChocoDrop(scene, {
1533
+ camera: camera,
1534
+ renderer: renderer,
1535
+ // GitHub Pagesなので生成機能なし、インポートのみ
1536
+ serverUrl: null,
1537
+ enableServerHealthCheck: false, // サーバーレス環境なのでヘルスチェック無効
1538
+ skipServiceDialog: true, // GitHub Pages用:サービス設定ダイアログを無効化
1539
+ sceneOptions: {
1540
+ enableMouseInteraction: true
1541
+ },
1542
+ onControlsToggle: (disabled) => {
1543
+ controls.enabled = !disabled;
1544
+ }
1545
+ });
1546
+
1547
+ console.log('🌌 Cosmic Space ready! Press @ to explore the universe!');
1548
+
1549
+ // 宇宙風のヒント
1550
+ window.addEventListener('keydown', (event) => {
1551
+ if (event.code === 'Space' || event.key === '@') {
1552
+ console.log('🚀 Try: "画像を中央に", "右上に配置", "ファイルを選択"');
1553
+ }
1554
+ });
1555
+
1556
+ } catch (error) {
1557
+ console.error('ChocoDrop initialization failed:', error);
1558
+ }
1559
+ }
1560
+
1561
+ // 初期化
1562
+ setTimeout(initChocoDrop, 1000);
1563
+
1564
+ // ★ VR/ARボタンを統合メニューと連携(bodyには追加しない)
1565
+ if (typeof VRButton !== 'undefined' && window.unifiedMenuVRButton) {
1566
+ console.log('✅ VRButton script loaded - connecting to unified menu');
1567
+
1568
+ // VRセッション開始関数を作成
1569
+ function startVRSession() {
1570
+ if ('xr' in navigator) {
1571
+ navigator.xr.isSessionSupported('immersive-vr').then(supported => {
1572
+ if (supported) {
1573
+ const sessionInit = { optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'layers'] };
1574
+ navigator.xr.requestSession('immersive-vr', sessionInit).then(session => {
1575
+ session.addEventListener('end', () => {
1576
+ renderer.xr.setSession(null);
1577
+ });
1578
+ renderer.xr.setSession(session);
1579
+ });
1580
+ } else {
1581
+ console.log('VR not supported on this device');
1582
+ // NOT SUPPORTEDメッセージは表示しない(静かに失敗)
1583
+ }
1584
+ });
1585
+ }
1586
+ }
1587
+
1588
+ window.unifiedMenuVRButton.onclick = startVRSession;
1589
+
1590
+ // サポートチェック - 未サポートの場合はボタンを非表示に
1591
+ if ('xr' in navigator) {
1592
+ navigator.xr.isSessionSupported('immersive-vr').then(supported => {
1593
+ if (!supported) {
1594
+ window.unifiedMenuVRButton.style.display = 'none';
1595
+ }
1596
+ });
1597
+ } else {
1598
+ window.unifiedMenuVRButton.style.display = 'none';
1599
+ }
1600
+ } else {
1601
+ console.log('❌ VRButton script not loaded or menu button not found');
1602
+ if (window.unifiedMenuVRButton) {
1603
+ window.unifiedMenuVRButton.style.display = 'none';
1604
+ }
1605
+ }
1606
+
1607
+ if (typeof ARButton !== 'undefined' && window.unifiedMenuARButton) {
1608
+ console.log('✅ ARButton script loaded - connecting to unified menu');
1609
+
1610
+ // ARセッション開始関数を作成
1611
+ function startARSession() {
1612
+ if ('xr' in navigator) {
1613
+ navigator.xr.isSessionSupported('immersive-ar').then(supported => {
1614
+ if (supported) {
1615
+ const sessionInit = {
1616
+ requiredFeatures: ['hit-test'],
1617
+ optionalFeatures: ['dom-overlay'],
1618
+ domOverlay: { root: document.body }
1619
+ };
1620
+ navigator.xr.requestSession('immersive-ar', sessionInit).then(session => {
1621
+ session.addEventListener('end', () => {
1622
+ renderer.xr.setSession(null);
1623
+ });
1624
+ renderer.xr.setSession(session);
1625
+ });
1626
+ } else {
1627
+ console.log('AR not supported on this device');
1628
+ // NOT SUPPORTEDメッセージは表示しない(静かに失敗)
1629
+ }
1630
+ });
1631
+ }
1632
+ }
1633
+
1634
+ window.unifiedMenuARButton.onclick = startARSession;
1635
+
1636
+ // サポートチェック - 未サポートの場合はボタンを非表示に
1637
+ if ('xr' in navigator) {
1638
+ navigator.xr.isSessionSupported('immersive-ar').then(supported => {
1639
+ if (!supported) {
1640
+ window.unifiedMenuARButton.style.display = 'none';
1641
+ }
1642
+ });
1643
+ } else {
1644
+ window.unifiedMenuARButton.style.display = 'none';
1645
+ }
1646
+ } else {
1647
+ console.log('❌ ARButton script not loaded or menu button not found');
1648
+ if (window.unifiedMenuARButton) {
1649
+ window.unifiedMenuARButton.style.display = 'none';
1650
+ }
1651
+ }
1652
+
1653
+ // 🎬 アニメーションループ
1654
+ function animate() {
1655
+ requestAnimationFrame(animate);
1656
+
1657
+ // 🎵 高度な音響データ解析
1658
+ if (musicData.isPlaying) {
1659
+ analyzeMusicData();
1660
+ }
1661
+
1662
+ // 🎵 8バンド周波数解析(惑星の個別反応用)
1663
+ let frequencies = new Array(8).fill(0);
1664
+ if (analyser && dataArray) {
1665
+ const bands = 8;
1666
+ const bandSize = Math.floor(dataArray.length / bands);
1667
+ for (let i = 0; i < bands; i++) {
1668
+ let sum = 0;
1669
+ for (let j = 0; j < bandSize; j++) {
1670
+ sum += dataArray[i * bandSize + j];
1671
+ }
1672
+ frequencies[i] = sum / bandSize / 255;
1673
+ }
1674
+ }
1675
+
1676
+ // 音響連動の強度(宇宙空間は迫力ある反応 + ビート反応)
1677
+ const audioIntensity = musicData.volume * 1.0;
1678
+ const bassBoost = musicData.bass * 2.0;
1679
+ const midBoost = musicData.mid * 1.5;
1680
+ const trebleBoost = musicData.treble * 1.2;
1681
+ const beatPulse = musicData.beatDetected ? 2.0 : 1.0; // ビート時に大きく反応(宇宙空間は派手に)
1682
+
1683
+ // 銀河のゆっくり回転 + 音響連動 + ビート反応
1684
+ // 銀河の動的アニメーション(時計回り、内側ほど速く)
1685
+ if (galaxy && galaxy.userData.rotationSpeed) {
1686
+ const time = performance.now() * 0.001;
1687
+ galaxy.userData.time = time;
1688
+
1689
+ // 全体をゆっくり時計回りに回転 + 音楽で速度アップ + ビート時に加速
1690
+ const rotationSpeedMultiplier = (1 + audioIntensity * 2) * beatPulse;
1691
+ galaxy.rotation.y += galaxy.userData.rotationSpeed * rotationSpeedMultiplier;
1692
+
1693
+ // 各星の個別動き(内側ほど速く回転)+ 音響連動 + ビート反応
1694
+ const positions = galaxy.geometry.attributes.position.array;
1695
+ const initialPositions = galaxy.userData.initialPositions;
1696
+
1697
+ for (let i = 0; i < positions.length; i += 3) {
1698
+ const x = initialPositions[i];
1699
+ const z = initialPositions[i + 2];
1700
+ const radius = Math.sqrt(x * x + z * z);
1701
+
1702
+ // 内側ほど速く回転(実際の銀河のように)+ 音響連動 + ビート時に加速
1703
+ const speedMultiplier = Math.max(0.3, 1 - (radius / 150) * 0.6) * (1 + bassBoost * 0.8) * beatPulse;
1704
+ const angle = time * speedMultiplier * 0.2;
1705
+
1706
+ // 時計回りの回転を適用
1707
+ const cosAngle = Math.cos(angle);
1708
+ const sinAngle = Math.sin(angle);
1709
+
1710
+ positions[i] = x * cosAngle - z * sinAngle;
1711
+ positions[i + 2] = x * sinAngle + z * cosAngle;
1712
+
1713
+ // Y軸にも微細な波打つような動き + 音響連動 + ビート時に激しく
1714
+ positions[i + 1] = initialPositions[i + 1] + Math.sin(time * 0.5 * (1 + midBoost) + i * 0.01) * 0.5 * (1 + trebleBoost * 0.5) * beatPulse;
1715
+ }
1716
+
1717
+ galaxy.geometry.attributes.position.needsUpdate = true;
1718
+
1719
+ // 音楽に合わせて銀河の明るさ変化 + ビート時に輝く
1720
+ if (galaxy.material && galaxy.material.opacity !== undefined) {
1721
+ galaxy.material.opacity = (0.8 + audioIntensity * 0.2) * Math.min(1.5, beatPulse);
1722
+ }
1723
+ }
1724
+
1725
+ // 惑星の自転 + 音響連動 + ビート反応
1726
+ planets.forEach((planet, planetIndex) => {
1727
+ if (planet.userData && planet.userData.rotationSpeed) {
1728
+ // 惑星本体の自転 + 音楽で速度アップ + ビート時に加速
1729
+ const planetAudioBoost = (1 + frequencies[planetIndex % 8] * 1.5) * beatPulse; // 各惑星が異なる周波数に反応
1730
+ planet.rotation.x += planet.userData.rotationSpeed.x * planetAudioBoost;
1731
+ planet.rotation.y += planet.userData.rotationSpeed.y * planetAudioBoost;
1732
+ planet.rotation.z += planet.userData.rotationSpeed.z * planetAudioBoost;
1733
+
1734
+ // 音楽に合わせて惑星の輝き変化 + ビート時に強く光る
1735
+ if (planet.material && planet.material.emissiveIntensity !== undefined) {
1736
+ planet.material.emissiveIntensity = (0.3 + frequencies[planetIndex % 8] * 0.7) * Math.min(1.5, beatPulse);
1737
+ }
1738
+
1739
+ // 🪐 リングの回転(ガス惑星)+ 音響連動 + ビート反応
1740
+ if (planet.userData.ring) {
1741
+ planet.userData.ring.rotation.z += planet.userData.ring.userData.rotationSpeed * (1 + midBoost * 1.2) * beatPulse;
1742
+ }
1743
+
1744
+ // 🌙 衛星の公転(月型惑星)+ 音響連動 + ビート反応
1745
+ if (planet.userData.moon) {
1746
+ const moon = planet.userData.moon;
1747
+ moon.userData.angle += moon.userData.orbitSpeed * (1 + trebleBoost * 0.8) * beatPulse;
1748
+ moon.position.x = Math.cos(moon.userData.angle) * moon.userData.orbitRadius;
1749
+ moon.position.z = Math.sin(moon.userData.angle) * moon.userData.orbitRadius;
1750
+
1751
+ // 衛星の自転 + 音響連動 + ビート時に高速回転
1752
+ moon.rotation.y += 0.05 * (1 + audioIntensity) * beatPulse;
1753
+ }
1754
+ }
1755
+ });
1756
+
1757
+ // スペースステーションの回転と点滅ライト + 音響連動 + ビート反応
1758
+ if (spaceStation && spaceStation.userData.rotationSpeed) {
1759
+ spaceStation.rotation.y += spaceStation.userData.rotationSpeed * (1 + audioIntensity * 0.5) * beatPulse;
1760
+
1761
+ // 点滅ライトアニメーション + 音響連動 + ビート時に激しく点滅
1762
+ const time = performance.now() * 0.001;
1763
+ spaceStation.userData.blinkTime = time;
1764
+
1765
+ spaceStation.userData.lights.forEach((light, index) => {
1766
+ // 各ライトが異なるタイミングで点滅 + 音楽に同期 + ビート時に明るく
1767
+ const blinkSpeed = (2 + index * 0.5) * (1 + trebleBoost * 2) * beatPulse;
1768
+ const intensity = Math.sin(time * blinkSpeed) * 0.5 + 0.5;
1769
+ light.material.opacity = (0.3 + intensity * 0.7) * (1 + audioIntensity * 0.3) * Math.min(1.5, beatPulse);
1770
+ });
1771
+ }
1772
+
1773
+ // ネビュラのゆっくりした動き + 音響連動 + ビート反応
1774
+ nebulas.forEach((nebula, index) => {
1775
+ if (nebula.userData && nebula.userData.rotationSpeed) {
1776
+ // 全体のゆっくりした回転 + 音響連動 + ビート時に加速
1777
+ const nebulaAudioBoost = (1 + frequencies[(index * 2) % 8] * 1.0) * beatPulse;
1778
+ nebula.rotation.y += nebula.userData.rotationSpeed * nebulaAudioBoost;
1779
+ nebula.rotation.z += nebula.userData.rotationSpeed * 0.5 * nebulaAudioBoost;
1780
+
1781
+ // 脈動効果(呼吸するような)+ 音響連動で膨張 + ビート時に大きく膨張
1782
+ const time = performance.now() * 0.001;
1783
+ const pulse = Math.sin(time * nebula.userData.pulseSpeed * (1 + bassBoost * 0.5)) * 0.1 + 1;
1784
+ const audioPulse = pulse * (1 + bassBoost * 0.3) * beatPulse; // 低音でネビュラが膨張 + ビート時にさらに膨張
1785
+ nebula.scale.setScalar(audioPulse);
1786
+
1787
+ // レイヤーごとに異なる回転速度 + 音響連動 + ビート反応
1788
+ nebula.children.forEach((layer, layerIndex) => {
1789
+ if (layer.children) {
1790
+ layer.rotation.y += nebula.userData.rotationSpeed * (layerIndex + 1) * 0.3 * (1 + midBoost * 0.8) * beatPulse;
1791
+ }
1792
+ });
1793
+
1794
+ // 音楽に合わせてネビュラの明るさ変化 + ビート時に輝く
1795
+ nebula.children.forEach((layer) => {
1796
+ if (layer.material && layer.material.opacity !== undefined) {
1797
+ layer.material.opacity = Math.min(1.0, layer.material.opacity * (1 + audioIntensity * 0.2) * Math.min(1.3, beatPulse));
1798
+ }
1799
+ });
1800
+ }
1801
+ });
1802
+
1803
+ controls.update();
1804
+ renderer.render(scene, camera);
1805
+ }
1806
+
1807
+ animate();
1808
+
1809
+ // 📱 レスポンシブ対応
1810
+ window.addEventListener('resize', () => {
1811
+ camera.aspect = window.innerWidth / window.innerHeight;
1812
+ camera.updateProjectionMatrix();
1813
+ renderer.setSize(window.innerWidth, window.innerHeight);
1814
+ });
1815
+
1816
+ // 30秒後の自動非表示タイマー開始
1817
+ scheduleInfoPanelAutoHide();
1818
+
1819
+ console.log('✅ コズミック空間が準備完了!🌌 宇宙で創造しましょう');
1820
+
1821
+ })();
1822
+ </script>
1823
+
1824
+ <!-- VR/AR Button inline implementation -->
1825
+ <script>
1826
+ // VRButton implementation
1827
+ const VRButton = {
1828
+ createButton: function(renderer) {
1829
+ const button = document.createElement('button');
1830
+
1831
+ function showEnterVR() {
1832
+ button.style.display = '';
1833
+ button.style.cursor = 'pointer';
1834
+ button.style.left = 'calc(50% - 50px)';
1835
+ button.style.width = '100px';
1836
+ button.textContent = 'ENTER VR';
1837
+ button.onmouseenter = function() { button.style.opacity = '1.0'; };
1838
+ button.onmouseleave = function() { button.style.opacity = '0.5'; };
1839
+ button.onclick = function() {
1840
+ const session = renderer.xr.getSession();
1841
+ if (session) {
1842
+ session.end();
1843
+ } else {
1844
+ navigator.xr.requestSession('immersive-vr', {
1845
+ optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking']
1846
+ }).then(session => {
1847
+ renderer.xr.setSession(session);
1848
+ button.textContent = 'EXIT VR';
1849
+
1850
+ session.addEventListener('end', () => {
1851
+ button.textContent = 'ENTER VR';
1852
+ });
1853
+ }).catch(err => {
1854
+ console.error('VR session failed:', err);
1855
+ });
1856
+ }
1857
+ };
1858
+ }
1859
+
1860
+ function showWebXRNotFound() {
1861
+ button.style.display = '';
1862
+ button.style.cursor = 'auto';
1863
+ button.style.left = 'calc(50% - 75px)';
1864
+ button.style.width = '150px';
1865
+ button.textContent = 'VR NOT SUPPORTED';
1866
+ button.onmouseenter = null;
1867
+ button.onmouseleave = null;
1868
+ button.onclick = null;
1869
+ }
1870
+
1871
+ button.style.position = 'absolute';
1872
+ button.style.bottom = '20px';
1873
+ button.style.padding = '12px 6px';
1874
+ button.style.border = '1px solid #fff';
1875
+ button.style.borderRadius = '4px';
1876
+ button.style.background = 'rgba(0,0,0,0.1)';
1877
+ button.style.color = '#fff';
1878
+ button.style.font = 'normal 13px sans-serif';
1879
+ button.style.textAlign = 'center';
1880
+ button.style.opacity = '0.5';
1881
+ button.style.outline = 'none';
1882
+ button.style.zIndex = '999';
1883
+
1884
+ if ('xr' in navigator) {
1885
+ navigator.xr.isSessionSupported('immersive-vr').then(supported => {
1886
+ supported ? showEnterVR() : showWebXRNotFound();
1887
+ });
1888
+ } else {
1889
+ showWebXRNotFound();
1890
+ }
1891
+
1892
+ return button;
1893
+ }
1894
+ };
1895
+
1896
+ // ARButton implementation
1897
+ const ARButton = {
1898
+ createButton: function(renderer, options) {
1899
+ const button = document.createElement('button');
1900
+
1901
+ function showEnterAR() {
1902
+ button.style.display = '';
1903
+ button.style.cursor = 'pointer';
1904
+ button.style.left = 'calc(50% - 50px)';
1905
+ button.style.width = '100px';
1906
+ button.textContent = 'ENTER AR';
1907
+ button.onmouseenter = function() { button.style.opacity = '1.0'; };
1908
+ button.onmouseleave = function() { button.style.opacity = '0.5'; };
1909
+ button.onclick = function() {
1910
+ const session = renderer.xr.getSession();
1911
+ if (session) {
1912
+ session.end();
1913
+ } else {
1914
+ const sessionInit = options || {};
1915
+ if (!sessionInit.optionalFeatures) {
1916
+ sessionInit.optionalFeatures = [];
1917
+ }
1918
+ if (!sessionInit.requiredFeatures) {
1919
+ sessionInit.requiredFeatures = ['hit-test'];
1920
+ }
1921
+
1922
+ navigator.xr.requestSession('immersive-ar', sessionInit).then(session => {
1923
+ renderer.xr.setSession(session);
1924
+ button.textContent = 'EXIT AR';
1925
+
1926
+ session.addEventListener('end', () => {
1927
+ button.textContent = 'ENTER AR';
1928
+ });
1929
+ }).catch(err => {
1930
+ console.error('AR session failed:', err);
1931
+ });
1932
+ }
1933
+ };
1934
+ }
1935
+
1936
+ function showWebXRNotFound() {
1937
+ button.style.display = '';
1938
+ button.style.cursor = 'auto';
1939
+ button.style.left = 'calc(50% - 75px)';
1940
+ button.style.width = '150px';
1941
+ button.textContent = 'AR NOT SUPPORTED';
1942
+ button.onmouseenter = null;
1943
+ button.onmouseleave = null;
1944
+ button.onclick = null;
1945
+ }
1946
+
1947
+ button.style.position = 'absolute';
1948
+ button.style.bottom = '20px';
1949
+ button.style.padding = '12px 6px';
1950
+ button.style.border = '1px solid #fff';
1951
+ button.style.borderRadius = '4px';
1952
+ button.style.background = 'rgba(0,0,0,0.1)';
1953
+ button.style.color = '#fff';
1954
+ button.style.font = 'normal 13px sans-serif';
1955
+ button.style.textAlign = 'center';
1956
+ button.style.opacity = '0.5';
1957
+ button.style.outline = 'none';
1958
+ button.style.zIndex = '999';
1959
+
1960
+ if ('xr' in navigator) {
1961
+ navigator.xr.isSessionSupported('immersive-ar').then(supported => {
1962
+ supported ? showEnterAR() : showWebXRNotFound();
1963
+ });
1964
+ } else {
1965
+ showWebXRNotFound();
1966
+ }
1967
+
1968
+ return button;
1969
+ }
1970
+ };
1971
+
1972
+ console.log('✅ VRButton/ARButton inline implementation loaded');
1973
+ </script>
1974
+ </body>
1975
+ </html>