soiz1 commited on
Commit
9f19fb0
·
verified ·
1 Parent(s): 4438c93

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1033 -723
index.html CHANGED
@@ -2,773 +2,1083 @@
2
  <html lang="ja">
3
 
4
  <head>
5
- <meta charset="UTF-8">
6
- <title>ラジオ体操動画プレイヤー</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c&display=swap" rel="stylesheet">
10
- <link rel="icon" href="icon.png" type="image/png">
11
- <style>
12
- body {
13
- display: flex;
14
- flex-direction: column;
15
- align-items: center;
16
- background-color: #0a0a12;
17
- color: #00ffcc;
18
- font-family: "M PLUS Rounded 1c", monospace;
19
- padding: 20px;
20
- margin: 0;
21
- overflow-x: hidden; /* 横スクロール禁止 */
22
- }
23
-
24
- h1 {
25
- color: #00aaff;
26
- text-shadow: 0 0 5px #0066ff;
27
- border-bottom: 1px solid #0066ff;
28
- padding-bottom: 10px;
29
- text-align: center;
30
- }
31
-
32
- .video-container {
33
- position: relative;
34
- max-width: 800px;
35
- margin-bottom: 20px;
36
- margin-top: 30px;
37
- border: 2px solid #0066ff;
38
- box-shadow: 0 0 15px rgba(0, 102, 255, 0.5);
39
- background: #000;
40
- }
41
-
42
- video {
43
- width: 100%;
44
- display: block;
45
- }
46
-
47
- /* 字幕スタイル */
48
- video::cue {
49
- background-color: rgba(0, 0, 0, 0.7) !important;
50
- color: #c7dbed !important;
51
- font-family: "M PLUS Rounded 1c", monospace !important;
52
- text-shadow: 1px 1px 2px #000 !important;
53
- outline: 3px solid #0b3e8f !important;
54
- border-radius: 10px !important; /* 角を丸める */
55
- }
56
-
57
- /* カスタム動画コントロール */
58
- video::-webkit-media-controls {
59
- display: none !important;
60
- }
61
-
62
- .custom-controls {
63
- position: absolute;
64
- bottom: 0;
65
- left: 0;
66
- right: 0;
67
- /*background: linear-gradient(to top, rgba(0, 20, 40, 0.9), transparent);*/
68
- padding: 10px;
69
- display: flex;
70
- flex-direction: column;
71
- opacity: 0;
72
- transition: opacity 0.3s;
73
- }
74
-
75
- .video-container:hover .custom-controls {
76
- opacity: 1;
77
- }
78
-
79
- .progress-container {
80
- width: 100%;
81
- height: 8px;
82
- background: #001133;
83
- margin-bottom: 10px;
84
- cursor: pointer;
85
- }
86
-
87
- .progress-bar {
88
- height: 100%;
89
- background: #00aaff;
90
- width: 0%;
91
- position: relative;
92
- }
93
-
94
- .progress-bar::after {
95
- content: '';
96
- position: absolute;
97
- right: -5px;
98
- top: 50%;
99
- transform: translateY(-50%);
100
- width: 10px;
101
- height: 10px;
102
- background: #00ccff;
103
- border-radius: 50%;
104
- box-shadow: 0 0 5px #00ccff;
105
- }
106
-
107
- .buttons-container {
108
- display: flex;
109
- align-items: center;
110
- justify-content: space-between;
111
- }
112
-
113
- .left-controls, .right-controls {
114
- display: flex;
115
- align-items: center;
116
- gap: 15px;
117
- }
118
-
119
- .control-btn {
120
- background: none;
121
- border: none;
122
- color: #00ccff;
123
- font-size: 16px;
124
- cursor: pointer;
125
- transition: all 0.3s;
126
- }
127
-
128
- .control-btn:hover {
129
- color: #00ffcc;
130
- text-shadow: 0 0 5px #00ffcc;
131
- }
132
-
133
- .time-display {
134
- font-size: 14px;
135
- color: #00aaff;
136
- box-shadow: 0.1px 0.1px 0.1px black;
137
- font-family: "M PLUS Rounded 1c", monospace;
138
- }
139
-
140
- .volume-container {
141
- display: flex;
142
- align-items: center;
143
- gap: 5px;
144
- }
145
-
146
- .volume-slider {
147
- width: 80px;
148
- -webkit-appearance: none;
149
- height: 4px;
150
- background: #001133;
151
- outline: none;
152
- }
153
-
154
- .volume-slider::-webkit-slider-thumb {
155
- -webkit-appearance: none;
156
- width: 12px;
157
- height: 12px;
158
- background: #00aaff;
159
- border-radius: 50%;
160
- cursor: pointer;
161
- }
162
-
163
- .controls {
164
- display: flex;
165
- flex-direction: column;
166
- gap: 15px;
167
- width: 100%;
168
- max-width: 800px;
169
- background-color: #0f0f1a;
170
- padding: 20px;
171
- border: 1px solid #0066ff;
172
- box-shadow: 0 0 15px rgba(0, 102, 255, 0.3);
173
- }
174
-
175
- .control-group {
176
- display: flex;
177
- flex-direction: row;
178
- align-items: center;
179
- justify-content: flex-start;
180
- gap: 10px;
181
- flex-wrap: nowrap;
182
- }
183
-
184
- .control-group label {
185
- white-space: nowrap;
186
- min-width: 100px;
187
- text-align: right;
188
- color: #00ccff;
189
- }
190
-
191
- input[type="range"] {
192
- flex-grow: 1;
193
- -webkit-appearance: none;
194
- height: 8px;
195
- background: #001133;
196
- border-radius: 5px;
197
- outline: none;
198
- }
199
-
200
- input[type="range"]::-webkit-slider-thumb {
201
- -webkit-appearance: none;
202
- width: 18px;
203
- height: 18px;
204
- background: #00aaff;
205
- border-radius: 50%;
206
- cursor: pointer;
207
- box-shadow: 0 0 5px #00aaff;
208
- }
209
-
210
- input[type="number"], select {
211
- background-color: #001133;
212
- color: #00ccff;
213
- border: 1px solid #0066ff;
214
- padding: 5px;
215
- font-family: "M PLUS Rounded 1c", monospace;
216
- }
217
-
218
- button {
219
- background-color: #001133;
220
- color: #00ccff;
221
- border: 1px solid #0066ff;
222
- padding: 8px 15px;
223
- cursor: pointer;
224
- font-family: "M PLUS Rounded 1c", monospace;
225
- transition: all 0.3s;
226
- align-self: flex-start;
227
- }
228
-
229
- button:hover {
230
- background-color: #0066ff;
231
- color: #000;
232
- box-shadow: 0 0 10px #0066ff;
233
- }
234
-
235
- select {
236
- width: 300px;
237
- background-color: #001133;
238
- color: #00ccff;
239
- border: 1px solid #0066ff;
240
- padding: 5px;
241
- }
242
-
243
- input[type="checkbox"] {
244
- -webkit-appearance: none;
245
- width: 18px;
246
- height: 18px;
247
- background: #001133;
248
- border: 1px solid #0066ff;
249
- position: relative;
250
- }
251
-
252
- input[type="checkbox"]:checked {
253
- background: #0066ff;
254
- box-shadow: 0 0 5px #0066ff;
255
- }
256
-
257
- input[type="checkbox"]:checked::after {
258
- content: "✓";
259
- position: absolute;
260
- color: #000;
261
- font-size: 14px;
262
- top: 50%;
263
- left: 50%;
264
- transform: translate(-50%, -50%);
265
- }
266
-
267
- /* 字幕設定用スタイル */
268
- .subtitle-settings {
269
- margin-top: 10px;
270
- padding: 10px;
271
- background-color: rgba(0, 20, 40, 0.5);
272
- border: 1px solid #0066ff;
273
- }
274
-
275
- /* 字幕サイズ調整用のCSS変数 */
276
- :root {
277
- --subtitle-scale: 1;
278
- --subtitle-border-radius: 10px;
279
- }
280
-
281
- video::cue {
282
- font-size: calc(16px * var(--subtitle-scale)) !important;
283
- line-height: 1.5 !important;
284
- border-radius: var(--subtitle-border-radius) !important;
285
- }
286
-
287
- /* 全画面時の字幕サイズ調整 */
288
- .video-container:fullscreen video::cue,
289
- .video-container:-webkit-full-screen video::cue,
290
- .video-container:-moz-full-screen video::cue,
291
- .video-container:-ms-fullscreen video::cue {
292
- font-size: calc(16px * var(--subtitle-scale) * var(--fullscreen-scale, 1)) !important;
293
- }
294
  body {
295
- margin: 0;
296
- padding: 0;
297
- background-color: #0a192f; /* 暗い青 */
298
- height: 100vh;
299
- width: 100vw;
 
 
 
 
300
  }
