soiz1 commited on
Commit
7b0f7f9
·
verified ·
1 Parent(s): 6ec4f66

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +502 -822
index.html CHANGED
@@ -8,6 +8,7 @@
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <link href="https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c&display=swap" rel="stylesheet">
10
  <link rel="icon" href="icon.png" type="image/png">
 
11
  <style>
12
  body {
13
  display: flex;
@@ -18,7 +19,7 @@
18
  font-family: "M PLUS Rounded 1c", monospace;
19
  padding: 20px;
20
  margin: 0;
21
- overflow-x: hidden; /* 横スクロール禁止 */
22
  }
23
 
24
  h1 {
@@ -38,38 +39,7 @@
38
  box-shadow: 0 0 15px rgba(0, 102, 255, 0.5);
39
  background: #000;
40
  }
41
- /* ホバー時の時間表示スタイル */
42
- .hover-time {
43
- position: absolute;
44
- top: -30px;
45
- transform: translateX(-50%);
46
- background: rgba(0, 20, 40, 0.9);
47
- color: #00ccff;
48
- padding: 3px 8px;
49
- border-radius: 4px;
50
- font-size: 12px;
51
- pointer-events: none;
52
- display: none;
53
- white-space: nowrap;
54
- font-family: "M PLUS Rounded 1c", monospace;
55
- }
56
-
57
- /* サムネイルプレビュー用スタイル */
58
- .thumbnail-preview {
59
- position: absolute;
60
- width: 160px;
61
- height: 90px;
62
- background: #000;
63
- border: 2px solid #00aaff;
64
- box-shadow: 0 0 10px rgba(0, 170, 255, 0.5);
65
- display: none;
66
- pointer-events: none;
67
- z-index: 10;
68
- bottom: 50px;
69
- transform: translateX(-50%);
70
- border-radius: 4px;
71
- object-fit: cover;
72
- }
73
  video {
74
  width: 100%;
75
  display: block;
@@ -82,7 +52,7 @@
82
  font-family: "M PLUS Rounded 1c", monospace !important;
83
  text-shadow: 1px 1px 2px #000 !important;
84
  outline: 3px solid #0b3e8f !important;
85
- border-radius: 10px !important; /* 角を丸める */
86
  }
87
 
88
  /* カスタム動画コントロール */
@@ -95,7 +65,6 @@
95
  bottom: 0;
96
  left: 0;
97
  right: 0;
98
- /*background: linear-gradient(to top, rgba(0, 20, 40, 0.9), transparent);*/
99
  padding: 10px;
100
  display: flex;
101
  flex-direction: column;
@@ -136,36 +105,6 @@
136
  box-shadow: 0 0 5px #00ccff;
137
  }
138
 
139
- /* プレビュー用スタイル */
140
- .preview-tooltip {
141
- position: absolute;
142
- bottom: 20px;
143
- transform: translateX(-50%);
144
- background: rgba(0, 20, 40, 0.9);
145
- border: 1px solid #0066ff;
146
- padding: 5px 10px;
147
- border-radius: 5px;
148
- display: none;
149
- z-index: 100;
150
- pointer-events: none;
151
- }
152
-
153
- .preview-time {
154
- color: #00ccff;
155
- font-size: 12px;
156
- text-align: center;
157
- margin-bottom: 5px;
158
- }
159
-
160
- .preview-frame {
161
- width: 160px;
162
- height: 90px;
163
- background-color: #000;
164
- border: 1px solid #0066ff;
165
- background-size: cover;
166
- background-position: center;
167
- }
168
-
169
  .buttons-container {
170
  display: flex;
171
  align-items: center;
@@ -353,53 +292,38 @@
353
  .video-container:-ms-fullscreen video::cue {
354
  font-size: calc(16px * var(--subtitle-scale) * var(--fullscreen-scale, 1)) !important;
355
  }
356
- .thumbnail-preview {
357
- position: absolute;
358
- width: 160px;
359
- height: 90px;
360
- background: #000;
361
- border: 2px solid #00aaff;
362
- box-shadow: 0 0 10px rgba(0, 170, 255, 0.5);
363
- display: none;
364
- pointer-events: none;
365
- z-index: 10;
366
- bottom: 50px; /* プログレスバーの上に表示 */
367
- transform: translateX(-50%);
368
- border-radius: 4px;
369
- object-fit: cover;
370
- }
371
  body {
372
  margin: 0;
373
- padding: 0;
374
- background-color: #0a192f; /* 暗い青 */
375
- height: 100vh;
376
- width: 100vw;
377
- }
378
 
379
- .ripple {
380
- position: absolute;
381
- border-radius: 50%;
382
- background: transparent;
383
- border: 1px solid rgba(100, 210, 255, 0.3); /* 半透明の薄い水色 */
384
- transform: translate(-50%, -50%);
385
- pointer-events: none;
386
- animation: ripple-animation 4s ease-out forwards;
387
- z-index: -1;
388
- position: absolute; /* または fixed / relative、必要に応じて調整 */
389
- }
390
 
391
- @keyframes ripple-animation {
392
- 0% {
393
- width: 0;
394
- height: 0;
395
- opacity: 0.6;
396
- }
397
- 100% {
398
- width: 600px;
399
- height: 600px;
400
- opacity: 0;
401
- }
402
- }
403
 
404
  /* ���ーディングアニメーション */
405
  .loading-overlay {
@@ -492,63 +416,86 @@
492
  transform: rotate(360deg);
493
  }
494
  }
495
-
496
- /* 全画面コンテキストメニュー */
497
- .fullscreen-context-menu {
498
- position: fixed;
499
- top: 50%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  left: 50%;
501
- transform: translate(-50%, -50%);
 
 
 
 
 
 
 
 
 
 
 
502
  background-color: #0f0f1a;
503
  border: 1px solid #0066ff;
504
  box-shadow: 0 0 15px rgba(0, 102, 255, 0.5);
505
- padding: 15px;
506
  z-index: 1000;
507
  display: none;
 
508
  }
509
-
510
- .fullscreen-context-menu button {
511
- display: block;
512
  width: 100%;
513
- margin-bottom: 5px;
 
 
 
 
 
 
 
514
  }
515
-
516
- .fullscreen-context-menu button:last-child {
517
- margin-bottom: 0;
 
518
  }
519
-
520
  /* 音声/字幕のみモード */
521
  .audio-only-mode {
522
- position: relative;
523
- }
524
-
525
- .audio-only-mode .video-placeholder {
526
- display: flex;
527
- justify-content: center;
528
- align-items: center;
529
- background-color: #000;
530
  color: #00ccff;
531
- font-size: 24px;
532
- height: 450px; /* 動画の高さに合わせて調整 */
533
- }
534
-
535
- .audio-only-mode video {
536
  display: none;
 
 
 
 
 
537
  }
