soiz1 commited on
Commit
3dcb013
·
verified ·
1 Parent(s): 17c073e

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1328 -1323
index.html CHANGED
@@ -2,1333 +2,1338 @@
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
- /* リセットと基本設定 */
14
- html, body {
15
- margin: 0;
16
- padding: 0;
17
- width: 100%;
18
- min-height: 100vh;
19
- background-color: #0a192f;
20
- color: #00ffcc;
21
- font-family: "M PLUS Rounded 1c", monospace;
22
- display: flex;
23
- flex-direction: column;
24
- align-items: center;
25
- overflow-x: hidden;
26
- position: relative;
27
- }
28
-
29
- /* 背景アニメーションコンテナ */
30
- .wave-container {
31
- position: fixed;
32
- top: 0;
33
- left: 0;
34
- width: 100vw;
35
- height: 100vh;
36
- z-index: -1; /* 他の要素より背面に */
37
- overflow: hidden;
38
- }
39
-
40
- /* グリッド線 */
41
- .grid-lines {
42
- position: absolute;
43
- top: 0;
44
- left: 0;
45
- width: 100%;
46
- height: 100%;
47
- background-image:
48
- linear-gradient(rgba(100, 200, 255, 0.1) 1px, transparent 1px),
49
- linear-gradient(90deg, rgba(100, 200, 255, 0.1) 1px, transparent 1px);
50
- background-size: 50px 50px;
51
- z-index: 1;
52
- }
53
- /* 波のアニメーション */
54
- .wave {
55
- position: absolute;
56
- top: 0;
57
- left: 0;
58
- width: 100%;
59
- height: 100%;
60
- background: linear-gradient(
61
- to bottom,
62
- transparent,
63
- rgba(100, 200, 255, 0.1) 20%,
64
- rgba(100, 200, 255, 0.3) 50%,
65
- rgba(100, 200, 255, 0.1) 80%,
66
- transparent
67
- );
68
- opacity: 0.7;
69
- animation: waveFlow 8s linear infinite;
70
- z-index: 2;
71
- }
72
-
73
- .wave:nth-child(2) {
74
- animation-delay: -2s;
75
- opacity: 0.5;
76
- }
77
-
78
- .wave:nth-child(3) {
79
- animation-delay: -4s;
80
- opacity: 0.3;
81
- }
82
-
83
- @keyframes waveFlow {
84
- 0% {
85
- transform: translateY(-100%);
86
- }
87
- 100% {
88
- transform: translateY(100%);
89
- }
90
- }
91
-
92
- /* テクノロジードット */
93
- .tech-dots {
94
- position: absolute;
95
- top: 0;
96
- left: 0;
97
- width: 100%;
98
- height: 100%;
99
- z-index: 3;
100
- }
101
-
102
- .tech-dot {
103
- position: absolute;
104
- width: 3px;
105
- height: 3px;
106
- background-color: rgba(100, 200, 255, 0.7);
107
- border-radius: 50%;
108
- animation: blink 2s infinite alternate;
109
- }
110
-
111
- @keyframes blink {
112
- 0% { opacity: 0.2; }
113
- 100% { opacity: 0.8; }
114
- }
115
-
116
- /* メインコンテンツ */
117
- .main-content {
118
- position: relative;
119
- z-index: 10;
120
- width: 100%;
121
- max-width: 1200px;
122
- padding: 20px;
123
- box-sizing: border-box;
124
- display: flex;
125
- flex-direction: column;
126
- align-items: center;
127
- }
128
-
129
- /* ヘッダー */
130
- h1 {
131
- color: #00aaff;
132
- text-shadow: 0 0 5px #0066ff;
133
- border-bottom: 1px solid #0066ff;
134
- padding-bottom: 10px;
135
- text-align: center;
136
- margin-top: 20px;
137
- }
138
-
139
- /* ビデオコンテナ */
140
- .video-container {
141
- position: relative;
142
- max-width: 800px;
143
- width: 100%;
144
- margin: 30px 0 20px;
145
- border: 2px solid #0066ff;
146
- box-shadow: 0 0 15px rgba(0, 102, 255, 0.5);
147
- background: #000;
148
- }
149
-
150
- video {
151
- width: 100%;
152
- display: block;
153
- }
154
-
155
- /* 字幕スタイル */
156
- video::cue {
157
- background-color: rgba(0, 0, 0, 0.7) !important;
158
- color: #c7dbed !important;
159
- font-family: "M PLUS Rounded 1c", monospace !important;
160
- text-shadow: 1px 1px 2px #000 !important;
161
- outline: 3px solid #0b3e8f !important;
162
- border-radius: 10px !important;
163
- }
164
-
165
- /* カスタム動画コントロール */
166
- video::-webkit-media-controls {
167
- display: none !important;
168
- }
169
-
170
- .custom-controls {
171
- position: absolute;
172
- bottom: 0;
173
- left: 0;
174
- right: 0;
175
- padding: 10px;
176
- display: flex;
177
- flex-direction: column;
178
- opacity: 0;
179
- transition: opacity 0.3s;
180
- }
181
-
182
- .video-container:hover .custom-controls {
183
- opacity: 1;
184
- }
185
-
186
- .progress-container {
187
- width: 100%;
188
- height: 8px;
189
- background: #001133;
190
- margin-bottom: 10px;
191
- cursor: pointer;
192
- position: relative;
193
- }
194
-
195
- .progress-bar {
196
- height: 100%;
197
- background: #00aaff;
198
- width: 0%;
199
- position: relative;
200
- }
201
-
202
- .progress-bar::after {
203
- content: '';
204
- position: absolute;
205
- right: -5px;
206
- top: 50%;
207
- transform: translateY(-50%);
208
- width: 10px;
209
- height: 10px;
210
- background: #00ccff;
211
- border-radius: 50%;
212
- box-shadow: 0 0 5px #00ccff;
213
- }
214
-
215
- .buttons-container {
216
- display: flex;
217
- align-items: center;
218
- justify-content: space-between;
219
- }
220
-
221
- .left-controls, .right-controls {
222
- display: flex;
223
- align-items: center;
224
- gap: 15px;
225
- }
226
-
227
- .control-btn {
228
- background: none;
229
- border: none;
230
- color: #00ccff;
231
- font-size: 16px;
232
- cursor: pointer;
233
- transition: all 0.3s;
234
- }
235
-
236
- .control-btn:hover {
237
- color: #00ffcc;
238
- text-shadow: 0 0 5px #00ffcc;
239
- }
240
-
241
- .time-display {
242
- font-size: 14px;
243
- color: #00aaff;
244
- box-shadow: 0.1px 0.1px 0.1px black;
245
- font-family: "M PLUS Rounded 1c", monospace;
246
- }
247
-
248
- .volume-container {
249
- display: flex;
250
- align-items: center;
251
- gap: 5px;
252
- }
253
-
254
- .volume-slider {
255
- width: 80px;
256
- -webkit-appearance: none;
257
- height: 4px;
258
- background: #001133;
259
- outline: none;
260
- }
261
-
262
- .volume-slider::-webkit-slider-thumb {
263
- -webkit-appearance: none;
264
- width: 12px;
265
- height: 12px;
266
- background: #00aaff;
267
- border-radius: 50%;
268
- cursor: pointer;
269
- }
270
-
271
- /* コントロールパネル */
272
- .controls, .new {
273
- width: 100%;
274
- max-width: 800px;
275
- background-color: #0f0f1a;
276
- padding: 20px;
277
- border: 1px solid #0066ff;
278
- box-shadow: 0 0 15px rgba(0, 102, 255, 0.3);
279
- margin-bottom: 20px;
280
- }
281
-
282
- .control-group {
283
- display: flex;
284
- flex-direction: row;
285
- align-items: center;
286
- justify-content: flex-start;
287
- gap: 10px;
288
- flex-wrap: nowrap;
289
- }
290
-
291
- .control-group label {
292
- white-space: nowrap;
293
- min-width: 100px;
294
- text-align: right;
295
- color: #00ccff;
296
- }
297
-
298
- input[type="range"] {
299
- flex-grow: 1;
300
- -webkit-appearance: none;
301
- height: 8px;
302
- background: #001133;
303
- border-radius: 5px;
304
- outline: none;
305
- }
306
-
307
- input[type="range"]::-webkit-slider-thumb {
308
- -webkit-appearance: none;
309
- width: 18px;
310
- height: 18px;
311
- background: #00aaff;
312
- border-radius: 50%;
313
- cursor: pointer;
314
- box-shadow: 0 0 5px #00aaff;
315
- }
316
-
317
- input[type="number"], select {
318
- background-color: #001133;
319
- color: #00ccff;
320
- border: 1px solid #0066ff;
321
- padding: 5px;
322
- font-family: "M PLUS Rounded 1c", monospace;
323
- }
324
-
325
- button {
326
- background-color: #001133;
327
- color: #00ccff;
328
- border: 1px solid #0066ff;
329
- padding: 8px 15px;
330
- cursor: pointer;
331
- font-family: "M PLUS Rounded 1c", monospace;
332
- transition: all 0.3s;
333
- align-self: flex-start;
334
- }
335
-
336
- button:hover {
337
- background-color: #0066ff;
338
- color: #000;
339
- box-shadow: 0 0 10px #0066ff;
340
- }
341
-
342
- select {
343
- width: 300px;
344
- background-color: #001133;
345
- color: #00ccff;
346
- border: 1px solid #0066ff;
347
- padding: 5px;
348
- }
349
-
350
- input[type="checkbox"] {
351
- -webkit-appearance: none;
352
- width: 18px;
353
- height: 18px;
354
- background: #001133;
355
- border: 1px solid #0066ff;
356
- position: relative;
357
- }
358
-
359
- input[type="checkbox"]:checked {
360
- background: #0066ff;
361
- box-shadow: 0 0 5px #0066ff;
362
- }
363
-
364
- input[type="checkbox"]:checked::after {
365
- content: "✓";
366
- position: absolute;
367
- color: #000;
368
- font-size: 14px;
369
- top: 50%;
370
- left: 50%;
371
- transform: translate(-50%, -50%);
372
- }
373
-
374
- /* 字幕設定用スタイル */
375
- .subtitle-settings {
376
- margin-top: 10px;
377
- padding: 10px;
378
- background-color: rgba(0, 20, 40, 0.5);
379
- border: 1px solid #0066ff;
380
- }
381
-
382
- /* 字幕サイズ調整用のCSS変数 */
383
- :root {
384
- --subtitle-scale: 1;
385
- --subtitle-border-radius: 10px;
386
- }
387
-
388
- video::cue {
389
- font-size: calc(16px * var(--subtitle-scale)) !important;
390
- line-height: 1.5 !important;
391
- border-radius: var(--subtitle-border-radius) !important;
392
- }
393
-
394
- /* 全画面時の字幕サイズ調整 */
395
- .video-container:fullscreen video::cue,
396
- .video-container:-webkit-full-screen video::cue,
397
- .video-container:-moz-full-screen video::cue,
398
- .video-container:-ms-fullscreen video::cue {
399
- font-size: calc(16px * var(--subtitle-scale) * var(--fullscreen-scale, 1)) !important;
400
- }
401
-
402
- /* ローディングアニメーション */
403
- .loading-overlay {
404
- position: fixed;
405
- top: 0;
406
- left: 0;
407
- width: 100%;
408
- height: 100%;
409
- background-color: rgba(0, 0, 0, 0.8);
410
- display: flex;
411
- justify-content: center;
412
- align-items: center;
413
- z-index: 9999;
414
- transition: opacity 1s ease-out;
415
- }
416
-
417
- .spinner-box {
418
- width: 300px;
419
- height: 300px;
420
- display: flex;
421
- justify-content: center;
422
- align-items: center;
423
- background-color: transparent;
424
- }
425
-
426
- /* 軌道スタイル */
427
- .leo {
428
- position: absolute;
429
- display: flex;
430
- justify-content: center;
431
- align-items: center;
432
- border-radius: 50%;
433
- }
434
-
435
- .blue-orbit {
436
- width: 165px;
437
- height: 165px;
438
- border: 1px solid #91daffa5;
439
- animation: spin3D 3s linear .2s infinite;
440
- }
441
-
442
- .green-orbit {
443
- width: 120px;
444
- height: 120px;
445
- border: 1px solid #91ffbfa5;
446
- animation: spin3D 2s linear 0s infinite;
447
- }
448
-
449
- .red-orbit {
450
- width: 90px;
451
- height: 90px;
452
- border: 1px solid #ffca91a5;
453
- animation: spin3D 1s linear 0s infinite;
454
- }
455
-
456
- .white-orbit {
457
- width: 60px;
458
- height: 60px;
459
- border: 2px solid #ffffff;
460
- animation: spin3D 10s linear 0s infinite;
461
- }
462
-
463
- .w1 {
464
- transform: rotate3D(1, 1, 1, 90deg);
465
- }
466
-
467
- .w2 {
468
- transform: rotate3D(1, 2, .5, 90deg);
469
- }
470
-
471
- .w3 {
472
- transform: rotate3D(.5, 1, 2, 90deg);
473
- }
474
-
475
- /* キーフレームアニメーション */
476
- @keyframes spin3D {
477
- from {
478
- transform: rotate3d(.5,.5,.5, 360deg);
479
- }
480
- to {
481
- transform: rotate3d(0,0,0, 0deg);
482
- }
483
- }
484
-
485
- /* フレームプレビュー */
486
- .frame-preview {
487
- position: fixed;
488
- bottom: 30px;
489
- width: 160px;
490
- height: 90px;
491
- background: #000;
492
- border: 2px solid #00aaff;
493
- box-shadow: 0 0 10px rgba(0, 170, 255, 0.7);
494
- display: none;
495
- z-index: 100;
496
- pointer-events: none;
497
- }
498
-
499
- .frame-preview canvas {
500
- width: 100%;
501
- height: 100%;
502
- object-fit: contain;
503
- }
504
-
505
- .frame-time {
506
- position: absolute;
507
- bottom: -25px;
508
- left: 50%;
509
- transform: translateX(-50%);
510
- background: rgba(0, 0, 0, 0.8);
511
- color: #00ccff;
512
- padding: 3px 8px;
513
- border-radius: 4px;
514
- font-size: 12px;
515
- white-space: nowrap;
516
- }
517
-
518
- /* 右クリックメニュー */
519
- .context-menu {
520
- position: fixed;
521
- background-color: #0f0f1a;
522
- border: 1px solid #0066ff;
523
- box-shadow: 0 0 15px rgba(0, 102, 255, 0.5);
524
- z-index: 1000;
525
- display: none;
526
- min-width: 200px;
527
- }
528
-
529
- .context-menu button {
530
- width: 100%;
531
- text-align: left;
532
- padding: 8px 15px;
533
- border: none;
534
- border-bottom: 1px solid #003366;
535
- background: none;
536
- color: #00ccff;
537
- font-family: "M PLUS Rounded 1c", monospace;
538
- cursor: pointer;
539
- }
540
-
541
- .context-menu button:hover {
542
- background-color: #0066ff;
543
- color: #000;
544
- }
545
-
546
- /* 音声/字幕のみモード */
547
- .audio-only-mode {
548
- position: absolute;
549
- top: 10px;
550
- right: 10px;
551
- background: rgba(0, 0, 0, 0.7);
552
- color: #00ccff;
553
- padding: 5px 10px;
554
- border-radius: 4px;
555
- font-size: 12px;
556
- display: none;
557
- z-index: 10;
558
- }
559
-
560
- .audio-only-mode.active {
561
- display: block;
562
- }
563
-
564
- /* リップルエフェクト */
565
- .ripple {
566
- position: absolute;
567
- border-radius: 50%;
568
- background: transparent;
569
- border: 1px solid rgba(100, 210, 255, 0.3);
570
- transform: translate(-50%, -50%);
571
- pointer-events: none;
572
- animation: ripple-animation 4s ease-out forwards;
573
- z-index: -1;
574
- }
575
-
576
- @keyframes ripple-animation {
577
- 0% {
578
- width: 0;
579
- height: 0;
580
- opacity: 0.6;
581
- }
582
- 100% {
583
- width: 600px;
584
- height: 600px;
585
- opacity: 0;
586
- }
587
- }
588
-
589
- /* レスポンシブ対応 */
590
- @media (max-width: 768px) {
591
- .video-container,
592
- .controls,
593
- .new {
594
- max-width: 95%;
595
- }
596
-
597
- .control-group {
598
- flex-direction: column;
599
- align-items: flex-start;
600
- }
601
-
602
- .control-group label {
603
- text-align: left;
604
- margin-bottom: 5px;
605
- }
606
-
607
- select {
608
- width: 100%;
609
- }
610
- }
611
- </style>
612
-
613
  </head>
