@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,2515 @@
|
|
|
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>侘び寂び - Wabi-Sabi Space | ChocoDrop Demo</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{
|
|
9
|
+
"imports": {
|
|
10
|
+
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
|
|
11
|
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
15
|
+
<style>
|
|
16
|
+
body {
|
|
17
|
+
margin: 0;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
font-family: 'Yu Gothic', 'Hiragino Sans', sans-serif;
|
|
20
|
+
background: #fafafa; /* ミニマルな白 */
|
|
21
|
+
}
|
|
22
|
+
canvas {
|
|
23
|
+
display: block;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#info {
|
|
27
|
+
position: fixed;
|
|
28
|
+
top: 16px;
|
|
29
|
+
left: 16px;
|
|
30
|
+
color: #2d3748;
|
|
31
|
+
background: rgba(250, 250, 250, 0.85);
|
|
32
|
+
backdrop-filter: blur(12px) saturate(180%);
|
|
33
|
+
-webkit-backdrop-filter: blur(12px) saturate(180%);
|
|
34
|
+
border: 1px solid rgba(255, 255, 255, 0.4);
|
|
35
|
+
padding: 14px 18px;
|
|
36
|
+
border-radius: 8px;
|
|
37
|
+
font-size: 12px;
|
|
38
|
+
font-weight: 400;
|
|
39
|
+
z-index: 2000;
|
|
40
|
+
max-width: 280px;
|
|
41
|
+
line-height: 1.5;
|
|
42
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04);
|
|
43
|
+
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#info.hidden {
|
|
47
|
+
opacity: 0;
|
|
48
|
+
transform: translateY(-10px);
|
|
49
|
+
pointer-events: none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.close-btn {
|
|
53
|
+
position: absolute;
|
|
54
|
+
top: 8px;
|
|
55
|
+
right: 8px;
|
|
56
|
+
background: none;
|
|
57
|
+
border: none;
|
|
58
|
+
font-size: 16px;
|
|
59
|
+
color: #666;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
width: 20px;
|
|
62
|
+
height: 20px;
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
border-radius: 50%;
|
|
67
|
+
transition: background-color 0.2s ease;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.close-btn:hover {
|
|
71
|
+
background-color: rgba(30, 30, 30, 0.1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* 統合メニュー */
|
|
75
|
+
.unified-menu {
|
|
76
|
+
position: fixed;
|
|
77
|
+
top: 16px;
|
|
78
|
+
right: 20px;
|
|
79
|
+
z-index: 1000;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.menu-toggle {
|
|
83
|
+
background: rgba(99, 102, 241, 0.15);
|
|
84
|
+
backdrop-filter: blur(16px);
|
|
85
|
+
-webkit-backdrop-filter: blur(16px);
|
|
86
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
87
|
+
color: #2d3748;
|
|
88
|
+
padding: 10px 14px;
|
|
89
|
+
border-radius: 8px;
|
|
90
|
+
font-size: 18px;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
93
|
+
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2), 0 2px 6px rgba(0, 0, 0, 0.05);
|
|
94
|
+
width: 44px;
|
|
95
|
+
height: 44px;
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
opacity: 0.8;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.menu-toggle:hover {
|
|
103
|
+
background: rgba(99, 102, 241, 0.2);
|
|
104
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
105
|
+
transform: scale(1.1) translateY(-2px);
|
|
106
|
+
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.3), 0 3px 8px rgba(0, 0, 0, 0.1);
|
|
107
|
+
opacity: 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.menu-dropdown {
|
|
111
|
+
position: absolute;
|
|
112
|
+
top: 56px;
|
|
113
|
+
right: 0;
|
|
114
|
+
min-width: 220px;
|
|
115
|
+
background: rgba(255, 255, 255, 0.85);
|
|
116
|
+
backdrop-filter: blur(16px) saturate(180%);
|
|
117
|
+
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
118
|
+
border: 1px solid rgba(99, 102, 241, 0.2);
|
|
119
|
+
border-radius: 12px;
|
|
120
|
+
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
121
|
+
opacity: 0;
|
|
122
|
+
visibility: hidden;
|
|
123
|
+
transform: translateY(-10px);
|
|
124
|
+
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.menu-dropdown.active {
|
|
129
|
+
opacity: 1;
|
|
130
|
+
visibility: visible;
|
|
131
|
+
transform: translateY(0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.menu-item {
|
|
135
|
+
display: block;
|
|
136
|
+
padding: 12px 16px;
|
|
137
|
+
color: #2d3748;
|
|
138
|
+
text-decoration: none;
|
|
139
|
+
font-size: 13px;
|
|
140
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
141
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
background: none;
|
|
144
|
+
border-left: none;
|
|
145
|
+
border-right: none;
|
|
146
|
+
border-top: none;
|
|
147
|
+
width: 100%;
|
|
148
|
+
text-align: left;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.menu-item:last-child {
|
|
152
|
+
border-bottom: none;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.menu-item:hover {
|
|
156
|
+
background: rgba(99, 102, 241, 0.08);
|
|
157
|
+
padding-left: 20px;
|
|
158
|
+
border-left: 2px solid rgba(99, 102, 241, 0.4);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.menu-item.has-submenu {
|
|
162
|
+
position: relative;
|
|
163
|
+
padding: 0;
|
|
164
|
+
border: none;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.submenu-toggle {
|
|
168
|
+
display: block;
|
|
169
|
+
padding: 12px 16px;
|
|
170
|
+
color: #2d3748;
|
|
171
|
+
font-size: 13px;
|
|
172
|
+
background: none;
|
|
173
|
+
border: none;
|
|
174
|
+
width: 100%;
|
|
175
|
+
text-align: left;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
178
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.submenu-toggle:hover {
|
|
182
|
+
background: rgba(99, 102, 241, 0.08);
|
|
183
|
+
padding-left: 20px;
|
|
184
|
+
border-left: 2px solid rgba(99, 102, 241, 0.4);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.submenu {
|
|
188
|
+
display: none;
|
|
189
|
+
background: rgba(99, 102, 241, 0.03);
|
|
190
|
+
border-top: 1px solid rgba(99, 102, 241, 0.1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.submenu.active {
|
|
194
|
+
display: block;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.submenu .menu-item {
|
|
198
|
+
padding-left: 32px;
|
|
199
|
+
font-size: 12px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.submenu .menu-item:hover {
|
|
203
|
+
padding-left: 36px;
|
|
204
|
+
background: rgba(99, 102, 241, 0.12);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* メニューアイコンのバウンスアニメーション */
|
|
208
|
+
.menu-icon {
|
|
209
|
+
display: inline-block;
|
|
210
|
+
animation: menu-bounce 2s ease-in-out infinite;
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
transition: transform 0.2s ease;
|
|
213
|
+
text-shadow: 0 0 10px currentColor;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.menu-icon:hover {
|
|
217
|
+
transform: scale(1.4);
|
|
218
|
+
animation: menu-bounce-fast 0.8s ease-in-out infinite;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@keyframes menu-bounce {
|
|
222
|
+
0%, 100% {
|
|
223
|
+
filter: brightness(1) drop-shadow(0 0 8px currentColor);
|
|
224
|
+
transform: scale(1) translateY(0px);
|
|
225
|
+
}
|
|
226
|
+
25% {
|
|
227
|
+
filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
|
|
228
|
+
transform: scale(1.1) translateY(-4px);
|
|
229
|
+
}
|
|
230
|
+
50% {
|
|
231
|
+
filter: brightness(1.5) drop-shadow(0 0 20px currentColor);
|
|
232
|
+
transform: scale(1.15) translateY(-8px);
|
|
233
|
+
}
|
|
234
|
+
75% {
|
|
235
|
+
filter: brightness(1.3) drop-shadow(0 0 15px currentColor);
|
|
236
|
+
transform: scale(1.1) translateY(-4px);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@keyframes menu-bounce-fast {
|
|
241
|
+
0%, 100% {
|
|
242
|
+
filter: brightness(1.4) drop-shadow(0 0 15px currentColor);
|
|
243
|
+
transform: translateY(0px);
|
|
244
|
+
}
|
|
245
|
+
50% {
|
|
246
|
+
filter: brightness(1.8) drop-shadow(0 0 25px currentColor);
|
|
247
|
+
transform: translateY(-12px);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#info strong {
|
|
252
|
+
font-size: 14px;
|
|
253
|
+
font-weight: 600;
|
|
254
|
+
color: #1a1a1a;
|
|
255
|
+
display: block;
|
|
256
|
+
margin-bottom: 10px;
|
|
257
|
+
text-align: center;
|
|
258
|
+
letter-spacing: 0.5px;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#info code {
|
|
262
|
+
background: rgba(30, 30, 30, 0.08);
|
|
263
|
+
color: #2d3748;
|
|
264
|
+
padding: 2px 5px;
|
|
265
|
+
border-radius: 3px;
|
|
266
|
+
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
|
267
|
+
font-size: 11px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.controls {
|
|
271
|
+
margin-top: 8px;
|
|
272
|
+
padding-top: 8px;
|
|
273
|
+
border-top: 1px solid rgba(30, 30, 30, 0.15);
|
|
274
|
+
font-size: 11px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.control-item {
|
|
278
|
+
margin: 4px 0;
|
|
279
|
+
display: flex;
|
|
280
|
+
align-items: center;
|
|
281
|
+
gap: 8px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.key {
|
|
285
|
+
background: rgba(30, 30, 30, 0.1);
|
|
286
|
+
padding: 2px 6px;
|
|
287
|
+
border-radius: 3px;
|
|
288
|
+
font-weight: 500;
|
|
289
|
+
min-width: fit-content;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* ChocoDrop 要素を墨絵風にカスタマイズ */
|
|
293
|
+
.chocodrop-media {
|
|
294
|
+
border: 2px solid rgba(30, 30, 30, 0.8) !important;
|
|
295
|
+
background: rgba(250, 250, 250, 0.9) !important;
|
|
296
|
+
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3) !important;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.chocodrop-overlay {
|
|
300
|
+
background: rgba(30, 30, 30, 0.8) !important;
|
|
301
|
+
border: none !important;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.chocodrop-button {
|
|
305
|
+
background: rgba(30, 30, 30, 0.9) !important;
|
|
306
|
+
color: white !important;
|
|
307
|
+
border: 1px solid rgba(30, 30, 30, 0.9) !important;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* オレンジ色の要素を完全に除去 */
|
|
311
|
+
.chocodrop-frame,
|
|
312
|
+
.chocodrop-selection-box,
|
|
313
|
+
.chocodrop-resize-handle {
|
|
314
|
+
border-color: rgba(30, 30, 30, 0.8) !important;
|
|
315
|
+
background-color: transparent !important;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.chocodrop-highlight {
|
|
319
|
+
background-color: rgba(30, 30, 30, 0.1) !important;
|
|
320
|
+
border-color: rgba(30, 30, 30, 0.8) !important;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* 全ての赤/オレンジ系の色を強制的に墨色に変更 */
|
|
324
|
+
* {
|
|
325
|
+
--chocodrop-primary-color: rgba(30, 30, 30, 0.9) !important;
|
|
326
|
+
--chocodrop-accent-color: rgba(30, 30, 30, 0.8) !important;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
div[style*="background-color: rgb(255"],
|
|
330
|
+
div[style*="background-color: #ff"],
|
|
331
|
+
div[style*="background: rgb(255"],
|
|
332
|
+
div[style*="background: #ff"],
|
|
333
|
+
div[style*="border-color: rgb(255"],
|
|
334
|
+
div[style*="border-color: #ff"] {
|
|
335
|
+
background-color: rgba(30, 30, 30, 0.1) !important;
|
|
336
|
+
border-color: rgba(30, 30, 30, 0.8) !important;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* ChocoDrop で使われる可能性のある全ての要素 */
|
|
340
|
+
div, span, button {
|
|
341
|
+
filter: none !important;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* 世界選択ドロップダウン */
|
|
345
|
+
.world-selector {
|
|
346
|
+
position: fixed;
|
|
347
|
+
bottom: 20px;
|
|
348
|
+
left: 20px;
|
|
349
|
+
z-index: 100;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.world-toggle-btn {
|
|
353
|
+
background: rgba(250, 250, 250, 0.85);
|
|
354
|
+
backdrop-filter: blur(12px);
|
|
355
|
+
-webkit-backdrop-filter: blur(12px);
|
|
356
|
+
border: 1px solid rgba(255, 255, 255, 0.4);
|
|
357
|
+
color: #2d3748;
|
|
358
|
+
padding: 10px 16px;
|
|
359
|
+
border-radius: 8px;
|
|
360
|
+
font-size: 13px;
|
|
361
|
+
font-weight: 500;
|
|
362
|
+
cursor: pointer;
|
|
363
|
+
transition: all 0.2s ease;
|
|
364
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04);
|
|
365
|
+
display: flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
gap: 6px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.world-toggle-btn:hover {
|
|
371
|
+
background: rgba(255, 255, 255, 0.95);
|
|
372
|
+
border-color: rgba(255, 255, 255, 0.6);
|
|
373
|
+
transform: translateY(-2px);
|
|
374
|
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.06);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.world-dropdown {
|
|
378
|
+
position: absolute;
|
|
379
|
+
bottom: 100%;
|
|
380
|
+
left: 0;
|
|
381
|
+
margin-bottom: 8px;
|
|
382
|
+
background: rgba(255, 255, 255, 0.9);
|
|
383
|
+
backdrop-filter: blur(16px) saturate(180%);
|
|
384
|
+
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
385
|
+
border: 1px solid rgba(255, 255, 255, 0.5);
|
|
386
|
+
border-radius: 12px;
|
|
387
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
388
|
+
min-width: 200px;
|
|
389
|
+
max-height: 400px;
|
|
390
|
+
overflow-y: auto;
|
|
391
|
+
opacity: 0;
|
|
392
|
+
visibility: hidden;
|
|
393
|
+
transform: translateY(10px);
|
|
394
|
+
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.world-dropdown.active {
|
|
398
|
+
opacity: 1;
|
|
399
|
+
visibility: visible;
|
|
400
|
+
transform: translateY(0);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.world-item {
|
|
404
|
+
display: block;
|
|
405
|
+
padding: 12px 16px;
|
|
406
|
+
color: #2d3748;
|
|
407
|
+
text-decoration: none;
|
|
408
|
+
font-size: 13px;
|
|
409
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
410
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
411
|
+
position: relative;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.world-item:last-child {
|
|
415
|
+
border-bottom: none;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.world-item:hover {
|
|
419
|
+
background: rgba(255, 255, 255, 0.6);
|
|
420
|
+
padding-left: 22px;
|
|
421
|
+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.world-item:first-child {
|
|
425
|
+
border-radius: 12px 12px 0 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.world-item:last-child {
|
|
429
|
+
border-radius: 0 0 12px 12px;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.world-dropdown::-webkit-scrollbar {
|
|
433
|
+
width: 6px;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.world-dropdown::-webkit-scrollbar-track {
|
|
437
|
+
background: rgba(30, 30, 30, 0.05);
|
|
438
|
+
border-radius: 4px;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.world-dropdown::-webkit-scrollbar-thumb {
|
|
442
|
+
background: rgba(30, 30, 30, 0.2);
|
|
443
|
+
border-radius: 4px;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.world-dropdown::-webkit-scrollbar-thumb:hover {
|
|
447
|
+
background: rgba(30, 30, 30, 0.3);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* より限定的なスタイル上書き */
|
|
451
|
+
.chocodrop-container * {
|
|
452
|
+
background-color: rgba(250, 250, 250, 0.9) !important;
|
|
453
|
+
border-color: rgba(30, 30, 30, 0.8) !important;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/* 🖤 墨モード - デフォルトはカラー表示 */
|
|
457
|
+
canvas {
|
|
458
|
+
/* mix-blend-modeを削除してThree.jsの描画を正常に */
|
|
459
|
+
background: transparent !important;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/* モノクロ変換用のクラス(ユーザーが要求した時のみ適用) */
|
|
463
|
+
.ink-mode .chocodrop-media,
|
|
464
|
+
.ink-mode img,
|
|
465
|
+
.ink-mode video {
|
|
466
|
+
mix-blend-mode: multiply !important;
|
|
467
|
+
filter: contrast(1.2) saturate(0) !important;
|
|
468
|
+
transition: filter 1s ease-in-out;
|
|
469
|
+
}
|
|
470
|
+
</style>
|
|
471
|
+
</head>
|
|
472
|
+
<body>
|
|
473
|
+
<!-- Navigation Links (hidden - now in unified menu) -->
|
|
474
|
+
<div class="navigation" style="display: none;">
|
|
475
|
+
<a href="../../index.html" class="nav-link">🏠 Home</a>
|
|
476
|
+
<a href="https://github.com/nyukicorn/chocodrop" class="nav-link" target="_blank">📦 GitHub</a>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<!-- Main Info Panel -->
|
|
480
|
+
<div id="info">
|
|
481
|
+
<button class="close-btn" onclick="hideInfoPanel()">×</button>
|
|
482
|
+
<strong>🖤 侘び寂びの世界へようこそ</strong>
|
|
483
|
+
|
|
484
|
+
<div style="font-size: 11px; opacity: 0.9; margin-bottom: 12px; line-height: 1.6; text-align: center; font-style: italic;">
|
|
485
|
+
音楽に共鳴する、墨が紡ぐ不完全な美の世界
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
💡 基本操作:<br>
|
|
489
|
+
1. 動画や画像をインポートしてみて!<br>
|
|
490
|
+
2. 🎵 音楽入り動画をやってみて!墨絵に魂が宿ります<br>
|
|
491
|
+
3. 視点操作: ChocoDropを最小化してドラッグ<br>
|
|
492
|
+
4. 🎨モノクロ変換をやってみて!(対象を選択 → 「モノクロに変更」)<br>
|
|
493
|
+
|
|
494
|
+
<div class="controls">
|
|
495
|
+
<div class="control-item">
|
|
496
|
+
<span class="key">@</span>
|
|
497
|
+
<span>ChocoDrop / ちょこドロップ起動</span>
|
|
498
|
+
</div>
|
|
499
|
+
<div class="control-item">
|
|
500
|
+
<span class="key">🍫</span>
|
|
501
|
+
<span>右下ボタンからも起動可</span>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<!-- 世界選択ドロップダウン (hidden - now in unified menu) -->
|
|
507
|
+
<div class="world-selector" style="display: none;">
|
|
508
|
+
<button class="world-toggle-btn" id="worldToggle">
|
|
509
|
+
<span>🌍</span>
|
|
510
|
+
<span>世界一覧</span>
|
|
511
|
+
<span style="font-size: 10px; margin-left: 2px;">▼</span>
|
|
512
|
+
</button>
|
|
513
|
+
<div class="world-dropdown" id="worldDropdown">
|
|
514
|
+
<a href="../basic/index.html" class="world-item">🍫 はじまりの国</a>
|
|
515
|
+
<a href="../pixel-ocean/index.html" class="world-item">🌊 海底世界</a>
|
|
516
|
+
<a href="../music-garden/index.html" class="world-item">🌸 硝子の花</a>
|
|
517
|
+
<a href="../space/index.html" class="world-item">🌌 宇宙空間</a>
|
|
518
|
+
<a href="../toy-city/index.html" class="world-item">🎡 AI遊園地</a>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<!-- 統合メニュー -->
|
|
523
|
+
<div class="unified-menu">
|
|
524
|
+
<button class="menu-toggle" id="unifiedMenuToggle" aria-label="Menu">☰</button>
|
|
525
|
+
<div class="menu-dropdown" id="unifiedMenuDropdown">
|
|
526
|
+
<button class="menu-item" id="showInfoBtn"><span class="menu-icon">📖</span> このワールドについて</button>
|
|
527
|
+
|
|
528
|
+
<div class="menu-item has-submenu">
|
|
529
|
+
<button class="submenu-toggle" id="worldsMenuToggle"><span class="menu-icon">🌍</span> 世界選択 <span style="font-size: 10px;">▼</span></button>
|
|
530
|
+
<div class="submenu" id="worldsSubmenu">
|
|
531
|
+
<a href="../basic/index.html" class="menu-item"><span class="menu-icon">🍫</span> はじまりの国</a>
|
|
532
|
+
<a href="../pixel-ocean/index.html" class="menu-item"><span class="menu-icon">🌊</span> 海底世界</a>
|
|
533
|
+
<a href="../music-garden/index.html" class="menu-item"><span class="menu-icon">🌸</span> 硝子の花</a>
|
|
534
|
+
<a href="../space/index.html" class="menu-item"><span class="menu-icon">🌌</span> 宇宙空間</a>
|
|
535
|
+
<a href="../toy-city/index.html" class="menu-item"><span class="menu-icon">🎡</span> AI遊園地</a>
|
|
536
|
+
<a href="index.html" class="menu-item"><span class="menu-icon">🖤</span> 侘び寂びの世界(現在地)</a>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<a href="../../index.html" class="menu-item"><span class="menu-icon">🏠</span> Home</a>
|
|
541
|
+
<a href="https://github.com/nyukicorn/chocodrop" class="menu-item" target="_blank"><span class="menu-icon">🔗</span> GitHub</a>
|
|
542
|
+
|
|
543
|
+
<button class="menu-item" id="vrMenuButton"><span class="menu-icon">🥽</span> VR</button>
|
|
544
|
+
<button class="menu-item" id="arMenuButton"><span class="menu-icon">🥽</span> AR</button>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<script type="module">
|
|
549
|
+
import ensureChocoDrop from '../../public/load-chocodrop.js';
|
|
550
|
+
|
|
551
|
+
// 🎯 情報パネルをローダー前に初期化(早期に閉じられるように)
|
|
552
|
+
let autoHideTimer = null;
|
|
553
|
+
|
|
554
|
+
const getInfoPanel = () => document.getElementById('info');
|
|
555
|
+
|
|
556
|
+
const clearInfoPanelAutoHide = () => {
|
|
557
|
+
if (autoHideTimer) {
|
|
558
|
+
clearTimeout(autoHideTimer);
|
|
559
|
+
autoHideTimer = null;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const scheduleInfoPanelAutoHide = () => {
|
|
564
|
+
clearInfoPanelAutoHide();
|
|
565
|
+
autoHideTimer = window.setTimeout(() => {
|
|
566
|
+
if (typeof window.hideInfoPanel === 'function') {
|
|
567
|
+
window.hideInfoPanel();
|
|
568
|
+
}
|
|
569
|
+
}, 30000);
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
window.scheduleInfoPanelAutoHide = scheduleInfoPanelAutoHide;
|
|
573
|
+
|
|
574
|
+
window.hideInfoPanel = function hideInfoPanel() {
|
|
575
|
+
const infoPanel = getInfoPanel();
|
|
576
|
+
if (!infoPanel) {
|
|
577
|
+
clearInfoPanelAutoHide();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
infoPanel.classList.add('hidden');
|
|
582
|
+
window.setTimeout(() => {
|
|
583
|
+
if (infoPanel.classList.contains('hidden')) {
|
|
584
|
+
infoPanel.style.display = 'none';
|
|
585
|
+
}
|
|
586
|
+
}, 300);
|
|
587
|
+
|
|
588
|
+
clearInfoPanelAutoHide();
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
window.showInfoPanel = function showInfoPanel() {
|
|
592
|
+
const infoPanel = getInfoPanel();
|
|
593
|
+
if (!infoPanel) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
infoPanel.style.display = 'block';
|
|
598
|
+
window.requestAnimationFrame(() => {
|
|
599
|
+
infoPanel.classList.remove('hidden');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
scheduleInfoPanelAutoHide();
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
(async () => {
|
|
606
|
+
await ensureChocoDrop();
|
|
607
|
+
|
|
608
|
+
const THREE = window.THREE;
|
|
609
|
+
const ChocoDrop = window.ChocoDrop;
|
|
610
|
+
|
|
611
|
+
// 統合メニューの制御
|
|
612
|
+
(function() {
|
|
613
|
+
const menuToggle = document.getElementById('unifiedMenuToggle');
|
|
614
|
+
const menuDropdown = document.getElementById('unifiedMenuDropdown');
|
|
615
|
+
const worldsMenuToggle = document.getElementById('worldsMenuToggle');
|
|
616
|
+
const worldsSubmenu = document.getElementById('worldsSubmenu');
|
|
617
|
+
const showInfoBtn = document.getElementById('showInfoBtn');
|
|
618
|
+
const infoPanel = document.getElementById('info');
|
|
619
|
+
|
|
620
|
+
// 初回訪問チェック - localStorage使用
|
|
621
|
+
const FIRST_VISIT_KEY = 'wabisabi_visited';
|
|
622
|
+
const hasVisited = localStorage.getItem(FIRST_VISIT_KEY);
|
|
623
|
+
|
|
624
|
+
if (!hasVisited) {
|
|
625
|
+
// 初回訪問時は情報パネルを表示
|
|
626
|
+
if (infoPanel) {
|
|
627
|
+
infoPanel.style.display = 'block';
|
|
628
|
+
}
|
|
629
|
+
localStorage.setItem(FIRST_VISIT_KEY, 'true');
|
|
630
|
+
} else {
|
|
631
|
+
// 2回目以降は非表示
|
|
632
|
+
if (infoPanel) {
|
|
633
|
+
infoPanel.style.display = 'none';
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// 統合メニューのトグル
|
|
638
|
+
if (menuToggle && menuDropdown) {
|
|
639
|
+
menuToggle.addEventListener('click', (e) => {
|
|
640
|
+
e.stopPropagation();
|
|
641
|
+
menuDropdown.classList.toggle('active');
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// 外側クリックで閉じる
|
|
645
|
+
document.addEventListener('click', (e) => {
|
|
646
|
+
if (!menuToggle.contains(e.target) && !menuDropdown.contains(e.target)) {
|
|
647
|
+
menuDropdown.classList.remove('active');
|
|
648
|
+
// サブメニューも閉じる
|
|
649
|
+
if (worldsSubmenu) {
|
|
650
|
+
worldsSubmenu.classList.remove('active');
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Escキーで閉じる
|
|
656
|
+
document.addEventListener('keydown', (e) => {
|
|
657
|
+
if (e.key === 'Escape') {
|
|
658
|
+
menuDropdown.classList.remove('active');
|
|
659
|
+
if (worldsSubmenu) {
|
|
660
|
+
worldsSubmenu.classList.remove('active');
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 世界選択サブメニューのトグル
|
|
667
|
+
if (worldsMenuToggle && worldsSubmenu) {
|
|
668
|
+
worldsMenuToggle.addEventListener('click', (e) => {
|
|
669
|
+
e.stopPropagation();
|
|
670
|
+
worldsSubmenu.classList.toggle('active');
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// 情報パネル表示ボタン
|
|
675
|
+
if (showInfoBtn) {
|
|
676
|
+
showInfoBtn.addEventListener('click', () => {
|
|
677
|
+
// グローバルに定義された showInfoPanel() を呼び出す
|
|
678
|
+
if (typeof showInfoPanel === 'function') {
|
|
679
|
+
showInfoPanel();
|
|
680
|
+
} else {
|
|
681
|
+
// フォールバック: 直接表示
|
|
682
|
+
const panel = document.getElementById('info');
|
|
683
|
+
if (panel) {
|
|
684
|
+
panel.style.display = 'block';
|
|
685
|
+
panel.classList.remove('hidden');
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
menuDropdown.classList.remove('active');
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// VR/ARボタンは後でThree.jsのrendererと連携
|
|
693
|
+
// グローバル変数として保存
|
|
694
|
+
window.unifiedMenuVRButton = document.getElementById('vrMenuButton');
|
|
695
|
+
window.unifiedMenuARButton = document.getElementById('arMenuButton');
|
|
696
|
+
})();
|
|
697
|
+
})(); // Close async IIFE
|
|
698
|
+
</script>
|
|
699
|
+
|
|
700
|
+
<!-- VR/AR Button inline implementation -->
|
|
701
|
+
<script>
|
|
702
|
+
// VRButton implementation
|
|
703
|
+
const VRButton = {
|
|
704
|
+
createButton: function(renderer) {
|
|
705
|
+
const button = document.createElement('button');
|
|
706
|
+
|
|
707
|
+
function showEnterVR() {
|
|
708
|
+
button.style.display = '';
|
|
709
|
+
button.style.cursor = 'pointer';
|
|
710
|
+
button.style.left = 'calc(50% - 50px)';
|
|
711
|
+
button.style.width = '100px';
|
|
712
|
+
button.textContent = 'ENTER VR';
|
|
713
|
+
button.onmouseenter = function() { button.style.opacity = '1.0'; };
|
|
714
|
+
button.onmouseleave = function() { button.style.opacity = '0.5'; };
|
|
715
|
+
button.onclick = function() {
|
|
716
|
+
const session = renderer.xr.getSession();
|
|
717
|
+
if (session) {
|
|
718
|
+
session.end();
|
|
719
|
+
} else {
|
|
720
|
+
navigator.xr.requestSession('immersive-vr', {
|
|
721
|
+
optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking']
|
|
722
|
+
}).then(session => {
|
|
723
|
+
renderer.xr.setSession(session);
|
|
724
|
+
button.textContent = 'EXIT VR';
|
|
725
|
+
|
|
726
|
+
session.addEventListener('end', () => {
|
|
727
|
+
button.textContent = 'ENTER VR';
|
|
728
|
+
});
|
|
729
|
+
}).catch(err => {
|
|
730
|
+
console.error('VR session failed:', err);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function showWebXRNotFound() {
|
|
737
|
+
button.style.display = '';
|
|
738
|
+
button.style.cursor = 'auto';
|
|
739
|
+
button.style.left = 'calc(50% - 75px)';
|
|
740
|
+
button.style.width = '150px';
|
|
741
|
+
button.textContent = 'VR NOT SUPPORTED';
|
|
742
|
+
button.onmouseenter = null;
|
|
743
|
+
button.onmouseleave = null;
|
|
744
|
+
button.onclick = null;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
button.style.position = 'absolute';
|
|
748
|
+
button.style.bottom = '20px';
|
|
749
|
+
button.style.padding = '12px 6px';
|
|
750
|
+
button.style.border = '1px solid #fff';
|
|
751
|
+
button.style.borderRadius = '4px';
|
|
752
|
+
button.style.background = 'rgba(0,0,0,0.1)';
|
|
753
|
+
button.style.color = '#fff';
|
|
754
|
+
button.style.font = 'normal 13px sans-serif';
|
|
755
|
+
button.style.textAlign = 'center';
|
|
756
|
+
button.style.opacity = '0.5';
|
|
757
|
+
button.style.outline = 'none';
|
|
758
|
+
button.style.zIndex = '999';
|
|
759
|
+
|
|
760
|
+
if ('xr' in navigator) {
|
|
761
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
762
|
+
supported ? showEnterVR() : showWebXRNotFound();
|
|
763
|
+
});
|
|
764
|
+
} else {
|
|
765
|
+
showWebXRNotFound();
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return button;
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// ARButton implementation
|
|
773
|
+
const ARButton = {
|
|
774
|
+
createButton: function(renderer, options) {
|
|
775
|
+
const button = document.createElement('button');
|
|
776
|
+
|
|
777
|
+
function showEnterAR() {
|
|
778
|
+
button.style.display = '';
|
|
779
|
+
button.style.cursor = 'pointer';
|
|
780
|
+
button.style.left = 'calc(50% - 50px)';
|
|
781
|
+
button.style.width = '100px';
|
|
782
|
+
button.textContent = 'ENTER AR';
|
|
783
|
+
button.onmouseenter = function() { button.style.opacity = '1.0'; };
|
|
784
|
+
button.onmouseleave = function() { button.style.opacity = '0.5'; };
|
|
785
|
+
button.onclick = function() {
|
|
786
|
+
const session = renderer.xr.getSession();
|
|
787
|
+
if (session) {
|
|
788
|
+
session.end();
|
|
789
|
+
} else {
|
|
790
|
+
navigator.xr.requestSession('immersive-ar', {
|
|
791
|
+
optionalFeatures: ['hit-test', 'dom-overlay', 'local-floor'],
|
|
792
|
+
domOverlay: options?.domOverlay || { root: document.body }
|
|
793
|
+
}).then(session => {
|
|
794
|
+
renderer.xr.setSession(session);
|
|
795
|
+
button.textContent = 'EXIT AR';
|
|
796
|
+
|
|
797
|
+
session.addEventListener('end', () => {
|
|
798
|
+
button.textContent = 'ENTER AR';
|
|
799
|
+
});
|
|
800
|
+
}).catch(err => {
|
|
801
|
+
console.error('AR session failed:', err);
|
|
802
|
+
alert('AR not supported: ' + err.message);
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function showWebXRNotFound() {
|
|
809
|
+
button.style.display = '';
|
|
810
|
+
button.style.cursor = 'auto';
|
|
811
|
+
button.style.left = 'calc(50% - 75px)';
|
|
812
|
+
button.style.width = '150px';
|
|
813
|
+
button.textContent = 'AR NOT SUPPORTED';
|
|
814
|
+
button.onmouseenter = null;
|
|
815
|
+
button.onmouseleave = null;
|
|
816
|
+
button.onclick = null;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
button.style.position = 'absolute';
|
|
820
|
+
button.style.bottom = '70px';
|
|
821
|
+
button.style.padding = '12px 6px';
|
|
822
|
+
button.style.border = '1px solid #fff';
|
|
823
|
+
button.style.borderRadius = '4px';
|
|
824
|
+
button.style.background = 'rgba(0,0,0,0.1)';
|
|
825
|
+
button.style.color = '#fff';
|
|
826
|
+
button.style.font = 'normal 13px sans-serif';
|
|
827
|
+
button.style.textAlign = 'center';
|
|
828
|
+
button.style.opacity = '0.5';
|
|
829
|
+
button.style.outline = 'none';
|
|
830
|
+
button.style.zIndex = '999';
|
|
831
|
+
|
|
832
|
+
if ('xr' in navigator) {
|
|
833
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
834
|
+
supported ? showEnterAR() : showWebXRNotFound();
|
|
835
|
+
});
|
|
836
|
+
} else {
|
|
837
|
+
showWebXRNotFound();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return button;
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
console.log('✅ VRButton/ARButton inline implementation loaded');
|
|
845
|
+
</script>
|
|
846
|
+
<script type="module">
|
|
847
|
+
import ensureChocoDrop from '../../public/load-chocodrop.js';
|
|
848
|
+
|
|
849
|
+
(async () => {
|
|
850
|
+
await ensureChocoDrop();
|
|
851
|
+
|
|
852
|
+
const THREE = window.THREE;
|
|
853
|
+
const ChocoDrop = window.ChocoDrop;
|
|
854
|
+
|
|
855
|
+
// Three.js 基本設定
|
|
856
|
+
const scene = new THREE.Scene();
|
|
857
|
+
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
858
|
+
const renderer = new THREE.WebGLRenderer({
|
|
859
|
+
antialias: true,
|
|
860
|
+
alpha: false, // 透明背景を無効化
|
|
861
|
+
powerPreference: "default"
|
|
862
|
+
});
|
|
863
|
+
renderer.setClearColor(0xfafafa, 1); // 明確に背景色を設定
|
|
864
|
+
|
|
865
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
866
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
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
|
+
// ★ WebXRサポートのデバッグ情報
|
|
873
|
+
console.log('🥽 WebXR Debug Info:');
|
|
874
|
+
console.log('- navigator.xr:', !!navigator.xr);
|
|
875
|
+
console.log('- isSecureContext:', window.isSecureContext);
|
|
876
|
+
if (navigator.xr) {
|
|
877
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
878
|
+
console.log('- immersive-vr supported:', supported);
|
|
879
|
+
});
|
|
880
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
881
|
+
console.log('- immersive-ar supported:', supported);
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ★ VR/ARボタンを統合メニューと連携(bodyには追加しない)
|
|
886
|
+
if (typeof VRButton !== 'undefined' && window.unifiedMenuVRButton) {
|
|
887
|
+
console.log('✅ VRButton script loaded - connecting to unified menu');
|
|
888
|
+
|
|
889
|
+
// VRセッション開始関数を作成
|
|
890
|
+
function startVRSession() {
|
|
891
|
+
if ('xr' in navigator) {
|
|
892
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
893
|
+
if (supported) {
|
|
894
|
+
const sessionInit = { optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'layers'] };
|
|
895
|
+
navigator.xr.requestSession('immersive-vr', sessionInit).then(session => {
|
|
896
|
+
session.addEventListener('end', () => {
|
|
897
|
+
renderer.xr.setSession(null);
|
|
898
|
+
});
|
|
899
|
+
renderer.xr.setSession(session);
|
|
900
|
+
});
|
|
901
|
+
} else {
|
|
902
|
+
console.log('VR not supported on this device');
|
|
903
|
+
// NOT SUPPORTEDメッセージは表示しない(静かに失敗)
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
window.unifiedMenuVRButton.onclick = startVRSession;
|
|
910
|
+
|
|
911
|
+
// サポートチェック - 未サポートの場合はボタンを非表示に
|
|
912
|
+
if ('xr' in navigator) {
|
|
913
|
+
navigator.xr.isSessionSupported('immersive-vr').then(supported => {
|
|
914
|
+
if (!supported) {
|
|
915
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
} else {
|
|
919
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
920
|
+
}
|
|
921
|
+
} else {
|
|
922
|
+
console.log('❌ VRButton script not loaded or menu button not found');
|
|
923
|
+
if (window.unifiedMenuVRButton) {
|
|
924
|
+
window.unifiedMenuVRButton.style.display = 'none';
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (typeof ARButton !== 'undefined' && window.unifiedMenuARButton) {
|
|
929
|
+
console.log('✅ ARButton script loaded - connecting to unified menu');
|
|
930
|
+
|
|
931
|
+
// ARセッション開始関数を作成
|
|
932
|
+
function startARSession() {
|
|
933
|
+
if ('xr' in navigator) {
|
|
934
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
935
|
+
if (supported) {
|
|
936
|
+
const sessionInit = {
|
|
937
|
+
requiredFeatures: ['hit-test'],
|
|
938
|
+
optionalFeatures: ['dom-overlay'],
|
|
939
|
+
domOverlay: { root: document.body }
|
|
940
|
+
};
|
|
941
|
+
navigator.xr.requestSession('immersive-ar', sessionInit).then(session => {
|
|
942
|
+
session.addEventListener('end', () => {
|
|
943
|
+
renderer.xr.setSession(null);
|
|
944
|
+
});
|
|
945
|
+
renderer.xr.setSession(session);
|
|
946
|
+
});
|
|
947
|
+
} else {
|
|
948
|
+
console.log('AR not supported on this device');
|
|
949
|
+
// NOT SUPPORTEDメッセージは表示しない(静かに失敗)
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
window.unifiedMenuARButton.onclick = startARSession;
|
|
956
|
+
|
|
957
|
+
// サポートチェック - 未サポートの場合はボタンを非表示に
|
|
958
|
+
if ('xr' in navigator) {
|
|
959
|
+
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
|
|
960
|
+
if (!supported) {
|
|
961
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
} else {
|
|
965
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
console.log('❌ ARButton script not loaded or menu button not found');
|
|
969
|
+
if (window.unifiedMenuARButton) {
|
|
970
|
+
window.unifiedMenuARButton.style.display = 'none';
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// 🎨 シンプルな白い空間(霧なし)
|
|
975
|
+
function createMinimalBackground() {
|
|
976
|
+
// シンプルな白い背景色のみ
|
|
977
|
+
scene.background = new THREE.Color(0xfafafa);
|
|
978
|
+
// 霧は削除(墨雲が見えなくなるため)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// 🖌️ シンプルな墨風スプライト作成
|
|
982
|
+
function createSimpleInk(type, x = 0, y = 0, z = 0) {
|
|
983
|
+
const canvas = document.createElement('canvas');
|
|
984
|
+
canvas.width = 256;
|
|
985
|
+
canvas.height = 256;
|
|
986
|
+
const ctx = canvas.getContext('2d');
|
|
987
|
+
|
|
988
|
+
// 白背景(墨モードで黒になる)
|
|
989
|
+
ctx.fillStyle = 'white';
|
|
990
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
991
|
+
|
|
992
|
+
// 墨の流れと塊を描画
|
|
993
|
+
ctx.strokeStyle = 'black';
|
|
994
|
+
ctx.fillStyle = 'black';
|
|
995
|
+
|
|
996
|
+
if (type.includes('flow')) {
|
|
997
|
+
// 🌊 流れる線の集合(Image #1のような)
|
|
998
|
+
ctx.lineWidth = 2;
|
|
999
|
+
for (let i = 0; i < 15; i++) {
|
|
1000
|
+
ctx.beginPath();
|
|
1001
|
+
ctx.moveTo(Math.random() * 50, Math.random() * 256);
|
|
1002
|
+
ctx.bezierCurveTo(
|
|
1003
|
+
Math.random() * 256, Math.random() * 256,
|
|
1004
|
+
Math.random() * 256, Math.random() * 256,
|
|
1005
|
+
200 + Math.random() * 56, Math.random() * 256
|
|
1006
|
+
);
|
|
1007
|
+
ctx.stroke();
|
|
1008
|
+
}
|
|
1009
|
+
} else if (type.includes('blob')) {
|
|
1010
|
+
// 🖤 濃い黒い塊(Image #2のような)
|
|
1011
|
+
const numBlobs = 3;
|
|
1012
|
+
for (let i = 0; i < numBlobs; i++) {
|
|
1013
|
+
ctx.beginPath();
|
|
1014
|
+
const centerX = 60 + Math.random() * 136;
|
|
1015
|
+
const centerY = 60 + Math.random() * 136;
|
|
1016
|
+
const size = 30 + Math.random() * 40;
|
|
1017
|
+
|
|
1018
|
+
// 不規則な形の塊
|
|
1019
|
+
ctx.moveTo(centerX, centerY);
|
|
1020
|
+
for (let j = 0; j < 8; j++) {
|
|
1021
|
+
const angle = (j / 8) * Math.PI * 2;
|
|
1022
|
+
const radius = size * (0.7 + Math.random() * 0.6);
|
|
1023
|
+
const x = centerX + Math.cos(angle) * radius;
|
|
1024
|
+
const y = centerY + Math.sin(angle) * radius;
|
|
1025
|
+
ctx.lineTo(x, y);
|
|
1026
|
+
}
|
|
1027
|
+
ctx.closePath();
|
|
1028
|
+
ctx.fill();
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
1033
|
+
const spriteMaterial = new THREE.SpriteMaterial({
|
|
1034
|
+
map: texture,
|
|
1035
|
+
transparent: false // 墨モードなので透明度不要
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
const sprite = new THREE.Sprite(spriteMaterial);
|
|
1039
|
+
sprite.scale.set(6, 6, 1);
|
|
1040
|
+
sprite.position.set(x, y, z);
|
|
1041
|
+
|
|
1042
|
+
// アニメーション用データ
|
|
1043
|
+
sprite.userData = {
|
|
1044
|
+
originalY: y,
|
|
1045
|
+
floatSpeed: 0.001,
|
|
1046
|
+
rotationSpeed: 0.0005,
|
|
1047
|
+
floatAmplitude: 2,
|
|
1048
|
+
phase: Math.random() * Math.PI * 2
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
return sprite;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// 🌿 墨の塵エフェクト(下部)
|
|
1055
|
+
function createInkDust() {
|
|
1056
|
+
const dustGroup = new THREE.Group();
|
|
1057
|
+
|
|
1058
|
+
// 墨の塵パーティクル
|
|
1059
|
+
const dustGeometry = new THREE.BufferGeometry();
|
|
1060
|
+
const dustCount = 200;
|
|
1061
|
+
const positions = new Float32Array(dustCount * 3);
|
|
1062
|
+
const colors = new Float32Array(dustCount * 3);
|
|
1063
|
+
const sizes = new Float32Array(dustCount);
|
|
1064
|
+
|
|
1065
|
+
for (let i = 0; i < dustCount; i++) {
|
|
1066
|
+
const i3 = i * 3;
|
|
1067
|
+
|
|
1068
|
+
// 下部に配置
|
|
1069
|
+
positions[i3] = (Math.random() - 0.5) * 30;
|
|
1070
|
+
positions[i3 + 1] = -10 + Math.random() * 2;
|
|
1071
|
+
positions[i3 + 2] = (Math.random() - 0.5) * 10;
|
|
1072
|
+
|
|
1073
|
+
// 墨の濃淡
|
|
1074
|
+
const darkness = 0.1 + Math.random() * 0.4;
|
|
1075
|
+
colors[i3] = darkness;
|
|
1076
|
+
colors[i3 + 1] = darkness;
|
|
1077
|
+
colors[i3 + 2] = darkness;
|
|
1078
|
+
|
|
1079
|
+
sizes[i] = Math.random() * 3 + 1;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
dustGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
1083
|
+
dustGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
1084
|
+
dustGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
|
1085
|
+
|
|
1086
|
+
const dustMaterial = new THREE.PointsMaterial({
|
|
1087
|
+
size: 2,
|
|
1088
|
+
sizeAttenuation: true,
|
|
1089
|
+
vertexColors: true,
|
|
1090
|
+
transparent: true,
|
|
1091
|
+
opacity: 0.5,
|
|
1092
|
+
blending: THREE.MultiplyBlending
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
const dust = new THREE.Points(dustGeometry, dustMaterial);
|
|
1096
|
+
dustGroup.add(dust);
|
|
1097
|
+
|
|
1098
|
+
// 墨の塵のパルス用データ
|
|
1099
|
+
dustGroup.userData = {
|
|
1100
|
+
pulseSpeed: 0.002,
|
|
1101
|
+
time: 0
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
return dustGroup;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// 🍂 墨の葉パーティクル
|
|
1108
|
+
function createInkLeaves() {
|
|
1109
|
+
const leafGeometry = new THREE.BufferGeometry();
|
|
1110
|
+
const leafCount = 8; // 少し増やす
|
|
1111
|
+
const positions = new Float32Array(leafCount * 3);
|
|
1112
|
+
const colors = new Float32Array(leafCount * 3);
|
|
1113
|
+
|
|
1114
|
+
for (let i = 0; i < leafCount; i++) {
|
|
1115
|
+
const i3 = i * 3;
|
|
1116
|
+
|
|
1117
|
+
positions[i3] = (Math.random() - 0.5) * 30;
|
|
1118
|
+
positions[i3 + 1] = 10 + Math.random() * 10;
|
|
1119
|
+
positions[i3 + 2] = (Math.random() - 0.5) * 20;
|
|
1120
|
+
|
|
1121
|
+
// 墨の濃淡
|
|
1122
|
+
const darkness = 0.1 + Math.random() * 0.3;
|
|
1123
|
+
colors[i3] = darkness;
|
|
1124
|
+
colors[i3 + 1] = darkness;
|
|
1125
|
+
colors[i3 + 2] = darkness;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
leafGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
1129
|
+
leafGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
1130
|
+
|
|
1131
|
+
const leafMaterial = new THREE.PointsMaterial({
|
|
1132
|
+
size: 6,
|
|
1133
|
+
sizeAttenuation: true,
|
|
1134
|
+
vertexColors: true,
|
|
1135
|
+
transparent: true,
|
|
1136
|
+
opacity: 0.8,
|
|
1137
|
+
blending: THREE.MultiplyBlending
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
const leaves = new THREE.Points(leafGeometry, leafMaterial);
|
|
1141
|
+
|
|
1142
|
+
// 落下アニメーション用
|
|
1143
|
+
leaves.userData = {
|
|
1144
|
+
fallSpeed: 0.008,
|
|
1145
|
+
swaySpeed: 0.002,
|
|
1146
|
+
time: 0
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
return leaves;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// 削除 - 桜の花びらパーティクル
|
|
1153
|
+
function createSakuraPetals_removed() {
|
|
1154
|
+
const petalCount = 20;
|
|
1155
|
+
const petalsGroup = new THREE.Group();
|
|
1156
|
+
|
|
1157
|
+
// 花びらのテクスチャを作成
|
|
1158
|
+
const canvas = document.createElement('canvas');
|
|
1159
|
+
canvas.width = 64;
|
|
1160
|
+
canvas.height = 64;
|
|
1161
|
+
const ctx = canvas.getContext('2d');
|
|
1162
|
+
|
|
1163
|
+
// 花びらの形を描画
|
|
1164
|
+
ctx.fillStyle = '#ffb7c5';
|
|
1165
|
+
ctx.beginPath();
|
|
1166
|
+
ctx.moveTo(32, 10);
|
|
1167
|
+
ctx.bezierCurveTo(20, 20, 20, 40, 32, 54);
|
|
1168
|
+
ctx.bezierCurveTo(44, 40, 44, 20, 32, 10);
|
|
1169
|
+
ctx.fill();
|
|
1170
|
+
|
|
1171
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
1172
|
+
|
|
1173
|
+
for (let i = 0; i < petalCount; i++) {
|
|
1174
|
+
const spriteMaterial = new THREE.SpriteMaterial({
|
|
1175
|
+
map: texture,
|
|
1176
|
+
transparent: true,
|
|
1177
|
+
opacity: 0.8,
|
|
1178
|
+
rotation: Math.random() * Math.PI
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
const petal = new THREE.Sprite(spriteMaterial);
|
|
1182
|
+
petal.scale.set(1, 1.2, 1);
|
|
1183
|
+
|
|
1184
|
+
// ランダムな開始位置
|
|
1185
|
+
petal.position.set(
|
|
1186
|
+
(Math.random() - 0.5) * 60,
|
|
1187
|
+
15 + Math.random() * 20,
|
|
1188
|
+
(Math.random() - 0.5) * 40
|
|
1189
|
+
);
|
|
1190
|
+
|
|
1191
|
+
// アニメーション用データ
|
|
1192
|
+
petal.userData = {
|
|
1193
|
+
velocity: new THREE.Vector3(
|
|
1194
|
+
(Math.random() - 0.5) * 0.02,
|
|
1195
|
+
-0.01 - Math.random() * 0.01,
|
|
1196
|
+
(Math.random() - 0.5) * 0.02
|
|
1197
|
+
),
|
|
1198
|
+
rotationSpeed: (Math.random() - 0.5) * 0.01,
|
|
1199
|
+
swayPhase: Math.random() * Math.PI * 2,
|
|
1200
|
+
swaySpeed: 0.002 + Math.random() * 0.002,
|
|
1201
|
+
swayAmplitude: 0.02 + Math.random() * 0.02
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
petalsGroup.add(petal);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
return petalsGroup;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// 🌟 墨の光点エフェクト
|
|
1211
|
+
function createInkSpirits() {
|
|
1212
|
+
const spiritsGroup = new THREE.Group();
|
|
1213
|
+
const spiritCount = 8;
|
|
1214
|
+
|
|
1215
|
+
for (let i = 0; i < spiritCount; i++) {
|
|
1216
|
+
const geometry = new THREE.SphereGeometry(0.15, 4, 4);
|
|
1217
|
+
const material = new THREE.MeshBasicMaterial({
|
|
1218
|
+
color: 0x333333,
|
|
1219
|
+
transparent: true,
|
|
1220
|
+
opacity: 0.6
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
const spirit = new THREE.Mesh(geometry, material);
|
|
1224
|
+
|
|
1225
|
+
// ランダムな位置
|
|
1226
|
+
spirit.position.set(
|
|
1227
|
+
(Math.random() - 0.5) * 50,
|
|
1228
|
+
Math.random() * 10 - 5,
|
|
1229
|
+
(Math.random() - 0.5) * 40
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
// 薄い光(モノクロ)
|
|
1233
|
+
const light = new THREE.PointLight(0xffffff, 0.2, 3);
|
|
1234
|
+
light.position.copy(spirit.position);
|
|
1235
|
+
spiritsGroup.add(light);
|
|
1236
|
+
|
|
1237
|
+
// アニメーション用データ
|
|
1238
|
+
spirit.userData = {
|
|
1239
|
+
light: light,
|
|
1240
|
+
phase: Math.random() * Math.PI * 2,
|
|
1241
|
+
moveSpeed: 0.001 + Math.random() * 0.001,
|
|
1242
|
+
glowSpeed: 0.002 + Math.random() * 0.003,
|
|
1243
|
+
wanderRadius: 10 + Math.random() * 10,
|
|
1244
|
+
originalPos: spirit.position.clone()
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
spiritsGroup.add(spirit);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return spiritsGroup;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// シーン構築
|
|
1254
|
+
createMinimalBackground();
|
|
1255
|
+
|
|
1256
|
+
// 🌫️ 星雲エフェクトベースの墨雲
|
|
1257
|
+
function createInkNebula(x, y, z, intensity = 1.0) {
|
|
1258
|
+
const nebulaGroup = new THREE.Group();
|
|
1259
|
+
|
|
1260
|
+
// ☁️ 墨雲用のキャンバステクスチャ作成
|
|
1261
|
+
function createInkTexture(size = 512) {
|
|
1262
|
+
const canvas = document.createElement('canvas');
|
|
1263
|
+
canvas.width = size;
|
|
1264
|
+
canvas.height = size;
|
|
1265
|
+
const context = canvas.getContext('2d');
|
|
1266
|
+
|
|
1267
|
+
// 白背景
|
|
1268
|
+
context.fillStyle = 'white';
|
|
1269
|
+
context.fillRect(0, 0, size, size);
|
|
1270
|
+
|
|
1271
|
+
// 墨のグラデーション(より薄く、自然に)
|
|
1272
|
+
const gradient = context.createRadialGradient(
|
|
1273
|
+
size/2, size/2, 0,
|
|
1274
|
+
size/2, size/2, size/2
|
|
1275
|
+
);
|
|
1276
|
+
gradient.addColorStop(0, 'rgba(0,0,0,0.3)');
|
|
1277
|
+
gradient.addColorStop(0.3, 'rgba(0,0,0,0.2)');
|
|
1278
|
+
gradient.addColorStop(0.7, 'rgba(0,0,0,0.05)');
|
|
1279
|
+
gradient.addColorStop(1, 'rgba(0,0,0,0)');
|
|
1280
|
+
|
|
1281
|
+
context.fillStyle = gradient;
|
|
1282
|
+
context.fillRect(0, 0, size, size);
|
|
1283
|
+
|
|
1284
|
+
// 墨の有機的な滲み
|
|
1285
|
+
for (let i = 0; i < 30; i++) {
|
|
1286
|
+
const x = Math.random() * size;
|
|
1287
|
+
const y = Math.random() * size;
|
|
1288
|
+
const radius = Math.random() * size * 0.2;
|
|
1289
|
+
const opacity = Math.random() * 0.3;
|
|
1290
|
+
|
|
1291
|
+
const grad = context.createRadialGradient(x, y, 0, x, y, radius);
|
|
1292
|
+
grad.addColorStop(0, `rgba(0,0,0,${opacity})`);
|
|
1293
|
+
grad.addColorStop(1, 'rgba(0,0,0,0)');
|
|
1294
|
+
|
|
1295
|
+
context.fillStyle = grad;
|
|
1296
|
+
context.fillRect(0, 0, size, size);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return new THREE.CanvasTexture(canvas);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const inkTexture = createInkTexture();
|
|
1303
|
+
|
|
1304
|
+
// 🌫️ 複数レイヤーの墨雲スプライト
|
|
1305
|
+
for (let layer = 0; layer < 3; layer++) {
|
|
1306
|
+
const layerGroup = new THREE.Group();
|
|
1307
|
+
|
|
1308
|
+
for (let i = 0; i < 6; i++) {
|
|
1309
|
+
const spriteMaterial = new THREE.SpriteMaterial({
|
|
1310
|
+
map: inkTexture,
|
|
1311
|
+
color: new THREE.Color(0x000000),
|
|
1312
|
+
transparent: true,
|
|
1313
|
+
opacity: (0.3 - layer * 0.05) * intensity, // 透明度を上げる
|
|
1314
|
+
blending: THREE.NormalBlending, // 通常ブレンディングに変更
|
|
1315
|
+
depthWrite: false,
|
|
1316
|
+
depthTest: true // 深度テストは有効
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
const sprite = new THREE.Sprite(spriteMaterial);
|
|
1320
|
+
|
|
1321
|
+
// 有機的な配置
|
|
1322
|
+
const angle = (i / 6) * Math.PI * 2;
|
|
1323
|
+
const radius = (15 + Math.random() * 25) * (1 + layer * 0.3);
|
|
1324
|
+
const height = (Math.random() - 0.5) * 15;
|
|
1325
|
+
|
|
1326
|
+
sprite.position.set(
|
|
1327
|
+
Math.cos(angle) * radius + (Math.random() - 0.5) * 10,
|
|
1328
|
+
height + (Math.random() - 0.5) * 8,
|
|
1329
|
+
Math.sin(angle) * radius + (Math.random() - 0.5) * 10
|
|
1330
|
+
);
|
|
1331
|
+
|
|
1332
|
+
// サイズを控えめに調整
|
|
1333
|
+
const scale = (8 + Math.random() * 12) * (1 + layer * 0.2);
|
|
1334
|
+
sprite.scale.set(scale, scale, scale);
|
|
1335
|
+
|
|
1336
|
+
// 回転
|
|
1337
|
+
sprite.material.rotation = Math.random() * Math.PI * 2;
|
|
1338
|
+
|
|
1339
|
+
layerGroup.add(sprite);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
nebulaGroup.add(layerGroup);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// ✨ 中心の濃い墨核
|
|
1346
|
+
const coreMaterial = new THREE.SpriteMaterial({
|
|
1347
|
+
map: inkTexture,
|
|
1348
|
+
color: new THREE.Color(0x000000),
|
|
1349
|
+
transparent: true,
|
|
1350
|
+
opacity: 0.4 * intensity,
|
|
1351
|
+
blending: THREE.MultiplyBlending,
|
|
1352
|
+
depthWrite: false,
|
|
1353
|
+
depthTest: false // 深度テストを無効化して常に表示
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
const coreSprite = new THREE.Sprite(coreMaterial);
|
|
1357
|
+
coreSprite.scale.set(15, 15, 15); // サイズを小さく
|
|
1358
|
+
nebulaGroup.add(coreSprite);
|
|
1359
|
+
|
|
1360
|
+
nebulaGroup.position.set(x, y, z);
|
|
1361
|
+
|
|
1362
|
+
// アニメーション用データ
|
|
1363
|
+
nebulaGroup.userData = {
|
|
1364
|
+
rotationSpeed: 0.0001 + Math.random() * 0.0001,
|
|
1365
|
+
pulseSpeed: 0.3 + Math.random() * 0.3,
|
|
1366
|
+
originalIntensity: intensity
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
scene.add(nebulaGroup);
|
|
1370
|
+
return nebulaGroup;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// 🔲 ふわふわ動く四角の墨塊
|
|
1374
|
+
function createFloatingSquare(x, y, z) {
|
|
1375
|
+
const geometry = new THREE.BoxGeometry(4, 4, 4);
|
|
1376
|
+
const material = new THREE.MeshBasicMaterial({
|
|
1377
|
+
color: 0x000000,
|
|
1378
|
+
transparent: true,
|
|
1379
|
+
opacity: 0.7
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
const cube = new THREE.Mesh(geometry, material);
|
|
1383
|
+
cube.position.set(x, y, z);
|
|
1384
|
+
|
|
1385
|
+
// アニメーション用データ
|
|
1386
|
+
cube.userData = {
|
|
1387
|
+
floatSpeed: 0.002 + Math.random() * 0.002,
|
|
1388
|
+
rotationSpeed: 0.001 + Math.random() * 0.001,
|
|
1389
|
+
floatAmplitude: 3 + Math.random() * 2,
|
|
1390
|
+
originalY: y,
|
|
1391
|
+
phase: Math.random() * Math.PI * 2
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
scene.add(cube);
|
|
1395
|
+
return cube;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// 🎋 竹の茎を生成
|
|
1399
|
+
function createBamboo(x, z) {
|
|
1400
|
+
const bambooGroup = new THREE.Group();
|
|
1401
|
+
|
|
1402
|
+
// 竹の節を複数作成
|
|
1403
|
+
const segments = 5 + Math.floor(Math.random() * 3);
|
|
1404
|
+
const height = 30 + Math.random() * 20;
|
|
1405
|
+
const radius = 0.5 + Math.random() * 0.3;
|
|
1406
|
+
|
|
1407
|
+
for (let i = 0; i < segments; i++) {
|
|
1408
|
+
const segmentHeight = height / segments;
|
|
1409
|
+
const geometry = new THREE.CylinderGeometry(
|
|
1410
|
+
radius * (1 - i * 0.05), // 上に行くほど細く
|
|
1411
|
+
radius * (1 - (i-1) * 0.05),
|
|
1412
|
+
segmentHeight,
|
|
1413
|
+
8
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
const material = new THREE.MeshPhongMaterial({
|
|
1417
|
+
color: 0x1a1a1a, // 暗い灰色
|
|
1418
|
+
emissive: 0x0a0a0a,
|
|
1419
|
+
shininess: 20
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
const segment = new THREE.Mesh(geometry, material);
|
|
1423
|
+
segment.position.y = i * segmentHeight - height/2 + segmentHeight/2;
|
|
1424
|
+
|
|
1425
|
+
// 節の部分に黒い線
|
|
1426
|
+
if (i > 0) {
|
|
1427
|
+
const nodeGeometry = new THREE.TorusGeometry(radius * 1.1, 0.05, 4, 8);
|
|
1428
|
+
const nodeMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
|
1429
|
+
const node = new THREE.Mesh(nodeGeometry, nodeMaterial);
|
|
1430
|
+
node.position.y = segment.position.y - segmentHeight/2;
|
|
1431
|
+
node.rotation.x = Math.PI / 2;
|
|
1432
|
+
bambooGroup.add(node);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
bambooGroup.add(segment);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
bambooGroup.position.set(x, 0, z);
|
|
1439
|
+
bambooGroup.userData = {
|
|
1440
|
+
swaySpeed: 0.001 + Math.random() * 0.001,
|
|
1441
|
+
swayAmplitude: 0.02,
|
|
1442
|
+
originalRotation: { x: 0, z: 0 }
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
return bambooGroup;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// 🪨 枯山水の石を生成
|
|
1449
|
+
function createZenStone(x, y, z, scale = 1) {
|
|
1450
|
+
const geometry = new THREE.DodecahedronGeometry(3 * scale, 0);
|
|
1451
|
+
|
|
1452
|
+
// 頂点をランダムに変形して自然な石の形に
|
|
1453
|
+
const positions = geometry.attributes.position.array;
|
|
1454
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
1455
|
+
positions[i] *= (0.7 + Math.random() * 0.6);
|
|
1456
|
+
positions[i + 1] *= (0.5 + Math.random() * 0.3);
|
|
1457
|
+
positions[i + 2] *= (0.7 + Math.random() * 0.6);
|
|
1458
|
+
}
|
|
1459
|
+
geometry.computeVertexNormals();
|
|
1460
|
+
|
|
1461
|
+
const material = new THREE.MeshPhongMaterial({
|
|
1462
|
+
color: 0x3a3a3a,
|
|
1463
|
+
emissive: 0x0a0a0a,
|
|
1464
|
+
shininess: 10
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
const stone = new THREE.Mesh(geometry, material);
|
|
1468
|
+
stone.position.set(x, y, z);
|
|
1469
|
+
stone.rotation.set(
|
|
1470
|
+
Math.random() * Math.PI,
|
|
1471
|
+
Math.random() * Math.PI,
|
|
1472
|
+
Math.random() * Math.PI
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
// 石の周りの砂紋
|
|
1476
|
+
const ripples = createSandRipples(x, z, scale * 8);
|
|
1477
|
+
|
|
1478
|
+
return { stone, ripples };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// 🌊 砂紋パターン
|
|
1482
|
+
function createSandRipples(centerX, centerZ, radius) {
|
|
1483
|
+
const rippleGroup = new THREE.Group();
|
|
1484
|
+
const rippleCount = 5;
|
|
1485
|
+
|
|
1486
|
+
// radiusが0やNaNの場合のフォールバック
|
|
1487
|
+
const safeRadius = (!radius || isNaN(radius) || radius <= 0) ? 10 : radius;
|
|
1488
|
+
|
|
1489
|
+
for (let i = 0; i < rippleCount; i++) {
|
|
1490
|
+
const innerRadius = Math.max(0.1, safeRadius * (0.5 + i * 0.2));
|
|
1491
|
+
const outerRadius = Math.max(innerRadius + 0.1, innerRadius + 0.5);
|
|
1492
|
+
const geometry = new THREE.RingGeometry(
|
|
1493
|
+
innerRadius,
|
|
1494
|
+
outerRadius,
|
|
1495
|
+
32
|
|
1496
|
+
);
|
|
1497
|
+
|
|
1498
|
+
const material = new THREE.MeshBasicMaterial({
|
|
1499
|
+
color: 0x1a1a1a,
|
|
1500
|
+
transparent: true,
|
|
1501
|
+
opacity: 0.3 - i * 0.05,
|
|
1502
|
+
side: THREE.DoubleSide
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
const ring = new THREE.Mesh(geometry, material);
|
|
1506
|
+
ring.rotation.x = -Math.PI / 2;
|
|
1507
|
+
ring.position.set(centerX, -9.9, centerZ);
|
|
1508
|
+
rippleGroup.add(ring);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
return rippleGroup;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// 💧 水滴エフェクト
|
|
1515
|
+
function createWaterDrop(x, y, z) {
|
|
1516
|
+
const geometry = new THREE.SphereGeometry(0.2, 8, 8);
|
|
1517
|
+
const material = new THREE.MeshPhysicalMaterial({
|
|
1518
|
+
color: 0x666666,
|
|
1519
|
+
transparent: true,
|
|
1520
|
+
opacity: 0.5,
|
|
1521
|
+
roughness: 0,
|
|
1522
|
+
metalness: 0,
|
|
1523
|
+
clearcoat: 1.0,
|
|
1524
|
+
clearcoatRoughness: 0,
|
|
1525
|
+
reflectivity: 0.9,
|
|
1526
|
+
refractionRatio: 0.98
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
const drop = new THREE.Mesh(geometry, material);
|
|
1530
|
+
drop.position.set(x, y, z);
|
|
1531
|
+
|
|
1532
|
+
drop.userData = {
|
|
1533
|
+
fallSpeed: 0.05 + Math.random() * 0.03,
|
|
1534
|
+
originalY: y,
|
|
1535
|
+
splashed: false,
|
|
1536
|
+
lifeTime: 0
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
return drop;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// 墨雲と四角を配置(位置と強度を調整)
|
|
1543
|
+
const inkNebulas = [];
|
|
1544
|
+
inkNebulas.push(createInkNebula(-40, 15, -30, 0.8)); // より遠くに、強度を下げる
|
|
1545
|
+
inkNebulas.push(createInkNebula(35, -10, -25, 0.7));
|
|
1546
|
+
inkNebulas.push(createInkNebula(-15, 25, 40, 0.9)); // より後ろに配置
|
|
1547
|
+
|
|
1548
|
+
const floatingSquares = [];
|
|
1549
|
+
for (let i = 0; i < 8; i++) {
|
|
1550
|
+
const angle = (i / 8) * Math.PI * 2;
|
|
1551
|
+
const radius = 40 + Math.random() * 20;
|
|
1552
|
+
const square = createFloatingSquare(
|
|
1553
|
+
Math.cos(angle) * radius + (Math.random() - 0.5) * 20,
|
|
1554
|
+
(Math.random() - 0.5) * 30,
|
|
1555
|
+
Math.sin(angle) * radius + (Math.random() - 0.5) * 20
|
|
1556
|
+
);
|
|
1557
|
+
floatingSquares.push(square);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// 🎋 竹を配置
|
|
1561
|
+
const bamboos = [];
|
|
1562
|
+
for (let i = 0; i < 6; i++) {
|
|
1563
|
+
const angle = Math.random() * Math.PI * 2;
|
|
1564
|
+
const distance = 25 + Math.random() * 35;
|
|
1565
|
+
const bamboo = createBamboo(
|
|
1566
|
+
Math.cos(angle) * distance,
|
|
1567
|
+
Math.sin(angle) * distance
|
|
1568
|
+
);
|
|
1569
|
+
scene.add(bamboo);
|
|
1570
|
+
bamboos.push(bamboo);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// 🪨 枯山水の石と砂紋を配置
|
|
1574
|
+
const stones = [];
|
|
1575
|
+
for (let i = 0; i < 3; i++) {
|
|
1576
|
+
const x = (Math.random() - 0.5) * 40;
|
|
1577
|
+
const z = (Math.random() - 0.5) * 30;
|
|
1578
|
+
const scale = 0.8 + Math.random() * 0.6;
|
|
1579
|
+
const { stone, ripples } = createZenStone(x, -8, z, scale);
|
|
1580
|
+
scene.add(stone);
|
|
1581
|
+
scene.add(ripples);
|
|
1582
|
+
stones.push({ stone, ripples });
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// 💧 水滴を配置
|
|
1586
|
+
const waterDrops = [];
|
|
1587
|
+
function spawnWaterDrop() {
|
|
1588
|
+
const x = (Math.random() - 0.5) * 50;
|
|
1589
|
+
const z = (Math.random() - 0.5) * 30;
|
|
1590
|
+
const drop = createWaterDrop(x, 25, z);
|
|
1591
|
+
scene.add(drop);
|
|
1592
|
+
waterDrops.push(drop);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// 定期的に水滴を生成
|
|
1596
|
+
setInterval(spawnWaterDrop, 3000);
|
|
1597
|
+
|
|
1598
|
+
// 初期水滴
|
|
1599
|
+
for (let i = 0; i < 5; i++) {
|
|
1600
|
+
setTimeout(spawnWaterDrop, i * 600);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// 墨の塵を追加
|
|
1604
|
+
const inkDust = createInkDust();
|
|
1605
|
+
scene.add(inkDust);
|
|
1606
|
+
|
|
1607
|
+
// 墨の葉を追加
|
|
1608
|
+
const inkLeaves = createInkLeaves();
|
|
1609
|
+
scene.add(inkLeaves);
|
|
1610
|
+
|
|
1611
|
+
// 墨の精霊を追加(ホタルの代わり)
|
|
1612
|
+
const inkSpirits = createInkSpirits();
|
|
1613
|
+
scene.add(inkSpirits);
|
|
1614
|
+
|
|
1615
|
+
// 🌟 金箔パーティクル
|
|
1616
|
+
function createGoldLeaves() {
|
|
1617
|
+
const goldGroup = new THREE.Group();
|
|
1618
|
+
const leafCount = 30;
|
|
1619
|
+
|
|
1620
|
+
const leafGeometry = new THREE.BufferGeometry();
|
|
1621
|
+
const positions = new Float32Array(leafCount * 3);
|
|
1622
|
+
const colors = new Float32Array(leafCount * 3);
|
|
1623
|
+
const sizes = new Float32Array(leafCount);
|
|
1624
|
+
|
|
1625
|
+
for (let i = 0; i < leafCount; i++) {
|
|
1626
|
+
const i3 = i * 3;
|
|
1627
|
+
|
|
1628
|
+
positions[i3] = (Math.random() - 0.5) * 60;
|
|
1629
|
+
positions[i3 + 1] = 20 + Math.random() * 30;
|
|
1630
|
+
positions[i3 + 2] = (Math.random() - 0.5) * 40;
|
|
1631
|
+
|
|
1632
|
+
// 金箔の色(明るい金色)
|
|
1633
|
+
const brightness = 0.8 + Math.random() * 0.2;
|
|
1634
|
+
colors[i3] = 1.0 * brightness; // R
|
|
1635
|
+
colors[i3 + 1] = 0.8 * brightness; // G
|
|
1636
|
+
colors[i3 + 2] = 0.1 * brightness; // B
|
|
1637
|
+
|
|
1638
|
+
sizes[i] = 3 + Math.random() * 4;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
leafGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
1642
|
+
leafGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
1643
|
+
leafGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
|
1644
|
+
|
|
1645
|
+
const leafMaterial = new THREE.PointsMaterial({
|
|
1646
|
+
size: 6,
|
|
1647
|
+
sizeAttenuation: true,
|
|
1648
|
+
vertexColors: true,
|
|
1649
|
+
transparent: true,
|
|
1650
|
+
opacity: 0.8,
|
|
1651
|
+
blending: THREE.AdditiveBlending
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
const goldLeaves = new THREE.Points(leafGeometry, leafMaterial);
|
|
1655
|
+
|
|
1656
|
+
// アニメーション用データ
|
|
1657
|
+
goldLeaves.userData = {
|
|
1658
|
+
fallSpeed: 0.005,
|
|
1659
|
+
swaySpeed: 0.001,
|
|
1660
|
+
glitterSpeed: 0.003,
|
|
1661
|
+
musicIntensity: 0,
|
|
1662
|
+
time: 0
|
|
1663
|
+
};
|
|
1664
|
+
|
|
1665
|
+
goldGroup.add(goldLeaves);
|
|
1666
|
+
return goldGroup;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// 金箔を追加
|
|
1670
|
+
const goldLeaves = createGoldLeaves();
|
|
1671
|
+
scene.add(goldLeaves);
|
|
1672
|
+
|
|
1673
|
+
// ライティング(ミニマル調整)
|
|
1674
|
+
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0);
|
|
1675
|
+
scene.add(ambientLight);
|
|
1676
|
+
|
|
1677
|
+
const spotLight = new THREE.SpotLight(0xffffff, 0.2);
|
|
1678
|
+
spotLight.position.set(0, 20, 10);
|
|
1679
|
+
scene.add(spotLight);
|
|
1680
|
+
|
|
1681
|
+
// カメラ位置
|
|
1682
|
+
camera.position.set(0, 0, 25);
|
|
1683
|
+
camera.lookAt(0, 0, 0);
|
|
1684
|
+
|
|
1685
|
+
// ★ ARレチクル(ヒット位置表示)
|
|
1686
|
+
const reticleGeometry = new THREE.RingGeometry(0.08, 0.10, 32);
|
|
1687
|
+
reticleGeometry.rotateX(-Math.PI / 2);
|
|
1688
|
+
reticleGeometry.computeBoundingSphere(); // NaN防止
|
|
1689
|
+
|
|
1690
|
+
const reticle = new THREE.Mesh(
|
|
1691
|
+
reticleGeometry,
|
|
1692
|
+
new THREE.MeshBasicMaterial({
|
|
1693
|
+
color: 0x333333,
|
|
1694
|
+
side: THREE.DoubleSide,
|
|
1695
|
+
transparent: true,
|
|
1696
|
+
opacity: 0.7
|
|
1697
|
+
})
|
|
1698
|
+
);
|
|
1699
|
+
reticle.matrixAutoUpdate = false;
|
|
1700
|
+
reticle.visible = false;
|
|
1701
|
+
scene.add(reticle);
|
|
1702
|
+
|
|
1703
|
+
// コントロール
|
|
1704
|
+
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
1705
|
+
controls.enableDamping = true;
|
|
1706
|
+
controls.dampingFactor = 0.05;
|
|
1707
|
+
controls.minDistance = 10;
|
|
1708
|
+
controls.maxDistance = 50;
|
|
1709
|
+
|
|
1710
|
+
// 筆ストローク機能は削除(ChocoDrop専用空間)
|
|
1711
|
+
|
|
1712
|
+
// ユーザーのクリック機能は削除(Alt+ドラッグの墨絵描画のみ)
|
|
1713
|
+
|
|
1714
|
+
// 🍫 ChocoDrop初期化
|
|
1715
|
+
let chocoDrop;
|
|
1716
|
+
|
|
1717
|
+
function initChocoDrop() {
|
|
1718
|
+
if (typeof window.ChocoDrop === 'undefined') {
|
|
1719
|
+
console.error('ChocoDrop not loaded. Please check the script path.');
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
try {
|
|
1724
|
+
chocoDrop = window.ChocoDrop.createChocoDrop(scene, {
|
|
1725
|
+
camera: camera,
|
|
1726
|
+
renderer: renderer,
|
|
1727
|
+
serverUrl: null, // GitHub Pagesなので
|
|
1728
|
+
enableServerHealthCheck: false, // サーバーレス環境なのでヘルスチェック無効
|
|
1729
|
+
skipServiceDialog: true, // GitHub Pages用:サービス設定ダイアログを無効化
|
|
1730
|
+
sceneOptions: {
|
|
1731
|
+
enableMouseInteraction: true
|
|
1732
|
+
},
|
|
1733
|
+
onControlsToggle: (disabled) => {
|
|
1734
|
+
controls.enabled = !disabled;
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
console.log('🍵 侘び寂びの世界へようこそ');
|
|
1739
|
+
console.log('ChocoDrop loaded successfully!');
|
|
1740
|
+
console.log('Press @ to activate ChocoDrop');
|
|
1741
|
+
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
console.error('ChocoDrop initialization failed:', error);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// ChocoDrop読み込み待機
|
|
1748
|
+
function waitForChocoDrop() {
|
|
1749
|
+
if (typeof window.ChocoDrop !== 'undefined') {
|
|
1750
|
+
initChocoDrop();
|
|
1751
|
+
} else {
|
|
1752
|
+
console.log('Waiting for ChocoDrop to load...');
|
|
1753
|
+
setTimeout(waitForChocoDrop, 500);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
setTimeout(waitForChocoDrop, 1000);
|
|
1758
|
+
|
|
1759
|
+
// 🎨 ChocoDrop のオレンジ色を動的に墨色に変更
|
|
1760
|
+
function enforceInkStyle() {
|
|
1761
|
+
const elements = document.querySelectorAll('*');
|
|
1762
|
+
elements.forEach(el => {
|
|
1763
|
+
const style = el.style;
|
|
1764
|
+
const computedStyle = window.getComputedStyle(el);
|
|
1765
|
+
|
|
1766
|
+
// オレンジ/赤系の背景色を墨色に変更
|
|
1767
|
+
if (style.backgroundColor &&
|
|
1768
|
+
(style.backgroundColor.includes('rgb(255') ||
|
|
1769
|
+
style.backgroundColor.includes('#ff') ||
|
|
1770
|
+
style.backgroundColor.includes('orange'))) {
|
|
1771
|
+
el.style.backgroundColor = 'rgba(30, 30, 30, 0.1)';
|
|
1772
|
+
el.style.borderColor = 'rgba(30, 30, 30, 0.8)';
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// 境界線の色も変更
|
|
1776
|
+
if (style.borderColor &&
|
|
1777
|
+
(style.borderColor.includes('rgb(255') ||
|
|
1778
|
+
style.borderColor.includes('#ff'))) {
|
|
1779
|
+
el.style.borderColor = 'rgba(30, 30, 30, 0.8)';
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// 定期的にスタイルをチェック・修正
|
|
1785
|
+
setInterval(enforceInkStyle, 500);
|
|
1786
|
+
|
|
1787
|
+
// 🎵 音楽解析システム
|
|
1788
|
+
let audioContext = null;
|
|
1789
|
+
let analyser = null;
|
|
1790
|
+
let dataArray = null;
|
|
1791
|
+
let beatDetectionHistory = [];
|
|
1792
|
+
let lastBeatTime = 0;
|
|
1793
|
+
let musicData = {
|
|
1794
|
+
volume: 0,
|
|
1795
|
+
bass: 0,
|
|
1796
|
+
mid: 0,
|
|
1797
|
+
treble: 0,
|
|
1798
|
+
tempo: 0,
|
|
1799
|
+
bpm: 0,
|
|
1800
|
+
beatDetected: false,
|
|
1801
|
+
isPlaying: false
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
function setupAudioAnalysis() {
|
|
1805
|
+
if (!audioContext) {
|
|
1806
|
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
1807
|
+
analyser = audioContext.createAnalyser();
|
|
1808
|
+
analyser.fftSize = 256;
|
|
1809
|
+
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function connectVideoToAnalyser(videoElement) {
|
|
1814
|
+
if (!audioContext || !analyser) return;
|
|
1815
|
+
|
|
1816
|
+
try {
|
|
1817
|
+
const source = audioContext.createMediaElementSource(videoElement);
|
|
1818
|
+
source.connect(analyser);
|
|
1819
|
+
analyser.connect(audioContext.destination);
|
|
1820
|
+
musicData.isPlaying = true;
|
|
1821
|
+
console.log('🎵 音楽解析開始');
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
console.log('音楽解析エラー:', error);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function analyzeMusicData() {
|
|
1828
|
+
if (!analyser || !dataArray) return;
|
|
1829
|
+
|
|
1830
|
+
analyser.getByteFrequencyData(dataArray);
|
|
1831
|
+
|
|
1832
|
+
// 音量計算
|
|
1833
|
+
let sum = 0;
|
|
1834
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
1835
|
+
sum += dataArray[i];
|
|
1836
|
+
}
|
|
1837
|
+
musicData.volume = sum / dataArray.length / 255;
|
|
1838
|
+
|
|
1839
|
+
// 周波数帯域分析
|
|
1840
|
+
const bassRange = Math.floor(dataArray.length * 0.1);
|
|
1841
|
+
const midRange = Math.floor(dataArray.length * 0.5);
|
|
1842
|
+
|
|
1843
|
+
let bassSum = 0, midSum = 0, trebleSum = 0;
|
|
1844
|
+
|
|
1845
|
+
for (let i = 0; i < bassRange; i++) {
|
|
1846
|
+
bassSum += dataArray[i];
|
|
1847
|
+
}
|
|
1848
|
+
for (let i = bassRange; i < midRange; i++) {
|
|
1849
|
+
midSum += dataArray[i];
|
|
1850
|
+
}
|
|
1851
|
+
for (let i = midRange; i < dataArray.length; i++) {
|
|
1852
|
+
trebleSum += dataArray[i];
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
musicData.bass = bassSum / bassRange / 255;
|
|
1856
|
+
musicData.mid = midSum / (midRange - bassRange) / 255;
|
|
1857
|
+
musicData.treble = trebleSum / (dataArray.length - midRange) / 255;
|
|
1858
|
+
|
|
1859
|
+
// 🥁 ビート検出とBPM計算
|
|
1860
|
+
const currentTime = performance.now();
|
|
1861
|
+
const bassThreshold = 0.3;
|
|
1862
|
+
const volumeThreshold = 0.2;
|
|
1863
|
+
|
|
1864
|
+
// ビート検出(ベース音量の急激な変化)
|
|
1865
|
+
if (musicData.bass > bassThreshold && musicData.volume > volumeThreshold) {
|
|
1866
|
+
if (currentTime - lastBeatTime > 300) { // 最小300ms間隔
|
|
1867
|
+
musicData.beatDetected = true;
|
|
1868
|
+
lastBeatTime = currentTime;
|
|
1869
|
+
|
|
1870
|
+
// BPM計算用履歴更新
|
|
1871
|
+
beatDetectionHistory.push(currentTime);
|
|
1872
|
+
if (beatDetectionHistory.length > 8) {
|
|
1873
|
+
beatDetectionHistory.shift();
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// BPM計算
|
|
1877
|
+
if (beatDetectionHistory.length >= 4) {
|
|
1878
|
+
const intervals = [];
|
|
1879
|
+
for (let i = 1; i < beatDetectionHistory.length; i++) {
|
|
1880
|
+
intervals.push(beatDetectionHistory[i] - beatDetectionHistory[i-1]);
|
|
1881
|
+
}
|
|
1882
|
+
const avgInterval = intervals.reduce((a, b) => a + b) / intervals.length;
|
|
1883
|
+
musicData.bpm = 60000 / avgInterval; // ms → BPM
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
} else {
|
|
1887
|
+
musicData.beatDetected = false;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// 🎛️ リアルタイム音楽データ表示を更新
|
|
1891
|
+
updateMusicDebugDisplay();
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// 🎛️ 音楽データのリアルタイム表示
|
|
1895
|
+
let musicDebugVisible = false; // デフォルトで非表示
|
|
1896
|
+
|
|
1897
|
+
function updateMusicDebugDisplay() {
|
|
1898
|
+
let debugPanel = document.getElementById('music-debug');
|
|
1899
|
+
|
|
1900
|
+
if (!debugPanel) {
|
|
1901
|
+
debugPanel = document.createElement('div');
|
|
1902
|
+
debugPanel.id = 'music-debug';
|
|
1903
|
+
debugPanel.style.cssText = `
|
|
1904
|
+
position: fixed;
|
|
1905
|
+
bottom: 20px;
|
|
1906
|
+
left: 20px;
|
|
1907
|
+
background: rgba(0, 0, 0, 0.8);
|
|
1908
|
+
color: #00ff88;
|
|
1909
|
+
padding: 10px;
|
|
1910
|
+
border-radius: 6px;
|
|
1911
|
+
font-family: 'Monaco', monospace;
|
|
1912
|
+
font-size: 10px;
|
|
1913
|
+
z-index: 100;
|
|
1914
|
+
min-width: 200px;
|
|
1915
|
+
backdrop-filter: blur(10px);
|
|
1916
|
+
border: 1px solid rgba(0, 255, 136, 0.3);
|
|
1917
|
+
display: none;
|
|
1918
|
+
`;
|
|
1919
|
+
document.body.appendChild(debugPanel);
|
|
1920
|
+
|
|
1921
|
+
// クリックで非表示
|
|
1922
|
+
debugPanel.addEventListener('click', () => {
|
|
1923
|
+
musicDebugVisible = false;
|
|
1924
|
+
debugPanel.style.display = 'none';
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// 非表示の時は更新をスキップ(パフォーマンス向上)
|
|
1929
|
+
if (!musicDebugVisible) {
|
|
1930
|
+
debugPanel.style.display = 'none';
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
debugPanel.style.display = 'block';
|
|
1935
|
+
|
|
1936
|
+
if (musicData.isPlaying) {
|
|
1937
|
+
debugPanel.innerHTML = `
|
|
1938
|
+
<div style="color: #00ff88; font-weight: bold;">🎵 MUSIC ACTIVE</div>
|
|
1939
|
+
<div>Volume: ${'█'.repeat(Math.floor(musicData.volume * 20))} ${(musicData.volume * 100).toFixed(0)}%</div>
|
|
1940
|
+
<div>Bass: ${'█'.repeat(Math.floor(musicData.bass * 20))} ${(musicData.bass * 100).toFixed(0)}%</div>
|
|
1941
|
+
<div>Mid: ${'█'.repeat(Math.floor(musicData.mid * 20))} ${(musicData.mid * 100).toFixed(0)}%</div>
|
|
1942
|
+
<div>Treble: ${'█'.repeat(Math.floor(musicData.treble * 20))} ${(musicData.treble * 100).toFixed(0)}%</div>
|
|
1943
|
+
<div>BPM: ${musicData.bpm ? musicData.bpm.toFixed(0) : '--'}</div>
|
|
1944
|
+
<div>${musicData.beatDetected ? '🥁 BEAT!' : '⚪ ----'}</div>
|
|
1945
|
+
<div style="font-size: 8px; color: #666; margin-top: 5px;">Press M or Click to hide</div>
|
|
1946
|
+
`;
|
|
1947
|
+
} else {
|
|
1948
|
+
debugPanel.innerHTML = `
|
|
1949
|
+
<div style="color: #ff6666;">🔇 NO MUSIC</div>
|
|
1950
|
+
<div style="font-size: 8px; color: #666;">Video with audio needed</div>
|
|
1951
|
+
<div style="font-size: 8px; color: #666; margin-top: 5px;">Press M or Click to hide</div>
|
|
1952
|
+
`;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// Mキーで音楽デバッグパネルを切り替え
|
|
1957
|
+
document.addEventListener('keydown', (e) => {
|
|
1958
|
+
if (e.key === 'm' || e.key === 'M') {
|
|
1959
|
+
musicDebugVisible = !musicDebugVisible;
|
|
1960
|
+
updateMusicDebugDisplay();
|
|
1961
|
+
console.log(`🎵 Music debug panel ${musicDebugVisible ? 'shown' : 'hidden'}`);
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
// ChocoDrop の動画要素を監視
|
|
1966
|
+
function observeVideoElements() {
|
|
1967
|
+
const observer = new MutationObserver((mutations) => {
|
|
1968
|
+
mutations.forEach((mutation) => {
|
|
1969
|
+
mutation.addedNodes.forEach((node) => {
|
|
1970
|
+
if (node.nodeType === 1) { // ELEMENT_NODE
|
|
1971
|
+
const videos = node.tagName === 'VIDEO' ? [node] : node.querySelectorAll('video');
|
|
1972
|
+
videos.forEach((video) => {
|
|
1973
|
+
video.addEventListener('play', () => {
|
|
1974
|
+
setupAudioAnalysis();
|
|
1975
|
+
connectVideoToAnalyser(video);
|
|
1976
|
+
});
|
|
1977
|
+
video.addEventListener('pause', () => {
|
|
1978
|
+
musicData.isPlaying = false;
|
|
1979
|
+
});
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
});
|
|
1984
|
+
});
|
|
1985
|
+
|
|
1986
|
+
observer.observe(document.body, {
|
|
1987
|
+
childList: true,
|
|
1988
|
+
subtree: true
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// 監視開始
|
|
1993
|
+
observeVideoElements();
|
|
1994
|
+
|
|
1995
|
+
// 🎵 強化されたVideoTexture音声接続システム
|
|
1996
|
+
function monitorVideoTextures() {
|
|
1997
|
+
let foundVideo = false;
|
|
1998
|
+
let connectionAttempts = 0;
|
|
1999
|
+
|
|
2000
|
+
// Scene内のすべてのオブジェクトを詳細検査
|
|
2001
|
+
scene.traverse((child) => {
|
|
2002
|
+
// VideoTexture検出(複数パターン対応)
|
|
2003
|
+
if (child.material) {
|
|
2004
|
+
const materials = Array.isArray(child.material) ? child.material : [child.material];
|
|
2005
|
+
|
|
2006
|
+
materials.forEach(material => {
|
|
2007
|
+
// map, emissiveMap, normalMapなど全テクスチャをチェック
|
|
2008
|
+
['map', 'emissiveMap', 'normalMap', 'roughnessMap'].forEach(texType => {
|
|
2009
|
+
const texture = material[texType];
|
|
2010
|
+
if (texture && texture.image instanceof HTMLVideoElement) {
|
|
2011
|
+
const video = texture.image;
|
|
2012
|
+
|
|
2013
|
+
if (!video.audioConnected) {
|
|
2014
|
+
console.log(`🎵 Found VideoTexture (${texType}):`, video.src);
|
|
2015
|
+
connectVideoWithRetry(video);
|
|
2016
|
+
foundVideo = true;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
// DOM内のvideo要素も監視(ChocoDrop要素用)
|
|
2025
|
+
document.querySelectorAll('video').forEach(video => {
|
|
2026
|
+
if (!video.audioConnected && video.src) {
|
|
2027
|
+
console.log('🎵 Found DOM video element:', video.src);
|
|
2028
|
+
connectVideoWithRetry(video);
|
|
2029
|
+
foundVideo = true;
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
|
|
2033
|
+
if (foundVideo) {
|
|
2034
|
+
console.log('✅ VideoTexture detection completed');
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// リトライ機能付き音声接続
|
|
2039
|
+
function connectVideoWithRetry(video, retries = 3) {
|
|
2040
|
+
const attemptConnection = () => {
|
|
2041
|
+
try {
|
|
2042
|
+
if (video.paused) {
|
|
2043
|
+
// 再生待ち
|
|
2044
|
+
video.addEventListener('play', () => {
|
|
2045
|
+
console.log('▶️ Video started playing, connecting audio...');
|
|
2046
|
+
setupAudioAnalysis();
|
|
2047
|
+
connectVideoToAnalyser(video);
|
|
2048
|
+
video.audioConnected = true;
|
|
2049
|
+
showMusicStatus(true);
|
|
2050
|
+
}, { once: true });
|
|
2051
|
+
} else {
|
|
2052
|
+
// 既に再生中
|
|
2053
|
+
console.log('🎬 Video already playing, connecting now...');
|
|
2054
|
+
setupAudioAnalysis();
|
|
2055
|
+
connectVideoToAnalyser(video);
|
|
2056
|
+
video.audioConnected = true;
|
|
2057
|
+
showMusicStatus(true);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// 停止/一時停止の監視
|
|
2061
|
+
video.addEventListener('pause', () => {
|
|
2062
|
+
musicData.isPlaying = false;
|
|
2063
|
+
showMusicStatus(false);
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
video.addEventListener('ended', () => {
|
|
2067
|
+
musicData.isPlaying = false;
|
|
2068
|
+
showMusicStatus(false);
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
console.log(`音声接続エラー (残り${retries}回):`, error);
|
|
2073
|
+
if (retries > 0) {
|
|
2074
|
+
setTimeout(() => connectVideoWithRetry(video, retries - 1), 1000);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
|
|
2079
|
+
attemptConnection();
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// 音楽状況の可視化
|
|
2083
|
+
function showMusicStatus(isConnected) {
|
|
2084
|
+
// 既存のステータス表示を削除
|
|
2085
|
+
const existingStatus = document.getElementById('music-status');
|
|
2086
|
+
if (existingStatus) {
|
|
2087
|
+
existingStatus.remove();
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// 新しいステータス表示を作成
|
|
2091
|
+
const statusDiv = document.createElement('div');
|
|
2092
|
+
statusDiv.id = 'music-status';
|
|
2093
|
+
statusDiv.style.cssText = `
|
|
2094
|
+
position: fixed;
|
|
2095
|
+
top: 80px;
|
|
2096
|
+
left: 16px;
|
|
2097
|
+
background: ${isConnected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(239, 68, 68, 0.15)'};
|
|
2098
|
+
border: 1px solid ${isConnected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)'};
|
|
2099
|
+
color: ${isConnected ? '#16a34a' : '#dc2626'};
|
|
2100
|
+
padding: 6px 10px;
|
|
2101
|
+
border-radius: 6px;
|
|
2102
|
+
font-size: 11px;
|
|
2103
|
+
z-index: 100;
|
|
2104
|
+
backdrop-filter: blur(10px);
|
|
2105
|
+
transition: all 0.3s ease;
|
|
2106
|
+
`;
|
|
2107
|
+
statusDiv.textContent = isConnected ? '🎵 音楽連動 ON' : '🔇 音楽未検出';
|
|
2108
|
+
document.body.appendChild(statusDiv);
|
|
2109
|
+
|
|
2110
|
+
// 5秒後に自動で非表示
|
|
2111
|
+
setTimeout(() => {
|
|
2112
|
+
if (statusDiv.parentNode) {
|
|
2113
|
+
statusDiv.style.opacity = '0';
|
|
2114
|
+
setTimeout(() => statusDiv.remove(), 300);
|
|
2115
|
+
}
|
|
2116
|
+
}, 5000);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// より頻繁な監視(500ms間隔)
|
|
2120
|
+
setInterval(monitorVideoTextures, 500);
|
|
2121
|
+
|
|
2122
|
+
// 初回実行を早める
|
|
2123
|
+
setTimeout(monitorVideoTextures, 1000);
|
|
2124
|
+
|
|
2125
|
+
// ★ AR ヒットテスト用変数
|
|
2126
|
+
let hitTestSource = null;
|
|
2127
|
+
let hitTestSourceRequested = false;
|
|
2128
|
+
|
|
2129
|
+
// ★ ARタップで墨のオブジェクトを配置
|
|
2130
|
+
const onSelect = () => {
|
|
2131
|
+
if (!reticle.visible) return;
|
|
2132
|
+
|
|
2133
|
+
// レチクル位置に墨の精霊を配置
|
|
2134
|
+
const spirit = createInkSpirits().children[0]; // 単体の精霊を作成
|
|
2135
|
+
spirit.position.copy(reticle.position);
|
|
2136
|
+
spirit.position.y += 0.5; // 少し浮かせる
|
|
2137
|
+
scene.add(spirit);
|
|
2138
|
+
|
|
2139
|
+
console.log('🖤 墨の精霊を配置:', reticle.position);
|
|
2140
|
+
};
|
|
2141
|
+
|
|
2142
|
+
// XRセッション開始時の設定
|
|
2143
|
+
renderer.xr.addEventListener('sessionstart', () => {
|
|
2144
|
+
const session = renderer.xr.getSession();
|
|
2145
|
+
if (session) {
|
|
2146
|
+
session.addEventListener('select', onSelect);
|
|
2147
|
+
console.log('🥽 XRセッション開始');
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
|
|
2151
|
+
// アニメーションループ(XR対応)
|
|
2152
|
+
renderer.setAnimationLoop((timestamp, frame) => {
|
|
2153
|
+
const time = performance.now() * 0.001;
|
|
2154
|
+
|
|
2155
|
+
// ★ ARヒットテスト処理
|
|
2156
|
+
const session = renderer.xr.getSession();
|
|
2157
|
+
if (session && frame) {
|
|
2158
|
+
// 初回だけヒットテストsourceを取得
|
|
2159
|
+
if (!hitTestSourceRequested) {
|
|
2160
|
+
session.requestReferenceSpace('viewer').then((viewerRefSpace) => {
|
|
2161
|
+
session.requestHitTestSource({ space: viewerRefSpace }).then((source) => {
|
|
2162
|
+
hitTestSource = source;
|
|
2163
|
+
});
|
|
2164
|
+
});
|
|
2165
|
+
hitTestSourceRequested = true;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
if (hitTestSource) {
|
|
2169
|
+
const referenceSpace = renderer.xr.getReferenceSpace();
|
|
2170
|
+
const hitTestResults = frame.getHitTestResults(hitTestSource);
|
|
2171
|
+
|
|
2172
|
+
if (hitTestResults.length) {
|
|
2173
|
+
const hit = hitTestResults[0];
|
|
2174
|
+
const pose = hit.getPose(referenceSpace);
|
|
2175
|
+
reticle.visible = true;
|
|
2176
|
+
reticle.matrix.fromArray(pose.transform.matrix);
|
|
2177
|
+
} else {
|
|
2178
|
+
reticle.visible = false;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// 🎵 音楽データ更新
|
|
2184
|
+
if (musicData.isPlaying) {
|
|
2185
|
+
analyzeMusicData();
|
|
2186
|
+
|
|
2187
|
+
// 高音域が強いときに追加の水滴を生成
|
|
2188
|
+
if (musicData.treble > 0.6 && Math.random() < 0.1) {
|
|
2189
|
+
const newDrop = createWaterDrop();
|
|
2190
|
+
scene.add(newDrop);
|
|
2191
|
+
waterDrops.push(newDrop);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// 墨雲のアニメーション(音楽連動・美しく控えめに)
|
|
2196
|
+
inkNebulas.forEach((nebula, index) => {
|
|
2197
|
+
if (nebula.userData && nebula.userData.rotationSpeed) {
|
|
2198
|
+
// 音楽に合わせて回転速度調整(控えめ)
|
|
2199
|
+
const musicBoost = musicData.isPlaying ? 1 + musicData.volume * 0.5 : 1;
|
|
2200
|
+
nebula.rotation.y += nebula.userData.rotationSpeed * musicBoost;
|
|
2201
|
+
nebula.rotation.z += nebula.userData.rotationSpeed * 0.3 * musicBoost;
|
|
2202
|
+
|
|
2203
|
+
// 音楽の低音に合わせて微細なパルス(美しく制限)
|
|
2204
|
+
const bassPulse = musicData.isPlaying ?
|
|
2205
|
+
Math.sin(time * 5) * musicData.bass * 0.05 : 0; // 大幅に縮小
|
|
2206
|
+
const naturalPulse = Math.sin(time * nebula.userData.pulseSpeed) * 0.02 + 1;
|
|
2207
|
+
const finalPulse = Math.max(0.8, Math.min(1.2, naturalPulse + bassPulse)); // 範囲制限
|
|
2208
|
+
nebula.scale.setScalar(finalPulse);
|
|
2209
|
+
|
|
2210
|
+
// 透明度で音楽表現(新しい美しさ)
|
|
2211
|
+
nebula.children.forEach(layer => {
|
|
2212
|
+
layer.children.forEach(sprite => {
|
|
2213
|
+
if (sprite.material) {
|
|
2214
|
+
const musicGlow = musicData.isPlaying ?
|
|
2215
|
+
Math.sin(time * 3) * musicData.volume * 0.1 : 0;
|
|
2216
|
+
const baseOpacity = sprite.material.userData?.originalOpacity || sprite.material.opacity;
|
|
2217
|
+
sprite.material.opacity = Math.max(0.1, Math.min(1.0, baseOpacity + musicGlow));
|
|
2218
|
+
|
|
2219
|
+
// 初回のみ元の透明度を保存
|
|
2220
|
+
if (!sprite.material.userData) {
|
|
2221
|
+
sprite.material.userData = { originalOpacity: sprite.material.opacity };
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
// ふわふわ四角のアニメーション(音楽連動)
|
|
2230
|
+
floatingSquares.forEach(square => {
|
|
2231
|
+
const userData = square.userData;
|
|
2232
|
+
|
|
2233
|
+
// 音楽ビートで適度な脈動
|
|
2234
|
+
const beatPulse = musicData.isPlaying && musicData.beatDetected ?
|
|
2235
|
+
Math.sin(time * 12) * 0.4 + 1.0 : 1.0; // 適度な脈動
|
|
2236
|
+
|
|
2237
|
+
// 音楽ボリュームで浮遊の強さを適度に調整
|
|
2238
|
+
const volumeBoost = musicData.isPlaying ?
|
|
2239
|
+
1 + musicData.volume * 0.6 : 1; // 適度なボリューム反応
|
|
2240
|
+
|
|
2241
|
+
// ベース音で追加の動き
|
|
2242
|
+
const bassFloat = musicData.isPlaying ?
|
|
2243
|
+
Math.sin(time * 6) * musicData.bass * 2 : 0;
|
|
2244
|
+
|
|
2245
|
+
// 上下浮遊(音楽で適度に活発に)
|
|
2246
|
+
square.position.y = userData.originalY +
|
|
2247
|
+
Math.sin(time * userData.floatSpeed * volumeBoost + userData.phase) *
|
|
2248
|
+
userData.floatAmplitude * beatPulse + bassFloat;
|
|
2249
|
+
|
|
2250
|
+
// より有機的な回転(音楽で適度に速く)
|
|
2251
|
+
const rotationBoost = musicData.isPlaying ? 1 + musicData.mid * 0.4 : 1;
|
|
2252
|
+
square.rotation.x += userData.rotationSpeed * rotationBoost;
|
|
2253
|
+
square.rotation.y += userData.rotationSpeed * 0.7 * rotationBoost;
|
|
2254
|
+
square.rotation.z += userData.rotationSpeed * 0.3 * rotationBoost;
|
|
2255
|
+
|
|
2256
|
+
// 透明度の微妙な変化(音楽で息づくように)
|
|
2257
|
+
if (square.material) {
|
|
2258
|
+
const breathe = Math.sin(time * 2 + userData.phase) * 0.1 + 0.9;
|
|
2259
|
+
const musicBreath = musicData.isPlaying ?
|
|
2260
|
+
musicData.volume * 0.1 : 0;
|
|
2261
|
+
square.material.opacity = Math.min(1.0, breathe + musicBreath);
|
|
2262
|
+
}
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
// 墨の塵のパルス
|
|
2266
|
+
if (inkDust.userData) {
|
|
2267
|
+
inkDust.userData.time = time;
|
|
2268
|
+
const pulse = Math.sin(time * inkDust.userData.pulseSpeed) * 0.5 + 0.5;
|
|
2269
|
+
inkDust.children[0].material.opacity = 0.3 + pulse * 0.3;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// 墨の葉の落下
|
|
2273
|
+
if (inkLeaves.userData) {
|
|
2274
|
+
inkLeaves.userData.time = time;
|
|
2275
|
+
const positions = inkLeaves.geometry.attributes.position.array;
|
|
2276
|
+
|
|
2277
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
2278
|
+
// ゆっくり落下
|
|
2279
|
+
positions[i + 1] -= inkLeaves.userData.fallSpeed;
|
|
2280
|
+
|
|
2281
|
+
// 左右に揺れる
|
|
2282
|
+
positions[i] += Math.sin(time * inkLeaves.userData.swaySpeed + i) * 0.01;
|
|
2283
|
+
|
|
2284
|
+
// 下まで落ちたら上に戻す
|
|
2285
|
+
if (positions[i + 1] < -10) {
|
|
2286
|
+
positions[i + 1] = 20;
|
|
2287
|
+
positions[i] = (Math.random() - 0.5) * 30;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
inkLeaves.geometry.attributes.position.needsUpdate = true;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// 🎋 竹のゆらぎアニメーション(音楽連動)
|
|
2295
|
+
bamboos.forEach(bamboo => {
|
|
2296
|
+
const userData = bamboo.userData;
|
|
2297
|
+
// 基本の揺れ + 音楽ビートで強調
|
|
2298
|
+
const beatBoost = musicData.isPlaying && musicData.beatDetected ? 2.0 : 1.0;
|
|
2299
|
+
const volumeBoost = musicData.isPlaying ? 1 + musicData.volume : 1;
|
|
2300
|
+
|
|
2301
|
+
bamboo.rotation.x = Math.sin(time * userData.swaySpeed * volumeBoost) *
|
|
2302
|
+
userData.swayAmplitude * beatBoost;
|
|
2303
|
+
bamboo.rotation.z = Math.cos(time * userData.swaySpeed * 1.3 * volumeBoost) *
|
|
2304
|
+
userData.swayAmplitude * 0.5 * beatBoost;
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
// 🪨 砂紋の波紋アニメーション(音楽連動・強化版)
|
|
2308
|
+
stones.forEach(({ stone, ripples }) => {
|
|
2309
|
+
// 石本体の音楽連動アニメーション(強化版)
|
|
2310
|
+
if (musicData.isPlaying) {
|
|
2311
|
+
// 音楽ビートで石が明確に震える(効果を大幅強化)
|
|
2312
|
+
const beatShake = musicData.beatDetected ?
|
|
2313
|
+
(Math.random() - 0.5) * 0.4 : 0;
|
|
2314
|
+
|
|
2315
|
+
// ベース音で上下に大きく動く
|
|
2316
|
+
const bassMovement = Math.sin(time * 8) * musicData.bass * 1.2;
|
|
2317
|
+
|
|
2318
|
+
// 音量で回転速度が大幅に変わる
|
|
2319
|
+
const rotationSpeed = musicData.volume * 0.01;
|
|
2320
|
+
|
|
2321
|
+
// 高音域で左右に揺れる
|
|
2322
|
+
const trebleShake = Math.sin(time * 12) * musicData.treble * 0.6;
|
|
2323
|
+
|
|
2324
|
+
// 石の位置を音楽に合わせて調整(より大きな変化)
|
|
2325
|
+
if (!stone.userData.originalPosition) {
|
|
2326
|
+
stone.userData.originalPosition = stone.position.clone();
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
stone.position.x = stone.userData.originalPosition.x + beatShake + trebleShake;
|
|
2330
|
+
stone.position.y = stone.userData.originalPosition.y + bassMovement;
|
|
2331
|
+
stone.position.z = stone.userData.originalPosition.z + beatShake * 0.8;
|
|
2332
|
+
|
|
2333
|
+
// 回転も音楽に大きく連動
|
|
2334
|
+
stone.rotation.y += rotationSpeed;
|
|
2335
|
+
stone.rotation.x += rotationSpeed * 0.5;
|
|
2336
|
+
stone.rotation.z += rotationSpeed * 0.3;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// 砂紋の波紋アニメーション
|
|
2340
|
+
ripples.children.forEach((ring, index) => {
|
|
2341
|
+
// 音楽ビートで大きく拡張(効果を強化)
|
|
2342
|
+
const beatPulse = musicData.beatDetected ? 0.3 : 0;
|
|
2343
|
+
const bassWave = musicData.isPlaying ? musicData.bass * 0.2 : 0;
|
|
2344
|
+
const volumeBoost = musicData.isPlaying ? musicData.volume * 0.15 : 0;
|
|
2345
|
+
|
|
2346
|
+
// より目立つスケール変化
|
|
2347
|
+
const baseWave = Math.sin(time * 0.8 - index * 0.5) * 0.1;
|
|
2348
|
+
const scale = 1 + baseWave + beatPulse + bassWave + volumeBoost;
|
|
2349
|
+
ring.scale.set(scale, scale, scale);
|
|
2350
|
+
|
|
2351
|
+
// より明瞭な透明度変化
|
|
2352
|
+
const baseOpacity = 0.4 - index * 0.06;
|
|
2353
|
+
const fadeWave = Math.sin(time * 0.8 - index * 0.5) * 0.1;
|
|
2354
|
+
const musicOpacity = beatPulse * 0.4 + volumeBoost * 0.3;
|
|
2355
|
+
ring.material.opacity = Math.max(0, Math.min(0.8, baseOpacity + fadeWave + musicOpacity));
|
|
2356
|
+
});
|
|
2357
|
+
});
|
|
2358
|
+
|
|
2359
|
+
// 💧 水滴の落下アニメーション(音楽連動)
|
|
2360
|
+
waterDrops.forEach((drop, index) => {
|
|
2361
|
+
if (drop.userData.splashed) {
|
|
2362
|
+
drop.userData.lifeTime += 0.016;
|
|
2363
|
+
if (drop.userData.lifeTime > 2) {
|
|
2364
|
+
// 水滴を削除して再利用
|
|
2365
|
+
scene.remove(drop);
|
|
2366
|
+
waterDrops.splice(index, 1);
|
|
2367
|
+
}
|
|
2368
|
+
} else {
|
|
2369
|
+
// 落下(高音域で速度増加)
|
|
2370
|
+
const trebleBoost = musicData.isPlaying ? 1 + musicData.treble * 0.5 : 1;
|
|
2371
|
+
drop.position.y -= drop.userData.fallSpeed * trebleBoost;
|
|
2372
|
+
|
|
2373
|
+
// 地面に達したら波紋エフェクト
|
|
2374
|
+
if (drop.position.y < -9) {
|
|
2375
|
+
drop.userData.splashed = true;
|
|
2376
|
+
// 波紋エフェクトの作成
|
|
2377
|
+
const splashGeometry = new THREE.RingGeometry(0.5, 2, 32);
|
|
2378
|
+
splashGeometry.computeBoundingSphere(); // NaN防止
|
|
2379
|
+
const splashMaterial = new THREE.MeshBasicMaterial({
|
|
2380
|
+
color: 0x444444,
|
|
2381
|
+
transparent: true,
|
|
2382
|
+
opacity: 0.2,
|
|
2383
|
+
side: THREE.DoubleSide
|
|
2384
|
+
});
|
|
2385
|
+
const splash = new THREE.Mesh(splashGeometry, splashMaterial);
|
|
2386
|
+
splash.rotation.x = -Math.PI / 2;
|
|
2387
|
+
splash.position.copy(drop.position);
|
|
2388
|
+
splash.position.y = -9.8;
|
|
2389
|
+
scene.add(splash);
|
|
2390
|
+
|
|
2391
|
+
// 波紋を広げるアニメーション
|
|
2392
|
+
const expandSplash = setInterval(() => {
|
|
2393
|
+
splash.scale.x *= 1.05;
|
|
2394
|
+
splash.scale.y *= 1.05;
|
|
2395
|
+
splash.material.opacity *= 0.95;
|
|
2396
|
+
if (splash.material.opacity < 0.01) {
|
|
2397
|
+
scene.remove(splash);
|
|
2398
|
+
clearInterval(expandSplash);
|
|
2399
|
+
}
|
|
2400
|
+
}, 16);
|
|
2401
|
+
|
|
2402
|
+
drop.visible = false;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
});
|
|
2406
|
+
|
|
2407
|
+
// 桜のアニメーションは削除
|
|
2408
|
+
|
|
2409
|
+
// 🌟 金箔パーティクルアニメーション(音楽連動)
|
|
2410
|
+
if (goldLeaves.userData) {
|
|
2411
|
+
const positions = goldLeaves.children[0].geometry.attributes.position.array;
|
|
2412
|
+
const colors = goldLeaves.children[0].geometry.attributes.color.array;
|
|
2413
|
+
|
|
2414
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
2415
|
+
// 音楽ビートで金箔が舞い踊る
|
|
2416
|
+
if (musicData.isPlaying) {
|
|
2417
|
+
const beatPulse = musicData.beatDetected ? 0.2 : 0;
|
|
2418
|
+
const volumeFloat = musicData.volume * 0.1;
|
|
2419
|
+
|
|
2420
|
+
// ゆっくり落下 + 音楽で浮上
|
|
2421
|
+
positions[i + 1] -= goldLeaves.userData.fallSpeed - volumeFloat;
|
|
2422
|
+
|
|
2423
|
+
// 左右に揺れる + ビートで激しく
|
|
2424
|
+
const swayIntensity = goldLeaves.userData.swaySpeed * (1 + beatPulse * 3);
|
|
2425
|
+
positions[i] += Math.sin(time * swayIntensity + i) * (0.01 + beatPulse);
|
|
2426
|
+
positions[i + 2] += Math.cos(time * swayIntensity + i) * (0.005 + beatPulse * 0.5);
|
|
2427
|
+
|
|
2428
|
+
// 金色の輝きを音楽で調整
|
|
2429
|
+
const brightness = 0.8 + musicData.mid * 0.4 + beatPulse * 0.6;
|
|
2430
|
+
colors[i] = brightness; // R
|
|
2431
|
+
colors[i + 1] = brightness * 0.8; // G
|
|
2432
|
+
colors[i + 2] = brightness * 0.2; // B
|
|
2433
|
+
} else {
|
|
2434
|
+
// 通常の落下
|
|
2435
|
+
positions[i + 1] -= goldLeaves.userData.fallSpeed;
|
|
2436
|
+
positions[i] += Math.sin(time * goldLeaves.userData.swaySpeed + i) * 0.01;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// 下まで落ちたら上に戻す
|
|
2440
|
+
if (positions[i + 1] < -15) {
|
|
2441
|
+
positions[i + 1] = 25;
|
|
2442
|
+
positions[i] = (Math.random() - 0.5) * 40;
|
|
2443
|
+
positions[i + 2] = (Math.random() - 0.5) * 20;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
goldLeaves.children[0].geometry.attributes.position.needsUpdate = true;
|
|
2448
|
+
goldLeaves.children[0].geometry.attributes.color.needsUpdate = true;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// 🌟 墨の精霊アニメーション(音楽連動)
|
|
2452
|
+
inkSpirits.children.forEach(child => {
|
|
2453
|
+
if (child instanceof THREE.Mesh && child.userData.originalPos) {
|
|
2454
|
+
const userData = child.userData;
|
|
2455
|
+
|
|
2456
|
+
// ゆらゆらと飛ぶ動き + 音楽で活発化
|
|
2457
|
+
const midBoost = musicData.isPlaying ? 1 + musicData.mid * 0.5 : 1;
|
|
2458
|
+
const beatBoost = musicData.beatDetected ? 1.5 : 1;
|
|
2459
|
+
|
|
2460
|
+
const wanderX = Math.sin(time * userData.moveSpeed * midBoost + userData.phase) *
|
|
2461
|
+
userData.wanderRadius * beatBoost;
|
|
2462
|
+
const wanderY = Math.cos(time * userData.moveSpeed * 1.3 * midBoost + userData.phase) *
|
|
2463
|
+
userData.wanderRadius * 0.5 * beatBoost;
|
|
2464
|
+
const wanderZ = Math.sin(time * userData.moveSpeed * 0.7 * midBoost + userData.phase) *
|
|
2465
|
+
userData.wanderRadius * 0.7 * beatBoost;
|
|
2466
|
+
|
|
2467
|
+
child.position.set(
|
|
2468
|
+
userData.originalPos.x + wanderX,
|
|
2469
|
+
userData.originalPos.y + wanderY,
|
|
2470
|
+
userData.originalPos.z + wanderZ
|
|
2471
|
+
);
|
|
2472
|
+
|
|
2473
|
+
// 墨の濃淡変化 + 音楽で明滅
|
|
2474
|
+
const volumePulse = musicData.isPlaying ? musicData.volume * 0.3 : 0;
|
|
2475
|
+
const fade = Math.sin(time * userData.glowSpeed) * 0.2 + 0.5 + volumePulse;
|
|
2476
|
+
child.material.opacity = Math.max(0.1, Math.min(1.0, fade));
|
|
2477
|
+
if (userData.light) {
|
|
2478
|
+
userData.light.intensity = fade * 0.2;
|
|
2479
|
+
userData.light.position.copy(child.position);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
});
|
|
2483
|
+
|
|
2484
|
+
// 筆ストロークアニメーションは削除
|
|
2485
|
+
|
|
2486
|
+
controls.update();
|
|
2487
|
+
renderer.render(scene, camera);
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
// レスポンシブ対応
|
|
2491
|
+
window.addEventListener('resize', () => {
|
|
2492
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
2493
|
+
camera.updateProjectionMatrix();
|
|
2494
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
2495
|
+
});
|
|
2496
|
+
|
|
2497
|
+
// 30秒後の自動非表示タイマー開始
|
|
2498
|
+
if (typeof window.scheduleInfoPanelAutoHide === 'function') {
|
|
2499
|
+
window.scheduleInfoPanelAutoHide();
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
console.log('✅ 侘寂の世界が準備完了!🎨 音楽入り動画で墨絵に魂を宿らせましょう');
|
|
2503
|
+
|
|
2504
|
+
// WebXR最終チェック
|
|
2505
|
+
console.log('=== WebXR Final Check ===');
|
|
2506
|
+
console.log('VRButton available:', typeof VRButton);
|
|
2507
|
+
console.log('ARButton available:', typeof ARButton);
|
|
2508
|
+
console.log('navigator.xr available:', !!navigator.xr);
|
|
2509
|
+
console.log('Window location:', window.location.href);
|
|
2510
|
+
console.log('Is HTTPS:', window.location.protocol === 'https:');
|
|
2511
|
+
|
|
2512
|
+
})();
|
|
2513
|
+
</script>
|
|
2514
|
+
</body>
|
|
2515
|
+
</html>
|