538
- .hover-time {
539
- position: absolute;
540
- top: -30px;
541
- transform: translateX(-50%);
542
- background: rgba(0, 20, 40, 0.9);
543
- color: #00ccff;
544
- padding: 3px 8px;
545
- border-radius: 4px;
546
- font-size: 12px;
547
- pointer-events: none;
548
- display: none;
549
- white-space: nowrap;
550
- font-family: "M PLUS Rounded 1c", monospace;
551
- }
552
  </style>
553
  </head>
554
 
@@ -565,144 +512,26 @@
565
  </div>
566
  </div>
567
 
568
- <!-- 全画面コンテキストメニュー -->
569
- <div class="fullscreen-context-menu" id="fullscreenContextMenu">
570
- <button id="audioOnlyBtn">音声/字幕のみモード</button>
571
- <button id="showVideoBtn">動画表示モード</button>
572
- <button id="exitFullscreenBtn">全画面を終了</button>
573
- <button id="closeContextMenuBtn">閉じる</button>
574
  </div>
575
-
576
- <div id="ripple-container">
 
 
 
 
 
 
577
  </div>
578
- <script>
579
- // ローディングアニメーションをフェードアウト
580
- window.addEventListener('load', function() {
581
- setTimeout(function() {
582
- const loadingOverlay = document.getElementById('loadingOverlay');
583
- loadingOverlay.style.opacity = '0';
584
- setTimeout(function() {
585
- loadingOverlay.style.display = 'none';
586
- }, 1000); // フェードアウト完了後に非表示にする
587
- }, 1500); // ページ読み込み後1.5秒でフェードアウト開始
588
- });
589
 
590
- document.addEventListener('DOMContentLoaded', function() {
591
- const container = document.getElementById('ripple-container');
592
-
593
- function createRipple() {
594
- const ripple = document.createElement('div');
595
- ripple.classList.add('ripple');
596
-
597
- // ランダムな位置
598
- const posX = Math.random() * 100;
599
- const posY = Math.random() * 100;
600
-
601
- // ランダムなサイズとアニメーション時間
602
- const maxSize = 400 + Math.random() * 500;
603
- const duration = 3 + Math.random() * 3;
604
-
605
- // 半透明の薄い水色のバリエーション
606
- const hue = 190 + Math.random() * 20 - 10; // 水色を中心に少し変化
607
- const saturation = 80 + Math.random() * 15;
608
- const lightness = 70 + Math.random() * 20;
609
- const opacity = 0.3 + Math.random() * 0.3;
610
-
611
- ripple.style.left = `${posX}%`;
612
- ripple.style.top = `${posY}%`;
613
- ripple.style.borderColor = `hsla(${hue}, ${saturation}%, ${lightness}%, ${opacity})`;
614
- ripple.style.animationDuration = `${duration}s`;
615
-
616
- // キーフレームを動的に変更
617
- //Math.random()で 0〜1 のランダムな数を生成。 .toString(36) で36進数(0-9 + a-z)に変換。
618
- //.substr(2, 9) で先頭の "0." を除いた部分から9文字取り出し、 結果として英数字のランダムな9文字列を生成。
619
- const animationName = `ripple-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
620
- ripple.style.animationName = animationName;
621
-
622
- const style = document.createElement('style');
623
- style.innerHTML = `
624
- @keyframes ${animationName} {
625
- 0% {
626
- width: 0;
627
- height: 0;
628
- opacity: ${opacity};
629
- }
630
- 100% {
631
- width: ${maxSize}px;
632
- height: ${maxSize}px;
633
- opacity: 0;
634
- }
635
- }
636
- `;
637
- document.head.appendChild(style);
638
-
639
- container.appendChild(ripple);
640
-
641
- // アニメーション終了後に要素を削除
642
- ripple.addEventListener('animationend', function() {
643
- ripple.remove();
644
- style.remove();
645
- });
646
- }
647
-
648
- // 最初の波紋をいくつか作成
649
- for (let i = 0; i < 8; i++) {
650
- setTimeout(createRipple, i * 600);
651
- }
652
-
653
- // 定期的に新しい波紋を作成
654
- setInterval(createRipple, 1200);
655
-
656
- // クリックでも波紋を作成
657
- document.addEventListener('click', function(e) {
658
- createRippleAtPosition(e.clientX, e.clientY);
659
- });
660
-
661
- function createRippleAtPosition(x, y) {
662
- const ripple = document.createElement('div');
663
- ripple.classList.add('ripple');
664
-
665
- const maxSize = 400 + Math.random() * 500;
666
- const duration = 3 + Math.random() * 3;
667
- const hue = 190 + Math.random() * 20 - 10;
668
- const saturation = 80 + Math.random() * 15;
669
- const lightness = 70 + Math.random() * 20;
670
- const opacity = 0.3 + Math.random() * 0.3;
671
-
672
- ripple.style.left = `${x}px`;
673
- ripple.style.top = `${y}px`;
674
- ripple.style.borderColor = `hsla(${hue}, ${saturation}%, ${lightness}%, ${opacity})`;
675
- ripple.style.animationDuration = `${duration}s`;
676
-
677
- const animationName = `ripple-click-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
678
- ripple.style.animationName = animationName;
679
-
680
- const style = document.createElement('style');
681
- style.innerHTML = `
682
- @keyframes ${animationName} {
683
- 0% {
684
- width: 0;
685
- height: 0;
686
- opacity: ${opacity};
687
- }
688
- 100% {
689
- width: ${maxSize}px;
690
- height: ${maxSize}px;
691
- opacity: 0;
692
- }
693
- }
694
- `;
695
- document.head.appendChild(style);
696
-
697
- container.appendChild(ripple);
698
-
699
- ripple.addEventListener('animationend', function() {
700
- ripple.remove();
701
- style.remove();
702
- });
703
- }
704
- });
705
- </script>
706
  <h1>ラジオ体操動画プレイヤー
707
  <br>For Kushihara</h1>
708
  <div class="controls">
@@ -746,24 +575,19 @@
746
  </select>
747
  </div>
748
  </div>
749
- <button onclick="goFullscreen()">全画面</button>
750
- </div>
751
- <div class="video-container" id="videoContainer">
752
- <div class="video-placeholder" id="videoPlaceholder" style="display: none;">
753
- 音声/字幕のみモード
754
  </div>