614
 
615
  <body>
616
- <div class="wave-container">
617
- <div class="grid-lines"></div>
618
- <div class="wave"></div>
619
- <div class="wave"></div>
620
- <div class="wave"></div>
621
- <div class="tech-dots" id="techDots"></div>
622
- </div>
623
- <script>
624
- // ランダムな位置にテクノロジードットを配置
625
- const techDots = document.getElementById('techDots');
626
- const dotCount = 50;
627
-
628
- for (let i = 0; i < dotCount; i++) {
629
- const dot = document.createElement('div');
630
- dot.className = 'tech-dot';
631
- dot.style.left = `${Math.random() * 100}%`;
632
- dot.style.top = `${Math.random() * 100}%`;
633
- dot.style.animationDelay = `${Math.random() * 2}s`;
634
- techDots.appendChild(dot);
635
- }
636
- </script>
637
- <!-- ローディングオーバーレイ -->
638
- <div class="main-content">
639
- <div class="loading-overlay" id="loadingOverlay">
640
- <div class="spinner-box">
641
- <div class="blue-orbit leo">
642
- </div>
643
- <div class="green-orbit leo">
644
- </div>
645
- <div class="red-orbit leo">
646
- </div>
647
- <div class="white-orbit w1 leo">
648
- </div>
649
- <div class="white-orbit w2 leo">
650
- </div>
651
- <div class="white-orbit w3 leo">
652
- </div>
653
- </div>
654
- </div>
655
- <div id="ripple-container">
656
- </div>
657
-
658
- <!-- 音声/字幕のみモード表示 -->
659
- <div class="audio-only-mode" id="audioOnlyModeIndicator">音声/字幕のみモード</div>
660
- <h1>ラジオ体操動画プレイヤー
661
- <br>For Kushihara</h1>
662
- <div id="new">
663
- <h3>新機能</h3>
664
- ・動画のプログレスバーにホバーしたときにそのフレームを表示<br>
665
- ・全画面時に右クリックメニューが表示されるように修正<br>
666
- ・音声/字幕モードを追加<br>
667
- </div>
668
- <div class="controls">
669
- <div class="control-group">
670
- <label for="videoSelect">動画の音量:</label>
671
- <select id="videoSelect">
672
- <option value="v.mp4">小</option>
673
- <option value="v-2.mp4">大(+50dB)</option>
674
- </select>
675
- </div>
676
- <div class="control-group">
677
- <label for="speedRange">再生速度:</label>
678
- <input type="range" id="speedRange" min="0.0001" max="10" step="0.0001" value="1" style="width:700px !important;">
679
- <input type="number" id="speedInput" min="0.0001" step="0.0001" value="1">
680
- </div>
681
- <div class="control-group">
682
- <label for="volumeRange">音量:</label>
683
- <input type="range" id="volumeRange" min="0" max="1" step="0.01" value="1">
684
- <input type="number" id="volumeInput" min="0" max="1" step="0.01" value="1">
685
- </div>
686
- <div class="control-group">
687
- <label for="loopCheckbox">ループ再生:</label>
688
- <input type="checkbox" id="loopCheckbox" checked>
689
- </div>
690
- <!-- 字幕設定セクション -->
691
- <div class="subtitle-settings">
692
- <div class="control-group">
693
- <label for="subtitleToggle">字幕表示:</label>
694
- <input type="checkbox" id="subtitleToggle" checked>
695
- </div>
696
- <div class="control-group">
697
- <label for="subtitleSize">文字サイズ:</label>
698
- <input type="range" id="subtitleSize" min="0.5" max="5" step="0.1" value="1.5">
699
- <input type="number" id="subtitleSizeInput" min="0.01" step="0.01" value="1.5">
700
- </div>
701
- <div class="control-group">
702
- <label for="subtitleTrack">字幕トラック:</label>
703
- <select id="subtitleTrack">
704
- <option value="v.vtt">日本語</option>
705
- <option value="">字幕なし</option>
706
- </select>
707
- </div>
708
- </div>
709
- <div class="control-group">
710
- <button onclick="goFullscreen()">全画面</button>
711
- <button onclick="toggleAudioOnlyMode()">音声/字幕のみモード</button>
712
- </div>
713
- </div>
714
- <div class="video-container">
715
- <video id="videoPlayer" src="v.mp4">
716
- <track id="subtitleTrackElement" kind="subtitles" src="v.vtt" srclang="ja" label="日本語" default>
717
- </track>
718
- </video>
719
- <div class="preview-container" id="previewContainer">
720
- <img id="preview" style="max-width: 200px; max-height: 150px;">
721
- <div class="preview-time" id="previewTime"></div>
722
- </div>
723
- <div class="custom-controls">
724
- <!-- 右クリックメニュー -->
725
- <div class="context-menu" id="contextMenu">
726
- <button onclick="togglePlayPause()">再生/一時停止</button>
727
- <button onclick="toggleMute()">ミュート切り替え</button>
728
- <button onclick="toggleSubtitles()">字幕表示切り替え</button>
729
- <button onclick="toggleAudioOnlyMode()">音声/字幕のみモード</button>
730
- <button onclick="goFullscreen()">全画面表示</button>
731
- </div>
732
- <!-- フレームプレビュー -->
733
- <div class="frame-preview" id="framePreview">
734
- <canvas id="canvas" crossorigin="anonymous" id="previewImage">
735
- <div class="frame-time" id="frameTime">
736
- </div>
737
- </div>
738
- <div class="progress-container" id="progressContainer">
739
- <div class="progress-bar" id="progressBar">
740
- </div>
741
- </div>
742
- <div class="buttons-container">
743
- <div class="left-controls">
744
- <button class="control-btn" id="playPauseBtn">▶</button>
745
- <span class="time-display" id="timeDisplay">00:00 / 00:00</span>
746
- </div>
747
- <div class="right-controls">
748
- <div class="volume-container">
749
- <button class="control-btn" id="volumeBtn">🔊</button>
750
- <input type="range" class="volume-slider" id="volumeSlider" min="0" max="1" step="0.01" value="1">
751
- </div>
752
- <button class="control-btn" id="subtitleBtn" title="字幕">🔤</button>
753
- <button class="control-btn" id="fullscreenBtn">⛶</button>
754
- </div>
755
- </div>
756
- </div>
757
- </div>
758
- <!--<canvas id="canvas" hidden crossorigin="anonymous">
 
 
 
 
 
 
759
  </canvas>-->
