soiz1 commited on
Commit
37bb9a3
·
verified ·
1 Parent(s): ca4d315

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1035 -1006
index.html CHANGED
@@ -2,1044 +2,1073 @@
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"></div>
507
- <div class="green-orbit leo"></div>
508
- <div class="red-orbit leo"></div>
509
- <div class="white-orbit w1 leo"></div>
510
- <div class="white-orbit w2 leo"></div>
511
- <div class="white-orbit w3 leo"></div>
512
- </div>
513
- </div>
514
-
515
- <div id="ripple-container"></div>
516
-
517
- <!-- フレームプレビュー -->
518
- <div class="frame-preview" id="framePreview">
519
- <img id="previewImage" src="">
520
- <div class="frame-time" id="frameTime"></div>
521
- </div>
522
-
523
- <!-- 右クリックメニュー -->
524
- <div class="context-menu" id="contextMenu">
525
- <button onclick="togglePlayPause()">再生/一時停止</button>
526
- <button onclick="toggleMute()">ミュート切り替え</button>
527
- <button onclick="toggleSubtitles()">字幕表示切り替え</button>
528
- <button onclick="toggleAudioOnlyMode()">音声/字幕のみモード</button>
529
- <button onclick="goFullscreen()">全画面表示</button>
530
- </div>
531
-
532
- <!-- 音声/字幕のみモード表示 -->
533
- <div class="audio-only-mode" id="audioOnlyModeIndicator">音声/字幕のみモード</div>
534
-
535
- <h1>ラジオ体操動画プレイヤー
536
- <br>For Kushihara</h1>
537
- <div class="controls">
538
- <div class="control-group">
539
- <label for="videoSelect">動画の音量:</label>
540
- <select id="videoSelect">
541
- <option value="v.mp4">小</option>
542
- <option value="v-2.mp4">大(+50dB)</option>
543
- </select>
544
- </div>
545
- <div class="control-group">
546
- <label for="speedRange">再生速度:</label>
547
- <input type="range" id="speedRange" min="0.0001" max="10" step="0.0001" value="1" style="width:700px !important;">
548
- <input type="number" id="speedInput" min="0.0001" step="0.0001" value="1">
549
  </div>
550
- <div class="control-group">
551
- <label for="volumeRange">音量:</label>
552
- <input type="range" id="volumeRange" min="0" max="1" step="0.01" value="1">
553
- <input type="number" id="volumeInput" min="0" max="1" step="0.01" value="1">
554
  </div>
555
- <div class="control-group">
556
- <label for="loopCheckbox">ループ再生:</label>
557
- <input type="checkbox" id="loopCheckbox" checked>
 
 
558
  </div>
559
- <!-- 字幕設定セクション -->
560
- <div class="subtitle-settings">
561
- <div class="control-group">
562
- <label for="subtitleToggle">字幕表示:</label>
563
- <input type="checkbox" id="subtitleToggle" checked>
564
- </div>
565
- <div class="control-group">
566
- <label for="subtitleSize">文字サイズ:</label>
567
- <input type="range" id="subtitleSize" min="0.5" max="5" step="0.1" value="1.5">
568
- <input type="number" id="subtitleSizeInput" min="0.01" step="0.01" value="1.5">
569
- </div>
570
- <div class="control-group">
571
- <label for="subtitleTrack">字幕トラック:</label>
572
- <select id="subtitleTrack">
573
- <option value="v.vtt">日本語</option>
574
- <option value="">字幕なし</option>
575
- </select>
576
- </div>
577
  </div>
578
- <div class="control-group">
579
- <button onclick="goFullscreen()">全画面</button>
580
- <button onclick="toggleAudioOnlyMode()">音声/字幕のみモード</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
  </div>
582
- </div>
583
- <div class="video-container">
584
- <video id="videoPlayer" src="v.mp4">
585
- <track id="subtitleTrackElement" kind="subtitles" src="v.vtt" srclang="ja" label="日本語" default>
586
- </track>
587
- </video>
588
- <div class="custom-controls">
589
- <div class="progress-container" id="progressContainer">
590
- <div class="progress-bar" id="progressBar"></div>
591
- </div>
592
- <div class="buttons-container">
593
- <div class="left-controls">
594
- <button class="control-btn" id="playPauseBtn">▶</button>
595
- <span class="time-display" id="timeDisplay">00:00 / 00:00</span>
596
  </div>
597
- <div class="right-controls">
598
- <div class="volume-container">
599
- <button class="control-btn" id="volumeBtn">🔊</button>
600
- <input type="range" class="volume-slider" id="volumeSlider" min="0" max="1" step="0.01" value="1">
601
- </div>
602
- <button class="control-btn" id="subtitleBtn" title="字幕">🔤</button>
603
- <button class="control-btn" id="fullscreenBtn">⛶</button>
 
 
 
 
 
 
 
 
 
 
 
 
604
  </div>
605
- </div>
606
  </div>
607
- </div>
608
-
609
- <script>
 
 
 
610
  const video = document.getElementById('videoPlayer');