755
- <video id="videoPlayer" src="v.mp4" crossorigin="anonymous">
 
 
756
  <track id="subtitleTrackElement" kind="subtitles" src="v.vtt" srclang="ja" label="日本語" default>
757
  </track>
758
  </video>
759
- <div class="preview-tooltip" id="previewTooltip">
760
- <div class="preview-time" id="previewTime">00:00</div>
761
- <div class="preview-frame" id="previewFrame"></div>
762
- </div>
763
  <div class="custom-controls">
764
  <div class="progress-container" id="progressContainer">
765
- <div class="progress-bar" id="progressBar">
766
- </div>
767
  </div>
768
  <div class="buttons-container">
769
  <div class="left-controls">
@@ -776,548 +600,404 @@
776
  <input type="range" class="volume-slider" id="volumeSlider" min="0" max="1" step="0.01" value="1">
777
  </div>
778
  <button class="control-btn" id="subtitleBtn" title="字幕">🔤</button>
779
- <button class="control-btn" id="audioOnlyBtn" title="音声/字幕のみ">🔈</button>
780
  <button class="control-btn" id="fullscreenBtn">⛶</button>
781
  </div>
782
  </div>
783
  </div>
784
  </div>
785
- <script>
786
- // 要素取得
787
- const video = document.getElementById('videoPlayer');
788
- const videoSelect = document.getElementById('videoSelect');
789
- const speedRange = document.getElementById('speedRange');
790
- const speedInput = document.getElementById('speedInput');
791
- const volumeRange = document.getElementById('volumeRange');
792
- const volumeInput = document.getElementById('volumeInput');
793
- const loopCheckbox = document.getElementById('loopCheckbox');
794
- const playPauseBtn = document.getElementById('playPauseBtn');
795
- const progressBar = document.getElementById('progressBar');
796
- const progressContainer = document.getElementById('progressContainer');
797
- const timeDisplay = document.getElementById('timeDisplay');
798
- const volumeBtn = document.getElementById('volumeBtn');
799
- const volumeSlider = document.getElementById('volumeSlider');
800
- const fullscreenBtn = document.getElementById('fullscreenBtn');
801
- const subtitleBtn = document.getElementById('subtitleBtn');
802
- const subtitleToggle = document.getElementById('subtitleToggle');
803
- const subtitleSize = document.getElementById('subtitleSize');
804
- const subtitleSizeInput = document.getElementById('subtitleSizeInput');
805
- const subtitleTrack = document.getElementById('subtitleTrack');
806
- const subtitleTrackElement = document.getElementById('subtitleTrackElement');
807
- const videoContainer = document.getElementById('videoContainer');
808
- const videoPlaceholder = document.getElementById('videoPlaceholder');
809
- const previewTooltip = document.getElementById('previewTooltip');
810
- const previewTime = document.getElementById('previewTime');
811
- const previewFrame = document.getElementById('previewFrame');
812
- const audioOnlyBtn = document.getElementById('audioOnlyBtn');
813
- const fullscreenContextMenu = document.getElementById('fullscreenContextMenu');
814
- const exitFullscreenBtn = document.getElementById('exitFullscreenBtn');
815
- const showVideoBtn = document.getElementById('showVideoBtn');
816
- const closeContextMenuBtn = document.getElementById('closeContextMenuBtn');
817
-
818
- // プレビュー用動画を動的に作成
819
- const previewVideo = document.createElement('video');
820
- previewVideo.id = 'previewVideo';
821
- previewVideo.className = 'thumbnail-preview';
822
- previewVideo.muted = true;
823
- previewVideo.preload = 'auto';
824
- videoContainer.appendChild(previewVideo);
825
-
826
- // 初期設定
827
- video.controls = false;
828
- previewVideo.src = video.src;
829
- let isDragging = false;
830
- let subtitlesEnabled = true;
831
- let normalVideoWidth = videoContainer.clientWidth;
832
- let isAudioOnlyMode = false;
833
- let hoverTimeout;
834
- let canvas = null;
835
- let previewCanvas = null;
836
- let previewContext = null;
837
- let isGeneratingPreview = false;
838
-
839
- // プレビュー用キャンバスを作成
840
- function createPreviewCanvas() {
841
- if (!canvas) {
842
- canvas = document.createElement('canvas');
843
- canvas.width = video.videoWidth || 640;
844
- canvas.height = video.videoHeight || 360;
845
- }
846
-
847
- if (!previewCanvas) {
848
- previewCanvas = document.createElement('canvas');
849
- previewCanvas.width = 160;
850
- previewCanvas.height = 90;
851
- previewContext = previewCanvas.getContext('2d');
852
- }
853
- }
854
-
855
- // プレビュー画像を生成
856
- function generatePreview(time) {
857
- if (isGeneratingPreview || !video.readyState) return;
858
 
859
- try {
860
- isGeneratingPreview = true;
861
- createPreviewCanvas();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
862
 
863
- const currentTime = video.currentTime;
864
- video.currentTime = time;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
865
 
866
- const onSeeked = () => {
867
- video.removeEventListener('seeked', onSeeked);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
868
 
869
- try {
870
- const ctx = canvas.getContext('2d');
871
- canvas.width = video.videoWidth;
872
- canvas.height = video.videoHeight;
873
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
874
- previewContext.drawImage(canvas, 0, 0, previewCanvas.width, previewCanvas.height);
875
- previewFrame.style.backgroundImage = `url(${previewCanvas.toDataURL()})`;
876
- } catch (e) {
877
- console.error('Preview generation error:', e);
878
- previewFrame.style.backgroundImage = 'linear-gradient(to bottom, #0066ff, #00aaff)';
879
  }
 
 
 
 
 
 
880
 
881
- video.currentTime = currentTime;
882
- isGeneratingPreview = false;
883
- };
 
 
 
 
 
 
 
 
884
 
885
- video.addEventListener('seeked', onSeeked);
886
- } catch (e) {
887
- console.error('Preview error:', e);
888
- isGeneratingPreview = false;
889
- }
890
- }
891
-
892
- // プレビューツールチップを表示
893
- function showPreviewTooltip(e) {
894
- if (!video.duration) return;
895
-
896
- const rect = progressContainer.getBoundingClientRect();
897
- const percent = (e.clientX - rect.left) / rect.width;
898
- const time = Math.max(0, Math.min(percent, 1)) * video.duration;
899
-
900
- const tooltipWidth = previewTooltip.offsetWidth;
901
- let left = e.clientX - rect.left;
902
- left = Math.max(tooltipWidth / 2, Math.min(left, rect.width - tooltipWidth / 2));
903
-
904
- previewTooltip.style.left = `${left}px`;
905
-
906
- const minutes = Math.floor(time / 60);
907
- const seconds = Math.floor(time % 60).toString().padStart(2, '0');
908
- previewTime.textContent = `${minutes}:${seconds}`;
909
-
910
- generatePreview(time);
911
- previewTooltip.style.display = 'block';
912
- }
913
-
914
- // プレビューツールチップを非表示
915
- function hidePreviewTooltip() {
916
- previewTooltip.style.display = 'none';
917
- }
918
-
919
- // ホバー時の時間表示設定
920
- function setupHoverTime() {
921
- const hoverTime = document.createElement('div');
922
- hoverTime.className = 'hover-time';
923
- progressContainer.appendChild(hoverTime);
924
-
925
- progressContainer.addEventListener("mousemove", (e) => {
926
- if (!video.duration) return;
927
 
928
- const rect = progressContainer.getBoundingClientRect();
929
- const pos = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1);
930
- const time = pos * video.duration;
931
 
932
- const minutes = Math.floor(time / 60);
933
- const seconds = Math.floor(time % 60).toString().padStart(2, '0');
934
- hoverTime.textContent = `${minutes}:${seconds}`;
935
- hoverTime.style.display = 'block';
936
- hoverTime.style.left = `${e.clientX - rect.left}px`;
 
 
937
 
938
- previewVideo.style.display = "block";
939
- previewVideo.style.left = `${e.clientX}px`;
 
940
 
941
- clearTimeout(hoverTimeout);
942
- hoverTimeout = setTimeout(() => {
943
- previewVideo.currentTime = time;
944
- }, 50);
945
- });
946
-
947
- progressContainer.addEventListener("mouseleave", () => {
948
- const hoverTime = document.querySelector('.hover-time');
949
- if (hoverTime) hoverTime.style.display = "none";
950
- previewVideo.style.display = "none";
951
- clearTimeout(hoverTimeout);
952
- });
953
- }
954
-
955
- // 再生速度を更新
956
- function updatePlaybackRate(value) {
957
- const speed = parseFloat(value);
958
- speedInput.value = speed;
959
- speedRange.value = speed;
960
- video.playbackRate = speed;
961
- }
962
-
963
- // 音量を更新
964
- function updateVolume(value) {
965
- const volume = parseFloat(value);
966
- volumeInput.value = volume;
967
- volumeRange.value = volume;
968
- volumeSlider.value = volume;
969
- video.volume = volume;
970
-
971
- if (volume === 0) {
972
- volumeBtn.textContent = '🔇';
973
- } else if (volume < 0.5) {
974
- volumeBtn.textContent = '🔈';
975
- } else {
976
- volumeBtn.textContent = '🔊';
977
- }
978
- }
979
-
980
-
981
- // プレビュー用動画のソースをメイン動画と同期
982
- previewVideo.src = video.src;
983
-
984
- // ホバー時のプレビュー更新関数(メイン動画を操作しない)
985
- function updateHoverPreview(e) {
986
- if (!video.duration) return;
987
-
988
- const rect = progressContainer.getBoundingClientRect();
989
- const pos = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1);
990
- const time = pos * video.duration;
991
-
992
- // 時間表示更新
993
- const minutes = Math.floor(time / 60);
994
- const seconds = Math.floor(time % 60).toString().padStart(2, '0');
995
- hoverTime.textContent = `${minutes}:${seconds}`;
996
- hoverTime.style.display = 'block';
997
- hoverTime.style.left = `${e.clientX - rect.left}px`;
998
-
999
- // プレビュー動画のみを更新(メイン動画は変更しない)
1000
- if (previewVideo.readyState > 0) {
1001
- previewVideo.currentTime = time;
1002
- }
1003
-
1004
- // プレビュー動画の位置調整
1005
- previewVideo.style.display = "block";
1006
- previewVideo.style.left = `${e.clientX}px`;
1007
- }
1008
-
1009
- // ホバーイベントリスナーの設定
1010
- function setupHoverEvents() {
1011
- const hoverTime = document.createElement('div');
1012
- hoverTime.className = 'hover-time';
1013
- progressContainer.appendChild(hoverTime);
1014
-
1015
- let hoverTimeout;
1016
-
1017
- progressContainer.addEventListener("mousemove", (e) => {
1018
- clearTimeout(hoverTimeout);
1019
- hoverTimeout = setTimeout(() => {
1020
- updateHoverPreview(e);
1021
- }, 30); // 30msのデバウンス
1022
- });
1023
-
1024
- progressContainer.addEventListener("mouseleave", () => {
1025
- hoverTime.style.display = "none";
1026
- previewVideo.style.display = "none";
1027
- clearTimeout(hoverTimeout);
1028
- });
1029
- }
1030
-
1031
- // 動画変更時の処理(プレビュー動画も同期)
1032
- function handleVideoChange() {
1033
- const selected = videoSelect.value;
1034
-
1035
- if (selected === 'v-2.mp4') {
1036
- const confirmPlay = confirm("この動画は音量が大きいです。あらかじめ、デバイスの音量をある程度下げてください。また、音割れが起きます。再生してもよろしいですか?");
1037
- if (!confirmPlay) {
1038
- videoSelect.value = video.src.split('/').pop();
1039
- return;
1040
  }
1041
- }
1042
-
1043
- video.src = selected;
1044
- previewVideo.src = selected; // プレビュー動画も更新
1045
-
1046
- // 両方の動画をロード
1047
- const loadVideo = video.load();
1048
- const loadPreview = previewVideo.load();
1049
-
1050
- Promise.all([loadVideo, loadPreview])
1051
- .then(() => video.play())
1052
- .catch(e => console.error("動画読み込みエラー:", e));
1053
- }
1054
-
1055
- // 初期化関数
1056
- function init() {
1057
- setupHoverEvents();
1058
-
1059
- // プレビュー動画のメタデータが読み込まれたら準備完了
1060
- previewVideo.addEventListener('loadedmetadata', () => {
1061
- console.log("プレビュー動画の準備が完了しました");
1062
- });
1063
-
1064
- // メイン動画のイベントリスナー設定
1065
- video.addEventListener('loadedmetadata', () => {
1066
- previewVideo.src = video.src; // ソースを再度同期
1067
- updatePlaybackRate(speedRange.value);
1068
- updateVolume(volumeRange.value);
1069
- });
1070
- }
1071
-
1072
- // 初期化を実行
1073
- init();
1074
- // 再生/一時停止を切り替え
1075
- function togglePlayPause() {
1076
- if (video.paused) {
1077
- video.play().then(() => {
1078
- playPauseBtn.textContent = '⏸';
1079
- }).catch(e => console.log(e));
1080
- } else {
1081
- video.pause();
1082
- playPauseBtn.textContent = '▶';
1083
- }
1084
- }
1085
-
1086
- // 進捗バーを更新
1087
- function updateProgress() {
1088
- if (!video.duration) return;
1089
-
1090
- const percent = (video.currentTime / video.duration) * 100;
1091
- progressBar.style.width = `${percent}%`;
1092
-
1093
- const currentMinutes = Math.floor(video.currentTime / 60);
1094
- const currentSeconds = Math.floor(video.currentTime % 60).toString().padStart(2, '0');
1095
- const durationMinutes = Math.floor(video.duration / 60);
1096
- const durationSeconds = Math.floor(video.duration % 60).toString().padStart(2, '0');
1097
-
1098
- timeDisplay.textContent = `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`;
1099
- }
1100
-
1101
- // 進捗バーを設定
1102
- function setProgress(e) {
1103
- const width = progressContainer.clientWidth;
1104
- const clickX = e.offsetX;
1105
- const duration = video.duration;
1106
- video.currentTime = (clickX / width) * duration;
1107
- }
1108
-
1109
- // ミュートを切り替え
1110
- function toggleMute() {
1111
- video.muted = !video.muted;
1112
- if (video.muted) {
1113
- volumeBtn.textContent = '🔇';
1114
- volumeSlider.value = 0;
1115
- } else {
1116
- updateVolume(video.volume);
1117
- }
1118
- }
1119
-
1120
- // 音量変更を処理
1121
- function handleVolumeChange() {
1122
- video.muted = false;
1123
- updateVolume(volumeSlider.value);
1124
- }
1125
-
1126
- // 全画面表示を切り替え
1127
- function goFullscreen() {
1128
- if (document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
1129
- if (document.exitFullscreen) {
1130
- document.exitFullscreen();
1131
- } else if (document.webkitExitFullscreen) {
1132
- document.webkitExitFullscreen();
1133
- } else if (document.msExitFullscreen) {
1134
- document.msExitFullscreen();
1135
- }
1136
- } else {
1137
- if (videoContainer.requestFullscreen) {
1138
- videoContainer.requestFullscreen();
1139
- } else if (videoContainer.webkitRequestFullscreen) {
1140
- videoContainer.webkitRequestFullscreen();
1141
- } else if (videoContainer.msRequestFullscreen) {
1142
- videoContainer.msRequestFullscreen();
1143
- }
1144
- }
1145
- }
1146
-
1147
- // 全画面コンテキストメニューを表示
1148
- function showContextMenu(e) {
1149
- if (!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement)) {
1150
- return;
1151
- }
1152
-
1153
- e.preventDefault();
1154
-
1155
- fullscreenContextMenu.style.display = 'block';
1156
- fullscreenContextMenu.style.left = `${e.clientX}px`;
1157
- fullscreenContextMenu.style.top = `${e.clientY}px`;
1158
- }
1159
-
1160
- // 全画面コンテキストメニューを非表示
1161
- function hideContextMenu() {
1162
- fullscreenContextMenu.style.display = 'none';
1163
- }
1164
-
1165
- // 音声/字幕のみモードを切り替え
1166
- function toggleAudioOnlyMode() {
1167
- isAudioOnlyMode = !isAudioOnlyMode;
1168
-
1169
- if (isAudioOnlyMode) {
1170
- videoContainer.classList.add('audio-only-mode');
1171
- videoPlaceholder.style.display = 'flex';
1172
- video.style.display = 'none';
1173
- audioOnlyBtn.textContent = '🎥';
1174
- audioOnlyBtn.title = '動画表示モード';
1175
- } else {
1176
- videoContainer.classList.remove('audio-only-mode');
1177
- videoPlaceholder.style.display = 'none';
1178
- video.style.display = 'block';
1179
- audioOnlyBtn.textContent = '🔈';
1180
- audioOnlyBtn.title = '音声/字幕のみ';
1181
- }
1182
-
1183
- hideContextMenu();
1184
- }
1185
-
1186
- // 全画面時の字幕サイズ調整
1187
- function updateSubtitleScaleForFullscreen() {
1188
- if (document.fullscreenElement || document.webkitFullscreenElement ||
1189
- document.mozFullScreenElement || document.msFullscreenElement) {
1190
- const fullscreenWidth = window.innerWidth;
1191
- const scaleFactor = fullscreenWidth / normalVideoWidth;
1192
- document.documentElement.style.setProperty('--fullscreen-scale', scaleFactor);
1193
- } else {
1194
- document.documentElement.style.setProperty('--fullscreen-scale', 1);
1195
- }
1196
- }
1197
-
1198
- // 字幕表示を切り替え
1199
- function toggleSubtitles() {
1200
- subtitlesEnabled = subtitleToggle.checked;
1201
- if (subtitleTrackElement.track) {
1202
- subtitleTrackElement.track.mode = subtitlesEnabled ? 'showing' : 'hidden';
1203
- }
1204
- subtitleBtn.style.color = subtitlesEnabled ? '#00ccff' : '#666';
1205
- }
1206
-
1207
- // 字幕サイズを更新
1208
- function updateSubtitleSize(value) {
1209
- const size = parseFloat(value);
1210
- subtitleSizeInput.value = size;
1211
- subtitleSize.value = size;
1212
- document.documentElement.style.setProperty('--subtitle-scale', size);
1213
-
1214
- const track = subtitleTrackElement.track;
1215
- if (track && track.cues) {
1216
- for (let i = 0; i < track.cues.length; i++) {
1217
- track.cues[i].line = 90;
1218
- track.cues[i].snapToLines = false;
1219
- }
1220
- }
1221
- }
1222
-
1223
- // 字幕トラックを変更
1224
- function changeSubtitleTrack() {
1225
- const selectedTrack = subtitleTrack.value;
1226
- subtitleTrackElement.src = selectedTrack;
1227
-
1228
- if (video.textTracks.length > 0) {
1229
- video.textTracks[0].mode = selectedTrack && subtitlesEnabled ? 'showing' : 'hidden';
1230
- }
1231
- }
1232
-
1233
- // 字幕メニューを切り替え
1234
- function toggleSubtitleMenu() {
1235
- subtitleToggle.checked = !subtitleToggle.checked;
1236
- toggleSubtitles();
1237
- }
1238
-
1239
- // イベントリスナーを設定
1240
- function setupEventListeners() {
1241
- videoSelect.addEventListener('change', handleVideoChange);
1242
-
1243
- ['input', 'change'].forEach(eventName => {
1244
- speedRange.addEventListener(eventName, () => updatePlaybackRate(speedRange.value));
1245
- volumeRange.addEventListener(eventName, () => updateVolume(volumeRange.value));
1246
- subtitleSize.addEventListener(eventName, () => updateSubtitleSize(subtitleSize.value));
1247
- });
1248
-
1249
- speedInput.addEventListener('input', () => updatePlaybackRate(speedInput.value));
1250
- volumeInput.addEventListener('input', () => updateVolume(volumeInput.value));
1251
- subtitleSizeInput.addEventListener('input', () => updateSubtitleSize(subtitleSizeInput.value));
1252
-
1253
- loopCheckbox.addEventListener('change', () => {
1254
- video.loop = loopCheckbox.checked;
1255
- });
1256
-
1257
- subtitleToggle.addEventListener('change', toggleSubtitles);
1258
- subtitleTrack.addEventListener('change', changeSubtitleTrack);
1259
- subtitleBtn.addEventListener('click', toggleSubtitleMenu);
1260
-
1261
- playPauseBtn.addEventListener('click', togglePlayPause);
1262
- video.addEventListener('click', togglePlayPause);
1263
- video.addEventListener('play', () => playPauseBtn.textContent = '⏸');
1264
- video.addEventListener('pause', () => playPauseBtn.textContent = '▶');
1265
- video.addEventListener('timeupdate', updateProgress);
1266
-
1267
- progressContainer.addEventListener('click', setProgress);
1268
- progressContainer.addEventListener('mousedown', () => isDragging = true);
1269
- document.addEventListener('mouseup', () => isDragging = false);
1270
- progressContainer.addEventListener('mousemove', (e) => {
1271
- if (isDragging) setProgress(e);
1272
- showPreviewTooltip(e);
1273
- });
1274
- progressContainer.addEventListener('mouseenter', showPreviewTooltip);
1275
- progressContainer.addEventListener('mouseleave', hidePreviewTooltip);
1276
-
1277
- volumeBtn.addEventListener('click', toggleMute);
1278
- volumeSlider.addEventListener('input', handleVolumeChange);
1279
- fullscreenBtn.addEventListener('click', goFullscreen);
1280
- audioOnlyBtn.addEventListener('click', toggleAudioOnlyMode);
1281
-
1282
- videoContainer.addEventListener('contextmenu', showContextMenu);
1283
- exitFullscreenBtn.addEventListener('click', () => {
1284
- if (document.exitFullscreen) document.exitFullscreen();
1285
- hideContextMenu();
1286
- });
1287
- showVideoBtn.addEventListener('click', () => {
1288
- if (isAudioOnlyMode) toggleAudioOnlyMode();
1289
- hideContextMenu();
1290
- });
1291
- closeContextMenuBtn.addEventListener('click', hideContextMenu);
1292
- document.addEventListener('click', hideContextMenu);
1293
-
1294
- document.addEventListener('fullscreenchange', updateSubtitleScaleForFullscreen);
1295
- document.addEventListener('webkitfullscreenchange', updateSubtitleScaleForFullscreen);
1296
- document.addEventListener('mozfullscreenchange', updateSubtitleScaleForFullscreen);
1297
- document.addEventListener('MSFullscreenChange', updateSubtitleScaleForFullscreen);
1298
-
1299
- video.addEventListener('loadedmetadata', () => {
1300
- updatePlaybackRate(speedRange.value);
1301
- updateVolume(volumeRange.value);
1302
- updateSubtitleSize(subtitleSize.value);
1303
- video.loop = loopCheckbox.checked;
1304
- toggleSubtitles();
1305
- updateProgress();
1306
- normalVideoWidth = videoContainer.clientWidth;
1307
- createPreviewCanvas();
1308
- });
1309
- }
1310
-
1311
- // CSS変数を初期設定
1312
- document.documentElement.style.setProperty('--subtitle-scale', '1');
1313
- document.documentElement.style.setProperty('--subtitle-border-radius', '10px');
1314
- document.documentElement.style.setProperty('--fullscreen-scale', '1');
1315
-
1316
- // 初期化
1317
- setupHoverTime();
1318
- setupEventListeners();
1319
- createPreviewCanvas();
1320
  </script>