760
- <!-- サムネイル用の非表示video要素 -->
761
- <video id="video-for-thumbnail" src="v.mp4" preload="auto" style="display:none;">
762
- </video>
763
- </div>
764
- <script>
765
- const video = document.getElementById('videoPlayer');
766
- const videoSelect = document.getElementById('videoSelect');
767
- const speedRange = document.getElementById('speedRange');
768
- const speedInput = document.getElementById('speedInput');
769
- const volumeRange = document.getElementById('volumeRange');
770
- const volumeInput = document.getElementById('volumeInput');
771
- const loopCheckbox = document.getElementById('loopCheckbox');
772
- const playPauseBtn = document.getElementById('playPauseBtn');
773
- const progressBar = document.getElementById('progressBar');
774
- const progressContainer = document.getElementById('progressContainer');
775
- const timeDisplay = document.getElementById('timeDisplay');
776
- const volumeBtn = document.getElementById('volumeBtn');
777
- const volumeSlider = document.getElementById('volumeSlider');
778
- const fullscreenBtn = document.getElementById('fullscreenBtn');
779
- const subtitleBtn = document.getElementById('subtitleBtn');
780
- const subtitleToggle = document.getElementById('subtitleToggle');
781
- const subtitleSize = document.getElementById('subtitleSize');
782
- const subtitleSizeInput = document.getElementById('subtitleSizeInput');
783
- const subtitleTrack = document.getElementById('subtitleTrack');
784
- const subtitleTrackElement = document.getElementById('subtitleTrackElement');
785
- const videoContainer = document.querySelector('.video-container');
786
- const framePreview = document.getElementById('framePreview');
787
- const previewImage = document.getElementById('previewImage');
788
- const frameTime = document.getElementById('frameTime');
789
- const audioOnlyModeIndicator = document.getElementById('audioOnlyModeIndicator');
790
- const contextMenu = document.getElementById('contextMenu');
791
- const previewContainer = document.getElementById('previewContainer');
792
- const preview = document.getElementById('preview');
793
- const previewTime = document.getElementById('previewTime');
794
- const VideoForThumbnail = document.getElementById('video-for-thumbnail');
795
- const canvas = document.getElementById('canvas');
796
- const ctx = canvas.getContext('2d');
797
-
798
- // 初期設定
799
- video.controls = false;
800
- let isDragging = false;
801
- let subtitlesEnabled = true;
802
- let normalVideoWidth = videoContainer.clientWidth;
803
- let isAudioOnlyMode = false;
804
- let frameCache = {};
805
- let isHoveringProgress = false;
806
- let hoverTimeout;
807
- let videoBlob = null;
808
-
809
- // ローディングアニメーションをフェードアウト
810
- window.addEventListener('load', function() {
811
- setTimeout(function() {
812
- const loadingOverlay = document.getElementById('loadingOverlay');
813
- loadingOverlay.style.opacity = '0';
814
- setTimeout(function() {
815
- loadingOverlay.style.display = 'none';
816
- }, 1000);
817
- }, 1500);
818
-
819
- // 動画をBlobとしてキャッシュ
820
- fetch(video.src)
821
- .then(response => response.blob())
822
- .then(blob => {
823
- videoBlob = blob;
824
- });
825
- });
826
-
827
- // 波紋エフェクトのコードは元のままなので省略...
828
-
829
- function updatePlaybackRate(value) {
830
- const speed = parseFloat(value);
831
- speedInput.value = speed;
832
- speedRange.value = speed;
833
- video.playbackRate = speed;
834
- }
835
-
836
- function updateVolume(value) {
837
- const volume = parseFloat(value);
838
- volumeInput.value = volume;
839
- volumeRange.value = volume;
840
- volumeSlider.value = volume;
841
- video.volume = volume;
842
-
843
- if (volume === 0) {
844
- volumeBtn.textContent = '🔇';
845
- } else if (volume < 0.5) {
846
- volumeBtn.textContent = '🔈';
847
- } else {
848
- volumeBtn.textContent = '🔊';
849
- }
850
- }
851
-
852
- // 動画ソース変更時にサムネイル用動画も更新
853
- function handleVideoChange() {
854
- const selected = videoSelect.value;
855
-
856
- if (selected === 'v-2.mp4') {
857
- const confirmPlay = confirm("この動画は音量が大きいです。あらかじめ、デバイスの音量をある程度下げてください。また、音割れが起きます。再生してもよろしいですか?");
858
- if (!confirmPlay) {
859
- videoSelect.value = video.src.split('/').pop();
860
- return;
861
- }
862
- }
863
-
864
- video.src = selected;
865
- VideoForThumbnail.src = selected;
866
- video.load();
867
- VideoForThumbnail.load();
868
- video.play().then(() => {
869
- playPauseBtn.textContent = '⏸';
870
- }).catch(e => console.log(e));
871
- }
872
-
873
- function togglePlayPause() {
874
- if (video.paused) {
875
- video.play();
876
- playPauseBtn.textContent = '⏸';
877
- } else {
878
- video.pause();
879
- playPauseBtn.textContent = '▶';
880
- }
881
- hideContextMenu();
882
- }
883
-
884
- function updateProgress() {
885
- const percent = (video.currentTime / video.duration) * 100;
886
- progressBar.style.width = `${percent}%`;
887
-
888
- const currentMinutes = Math.floor(video.currentTime / 60);
889
- const currentSeconds = Math.floor(video.currentTime % 60).toString().padStart(2, '0');
890
- const durationMinutes = Math.floor(video.duration / 60);
891
- const durationSeconds = Math.floor(video.duration % 60).toString().padStart(2, '0');
892
-
893
- timeDisplay.textContent = `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`;
894
- }
895
-
896
- function setProgress(e) {
897
- const width = progressContainer.clientWidth;
898
- const clickX = e.offsetX;
899
- const duration = video.duration;
900
- video.currentTime = (clickX / width) * duration;
901
- }
902
-
903
- function toggleMute() {
904
- video.muted = !video.muted;
905
- if (video.muted) {
906
- volumeBtn.textContent = '🔇';
907
- volumeSlider.value = 0;
908
- } else {
909
- updateVolume(video.volume);
910
- }
911
- hideContextMenu();
912
- }
913
-
914
- function handleVolumeChange() {
915
- video.muted = false;
916
- updateVolume(volumeSlider.value);
917
- }
918
-
919
- function goFullscreen() {
920
- if (
921
- document.fullscreenElement ||
922
- document.webkitFullscreenElement ||
923
- document.msFullscreenElement
924
- ) {
925
- // フルスクリーンを解除
926
- if (document.exitFullscreen) {
927
- document.exitFullscreen();
928
- } else if (document.webkitExitFullscreen) {
929
- document.webkitExitFullscreen();
930
- } else if (document.msExitFullscreen) {
931
- document.msExitFullscreen();
932
- }
933
- } else {
934
- // フルスクリーンにする
935
- if (videoContainer.requestFullscreen) {
936
- videoContainer.requestFullscreen();
937
- } else if (videoContainer.webkitRequestFullscreen) {
938
- videoContainer.webkitRequestFullscreen();
939
- } else if (videoContainer.msRequestFullscreen) {
940
- videoContainer.msRequestFullscreen();
941
- }
942
- }
943
- hideContextMenu();
944
- }
945
- function setupFullscreenContextMenu() {
946
- const fullscreenElement = document.fullscreenElement ||
947
- document.webkitFullscreenElement ||
948
- document.msFullscreenElement;
949
-
950
- if (fullscreenElement) {
951
- fullscreenElement.addEventListener('contextmenu', showContextMenu);
952
- }
953
- }
954
- function updateSubtitleScaleForFullscreen() {
955
- if (document.fullscreenElement || document.webkitFullscreenElement ||
956
- document.mozFullScreenElement || document.msFullscreenElement) {
957
- // 全画面モード
958
- const fullscreenWidth = window.innerWidth;
959
- const scaleFactor = fullscreenWidth / normalVideoWidth;
960
- document.documentElement.style.setProperty('--fullscreen-scale', scaleFactor);
961
-
962
- // 全画面要素にイベントリスナーを追加
963
- const fsElement = document.fullscreenElement || document.webkitFullscreenElement ||
964
- document.mozFullScreenElement || document.msFullscreenElement;
965
- fsElement.addEventListener('contextmenu', showContextMenu);
966
- } else {
967
- // 通常モード
968
- document.documentElement.style.setProperty('--fullscreen-scale', 1);
969
- }
970
- }
971
- function setupFramePreview() {
972
- let previewTimeout;
973
-
974
- progressContainer.addEventListener('mousemove', (e) => {
975
- if (!videoBlob || !video.duration) return;
976
-
977
- clearTimeout(previewTimeout);
978
-
979
- // 全画面モードかどうかを判定
980
- const isFullscreen = document.fullscreenElement ||
981
- document.webkitFullscreenElement ||
982
- document.msFullscreenElement;
983
-
984
- // プログレスバーの位置とサイズを取得
985
- const progressRect = progressContainer.getBoundingClientRect();
986
-
987
- // マウス座標を正しく計算(全画面モードに対応)
988
- let clickX;
989
- if (isFullscreen) {
990
- // 全画面モードではe.offsetXが正しくない場合があるので、clientXを使用
991
- clickX = e.clientX - progressRect.left;
992
- } else {
993
- clickX = e.offsetX;
994
- }
995
-
996
- // クリック位置をプログレスバーの範囲内に制限
997
- clickX = Math.max(0, Math.min(clickX, progressRect.width));
998
-
999
- const previewTime = (clickX / progressRect.width) * video.duration;
1000
-
1001
- // 時間表示を更新
1002
- const previewMinutes = Math.floor(previewTime / 60);
1003
- const previewSeconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
1004
- frameTime.textContent = `${previewMinutes}:${previewSeconds}`;
1005
-
1006
- // プレビュー位置を更新(全画面モードに合わせて調整)
1007
- const previewLeft = isFullscreen ?
1008
- (e.clientX - framePreview.offsetWidth / 2) :
1009
- (progressRect.left + clickX - framePreview.offsetWidth / 2);
1010
-
1011
- framePreview.style.left = `${previewLeft}px`;
1012
- framePreview.style.top = isFullscreen ?
1013
- `${progressRect.top - framePreview.offsetHeight - 10}px` :
1014
- `${progressRect.top - framePreview.offsetHeight - 10}px`;
1015
- framePreview.style.display = 'block';
1016
-
1017
- // キャッシュがあればそれを使う
1018
- const cacheKey = Math.floor(previewTime);
1019
- if (frameCache[cacheKey]) {
1020
- return;
1021
- }
1022
-
1023
- // フレームを取得
1024
- VideoForThumbnail.currentTime = previewTime;
1025
- VideoForThumbnail.crossOrigin = 'anonymous';
1026
- VideoForThumbnail.addEventListener('seeked', function() {
1027
- canvas.width = VideoForThumbnail.videoWidth;
1028
- canvas.height = VideoForThumbnail.videoHeight;
1029
- ctx.drawImage(VideoForThumbnail, 0, 0, canvas.width, canvas.height);
1030
- frameCache[cacheKey] = imageData; // キャッシュに保存
1031
- }, { once: true });
1032
- });
1033
-
1034
- progressContainer.addEventListener('mouseleave', () => {
1035
- previewTimeout = setTimeout(() => {
1036
- framePreview.style.display = 'none';
1037
- }, 300);
1038
- });
1039
-
1040
- framePreview.addEventListener('mouseenter', () => {
1041
- clearTimeout(previewTimeout);
1042
- });
1043
-
1044
- framePreview.addEventListener('mouseleave', () => {
1045
- framePreview.style.display = 'none';
1046
- });
1047
- }
1048
- // 字幕関連の関数
1049
- function toggleSubtitles() {
1050
- subtitlesEnabled = !subtitlesEnabled;
1051
- subtitleToggle.checked = subtitlesEnabled;
1052
- subtitleTrackElement.track.mode = subtitlesEnabled ? 'showing' : 'hidden';
1053
- subtitleBtn.style.color = subtitlesEnabled ? '#00ccff' : '#666';
1054
- hideContextMenu();
1055
- }
1056
-
1057
- function updateSubtitleSize(value) {
1058
- const size = parseFloat(value);
1059
- subtitleSizeInput.value = size;
1060
- subtitleSize.value = size;
1061
-
1062
- // 字幕サイズを制御
1063
- document.documentElement.style.setProperty('--subtitle-scale', size);
1064
-
1065
- // VTTCueのlineプロパティには数値のみを設定
1066
- const track = subtitleTrackElement.track;
1067
- if (track && track.cues) {
1068
- for (let i = 0; i < track.cues.length; i++) {
1069
- track.cues[i].line = 90;
1070
- track.cues[i].snapToLines = false;
1071
- }
1072
- }
1073
- }
1074
-
1075
- function changeSubtitleTrack() {
1076
- const selectedTrack = subtitleTrack.value;
1077
- subtitleTrackElement.src = selectedTrack;
1078
- subtitleTrackElement.track.mode = selectedTrack && subtitlesEnabled ? 'showing' : 'hidden';
1079
-
1080
- // トラック変更後に再度読み込み
1081
- video.textTracks[0].mode = 'hidden';
1082
- if (selectedTrack) {
1083
- video.textTracks[0].mode = subtitlesEnabled ? 'showing' : 'hidden';
1084
- }
1085
- }
1086
-
1087
- function toggleSubtitleMenu() {
1088
- document.getElementById('subtitleToggle').checked ^= true;
1089
- toggleSubtitles();
1090
- }
1091
-
1092
- // フレームプレビュー関連
1093
- function showFramePreview(e) {
1094
- if (!videoBlob) return;
1095
-
1096
- const progressRect = progressContainer.getBoundingClientRect();
1097
- const clickX = e.clientX - progressRect.left;
1098
- const duration = video.duration;
1099
- const previewTime = (clickX / progressRect.width) * duration;
1100
-
1101
- // 時間表示を更新
1102
- const previewMinutes = Math.floor(previewTime / 60);
1103
- const previewSeconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
1104
- frameTime.textContent = `${previewMinutes}:${previewSeconds}`;
1105
-
1106
- // プレビュー位置を更新
1107
- framePreview.style.left = `${e.clientX}px`;
1108
- framePreview.style.display = 'block';
1109
-
1110
- // キャッシュがあればそれを使う
1111
- const cacheKey = Math.floor(previewTime);
1112
- if (frameCache[cacheKey]) {
1113
- return;
1114
- }
1115
-
1116
- // フレームを取得
1117
- videoFrames({
1118
- url: URL.createObjectURL(videoBlob),
1119
- count: 1,
1120
- startTime: previewTime,
1121
- endTime: previewTime + 0.1
1122
- }).then((frames) => {
1123
- if (frames.length > 0) {
1124
- frameCache[cacheKey] = frames[0].image; // キャッシュに保存
1125
- }
1126
- }).catch(err => {
1127
- console.error('Error getting video frame:', err);
1128
- });
1129
- }
1130
-
1131
- function hideFramePreview() {
1132
- framePreview.style.display = 'none';
1133
- }
1134
-
1135
- // 右クリックメニュー関連
1136
- function showContextMenu(e) {
1137
- e.preventDefault();
1138
- contextMenu.style.display = 'block';
1139
- contextMenu.style.left = `${e.clientX}px`;
1140
- contextMenu.style.top = `${e.clientY}px`;
1141
- }
1142
-
1143
- function hideContextMenu() {
1144
- contextMenu.style.display = 'none';
1145
- }
1146
-
1147
- // 音声/字幕のみモード
1148
- function toggleAudioOnlyMode() {
1149
- isAudioOnlyMode = !isAudioOnlyMode;
1150
-
1151
- if (isAudioOnlyMode) {
1152
- video.style.opacity = '0';
1153
- audioOnlyModeIndicator.classList.add('active');
1154
- } else {
1155
- video.style.opacity = '1';
1156
- audioOnlyModeIndicator.classList.remove('active');
1157
- }
1158
-
1159
- hideContextMenu();
1160
- }
1161
-
1162
- // イベントリスナー
1163
- videoSelect.addEventListener('change', handleVideoChange);
1164
-
1165
- ['input', 'change', 'mouseup'].forEach(eventName => {
1166
- speedRange.addEventListener(eventName, () => updatePlaybackRate(speedRange.value));
1167
- volumeRange.addEventListener(eventName, () => updateVolume(volumeRange.value));
1168
- subtitleSize.addEventListener(eventName, () => updateSubtitleSize(subtitleSize.value));
1169
- });
1170
-
1171
- speedInput.addEventListener('input', () => updatePlaybackRate(speedInput.value));
1172
- volumeInput.addEventListener('input', () => updateVolume(volumeInput.value));
1173
- subtitleSizeInput.addEventListener('input', () => updateSubtitleSize(subtitleSizeInput.value));
1174
-
1175
- loopCheckbox.addEventListener('change', () => {
1176
- video.loop = loopCheckbox.checked;
1177
- });
1178
-
1179
- subtitleToggle.addEventListener('change', toggleSubtitles);
1180
- subtitleTrack.addEventListener('change', changeSubtitleTrack);
1181
- subtitleBtn.addEventListener('click', toggleSubtitleMenu);
1182
-
1183
- playPauseBtn.addEventListener('click', togglePlayPause);
1184
- video.addEventListener('click', togglePlayPause);
1185
- video.addEventListener('play', () => playPauseBtn.textContent = '⏸');
1186
- video.addEventListener('pause', () => playPauseBtn.textContent = '▶');
1187
- video.addEventListener('timeupdate', updateProgress);
1188
- progressContainer.addEventListener('click', setProgress);
1189
- progressContainer.addEventListener('mousedown', () => isDragging = true);
1190
- document.addEventListener('mouseup', () => isDragging = false);
1191
- // マウスホバー時のプレビュー表示
1192
- progressContainer.addEventListener('mousemove', function(e) {
1193
- if (isDragging) {
1194
- const width = progressContainer.clientWidth;
1195
- const clickX = e.offsetX;
1196
- const duration = video.duration;
1197
- const previewTime = (clickX / width) * duration;
1198
-
1199
- // プレビュー位置を更新
1200
- previewContainer.style.left = `${e.clientX - 100}px`;
1201
- previewContainer.style.bottom = '60px';
1202
- previewContainer.style.display = 'block';
1203
-
1204
- // 時間表示を更新
1205
- const minutes = Math.floor(previewTime / 60);
1206
- const seconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
1207
- document.getElementById('previewTime').textContent = `${minutes}:${seconds}`;
1208
-
1209
- // サムネイル画像を更新
1210
- updateThumbnail(previewTime);
1211
- } else {
1212
- previewContainer.style.display = 'none';
1213
- }
1214
- });
1215
-
1216
- // サムネイル画像更新関数
1217
- function updateThumbnail(time) {
1218
- VideoForThumbnail.currentTime = time;
1219
-
1220
- VideoForThumbnail.addEventListener('seeked', function() {
1221
- canvas.width = VideoForThumbnail.videoWidth;
1222
- canvas.height = VideoForThumbnail.videoHeight;
1223
- ctx.drawImage(VideoForThumbnail, 0, 0, canvas.width, canvas.height);
1224
- preview.src = canvas.toDataURL('image/jpeg');
1225
- }, { once: true });
1226
- }
1227
-
1228
-
1229
- // プログレスバーのホバーイベント
1230
- progressContainer.addEventListener('mouseenter', () => {
1231
- isHoveringProgress = true;
1232
- clearTimeout(hoverTimeout);
1233
- });
1234
-
1235
- progressContainer.addEventListener('mouseleave', () => {
1236
- isHoveringProgress = false;
1237
- hoverTimeout = setTimeout(() => {
1238
- if (!isDragging) hideFramePreview();
1239
- }, 300);
1240
- });
1241
-
1242
- volumeBtn.addEventListener('click', toggleMute);
1243
- volumeSlider.addEventListener('input', handleVolumeChange);
1244
- fullscreenBtn.addEventListener('click', goFullscreen);
1245
-
1246
- // 全画面変更イベントを監視
1247
- document.addEventListener('fullscreenchange', handleFullscreenChange);
1248
- document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
1249
- document.addEventListener('mozfullscreenchange', handleFullscreenChange);
1250
- document.addEventListener('MSFullscreenChange', handleFullscreenChange);
1251
-
1252
- // 右クリックメニューイベント
1253
- videoContainer.addEventListener('contextmenu', showContextMenu);
1254
- document.addEventListener('click', hideContextMenu);
1255
- document.addEventListener('keydown', (e) => {
1256
- if (e.key === 'Escape') hideContextMenu();
1257
- });
1258
-
1259
- video.addEventListener('loadedmetadata', () => {
1260
- updatePlaybackRate(speedRange.value);
1261
- updateVolume(volumeRange.value);
1262
- updateSubtitleSize(subtitleSize.value);
1263
- video.loop = loopCheckbox.checked;
1264
- toggleSubtitles();
1265
- updateProgress();
1266
- normalVideoWidth = videoContainer.clientWidth;
1267
- });
1268
- video.addEventListener("loadeddata", async () => {
1269
- const response = await fetch(video.src);
1270
- videoBlob = await response.blob();
1271
- });
1272
-
1273
- // 保存
1274
- video.addEventListener('timeupdate', () => {
1275
- localStorage.setItem('radioTaisoTime', video.currentTime);
1276
- });
1277
-
1278
- // 復元
1279
- window.addEventListener('load', () => {
1280
- setupFramePreview();
1281
- setupFullscreenContextMenu();
1282
- const savedTime = parseFloat(localStorage.getItem('radioTaisoTime'));
1283
- if (!isNaN(savedTime)) {
1284
- video.currentTime = savedTime;
1285
- }
1286
-
1287
- document.addEventListener('fullscreenchange', () => {
1288
- updateSubtitleScaleForFullscreen();
1289
- setupFullscreenContextMenu();
1290
- });
1291
- document.addEventListener('webkitfullscreenchange', () => {
1292
- updateSubtitleScaleForFullscreen();
1293
- setupFullscreenContextMenu();
1294
- });
1295
- document.addEventListener('mozfullscreenchange', () => {
1296
- updateSubtitleScaleForFullscreen();
1297
- setupFullscreenContextMenu();
1298
- });
1299
- document.addEventListener('MSFullscreenChange', () => {
1300
- updateSubtitleScaleForFullscreen();
1301
- setupFullscreenContextMenu();
1302
- });
1303
- });
1304
- document.addEventListener('keydown', (e) => {
1305
- if (e.target.tagName === 'INPUT') return; // 入力中は無視
1306
- switch (e.key.toLowerCase()) {
1307
- case ' ': e.preventDefault(); togglePlayPause(); break;
1308
- case 'f': goFullscreen(); break;
1309
- case 'm': toggleMute(); break;
1310
- case 'arrowright': video.currentTime += 5; break;
1311
- case 'arrowleft': video.currentTime -= 5; break;
1312
- }
1313
- });
1314
-
1315
- document.addEventListener('fullscreenchange', handleFullscreenChange);
1316
- document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
1317
- document.addEventListener('mozfullscreenchange', handleFullscreenChange);
1318
- document.addEventListener('MSFullscreenChange', handleFullscreenChange);
1319
-
1320
- function handleFullscreenChange() {
1321
- updateSubtitleScaleForFullscreen();
1322
- setupFullscreenContextMenu();
1323
- normalVideoWidth = videoContainer.clientWidth;
1324
- setupFramePreview(); // フレームプレビューを再設定
1325
- }
1326
-
1327
- // CSS変数を設定
1328
- document.documentElement.style.setProperty('--subtitle-scale', '1');
1329
- document.documentElement.style.setProperty('--subtitle-border-radius', '10px');
1330
- document.documentElement.style.setProperty('--fullscreen-scale', '1');
1331
- </script>
1332
  </body>