611
- const videoSelect = document.getElementById('videoSelect');
612
- const speedRange = document.getElementById('speedRange');
613
- const speedInput = document.getElementById('speedInput');
614
- const volumeRange = document.getElementById('volumeRange');
615
- const volumeInput = document.getElementById('volumeInput');
616
- const loopCheckbox = document.getElementById('loopCheckbox');
617
- const playPauseBtn = document.getElementById('playPauseBtn');
618
- const progressBar = document.getElementById('progressBar');
619
- const progressContainer = document.getElementById('progressContainer');
620
- const timeDisplay = document.getElementById('timeDisplay');
621
- const volumeBtn = document.getElementById('volumeBtn');
622
- const volumeSlider = document.getElementById('volumeSlider');
623
- const fullscreenBtn = document.getElementById('fullscreenBtn');
624
- const subtitleBtn = document.getElementById('subtitleBtn');
625
- const subtitleToggle = document.getElementById('subtitleToggle');
626
- const subtitleSize = document.getElementById('subtitleSize');
627
- const subtitleSizeInput = document.getElementById('subtitleSizeInput');
628
- const subtitleTrack = document.getElementById('subtitleTrack');
629
- const subtitleTrackElement = document.getElementById('subtitleTrackElement');
630
- const videoContainer = document.querySelector('.video-container');
631
- const framePreview = document.getElementById('framePreview');
632
- const previewImage = document.getElementById('previewImage');
633
- const frameTime = document.getElementById('frameTime');
634
- const contextMenu = document.getElementById('contextMenu');
635
- const audioOnlyModeIndicator = document.getElementById('audioOnlyModeIndicator');
636
- const contextMenu = document.getElementById('contextMenu');
637
- const previewContainer = document.getElementById('previewContainer');
638
- const preview = document.getElementById('preview');
639
- const previewTime = document.getElementById('previewTime');
640
- const VideoForThumbnail = document.getElementById('video-for-thumbnail');
641
- const canvas = document.getElementById('canvas');
642
- const ctx = canvas.getContext('2d');
643
-
644
- // 初期設定
645
- video.controls = false;
646
- let isDragging = false;
647
- let subtitlesEnabled = true;
648
- let normalVideoWidth = videoContainer.clientWidth;
649
- let isAudioOnlyMode = false;
650
- let frameCache = {};
651
- let isHoveringProgress = false;
652
- let hoverTimeout;
653
- let videoBlob = null;
654
-
655
- // ローディングアニメーションをフェードアウト
656
- window.addEventListener('load', function() {
657
- setTimeout(function() {
658
- const loadingOverlay = document.getElementById('loadingOverlay');
659
- loadingOverlay.style.opacity = '0';
660
- setTimeout(function() {
661
- loadingOverlay.style.display = 'none';
662
- }, 1000);
663
- }, 1500);
664
-
665
- // 動画をBlobとしてキャッシュ
666
- fetch(video.src)
667
- .then(response => response.blob())
668
- .then(blob => {
669
- videoBlob = blob;
670
  });
671
- });
672
-
673
- // 波紋エフェクトのコードは元のままなので省略...
674
-
675
- function updatePlaybackRate(value) {
676
- const speed = parseFloat(value);
677
- speedInput.value = speed;
678
- speedRange.value = speed;
679
- video.playbackRate = speed;
680
- }
681
-
682
- function updateVolume(value) {
683
- const volume = parseFloat(value);
684
- volumeInput.value = volume;
685
- volumeRange.value = volume;
686
- volumeSlider.value = volume;
687
- video.volume = volume;
688
-
689
- if (volume === 0) {
690
- volumeBtn.textContent = '🔇';
691
- } else if (volume < 0.5) {
692
- volumeBtn.textContent = '🔈';
693
- } else {
694
- volumeBtn.textContent = '🔊';
695
- }
696
- }
697
-
698
- // 動画ソース変更時にサムネイル用動画も更新
699
- function handleVideoChange() {
700
- const selected = videoSelect.value;
701
-
702
- if (selected === 'v-2.mp4') {
703
- const confirmPlay = confirm("この動画は音量が大きいです。あらかじめ、デバイスの音量をある程度下げてください。また、音割れが起きます。再生してもよろしいですか?");
704
- if (!confirmPlay) {
705
- videoSelect.value = video.src.split('/').pop();
706
- return;
707
  }
708
- }
709
-
710
- video.src = selected;
711
- VideoForThumbnail.src = selected;
712
- video.load();
713
- VideoForThumbnail.load();
714
- video.play().then(() => {
715
- playPauseBtn.textContent = '⏸';
716
- }).catch(e => console.log(e));
717
- }
718
-
719
- function togglePlayPause() {
720
- if (video.paused) {
721
- video.play();
722
- playPauseBtn.textContent = '⏸';
723
- } else {
724
- video.pause();
725
- playPauseBtn.textContent = '▶';
726
- }
727
- hideContextMenu();
728
- }
729
-
730
- function updateProgress() {
731
- const percent = (video.currentTime / video.duration) * 100;
732
- progressBar.style.width = `${percent}%`;
733
-
734
- const currentMinutes = Math.floor(video.currentTime / 60);
735
- const currentSeconds = Math.floor(video.currentTime % 60).toString().padStart(2, '0');
736
- const durationMinutes = Math.floor(video.duration / 60);
737
- const durationSeconds = Math.floor(video.duration % 60).toString().padStart(2, '0');
738
-
739
- timeDisplay.textContent = `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`;
740
- }
741
-
742
- function setProgress(e) {
743
- const width = progressContainer.clientWidth;
744
- const clickX = e.offsetX;
745
- const duration = video.duration;
746
- video.currentTime = (clickX / width) * duration;
747
- }
748
-
749
- function toggleMute() {
750
- video.muted = !video.muted;
751
- if (video.muted) {
752
- volumeBtn.textContent = '🔇';
753
- volumeSlider.value = 0;
754
- } else {
755
- updateVolume(video.volume);
756
- }
757
- hideContextMenu();
758
- }
759
-
760
- function handleVolumeChange() {
761
- video.muted = false;
762
- updateVolume(volumeSlider.value);
763
- }
764
-
765
- function goFullscreen() {
766
- if (
767
- document.fullscreenElement ||
768
- document.webkitFullscreenElement ||
769
- document.msFullscreenElement
770
- ) {
771
- // フルスクリーンを解除
772
- if (document.exitFullscreen) {
773
- document.exitFullscreen();
774
- } else if (document.webkitExitFullscreen) {
775
- document.webkitExitFullscreen();
776
- } else if (document.msExitFullscreen) {
777
- document.msExitFullscreen();
778
- }
779
- } else {
780
- // フルスクリーンにする
781
- if (videoContainer.requestFullscreen) {
782
- videoContainer.requestFullscreen();
783
- } else if (videoContainer.webkitRequestFullscreen) {
784
- videoContainer.webkitRequestFullscreen();
785
- } else if (videoContainer.msRequestFullscreen) {
786
- videoContainer.msRequestFullscreen();
787
  }
788
- }
789
- hideContextMenu();
790
- }
791
-
792
- // 全画面変更時の字幕サイズ調整
793
- function updateSubtitleScaleForFullscreen() {
794
- if (document.fullscreenElement || document.webkitFullscreenElement ||
795
- document.mozFullScreenElement || document.msFullscreenElement) {
796
- // 全画面モード
797
- const fullscreenWidth = window.innerWidth;
798
- const scaleFactor = fullscreenWidth / normalVideoWidth;
799
- document.documentElement.style.setProperty('--fullscreen-scale', scaleFactor);
800
- } else {
801
- // 通常モード
802
- document.documentElement.style.setProperty('--fullscreen-scale', 1);
803
- }
804
- }
805
-
806
- // 字幕関連の関数
807
- function toggleSubtitles() {
808
- subtitlesEnabled = !subtitlesEnabled;
809
- subtitleToggle.checked = subtitlesEnabled;
810
- subtitleTrackElement.track.mode = subtitlesEnabled ? 'showing' : 'hidden';
811
- subtitleBtn.style.color = subtitlesEnabled ? '#00ccff' : '#666';
812
- hideContextMenu();
813
- }
814
-
815
- function updateSubtitleSize(value) {
816
- const size = parseFloat(value);
817
- subtitleSizeInput.value = size;
818
- subtitleSize.value = size;
819
 
820
- // 字幕サイズを制御
821
- document.documentElement.style.setProperty('--subtitle-scale', size);
 
 
 
 
 
822
 
823
- // VTTCueのlineプロパティには数値のみを設定
824
- const track = subtitleTrackElement.track;
825
- if (track && track.cues) {
826
- for (let i = 0; i < track.cues.length; i++) {
827
- track.cues[i].line = 90;
828
- track.cues[i].snapToLines = false;
 
829
  }
830
- }
831
- }
832
 