301
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  .ripple {
303
- position: absolute;
304
- border-radius: 50%;
305
- background: transparent;
306
- border: 1px solid rgba(100, 210, 255, 0.3); /* 半透明の薄い水色 */
307
- transform: translate(-50%, -50%);
308
- pointer-events: none;
309
- animation: ripple-animation 4s ease-out forwards;
310
- z-index: -1;
311
- position: absolute; /* または fixed / relative、必要に応じて調整 */
312
  }
313
-
314
  @keyframes ripple-animation {
315
- 0% {
316
- width: 0;
317
- height: 0;
318
- opacity: 0.6;
319
- }
320
- 100% {
321
- width: 600px;
322
- height: 600px;
323
- opacity: 0;
324
- }
325
- }
326
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  </head>
328
 
329
  <body>
330
- <div id="ripple-container">
331
- </div>
332
- <script>
333
- document.addEventListener('DOMContentLoaded', function() {
334
- const container = document.getElementById('ripple-container');
335
-
336
- function createRipple() {
337
- const ripple = document.createElement('div');
338
- ripple.classList.add('ripple');
339
-
340
- // ランダムな位置
341
- const posX = Math.random() * 100;
342
- const posY = Math.random() * 100;
343
-
344
- // ランダムなサイズとアニメーション時間
345
- const maxSize = 400 + Math.random() * 500;
346
- const duration = 3 + Math.random() * 3;
347
-
348
- // 半透明の薄い水色のバリエーション
349
- const hue = 190 + Math.random() * 20 - 10; // 水色を中心に少し変化
350
- const saturation = 80 + Math.random() * 15;
351
- const lightness = 70 + Math.random() * 20;
352
- const opacity = 0.3 + Math.random() * 0.3;
353
-
354
- ripple.style.left = `${posX}%`;
355
- ripple.style.top = `${posY}%`;
356
- ripple.style.borderColor = `hsla(${hue}, ${saturation}%, ${lightness}%, ${opacity})`;
357
- ripple.style.animationDuration = `${duration}s`;
358
-
359
- // キーフレームを動的に変更
360
- const animationName = `ripple-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
361
- ripple.style.animationName = animationName;
362
-
363
- const style = document.createElement('style');
364
- style.innerHTML = `
365
- @keyframes ${animationName} {
366
- 0% {
367
- width: 0;
368
- height: 0;
369
- opacity: ${opacity};
370
- }
371
- 100% {
372
- width: ${maxSize}px;
373
- height: ${maxSize}px;
374
- opacity: 0;
375
- }
376
- }
377
- `;
378
- document.head.appendChild(style);
379
-
380
- container.appendChild(ripple);
381
-
382
- // アニメーション終了後に要素を削除
383
- ripple.addEventListener('animationend', function() {
384
- ripple.remove();
385
- style.remove();
386
- });
387
- }
388
-
389
- // 最初の波紋をいくつか作成
390
- for (let i = 0; i < 8; i++) {
391
- setTimeout(createRipple, i * 600);
392
- }
393
-
394
- // 定期的に新しい波紋を作成
395
- setInterval(createRipple, 1200);
396
-
397
- // クリックでも波紋を作成
398
- document.addEventListener('click', function(e) {
399
- createRippleAtPosition(e.clientX, e.clientY);
400
- });
401
-
402
- function createRippleAtPosition(x, y) {
403
- const ripple = document.createElement('div');
404
- ripple.classList.add('ripple');
405
-
406
- const maxSize = 400 + Math.random() * 500;
407
- const duration = 3 + Math.random() * 3;
408
- const hue = 190 + Math.random() * 20 - 10;
409
- const saturation = 80 + Math.random() * 15;
410
- const lightness = 70 + Math.random() * 20;
411
- const opacity = 0.3 + Math.random() * 0.3;
412
-
413
- ripple.style.left = `${x}px`;
414
- ripple.style.top = `${y}px`;
415
- ripple.style.borderColor = `hsla(${hue}, ${saturation}%, ${lightness}%, ${opacity})`;
416
- ripple.style.animationDuration = `${duration}s`;
417
-
418
- const animationName = `ripple-click-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
419
- ripple.style.animationName = animationName;
420
-
421
- const style = document.createElement('style');
422
- style.innerHTML = `
423
- @keyframes ${animationName} {
424
- 0% {
425
- width: 0;
426
- height: 0;
427
- opacity: ${opacity};
428
- }
429
- 100% {
430
- width: ${maxSize}px;
431
- height: ${maxSize}px;
432
- opacity: 0;
433
- }
434
- }
435
- `;
436
- document.head.appendChild(style);
437
-
438
- container.appendChild(ripple);
439
-
440
- ripple.addEventListener('animationend', function() {
441
- ripple.remove();
442
- style.remove();
443
- });
444
- }
445
- });
446
- </script>
447
- <h1>ラジオ体操動画プレイヤー
448
- <br>For Kushihara</h1>
449
- <div class="controls">
450
- <div class="control-group">
451
- <label for="videoSelect">動画の音量:</label>
452
- <select id="videoSelect">
453
- <option value="v.mp4">小</option>
454
- <option value="v-2.mp4">大(+50dB)</option>
455
- </select>
456
- </div>
457
- <div class="control-group">
458
- <label for="speedRange">再生速度:</label>
459
- <input type="range" id="speedRange" min="0.0001" max="20" step="0.0001" value="1" style="width:700px !important;">
460
- <input type="number" id="speedInput" min="0.0001" step="0.0001" value="1">
461
  </div>
462
- <div class="control-group">
463
- <label for="volumeRange">音量:</label>
464
- <input type="range" id="volumeRange" min="0" max="1" step="0.01" value="1">
465
- <input type="number" id="volumeInput" min="0" max="1" step="0.01" value="1">
466
  </div>
467
- <div class="control-group">
468
- <label for="loopCheckbox">ループ再生:</label>
469
- <input type="checkbox" id="loopCheckbox" checked>
 
 
470
  </div>
471
- <!-- 字幕設定セクション -->
472
- <div class="subtitle-settings">
473
- <div class="control-group">
474
- <label for="subtitleToggle">字幕表示:</label>
475
- <input type="checkbox" id="subtitleToggle" checked>
476
- </div>
477
- <div class="control-group">
478
- <label for="subtitleSize">文字サイズ:</label>
479
- <input type="range" id="subtitleSize" min="0.5" max="5" step="0.1" value="1.5">
480
- <input type="number" id="subtitleSizeInput" min="0.01" step="0.01" value="1.5">
481
- </div>
482
- <div class="control-group">
483
- <label for="subtitleTrack">字幕トラック:</label>
484
- <select id="subtitleTrack">
485
- <option value="v.vtt">日本語</option>
486
- <option value="">字幕なし</option>
487
- </select>
488
- </div>
489
  </div>
490
- <button onclick="goFullscreen()">全画面</button>
491
- </div>
492
- <div class="video-container">
493
- <video id="videoPlayer" src="v.mp4">
494
- <track id="subtitleTrackElement" kind="subtitles" src="v.vtt" srclang="ja" label="日本語" default>
495
- </track>
496
- </video>
497
- <div class="custom-controls">
498
- <div class="progress-container" id="progressContainer">
499
- <div class="progress-bar" id="progressBar">
 
500
  </div>
501
- </div>
502
- <div class="buttons-container">
503
- <div class="left-controls">
504
- <button class="control-btn" id="playPauseBtn">▶</button>
505
- <span class="time-display" id="timeDisplay">00:00 / 00:00</span>
506
  </div>
507
- <div class="right-controls">
508
- <div class="volume-container">
509
- <button class="control-btn" id="volumeBtn">🔊</button>
510
- <input type="range" class="volume-slider" id="volumeSlider" min="0" max="1" step="0.01" value="1">
511
- </div>
512
- <button class="control-btn" id="subtitleBtn" title="字幕">🔤</button>
513
- <button class="control-btn" id="fullscreenBtn">⛶</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  </div>
515
- </div>
516
  </div>
