@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,2288 @@
|
|
|
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 Toy City</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: 'Comic Sans MS', 'Arial', sans-serif;
|
|
21
|
+
background: linear-gradient(180deg, #FFE4E1 0%, #E0F6FF 30%, #E6FFE6 60%, #FFF8DC 100%);
|
|
22
|
+
cursor: grab;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
body:active {
|
|
26
|
+
cursor: grabbing;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#info {
|
|
30
|
+
position: fixed;
|
|
31
|
+
top: 16px;
|
|
32
|
+
left: 16px;
|
|
33
|
+
color: #2d3748;
|
|
34
|
+
background: rgba(255, 255, 255, 0.85);
|
|
35
|
+
backdrop-filter: blur(10px);
|
|
36
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
37
|
+
padding: 14px 18px;
|
|
38
|
+
border-radius: 8px;
|
|
39
|
+
font-size: 12px;
|
|
40
|
+
font-weight: 400;
|
|
41
|
+
z-index: 2000;
|
|
42
|
+
max-width: 280px;
|
|
43
|
+
line-height: 1.5;
|
|
44
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
45
|
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#info.hidden {
|
|
49
|
+
opacity: 0;
|
|
50
|
+
transform: translateY(-10px);
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.close-btn {
|
|
55
|
+
position: absolute;
|
|
56
|
+
top: 8px;
|
|
57
|
+
right: 8px;
|
|
58
|
+
background: none;
|
|
59
|
+
border: none;
|
|
60
|
+
font-size: 18px;
|
|
61
|
+
color: #FF1493;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
width: 24px;
|
|
64
|
+
height: 24px;
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
border-radius: 50%;
|
|
69
|
+
transition: background-color 0.2s ease;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.close-btn:hover {
|
|
73
|
+
background-color: rgba(255, 20, 147, 0.2);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* 統合メニュー */
|
|
77
|
+
.unified-menu {
|
|
78
|
+
position: fixed;
|
|
79
|
+
top: 16px;
|
|
80
|
+
right: 20px;
|
|
81
|
+
z-index: 1000;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.menu-toggle {
|
|
85
|
+
background: rgba(255, 255, 255, 0.85);
|
|
86
|
+
backdrop-filter: blur(16px);
|
|
87
|
+
-webkit-backdrop-filter: blur(16px);
|
|
88
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
89
|
+
color: #2d3748;
|
|
90
|
+
padding: 10px 14px;
|
|
91
|
+
border-radius: 8px;
|
|
92
|
+
font-size: 18px;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
95
|
+
box-shadow: 0 4px 12px rgba(255, 105, 180, 0.2), 0 2px 6px rgba(0, 0, 0, 0.05);
|
|
96
|
+
width: 44px;
|
|
97
|
+
height: 44px;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
opacity: 0.8;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.menu-toggle:hover {
|
|
105
|
+
background: rgba(255, 255, 255, 0.95);
|
|
106
|
+
border-color: rgba(255, 105, 180, 0.4);
|
|
107
|
+
transform: scale(1.1) translateY(-2px);
|
|
108
|
+
box-shadow: 0 6px 16px rgba(255, 105, 180, 0.3), 0 3px 8px rgba(0, 0, 0, 0.1);
|
|
109
|
+
opacity: 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.menu-dropdown {
|
|
113
|
+
position: absolute;
|
|
114
|
+
top: 56px;
|
|
115
|
+
right: 0;
|
|
116
|
+
min-width: 220px;
|
|
117
|
+
background: rgba(255, 255, 255, 0.9);
|
|
118
|
+
backdrop-filter: blur(16px) saturate(180%);
|
|
119
|
+
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
120
|
+
border: 1px solid rgba(255, 105, 180, 0.2);
|
|
121
|
+
border-radius: 12px;
|
|
122
|
+
box-shadow: 0 8px 24px rgba(255, 105, 180, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
123
|
+
opacity: 0;
|
|
124
|
+
visibility: hidden;
|
|
125
|
+
transform: translateY(-10px);
|
|
126
|
+
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
127
|
+
overflow: hidden;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.menu-dropdown.active {
|
|
131
|
+
opacity: 1;
|
|
132
|
+
visibility: visible;
|
|
133
|
+
transform: translateY(0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.menu-item {
|
|
137
|
+
display: block;
|
|
138
|
+
padding: 12px 16px;
|
|
139
|
+
color: #2d3748;
|
|
140
|
+
text-decoration: none;
|
|
141
|
+
font-size: 13px;
|
|
142
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
143
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
background: none;
|
|
146
|
+
border-left: none;
|
|
147
|
+
border-right: none;
|
|
148
|
+
border-top: none;
|
|
149
|
+
width: 100%;
|
|
150
|
+
text-align: left;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.menu-item:last-child {
|
|
154
|
+
border-bottom: none;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.menu-item:hover {
|
|
158
|
+
background: rgba(255, 105, 180, 0.08);
|
|
159
|
+
padding-left: 20px;
|
|
160
|
+
border-left: 2px solid rgba(255, 105, 180, 0.4);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.menu-item.has-submenu {
|
|
164
|
+
position: relative;
|
|
165
|
+
padding: 0;
|
|
166
|
+
border: none;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.submenu-toggle {
|
|
170
|
+
display: block;
|
|
171
|
+
padding: 12px 16px;
|
|
172
|
+
color: #2d3748;
|
|
173
|
+
font-size: 13px;
|
|
174
|
+
background: none;
|
|
175
|
+
border: none;
|
|
176
|
+
width: 100%;
|
|
177
|
+
text-align: left;
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
180
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.submenu-toggle:hover {
|
|
184
|
+
background: rgba(255, 105, 180, 0.08);
|
|
185
|
+
padding-left: 20px;
|
|
186
|
+
border-left: 2px solid rgba(255, 105, 180, 0.4);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.submenu {
|
|
190
|
+
display: none;
|
|
191
|
+
background: rgba(255, 192, 203, 0.05);
|
|
192
|
+
border-top: 1px solid rgba(255, 105, 180, 0.1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.submenu.active {
|
|
196
|
+
display: block;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.submenu .menu-item {
|
|
200
|
+
padding-left: 32px;
|
|
201
|
+
font-size: 12px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.submenu .menu-item:hover {
|
|
205
|
+
padding-left: 36px;
|
|
206
|
+
background: rgba(255, 105, 180, 0.12);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#hint-toggle {
|
|
210
|
+
position: fixed;
|
|
211
|
+
top: 16px;
|
|
212
|
+
left: 16px;
|
|
213
|
+
background: rgba(255, 192, 203, 0.2);
|
|
214
|
+
backdrop-filter: blur(10px);
|
|
215
|
+
border: 2px solid rgba(255, 105, 180, 0.4);
|
|
216
|
+
color: #FF1493;
|
|
217
|
+
padding: 6px 8px;
|
|
218
|
+
border-radius: 8px;
|
|
219
|
+
font-size: 12px;
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
z-index: 99;
|
|
222
|
+
display: none;
|
|
223
|
+
transition: all 0.2s ease;
|
|
224
|
+
box-shadow: 0 2px 8px rgba(255, 105, 180, 0.3);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#hint-toggle:hover {
|
|
228
|
+
background: rgba(255, 255, 255, 0.9);
|
|
229
|
+
transform: translateY(-1px);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@keyframes toy-sparkle-fast {
|
|
234
|
+
0%, 100% {
|
|
235
|
+
filter: brightness(1.3) drop-shadow(0 0 12px rgba(255, 105, 180, 0.7));
|
|
236
|
+
transform: rotate(0deg);
|
|
237
|
+
}
|
|
238
|
+
33% {
|
|
239
|
+
filter: brightness(1.6) drop-shadow(0 0 20px rgba(255, 105, 180, 0.9));
|
|
240
|
+
transform: rotate(10deg);
|
|
241
|
+
}
|
|
242
|
+
66% {
|
|
243
|
+
filter: brightness(1.4) drop-shadow(0 0 16px rgba(255, 105, 180, 0.8));
|
|
244
|
+
transform: rotate(-5deg);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.menu-icon {
|
|
249
|
+
display: inline-block;
|
|
250
|
+
animation: toy-bounce 2s ease-in-out infinite;
|
|
251
|
+
cursor: pointer;
|
|
252
|
+
transition: transform 0.2s ease;
|
|
253
|
+
text-shadow: 0 0 10px currentColor;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.menu-icon:hover {
|
|
257
|
+
transform: scale(1.4);
|
|
258
|
+
animation: toy-bounce-fast 0.8s ease-in-out infinite;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.toy-title {
|
|
262
|
+
display: inline-block;
|
|
263
|
+
animation: toy-title-glow 3s ease-in-out infinite;
|
|
264
|
+
text-shadow: 0 0 20px #FF69B4, 0 0 30px #FFD700;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@keyframes toy-title-glow {
|
|
268
|
+
0%, 100% {
|
|
269
|
+
text-shadow: 0 0 20px #FF69B4, 0 0 30px #FFD700;
|
|
270
|
+
transform: scale(1);
|
|
271
|
+
}
|
|
272
|
+
50% {
|
|
273
|
+
text-shadow: 0 0 25px #FF1493, 0 0 40px #FFA500, 0 0 50px #FF6347;
|
|
274
|
+
transform: scale(1.05);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@keyframes toy-bounce {
|
|
279
|
+
0%, 100% {
|
|
280
|
+
filter: brightness(1) drop-shadow(0 0 8px currentColor);
|
|
281
|
+
transform: scale(1) translateY(0px);
|
|
282
|
+
}
|
|
283
|
+
25% {
|
|
284
|
+
filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
|
|
285
|
+
transform: scale(1.1) translateY(-4px);
|
|
286
|
+
}
|
|
287
|
+
50% {
|
|
288
|
+
filter: brightness(1.5) drop-shadow(0 0 20px currentColor);
|
|
289
|
+
transform: scale(1.15) translateY(-8px);
|
|
290
|
+
}
|
|
291
|
+
75% {
|
|
292
|
+
filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
|
|
293
|
+
transform: scale(1.1) translateY(-4px);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@keyframes toy-bounce-fast {
|
|
298
|
+
0%, 100% {
|
|
299
|
+
filter: brightness(1.4) drop-shadow(0 0 15px currentColor);
|
|
300
|
+
transform: translateY(0px);
|
|
301
|
+
}
|
|
302
|
+
50% {
|
|
303
|
+
filter: brightness(1.8) drop-shadow(0 0 25px currentColor);
|
|
304
|
+
transform: translateY(-12px);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#info strong {
|
|
309
|
+
font-size: 12px;
|
|
310
|
+
font-weight: 600;
|
|
311
|
+
color: #1a202c;
|
|
312
|
+
display: block;
|
|
313
|
+
margin-bottom: 10px;
|
|
314
|
+
text-align: center;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#info code {
|
|
318
|
+
background: rgba(255, 192, 203, 0.4);
|
|
319
|
+
color: #8B0000;
|
|
320
|
+
padding: 4px 8px;
|
|
321
|
+
border-radius: 6px;
|
|
322
|
+
font-family: 'Comic Sans MS', monospace;
|
|
323
|
+
font-size: 12px;
|
|
324
|
+
font-weight: bold;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.tool-button {
|
|
328
|
+
display: inline-flex;
|
|
329
|
+
align-items: center;
|
|
330
|
+
gap: 6px;
|
|
331
|
+
border: none;
|
|
332
|
+
border-radius: 999px;
|
|
333
|
+
padding: 10px 18px;
|
|
334
|
+
margin-top: 12px;
|
|
335
|
+
font-size: 13px;
|
|
336
|
+
font-weight: 600;
|
|
337
|
+
cursor: pointer;
|
|
338
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.28), rgba(255, 192, 203, 0.35));
|
|
339
|
+
color: #fff;
|
|
340
|
+
box-shadow: 0 6px 18px rgba(255, 105, 180, 0.35);
|
|
341
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.tool-button:hover {
|
|
345
|
+
transform: translateY(-2px) scale(1.02);
|
|
346
|
+
box-shadow: 0 10px 28px rgba(255, 105, 180, 0.45);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.controls {
|
|
350
|
+
margin-top: 10px;
|
|
351
|
+
padding-top: 10px;
|
|
352
|
+
border-top: 2px solid rgba(255, 215, 0, 0.4);
|
|
353
|
+
font-size: 12px;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.control-item {
|
|
357
|
+
margin: 6px 0;
|
|
358
|
+
display: flex;
|
|
359
|
+
align-items: center;
|
|
360
|
+
gap: 10px;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.key {
|
|
364
|
+
background: rgba(255, 105, 180, 0.4);
|
|
365
|
+
padding: 3px 8px;
|
|
366
|
+
border-radius: 5px;
|
|
367
|
+
font-weight: bold;
|
|
368
|
+
min-width: fit-content;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.nav-link:hover {
|
|
372
|
+
background: rgba(255, 255, 255, 1) !important;
|
|
373
|
+
transform: translateY(-1px);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
@keyframes gentle-float {
|
|
377
|
+
0%, 100% {
|
|
378
|
+
transform: translateX(-50%) translateY(0px);
|
|
379
|
+
filter: drop-shadow(0 8px 16px rgba(138, 43, 226, 0.2));
|
|
380
|
+
}
|
|
381
|
+
50% {
|
|
382
|
+
transform: translateX(-50%) translateY(-4px);
|
|
383
|
+
filter: drop-shadow(0 12px 24px rgba(138, 43, 226, 0.3));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* 🎵 音響ステータス表示 */
|
|
388
|
+
#audio-status {
|
|
389
|
+
position: fixed;
|
|
390
|
+
top: 70px;
|
|
391
|
+
right: 20px;
|
|
392
|
+
z-index: 100;
|
|
393
|
+
background: linear-gradient(135deg, rgba(255, 105, 180, 0.9) 0%, rgba(255, 215, 0, 0.9) 100%);
|
|
394
|
+
backdrop-filter: blur(12px);
|
|
395
|
+
-webkit-backdrop-filter: blur(12px);
|
|
396
|
+
padding: 10px 15px;
|
|
397
|
+
border-radius: 20px;
|
|
398
|
+
font-size: 12px;
|
|
399
|
+
border: 2px solid rgba(255, 255, 255, 0.5);
|
|
400
|
+
box-shadow: 0 0 20px rgba(255, 105, 180, 0.5);
|
|
401
|
+
color: #ffffff;
|
|
402
|
+
font-weight: 600;
|
|
403
|
+
display: none;
|
|
404
|
+
transition: opacity 0.3s ease;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
#audio-status.active {
|
|
408
|
+
display: block;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#audio-status .status-icon {
|
|
412
|
+
display: inline-block;
|
|
413
|
+
margin-right: 5px;
|
|
414
|
+
animation: audioPulse 1.5s ease-in-out infinite;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
@keyframes audioPulse {
|
|
418
|
+
0%, 100% { opacity: 0.8; transform: scale(1); }
|
|
419
|
+
50% { opacity: 1; transform: scale(1.1); }
|
|
420
|
+
}
|
|
421
|
+
</style>
|
|
422
|
+
</head>
|
|
423
|
+
<body>
|
|
424
|
+
<!-- 🎵 音響ステータス表示 -->
|
|
425
|
+
<div id="audio-status">
|
|
426
|
+
<span class="status-icon">🎤</span><span id="audio-text">音声待機中...</span>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<!-- Navigation Links (非表示) -->
|
|
430
|
+
<div class="navigation" style="display: none;">
|
|
431
|
+
<a href="../../index.html" class="nav-link">🏠 Home</a>
|
|
432
|
+
<a href="https://github.com/nyukicorn/chocodrop" class="nav-link" target="_blank">📦 GitHub</a>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<!-- 統合メニュー -->
|
|
436
|
+
<div class="unified-menu">
|
|
437
|
+
<button class="menu-toggle" id="unifiedMenuToggle" aria-label="Menu">☰</button>
|
|
438
|
+
<div class="menu-dropdown" id="unifiedMenuDropdown">
|
|
439
|
+
<button class="menu-item" id="showInfoBtn"><span class="menu-icon">📖</span> このワールドについて</button>
|
|
440
|
+
|
|
441
|
+
<div class="menu-item has-submenu">
|
|
442
|
+
<button class="submenu-toggle" id="worldsMenuToggle"><span class="menu-icon">🌍</span> 世界選択 <span style="font-size: 10px;">▼</span></button>
|
|
443
|
+
<div class="submenu" id="worldsSubmenu">
|
|
444
|
+
<a href="../basic/index.html" class="menu-item"><span class="menu-icon">🍫</span> はじまりの国</a>
|
|
445
|
+
<a href="../pixel-ocean/index.html" class="menu-item"><span class="menu-icon">🌊</span> 海底世界</a>
|
|
446
|
+
<a href="../music-garden/index.html" class="menu-item"><span class="menu-icon">🌸</span> 硝子の花</a>
|
|
447
|
+
<a href="../wabi-sabi/index.html" class="menu-item"><span class="menu-icon">🖤</span> 侘び寂びの世界</a>
|
|
448
|
+
<a href="../space/index.html" class="menu-item"><span class="menu-icon">🌌</span> 宇宙空間</a>
|
|
449
|
+
<a href="index.html" class="menu-item"><span class="menu-icon">🎡</span> AI遊園地(現在地)</a>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
<a href="../../index.html" class="menu-item"><span class="menu-icon">🏠</span> Home</a>
|
|
454
|
+
<a href="https://github.com/nyukicorn/chocodrop" class="menu-item" target="_blank"><span class="menu-icon">🔗</span> GitHub</a>
|
|
455
|
+
|
|
456
|
+
<button class="menu-item" id="vrMenuButton"><span class="menu-icon">🥽</span> VR</button>
|
|
457
|
+
<button class="menu-item" id="arMenuButton"><span class="menu-icon">🥽</span> AR</button>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
|
|
461
|
+
<div id="hint-toggle" style="display: none;">💡 ヒント</div>
|
|
462
|
+
|
|
463
|
+
<div id="info">
|
|
464
|
+
<button class="close-btn" onclick="window.hideInfoPanel()">×</button>
|
|
465
|
+
<strong class="toy-title" style="text-align: center; display: block;">🧸 <span class="toy-icon">🎪</span> AI遊園地 <span class="toy-icon">🎠</span> 🧸</strong>
|
|
466
|
+
|
|
467
|
+
<div style="font-size: 11px; opacity: 0.9; margin: 10px 0 12px 0; line-height: 1.6; text-align: left; font-style: italic;">
|
|
468
|
+
個性豊かなAIたちが遊ぶ、色鮮やかな夢の遊園地。夜には別の夢が咲く
|
|
469
|
+
</div>
|
|
470
|
+
|
|
471
|
+
<p style="text-align: left;">🎪 カラフルなおもちゃの街へようこそ!</p>
|
|
472
|
+
|
|
473
|
+
<p style="text-align: left;">
|
|
474
|
+
✨ <span class="toy-icon">🎡</span> 観覧車や <span class="toy-icon">🎠</span> メリーゴーランド、そして可愛いAIたちが回ってます!
|
|
475
|
+
</p>
|
|
476
|
+
|
|
477
|
+
<p style="text-align: left;">
|
|
478
|
+
<strong>🧸 キャラクターを追加してみませんか?</strong><br>
|
|
479
|
+
<span class="toy-icon">👥</span> 人、<span class="toy-icon">🤖</span> ロボット、<span class="toy-icon">🐱</span> ネコなど自由に配置できます!
|
|
480
|
+
</p>
|
|
481
|
+
|
|
482
|
+
<p style="text-align: left;">🕹️ オブジェクトをクリック → WASDキーで移動</p>
|
|
483
|
+
|
|
484
|
+
<div class="controls">
|
|
485
|
+
<div class="control-item">
|
|
486
|
+
<span class="key">移動</span>
|
|
487
|
+
<span>マウスドラッグ</span>
|
|
488
|
+
</div>
|
|
489
|
+
<div class="control-item">
|
|
490
|
+
<span class="key">ズーム</span>
|
|
491
|
+
<span>マウスホイール</span>
|
|
492
|
+
</div>
|
|
493
|
+
<div class="control-item">
|
|
494
|
+
<span class="key">回転</span>
|
|
495
|
+
<span>右クリック+ドラッグ</span>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
<!-- 世界選択リンク (非表示 - 統合メニューに移行) -->
|
|
501
|
+
<div class="demo-links" style="display: none;">
|
|
502
|
+
<a href="../basic/index.html" class="demo-link">🍫 はじまりの国</a>
|
|
503
|
+
<a href="../pixel-ocean/index.html" class="demo-link">🌊 海底世界</a>
|
|
504
|
+
<a href="../music-garden/index.html" class="demo-link">🌸 硝子の花</a>
|
|
505
|
+
<a href="../wabi-sabi/index.html" class="demo-link">🖤 侘び寂び</a>
|
|
506
|
+
<a href="../space/index.html" class="demo-link">🌌 宇宙空間</a>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
<script type="module">
|
|
510
|
+
import ensureChocoDrop from '../../public/load-chocodrop.js';
|
|
511
|
+
|
|
512
|
+
// 🎯 情報パネル制御を即時初期化
|
|
513
|
+
let autoHideTimer = null;
|
|
514
|
+
|
|
515
|
+
const getInfoPanel = () => document.getElementById('info');
|
|
516
|
+
const getHintToggle = () => document.getElementById('hint-toggle');
|
|
517
|
+
|
|
518
|
+
const clearInfoPanelAutoHide = () => {
|
|
519
|
+
if (autoHideTimer) {
|
|
520
|
+
clearTimeout(autoHideTimer);
|
|
521
|
+
autoHideTimer = null;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const scheduleInfoPanelAutoHide = () => {
|
|
526
|
+
clearInfoPanelAutoHide();
|
|
527
|
+
autoHideTimer = window.setTimeout(() => {
|
|
528
|
+
if (typeof window.hideInfoPanel === 'function') {
|
|
529
|
+
window.hideInfoPanel();
|
|
530
|
+
}
|
|
531
|
+
}, 30000);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
window.hideInfoPanel = function hideInfoPanel() {
|
|
535
|
+
const infoPanel = getInfoPanel();
|
|
536
|
+
if (!infoPanel) {
|
|
537
|
+
clearInfoPanelAutoHide();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
infoPanel.classList.add('hidden');
|
|
542
|
+
window.setTimeout(() => {
|
|
543
|
+
if (infoPanel.classList.contains('hidden')) {
|
|
544
|
+
infoPanel.style.display = 'none';
|
|
545
|
+
const hintToggle = getHintToggle();
|
|
546
|
+
if (hintToggle) {
|
|
547
|
+
hintToggle.style.display = 'block';
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}, 300);
|
|
551
|
+
|
|
552
|
+
clearInfoPanelAutoHide();
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
window.showInfoPanel = function showInfoPanel() {
|
|
556
|
+
const infoPanel = getInfoPanel();
|
|
557
|
+
if (!infoPanel) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const hintToggle = getHintToggle();
|
|
562
|
+
if (hintToggle) {
|
|
563
|
+
hintToggle.style.display = 'none';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
infoPanel.style.display = 'block';
|
|
567
|
+
window.requestAnimationFrame(() => {
|
|
568
|
+
infoPanel.classList.remove('hidden');
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
scheduleInfoPanelAutoHide();
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const initialHintToggle = getHintToggle();
|
|
575
|
+
if (initialHintToggle) {
|
|
576
|
+
initialHintToggle.addEventListener('click', window.showInfoPanel);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
(async () => {
|
|
580
|
+
await ensureChocoDrop();
|
|
581
|
+
|
|
582
|
+
const THREE = window.THREE;
|
|
583
|
+
const ChocoDrop = window.ChocoDrop;
|
|
584
|
+
|
|
585
|
+
// 統合メニューの制御
|
|
586
|
+
(function() {
|
|
587
|
+
const menuToggle = document.getElementById('unifiedMenuToggle');
|
|
588
|
+
const menuDropdown = document.getElementById('unifiedMenuDropdown');
|
|
589
|
+
const worldsMenuToggle = document.getElementById('worldsMenuToggle');
|
|
590
|
+
const worldsSubmenu = document.getElementById('worldsSubmenu');
|
|
591
|
+
const showInfoBtn = document.getElementById('showInfoBtn');
|
|
592
|
+
const infoPanel = document.getElementById('info');
|
|
593
|
+
|
|
594
|
+
// 初回訪問チェック - localStorage使用
|
|
595
|
+
const FIRST_VISIT_KEY = 'toy_city_visited';
|
|
596
|
+
const hasVisited = localStorage.getItem(FIRST_VISIT_KEY);
|
|
597
|
+
|
|
598
|
+
if (!hasVisited) {
|
|
599
|
+
// 初回訪問時は情報パネルを表示
|
|
600
|
+
if (infoPanel) {
|
|
601
|
+
infoPanel.style.display = 'block';
|
|
602
|
+
}
|
|
603
|
+
localStorage.setItem(FIRST_VISIT_KEY, 'true');
|
|
604
|
+
} else {
|
|
605
|
+
// 2回目以降は非表示
|
|
606
|
+
if (infoPanel) {
|
|
607
|
+
infoPanel.style.display = 'none';
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 統合メニューのトグル
|
|
612
|
+
if (menuToggle && menuDropdown) {
|
|
613
|
+
menuToggle.addEventListener('click', (e) => {
|
|
614
|
+
e.stopPropagation();
|
|
615
|
+
menuDropdown.classList.toggle('active');
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// 外側クリックで閉じる
|
|
619
|
+
document.addEventListener('click', (e) => {
|
|
620
|
+
if (!menuToggle.contains(e.target) && !menuDropdown.contains(e.target)) {
|
|
621
|
+
menuDropdown.classList.remove('active');
|
|
622
|
+
// サブメニューも閉じる
|
|
623
|
+
if (worldsSubmenu) {
|
|
624
|
+
worldsSubmenu.classList.remove('active');
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// Escキーで閉じる
|
|
630
|
+
document.addEventListener('keydown', (e) => {
|
|
631
|
+
if (e.key === 'Escape') {
|
|
632
|
+
menuDropdown.classList.remove('active');
|
|
633
|
+
if (worldsSubmenu) {
|
|
634
|
+
worldsSubmenu.classList.remove('active');
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 世界選択サブメニューのトグル
|
|
641
|
+
if (worldsMenuToggle && worldsSubmenu) {
|
|
642
|
+
worldsMenuToggle.addEventListener('click', (e) => {
|
|
643
|
+
e.stopPropagation();
|
|
644
|
+
worldsSubmenu.classList.toggle('active');
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// 情報パネル表示ボタン
|
|
649
|
+
if (showInfoBtn) {
|
|
650
|
+
showInfoBtn.addEventListener('click', () => {
|
|
651
|
+
// グローバルに定義された showInfoPanel() を呼び出す
|
|
652
|
+
if (typeof showInfoPanel === 'function') {
|
|
653
|
+
showInfoPanel();
|
|
654
|
+
} else {
|
|
655
|
+
// フォールバック: 直接表示
|
|
656
|
+
const panel = document.getElementById('info');
|
|
657
|
+
if (panel) {
|
|
658
|
+
panel.style.display = 'block';
|
|
659
|
+
panel.classList.remove('hidden');
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
menuDropdown.classList.remove('active');
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// VR/ARボタンは後でThree.jsのrendererと連携
|
|
667
|
+
// グローバル変数として保存
|
|
668
|
+
window.unifiedMenuVRButton = document.getElementById('vrMenuButton');
|
|
669
|
+
window.unifiedMenuARButton = document.getElementById('arMenuButton');
|
|
670
|
+
})();
|
|
671
|
+
|
|
672
|
+
// Basic Three.js setup
|
|
673
|
+
// Disable ColorManagement for legacy color behavior (required with ambientLight intensity fix)
|
|
674
|
+
THREE.ColorManagement.enabled = false;
|
|
675
|
+
|
|
676
|
+
const scene = new THREE.Scene();
|
|
677
|
+
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
678
|
+
const renderer = new THREE.WebGLRenderer({
|
|
679
|
+
antialias: true,
|
|
680
|
+
powerPreference: "high-performance"
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
684
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
685
|
+
renderer.shadowMap.enabled = true;
|
|
686
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 柔らかい影
|
|
687
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
688
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
689
|
+
renderer.toneMappingExposure = 1.0;
|
|
690
|
+
document.body.appendChild(renderer.domElement);
|
|
691
|
+
|
|
692
|
+
window.toyCityContext = { scene, camera, renderer };
|
|
693
|
+
|
|
694
|
+
const carouselCreatures = [];
|
|
695
|
+
const neonElements = [];
|
|
696
|
+
const hoverDrones = [];
|
|
697
|
+
const trainMovers = [];
|
|
698
|
+
const serviceBots = [];
|
|
699
|
+
const particleEmitters = [];
|
|
700
|
+
const nightAccentLights = [];
|
|
701
|
+
let isNightMode = false;
|
|
702
|
+
let neonModeBoost = 1;
|
|
703
|
+
|
|
704
|
+
// 🧸 おもちゃの街の環境設定
|
|
705
|
+
scene.background = new THREE.Color(0xFFF8DC); // クリーム色の空
|
|
706
|
+
scene.fog = new THREE.Fog(0xFFF8DC, 20, 80);
|
|
707
|
+
|
|
708
|
+
// 🌈 おもちゃの街のライティング - 暖かく明るく
|
|
709
|
+
const ambientLight = new THREE.AmbientLight(0xffffff, 2.2);
|
|
710
|
+
scene.add(ambientLight);
|
|
711
|
+
|
|
712
|
+
// メインライト(太陽)
|
|
713
|
+
const sunLight = new THREE.DirectionalLight(0xFFE4B5, 1.0);
|
|
714
|
+
sunLight.position.set(25, 40, 15);
|
|
715
|
+
sunLight.castShadow = true;
|
|
716
|
+
sunLight.shadow.mapSize.width = 1024;
|
|
717
|
+
sunLight.shadow.mapSize.height = 1024;
|
|
718
|
+
sunLight.shadow.camera.near = 0.5;
|
|
719
|
+
sunLight.shadow.camera.far = 120;
|
|
720
|
+
sunLight.shadow.camera.left = -40;
|
|
721
|
+
sunLight.shadow.camera.right = 40;
|
|
722
|
+
sunLight.shadow.camera.top = 40;
|
|
723
|
+
sunLight.shadow.camera.bottom = -40;
|
|
724
|
+
scene.add(sunLight);
|
|
725
|
+
|
|
726
|
+
const moonLight = new THREE.DirectionalLight(0x6C63FF, 0.55);
|
|
727
|
+
moonLight.position.set(-25, 35, -18);
|
|
728
|
+
moonLight.castShadow = false;
|
|
729
|
+
moonLight.visible = false;
|
|
730
|
+
scene.add(moonLight);
|
|
731
|
+
|
|
732
|
+
// 🌈 カラフルな地面(おもちゃのプレイマット)
|
|
733
|
+
const groundGeometry = new THREE.PlaneGeometry(100, 100, 20, 20);
|
|
734
|
+
|
|
735
|
+
// 虹色のグラデーション地面
|
|
736
|
+
const canvas = document.createElement('canvas');
|
|
737
|
+
canvas.width = 512;
|
|
738
|
+
canvas.height = 512;
|
|
739
|
+
const ctx = canvas.getContext('2d');
|
|
740
|
+
|
|
741
|
+
// カラフルなチェッカーボード模様
|
|
742
|
+
const colors = ['#FFB6C1', '#87CEEB', '#98FB98', '#F0E68C', '#DDA0DD', '#FFE4B5'];
|
|
743
|
+
const tileSize = 85;
|
|
744
|
+
|
|
745
|
+
for (let i = 0; i < 6; i++) {
|
|
746
|
+
for (let j = 0; j < 6; j++) {
|
|
747
|
+
ctx.fillStyle = colors[(i + j) % colors.length];
|
|
748
|
+
ctx.fillRect(i * tileSize, j * tileSize, tileSize, tileSize);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const groundTexture = new THREE.CanvasTexture(canvas);
|
|
753
|
+
groundTexture.wrapS = THREE.RepeatWrapping;
|
|
754
|
+
groundTexture.wrapT = THREE.RepeatWrapping;
|
|
755
|
+
groundTexture.repeat.set(2, 2);
|
|
756
|
+
|
|
757
|
+
const groundMaterial = new THREE.MeshLambertMaterial({
|
|
758
|
+
map: groundTexture
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
|
762
|
+
ground.rotation.x = -Math.PI / 2;
|
|
763
|
+
ground.receiveShadow = true;
|
|
764
|
+
scene.add(ground);
|
|
765
|
+
|
|
766
|
+
// 🏠 レゴブロック風の建物を作成
|
|
767
|
+
function createLegoBuilding(x, z, width, height, depth, color) {
|
|
768
|
+
const buildingGroup = new THREE.Group();
|
|
769
|
+
|
|
770
|
+
// メイン建物
|
|
771
|
+
const buildingGeometry = new THREE.BoxGeometry(width, height, depth);
|
|
772
|
+
const buildingMaterial = new THREE.MeshLambertMaterial({
|
|
773
|
+
color: color,
|
|
774
|
+
transparent: true,
|
|
775
|
+
opacity: 0.9
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
const building = new THREE.Mesh(buildingGeometry, buildingMaterial);
|
|
779
|
+
building.position.set(0, height / 2, 0);
|
|
780
|
+
building.castShadow = true;
|
|
781
|
+
building.receiveShadow = true;
|
|
782
|
+
buildingGroup.add(building);
|
|
783
|
+
|
|
784
|
+
// レゴの突起(上面に追加)
|
|
785
|
+
const studSize = Math.min(width, depth) / 4;
|
|
786
|
+
const studHeight = 0.5;
|
|
787
|
+
const studsX = Math.floor(width / studSize);
|
|
788
|
+
const studsZ = Math.floor(depth / studSize);
|
|
789
|
+
|
|
790
|
+
for (let i = 0; i < studsX; i++) {
|
|
791
|
+
for (let j = 0; j < studsZ; j++) {
|
|
792
|
+
const studGeometry = new THREE.CylinderGeometry(studSize * 0.3, studSize * 0.3, studHeight, 8);
|
|
793
|
+
const studMaterial = new THREE.MeshLambertMaterial({
|
|
794
|
+
color: color,
|
|
795
|
+
transparent: true,
|
|
796
|
+
opacity: 0.95
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
const stud = new THREE.Mesh(studGeometry, studMaterial);
|
|
800
|
+
stud.position.set(
|
|
801
|
+
(i - (studsX - 1) / 2) * studSize,
|
|
802
|
+
height + studHeight / 2,
|
|
803
|
+
(j - (studsZ - 1) / 2) * studSize
|
|
804
|
+
);
|
|
805
|
+
stud.castShadow = true;
|
|
806
|
+
buildingGroup.add(stud);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
buildingGroup.position.set(x, 0, z);
|
|
811
|
+
return buildingGroup;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function createCanvasTexture(drawFn, size = 256) {
|
|
815
|
+
const canvas = document.createElement('canvas');
|
|
816
|
+
canvas.width = size;
|
|
817
|
+
canvas.height = size;
|
|
818
|
+
const ctx = canvas.getContext('2d');
|
|
819
|
+
drawFn(ctx, size);
|
|
820
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
821
|
+
texture.needsUpdate = true;
|
|
822
|
+
return texture;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function createAilabHub(x, z) {
|
|
826
|
+
const hub = new THREE.Group();
|
|
827
|
+
hub.userData.skipWiggle = true;
|
|
828
|
+
|
|
829
|
+
const podium = new THREE.Mesh(new THREE.CylinderGeometry(6, 6.5, 1.2, 24), new THREE.MeshLambertMaterial({ color: 0x2EC4B6 }));
|
|
830
|
+
podium.position.y = 0.6;
|
|
831
|
+
podium.castShadow = true;
|
|
832
|
+
podium.receiveShadow = true;
|
|
833
|
+
hub.add(podium);
|
|
834
|
+
|
|
835
|
+
const lowerBlock = new THREE.Mesh(new THREE.BoxGeometry(10, 4, 6), new THREE.MeshLambertMaterial({ color: 0x011627 }));
|
|
836
|
+
lowerBlock.position.y = 2.6;
|
|
837
|
+
lowerBlock.castShadow = true;
|
|
838
|
+
lowerBlock.receiveShadow = true;
|
|
839
|
+
hub.add(lowerBlock);
|
|
840
|
+
|
|
841
|
+
const upperBlock = new THREE.Mesh(new THREE.BoxGeometry(6, 5, 4), new THREE.MeshLambertMaterial({ color: 0xFF9F1C }));
|
|
842
|
+
upperBlock.position.set(-1, 6.5, 0);
|
|
843
|
+
upperBlock.castShadow = true;
|
|
844
|
+
hub.add(upperBlock);
|
|
845
|
+
|
|
846
|
+
const antenna = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 4, 12), new THREE.MeshLambertMaterial({ color: 0xA0E7E5 }));
|
|
847
|
+
antenna.position.set(2.4, 9, 0);
|
|
848
|
+
hub.add(antenna);
|
|
849
|
+
|
|
850
|
+
const logoTexture = createCanvasTexture((ctx, size) => {
|
|
851
|
+
ctx.fillStyle = '#0B132B';
|
|
852
|
+
ctx.fillRect(0, 0, size, size);
|
|
853
|
+
ctx.fillStyle = '#2EC4B6';
|
|
854
|
+
ctx.beginPath();
|
|
855
|
+
ctx.arc(size / 2, size / 2, size * 0.38, 0, Math.PI * 2);
|
|
856
|
+
ctx.fill();
|
|
857
|
+
ctx.fillStyle = '#FFFFFF';
|
|
858
|
+
ctx.font = `${size * 0.3}px "Arial"`;
|
|
859
|
+
ctx.textAlign = 'center';
|
|
860
|
+
ctx.textBaseline = 'middle';
|
|
861
|
+
ctx.fillText('AI', size / 2, size / 2 - size * 0.08);
|
|
862
|
+
ctx.font = `${size * 0.18}px "Arial"`;
|
|
863
|
+
ctx.fillText('LAB', size / 2, size / 2 + size * 0.2);
|
|
864
|
+
}, 512);
|
|
865
|
+
const signage = new THREE.Mesh(new THREE.PlaneGeometry(4.5, 3), new THREE.MeshBasicMaterial({ map: logoTexture, transparent: true }));
|
|
866
|
+
signage.position.set(0, 7.5, 2.6);
|
|
867
|
+
hub.add(signage);
|
|
868
|
+
neonElements.push({ mesh: signage, type: 'pulse', speed: 5.2, baseOpacity: 1, phase: Math.random() * Math.PI * 2 });
|
|
869
|
+
|
|
870
|
+
const holoRing = new THREE.Mesh(new THREE.TorusGeometry(3.2, 0.2, 16, 64), new THREE.MeshBasicMaterial({ color: 0x7DF9FF, transparent: true, opacity: 0.5 }));
|
|
871
|
+
holoRing.position.y = 5;
|
|
872
|
+
holoRing.rotation.x = Math.PI / 2;
|
|
873
|
+
hub.add(holoRing);
|
|
874
|
+
neonElements.push({ mesh: holoRing, type: 'rotateZ', speed: 0.025, baseOpacity: holoRing.material.opacity, phase: Math.random() * Math.PI * 2 });
|
|
875
|
+
|
|
876
|
+
const labLight = new THREE.PointLight(0x7DF9FF, 1.6, 45, 2);
|
|
877
|
+
labLight.position.set(0, 8.2, 0);
|
|
878
|
+
hub.add(labLight);
|
|
879
|
+
registerNightLight(labLight, 1.6);
|
|
880
|
+
|
|
881
|
+
hub.position.set(x, 0, z);
|
|
882
|
+
return hub;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function createDisplayStand(x, z, label, accentColor) {
|
|
886
|
+
const stand = new THREE.Group();
|
|
887
|
+
stand.userData.skipWiggle = true;
|
|
888
|
+
|
|
889
|
+
const base = new THREE.Mesh(new THREE.CylinderGeometry(2, 2.4, 0.8, 16), new THREE.MeshLambertMaterial({ color: accentColor }));
|
|
890
|
+
base.position.y = 0.4;
|
|
891
|
+
base.castShadow = true;
|
|
892
|
+
stand.add(base);
|
|
893
|
+
|
|
894
|
+
const glass = new THREE.Mesh(new THREE.CylinderGeometry(1.3, 1.3, 2.2, 16), new THREE.MeshLambertMaterial({ color: 0xFFFFFF, transparent: true, opacity: 0.45 }));
|
|
895
|
+
glass.position.y = 1.8;
|
|
896
|
+
stand.add(glass);
|
|
897
|
+
|
|
898
|
+
const panelTexture = createCanvasTexture((ctx, size) => {
|
|
899
|
+
ctx.fillStyle = '#1B263B';
|
|
900
|
+
ctx.fillRect(0, 0, size, size);
|
|
901
|
+
ctx.fillStyle = accentColor;
|
|
902
|
+
ctx.font = `${size * 0.22}px "Arial"`;
|
|
903
|
+
ctx.textAlign = 'center';
|
|
904
|
+
ctx.textBaseline = 'middle';
|
|
905
|
+
ctx.fillText(label, size / 2, size / 2);
|
|
906
|
+
});
|
|
907
|
+
const panel = new THREE.Mesh(new THREE.PlaneGeometry(2.6, 1.6), new THREE.MeshBasicMaterial({ map: panelTexture, transparent: true }));
|
|
908
|
+
panel.position.set(0, 3.2, 0);
|
|
909
|
+
stand.add(panel);
|
|
910
|
+
neonElements.push({ mesh: panel, type: 'pulse', speed: 4.4, baseOpacity: 1, phase: Math.random() * Math.PI * 2 });
|
|
911
|
+
|
|
912
|
+
const showcaseLight = new THREE.PointLight(accentColor, 1.1, 18, 2.2);
|
|
913
|
+
showcaseLight.position.set(0, 4.1, 0);
|
|
914
|
+
stand.add(showcaseLight);
|
|
915
|
+
registerNightLight(showcaseLight, 1.1);
|
|
916
|
+
|
|
917
|
+
stand.position.set(x, 0, z);
|
|
918
|
+
return stand;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function createServiceBot(x, z, color) {
|
|
922
|
+
const bot = new THREE.Group();
|
|
923
|
+
bot.userData.skipWiggle = true;
|
|
924
|
+
|
|
925
|
+
const body = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.5, 1.2, 12), new THREE.MeshLambertMaterial({ color }));
|
|
926
|
+
body.position.y = 0.6;
|
|
927
|
+
body.castShadow = true;
|
|
928
|
+
bot.add(body);
|
|
929
|
+
|
|
930
|
+
const head = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.5, 0.7), new THREE.MeshLambertMaterial({ color: 0x1B263B }));
|
|
931
|
+
head.position.y = 1.2;
|
|
932
|
+
bot.add(head);
|
|
933
|
+
|
|
934
|
+
const eye = new THREE.Mesh(new THREE.PlaneGeometry(0.4, 0.18), new THREE.MeshBasicMaterial({ color: 0x00F5D4, transparent: true, opacity: 0.8 }));
|
|
935
|
+
eye.position.set(0, 1.2, 0.36);
|
|
936
|
+
bot.add(eye);
|
|
937
|
+
|
|
938
|
+
const antenna = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.6, 8), new THREE.MeshLambertMaterial({ color: 0xFFFFFF }));
|
|
939
|
+
antenna.position.y = 1.6;
|
|
940
|
+
bot.add(antenna);
|
|
941
|
+
|
|
942
|
+
bot.position.set(x, 0, z);
|
|
943
|
+
serviceBots.push({ mesh: bot, baseY: bot.position.y, radius: 1 + Math.random(), speed: 0.6 + Math.random() * 0.5, offset: Math.random() * Math.PI * 2, center: new THREE.Vector3(x, 0, z) });
|
|
944
|
+
return bot;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function createParticleEmitter(position, color, options = {}) {
|
|
948
|
+
const geometry = new THREE.BufferGeometry();
|
|
949
|
+
const count = 200;
|
|
950
|
+
const positions = new Float32Array(count * 3);
|
|
951
|
+
for (let i = 0; i < count; i++) {
|
|
952
|
+
positions[i * 3 + 0] = (Math.random() - 0.5) * 2;
|
|
953
|
+
positions[i * 3 + 1] = Math.random() * 3;
|
|
954
|
+
positions[i * 3 + 2] = (Math.random() - 0.5) * 2;
|
|
955
|
+
}
|
|
956
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
957
|
+
|
|
958
|
+
const material = new THREE.PointsMaterial({ color, size: 0.08, transparent: true, opacity: 0.8 });
|
|
959
|
+
const points = new THREE.Points(geometry, material);
|
|
960
|
+
points.position.copy(position);
|
|
961
|
+
particleEmitters.push({ points, baseY: position.y, color, start: Date.now(), nightOnly: !!options.nightOnly });
|
|
962
|
+
return points;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function registerNightLight(light, intensity) {
|
|
966
|
+
light.visible = false;
|
|
967
|
+
nightAccentLights.push({ light, intensity });
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function applySceneTheme(mode) {
|
|
971
|
+
const night = mode === 'night';
|
|
972
|
+
isNightMode = night;
|
|
973
|
+
scene.background.set(night ? 0x141237 : 0xFFF8DC);
|
|
974
|
+
scene.fog.color.set(night ? 0x141237 : 0xFFF8DC);
|
|
975
|
+
scene.fog.near = night ? 10 : 20;
|
|
976
|
+
scene.fog.far = night ? 85 : 80;
|
|
977
|
+
|
|
978
|
+
ambientLight.color.set(night ? 0xC3D2FF : 0xFFFFFF);
|
|
979
|
+
ambientLight.intensity = night ? 1.8 : 2.2;
|
|
980
|
+
|
|
981
|
+
sunLight.visible = !night;
|
|
982
|
+
sunLight.intensity = night ? 0.18 : 1.0;
|
|
983
|
+
moonLight.visible = night;
|
|
984
|
+
moonLight.intensity = night ? 0.75 : 0.0;
|
|
985
|
+
|
|
986
|
+
renderer.toneMappingExposure = night ? 1.25 : 1.0;
|
|
987
|
+
|
|
988
|
+
groundMaterial.color.set(night ? 0xADB5FF : 0xFFFFFF);
|
|
989
|
+
neonModeBoost = night ? 1.4 : 0.7;
|
|
990
|
+
|
|
991
|
+
particleEmitters.forEach(emitter => {
|
|
992
|
+
emitter.points.visible = night ? true : !emitter.nightOnly;
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
nightAccentLights.forEach(entry => {
|
|
996
|
+
entry.light.visible = night;
|
|
997
|
+
entry.light.intensity = night ? entry.intensity : 0;
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// 🎪 おもちゃのテント
|
|
1002
|
+
function createToyTent(x, z, color1, color2) {
|
|
1003
|
+
const tentGroup = new THREE.Group();
|
|
1004
|
+
|
|
1005
|
+
// テント本体(円錐)
|
|
1006
|
+
const tentGeometry = new THREE.ConeGeometry(4, 8, 8);
|
|
1007
|
+
const tentMaterial = new THREE.MeshLambertMaterial({
|
|
1008
|
+
color: color1,
|
|
1009
|
+
transparent: true,
|
|
1010
|
+
opacity: 0.8
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
const tent = new THREE.Mesh(tentGeometry, tentMaterial);
|
|
1014
|
+
tent.position.set(0, 4, 0);
|
|
1015
|
+
tent.castShadow = true;
|
|
1016
|
+
tentGroup.add(tent);
|
|
1017
|
+
|
|
1018
|
+
// 旗
|
|
1019
|
+
const flagGeometry = new THREE.PlaneGeometry(2, 1);
|
|
1020
|
+
const flagMaterial = new THREE.MeshLambertMaterial({
|
|
1021
|
+
color: color2,
|
|
1022
|
+
side: THREE.DoubleSide
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
const flag = new THREE.Mesh(flagGeometry, flagMaterial);
|
|
1026
|
+
flag.position.set(0, 8.5, 0);
|
|
1027
|
+
tentGroup.add(flag);
|
|
1028
|
+
|
|
1029
|
+
tentGroup.position.set(x, 0, z);
|
|
1030
|
+
return tentGroup;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// 🎠 メリーゴーランド
|
|
1034
|
+
function createMerryGoRound(x, z) {
|
|
1035
|
+
const merryGroup = new THREE.Group();
|
|
1036
|
+
merryGroup.userData.skipWiggle = true;
|
|
1037
|
+
|
|
1038
|
+
// ベース
|
|
1039
|
+
const basePlatform = new THREE.Mesh(new THREE.CylinderGeometry(6.5, 6.5, 1, 24), new THREE.MeshLambertMaterial({ color: 0xFFD166 }));
|
|
1040
|
+
basePlatform.position.y = 0.5;
|
|
1041
|
+
basePlatform.castShadow = true;
|
|
1042
|
+
merryGroup.add(basePlatform);
|
|
1043
|
+
|
|
1044
|
+
const baseGlow = new THREE.Mesh(new THREE.CylinderGeometry(6.3, 6.3, 0.25, 32), new THREE.MeshBasicMaterial({ color: 0xFFFFFF, transparent: true, opacity: 0.35 }));
|
|
1045
|
+
baseGlow.position.y = 1.1;
|
|
1046
|
+
merryGroup.add(baseGlow);
|
|
1047
|
+
neonElements.push({ mesh: baseGlow, type: 'pulse', baseOpacity: baseGlow.material.opacity, speed: 3.2, phase: Math.random() * Math.PI * 2 });
|
|
1048
|
+
|
|
1049
|
+
// 中央ポール
|
|
1050
|
+
const poleMaterial = new THREE.MeshLambertMaterial({ color: 0xFF6FB5 });
|
|
1051
|
+
const centerPole = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.6, 8.5, 16), poleMaterial);
|
|
1052
|
+
centerPole.position.y = 5.25;
|
|
1053
|
+
centerPole.castShadow = true;
|
|
1054
|
+
merryGroup.add(centerPole);
|
|
1055
|
+
|
|
1056
|
+
const holoCore = new THREE.Mesh(new THREE.CylinderGeometry(1.2, 1.2, 6.5, 24), new THREE.MeshLambertMaterial({ color: 0x7F5AF0, transparent: true, opacity: 0.35 }));
|
|
1057
|
+
holoCore.position.y = 5.25;
|
|
1058
|
+
merryGroup.add(holoCore);
|
|
1059
|
+
|
|
1060
|
+
// キャノピー
|
|
1061
|
+
const canopyMaterial = new THREE.MeshLambertMaterial({ color: 0xFF3F81 });
|
|
1062
|
+
const canopy = new THREE.Mesh(new THREE.ConeGeometry(7.5, 3.2, 24), canopyMaterial);
|
|
1063
|
+
canopy.position.y = 11.2;
|
|
1064
|
+
canopy.castShadow = true;
|
|
1065
|
+
merryGroup.add(canopy);
|
|
1066
|
+
|
|
1067
|
+
const canopyRing = new THREE.Mesh(new THREE.TorusGeometry(6.8, 0.15, 16, 64), new THREE.MeshBasicMaterial({ color: 0xFFFFFF, transparent: true, opacity: 0.6 }));
|
|
1068
|
+
canopyRing.position.y = 9.9;
|
|
1069
|
+
canopyRing.rotation.x = Math.PI / 2;
|
|
1070
|
+
merryGroup.add(canopyRing);
|
|
1071
|
+
neonElements.push({ mesh: canopyRing, type: 'rotateY', speed: 0.02, baseOpacity: canopyRing.material.opacity, phase: Math.random() * Math.PI * 2 });
|
|
1072
|
+
|
|
1073
|
+
const finial = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.5, 1.6, 12), new THREE.MeshLambertMaterial({ color: 0xFFE066 }));
|
|
1074
|
+
finial.position.y = 12.7;
|
|
1075
|
+
merryGroup.add(finial);
|
|
1076
|
+
|
|
1077
|
+
const canopyLight = new THREE.PointLight(0xFF80FF, 1.4, 40, 2);
|
|
1078
|
+
canopyLight.position.set(0, 10.5, 0);
|
|
1079
|
+
merryGroup.add(canopyLight);
|
|
1080
|
+
registerNightLight(canopyLight, 1.4);
|
|
1081
|
+
|
|
1082
|
+
const baseGlowLight = new THREE.PointLight(0xFFD166, 1.1, 26, 2);
|
|
1083
|
+
baseGlowLight.position.set(0, 2.8, 0);
|
|
1084
|
+
merryGroup.add(baseGlowLight);
|
|
1085
|
+
registerNightLight(baseGlowLight, 1.1);
|
|
1086
|
+
|
|
1087
|
+
// クリーチャー(AIトイアニマル)
|
|
1088
|
+
const creatureColors = [0xFF6B6B, 0x4ECDC4, 0x45B7D1, 0x96CEB4, 0xFECEA8, 0xD665FF];
|
|
1089
|
+
for (let i = 0; i < 6; i++) {
|
|
1090
|
+
const angle = (i / 6) * Math.PI * 2;
|
|
1091
|
+
const radius = 4.3;
|
|
1092
|
+
const creatureGroup = new THREE.Group();
|
|
1093
|
+
creatureGroup.position.set(Math.cos(angle) * radius, 1.2, Math.sin(angle) * radius);
|
|
1094
|
+
creatureGroup.rotation.y = -angle + Math.PI / 2;
|
|
1095
|
+
|
|
1096
|
+
const supportPole = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 6.2, 12), new THREE.MeshLambertMaterial({ color: 0xFFE066 }));
|
|
1097
|
+
supportPole.position.y = 3.2;
|
|
1098
|
+
supportPole.castShadow = true;
|
|
1099
|
+
creatureGroup.add(supportPole);
|
|
1100
|
+
|
|
1101
|
+
const bodyMaterial = new THREE.MeshLambertMaterial({ color: creatureColors[i] });
|
|
1102
|
+
const body = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.6, 1.8, 12), bodyMaterial);
|
|
1103
|
+
body.rotation.z = Math.PI / 2;
|
|
1104
|
+
body.position.set(0, 3.1, 0.4);
|
|
1105
|
+
body.castShadow = true;
|
|
1106
|
+
creatureGroup.add(body);
|
|
1107
|
+
|
|
1108
|
+
const head = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 16), bodyMaterial);
|
|
1109
|
+
head.position.set(0.8, 3.6, 0.4);
|
|
1110
|
+
creatureGroup.add(head);
|
|
1111
|
+
|
|
1112
|
+
const visor = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.3, 0.4), new THREE.MeshLambertMaterial({ color: 0xFFFFFF }));
|
|
1113
|
+
visor.position.set(1.05, 3.6, 0.4);
|
|
1114
|
+
creatureGroup.add(visor);
|
|
1115
|
+
|
|
1116
|
+
const legMaterial = new THREE.MeshLambertMaterial({ color: 0xFFD8A8 });
|
|
1117
|
+
for (let j = 0; j < 4; j++) {
|
|
1118
|
+
const leg = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.15, 1.4, 8), legMaterial);
|
|
1119
|
+
leg.position.set(-0.4 + (j % 2) * 0.8, 2.2, j < 2 ? 0.2 : 0.7);
|
|
1120
|
+
creatureGroup.add(leg);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const wingMaterial = new THREE.MeshLambertMaterial({ color: 0xFFFFFF, transparent: true, opacity: 0.65, side: THREE.DoubleSide });
|
|
1124
|
+
const wing = new THREE.Mesh(new THREE.PlaneGeometry(1.6, 1.2), wingMaterial);
|
|
1125
|
+
wing.position.set(0, 3.6, -0.1);
|
|
1126
|
+
wing.rotation.y = Math.PI / 2;
|
|
1127
|
+
creatureGroup.add(wing);
|
|
1128
|
+
|
|
1129
|
+
merryGroup.add(creatureGroup);
|
|
1130
|
+
carouselCreatures.push({ group: creatureGroup, baseY: creatureGroup.position.y, phase: i * (Math.PI / 3) });
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
merryGroup.position.set(x, 0, z);
|
|
1134
|
+
|
|
1135
|
+
// 回転アニメーション
|
|
1136
|
+
function animateMerry() {
|
|
1137
|
+
merryGroup.rotation.y += 0.008;
|
|
1138
|
+
requestAnimationFrame(animateMerry);
|
|
1139
|
+
}
|
|
1140
|
+
animateMerry();
|
|
1141
|
+
|
|
1142
|
+
return merryGroup;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// 🎡 大きな観覧車
|
|
1146
|
+
function createFerrisWheel(x, z) {
|
|
1147
|
+
const ferrisGroup = new THREE.Group();
|
|
1148
|
+
const wheelRadius = 10;
|
|
1149
|
+
const gondolaCount = 10;
|
|
1150
|
+
ferrisGroup.userData.skipWiggle = true;
|
|
1151
|
+
|
|
1152
|
+
// ベース
|
|
1153
|
+
const baseMaterial = new THREE.MeshLambertMaterial({ color: 0xFFE4A1 });
|
|
1154
|
+
const basePlatform = new THREE.Mesh(new THREE.BoxGeometry(26, 1, 10), baseMaterial);
|
|
1155
|
+
basePlatform.position.set(0, 0.5, 0);
|
|
1156
|
+
basePlatform.receiveShadow = true;
|
|
1157
|
+
ferrisGroup.add(basePlatform);
|
|
1158
|
+
|
|
1159
|
+
// 支柱
|
|
1160
|
+
const supportMaterial = new THREE.MeshLambertMaterial({ color: 0xFFD45C });
|
|
1161
|
+
const supportGroup = new THREE.Group();
|
|
1162
|
+
|
|
1163
|
+
const createSupport = (side) => {
|
|
1164
|
+
const support = new THREE.Group();
|
|
1165
|
+
|
|
1166
|
+
const legGeometry = new THREE.BoxGeometry(1.4, wheelRadius * 1.6, 1.4);
|
|
1167
|
+
const legFront = new THREE.Mesh(legGeometry, supportMaterial);
|
|
1168
|
+
legFront.position.set(0, wheelRadius * 0.8, 2);
|
|
1169
|
+
legFront.rotation.z = -side * Math.PI / 11;
|
|
1170
|
+
legFront.castShadow = true;
|
|
1171
|
+
|
|
1172
|
+
const legBack = legFront.clone();
|
|
1173
|
+
legBack.position.z = -2;
|
|
1174
|
+
|
|
1175
|
+
support.add(legFront, legBack);
|
|
1176
|
+
|
|
1177
|
+
const crossBeam = new THREE.Mesh(new THREE.BoxGeometry(1.6, 2.4, 8), supportMaterial);
|
|
1178
|
+
crossBeam.position.set(0, wheelRadius * 1.4, 0);
|
|
1179
|
+
crossBeam.castShadow = true;
|
|
1180
|
+
support.add(crossBeam);
|
|
1181
|
+
|
|
1182
|
+
const foot = new THREE.Mesh(new THREE.BoxGeometry(4, 0.6, 6), baseMaterial);
|
|
1183
|
+
foot.position.set(0, 0.3, 0);
|
|
1184
|
+
foot.receiveShadow = true;
|
|
1185
|
+
support.add(foot);
|
|
1186
|
+
|
|
1187
|
+
support.position.x = side * (wheelRadius + 2.5);
|
|
1188
|
+
return support;
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
supportGroup.add(createSupport(-1), createSupport(1));
|
|
1192
|
+
ferrisGroup.add(supportGroup);
|
|
1193
|
+
|
|
1194
|
+
// ホイール
|
|
1195
|
+
const wheelGroup = new THREE.Group();
|
|
1196
|
+
wheelGroup.position.set(0, wheelRadius + 5, 0);
|
|
1197
|
+
|
|
1198
|
+
const rimMaterial = new THREE.MeshLambertMaterial({ color: 0x63E6BE });
|
|
1199
|
+
const rim = new THREE.Mesh(new THREE.TorusGeometry(wheelRadius, 0.6, 32, 96), rimMaterial);
|
|
1200
|
+
rim.rotation.y = Math.PI / 2;
|
|
1201
|
+
rim.castShadow = true;
|
|
1202
|
+
wheelGroup.add(rim);
|
|
1203
|
+
|
|
1204
|
+
// スポーク
|
|
1205
|
+
const spokeMaterial = new THREE.MeshLambertMaterial({ color: 0x45B7D1 });
|
|
1206
|
+
const spokeGeometry = new THREE.BoxGeometry(0.3, wheelRadius * 2 - 2, 0.3);
|
|
1207
|
+
for (let i = 0; i < 12; i++) {
|
|
1208
|
+
const spoke = new THREE.Mesh(spokeGeometry, spokeMaterial);
|
|
1209
|
+
spoke.position.y = 0;
|
|
1210
|
+
spoke.castShadow = true;
|
|
1211
|
+
const holder = new THREE.Group();
|
|
1212
|
+
holder.rotation.x = (i / 12) * Math.PI * 2;
|
|
1213
|
+
holder.add(spoke);
|
|
1214
|
+
wheelGroup.add(holder);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// ゴンドラ
|
|
1218
|
+
const gondolaColors = [0xFF69B4, 0x32CD32, 0xFFD700, 0xFF6F61, 0x9370DB, 0x00CED1, 0xFFA07A, 0xFF1493, 0x2E8B57, 0xFFB347];
|
|
1219
|
+
const gondolaControllers = [];
|
|
1220
|
+
const hangerMaterial = new THREE.MeshLambertMaterial({ color: 0xF4A261 });
|
|
1221
|
+
|
|
1222
|
+
for (let i = 0; i < gondolaCount; i++) {
|
|
1223
|
+
const angle = (i / gondolaCount) * Math.PI * 2;
|
|
1224
|
+
|
|
1225
|
+
const pivot = new THREE.Group();
|
|
1226
|
+
pivot.rotation.x = angle;
|
|
1227
|
+
|
|
1228
|
+
const gondolaSwing = new THREE.Group();
|
|
1229
|
+
gondolaSwing.position.set(0, 0, wheelRadius - 0.8);
|
|
1230
|
+
pivot.add(gondolaSwing);
|
|
1231
|
+
|
|
1232
|
+
const hanger = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 3.4, 8), hangerMaterial);
|
|
1233
|
+
hanger.position.y = -1.7;
|
|
1234
|
+
gondolaSwing.add(hanger);
|
|
1235
|
+
|
|
1236
|
+
const cabin = new THREE.Mesh(new THREE.BoxGeometry(2.8, 2.2, 2.4), new THREE.MeshLambertMaterial({ color: gondolaColors[i] }));
|
|
1237
|
+
cabin.position.y = -3.4;
|
|
1238
|
+
cabin.castShadow = true;
|
|
1239
|
+
gondolaSwing.add(cabin);
|
|
1240
|
+
|
|
1241
|
+
const roof = new THREE.Mesh(new THREE.CylinderGeometry(0, 1.6, 0.6, 8), new THREE.MeshLambertMaterial({ color: 0xFFFFFF }));
|
|
1242
|
+
roof.position.y = -2.2;
|
|
1243
|
+
gondolaSwing.add(roof);
|
|
1244
|
+
|
|
1245
|
+
const base = new THREE.Mesh(new THREE.BoxGeometry(2.6, 0.35, 2.2), new THREE.MeshLambertMaterial({ color: 0xE29578 }));
|
|
1246
|
+
base.position.y = -4.5;
|
|
1247
|
+
gondolaSwing.add(base);
|
|
1248
|
+
|
|
1249
|
+
gondolaSwing.rotation.x = 0;
|
|
1250
|
+
wheelGroup.add(pivot);
|
|
1251
|
+
gondolaControllers.push({ swing: gondolaSwing });
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// 中央軸
|
|
1255
|
+
const axleMaterial = new THREE.MeshLambertMaterial({ color: 0x8D99AE });
|
|
1256
|
+
const axle = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.6, 16, 24), axleMaterial);
|
|
1257
|
+
axle.rotation.z = Math.PI / 2;
|
|
1258
|
+
axle.position.set(0, 0, 0);
|
|
1259
|
+
axle.castShadow = true;
|
|
1260
|
+
wheelGroup.add(axle);
|
|
1261
|
+
|
|
1262
|
+
const hubMaterial = new THREE.MeshLambertMaterial({ color: 0xF1FAEE });
|
|
1263
|
+
const hub = new THREE.Mesh(new THREE.CylinderGeometry(1.6, 1.6, 1.2, 24), hubMaterial);
|
|
1264
|
+
hub.rotation.z = Math.PI / 2;
|
|
1265
|
+
hub.position.set(0, 0, 0);
|
|
1266
|
+
hub.castShadow = true;
|
|
1267
|
+
wheelGroup.add(hub);
|
|
1268
|
+
|
|
1269
|
+
const gondolaLight = new THREE.PointLight(0xFFD6FF, 1.3, 50, 2.4);
|
|
1270
|
+
gondolaLight.position.set(0, 0, 0);
|
|
1271
|
+
wheelGroup.add(gondolaLight);
|
|
1272
|
+
registerNightLight(gondolaLight, 1.3);
|
|
1273
|
+
|
|
1274
|
+
ferrisGroup.add(wheelGroup);
|
|
1275
|
+
|
|
1276
|
+
// ベース位置
|
|
1277
|
+
ferrisGroup.position.set(x, 0, z);
|
|
1278
|
+
|
|
1279
|
+
// アニメーション
|
|
1280
|
+
function animateFerris() {
|
|
1281
|
+
wheelGroup.rotation.x = (wheelGroup.rotation.x + 0.004) % (Math.PI * 2);
|
|
1282
|
+
gondolaControllers.forEach(controller => {
|
|
1283
|
+
controller.swing.rotation.x = -wheelGroup.rotation.x;
|
|
1284
|
+
});
|
|
1285
|
+
requestAnimationFrame(animateFerris);
|
|
1286
|
+
}
|
|
1287
|
+
animateFerris();
|
|
1288
|
+
|
|
1289
|
+
return ferrisGroup;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function createNeonCircuit(x, z) {
|
|
1293
|
+
const group = new THREE.Group();
|
|
1294
|
+
group.userData.skipWiggle = true;
|
|
1295
|
+
|
|
1296
|
+
const outerRingMaterial = new THREE.MeshBasicMaterial({ color: 0x9BF6FF, transparent: true, opacity: 0.45, side: THREE.DoubleSide });
|
|
1297
|
+
const outerRing = new THREE.Mesh(new THREE.RingGeometry(11.5, 12.8, 64), outerRingMaterial);
|
|
1298
|
+
outerRing.rotation.x = -Math.PI / 2;
|
|
1299
|
+
group.add(outerRing);
|
|
1300
|
+
neonElements.push({ mesh: outerRing, type: 'rotateZ', speed: 0.006, baseOpacity: outerRingMaterial.opacity, phase: Math.random() * Math.PI * 2 });
|
|
1301
|
+
|
|
1302
|
+
const tileMaterial = new THREE.MeshBasicMaterial({ color: 0xFFFFFF, transparent: true, opacity: 0.55 });
|
|
1303
|
+
for (let i = 0; i < 16; i++) {
|
|
1304
|
+
const angle = (i / 16) * Math.PI * 2;
|
|
1305
|
+
const tile = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.2, 0.8), tileMaterial.clone());
|
|
1306
|
+
tile.position.set(Math.cos(angle) * 12.2, 0.12, Math.sin(angle) * 12.2);
|
|
1307
|
+
tile.rotation.y = -angle;
|
|
1308
|
+
group.add(tile);
|
|
1309
|
+
neonElements.push({ mesh: tile, type: 'pulse', speed: 4.5 + i * 0.2, baseOpacity: tile.material.opacity, phase: Math.random() * Math.PI * 2 });
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const circuitLights = [
|
|
1313
|
+
new THREE.PointLight(0xBDE0FE, 1.2, 40, 2),
|
|
1314
|
+
new THREE.PointLight(0xFFAFCC, 1.0, 40, 2)
|
|
1315
|
+
];
|
|
1316
|
+
circuitLights[0].position.set(0, 2.5, 0);
|
|
1317
|
+
circuitLights[1].position.set(6, 2.8, -6);
|
|
1318
|
+
circuitLights.forEach(light => {
|
|
1319
|
+
group.add(light);
|
|
1320
|
+
registerNightLight(light, light.intensity);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
const innerPlatform = new THREE.Mesh(new THREE.CylinderGeometry(6, 6, 0.4, 32), new THREE.MeshLambertMaterial({ color: 0xFFF6E0, transparent: true, opacity: 0.8 }));
|
|
1324
|
+
innerPlatform.position.y = 0.2;
|
|
1325
|
+
innerPlatform.receiveShadow = true;
|
|
1326
|
+
group.add(innerPlatform);
|
|
1327
|
+
|
|
1328
|
+
group.position.set(x, 0.05, z);
|
|
1329
|
+
return group;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function createHoverDroneGarden(x, z) {
|
|
1333
|
+
const group = new THREE.Group();
|
|
1334
|
+
group.userData.skipWiggle = true;
|
|
1335
|
+
|
|
1336
|
+
const pad = new THREE.Mesh(new THREE.CylinderGeometry(4, 4, 0.4, 24), new THREE.MeshLambertMaterial({ color: 0x22223B, transparent: true, opacity: 0.35 }));
|
|
1337
|
+
pad.position.y = 0.2;
|
|
1338
|
+
pad.receiveShadow = true;
|
|
1339
|
+
group.add(pad);
|
|
1340
|
+
|
|
1341
|
+
const droneCount = 6;
|
|
1342
|
+
for (let i = 0; i < droneCount; i++) {
|
|
1343
|
+
const pivot = new THREE.Group();
|
|
1344
|
+
pivot.rotation.y = (i / droneCount) * Math.PI * 2;
|
|
1345
|
+
|
|
1346
|
+
const arm = new THREE.Group();
|
|
1347
|
+
arm.position.set(0, 2.6, 3.2);
|
|
1348
|
+
pivot.add(arm);
|
|
1349
|
+
|
|
1350
|
+
const body = new THREE.Mesh(new THREE.SphereGeometry(0.8, 24, 24), new THREE.MeshLambertMaterial({ color: 0x80ED99 }));
|
|
1351
|
+
body.castShadow = true;
|
|
1352
|
+
arm.add(body);
|
|
1353
|
+
|
|
1354
|
+
const eye = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.18, 0.6, 16), new THREE.MeshLambertMaterial({ color: 0xFFFFFF }));
|
|
1355
|
+
eye.rotation.x = Math.PI / 2;
|
|
1356
|
+
eye.position.z = 0.8;
|
|
1357
|
+
body.add(eye);
|
|
1358
|
+
|
|
1359
|
+
const halo = new THREE.Mesh(new THREE.TorusGeometry(1.1, 0.06, 8, 32), new THREE.MeshBasicMaterial({ color: 0xC0FDFB, transparent: true, opacity: 0.75 }));
|
|
1360
|
+
halo.rotation.x = Math.PI / 2;
|
|
1361
|
+
arm.add(halo);
|
|
1362
|
+
neonElements.push({ mesh: halo, type: 'rotateY', speed: 0.04, baseOpacity: halo.material.opacity, phase: Math.random() * Math.PI * 2 });
|
|
1363
|
+
|
|
1364
|
+
group.add(pivot);
|
|
1365
|
+
hoverDrones.push({ pivot, arm, baseY: arm.position.y, amplitude: 0.6 + Math.random() * 0.4, speed: 0.01 + i * 0.002, phase: Math.random() * Math.PI * 2 });
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
group.position.set(x, 0, z);
|
|
1369
|
+
const hubLight = new THREE.PointLight(0x80ED99, 1.1, 28, 2.2);
|
|
1370
|
+
hubLight.position.set(0, 3.2, 0);
|
|
1371
|
+
group.add(hubLight);
|
|
1372
|
+
registerNightLight(hubLight, 1.1);
|
|
1373
|
+
return group;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function createAICrystalSpire(x, z) {
|
|
1377
|
+
const group = new THREE.Group();
|
|
1378
|
+
group.userData.skipWiggle = true;
|
|
1379
|
+
|
|
1380
|
+
const base = new THREE.Mesh(new THREE.CylinderGeometry(2.6, 3.2, 1.4, 16), new THREE.MeshLambertMaterial({ color: 0x8D99AE }));
|
|
1381
|
+
base.position.y = 0.7;
|
|
1382
|
+
base.castShadow = true;
|
|
1383
|
+
group.add(base);
|
|
1384
|
+
|
|
1385
|
+
const pillar = new THREE.Mesh(new THREE.CylinderGeometry(1.4, 1.4, 7, 20), new THREE.MeshLambertMaterial({ color: 0x5A189A, transparent: true, opacity: 0.5 }));
|
|
1386
|
+
pillar.position.y = 4.2;
|
|
1387
|
+
pillar.castShadow = true;
|
|
1388
|
+
group.add(pillar);
|
|
1389
|
+
|
|
1390
|
+
const crystal = new THREE.Mesh(new THREE.OctahedronGeometry(1.6), new THREE.MeshLambertMaterial({ color: 0x9D4EDD, transparent: true, opacity: 0.6 }));
|
|
1391
|
+
crystal.position.y = 8.6;
|
|
1392
|
+
group.add(crystal);
|
|
1393
|
+
|
|
1394
|
+
const halo = new THREE.Mesh(new THREE.TorusGeometry(2.8, 0.12, 12, 48), new THREE.MeshBasicMaterial({ color: 0xA0E7E5, transparent: true, opacity: 0.6 }));
|
|
1395
|
+
halo.rotation.x = Math.PI / 2;
|
|
1396
|
+
halo.position.y = 6.5;
|
|
1397
|
+
group.add(halo);
|
|
1398
|
+
neonElements.push({ mesh: halo, type: 'rotateY', speed: 0.03, baseOpacity: halo.material.opacity, phase: Math.random() * Math.PI * 2 });
|
|
1399
|
+
|
|
1400
|
+
const orbMaterial = new THREE.MeshBasicMaterial({ color: 0xFFFFFF, transparent: true, opacity: 0.8 });
|
|
1401
|
+
const orbPivot = new THREE.Group();
|
|
1402
|
+
const orb = new THREE.Mesh(new THREE.SphereGeometry(0.6, 16, 16), orbMaterial);
|
|
1403
|
+
orb.position.set(0, 0, 4);
|
|
1404
|
+
orbPivot.add(orb);
|
|
1405
|
+
orbPivot.position.y = 6.5;
|
|
1406
|
+
group.add(orbPivot);
|
|
1407
|
+
hoverDrones.push({ pivot: orbPivot, arm: orb, baseY: orb.position.y, amplitude: 0.4, speed: 0.02, phase: 0 });
|
|
1408
|
+
neonElements.push({ mesh: orb, type: 'pulse', speed: 5.5, baseOpacity: orbMaterial.opacity, phase: Math.random() * Math.PI * 2 });
|
|
1409
|
+
|
|
1410
|
+
const spireLight = new THREE.PointLight(0xC77DFF, 1.8, 55, 2);
|
|
1411
|
+
spireLight.position.set(0, 8.3, 0);
|
|
1412
|
+
group.add(spireLight);
|
|
1413
|
+
registerNightLight(spireLight, 1.8);
|
|
1414
|
+
|
|
1415
|
+
group.position.set(x, 0, z);
|
|
1416
|
+
return group;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function createMaglevTrainLoop() {
|
|
1420
|
+
const loopGroup = new THREE.Group();
|
|
1421
|
+
loopGroup.userData.skipWiggle = true;
|
|
1422
|
+
|
|
1423
|
+
const radiusX = 40;
|
|
1424
|
+
const radiusZ = 32;
|
|
1425
|
+
const railHeight = 1.2;
|
|
1426
|
+
|
|
1427
|
+
const pathPoints = [];
|
|
1428
|
+
const segments = 120;
|
|
1429
|
+
for (let i = 0; i < segments; i++) {
|
|
1430
|
+
const angle = (i / segments) * Math.PI * 2;
|
|
1431
|
+
const x = Math.cos(angle) * radiusX;
|
|
1432
|
+
const z = Math.sin(angle) * radiusZ;
|
|
1433
|
+
pathPoints.push(new THREE.Vector3(x, railHeight, z));
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const railCurve = new THREE.CatmullRomCurve3(pathPoints, true, 'catmullrom', 0.8);
|
|
1437
|
+
const railGeometry = new THREE.TubeGeometry(railCurve, segments * 3, 0.6, 12, true);
|
|
1438
|
+
const railMaterial = new THREE.MeshLambertMaterial({ color: 0x95D5B2 });
|
|
1439
|
+
const railMesh = new THREE.Mesh(railGeometry, railMaterial);
|
|
1440
|
+
railMesh.castShadow = true;
|
|
1441
|
+
railMesh.receiveShadow = true;
|
|
1442
|
+
loopGroup.add(railMesh);
|
|
1443
|
+
|
|
1444
|
+
const railGlow = railMesh.clone();
|
|
1445
|
+
railGlow.material = new THREE.MeshBasicMaterial({ color: 0xE4C1F9, transparent: true, opacity: 0.35 });
|
|
1446
|
+
railGlow.scale.set(1.04, 1.04, 1.04);
|
|
1447
|
+
loopGroup.add(railGlow);
|
|
1448
|
+
neonElements.push({ mesh: railGlow, type: 'pulse', speed: 3.8, baseOpacity: railGlow.material.opacity, phase: Math.random() * Math.PI * 2 });
|
|
1449
|
+
|
|
1450
|
+
const sleeperMaterial = new THREE.MeshLambertMaterial({ color: 0xF25F5C });
|
|
1451
|
+
for (let i = 0; i < segments; i += 6) {
|
|
1452
|
+
const t = i / segments;
|
|
1453
|
+
const point = railCurve.getPoint(t);
|
|
1454
|
+
const tangent = railCurve.getTangent(t);
|
|
1455
|
+
const sleeper = new THREE.Mesh(new THREE.BoxGeometry(2.5, 0.3, 0.8), sleeperMaterial);
|
|
1456
|
+
sleeper.position.copy(point);
|
|
1457
|
+
const angle = Math.atan2(tangent.x, tangent.z);
|
|
1458
|
+
sleeper.rotation.y = angle;
|
|
1459
|
+
sleeper.receiveShadow = true;
|
|
1460
|
+
loopGroup.add(sleeper);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const trainGroup = new THREE.Group();
|
|
1464
|
+
trainGroup.castShadow = true;
|
|
1465
|
+
trainGroup.receiveShadow = true;
|
|
1466
|
+
|
|
1467
|
+
const carColors = [0xFF9F1C, 0xFFBF69, 0xFFCAD4];
|
|
1468
|
+
const carCount = 3;
|
|
1469
|
+
const carSpacing = 3.6;
|
|
1470
|
+
const cars = [];
|
|
1471
|
+
for (let i = 0; i < carCount; i++) {
|
|
1472
|
+
const car = new THREE.Group();
|
|
1473
|
+
const body = new THREE.Mesh(new THREE.BoxGeometry(2.8, 1.6, 1.8), new THREE.MeshLambertMaterial({ color: carColors[i % carColors.length] }));
|
|
1474
|
+
body.position.y = 1;
|
|
1475
|
+
body.castShadow = true;
|
|
1476
|
+
car.add(body);
|
|
1477
|
+
|
|
1478
|
+
const windowMaterial = new THREE.MeshLambertMaterial({ color: 0xF8F9FA, transparent: true, opacity: 0.8 });
|
|
1479
|
+
const window = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.8, 0.1), windowMaterial);
|
|
1480
|
+
window.position.set(0, 1.1, 0.95);
|
|
1481
|
+
car.add(window);
|
|
1482
|
+
const windowBack = window.clone();
|
|
1483
|
+
windowBack.position.z = -0.95;
|
|
1484
|
+
car.add(windowBack);
|
|
1485
|
+
|
|
1486
|
+
const neonStrip = new THREE.Mesh(new THREE.BoxGeometry(2.6, 0.1, 0.1), new THREE.MeshBasicMaterial({ color: 0xFFFFFF, transparent: true, opacity: 0.7 }));
|
|
1487
|
+
neonStrip.position.set(0, 0.4, 1.0);
|
|
1488
|
+
car.add(neonStrip);
|
|
1489
|
+
const neonStripBack = neonStrip.clone();
|
|
1490
|
+
neonStripBack.position.z = -1.0;
|
|
1491
|
+
car.add(neonStripBack);
|
|
1492
|
+
neonElements.push({ mesh: neonStrip, type: 'pulse', speed: 6 + i, baseOpacity: neonStrip.material.opacity, phase: Math.random() * Math.PI * 2 });
|
|
1493
|
+
neonElements.push({ mesh: neonStripBack, type: 'pulse', speed: 6 + i, baseOpacity: neonStripBack.material.opacity, phase: Math.random() * Math.PI * 2 });
|
|
1494
|
+
|
|
1495
|
+
trainGroup.add(car);
|
|
1496
|
+
cars.push(car);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
loopGroup.add(trainGroup);
|
|
1500
|
+
|
|
1501
|
+
trainMovers.push({ cars, curve: railCurve, offset: Math.random(), speed: 0.0016, spacing: carSpacing, length: railCurve.getLength() });
|
|
1502
|
+
|
|
1503
|
+
return loopGroup;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// 👥 小さなキャラクターフィギュア
|
|
1507
|
+
function createCharacterFigure(x, z, color, type = 'person') {
|
|
1508
|
+
const figureGroup = new THREE.Group();
|
|
1509
|
+
|
|
1510
|
+
if (type === 'person') {
|
|
1511
|
+
// 体
|
|
1512
|
+
const bodyGeometry = new THREE.BoxGeometry(0.8, 1.5, 0.5);
|
|
1513
|
+
const bodyMaterial = new THREE.MeshLambertMaterial({ color: color });
|
|
1514
|
+
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
|
1515
|
+
body.position.set(0, 1, 0);
|
|
1516
|
+
body.castShadow = true;
|
|
1517
|
+
figureGroup.add(body);
|
|
1518
|
+
|
|
1519
|
+
// 頭
|
|
1520
|
+
const headGeometry = new THREE.BoxGeometry(0.6, 0.6, 0.6);
|
|
1521
|
+
const headMaterial = new THREE.MeshLambertMaterial({ color: 0xFFDBB5 });
|
|
1522
|
+
const head = new THREE.Mesh(headGeometry, headMaterial);
|
|
1523
|
+
head.position.set(0, 2, 0);
|
|
1524
|
+
head.castShadow = true;
|
|
1525
|
+
figureGroup.add(head);
|
|
1526
|
+
} else if (type === 'cat') {
|
|
1527
|
+
// 猫の体
|
|
1528
|
+
const catBodyGeometry = new THREE.BoxGeometry(1, 0.6, 0.4);
|
|
1529
|
+
const catBodyMaterial = new THREE.MeshLambertMaterial({ color: color });
|
|
1530
|
+
const catBody = new THREE.Mesh(catBodyGeometry, catBodyMaterial);
|
|
1531
|
+
catBody.position.set(0, 0.3, 0);
|
|
1532
|
+
catBody.castShadow = true;
|
|
1533
|
+
figureGroup.add(catBody);
|
|
1534
|
+
|
|
1535
|
+
// 猫の頭
|
|
1536
|
+
const catHeadGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
|
|
1537
|
+
const catHead = new THREE.Mesh(catHeadGeometry, catBodyMaterial);
|
|
1538
|
+
catHead.position.set(0.5, 0.5, 0);
|
|
1539
|
+
catHead.castShadow = true;
|
|
1540
|
+
figureGroup.add(catHead);
|
|
1541
|
+
} else if (type === 'robot') {
|
|
1542
|
+
// ロボットの体
|
|
1543
|
+
const robotBodyGeometry = new THREE.BoxGeometry(0.8, 1.2, 0.8);
|
|
1544
|
+
const robotBodyMaterial = new THREE.MeshLambertMaterial({ color: color });
|
|
1545
|
+
const robotBody = new THREE.Mesh(robotBodyGeometry, robotBodyMaterial);
|
|
1546
|
+
robotBody.position.set(0, 0.8, 0);
|
|
1547
|
+
robotBody.castShadow = true;
|
|
1548
|
+
figureGroup.add(robotBody);
|
|
1549
|
+
|
|
1550
|
+
// ロボットの頭
|
|
1551
|
+
const robotHeadGeometry = new THREE.BoxGeometry(0.6, 0.6, 0.6);
|
|
1552
|
+
const robotHead = new THREE.Mesh(robotHeadGeometry, robotBodyMaterial);
|
|
1553
|
+
robotHead.position.set(0, 1.6, 0);
|
|
1554
|
+
robotHead.castShadow = true;
|
|
1555
|
+
figureGroup.add(robotHead);
|
|
1556
|
+
|
|
1557
|
+
// アンテナ
|
|
1558
|
+
const antennaGeometry = new THREE.BoxGeometry(0.1, 0.5, 0.1);
|
|
1559
|
+
const antenna = new THREE.Mesh(antennaGeometry, robotBodyMaterial);
|
|
1560
|
+
antenna.position.set(0, 2.1, 0);
|
|
1561
|
+
figureGroup.add(antenna);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
figureGroup.position.set(x, 0, z);
|
|
1565
|
+
|
|
1566
|
+
// 微細なアニメーション
|
|
1567
|
+
const originalY = figureGroup.position.y;
|
|
1568
|
+
function animateCharacter() {
|
|
1569
|
+
figureGroup.position.y = originalY + Math.sin(Date.now() * 0.003 + x + z) * 0.1;
|
|
1570
|
+
figureGroup.rotation.y = Math.sin(Date.now() * 0.002 + x + z) * 0.1;
|
|
1571
|
+
requestAnimationFrame(animateCharacter);
|
|
1572
|
+
}
|
|
1573
|
+
animateCharacter();
|
|
1574
|
+
|
|
1575
|
+
return figureGroup;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// 🧸 おもちゃの街を構築
|
|
1579
|
+
const toyCity = new THREE.Group();
|
|
1580
|
+
|
|
1581
|
+
// カラフルなレゴ建物
|
|
1582
|
+
const building1 = createLegoBuilding(-15, -15, 8, 6, 8, 0xFF6B6B);
|
|
1583
|
+
const building2 = createLegoBuilding(15, -15, 6, 8, 6, 0x4ECDC4);
|
|
1584
|
+
const building3 = createLegoBuilding(-20, 22, 10, 5, 6, 0x45B7D1);
|
|
1585
|
+
const building4 = createLegoBuilding(15, 15, 6, 10, 8, 0x96CEB4);
|
|
1586
|
+
const building5 = createLegoBuilding(0, -25, 12, 4, 6, 0xFECEA8);
|
|
1587
|
+
|
|
1588
|
+
const aiLabHub = createAilabHub(-5, 18);
|
|
1589
|
+
toyCity.add(building1, building2, building3, building4, building5, aiLabHub);
|
|
1590
|
+
|
|
1591
|
+
const stand1 = createDisplayStand(-8, 6, '作品A', '#FF6B6B');
|
|
1592
|
+
const stand2 = createDisplayStand(8, 6, '作品B', '#4ECDC4');
|
|
1593
|
+
const stand3 = createDisplayStand(0, -12, '作品C', '#FF9F1C');
|
|
1594
|
+
toyCity.add(stand1, stand2, stand3);
|
|
1595
|
+
|
|
1596
|
+
// テント
|
|
1597
|
+
const tent1 = createToyTent(-25, 0, 0xFF1493, 0xFFD700);
|
|
1598
|
+
const tent2 = createToyTent(25, 0, 0x9B59B6, 0xFF6B6B);
|
|
1599
|
+
toyCity.add(tent1, tent2);
|
|
1600
|
+
|
|
1601
|
+
const neonCircuit = createNeonCircuit(5, -8);
|
|
1602
|
+
const droneGarden = createHoverDroneGarden(-18, 6);
|
|
1603
|
+
const aiSpire = createAICrystalSpire(18, 10);
|
|
1604
|
+
toyCity.add(neonCircuit, droneGarden, aiSpire);
|
|
1605
|
+
|
|
1606
|
+
const botColors = [0xFF6B6B, 0x4ECDC4, 0xFFBF69, 0xA0E7E5];
|
|
1607
|
+
botColors.forEach((color, idx) => {
|
|
1608
|
+
const angle = (idx / botColors.length) * Math.PI * 2;
|
|
1609
|
+
const radius = 12;
|
|
1610
|
+
const bot = createServiceBot(Math.cos(angle) * radius, Math.sin(angle) * radius, color);
|
|
1611
|
+
toyCity.add(bot);
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
// メリーゴーランド
|
|
1615
|
+
const merryGoRound = createMerryGoRound(0, 0);
|
|
1616
|
+
toyCity.add(merryGoRound);
|
|
1617
|
+
|
|
1618
|
+
// 観覧車
|
|
1619
|
+
const ferrisWheel = createFerrisWheel(30, -20);
|
|
1620
|
+
toyCity.add(ferrisWheel);
|
|
1621
|
+
|
|
1622
|
+
const maglevLoop = createMaglevTrainLoop();
|
|
1623
|
+
toyCity.add(maglevLoop);
|
|
1624
|
+
|
|
1625
|
+
const spireEmitter = createParticleEmitter(new THREE.Vector3(18, 9, 10), 0xC77DFF, { nightOnly: false });
|
|
1626
|
+
scene.add(spireEmitter);
|
|
1627
|
+
const labEmitter = createParticleEmitter(new THREE.Vector3(-5, 7.5, 18), 0x2EC4B6, { nightOnly: false });
|
|
1628
|
+
scene.add(labEmitter);
|
|
1629
|
+
const starField = createParticleEmitter(new THREE.Vector3(0, 34, 0), 0xFFFFFF, { nightOnly: true });
|
|
1630
|
+
starField.visible = false;
|
|
1631
|
+
scene.add(starField);
|
|
1632
|
+
|
|
1633
|
+
applySceneTheme('day');
|
|
1634
|
+
// キャラクターフィギュア(ユーザーが置きたくなるようなサンプル)
|
|
1635
|
+
const character1 = createCharacterFigure(5, 8, 0xFF69B4, 'person');
|
|
1636
|
+
const character2 = createCharacterFigure(-8, 6, 0x32CD32, 'robot');
|
|
1637
|
+
const character3 = createCharacterFigure(12, -5, 0xFFD700, 'cat');
|
|
1638
|
+
const character4 = createCharacterFigure(-3, -12, 0xFF4500, 'person');
|
|
1639
|
+
const character5 = createCharacterFigure(20, 10, 0x9370DB, 'robot');
|
|
1640
|
+
const character6 = createCharacterFigure(-18, -8, 0x00CED1, 'cat');
|
|
1641
|
+
|
|
1642
|
+
toyCity.add(character1, character2, character3, character4, character5, character6);
|
|
1643
|
+
|
|
1644
|
+
scene.add(toyCity);
|
|
1645
|
+
|
|
1646
|
+
// カメラポジション
|
|
1647
|
+
camera.position.set(0, 20, 30);
|
|
1648
|
+
camera.lookAt(0, 0, 0);
|
|
1649
|
+
|
|
1650
|
+
// カメラコントロール
|
|
1651
|
+
let isMouseDown = false;
|
|
1652
|
+
let mouseX = 0;
|
|
1653
|
+
let mouseY = 0;
|
|
1654
|
+
let cameraAngleX = 0;
|
|
1655
|
+
let cameraAngleY = 0;
|
|
1656
|
+
let cameraDistance = 35;
|
|
1657
|
+
let cameraControlsEnabled = true;
|
|
1658
|
+
|
|
1659
|
+
// マウスイベント
|
|
1660
|
+
renderer.domElement.addEventListener('mousedown', (event) => {
|
|
1661
|
+
if (cameraControlsEnabled) {
|
|
1662
|
+
isMouseDown = true;
|
|
1663
|
+
mouseX = event.clientX;
|
|
1664
|
+
mouseY = event.clientY;
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
renderer.domElement.addEventListener('mouseup', () => {
|
|
1669
|
+
isMouseDown = false;
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
renderer.domElement.addEventListener('mousemove', (event) => {
|
|
1673
|
+
if (isMouseDown && cameraControlsEnabled) {
|
|
1674
|
+
const deltaX = event.clientX - mouseX;
|
|
1675
|
+
const deltaY = event.clientY - mouseY;
|
|
1676
|
+
|
|
1677
|
+
cameraAngleY -= deltaX * 0.01;
|
|
1678
|
+
cameraAngleX += deltaY * 0.01;
|
|
1679
|
+
cameraAngleX = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, cameraAngleX));
|
|
1680
|
+
|
|
1681
|
+
mouseX = event.clientX;
|
|
1682
|
+
mouseY = event.clientY;
|
|
1683
|
+
|
|
1684
|
+
updateCameraPosition();
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
renderer.domElement.addEventListener('wheel', (event) => {
|
|
1689
|
+
cameraDistance += event.deltaY * 0.01;
|
|
1690
|
+
cameraDistance = Math.max(10, Math.min(80, cameraDistance));
|
|
1691
|
+
updateCameraPosition();
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
function updateCameraPosition() {
|
|
1695
|
+
camera.position.x = Math.sin(cameraAngleY) * Math.cos(cameraAngleX) * cameraDistance;
|
|
1696
|
+
camera.position.y = Math.sin(cameraAngleX) * cameraDistance + 10;
|
|
1697
|
+
camera.position.z = Math.cos(cameraAngleY) * Math.cos(cameraAngleX) * cameraDistance;
|
|
1698
|
+
camera.lookAt(0, 0, 0);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// リサイズ対応
|
|
1702
|
+
window.addEventListener('resize', () => {
|
|
1703
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
1704
|
+
camera.updateProjectionMatrix();
|
|
1705
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
// 30秒後の自動非表示タイマー開始
|
|
1709
|
+
scheduleInfoPanelAutoHide();
|
|
1710
|
+
|
|
1711
|
+
// ナイトモードボタンを中央上部に独立配置
|
|
1712
|
+
const themeToggle = document.createElement('button');
|
|
1713
|
+
themeToggle.className = 'tool-button';
|
|
1714
|
+
themeToggle.id = 'theme-toggle';
|
|
1715
|
+
themeToggle.innerHTML = '🌙 ナイトモード';
|
|
1716
|
+
|
|
1717
|
+
// テーマに応じたボタンスタイル
|
|
1718
|
+
function updateThemeButtonStyle() {
|
|
1719
|
+
if (isNightMode) {
|
|
1720
|
+
// ナイトモード時:美しい元の色を維持
|
|
1721
|
+
themeToggle.style.cssText = `
|
|
1722
|
+
position: fixed;
|
|
1723
|
+
top: 20px;
|
|
1724
|
+
left: 50%;
|
|
1725
|
+
transform: translateX(-50%);
|
|
1726
|
+
z-index: 100;
|
|
1727
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.28), rgba(255, 192, 203, 0.35));
|
|
1728
|
+
color: #fff;
|
|
1729
|
+
border: none;
|
|
1730
|
+
border-radius: 999px;
|
|
1731
|
+
padding: 10px 18px;
|
|
1732
|
+
font-size: 13px;
|
|
1733
|
+
font-weight: 600;
|
|
1734
|
+
cursor: pointer;
|
|
1735
|
+
box-shadow: 0 6px 18px rgba(255, 105, 180, 0.35);
|
|
1736
|
+
transition: all 0.2s ease;
|
|
1737
|
+
backdrop-filter: blur(10px);
|
|
1738
|
+
`;
|
|
1739
|
+
} else {
|
|
1740
|
+
// デイモード時:ガラスモーフィングの美しい薄紫
|
|
1741
|
+
themeToggle.style.cssText = `
|
|
1742
|
+
position: fixed;
|
|
1743
|
+
top: 20px;
|
|
1744
|
+
left: 50%;
|
|
1745
|
+
transform: translateX(-50%);
|
|
1746
|
+
z-index: 100;
|
|
1747
|
+
background: rgba(255, 255, 255, 0.25);
|
|
1748
|
+
color: #6a4c93;
|
|
1749
|
+
border: 1px solid rgba(255, 255, 255, 0.4);
|
|
1750
|
+
border-radius: 999px;
|
|
1751
|
+
padding: 10px 18px;
|
|
1752
|
+
font-size: 13px;
|
|
1753
|
+
font-weight: 600;
|
|
1754
|
+
cursor: pointer;
|
|
1755
|
+
box-shadow: 0 8px 32px rgba(138, 43, 226, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.6);
|
|
1756
|
+
transition: all 0.3s ease;
|
|
1757
|
+
backdrop-filter: blur(25px) saturate(200%);
|
|
1758
|
+
animation: gentle-float 3s ease-in-out infinite;
|
|
1759
|
+
`;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
updateThemeButtonStyle();
|
|
1764
|
+
document.body.appendChild(themeToggle);
|
|
1765
|
+
|
|
1766
|
+
themeToggle.addEventListener('mouseover', () => {
|
|
1767
|
+
themeToggle.style.transform = 'translateX(-50%) translateY(-2px) scale(1.05)';
|
|
1768
|
+
if (isNightMode) {
|
|
1769
|
+
themeToggle.style.boxShadow = '0 10px 28px rgba(255, 105, 180, 0.45)';
|
|
1770
|
+
} else {
|
|
1771
|
+
themeToggle.style.boxShadow = '0 10px 28px rgba(75, 0, 130, 0.6)';
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
themeToggle.addEventListener('mouseout', () => {
|
|
1776
|
+
themeToggle.style.transform = 'translateX(-50%)';
|
|
1777
|
+
if (isNightMode) {
|
|
1778
|
+
themeToggle.style.boxShadow = '0 6px 18px rgba(255, 105, 180, 0.35)';
|
|
1779
|
+
} else {
|
|
1780
|
+
themeToggle.style.boxShadow = '0 6px 18px rgba(75, 0, 130, 0.4)';
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
themeToggle.addEventListener('click', () => {
|
|
1785
|
+
const nextMode = isNightMode ? 'day' : 'night';
|
|
1786
|
+
applySceneTheme(nextMode);
|
|
1787
|
+
themeToggle.innerHTML = isNightMode ? '🌞 デイモード' : '🌙 ナイトモード';
|
|
1788
|
+
updateThemeButtonStyle(); // テーマ変更時にボタンスタイルも更新
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
// 🎵 高度な音響システムの初期化(wabi-sabi方式)
|
|
1792
|
+
let audioContext = null;
|
|
1793
|
+
let analyser = null;
|
|
1794
|
+
let dataArray = null;
|
|
1795
|
+
let beatDetectionHistory = [];
|
|
1796
|
+
let lastBeatTime = 0;
|
|
1797
|
+
let musicData = {
|
|
1798
|
+
volume: 0,
|
|
1799
|
+
bass: 0,
|
|
1800
|
+
mid: 0,
|
|
1801
|
+
treble: 0,
|
|
1802
|
+
tempo: 0,
|
|
1803
|
+
bpm: 0,
|
|
1804
|
+
beatDetected: false,
|
|
1805
|
+
isPlaying: false
|
|
1806
|
+
};
|
|
1807
|
+
const connectedVideos = new WeakSet(); // 接続済みのvideoを追跡
|
|
1808
|
+
const audioStatusEl = document.getElementById('audio-status');
|
|
1809
|
+
const audioTextEl = document.getElementById('audio-text');
|
|
1810
|
+
|
|
1811
|
+
function setupAudioAnalysis() {
|
|
1812
|
+
if (!audioContext) {
|
|
1813
|
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
1814
|
+
analyser = audioContext.createAnalyser();
|
|
1815
|
+
analyser.fftSize = 256;
|
|
1816
|
+
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
1817
|
+
console.log('🎵 Advanced audio analysis initialized');
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// 動画要素を音響分析に接続
|
|
1822
|
+
function connectVideoToAudio(videoElement) {
|
|
1823
|
+
// すでに接続済みの場合はスキップ
|
|
1824
|
+
if (connectedVideos.has(videoElement)) {
|
|
1825
|
+
console.log('🎵 Video already connected, skipping');
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
if (!audioContext || !analyser) {
|
|
1830
|
+
setupAudioAnalysis();
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
try {
|
|
1834
|
+
if (audioContext.state === 'suspended') {
|
|
1835
|
+
audioContext.resume();
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
const source = audioContext.createMediaElementSource(videoElement);
|
|
1839
|
+
source.connect(analyser);
|
|
1840
|
+
analyser.connect(audioContext.destination);
|
|
1841
|
+
|
|
1842
|
+
connectedVideos.add(videoElement); // 接続済みとしてマーク
|
|
1843
|
+
|
|
1844
|
+
musicData.isPlaying = true;
|
|
1845
|
+
if (audioStatusEl) {
|
|
1846
|
+
audioStatusEl.classList.add('active');
|
|
1847
|
+
if (audioTextEl) {
|
|
1848
|
+
audioTextEl.textContent = '音響連動中 ♪ (BPM検出中)';
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
console.log('🎵 Video connected to advanced audio analyser');
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
console.warn('Audio connection error:', error);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// 音楽データの詳細解析
|
|
1858
|
+
function analyzeMusicData() {
|
|
1859
|
+
if (!analyser || !dataArray) return;
|
|
1860
|
+
|
|
1861
|
+
analyser.getByteFrequencyData(dataArray);
|
|
1862
|
+
|
|
1863
|
+
let sum = 0;
|
|
1864
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
1865
|
+
sum += dataArray[i];
|
|
1866
|
+
}
|
|
1867
|
+
musicData.volume = sum / dataArray.length / 255;
|
|
1868
|
+
|
|
1869
|
+
const bassRange = Math.floor(dataArray.length * 0.1);
|
|
1870
|
+
const midRange = Math.floor(dataArray.length * 0.5);
|
|
1871
|
+
|
|
1872
|
+
let bassSum = 0, midSum = 0, trebleSum = 0;
|
|
1873
|
+
|
|
1874
|
+
for (let i = 0; i < bassRange; i++) {
|
|
1875
|
+
bassSum += dataArray[i];
|
|
1876
|
+
}
|
|
1877
|
+
for (let i = bassRange; i < midRange; i++) {
|
|
1878
|
+
midSum += dataArray[i];
|
|
1879
|
+
}
|
|
1880
|
+
for (let i = midRange; i < dataArray.length; i++) {
|
|
1881
|
+
trebleSum += dataArray[i];
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
musicData.bass = bassSum / bassRange / 255;
|
|
1885
|
+
musicData.mid = midSum / (midRange - bassRange) / 255;
|
|
1886
|
+
musicData.treble = trebleSum / (dataArray.length - midRange) / 255;
|
|
1887
|
+
|
|
1888
|
+
const currentTime = performance.now();
|
|
1889
|
+
const bassThreshold = 0.3;
|
|
1890
|
+
const volumeThreshold = 0.2;
|
|
1891
|
+
|
|
1892
|
+
musicData.beatDetected = false;
|
|
1893
|
+
|
|
1894
|
+
if (musicData.bass > bassThreshold && musicData.volume > volumeThreshold) {
|
|
1895
|
+
if (currentTime - lastBeatTime > 300) {
|
|
1896
|
+
musicData.beatDetected = true;
|
|
1897
|
+
lastBeatTime = currentTime;
|
|
1898
|
+
|
|
1899
|
+
beatDetectionHistory.push(currentTime);
|
|
1900
|
+
if (beatDetectionHistory.length > 8) {
|
|
1901
|
+
beatDetectionHistory.shift();
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
if (beatDetectionHistory.length >= 4) {
|
|
1905
|
+
const intervals = [];
|
|
1906
|
+
for (let i = 1; i < beatDetectionHistory.length; i++) {
|
|
1907
|
+
intervals.push(beatDetectionHistory[i] - beatDetectionHistory[i-1]);
|
|
1908
|
+
}
|
|
1909
|
+
const avgInterval = intervals.reduce((a, b) => a + b) / intervals.length;
|
|
1910
|
+
musicData.bpm = Math.round(60000 / avgInterval);
|
|
1911
|
+
|
|
1912
|
+
if (audioTextEl && musicData.bpm > 0) {
|
|
1913
|
+
audioTextEl.textContent = `音響連動中 ♪ (${musicData.bpm} BPM)`;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// ビデオ要素を監視して自動接続
|
|
1921
|
+
function observeVideoElements() {
|
|
1922
|
+
const videoObserver = new MutationObserver((mutations) => {
|
|
1923
|
+
mutations.forEach((mutation) => {
|
|
1924
|
+
mutation.addedNodes.forEach((node) => {
|
|
1925
|
+
if (node.nodeName === 'VIDEO') {
|
|
1926
|
+
console.log('🎬 Video element detected, connecting to audio...');
|
|
1927
|
+
connectVideoToAudio(node);
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
});
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
videoObserver.observe(document.body, {
|
|
1934
|
+
childList: true,
|
|
1935
|
+
subtree: true
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
const existingVideos = document.querySelectorAll('video');
|
|
1939
|
+
existingVideos.forEach(video => {
|
|
1940
|
+
connectVideoToAudio(video);
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
if (audioStatusEl) {
|
|
1945
|
+
audioStatusEl.classList.add('active');
|
|
1946
|
+
}
|
|
1947
|
+
observeVideoElements();
|
|
1948
|
+
|
|
1949
|
+
// アニメーションループ
|
|
1950
|
+
function animate() {
|
|
1951
|
+
requestAnimationFrame(animate);
|
|
1952
|
+
|
|
1953
|
+
const t = Date.now() * 0.001;
|
|
1954
|
+
|
|
1955
|
+
// 🎵 高度な音響データ解析
|
|
1956
|
+
if (musicData.isPlaying) {
|
|
1957
|
+
analyzeMusicData();
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// 音響連動の強度(遊園地は楽しげな反応 + ビート反応 + 昼夜モード対応)
|
|
1961
|
+
const audioIntensity = musicData.volume * 0.9;
|
|
1962
|
+
const bassBoost = musicData.bass * 1.8;
|
|
1963
|
+
const midBoost = musicData.mid * 1.4;
|
|
1964
|
+
const trebleBoost = musicData.treble * 1.3;
|
|
1965
|
+
// 昼モード: 元気よく跳ねる、夜モード: ネオンが激しく光る
|
|
1966
|
+
const beatPulse = musicData.beatDetected ? (isNightMode ? 2.5 : 1.8) : 1.0;
|
|
1967
|
+
|
|
1968
|
+
// メリーゴーランドのクリーチャー + 音響連動 + ビート反応
|
|
1969
|
+
carouselCreatures.forEach((entry, idx) => {
|
|
1970
|
+
entry.group.position.y = entry.baseY + Math.sin(t * 1.8 * (1 + audioIntensity * 0.5) + entry.phase) * 0.7 * (1 + bassBoost * 0.4) * beatPulse;
|
|
1971
|
+
entry.group.rotation.y = Math.sin(t * 0.9 + entry.phase) * 0.4 * (1 + midBoost * 0.3);
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
// ホバリングドローン + 音響連動 + ビート反応
|
|
1975
|
+
hoverDrones.forEach(entry => {
|
|
1976
|
+
entry.pivot.rotation.y += entry.speed * (1 + audioIntensity * 0.6);
|
|
1977
|
+
if (entry.arm) {
|
|
1978
|
+
entry.arm.position.y = entry.baseY + Math.sin(t * 2.4 * (1 + trebleBoost * 0.8) + entry.phase) * entry.amplitude * (1 + audioIntensity * 0.5) * beatPulse;
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
// サービスボット + 音響連動 + ビート反応
|
|
1983
|
+
serviceBots.forEach(bot => {
|
|
1984
|
+
const speedMultiplier = 1 + audioIntensity * 0.7;
|
|
1985
|
+
const angle = t * bot.speed * speedMultiplier + bot.offset;
|
|
1986
|
+
bot.mesh.position.x = bot.center.x + Math.cos(angle) * bot.radius;
|
|
1987
|
+
bot.mesh.position.z = bot.center.z + Math.sin(angle) * bot.radius;
|
|
1988
|
+
bot.mesh.position.y = bot.baseY + 0.25 * Math.sin(t * 3 + bot.offset) * (1 + bassBoost * 0.5) * beatPulse;
|
|
1989
|
+
bot.mesh.rotation.y = -angle + Math.PI / 2;
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
// ネオンエフェクト + 音響連動 + ビート反応(夜モードで強調)
|
|
1993
|
+
neonElements.forEach(entry => {
|
|
1994
|
+
const neonAudioBoost = isNightMode ? (1 + audioIntensity * 1.2) * beatPulse : (1 + audioIntensity * 0.3);
|
|
1995
|
+
if (entry.type === 'rotateY') {
|
|
1996
|
+
entry.mesh.rotation.y += entry.speed * neonAudioBoost;
|
|
1997
|
+
} else if (entry.type === 'rotateZ') {
|
|
1998
|
+
entry.mesh.rotation.z += entry.speed * neonAudioBoost;
|
|
1999
|
+
} else if (entry.type === 'pulse') {
|
|
2000
|
+
const pulse = entry.baseOpacity * (0.5 * neonModeBoost + 0.4 * Math.sin(t * entry.speed * (1 + trebleBoost) + entry.phase) * neonModeBoost * neonAudioBoost);
|
|
2001
|
+
entry.mesh.material.opacity = pulse;
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
// パーティクルエミッター + 音響連動 + ビート反応(夜モードでキラキラ)
|
|
2006
|
+
particleEmitters.forEach(emitter => {
|
|
2007
|
+
const particleAudioBoost = isNightMode ? (1 + trebleBoost * 1.5) * beatPulse : (1 + audioIntensity * 0.4);
|
|
2008
|
+
emitter.points.rotation.y += 0.0006 * particleAudioBoost;
|
|
2009
|
+
emitter.points.position.y = emitter.baseY + 0.25 * Math.sin(t * 1.6 * (1 + midBoost * 0.6) + emitter.start * 0.001) * beatPulse;
|
|
2010
|
+
if (emitter.nightOnly) {
|
|
2011
|
+
emitter.points.visible = isNightMode;
|
|
2012
|
+
}
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
// ジェットコースター + 音響連動 + ビート反応
|
|
2016
|
+
trainMovers.forEach(mover => {
|
|
2017
|
+
const trainSpeedMultiplier = 1 + audioIntensity * 0.8;
|
|
2018
|
+
mover.offset = (mover.offset + mover.speed * trainSpeedMultiplier) % 1;
|
|
2019
|
+
const normalizedSpacing = mover.spacing / mover.length;
|
|
2020
|
+
mover.cars.forEach((car, idx) => {
|
|
2021
|
+
let tCar = mover.offset - normalizedSpacing * idx;
|
|
2022
|
+
tCar = ((tCar % 1) + 1) % 1;
|
|
2023
|
+
const point = mover.curve.getPointAt(tCar);
|
|
2024
|
+
const tangent = mover.curve.getTangentAt(tCar);
|
|
2025
|
+
car.position.copy(point);
|
|
2026
|
+
const target = point.clone().add(tangent);
|
|
2027
|
+
car.lookAt(target);
|
|
2028
|
+
car.rotateY(Math.PI / 2);
|
|
2029
|
+
car.position.y += 0.2 * Math.sin(t * 2 + idx);
|
|
2030
|
+
});
|
|
2031
|
+
});
|
|
2032
|
+
|
|
2033
|
+
// おもちゃたちの微細なアニメーション + 音響連動 + ビート反応
|
|
2034
|
+
toyCity.children.forEach((child, index) => {
|
|
2035
|
+
if (child !== merryGoRound && !child.userData?.skipWiggle) {
|
|
2036
|
+
child.rotation.y = Math.sin(Date.now() * 0.001 * (1 + audioIntensity * 0.4) + index) * 0.02 * (1 + midBoost * 0.5);
|
|
2037
|
+
child.position.y = Math.sin(Date.now() * 0.002 + index) * 0.2 * (1 + bassBoost * 0.4) * beatPulse;
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
renderer.render(scene, camera);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
animate();
|
|
2045
|
+
|
|
2046
|
+
console.log('🧸 AI遊園地が完成しました!');
|
|
2047
|
+
|
|
2048
|
+
// ★ VR/ARボタンを統合メニューと連携
|
|
2049
|
+
if (window.unifiedMenuVRButton) {
|
|
2050
|
+
// VRセッション開始関数を作成
|
|
2051
|
+
function startVRSession() {
|
|
2052
|
+
if ('xr' in navigator) {
|
|
2053
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
2054
|
+
if (supported) {
|
|
2055
|
+
const sessionInit = { optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'layers'] };
|
|
2056
|
+
navigator.xr.requestSession('immersive-vr', sessionInit).then(session => {
|
|
2057
|
+
session.addEventListener('end', () => {
|
|
2058
|
+
renderer.xr.setSession(null);
|
|
2059
|
+
});
|
|
2060
|
+
renderer.xr.setSession(session);
|
|
2061
|
+
});
|
|
2062
|
+
} else {
|
|
2063
|
+
console.log('VR not supported on this device');
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
window.unifiedMenuVRButton.onclick = startVRSession;
|
|
2070
|
+
|
|
2071
|
+
// サポートチェック - 未サポートの場合はボタンを非表示に
|
|
2072
|
+
if ('xr' in navigator) {
|
|
2073
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
2074
|
+
if (!supported) {
|
|
2075
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
} else {
|
|
2079
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
if (window.unifiedMenuARButton) {
|
|
2084
|
+
// ARセッション開始関数を作成
|
|
2085
|
+
function startARSession() {
|
|
2086
|
+
if ('xr' in navigator) {
|
|
2087
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
2088
|
+
if (supported) {
|
|
2089
|
+
const sessionInit = {
|
|
2090
|
+
requiredFeatures: ['hit-test'],
|
|
2091
|
+
optionalFeatures: ['dom-overlay'],
|
|
2092
|
+
domOverlay: { root: document.body }
|
|
2093
|
+
};
|
|
2094
|
+
navigator.xr.requestSession('immersive-ar', sessionInit).then(session => {
|
|
2095
|
+
session.addEventListener('end', () => {
|
|
2096
|
+
renderer.xr.setSession(null);
|
|
2097
|
+
});
|
|
2098
|
+
renderer.xr.setSession(session);
|
|
2099
|
+
});
|
|
2100
|
+
} else {
|
|
2101
|
+
console.log('AR not supported on this device');
|
|
2102
|
+
}
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
window.unifiedMenuARButton.onclick = startARSession;
|
|
2108
|
+
|
|
2109
|
+
// サポートチェック - 未サポートの場合はボタンを非表示に
|
|
2110
|
+
if ('xr' in navigator) {
|
|
2111
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
2112
|
+
if (!supported) {
|
|
2113
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
} else {
|
|
2117
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
const context = window.toyCityContext;
|
|
2122
|
+
if (context && context.scene && context.camera && context.renderer) {
|
|
2123
|
+
ChocoDrop.createChocoDrop(context.scene, {
|
|
2124
|
+
camera: context.camera,
|
|
2125
|
+
renderer: context.renderer,
|
|
2126
|
+
serverUrl: null,
|
|
2127
|
+
enableServerHealthCheck: false,
|
|
2128
|
+
skipServiceDialog: true,
|
|
2129
|
+
sceneOptions: {
|
|
2130
|
+
enableMouseInteraction: true
|
|
2131
|
+
},
|
|
2132
|
+
onControlsToggle: (disabled) => {
|
|
2133
|
+
cameraControlsEnabled = !disabled;
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
})();
|
|
2139
|
+
</script>
|
|
2140
|
+
|
|
2141
|
+
<!-- VR/AR Button inline implementation -->
|
|
2142
|
+
<script>
|
|
2143
|
+
// VRButton implementation
|
|
2144
|
+
const VRButton = {
|
|
2145
|
+
createButton: function(renderer) {
|
|
2146
|
+
const button = document.createElement('button');
|
|
2147
|
+
|
|
2148
|
+
function showEnterVR() {
|
|
2149
|
+
button.style.display = '';
|
|
2150
|
+
button.style.cursor = 'pointer';
|
|
2151
|
+
button.style.left = 'calc(50% - 50px)';
|
|
2152
|
+
button.style.width = '100px';
|
|
2153
|
+
button.textContent = 'ENTER VR';
|
|
2154
|
+
button.onmouseenter = function() { button.style.opacity = '1.0'; };
|
|
2155
|
+
button.onmouseleave = function() { button.style.opacity = '0.5'; };
|
|
2156
|
+
button.onclick = function() {
|
|
2157
|
+
const session = renderer.xr.getSession();
|
|
2158
|
+
if (session) {
|
|
2159
|
+
session.end();
|
|
2160
|
+
} else {
|
|
2161
|
+
navigator.xr.requestSession('immersive-vr', {
|
|
2162
|
+
optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking']
|
|
2163
|
+
}).then(session => {
|
|
2164
|
+
renderer.xr.setSession(session);
|
|
2165
|
+
button.textContent = 'EXIT VR';
|
|
2166
|
+
|
|
2167
|
+
session.addEventListener('end', () => {
|
|
2168
|
+
button.textContent = 'ENTER VR';
|
|
2169
|
+
});
|
|
2170
|
+
}).catch(err => {
|
|
2171
|
+
console.error('VR session failed:', err);
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
function showWebXRNotFound() {
|
|
2178
|
+
button.style.display = '';
|
|
2179
|
+
button.style.cursor = 'auto';
|
|
2180
|
+
button.style.left = 'calc(50% - 75px)';
|
|
2181
|
+
button.style.width = '150px';
|
|
2182
|
+
button.textContent = 'VR NOT SUPPORTED';
|
|
2183
|
+
button.onmouseenter = null;
|
|
2184
|
+
button.onmouseleave = null;
|
|
2185
|
+
button.onclick = null;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
button.style.position = 'absolute';
|
|
2189
|
+
button.style.bottom = '20px';
|
|
2190
|
+
button.style.padding = '12px 6px';
|
|
2191
|
+
button.style.border = '1px solid #fff';
|
|
2192
|
+
button.style.borderRadius = '4px';
|
|
2193
|
+
button.style.background = 'rgba(0,0,0,0.1)';
|
|
2194
|
+
button.style.color = '#fff';
|
|
2195
|
+
button.style.font = 'normal 13px sans-serif';
|
|
2196
|
+
button.style.textAlign = 'center';
|
|
2197
|
+
button.style.opacity = '0.5';
|
|
2198
|
+
button.style.outline = 'none';
|
|
2199
|
+
button.style.zIndex = '999';
|
|
2200
|
+
|
|
2201
|
+
if ('xr' in navigator) {
|
|
2202
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
2203
|
+
supported ? showEnterVR() : showWebXRNotFound();
|
|
2204
|
+
});
|
|
2205
|
+
} else {
|
|
2206
|
+
showWebXRNotFound();
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
return button;
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
|
|
2213
|
+
// ARButton implementation
|
|
2214
|
+
const ARButton = {
|
|
2215
|
+
createButton: function(renderer, options) {
|
|
2216
|
+
const button = document.createElement('button');
|
|
2217
|
+
|
|
2218
|
+
function showEnterAR() {
|
|
2219
|
+
button.style.display = '';
|
|
2220
|
+
button.style.cursor = 'pointer';
|
|
2221
|
+
button.style.left = 'calc(50% - 50px)';
|
|
2222
|
+
button.style.width = '100px';
|
|
2223
|
+
button.textContent = 'ENTER AR';
|
|
2224
|
+
button.onmouseenter = function() { button.style.opacity = '1.0'; };
|
|
2225
|
+
button.onmouseleave = function() { button.style.opacity = '0.5'; };
|
|
2226
|
+
button.onclick = function() {
|
|
2227
|
+
const session = renderer.xr.getSession();
|
|
2228
|
+
if (session) {
|
|
2229
|
+
session.end();
|
|
2230
|
+
} else {
|
|
2231
|
+
navigator.xr.requestSession('immersive-ar', {
|
|
2232
|
+
optionalFeatures: ['hit-test', 'dom-overlay', 'local-floor'],
|
|
2233
|
+
domOverlay: options?.domOverlay || { root: document.body }
|
|
2234
|
+
}).then(session => {
|
|
2235
|
+
renderer.xr.setSession(session);
|
|
2236
|
+
button.textContent = 'EXIT AR';
|
|
2237
|
+
|
|
2238
|
+
session.addEventListener('end', () => {
|
|
2239
|
+
button.textContent = 'ENTER AR';
|
|
2240
|
+
});
|
|
2241
|
+
}).catch(err => {
|
|
2242
|
+
console.error('AR session failed:', err);
|
|
2243
|
+
alert('AR not supported: ' + err.message);
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
function showWebXRNotFound() {
|
|
2250
|
+
button.style.display = '';
|
|
2251
|
+
button.style.cursor = 'auto';
|
|
2252
|
+
button.style.left = 'calc(50% - 75px)';
|
|
2253
|
+
button.style.width = '150px';
|
|
2254
|
+
button.textContent = 'AR NOT SUPPORTED';
|
|
2255
|
+
button.onmouseenter = null;
|
|
2256
|
+
button.onmouseleave = null;
|
|
2257
|
+
button.onclick = null;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
button.style.position = 'absolute';
|
|
2261
|
+
button.style.bottom = '70px';
|
|
2262
|
+
button.style.padding = '12px 6px';
|
|
2263
|
+
button.style.border = '1px solid #fff';
|
|
2264
|
+
button.style.borderRadius = '4px';
|
|
2265
|
+
button.style.background = 'rgba(0,0,0,0.1)';
|
|
2266
|
+
button.style.color = '#fff';
|
|
2267
|
+
button.style.font = 'normal 13px sans-serif';
|
|
2268
|
+
button.style.textAlign = 'center';
|
|
2269
|
+
button.style.opacity = '0.5';
|
|
2270
|
+
button.style.outline = 'none';
|
|
2271
|
+
button.style.zIndex = '999';
|
|
2272
|
+
|
|
2273
|
+
if ('xr' in navigator) {
|
|
2274
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
2275
|
+
supported ? showEnterAR() : showWebXRNotFound();
|
|
2276
|
+
});
|
|
2277
|
+
} else {
|
|
2278
|
+
showWebXRNotFound();
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
return button;
|
|
2282
|
+
}
|
|
2283
|
+
};
|
|
2284
|
+
|
|
2285
|
+
console.log('✅ VRButton/ARButton inline implementation loaded');
|
|
2286
|
+
</script>
|
|
2287
|
+
</body>
|
|
2288
|
+
</html>
|