833
- function changeSubtitleTrack() {
834
- const selectedTrack = subtitleTrack.value;
835
- subtitleTrackElement.src = selectedTrack;
836
- subtitleTrackElement.track.mode = selectedTrack && subtitlesEnabled ? 'showing' : 'hidden';
837
-
838
- // トラック変更後に再度読み込み
839
- video.textTracks[0].mode = 'hidden';
840
- if (selectedTrack) {
841
- video.textTracks[0].mode = subtitlesEnabled ? 'showing' : 'hidden';
842
- }
843
- }
844
-
845
- function toggleSubtitleMenu() {
846
- document.getElementById('subtitleToggle').checked ^= true;
847
- toggleSubtitles();
848
- }
849
-
850
- // フレームプレビュー関連
851
- function showFramePreview(e) {
852
- if (!videoBlob) return;
853
-
854
- const progressRect = progressContainer.getBoundingClientRect();
855
- const clickX = e.clientX - progressRect.left;
856
- const duration = video.duration;
857
- const previewTime = (clickX / progressRect.width) * duration;
858
-
859
- // 時間表示を更新
860
- const previewMinutes = Math.floor(previewTime / 60);
861
- const previewSeconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
862
- frameTime.textContent = `${previewMinutes}:${previewSeconds}`;
863
-
864
- // プレビュー位置を更新
865
- framePreview.style.left = `${e.clientX}px`;
866
- framePreview.style.display = 'block';
867
-
868
- // キャッシュがあればそれを使う
869
- const cacheKey = Math.floor(previewTime);
870
- if (frameCache[cacheKey]) {
871
- previewImage.src = frameCache[cacheKey];
872
- return;
873
- }
874
-
875
- // フレームを取得
876
- videoFrames({
877
- url: URL.createObjectURL(videoBlob),
878
- count: 1,
879
- startTime: previewTime,
880
- endTime: previewTime + 0.1
881
- }).then((frames) => {
882
- if (frames.length > 0) {
883
- previewImage.src = frames[0].image;
884
- frameCache[cacheKey] = frames[0].image; // キャッシュに保存
885
- }
886
- }).catch(err => {
887
- console.error('Error getting video frame:', err);
888
- });
889
- }
890
-
891
- function hideFramePreview() {
892
- framePreview.style.display = 'none';
893
- }
894
-
895
- // 右クリックメニュー関連
896
- function showContextMenu(e) {
897
- e.preventDefault();
898
- contextMenu.style.display = 'block';
899
- contextMenu.style.left = `${e.clientX}px`;
900
- contextMenu.style.top = `${e.clientY}px`;
901
- }
902
-
903
- function hideContextMenu() {
904
- contextMenu.style.display = 'none';
905
- }
906
-
907
- // 音声/字幕のみモード
908
- function toggleAudioOnlyMode() {
909
- isAudioOnlyMode = !isAudioOnlyMode;
910
-
911
- if (isAudioOnlyMode) {
912
- video.style.opacity = '0';
913
- audioOnlyModeIndicator.classList.add('active');
914
- } else {
915
- video.style.opacity = '1';
916
- audioOnlyModeIndicator.classList.remove('active');
917
- }
918
-
919
- hideContextMenu();
920
- }
921
 