517
- </div>
518
- <script>
519
- const video = document.getElementById('videoPlayer');
520
- const videoSelect = document.getElementById('videoSelect');
521
- const speedRange = document.getElementById('speedRange');
522
- const speedInput = document.getElementById('speedInput');
523
- const volumeRange = document.getElementById('volumeRange');
524
- const volumeInput = document.getElementById('volumeInput');
525
- const loopCheckbox = document.getElementById('loopCheckbox');
526
- const playPauseBtn = document.getElementById('playPauseBtn');
527
- const progressBar = document.getElementById('progressBar');
528
- const progressContainer = document.getElementById('progressContainer');
529
- const timeDisplay = document.getElementById('timeDisplay');
530
- const volumeBtn = document.getElementById('volumeBtn');
531
- const volumeSlider = document.getElementById('volumeSlider');
532
- const fullscreenBtn = document.getElementById('fullscreenBtn');
533
- const subtitleBtn = document.getElementById('subtitleBtn');
534
- const subtitleToggle = document.getElementById('subtitleToggle');
535
- const subtitleSize = document.getElementById('subtitleSize');
536
- const subtitleSizeInput = document.getElementById('subtitleSizeInput');
537
- const subtitleTrack = document.getElementById('subtitleTrack');
538
- const subtitleTrackElement = document.getElementById('subtitleTrackElement');
539
- const videoContainer = document.querySelector('.video-container');
540
-
541
- // 初期設定
542
- video.controls = false;
543
- let isDragging = false;
544
- let subtitlesEnabled = true;
545
- let normalVideoWidth = videoContainer.clientWidth;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
 
547
- function updatePlaybackRate(value) {
548
- const speed = parseFloat(value);
549
- speedInput.value = speed;
550
- speedRange.value = speed;
551
- video.playbackRate = speed;
552
- }
 
 
 
 
 
553
 
