@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,1730 @@
|
|
|
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 - Basic Three.js Example</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect x='8' y='8' width='48' height='48' rx='12' fill='%2363410f'/%3E%3Crect x='18' y='18' width='28' height='28' rx='6' fill='%23a16207'/%3E%3C/svg%3E">
|
|
8
|
+
<script type="importmap">
|
|
9
|
+
{
|
|
10
|
+
"imports": {
|
|
11
|
+
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
|
|
12
|
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
<style>
|
|
17
|
+
body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding: 0;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
font-family: "Helvetica Neue", Arial, "Hiragino Sans", "Hiragino Kaku Gothic ProN", "BIZ UDPGothic", "Noto Sans JP", "Yu Gothic", Meiryo, sans-serif;
|
|
22
|
+
background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 40%, #16213e 100%);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#info {
|
|
26
|
+
position: fixed;
|
|
27
|
+
top: 18px;
|
|
28
|
+
left: 18px;
|
|
29
|
+
color: #1b2337;
|
|
30
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.88), rgba(236, 233, 255, 0.82));
|
|
31
|
+
backdrop-filter: blur(18px);
|
|
32
|
+
-webkit-backdrop-filter: blur(18px);
|
|
33
|
+
border: 1px solid rgba(180, 198, 255, 0.45);
|
|
34
|
+
padding: 18px 22px;
|
|
35
|
+
border-radius: 18px;
|
|
36
|
+
font-size: 13px;
|
|
37
|
+
font-weight: 500;
|
|
38
|
+
z-index: 2000;
|
|
39
|
+
max-width: 320px;
|
|
40
|
+
line-height: 1.6;
|
|
41
|
+
box-shadow: 0 18px 38px rgba(15, 23, 42, 0.25);
|
|
42
|
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#info.hidden {
|
|
46
|
+
opacity: 0;
|
|
47
|
+
transform: translateY(-10px);
|
|
48
|
+
pointer-events: none;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.close-btn {
|
|
52
|
+
position: absolute;
|
|
53
|
+
top: 10px;
|
|
54
|
+
right: 10px;
|
|
55
|
+
background: rgba(99, 102, 241, 0.12);
|
|
56
|
+
border: 1px solid rgba(99, 102, 241, 0.2);
|
|
57
|
+
font-size: 14px;
|
|
58
|
+
color: #4338ca;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
width: 26px;
|
|
61
|
+
height: 26px;
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: center;
|
|
65
|
+
border-radius: 50%;
|
|
66
|
+
transition: all 0.2s ease;
|
|
67
|
+
backdrop-filter: blur(6px);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.close-btn:hover {
|
|
71
|
+
background: rgba(99, 102, 241, 0.22);
|
|
72
|
+
border-color: rgba(129, 140, 248, 0.45);
|
|
73
|
+
transform: scale(1.05);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#info .info-header strong {
|
|
77
|
+
display: block;
|
|
78
|
+
font-size: 16px;
|
|
79
|
+
font-weight: 700;
|
|
80
|
+
letter-spacing: 0.03em;
|
|
81
|
+
margin-bottom: 6px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#info .info-header .subtitle {
|
|
85
|
+
margin: 0 0 16px;
|
|
86
|
+
font-size: 12px;
|
|
87
|
+
font-weight: 500;
|
|
88
|
+
color: rgba(30, 64, 175, 0.75);
|
|
89
|
+
line-height: 1.7;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#info .info-list {
|
|
93
|
+
list-style: none;
|
|
94
|
+
padding: 0;
|
|
95
|
+
margin: 0 0 16px;
|
|
96
|
+
display: grid;
|
|
97
|
+
gap: 10px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#info .info-list li {
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: flex-start;
|
|
103
|
+
gap: 10px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#info .info-icon {
|
|
107
|
+
font-size: 18px;
|
|
108
|
+
line-height: 1;
|
|
109
|
+
filter: drop-shadow(0 4px 10px rgba(99, 102, 241, 0.3));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#info code {
|
|
113
|
+
background: rgba(99, 102, 241, 0.15);
|
|
114
|
+
color: #4338ca;
|
|
115
|
+
padding: 2px 6px;
|
|
116
|
+
border-radius: 6px;
|
|
117
|
+
font-family: 'SFMono-Regular', ui-monospace, SFMono, menlo, monospace;
|
|
118
|
+
font-size: 11px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#info .info-cta {
|
|
122
|
+
display: flex;
|
|
123
|
+
gap: 10px;
|
|
124
|
+
align-items: center;
|
|
125
|
+
flex-wrap: wrap;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#info .cta-primary,
|
|
129
|
+
#info .cta-secondary {
|
|
130
|
+
border-radius: 999px;
|
|
131
|
+
padding: 10px 18px;
|
|
132
|
+
font-size: 12px;
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
border: none;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
transition: all 0.2s ease;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#info .cta-primary {
|
|
140
|
+
background: linear-gradient(135deg, #a855f7 0%, #ec4899 100%);
|
|
141
|
+
color: #fff;
|
|
142
|
+
box-shadow: 0 12px 24px rgba(168, 85, 247, 0.28);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#info .cta-primary:hover {
|
|
146
|
+
box-shadow: 0 18px 32px rgba(236, 72, 153, 0.32);
|
|
147
|
+
transform: translateY(-1px);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#info .cta-secondary {
|
|
151
|
+
background: rgba(99, 102, 241, 0.1);
|
|
152
|
+
color: #3730a3;
|
|
153
|
+
border: 1px solid rgba(99, 102, 241, 0.2);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#info .cta-secondary:hover {
|
|
157
|
+
background: rgba(99, 102, 241, 0.18);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.navigation {
|
|
161
|
+
position: fixed;
|
|
162
|
+
top: 16px;
|
|
163
|
+
right: 16px;
|
|
164
|
+
z-index: 100;
|
|
165
|
+
display: flex;
|
|
166
|
+
gap: 8px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.nav-link {
|
|
170
|
+
background: rgba(255, 255, 255, 0.9);
|
|
171
|
+
backdrop-filter: blur(10px);
|
|
172
|
+
-webkit-backdrop-filter: blur(10px);
|
|
173
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
174
|
+
color: #2d3748;
|
|
175
|
+
text-decoration: none;
|
|
176
|
+
padding: 8px 12px;
|
|
177
|
+
border-radius: 6px;
|
|
178
|
+
font-size: 12px;
|
|
179
|
+
transition: all 0.2s ease;
|
|
180
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.nav-link:hover {
|
|
184
|
+
background: rgba(255, 255, 255, 1);
|
|
185
|
+
transform: translateY(-1px);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#info strong {
|
|
189
|
+
font-size: 14px;
|
|
190
|
+
font-weight: 600;
|
|
191
|
+
color: #1a202c;
|
|
192
|
+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #ec4899 100%);
|
|
193
|
+
-webkit-background-clip: text;
|
|
194
|
+
-webkit-text-fill-color: transparent;
|
|
195
|
+
background-clip: text;
|
|
196
|
+
display: block;
|
|
197
|
+
margin-bottom: 10px;
|
|
198
|
+
text-align: center;
|
|
199
|
+
letter-spacing: 0.5px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.controls {
|
|
203
|
+
margin-top: 8px;
|
|
204
|
+
padding-top: 8px;
|
|
205
|
+
border-top: 1px solid rgba(0, 0, 0, 0.15);
|
|
206
|
+
font-size: 11px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.control-item {
|
|
210
|
+
margin: 4px 0;
|
|
211
|
+
display: flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
gap: 8px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.key {
|
|
217
|
+
background: rgba(0, 0, 0, 0.1);
|
|
218
|
+
padding: 2px 6px;
|
|
219
|
+
border-radius: 3px;
|
|
220
|
+
font-weight: 500;
|
|
221
|
+
min-width: fit-content;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#info code {
|
|
225
|
+
background: rgba(139, 92, 246, 0.15);
|
|
226
|
+
color: #4a5568;
|
|
227
|
+
padding: 1px 4px;
|
|
228
|
+
border-radius: 3px;
|
|
229
|
+
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
|
230
|
+
font-size: 11px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#info .emoji {
|
|
234
|
+
font-size: 14px;
|
|
235
|
+
margin: 0 1px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* チョコアイコンのキラキラアニメーション */
|
|
239
|
+
.choco-icon {
|
|
240
|
+
display: inline-block;
|
|
241
|
+
animation: choco-sparkle 2s ease-in-out infinite;
|
|
242
|
+
cursor: pointer;
|
|
243
|
+
transition: transform 0.2s ease;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.choco-icon:hover {
|
|
247
|
+
transform: scale(1.2);
|
|
248
|
+
animation: choco-sparkle-fast 0.8s ease-in-out infinite;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* タイトル用の控えめなチョコアニメーション */
|
|
252
|
+
.choco-title {
|
|
253
|
+
display: inline-block;
|
|
254
|
+
animation: choco-title-glow 3s ease-in-out infinite;
|
|
255
|
+
-webkit-text-fill-color: initial !important;
|
|
256
|
+
color: currentColor !important;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@keyframes choco-title-glow {
|
|
260
|
+
0%, 100% {
|
|
261
|
+
opacity: 1;
|
|
262
|
+
filter: brightness(1);
|
|
263
|
+
}
|
|
264
|
+
50% {
|
|
265
|
+
opacity: 1;
|
|
266
|
+
filter: brightness(1.1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@keyframes choco-sparkle {
|
|
271
|
+
0%, 100% {
|
|
272
|
+
filter: brightness(1) drop-shadow(0 0 2px rgba(139, 92, 246, 0.3));
|
|
273
|
+
transform: scale(1) rotate(0deg);
|
|
274
|
+
}
|
|
275
|
+
25% {
|
|
276
|
+
filter: brightness(1.4) drop-shadow(0 0 12px rgba(139, 92, 246, 0.9));
|
|
277
|
+
transform: scale(1.08) rotate(2deg);
|
|
278
|
+
}
|
|
279
|
+
50% {
|
|
280
|
+
filter: brightness(1.6) drop-shadow(0 0 16px rgba(139, 92, 246, 1)) drop-shadow(0 0 8px rgba(255, 255, 255, 0.8));
|
|
281
|
+
transform: scale(1.12) rotate(0deg);
|
|
282
|
+
}
|
|
283
|
+
75% {
|
|
284
|
+
filter: brightness(1.4) drop-shadow(0 0 12px rgba(139, 92, 246, 0.9));
|
|
285
|
+
transform: scale(1.08) rotate(-2deg);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@keyframes choco-sparkle-fast {
|
|
290
|
+
0%, 100% {
|
|
291
|
+
filter: brightness(1.3) drop-shadow(0 0 8px rgba(139, 92, 246, 0.8)) drop-shadow(0 0 4px rgba(255, 255, 255, 0.6));
|
|
292
|
+
transform: rotate(0deg);
|
|
293
|
+
}
|
|
294
|
+
33% {
|
|
295
|
+
filter: brightness(1.8) drop-shadow(0 0 20px rgba(139, 92, 246, 1)) drop-shadow(0 0 12px rgba(255, 255, 255, 1));
|
|
296
|
+
transform: rotate(5deg);
|
|
297
|
+
}
|
|
298
|
+
66% {
|
|
299
|
+
filter: brightness(1.8) drop-shadow(0 0 20px rgba(139, 92, 246, 1)) drop-shadow(0 0 12px rgba(255, 255, 255, 1));
|
|
300
|
+
transform: rotate(-5deg);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#info .highlight {
|
|
305
|
+
color: #6b46c1;
|
|
306
|
+
font-weight: 500;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#info .subtitle {
|
|
310
|
+
color: #4a5568;
|
|
311
|
+
font-size: 11px;
|
|
312
|
+
margin-top: 6px;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
canvas {
|
|
316
|
+
display: block;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.demo-links {
|
|
320
|
+
position: fixed;
|
|
321
|
+
bottom: 20px;
|
|
322
|
+
right: 100px;
|
|
323
|
+
display: flex;
|
|
324
|
+
gap: 12px;
|
|
325
|
+
z-index: 100;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.demo-link {
|
|
329
|
+
background: linear-gradient(135deg, rgba(139, 92, 246, 0.9) 0%, rgba(236, 72, 153, 0.9) 100%);
|
|
330
|
+
color: white;
|
|
331
|
+
text-decoration: none;
|
|
332
|
+
padding: 10px 14px;
|
|
333
|
+
border-radius: 8px;
|
|
334
|
+
font-size: 11px;
|
|
335
|
+
font-weight: 600;
|
|
336
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
337
|
+
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
|
|
338
|
+
transition: all 0.3s ease;
|
|
339
|
+
backdrop-filter: blur(10px);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.demo-link:hover {
|
|
343
|
+
transform: translateY(-2px);
|
|
344
|
+
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.5);
|
|
345
|
+
border-color: rgba(255, 255, 255, 0.6);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* 統合メニュー */
|
|
349
|
+
.unified-menu {
|
|
350
|
+
position: fixed;
|
|
351
|
+
top: 16px;
|
|
352
|
+
right: 20px;
|
|
353
|
+
z-index: 1000;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.menu-toggle {
|
|
357
|
+
background: rgba(99, 102, 241, 0.15);
|
|
358
|
+
backdrop-filter: blur(16px);
|
|
359
|
+
-webkit-backdrop-filter: blur(16px);
|
|
360
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
361
|
+
color: #2d3748;
|
|
362
|
+
padding: 10px 14px;
|
|
363
|
+
border-radius: 8px;
|
|
364
|
+
font-size: 18px;
|
|
365
|
+
cursor: pointer;
|
|
366
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
367
|
+
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2), 0 2px 6px rgba(0, 0, 0, 0.05);
|
|
368
|
+
width: 44px;
|
|
369
|
+
height: 44px;
|
|
370
|
+
display: flex;
|
|
371
|
+
align-items: center;
|
|
372
|
+
justify-content: center;
|
|
373
|
+
opacity: 0.8;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.menu-toggle:hover {
|
|
377
|
+
background: rgba(99, 102, 241, 0.2);
|
|
378
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
379
|
+
transform: scale(1.1) translateY(-2px);
|
|
380
|
+
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.3), 0 3px 8px rgba(0, 0, 0, 0.1);
|
|
381
|
+
opacity: 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.menu-dropdown {
|
|
385
|
+
position: absolute;
|
|
386
|
+
top: 56px;
|
|
387
|
+
right: 0;
|
|
388
|
+
min-width: 220px;
|
|
389
|
+
background: rgba(255, 255, 255, 0.85);
|
|
390
|
+
backdrop-filter: blur(16px) saturate(180%);
|
|
391
|
+
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
392
|
+
border: 1px solid rgba(99, 102, 241, 0.2);
|
|
393
|
+
border-radius: 12px;
|
|
394
|
+
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
395
|
+
opacity: 0;
|
|
396
|
+
visibility: hidden;
|
|
397
|
+
transform: translateY(-10px);
|
|
398
|
+
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
399
|
+
overflow: hidden;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.menu-dropdown.active {
|
|
403
|
+
opacity: 1;
|
|
404
|
+
visibility: visible;
|
|
405
|
+
transform: translateY(0);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.menu-item {
|
|
409
|
+
display: block;
|
|
410
|
+
padding: 12px 16px;
|
|
411
|
+
color: #2d3748;
|
|
412
|
+
text-decoration: none;
|
|
413
|
+
font-size: 13px;
|
|
414
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
415
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
416
|
+
cursor: pointer;
|
|
417
|
+
background: none;
|
|
418
|
+
border-left: none;
|
|
419
|
+
border-right: none;
|
|
420
|
+
border-top: none;
|
|
421
|
+
width: 100%;
|
|
422
|
+
text-align: left;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.menu-item:last-child {
|
|
426
|
+
border-bottom: none;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.menu-item:hover {
|
|
430
|
+
background: rgba(99, 102, 241, 0.08);
|
|
431
|
+
padding-left: 20px;
|
|
432
|
+
border-left: 2px solid rgba(99, 102, 241, 0.4);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.menu-item.has-submenu {
|
|
436
|
+
position: relative;
|
|
437
|
+
padding: 0;
|
|
438
|
+
border: none;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.submenu-toggle {
|
|
442
|
+
display: block;
|
|
443
|
+
padding: 12px 16px;
|
|
444
|
+
color: #2d3748;
|
|
445
|
+
font-size: 13px;
|
|
446
|
+
background: none;
|
|
447
|
+
border: none;
|
|
448
|
+
width: 100%;
|
|
449
|
+
text-align: left;
|
|
450
|
+
cursor: pointer;
|
|
451
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
452
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.submenu-toggle:hover {
|
|
456
|
+
background: rgba(99, 102, 241, 0.08);
|
|
457
|
+
padding-left: 20px;
|
|
458
|
+
border-left: 2px solid rgba(99, 102, 241, 0.4);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.submenu {
|
|
462
|
+
display: none;
|
|
463
|
+
background: rgba(99, 102, 241, 0.03);
|
|
464
|
+
border-top: 1px solid rgba(99, 102, 241, 0.1);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.submenu.active {
|
|
468
|
+
display: block;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.submenu .menu-item {
|
|
472
|
+
padding-left: 32px;
|
|
473
|
+
font-size: 12px;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.submenu .menu-item:hover {
|
|
477
|
+
padding-left: 36px;
|
|
478
|
+
background: rgba(99, 102, 241, 0.12);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/* 🎵 音響ステータス表示 */
|
|
482
|
+
#audio-status {
|
|
483
|
+
position: fixed;
|
|
484
|
+
top: 70px;
|
|
485
|
+
right: 20px;
|
|
486
|
+
z-index: 100;
|
|
487
|
+
background: rgba(255, 255, 255, 0.85);
|
|
488
|
+
backdrop-filter: blur(10px);
|
|
489
|
+
-webkit-backdrop-filter: blur(10px);
|
|
490
|
+
padding: 10px 15px;
|
|
491
|
+
border-radius: 20px;
|
|
492
|
+
font-size: 12px;
|
|
493
|
+
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
494
|
+
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.2);
|
|
495
|
+
color: #2d3748;
|
|
496
|
+
display: none;
|
|
497
|
+
transition: opacity 0.3s ease;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
#audio-status.active {
|
|
501
|
+
display: block;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
#audio-status .status-icon {
|
|
505
|
+
display: inline-block;
|
|
506
|
+
margin-right: 5px;
|
|
507
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
@keyframes pulse {
|
|
511
|
+
0%, 100% { opacity: 0.8; transform: scale(1); }
|
|
512
|
+
50% { opacity: 1; transform: scale(1.1); }
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/* メニューアイコンのバウンスアニメーション */
|
|
516
|
+
.menu-icon {
|
|
517
|
+
display: inline-block;
|
|
518
|
+
animation: menu-bounce 2s ease-in-out infinite;
|
|
519
|
+
cursor: pointer;
|
|
520
|
+
transition: transform 0.2s ease;
|
|
521
|
+
text-shadow: 0 0 10px currentColor;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.menu-icon:hover {
|
|
525
|
+
transform: scale(1.4);
|
|
526
|
+
animation: menu-bounce-fast 0.8s ease-in-out infinite;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
@keyframes menu-bounce {
|
|
530
|
+
0%, 100% {
|
|
531
|
+
filter: brightness(1) drop-shadow(0 0 8px currentColor);
|
|
532
|
+
transform: scale(1) translateY(0px);
|
|
533
|
+
}
|
|
534
|
+
25% {
|
|
535
|
+
filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
|
|
536
|
+
transform: scale(1.1) translateY(-4px);
|
|
537
|
+
}
|
|
538
|
+
50% {
|
|
539
|
+
filter: brightness(1.5) drop-shadow(0 0 20px currentColor);
|
|
540
|
+
transform: scale(1.15) translateY(-8px);
|
|
541
|
+
}
|
|
542
|
+
75% {
|
|
543
|
+
filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
|
|
544
|
+
transform: scale(1.1) translateY(-4px);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
@keyframes menu-bounce-fast {
|
|
549
|
+
0%, 100% {
|
|
550
|
+
filter: brightness(1.4) drop-shadow(0 0 15px currentColor);
|
|
551
|
+
transform: translateY(0px);
|
|
552
|
+
}
|
|
553
|
+
50% {
|
|
554
|
+
filter: brightness(1.8) drop-shadow(0 0 25px currentColor);
|
|
555
|
+
transform: translateY(-12px);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
</style>
|
|
559
|
+
</head>
|
|
560
|
+
<body>
|
|
561
|
+
<!-- 🎵 音響ステータス表示 -->
|
|
562
|
+
<div id="audio-status">
|
|
563
|
+
<span class="status-icon">🎤</span><span id="audio-text">音声待機中...</span>
|
|
564
|
+
</div>
|
|
565
|
+
<!-- Navigation Links (hidden - now in unified menu) -->
|
|
566
|
+
<div class="navigation" style="display: none;">
|
|
567
|
+
<a href="../../index.html" class="nav-link">🏠 Home</a>
|
|
568
|
+
<a href="https://github.com/nyukicorn/chocodrop" class="nav-link" target="_blank">📦 GitHub</a>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<!-- Main Info Panel -->
|
|
572
|
+
<div id="info">
|
|
573
|
+
<button class="close-btn" type="button" aria-label="ヘルプを閉じる" onclick="hideInfoPanel()">×</button>
|
|
574
|
+
<div class="info-header">
|
|
575
|
+
<strong><span class="emoji choco-title">🍫</span>はじまりの国へようこそ</strong>
|
|
576
|
+
<p class="subtitle">手元の画像・動画・3Dモデルを配置できます。アカウント・MCP・生成サービスの設定は不要です。</p>
|
|
577
|
+
</div>
|
|
578
|
+
<ul class="info-list">
|
|
579
|
+
<li><span class="info-icon">🧭</span><span>右下の <code>🍫</code> をクリックしてコマンドUIを呼び出しましょう。</span></li>
|
|
580
|
+
<li><span class="info-icon">💡</span><span>「Import」でファイルを選ぶと、自動で配置します。まずは下のサンプルでも試せます。</span></li>
|
|
581
|
+
<li><span class="info-icon">🎨</span><span>素材はドラッグ&ドロップ、または「メディアインポート」モードで配置できます。</span></li>
|
|
582
|
+
<li><span class="info-icon">🎥</span><span>撮影モードでは UI を隠し、WASD でふわりと視点を滑らせましょう。</span></li>
|
|
583
|
+
</ul>
|
|
584
|
+
<div class="info-cta">
|
|
585
|
+
<button id="try-sample" class="cta-primary" disabled>サンプルを配置</button>
|
|
586
|
+
<a href="../../getting-started.html" class="cta-secondary">使い方・ツール連携</a>
|
|
587
|
+
<button id="info-open-guide" class="cta-secondary">詳しいガイド</button>
|
|
588
|
+
<button id="info-show-shortcuts" class="cta-secondary">ショートカットを表示</button>
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
<!-- 世界選択リンク (hidden - now in unified menu) -->
|
|
593
|
+
<div class="demo-links" style="display: none;">
|
|
594
|
+
<a href="../pixel-ocean/index.html" class="demo-link">🌊 海底世界</a>
|
|
595
|
+
<a href="../music-garden/index.html" class="demo-link">🌸 硝子の花</a>
|
|
596
|
+
<a href="../wabi-sabi/index.html" class="demo-link">🖤 侘び寂び</a>
|
|
597
|
+
<a href="../space/index.html" class="demo-link">🌌 宇宙空間</a>
|
|
598
|
+
<a href="../toy-city/index.html" class="demo-link">🎡 AI遊園地</a>
|
|
599
|
+
</div>
|
|
600
|
+
|
|
601
|
+
<!-- 統合メニュー -->
|
|
602
|
+
<div class="unified-menu">
|
|
603
|
+
<button class="menu-toggle" id="unifiedMenuToggle" aria-label="Menu">☰</button>
|
|
604
|
+
<div class="menu-dropdown" id="unifiedMenuDropdown">
|
|
605
|
+
<button class="menu-item" id="showInfoBtn"><span class="menu-icon">📖</span> このワールドについて</button>
|
|
606
|
+
|
|
607
|
+
<div class="menu-item has-submenu">
|
|
608
|
+
<button class="submenu-toggle" id="worldsMenuToggle"><span class="menu-icon">🌍</span> 世界選択 <span style="font-size: 10px;">▼</span></button>
|
|
609
|
+
<div class="submenu" id="worldsSubmenu">
|
|
610
|
+
<a href="index.html" class="menu-item"><span class="menu-icon">🍫</span> はじまりの国(現在地)</a>
|
|
611
|
+
<a href="../pixel-ocean/index.html" class="menu-item"><span class="menu-icon">🌊</span> 海底世界</a>
|
|
612
|
+
<a href="../music-garden/index.html" class="menu-item"><span class="menu-icon">🌸</span> 硝子の花</a>
|
|
613
|
+
<a href="../space/index.html" class="menu-item"><span class="menu-icon">🌌</span> 宇宙空間</a>
|
|
614
|
+
<a href="../toy-city/index.html" class="menu-item"><span class="menu-icon">🎡</span> AI遊園地</a>
|
|
615
|
+
<a href="../wabi-sabi/index.html" class="menu-item"><span class="menu-icon">🖤</span> 侘び寂びの世界</a>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
|
|
619
|
+
<a href="../../index.html" class="menu-item"><span class="menu-icon">🏠</span> Home</a>
|
|
620
|
+
<a href="https://github.com/nyukicorn/chocodrop" class="menu-item" target="_blank"><span class="menu-icon">🔗</span> GitHub</a>
|
|
621
|
+
|
|
622
|
+
<button class="menu-item" id="vrMenuButton"><span class="menu-icon">🥽</span> VR</button>
|
|
623
|
+
<button class="menu-item" id="arMenuButton"><span class="menu-icon">🥽</span> AR</button>
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
<p id="demo-status" role="status" aria-live="polite" style="position: fixed; bottom: 12px; left: 16px; z-index: 1500; max-width: min(380px, 80vw); padding: 10px 14px; border-radius: 12px; background: rgba(255,255,255,.94); color: #25243c; font-size: 13px; pointer-events: none;">3Dシーンを準備しています…</p>
|
|
628
|
+
|
|
629
|
+
<!-- Three.js r170 + ChocoDrop (module preload) -->
|
|
630
|
+
<script type="module">
|
|
631
|
+
// GitHub Pages対応: プロトコルとホスト名を取得し、適切なベースURLを構築
|
|
632
|
+
const isGitHubPages = window.location.hostname.includes('github.io');
|
|
633
|
+
const normalizeBase = (base) => {
|
|
634
|
+
if (!base) return '';
|
|
635
|
+
return base.replace(/\/$/, '');
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const localLoaderBase = `${window.location.protocol}//${window.location.hostname}:3015`;
|
|
639
|
+
const fallbackBase = window.CHOCODROP_PUBLIC_BASE ||
|
|
640
|
+
(isGitHubPages
|
|
641
|
+
? `${window.location.origin}${window.location.pathname.split('/examples/')[0]}/public`
|
|
642
|
+
: localLoaderBase
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
const loaderCandidates = [];
|
|
646
|
+
const addCandidate = (candidate) => {
|
|
647
|
+
if (!candidate) return;
|
|
648
|
+
const normalized = candidate.endsWith('.js')
|
|
649
|
+
? candidate
|
|
650
|
+
: `${normalizeBase(candidate)}/load-chocodrop.js`;
|
|
651
|
+
if (!loaderCandidates.includes(normalized)) {
|
|
652
|
+
loaderCandidates.push(normalized);
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// 🎯 情報パネル制御(loader取得待ちの間でも動作させる)
|
|
657
|
+
let autoHideTimer = null;
|
|
658
|
+
|
|
659
|
+
const getInfoPanel = () => document.getElementById('info');
|
|
660
|
+
|
|
661
|
+
const clearInfoPanelAutoHide = () => {
|
|
662
|
+
if (autoHideTimer) {
|
|
663
|
+
clearTimeout(autoHideTimer);
|
|
664
|
+
autoHideTimer = null;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
const scheduleInfoPanelAutoHide = () => {
|
|
669
|
+
clearInfoPanelAutoHide();
|
|
670
|
+
autoHideTimer = window.setTimeout(() => {
|
|
671
|
+
if (typeof window.hideInfoPanel === 'function') {
|
|
672
|
+
window.hideInfoPanel();
|
|
673
|
+
}
|
|
674
|
+
}, 30000);
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
window.hideInfoPanel = function hideInfoPanel() {
|
|
678
|
+
const infoPanel = getInfoPanel();
|
|
679
|
+
if (!infoPanel) {
|
|
680
|
+
clearInfoPanelAutoHide();
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
infoPanel.classList.add('hidden');
|
|
685
|
+
window.setTimeout(() => {
|
|
686
|
+
if (infoPanel.classList.contains('hidden')) {
|
|
687
|
+
infoPanel.style.display = 'none';
|
|
688
|
+
}
|
|
689
|
+
}, 300);
|
|
690
|
+
|
|
691
|
+
clearInfoPanelAutoHide();
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
window.showInfoPanel = function showInfoPanel() {
|
|
695
|
+
const infoPanel = getInfoPanel();
|
|
696
|
+
if (!infoPanel) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
infoPanel.style.display = 'block';
|
|
701
|
+
window.requestAnimationFrame(() => {
|
|
702
|
+
infoPanel.classList.remove('hidden');
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
scheduleInfoPanelAutoHide();
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const localHostnames = new Set(['localhost', '127.0.0.1', '::1']);
|
|
709
|
+
const isLanIp = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(window.location.hostname);
|
|
710
|
+
const shouldUseLocalLoader = !isGitHubPages && (localHostnames.has(window.location.hostname) || window.location.hostname.endsWith('.local') || isLanIp);
|
|
711
|
+
|
|
712
|
+
addCandidate(window.CHOCODROP_PUBLIC_BASE);
|
|
713
|
+
addCandidate(new URL('../../public/load-chocodrop.js', import.meta.url).href);
|
|
714
|
+
if (shouldUseLocalLoader) addCandidate(localLoaderBase);
|
|
715
|
+
addCandidate(fallbackBase);
|
|
716
|
+
|
|
717
|
+
let ensureChocoDrop = null;
|
|
718
|
+
for (const candidate of loaderCandidates) {
|
|
719
|
+
try {
|
|
720
|
+
const module = await import(candidate);
|
|
721
|
+
ensureChocoDrop = module?.default || module?.ensureChocoDrop || null;
|
|
722
|
+
if (ensureChocoDrop) {
|
|
723
|
+
console.log(`✅ ChocoDrop loader resolved via ${candidate}`);
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
} catch (error) {
|
|
727
|
+
console.warn(`⚠️ Failed to load ChocoDrop loader from ${candidate}`, error);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (!ensureChocoDrop) {
|
|
732
|
+
throw new Error('ChocoDrop ローダーを取得できませんでした。`load-chocodrop.js` の提供元を確認してください。');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
(async () => {
|
|
736
|
+
await ensureChocoDrop();
|
|
737
|
+
|
|
738
|
+
const THREE = window.THREE;
|
|
739
|
+
const ChocoDrop = window.ChocoDrop;
|
|
740
|
+
// 統合メニューの制御
|
|
741
|
+
(function() {
|
|
742
|
+
const menuToggle = document.getElementById('unifiedMenuToggle');
|
|
743
|
+
const menuDropdown = document.getElementById('unifiedMenuDropdown');
|
|
744
|
+
const worldsMenuToggle = document.getElementById('worldsMenuToggle');
|
|
745
|
+
const worldsSubmenu = document.getElementById('worldsSubmenu');
|
|
746
|
+
const showInfoBtn = document.getElementById('showInfoBtn');
|
|
747
|
+
const infoPanel = document.getElementById('info');
|
|
748
|
+
const guideButton = document.getElementById('info-open-guide');
|
|
749
|
+
const shortcutsButton = document.getElementById('info-show-shortcuts');
|
|
750
|
+
|
|
751
|
+
// 初回訪問チェック - localStorage使用
|
|
752
|
+
const FIRST_VISIT_KEY = 'basic_visited';
|
|
753
|
+
const hasVisited = localStorage.getItem(FIRST_VISIT_KEY);
|
|
754
|
+
|
|
755
|
+
if (!hasVisited) {
|
|
756
|
+
// 初回訪問時は情報パネルを表示
|
|
757
|
+
if (infoPanel) {
|
|
758
|
+
infoPanel.style.display = 'block';
|
|
759
|
+
}
|
|
760
|
+
localStorage.setItem(FIRST_VISIT_KEY, 'true');
|
|
761
|
+
} else {
|
|
762
|
+
// 2回目以降は非表示
|
|
763
|
+
if (infoPanel) {
|
|
764
|
+
infoPanel.style.display = 'none';
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// 統合メニューのトグル
|
|
769
|
+
if (menuToggle && menuDropdown) {
|
|
770
|
+
menuToggle.addEventListener('click', (e) => {
|
|
771
|
+
e.stopPropagation();
|
|
772
|
+
menuDropdown.classList.toggle('active');
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// 外側クリックで閉じる
|
|
776
|
+
document.addEventListener('click', (e) => {
|
|
777
|
+
if (!menuToggle.contains(e.target) && !menuDropdown.contains(e.target)) {
|
|
778
|
+
menuDropdown.classList.remove('active');
|
|
779
|
+
// サブメニューも閉じる
|
|
780
|
+
if (worldsSubmenu) {
|
|
781
|
+
worldsSubmenu.classList.remove('active');
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// Escキーで閉じる
|
|
787
|
+
document.addEventListener('keydown', (e) => {
|
|
788
|
+
if (e.key === 'Escape') {
|
|
789
|
+
menuDropdown.classList.remove('active');
|
|
790
|
+
if (worldsSubmenu) {
|
|
791
|
+
worldsSubmenu.classList.remove('active');
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// 世界選択サブメニューのトグル
|
|
798
|
+
if (worldsMenuToggle && worldsSubmenu) {
|
|
799
|
+
worldsMenuToggle.addEventListener('click', (e) => {
|
|
800
|
+
e.stopPropagation();
|
|
801
|
+
worldsSubmenu.classList.toggle('active');
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// 情報パネル表示ボタン
|
|
806
|
+
if (showInfoBtn) {
|
|
807
|
+
showInfoBtn.addEventListener('click', () => {
|
|
808
|
+
// グローバルに定義された showInfoPanel() を呼び出す
|
|
809
|
+
if (typeof showInfoPanel === 'function') {
|
|
810
|
+
showInfoPanel();
|
|
811
|
+
} else {
|
|
812
|
+
// フォールバック: 直接表示
|
|
813
|
+
const panel = document.getElementById('info');
|
|
814
|
+
if (panel) {
|
|
815
|
+
panel.style.display = 'block';
|
|
816
|
+
panel.classList.remove('hidden');
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
menuDropdown.classList.remove('active');
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (guideButton) {
|
|
824
|
+
guideButton.addEventListener('click', () => {
|
|
825
|
+
try {
|
|
826
|
+
if (window.ChocoDrop?.ui?.onboardingCoach) {
|
|
827
|
+
window.ChocoDrop.ui.onboardingCoach.start({ force: true });
|
|
828
|
+
} else {
|
|
829
|
+
window.dispatchEvent(new CustomEvent('chocodrop:request-onboarding'));
|
|
830
|
+
}
|
|
831
|
+
} finally {
|
|
832
|
+
if (typeof hideInfoPanel === 'function') {
|
|
833
|
+
hideInfoPanel();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (shortcutsButton) {
|
|
840
|
+
shortcutsButton.addEventListener('click', () => {
|
|
841
|
+
if (menuDropdown) {
|
|
842
|
+
menuDropdown.classList.add('active');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
window.dispatchEvent(new CustomEvent('chocodrop:show-shortcuts-request'));
|
|
846
|
+
if (typeof hideInfoPanel === 'function') {
|
|
847
|
+
hideInfoPanel();
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// VR/ARボタンは後でThree.jsのrendererと連携
|
|
853
|
+
// グローバル変数として保存
|
|
854
|
+
window.unifiedMenuVRButton = document.getElementById('vrMenuButton');
|
|
855
|
+
window.unifiedMenuARButton = document.getElementById('arMenuButton');
|
|
856
|
+
})();
|
|
857
|
+
|
|
858
|
+
console.log('THREE loaded:', typeof THREE !== 'undefined');
|
|
859
|
+
console.log('OrbitControls loaded:', typeof THREE.OrbitControls !== 'undefined');
|
|
860
|
+
|
|
861
|
+
// Basic Three.js setup
|
|
862
|
+
const scene = new THREE.Scene();
|
|
863
|
+
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
864
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
865
|
+
|
|
866
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
867
|
+
renderer.shadowMap.enabled = true;
|
|
868
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
869
|
+
renderer.xr.enabled = true; // ★ VR/AR有効化
|
|
870
|
+
document.body.appendChild(renderer.domElement);
|
|
871
|
+
|
|
872
|
+
// ★ VR/ARボタンを統合メニューと連携(bodyには追加しない)
|
|
873
|
+
if (typeof VRButton !== 'undefined' && window.unifiedMenuVRButton) {
|
|
874
|
+
console.log('✅ VRButton script loaded - connecting to unified menu');
|
|
875
|
+
|
|
876
|
+
// VRセッション開始関数を作成
|
|
877
|
+
function startVRSession() {
|
|
878
|
+
if ('xr' in navigator) {
|
|
879
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
880
|
+
if (supported) {
|
|
881
|
+
const sessionInit = { optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'layers'] };
|
|
882
|
+
navigator.xr.requestSession('immersive-vr', sessionInit).then(session => {
|
|
883
|
+
session.addEventListener('end', () => {
|
|
884
|
+
renderer.xr.setSession(null);
|
|
885
|
+
});
|
|
886
|
+
renderer.xr.setSession(session);
|
|
887
|
+
});
|
|
888
|
+
} else {
|
|
889
|
+
console.log('VR not supported on this device');
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
window.unifiedMenuVRButton.onclick = startVRSession;
|
|
896
|
+
|
|
897
|
+
// サポートチェック - 未サポートの場合はボタンを非表示に
|
|
898
|
+
if ('xr' in navigator) {
|
|
899
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
900
|
+
if (!supported) {
|
|
901
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
} else {
|
|
905
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
906
|
+
}
|
|
907
|
+
} else {
|
|
908
|
+
console.log('❌ VRButton script not loaded or menu button not found');
|
|
909
|
+
if (window.unifiedMenuVRButton) {
|
|
910
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (typeof ARButton !== 'undefined' && window.unifiedMenuARButton) {
|
|
915
|
+
console.log('✅ ARButton script loaded - connecting to unified menu');
|
|
916
|
+
|
|
917
|
+
// ARセッション開始関数を作成
|
|
918
|
+
function startARSession() {
|
|
919
|
+
if ('xr' in navigator) {
|
|
920
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
921
|
+
if (supported) {
|
|
922
|
+
const sessionInit = {
|
|
923
|
+
requiredFeatures: ['hit-test'],
|
|
924
|
+
optionalFeatures: ['dom-overlay'],
|
|
925
|
+
domOverlay: { root: document.body }
|
|
926
|
+
};
|
|
927
|
+
navigator.xr.requestSession('immersive-ar', sessionInit).then(session => {
|
|
928
|
+
session.addEventListener('end', () => {
|
|
929
|
+
renderer.xr.setSession(null);
|
|
930
|
+
});
|
|
931
|
+
renderer.xr.setSession(session);
|
|
932
|
+
});
|
|
933
|
+
} else {
|
|
934
|
+
console.log('AR not supported on this device');
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
window.unifiedMenuARButton.onclick = startARSession;
|
|
941
|
+
|
|
942
|
+
// サポートチェック - 未サポートの場合はボタンを非表示に
|
|
943
|
+
if ('xr' in navigator) {
|
|
944
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
945
|
+
if (!supported) {
|
|
946
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
} else {
|
|
950
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
951
|
+
}
|
|
952
|
+
} else {
|
|
953
|
+
console.log('❌ ARButton script not loaded or menu button not found');
|
|
954
|
+
if (window.unifiedMenuARButton) {
|
|
955
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// 🌈 夢の国の環境設定 - ミルキーホワイト背景
|
|
960
|
+
scene.background = new THREE.Color(0xfaf8ff); // ミルキーホワイト
|
|
961
|
+
scene.fog = new THREE.FogExp2(0xfaf8ff, 0.002);
|
|
962
|
+
|
|
963
|
+
// ✨ ミルキーな夢ライティング
|
|
964
|
+
const ambientLight = new THREE.AmbientLight(0xfff8ff, 0.8);
|
|
965
|
+
scene.add(ambientLight);
|
|
966
|
+
|
|
967
|
+
// ソフトライト1
|
|
968
|
+
const directionalLight1 = new THREE.DirectionalLight(0xffe6ff, 0.6);
|
|
969
|
+
directionalLight1.position.set(30, 40, 30);
|
|
970
|
+
directionalLight1.castShadow = true;
|
|
971
|
+
directionalLight1.shadow.mapSize.width = 2048;
|
|
972
|
+
directionalLight1.shadow.mapSize.height = 2048;
|
|
973
|
+
scene.add(directionalLight1);
|
|
974
|
+
|
|
975
|
+
// ソフトライト2
|
|
976
|
+
const directionalLight2 = new THREE.DirectionalLight(0xf0f8ff, 0.4);
|
|
977
|
+
directionalLight2.position.set(-30, 40, -30);
|
|
978
|
+
scene.add(directionalLight2);
|
|
979
|
+
|
|
980
|
+
// ミルキー環境光
|
|
981
|
+
const hemisphereLight = new THREE.HemisphereLight(0xfff8ff, 0xf8f0ff, 0.6);
|
|
982
|
+
scene.add(hemisphereLight);
|
|
983
|
+
|
|
984
|
+
// ☁️ 雲のプラットフォーム(複数レイヤー)
|
|
985
|
+
function createCloudPlatform(x, y, z, scale) {
|
|
986
|
+
const cloudGroup = new THREE.Group();
|
|
987
|
+
|
|
988
|
+
// 雲の各パーツを球体で作成
|
|
989
|
+
const cloudMaterial = new THREE.MeshPhongMaterial({
|
|
990
|
+
color: 0xffddee, // ミルキーピンク
|
|
991
|
+
transparent: true,
|
|
992
|
+
opacity: 0.8,
|
|
993
|
+
emissive: 0xffb3d9,
|
|
994
|
+
emissiveIntensity: 0.3
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// メインの雲
|
|
998
|
+
for(let i = 0; i < 8; i++) {
|
|
999
|
+
const radius = Math.random() * 3 + 2;
|
|
1000
|
+
const cloud = new THREE.Mesh(
|
|
1001
|
+
new THREE.SphereGeometry(radius * scale, 16, 12),
|
|
1002
|
+
cloudMaterial
|
|
1003
|
+
);
|
|
1004
|
+
cloud.position.set(
|
|
1005
|
+
x + (Math.random() - 0.5) * 10 * scale,
|
|
1006
|
+
y + (Math.random() - 0.5) * 2 * scale,
|
|
1007
|
+
z + (Math.random() - 0.5) * 10 * scale
|
|
1008
|
+
);
|
|
1009
|
+
cloud.receiveShadow = true;
|
|
1010
|
+
cloud.castShadow = true;
|
|
1011
|
+
cloudGroup.add(cloud);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
scene.add(cloudGroup);
|
|
1015
|
+
return cloudGroup;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// メインの雲プラットフォーム
|
|
1019
|
+
const mainCloud = createCloudPlatform(0, -5, 0, 2);
|
|
1020
|
+
|
|
1021
|
+
// 周りの小さな雲
|
|
1022
|
+
createCloudPlatform(-20, 2, -15, 1);
|
|
1023
|
+
createCloudPlatform(20, 4, -10, 0.8);
|
|
1024
|
+
createCloudPlatform(-15, 1, 20, 1.2);
|
|
1025
|
+
createCloudPlatform(25, 3, 15, 0.9);
|
|
1026
|
+
|
|
1027
|
+
// 🌈 虹色のクリスタル
|
|
1028
|
+
function createRainbowCrystal(x, y, z, scale = 1) {
|
|
1029
|
+
const geometry = new THREE.OctahedronGeometry(1.5 * scale, 0);
|
|
1030
|
+
|
|
1031
|
+
// キラキラクリスタルマテリアル
|
|
1032
|
+
const material = new THREE.MeshPhongMaterial({
|
|
1033
|
+
color: 0xffeeff, // ミルキーピンク
|
|
1034
|
+
transparent: true,
|
|
1035
|
+
opacity: 0.8,
|
|
1036
|
+
emissive: 0xffc0ff,
|
|
1037
|
+
emissiveIntensity: 0.4,
|
|
1038
|
+
shininess: 100,
|
|
1039
|
+
specular: 0xffffff
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
const crystal = new THREE.Mesh(geometry, material);
|
|
1043
|
+
crystal.position.set(x, y, z);
|
|
1044
|
+
crystal.castShadow = true;
|
|
1045
|
+
crystal.receiveShadow = true;
|
|
1046
|
+
|
|
1047
|
+
// ゆっくり回転
|
|
1048
|
+
crystal.userData.rotationSpeed = Math.random() * 0.02 + 0.01;
|
|
1049
|
+
|
|
1050
|
+
scene.add(crystal);
|
|
1051
|
+
return crystal;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// クリスタル配置
|
|
1055
|
+
const crystals = [];
|
|
1056
|
+
crystals.push(createRainbowCrystal(-8, 2, -5, 1.2));
|
|
1057
|
+
crystals.push(createRainbowCrystal(7, 3, -8, 0.8));
|
|
1058
|
+
crystals.push(createRainbowCrystal(-12, 1, 8, 1.5));
|
|
1059
|
+
crystals.push(createRainbowCrystal(10, 2.5, 6, 1));
|
|
1060
|
+
crystals.push(createRainbowCrystal(0, 4, -12, 0.9));
|
|
1061
|
+
crystals.push(createRainbowCrystal(-5, 2, 3, 1.1));
|
|
1062
|
+
|
|
1063
|
+
// ⭐ キラキラ星パーティクル
|
|
1064
|
+
function createSparkleParticles() {
|
|
1065
|
+
const particleCount = 200;
|
|
1066
|
+
const geometry = new THREE.BufferGeometry();
|
|
1067
|
+
const positions = [];
|
|
1068
|
+
const colors = [];
|
|
1069
|
+
const sizes = [];
|
|
1070
|
+
|
|
1071
|
+
for(let i = 0; i < particleCount; i++) {
|
|
1072
|
+
positions.push(
|
|
1073
|
+
(Math.random() - 0.5) * 100,
|
|
1074
|
+
Math.random() * 50 - 10,
|
|
1075
|
+
(Math.random() - 0.5) * 100
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
// ミルキーパステルカラー
|
|
1079
|
+
const hue = Math.random();
|
|
1080
|
+
colors.push(
|
|
1081
|
+
0.9 + Math.random() * 0.1, // より白っぽく
|
|
1082
|
+
0.7 + Math.random() * 0.3, // ミルキー
|
|
1083
|
+
0.9 + Math.random() * 0.1 // 明るく
|
|
1084
|
+
);
|
|
1085
|
+
|
|
1086
|
+
sizes.push(Math.random() * 3 + 1);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
1090
|
+
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
|
1091
|
+
geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1));
|
|
1092
|
+
|
|
1093
|
+
const material = new THREE.PointsMaterial({
|
|
1094
|
+
size: 2,
|
|
1095
|
+
sizeAttenuation: true,
|
|
1096
|
+
vertexColors: true,
|
|
1097
|
+
transparent: true,
|
|
1098
|
+
opacity: 0.9,
|
|
1099
|
+
alphaTest: 0.1, // 透明度の閾値設定
|
|
1100
|
+
blending: THREE.NormalBlending, // AdditiveBlendingをやめる
|
|
1101
|
+
map: createStarTexture()
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
const particles = new THREE.Points(geometry, material);
|
|
1105
|
+
scene.add(particles);
|
|
1106
|
+
return particles;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// 星のテクスチャ作成(完全透明背景)
|
|
1110
|
+
function createStarTexture() {
|
|
1111
|
+
const canvas = document.createElement('canvas');
|
|
1112
|
+
canvas.width = 64;
|
|
1113
|
+
canvas.height = 64;
|
|
1114
|
+
const ctx = canvas.getContext('2d');
|
|
1115
|
+
|
|
1116
|
+
// 背景を透明にクリア(白で塗らない)
|
|
1117
|
+
ctx.clearRect(0, 0, 64, 64);
|
|
1118
|
+
|
|
1119
|
+
// 中心から外側へのグラデーション(外側は完全透明)
|
|
1120
|
+
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 24);
|
|
1121
|
+
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
|
1122
|
+
gradient.addColorStop(0.3, 'rgba(255,240,255,0.8)');
|
|
1123
|
+
gradient.addColorStop(0.6, 'rgba(255,220,255,0.4)');
|
|
1124
|
+
gradient.addColorStop(0.9, 'rgba(255,200,255,0.1)');
|
|
1125
|
+
gradient.addColorStop(1, 'rgba(0,0,0,0)'); // 完全透明
|
|
1126
|
+
|
|
1127
|
+
// 円形だけ描画(背景は触らない)
|
|
1128
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
1129
|
+
ctx.beginPath();
|
|
1130
|
+
ctx.arc(32, 32, 24, 0, Math.PI * 2);
|
|
1131
|
+
ctx.fillStyle = gradient;
|
|
1132
|
+
ctx.fill();
|
|
1133
|
+
|
|
1134
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
1135
|
+
texture.premultiplyAlpha = false; // アルファ事前乗算を無効
|
|
1136
|
+
return texture;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const sparkles = createSparkleParticles();
|
|
1140
|
+
|
|
1141
|
+
// 🌈 太いグラデーション虹を追加
|
|
1142
|
+
function createRainbow() {
|
|
1143
|
+
// アーチカーブを作成
|
|
1144
|
+
const curve = new THREE.CatmullRomCurve3([
|
|
1145
|
+
new THREE.Vector3(-20, 5, -15),
|
|
1146
|
+
new THREE.Vector3(-10, 18, -15),
|
|
1147
|
+
new THREE.Vector3(0, 22, -15),
|
|
1148
|
+
new THREE.Vector3(10, 18, -15),
|
|
1149
|
+
new THREE.Vector3(20, 5, -15)
|
|
1150
|
+
]);
|
|
1151
|
+
|
|
1152
|
+
// 太いチューブジオメトリ(高解像度)
|
|
1153
|
+
const geometry = new THREE.TubeGeometry(curve, 100, 2.5, 32, false);
|
|
1154
|
+
|
|
1155
|
+
// グラデーション用のvertex shader
|
|
1156
|
+
const vertexShader = `
|
|
1157
|
+
varying vec2 vUv;
|
|
1158
|
+
void main() {
|
|
1159
|
+
vUv = uv;
|
|
1160
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
1161
|
+
}
|
|
1162
|
+
`;
|
|
1163
|
+
|
|
1164
|
+
// フラグメントシェーダーで虹色グラデーション
|
|
1165
|
+
const fragmentShader = `
|
|
1166
|
+
varying vec2 vUv;
|
|
1167
|
+
void main() {
|
|
1168
|
+
float t = vUv.y; // 長さ方向でグラデーション(修正!)
|
|
1169
|
+
|
|
1170
|
+
vec3 color;
|
|
1171
|
+
if (t < 0.167) {
|
|
1172
|
+
color = mix(vec3(1.0, 0.67, 0.67), vec3(1.0, 0.8, 0.6), t / 0.167);
|
|
1173
|
+
} else if (t < 0.333) {
|
|
1174
|
+
color = mix(vec3(1.0, 0.8, 0.6), vec3(1.0, 1.0, 0.67), (t - 0.167) / 0.167);
|
|
1175
|
+
} else if (t < 0.5) {
|
|
1176
|
+
color = mix(vec3(1.0, 1.0, 0.67), vec3(0.67, 1.0, 0.67), (t - 0.333) / 0.167);
|
|
1177
|
+
} else if (t < 0.667) {
|
|
1178
|
+
color = mix(vec3(0.67, 1.0, 0.67), vec3(0.67, 0.8, 1.0), (t - 0.5) / 0.167);
|
|
1179
|
+
} else if (t < 0.833) {
|
|
1180
|
+
color = mix(vec3(0.67, 0.8, 1.0), vec3(0.8, 0.67, 1.0), (t - 0.667) / 0.167);
|
|
1181
|
+
} else {
|
|
1182
|
+
color = mix(vec3(0.8, 0.67, 1.0), vec3(1.0, 0.67, 1.0), (t - 0.833) / 0.167);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
gl_FragColor = vec4(color, 0.8);
|
|
1186
|
+
}
|
|
1187
|
+
`;
|
|
1188
|
+
|
|
1189
|
+
const material = new THREE.ShaderMaterial({
|
|
1190
|
+
vertexShader: vertexShader,
|
|
1191
|
+
fragmentShader: fragmentShader,
|
|
1192
|
+
transparent: true,
|
|
1193
|
+
side: THREE.DoubleSide
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
const rainbow = new THREE.Mesh(geometry, material);
|
|
1197
|
+
|
|
1198
|
+
// 筒の向きを調整(より自然な角度に)
|
|
1199
|
+
rainbow.rotation.x = Math.PI * 0.1; // X軸を少し回転
|
|
1200
|
+
rainbow.rotation.y = Math.PI * 0.05; // Y軸を少し回転
|
|
1201
|
+
rainbow.rotation.z = Math.PI * 0.02; // Z軸を少し回転
|
|
1202
|
+
|
|
1203
|
+
scene.add(rainbow);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
createRainbow();
|
|
1207
|
+
|
|
1208
|
+
// 🦄 ユニコーンの角みたいなオブジェクト
|
|
1209
|
+
function createUnicornHorn(x, y, z) {
|
|
1210
|
+
const geometry = new THREE.ConeGeometry(0.5, 3, 8);
|
|
1211
|
+
const material = new THREE.MeshPhysicalMaterial({
|
|
1212
|
+
color: 0xffffff,
|
|
1213
|
+
metalness: 0.3,
|
|
1214
|
+
roughness: 0.2,
|
|
1215
|
+
iridescence: 1,
|
|
1216
|
+
iridescenceIOR: 2.333,
|
|
1217
|
+
iridescenceThicknessRange: [100, 400],
|
|
1218
|
+
emissive: 0xffd0ff,
|
|
1219
|
+
emissiveIntensity: 0.3
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
const horn = new THREE.Mesh(geometry, material);
|
|
1223
|
+
horn.position.set(x, y, z);
|
|
1224
|
+
horn.rotation.z = Math.PI / 8;
|
|
1225
|
+
horn.castShadow = true;
|
|
1226
|
+
|
|
1227
|
+
// ゴールドのらせん
|
|
1228
|
+
const spiralGeometry = new THREE.TorusGeometry(0.6, 0.1, 8, 20);
|
|
1229
|
+
const spiralMaterial = new THREE.MeshPhongMaterial({
|
|
1230
|
+
color: 0xffd700,
|
|
1231
|
+
emissive: 0xffd700,
|
|
1232
|
+
emissiveIntensity: 0.3
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
for(let i = 0; i < 3; i++) {
|
|
1236
|
+
const spiral = new THREE.Mesh(spiralGeometry, spiralMaterial);
|
|
1237
|
+
spiral.position.y = i * 0.8 - 1;
|
|
1238
|
+
spiral.scale.set(1 - i * 0.3, 1 - i * 0.3, 1);
|
|
1239
|
+
horn.add(spiral);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
scene.add(horn);
|
|
1243
|
+
return horn;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// ユニコーンの角を配置
|
|
1247
|
+
const horn1 = createUnicornHorn(5, 3, 0);
|
|
1248
|
+
const horn2 = createUnicornHorn(-8, 2, 5);
|
|
1249
|
+
|
|
1250
|
+
// Camera position
|
|
1251
|
+
camera.position.set(0, 8, 25);
|
|
1252
|
+
camera.lookAt(0, 0, 0);
|
|
1253
|
+
|
|
1254
|
+
// Controls
|
|
1255
|
+
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
1256
|
+
controls.enableDamping = true;
|
|
1257
|
+
controls.dampingFactor = 0.05;
|
|
1258
|
+
controls.maxPolarAngle = Math.PI / 2.2;
|
|
1259
|
+
|
|
1260
|
+
// Initialize ChocoDrop (本物!)
|
|
1261
|
+
const chocoDrop = ChocoDrop.createChocoDrop(scene, {
|
|
1262
|
+
camera: camera,
|
|
1263
|
+
renderer: renderer,
|
|
1264
|
+
// GitHub Pagesなので生成機能なし、インポートのみ
|
|
1265
|
+
serverUrl: null,
|
|
1266
|
+
enableServerHealthCheck: false, // サーバーレス環境なのでヘルスチェック無効
|
|
1267
|
+
uiOptions: { showGuidedOnboarding: false },
|
|
1268
|
+
sceneOptions: {
|
|
1269
|
+
enableMouseInteraction: true
|
|
1270
|
+
},
|
|
1271
|
+
onControlsToggle: (disabled) => {
|
|
1272
|
+
controls.enabled = !disabled;
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
const statusElement = document.getElementById('demo-status');
|
|
1277
|
+
statusElement.textContent = '準備完了 · ファイルはこのブラウザ内で配置します';
|
|
1278
|
+
const sampleButton = document.getElementById('try-sample');
|
|
1279
|
+
sampleButton.disabled = false;
|
|
1280
|
+
sampleButton.addEventListener('click', async () => {
|
|
1281
|
+
sampleButton.disabled = true;
|
|
1282
|
+
statusElement.textContent = 'サンプルを読み込んでいます…';
|
|
1283
|
+
try {
|
|
1284
|
+
await chocoDrop.sceneManager.loadImageFile('../../public/sample-card.svg', {
|
|
1285
|
+
fileName: 'ChocoDrop-sample.svg', position: { x: 0, y: 0, z: 12 }
|
|
1286
|
+
});
|
|
1287
|
+
statusElement.textContent = 'サンプルを配置しました · 次はImportからご自分の素材を選べます';
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
statusElement.textContent = `配置できませんでした: ${error.message}`;
|
|
1290
|
+
} finally {
|
|
1291
|
+
sampleButton.disabled = false;
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
if (new URLSearchParams(location.search).get('local') === '1') {
|
|
1295
|
+
const { connectLocalBridge } = await import('../../src/client/local-bridge.js');
|
|
1296
|
+
connectLocalBridge(chocoDrop.sceneManager, { statusElement });
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// GitHub Pages用: サービス設定ダイアログを強制非表示 + UIサイズ調整
|
|
1300
|
+
const hideServiceDialog = () => {
|
|
1301
|
+
// 複数の方法でサービス設定ダイアログを非表示
|
|
1302
|
+
const modals = document.querySelectorAll('[style*="position: fixed"], [style*="backdrop"], div[style*="rgba"], div[style*="blur"]');
|
|
1303
|
+
modals.forEach(modal => {
|
|
1304
|
+
if (modal.textContent && modal.textContent.includes('サービス設定')) {
|
|
1305
|
+
modal.style.display = 'none !important';
|
|
1306
|
+
modal.remove();
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
// UIサイズ調整は削除(文字が崩れるため)
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
// 複数回実行して確実に非表示
|
|
1314
|
+
setTimeout(hideServiceDialog, 50);
|
|
1315
|
+
setTimeout(hideServiceDialog, 200);
|
|
1316
|
+
setTimeout(hideServiceDialog, 500);
|
|
1317
|
+
|
|
1318
|
+
// DOM変更監視
|
|
1319
|
+
const observer = new MutationObserver(() => {
|
|
1320
|
+
hideServiceDialog();
|
|
1321
|
+
});
|
|
1322
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
1323
|
+
|
|
1324
|
+
// 🎵 高度な音響システムの初期化(wabi-sabi方式)
|
|
1325
|
+
let audioContext = null;
|
|
1326
|
+
let analyser = null;
|
|
1327
|
+
let dataArray = null;
|
|
1328
|
+
let beatDetectionHistory = [];
|
|
1329
|
+
let lastBeatTime = 0;
|
|
1330
|
+
let musicData = {
|
|
1331
|
+
volume: 0,
|
|
1332
|
+
bass: 0,
|
|
1333
|
+
mid: 0,
|
|
1334
|
+
treble: 0,
|
|
1335
|
+
tempo: 0,
|
|
1336
|
+
bpm: 0,
|
|
1337
|
+
beatDetected: false,
|
|
1338
|
+
isPlaying: false
|
|
1339
|
+
};
|
|
1340
|
+
const audioStatusEl = document.getElementById('audio-status');
|
|
1341
|
+
const audioTextEl = document.getElementById('audio-text');
|
|
1342
|
+
|
|
1343
|
+
function setupAudioAnalysis() {
|
|
1344
|
+
if (!audioContext) {
|
|
1345
|
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
1346
|
+
analyser = audioContext.createAnalyser();
|
|
1347
|
+
analyser.fftSize = 256;
|
|
1348
|
+
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
1349
|
+
console.log('🎵 Advanced audio analysis initialized');
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// 動画要素を音響分析に接続
|
|
1354
|
+
function connectVideoToAudio(videoElement) {
|
|
1355
|
+
if (!audioContext || !analyser) {
|
|
1356
|
+
setupAudioAnalysis();
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
try {
|
|
1360
|
+
// AudioContextがsuspended状態の場合は再開
|
|
1361
|
+
if (audioContext.state === 'suspended') {
|
|
1362
|
+
audioContext.resume();
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const source = audioContext.createMediaElementSource(videoElement);
|
|
1366
|
+
source.connect(analyser);
|
|
1367
|
+
analyser.connect(audioContext.destination);
|
|
1368
|
+
|
|
1369
|
+
musicData.isPlaying = true;
|
|
1370
|
+
if (audioStatusEl) {
|
|
1371
|
+
audioStatusEl.classList.add('active');
|
|
1372
|
+
if (audioTextEl) {
|
|
1373
|
+
audioTextEl.textContent = '音響連動中 ♪ (BPM検出中)';
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
console.log('🎵 Video connected to advanced audio analyser');
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
console.warn('Audio connection error:', error);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// 音楽データの詳細解析
|
|
1383
|
+
function analyzeMusicData() {
|
|
1384
|
+
if (!analyser || !dataArray) return;
|
|
1385
|
+
|
|
1386
|
+
analyser.getByteFrequencyData(dataArray);
|
|
1387
|
+
|
|
1388
|
+
// 音量計算
|
|
1389
|
+
let sum = 0;
|
|
1390
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
1391
|
+
sum += dataArray[i];
|
|
1392
|
+
}
|
|
1393
|
+
musicData.volume = sum / dataArray.length / 255;
|
|
1394
|
+
|
|
1395
|
+
// 周波数帯域分析
|
|
1396
|
+
const bassRange = Math.floor(dataArray.length * 0.1);
|
|
1397
|
+
const midRange = Math.floor(dataArray.length * 0.5);
|
|
1398
|
+
|
|
1399
|
+
let bassSum = 0, midSum = 0, trebleSum = 0;
|
|
1400
|
+
|
|
1401
|
+
for (let i = 0; i < bassRange; i++) {
|
|
1402
|
+
bassSum += dataArray[i];
|
|
1403
|
+
}
|
|
1404
|
+
for (let i = bassRange; i < midRange; i++) {
|
|
1405
|
+
midSum += dataArray[i];
|
|
1406
|
+
}
|
|
1407
|
+
for (let i = midRange; i < dataArray.length; i++) {
|
|
1408
|
+
trebleSum += dataArray[i];
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
musicData.bass = bassSum / bassRange / 255;
|
|
1412
|
+
musicData.mid = midSum / (midRange - bassRange) / 255;
|
|
1413
|
+
musicData.treble = trebleSum / (dataArray.length - midRange) / 255;
|
|
1414
|
+
|
|
1415
|
+
// 🥁 ビート検出とBPM計算
|
|
1416
|
+
const currentTime = performance.now();
|
|
1417
|
+
const bassThreshold = 0.3;
|
|
1418
|
+
const volumeThreshold = 0.2;
|
|
1419
|
+
|
|
1420
|
+
musicData.beatDetected = false;
|
|
1421
|
+
|
|
1422
|
+
// ビート検出(ベース音量の急激な変化)
|
|
1423
|
+
if (musicData.bass > bassThreshold && musicData.volume > volumeThreshold) {
|
|
1424
|
+
if (currentTime - lastBeatTime > 300) { // 最小300ms間隔
|
|
1425
|
+
musicData.beatDetected = true;
|
|
1426
|
+
lastBeatTime = currentTime;
|
|
1427
|
+
|
|
1428
|
+
// BPM計算用履歴更新
|
|
1429
|
+
beatDetectionHistory.push(currentTime);
|
|
1430
|
+
if (beatDetectionHistory.length > 8) {
|
|
1431
|
+
beatDetectionHistory.shift();
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// BPM計算
|
|
1435
|
+
if (beatDetectionHistory.length >= 4) {
|
|
1436
|
+
const intervals = [];
|
|
1437
|
+
for (let i = 1; i < beatDetectionHistory.length; i++) {
|
|
1438
|
+
intervals.push(beatDetectionHistory[i] - beatDetectionHistory[i-1]);
|
|
1439
|
+
}
|
|
1440
|
+
const avgInterval = intervals.reduce((a, b) => a + b) / intervals.length;
|
|
1441
|
+
musicData.bpm = Math.round(60000 / avgInterval);
|
|
1442
|
+
|
|
1443
|
+
// ステータス更新
|
|
1444
|
+
if (audioTextEl && musicData.bpm > 0) {
|
|
1445
|
+
audioTextEl.textContent = `音響連動中 ♪ (${musicData.bpm} BPM)`;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// ChocoDrop のビデオ要素を監視して自動接続
|
|
1453
|
+
function observeVideoElements() {
|
|
1454
|
+
const videoObserver = new MutationObserver((mutations) => {
|
|
1455
|
+
mutations.forEach((mutation) => {
|
|
1456
|
+
mutation.addedNodes.forEach((node) => {
|
|
1457
|
+
if (node.nodeName === 'VIDEO') {
|
|
1458
|
+
console.log('🎬 Video element detected, connecting to audio...');
|
|
1459
|
+
connectVideoToAudio(node);
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
});
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
videoObserver.observe(document.body, {
|
|
1466
|
+
childList: true,
|
|
1467
|
+
subtree: true
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
// 既存のビデオ要素もチェック
|
|
1471
|
+
const existingVideos = document.querySelectorAll('video');
|
|
1472
|
+
existingVideos.forEach(video => {
|
|
1473
|
+
connectVideoToAudio(video);
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// 初期化
|
|
1478
|
+
if (audioStatusEl) {
|
|
1479
|
+
audioStatusEl.classList.add('active');
|
|
1480
|
+
}
|
|
1481
|
+
observeVideoElements();
|
|
1482
|
+
|
|
1483
|
+
// Animation loop
|
|
1484
|
+
function animate() {
|
|
1485
|
+
requestAnimationFrame(animate);
|
|
1486
|
+
controls.update();
|
|
1487
|
+
|
|
1488
|
+
// ✨ アニメーション
|
|
1489
|
+
const time = Date.now() * 0.001;
|
|
1490
|
+
|
|
1491
|
+
// 🎵 高度な音響データ解析
|
|
1492
|
+
if (musicData.isPlaying) {
|
|
1493
|
+
analyzeMusicData();
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// 音響連動の強度(優しく穏やかに + ビート反応)
|
|
1497
|
+
const audioIntensity = musicData.volume * 0.6; // basicは控えめな反応
|
|
1498
|
+
const bassBoost = musicData.bass * 1.2;
|
|
1499
|
+
const midBoost = musicData.mid * 0.8;
|
|
1500
|
+
const trebleBoost = musicData.treble * 1.0;
|
|
1501
|
+
const beatPulse = musicData.beatDetected ? 1.5 : 1.0; // ビート時に大きく反応
|
|
1502
|
+
|
|
1503
|
+
// クリスタルの回転 + 音響連動 + ビート反応
|
|
1504
|
+
crystals.forEach((crystal, i) => {
|
|
1505
|
+
crystal.rotation.y = time * crystal.userData.rotationSpeed * (1 + audioIntensity * 0.5) * beatPulse;
|
|
1506
|
+
crystal.rotation.x = Math.sin(time * 0.5 + i) * 0.1 * (1 + bassBoost * 0.3) * beatPulse;
|
|
1507
|
+
crystal.position.y += Math.sin(time + i) * 0.002 * (1 + midBoost * 0.4); // ふわふわ浮遊
|
|
1508
|
+
|
|
1509
|
+
// 音楽に合わせて輝きを変化 + ビート時に強く光る
|
|
1510
|
+
if (crystal.material && crystal.material.emissiveIntensity !== undefined) {
|
|
1511
|
+
crystal.material.emissiveIntensity = (0.4 + audioIntensity * 0.6) * beatPulse;
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
// 雲のふわふわアニメーション + 音響連動 + ビート反応
|
|
1516
|
+
if (mainCloud) {
|
|
1517
|
+
mainCloud.position.y = -5 + Math.sin(time * 0.3) * 0.5 * (1 + bassBoost * 0.5) * beatPulse;
|
|
1518
|
+
mainCloud.rotation.y = time * 0.05 * (1 + audioIntensity * 0.3);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// キラキラパーティクルの回転 + 音響連動 + ビート反応
|
|
1522
|
+
if (sparkles) {
|
|
1523
|
+
sparkles.rotation.y = time * 0.1 * (1 + trebleBoost * 0.4) * beatPulse;
|
|
1524
|
+
sparkles.rotation.x = time * 0.05 * (1 + midBoost * 0.3);
|
|
1525
|
+
|
|
1526
|
+
// パーティクルの瞬き効果 + 音響連動 + ビート時に激しく
|
|
1527
|
+
const positions = sparkles.geometry.attributes.position.array;
|
|
1528
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
1529
|
+
const waveEffect = Math.sin(time * 2 + i) * 0.01 * (1 + audioIntensity * 1.5) * beatPulse;
|
|
1530
|
+
positions[i + 1] += waveEffect;
|
|
1531
|
+
}
|
|
1532
|
+
sparkles.geometry.attributes.position.needsUpdate = true;
|
|
1533
|
+
|
|
1534
|
+
// 音楽に合わせて明るさ変化 + ビート時に明るく
|
|
1535
|
+
if (sparkles.material && sparkles.material.opacity !== undefined) {
|
|
1536
|
+
sparkles.material.opacity = (0.9 + audioIntensity * 0.1) * beatPulse;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// ユニコーンの角の回転 + 音響連動 + ビート反応
|
|
1541
|
+
if (horn1) {
|
|
1542
|
+
horn1.rotation.y = time * 0.3 * (1 + audioIntensity * 0.4) * beatPulse;
|
|
1543
|
+
horn1.position.y = 3 + Math.sin(time * 0.7) * 0.3 * (1 + bassBoost * 0.6) * beatPulse;
|
|
1544
|
+
|
|
1545
|
+
// 音楽に合わせて輝きを変化 + ビート時に強く光る
|
|
1546
|
+
if (horn1.material && horn1.material.emissiveIntensity !== undefined) {
|
|
1547
|
+
horn1.material.emissiveIntensity = (0.3 + trebleBoost * 0.5) * beatPulse;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
if (horn2) {
|
|
1551
|
+
horn2.rotation.y = -time * 0.2 * (1 + audioIntensity * 0.4) * beatPulse;
|
|
1552
|
+
horn2.position.y = 2 + Math.sin(time * 0.5 + 1) * 0.2 * (1 + bassBoost * 0.6) * beatPulse;
|
|
1553
|
+
|
|
1554
|
+
// 音楽に合わせて輝きを変化 + ビート時に強く光る
|
|
1555
|
+
if (horn2.material && horn2.material.emissiveIntensity !== undefined) {
|
|
1556
|
+
horn2.material.emissiveIntensity = (0.3 + trebleBoost * 0.5) * beatPulse;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
renderer.render(scene, camera);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
animate();
|
|
1564
|
+
|
|
1565
|
+
// Handle window resize
|
|
1566
|
+
window.addEventListener('resize', () => {
|
|
1567
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
1568
|
+
camera.updateProjectionMatrix();
|
|
1569
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
// 30秒後の自動非表示タイマー開始
|
|
1573
|
+
scheduleInfoPanelAutoHide();
|
|
1574
|
+
|
|
1575
|
+
console.log('✅ ベーシックな世界が準備完了!🍫 ChocoDrop でコンテンツを配置しましょう');
|
|
1576
|
+
|
|
1577
|
+
})().catch(error => {
|
|
1578
|
+
document.getElementById('demo-status').textContent = `起動できませんでした: ${error.message}。通信とWebGL対応を確認して再読み込みしてください。`;
|
|
1579
|
+
console.error(error);
|
|
1580
|
+
});
|
|
1581
|
+
</script>
|
|
1582
|
+
|
|
1583
|
+
<!-- VR/AR Button inline implementation -->
|
|
1584
|
+
<script>
|
|
1585
|
+
// VRButton implementation
|
|
1586
|
+
const VRButton = {
|
|
1587
|
+
createButton: function(renderer) {
|
|
1588
|
+
const button = document.createElement('button');
|
|
1589
|
+
|
|
1590
|
+
function showEnterVR() {
|
|
1591
|
+
button.style.display = '';
|
|
1592
|
+
button.style.cursor = 'pointer';
|
|
1593
|
+
button.style.left = 'calc(50% - 50px)';
|
|
1594
|
+
button.style.width = '100px';
|
|
1595
|
+
button.textContent = 'ENTER VR';
|
|
1596
|
+
button.onmouseenter = function() { button.style.opacity = '1.0'; };
|
|
1597
|
+
button.onmouseleave = function() { button.style.opacity = '0.5'; };
|
|
1598
|
+
button.onclick = function() {
|
|
1599
|
+
const session = renderer.xr.getSession();
|
|
1600
|
+
if (session) {
|
|
1601
|
+
session.end();
|
|
1602
|
+
} else {
|
|
1603
|
+
navigator.xr.requestSession('immersive-vr', {
|
|
1604
|
+
optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking']
|
|
1605
|
+
}).then(session => {
|
|
1606
|
+
renderer.xr.setSession(session);
|
|
1607
|
+
button.textContent = 'EXIT VR';
|
|
1608
|
+
|
|
1609
|
+
session.addEventListener('end', () => {
|
|
1610
|
+
button.textContent = 'ENTER VR';
|
|
1611
|
+
});
|
|
1612
|
+
}).catch(err => {
|
|
1613
|
+
console.error('VR session failed:', err);
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function showWebXRNotFound() {
|
|
1620
|
+
button.style.display = '';
|
|
1621
|
+
button.style.cursor = 'auto';
|
|
1622
|
+
button.style.left = 'calc(50% - 75px)';
|
|
1623
|
+
button.style.width = '150px';
|
|
1624
|
+
button.textContent = 'VR NOT SUPPORTED';
|
|
1625
|
+
button.onmouseenter = null;
|
|
1626
|
+
button.onmouseleave = null;
|
|
1627
|
+
button.onclick = null;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
button.style.position = 'absolute';
|
|
1631
|
+
button.style.bottom = '20px';
|
|
1632
|
+
button.style.padding = '12px 6px';
|
|
1633
|
+
button.style.border = '1px solid #fff';
|
|
1634
|
+
button.style.borderRadius = '4px';
|
|
1635
|
+
button.style.background = 'rgba(0,0,0,0.1)';
|
|
1636
|
+
button.style.color = '#fff';
|
|
1637
|
+
button.style.font = 'normal 13px sans-serif';
|
|
1638
|
+
button.style.textAlign = 'center';
|
|
1639
|
+
button.style.opacity = '0.5';
|
|
1640
|
+
button.style.outline = 'none';
|
|
1641
|
+
button.style.zIndex = '999';
|
|
1642
|
+
|
|
1643
|
+
if ('xr' in navigator) {
|
|
1644
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
1645
|
+
supported ? showEnterVR() : showWebXRNotFound();
|
|
1646
|
+
});
|
|
1647
|
+
} else {
|
|
1648
|
+
showWebXRNotFound();
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
return button;
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1655
|
+
// ARButton implementation
|
|
1656
|
+
const ARButton = {
|
|
1657
|
+
createButton: function(renderer, options) {
|
|
1658
|
+
const button = document.createElement('button');
|
|
1659
|
+
|
|
1660
|
+
function showEnterAR() {
|
|
1661
|
+
button.style.display = '';
|
|
1662
|
+
button.style.cursor = 'pointer';
|
|
1663
|
+
button.style.left = 'calc(50% - 50px)';
|
|
1664
|
+
button.style.width = '100px';
|
|
1665
|
+
button.textContent = 'ENTER AR';
|
|
1666
|
+
button.onmouseenter = function() { button.style.opacity = '1.0'; };
|
|
1667
|
+
button.onmouseleave = function() { button.style.opacity = '0.5'; };
|
|
1668
|
+
button.onclick = function() {
|
|
1669
|
+
const session = renderer.xr.getSession();
|
|
1670
|
+
if (session) {
|
|
1671
|
+
session.end();
|
|
1672
|
+
} else {
|
|
1673
|
+
navigator.xr.requestSession('immersive-ar', {
|
|
1674
|
+
optionalFeatures: ['hit-test', 'dom-overlay', 'local-floor'],
|
|
1675
|
+
domOverlay: options?.domOverlay || { root: document.body }
|
|
1676
|
+
}).then(session => {
|
|
1677
|
+
renderer.xr.setSession(session);
|
|
1678
|
+
button.textContent = 'EXIT AR';
|
|
1679
|
+
|
|
1680
|
+
session.addEventListener('end', () => {
|
|
1681
|
+
button.textContent = 'ENTER AR';
|
|
1682
|
+
});
|
|
1683
|
+
}).catch(err => {
|
|
1684
|
+
console.error('AR session failed:', err);
|
|
1685
|
+
alert('AR not supported: ' + err.message);
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function showWebXRNotFound() {
|
|
1692
|
+
button.style.display = '';
|
|
1693
|
+
button.style.cursor = 'auto';
|
|
1694
|
+
button.style.left = 'calc(50% - 75px)';
|
|
1695
|
+
button.style.width = '150px';
|
|
1696
|
+
button.textContent = 'AR NOT SUPPORTED';
|
|
1697
|
+
button.onmouseenter = null;
|
|
1698
|
+
button.onmouseleave = null;
|
|
1699
|
+
button.onclick = null;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
button.style.position = 'absolute';
|
|
1703
|
+
button.style.bottom = '70px';
|
|
1704
|
+
button.style.padding = '12px 6px';
|
|
1705
|
+
button.style.border = '1px solid #fff';
|
|
1706
|
+
button.style.borderRadius = '4px';
|
|
1707
|
+
button.style.background = 'rgba(0,0,0,0.1)';
|
|
1708
|
+
button.style.color = '#fff';
|
|
1709
|
+
button.style.font = 'normal 13px sans-serif';
|
|
1710
|
+
button.style.textAlign = 'center';
|
|
1711
|
+
button.style.opacity = '0.5';
|
|
1712
|
+
button.style.outline = 'none';
|
|
1713
|
+
button.style.zIndex = '999';
|
|
1714
|
+
|
|
1715
|
+
if ('xr' in navigator) {
|
|
1716
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
1717
|
+
supported ? showEnterAR() : showWebXRNotFound();
|
|
1718
|
+
});
|
|
1719
|
+
} else {
|
|
1720
|
+
showWebXRNotFound();
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
return button;
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
|
|
1727
|
+
console.log('✅ VRButton/ARButton inline implementation loaded');
|
|
1728
|
+
</script>
|
|
1729
|
+
</body>
|
|
1730
|
+
</html>
|