922
- // イベントリスナー
923
- videoSelect.addEventListener('change', handleVideoChange);
924
-
925
- ['input', 'change', 'mouseup'].forEach(eventName => {
926
- speedRange.addEventListener(eventName, () => updatePlaybackRate(speedRange.value));
927
- volumeRange.addEventListener(eventName, () => updateVolume(volumeRange.value));
928
- subtitleSize.addEventListener(eventName, () => updateSubtitleSize(subtitleSize.value));
929
- });
930
-
931
- speedInput.addEventListener('input', () => updatePlaybackRate(speedInput.value));
932
- volumeInput.addEventListener('input', () => updateVolume(volumeInput.value));
933
- subtitleSizeInput.addEventListener('input', () => updateSubtitleSize(subtitleSizeInput.value));
934
-
935
- loopCheckbox.addEventListener('change', () => {
936
- video.loop = loopCheckbox.checked;
937
- });
938
-
939
- subtitleToggle.addEventListener('change', toggleSubtitles);
940
- subtitleTrack.addEventListener('change', changeSubtitleTrack);
941
- subtitleBtn.addEventListener('click', toggleSubtitleMenu);
942
-
943
- playPauseBtn.addEventListener('click', togglePlayPause);
944
- video.addEventListener('click', togglePlayPause);
945
- video.addEventListener('play', () => playPauseBtn.textContent = '⏸');
946
- video.addEventListener('pause', () => playPauseBtn.textContent = '▶');
947
- video.addEventListener('timeupdate', updateProgress);
948
- progressContainer.addEventListener('click', setProgress);
949
- progressContainer.addEventListener('mousedown', () => isDragging = true);
950
- document.addEventListener('mouseup', () => isDragging = false);
951
- // 音声/字幕のみモード
952
- function toggleAudioOnlyMode() {
953
- document.body.classList.toggle('audio-only-mode');
954
- contextMenu.style.display = 'none';
955
- }
956
-
957
- // マウスホバー時のプレビュー表示
958
- progressContainer.addEventListener('mousemove', function(e) {
959
- if (isDragging) {
960
- const width = progressContainer.clientWidth;
961
- const clickX = e.offsetX;
962
- const duration = video.duration;
963
- const previewTime = (clickX / width) * duration;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
964
 
965
- // プレビュー位置を更新
966
- previewContainer.style.left = `${e.clientX - 100}px`;
967
- previewContainer.style.bottom = '60px';
968
- previewContainer.style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
969
 
970
- // 時間表示を更新
971
- const minutes = Math.floor(previewTime / 60);
972
- const seconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
973
- document.getElementById('previewTime').textContent = `${minutes}:${seconds}`;
 
 
 
 
 
 
 
 
 
 
 
 
974
 
975
- // サムネイル画像を更新
976
- updateThumbnail(previewTime);
977
- } else {
978
- previewContainer.style.display = 'none';
979
- }
980
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
981
 
982
- progressContainer.addEventListener('mouseleave', function() {
983
- previewContainer.style.display = 'none';
984
- });
 
 
985
 
986
- // サムネイル画像更新関数
987
- function updateThumbnail(time) {
988
- VideoForThumbnail.currentTime = time;
989
-
990
- VideoForThumbnail.addEventListener('seeked', function() {
991
- canvas.width = VideoForThumbnail.videoWidth;
992
- canvas.height = VideoForThumbnail.videoHeight;
993
- ctx.drawImage(VideoForThumbnail, 0, 0, canvas.width, canvas.height);
994
- preview.src = canvas.toDataURL('image/jpeg');
995
- }, { once: true });
996
- }
997
-
998
 
999
- // プログレスバーのホバーイベント
1000
- progressContainer.addEventListener('mouseenter', () => {
1001
- isHoveringProgress = true;
1002
- clearTimeout(hoverTimeout);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1003
  });
1004
 
1005
- progressContainer.addEventListener('mouseleave', () => {
1006
- isHoveringProgress = false;
1007
- hoverTimeout = setTimeout(() => {
1008
- if (!isDragging) hideFramePreview();
1009
- }, 300);
1010
  });
1011
 
1012
- volumeBtn.addEventListener('click', toggleMute);
1013
- volumeSlider.addEventListener('input', handleVolumeChange);
1014
- fullscreenBtn.addEventListener('click', goFullscreen);
1015
-
1016
- // 全画面変更イベントを監視
1017
- document.addEventListener('fullscreenchange', updateSubtitleScaleForFullscreen);
1018
- document.addEventListener('webkitfullscreenchange', updateSubtitleScaleForFullscreen);
1019
- document.addEventListener('mozfullscreenchange', updateSubtitleScaleForFullscreen);
1020
- document.addEventListener('MSFullscreenChange', updateSubtitleScaleForFullscreen);
1021
-
1022
- // 右クリックメニューイベント
1023
- videoContainer.addEventListener('contextmenu', showContextMenu);
1024
- document.addEventListener('click', hideContextMenu);
1025
- document.addEventListener('keydown', (e) => {
1026
- if (e.key === 'Escape') hideContextMenu();
1027
  });
1028
-
1029
- video.addEventListener('loadedmetadata', () => {
1030
- updatePlaybackRate(speedRange.value);
1031
- updateVolume(volumeRange.value);
1032
- updateSubtitleSize(subtitleSize.value);
1033
- video.loop = loopCheckbox.checked;
1034
- toggleSubtitles();
1035
- updateProgress();
1036
- normalVideoWidth = videoContainer.clientWidth;
1037
  });
1038
-
1039
- // CSS変数を設定
1040
- document.documentElement.style.setProperty('--subtitle-scale', '1');
1041
- document.documentElement.style.setProperty('--subtitle-border-radius', '10px');
1042
- document.documentElement.style.setProperty('--fullscreen-scale', '1');
1043
- </script>
1044
  </body>
 
1045
  </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
  const video = document.getElementById('videoPlayer');
623
+ const videoSelect = document.getElementById('videoSelect');
624
+ const speedRange = document.getElementById('speedRange');
625
+ const speedInput = document.getElementById('speedInput');
626
+ const volumeRange = document.getElementById('volumeRange');
627
+ const volumeInput = document.getElementById('volumeInput');
628
+ const loopCheckbox = document.getElementById('loopCheckbox');
629
+ const playPauseBtn = document.getElementById('playPauseBtn');
630
+ const progressBar = document.getElementById('progressBar');
631
+ const progressContainer = document.getElementById('progressContainer');
632
+ const timeDisplay = document.getElementById('timeDisplay');
633
+ const volumeBtn = document.getElementById('volumeBtn');
634
+ const volumeSlider = document.getElementById('volumeSlider');
635
+ const fullscreenBtn = document.getElementById('fullscreenBtn');
636
+ const subtitleBtn = document.getElementById('subtitleBtn');
637
+ const subtitleToggle = document.getElementById('subtitleToggle');
638
+ const subtitleSize = document.getElementById('subtitleSize');
639
+ const subtitleSizeInput = document.getElementById('subtitleSizeInput');
640
+ const subtitleTrack = document.getElementById('subtitleTrack');
641
+ const subtitleTrackElement = document.getElementById('subtitleTrackElement');
642
+ const videoContainer = document.querySelector('.video-container');
643
+ const framePreview = document.getElementById('framePreview');
644
+ const previewImage = document.getElementById('previewImage');
645
+ const frameTime = document.getElementById('frameTime');
646
+ const audioOnlyModeIndicator = document.getElementById('audioOnlyModeIndicator');
647
+ const contextMenu = document.getElementById('contextMenu');
648
+ const previewContainer = document.getElementById('previewContainer');
649
+ const preview = document.getElementById('preview');
650
+ const previewTime = document.getElementById('previewTime');
651
+ const VideoForThumbnail = document.getElementById('video-for-thumbnail');
652
+ const canvas = document.getElementById('canvas');
653
+ const ctx = canvas.getContext('2d');
654
+
655
+ // 初期設定
656
+ video.controls = false;
657
+ let isDragging = false;
658
+ let subtitlesEnabled = true;
659
+ let normalVideoWidth = videoContainer.clientWidth;
660
+ let isAudioOnlyMode = false;
661
+ let frameCache = {};
662
+ let isHoveringProgress = false;
663
+ let hoverTimeout;
664
+ let videoBlob = null;
665
+
666
+ // ローディングアニメーションをフェードアウト
667
+ window.addEventListener('load', function() {
668
+ setTimeout(function() {
669
+ const loadingOverlay = document.getElementById('loadingOverlay');
670
+ loadingOverlay.style.opacity = '0';
671
+ setTimeout(function() {
672
+ loadingOverlay.style.display = 'none';
673
+ }, 1000);
674
+ }, 1500);
675
+
676
+ // 動画をBlobとしてキャッシュ
677
+ fetch(video.src)
678
+ .then(response => response.blob())
679
+ .then(blob => {
680
+ videoBlob = blob;
681
+ });
682
  });
683
+
684
+ // 波紋エフェクトのコードは元のままなので省略...
685
+
686
+ function updatePlaybackRate(value) {
687
+ const speed = parseFloat(value);
688
+ speedInput.value = speed;
689
+ speedRange.value = speed;
690
+ video.playbackRate = speed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
  }
692
+
693
+ function updateVolume(value) {
694
+ const volume = parseFloat(value);
695
+ volumeInput.value = volume;
696
+ volumeRange.value = volume;
697
+ volumeSlider.value = volume;
698
+ video.volume = volume;
699
+
700
+ if (volume === 0) {
701
+ volumeBtn.textContent = '🔇';
702
+ } else if (volume < 0.5) {
703
+ volumeBtn.textContent = '🔈';
704
+ } else {
705
+ volumeBtn.textContent = '🔊';
706
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
  }
708
+
709
+ // 動画ソース変更時にサムネイル用動画も更新
710
+ function handleVideoChange() {
711
+ const selected = videoSelect.value;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
 
713
+ if (selected === 'v-2.mp4') {
714
+ const confirmPlay = confirm("この動画は音量が大きいです。あらかじめ、デバイスの音量をある程度下げてください。また、音割れが起きます。再生してもよろしいですか?");
715
+ if (!confirmPlay) {
716
+ videoSelect.value = video.src.split('/').pop();
717
+ return;
718
+ }
719
+ }
720
 
721
+ video.src = selected;
722
+ VideoForThumbnail.src = selected;
723
+ video.load();
724
+ VideoForThumbnail.load();
725
+ video.play().then(() => {
726
+ playPauseBtn.textContent = '⏸';
727
+ }).catch(e => console.log(e));
728
  }
 
 
729
 
730
+ function togglePlayPause() {
731
+ if (video.paused) {
732
+ video.play();
733
+ playPauseBtn.textContent = '';
734
+ } else {
735
+ video.pause();
736
+ playPauseBtn.textContent = '';
737
+ }
738
+ hideContextMenu();
739
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
740
 
741
+ function updateProgress() {
742
+ const percent = (video.currentTime / video.duration) * 100;
743
+ progressBar.style.width = `${percent}%`;
744
+
745
+ const currentMinutes = Math.floor(video.currentTime / 60);
746
+ const currentSeconds = Math.floor(video.currentTime % 60).toString().padStart(2, '0');
747
+ const durationMinutes = Math.floor(video.duration / 60);
748
+ const durationSeconds = Math.floor(video.duration % 60).toString().padStart(2, '0');
749
+
750
+ timeDisplay.textContent = `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`;
751
+ }
752
+
753
+ function setProgress(e) {
754
+ const width = progressContainer.clientWidth;
755
+ const clickX = e.offsetX;
756
+ const duration = video.duration;
757
+ video.currentTime = (clickX / width) * duration;
758
+ }
759
+
760
+ function toggleMute() {
761
+ video.muted = !video.muted;
762
+ if (video.muted) {
763
+ volumeBtn.textContent = '🔇';
764
+ volumeSlider.value = 0;
765
+ } else {
766
+ updateVolume(video.volume);
767
+ }
768
+ hideContextMenu();
769
+ }
770
+
771
+ function handleVolumeChange() {
772
+ video.muted = false;
773
+ updateVolume(volumeSlider.value);
774
+ }
775
+
776
+ function goFullscreen() {
777
+ if (
778
+ document.fullscreenElement ||
779
+ document.webkitFullscreenElement ||
780
+ document.msFullscreenElement
781
+ ) {
782
+ // フルスクリーンを解除
783
+ if (document.exitFullscreen) {
784
+ document.exitFullscreen();
785
+ } else if (document.webkitExitFullscreen) {
786
+ document.webkitExitFullscreen();
787
+ } else if (document.msExitFullscreen) {
788
+ document.msExitFullscreen();
789
+ }
790
+ } else {
791
+ // フルスクリーンにする
792
+ if (videoContainer.requestFullscreen) {
793
+ videoContainer.requestFullscreen();
794
+ } else if (videoContainer.webkitRequestFullscreen) {
795
+ videoContainer.webkitRequestFullscreen();
796
+ } else if (videoContainer.msRequestFullscreen) {
797
+ videoContainer.msRequestFullscreen();
798
+ }
799
+ }
800
+ hideContextMenu();
801
+ }
802
 
803
+ // 全画面変更時の字幕サイズ調整
804
+ function updateSubtitleScaleForFullscreen() {
805
+ if (document.fullscreenElement || document.webkitFullscreenElement ||
806
+ document.mozFullScreenElement || document.msFullscreenElement) {
807
+ // 全画面モード
808
+ const fullscreenWidth = window.innerWidth;
809
+ const scaleFactor = fullscreenWidth / normalVideoWidth;
810
+ document.documentElement.style.setProperty('--fullscreen-scale', scaleFactor);
811
+ } else {
812
+ // 通常モード
813
+ document.documentElement.style.setProperty('--fullscreen-scale', 1);
814
+ }
815
+ }
816
+
817
+ // 字幕関連の関数
818
+ function toggleSubtitles() {
819
+ subtitlesEnabled = !subtitlesEnabled;
820
+ subtitleToggle.checked = subtitlesEnabled;
821
+ subtitleTrackElement.track.mode = subtitlesEnabled ? 'showing' : 'hidden';
822
+ subtitleBtn.style.color = subtitlesEnabled ? '#00ccff' : '#666';
823
+ hideContextMenu();
824
+ }
825
+
826
+ function updateSubtitleSize(value) {
827
+ const size = parseFloat(value);
828
+ subtitleSizeInput.value = size;
829
+ subtitleSize.value = size;
830
+
831
+ // 字幕サイズを制御
832
+ document.documentElement.style.setProperty('--subtitle-scale', size);
833
+
834
+ // VTTCueのlineプロパティには数値のみを設定
835
+ const track = subtitleTrackElement.track;
836
+ if (track && track.cues) {
837
+ for (let i = 0; i < track.cues.length; i++) {
838
+ track.cues[i].line = 90;
839
+ track.cues[i].snapToLines = false;
840
+ }
841
+ }
842
+ }
843
 
844
+ function changeSubtitleTrack() {
845
+ const selectedTrack = subtitleTrack.value;
846
+ subtitleTrackElement.src = selectedTrack;
847
+ subtitleTrackElement.track.mode = selectedTrack && subtitlesEnabled ? 'showing' : 'hidden';
848
+
849
+ // トラック変更後に再度読み込み
850
+ video.textTracks[0].mode = 'hidden';
851
+ if (selectedTrack) {
852
+ video.textTracks[0].mode = subtitlesEnabled ? 'showing' : 'hidden';
853
+ }
854
+ }
855
+
856
+ function toggleSubtitleMenu() {
857
+ document.getElementById('subtitleToggle').checked ^= true;
858
+ toggleSubtitles();
859
+ }
860
 
861
+ // フレームプレビュー関連
862
+ function showFramePreview(e) {
863
+ if (!videoBlob) return;
864
+
865
+ const progressRect = progressContainer.getBoundingClientRect();
866
+ const clickX = e.clientX - progressRect.left;
867
+ const duration = video.duration;
868
+ const previewTime = (clickX / progressRect.width) * duration;
869
+
870
+ // 時間表示を更新
871
+ const previewMinutes = Math.floor(previewTime / 60);
872
+ const previewSeconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
873
+ frameTime.textContent = `${previewMinutes}:${previewSeconds}`;
874
+
875
+ // プレビュー位置を更新
876
+ framePreview.style.left = `${e.clientX}px`;
877
+ framePreview.style.display = 'block';
878
+
879
+ // キャッシュがあればそれを使う
880
+ const cacheKey = Math.floor(previewTime);
881
+ if (frameCache[cacheKey]) {
882
+ previewImage.src = frameCache[cacheKey];
883
+ return;
884
+ }
885
+
886
+ // フレームを取得
887
+ videoFrames({
888
+ url: URL.createObjectURL(videoBlob),
889
+ count: 1,
890
+ startTime: previewTime,
891
+ endTime: previewTime + 0.1
892
+ }).then((frames) => {
893
+ if (frames.length > 0) {
894
+ previewImage.src = frames[0].image;
895
+ frameCache[cacheKey] = frames[0].image; // キャッシュに保存
896
+ }
897
+ }).catch(err => {
898
+ console.error('Error getting video frame:', err);
899
+ });
900
+ }
901
+
902
+ function hideFramePreview() {
903
+ framePreview.style.display = 'none';
904
+ }
905
+
906
+ // 右クリックメニュー関連
907
+ function showContextMenu(e) {
908
+ e.preventDefault();
909
+ contextMenu.style.display = 'block';
910
+ contextMenu.style.left = `${e.clientX}px`;
911
+ contextMenu.style.top = `${e.clientY}px`;
912
+ }
913
+
914
+ function hideContextMenu() {
915
+ contextMenu.style.display = 'none';
916
+ }
917
+
918
+ // 音声/字幕のみモード
919
+ function toggleAudioOnlyMode() {
920
+ isAudioOnlyMode = !isAudioOnlyMode;
921
+
922
+ if (isAudioOnlyMode) {
923
+ video.style.opacity = '0';
924
+ audioOnlyModeIndicator.classList.add('active');
925
+ } else {
926
+ video.style.opacity = '1';
927
+ audioOnlyModeIndicator.classList.remove('active');
928
+ }
929
+
930
+ hideContextMenu();
931
+ }
932
+
933
+ // イベントリスナー
934
+ videoSelect.addEventListener('change', handleVideoChange);
935
 
936
+ ['input', 'change', 'mouseup'].forEach(eventName => {
937
+ speedRange.addEventListener(eventName, () => updatePlaybackRate(speedRange.value));
938
+ volumeRange.addEventListener(eventName, () => updateVolume(volumeRange.value));
939
+ subtitleSize.addEventListener(eventName, () => updateSubtitleSize(subtitleSize.value));
940
+ });
941
 
942
+ speedInput.addEventListener('input', () => updatePlaybackRate(speedInput.value));
943
+ volumeInput.addEventListener('input', () => updateVolume(volumeInput.value));
944
+ subtitleSizeInput.addEventListener('input', () => updateSubtitleSize(subtitleSizeInput.value));
945
+
946
+ loopCheckbox.addEventListener('change', () => {
947
+ video.loop = loopCheckbox.checked;
948
+ });
 
 
 
 
 
949
 
950
+ subtitleToggle.addEventListener('change', toggleSubtitles);
951
+ subtitleTrack.addEventListener('change', changeSubtitleTrack);
952
+ subtitleBtn.addEventListener('click', toggleSubtitleMenu);
953
+
954
+ playPauseBtn.addEventListener('click', togglePlayPause);
955
+ video.addEventListener('click', togglePlayPause);
956
+ video.addEventListener('play', () => playPauseBtn.textContent = '⏸');
957
+ video.addEventListener('pause', () => playPauseBtn.textContent = '▶');
958
+ video.addEventListener('timeupdate', updateProgress);
959
+ progressContainer.addEventListener('click', setProgress);
960
+ progressContainer.addEventListener('mousedown', () => isDragging = true);
961
+ document.addEventListener('mouseup', () => isDragging = false);
962
+ // マウスホバー時のプレビュー表示
963
+ progressContainer.addEventListener('mousemove', function(e) {
964
+ if (isDragging) {
965
+ const width = progressContainer.clientWidth;
966
+ const clickX = e.offsetX;
967
+ const duration = video.duration;
968
+ const previewTime = (clickX / width) * duration;
969
+
970
+ // プレビュー位置を更新
971
+ previewContainer.style.left = `${e.clientX - 100}px`;
972
+ previewContainer.style.bottom = '60px';
973
+ previewContainer.style.display = 'block';
974
+
975
+ // 時間表示を更新
976
+ const minutes = Math.floor(previewTime / 60);
977
+ const seconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
978
+ document.getElementById('previewTime').textContent = `${minutes}:${seconds}`;
979
+
980
+ // サムネイル画像を更新
981
+ updateThumbnail(previewTime);
982
+ } else {
983
+ previewContainer.style.display = 'none';
984
+ }
985
+ });
986
+
987
+ // サムネイル画像更新関数
988
+ function updateThumbnail(time) {
989
+ VideoForThumbnail.currentTime = time;
990
+
991
+ VideoForThumbnail.addEventListener('seeked', function() {
992
+ canvas.width = VideoForThumbnail.videoWidth;
993
+ canvas.height = VideoForThumbnail.videoHeight;
994
+ ctx.drawImage(VideoForThumbnail, 0, 0, canvas.width, canvas.height);
995
+ preview.src = canvas.toDataURL('image/jpeg');
996
+ }, { once: true });
997
+ }
998
+
999
+
1000
+ // プログレスバーのホバーイベント
1001
+ progressContainer.addEventListener('mouseenter', () => {
1002
+ isHoveringProgress = true;
1003
+ clearTimeout(hoverTimeout);
1004
+ });
1005
+
1006
+ progressContainer.addEventListener('mouseleave', () => {
1007
+ isHoveringProgress = false;
1008
+ hoverTimeout = setTimeout(() => {
1009
+ if (!isDragging) hideFramePreview();
1010
+ }, 300);
1011
+ });
1012
+
1013
+ volumeBtn.addEventListener('click', toggleMute);
1014
+ volumeSlider.addEventListener('input', handleVolumeChange);
1015
+ fullscreenBtn.addEventListener('click', goFullscreen);
1016
+
1017
+ // 全画面変更イベントを監視
1018
+ document.addEventListener('fullscreenchange', updateSubtitleScaleForFullscreen);
1019
+ document.addEventListener('webkitfullscreenchange', updateSubtitleScaleForFullscreen);
1020
+ document.addEventListener('mozfullscreenchange', updateSubtitleScaleForFullscreen);
1021
+ document.addEventListener('MSFullscreenChange', updateSubtitleScaleForFullscreen);
1022
+
1023
+ // 右クリックメニューイベント
1024
+ videoContainer.addEventListener('contextmenu', showContextMenu);
1025
+ document.addEventListener('click', hideContextMenu);
1026
+ document.addEventListener('keydown', (e) => {
1027
+ if (e.key === 'Escape') hideContextMenu();
1028
+ });
1029
+
1030
+ video.addEventListener('loadedmetadata', () => {
1031
+ updatePlaybackRate(speedRange.value);
1032
+ updateVolume(volumeRange.value);
1033
+ updateSubtitleSize(subtitleSize.value);
1034
+ video.loop = loopCheckbox.checked;
1035
+ toggleSubtitles();
1036
+ updateProgress();
1037
+ normalVideoWidth = videoContainer.clientWidth;
1038
+ });
1039
+ video.addEventListener("loadeddata", async () => {
1040
+ const response = await fetch(video.src);
1041
+ videoBlob = await response.blob();
1042
  });
1043
 
1044
+ // 保存
1045
+ video.addEventListener('timeupdate', () => {
1046
+ localStorage.setItem('radioTaisoTime', video.currentTime);
 
 
1047
  });
1048
 
1049
+ // 復元
1050
+ window.addEventListener('load', () => {
1051
+ const savedTime = parseFloat(localStorage.getItem('radioTaisoTime'));
1052
+ if (!isNaN(savedTime)) {
1053
+ video.currentTime = savedTime;
1054
+ }
 
 
 
 
 
 
 
 
 
1055
  });
1056
+ document.addEventListener('keydown', (e) => {
1057
+ if (e.target.tagName === 'INPUT') return; // 入力中は無視
1058
+ switch (e.key.toLowerCase()) {
1059
+ case ' ': e.preventDefault(); togglePlayPause(); break;
1060
+ case 'f': goFullscreen(); break;
1061
+ case 'm': toggleMute(); break;
1062
+ case 'arrowright': video.currentTime += 5; break;
1063
+ case 'arrowleft': video.currentTime -= 5; break;
1064
+ }
1065
  });
1066
+
1067
+ // CSS変数を設定
1068
+ document.documentElement.style.setProperty('--subtitle-scale', '1');
1069
+ document.documentElement.style.setProperty('--subtitle-border-radius', '10px');
1070
+ document.documentElement.style.setProperty('--fullscreen-scale', '1');
1071
+ </script>
1072
  </body>
1073
+
1074
  </html>