1321
  </body>
1322
-
1323
  </html>
 
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;
 
19
  font-family: "M PLUS Rounded 1c", monospace;
20
  padding: 20px;
21
  margin: 0;
22
+ overflow-x: hidden;
23
  }
24
 
25
  h1 {
 
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;
 
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
  /* カスタム動画コントロール */
 
65
  bottom: 0;
66
  left: 0;
67
  right: 0;
 
68
  padding: 10px;
69
  display: flex;
70
  flex-direction: column;
 
105
  box-shadow: 0 0 5px #00ccff;
106
  }
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  .buttons-container {
109
  display: flex;
110
  align-items: center;
 
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 {
 
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
 
 
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">
 
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">
 
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
 
637
+ // 初期設定
638
+ video.controls = false;
639
+ let isDragging = false;
640
+ let subtitlesEnabled = true;
641
+ let normalVideoWidth = videoContainer.clientWidth;
642
+ let isAudioOnlyMode = false;
643
+ let frameCache = {};
644
+ let isHoveringProgress = false;
645
+ let hoverTimeout;
646
+ let videoBlob = null;
647
+
648
+ // ローディングアニメーションをフェードアウト
649
+ window.addEventListener('load', function() {
650
+ setTimeout(function() {
651
+ const loadingOverlay = document.getElementById('loadingOverlay');
652
+ loadingOverlay.style.opacity = '0';
653
+ setTimeout(function() {
654
+ loadingOverlay.style.display = 'none';
655
+ }, 1000);
656
+ }, 1500);
657
+
658
+ // 動画をBlobとしてキャッシュ
659
+ fetch(video.src)
660
+ .then(response => response.blob())
661
+ .then(blob => {
662
+ videoBlob = blob;
663
+ });
664
+ });
665
+
666
+ // 波紋エフェクトのコードは元のままなので省略...
667
+
668
+ function updatePlaybackRate(value) {
669
+ const speed = parseFloat(value);
670
+ speedInput.value = speed;
671
+ speedRange.value = speed;
672
+ video.playbackRate = speed;
673
+ }
674
+
675
+ function updateVolume(value) {
676
+ const volume = parseFloat(value);
677
+ volumeInput.value = volume;
678
+ volumeRange.value = volume;
679
+ volumeSlider.value = volume;
680
+ video.volume = volume;
681
+
682
+ if (volume === 0) {
683
+ volumeBtn.textContent = '🔇';
684
+ } else if (volume < 0.5) {
685
+ volumeBtn.textContent = '🔈';
686
+ } else {
687
+ volumeBtn.textContent = '🔊';
688
+ }
689
+ }
690
+
691
+ function handleVideoChange() {
692
+ const selected = videoSelect.value;
693
+
694
+ if (selected === 'v-2.mp4') {
695
+ const confirmPlay = confirm("この動画は音量が大きいです。あらかじめ、デバイスの音量をある程度下げてください。また、音割れが起きます。再生してもよろしいですか?");
696
+ if (!confirmPlay) {
697
+ videoSelect.value = video.src.split('/').pop();
698
+ return;
699
+ }
700
+ }
701
+
702
+ video.src = selected;
703
+ video.load();
704
+ video.play().then(() => {
705
+ playPauseBtn.textContent = '⏸';
706
+ }).catch(e => console.log(e));
707
+
708
+ // 新しい動画のBlobをキャッシュ
709
+ fetch(selected)
710
+ .then(response => response.blob())
711
+ .then(blob => {
712
+ videoBlob = blob;
713
+ frameCache = {}; // キャッシュをクリア
714
+ });
715
+ }
716
+
717
+ function togglePlayPause() {
718
+ if (video.paused) {
719
+ video.play();
720
+ playPauseBtn.textContent = '⏸';
721
+ } else {
722
+ video.pause();
723
+ playPauseBtn.textContent = '▶';
724
+ }
725
+ hideContextMenu();
726
+ }
727
+
728
+ function updateProgress() {
729
+ const percent = (video.currentTime / video.duration) * 100;
730
+ progressBar.style.width = `${percent}%`;
731
+
732
+ const currentMinutes = Math.floor(video.currentTime / 60);
733
+ const currentSeconds = Math.floor(video.currentTime % 60).toString().padStart(2, '0');
734
+ const durationMinutes = Math.floor(video.duration / 60);
735
+ const durationSeconds = Math.floor(video.duration % 60).toString().padStart(2, '0');
736
+
737
+ timeDisplay.textContent = `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`;
738
+ }
739
+
740
+ function setProgress(e) {
741
+ const width = progressContainer.clientWidth;
742
+ const clickX = e.offsetX;
743
+ const duration = video.duration;
744
+ video.currentTime = (clickX / width) * duration;
745
+ }
746
+
747
+ function toggleMute() {
748
+ video.muted = !video.muted;
749
+ if (video.muted) {
750
+ volumeBtn.textContent = '🔇';
751
+ volumeSlider.value = 0;
752
+ } else {
753
+ updateVolume(video.volume);
754
+ }
755
+ hideContextMenu();
756
+ }
757
+
758
+ function handleVolumeChange() {
759
+ video.muted = false;
760
+ updateVolume(volumeSlider.value);
761
+ }
762
+
763
+ function goFullscreen() {
764
+ if (
765
+ document.fullscreenElement ||
766
+ document.webkitFullscreenElement ||
767
+ document.msFullscreenElement
768
+ ) {
769
+ // フルスクリーンを解除
770
+ if (document.exitFullscreen) {
771
+ document.exitFullscreen();
772
+ } else if (document.webkitExitFullscreen) {
773
+ document.webkitExitFullscreen();
774
+ } else if (document.msExitFullscreen) {
775
+ document.msExitFullscreen();
776
+ }
777
+ } else {
778
+ // フルスクリーンにする
779
+ if (videoContainer.requestFullscreen) {
780
+ videoContainer.requestFullscreen();
781
+ } else if (videoContainer.webkitRequestFullscreen) {
782
+ videoContainer.webkitRequestFullscreen();
783
+ } else if (videoContainer.msRequestFullscreen) {
784
+ videoContainer.msRequestFullscreen();
785
+ }
786
+ }
787
+ hideContextMenu();
788
+ }
789
 
790
+ // 全画面変更時の字幕サイズ調整
791
+ function updateSubtitleScaleForFullscreen() {
792
+ if (document.fullscreenElement || document.webkitFullscreenElement ||
793
+ document.mozFullScreenElement || document.msFullscreenElement) {
794
+ // 全画面モード
795
+ const fullscreenWidth = window.innerWidth;
796
+ const scaleFactor = fullscreenWidth / normalVideoWidth;
797
+ document.documentElement.style.setProperty('--fullscreen-scale', scaleFactor);
798
+ } else {
799
+ // 通常モード
800
+ document.documentElement.style.setProperty('--fullscreen-scale', 1);
801
+ }
802
+ }
803
+
804
+ // 字幕関連の関数
805
+ function toggleSubtitles() {
806
+ subtitlesEnabled = !subtitlesEnabled;
807
+ subtitleToggle.checked = subtitlesEnabled;
808
+ subtitleTrackElement.track.mode = subtitlesEnabled ? 'showing' : 'hidden';
809
+ subtitleBtn.style.color = subtitlesEnabled ? '#00ccff' : '#666';
810
+ hideContextMenu();
811
+ }
812
+
813
+ function updateSubtitleSize(value) {
814
+ const size = parseFloat(value);
815
+ subtitleSizeInput.value = size;
816
+ subtitleSize.value = size;
817
 
818
+ // 字幕サイズを制御
819
+ document.documentElement.style.setProperty('--subtitle-scale', size);
820
+
821
+ // VTTCueのlineプロパティには数値のみを設定
822
+ const track = subtitleTrackElement.track;
823
+ if (track && track.cues) {
824
+ for (let i = 0; i < track.cues.length; i++) {
825
+ track.cues[i].line = 90;
826
+ track.cues[i].snapToLines = false;
827
+ }
828
  }
829
+ }
830
+
831
+ function changeSubtitleTrack() {
832
+ const selectedTrack = subtitleTrack.value;
833
+ subtitleTrackElement.src = selectedTrack;
834
+ subtitleTrackElement.track.mode = selectedTrack && subtitlesEnabled ? 'showing' : 'hidden';
835
 
836
+ // トラック変更後に再度読み込み
837
+ video.textTracks[0].mode = 'hidden';
838
+ if (selectedTrack) {
839
+ video.textTracks[0].mode = subtitlesEnabled ? 'showing' : 'hidden';
840
+ }
841
+ }
842
+
843
+ function toggleSubtitleMenu() {
844
+ document.getElementById('subtitleToggle').checked ^= true;
845
+ toggleSubtitles();
846
+ }
847
 
848
+ // フレームプレビュー関連
849
+ function showFramePreview(e) {
850
+ if (!videoBlob) return;
851
+
852
+ const progressRect = progressContainer.getBoundingClientRect();
853
+ const clickX = e.clientX - progressRect.left;
854
+ const duration = video.duration;
855
+ const previewTime = (clickX / progressRect.width) * duration;
856
+
857
+ // 時間表示を更新
858
+ const previewMinutes = Math.floor(previewTime / 60);
859
+ const previewSeconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
860
+ frameTime.textContent = `${previewMinutes}:${previewSeconds}`;
861
+
862
+ // プレビュー位置を更新
863
+ framePreview.style.left = `${e.clientX}px`;
864
+ framePreview.style.display = 'block';
865
+
866
+ // キャッシュがあればそれを使う
867
+ const cacheKey = Math.floor(previewTime);
868
+ if (frameCache[cacheKey]) {
869
+ previewImage.src = frameCache[cacheKey];
870
+ return;
871
+ }
872
+
873
+ // フレームを取得
874
+ videoFrames({
875
+ url: URL.createObjectURL(videoBlob),
876
+ count: 1,
877
+ startTime: previewTime,
878
+ endTime: previewTime + 0.1
879
+ }).then((frames) => {
880
+ if (frames.length > 0) {
881
+ previewImage.src = frames[0].image;
882
+ frameCache[cacheKey] = frames[0].image; // キャッシュに保存
883
+ }
884
+ }).catch(err => {
885
+ console.error('Error getting video frame:', err);
886
+ });
887
+ }
 
 
888
 
889
+ function hideFramePreview() {
890
+ framePreview.style.display = 'none';
891
+ }
892
 
893
+ // 右クリックメニュー関連
894
+ function showContextMenu(e) {
895
+ e.preventDefault();
896
+ contextMenu.style.display = 'block';
897
+ contextMenu.style.left = `${e.clientX}px`;
898
+ contextMenu.style.top = `${e.clientY}px`;
899
+ }
900
 
901
+ function hideContextMenu() {
902
+ contextMenu.style.display = 'none';
903
+ }
904
 
905
+ // 音声/字幕のみモード
906
+ function toggleAudioOnlyMode() {
907
+ isAudioOnlyMode = !isAudioOnlyMode;
908
+
909
+ if (isAudioOnlyMode) {
910
+ video.style.opacity = '0';
911
+ audioOnlyModeIndicator.classList.add('active');
912
+ } else {
913
+ video.style.opacity = '1';
914
+ audioOnlyModeIndicator.classList.remove('active');
915
+ }
916
+
917
+ hideContextMenu();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918
  }
919
+
920
+ // イベントリスナー
921
+ videoSelect.addEventListener('change', handleVideoChange);
922
+
923
+ ['input', 'change', 'mouseup'].forEach(eventName => {
924
+ speedRange.addEventListener(eventName, () => updatePlaybackRate(speedRange.value));
925
+ volumeRange.addEventListener(eventName, () => updateVolume(volumeRange.value));
926
+ subtitleSize.addEventListener(eventName, () => updateSubtitleSize(subtitleSize.value));
927
+ });
928
+
929
+ speedInput.addEventListener('input', () => updatePlaybackRate(speedInput.value));
930
+ volumeInput.addEventListener('input', () => updateVolume(volumeInput.value));
931
+ subtitleSizeInput.addEventListener('input', () => updateSubtitleSize(subtitleSizeInput.value));
932
+
933
+ loopCheckbox.addEventListener('change', () => {
934
+ video.loop = loopCheckbox.checked;
935
+ });
936
+
937
+ subtitleToggle.addEventListener('change', toggleSubtitles);
938
+ subtitleTrack.addEventListener('change', changeSubtitleTrack);
939
+ subtitleBtn.addEventListener('click', toggleSubtitleMenu);
940
+
941
+ playPauseBtn.addEventListener('click', togglePlayPause);
942
+ video.addEventListener('click', togglePlayPause);
943
+ video.addEventListener('play', () => playPauseBtn.textContent = '⏸');
944
+ video.addEventListener('pause', () => playPauseBtn.textContent = '▶');
945
+ video.addEventListener('timeupdate', updateProgress);
946
+ progressContainer.addEventListener('click', setProgress);
947
+ progressContainer.addEventListener('mousedown', () => isDragging = true);
948
+ document.addEventListener('mouseup', () => isDragging = false);
949
+ progressContainer.addEventListener('mousemove', (e) => {
950
+ if (isDragging) {
951
+ setProgress(e);
952
+ } else {
953
+ showFramePreview(e);
954
+ }
955
+ });
956
+
957
+ // プログレスバーのホバーイベント
958
+ progressContainer.addEventListener('mouseenter', () => {
959
+ isHoveringProgress = true;
960
+ clearTimeout(hoverTimeout);
961
+ });
962
+
963
+ progressContainer.addEventListener('mouseleave', () => {
964
+ isHoveringProgress = false;
965
+ hoverTimeout = setTimeout(() => {
966
+ if (!isDragging) hideFramePreview();
967
+ }, 300);
968
+ });
969
+
970
+ volumeBtn.addEventListener('click', toggleMute);
971
+ volumeSlider.addEventListener('input', handleVolumeChange);
972
+ fullscreenBtn.addEventListener('click', goFullscreen);
973
+
974
+ // 全画面変更イベントを監視
975
+ document.addEventListener('fullscreenchange', updateSubtitleScaleForFullscreen);
976
+ document.addEventListener('webkitfullscreenchange', updateSubtitleScaleForFullscreen);
977
+ document.addEventListener('mozfullscreenchange', updateSubtitleScaleForFullscreen);
978
+ document.addEventListener('MSFullscreenChange', updateSubtitleScaleForFullscreen);
979
+
980
+ // 右クリックメニューイベント
981
+ videoContainer.addEventListener('contextmenu', showContextMenu);
982
+ document.addEventListener('click', hideContextMenu);
983
+ document.addEventListener('keydown', (e) => {
984
+ if (e.key === 'Escape') hideContextMenu();
985
+ });
986
+
987
+ video.addEventListener('loadedmetadata', () => {
988
+ updatePlaybackRate(speedRange.value);
989
+ updateVolume(volumeRange.value);
990
+ updateSubtitleSize(subtitleSize.value);
991
+ video.loop = loopCheckbox.checked;
992
+ toggleSubtitles();
993
+ updateProgress();
994
+ normalVideoWidth = videoContainer.clientWidth;
995
+ });
996
+
997
+ // CSS変数を設定
998
+ document.documentElement.style.setProperty('--subtitle-scale', '1');
999
+ document.documentElement.style.setProperty('--subtitle-border-radius', '10px');
1000
+ document.documentElement.style.setProperty('--fullscreen-scale', '1');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1001
  </script>
1002
  </body>
 
1003
  </html>