1333
 
1334
  </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
+ /* リセットと基本設定 */
14
+ html, body {
15
+ margin: 0;
16
+ padding: 0;
17
+ width: 100%;
18
+ min-height: 100vh;
19
+ background-color: #0a192f;
20
+ color: #00ffcc;
21
+ font-family: "M PLUS Rounded 1c", monospace;
22
+ display: flex;
23
+ flex-direction: column;
24
+ align-items: center;
25
+ overflow-x: hidden;
26
+ position: relative;
27
+ }
28
+
29
+ /* 背景アニメーションコンテナ */
30
+ .wave-container {
31
+ position: fixed;
32
+ top: 0;
33
+ left: 0;
34
+ width: 100vw;
35
+ height: 100vh;
36
+ z-index: -1; /* 他の要素より背面に */
37
+ overflow: hidden;
38
+ }
39
+
40
+ /* グリッド線 */
41
+ .grid-lines {
42
+ position: absolute;
43
+ top: 0;
44
+ left: 0;
45
+ width: 100%;
46
+ height: 100%;
47
+ background-image:
48
+ linear-gradient(rgba(100, 200, 255, 0.1) 1px, transparent 1px),
49
+ linear-gradient(90deg, rgba(100, 200, 255, 0.1) 1px, transparent 1px);
50
+ background-size: 50px 50px;
51
+ z-index: 1;
52
+ }
53
+ /* 波のアニメーション */
54
+ .wave {
55
+ position: absolute;
56
+ top: 0;
57
+ left: 0;
58
+ width: 100%;
59
+ height: 100%;
60
+ background: linear-gradient(
61
+ to bottom,
62
+ transparent,
63
+ rgba(100, 200, 255, 0.1) 20%,
64
+ rgba(100, 200, 255, 0.3) 50%,
65
+ rgba(100, 200, 255, 0.1) 80%,
66
+ transparent
67
+ );
68
+ opacity: 0.7;
69
+ animation: waveFlow 8s linear infinite;
70
+ z-index: 2;
71
+ }
72
+
73
+ .wave:nth-child(2) {
74
+ animation-delay: -2s;
75
+ opacity: 0.5;
76
+ }
77
+
78
+ .wave:nth-child(3) {
79
+ animation-delay: -4s;
80
+ opacity: 0.3;
81
+ }
82
+
83
+ @keyframes waveFlow {
84
+ 0% {
85
+ transform: translateY(-100%);
86
+ }
87
+ 100% {
88
+ transform: translateY(100%);
89
+ }
90
+ }
91
+
92
+ /* テクノロジードット */
93
+ .tech-dots {
94
+ position: absolute;
95
+ top: 0;
96
+ left: 0;
97
+ width: 100%;
98
+ height: 100%;
99
+ z-index: 3;
100
+ }
101
+
102
+ .tech-dot {
103
+ position: absolute;
104
+ width: 3px;
105
+ height: 3px;
106
+ background-color: rgba(100, 200, 255, 0.7);
107
+ border-radius: 50%;
108
+ animation: blink 2s infinite alternate;
109
+ }
110
+
111
+ @keyframes blink {
112
+ 0% { opacity: 0.2; }
113
+ 100% { opacity: 0.8; }
114
+ }
115
+
116
+ /* メインコンテンツ */
117
+ .main-content {
118
+ position: relative;
119
+ z-index: 10;
120
+ width: 100%;
121
+ max-width: 1200px;
122
+ padding: 20px;
123
+ box-sizing: border-box;
124
+ display: flex;
125
+ flex-direction: column;
126
+ align-items: center;
127
+ }
128
+
129
+ /* ヘッダー */
130
+ h1 {
131
+ color: #00aaff;
132
+ text-shadow: 0 0 5px #0066ff;
133
+ border-bottom: 1px solid #0066ff;
134
+ padding-bottom: 10px;
135
+ text-align: center;
136
+ margin-top: 20px;
137
+ }
138
+
139
+ /* ビデオコンテナ */
140
+ .video-container {
141
+ position: relative;
142
+ max-width: 800px;
143
+ width: 100%;
144
+ margin: 30px 0 20px;
145
+ border: 2px solid #0066ff;
146
+ box-shadow: 0 0 15px rgba(0, 102, 255, 0.5);
147
+ background: #000;
148
+ }
149
+
150
+ video {
151
+ width: 100%;
152
+ display: block;
153
+ }
154
+
155
+ /* 字幕スタイル */
156
+ video::cue {
157
+ background-color: rgba(0, 0, 0, 0.7) !important;
158
+ color: #c7dbed !important;
159
+ font-family: "M PLUS Rounded 1c", monospace !important;
160
+ text-shadow: 1px 1px 2px #000 !important;
161
+ outline: 3px solid #0b3e8f !important;
162
+ border-radius: 10px !important;
163
+ }
164
+
165
+ /* カスタム動画コントロール */
166
+ video::-webkit-media-controls {
167
+ display: none !important;
168
+ }
169
+
170
+ .custom-controls {
171
+ position: absolute;
172
+ bottom: 0;
173
+ left: 0;
174
+ right: 0;
175
+ padding: 10px;
176
+ display: flex;
177
+ flex-direction: column;
178
+ opacity: 0;
179
+ transition: opacity 0.3s;
180
+ }
181
+
182
+ .video-container:hover .custom-controls {
183
+ opacity: 1;
184
+ }
185
+
186
+ .progress-container {
187
+ width: 100%;
188
+ height: 8px;
189
+ background: #001133;
190
+ margin-bottom: 10px;
191
+ cursor: pointer;
192
+ position: relative;
193
+ }
194
+
195
+ .progress-bar {
196
+ height: 100%;
197
+ background: #00aaff;
198
+ width: 0%;
199
+ position: relative;
200
+ }
201
+
202
+ .progress-bar::after {
203
+ content: '';
204
+ position: absolute;
205
+ right: -5px;
206
+ top: 50%;
207
+ transform: translateY(-50%);
208
+ width: 10px;
209
+ height: 10px;
210
+ background: #00ccff;
211
+ border-radius: 50%;
212
+ box-shadow: 0 0 5px #00ccff;
213
+ }
214
+
215
+ .buttons-container {
216
+ display: flex;
217
+ align-items: center;
218
+ justify-content: space-between;
219
+ }
220
+
221
+ .left-controls, .right-controls {
222
+ display: flex;
223
+ align-items: center;
224
+ gap: 15px;
225
+ }
226
+
227
+ .control-btn {
228
+ background: none;
229
+ border: none;
230
+ color: #00ccff;
231
+ font-size: 16px;
232
+ cursor: pointer;
233
+ transition: all 0.3s;
234
+ }
235
+
236
+ .control-btn:hover {
237
+ color: #00ffcc;
238
+ text-shadow: 0 0 5px #00ffcc;
239
+ }
240
+
241
+ .time-display {
242
+ font-size: 14px;
243
+ color: #00aaff;
244
+ box-shadow: 0.1px 0.1px 0.1px black;
245
+ font-family: "M PLUS Rounded 1c", monospace;
246
+ }
247
+
248
+ .volume-container {
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 5px;
252
+ }
253
+
254
+ .volume-slider {
255
+ width: 80px;
256
+ -webkit-appearance: none;
257
+ height: 4px;
258
+ background: #001133;
259
+ outline: none;
260
+ }
261
+
262
+ .volume-slider::-webkit-slider-thumb {
263
+ -webkit-appearance: none;
264
+ width: 12px;
265
+ height: 12px;
266
+ background: #00aaff;
267
+ border-radius: 50%;
268
+ cursor: pointer;
269
+ }
270
+
271
+ /* コントロールパネル */
272
+ .controls, .new {
273
+ width: 100%;
274
+ max-width: 800px;
275
+ background-color: #0f0f1a;
276
+ padding: 20px;
277
+ border: 1px solid #0066ff;
278
+ box-shadow: 0 0 15px rgba(0, 102, 255, 0.3);
279
+ margin-bottom: 20px;
280
+ }
281
+
282
+ .control-group {
283
+ display: flex;
284
+ flex-direction: row;
285
+ align-items: center;
286
+ justify-content: flex-start;
287
+ gap: 10px;
288
+ flex-wrap: nowrap;
289
+ }
290
+
291
+ .control-group label {
292
+ white-space: nowrap;
293
+ min-width: 100px;
294
+ text-align: right;
295
+ color: #00ccff;
296
+ }
297
+
298
+ input[type="range"] {
299
+ flex-grow: 1;
300
+ -webkit-appearance: none;
301
+ height: 8px;
302
+ background: #001133;
303
+ border-radius: 5px;
304
+ outline: none;
305
+ }
306
+
307
+ input[type="range"]::-webkit-slider-thumb {
308
+ -webkit-appearance: none;
309
+ width: 18px;
310
+ height: 18px;
311
+ background: #00aaff;
312
+ border-radius: 50%;
313
+ cursor: pointer;
314
+ box-shadow: 0 0 5px #00aaff;
315
+ }
316
+
317
+ input[type="number"], select {
318
+ background-color: #001133;
319
+ color: #00ccff;
320
+ border: 1px solid #0066ff;
321
+ padding: 5px;
322
+ font-family: "M PLUS Rounded 1c", monospace;
323
+ }
324
+
325
+ button {
326
+ background-color: #001133;
327
+ color: #00ccff;
328
+ border: 1px solid #0066ff;
329
+ padding: 8px 15px;
330
+ cursor: pointer;
331
+ font-family: "M PLUS Rounded 1c", monospace;
332
+ transition: all 0.3s;
333
+ align-self: flex-start;
334
+ }
335
+
336
+ button:hover {
337
+ background-color: #0066ff;
338
+ color: #000;
339
+ box-shadow: 0 0 10px #0066ff;
340
+ }
341
+
342
+ select {
343
+ width: 300px;
344
+ background-color: #001133;
345
+ color: #00ccff;
346
+ border: 1px solid #0066ff;
347
+ padding: 5px;
348
+ }
349
+
350
+ input[type="checkbox"] {
351
+ -webkit-appearance: none;
352
+ width: 18px;
353
+ height: 18px;
354
+ background: #001133;
355
+ border: 1px solid #0066ff;
356
+ position: relative;
357
+ }
358
+
359
+ input[type="checkbox"]:checked {
360
+ background: #0066ff;
361
+ box-shadow: 0 0 5px #0066ff;
362
+ }
363
+
364
+ input[type="checkbox"]:checked::after {
365
+ content: "✓";
366
+ position: absolute;
367
+ color: #000;
368
+ font-size: 14px;
369
+ top: 50%;
370
+ left: 50%;
371
+ transform: translate(-50%, -50%);
372
+ }
373
+
374
+ /* 字幕設定用スタイル */
375
+ .subtitle-settings {
376
+ margin-top: 10px;
377
+ padding: 10px;
378
+ background-color: rgba(0, 20, 40, 0.5);
379
+ border: 1px solid #0066ff;
380
+ }
381
+
382
+ /* 字幕サイズ調整用のCSS変数 */
383
+ :root {
384
+ --subtitle-scale: 1;
385
+ --subtitle-border-radius: 10px;
386
+ }
387
+
388
+ video::cue {
389
+ font-size: calc(16px * var(--subtitle-scale)) !important;
390
+ line-height: 1.5 !important;
391
+ border-radius: var(--subtitle-border-radius) !important;
392
+ }
393
+
394
+ /* 全画面時の字幕サイズ調整 */
395
+ .video-container:fullscreen video::cue,
396
+ .video-container:-webkit-full-screen video::cue,
397
+ .video-container:-moz-full-screen video::cue,
398
+ .video-container:-ms-fullscreen video::cue {
399
+ font-size: calc(16px * var(--subtitle-scale) * var(--fullscreen-scale, 1)) !important;
400
+ }
401
+
402
+ /* ローディングアニメーション */
403
+ .loading-overlay {
404
+ position: fixed;
405
+ top: 0;
406
+ left: 0;
407
+ width: 100%;
408
+ height: 100%;
409
+ background-color: rgba(0, 0, 0, 0.8);
410
+ display: flex;
411
+ justify-content: center;
412
+ align-items: center;
413
+ z-index: 9999;
414
+ transition: opacity 1s ease-out;
415
+ }
416
+
417
+ .spinner-box {
418
+ width: 300px;
419
+ height: 300px;
420
+ display: flex;
421
+ justify-content: center;
422
+ align-items: center;
423
+ background-color: transparent;
424
+ }
425
+
426
+ /* 軌道スタイル */
427
+ .leo {
428
+ position: absolute;
429
+ display: flex;
430
+ justify-content: center;
431
+ align-items: center;
432
+ border-radius: 50%;
433
+ }
434
+
435
+ .blue-orbit {
436
+ width: 165px;
437
+ height: 165px;
438
+ border: 1px solid #91daffa5;
439
+ animation: spin3D 3s linear .2s infinite;
440
+ }
441
+
442
+ .green-orbit {
443
+ width: 120px;
444
+ height: 120px;
445
+ border: 1px solid #91ffbfa5;
446
+ animation: spin3D 2s linear 0s infinite;
447
+ }
448
+
449
+ .red-orbit {
450
+ width: 90px;
451
+ height: 90px;
452
+ border: 1px solid #ffca91a5;
453
+ animation: spin3D 1s linear 0s infinite;
454
+ }
455
+
456
+ .white-orbit {
457
+ width: 60px;
458
+ height: 60px;
459
+ border: 2px solid #ffffff;
460
+ animation: spin3D 10s linear 0s infinite;
461
+ }
462
+
463
+ .w1 {
464
+ transform: rotate3D(1, 1, 1, 90deg);
465
+ }
466
+
467
+ .w2 {
468
+ transform: rotate3D(1, 2, .5, 90deg);
469
+ }
470
+
471
+ .w3 {
472
+ transform: rotate3D(.5, 1, 2, 90deg);
473
+ }
474
+
475
+ /* キーフレームアニメーション */
476
+ @keyframes spin3D {
477
+ from {
478
+ transform: rotate3d(.5,.5,.5, 360deg);
479
+ }
480
+ to {
481
+ transform: rotate3d(0,0,0, 0deg);
482
+ }
483
+ }
484
+
485
+ /* フレームプレビュー */
486
+ .frame-preview {
487
+ position: fixed;
488
+ bottom: 30px;
489
+ width: 160px;
490
+ height: 90px;
491
+ background: #000;
492
+ border: 2px solid #00aaff;
493
+ box-shadow: 0 0 10px rgba(0, 170, 255, 0.7);
494
+ display: none;
495
+ z-index: 100;
496
+ pointer-events: none;
497
+ }
498
+
499
+ .frame-preview canvas {
500
+ width: 100%;
501
+ height: 100%;
502
+ object-fit: contain;
503
+ }
504
+
505
+ .frame-time {
506
+ position: absolute;
507
+ bottom: -25px;
508
+ left: 50%;
509
+ transform: translateX(-50%);
510
+ background: rgba(0, 0, 0, 0.8);
511
+ color: #00ccff;
512
+ padding: 3px 8px;
513
+ border-radius: 4px;
514
+ font-size: 12px;
515
+ white-space: nowrap;
516
+ }
517
+
518
+ /* 右クリックメニュー */
519
+ .context-menu {
520
+ position: fixed;
521
+ background-color: #0f0f1a;
522
+ border: 1px solid #0066ff;
523
+ box-shadow: 0 0 15px rgba(0, 102, 255, 0.5);
524
+ z-index: 1000;
525
+ display: none;
526
+ min-width: 200px;
527
+ }
528
+
529
+ .context-menu button {
530
+ width: 100%;
531
+ text-align: left;
532
+ padding: 8px 15px;
533
+ border: none;
534
+ border-bottom: 1px solid #003366;
535
+ background: none;
536
+ color: #00ccff;
537
+ font-family: "M PLUS Rounded 1c", monospace;
538
+ cursor: pointer;
539
+ }
540
+
541
+ .context-menu button:hover {
542
+ background-color: #0066ff;
543
+ color: #000;
544
+ }
545
+
546
+ /* 音声/字幕のみモード */
547
+ .audio-only-mode {
548
+ position: absolute;
549
+ top: 10px;
550
+ right: 10px;
551
+ background: rgba(0, 0, 0, 0.7);
552
+ color: #00ccff;
553
+ padding: 5px 10px;
554
+ border-radius: 4px;
555
+ font-size: 12px;
556
+ display: none;
557
+ z-index: 10;
558
+ }
559
+
560
+ .audio-only-mode.active {
561
+ display: block;
562
+ }
563
+
564
+ /* リップルエフェクト */
565
+ .ripple {
566
+ position: absolute;
567
+ border-radius: 50%;
568
+ background: transparent;
569
+ border: 1px solid rgba(100, 210, 255, 0.3);
570
+ transform: translate(-50%, -50%);
571
+ pointer-events: none;
572
+ animation: ripple-animation 4s ease-out forwards;
573
+ z-index: -1;
574
+ }
575
+
576
+ @keyframes ripple-animation {
577
+ 0% {
578
+ width: 0;
579
+ height: 0;
580
+ opacity: 0.6;
581
+ }
582
+ 100% {
583
+ width: 600px;
584
+ height: 600px;
585
+ opacity: 0;
586
+ }
587
+ }
588
+
589
+ /* レスポンシブ対応 */
590
+ @media (max-width: 768px) {
591
+ .video-container,
592
+ .controls,
593
+ .new {
594
+ max-width: 95%;
595
+ }
596
+
597
+ .control-group {
598
+ flex-direction: column;
599
+ align-items: flex-start;
600
+ }
601
+
602
+ .control-group label {
603
+ text-align: left;
604
+ margin-bottom: 5px;
605
+ }
606
+
607
+ select {
608
+ width: 100%;
609
+ }
610
+ }
611
+ </style>
 
