Update p1/index.html

#1
by soiz1 - opened
Files changed (1) hide show
  1. p1/index.html +640 -1006
p1/index.html CHANGED
@@ -255,6 +255,23 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
255
  min-height: 100vh;
256
  }
257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  h1 {
259
  color: #a2c2e8;
260
  text-align: center;
@@ -491,9 +508,6 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
491
  outline: none;
492
  opacity: 0;
493
  transition: opacity 0.3s, width 0.3s;
494
- background-image: linear-gradient(#6aebfc, #6aebfc);
495
- background-size: 100% 100%;
496
- background-repeat: no-repeat;
497
  }
498
 
499
  .volume-control:hover .volume-slider {
@@ -523,9 +537,6 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
523
  background: #1e2a47;
524
  border-radius: 3px;
525
  outline: none;
526
- background-image: linear-gradient(#64d1ff, #64d1ff);
527
- background-size: 100% 100%;
528
- background-repeat: no-repeat;
529
  }
530
 
531
  .speed-slider::-webkit-slider-thumb {
@@ -585,9 +596,6 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
585
  background: #1e2a47;
586
  border-radius: 5px;
587
  outline: none;
588
- background-image: linear-gradient(#64ffda, #64ffda);
589
- background-size: 100% 100%;
590
- background-repeat: no-repeat;
591
  }
592
 
593
  .global-volume-slider::-webkit-slider-thumb,
@@ -721,15 +729,6 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
721
  }
722
  }
723
 
724
- @keyframes spin {
725
- from {
726
- transform: rotate(0deg);
727
- }
728
- to {
729
- transform: rotate(360deg);
730
- }
731
- }
732
-
733
  .time-set-button {
734
  background-color: #112240;
735
  border: 1px solid #64ffda;
@@ -771,149 +770,6 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
771
  box-shadow: none;
772
  }
773
 
774
- .combine-status {
775
- margin-top: 10px;
776
- color: #97c2f0;
777
- font-size: 14px;
778
- height: 20px;
779
- }
780
-
781
- .preview-section {
782
- margin-top: 20px;
783
- padding: 15px;
784
- background-color: rgba(17, 34, 64, 0.7);
785
- border-radius: 5px;
786
- display: none;
787
- }
788
-
789
- .preview-section h3 {
790
- margin-top: 0;
791
- color: #97c2f0;
792
- border-bottom: 1px solid #64ffda;
793
- padding-bottom: 5px;
794
- }
795
-
796
- .disabled-overlay {
797
- position: absolute;
798
- top: 0;
799
- left: 0;
800
- width: 100%;
801
- height: 100%;
802
- background-color: rgba(10, 25, 47, 0.7);
803
- display: flex;
804
- justify-content: center;
805
- align-items: center;
806
- z-index: 10;
807
- border-radius: 5px;
808
- }
809
-
810
- .disabled-message {
811
- background-color: rgba(30, 42, 71, 0.9);
812
- padding: 20px;
813
- border-radius: 5px;
814
- text-align: center;
815
- max-width: 80%;
816
- }
817
-
818
- .disabled-message p {
819
- margin-bottom: 15px;
820
- }
821
-
822
- .loader {
823
- width: 80px;
824
- aspect-ratio: 1;
825
- border: 10px solid #000;
826
- box-sizing: border-box;
827
- background:
828
- radial-gradient(farthest-side, #fff 98%, #0000) 50%/20px 20px,
829
- radial-gradient(farthest-side, #fff 98%, #0000) 50%/20px 20px,
830
- radial-gradient(farthest-side, #fff 98%, #0000) 50%/20px 20px,
831
- radial-gradient(farthest-side, #fff 98%, #0000) 50%/20px 20px,
832
- radial-gradient(farthest-side, #fff 98%, #0000) 50%/80% 80%,
833
- #000;
834
- background-repeat: no-repeat;
835
- filter: blur(4px) contrast(10);
836
- animation: squarePulse 1s infinite alternate;
837
- }
838
-
839
- @keyframes squarePulse {
840
- 0% {
841
- background-position: 50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%;
842
- }
843
- 25% {
844
- background-position: 50% 0, 50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%;
845
- }
846
- 50% {
847
- background-position: 50% 0, 50% 100%, 50% 50%, 50% 50%, 50% 50%, 50% 50%;
848
- }
849
- 75% {
850
- background-position: 50% 0, 50% 100%, 0 50%, 50% 50%, 50% 50%, 50% 50%;
851
- }
852
- 100% {
853
- background-position: 50% 0, 50% 100%, 0 50%, 100% 50%, 50% 50%, 50% 50%;
854
- }
855
- }
856
-
857
- #buffering-indicator {
858
- position: absolute;
859
- top: 50%;
860
- left: 50%;
861
- transform: translate(-50%, -50%);
862
- z-index: 10;
863
- display: none;
864
- }
865
-
866
- .sync-status {
867
- position: absolute;
868
- bottom: 100px;
869
- left: 10px;
870
- width: 150px;
871
- height: 30px;
872
- background-color: rgba(0, 0, 0, 0.7);
873
- color: #97c2f0;
874
- padding: 5px 10px;
875
- border-radius: 3px;
876
- font-size: 12px;
877
- z-index: 5;
878
- display: flex;
879
- align-items: center;
880
- gap: 5px;
881
- white-space: nowrap;
882
- overflow: hidden;
883
- text-overflow: ellipsis;
884
- user-select: none;
885
- contain: strict;
886
- }
887
-
888
- .sync-status button {
889
- background: none;
890
- border: none;
891
- color: #fff;
892
- cursor: pointer;
893
- font-size: 12px;
894
- }
895
-
896
- .lock-controls-btn {
897
- position: fixed;
898
- bottom: 20px;
899
- right: 20px;
900
- background-color: rgba(0, 0, 0, 0.7);
901
- border: none;
902
- color: #fff;
903
- width: 36px;
904
- height: 36px;
905
- border-radius: 50%;
906
- display: flex;
907
- align-items: center;
908
- justify-content: center;
909
- cursor: pointer;
910
- z-index: 100;
911
- }
912
-
913
- .lock-controls-btn.locked {
914
- color: #97c2f0;
915
- }
916
-
917
  .time-markers-container {
918
  display: flex;
919
  flex-wrap: wrap;
@@ -1043,289 +899,6 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
1043
  <canvas id="effectCanvas"></canvas>
1044
  <div class="grid"></div>
1045
  </div>
1046
- <script>
1047
- document.addEventListener('DOMContentLoaded', function () {
1048
- const starCanvas = document.getElementById('starCanvas');
1049
- const effectCanvas = document.getElementById('effectCanvas');
1050
- const starCtx = starCanvas.getContext('2d');
1051
- const effectCtx = effectCanvas.getContext('2d');
1052
-
1053
- const dpr = window.devicePixelRatio || 1;
1054
-
1055
- function resizeCanvas() {
1056
- starCanvas.width = window.innerWidth * dpr;
1057
- starCanvas.height = window.innerHeight * dpr;
1058
- effectCanvas.width = window.innerWidth * dpr;
1059
- effectCanvas.height = window.innerHeight * dpr;
1060
- starCtx.scale(dpr, dpr);
1061
- effectCtx.scale(dpr, dpr);
1062
- }
1063
-
1064
- window.addEventListener('resize', resizeCanvas);
1065
- resizeCanvas();
1066
-
1067
- const config = {
1068
- density: 0.95,
1069
- speed: 2.5,
1070
- glowSize: 5,
1071
- starOpacity: 1.0,
1072
- glowOpacity: 0.08,
1073
- milkyWayIntensity: 2.0,
1074
- bandThickness: 0.15,
1075
- clusterStrength: 3.0
1076
- };
1077
-
1078
- let stars = [];
1079
- let glows = [];
1080
- let lightSpots = [];
1081
-
1082
- const glowColors = [
1083
- { r: 142, g: 61, b: 235 },
1084
- { r: 180, g: 100, b: 230 },
1085
- { r: 70, g: 61, b: 230 },
1086
- { r: 160, g: 80, b: 210 },
1087
- { r: 70, g: 61, b: 235 }
1088
- ];
1089
-
1090
- const BAND_ANGLE = -12 * Math.PI / 180;
1091
- let BASE_BAND_THICKNESS = Math.min(window.innerWidth, window.innerHeight) * config.bandThickness;
1092
- const BAND_LENGTH = Math.max(window.innerWidth, window.innerHeight) * 1.1;
1093
- const BAND_CURVE = 0.4;
1094
-
1095
- let clusters = [];
1096
- const NUM_CLUSTERS = Math.floor(BAND_LENGTH / 120);
1097
-
1098
- function initClusters() {
1099
- clusters = [];
1100
- for (let i = 0; i < NUM_CLUSTERS; i++) {
1101
- clusters.push({
1102
- t: Math.random(),
1103
- radius: (Math.random() * 0.15 + 0.05) * BAND_LENGTH,
1104
- strength: (Math.random() * 2.5 + 0.5) * config.clusterStrength
1105
- });
1106
- }
1107
- }
1108
-
1109
- function bandCenter(t) {
1110
- const cx = window.innerWidth / 2;
1111
- const cy = window.innerHeight / 2;
1112
- const lx = (t - 0.5) * BAND_LENGTH;
1113
- const curveOffset = Math.sin((t - 0.5) * Math.PI * 2 * BAND_CURVE) * (BASE_BAND_THICKNESS * 0.4);
1114
- const cosVariation = Math.cos((t - 0.5) * Math.PI * 10) * (BASE_BAND_THICKNESS * 0.05) +
1115
- Math.cos((t - 0.5) * Math.PI * 4) * (BASE_BAND_THICKNESS * 0.082);
1116
- const localThickness = BASE_BAND_THICKNESS + cosVariation;
1117
- const cosA = Math.cos(BAND_ANGLE);
1118
- const sinA = Math.sin(BAND_ANGLE);
1119
- const x = cx + lx * cosA - curveOffset * sinA;
1120
- const y = cy + lx * sinA + curveOffset * cosA;
1121
- return { x, y, localThickness };
1122
- }
1123
-
1124
- function gaussian(mean = 0, std = 1) {
1125
- let u = 0, v = 0;
1126
- while (u === 0) u = Math.random();
1127
- while (v === 0) v = Math.random();
1128
- const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
1129
- return z * std + mean;
1130
- }
1131
-
1132
- function initStars() {
1133
- stars = [];
1134
- glows = [];
1135
- lightSpots = [];
1136
-
1137
- BASE_BAND_THICKNESS = Math.min(window.innerWidth, window.innerHeight) * config.bandThickness;
1138
-
1139
- const WIDTH = window.innerWidth;
1140
- const HEIGHT = window.innerHeight;
1141
-
1142
- const NUM_BACKGROUND = Math.floor((WIDTH * HEIGHT) / 1000 * config.density);
1143
- const NUM_BAND_TRIALS = Math.floor((WIDTH * HEIGHT) / 340 * config.density);
1144
-
1145
- for (let i = 0; i < NUM_BACKGROUND; i++) {
1146
- const x = Math.random() * WIDTH;
1147
- const y = Math.random() * HEIGHT;
1148
- const size = Math.random() * 1 + 0.5;
1149
- const opacity = Math.random() * 0.5 + 0.3;
1150
- const twinkleSpeed = Math.random() * 5 + 3;
1151
- const twinkleOffset = Math.random() * Math.PI * 2;
1152
- const glowColor = glowColors[Math.floor(Math.random() * glowColors.length)];
1153
-
1154
- stars.push({
1155
- x, y, size, opacity, twinkleSpeed, twinkleOffset, glowColor,
1156
- glowSize: Math.random() * (config.glowSize - 20) + 20,
1157
- isBackground: true
1158
- });
1159
- }
1160
-
1161
- for (let i = 0; i < NUM_BAND_TRIALS; i++) {
1162
- const t = Math.random() ** 0.7;
1163
- const t_signed = Math.random() < 0.5 ? t : 1 - t;
1164
- const bc = bandCenter(t_signed);
1165
- const localThickness = bc.localThickness;
1166
-
1167
- const perp = gaussian(0, localThickness * 0.45);
1168
- const ux = -Math.sin(BAND_ANGLE);
1169
- const uy = Math.cos(BAND_ANGLE);
1170
- const x = bc.x + perp * ux;
1171
- const y = bc.y + perp * uy;
1172
-
1173
- const dist = Math.hypot(x - bc.x, y - bc.y);
1174
- const p_base = Math.exp(-(dist * dist) / (2 * (localThickness * 0.4) ** 2));
1175
-
1176
- let clusterBoost = 0;
1177
- for (const cl of clusters) {
1178
- const cc = bandCenter(cl.t);
1179
- const dcl = Math.hypot(x - cc.x, y - cc.y);
1180
- clusterBoost += cl.strength * Math.exp(-(dcl * dcl) / (2 * (cl.radius * 0.6) ** 2));
1181
- }
1182
-
1183
- const prob = Math.min(1, (0.5 * p_base) + (0.18 * clusterBoost * config.milkyWayIntensity));
1184
- if (Math.random() < prob) {
1185
- const size = (Math.random() < 0.02 ? (2.0 + Math.random() * 2.5) : (0.5 + Math.random() * 1.2)) * 0.5;
1186
- const opacity = 0.6 + Math.random() * 0.5;
1187
- const twinkleSpeed = Math.random() * 5 + 3;
1188
- const twinkleOffset = Math.random() * Math.PI * 2;
1189
- const glowColor = glowColors[Math.floor(Math.random() * glowColors.length)];
1190
-
1191
- stars.push({
1192
- x, y, size, opacity, twinkleSpeed, twinkleOffset, glowColor,
1193
- opacity: config.starOpacity,
1194
- glowSize: Math.random() * (config.glowSize - 20) + 20,
1195
- isBackground: false
1196
- });
1197
-
1198
- if (Math.random() > 0.8) {
1199
- glows.push({
1200
- x, y,
1201
- size: size * 5,
1202
- color: glowColor,
1203
- phase: Math.random() * Math.PI * 2
1204
- });
1205
- }
1206
- }
1207
- }
1208
-
1209
- const lightSpotCount = 8;
1210
- for (let i = 0; i < lightSpotCount; i++) {
1211
- lightSpots.push({
1212
- x: Math.random() * WIDTH,
1213
- y: Math.random() * HEIGHT,
1214
- size: Math.random() * 200 + 100,
1215
- progress: Math.random() * 100,
1216
- speed: Math.random() * 0.5 + 0.3
1217
- });
1218
- }
1219
- }
1220
-
1221
- function drawStars(time) {
1222
- starCtx.clearRect(0, 0, starCanvas.width, starCanvas.height);
1223
-
1224
- for (const star of stars) {
1225
- const twinkle = 0.7 + 0.3 * Math.sin(time * config.speed / star.twinkleSpeed + star.twinkleOffset);
1226
- const currentOpacity = star.opacity * twinkle * 0.5;
1227
-
1228
- const gradient = starCtx.createRadialGradient(
1229
- star.x, star.y, 0,
1230
- star.x, star.y, star.glowSize
1231
- );
1232
- gradient.addColorStop(0, `rgba(255, 255, 255, ${currentOpacity})`);
1233
- gradient.addColorStop(0.2, `rgba(${star.glowColor.r}, ${star.glowColor.g}, ${star.glowColor.b}, ${currentOpacity * config.glowOpacity})`);
1234
- gradient.addColorStop(1, `rgba(${star.glowColor.r}, ${star.glowColor.g}, ${star.glowColor.b}, 0)`);
1235
-
1236
- starCtx.beginPath();
1237
- starCtx.arc(star.x, star.y, star.glowSize, 0, Math.PI * 2);
1238
- starCtx.fillStyle = gradient;
1239
- starCtx.fill();
1240
-
1241
- starCtx.beginPath();
1242
- starCtx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
1243
- starCtx.fillStyle = `rgba(255, 255, 255, ${currentOpacity})`;
1244
- starCtx.fill();
1245
- }
1246
-
1247
- for (const glow of glows) {
1248
- const scale = 1 + 0.5 * Math.sin(time * config.speed * 0.5 + glow.phase);
1249
- const opacity = 0.3 * Math.sin(time * config.speed * 0.5 + glow.phase);
1250
-
1251
- if (opacity > 0) {
1252
- const gradient = starCtx.createRadialGradient(
1253
- glow.x, glow.y, 0,
1254
- glow.x, glow.y, glow.size * scale * 0.5
1255
- );
1256
- gradient.addColorStop(0, `rgba(${glow.color.r}, ${glow.color.g}, ${glow.color.b}, ${opacity * 0.5})`);
1257
- gradient.addColorStop(1, `rgba(${glow.color.r}, ${glow.color.g}, ${glow.color.b}, 0)`);
1258
-
1259
- starCtx.beginPath();
1260
- starCtx.arc(glow.x, glow.y, glow.size * scale, 0, Math.PI * 2);
1261
- starCtx.fillStyle = gradient;
1262
- starCtx.fill();
1263
- }
1264
- }
1265
- }
1266
-
1267
- function drawEffects(time) {
1268
- effectCtx.clearRect(0, 0, effectCanvas.width, effectCanvas.height);
1269
-
1270
- const WIDTH = window.innerWidth;
1271
- const HEIGHT = window.innerHeight;
1272
-
1273
- for (const spot of lightSpots) {
1274
- spot.progress += spot.speed * config.speed;
1275
- if (spot.progress > 100) spot.progress = 0;
1276
-
1277
- const progress = spot.progress / 100;
1278
- const opacity = Math.sin(progress * Math.PI) * 0.3;
1279
- const size = spot.size * (0.5 + progress);
1280
-
1281
- const gradient = effectCtx.createRadialGradient(
1282
- spot.x, spot.y, 0,
1283
- spot.x, spot.y, size
1284
- );
1285
- gradient.addColorStop(0, `rgba(100, 200, 255, ${opacity})`);
1286
- gradient.addColorStop(1, 'rgba(100, 200, 255, 0)');
1287
-
1288
- effectCtx.beginPath();
1289
- effectCtx.arc(spot.x, spot.y, size, 0, Math.PI * 2);
1290
- effectCtx.fillStyle = gradient;
1291
- effectCtx.fill();
1292
- }
1293
- }
1294
- let lastTime = 0;
1295
- const fps = 5;
1296
- const interval = 1000 / fps;
1297
-
1298
- function animate(time) {
1299
- if (time - lastTime >= interval) {
1300
- const seconds = time / 1000;
1301
- drawStars(seconds);
1302
- drawEffects(seconds);
1303
- lastTime = time;
1304
- }
1305
- requestAnimationFrame(animate);
1306
- }
1307
-
1308
- initClusters();
1309
- initStars();
1310
- requestAnimationFrame(animate);
1311
-
1312
- const refreshBackgroundBtn = document.getElementById('refresh-background-btn');
1313
- if (refreshBackgroundBtn) {
1314
- refreshBackgroundBtn.addEventListener('click', function () {
1315
- initClusters();
1316
- initStars();
1317
- console.log('背景を更新しました');
1318
- const originalText = this.textContent;
1319
- this.textContent = '更新中...';
1320
- this.disabled = true;
1321
- setTimeout(() => {
1322
- this.textContent = originalText;
1323
- this.disabled = false;
1324
- }, 1000);
1325
- });
1326
- }
1327
- });
1328
- </script>
1329
 
1330
  <div class="loading-overlay" id="loadingOverlay">
1331
  <div class="spinner-box">
@@ -1380,7 +953,7 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
1380
  ブラウザの仕様により、オフラインで開く際に<b>最初のみ二回再読み込みボタンを押さなければいけません。</b><br>
1381
  不安定な機能で、ページの更新などができなくなるバグが発生する可能性があります。</a>
1382
  <br>
1383
- <button class="combine-button" id="sw-register-btn" disabled>登録を開始</button>
1384
  <div class="combine-status" id="sw-status"></div>
1385
  </div>
1386
  </details><br>
@@ -1398,25 +971,25 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
1398
  <div class="progress-marker" id="end-marker" style="left: 100%; display: none;"></div>
1399
  </div>
1400
  <div class="main-controls">
1401
- <button class="control-button" id="play-pause-btn" disabled>
1402
  <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M320-200v-560l440 280-440 280Z"/></svg>
1403
  </button>
1404
- <button class="control-button" id="reset-btn" disabled title="再生開始秒数から再生">
1405
  <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"/></svg>
1406
  </button>
1407
  <div class="time-display" id="time-display">00:00.00 / 00:00.00</div>
1408
  <div class="volume-control">
1409
- <button class="volume-button" id="volume-btn" disabled>
1410
  <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Z"/></svg>
1411
  </button>
1412
- <input type="range" class="volume-slider" id="volume-slider" min="0" max="1" step="0.01" value="1" disabled>
1413
  </div>
1414
  <div class="speed-control">
1415
  <span class="speed-value" id="speed-value">1.00x</span>
1416
- <input type="range" class="speed-slider" id="speed-slider" min="0.01" max="5" step="0.01" value="1" disabled>
1417
  </div>
1418
- <button class="control-button" id="pip-btn" disabled title="ピクチャーインピクチャー">⇲</button>
1419
- <button class="control-button fullscreen-button" id="fullscreen-btn" disabled>⛶</button>
1420
  </div>
1421
  </div>
1422
 
@@ -1427,39 +1000,35 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
1427
  <div class="volume-box-inline">
1428
  <span>🔊 BGM音量調整</span>
1429
  <div class="volume-item">
1430
- <input type="range" id="bgm-volume-slider" min="0" max="1" step="0.01" value="1">
1431
- <span id="bgm-volume-value">1.00</span>
1432
  </div>
1433
  </div>
1434
  <span>このボックスをドラッグして「再生開始秒数」や「再生終了秒数」の入力ボックスの上で離すと秒数を簡単に変更できます。</span>
1435
- <div class="time-marker" data-time="89.23">1:29.23</div>
1436
- <div class="time-marker" data-time="137.22">2:17.22</div>
1437
  </div>
1438
  <div class="setting-item">
1439
  <label for="start-time">再生開始秒数:</label>
1440
  <div class="time-input-container">
1441
- <input type="number" id="start-time" min="0" value="0" step="0.01" disabled>
1442
- <button class="time-set-button" id="set-start-time" disabled>現在の秒数に設定</button>
1443
  </div>
1444
  </div>
1445
  <div class="setting-item">
1446
  <label for="end-time">再生終了秒数:</label>
1447
  <div class="time-input-container">
1448
- <input type="number" id="end-time" min="0" value="0" step="0.01" disabled>
1449
- <button class="time-set-button" id="set-end-time" disabled>現在の秒数に設定</button>
1450
- <button class="time-set-button" id="reset-end-time" disabled>動画の長さに戻す</button>
1451
  </div>
1452
  </div>
1453
  <div class="setting-item">
1454
  <label for="loop">ループ再生:</label>
1455
- <input type="checkbox" id="loop" disabled>
1456
  </div>
1457
  <div class="setting-item">
1458
- <label for="loop-interval">繰り返し隔(秒):</label>
1459
- <input type="number" id="loop-interval" min="0" value="0" step="0.1" disabled>
1460
- </div>
1461
- <div class="setting-item">
1462
- <button class="combine-button" id="apply-time-btn" disabled>時間設定を適用</button>
1463
  </div>
1464
 
1465
  <h2>設定</h2>
@@ -1470,14 +1039,14 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
1470
  <div class="setting-item">
1471
  <div class="global-volume-container">
1472
  <label>全体音量係数:</label>
1473
- <input type="range" class="global-volume-slider" id="global-volume" min="0" max="10" step="0.01" value="5" disabled>
1474
- <span class="slider-value" id="global-volume-value">0.5</span>
1475
  </div>
1476
  </div>
1477
  <div class="setting-item">
1478
  <div class="playback-speed-container">
1479
  <label>再生速度:</label>
1480
- <input type="range" class="playback-speed-slider" id="playback-speed" min="0.01" max="5" step="0.001" value="1" disabled>
1481
  <span class="slider-value" id="playback-speed-value">1.00x</span>
1482
  </div>
1483
  </div>
@@ -1494,14 +1063,28 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
1494
  import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm';
1495
  import { VRMAnimationLoaderPlugin, createVRMAnimationClip } from '@pixiv/three-vrm-animation';
1496
 
1497
- // --- グローバル変数 ---
1498
- window.audioContext = new (window.AudioContext || window.webkitAudioContext)();
1499
-
1500
  const AUDIO_SRC = './m.mp3';
1501
- const VIDEO_SOURCES = ['v.mp4', '/t/v.mp4'];
1502
  const VRM_MODELS = ['default.vrm'];
1503
- const VRMA_ANIMATIONS = ['idle.vrma', 'bakuretsu.vrma'];
1504
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1505
  let panes = [];
1506
  let activePane = null;
1507
  let globalTimeline = {
@@ -1511,85 +1094,238 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
1511
  playbackRate: 1.0,
1512
  startTime: 0,
1513
  endTime: 0,
1514
- loopEnabled: false,
1515
- loopInterval: 0
1516
  };
1517
  let bgmAudioElement = null;
1518
- let bgmVolume = 1.0;
1519
- let splitInstance = null;
 
 
1520
 
1521
- // Three.js関連
1522
- class ThreeContext {
1523
- constructor(canvas, vrmPath, vrmaPath, paneId) {
1524
- this.canvas = canvas;
1525
- this.vrmPath = vrmPath;
1526
- this.vrmaPath = vrmaPath;
1527
- this.paneId = paneId;
1528
- this.clock = new THREE.Clock();
1529
- this.mixer = null;
1530
- this.vrm = null;
1531
- this.vrma = null;
1532
- this.animationAction = null;
1533
- this.duration = 0;
1534
- this.playbackRate = 1.0;
1535
- this.isPlaying = false;
1536
- this.currentTime = 0;
1537
-
1538
- this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
1539
- this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);
1540
- this.renderer.setPixelRatio(window.devicePixelRatio);
1541
-
1542
- this.scene = new THREE.Scene();
1543
- this.scene.background = new THREE.Color(0x112240);
1544
-
1545
- this.camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
1546
- this.camera.position.set(0, 1, 3);
1547
-
1548
- this.controls = new OrbitControls(this.camera, this.renderer.domElement);
1549
- this.controls.enableDamping = true;
1550
- this.controls.dampingFactor = 0.05;
1551
- this.controls.screenSpacePanning = true;
1552
- this.controls.maxPolarAngle = Math.PI;
1553
- this.controls.target.set(0, 1, 0);
1554
-
1555
- // キーボード制御用
1556
- this.keyState = {
1557
- w: false, a: false, s: false, d: false, q: false, e: false
1558
- };
1559
- this.moveSpeed = 5;
1560
-
1561
- // ライト
1562
- const dirLight = new THREE.DirectionalLight(0xffffff, 1);
1563
- dirLight.position.set(1, 2, 1);
1564
- this.scene.add(dirLight);
1565
- const backLight = new THREE.DirectionalLight(0x88aaff, 0.5);
1566
- backLight.position.set(-1, 1, -1);
1567
- this.scene.add(backLight);
1568
- this.scene.add(new THREE.AmbientLight(0x404060));
1569
-
1570
- // グリッド
1571
- const gridHelper = new THREE.GridHelper(10, 20, 0x64ffda, 0x334466);
1572
- this.scene.add(gridHelper);
1573
-
1574
- // 地面プレーン
1575
- const planeMat = new THREE.MeshStandardMaterial({ color: 0x1a2a4a, roughness: 0.8, metalness: 0.1 });
1576
- const plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 8), planeMat);
1577
- plane.rotation.x = -Math.PI / 2;
1578
- plane.position.y = -0.01;
1579
- this.scene.add(plane);
1580
-
1581
- this.loadModel();
1582
- this.setupKeyboardControls();
1583
- this.animate();
1584
- }
1585
-
1586
- setupKeyboardControls() {
1587
- const handleKeyDown = (e) => {
1588
- if (activePane !== this.paneId) return;
1589
- const key = e.key.toLowerCase();
1590
- if (this.keyState.hasOwnProperty(key)) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1591
  this.keyState[key] = true;
1592
- e.preventDefault();
 
 
1593
  }
1594
  };
1595
 
@@ -1598,7 +1334,9 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
1598
  const key = e.key.toLowerCase();
1599
  if (this.keyState.hasOwnProperty(key)) {
1600
  this.keyState[key] = false;
1601
- e.preventDefault();
 
 
1602
  }
1603
  };
1604
 
@@ -1629,27 +1367,26 @@ ${unsupportedFeatures.map(feature => `• ${feature}`).join('\n')}
1629
  if (this.keyState.q) move.y += 1;
1630
  if (this.keyState.e) move.y -= 1;
1631
 
1632
- move.normalize();
1633
- this.camera.position.x += move.x * speed;
1634
- this.camera.position.y += move.y * speed;
1635
- this.camera.position.z += move.z * speed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1636
  }
1637
 
1638
- updateCameraAspect() {
1639
- const width = this.canvas.clientWidth;
1640
- const height = this.canvas.clientHeight;
1641
-
1642
- this.camera.aspect = width / height;
1643
- this.camera.updateProjectionMatrix();
1644
- this.renderer.setSize(width, height);
1645
-
1646
- // ★ ここに入れる
1647
- const center = new THREE.Vector3(0, 1, 0);
1648
- const offset = this.camera.position.clone().sub(this.controls.target);
1649
-
1650
- this.controls.target.copy(center);
1651
- this.camera.position.copy(center.clone().add(offset));
1652
- }
1653
  async loadModel() {
1654
  const loader = new GLTFLoader();
1655
  loader.register(parser => new VRMLoaderPlugin(parser));
@@ -1658,62 +1395,47 @@ updateCameraAspect() {
1658
  const gltf = await loader.loadAsync(this.vrmPath);
1659
  this.vrm = gltf.userData.vrm;
1660
  VRMUtils.rotateVRM0(this.vrm);
1661
- console.log('humanoid:', this.vrm.humanoid);
1662
  this.scene.add(this.vrm.scene);
1663
-
1664
- // モデルの位置を調整
1665
  this.vrm.scene.position.y = 0;
1666
 
1667
  if (this.vrmaPath) {
1668
  const vrmaLoader = new GLTFLoader();
1669
  vrmaLoader.register(parser => new VRMAnimationLoaderPlugin(parser));
1670
  const vrmaGltf = await vrmaLoader.loadAsync(this.vrmaPath);
1671
- console.log(vrmaGltf);
1672
  this.vrma = vrmaGltf.userData.vrmAnimations?.[0];
1673
- console.log('vrma:', this.vrma);
1674
- console.log('vrma userData:', vrmaGltf.userData);
1675
 
1676
  if (this.vrma) {
1677
  this.mixer = new THREE.AnimationMixer(this.vrm.scene);
1678
  const clip = createVRMAnimationClip(this.vrma, this.vrm);
1679
- console.log('clip:', clip);
1680
  if (clip) {
1681
  this.animationAction = this.mixer.clipAction(clip);
1682
  this.animationAction.setLoop(THREE.LoopRepeat);
1683
  this.animationAction.clampWhenFinished = false;
1684
  this.animationAction.play();
1685
-
1686
- this.mixer.timeScale = 1;
1687
- this.isPlaying = true; // ←これ重要
1688
  this.duration = clip.duration;
 
1689
  }
1690
  }
1691
  }
1692
  } catch(e) {
1693
- console.error('VRM load error:', e);
 
 
1694
  }
1695
  }
1696
 
1697
  animate() {
1698
  requestAnimationFrame(() => this.animate());
1699
-
1700
- const width = this.canvas.clientWidth;
1701
- const height = this.canvas.clientHeight;
1702
-
1703
- if (this.renderer.domElement.width !== width ||
1704
- this.renderer.domElement.height !== height) {
1705
-
1706
- this.renderer.setSize(width, height, false);
1707
- this.camera.aspect = width / height;
1708
- this.camera.updateProjectionMatrix();
1709
- }
1710
-
1711
  const delta = Math.min(this.clock.getDelta(), 0.033);
1712
 
1713
  if (activePane === this.paneId) {
1714
  this.updateKeyboardMovement(delta);
1715
-
1716
  }
 
1717
  if (this.vrm) {
1718
  this.vrm.update(delta);
1719
  }
@@ -1764,7 +1486,7 @@ updateCameraAspect() {
1764
  }
1765
  }
1766
 
1767
- // ペインクラス
1768
  class Pane {
1769
  constructor(container, id) {
1770
  this.id = id;
@@ -1773,7 +1495,7 @@ updateCameraAspect() {
1773
  this.videoSrc = VIDEO_SOURCES[0];
1774
  this.vrmSrc = VRM_MODELS[0];
1775
  this.vrmaSrc = VRMA_ANIMATIONS[0];
1776
- this.volume = 0;
1777
  this.element = null;
1778
  this.threeContext = null;
1779
  this.videoElement = null;
@@ -1816,13 +1538,28 @@ updateCameraAspect() {
1816
  const updateSourceList = () => {
1817
  sourceSelect.innerHTML = '';
1818
  if (this.type === 'video') {
1819
- VIDEO_SOURCES.forEach(src => { const opt = document.createElement('option'); opt.value = src; opt.textContent = src.split('/').pop(); sourceSelect.appendChild(opt); });
 
 
 
 
 
1820
  vrmSelect.style.display = 'none';
1821
  } else {
1822
- VRMA_ANIMATIONS.forEach(src => { const opt = document.createElement('option'); opt.value = src; opt.textContent = src.split('/').pop(); sourceSelect.appendChild(opt); });
 
 
 
 
 
1823
  vrmSelect.style.display = 'inline-block';
1824
  vrmSelect.innerHTML = '';
1825
- VRM_MODELS.forEach(src => { const opt = document.createElement('option'); opt.value = src; opt.textContent = src.split('/').pop(); vrmSelect.appendChild(opt); });
 
 
 
 
 
1826
  }
1827
  sourceSelect.value = this.type === 'video' ? this.videoSrc : this.vrmaSrc;
1828
  if (this.type === 'vrm') vrmSelect.value = this.vrmSrc;
@@ -1870,92 +1607,74 @@ updateCameraAspect() {
1870
  });
1871
  }
1872
 
1873
- split(direction) {
1874
- const container = this.container.parentElement;
1875
-
1876
- const newPaneContainer = document.createElement('div');
1877
- newPaneContainer.className = 'split-pane';
1878
-
1879
- const newId = 'pane-' + Date.now();
1880
- const newPane = new Pane(newPaneContainer, newId);
1881
-
1882
- // ラップ用コンテナ
1883
- const wrapper = document.createElement('div');
1884
- wrapper.className = 'split-wrapper';
1885
- wrapper.style.display = 'flex';
1886
- wrapper.style.flexDirection =
1887
- (direction === 'left' || direction === 'right') ? 'row' : 'column';
1888
-
1889
- // 既存と新規を入れる順番
1890
- if (direction === 'left' || direction === 'top') {
1891
- wrapper.appendChild(newPaneContainer);
1892
- wrapper.appendChild(this.container);
1893
- } else {
1894
- wrapper.appendChild(this.container);
1895
- wrapper.appendChild(newPaneContainer);
1896
- }
1897
-
1898
- container.replaceChild(wrapper, this.container);
1899
-
1900
- panes.push(newPane);
1901
-
1902
- // Split適用
1903
- Split(Array.from(wrapper.children), {
1904
- direction:
1905
- (direction === 'left' || direction === 'right')
1906
- ? 'horizontal'
1907
- : 'vertical',
1908
- sizes: [50, 50],
1909
- minSize: 100,
1910
- gutterSize: 6,
1911
- });
1912
- }
1913
-
1914
- reinitializeSplit(container) {
1915
- // すべての.split-pane要素を収集
1916
- const getAllPanes = (element) => {
1917
- let panesList = [];
1918
- if (element.classList && element.classList.contains('split-pane')) {
1919
- panesList.push(element);
1920
- } else if (element.children) {
1921
- for (let child of element.children) {
1922
- panesList = panesList.concat(getAllPanes(child));
1923
  }
1924
- }
1925
- return panesList;
1926
- };
1927
-
1928
- const allPanes = getAllPanes(container);
1929
-
1930
- if (allPanes.length > 1) {
1931
- if (splitInstance && typeof splitInstance.destroy === 'function') {
1932
- splitInstance.destroy();
1933
- }
1934
-
1935
- // すべてのペインが同じ親を持つとは限らないので、グループ化が必要
1936
- // 簡易的に最初のレベルのペインだけを分割
1937
- const directChildren = Array.from(container.children).filter(
1938
- child => child.classList && child.classList.contains('split-pane')
1939
- );
1940
-
1941
- if (directChildren.length > 1) {
1942
- splitInstance = Split(directChildren, {
1943
- direction: 'horizontal',
1944
- sizes: Array(directChildren.length).fill(100 / directChildren.length),
1945
- minSize: 100,
1946
- gutterSize: 4,
1947
- cursor: 'col-resize',
1948
- onDragEnd: () => {
1949
- panes.forEach(pane => {
1950
- if (pane.type === 'vrm' && pane.threeContext) {
1951
- setTimeout(() => pane.threeContext.updateCameraAspect(), 50);
1952
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1953
  });
1954
  }
1955
- });
1956
- }
1957
- }
1958
- }
1959
  updateContent() {
1960
  const contentDiv = this.container.querySelector('.pane-content');
1961
  contentDiv.innerHTML = '';
@@ -1967,14 +1686,25 @@ reinitializeSplit(container) {
1967
  this.videoElement.loop = false;
1968
  this.videoElement.volume = this.volume;
1969
  this.videoElement.playsInline = true;
1970
- contentDiv.appendChild(this.videoElement);
 
 
 
1971
 
1972
  this.videoElement.addEventListener('loadedmetadata', () => {
1973
- if (globalTimeline.duration === 0 && bgmAudioElement) {
1974
- globalTimeline.duration = Math.max(this.videoElement.duration, bgmAudioElement.duration);
 
 
 
 
 
 
1975
  }
1976
  });
1977
 
 
 
1978
  if (this.threeContext) {
1979
  this.threeContext.cleanup();
1980
  this.threeContext = null;
@@ -2028,298 +1758,103 @@ reinitializeSplit(container) {
2028
  if (this.videoElement) this.videoElement.volume = vol;
2029
  }
2030
  }
2031
- function syncTimeline() {
2032
- if (globalTimeline.isPlaying && bgmAudioElement) {
2033
- let newTime = bgmAudioElement.currentTime;
2034
-
2035
- // 範囲制御
2036
- if (newTime >= globalTimeline.endTime && globalTimeline.endTime > 0) {
2037
- if (globalTimeline.loopEnabled) {
2038
- newTime = globalTimeline.startTime;
2039
- window.seekMedia(newTime);
2040
- } else {
2041
- window.pauseMedia();
2042
- return;
2043
- }
2044
- }
2045
-
2046
- if (newTime < globalTimeline.startTime) {
2047
- newTime = globalTimeline.startTime;
2048
- window.seekMedia(newTime);
2049
- }
2050
-
2051
- globalTimeline.currentTime = newTime;
2052
-
2053
- // 同期(ズレた時だけ)
2054
- panes.forEach(pane => {
2055
- if (pane.type === 'video' && pane.videoElement) {
2056
- if (Math.abs(pane.videoElement.currentTime - newTime) > 0.05) {
2057
- pane.videoElement.currentTime = newTime;
2058
- }
2059
- } else if (pane.type === 'vrm' && pane.threeContext) {
2060
- pane.threeContext.seek(newTime);
2061
- }
2062
- });
2063
-
2064
- updateProgressBar();
2065
- updateTimeDisplay();
2066
- }
2067
-
2068
- requestAnimationFrame(syncTimeline);
2069
- }
2070
- // BGM読み込み
2071
  function loadBGMAudio() {
2072
  bgmAudioElement = new Audio(AUDIO_SRC);
2073
  bgmAudioElement.loop = false;
2074
- bgmAudioElement.volume = bgmVolume;
2075
 
2076
- let isLoaded = false;
 
 
 
 
 
2077
 
2078
- const checkAndCloseLoading = () => {
2079
- if (!isLoaded && bgmAudioElement.readyState >= 2 && !isNaN(bgmAudioElement.duration) && isFinite(bgmAudioElement.duration)) {
2080
- isLoaded = true;
 
2081
  globalTimeline.duration = bgmAudioElement.duration;
2082
-
2083
- panes.forEach(pane => {
2084
- if (pane.type === 'video' && pane.videoElement && pane.videoElement.duration) {
2085
- globalTimeline.duration = Math.max(globalTimeline.duration, pane.videoElement.duration);
2086
- }
2087
- });
2088
-
2089
  globalTimeline.endTime = globalTimeline.duration;
2090
-
2091
- if (!isNaN(globalTimeline.duration) && isFinite(globalTimeline.duration)) {
2092
- document.getElementById('end-time').value = globalTimeline.duration;
2093
- }
2094
  updateTimeDisplay();
2095
- closeLoadingOverlay();
2096
  }
2097
- };
2098
-
2099
- bgmAudioElement.addEventListener('loadedmetadata', checkAndCloseLoading);
2100
- bgmAudioElement.addEventListener('canplaythrough', checkAndCloseLoading);
2101
-
2102
- setTimeout(() => {
2103
- if (!isLoaded) {
2104
- console.warn('音声読み込みが遅延しています。強制的にローディングを終了します。');
2105
- isLoaded = true;
2106
- globalTimeline.duration = 0;
2107
- closeLoadingOverlay();
2108
- }
2109
- }, 5000);
2110
 
2111
  bgmAudioElement.addEventListener('error', (e) => {
2112
  console.error('BGM読み込みエラー:', e);
2113
- if (!isLoaded) {
2114
- isLoaded = true;
2115
- globalTimeline.duration = 0;
2116
- document.getElementById('end-time').value = 0;
2117
- closeLoadingOverlay();
2118
- }
2119
  });
2120
 
 
2121
  bgmAudioElement.addEventListener('timeupdate', () => {
2122
  if (!globalTimeline.isPlaying) return;
2123
-
2124
- let newTime = bgmAudioElement.currentTime;
2125
-
2126
- // 範囲チェック
2127
- if (newTime >= globalTimeline.endTime && globalTimeline.endTime > 0) {
2128
- if (globalTimeline.loopEnabled) {
2129
- newTime = globalTimeline.startTime;
2130
- window.seekMedia(newTime);
2131
- } else {
2132
- window.pauseMedia();
2133
- return;
2134
- }
2135
- }
2136
-
2137
- if (newTime < globalTimeline.startTime && globalTimeline.startTime > 0) {
2138
- newTime = globalTimeline.startTime;
2139
- window.seekMedia(newTime);
2140
- }
2141
-
2142
- globalTimeline.currentTime = newTime;
2143
- updateProgressBar();
2144
-
2145
- panes.forEach(pane => {
2146
- if (pane.type === 'video' && pane.videoElement && Math.abs(pane.videoElement.currentTime - globalTimeline.currentTime) > 0.1) {
2147
- pane.videoElement.currentTime = globalTimeline.currentTime;
2148
- } else if (pane.type === 'vrm' && pane.threeContext) {
2149
- pane.threeContext.seek(globalTimeline.currentTime);
2150
- }
2151
- });
2152
  });
2153
- }
2154
-
2155
- function setBGMVolume(vol) {
2156
- bgmVolume = Math.min(1, Math.max(0, vol));
2157
- if (bgmAudioElement) bgmAudioElement.volume = bgmVolume;
2158
- document.getElementById('bgm-volume-value').textContent = bgmVolume.toFixed(2);
2159
- }
2160
-
2161
- window.playMedia = function() {
2162
- if (
2163
- globalTimeline.currentTime >= globalTimeline.endTime ||
2164
- globalTimeline.currentTime < globalTimeline.startTime
2165
- ) {
2166
- window.seekMedia(globalTimeline.startTime);
2167
- }
2168
-
2169
- globalTimeline.isPlaying = true;
2170
-
2171
- panes.forEach(p => p.play());
2172
-
2173
- if (bgmAudioElement) {
2174
- bgmAudioElement.currentTime = globalTimeline.currentTime; // 念のため
2175
- bgmAudioElement.play().catch(e => console.warn('BGM play failed:', e));
2176
- }
2177
-
2178
- document.getElementById('play-pause-btn').innerHTML = '...';
2179
- };
2180
-
2181
- window.pauseMedia = function() {
2182
- globalTimeline.isPlaying = false;
2183
- panes.forEach(p => p.pause());
2184
- if (bgmAudioElement) bgmAudioElement.pause();
2185
- document.getElementById('play-pause-btn').innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M320-200v-560l440 280-440 280Z"/></svg>';
2186
- };
2187
-
2188
- window.seekMedia = function(time) {
2189
- let newTime = Math.min(Math.max(time, 0), globalTimeline.duration);
2190
-
2191
- if (globalTimeline.startTime > 0 && newTime < globalTimeline.startTime) {
2192
- newTime = globalTimeline.startTime;
2193
- }
2194
- if (globalTimeline.endTime > 0 && newTime > globalTimeline.endTime) {
2195
- newTime = globalTimeline.endTime;
2196
- }
2197
 
2198
- if (bgmAudioElement) bgmAudioElement.currentTime = newTime;
2199
- panes.forEach(p => p.seek(newTime));
2200
- globalTimeline.currentTime = newTime;
2201
- updateProgressBar();
2202
- updateTimeDisplay();
2203
- };
2204
-
2205
- function updateProgressBar() {
2206
- const progress = (globalTimeline.currentTime / globalTimeline.duration) * 100;
2207
- document.getElementById('progress-bar').style.width = `${progress}%`;
2208
-
2209
- if (globalTimeline.startTime > 0) {
2210
- const startPercent = (globalTimeline.startTime / globalTimeline.duration) * 100;
2211
- const startMarker = document.getElementById('start-marker');
2212
- startMarker.style.left = `${startPercent}%`;
2213
- startMarker.style.display = 'block';
2214
- } else {
2215
- document.getElementById('start-marker').style.display = 'none';
2216
- }
2217
-
2218
- if (globalTimeline.endTime > 0 && globalTimeline.endTime < globalTimeline.duration) {
2219
- const endPercent = (globalTimeline.endTime / globalTimeline.duration) * 100;
2220
- const endMarker = document.getElementById('end-marker');
2221
- endMarker.style.left = `${endPercent}%`;
2222
- endMarker.style.display = 'block';
2223
- } else {
2224
- document.getElementById('end-marker').style.display = 'none';
2225
- }
2226
- }
2227
-
2228
- function updateTimeDisplay() {
2229
- const current = formatTime(globalTimeline.currentTime);
2230
- const total = formatTime(globalTimeline.duration);
2231
- document.getElementById('time-display').textContent = `${current} / ${total}`;
2232
- }
2233
-
2234
- function formatTime(seconds) {
2235
- const mins = Math.floor(seconds / 60);
2236
- const secs = (seconds % 60).toFixed(2);
2237
- return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(5, '0')}`;
2238
  }
2239
 
2240
- function applyTimeSettings() {
2241
- const startTime = parseFloat(document.getElementById('start-time').value);
2242
- const endTime = parseFloat(document.getElementById('end-time').value);
2243
- const loopEnabled = document.getElementById('loop').checked;
2244
- const loopInterval = parseFloat(document.getElementById('loop-interval').value);
2245
-
2246
- if (!isNaN(startTime) && startTime >= 0) {
2247
- globalTimeline.startTime = startTime;
2248
  }
2249
- if (!isNaN(endTime) && endTime > 0 && endTime <= globalTimeline.duration) {
2250
- globalTimeline.endTime = endTime;
2251
- } else if (endTime === 0 || isNaN(endTime)) {
2252
- globalTimeline.endTime = globalTimeline.duration;
2253
- document.getElementById('end-time').value = globalTimeline.duration;
2254
- }
2255
-
2256
- globalTimeline.loopEnabled = loopEnabled;
2257
- globalTimeline.loopInterval = loopInterval;
2258
-
2259
- // 現在の時間が範囲外なら調整
2260
- if (globalTimeline.currentTime < globalTimeline.startTime) {
2261
- window.seekMedia(globalTimeline.startTime);
2262
- } else if (globalTimeline.currentTime > globalTimeline.endTime) {
2263
- window.seekMedia(globalTimeline.startTime);
2264
- }
2265
-
2266
- updateProgressBar();
2267
  }
2268
 
2269
- function resetToStartTime() {
2270
- window.seekMedia(globalTimeline.startTime);
2271
- if (!globalTimeline.isPlaying) {
2272
- window.playMedia();
 
 
 
 
2273
  }
2274
  }
2275
 
2276
- // プログレスバークリック
2277
- function setupProgressClick() {
2278
- const progressContainer = document.getElementById('progress-container');
2279
- progressContainer.addEventListener('click', (e) => {
2280
- const rect = progressContainer.getBoundingClientRect();
2281
- const percent = (e.clientX - rect.left) / rect.width;
2282
- let newTime = percent * globalTimeline.duration;
2283
-
2284
- if (globalTimeline.startTime > 0 && newTime < globalTimeline.startTime) {
2285
- newTime = globalTimeline.startTime;
2286
- }
2287
- if (globalTimeline.endTime > 0 && newTime > globalTimeline.endTime) {
2288
- newTime = globalTimeline.endTime;
2289
- }
2290
-
2291
- window.seekMedia(newTime);
2292
- });
2293
- }
2294
-
2295
- // 初期化
2296
- function init() {
2297
- const splitContainer = document.getElementById('split-container');
2298
- const defaultPaneContainer = document.createElement('div');
2299
- defaultPaneContainer.className = 'split-pane';
2300
- splitContainer.appendChild(defaultPaneContainer);
2301
- const pane = new Pane(defaultPaneContainer, 'pane-main');
2302
- panes.push(pane);
2303
-
2304
- loadBGMAudio();
2305
- setupProgressClick();
2306
- syncTimeline();
2307
-
2308
- // コントロール有効化
2309
- setTimeout(() => {
2310
- document.querySelectorAll('#play-pause-btn, #reset-btn, #volume-btn, #volume-slider, #speed-slider, #fullscreen-btn, #start-time, #end-time, #reset-end-time, #loop, #loop-interval, #global-volume, #set-start-time, #set-end-time, #playback-speed, #apply-time-btn, #pip-btn').forEach(el => el.disabled = false);
2311
- }, 1000);
2312
-
2313
- // イベントリスナー
2314
  document.getElementById('play-pause-btn').addEventListener('click', () => {
2315
- if (globalTimeline.isPlaying) window.pauseMedia();
2316
- else window.playMedia();
2317
  });
2318
 
 
2319
  document.getElementById('reset-btn').addEventListener('click', resetToStartTime);
2320
 
2321
- document.getElementById('apply-time-btn').addEventListener('click', applyTimeSettings);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2322
 
 
2323
  document.getElementById('set-start-time').addEventListener('click', () => {
2324
  document.getElementById('start-time').value = globalTimeline.currentTime;
2325
  });
@@ -2336,10 +1871,6 @@ window.playMedia = function() {
2336
  globalTimeline.loopEnabled = e.target.checked;
2337
  });
2338
 
2339
- document.getElementById('loop-interval').addEventListener('change', (e) => {
2340
- globalTimeline.loopInterval = parseFloat(e.target.value);
2341
- });
2342
-
2343
  // 音量関連
2344
  const volumeSlider = document.getElementById('volume-slider');
2345
  volumeSlider.addEventListener('input', (e) => {
@@ -2350,73 +1881,91 @@ window.playMedia = function() {
2350
  const globalVolumeSlider = document.getElementById('global-volume');
2351
  const globalVolumeValue = document.getElementById('global-volume-value');
2352
  globalVolumeSlider.addEventListener('input', (e) => {
2353
- const val = parseFloat(e.target.value);
2354
- const normalized = val / 10;
2355
- globalVolumeValue.textContent = normalized.toFixed(2);
2356
- if (bgmAudioElement) bgmAudioElement.volume = normalized * bgmVolume;
2357
  });
2358
 
2359
- const playbackSpeedSlider = document.getElementById('playback-speed');
2360
- const playbackSpeedValue = document.getElementById('playback-speed-value');
2361
- playbackSpeedSlider.addEventListener('input', (e) => {
2362
- const rate = parseFloat(e.target.value);
2363
- playbackSpeedValue.textContent = rate.toFixed(2) + 'x';
2364
- globalTimeline.playbackRate = rate;
2365
- if (bgmAudioElement) bgmAudioElement.playbackRate = rate;
2366
- panes.forEach(pane => pane.setPlaybackRate(rate));
2367
  });
2368
 
 
2369
  const speedSlider = document.getElementById('speed-slider');
2370
  const speedValue = document.getElementById('speed-value');
2371
- speedSlider.addEventListener('input', (e) => {
2372
- const rate = parseFloat(e.target.value);
2373
- speedValue.textContent = rate.toFixed(2) + 'x';
 
2374
  globalTimeline.playbackRate = rate;
2375
  if (bgmAudioElement) bgmAudioElement.playbackRate = rate;
2376
  panes.forEach(pane => pane.setPlaybackRate(rate));
2377
- playbackSpeedSlider.value = rate;
2378
  playbackSpeedValue.textContent = rate.toFixed(2) + 'x';
2379
- });
 
 
2380
 
2381
- // BGM音量
2382
- const bgmSlider = document.getElementById('bgm-volume-slider');
2383
- bgmSlider.addEventListener('input', (e) => {
2384
- setBGMVolume(parseFloat(e.target.value));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2385
  });
2386
- setBGMVolume(1);
2387
 
2388
  // 全画面
2389
- document.getElementById('fullscreen-btn').addEventListener('click', () => {
 
2390
  const viewingBox = document.getElementById('viewing-box');
2391
- if (document.fullscreenElement) {
2392
- document.exitFullscreen();
2393
  } else {
2394
- viewingBox.requestFullscreen();
2395
  }
2396
  });
2397
 
2398
- // ピクチャーインピクチャー
2399
- document.getElementById('pip-btn').addEventListener('click', async () => {
2400
- const video = panes.find(p => p.type === 'video')?.videoElement;
2401
- if (video && document.pictureInPictureEnabled) {
 
2402
  try {
2403
  if (document.pictureInPictureElement) {
2404
  await document.exitPictureInPicture();
2405
  } else {
2406
- await video.requestPictureInPicture();
2407
  }
2408
  } catch (err) {
2409
  console.warn('PiP failed:', err);
 
2410
  }
 
 
2411
  }
2412
  });
2413
 
2414
- // タイムマーカードラッグ
2415
- const markers = document.querySelectorAll('.time-marker');
2416
  const startTimeInput = document.getElementById('start-time');
2417
  const endTimeInput = document.getElementById('end-time');
2418
 
2419
- markers.forEach(marker => {
2420
  marker.setAttribute('draggable', 'true');
2421
  marker.addEventListener('dragstart', (e) => {
2422
  e.dataTransfer.setData('text/plain', marker.getAttribute('data-time'));
@@ -2425,7 +1974,9 @@ window.playMedia = function() {
2425
  marker.addEventListener('dragend', () => {
2426
  marker.classList.remove('dragging');
2427
  });
2428
- });
 
 
2429
 
2430
  startTimeInput.addEventListener('dragover', (e) => e.preventDefault());
2431
  startTimeInput.addEventListener('drop', (e) => {
@@ -2442,46 +1993,129 @@ window.playMedia = function() {
2442
  });
2443
  }
2444
 
2445
- function closeLoadingOverlay() {
2446
- const loadingOverlay = document.getElementById('loadingOverlay');
2447
- if (loadingOverlay) {
2448
- loadingOverlay.style.transition = 'opacity 1s ease-out';
2449
- loadingOverlay.style.opacity = '0';
2450
- setTimeout(() => {
2451
- loadingOverlay.style.display = 'none';
2452
- }, 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
2453
  }
2454
  }
2455
 
2456
- window.addEventListener('load', () => {
2457
- document.getElementById('title-name').textContent = new URLSearchParams(window.location.search).get('mode') === 't' ? "地球星歌" : "時の旅人";
2458
- init();
2459
- });
2460
-
2461
- document.addEventListener('DOMContentLoaded', () => {
2462
- const loadingOverlay = document.getElementById('loadingOverlay');
2463
- if (loadingOverlay) {
2464
- loadingOverlay.style.display = 'flex';
2465
- loadingOverlay.style.opacity = '1';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2466
  }
2467
  });
2468
- </script>
2469
- <script>
2470
  window.addEventListener('load', async () => {
2471
  const registerBtn = document.getElementById('sw-register-btn');
 
2472
  if (registerBtn) {
2473
- registerBtn.disabled = false;
2474
  registerBtn.addEventListener('click', async () => {
 
 
 
 
2475
  registerBtn.disabled = true;
 
2476
  try {
2477
- await navigator.serviceWorker.register('/sw.js');
 
 
2478
  registerBtn.textContent = '登録完了';
2479
  } catch(e) {
2480
- registerBtn.textContent = '失敗';
 
 
 
2481
  }
2482
  });
2483
  }
2484
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
2485
  </script>
2486
  </body>
 
2487
  </html>
 
255
  min-height: 100vh;
256
  }
257
 
258
+ .error-message {
259
+ position: fixed;
260
+ top: 20px;
261
+ left: 50%;
262
+ transform: translateX(-50%);
263
+ background: rgba(255, 50, 50, 0.95);
264
+ color: white;
265
+ padding: 15px 25px;
266
+ border-radius: 8px;
267
+ z-index: 10000;
268
+ font-size: 14px;
269
+ text-align: center;
270
+ max-width: 80%;
271
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
272
+ border-left: 4px solid #ff0000;
273
+ }
274
+
275
  h1 {
276
  color: #a2c2e8;
277
  text-align: center;
 
508
  outline: none;
509
  opacity: 0;
510
  transition: opacity 0.3s, width 0.3s;
 
 
 
511
  }
512
 
513
  .volume-control:hover .volume-slider {
 
537
  background: #1e2a47;
538
  border-radius: 3px;
539
  outline: none;
 
 
 
540
  }
541
 
542
  .speed-slider::-webkit-slider-thumb {
 
596
  background: #1e2a47;
597
  border-radius: 5px;
598
  outline: none;
 
 
 
599
  }
600
 
601
  .global-volume-slider::-webkit-slider-thumb,
 
729
  }
730
  }
731
 
 
 
 
 
 
 
 
 
 
732
  .time-set-button {
733
  background-color: #112240;
734
  border: 1px solid #64ffda;
 
770
  box-shadow: none;
771
  }
772
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773
  .time-markers-container {
774
  display: flex;
775
  flex-wrap: wrap;
 
899
  <canvas id="effectCanvas"></canvas>
900
  <div class="grid"></div>
901
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
902
 
903
  <div class="loading-overlay" id="loadingOverlay">
904
  <div class="spinner-box">
 
953
  ブラウザの仕様により、オフラインで開く際に<b>最初のみ二回再読み込みボタンを押さなければいけません。</b><br>
954
  不安定な機能で、ページの更新などができなくなるバグが発生する可能性があります。</a>
955
  <br>
956
+ <button class="combine-button" id="sw-register-btn">登録を開始</button>
957
  <div class="combine-status" id="sw-status"></div>
958
  </div>
959
  </details><br>
 
971
  <div class="progress-marker" id="end-marker" style="left: 100%; display: none;"></div>
972
  </div>
973
  <div class="main-controls">
974
+ <button class="control-button" id="play-pause-btn">
975
  <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M320-200v-560l440 280-440 280Z"/></svg>
976
  </button>
977
+ <button class="control-button" id="reset-btn" title="再生開始秒数から再生">
978
  <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"/></svg>
979
  </button>
980
  <div class="time-display" id="time-display">00:00.00 / 00:00.00</div>
981
  <div class="volume-control">
982
+ <button class="volume-button" id="volume-btn">
983
  <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Z"/></svg>
984
  </button>
985
+ <input type="range" class="volume-slider" id="volume-slider" min="0" max="1" step="0.01" value="0.5">
986
  </div>
987
  <div class="speed-control">
988
  <span class="speed-value" id="speed-value">1.00x</span>
989
+ <input type="range" class="speed-slider" id="speed-slider" min="0.01" max="5" step="0.01" value="1">
990
  </div>
991
+ <button class="control-button" id="pip-btn" title="ピクチャーインピクチャー">⇲</button>
992
+ <button class="control-button fullscreen-button" id="fullscreen-btn">⛶</button>
993
  </div>
994
  </div>
995
 
 
1000
  <div class="volume-box-inline">
1001
  <span>🔊 BGM音量調整</span>
1002
  <div class="volume-item">
1003
+ <input type="range" id="bgm-volume-slider" min="0" max="1" step="0.01" value="0.8">
1004
+ <span id="bgm-volume-value">0.80</span>
1005
  </div>
1006
  </div>
1007
  <span>このボックスをドラッグして「再生開始秒数」や「再生終了秒数」の入力ボックスの上で離すと秒数を簡単に変更できます。</span>
1008
+ <div class="time-marker" data-time="89.23" draggable="true">1:29.23</div>
1009
+ <div class="time-marker" data-time="137.22" draggable="true">2:17.22</div>
1010
  </div>
1011
  <div class="setting-item">
1012
  <label for="start-time">再生開始秒数:</label>
1013
  <div class="time-input-container">
1014
+ <input type="number" id="start-time" min="0" value="0" step="0.01">
1015
+ <button class="time-set-button" id="set-start-time">現在の秒数に設定</button>
1016
  </div>
1017
  </div>
1018
  <div class="setting-item">
1019
  <label for="end-time">再生終了秒数:</label>
1020
  <div class="time-input-container">
1021
+ <input type="number" id="end-time" min="0" value="0" step="0.01">
1022
+ <button class="time-set-button" id="set-end-time">現在の秒数に設定</button>
1023
+ <button class="time-set-button" id="reset-end-time">動画の長さに戻す</button>
1024
  </div>
1025
  </div>
1026
  <div class="setting-item">
1027
  <label for="loop">ループ再生:</label>
1028
+ <input type="checkbox" id="loop">
1029
  </div>
1030
  <div class="setting-item">
1031
+ <button class="combine-button" id="apply-time-btn">設定を適用</button>
 
 
 
 
1032
  </div>
1033
 
1034
  <h2>設定</h2>
 
1039
  <div class="setting-item">
1040
  <div class="global-volume-container">
1041
  <label>全体音量係数:</label>
1042
+ <input type="range" class="global-volume-slider" id="global-volume" min="0" max="10" step="0.01" value="10">
1043
+ <span class="slider-value" id="global-volume-value">1.0</span>
1044
  </div>
1045
  </div>
1046
  <div class="setting-item">
1047
  <div class="playback-speed-container">
1048
  <label>再生速度:</label>
1049
+ <input type="range" class="playback-speed-slider" id="playback-speed" min="0.01" max="5" step="0.001" value="1">
1050
  <span class="slider-value" id="playback-speed-value">1.00x</span>
1051
  </div>
1052
  </div>
 
1063
  import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm';
1064
  import { VRMAnimationLoaderPlugin, createVRMAnimationClip } from '@pixiv/three-vrm-animation';
1065
 
1066
+ // --- 設定 ---
 
 
1067
  const AUDIO_SRC = './m.mp3';
1068
+ const VIDEO_SOURCES = ['v.mp4'];
1069
  const VRM_MODELS = ['default.vrm'];
1070
+ const VRMA_ANIMATIONS = ['idle.vrma'];
1071
 
1072
+ // --- エラーハンドリング用 ---
1073
+ function showErrorMessage(message) {
1074
+ const existing = document.querySelector('.error-message');
1075
+ if (existing) existing.remove();
1076
+
1077
+ const errorDiv = document.createElement('div');
1078
+ errorDiv.className = 'error-message';
1079
+ errorDiv.innerHTML = `⚠️ ${message}<br><small>詳細はコンソール(F12)を確認してください</small>`;
1080
+ document.body.appendChild(errorDiv);
1081
+
1082
+ setTimeout(() => {
1083
+ if (errorDiv.parentNode) errorDiv.remove();
1084
+ }, 8000);
1085
+ }
1086
+
1087
+ // --- グローバル変数 ---
1088
  let panes = [];
1089
  let activePane = null;
1090
  let globalTimeline = {
 
1094
  playbackRate: 1.0,
1095
  startTime: 0,
1096
  endTime: 0,
1097
+ loopEnabled: false
 
1098
  };
1099
  let bgmAudioElement = null;
1100
+ let bgmVolume = 0.8;
1101
+ let globalVolume = 1.0;
1102
+ let currentSplitInstance = null;
1103
+ let syncTimer = null;
1104
 
1105
+ // メディア読み込み状態
1106
+ let mediaLoadStatus = {
1107
+ bgm: false,
1108
+ video: false,
1109
+ vrm: false
1110
+ };
1111
+
1112
+ // --- ヘルパー関数 ---
1113
+ function formatTime(seconds) {
1114
+ if (isNaN(seconds)) return '00:00.00';
1115
+ const mins = Math.floor(seconds / 60);
1116
+ const secs = (seconds % 60).toFixed(2);
1117
+ return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(5, '0')}`;
1118
+ }
1119
+
1120
+ function updateProgressBar() {
1121
+ if (globalTimeline.duration <= 0) return;
1122
+ const progress = (globalTimeline.currentTime / globalTimeline.duration) * 100;
1123
+ const progressBar = document.getElementById('progress-bar');
1124
+ if (progressBar) progressBar.style.width = `${progress}%`;
1125
+
1126
+ const startMarker = document.getElementById('start-marker');
1127
+ if (startMarker && globalTimeline.startTime > 0) {
1128
+ const startPercent = (globalTimeline.startTime / globalTimeline.duration) * 100;
1129
+ startMarker.style.left = `${startPercent}%`;
1130
+ startMarker.style.display = 'block';
1131
+ } else if (startMarker) {
1132
+ startMarker.style.display = 'none';
1133
+ }
1134
+
1135
+ const endMarker = document.getElementById('end-marker');
1136
+ if (endMarker && globalTimeline.endTime > 0 && globalTimeline.endTime < globalTimeline.duration) {
1137
+ const endPercent = (globalTimeline.endTime / globalTimeline.duration) * 100;
1138
+ endMarker.style.left = `${endPercent}%`;
1139
+ endMarker.style.display = 'block';
1140
+ } else if (endMarker) {
1141
+ endMarker.style.display = 'none';
1142
+ }
1143
+ }
1144
+
1145
+ function updateTimeDisplay() {
1146
+ const current = formatTime(globalTimeline.currentTime);
1147
+ const total = formatTime(globalTimeline.duration);
1148
+ const display = document.getElementById('time-display');
1149
+ if (display) display.textContent = `${current} / ${total}`;
1150
+ }
1151
+
1152
+ // --- メディア同期(単一化)---
1153
+ function syncAllMedia() {
1154
+ if (!globalTimeline.isPlaying) return;
1155
+
1156
+ let newTime = bgmAudioElement ? bgmAudioElement.currentTime : globalTimeline.currentTime;
1157
+
1158
+ // 範囲制御
1159
+ if (globalTimeline.endTime > 0 && newTime >= globalTimeline.endTime) {
1160
+ if (globalTimeline.loopEnabled) {
1161
+ newTime = globalTimeline.startTime;
1162
+ seekAllMedia(newTime);
1163
+ } else {
1164
+ pauseAllMedia();
1165
+ return;
1166
+ }
1167
+ }
1168
+
1169
+ if (globalTimeline.startTime > 0 && newTime < globalTimeline.startTime) {
1170
+ newTime = globalTimeline.startTime;
1171
+ seekAllMedia(newTime);
1172
+ }
1173
+
1174
+ globalTimeline.currentTime = newTime;
1175
+
1176
+ // 各ペインを同期
1177
+ panes.forEach(pane => {
1178
+ if (pane.type === 'video' && pane.videoElement) {
1179
+ if (Math.abs(pane.videoElement.currentTime - newTime) > 0.05) {
1180
+ pane.videoElement.currentTime = newTime;
1181
+ }
1182
+ } else if (pane.type === 'vrm' && pane.threeContext) {
1183
+ pane.threeContext.seek(newTime);
1184
+ }
1185
+ });
1186
+
1187
+ updateProgressBar();
1188
+ updateTimeDisplay();
1189
+ }
1190
+
1191
+ function seekAllMedia(time) {
1192
+ let newTime = Math.min(Math.max(time, 0), globalTimeline.duration);
1193
+
1194
+ if (globalTimeline.startTime > 0 && newTime < globalTimeline.startTime) {
1195
+ newTime = globalTimeline.startTime;
1196
+ }
1197
+ if (globalTimeline.endTime > 0 && newTime > globalTimeline.endTime) {
1198
+ newTime = globalTimeline.endTime;
1199
+ }
1200
+
1201
+ if (bgmAudioElement) {
1202
+ bgmAudioElement.currentTime = newTime;
1203
+ }
1204
+ panes.forEach(p => p.seek(newTime));
1205
+ globalTimeline.currentTime = newTime;
1206
+ updateProgressBar();
1207
+ updateTimeDisplay();
1208
+ }
1209
+
1210
+ function playAllMedia() {
1211
+ if (globalTimeline.currentTime >= globalTimeline.endTime && globalTimeline.endTime > 0) {
1212
+ seekAllMedia(globalTimeline.startTime);
1213
+ }
1214
+
1215
+ globalTimeline.isPlaying = true;
1216
+
1217
+ panes.forEach(p => p.play());
1218
+
1219
+ if (bgmAudioElement) {
1220
+ bgmAudioElement.currentTime = globalTimeline.currentTime;
1221
+ bgmAudioElement.play().catch(e => console.warn('BGM play failed:', e));
1222
+ }
1223
+
1224
+ const playBtn = document.getElementById('play-pause-btn');
1225
+ if (playBtn) {
1226
+ playBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-120q-33 0-56.5-23.5T400-200q0-33 23.5-56.5T480-280q33 0 56.5 23.5T560-200q0 33-23.5 56.5T480-120Zm0-280q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0-280q-33 0-56.5-23.5T400-760q0-33 23.5-56.5T480-840q33 0 56.5 23.5T560-760q0 33-23.5 56.5T480-680Z"/></svg>';
1227
+ }
1228
+ }
1229
+
1230
+ function pauseAllMedia() {
1231
+ globalTimeline.isPlaying = false;
1232
+ panes.forEach(p => p.pause());
1233
+ if (bgmAudioElement) bgmAudioElement.pause();
1234
+
1235
+ const playBtn = document.getElementById('play-pause-btn');
1236
+ if (playBtn) {
1237
+ playBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M320-200v-560l440 280-440 280Z"/></svg>';
1238
+ }
1239
+ }
1240
+
1241
+ function resetToStartTime() {
1242
+ seekAllMedia(globalTimeline.startTime);
1243
+ if (!globalTimeline.isPlaying) {
1244
+ playAllMedia();
1245
+ }
1246
+ }
1247
+
1248
+ function applyGlobalVolume() {
1249
+ const effectiveVolume = bgmVolume * globalVolume;
1250
+ if (bgmAudioElement) bgmAudioElement.volume = Math.min(1, Math.max(0, effectiveVolume));
1251
+ }
1252
+
1253
+ // --- ThreeContext(修正版)---
1254
+ class ThreeContext {
1255
+ constructor(canvas, vrmPath, vrmaPath, paneId) {
1256
+ this.canvas = canvas;
1257
+ this.vrmPath = vrmPath;
1258
+ this.vrmaPath = vrmaPath;
1259
+ this.paneId = paneId;
1260
+ this.clock = new THREE.Clock();
1261
+ this.mixer = null;
1262
+ this.vrm = null;
1263
+ this.vrma = null;
1264
+ this.animationAction = null;
1265
+ this.duration = 0;
1266
+ this.playbackRate = 1.0;
1267
+ this.isPlaying = false;
1268
+ this.currentTime = 0;
1269
+ this.loadError = false;
1270
+
1271
+ this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
1272
+ this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);
1273
+ this.renderer.setPixelRatio(window.devicePixelRatio);
1274
+
1275
+ this.scene = new THREE.Scene();
1276
+ this.scene.background = new THREE.Color(0x112240);
1277
+
1278
+ this.camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
1279
+ this.camera.position.set(0, 1.2, 2.5);
1280
+ this.camera.lookAt(0, 1, 0);
1281
+
1282
+ this.controls = new OrbitControls(this.camera, this.renderer.domElement);
1283
+ this.controls.enableDamping = true;
1284
+ this.controls.dampingFactor = 0.05;
1285
+ this.controls.screenSpacePanning = true;
1286
+ this.controls.maxPolarAngle = Math.PI / 2;
1287
+ this.controls.target.set(0, 1, 0);
1288
+
1289
+ // キーボード制御(単一化のためにグローバルイベントは使わない)
1290
+ this.keyState = {
1291
+ w: false, a: false, s: false, d: false, q: false, e: false
1292
+ };
1293
+ this.moveSpeed = 5;
1294
+
1295
+ // ライト
1296
+ const dirLight = new THREE.DirectionalLight(0xffffff, 1);
1297
+ dirLight.position.set(1, 2, 1);
1298
+ this.scene.add(dirLight);
1299
+ const backLight = new THREE.DirectionalLight(0x88aaff, 0.5);
1300
+ backLight.position.set(-1, 1, -1);
1301
+ this.scene.add(backLight);
1302
+ this.scene.add(new THREE.AmbientLight(0x404060));
1303
+
1304
+ // グリッド
1305
+ const gridHelper = new THREE.GridHelper(10, 20, 0x64ffda, 0x334466);
1306
+ this.scene.add(gridHelper);
1307
+
1308
+ // 地面プレーン
1309
+ const planeMat = new THREE.MeshStandardMaterial({ color: 0x1a2a4a, roughness: 0.8, metalness: 0.1 });
1310
+ const plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 8), planeMat);
1311
+ plane.rotation.x = -Math.PI / 2;
1312
+ plane.position.y = -0.01;
1313
+ this.scene.add(plane);
1314
+
1315
+ this.loadModel();
1316
+ this.setupKeyboardControls();
1317
+ this.animate();
1318
+ }
1319
+
1320
+ setupKeyboardControls() {
1321
+ const handleKeyDown = (e) => {
1322
+ if (activePane !== this.paneId) return;
1323
+ const key = e.key.toLowerCase();
1324
+ if (this.keyState.hasOwnProperty(key)) {
1325
  this.keyState[key] = true;
1326
+ if (key === 'w' || key === 'a' || key === 's' || key === 'd' || key === 'q' || key === 'e') {
1327
+ e.preventDefault();
1328
+ }
1329
  }
1330
  };
1331
 
 
1334
  const key = e.key.toLowerCase();
1335
  if (this.keyState.hasOwnProperty(key)) {
1336
  this.keyState[key] = false;
1337
+ if (key === 'w' || key === 'a' || key === 's' || key === 'd' || key === 'q' || key === 'e') {
1338
+ e.preventDefault();
1339
+ }
1340
  }
1341
  };
1342
 
 
1367
  if (this.keyState.q) move.y += 1;
1368
  if (this.keyState.e) move.y -= 1;
1369
 
1370
+ if (move.length() > 0) {
1371
+ move.normalize();
1372
+ this.camera.position.x += move.x * speed;
1373
+ this.camera.position.y += move.y * speed;
1374
+ this.camera.position.z += move.z * speed;
1375
+ this.controls.target.copy(this.camera.position.clone().sub(forward.multiplyScalar(2)));
1376
+ this.controls.update();
1377
+ }
1378
+ }
1379
+
1380
+ updateCameraAspect() {
1381
+ const width = this.canvas.clientWidth;
1382
+ const height = this.canvas.clientHeight;
1383
+ if (width === 0 || height === 0) return;
1384
+
1385
+ this.camera.aspect = width / height;
1386
+ this.camera.updateProjectionMatrix();
1387
+ this.renderer.setSize(width, height);
1388
  }
1389
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1390
  async loadModel() {
1391
  const loader = new GLTFLoader();
1392
  loader.register(parser => new VRMLoaderPlugin(parser));
 
1395
  const gltf = await loader.loadAsync(this.vrmPath);
1396
  this.vrm = gltf.userData.vrm;
1397
  VRMUtils.rotateVRM0(this.vrm);
 
1398
  this.scene.add(this.vrm.scene);
 
 
1399
  this.vrm.scene.position.y = 0;
1400
 
1401
  if (this.vrmaPath) {
1402
  const vrmaLoader = new GLTFLoader();
1403
  vrmaLoader.register(parser => new VRMAnimationLoaderPlugin(parser));
1404
  const vrmaGltf = await vrmaLoader.loadAsync(this.vrmaPath);
 
1405
  this.vrma = vrmaGltf.userData.vrmAnimations?.[0];
 
 
1406
 
1407
  if (this.vrma) {
1408
  this.mixer = new THREE.AnimationMixer(this.vrm.scene);
1409
  const clip = createVRMAnimationClip(this.vrma, this.vrm);
 
1410
  if (clip) {
1411
  this.animationAction = this.mixer.clipAction(clip);
1412
  this.animationAction.setLoop(THREE.LoopRepeat);
1413
  this.animationAction.clampWhenFinished = false;
1414
  this.animationAction.play();
1415
+ this.mixer.timeScale = 0; // 初期は一時停止
 
 
1416
  this.duration = clip.duration;
1417
+ console.log(`VRM animation loaded (${this.paneId}), duration: ${this.duration}`);
1418
  }
1419
  }
1420
  }
1421
  } catch(e) {
1422
+ console.error(`VRM load error (${this.paneId}):`, e);
1423
+ this.loadError = true;
1424
+ showErrorMessage(`3Dモデルの読み込みに失敗しました: ${this.vrmPath}`);
1425
  }
1426
  }
1427
 
1428
  animate() {
1429
  requestAnimationFrame(() => this.animate());
1430
+
1431
+ this.updateCameraAspect();
1432
+
 
 
 
 
 
 
 
 
 
1433
  const delta = Math.min(this.clock.getDelta(), 0.033);
1434
 
1435
  if (activePane === this.paneId) {
1436
  this.updateKeyboardMovement(delta);
 
1437
  }
1438
+
1439
  if (this.vrm) {
1440
  this.vrm.update(delta);
1441
  }
 
1486
  }
1487
  }
1488
 
1489
+ // --- Pane クラス ---
1490
  class Pane {
1491
  constructor(container, id) {
1492
  this.id = id;
 
1495
  this.videoSrc = VIDEO_SOURCES[0];
1496
  this.vrmSrc = VRM_MODELS[0];
1497
  this.vrmaSrc = VRMA_ANIMATIONS[0];
1498
+ this.volume = 0.5;
1499
  this.element = null;
1500
  this.threeContext = null;
1501
  this.videoElement = null;
 
1538
  const updateSourceList = () => {
1539
  sourceSelect.innerHTML = '';
1540
  if (this.type === 'video') {
1541
+ VIDEO_SOURCES.forEach(src => {
1542
+ const opt = document.createElement('option');
1543
+ opt.value = src;
1544
+ opt.textContent = src.split('/').pop();
1545
+ sourceSelect.appendChild(opt);
1546
+ });
1547
  vrmSelect.style.display = 'none';
1548
  } else {
1549
+ VRMA_ANIMATIONS.forEach(src => {
1550
+ const opt = document.createElement('option');
1551
+ opt.value = src;
1552
+ opt.textContent = src.split('/').pop();
1553
+ sourceSelect.appendChild(opt);
1554
+ });
1555
  vrmSelect.style.display = 'inline-block';
1556
  vrmSelect.innerHTML = '';
1557
+ VRM_MODELS.forEach(src => {
1558
+ const opt = document.createElement('option');
1559
+ opt.value = src;
1560
+ opt.textContent = src.split('/').pop();
1561
+ vrmSelect.appendChild(opt);
1562
+ });
1563
  }
1564
  sourceSelect.value = this.type === 'video' ? this.videoSrc : this.vrmaSrc;
1565
  if (this.type === 'vrm') vrmSelect.value = this.vrmSrc;
 
1607
  });
1608
  }
1609
 
1610
+ split(direction) {
1611
+ const container = this.container.parentElement;
1612
+ const newPaneContainer = document.createElement('div');
1613
+ newPaneContainer.className = 'split-pane';
1614
+ const newId = 'pane-' + Date.now() + '-' + Math.random();
1615
+ const newPane = new Pane(newPaneContainer, newId);
1616
+
1617
+ const wrapper = document.createElement('div');
1618
+ wrapper.style.display = 'flex';
1619
+ wrapper.style.flexDirection = (direction === 'left' || direction === 'right') ? 'row' : 'column';
1620
+ wrapper.style.flex = '1';
1621
+ wrapper.style.width = '100%';
1622
+ wrapper.style.height = '100%';
1623
+
1624
+ if (direction === 'left' || direction === 'top') {
1625
+ wrapper.appendChild(newPaneContainer);
1626
+ wrapper.appendChild(this.container);
1627
+ } else {
1628
+ wrapper.appendChild(this.container);
1629
+ wrapper.appendChild(newPaneContainer);
1630
+ }
1631
+
1632
+ container.replaceChild(wrapper, this.container);
1633
+ panes.push(newPane);
1634
+
1635
+ // 分割を再初期化
1636
+ this.reinitializeSplit(document.getElementById('split-container'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1637
  }
1638
+
1639
+ reinitializeSplit(rootContainer) {
1640
+ // 既存のSplitインスタンスを破棄
1641
+ if (currentSplitInstance && typeof currentSplitInstance.destroy === 'function') {
1642
+ currentSplitInstance.destroy();
1643
+ }
1644
+
1645
+ // 直接の子要素であるsplit-paneまたはsplit-wrapperを収集
1646
+ const getTopLevelPanes = (element) => {
1647
+ const panesList = [];
1648
+ for (let child of element.children) {
1649
+ if (child.classList && child.classList.contains('split-pane')) {
1650
+ panesList.push(child);
1651
+ } else if (child.children) {
1652
+ // wrapperの場合、その中のsplit-paneを取得
1653
+ const innerPanes = Array.from(child.children).filter(c =>
1654
+ c.classList && c.classList.contains('split-pane')
1655
+ );
1656
+ if (innerPanes.length > 0) {
1657
+ panesList.push(child);
1658
+ }
 
 
 
 
 
 
 
1659
  }
1660
+ }
1661
+ return panesList;
1662
+ };
1663
+
1664
+ const topPanes = getTopLevelPanes(rootContainer);
1665
+
1666
+ if (topPanes.length > 1) {
1667
+ const isHorizontal = topPanes[0].parentElement.style.flexDirection !== 'column';
1668
+ currentSplitInstance = Split(topPanes, {
1669
+ direction: isHorizontal ? 'horizontal' : 'vertical',
1670
+ sizes: Array(topPanes.length).fill(100 / topPanes.length),
1671
+ minSize: 100,
1672
+ gutterSize: 4,
1673
+ cursor: isHorizontal ? 'col-resize' : 'row-resize'
1674
  });
1675
  }
1676
+ }
1677
+
 
 
1678
  updateContent() {
1679
  const contentDiv = this.container.querySelector('.pane-content');
1680
  contentDiv.innerHTML = '';
 
1686
  this.videoElement.loop = false;
1687
  this.videoElement.volume = this.volume;
1688
  this.videoElement.playsInline = true;
1689
+
1690
+ this.videoElement.addEventListener('error', () => {
1691
+ showErrorMessage(`動画の読み込みに失敗しました: ${this.videoSrc}`);
1692
+ });
1693
 
1694
  this.videoElement.addEventListener('loadedmetadata', () => {
1695
+ if (globalTimeline.duration === 0 && this.videoElement.duration) {
1696
+ globalTimeline.duration = Math.max(globalTimeline.duration, this.videoElement.duration);
1697
+ if (bgmAudioElement && bgmAudioElement.duration) {
1698
+ globalTimeline.duration = Math.max(globalTimeline.duration, bgmAudioElement.duration);
1699
+ }
1700
+ globalTimeline.endTime = globalTimeline.duration;
1701
+ document.getElementById('end-time').value = globalTimeline.duration;
1702
+ updateTimeDisplay();
1703
  }
1704
  });
1705
 
1706
+ contentDiv.appendChild(this.videoElement);
1707
+
1708
  if (this.threeContext) {
1709
  this.threeContext.cleanup();
1710
  this.threeContext = null;
 
1758
  if (this.videoElement) this.videoElement.volume = vol;
1759
  }
1760
  }
1761
+
1762
+ // --- BGM読み込み(エラーハンドリング強化)---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1763
  function loadBGMAudio() {
1764
  bgmAudioElement = new Audio(AUDIO_SRC);
1765
  bgmAudioElement.loop = false;
1766
+ bgmAudioElement.volume = bgmVolume * globalVolume;
1767
 
1768
+ let loadTimeout = setTimeout(() => {
1769
+ if (!mediaLoadStatus.bgm) {
1770
+ showErrorMessage(`BGMファイルが見つからないか読み込めません: ${AUDIO_SRC}`);
1771
+ closeLoadingOverlay();
1772
+ }
1773
+ }, 5000);
1774
 
1775
+ bgmAudioElement.addEventListener('canplaythrough', () => {
1776
+ if (!mediaLoadStatus.bgm) {
1777
+ mediaLoadStatus.bgm = true;
1778
+ clearTimeout(loadTimeout);
1779
  globalTimeline.duration = bgmAudioElement.duration;
 
 
 
 
 
 
 
1780
  globalTimeline.endTime = globalTimeline.duration;
1781
+ document.getElementById('end-time').value = globalTimeline.duration;
 
 
 
1782
  updateTimeDisplay();
1783
+ checkAllMediaLoaded();
1784
  }
1785
+ });
 
 
 
 
 
 
 
 
 
 
 
 
1786
 
1787
  bgmAudioElement.addEventListener('error', (e) => {
1788
  console.error('BGM読み込みエラー:', e);
1789
+ showErrorMessage(`BGMの読み込みに失敗しました: ${AUDIO_SRC}`);
1790
+ clearTimeout(loadTimeout);
1791
+ closeLoadingOverlay();
 
 
 
1792
  });
1793
 
1794
+ // BGMのtimeupdateで同期
1795
  bgmAudioElement.addEventListener('timeupdate', () => {
1796
  if (!globalTimeline.isPlaying) return;
1797
+ syncAllMedia();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1798
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1799
 
1800
+ bgmAudioElement.load();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1801
  }
1802
 
1803
+ function checkAllMediaLoaded() {
1804
+ if (mediaLoadStatus.bgm) {
1805
+ closeLoadingOverlay();
 
 
 
 
 
1806
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1807
  }
1808
 
1809
+ function closeLoadingOverlay() {
1810
+ const loadingOverlay = document.getElementById('loadingOverlay');
1811
+ if (loadingOverlay && loadingOverlay.style.display !== 'none') {
1812
+ loadingOverlay.style.transition = 'opacity 1s ease-out';
1813
+ loadingOverlay.style.opacity = '0';
1814
+ setTimeout(() => {
1815
+ loadingOverlay.style.display = 'none';
1816
+ }, 1000);
1817
  }
1818
  }
1819
 
1820
+ function setupEventListeners() {
1821
+ // 再生/一時停止
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1822
  document.getElementById('play-pause-btn').addEventListener('click', () => {
1823
+ if (globalTimeline.isPlaying) pauseAllMedia();
1824
+ else playAllMedia();
1825
  });
1826
 
1827
+ // リセット
1828
  document.getElementById('reset-btn').addEventListener('click', resetToStartTime);
1829
 
1830
+ // 時間設定適用
1831
+ document.getElementById('apply-time-btn').addEventListener('click', () => {
1832
+ const startTime = parseFloat(document.getElementById('start-time').value);
1833
+ const endTime = parseFloat(document.getElementById('end-time').value);
1834
+ const loopEnabled = document.getElementById('loop').checked;
1835
+
1836
+ if (!isNaN(startTime) && startTime >= 0) {
1837
+ globalTimeline.startTime = startTime;
1838
+ }
1839
+ if (!isNaN(endTime) && endTime > 0 && endTime <= globalTimeline.duration) {
1840
+ globalTimeline.endTime = endTime;
1841
+ } else if (endTime === 0 || isNaN(endTime)) {
1842
+ globalTimeline.endTime = globalTimeline.duration;
1843
+ document.getElementById('end-time').value = globalTimeline.duration;
1844
+ }
1845
+
1846
+ globalTimeline.loopEnabled = loopEnabled;
1847
+
1848
+ if (globalTimeline.currentTime < globalTimeline.startTime) {
1849
+ seekAllMedia(globalTimeline.startTime);
1850
+ } else if (globalTimeline.currentTime > globalTimeline.endTime) {
1851
+ seekAllMedia(globalTimeline.startTime);
1852
+ }
1853
+
1854
+ updateProgressBar();
1855
+ });
1856
 
1857
+ // 現在時間を開始/終了に設定
1858
  document.getElementById('set-start-time').addEventListener('click', () => {
1859
  document.getElementById('start-time').value = globalTimeline.currentTime;
1860
  });
 
1871
  globalTimeline.loopEnabled = e.target.checked;
1872
  });
1873
 
 
 
 
 
1874
  // 音量関連
1875
  const volumeSlider = document.getElementById('volume-slider');
1876
  volumeSlider.addEventListener('input', (e) => {
 
1881
  const globalVolumeSlider = document.getElementById('global-volume');
1882
  const globalVolumeValue = document.getElementById('global-volume-value');
1883
  globalVolumeSlider.addEventListener('input', (e) => {
1884
+ globalVolume = parseFloat(e.target.value) / 10;
1885
+ globalVolumeValue.textContent = globalVolume.toFixed(2);
1886
+ applyGlobalVolume();
 
1887
  });
1888
 
1889
+ const bgmSlider = document.getElementById('bgm-volume-slider');
1890
+ const bgmValue = document.getElementById('bgm-volume-value');
1891
+ bgmSlider.addEventListener('input', (e) => {
1892
+ bgmVolume = parseFloat(e.target.value);
1893
+ bgmValue.textContent = bgmVolume.toFixed(2);
1894
+ applyGlobalVolume();
 
 
1895
  });
1896
 
1897
+ // 再生速度
1898
  const speedSlider = document.getElementById('speed-slider');
1899
  const speedValue = document.getElementById('speed-value');
1900
+ const playbackSpeedSlider = document.getElementById('playback-speed');
1901
+ const playbackSpeedValue = document.getElementById('playback-speed-value');
1902
+
1903
+ const updateSpeed = (rate) => {
1904
  globalTimeline.playbackRate = rate;
1905
  if (bgmAudioElement) bgmAudioElement.playbackRate = rate;
1906
  panes.forEach(pane => pane.setPlaybackRate(rate));
1907
+ speedValue.textContent = rate.toFixed(2) + 'x';
1908
  playbackSpeedValue.textContent = rate.toFixed(2) + 'x';
1909
+ speedSlider.value = rate;
1910
+ playbackSpeedSlider.value = rate;
1911
+ };
1912
 
1913
+ speedSlider.addEventListener('input', (e) => updateSpeed(parseFloat(e.target.value)));
1914
+ playbackSpeedSlider.addEventListener('input', (e) => updateSpeed(parseFloat(e.target.value)));
1915
+
1916
+ // プログレスバークリック
1917
+ const progressContainer = document.getElementById('progress-container');
1918
+ progressContainer.addEventListener('click', (e) => {
1919
+ const rect = progressContainer.getBoundingClientRect();
1920
+ const percent = (e.clientX - rect.left) / rect.width;
1921
+ let newTime = percent * globalTimeline.duration;
1922
+
1923
+ if (globalTimeline.startTime > 0 && newTime < globalTimeline.startTime) {
1924
+ newTime = globalTimeline.startTime;
1925
+ }
1926
+ if (globalTimeline.endTime > 0 && newTime > globalTimeline.endTime) {
1927
+ newTime = globalTimeline.endTime;
1928
+ }
1929
+
1930
+ seekAllMedia(newTime);
1931
  });
 
1932
 
1933
  // 全画面
1934
+ const fullscreenBtn = document.getElementById('fullscreen-btn');
1935
+ fullscreenBtn.addEventListener('click', () => {
1936
  const viewingBox = document.getElementById('viewing-box');
1937
+ if (!document.fullscreenElement) {
1938
+ viewingBox.requestFullscreen().catch(e => console.warn('Fullscreen failed:', e));
1939
  } else {
1940
+ document.exitFullscreen();
1941
  }
1942
  });
1943
 
1944
+ // ピクチャーインピクチャー(最初の動画ペイン)
1945
+ const pipBtn = document.getElementById('pip-btn');
1946
+ pipBtn.addEventListener('click', async () => {
1947
+ const videoPane = panes.find(p => p.type === 'video' && p.videoElement);
1948
+ if (videoPane && videoPane.videoElement && document.pictureInPictureEnabled) {
1949
  try {
1950
  if (document.pictureInPictureElement) {
1951
  await document.exitPictureInPicture();
1952
  } else {
1953
+ await videoPane.videoElement.requestPictureInPicture();
1954
  }
1955
  } catch (err) {
1956
  console.warn('PiP failed:', err);
1957
+ showErrorMessage('ピクチャーインピクチャーを開始できませんでした');
1958
  }
1959
+ } else if (!videoPane) {
1960
+ showErrorMessage('ピクチャーインピクチャーを使用するには動画ペインが必要です');
1961
  }
1962
  });
1963
 
1964
+ // タイムマーカードラッグ&ドロップ
 
1965
  const startTimeInput = document.getElementById('start-time');
1966
  const endTimeInput = document.getElementById('end-time');
1967
 
1968
+ const setupDragDrop = (marker) => {
1969
  marker.setAttribute('draggable', 'true');
1970
  marker.addEventListener('dragstart', (e) => {
1971
  e.dataTransfer.setData('text/plain', marker.getAttribute('data-time'));
 
1974
  marker.addEventListener('dragend', () => {
1975
  marker.classList.remove('dragging');
1976
  });
1977
+ };
1978
+
1979
+ document.querySelectorAll('.time-marker').forEach(setupDragDrop);
1980
 
1981
  startTimeInput.addEventListener('dragover', (e) => e.preventDefault());
1982
  startTimeInput.addEventListener('drop', (e) => {
 
1993
  });
1994
  }
1995
 
1996
+ // --- 初期化 ---
1997
+ function init() {
1998
+ const splitContainer = document.getElementById('split-container');
1999
+ const defaultPaneContainer = document.createElement('div');
2000
+ defaultPaneContainer.className = 'split-pane';
2001
+ splitContainer.appendChild(defaultPaneContainer);
2002
+ const pane = new Pane(defaultPaneContainer, 'pane-main');
2003
+ panes.push(pane);
2004
+
2005
+ loadBGMAudio();
2006
+ setupEventListeners();
2007
+
2008
+ // 全体音量の初期値
2009
+ globalVolume = 1.0;
2010
+ applyGlobalVolume();
2011
+
2012
+ // タイトル設定
2013
+ const urlParams = new URLSearchParams(window.location.search);
2014
+ const titleElem = document.getElementById('title-name');
2015
+ if (titleElem) {
2016
+ titleElem.textContent = urlParams.get('mode') === 't' ? "地球星歌" : "時の旅人";
2017
  }
2018
  }
2019
 
2020
+ // 背景アニメーション(簡易版 - 元のコードから継承)
2021
+ document.addEventListener('DOMContentLoaded', function () {
2022
+ const starCanvas = document.getElementById('starCanvas');
2023
+ const effectCanvas = document.getElementById('effectCanvas');
2024
+ if (!starCanvas || !effectCanvas) return;
2025
+
2026
+ const starCtx = starCanvas.getContext('2d');
2027
+ const effectCtx = effectCanvas.getContext('2d');
2028
+ const dpr = window.devicePixelRatio || 1;
2029
+
2030
+ function resizeCanvas() {
2031
+ starCanvas.width = window.innerWidth * dpr;
2032
+ starCanvas.height = window.innerHeight * dpr;
2033
+ effectCanvas.width = window.innerWidth * dpr;
2034
+ effectCanvas.height = window.innerHeight * dpr;
2035
+ starCtx.scale(dpr, dpr);
2036
+ effectCtx.scale(dpr, dpr);
2037
+ }
2038
+
2039
+ window.addEventListener('resize', resizeCanvas);
2040
+ resizeCanvas();
2041
+
2042
+ // シンプルな星の描画
2043
+ const stars = [];
2044
+ for (let i = 0; i < 300; i++) {
2045
+ stars.push({
2046
+ x: Math.random() * window.innerWidth,
2047
+ y: Math.random() * window.innerHeight,
2048
+ size: Math.random() * 2 + 0.5,
2049
+ opacity: Math.random() * 0.5 + 0.3
2050
+ });
2051
+ }
2052
+
2053
+ function drawStars() {
2054
+ starCtx.clearRect(0, 0, starCanvas.width, starCanvas.height);
2055
+ for (const star of stars) {
2056
+ starCtx.beginPath();
2057
+ starCtx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
2058
+ starCtx.fillStyle = `rgba(255, 255, 255, ${star.opacity})`;
2059
+ starCtx.fill();
2060
+ }
2061
+ }
2062
+
2063
+ drawStars();
2064
+
2065
+ const refreshBtn = document.getElementById('refresh-background-btn');
2066
+ if (refreshBtn) {
2067
+ refreshBtn.addEventListener('click', () => {
2068
+ for (let i = 0; i < stars.length; i++) {
2069
+ stars[i].x = Math.random() * window.innerWidth;
2070
+ stars[i].y = Math.random() * window.innerHeight;
2071
+ }
2072
+ drawStars();
2073
+ refreshBtn.textContent = '更新しました';
2074
+ setTimeout(() => { refreshBtn.textContent = 'おまけ:背景を更新'; }, 1000);
2075
+ });
2076
  }
2077
  });
2078
+
2079
+ // サービスワーカー
2080
  window.addEventListener('load', async () => {
2081
  const registerBtn = document.getElementById('sw-register-btn');
2082
+ const swStatus = document.getElementById('sw-status');
2083
  if (registerBtn) {
 
2084
  registerBtn.addEventListener('click', async () => {
2085
+ if (!('serviceWorker' in navigator)) {
2086
+ if (swStatus) swStatus.textContent = 'このブラウザはService Workerをサポートしていません';
2087
+ return;
2088
+ }
2089
  registerBtn.disabled = true;
2090
+ registerBtn.textContent = '登録中...';
2091
  try {
2092
+ const registration = await navigator.serviceWorker.register('/sw.js');
2093
+ console.log('ServiceWorker registration successful');
2094
+ if (swStatus) swStatus.textContent = '✅ 登録完了 (オフライン対応)';
2095
  registerBtn.textContent = '登録完了';
2096
  } catch(e) {
2097
+ console.error('ServiceWorker registration failed:', e);
2098
+ if (swStatus) swStatus.textContent = '❌ 登録失敗: sw.jsが見つからない可能性があります';
2099
+ registerBtn.textContent = '再試行';
2100
+ registerBtn.disabled = false;
2101
  }
2102
  });
2103
  }
2104
  });
2105
+
2106
+ // アプリケーション開始
2107
+ window.addEventListener('load', () => {
2108
+ init();
2109
+ });
2110
+
2111
+ document.addEventListener('DOMContentLoaded', () => {
2112
+ const loadingOverlay = document.getElementById('loadingOverlay');
2113
+ if (loadingOverlay) {
2114
+ loadingOverlay.style.display = 'flex';
2115
+ loadingOverlay.style.opacity = '1';
2116
+ }
2117
+ });
2118
  </script>
2119
  </body>
2120
+
2121
  </html>