@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,1780 @@
|
|
|
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 - Pixel Ocean Depths</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: 'Courier New', 'Monaco', monospace;
|
|
21
|
+
background: linear-gradient(180deg, #0066cc 0%, #004499 30%, #002266 70%, #001133 100%);
|
|
22
|
+
image-rendering: pixelated;
|
|
23
|
+
image-rendering: crisp-edges;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#info {
|
|
27
|
+
position: fixed;
|
|
28
|
+
top: 16px;
|
|
29
|
+
left: 16px;
|
|
30
|
+
color: #ffffff;
|
|
31
|
+
background: linear-gradient(135deg, rgba(0, 102, 204, 0.9) 0%, rgba(0, 34, 102, 0.95) 100%);
|
|
32
|
+
backdrop-filter: blur(12px);
|
|
33
|
+
border: 2px solid rgba(102, 204, 255, 0.6);
|
|
34
|
+
padding: 14px 18px;
|
|
35
|
+
border-radius: 8px;
|
|
36
|
+
font-size: 12px;
|
|
37
|
+
font-weight: 400;
|
|
38
|
+
z-index: 2000;
|
|
39
|
+
max-width: 280px;
|
|
40
|
+
text-align: center;
|
|
41
|
+
line-height: 1.5;
|
|
42
|
+
box-shadow: 0 0 25px rgba(102, 204, 255, 0.4);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#info strong {
|
|
46
|
+
font-size: 16px;
|
|
47
|
+
font-weight: 900;
|
|
48
|
+
color: #66ccff;
|
|
49
|
+
display: block;
|
|
50
|
+
margin-bottom: 12px;
|
|
51
|
+
text-align: center;
|
|
52
|
+
text-shadow: 0 0 15px #0099ff;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#info code {
|
|
56
|
+
background: rgba(102, 204, 255, 0.3);
|
|
57
|
+
color: #ccf0ff;
|
|
58
|
+
padding: 3px 6px;
|
|
59
|
+
border-radius: 4px;
|
|
60
|
+
font-family: 'Courier New', monospace;
|
|
61
|
+
font-size: 12px;
|
|
62
|
+
font-weight: bold;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.ocean-icon {
|
|
66
|
+
display: inline-block;
|
|
67
|
+
animation: ocean-wave 2.5s ease-in-out infinite;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
transition: transform 0.2s ease;
|
|
70
|
+
text-shadow: 0 0 10px currentColor;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ocean-icon:hover {
|
|
74
|
+
transform: scale(1.3);
|
|
75
|
+
animation: ocean-wave-fast 0.8s ease-in-out infinite;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ocean-title {
|
|
79
|
+
display: inline-block;
|
|
80
|
+
animation: ocean-glow 3.5s ease-in-out infinite;
|
|
81
|
+
background: linear-gradient(45deg, #66ccff, #0099ff, #00ccff);
|
|
82
|
+
-webkit-background-clip: text;
|
|
83
|
+
-webkit-text-fill-color: transparent;
|
|
84
|
+
background-clip: text;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@keyframes ocean-glow {
|
|
88
|
+
0%, 100% {
|
|
89
|
+
text-shadow: 0 0 20px rgba(102, 204, 255, 0.8);
|
|
90
|
+
filter: brightness(1);
|
|
91
|
+
}
|
|
92
|
+
50% {
|
|
93
|
+
text-shadow: 0 0 30px rgba(0, 153, 255, 1), 0 0 40px rgba(0, 204, 255, 0.8);
|
|
94
|
+
filter: brightness(1.3);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@keyframes ocean-wave {
|
|
99
|
+
0%, 100% {
|
|
100
|
+
transform: translateY(0px) rotate(0deg);
|
|
101
|
+
filter: brightness(1) drop-shadow(0 0 8px currentColor);
|
|
102
|
+
}
|
|
103
|
+
25% {
|
|
104
|
+
transform: translateY(-4px) rotate(2deg);
|
|
105
|
+
filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
|
|
106
|
+
}
|
|
107
|
+
50% {
|
|
108
|
+
transform: translateY(-2px) rotate(0deg);
|
|
109
|
+
filter: brightness(1.5) drop-shadow(0 0 20px currentColor);
|
|
110
|
+
}
|
|
111
|
+
75% {
|
|
112
|
+
transform: translateY(-6px) rotate(-2deg);
|
|
113
|
+
filter: brightness(1.2) drop-shadow(0 0 12px currentColor);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@keyframes ocean-wave-fast {
|
|
118
|
+
0%, 100% {
|
|
119
|
+
filter: brightness(1.4) drop-shadow(0 0 15px currentColor);
|
|
120
|
+
transform: rotate(0deg) scale(1);
|
|
121
|
+
}
|
|
122
|
+
50% {
|
|
123
|
+
filter: brightness(2) drop-shadow(0 0 25px currentColor) drop-shadow(0 0 15px #fff);
|
|
124
|
+
transform: rotate(5deg) scale(1.1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.highlight {
|
|
129
|
+
color: #66ccff;
|
|
130
|
+
font-weight: bold;
|
|
131
|
+
text-shadow: 0 0 10px rgba(102, 204, 255, 0.8);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.subtitle {
|
|
135
|
+
color: #b3e5ff;
|
|
136
|
+
font-size: 12px;
|
|
137
|
+
margin-top: 8px;
|
|
138
|
+
opacity: 0.9;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#info.hidden {
|
|
142
|
+
opacity: 0;
|
|
143
|
+
transform: translateY(-10px);
|
|
144
|
+
pointer-events: none;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.close-btn {
|
|
148
|
+
position: absolute;
|
|
149
|
+
top: 8px;
|
|
150
|
+
right: 8px;
|
|
151
|
+
background: none;
|
|
152
|
+
border: none;
|
|
153
|
+
font-size: 16px;
|
|
154
|
+
color: #66ccff;
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
width: 20px;
|
|
157
|
+
height: 20px;
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: center;
|
|
161
|
+
border-radius: 50%;
|
|
162
|
+
transition: background-color 0.2s ease;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.close-btn:hover {
|
|
166
|
+
background-color: rgba(102, 204, 255, 0.2);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* 統合メニュー */
|
|
170
|
+
.unified-menu {
|
|
171
|
+
position: fixed;
|
|
172
|
+
top: 16px;
|
|
173
|
+
right: 20px;
|
|
174
|
+
z-index: 1000;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.menu-toggle {
|
|
178
|
+
background: rgba(255, 255, 255, 0.15);
|
|
179
|
+
backdrop-filter: blur(16px);
|
|
180
|
+
-webkit-backdrop-filter: blur(16px);
|
|
181
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
182
|
+
color: #2d3748;
|
|
183
|
+
padding: 10px 14px;
|
|
184
|
+
border-radius: 8px;
|
|
185
|
+
font-size: 18px;
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
188
|
+
box-shadow: 0 4px 12px rgba(102, 204, 255, 0.2), 0 2px 6px rgba(0, 0, 0, 0.05);
|
|
189
|
+
width: 44px;
|
|
190
|
+
height: 44px;
|
|
191
|
+
display: flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
justify-content: center;
|
|
194
|
+
opacity: 0.8;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.menu-toggle:hover {
|
|
198
|
+
background: rgba(255, 255, 255, 0.25);
|
|
199
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
200
|
+
transform: scale(1.1) translateY(-2px);
|
|
201
|
+
box-shadow: 0 6px 16px rgba(102, 204, 255, 0.3), 0 3px 8px rgba(0, 0, 0, 0.1);
|
|
202
|
+
opacity: 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.menu-dropdown {
|
|
206
|
+
position: absolute;
|
|
207
|
+
top: 56px;
|
|
208
|
+
right: 0;
|
|
209
|
+
min-width: 220px;
|
|
210
|
+
background: rgba(255, 255, 255, 0.85);
|
|
211
|
+
backdrop-filter: blur(16px) saturate(180%);
|
|
212
|
+
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
213
|
+
border: 1px solid rgba(102, 204, 255, 0.2);
|
|
214
|
+
border-radius: 12px;
|
|
215
|
+
box-shadow: 0 8px 24px rgba(102, 204, 255, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
216
|
+
opacity: 0;
|
|
217
|
+
visibility: hidden;
|
|
218
|
+
transform: translateY(-10px);
|
|
219
|
+
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
220
|
+
overflow: hidden;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.menu-dropdown.active {
|
|
224
|
+
opacity: 1;
|
|
225
|
+
visibility: visible;
|
|
226
|
+
transform: translateY(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.menu-item {
|
|
230
|
+
display: block;
|
|
231
|
+
padding: 12px 16px;
|
|
232
|
+
color: #2d3748;
|
|
233
|
+
text-decoration: none;
|
|
234
|
+
font-size: 13px;
|
|
235
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
236
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
background: none;
|
|
239
|
+
border-left: none;
|
|
240
|
+
border-right: none;
|
|
241
|
+
border-top: none;
|
|
242
|
+
width: 100%;
|
|
243
|
+
text-align: left;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.menu-item:last-child {
|
|
247
|
+
border-bottom: none;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.menu-item:hover {
|
|
251
|
+
background: rgba(102, 204, 255, 0.08);
|
|
252
|
+
padding-left: 20px;
|
|
253
|
+
border-left: 2px solid rgba(102, 204, 255, 0.4);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.menu-item.has-submenu {
|
|
257
|
+
position: relative;
|
|
258
|
+
padding: 0;
|
|
259
|
+
border: none;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.submenu-toggle {
|
|
263
|
+
display: block;
|
|
264
|
+
padding: 12px 16px;
|
|
265
|
+
color: #2d3748;
|
|
266
|
+
font-size: 13px;
|
|
267
|
+
background: none;
|
|
268
|
+
border: none;
|
|
269
|
+
width: 100%;
|
|
270
|
+
text-align: left;
|
|
271
|
+
cursor: pointer;
|
|
272
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
273
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.submenu-toggle:hover {
|
|
277
|
+
background: rgba(102, 204, 255, 0.08);
|
|
278
|
+
padding-left: 20px;
|
|
279
|
+
border-left: 2px solid rgba(102, 204, 255, 0.4);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.submenu {
|
|
283
|
+
max-height: 0;
|
|
284
|
+
overflow: hidden;
|
|
285
|
+
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
286
|
+
background: rgba(250, 250, 250, 0.5);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.submenu.active {
|
|
290
|
+
max-height: 400px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.submenu .menu-item {
|
|
294
|
+
padding-left: 28px;
|
|
295
|
+
font-size: 12px;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* 🎵 音響ステータス表示 */
|
|
299
|
+
#audio-status {
|
|
300
|
+
position: fixed;
|
|
301
|
+
top: 70px;
|
|
302
|
+
right: 20px;
|
|
303
|
+
z-index: 100;
|
|
304
|
+
background: linear-gradient(135deg, rgba(0, 102, 204, 0.9) 0%, rgba(0, 34, 102, 0.95) 100%);
|
|
305
|
+
backdrop-filter: blur(12px);
|
|
306
|
+
-webkit-backdrop-filter: blur(12px);
|
|
307
|
+
padding: 10px 15px;
|
|
308
|
+
border-radius: 20px;
|
|
309
|
+
font-size: 12px;
|
|
310
|
+
border: 2px solid rgba(102, 204, 255, 0.6);
|
|
311
|
+
box-shadow: 0 0 25px rgba(102, 204, 255, 0.4);
|
|
312
|
+
color: #ffffff;
|
|
313
|
+
display: none;
|
|
314
|
+
transition: opacity 0.3s ease;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#audio-status.active {
|
|
318
|
+
display: block;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#audio-status .status-icon {
|
|
322
|
+
display: inline-block;
|
|
323
|
+
margin-right: 5px;
|
|
324
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@keyframes pulse {
|
|
328
|
+
0%, 100% { opacity: 0.8; transform: scale(1); }
|
|
329
|
+
50% { opacity: 1; transform: scale(1.1); }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* メニューアイコンのバウンスアニメーション */
|
|
333
|
+
.menu-icon {
|
|
334
|
+
display: inline-block;
|
|
335
|
+
animation: menu-bounce 2s ease-in-out infinite;
|
|
336
|
+
cursor: pointer;
|
|
337
|
+
transition: transform 0.2s ease;
|
|
338
|
+
text-shadow: 0 0 10px currentColor;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.menu-icon:hover {
|
|
342
|
+
transform: scale(1.4);
|
|
343
|
+
animation: menu-bounce-fast 0.8s ease-in-out infinite;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@keyframes menu-bounce {
|
|
347
|
+
0%, 100% {
|
|
348
|
+
filter: brightness(1) drop-shadow(0 0 8px currentColor);
|
|
349
|
+
transform: scale(1) translateY(0px);
|
|
350
|
+
}
|
|
351
|
+
25% {
|
|
352
|
+
filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
|
|
353
|
+
transform: scale(1.1) translateY(-4px);
|
|
354
|
+
}
|
|
355
|
+
50% {
|
|
356
|
+
filter: brightness(1.5) drop-shadow(0 0 20px currentColor);
|
|
357
|
+
transform: scale(1.15) translateY(-8px);
|
|
358
|
+
}
|
|
359
|
+
75% {
|
|
360
|
+
filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
|
|
361
|
+
transform: scale(1.1) translateY(-4px);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@keyframes menu-bounce-fast {
|
|
366
|
+
0%, 100% {
|
|
367
|
+
filter: brightness(1.4) drop-shadow(0 0 15px currentColor);
|
|
368
|
+
transform: translateY(0px);
|
|
369
|
+
}
|
|
370
|
+
50% {
|
|
371
|
+
filter: brightness(1.8) drop-shadow(0 0 25px currentColor);
|
|
372
|
+
transform: translateY(-12px);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.demo-links {
|
|
377
|
+
display: none; /* 統合メニューに移動したため非表示 */
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.demo-link {
|
|
381
|
+
background: linear-gradient(135deg, rgba(0, 102, 204, 0.9) 0%, rgba(0, 34, 102, 0.95) 100%);
|
|
382
|
+
color: white;
|
|
383
|
+
text-decoration: none;
|
|
384
|
+
padding: 12px 16px;
|
|
385
|
+
border-radius: 8px;
|
|
386
|
+
font-size: 12px;
|
|
387
|
+
font-weight: bold;
|
|
388
|
+
border: 2px solid rgba(102, 204, 255, 0.5);
|
|
389
|
+
box-shadow: 0 0 20px rgba(102, 204, 255, 0.3);
|
|
390
|
+
transition: all 0.3s ease;
|
|
391
|
+
backdrop-filter: blur(10px);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.demo-link:hover {
|
|
395
|
+
transform: translateY(-2px);
|
|
396
|
+
box-shadow: 0 5px 25px rgba(102, 204, 255, 0.6);
|
|
397
|
+
border-color: rgba(102, 204, 255, 0.8);
|
|
398
|
+
text-shadow: 0 0 10px #fff;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
canvas {
|
|
402
|
+
display: block;
|
|
403
|
+
}
|
|
404
|
+
</style>
|
|
405
|
+
</head>
|
|
406
|
+
<body>
|
|
407
|
+
<!-- 🎵 音響ステータス表示 -->
|
|
408
|
+
<div id="audio-status">
|
|
409
|
+
<span class="status-icon">🎤</span><span id="audio-text">音声待機中...</span>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<div id="info">
|
|
413
|
+
<button class="close-btn" onclick="hideInfoPanel()">×</button>
|
|
414
|
+
<strong><span class="ocean-title">🌊 ピクセル海底世界</span></strong>
|
|
415
|
+
|
|
416
|
+
<div style="font-size: 11px; opacity: 0.9; margin: 10px 0 12px 0; line-height: 1.6; font-style: italic;">
|
|
417
|
+
ピクセルアートのお魚と泡が、静かに浮かぶ幾何学の海
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<div style="margin: 16px 0 12px 0;">
|
|
421
|
+
<code>@</code>キー または <span class="ocean-icon">🍫</span> で起動<br>
|
|
422
|
+
画像を海底に配置
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<div class="highlight">
|
|
426
|
+
✨ 試してみて:<br>
|
|
427
|
+
「画像を中央に」「右上に配置」<br>
|
|
428
|
+
オブジェクトをクリック → WASDキーで移動
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<!-- 統合メニュー -->
|
|
433
|
+
<div class="unified-menu">
|
|
434
|
+
<button class="menu-toggle" id="unifiedMenuToggle" aria-label="Menu">☰</button>
|
|
435
|
+
<div class="menu-dropdown" id="unifiedMenuDropdown">
|
|
436
|
+
<button class="menu-item" id="showInfoBtn"><span class="menu-icon">📖</span> このワールドについて</button>
|
|
437
|
+
|
|
438
|
+
<div class="menu-item has-submenu">
|
|
439
|
+
<button class="submenu-toggle" id="worldsMenuToggle"><span class="menu-icon">🌍</span> 世界選択 <span style="font-size: 10px;">▼</span></button>
|
|
440
|
+
<div class="submenu" id="worldsSubmenu">
|
|
441
|
+
<a href="../basic/index.html" class="menu-item"><span class="menu-icon">🍫</span> はじまりの国</a>
|
|
442
|
+
<a href="index.html" class="menu-item"><span class="menu-icon">🌊</span> 海底世界(現在地)</a>
|
|
443
|
+
<a href="../music-garden/index.html" class="menu-item"><span class="menu-icon">🌸</span> 硝子の花</a>
|
|
444
|
+
<a href="../space/index.html" class="menu-item"><span class="menu-icon">🌌</span> 宇宙空間</a>
|
|
445
|
+
<a href="../toy-city/index.html" class="menu-item"><span class="menu-icon">🎡</span> AI遊園地</a>
|
|
446
|
+
<a href="../wabi-sabi/index.html" class="menu-item"><span class="menu-icon">🖤</span> 侘び寂びの世界</a>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<a href="../../index.html" class="menu-item"><span class="menu-icon">🏠</span> Home</a>
|
|
451
|
+
<a href="https://github.com/nyukicorn/chocodrop" class="menu-item" target="_blank"><span class="menu-icon">🔗</span> GitHub</a>
|
|
452
|
+
|
|
453
|
+
<button class="menu-item" id="vrMenuButton"><span class="menu-icon">🥽</span> VR</button>
|
|
454
|
+
<button class="menu-item" id="arMenuButton"><span class="menu-icon">🥽</span> AR</button>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
<div class="demo-links">
|
|
459
|
+
<a href="../basic/index.html" class="demo-link">🍫 はじまりの国</a>
|
|
460
|
+
<a href="../music-garden/index.html" class="demo-link">🌸 硝子の花</a>
|
|
461
|
+
<a href="../wabi-sabi/index.html" class="demo-link">🖤 侘び寂び</a>
|
|
462
|
+
<a href="../space/index.html" class="demo-link">🌌 宇宙空間</a>
|
|
463
|
+
<a href="../toy-city/index.html" class="demo-link">🎡 AI遊園地</a>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<!-- Three.js + ChocoDrop (module preload) -->
|
|
467
|
+
<script type="module">
|
|
468
|
+
import ensureChocoDrop from '../../public/load-chocodrop.js';
|
|
469
|
+
|
|
470
|
+
// 🎯 情報パネルを即応できるようにグローバル初期化
|
|
471
|
+
let autoHideTimer = null;
|
|
472
|
+
|
|
473
|
+
const getInfoPanel = () => document.getElementById('info');
|
|
474
|
+
|
|
475
|
+
const clearInfoPanelAutoHide = () => {
|
|
476
|
+
if (autoHideTimer) {
|
|
477
|
+
clearTimeout(autoHideTimer);
|
|
478
|
+
autoHideTimer = null;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const scheduleInfoPanelAutoHide = () => {
|
|
483
|
+
clearInfoPanelAutoHide();
|
|
484
|
+
autoHideTimer = window.setTimeout(() => {
|
|
485
|
+
if (typeof window.hideInfoPanel === 'function') {
|
|
486
|
+
window.hideInfoPanel();
|
|
487
|
+
}
|
|
488
|
+
}, 30000);
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
window.hideInfoPanel = function hideInfoPanel() {
|
|
492
|
+
const infoPanel = getInfoPanel();
|
|
493
|
+
if (!infoPanel) {
|
|
494
|
+
clearInfoPanelAutoHide();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
infoPanel.classList.add('hidden');
|
|
499
|
+
window.setTimeout(() => {
|
|
500
|
+
if (infoPanel.classList.contains('hidden')) {
|
|
501
|
+
infoPanel.style.display = 'none';
|
|
502
|
+
}
|
|
503
|
+
}, 300);
|
|
504
|
+
|
|
505
|
+
clearInfoPanelAutoHide();
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
window.showInfoPanel = function showInfoPanel() {
|
|
509
|
+
const infoPanel = getInfoPanel();
|
|
510
|
+
if (!infoPanel) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
infoPanel.style.display = 'block';
|
|
515
|
+
window.requestAnimationFrame(() => {
|
|
516
|
+
infoPanel.classList.remove('hidden');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
scheduleInfoPanelAutoHide();
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
(async () => {
|
|
523
|
+
await ensureChocoDrop();
|
|
524
|
+
|
|
525
|
+
const THREE = window.THREE;
|
|
526
|
+
const ChocoDrop = window.ChocoDrop;
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
// 統合メニューの制御
|
|
530
|
+
(function() {
|
|
531
|
+
const menuToggle = document.getElementById('unifiedMenuToggle');
|
|
532
|
+
const menuDropdown = document.getElementById('unifiedMenuDropdown');
|
|
533
|
+
const worldsMenuToggle = document.getElementById('worldsMenuToggle');
|
|
534
|
+
const worldsSubmenu = document.getElementById('worldsSubmenu');
|
|
535
|
+
const showInfoBtn = document.getElementById('showInfoBtn');
|
|
536
|
+
const infoPanel = document.getElementById('info');
|
|
537
|
+
|
|
538
|
+
// 初回訪問チェック - localStorage使用
|
|
539
|
+
const FIRST_VISIT_KEY = 'pixel_ocean_visited';
|
|
540
|
+
const hasVisited = localStorage.getItem(FIRST_VISIT_KEY);
|
|
541
|
+
|
|
542
|
+
if (!hasVisited) {
|
|
543
|
+
// 初回訪問時は情報パネルを表示
|
|
544
|
+
if (infoPanel) {
|
|
545
|
+
infoPanel.style.display = 'block';
|
|
546
|
+
}
|
|
547
|
+
localStorage.setItem(FIRST_VISIT_KEY, 'true');
|
|
548
|
+
} else {
|
|
549
|
+
// 2回目以降は非表示
|
|
550
|
+
if (infoPanel) {
|
|
551
|
+
infoPanel.style.display = 'none';
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// 統合メニューのトグル
|
|
556
|
+
if (menuToggle && menuDropdown) {
|
|
557
|
+
menuToggle.addEventListener('click', (e) => {
|
|
558
|
+
e.stopPropagation();
|
|
559
|
+
menuDropdown.classList.toggle('active');
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// 外側クリックで閉じる
|
|
563
|
+
document.addEventListener('click', (e) => {
|
|
564
|
+
if (!menuToggle.contains(e.target) && !menuDropdown.contains(e.target)) {
|
|
565
|
+
menuDropdown.classList.remove('active');
|
|
566
|
+
// サブメニューも閉じる
|
|
567
|
+
if (worldsSubmenu) {
|
|
568
|
+
worldsSubmenu.classList.remove('active');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Escキーで閉じる
|
|
574
|
+
document.addEventListener('keydown', (e) => {
|
|
575
|
+
if (e.key === 'Escape') {
|
|
576
|
+
menuDropdown.classList.remove('active');
|
|
577
|
+
if (worldsSubmenu) {
|
|
578
|
+
worldsSubmenu.classList.remove('active');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 世界選択サブメニューのトグル
|
|
585
|
+
if (worldsMenuToggle && worldsSubmenu) {
|
|
586
|
+
worldsMenuToggle.addEventListener('click', (e) => {
|
|
587
|
+
e.stopPropagation();
|
|
588
|
+
worldsSubmenu.classList.toggle('active');
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// 情報パネル表示ボタン
|
|
593
|
+
if (showInfoBtn) {
|
|
594
|
+
showInfoBtn.addEventListener('click', () => {
|
|
595
|
+
// グローバルに定義された showInfoPanel() を呼び出す
|
|
596
|
+
if (typeof showInfoPanel === 'function') {
|
|
597
|
+
showInfoPanel();
|
|
598
|
+
} else {
|
|
599
|
+
// フォールバック: 直接表示
|
|
600
|
+
const panel = document.getElementById('info');
|
|
601
|
+
if (panel) {
|
|
602
|
+
panel.style.display = 'block';
|
|
603
|
+
panel.classList.remove('hidden');
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
menuDropdown.classList.remove('active');
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// VR/ARボタンは後でThree.jsのrendererと連携
|
|
611
|
+
// グローバル変数として保存
|
|
612
|
+
window.unifiedMenuVRButton = document.getElementById('vrMenuButton');
|
|
613
|
+
window.unifiedMenuARButton = document.getElementById('arMenuButton');
|
|
614
|
+
})();
|
|
615
|
+
|
|
616
|
+
// THREE.js main scene code (moved from global scope to module scope)
|
|
617
|
+
console.log('🌊 Pixel Ocean Loading...');
|
|
618
|
+
console.log('THREE loaded:', typeof THREE !== 'undefined');
|
|
619
|
+
|
|
620
|
+
// Basic Three.js setup
|
|
621
|
+
const scene = new THREE.Scene();
|
|
622
|
+
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
623
|
+
const renderer = new THREE.WebGLRenderer({
|
|
624
|
+
antialias: false, // ピクセルアート風に
|
|
625
|
+
powerPreference: "high-performance"
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
629
|
+
renderer.setPixelRatio(1); // ピクセルアート感を強化
|
|
630
|
+
renderer.shadowMap.enabled = false; // 軽量化のため
|
|
631
|
+
renderer.xr.enabled = true; // VR/AR有効化
|
|
632
|
+
document.body.appendChild(renderer.domElement);
|
|
633
|
+
|
|
634
|
+
// ピクセルアート風のシャープな描画
|
|
635
|
+
renderer.domElement.style.imageRendering = 'pixelated';
|
|
636
|
+
renderer.domElement.style.imageRendering = 'crisp-edges';
|
|
637
|
+
|
|
638
|
+
// VR/ARボタンを統合メニューと連携
|
|
639
|
+
if (typeof VRButton !== 'undefined' && window.unifiedMenuVRButton) {
|
|
640
|
+
console.log('✅ VRButton script loaded - connecting to unified menu');
|
|
641
|
+
|
|
642
|
+
// VRセッション開始関数を作成
|
|
643
|
+
function startVRSession() {
|
|
644
|
+
if ('xr' in navigator) {
|
|
645
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
646
|
+
if (supported) {
|
|
647
|
+
const sessionInit = { optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'layers'] };
|
|
648
|
+
navigator.xr.requestSession('immersive-vr', sessionInit).then(session => {
|
|
649
|
+
session.addEventListener('end', () => {
|
|
650
|
+
renderer.xr.setSession(null);
|
|
651
|
+
});
|
|
652
|
+
renderer.xr.setSession(session);
|
|
653
|
+
});
|
|
654
|
+
} else {
|
|
655
|
+
console.log('VR not supported on this device');
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
window.unifiedMenuVRButton.onclick = startVRSession;
|
|
662
|
+
|
|
663
|
+
// サポートチェック - 未サポートの場合はボタンを非表示に
|
|
664
|
+
if ('xr' in navigator) {
|
|
665
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
666
|
+
if (!supported) {
|
|
667
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
} else {
|
|
671
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
672
|
+
}
|
|
673
|
+
} else {
|
|
674
|
+
console.log('❌ VRButton script not loaded or menu button not found');
|
|
675
|
+
if (window.unifiedMenuVRButton) {
|
|
676
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (typeof ARButton !== 'undefined' && window.unifiedMenuARButton) {
|
|
681
|
+
console.log('✅ ARButton script loaded - connecting to unified menu');
|
|
682
|
+
|
|
683
|
+
// ARセッション開始関数を作成
|
|
684
|
+
function startARSession() {
|
|
685
|
+
if ('xr' in navigator) {
|
|
686
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
687
|
+
if (supported) {
|
|
688
|
+
const sessionInit = {
|
|
689
|
+
requiredFeatures: ['hit-test'],
|
|
690
|
+
optionalFeatures: ['dom-overlay'],
|
|
691
|
+
domOverlay: { root: document.body }
|
|
692
|
+
};
|
|
693
|
+
navigator.xr.requestSession('immersive-ar', sessionInit).then(session => {
|
|
694
|
+
session.addEventListener('end', () => {
|
|
695
|
+
renderer.xr.setSession(null);
|
|
696
|
+
});
|
|
697
|
+
renderer.xr.setSession(session);
|
|
698
|
+
});
|
|
699
|
+
} else {
|
|
700
|
+
console.log('AR not supported on this device');
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
window.unifiedMenuARButton.onclick = startARSession;
|
|
707
|
+
|
|
708
|
+
// サポートチェック - 未サポートの場合はボタンを非表示に
|
|
709
|
+
if ('xr' in navigator) {
|
|
710
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
711
|
+
if (!supported) {
|
|
712
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
} else {
|
|
716
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
717
|
+
}
|
|
718
|
+
} else {
|
|
719
|
+
console.log('❌ ARButton script not loaded or menu button not found');
|
|
720
|
+
if (window.unifiedMenuARButton) {
|
|
721
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// 🌊 お手本の層状グラデーション背景を作成
|
|
726
|
+
function createLayeredOceanBackground() {
|
|
727
|
+
// 背景用の大きなジオメトリ(低ポリゴンでピクセル感)
|
|
728
|
+
const bgGeometry = new THREE.SphereGeometry(200, 16, 8);
|
|
729
|
+
const bgMaterial = new THREE.ShaderMaterial({
|
|
730
|
+
vertexShader: `
|
|
731
|
+
varying vec3 vWorldPosition;
|
|
732
|
+
void main() {
|
|
733
|
+
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
|
734
|
+
vWorldPosition = worldPosition.xyz;
|
|
735
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
736
|
+
}
|
|
737
|
+
`,
|
|
738
|
+
fragmentShader: `
|
|
739
|
+
varying vec3 vWorldPosition;
|
|
740
|
+
void main() {
|
|
741
|
+
// Y座標に基づく垂直グラデーション(お手本風)
|
|
742
|
+
float normalizedY = (vWorldPosition.y + 100.0) / 200.0;
|
|
743
|
+
normalizedY = clamp(normalizedY, 0.0, 1.0);
|
|
744
|
+
|
|
745
|
+
// お手本の色調: 上部明るい青緑 → 下部深い青
|
|
746
|
+
vec3 topColor = vec3(0.4, 0.8, 0.9); // 明るい青緑
|
|
747
|
+
vec3 midColor = vec3(0.2, 0.6, 0.8); // 中間の青
|
|
748
|
+
vec3 bottomColor = vec3(0.1, 0.2, 0.4); // 深い青
|
|
749
|
+
|
|
750
|
+
vec3 color;
|
|
751
|
+
if (normalizedY > 0.6) {
|
|
752
|
+
// 上部: topColor → midColor
|
|
753
|
+
float t = (normalizedY - 0.6) / 0.4;
|
|
754
|
+
color = mix(midColor, topColor, t);
|
|
755
|
+
} else {
|
|
756
|
+
// 下部: bottomColor → midColor
|
|
757
|
+
float t = normalizedY / 0.6;
|
|
758
|
+
color = mix(bottomColor, midColor, t);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
gl_FragColor = vec4(color, 1.0);
|
|
762
|
+
}
|
|
763
|
+
`,
|
|
764
|
+
side: THREE.BackSide
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
const bgMesh = new THREE.Mesh(bgGeometry, bgMaterial);
|
|
768
|
+
scene.add(bgMesh);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
createLayeredOceanBackground();
|
|
772
|
+
|
|
773
|
+
// フォグも青緑系に調整
|
|
774
|
+
scene.fog = new THREE.FogExp2(0x336699, 0.006);
|
|
775
|
+
|
|
776
|
+
// 💡 深海ライティング - 神秘的な光
|
|
777
|
+
const ambientLight = new THREE.AmbientLight(0x66ccff, 0.4);
|
|
778
|
+
scene.add(ambientLight);
|
|
779
|
+
|
|
780
|
+
// 水面からの光(上から斜めに)
|
|
781
|
+
const sunLight = new THREE.DirectionalLight(0x66ddff, 0.8);
|
|
782
|
+
sunLight.position.set(8, 20, 5);
|
|
783
|
+
scene.add(sunLight);
|
|
784
|
+
|
|
785
|
+
// 深海の神秘的な光
|
|
786
|
+
const mysticalLight = new THREE.PointLight(0x00ccff, 0.6, 50);
|
|
787
|
+
mysticalLight.position.set(-10, 8, -5);
|
|
788
|
+
scene.add(mysticalLight);
|
|
789
|
+
|
|
790
|
+
// 🌊 ピクセルアート風の海底(低解像度でピクセル感強化)
|
|
791
|
+
const seabedGeometry = new THREE.PlaneGeometry(100, 100, 8, 8); // より低解像度
|
|
792
|
+
const seabedMaterial = new THREE.MeshLambertMaterial({
|
|
793
|
+
color: 0x1B4F72, // より鮮やかな深い海の色
|
|
794
|
+
flatShading: true
|
|
795
|
+
});
|
|
796
|
+
const seabed = new THREE.Mesh(seabedGeometry, seabedMaterial);
|
|
797
|
+
seabed.rotation.x = -Math.PI / 2;
|
|
798
|
+
seabed.position.y = -8;
|
|
799
|
+
scene.add(seabed);
|
|
800
|
+
|
|
801
|
+
// 🐠 ピクセルアート魚(InstancedMeshで効率的に!)
|
|
802
|
+
function createPixelFish(x, y, z, scale = 1, color = null, fishType = 'fish') {
|
|
803
|
+
const fishGroup = new THREE.Group();
|
|
804
|
+
|
|
805
|
+
// ピクセルアート魚のパターン定義(1=体、2=ひれ、3=目、4=うろこ模様)
|
|
806
|
+
const fishPatterns = {
|
|
807
|
+
small: [
|
|
808
|
+
[0,0,2,2,0,0,0],
|
|
809
|
+
[0,2,1,4,2,0,0],
|
|
810
|
+
[2,1,1,1,1,2,2],
|
|
811
|
+
[1,1,3,4,1,1,1],
|
|
812
|
+
[2,1,1,1,1,2,2],
|
|
813
|
+
[0,2,4,1,2,0,0],
|
|
814
|
+
[0,0,2,2,0,0,0]
|
|
815
|
+
],
|
|
816
|
+
medium: [
|
|
817
|
+
[0,0,0,2,2,0,0,0,0],
|
|
818
|
+
[0,0,2,1,4,2,0,0,0],
|
|
819
|
+
[0,2,1,1,1,4,2,2,0],
|
|
820
|
+
[2,1,4,1,1,1,1,1,2],
|
|
821
|
+
[1,1,3,1,4,1,1,1,1],
|
|
822
|
+
[2,1,1,1,1,1,4,1,2],
|
|
823
|
+
[0,2,1,4,1,1,2,2,0],
|
|
824
|
+
[0,0,2,1,1,2,0,0,0],
|
|
825
|
+
[0,0,0,2,2,0,0,0,0]
|
|
826
|
+
],
|
|
827
|
+
dolphin: [
|
|
828
|
+
[0,0,0,0,2,0,0,0,0,0],
|
|
829
|
+
[0,0,0,2,1,2,0,0,0,0],
|
|
830
|
+
[0,0,2,1,4,1,2,0,0,0],
|
|
831
|
+
[0,2,1,1,1,4,1,2,2,0],
|
|
832
|
+
[2,1,1,3,1,1,4,1,1,2],
|
|
833
|
+
[1,4,1,1,1,1,1,1,4,1],
|
|
834
|
+
[2,1,1,4,1,1,1,4,1,2],
|
|
835
|
+
[0,2,1,1,1,1,1,2,2,0],
|
|
836
|
+
[0,0,2,2,2,2,2,0,0,0]
|
|
837
|
+
]
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// 使用するパターンを選択
|
|
841
|
+
const pattern = fishPatterns[fishType] || fishPatterns.small;
|
|
842
|
+
const pixelSize = 0.15 * scale;
|
|
843
|
+
|
|
844
|
+
// カラーパレット設定(4番目は控えめなうろこ模様)
|
|
845
|
+
const colors = {
|
|
846
|
+
dolphin: [0x4A90E2, 0x5BA3F5, 0x000000, 0x6BB6FF], // 青系+薄い青のうろこ
|
|
847
|
+
medium: [0x40E0D0, 0x48CAE4, 0x000000, 0x66D9EF], // ターコイズ系+薄いターコイズ
|
|
848
|
+
small: [0x6BB6FF, 0x87CEEB, 0x000000, 0x9ACD32] // スカイブルー系+淡い青緑
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const palette = colors[fishType] || colors.small;
|
|
852
|
+
|
|
853
|
+
// InstancedMesh用のジオメトリとマテリアル
|
|
854
|
+
const pixelGeometry = new THREE.BoxGeometry(pixelSize, pixelSize, pixelSize * 0.8);
|
|
855
|
+
const pixelMaterial = new THREE.MeshLambertMaterial({
|
|
856
|
+
flatShading: true
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// ピクセル数をカウント
|
|
860
|
+
let pixelCount = 0;
|
|
861
|
+
pattern.forEach(row => {
|
|
862
|
+
row.forEach(pixel => {
|
|
863
|
+
if (pixel > 0) pixelCount++;
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// InstancedMeshを作成
|
|
868
|
+
const instancedMesh = new THREE.InstancedMesh(
|
|
869
|
+
pixelGeometry,
|
|
870
|
+
pixelMaterial,
|
|
871
|
+
pixelCount
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
// 各ピクセルの位置と色を設定
|
|
875
|
+
let instanceIndex = 0;
|
|
876
|
+
const matrix = new THREE.Matrix4();
|
|
877
|
+
const pixelColor = new THREE.Color();
|
|
878
|
+
|
|
879
|
+
for (let row = 0; row < pattern.length; row++) {
|
|
880
|
+
for (let col = 0; col < pattern[row].length; col++) {
|
|
881
|
+
const pixel = pattern[row][col];
|
|
882
|
+
if (pixel > 0) {
|
|
883
|
+
// 位置を設定
|
|
884
|
+
const px = (col - pattern[row].length / 2) * pixelSize;
|
|
885
|
+
const py = (pattern.length / 2 - row) * pixelSize;
|
|
886
|
+
const pz = 0;
|
|
887
|
+
|
|
888
|
+
matrix.makeTranslation(px, py, pz);
|
|
889
|
+
instancedMesh.setMatrixAt(instanceIndex, matrix);
|
|
890
|
+
|
|
891
|
+
// 色を設定(1=体、2=ひれ、3=目)
|
|
892
|
+
pixelColor.setHex(palette[pixel - 1]);
|
|
893
|
+
instancedMesh.setColorAt(instanceIndex, pixelColor);
|
|
894
|
+
|
|
895
|
+
instanceIndex++;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
901
|
+
instancedMesh.instanceColor.needsUpdate = true;
|
|
902
|
+
|
|
903
|
+
fishGroup.add(instancedMesh);
|
|
904
|
+
|
|
905
|
+
fishGroup.position.set(x, y, z);
|
|
906
|
+
|
|
907
|
+
// アニメーション用データ
|
|
908
|
+
fishGroup.userData = {
|
|
909
|
+
swimSpeed: fishType === 'dolphin' ? 0.008 : 0.015,
|
|
910
|
+
swimRadius: fishType === 'dolphin' ? 12 : 8,
|
|
911
|
+
swimPhase: Math.random() * Math.PI * 2,
|
|
912
|
+
originalY: y,
|
|
913
|
+
originalX: x,
|
|
914
|
+
originalZ: z,
|
|
915
|
+
fishType: fishType
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
scene.add(fishGroup);
|
|
919
|
+
return fishGroup;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
// 🪸 ピクセルアートサンゴ礁(InstancedMesh)
|
|
925
|
+
function createPixelCoral(x, z, scale = 1, type = 'coral1') {
|
|
926
|
+
const coralGroup = new THREE.Group();
|
|
927
|
+
|
|
928
|
+
// サンゴのピクセルパターン
|
|
929
|
+
const coralPatterns = {
|
|
930
|
+
coral1: [
|
|
931
|
+
[0,0,2,0,0],
|
|
932
|
+
[0,2,1,2,0],
|
|
933
|
+
[2,1,1,1,2],
|
|
934
|
+
[0,1,1,1,0],
|
|
935
|
+
[0,1,1,1,0],
|
|
936
|
+
[1,1,1,1,1]
|
|
937
|
+
],
|
|
938
|
+
coral2: [
|
|
939
|
+
[0,2,0,2,0],
|
|
940
|
+
[2,1,2,1,2],
|
|
941
|
+
[1,1,1,1,1],
|
|
942
|
+
[0,1,1,1,0],
|
|
943
|
+
[0,1,1,1,0],
|
|
944
|
+
[1,1,1,1,1]
|
|
945
|
+
],
|
|
946
|
+
coral3: [
|
|
947
|
+
[0,0,2,0,0],
|
|
948
|
+
[0,2,1,2,0],
|
|
949
|
+
[0,1,1,1,0],
|
|
950
|
+
[2,1,1,1,2],
|
|
951
|
+
[1,1,1,1,1],
|
|
952
|
+
[1,1,1,1,1]
|
|
953
|
+
]
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
const pattern = coralPatterns[type] || coralPatterns.coral1;
|
|
957
|
+
const pixelSize = 0.2 * scale;
|
|
958
|
+
|
|
959
|
+
// サンゴの色パレット(海らしい色)
|
|
960
|
+
const colors = [
|
|
961
|
+
0x40E0D0, // ターコイズ(体)
|
|
962
|
+
0x7FFFD4 // アクアマリン(先端・より明るく)
|
|
963
|
+
];
|
|
964
|
+
|
|
965
|
+
// ピクセル数カウント
|
|
966
|
+
let pixelCount = 0;
|
|
967
|
+
pattern.forEach(row => {
|
|
968
|
+
row.forEach(pixel => {
|
|
969
|
+
if (pixel > 0) pixelCount++;
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// InstancedMesh作成
|
|
974
|
+
const pixelGeometry = new THREE.BoxGeometry(pixelSize, pixelSize, pixelSize);
|
|
975
|
+
const pixelMaterial = new THREE.MeshLambertMaterial({
|
|
976
|
+
flatShading: true
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
const instancedMesh = new THREE.InstancedMesh(
|
|
980
|
+
pixelGeometry,
|
|
981
|
+
pixelMaterial,
|
|
982
|
+
pixelCount
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
// 各ピクセルの配置
|
|
986
|
+
let instanceIndex = 0;
|
|
987
|
+
const matrix = new THREE.Matrix4();
|
|
988
|
+
const pixelColor = new THREE.Color();
|
|
989
|
+
|
|
990
|
+
for (let row = 0; row < pattern.length; row++) {
|
|
991
|
+
for (let col = 0; col < pattern[row].length; col++) {
|
|
992
|
+
const pixel = pattern[row][col];
|
|
993
|
+
if (pixel > 0) {
|
|
994
|
+
const px = (col - pattern[row].length / 2) * pixelSize;
|
|
995
|
+
const py = (pattern.length - row - 1) * pixelSize - 8; // 海底に配置
|
|
996
|
+
const pz = 0;
|
|
997
|
+
|
|
998
|
+
matrix.makeTranslation(px, py, pz);
|
|
999
|
+
instancedMesh.setMatrixAt(instanceIndex, matrix);
|
|
1000
|
+
|
|
1001
|
+
pixelColor.setHex(colors[pixel - 1]);
|
|
1002
|
+
instancedMesh.setColorAt(instanceIndex, pixelColor);
|
|
1003
|
+
|
|
1004
|
+
instanceIndex++;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
1010
|
+
instancedMesh.instanceColor.needsUpdate = true;
|
|
1011
|
+
|
|
1012
|
+
coralGroup.add(instancedMesh);
|
|
1013
|
+
coralGroup.position.set(x, 0, z);
|
|
1014
|
+
|
|
1015
|
+
scene.add(coralGroup);
|
|
1016
|
+
return coralGroup;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// 🌿 ピクセルアート海藻
|
|
1020
|
+
function createPixelSeaweed(x, z, height = 4) {
|
|
1021
|
+
const seaweedGroup = new THREE.Group();
|
|
1022
|
+
|
|
1023
|
+
// 海藻のピクセルパターン(縦長)
|
|
1024
|
+
const seaweedPattern = [
|
|
1025
|
+
[0,1,0],
|
|
1026
|
+
[0,1,0],
|
|
1027
|
+
[1,1,1],
|
|
1028
|
+
[0,1,0],
|
|
1029
|
+
[0,1,0],
|
|
1030
|
+
[1,1,1],
|
|
1031
|
+
[0,1,0],
|
|
1032
|
+
[1,1,0]
|
|
1033
|
+
];
|
|
1034
|
+
|
|
1035
|
+
const pixelSize = 0.3;
|
|
1036
|
+
const pixelGeometry = new THREE.BoxGeometry(pixelSize, pixelSize * 1.5, pixelSize * 0.5);
|
|
1037
|
+
|
|
1038
|
+
// 海藻の色(緑系のグラデーション)
|
|
1039
|
+
const colors = [0x2E8B57, 0x3CB371, 0x228B22];
|
|
1040
|
+
const selectedColor = colors[Math.floor(Math.random() * colors.length)];
|
|
1041
|
+
|
|
1042
|
+
const material = new THREE.MeshLambertMaterial({
|
|
1043
|
+
color: selectedColor,
|
|
1044
|
+
flatShading: true,
|
|
1045
|
+
transparent: true,
|
|
1046
|
+
opacity: 0.8
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
// ピクセル配置
|
|
1050
|
+
seaweedPattern.forEach((row, rowIndex) => {
|
|
1051
|
+
row.forEach((pixel, colIndex) => {
|
|
1052
|
+
if (pixel > 0) {
|
|
1053
|
+
const mesh = new THREE.Mesh(pixelGeometry, material);
|
|
1054
|
+
mesh.position.set(
|
|
1055
|
+
(colIndex - 1) * pixelSize * 0.8,
|
|
1056
|
+
rowIndex * pixelSize * 1.2 - 8 + height,
|
|
1057
|
+
0
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
// 少し揺れるような傾き
|
|
1061
|
+
mesh.rotation.z = Math.sin(rowIndex * 0.5) * 0.1;
|
|
1062
|
+
|
|
1063
|
+
seaweedGroup.add(mesh);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
seaweedGroup.position.set(x, 0, z);
|
|
1069
|
+
seaweedGroup.rotation.y = Math.random() * Math.PI * 2;
|
|
1070
|
+
|
|
1071
|
+
// アニメーション用データ
|
|
1072
|
+
seaweedGroup.userData = {
|
|
1073
|
+
swaySpeed: 0.001 + Math.random() * 0.002,
|
|
1074
|
+
swayAmount: 0.05 + Math.random() * 0.05,
|
|
1075
|
+
originalRotation: seaweedGroup.rotation.z
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
scene.add(seaweedGroup);
|
|
1079
|
+
return seaweedGroup;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// 🫧 泡パーティクルシステム
|
|
1083
|
+
function createBubbleParticles() {
|
|
1084
|
+
const bubbleCount = 300;
|
|
1085
|
+
const geometry = new THREE.BufferGeometry();
|
|
1086
|
+
const positions = new Float32Array(bubbleCount * 3);
|
|
1087
|
+
const scales = new Float32Array(bubbleCount);
|
|
1088
|
+
const velocities = new Float32Array(bubbleCount * 3);
|
|
1089
|
+
|
|
1090
|
+
for (let i = 0; i < bubbleCount; i++) {
|
|
1091
|
+
const i3 = i * 3;
|
|
1092
|
+
|
|
1093
|
+
positions[i3] = (Math.random() - 0.5) * 80; // x
|
|
1094
|
+
positions[i3 + 1] = Math.random() * 40 - 20; // y
|
|
1095
|
+
positions[i3 + 2] = (Math.random() - 0.5) * 80; // z
|
|
1096
|
+
|
|
1097
|
+
scales[i] = Math.random() * 0.5 + 0.2;
|
|
1098
|
+
|
|
1099
|
+
// 上昇速度
|
|
1100
|
+
velocities[i3] = (Math.random() - 0.5) * 0.01; // x drift
|
|
1101
|
+
velocities[i3 + 1] = 0.02 + Math.random() * 0.03; // y (up)
|
|
1102
|
+
velocities[i3 + 2] = (Math.random() - 0.5) * 0.01; // z drift
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
1106
|
+
geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1));
|
|
1107
|
+
|
|
1108
|
+
// 円形テクスチャを作成して泡を丸くする
|
|
1109
|
+
const canvas = document.createElement('canvas');
|
|
1110
|
+
canvas.width = 32;
|
|
1111
|
+
canvas.height = 32;
|
|
1112
|
+
const ctx = canvas.getContext('2d');
|
|
1113
|
+
const gradient = ctx.createRadialGradient(16, 16, 0, 16, 16, 16);
|
|
1114
|
+
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
|
|
1115
|
+
gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.5)');
|
|
1116
|
+
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
|
1117
|
+
ctx.fillStyle = gradient;
|
|
1118
|
+
ctx.fillRect(0, 0, 32, 32);
|
|
1119
|
+
|
|
1120
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
1121
|
+
|
|
1122
|
+
const material = new THREE.PointsMaterial({
|
|
1123
|
+
color: 0x87CEEB, // より鮮やかなスカイブルー
|
|
1124
|
+
size: 1.2, // 少し大きめでピクセル感
|
|
1125
|
+
sizeAttenuation: true,
|
|
1126
|
+
transparent: true,
|
|
1127
|
+
opacity: 0.8, // より見やすく
|
|
1128
|
+
blending: THREE.AdditiveBlending,
|
|
1129
|
+
map: texture // 円形テクスチャを適用
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
const bubbles = new THREE.Points(geometry, material);
|
|
1133
|
+
bubbles.userData = { velocities };
|
|
1134
|
+
scene.add(bubbles);
|
|
1135
|
+
return bubbles;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// 🏔️ 自然な岩場と地形
|
|
1139
|
+
function createDistantTerrain() {
|
|
1140
|
+
// 低ポリゴンの岩を作成
|
|
1141
|
+
function createRock(x, y, z, size) {
|
|
1142
|
+
const rockGeometry = new THREE.DodecahedronGeometry(size, 0); // 12面体で岩らしさ
|
|
1143
|
+
const rockMaterial = new THREE.MeshLambertMaterial({
|
|
1144
|
+
color: 0x1B4F72, // 深い海の青
|
|
1145
|
+
flatShading: true,
|
|
1146
|
+
transparent: true,
|
|
1147
|
+
opacity: 0.7
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
const rock = new THREE.Mesh(rockGeometry, rockMaterial);
|
|
1151
|
+
rock.position.set(x, y, z);
|
|
1152
|
+
|
|
1153
|
+
// ランダムに回転させて自然な見た目に
|
|
1154
|
+
rock.rotation.x = Math.random() * Math.PI;
|
|
1155
|
+
rock.rotation.y = Math.random() * Math.PI;
|
|
1156
|
+
rock.scale.y = 0.5 + Math.random() * 0.5; // 高さをランダムに
|
|
1157
|
+
|
|
1158
|
+
return rock;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// 複数の岩を配置
|
|
1162
|
+
const rockPositions = [
|
|
1163
|
+
{x: -60, y: -5, z: -40, size: 8},
|
|
1164
|
+
{x: -40, y: -6, z: -45, size: 6},
|
|
1165
|
+
{x: 40, y: -4, z: -50, size: 10},
|
|
1166
|
+
{x: 65, y: -7, z: -35, size: 5},
|
|
1167
|
+
{x: 0, y: -3, z: -55, size: 12},
|
|
1168
|
+
{x: -30, y: -6, z: -42, size: 7},
|
|
1169
|
+
{x: 50, y: -5, z: -48, size: 9}
|
|
1170
|
+
];
|
|
1171
|
+
|
|
1172
|
+
rockPositions.forEach(rock => {
|
|
1173
|
+
scene.add(createRock(rock.x, rock.y, rock.z, rock.size));
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
createDistantTerrain();
|
|
1178
|
+
|
|
1179
|
+
// 🌊 海底世界を構築
|
|
1180
|
+
const fishes = [];
|
|
1181
|
+
const corals = [];
|
|
1182
|
+
const seaweeds = [];
|
|
1183
|
+
|
|
1184
|
+
// ピクセルアート魚群を配置
|
|
1185
|
+
// 大きなイルカ群(美しい青)
|
|
1186
|
+
fishes.push(createPixelFish(-15, 2, -10, 3, null, 'dolphin'));
|
|
1187
|
+
fishes.push(createPixelFish(20, 5, -15, 2.8, null, 'dolphin'));
|
|
1188
|
+
fishes.push(createPixelFish(-25, -2, 5, 3.2, null, 'dolphin'));
|
|
1189
|
+
|
|
1190
|
+
// 中型魚群
|
|
1191
|
+
fishes.push(createPixelFish(10, -1, -8, 2, null, 'medium'));
|
|
1192
|
+
fishes.push(createPixelFish(-8, 3, -12, 2.2, null, 'medium'));
|
|
1193
|
+
fishes.push(createPixelFish(15, 6, -5, 1.8, null, 'medium'));
|
|
1194
|
+
fishes.push(createPixelFish(-20, -3, -2, 2, null, 'medium'));
|
|
1195
|
+
|
|
1196
|
+
// 小魚の群れ
|
|
1197
|
+
for (let i = 0; i < 8; i++) {
|
|
1198
|
+
const angle = (i / 8) * Math.PI * 2;
|
|
1199
|
+
const radius = 15 + Math.random() * 10;
|
|
1200
|
+
fishes.push(createPixelFish(
|
|
1201
|
+
Math.cos(angle) * radius,
|
|
1202
|
+
Math.random() * 6 - 3,
|
|
1203
|
+
Math.sin(angle) * radius,
|
|
1204
|
+
1 + Math.random() * 0.5,
|
|
1205
|
+
null,
|
|
1206
|
+
'small'
|
|
1207
|
+
));
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// サンゴ礁を配置(青緑系に統一)
|
|
1211
|
+
const coralPositions = [
|
|
1212
|
+
[-15, -12], [10, -18], [-25, 8], [20, 15],
|
|
1213
|
+
[8, -25], [-30, -5], [25, -8], [-8, 20]
|
|
1214
|
+
];
|
|
1215
|
+
coralPositions.forEach((pos, index) => {
|
|
1216
|
+
// 3種類のサンゴをランダムに配置
|
|
1217
|
+
const types = ['coral1', 'coral2', 'coral3'];
|
|
1218
|
+
const type = types[index % 3];
|
|
1219
|
+
const scale = 1 + Math.random() * 0.5;
|
|
1220
|
+
corals.push(createPixelCoral(pos[0], pos[1], scale, type));
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
// 海藻の森を配置
|
|
1224
|
+
for (let i = 0; i < 20; i++) {
|
|
1225
|
+
const x = (Math.random() - 0.5) * 80;
|
|
1226
|
+
const z = (Math.random() - 0.5) * 80;
|
|
1227
|
+
const height = 3 + Math.random() * 5;
|
|
1228
|
+
seaweeds.push(createPixelSeaweed(x, z, height));
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// 泡パーティクル
|
|
1232
|
+
const bubbles = createBubbleParticles();
|
|
1233
|
+
|
|
1234
|
+
// ✨ 水中の光粒子(キラキラエフェクト)
|
|
1235
|
+
function createLightParticles() {
|
|
1236
|
+
const particleCount = 200;
|
|
1237
|
+
const geometry = new THREE.BufferGeometry();
|
|
1238
|
+
const positions = new Float32Array(particleCount * 3);
|
|
1239
|
+
const colors = new Float32Array(particleCount * 3);
|
|
1240
|
+
const sizes = new Float32Array(particleCount);
|
|
1241
|
+
|
|
1242
|
+
const lightColors = [
|
|
1243
|
+
new THREE.Color(0xFFFFFF), // 純白
|
|
1244
|
+
new THREE.Color(0xE0FFFF), // 水色
|
|
1245
|
+
new THREE.Color(0xF0F8FF), // アリスブルー
|
|
1246
|
+
new THREE.Color(0xFFF8DC) // コーンシルク(暖かい白)
|
|
1247
|
+
];
|
|
1248
|
+
|
|
1249
|
+
for (let i = 0; i < particleCount; i++) {
|
|
1250
|
+
const i3 = i * 3;
|
|
1251
|
+
|
|
1252
|
+
// ランダムな位置
|
|
1253
|
+
positions[i3] = (Math.random() - 0.5) * 60;
|
|
1254
|
+
positions[i3 + 1] = Math.random() * 30 - 10;
|
|
1255
|
+
positions[i3 + 2] = (Math.random() - 0.5) * 60;
|
|
1256
|
+
|
|
1257
|
+
// ランダムな色
|
|
1258
|
+
const color = lightColors[Math.floor(Math.random() * lightColors.length)];
|
|
1259
|
+
colors[i3] = color.r;
|
|
1260
|
+
colors[i3 + 1] = color.g;
|
|
1261
|
+
colors[i3 + 2] = color.b;
|
|
1262
|
+
|
|
1263
|
+
// サイズ(もっと小さく控えめに)
|
|
1264
|
+
sizes[i] = Math.random() * 0.3 + 0.1;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
1268
|
+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
1269
|
+
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
|
1270
|
+
|
|
1271
|
+
// 円形テクスチャを作成して光粒子を丸くする(キラキラ輝く光)
|
|
1272
|
+
const canvas = document.createElement('canvas');
|
|
1273
|
+
canvas.width = 32;
|
|
1274
|
+
canvas.height = 32;
|
|
1275
|
+
const ctx = canvas.getContext('2d');
|
|
1276
|
+
|
|
1277
|
+
// 背景を完全に透明にクリア
|
|
1278
|
+
ctx.clearRect(0, 0, 32, 32);
|
|
1279
|
+
|
|
1280
|
+
// 鋭く強く輝く中心
|
|
1281
|
+
const gradient = ctx.createRadialGradient(16, 16, 0, 16, 16, 16);
|
|
1282
|
+
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)'); // 中心は真っ白
|
|
1283
|
+
gradient.addColorStop(0.2, 'rgba(255, 255, 255, 0.9)'); // 強い輝き
|
|
1284
|
+
gradient.addColorStop(0.4, 'rgba(255, 255, 255, 0.6)'); // まだ明るい
|
|
1285
|
+
gradient.addColorStop(0.7, 'rgba(255, 255, 255, 0.3)'); // 徐々にフェード
|
|
1286
|
+
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); // 完全に透明
|
|
1287
|
+
ctx.fillStyle = gradient;
|
|
1288
|
+
ctx.fillRect(0, 0, 32, 32);
|
|
1289
|
+
|
|
1290
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
1291
|
+
|
|
1292
|
+
const material = new THREE.PointsMaterial({
|
|
1293
|
+
size: 1, // 3から1に縮小
|
|
1294
|
+
sizeAttenuation: true,
|
|
1295
|
+
vertexColors: true,
|
|
1296
|
+
transparent: true,
|
|
1297
|
+
opacity: 0.3, // 0.9から0.3に減少
|
|
1298
|
+
blending: THREE.AdditiveBlending,
|
|
1299
|
+
map: texture // 円形テクスチャを適用
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
const lightParticles = new THREE.Points(geometry, material);
|
|
1303
|
+
lightParticles.userData = {
|
|
1304
|
+
twinkleSpeed: new Float32Array(particleCount).map(() => Math.random() * 0.05 + 0.01)
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
scene.add(lightParticles);
|
|
1308
|
+
return lightParticles;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const lightParticles = createLightParticles();
|
|
1312
|
+
|
|
1313
|
+
// 📷 カメラ位置(水中感を演出)
|
|
1314
|
+
camera.position.set(0, 0, 15);
|
|
1315
|
+
camera.lookAt(0, -3, 0);
|
|
1316
|
+
|
|
1317
|
+
// 🎮 コントロール
|
|
1318
|
+
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
1319
|
+
controls.enableDamping = true;
|
|
1320
|
+
controls.dampingFactor = 0.03; // より滑らかな水中感
|
|
1321
|
+
controls.minDistance = 5;
|
|
1322
|
+
controls.maxDistance = 50;
|
|
1323
|
+
controls.maxPolarAngle = Math.PI;
|
|
1324
|
+
|
|
1325
|
+
// 🎵 高度な音響システムの初期化(wabi-sabi方式)
|
|
1326
|
+
let audioContext = null;
|
|
1327
|
+
let analyser = null;
|
|
1328
|
+
let dataArray = null;
|
|
1329
|
+
let beatDetectionHistory = [];
|
|
1330
|
+
let lastBeatTime = 0;
|
|
1331
|
+
let musicData = {
|
|
1332
|
+
volume: 0,
|
|
1333
|
+
bass: 0,
|
|
1334
|
+
mid: 0,
|
|
1335
|
+
treble: 0,
|
|
1336
|
+
tempo: 0,
|
|
1337
|
+
bpm: 0,
|
|
1338
|
+
beatDetected: false,
|
|
1339
|
+
isPlaying: false
|
|
1340
|
+
};
|
|
1341
|
+
const audioStatusEl = document.getElementById('audio-status');
|
|
1342
|
+
const audioTextEl = document.getElementById('audio-text');
|
|
1343
|
+
|
|
1344
|
+
function setupAudioAnalysis() {
|
|
1345
|
+
if (!audioContext) {
|
|
1346
|
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
1347
|
+
analyser = audioContext.createAnalyser();
|
|
1348
|
+
analyser.fftSize = 256;
|
|
1349
|
+
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
1350
|
+
console.log('🎵 Advanced audio analysis initialized');
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// 動画要素を音響分析に接続
|
|
1355
|
+
function connectVideoToAudio(videoElement) {
|
|
1356
|
+
if (!audioContext || !analyser) {
|
|
1357
|
+
setupAudioAnalysis();
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
try {
|
|
1361
|
+
// AudioContextがsuspended状態の場合は再開
|
|
1362
|
+
if (audioContext.state === 'suspended') {
|
|
1363
|
+
audioContext.resume();
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const source = audioContext.createMediaElementSource(videoElement);
|
|
1367
|
+
source.connect(analyser);
|
|
1368
|
+
analyser.connect(audioContext.destination);
|
|
1369
|
+
|
|
1370
|
+
musicData.isPlaying = true;
|
|
1371
|
+
if (audioStatusEl) {
|
|
1372
|
+
audioStatusEl.classList.add('active');
|
|
1373
|
+
if (audioTextEl) {
|
|
1374
|
+
audioTextEl.textContent = '音響連動中 ♪ (BPM検出中)';
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
console.log('🎵 Video connected to advanced audio analyser');
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
console.warn('Audio connection error:', error);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// 音楽データの詳細解析
|
|
1384
|
+
function analyzeMusicData() {
|
|
1385
|
+
if (!analyser || !dataArray) return;
|
|
1386
|
+
|
|
1387
|
+
analyser.getByteFrequencyData(dataArray);
|
|
1388
|
+
|
|
1389
|
+
// 音量計算
|
|
1390
|
+
let sum = 0;
|
|
1391
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
1392
|
+
sum += dataArray[i];
|
|
1393
|
+
}
|
|
1394
|
+
musicData.volume = sum / dataArray.length / 255;
|
|
1395
|
+
|
|
1396
|
+
// 周波数帯域分析
|
|
1397
|
+
const bassRange = Math.floor(dataArray.length * 0.1);
|
|
1398
|
+
const midRange = Math.floor(dataArray.length * 0.5);
|
|
1399
|
+
|
|
1400
|
+
let bassSum = 0, midSum = 0, trebleSum = 0;
|
|
1401
|
+
|
|
1402
|
+
for (let i = 0; i < bassRange; i++) {
|
|
1403
|
+
bassSum += dataArray[i];
|
|
1404
|
+
}
|
|
1405
|
+
for (let i = bassRange; i < midRange; i++) {
|
|
1406
|
+
midSum += dataArray[i];
|
|
1407
|
+
}
|
|
1408
|
+
for (let i = midRange; i < dataArray.length; i++) {
|
|
1409
|
+
trebleSum += dataArray[i];
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
musicData.bass = bassSum / bassRange / 255;
|
|
1413
|
+
musicData.mid = midSum / (midRange - bassRange) / 255;
|
|
1414
|
+
musicData.treble = trebleSum / (dataArray.length - midRange) / 255;
|
|
1415
|
+
|
|
1416
|
+
// 🥁 ビート検出とBPM計算
|
|
1417
|
+
const currentTime = performance.now();
|
|
1418
|
+
const bassThreshold = 0.3;
|
|
1419
|
+
const volumeThreshold = 0.2;
|
|
1420
|
+
|
|
1421
|
+
musicData.beatDetected = false;
|
|
1422
|
+
|
|
1423
|
+
// ビート検出(ベース音量の急激な変化)
|
|
1424
|
+
if (musicData.bass > bassThreshold && musicData.volume > volumeThreshold) {
|
|
1425
|
+
if (currentTime - lastBeatTime > 300) { // 最小300ms間隔
|
|
1426
|
+
musicData.beatDetected = true;
|
|
1427
|
+
lastBeatTime = currentTime;
|
|
1428
|
+
|
|
1429
|
+
// BPM計算用履歴更新
|
|
1430
|
+
beatDetectionHistory.push(currentTime);
|
|
1431
|
+
if (beatDetectionHistory.length > 8) {
|
|
1432
|
+
beatDetectionHistory.shift();
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// BPM計算
|
|
1436
|
+
if (beatDetectionHistory.length >= 4) {
|
|
1437
|
+
const intervals = [];
|
|
1438
|
+
for (let i = 1; i < beatDetectionHistory.length; i++) {
|
|
1439
|
+
intervals.push(beatDetectionHistory[i] - beatDetectionHistory[i-1]);
|
|
1440
|
+
}
|
|
1441
|
+
const avgInterval = intervals.reduce((a, b) => a + b) / intervals.length;
|
|
1442
|
+
musicData.bpm = Math.round(60000 / avgInterval);
|
|
1443
|
+
|
|
1444
|
+
// ステータス更新
|
|
1445
|
+
if (audioTextEl && musicData.bpm > 0) {
|
|
1446
|
+
audioTextEl.textContent = `音響連動中 ♪ (${musicData.bpm} BPM)`;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// ChocoDrop のビデオ要素を監視して自動接続
|
|
1454
|
+
function observeVideoElements() {
|
|
1455
|
+
const videoObserver = new MutationObserver((mutations) => {
|
|
1456
|
+
mutations.forEach((mutation) => {
|
|
1457
|
+
mutation.addedNodes.forEach((node) => {
|
|
1458
|
+
if (node.nodeName === 'VIDEO') {
|
|
1459
|
+
console.log('🎬 Video element detected, connecting to audio...');
|
|
1460
|
+
connectVideoToAudio(node);
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
});
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
videoObserver.observe(document.body, {
|
|
1467
|
+
childList: true,
|
|
1468
|
+
subtree: true
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
// 既存のビデオ要素もチェック
|
|
1472
|
+
const existingVideos = document.querySelectorAll('video');
|
|
1473
|
+
existingVideos.forEach(video => {
|
|
1474
|
+
connectVideoToAudio(video);
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// 初期化
|
|
1479
|
+
if (audioStatusEl) {
|
|
1480
|
+
audioStatusEl.classList.add('active');
|
|
1481
|
+
}
|
|
1482
|
+
observeVideoElements();
|
|
1483
|
+
|
|
1484
|
+
// 🍫 ChocoDrop初期化
|
|
1485
|
+
let chocoDrop;
|
|
1486
|
+
|
|
1487
|
+
function initChocoDrop() {
|
|
1488
|
+
try {
|
|
1489
|
+
chocoDrop = ChocoDrop.createChocoDrop(scene, {
|
|
1490
|
+
camera: camera,
|
|
1491
|
+
renderer: renderer,
|
|
1492
|
+
// GitHub Pagesなので生成機能なし、インポートのみ
|
|
1493
|
+
serverUrl: null,
|
|
1494
|
+
enableServerHealthCheck: false, // サーバーレス環境なのでヘルスチェック無効
|
|
1495
|
+
skipServiceDialog: true, // GitHub Pages用:サービス設定ダイアログを無効化
|
|
1496
|
+
sceneOptions: {
|
|
1497
|
+
enableMouseInteraction: true
|
|
1498
|
+
},
|
|
1499
|
+
onControlsToggle: (disabled) => {
|
|
1500
|
+
controls.enabled = !disabled;
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
console.log('🌊 Pixel Ocean ready! Press @ to dive in!');
|
|
1505
|
+
|
|
1506
|
+
// 海底風のヒント
|
|
1507
|
+
window.addEventListener('keydown', (event) => {
|
|
1508
|
+
if (event.code === 'Space' || event.key === '@') {
|
|
1509
|
+
console.log('🐠 Try: "画像を中央に", "右上に配置", "ファイルを選択"');
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
console.error('ChocoDrop initialization failed:', error);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// 初期化
|
|
1519
|
+
setTimeout(initChocoDrop, 1000);
|
|
1520
|
+
|
|
1521
|
+
// 🎬 アニメーションループ
|
|
1522
|
+
function animate() {
|
|
1523
|
+
requestAnimationFrame(animate);
|
|
1524
|
+
|
|
1525
|
+
const time = Date.now() * 0.001;
|
|
1526
|
+
|
|
1527
|
+
// 🎵 高度な音響データ解析
|
|
1528
|
+
if (musicData.isPlaying) {
|
|
1529
|
+
analyzeMusicData();
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// 音響連動の強度(海底世界は中程度の反応 + ビート反応)
|
|
1533
|
+
const audioIntensity = musicData.volume * 0.8;
|
|
1534
|
+
const bassBoost = musicData.bass * 1.5;
|
|
1535
|
+
const midBoost = musicData.mid * 1.2;
|
|
1536
|
+
const trebleBoost = musicData.treble * 1.0;
|
|
1537
|
+
const beatPulse = musicData.beatDetected ? 1.3 : 1.0; // ビート時に反応
|
|
1538
|
+
|
|
1539
|
+
// 魚の泳ぎアニメーション + 音響連動 + ビート反応
|
|
1540
|
+
fishes.forEach(fish => {
|
|
1541
|
+
if (fish.userData) {
|
|
1542
|
+
const data = fish.userData;
|
|
1543
|
+
const swimSpeedMultiplier = (1 + audioIntensity * 0.7) * beatPulse; // 音楽で泳ぎ速度up + ビート時にダッシュ
|
|
1544
|
+
fish.position.x = data.originalX + Math.sin(time * data.swimSpeed * swimSpeedMultiplier + data.swimPhase) * data.swimRadius * (1 + bassBoost * 0.2);
|
|
1545
|
+
fish.position.y = data.originalY + Math.sin(time * data.swimSpeed * 0.5 * swimSpeedMultiplier) * 2 * (1 + midBoost * 0.3) * beatPulse;
|
|
1546
|
+
fish.position.z = data.originalZ + Math.cos(time * data.swimSpeed * swimSpeedMultiplier + data.swimPhase) * data.swimRadius * 0.5;
|
|
1547
|
+
|
|
1548
|
+
// 向きを進行方向に
|
|
1549
|
+
const dx = Math.cos(time * data.swimSpeed * swimSpeedMultiplier + data.swimPhase) * data.swimRadius;
|
|
1550
|
+
fish.rotation.y = Math.atan2(dx, 0) + Math.PI / 2;
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
// 海藻の揺らぎ + 音響連動 + ビート反応
|
|
1555
|
+
seaweeds.forEach(seaweed => {
|
|
1556
|
+
seaweed.children.forEach(segment => {
|
|
1557
|
+
if (segment.userData) {
|
|
1558
|
+
const data = segment.userData;
|
|
1559
|
+
const swayIntensity = (1 + bassBoost * 0.8) * beatPulse; // 低音で大きく揺れる + ビート時に激しく
|
|
1560
|
+
const sway = Math.sin(time * data.swaySpeed * (1 + audioIntensity * 0.5) + data.swayPhase) * 0.3 * swayIntensity;
|
|
1561
|
+
segment.rotation.z = data.swayAmount * 0.3 + sway;
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
// 光粒子のキラキラアニメーション + 音響連動 + ビート反応
|
|
1567
|
+
if (lightParticles) {
|
|
1568
|
+
const time2 = time * 2;
|
|
1569
|
+
const sizes = lightParticles.geometry.attributes.size.array;
|
|
1570
|
+
const twinkleSpeed = lightParticles.userData.twinkleSpeed;
|
|
1571
|
+
|
|
1572
|
+
for (let i = 0; i < sizes.length; i++) {
|
|
1573
|
+
// サイズを変化させてキラキラ感を演出 + 音響で強化 + ビート時に光る
|
|
1574
|
+
const baseSize = (Math.sin(time2 * twinkleSpeed[i] * (1 + trebleBoost * 0.5)) * 0.5 + 0.5) * 0.4 + 0.1;
|
|
1575
|
+
sizes[i] = baseSize * (1 + trebleBoost * 0.8) * beatPulse; // 高音で光粒子が大きく輝く + ビート時にさらに輝く
|
|
1576
|
+
}
|
|
1577
|
+
lightParticles.geometry.attributes.size.needsUpdate = true;
|
|
1578
|
+
|
|
1579
|
+
// ゆっくり回転 + 音響連動 + ビート時に速く
|
|
1580
|
+
lightParticles.rotation.y += 0.0001 * (1 + audioIntensity * 2) * beatPulse;
|
|
1581
|
+
|
|
1582
|
+
// 音楽に合わせて明るさ変化 + ビート時に明るく
|
|
1583
|
+
if (lightParticles.material && lightParticles.material.opacity !== undefined) {
|
|
1584
|
+
lightParticles.material.opacity = (0.3 + trebleBoost * 0.4) * beatPulse;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// 泡の上昇アニメーション + 音響連動 + ビート反応
|
|
1589
|
+
if (bubbles && bubbles.userData.velocities) {
|
|
1590
|
+
const positions = bubbles.geometry.attributes.position.array;
|
|
1591
|
+
const velocities = bubbles.userData.velocities;
|
|
1592
|
+
const bubbleSpeedMultiplier = (1 + midBoost * 1.5) * beatPulse; // 中音で泡の上昇速度up + ビート時に急上昇
|
|
1593
|
+
|
|
1594
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
1595
|
+
positions[i] += velocities[i] * bubbleSpeedMultiplier; // x
|
|
1596
|
+
positions[i + 1] += velocities[i + 1] * bubbleSpeedMultiplier; // y
|
|
1597
|
+
positions[i + 2] += velocities[i + 2] * bubbleSpeedMultiplier; // z
|
|
1598
|
+
|
|
1599
|
+
// 泡が水面に達したらリセット
|
|
1600
|
+
if (positions[i + 1] > 25) {
|
|
1601
|
+
positions[i] = (Math.random() - 0.5) * 80;
|
|
1602
|
+
positions[i + 1] = -20;
|
|
1603
|
+
positions[i + 2] = (Math.random() - 0.5) * 80;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
bubbles.geometry.attributes.position.needsUpdate = true;
|
|
1607
|
+
|
|
1608
|
+
// 音楽に合わせて明るさ変化 + ビート時に明るく
|
|
1609
|
+
if (bubbles.material && bubbles.material.opacity !== undefined) {
|
|
1610
|
+
bubbles.material.opacity = (0.8 + audioIntensity * 0.2) * beatPulse;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
controls.update();
|
|
1615
|
+
renderer.render(scene, camera);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
animate();
|
|
1619
|
+
|
|
1620
|
+
// 📱 レスポンシブ対応
|
|
1621
|
+
window.addEventListener('resize', () => {
|
|
1622
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
1623
|
+
camera.updateProjectionMatrix();
|
|
1624
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
// 30秒後の自動非表示タイマー開始
|
|
1628
|
+
scheduleInfoPanelAutoHide();
|
|
1629
|
+
|
|
1630
|
+
console.log('✅ Pixel Ocean World Ready! 🌊');
|
|
1631
|
+
|
|
1632
|
+
})();
|
|
1633
|
+
</script>
|
|
1634
|
+
|
|
1635
|
+
<!-- VR/AR Button inline implementation -->
|
|
1636
|
+
<script>
|
|
1637
|
+
// VRButton implementation
|
|
1638
|
+
const VRButton = {
|
|
1639
|
+
createButton: function(renderer) {
|
|
1640
|
+
const button = document.createElement('button');
|
|
1641
|
+
|
|
1642
|
+
function showEnterVR() {
|
|
1643
|
+
button.style.display = '';
|
|
1644
|
+
button.style.cursor = 'pointer';
|
|
1645
|
+
button.style.left = 'calc(50% - 50px)';
|
|
1646
|
+
button.style.width = '100px';
|
|
1647
|
+
button.textContent = 'ENTER VR';
|
|
1648
|
+
button.onmouseenter = function() { button.style.opacity = '1.0'; };
|
|
1649
|
+
button.onmouseleave = function() { button.style.opacity = '0.5'; };
|
|
1650
|
+
button.onclick = function() {
|
|
1651
|
+
const session = renderer.xr.getSession();
|
|
1652
|
+
if (session) {
|
|
1653
|
+
session.end();
|
|
1654
|
+
} else {
|
|
1655
|
+
navigator.xr.requestSession('immersive-vr', {
|
|
1656
|
+
optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking']
|
|
1657
|
+
}).then(session => {
|
|
1658
|
+
renderer.xr.setSession(session);
|
|
1659
|
+
button.textContent = 'EXIT VR';
|
|
1660
|
+
|
|
1661
|
+
session.addEventListener('end', () => {
|
|
1662
|
+
button.textContent = 'ENTER VR';
|
|
1663
|
+
});
|
|
1664
|
+
}).catch(err => {
|
|
1665
|
+
console.error('VR session failed:', err);
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function showWebXRNotFound() {
|
|
1672
|
+
button.style.display = '';
|
|
1673
|
+
button.style.cursor = 'auto';
|
|
1674
|
+
button.style.left = 'calc(50% - 75px)';
|
|
1675
|
+
button.style.width = '150px';
|
|
1676
|
+
button.textContent = 'VR NOT SUPPORTED';
|
|
1677
|
+
button.onmouseenter = null;
|
|
1678
|
+
button.onmouseleave = null;
|
|
1679
|
+
button.onclick = null;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
button.style.position = 'absolute';
|
|
1683
|
+
button.style.bottom = '20px';
|
|
1684
|
+
button.style.padding = '12px 6px';
|
|
1685
|
+
button.style.border = '1px solid #fff';
|
|
1686
|
+
button.style.borderRadius = '4px';
|
|
1687
|
+
button.style.background = 'rgba(0,0,0,0.1)';
|
|
1688
|
+
button.style.color = '#fff';
|
|
1689
|
+
button.style.font = 'normal 13px sans-serif';
|
|
1690
|
+
button.style.textAlign = 'center';
|
|
1691
|
+
button.style.opacity = '0.5';
|
|
1692
|
+
button.style.outline = 'none';
|
|
1693
|
+
button.style.zIndex = '999';
|
|
1694
|
+
|
|
1695
|
+
if ('xr' in navigator) {
|
|
1696
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
1697
|
+
supported ? showEnterVR() : showWebXRNotFound();
|
|
1698
|
+
});
|
|
1699
|
+
} else {
|
|
1700
|
+
showWebXRNotFound();
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
return button;
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
|
|
1707
|
+
// ARButton implementation
|
|
1708
|
+
const ARButton = {
|
|
1709
|
+
createButton: function(renderer, options) {
|
|
1710
|
+
const button = document.createElement('button');
|
|
1711
|
+
|
|
1712
|
+
function showEnterAR() {
|
|
1713
|
+
button.style.display = '';
|
|
1714
|
+
button.style.cursor = 'pointer';
|
|
1715
|
+
button.style.left = 'calc(50% - 50px)';
|
|
1716
|
+
button.style.width = '100px';
|
|
1717
|
+
button.textContent = 'ENTER AR';
|
|
1718
|
+
button.onmouseenter = function() { button.style.opacity = '1.0'; };
|
|
1719
|
+
button.onmouseleave = function() { button.style.opacity = '0.5'; };
|
|
1720
|
+
button.onclick = function() {
|
|
1721
|
+
const session = renderer.xr.getSession();
|
|
1722
|
+
if (session) {
|
|
1723
|
+
session.end();
|
|
1724
|
+
} else {
|
|
1725
|
+
navigator.xr.requestSession('immersive-ar', {
|
|
1726
|
+
optionalFeatures: ['hit-test', 'dom-overlay', 'local-floor'],
|
|
1727
|
+
domOverlay: options?.domOverlay || { root: document.body }
|
|
1728
|
+
}).then(session => {
|
|
1729
|
+
renderer.xr.setSession(session);
|
|
1730
|
+
button.textContent = 'EXIT AR';
|
|
1731
|
+
|
|
1732
|
+
session.addEventListener('end', () => {
|
|
1733
|
+
button.textContent = 'ENTER AR';
|
|
1734
|
+
});
|
|
1735
|
+
}).catch(err => {
|
|
1736
|
+
console.error('AR session failed:', err);
|
|
1737
|
+
alert('AR not supported: ' + err.message);
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function showWebXRNotFound() {
|
|
1744
|
+
button.style.display = '';
|
|
1745
|
+
button.style.cursor = 'auto';
|
|
1746
|
+
button.style.left = 'calc(50% - 75px)';
|
|
1747
|
+
button.style.width = '150px';
|
|
1748
|
+
button.textContent = 'AR NOT SUPPORTED';
|
|
1749
|
+
button.onmouseenter = null;
|
|
1750
|
+
button.onmouseleave = null;
|
|
1751
|
+
button.onclick = null;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
button.style.position = 'absolute';
|
|
1755
|
+
button.style.bottom = '70px';
|
|
1756
|
+
button.style.padding = '12px 6px';
|
|
1757
|
+
button.style.border = '1px solid #fff';
|
|
1758
|
+
button.style.borderRadius = '4px';
|
|
1759
|
+
button.style.background = 'rgba(0,0,0,0.1)';
|
|
1760
|
+
button.style.color = '#fff';
|
|
1761
|
+
button.style.font = 'normal 13px sans-serif';
|
|
1762
|
+
button.style.textAlign = 'center';
|
|
1763
|
+
button.style.opacity = '0.5';
|
|
1764
|
+
button.style.outline = 'none';
|
|
1765
|
+
button.style.zIndex = '999';
|
|
1766
|
+
|
|
1767
|
+
if ('xr' in navigator) {
|
|
1768
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
1769
|
+
supported ? showEnterAR() : showWebXRNotFound();
|
|
1770
|
+
});
|
|
1771
|
+
} else {
|
|
1772
|
+
showWebXRNotFound();
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
return button;
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
|
|
1779
|
+
console.log('✅ VRButton/ARButton inline implementation loaded');
|
|
1780
|
+
</script>
|