612
  </head>
613
 
614
  <body>
615
+ <div class="wave-container">
616
+ <div class="grid-lines">
617
+ </div>
618
+ <div class="wave">
619
+ </div>
620
+ <div class="wave">
621
+ </div>
622
+ <div class="wave">
623
+ </div>
624
+ <div class="tech-dots" id="techDots">
625
+ </div>
626
+ </div>
627
+ <script>
628
+ // ランダムな位置にテクノロジードットを配置
629
+ const techDots = document.getElementById('techDots');
630
+ const dotCount = 50;
631
+
632
+ for (let i = 0; i < dotCount; i++) {
633
+ const dot = document.createElement('div');
634
+ dot.className = 'tech-dot';
635
+ dot.style.left = `${Math.random() * 100}%`;
636
+ dot.style.top = `${Math.random() * 100}%`;
637
+ dot.style.animationDelay = `${Math.random() * 2}s`;
638
+ techDots.appendChild(dot);
639
+ }
640
+ </script>
641
+ <!-- ローディングオーバーレイ -->
642
+ <div class="main-content">
643
+ <div class="loading-overlay" id="loadingOverlay">
644
+ <div class="spinner-box">
645
+ <div class="blue-orbit leo">
646
+ </div>
647
+ <div class="green-orbit leo">
648
+ </div>
649
+ <div class="red-orbit leo">
650
+ </div>
651
+ <div class="white-orbit w1 leo">
652
+ </div>
653
+ <div class="white-orbit w2 leo">
654
+ </div>
655
+ <div class="white-orbit w3 leo">
656
+ </div>
657
+ </div>
658
+ </div>
659
+ <div id="ripple-container">
660
+ </div>
661
+ <!-- 音声/字幕のみモード表示 -->
662
+ <div class="audio-only-mode" id="audioOnlyModeIndicator">音声/字幕のみモード</div>
663
+ <h1>ラジオ体操動画プレイヤー
664
+ <br>For Kushihara</h1>
665
+ <div id="new">
666
+ <h3>新機能</h3>・動画のプログレスバーにホバーしたときにそのフレームを表示
667
+ <br>・全画面時に右クリックメニューが表示されるように修正
668
+ <br>・音声/字幕モードを追加
669
+ <br>
670
+ </div>
671
+ <div class="controls">
672
+ <div class="control-group">
673
+ <label for="videoSelect">動画の音量:</label>
674
+ <select id="videoSelect">
675
+ <option value="v.mp4">小</option>
676
+ <option value="v-2.mp4">大(+50dB)</option>
677
+ </select>
678
+ </div>
679
+ <div class="control-group">
680
+ <label for="speedRange">再生速度:</label>
681
+ <input type="range" id="speedRange" min="0.0001" max="10" step="0.0001" value="1" style="width:700px !important;">
682
+ <input type="number" id="speedInput" min="0.0001" step="0.0001" value="1">
683
+ </div>
684
+ <div class="control-group">
685
+ <label for="volumeRange">音量:</label>
686
+ <input type="range" id="volumeRange" min="0" max="1" step="0.01" value="1">
687
+ <input type="number" id="volumeInput" min="0" max="1" step="0.01" value="1">
688
+ </div>
689
+ <div class="control-group">
690
+ <label for="loopCheckbox">ループ再生:</label>
691
+ <input type="checkbox" id="loopCheckbox" checked>
692
+ </div>
693
+ <!-- 字幕設定セクション -->
694
+ <div class="subtitle-settings">
695
+ <div class="control-group">
696
+ <label for="subtitleToggle">字幕表示:</label>
697
+ <input type="checkbox" id="subtitleToggle" checked>
698
+ </div>
699
+ <div class="control-group">
700
+ <label for="subtitleSize">文字サイズ:</label>
701
+ <input type="range" id="subtitleSize" min="0.5" max="5" step="0.1" value="1.5">
702
+ <input type="number" id="subtitleSizeInput" min="0.01" step="0.01" value="1.5">
703
+ </div>
704
+ <div class="control-group">
705
+ <label for="subtitleTrack">字幕トラック:</label>
706
+ <select id="subtitleTrack">
707
+ <option value="v.vtt">日本語</option>
708
+ <option value="">字幕なし</option>
709
+ </select>
710
+ </div>
711
+ </div>
712
+ <div class="control-group">
713
+ <button onclick="goFullscreen()">全画面</button>
714
+ <button onclick="toggleAudioOnlyMode()">音声/字幕のみモード</button>
715
+ </div>
716
+ </div>
717
+ <div class="video-container">
718
+ <video id="videoPlayer" src="v.mp4">
719
+ <track id="subtitleTrackElement" kind="subtitles" src="v.vtt" srclang="ja" label="日本語" default>
720
+ </track>
721
+ </video>
722
+ <div class="preview-container" id="previewContainer">
723
+ <img id="preview" style="max-width: 200px; max-height: 150px;">
724
+ <div class="preview-time" id="previewTime">
725
+ </div>
726
+ </div>
727
+ <div class="custom-controls">
728
+ <!-- 右クリックメニュー -->
729
+ <div class="context-menu" id="contextMenu">
730
+ <button onclick="togglePlayPause()">再生/一時停止</button>
731
+ <button onclick="toggleMute()">ミュート切り替え</button>
732
+ <button onclick="toggleSubtitles()">字幕表示切り替え</button>
733
+ <button onclick="toggleAudioOnlyMode()">音声/字幕のみモード</button>
734
+ <button onclick="goFullscreen()">全画面表示</button>
735
+ </div>
736
+ <!-- フレームプレビュー -->
737
+ <div class="frame-preview" id="framePreview">
738
+ <canvas id="canvas" crossorigin="anonymous">
739
+ <div class="frame-time" id="frameTime">
740
+ </div>
741
+ </canvas>
742
+ </div>
743
+ <div class="progress-container" id="progressContainer">
744
+ <div class="progress-bar" id="progressBar">
745
+ </div>
746
+ </div>
747
+ <div class="buttons-container">
748
+ <div class="left-controls">
749
+ <button class="control-btn" id="playPauseBtn">▶</button>
750
+ <span class="time-display" id="timeDisplay">00:00 / 00:00</span>
751
+ </div>
752
+ <div class="right-controls">
753
+ <div class="volume-container">
754
+ <button class="control-btn" id="volumeBtn">🔊</button>
755
+ <input type="range" class="volume-slider" id="volumeSlider" min="0" max="1" step="0.01" value="1">
756
+ </div>
757
+ <button class="control-btn" id="subtitleBtn" title="字幕">🔤</button>
758
+ <button class="control-btn" id="fullscreenBtn">⛶</button>
759
+ </div>
760
+ </div>
761
+ </div>
762
+ </div>
763
+ <!--<canvas id="canvas" hidden crossorigin="anonymous">
764
  </canvas>-->