554
- function updateVolume(value) {
555
- const volume = parseFloat(value);
556
- volumeInput.value = volume;
557
- volumeRange.value = volume;
558
- volumeSlider.value = volume;
559
- video.volume = volume;
 
 
 
560
 
561
- if (volume === 0) {
562
- volumeBtn.textContent = '🔇';
563
- } else if (volume < 0.5) {
564
- volumeBtn.textContent = '🔈';
565
- } else {
566
- volumeBtn.textContent = '🔊';
 
 
 
 
567
  }
568
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
- function handleVideoChange() {
571
- const selected = videoSelect.value;
 
572
 
573
- if (selected === 'v-2.mp4') {
574
- const confirmPlay = confirm("この動画は音量が大きいです。あらかじめ、デバイスの音量をある程度下げてください。また、音割れが起きます。再生してもよろしいですか?");
575
- if (!confirmPlay) {
576
- videoSelect.value = video.src.split('/').pop();
577
- return;
578
- }
579
- }
580
 
581
- video.src = selected;
582
- video.load();
583
- video.play().then(() => {
584
- playPauseBtn.textContent = '⏸';
585
- }).catch(e => console.log(e));
586
- }
587
 
588
- function togglePlayPause() {
589
- if (video.paused) {
590
- video.play();
591
- playPauseBtn.textContent = '⏸';
592
- } else {
593
- video.pause();
594
- playPauseBtn.textContent = '▶';
595
- }
596
- }
597
-
598
- function updateProgress() {
599
- const percent = (video.currentTime / video.duration) * 100;
600
- progressBar.style.width = `${percent}%`;
601
-
602
- const currentMinutes = Math.floor(video.currentTime / 60);
603
- const currentSeconds = Math.floor(video.currentTime % 60).toString().padStart(2, '0');
604
- const durationMinutes = Math.floor(video.duration / 60);
605
- const durationSeconds = Math.floor(video.duration % 60).toString().padStart(2, '0');
606
-
607
- timeDisplay.textContent = `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`;
608
- }
609
 
610
- function setProgress(e) {
611
- const width = progressContainer.clientWidth;
612
- const clickX = e.offsetX;
613
- const duration = video.duration;
614
- video.currentTime = (clickX / width) * duration;
615
- }
616
-
617
- function toggleMute() {
618
- video.muted = !video.muted;
619
- if (video.muted) {
620
- volumeBtn.textContent = '🔇';
621
- volumeSlider.value = 0;
622
- } else {
623
- updateVolume(video.volume);
624
- }
625
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
 
627
- function handleVolumeChange() {
628
- video.muted = false;
629
- updateVolume(volumeSlider.value);
630
- }
 
 
 
 
 
 
 
 
631
 
632
- function goFullscreen() {
633
- if (
634
- document.fullscreenElement ||
635
- document.webkitFullscreenElement ||
636
- document.msFullscreenElement
637
- ) {
638
- // フルスクリーンを解除
639
- if (document.exitFullscreen) {
640
- document.exitFullscreen();
641
- } else if (document.webkitExitFullscreen) {
642
- document.webkitExitFullscreen();
643
- } else if (document.msExitFullscreen) {
644
- document.msExitFullscreen();
645
- }
646
- } else {
647
- // フルスクリーンにする
648
- if (videoContainer.requestFullscreen) {
649
- videoContainer.requestFullscreen();
650
- } else if (videoContainer.webkitRequestFullscreen) {
651
- videoContainer.webkitRequestFullscreen();
652
- } else if (videoContainer.msRequestFullscreen) {
653
- videoContainer.msRequestFullscreen();
654
- }
655
  }
656
  }
657
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
 
659
- // 全画面変更時の字幕サイズ調整
660
- function updateSubtitleScaleForFullscreen() {
661
- if (document.fullscreenElement || document.webkitFullscreenElement ||
662
- document.mozFullScreenElement || document.msFullscreenElement) {
663
- // 全画面モード
664
- const fullscreenWidth = window.innerWidth;
665
- const scaleFactor = fullscreenWidth / normalVideoWidth;
666
- document.documentElement.style.setProperty('--fullscreen-scale', scaleFactor);
667
- } else {
668
- // 通常モード
669
- document.documentElement.style.setProperty('--fullscreen-scale', 1);
670
- }
671
- }
672
 
673
- // 字幕関連の関数
674
- function toggleSubtitles() {
675
- subtitlesEnabled = subtitleToggle.checked;
676
- subtitleTrackElement.track.mode = subtitlesEnabled ? 'showing' : 'hidden';
677
- subtitleBtn.style.color = subtitlesEnabled ? '#00ccff' : '#666';
678
- }
679
 
680
- function updateSubtitleSize(value) {
681
- const size = parseFloat(value);
682
- subtitleSizeInput.value = size;
683
- subtitleSize.value = size;
684
-
685
- // CSS変数で字幕サイズを制御
686
- document.documentElement.style.setProperty('--subtitle-scale', size);
687
-
688
- // VTTCueのlineプロパティには数値のみを設定('bottom'は無効)
689
- const track = subtitleTrackElement.track;
690
- if (track && track.cues) {
691
- for (let i = 0; i < track.cues.length; i++) {
692
- // 画面下部に表示するため、適切な数値を設定(例: 90)
693
- track.cues[i].line = 90;
694
- track.cues[i].snapToLines = false; // ラインスナップを無効に
695
- }
696
- }
697
- }
698
-
699
- function changeSubtitleTrack() {
700
- const selectedTrack = subtitleTrack.value;
701
- subtitleTrackElement.src = selectedTrack;
702
- subtitleTrackElement.track.mode = selectedTrack && subtitlesEnabled ? 'showing' : 'hidden';
703
-
704
- // トラック変更後に再度読み込み
705
- video.textTracks[0].mode = 'hidden';
706
- if (selectedTrack) {
707
- video.textTracks[0].mode = subtitlesEnabled ? 'showing' : 'hidden';
708
- }
 
 
 
 
 
 
 
 
709
  }
710
-
711
- function toggleSubtitleMenu() {
712
- document.getElementById('subtitleToggle').checked ^= true;
713
- toggleSubtitles();
 
 
 
714
  }
 
 
 
 
 
 
 
 
715
 
716
- // イベントリスナー
717
- videoSelect.addEventListener('change', handleVideoChange);
718
 
719
- ['input', 'change', 'mouseup'].forEach(eventName => {
720
- speedRange.addEventListener(eventName, () => updatePlaybackRate(speedRange.value));
721
- volumeRange.addEventListener(eventName, () => updateVolume(volumeRange.value));
722
- subtitleSize.addEventListener(eventName, () => updateSubtitleSize(subtitleSize.value));
723
- });
 
 
 
 
 
 
 
 
724
 
725
- speedInput.addEventListener('input', () => updatePlaybackRate(speedInput.value));
726
- volumeInput.addEventListener('input', () => updateVolume(volumeInput.value));
727
- subtitleSizeInput.addEventListener('input', () => updateSubtitleSize(subtitleSizeInput.value));
 
 
 
 
 
 
 
 
 
 
728
 
729
- loopCheckbox.addEventListener('change', () => {
730
- video.loop = loopCheckbox.checked;
731
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
 
733
- subtitleToggle.addEventListener('change', toggleSubtitles);
734
- subtitleTrack.addEventListener('change', changeSubtitleTrack);
735
- subtitleBtn.addEventListener('click', toggleSubtitleMenu);
 
 
 
 
 
 
 
 
 
 
736
 
737
- playPauseBtn.addEventListener('click', togglePlayPause);
738
- video.addEventListener('click', togglePlayPause);
739
- video.addEventListener('play', () => playPauseBtn.textContent = '⏸');
740
- video.addEventListener('pause', () => playPauseBtn.textContent = '▶');
741
- video.addEventListener('timeupdate', updateProgress);
742
- progressContainer.addEventListener('click', setProgress);
743
- progressContainer.addEventListener('mousedown', () => isDragging = true);
744
- document.addEventListener('mouseup', () => isDragging = false);
745
- progressContainer.addEventListener('mousemove', (e) => isDragging && setProgress(e));
746
- volumeBtn.addEventListener('click', toggleMute);
747
- volumeSlider.addEventListener('input', handleVolumeChange);
748
- fullscreenBtn.addEventListener('click', goFullscreen);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
 
750
- // 全画面変更イベントを監視
751
- document.addEventListener('fullscreenchange', updateSubtitleScaleForFullscreen);
752
- document.addEventListener('webkitfullscreenchange', updateSubtitleScaleForFullscreen);
753
- document.addEventListener('mozfullscreenchange', updateSubtitleScaleForFullscreen);
754
- document.addEventListener('MSFullscreenChange', updateSubtitleScaleForFullscreen);
 
 
 
 
755
 
756
- video.addEventListener('loadedmetadata', () => {
757
- updatePlaybackRate(speedRange.value);
758
- updateVolume(volumeRange.value);
759
- updateSubtitleSize(subtitleSize.value);
760
- video.loop = loopCheckbox.checked;
761
- toggleSubtitles();
762
- updateProgress();
763
- // 通常時の動画幅を記録
764
- normalVideoWidth = videoContainer.clientWidth;
765
- });
766
 
767
- // CSS変数を設定
768
- document.documentElement.style.setProperty('--subtitle-scale', '1');
769
- document.documentElement.style.setProperty('--subtitle-border-radius', '10px');
770
- document.documentElement.style.setProperty('--fullscreen-scale', '1');
771
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  </body>
773
 
774
  </html>
 
2
  <html lang="ja">
3
 
4
  <head>
5
+ <meta charset="UTF-8">
6
+ <title>ラジオ体操動画プレイヤー</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c&display=swap" rel="stylesheet">
10
+ <link rel="icon" href="icon.png" type="image/png">
11
+ <script src="https://cdn.jsdelivr.net/npm/video-frames@1/dist/videoframes.umd.min.js"></script>
12
+ <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  body {
14
+ display: flex;
15
+ flex-direction: column;
16
+ align-items: center;
17
+ background-color: #0a0a12;
18
+ color: #00ffcc;
19
+ font-family: "M PLUS Rounded 1c", monospace;
20
+ padding: 20px;
21
+ margin: 0;
22
+ overflow-x: hidden;
23
  }
24
+
25
+ h1 {
26
+ color: #00aaff;
27
+ text-shadow: 0 0 5px #0066ff;
28
+ border-bottom: 1px solid #0066ff;
29
+ padding-bottom: 10px;
30
+ text-align: center;
31
+ }
32
+
33
+ .video-container {
34
+ position: relative;
35
+ max-width: 800px;
36
+ margin-bottom: 20px;
37
+ margin-top: 30px;
38
+ border: 2px solid #0066ff;
39
+ box-shadow: 0 0 15px rgba(0, 102, 255, 0.5);
40
+ background: #000;
41
+ }
42
+
43
+ video {
44
+ width: 100%;
45
+ display: block;
46
+ }
47
+
48
+ /* 字幕スタイル */
49
+ video::cue {
50
+ background-color: rgba(0, 0, 0, 0.7) !important;
51
+ color: #c7dbed !important;
52
+ font-family: "M PLUS Rounded 1c", monospace !important;
53
+ text-shadow: 1px 1px 2px #000 !important;
54
+ outline: 3px solid #0b3e8f !important;
55
+ border-radius: 10px !important;
56
+ }
57
+
58
+ /* カスタム動画コントロール */
59
+ video::-webkit-media-controls {
60
+ display: none !important;
61
+ }
62
+
63
+ .custom-controls {
64
+ position: absolute;
65
+ bottom: 0;
66
+ left: 0;
67
+ right: 0;
68
+ padding: 10px;
69
+ display: flex;
70
+ flex-direction: column;
71
+ opacity: 0;
72
+ transition: opacity 0.3s;
73
+ }
74
+
75
+ .video-container:hover .custom-controls {
76
+ opacity: 1;
77
+ }
78
+
79
+ .progress-container {
80
+ width: 100%;
81
+ height: 8px;
82
+ background: #001133;
83
+ margin-bottom: 10px;
84
+ cursor: pointer;
85
+ position: relative;
86
+ }
87
+
88
+ .progress-bar {
89
+ height: 100%;
90
+ background: #00aaff;
91
+ width: 0%;
92
+ position: relative;
93
+ }
94
+
95
+ .progress-bar::after {
96
+ content: '';
97
+ position: absolute;
98
+ right: -5px;
99
+ top: 50%;
100
+ transform: translateY(-50%);
101
+ width: 10px;
102
+ height: 10px;
103
+ background: #00ccff;
104
+ border-radius: 50%;
105
+ box-shadow: 0 0 5px #00ccff;
106
+ }
107
+
108
+ .buttons-container {
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: space-between;
112
+ }
113
+
114
+ .left-controls, .right-controls {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: 15px;
118
+ }
119
+
120
+ .control-btn {
121
+ background: none;
122
+ border: none;
123
+ color: #00ccff;
124
+ font-size: 16px;
125
+ cursor: pointer;
126
+ transition: all 0.3s;
127
+ }
128
+
129
+ .control-btn:hover {
130
+ color: #00ffcc;
131
+ text-shadow: 0 0 5px #00ffcc;
132
+ }
133
+
134
+ .time-display {
135
+ font-size: 14px;
136
+ color: #00aaff;
137
+ box-shadow: 0.1px 0.1px 0.1px black;
138
+ font-family: "M PLUS Rounded 1c", monospace;
139
+ }
140
+
141
+ .volume-container {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 5px;
145
+ }
146
+
147
+ .volume-slider {
148
+ width: 80px;
149
+ -webkit-appearance: none;
150
+ height: 4px;
151
+ background: #001133;
152
+ outline: none;
153
+ }
154
+
155
+ .volume-slider::-webkit-slider-thumb {
156
+ -webkit-appearance: none;
157
+ width: 12px;
158
+ height: 12px;
159
+ background: #00aaff;
160
+ border-radius: 50%;
161
+ cursor: pointer;
162
+ }
163
+
164
+ .controls {
165
+ display: flex;
166
+ flex-direction: column;
167
+ gap: 15px;
168
+ width: 100%;
169
+ max-width: 800px;
170
+ background-color: #0f0f1a;
171
+ padding: 20px;
172
+ border: 1px solid #0066ff;
173
+ box-shadow: 0 0 15px rgba(0, 102, 255, 0.3);
174
+ }
175
+
176
+ .control-group {
177
+ display: flex;
178
+ flex-direction: row;
179
+ align-items: center;
180
+ justify-content: flex-start;
181
+ gap: 10px;
182
+ flex-wrap: nowrap;
183
+ }
184
+
185
+ .control-group label {
186
+ white-space: nowrap;
187
+ min-width: 100px;
188
+ text-align: right;
189
+ color: #00ccff;
190
+ }
191
+
192
+ input[type="range"] {
193
+ flex-grow: 1;
194
+ -webkit-appearance: none;
195
+ height: 8px;
196
+ background: #001133;
197
+ border-radius: 5px;
198
+ outline: none;
199
+ }
200
+
201
+ input[type="range"]::-webkit-slider-thumb {
202
+ -webkit-appearance: none;
203
+ width: 18px;
204
+ height: 18px;
205
+ background: #00aaff;
206
+ border-radius: 50%;
207
+ cursor: pointer;
208
+ box-shadow: 0 0 5px #00aaff;
209
+ }
210
+
211
+ input[type="number"], select {
212
+ background-color: #001133;
213
+ color: #00ccff;
214
+ border: 1px solid #0066ff;
215
+ padding: 5px;
216
+ font-family: "M PLUS Rounded 1c", monospace;
217
+ }
218
+
219
+ button {
220
+ background-color: #001133;
221
+ color: #00ccff;
222
+ border: 1px solid #0066ff;
223
+ padding: 8px 15px;
224
+ cursor: pointer;
225
+ font-family: "M PLUS Rounded 1c", monospace;
226
+ transition: all 0.3s;
227
+ align-self: flex-start;
228
+ }
229
+
230
+ button:hover {
231
+ background-color: #0066ff;
232
+ color: #000;
233
+ box-shadow: 0 0 10px #0066ff;
234
+ }
235
+
236
+ select {
237
+ width: 300px;
238
+ background-color: #001133;
239
+ color: #00ccff;
240
+ border: 1px solid #0066ff;
241
+ padding: 5px;
242
+ }
243
+
244
+ input[type="checkbox"] {
245
+ -webkit-appearance: none;
246
+ width: 18px;
247
+ height: 18px;
248
+ background: #001133;
249
+ border: 1px solid #0066ff;
250
+ position: relative;
251
+ }
252
+
253
+ input[type="checkbox"]:checked {
254
+ background: #0066ff;
255
+ box-shadow: 0 0 5px #0066ff;
256
+ }
257
+
258
+ input[type="checkbox"]:checked::after {
259
+ content: "✓";
260
+ position: absolute;
261
+ color: #000;
262
+ font-size: 14px;
263
+ top: 50%;
264
+ left: 50%;
265
+ transform: translate(-50%, -50%);
266
+ }
267
+
268
+ /* 字幕設定用スタイル */
269
+ .subtitle-settings {
270
+ margin-top: 10px;
271
+ padding: 10px;
272
+ background-color: rgba(0, 20, 40, 0.5);
273
+ border: 1px solid #0066ff;
274
+ }
275
+
276
+ /* 字幕サイズ調整用のCSS変数 */
277
+ :root {
278
+ --subtitle-scale: 1;
279
+ --subtitle-border-radius: 10px;
280
+ }
281
+
282
+ video::cue {
283
+ font-size: calc(16px * var(--subtitle-scale)) !important;
284
+ line-height: 1.5 !important;
285
+ border-radius: var(--subtitle-border-radius) !important;
286
+ }
287
+
288
+ /* 全画面時の字幕サイズ調整 */
289
+ .video-container:fullscreen video::cue,
290
+ .video-container:-webkit-full-screen video::cue,
291
+ .video-container:-moz-full-screen video::cue,
292
+ .video-container:-ms-fullscreen video::cue {
293
+ font-size: calc(16px * var(--subtitle-scale) * var(--fullscreen-scale, 1)) !important;
294
+ }
295
+ body {
296
+ margin: 0;
297
+ padding: 0;
298
+ background-color: #0a192f;
299
+ height: 100vh;
300
+ width: 100vw;
301
+ }
302
+
303
  .ripple {
304
+ position: absolute;
305
+ border-radius: 50%;
306
+ background: transparent;
307
+ border: 1px solid rgba(100, 210, 255, 0.3);
308
+ transform: translate(-50%, -50%);
309
+ pointer-events: none;
310
+ animation: ripple-animation 4s ease-out forwards;
311
+ z-index: -1;
312
+ position: absolute;
313
  }
314
+
315
  @keyframes ripple-animation {
316
+ 0% {
317
+ width: 0;
318
+ height: 0;
319
+ opacity: 0.6;
320
+ }
321
+ 100% {
322
+ width: 600px;
323
+ height: 600px;
324
+ opacity: 0;
325
+ }
326
+ }
327
+
328
+ /* ローディングアニメーション */
329
+ .loading-overlay {
330
+ position: fixed;
331
+ top: 0;
332
+ left: 0;
333
+ width: 100%;
334
+ height: 100%;
335
+ background-color: rgba(0, 0, 0, 0.8);
336
+ display: flex;
337
+ justify-content: center;
338
+ align-items: center;
339
+ z-index: 9999;
340
+ transition: opacity 1s ease-out;
341
+ }
342
+
343
+ .spinner-box {
344
+ width: 300px;
345
+ height: 300px;
346
+ display: flex;
347
+ justify-content: center;
348
+ align-items: center;
349
+ background-color: transparent;
350
+ }
351
+
352
+ /* 軌道スタイル */
353
+ .leo {
354
+ position: absolute;
355
+ display: flex;
356
+ justify-content: center;
357
+ align-items: center;
358
+ border-radius: 50%;
359
+ }
360
+
361
+ .blue-orbit {
362
+ width: 165px;
363
+ height: 165px;
364
+ border: 1px solid #91daffa5;
365
+ animation: spin3D 3s linear .2s infinite;
366
+ }
367
+
368
+ .green-orbit {
369
+ width: 120px;
370
+ height: 120px;
371
+ border: 1px solid #91ffbfa5;
372
+ animation: spin3D 2s linear 0s infinite;
373
+ }
374
+
375
+ .red-orbit {
376
+ width: 90px;
377
+ height: 90px;
378
+ border: 1px solid #ffca91a5;
379
+ animation: spin3D 1s linear 0s infinite;
380
+ }
381
+
382
+ .white-orbit {
383
+ width: 60px;
384
+ height: 60px;
385
+ border: 2px solid #ffffff;
386
+ animation: spin3D 10s linear 0s infinite;
387
+ }
388
+
389
+ .w1 {
390
+ transform: rotate3D(1, 1, 1, 90deg);
391
+ }
392
+
393
+ .w2 {
394
+ transform: rotate3D(1, 2, .5, 90deg);
395
+ }
396
+
397
+ .w3 {
398
+ transform: rotate3D(.5, 1, 2, 90deg);
399
+ }
400
+
401
+ /* キーフレームアニメーション */
402
+ @keyframes spin3D {
403
+ from {
404
+ transform: rotate3d(.5,.5,.5, 360deg);
405
+ }
406
+ to {
407
+ transform: rotate3d(0,0,0, 0deg);
408
+ }
409
+ }
410
+
411
+ @keyframes spin {
412
+ from {
413
+ transform: rotate(0deg);
414
+ }
415
+ to {
416
+ transform: rotate(360deg);
417
+ }
418
+ }
419
+
420
+ /* フレームプレビュー */
421
+ .frame-preview {
422
+ position: absolute;
423
+ bottom: 30px;
424
+ transform: translateX(-50%);
425
+ width: 160px;
426
+ height: 90px;
427
+ background: #000;
428
+ border: 2px solid #00aaff;
429
+ box-shadow: 0 0 10px rgba(0, 170, 255, 0.7);
430
+ display: none;
431
+ z-index: 100;
432
+ pointer-events: none;
433
+ }
434
+
435
+ .frame-preview img {
436
+ width: 100%;
437
+ height: 100%;
438
+ object-fit: contain;
439
+ }
440
+
441
+ .frame-time {
442
+ position: absolute;
443
+ bottom: -25px;
444
+ left: 50%;
445
+ transform: translateX(-50%);
446
+ background: rgba(0, 0, 0, 0.8);
447
+ color: #00ccff;
448
+ padding: 3px 8px;
449
+ border-radius: 4px;
450
+ font-size: 12px;
451
+ white-space: nowrap;
452
+ }
453
+
454
+ /* 右クリックメニュー */
455
+ .context-menu {
456
+ position: fixed;
457
+ background-color: #0f0f1a;
458
+ border: 1px solid #0066ff;
459
+ box-shadow: 0 0 15px rgba(0, 102, 255, 0.5);
460
+ z-index: 1000;
461
+ display: none;
462
+ min-width: 200px;
463
+ }
464
+
465
+ .context-menu button {
466
+ width: 100%;
467
+ text-align: left;
468
+ padding: 8px 15px;
469
+ border: none;
470
+ border-bottom: 1px solid #003366;
471
+ background: none;
472
+ color: #00ccff;
473
+ font-family: "M PLUS Rounded 1c", monospace;
474
+ cursor: pointer;
475
+ }
476
+
477
+ .context-menu button:hover {
478
+ background-color: #0066ff;
479
+ color: #000;
480
+ }
481
+
482
+ /* 音声/字幕のみモード */
483
+ .audio-only-mode {
484
+ position: absolute;
485
+ top: 10px;
486
+ right: 10px;
487
+ background: rgba(0, 0, 0, 0.7);
488
+ color: #00ccff;
489
+ padding: 5px 10px;
490
+ border-radius: 4px;
491
+ font-size: 12px;
492
+ display: none;
493
+ z-index: 10;
494
+ }
495
+
496
+ .audio-only-mode.active {
497
+ display: block;
498
+ }
499
+ </style>
500
  </head>
501
 
502
  <body>
503
+ <!-- ローディングオーバーレイ -->
504
+ <div class="loading-overlay" id="loadingOverlay">
505
+ <div class="spinner-box">
506
+ <div class="blue-orbit leo">
507
+ </div>
508
+ <div class="green-orbit leo">
509
+ </div>
510
+ <div class="red-orbit leo">
511
+ </div>
512
+ <div class="white-orbit w1 leo">
513
+ </div>
514
+ <div class="white-orbit w2 leo">
515
+ </div>
516
+ <div class="white-orbit w3 leo">
517
+ </div>
518
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
  </div>
520
+ <div id="ripple-container">
 
 
 
521
  </div>
522
+ <!-- フレームプレビュー -->
523
+ <div class="frame-preview" id="framePreview">
524
+ <img id="previewImage" src="">
525
+ <div class="frame-time" id="frameTime">
526
+ </div>
527
  </div>
528
+ <!-- 右クリックメニュー -->
529
+ <div class="context-menu" id="contextMenu">
530
+ <button onclick="togglePlayPause()">再生/一時停止</button>
531
+ <button onclick="toggleMute()">ミュート切り替え</button>
532
+ <button onclick="toggleSubtitles()">字幕表示切り替え</button>
533
+ <button onclick="toggleAudioOnlyMode()">音声/字幕のみモード</button>
534
+ <button onclick="goFullscreen()">全画面表示</button>
 
 
 
 
 
 
 
 
 
 
 
535
  </div>
536
+ <!-- 音声/字幕のみモード表示 -->
537
+ <div class="audio-only-mode" id="audioOnlyModeIndicator">音声/字幕のみモード</div>
538
+ <h1>ラジオ体操動画プレイヤー
539
+ <br>For Kushihara</h1>
540
+ <div class="controls">
541
+ <div class="control-group">
542
+ <label for="videoSelect">動画の音量:</label>
543
+ <select id="videoSelect">
544
+ <option value="v.mp4">小</option>
545
+ <option value="v-2.mp4">大(+50dB)</option>
546
+ </select>
547
  </div>
548
+ <div class="control-group">
549
+ <label for="speedRange">再生速度:</label>
550
+ <input type="range" id="speedRange" min="0.0001" max="10" step="0.0001" value="1" style="width:700px !important;">
551
+ <input type="number" id="speedInput" min="0.0001" step="0.0001" value="1">
 
552
  </div>
553
+ <div class="control-group">
554
+ <label for="volumeRange">音量:</label>
555
+ <input type="range" id="volumeRange" min="0" max="1" step="0.01" value="1">
556
+ <input type="number" id="volumeInput" min="0" max="1" step="0.01" value="1">
557
+ </div>
558
+ <div class="control-group">
559
+ <label for="loopCheckbox">ループ再生:</label>
560
+ <input type="checkbox" id="loopCheckbox" checked>
561
+ </div>
562
+ <!-- 字幕設定セクション -->
563
+ <div class="subtitle-settings">
564
+ <div class="control-group">
565
+ <label for="subtitleToggle">字幕表示:</label>
566
+ <input type="checkbox" id="subtitleToggle" checked>
567
+ </div>
568
+ <div class="control-group">
569
+ <label for="subtitleSize">文字サイズ:</label>
570
+ <input type="range" id="subtitleSize" min="0.5" max="5" step="0.1" value="1.5">
571
+ <input type="number" id="subtitleSizeInput" min="0.01" step="0.01" value="1.5">
572
+ </div>
573
+ <div class="control-group">
574
+ <label for="subtitleTrack">字幕トラック:</label>
575
+ <select id="subtitleTrack">
576
+ <option value="v.vtt">日本語</option>
577
+ <option value="">字幕なし</option>
578
+ </select>
579
+ </div>
580
+ </div>
581
+ <div class="control-group">
582
+ <button onclick="goFullscreen()">全画面</button>
583
+ <button onclick="toggleAudioOnlyMode()">音声/字幕のみモード</button>
584
  </div>
 
585
  </div>
586
+ <div class="video-container">
587
+ <video id="videoPlayer" src="v.mp4">
588
+ <track id="subtitleTrackElement" kind="subtitles" src="v.vtt" srclang="ja" label="日本語" default>
589
+ </track>
590
+ </video>
591
+ <div class="preview-container" id="previewContainer">
592
+ <img id="preview" style="max-width: 200px; max-height: 150px;">
593
+ <div class="preview-time" id="previewTime"></div>
594
+ </div>
595
+ <div class="custom-controls">
596
+ <div class="progress-container" id="progressContainer">
597
+ <div class="progress-bar" id="progressBar">
598
+ </div>
599
+ </div>
600
+ <div class="buttons-container">
601
+ <div class="left-controls">
602
+ <button class="control-btn" id="playPauseBtn">▶</button>
603
+ <span class="time-display" id="timeDisplay">00:00 / 00:00</span>
604
+ </div>
605
+ <div class="right-controls">
606
+ <div class="volume-container">
607
+ <button class="control-btn" id="volumeBtn">🔊</button>
608
+ <input type="range" class="volume-slider" id="volumeSlider" min="0" max="1" step="0.01" value="1">
609
+ </div>
610
+ <button class="control-btn" id="subtitleBtn" title="字幕">🔤</button>
611
+ <button class="control-btn" id="fullscreenBtn">⛶</button>
612
+ </div>
613
+ </div>
614
+ </div>
615
+ </div>
616
+ <canvas id="canvas">
617
+ </canvas>
618
+ <!-- サムネイル用の非表示video要素 -->
619
+ <video id="video-for-thumbnail" src="v.mp4" preload="auto" style="display:none;">
620
+ </video>
621
+ <script>
622
+ // 要素取得
623
+ const video = document.getElementById('videoPlayer');
624
+ const videoSelect = document.getElementById('videoSelect');
625
+ const speedRange = document.getElementById('speedRange');
626
+ const speedInput = document.getElementById('speedInput');
627
+ const volumeRange = document.getElementById('volumeRange');
628
+ const volumeInput = document.getElementById('volumeInput');
629
+ const loopCheckbox = document.getElementById('loopCheckbox');
630
+ const playPauseBtn = document.getElementById('playPauseBtn');
631
+ const progressBar = document.getElementById('progressBar');
632
+ const progressContainer = document.getElementById('progressContainer');
633
+ const timeDisplay = document.getElementById('timeDisplay');
634
+ const volumeBtn = document.getElementById('volumeBtn');
635
+ const volumeSlider = document.getElementById('volumeSlider');
636
+ const fullscreenBtn = document.getElementById('fullscreenBtn');
637
+ const subtitleBtn = document.getElementById('subtitleBtn');
638
+ const subtitleToggle = document.getElementById('subtitleToggle');
639
+ const subtitleSize = document.getElementById('subtitleSize');
640
+ const subtitleSizeInput = document.getElementById('subtitleSizeInput');
641
+ const subtitleTrack = document.getElementById('subtitleTrack');
642
+ const subtitleTrackElement = document.getElementById('subtitleTrackElement');
643
+ const videoContainer = document.getElementById('videoContainer');
644
+ const videoPlaceholder = document.getElementById('videoPlaceholder');
645
+ const previewTooltip = document.getElementById('previewTooltip');
646
+ const previewTime = document.getElementById('previewTime');
647
+ const previewFrame = document.getElementById('previewFrame');
648
+ const audioOnlyBtn = document.getElementById('audioOnlyBtn');
649
+ const fullscreenContextMenu = document.getElementById('fullscreenContextMenu');
650
+ const exitFullscreenBtn = document.getElementById('exitFullscreenBtn');
651
+ const showVideoBtn = document.getElementById('showVideoBtn');
652
+ const closeContextMenuBtn = document.getElementById('closeContextMenuBtn');
653
+
654
+ // プレビュー用動画を動的に作成
655
+ const previewVideo = document.createElement('video');
656
+ previewVideo.id = 'previewVideo';
657
+ previewVideo.className = 'thumbnail-preview';
658
+ previewVideo.muted = true;
659
+ previewVideo.preload = 'auto';
660
+ videoContainer.appendChild(previewVideo);
661
+
662
+ // 初期設定
663
+ video.controls = false;
664
+ previewVideo.src = video.src;
665
+ let isDragging = false;
666
+ let subtitlesEnabled = true;
667
+ let normalVideoWidth = videoContainer.clientWidth;
668
+ let isAudioOnlyMode = false;
669
+ let hoverTimeout;
670
+ let canvas = null;
671
+ let previewCanvas = null;
672
+ let previewContext = null;
673
+ let isGeneratingPreview = false;
674
+
675
+ // プレビュー用キャンバスを作成
676
+ function createPreviewCanvas() {
677
+ if (!canvas) {
678
+ canvas = document.createElement('canvas');
679
+ canvas.width = video.videoWidth || 640;
680
+ canvas.height = video.videoHeight || 360;
681
+ }
682
 
683
+ if (!previewCanvas) {
684
+ previewCanvas = document.createElement('canvas');
685
+ previewCanvas.width = 160;
686
+ previewCanvas.height = 90;
687
+ previewContext = previewCanvas.getContext('2d');
688
+ }
689
+ }
690
+
691
+ // プレビュー画像を生成
692
+ function generatePreview(time) {
693
+ if (isGeneratingPreview || !video.readyState) return;
694
 
695
+ try {
696
+ isGeneratingPreview = true;
697
+ createPreviewCanvas();
698
+
699
+ const currentTime = video.currentTime;
700
+ video.currentTime = time;
701
+
702
+ const onSeeked = () => {
703
+ video.removeEventListener('seeked', onSeeked);
704
 
705
+ try {
706
+ const ctx = canvas.getContext('2d');
707
+ canvas.width = video.videoWidth;
708
+ canvas.height = video.videoHeight;
709
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
710
+ previewContext.drawImage(canvas, 0, 0, previewCanvas.width, previewCanvas.height);
711
+ previewFrame.style.backgroundImage = `url(${previewCanvas.toDataURL()})`;
712
+ } catch (e) {
713
+ console.error('Preview generation error:', e);
714
+ previewFrame.style.backgroundImage = 'linear-gradient(to bottom, #0066ff, #00aaff)';
715
  }
716
+
717
+ video.currentTime = currentTime;
718
+ isGeneratingPreview = false;
719
+ };
720
+
721
+ video.addEventListener('seeked', onSeeked);
722
+ } catch (e) {
723
+ console.error('Preview error:', e);
724
+ isGeneratingPreview = false;
725
+ }
726
+ }
727
+
728
+ // プレビューツールチップを表示
729
+ function showPreviewTooltip(e) {
730
+ if (!video.duration) return;
731
 
732
+ const rect = progressContainer.getBoundingClientRect();
733
+ const percent = (e.clientX - rect.left) / rect.width;
734
+ const time = Math.max(0, Math.min(percent, 1)) * video.duration;
735
 
736
+ const tooltipWidth = previewTooltip.offsetWidth;
737
+ let left = e.clientX - rect.left;
738
+ left = Math.max(tooltipWidth / 2, Math.min(left, rect.width - tooltipWidth / 2));
 
 
 
 
739
 
740
+ previewTooltip.style.left = `${left}px`;
 
 
 
 
 
741
 
742
+ const minutes = Math.floor(time / 60);
743
+ const seconds = Math.floor(time % 60).toString().padStart(2, '0');
744
+ previewTime.textContent = `${minutes}:${seconds}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
745
 
746
+ generatePreview(time);
747
+ previewTooltip.style.display = 'block';
748
+ }
749
+
750
+ // プレビューツールチップを非表示
751
+ function hidePreviewTooltip() {
752
+ previewTooltip.style.display = 'none';
753
+ }
754
+
755
+ // ホバー時の時間表示設定
756
+ function setupHoverTime() {
757
+ const hoverTime = document.createElement('div');
758
+ hoverTime.className = 'hover-time';
759
+ progressContainer.appendChild(hoverTime);
760
+
761
+ progressContainer.addEventListener("mousemove", (e) => {
762
+ if (!video.duration) return;
763
+
764
+ const rect = progressContainer.getBoundingClientRect();
765
+ const pos = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1);
766
+ const time = pos * video.duration;
767
+
768
+ const minutes = Math.floor(time / 60);
769
+ const seconds = Math.floor(time % 60).toString().padStart(2, '0');
770
+ hoverTime.textContent = `${minutes}:${seconds}`;
771
+ hoverTime.style.display = 'block';
772
+ hoverTime.style.left = `${e.clientX - rect.left}px`;
773
+
774
+ previewVideo.style.display = "block";
775
+ previewVideo.style.left = `${e.clientX}px`;
776
+
777
+ clearTimeout(hoverTimeout);
778
+ hoverTimeout = setTimeout(() => {
779
+ previewVideo.currentTime = time;
780
+ }, 50);
781
+ });
782
+
783
+ progressContainer.addEventListener("mouseleave", () => {
784
+ const hoverTime = document.querySelector('.hover-time');
785
+ if (hoverTime) hoverTime.style.display = "none";
786
+ previewVideo.style.display = "none";
787
+ clearTimeout(hoverTimeout);
788
+ });
789
+ }
790
+
791
+ // 再生速度を更新
792
+ function updatePlaybackRate(value) {
793
+ const speed = parseFloat(value);
794
+ speedInput.value = speed;
795
+ speedRange.value = speed;
796
+ video.playbackRate = speed;
797
+ }
798
+
799
+ // 音量を更新
800
+ function updateVolume(value) {
801
+ const volume = parseFloat(value);
802
+ volumeInput.value = volume;
803
+ volumeRange.value = volume;
804
+ volumeSlider.value = volume;
805
+ video.volume = volume;
806
 
807
+ if (volume === 0) {
808
+ volumeBtn.textContent = '🔇';
809
+ } else if (volume < 0.5) {
810
+ volumeBtn.textContent = '🔈';
811
+ } else {
812
+ volumeBtn.textContent = '🔊';
813
+ }
814
+ }
815
+
816
+ // 動画変更処理
817
+ function handleVideoChange() {
818
+ const selected = videoSelect.value;
819
 
820
+ if (selected === 'v-2.mp4') {
821
+ const confirmPlay = confirm("この動画は音量が大きいです。あらかじめ、デバイスの音量をある程度下げてください。また、音割れが起きます。再生してもよろしいですか?");
822
+ if (!confirmPlay) {
823
+ videoSelect.value = video.src.split('/').pop();
824
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
825
  }
826
  }
827
 
828
+ video.src = selected;
829
+ previewVideo.src = selected;
830
+ video.load();
831
+ previewVideo.load();
832
+ video.play().catch(e => console.log(e));
833
+ }
834
+
835
+ // 再生/一時停止を切り替え
836
+ function togglePlayPause() {
837
+ if (video.paused) {
838
+ video.play().then(() => {
839
+ playPauseBtn.textContent = '⏸';
840
+ }).catch(e => console.log(e));
841
+ } else {
842
+ video.pause();
843
+ playPauseBtn.textContent = '▶';
844
+ }
845
+ }
846
+
847
+ // 進捗バーを更新
848
+ function updateProgress() {
849
+ if (!video.duration) return;
850
 
851
+ const percent = (video.currentTime / video.duration) * 100;
852
+ progressBar.style.width = `${percent}%`;
 
 
 
 
 
 
 
 
 
 
 
853
 
854
+ const currentMinutes = Math.floor(video.currentTime / 60);
855
+ const currentSeconds = Math.floor(video.currentTime % 60).toString().padStart(2, '0');
856
+ const durationMinutes = Math.floor(video.duration / 60);
857
+ const durationSeconds = Math.floor(video.duration % 60).toString().padStart(2, '0');
 
 
858
 
859
+ timeDisplay.textContent = `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`;
860
+ }
861
+
862
+ // 進捗バーを設定
863
+ function setProgress(e) {
864
+ const width = progressContainer.clientWidth;
865
+ const clickX = e.offsetX;
866
+ const duration = video.duration;
867
+ video.currentTime = (clickX / width) * duration;
868
+ }
869
+
870
+ // ミュートを切り替え
871
+ function toggleMute() {
872
+ video.muted = !video.muted;
873
+ if (video.muted) {
874
+ volumeBtn.textContent = '🔇';
875
+ volumeSlider.value = 0;
876
+ } else {
877
+ updateVolume(video.volume);
878
+ }
879
+ }
880
+
881
+ // 音量変更を処理
882
+ function handleVolumeChange() {
883
+ video.muted = false;
884
+ updateVolume(volumeSlider.value);
885
+ }
886
+
887
+ // 全画面表示を切り替え
888
+ function goFullscreen() {
889
+ if (document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
890
+ if (document.exitFullscreen) {
891
+ document.exitFullscreen();
892
+ } else if (document.webkitExitFullscreen) {
893
+ document.webkitExitFullscreen();
894
+ } else if (document.msExitFullscreen) {
895
+ document.msExitFullscreen();
896
  }
897
+ } else {
898
+ if (videoContainer.requestFullscreen) {
899
+ videoContainer.requestFullscreen();
900
+ } else if (videoContainer.webkitRequestFullscreen) {
901
+ videoContainer.webkitRequestFullscreen();
902
+ } else if (videoContainer.msRequestFullscreen) {
903
+ videoContainer.msRequestFullscreen();
904
  }
905
+ }
906
+ }
907
+
908
+ // 全画面コンテキストメニューを表示
909
+ function showContextMenu(e) {
910
+ if (!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement)) {
911
+ return;
912
+ }
913
 
914
+ e.preventDefault();
 
915
 
916
+ fullscreenContextMenu.style.display = 'block';
917
+ fullscreenContextMenu.style.left = `${e.clientX}px`;
918
+ fullscreenContextMenu.style.top = `${e.clientY}px`;
919
+ }
920
+
921
+ // 全画面コンテキストメニューを非表示
922
+ function hideContextMenu() {
923
+ fullscreenContextMenu.style.display = 'none';
924
+ }
925
+
926
+ // 音声/字幕のみモードを切り替え
927
+ function toggleAudioOnlyMode() {
928
+ isAudioOnlyMode = !isAudioOnlyMode;
929
 
930
+ if (isAudioOnlyMode) {
931
+ videoContainer.classList.add('audio-only-mode');
932
+ videoPlaceholder.style.display = 'flex';
933
+ video.style.display = 'none';
934
+ audioOnlyBtn.textContent = '🎥';
935
+ audioOnlyBtn.title = '動画表示モード';
936
+ } else {
937
+ videoContainer.classList.remove('audio-only-mode');
938
+ videoPlaceholder.style.display = 'none';
939
+ video.style.display = 'block';
940
+ audioOnlyBtn.textContent = '🔈';
941
+ audioOnlyBtn.title = '音声/字幕のみ';
942
+ }
943
 
944
+ hideContextMenu();
945
+ }
946
+
947
+ // 全画面時の字幕サイズ調整
948
+ function updateSubtitleScaleForFullscreen() {
949
+ if (document.fullscreenElement || document.webkitFullscreenElement ||
950
+ document.mozFullScreenElement || document.msFullscreenElement) {
951
+ const fullscreenWidth = window.innerWidth;
952
+ const scaleFactor = fullscreenWidth / normalVideoWidth;
953
+ document.documentElement.style.setProperty('--fullscreen-scale', scaleFactor);
954
+ } else {
955
+ document.documentElement.style.setProperty('--fullscreen-scale', 1);
956
+ }
957
+ }
958
+
959
+ // 字幕表示を切り替え
960
+ function toggleSubtitles() {
961
+ subtitlesEnabled = subtitleToggle.checked;
962
+ if (subtitleTrackElement.track) {
963
+ subtitleTrackElement.track.mode = subtitlesEnabled ? 'showing' : 'hidden';
964
+ }
965
+ subtitleBtn.style.color = subtitlesEnabled ? '#00ccff' : '#666';
966
+ }
967
+
968
+ // 字幕サイズを更新
969
+ function updateSubtitleSize(value) {
970
+ const size = parseFloat(value);
971
+ subtitleSizeInput.value = size;
972
+ subtitleSize.value = size;
973
+ document.documentElement.style.setProperty('--subtitle-scale', size);
974
 
975
+ const track = subtitleTrackElement.track;
976
+ if (track && track.cues) {
977
+ for (let i = 0; i < track.cues.length; i++) {
978
+ track.cues[i].line = 90;
979
+ track.cues[i].snapToLines = false;
980
+ }
981
+ }
982
+ }
983
+
984
+ // 字幕トラックを変更
985
+ function changeSubtitleTrack() {
986
+ const selectedTrack = subtitleTrack.value;
987
+ subtitleTrackElement.src = selectedTrack;
988
 
989
+ if (video.textTracks.length > 0) {
990
+ video.textTracks[0].mode = selectedTrack && subtitlesEnabled ? 'showing' : 'hidden';
991
+ }
992
+ }
993
+
994
+ // 字幕メニューを切り替え
995
+ function toggleSubtitleMenu() {
996
+ subtitleToggle.checked = !subtitleToggle.checked;
997
+ toggleSubtitles();
998
+ }
999
+
1000
+ // イベントリスナーを設定
1001
+ function setupEventListeners() {
1002
+ videoSelect.addEventListener('change', handleVideoChange);
1003
+
1004
+ ['input', 'change'].forEach(eventName => {
1005
+ speedRange.addEventListener(eventName, () => updatePlaybackRate(speedRange.value));
1006
+ volumeRange.addEventListener(eventName, () => updateVolume(volumeRange.value));
1007
+ subtitleSize.addEventListener(eventName, () => updateSubtitleSize(subtitleSize.value));
1008
+ });
1009
+
1010
+ speedInput.addEventListener('input', () => updatePlaybackRate(speedInput.value));
1011
+ volumeInput.addEventListener('input', () => updateVolume(volumeInput.value));
1012
+ subtitleSizeInput.addEventListener('input', () => updateSubtitleSize(subtitleSizeInput.value));
1013
+
1014
+ loopCheckbox.addEventListener('change', () => {
1015
+ video.loop = loopCheckbox.checked;
1016
+ });
1017
+
1018
+ subtitleToggle.addEventListener('change', toggleSubtitles);
1019
+ subtitleTrack.addEventListener('change', changeSubtitleTrack);
1020
+ subtitleBtn.addEventListener('click', toggleSubtitleMenu);
1021
+
1022
+ playPauseBtn.addEventListener('click', togglePlayPause);
1023
+ video.addEventListener('click', togglePlayPause);
1024
+ video.addEventListener('play', () => playPauseBtn.textContent = '⏸');
1025
+ video.addEventListener('pause', () => playPauseBtn.textContent = '▶');
1026
+ video.addEventListener('timeupdate', updateProgress);
1027
 
1028
+ progressContainer.addEventListener('click', setProgress);
1029
+ progressContainer.addEventListener('mousedown', () => isDragging = true);
1030
+ document.addEventListener('mouseup', () => isDragging = false);
1031
+ progressContainer.addEventListener('mousemove', (e) => {
1032
+ if (isDragging) setProgress(e);
1033
+ showPreviewTooltip(e);
1034
+ });
1035
+ progressContainer.addEventListener('mouseenter', showPreviewTooltip);
1036
+ progressContainer.addEventListener('mouseleave', hidePreviewTooltip);
1037
 
1038
+ volumeBtn.addEventListener('click', toggleMute);
1039
+ volumeSlider.addEventListener('input', handleVolumeChange);
1040
+ fullscreenBtn.addEventListener('click', goFullscreen);
1041
+ audioOnlyBtn.addEventListener('click', toggleAudioOnlyMode);
 
 
 
 
 
 
1042
 
1043
+ videoContainer.addEventListener('contextmenu', showContextMenu);
1044
+ exitFullscreenBtn.addEventListener('click', () => {
1045
+ if (document.exitFullscreen) document.exitFullscreen();
1046
+ hideContextMenu();
1047
+ });
1048
+ showVideoBtn.addEventListener('click', () => {
1049
+ if (isAudioOnlyMode) toggleAudioOnlyMode();
1050
+ hideContextMenu();
1051
+ });
1052
+ closeContextMenuBtn.addEventListener('click', hideContextMenu);
1053
+ document.addEventListener('click', hideContextMenu);
1054
+
1055
+ document.addEventListener('fullscreenchange', updateSubtitleScaleForFullscreen);
1056
+ document.addEventListener('webkitfullscreenchange', updateSubtitleScaleForFullscreen);
1057
+ document.addEventListener('mozfullscreenchange', updateSubtitleScaleForFullscreen);
1058
+ document.addEventListener('MSFullscreenChange', updateSubtitleScaleForFullscreen);
1059
+
1060
+ video.addEventListener('loadedmetadata', () => {
1061
+ updatePlaybackRate(speedRange.value);
1062
+ updateVolume(volumeRange.value);
1063
+ updateSubtitleSize(subtitleSize.value);
1064
+ video.loop = loopCheckbox.checked;
1065
+ toggleSubtitles();
1066
+ updateProgress();
1067
+ normalVideoWidth = videoContainer.clientWidth;
1068
+ createPreviewCanvas();
1069
+ });
1070
+ }
1071
+
1072
+ // CSS変数を初期設定
1073
+ document.documentElement.style.setProperty('--subtitle-scale', '1');
1074
+ document.documentElement.style.setProperty('--subtitle-border-radius', '10px');
1075
+ document.documentElement.style.setProperty('--fullscreen-scale', '1');
1076
+
1077
+ // 初期化
1078
+ setupHoverTime();
1079
+ setupEventListeners();
1080
+ createPreviewCanvas();
1081
+ </script>
1082
  </body>
1083
 
1084
  </html>