영상 측정 기능 추가
This commit is contained in:
@@ -190,6 +190,111 @@
|
||||
background-color: #d35400;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 12px 0;
|
||||
font-size: 17px;
|
||||
background: #eaf2f8;
|
||||
border: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
color: #2980b9;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
.tab-btn.active {
|
||||
background: #fff;
|
||||
color: #222;
|
||||
border-bottom: 2px solid #2980b9;
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
animation: fadein 0.25s;
|
||||
}
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
.video-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.video-controls {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.video-upload-btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background: #3498db;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1.08em;
|
||||
padding: 12px 32px;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s;
|
||||
box-shadow: 0 1px 4px rgba(52,152,219,0.08);
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
.video-upload-btn:hover, .video-upload-btn:focus {
|
||||
background: #217dbb;
|
||||
}
|
||||
.video-preview {
|
||||
display: none;
|
||||
}
|
||||
.video-preview.active {
|
||||
display: block;
|
||||
}
|
||||
.video-preview-large {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
.video-preview-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.timeline-btns {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.timeline-btn {
|
||||
flex: 1;
|
||||
background: #3498db;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1.08em;
|
||||
padding: 12px 32px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s;
|
||||
box-shadow: 0 1px 4px rgba(52,152,219,0.08);
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
.timeline-btn:hover, .timeline-btn:focus {
|
||||
background: #217dbb;
|
||||
}
|
||||
.reset-frame-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 12px 0 0 0;
|
||||
}
|
||||
.frame-time-info {
|
||||
margin-bottom: 10px;
|
||||
color: #2980b9;
|
||||
}
|
||||
</style>
|
||||
<!-- Google Tag Manager -->
|
||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
@@ -208,6 +313,7 @@
|
||||
page_location: window.location.href
|
||||
});
|
||||
</script>
|
||||
<script src="mp4box.all.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
@@ -222,8 +328,12 @@
|
||||
<p>시간 입력 또는 원하는 속도 입력으로 양방향 계산이 가능합니다. 볼링 레인의 기본 거리는 60피트(18.288미터)입니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="stopwatch">
|
||||
<h3>스톱워치</h3>
|
||||
<div class="tab-container">
|
||||
<button class="tab-btn active" id="tab-stopwatch">스톱워치</button>
|
||||
<button class="tab-btn" id="tab-video">영상</button>
|
||||
</div>
|
||||
<div id="tab-content-stopwatch" class="tab-content active">
|
||||
<h3>스톱워치로 측정</h3>
|
||||
<p>볼링공이 파울라인을 지날 때 '시작'을 누르고, 헤드핀에 도달할 때 '정지'를 누르세요.</p>
|
||||
<div class="stopwatch-display" id="stopwatch-display">00:00.000</div>
|
||||
<div class="stopwatch-container">
|
||||
@@ -237,6 +347,24 @@
|
||||
</button> <!-- [GA] 초기화 버튼 클릭 추적 -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- 영상 업로드/촬영 및 프레임 선택 UI -->
|
||||
<div id="tab-content-video" class="video-section tab-content">
|
||||
<h2>볼링 영상으로 측정</h2>
|
||||
<div class="video-controls">
|
||||
<label class="video-upload-btn">
|
||||
영상 업로드/촬영
|
||||
<input type="file" id="video-upload" accept="video/*" capture="environment" style="display:none;">
|
||||
</label>
|
||||
</div>
|
||||
<div class="video-preview-wrapper">
|
||||
<video id="video-preview" class="video-preview-large" controls></video>
|
||||
</div>
|
||||
<div class="timeline-btns">
|
||||
<button id="set-start-btn" class="timeline-btn" type="button">시작 지점 선택</button>
|
||||
<button id="set-end-btn" class="timeline-btn" type="button">종료 지점 선택</button>
|
||||
</div>
|
||||
<button id="reset-frame-btn" class="timeline-btn reset-frame-btn" type="button">구간 초기화</button>
|
||||
</div>
|
||||
|
||||
<div class="calculator">
|
||||
<div class="input-group">
|
||||
@@ -490,19 +618,19 @@
|
||||
|
||||
// 스톱워치 관련 변수
|
||||
let stopwatchInterval;
|
||||
let startTime;
|
||||
let elapsedTime = 0;
|
||||
let isRunning = false;
|
||||
let wStartTime;
|
||||
let wElapsedTime = 0;
|
||||
let wIsRunning = false;
|
||||
|
||||
// 스톱워치 시작/정지 토글 함수
|
||||
function toggleStopwatch() {
|
||||
const toggleBtn = document.getElementById('toggle-btn');
|
||||
|
||||
if (!isRunning) {
|
||||
if (!wIsRunning) {
|
||||
// 시작
|
||||
startTime = Date.now() - elapsedTime;
|
||||
wStartTime = Date.now() - wElapsedTime;
|
||||
stopwatchInterval = setInterval(updateStopwatch, 10);
|
||||
isRunning = true;
|
||||
wIsRunning = true;
|
||||
|
||||
// 버튼 상태 변경
|
||||
toggleBtn.textContent = '정지';
|
||||
@@ -510,8 +638,8 @@
|
||||
} else {
|
||||
// 정지
|
||||
clearInterval(stopwatchInterval);
|
||||
elapsedTime = Date.now() - startTime;
|
||||
isRunning = false;
|
||||
wElapsedTime = Date.now() - wStartTime;
|
||||
wIsRunning = false;
|
||||
|
||||
// 버튼 상태 변경
|
||||
toggleBtn.textContent = '시작';
|
||||
@@ -521,7 +649,7 @@
|
||||
updateStopwatchDisplay(); // 스톱워치 표시 갱신
|
||||
|
||||
// 스톱워치와 동일한 값을 입력 필드에 설정
|
||||
const exactSeconds = Math.floor((elapsedTime % 60000) / 1000) + ((elapsedTime % 1000) / 1000);
|
||||
const exactSeconds = Math.floor((wElapsedTime % 60000) / 1000) + ((wElapsedTime % 1000) / 1000);
|
||||
document.getElementById('time').value = exactSeconds.toFixed(3);
|
||||
|
||||
calculateSpeed();
|
||||
@@ -531,8 +659,8 @@
|
||||
// 스톱워치 초기화 함수
|
||||
function resetStopwatch() {
|
||||
clearInterval(stopwatchInterval);
|
||||
elapsedTime = 0;
|
||||
isRunning = false;
|
||||
wElapsedTime = 0;
|
||||
wIsRunning = false;
|
||||
updateStopwatchDisplay();
|
||||
|
||||
// 버튼 상태 변경
|
||||
@@ -543,15 +671,15 @@
|
||||
|
||||
// 스톱워치 표시 업데이트 함수
|
||||
function updateStopwatch() {
|
||||
elapsedTime = Date.now() - startTime;
|
||||
wElapsedTime = Date.now() - wStartTime;
|
||||
updateStopwatchDisplay();
|
||||
}
|
||||
|
||||
// 스톱워치 시간 표시 형식 변환 함수
|
||||
function updateStopwatchDisplay() {
|
||||
const minutes = Math.floor(elapsedTime / 60000);
|
||||
const seconds = Math.floor((elapsedTime % 60000) / 1000);
|
||||
const milliseconds = elapsedTime % 1000;
|
||||
const minutes = Math.floor(wElapsedTime / 60000);
|
||||
const seconds = Math.floor((wElapsedTime % 60000) / 1000);
|
||||
const milliseconds = wElapsedTime % 1000;
|
||||
|
||||
const display = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
|
||||
document.getElementById('stopwatch-display').textContent = display;
|
||||
@@ -581,6 +709,136 @@
|
||||
resetStopwatch();
|
||||
});
|
||||
});
|
||||
// --- 탭 전환 기능 개선 ---
|
||||
const tabStopwatch = document.getElementById('tab-stopwatch');
|
||||
const tabVideo = document.getElementById('tab-video');
|
||||
const tabContentStopwatch = document.getElementById('tab-content-stopwatch');
|
||||
const tabContentVideo = document.getElementById('tab-content-video');
|
||||
function activateTab(tab) {
|
||||
[tabStopwatch, tabVideo].forEach(btn => btn.classList.remove('active'));
|
||||
[tabContentStopwatch, tabContentVideo].forEach(c => c.classList.remove('active'));
|
||||
if(tab === 'stopwatch') {
|
||||
tabStopwatch.classList.add('active');
|
||||
tabContentStopwatch.classList.add('active');
|
||||
} else {
|
||||
tabVideo.classList.add('active');
|
||||
tabContentVideo.classList.add('active');
|
||||
}
|
||||
}
|
||||
tabStopwatch.onclick = () => activateTab('stopwatch');
|
||||
tabVideo.onclick = () => activateTab('video');
|
||||
// 최초 로드 시
|
||||
activateTab('stopwatch');
|
||||
|
||||
// --- 영상 업로드/촬영 & 구간(프레임) 선택 기능 (시커바 기반) ---
|
||||
const videoUpload = document.getElementById('video-upload');
|
||||
const videoPreview = document.getElementById('video-preview');
|
||||
const fileNameLabel = document.getElementById('file-name-label');
|
||||
|
||||
const setStartBtn = document.getElementById('set-start-btn');
|
||||
const setEndBtn = document.getElementById('set-end-btn');
|
||||
const resetFrameBtn = document.getElementById('reset-frame-btn');
|
||||
let mediaRecorder, stream = null;
|
||||
let videoFile = null;
|
||||
let fps = 30;
|
||||
let duration = 0;
|
||||
// mp4box.js를 이용한 FPS 추출 함수 (Promise 반환)
|
||||
function getFPSfromMP4(file) {
|
||||
return new Promise((resolve) => {
|
||||
if (!file || !file.type.includes('mp4')) {
|
||||
resolve(30);
|
||||
return;
|
||||
}
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function(e) {
|
||||
const arrayBuffer = e.target.result;
|
||||
// mp4box.js는 appendBuffer의 인자로 fileStart 속성이 반드시 필요함
|
||||
const mp4boxFile = MP4Box.createFile();
|
||||
mp4boxFile.onReady = function(info) {
|
||||
let fps = 30;
|
||||
try {
|
||||
const videoTrack = info.tracks.find(t => t.video);
|
||||
if (videoTrack && videoTrack.video && videoTrack.video.frame_rate) {
|
||||
fps = videoTrack.video.frame_rate;
|
||||
}
|
||||
} catch (e) { fps = 30; }
|
||||
resolve(fps || 30);
|
||||
};
|
||||
arrayBuffer.fileStart = 0;
|
||||
mp4boxFile.appendBuffer(arrayBuffer, 0);
|
||||
mp4boxFile.flush();
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
// startTime, endTime은 오직 여기서만 선언
|
||||
let startTime = null;
|
||||
let endTime = null;
|
||||
const DIST_M = 18.288;
|
||||
|
||||
// 파일 업로드/촬영
|
||||
videoUpload.onchange = function(e) {
|
||||
if (!e.target.files || !e.target.files[0]) {
|
||||
return;
|
||||
}
|
||||
videoFile = e.target.files[0];
|
||||
videoPreview.src = URL.createObjectURL(videoFile);
|
||||
videoPreview.classList.add('active');
|
||||
videoPreview.onloadedmetadata = async function() {
|
||||
duration = videoPreview.duration;
|
||||
fps = await getFPSfromMP4(videoFile);
|
||||
startTime = null;
|
||||
endTime = null;
|
||||
};
|
||||
};
|
||||
|
||||
// 비디오 컨트롤(내장 시커바) 기반 시작/종료 지점 선택
|
||||
setStartBtn.onclick = function() {
|
||||
if (duration === 0) return;
|
||||
startTime = videoPreview.currentTime;
|
||||
setStartBtn.textContent = `시작 지점: ${startTime.toFixed(3)}초`;
|
||||
if (endTime !== null && endTime <= startTime) {
|
||||
endTime = null;
|
||||
setEndBtn.textContent = '종료 지점 선택';
|
||||
}
|
||||
updateSection();
|
||||
};
|
||||
setEndBtn.onclick = function() {
|
||||
if (duration === 0) return;
|
||||
if (startTime === null) {
|
||||
alert('먼저 시작 지점을 선택하세요.');
|
||||
return;
|
||||
}
|
||||
endTime = videoPreview.currentTime;
|
||||
if (endTime <= startTime) {
|
||||
alert('종료 지점은 시작 지점보다 뒤여야 합니다.');
|
||||
endTime = null;
|
||||
return;
|
||||
}
|
||||
setEndBtn.textContent = `종료 지점: ${endTime.toFixed(3)}초`;
|
||||
updateSection();
|
||||
};
|
||||
|
||||
// 구간 초기화 버튼 클릭 시 버튼 텍스트도 초기화
|
||||
resetFrameBtn.onclick = function() {
|
||||
startTime = null;
|
||||
endTime = null;
|
||||
setStartBtn.textContent = '시작 지점 선택';
|
||||
setEndBtn.textContent = '종료 지점 선택';
|
||||
document.getElementById('distance-meters').value = DIST_M;
|
||||
document.getElementById('distance-feet').value = perRound(DIST_M * 3.2808399, 2);
|
||||
document.getElementById('time').value = '2.000';
|
||||
calculateSpeed();
|
||||
};
|
||||
|
||||
// 구간 시간 및 속도 계산
|
||||
function updateSection() {
|
||||
if (startTime !== null && endTime !== null) {
|
||||
const dt = endTime - startTime;
|
||||
document.getElementById('time').value = dt.toFixed(3);
|
||||
calculateSpeed();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user