765
+ <!-- サムネイル用の非表示video要素 -->
766
+ <video id="video-for-thumbnail" src="v.mp4" preload="auto" style="display:none;">
767
+ </video>
768
+ </div>
769
+ <script>
770
+ const video = document.getElementById('videoPlayer');
771
+ const videoSelect = document.getElementById('videoSelect');
772
+ const speedRange = document.getElementById('speedRange');
773
+ const speedInput = document.getElementById('speedInput');
774
+ const volumeRange = document.getElementById('volumeRange');
775
+ const volumeInput = document.getElementById('volumeInput');
776
+ const loopCheckbox = document.getElementById('loopCheckbox');
777
+ const playPauseBtn = document.getElementById('playPauseBtn');
778
+ const progressBar = document.getElementById('progressBar');
779
+ const progressContainer = document.getElementById('progressContainer');
780
+ const timeDisplay = document.getElementById('timeDisplay');
781
+ const volumeBtn = document.getElementById('volumeBtn');
782
+ const volumeSlider = document.getElementById('volumeSlider');
783
+ const fullscreenBtn = document.getElementById('fullscreenBtn');
784
+ const subtitleBtn = document.getElementById('subtitleBtn');
785
+ const subtitleToggle = document.getElementById('subtitleToggle');
786
+ const subtitleSize = document.getElementById('subtitleSize');
787
+ const subtitleSizeInput = document.getElementById('subtitleSizeInput');
788
+ const subtitleTrack = document.getElementById('subtitleTrack');
789
+ const subtitleTrackElement = document.getElementById('subtitleTrackElement');
790
+ const videoContainer = document.querySelector('.video-container');
791
+ const framePreview = document.getElementById('framePreview');
792
+ const previewImage = document.getElementById('previewImage');
793
+ const frameTime = document.getElementById('frameTime');
794
+ const audioOnlyModeIndicator = document.getElementById('audioOnlyModeIndicator');
795
+ const contextMenu = document.getElementById('contextMenu');
796
+ const previewContainer = document.getElementById('previewContainer');
797
+ const preview = document.getElementById('preview');
798
+ const previewTime = document.getElementById('previewTime');
799
+ const VideoForThumbnail = document.getElementById('video-for-thumbnail');
800
+ const canvas = document.getElementById('canvas');
801
+ const ctx = canvas.getContext('2d');
802
+
803
+ // 初期設定
804
+ video.controls = false;
805
+ let isDragging = false;
806
+ let subtitlesEnabled = true;
807
+ let normalVideoWidth = videoContainer.clientWidth;
808
+ let isAudioOnlyMode = false;
809
+ let frameCache = {};
810
+ let isHoveringProgress = false;
811
+ let hoverTimeout;
812
+ let videoBlob = null;
813
+
814
+ // ローディングアニメーションをフェードアウト
815
+ window.addEventListener('load', function() {
816
+ setTimeout(function() {
817
+ const loadingOverlay = document.getElementById('loadingOverlay');
818
+ loadingOverlay.style.opacity = '0';
819
+ setTimeout(function() {
820
+ loadingOverlay.style.display = 'none';
821
+ }, 1000);
822
+ }, 1500);
823
+
824
+ // 動画をBlobとしてキャッシュ
825
+ fetch(video.src)
826
+ .then(response => response.blob())
827
+ .then(blob => {
828
+ videoBlob = blob;
829
+ });
830
+ });
831
+
832
+ // 波紋エフェクトのコードは元のままなので省略...
833
+
834
+ function updatePlaybackRate(value) {
835
+ const speed = parseFloat(value);
836
+ speedInput.value = speed;
837
+ speedRange.value = speed;
838
+ video.playbackRate = speed;
839
+ }
840
+
841
+ function updateVolume(value) {
842
+ const volume = parseFloat(value);
843
+ volumeInput.value = volume;
844
+ volumeRange.value = volume;
845
+ volumeSlider.value = volume;
846
+ video.volume = volume;
847
+
848
+ if (volume === 0) {
849
+ volumeBtn.textContent = '🔇';
850
+ } else if (volume < 0.5) {
851
+ volumeBtn.textContent = '🔈';
852
+ } else {
853
+ volumeBtn.textContent = '🔊';
854
+ }
855
+ }
856
+
857
+ // 動画ソース変更時にサムネイル用動画も更新
858
+ function handleVideoChange() {
859
+ const selected = videoSelect.value;
860
+
861
+ if (selected === 'v-2.mp4') {
862
+ const confirmPlay = confirm("この動画は音量が大きいです。あらかじめ、デバイスの音量をある程度下げてください。また、音割れが起きます。再生してもよろしいですか?");
863
+ if (!confirmPlay) {
864
+ videoSelect.value = video.src.split('/').pop();
865
+ return;
866
+ }
867
+ }
868
+
869
+ video.src = selected;
870
+ VideoForThumbnail.src = selected;
871
+ video.load();
872
+ VideoForThumbnail.load();
873
+ video.play().then(() => {
874
+ playPauseBtn.textContent = '⏸';
875
+ }).catch(e => console.log(e));
876
+ }
877
+
878
+ function togglePlayPause() {
879
+ if (video.paused) {
880
+ video.play();
881
+ playPauseBtn.textContent = '⏸';
882
+ } else {
883
+ video.pause();
884
+ playPauseBtn.textContent = '▶';
885
+ }
886
+ hideContextMenu();
887
+ }
888
+
889
+ function updateProgress() {
890
+ const percent = (video.currentTime / video.duration) * 100;
891
+ progressBar.style.width = `${percent}%`;
892
+
893
+ const currentMinutes = Math.floor(video.currentTime / 60);
894
+ const currentSeconds = Math.floor(video.currentTime % 60).toString().padStart(2, '0');
895
+ const durationMinutes = Math.floor(video.duration / 60);
896
+ const durationSeconds = Math.floor(video.duration % 60).toString().padStart(2, '0');
897
+
898
+ timeDisplay.textContent = `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`;
899
+ }
900
+
901
+ function setProgress(e) {
902
+ const width = progressContainer.clientWidth;
903
+ const clickX = e.offsetX;
904
+ const duration = video.duration;
905
+ video.currentTime = (clickX / width) * duration;
906
+ }
907
+
908
+ function toggleMute() {
909
+ video.muted = !video.muted;
910
+ if (video.muted) {
911
+ volumeBtn.textContent = '🔇';
912
+ volumeSlider.value = 0;
913
+ } else {
914
+ updateVolume(video.volume);
915
+ }
916
+ hideContextMenu();
917
+ }
918
+
919
+ function handleVolumeChange() {
920
+ video.muted = false;
921
+ updateVolume(volumeSlider.value);
922
+ }
923
+
924
+ function goFullscreen() {
925
+ if (
926
+ document.fullscreenElement ||
927
+ document.webkitFullscreenElement ||
928
+ document.msFullscreenElement
929
+ ) {
930
+ // フルスクリーンを解除
931
+ if (document.exitFullscreen) {
932
+ document.exitFullscreen();
933
+ } else if (document.webkitExitFullscreen) {
934
+ document.webkitExitFullscreen();
935
+ } else if (document.msExitFullscreen) {
936
+ document.msExitFullscreen();
937
+ }
938
+ } else {
939
+ // フルスクリーンにする
940
+ if (videoContainer.requestFullscreen) {
941
+ videoContainer.requestFullscreen();
942
+ } else if (videoContainer.webkitRequestFullscreen) {
943
+ videoContainer.webkitRequestFullscreen();
944
+ } else if (videoContainer.msRequestFullscreen) {
945
+ videoContainer.msRequestFullscreen();
946
+ }
947
+ }
948
+ hideContextMenu();
949
+ }
950
+ function setupFullscreenContextMenu() {
951
+ const fullscreenElement = document.fullscreenElement ||
952
+ document.webkitFullscreenElement ||
953
+ document.msFullscreenElement;
954
+
955
+ if (fullscreenElement) {
956
+ fullscreenElement.addEventListener('contextmenu', showContextMenu);
957
+ }
958
+ }
959
+ function updateSubtitleScaleForFullscreen() {
960
+ if (document.fullscreenElement || document.webkitFullscreenElement ||
961
+ document.mozFullScreenElement || document.msFullscreenElement) {
962
+ // 全画面モード
963
+ const fullscreenWidth = window.innerWidth;
964
+ const scaleFactor = fullscreenWidth / normalVideoWidth;
965
+ document.documentElement.style.setProperty('--fullscreen-scale', scaleFactor);
966
+
967
+ // 全画面要素にイベントリスナーを追加
968
+ const fsElement = document.fullscreenElement || document.webkitFullscreenElement ||
969
+ document.mozFullScreenElement || document.msFullscreenElement;
970
+ fsElement.addEventListener('contextmenu', showContextMenu);
971
+ } else {
972
+ // 通常モード
973
+ document.documentElement.style.setProperty('--fullscreen-scale', 1);
974
+ }
975
+ }
976
+ function setupFramePreview() {
977
+ let previewTimeout;
978
+
979
+ progressContainer.addEventListener('mousemove', (e) => {
980
+ if (!videoBlob || !video.duration) return;
981
+
982
+ clearTimeout(previewTimeout);
983
+
984
+ // 全画面モードかどうかを判定
985
+ const isFullscreen = document.fullscreenElement ||
986
+ document.webkitFullscreenElement ||
987
+ document.msFullscreenElement;
988
+
989
+ // プログレスバーの位置とサイズを取得
990
+ const progressRect = progressContainer.getBoundingClientRect();
991
+
992
+ // マウス座標を正しく計算(全画面モードに対応)
993
+ let clickX;
994
+ if (isFullscreen) {
995
+ // 全画面モードではe.offsetXが正しくない場合があるので、clientXを使用
996
+ clickX = e.clientX - progressRect.left;
997
+ } else {
998
+ clickX = e.offsetX;
999
+ }
1000
+
1001
+ // クリック位置をプログレスバーの範囲内に制限
1002
+ clickX = Math.max(0, Math.min(clickX, progressRect.width));
1003
+
1004
+ const previewTime = (clickX / progressRect.width) * video.duration;
1005
+
1006
+ // 時間表示を更新
1007
+ const previewMinutes = Math.floor(previewTime / 60);
1008
+ const previewSeconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
1009
+ frameTime.textContent = `${previewMinutes}:${previewSeconds}`;
1010
+
1011
+ // プレビュー位置を更新(全画面モードに合わせて調整)
1012
+ const previewLeft = isFullscreen ?
1013
+ (e.clientX - framePreview.offsetWidth / 2) :
1014
+ (progressRect.left + clickX - framePreview.offsetWidth / 2);
1015
+
1016
+ framePreview.style.left = `${previewLeft}px`;
1017
+ framePreview.style.top = isFullscreen ?
1018
+ `${progressRect.top - framePreview.offsetHeight - 10}px` :
1019
+ `${progressRect.top - framePreview.offsetHeight - 10}px`;
1020
+ framePreview.style.display = 'block';
1021
+
1022
+ // キャッシュがあればそれを使う
1023
+ const cacheKey = Math.floor(previewTime);
1024
+ if (frameCache[cacheKey]) {
1025
+ return;
1026
+ }
1027
+
1028
+ // フレームを取得
1029
+ VideoForThumbnail.currentTime = previewTime;
1030
+ VideoForThumbnail.crossOrigin = 'anonymous';
1031
+ VideoForThumbnail.addEventListener('seeked', function() {
1032
+ canvas.width = VideoForThumbnail.videoWidth;
1033
+ canvas.height = VideoForThumbnail.videoHeight;
1034
+ ctx.drawImage(VideoForThumbnail, 0, 0, canvas.width, canvas.height);
1035
+ frameCache[cacheKey] = imageData; // キャッシュに保存
1036
+ }, { once: true });
1037
+ });
1038
+
1039
+ progressContainer.addEventListener('mouseleave', () => {
1040
+ previewTimeout = setTimeout(() => {
1041
+ framePreview.style.display = 'none';
1042
+ }, 300);
1043
+ });
1044
+
1045
+ framePreview.addEventListener('mouseenter', () => {
1046
+ clearTimeout(previewTimeout);
1047
+ });
1048
+
1049
+ framePreview.addEventListener('mouseleave', () => {
1050
+ framePreview.style.display = 'none';
1051
+ });
1052
+ }
1053
+ // 字幕関連の関数
1054
+ function toggleSubtitles() {
1055
+ subtitlesEnabled = !subtitlesEnabled;
1056
+ subtitleToggle.checked = subtitlesEnabled;
1057
+ subtitleTrackElement.track.mode = subtitlesEnabled ? 'showing' : 'hidden';
1058
+ subtitleBtn.style.color = subtitlesEnabled ? '#00ccff' : '#666';
1059
+ hideContextMenu();
1060
+ }
1061
+
1062
+ function updateSubtitleSize(value) {
1063
+ const size = parseFloat(value);
1064
+ subtitleSizeInput.value = size;
1065
+ subtitleSize.value = size;
1066
+
1067
+ // 字幕サイズを制御
1068
+ document.documentElement.style.setProperty('--subtitle-scale', size);
1069
+
1070
+ // VTTCueのlineプロパティには数値のみを設定
1071
+ const track = subtitleTrackElement.track;
1072
+ if (track && track.cues) {
1073
+ for (let i = 0; i < track.cues.length; i++) {
1074
+ track.cues[i].line = 90;
1075
+ track.cues[i].snapToLines = false;
1076
+ }
1077
+ }
1078
+ }
1079
+
1080
+ function changeSubtitleTrack() {
1081
+ const selectedTrack = subtitleTrack.value;
1082
+ subtitleTrackElement.src = selectedTrack;
1083
+ subtitleTrackElement.track.mode = selectedTrack && subtitlesEnabled ? 'showing' : 'hidden';
1084
+
1085
+ // トラック変更後に再度読み込み
1086
+ video.textTracks[0].mode = 'hidden';
1087
+ if (selectedTrack) {
1088
+ video.textTracks[0].mode = subtitlesEnabled ? 'showing' : 'hidden';
1089
+ }
1090
+ }
1091
+
1092
+ function toggleSubtitleMenu() {
1093
+ document.getElementById('subtitleToggle').checked ^= true;
1094
+ toggleSubtitles();
1095
+ }
1096
+
1097
+ // フレームプレビュー関連
1098
+ function showFramePreview(e) {
1099
+ if (!videoBlob) return;
1100
+
1101
+ const progressRect = progressContainer.getBoundingClientRect();
1102
+ const clickX = e.clientX - progressRect.left;
1103
+ const duration = video.duration;
1104
+ const previewTime = (clickX / progressRect.width) * duration;
1105
+
1106
+ // 時間表示を更新
1107
+ const previewMinutes = Math.floor(previewTime / 60);
1108
+ const previewSeconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
1109
+ frameTime.textContent = `${previewMinutes}:${previewSeconds}`;
1110
+
1111
+ // プレビュー位置を更新
1112
+ framePreview.style.left = `${e.clientX}px`;
1113
+ framePreview.style.display = 'block';
1114
+
1115
+ // キャッシュがあればそれを使う
1116
+ const cacheKey = Math.floor(previewTime);
1117
+ if (frameCache[cacheKey]) {
1118
+ return;
1119
+ }
1120
+
1121
+ // フレームを取得
1122
+ videoFrames({
1123
+ url: URL.createObjectURL(videoBlob),
1124
+ count: 1,
1125
+ startTime: previewTime,
1126
+ endTime: previewTime + 0.1
1127
+ }).then((frames) => {
1128
+ if (frames.length > 0) {
1129
+ frameCache[cacheKey] = frames[0].image; // キャッシュに保存
1130
+ }
1131
+ }).catch(err => {
1132
+ console.error('Error getting video frame:', err);
1133
+ });
1134
+ }
1135
+
1136
+ function hideFramePreview() {
1137
+ framePreview.style.display = 'none';
1138
+ }
1139
+
1140
+ // 右クリックメニュー関連
1141
+ function showContextMenu(e) {
1142
+ e.preventDefault();
1143
+ contextMenu.style.display = 'block';
1144
+ contextMenu.style.left = `${e.clientX}px`;
1145
+ contextMenu.style.top = `${e.clientY}px`;
1146
+ }
1147
+
1148
+ function hideContextMenu() {
1149
+ contextMenu.style.display = 'none';
1150
+ }
1151
+
1152
+ // 音声/字幕のみモード
1153
+ function toggleAudioOnlyMode() {
1154
+ isAudioOnlyMode = !isAudioOnlyMode;
1155
+
1156
+ if (isAudioOnlyMode) {
1157
+ video.style.opacity = '0';
1158
+ audioOnlyModeIndicator.classList.add('active');
1159
+ } else {
1160
+ video.style.opacity = '1';
1161
+ audioOnlyModeIndicator.classList.remove('active');
1162
+ }
1163
+
1164
+ hideContextMenu();
1165
+ }
1166
+
1167
+ // イベントリスナー
1168
+ videoSelect.addEventListener('change', handleVideoChange);
1169
+
1170
+ ['input', 'change', 'mouseup'].forEach(eventName => {
1171
+ speedRange.addEventListener(eventName, () => updatePlaybackRate(speedRange.value));
1172
+ volumeRange.addEventListener(eventName, () => updateVolume(volumeRange.value));
1173
+ subtitleSize.addEventListener(eventName, () => updateSubtitleSize(subtitleSize.value));
1174
+ });
1175
+
1176
+ speedInput.addEventListener('input', () => updatePlaybackRate(speedInput.value));
1177
+ volumeInput.addEventListener('input', () => updateVolume(volumeInput.value));
1178
+ subtitleSizeInput.addEventListener('input', () => updateSubtitleSize(subtitleSizeInput.value));
1179
+
1180
+ loopCheckbox.addEventListener('change', () => {
1181
+ video.loop = loopCheckbox.checked;
1182
+ });
1183
+
1184
+ subtitleToggle.addEventListener('change', toggleSubtitles);
1185
+ subtitleTrack.addEventListener('change', changeSubtitleTrack);
1186
+ subtitleBtn.addEventListener('click', toggleSubtitleMenu);
1187
+
1188
+ playPauseBtn.addEventListener('click', togglePlayPause);
1189
+ video.addEventListener('click', togglePlayPause);
1190
+ video.addEventListener('play', () => playPauseBtn.textContent = '⏸');
1191
+ video.addEventListener('pause', () => playPauseBtn.textContent = '▶');
1192
+ video.addEventListener('timeupdate', updateProgress);
1193
+ progressContainer.addEventListener('click', setProgress);
1194
+ progressContainer.addEventListener('mousedown', () => isDragging = true);
1195
+ document.addEventListener('mouseup', () => isDragging = false);
1196
+ // マウスホバー時のプレビュー表示
1197
+ progressContainer.addEventListener('mousemove', function(e) {
1198
+ if (isDragging) {
1199
+ const width = progressContainer.clientWidth;
1200
+ const clickX = e.offsetX;
1201
+ const duration = video.duration;
1202
+ const previewTime = (clickX / width) * duration;
1203
+
1204
+ // プレビュー位置を更新
1205
+ previewContainer.style.left = `${e.clientX - 100}px`;
1206
+ previewContainer.style.bottom = '60px';
1207
+ previewContainer.style.display = 'block';
1208
+
1209
+ // 時間表示を更新
1210
+ const minutes = Math.floor(previewTime / 60);
1211
+ const seconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
1212
+ document.getElementById('previewTime').textContent = `${minutes}:${seconds}`;
1213
+
1214
+ // サムネイル画像を更新
1215
+ updateThumbnail(previewTime);
1216
+ } else {
1217
+ previewContainer.style.display = 'none';
1218
+ }
1219
+ });
1220
+
1221
+ // サムネイル画像更新関数
1222
+ function updateThumbnail(time) {
1223
+ VideoForThumbnail.currentTime = time;
1224
+
1225
+ VideoForThumbnail.addEventListener('seeked', function() {
1226
+ canvas.width = VideoForThumbnail.videoWidth;
1227
+ canvas.height = VideoForThumbnail.videoHeight;
1228
+ ctx.drawImage(VideoForThumbnail, 0, 0, canvas.width, canvas.height);
1229
+ preview.src = canvas.toDataURL('image/jpeg');
1230
+ }, { once: true });
1231
+ }
1232
+
1233
+
1234
+ // プログレスバーのホバーイベント
1235
+ progressContainer.addEventListener('mouseenter', () => {
1236
+ isHoveringProgress = true;
1237
+ clearTimeout(hoverTimeout);
1238
+ });
1239
+
1240
+ progressContainer.addEventListener('mouseleave', () => {
1241
+ isHoveringProgress = false;
1242
+ hoverTimeout = setTimeout(() => {
1243
+ if (!isDragging) hideFramePreview();
1244
+ }, 300);
1245
+ });
1246
+
1247
+ volumeBtn.addEventListener('click', toggleMute);
1248
+ volumeSlider.addEventListener('input', handleVolumeChange);
1249
+ fullscreenBtn.addEventListener('click', goFullscreen);
1250
+
1251
+ // 全画面変更イベントを監視
1252
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
1253
+ document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
1254
+ document.addEventListener('mozfullscreenchange', handleFullscreenChange);
1255
+ document.addEventListener('MSFullscreenChange', handleFullscreenChange);
1256
+
1257
+ // 右クリックメニューイベント
1258
+ videoContainer.addEventListener('contextmenu', showContextMenu);
1259
+ document.addEventListener('click', hideContextMenu);
1260
+ document.addEventListener('keydown', (e) => {
1261
+ if (e.key === 'Escape') hideContextMenu();
1262
+ });
1263
+
1264
+ video.addEventListener('loadedmetadata', () => {
1265
+ updatePlaybackRate(speedRange.value);
1266
+ updateVolume(volumeRange.value);
1267
+ updateSubtitleSize(subtitleSize.value);
1268
+ video.loop = loopCheckbox.checked;
1269
+ toggleSubtitles();
1270
+ updateProgress();
1271
+ normalVideoWidth = videoContainer.clientWidth;
1272
+ });
1273
+ video.addEventListener("loadeddata", async () => {
1274
+ const response = await fetch(video.src);
1275
+ videoBlob = await response.blob();
1276
+ });
1277
+
1278
+ // 保存
1279
+ video.addEventListener('timeupdate', () => {
1280
+ localStorage.setItem('radioTaisoTime', video.currentTime);
1281
+ });
1282
+
1283
+ // 復元
1284
+ window.addEventListener('load', () => {
1285
+ setupFramePreview();
1286
+ setupFullscreenContextMenu();
1287
+ const savedTime = parseFloat(localStorage.getItem('radioTaisoTime'));
1288
+ if (!isNaN(savedTime)) {
1289
+ video.currentTime = savedTime;
1290
+ }
1291
+
1292
+ document.addEventListener('fullscreenchange', () => {
1293
+ updateSubtitleScaleForFullscreen();
1294
+ setupFullscreenContextMenu();
1295
+ });
1296
+ document.addEventListener('webkitfullscreenchange', () => {
1297
+ updateSubtitleScaleForFullscreen();
1298
+ setupFullscreenContextMenu();
1299
+ });
1300
+ document.addEventListener('mozfullscreenchange', () => {
1301
+ updateSubtitleScaleForFullscreen();
1302
+ setupFullscreenContextMenu();
1303
+ });
1304
+ document.addEventListener('MSFullscreenChange', () => {
1305
+ updateSubtitleScaleForFullscreen();
1306
+ setupFullscreenContextMenu();
1307
+ });
1308
+ });
1309
+ document.addEventListener('keydown', (e) => {
1310
+ if (e.target.tagName === 'INPUT') return; // 入力中は無視
1311
+ switch (e.key.toLowerCase()) {
1312
+ case ' ': e.preventDefault(); togglePlayPause(); break;
1313
+ case 'f': goFullscreen(); break;
1314
+ case 'm': toggleMute(); break;
1315
+ case 'arrowright': video.currentTime += 5; break;
1316
+ case 'arrowleft': video.currentTime -= 5; break;
1317
+ }
1318
+ });
1319
+
1320
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
1321
+ document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
1322
+ document.addEventListener('mozfullscreenchange', handleFullscreenChange);
1323
+ document.addEventListener('MSFullscreenChange', handleFullscreenChange);
1324
+
1325
+ function handleFullscreenChange() {
1326
+ updateSubtitleScaleForFullscreen();
1327
+ setupFullscreenContextMenu();
1328
+ normalVideoWidth = videoContainer.clientWidth;
1329
+ setupFramePreview(); // フレームプレビューを再設定
1330
+ }
1331
+
1332
+ // CSS変数を設定
1333
+ document.documentElement.style.setProperty('--subtitle-scale', '1');
1334
+ document.documentElement.style.setProperty('--subtitle-border-radius', '10px');
1335
+ document.documentElement.style.setProperty('--fullscreen-scale', '1');
1336
+ </script>
1337
  </body>
1338
 
1339
  </html>