847 lines
32 KiB
HTML
847 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<title>레인보우 - 볼링공 속도 계산기</title>
|
|
<style>
|
|
body {
|
|
font-family: 'Nanum Gothic', Arial, sans-serif;
|
|
background-color: #f5f5f5;
|
|
margin: 0;
|
|
padding: 20px;
|
|
color: #333;
|
|
touch-action: pan-y;
|
|
}
|
|
.container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
background-color: #fff;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
}
|
|
h1 {
|
|
color: #2c3e50;
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
}
|
|
.description {
|
|
background-color: #eaf2f8;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
margin-bottom: 20px;
|
|
line-height: 1.6;
|
|
}
|
|
.calculator {
|
|
background-color: #f9f9f9;
|
|
padding: 20px;
|
|
border-radius: 5px;
|
|
border: 1px solid #ddd;
|
|
}
|
|
.input-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
label {
|
|
display: inline-block;
|
|
margin-bottom: 5px;
|
|
font-weight: bold;
|
|
}
|
|
input[type="text"], input[type="number"] {
|
|
width: 100px;
|
|
padding: 6px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
box-sizing: border-box;
|
|
text-align: right;
|
|
}
|
|
|
|
.result-input {
|
|
width: 100px;
|
|
text-align: right;
|
|
}
|
|
|
|
.distance-container {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 20px;
|
|
}
|
|
|
|
.distance-half {
|
|
flex: 1;
|
|
}
|
|
.buttons {
|
|
margin: 20px 0;
|
|
text-align: center;
|
|
}
|
|
button {
|
|
background-color: #3498db;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
margin: 0 10px;
|
|
}
|
|
button:hover {
|
|
background-color: #2980b9;
|
|
}
|
|
.results {
|
|
margin-top: 20px;
|
|
background-color: #f0f8ff;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
border: 1px solid #b3d8ff;
|
|
}
|
|
.result-item {
|
|
margin-bottom: 10px;
|
|
font-size: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.result-item span:first-child {
|
|
min-width: 120px;
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.result-item .unit {
|
|
min-width: 40px;
|
|
text-align: left;
|
|
margin-left: 5px;
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.result-item {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.result-item span:first-child {
|
|
min-width: 100px;
|
|
}
|
|
|
|
input[type="text"], input[type="number"], .result-input {
|
|
width: 90px;
|
|
}
|
|
}
|
|
|
|
.result-value {
|
|
font-weight: bold;
|
|
color: #2980b9;
|
|
margin-left: 10px;
|
|
}
|
|
footer {
|
|
margin-top: 30px;
|
|
text-align: center;
|
|
font-size: 14px;
|
|
color: #777;
|
|
}
|
|
.stopwatch {
|
|
background-color: #e8f4fc;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
margin-bottom: 20px;
|
|
border: 1px solid #b3d8ff;
|
|
}
|
|
.stopwatch-display {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
margin: 10px 0;
|
|
color: #2c3e50;
|
|
font-family: monospace;
|
|
}
|
|
.stopwatch-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
.toggle-btn {
|
|
width: 120px;
|
|
height: 120px;
|
|
border-radius: 50%;
|
|
background-color: #27ae60;
|
|
font-size: 22px;
|
|
font-weight: bold;
|
|
margin-bottom: 15px;
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
transition: all 0.3s ease;
|
|
}
|
|
.toggle-btn.running {
|
|
background-color: #e74c3c;
|
|
}
|
|
.toggle-btn:hover {
|
|
background-color: #219653;
|
|
transform: scale(1.05);
|
|
}
|
|
.toggle-btn.running:hover {
|
|
background-color: #c0392b;
|
|
}
|
|
.reset-btn {
|
|
background-color: #f39c12;
|
|
font-size: 16px;
|
|
padding: 8px 16px;
|
|
margin-top: 10px;
|
|
}
|
|
.reset-btn:hover {
|
|
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 {
|
|
text-align: center;
|
|
margin-bottom: 10px;
|
|
color: #2980b9;
|
|
font-size: 1em;
|
|
}
|
|
</style>
|
|
<!-- Google Tag Manager -->
|
|
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
|
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
|
})(window,document,'script','dataLayer','GTM-PPDRKT7Q');</script>
|
|
<!-- End Google Tag Manager -->
|
|
<script>
|
|
// 페이지 진입 시 GA 페이지뷰 이벤트 전송
|
|
window.dataLayer = window.dataLayer || [];
|
|
window.dataLayer.push({
|
|
event: 'page_view',
|
|
page_title: document.title,
|
|
page_path: window.location.pathname,
|
|
page_location: window.location.href
|
|
});
|
|
</script>
|
|
<script src="mp4box.all.min.js"></script>
|
|
</head>
|
|
<body>
|
|
<!-- Google Tag Manager (noscript) -->
|
|
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PPDRKT7Q"
|
|
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
|
<!-- End Google Tag Manager (noscript) -->
|
|
<div class="container">
|
|
<h1>볼링공 속도 계산기</h1>
|
|
|
|
<div class="description">
|
|
<p>이 계산기는 볼링공이 파울라인에서 헤드핀까지 이동하는 속도를 계산합니다.</p>
|
|
<p>시간 입력 또는 원하는 속도 입력으로 양방향 계산이 가능합니다. 볼링 레인의 기본 거리는 60피트(18.288미터)입니다.</p>
|
|
</div>
|
|
|
|
<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">
|
|
<button class="toggle-btn" id="toggle-btn"
|
|
onclick="dataLayer.push({event:'button_click', event_category:'볼스피드', event_label:'스톱워치'});">
|
|
시작
|
|
</button> <!-- [GA] 시작 버튼 클릭 추적 -->
|
|
<button class="reset-btn" id="reset-btn"
|
|
onclick="dataLayer.push({event:'button_click', event_category:'볼스피드', event_label:'초기화'});">
|
|
초기화
|
|
</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/*" style="display:none;">
|
|
</label>
|
|
</div>
|
|
<div class="video-preview-wrapper">
|
|
<video id="video-preview" class="video-preview-large" controls></video>
|
|
</div>
|
|
<p class="frame-time-info">영상을 이동하여 각 지점 선택 버튼을 눌러주세요.</p>
|
|
<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">
|
|
<label for="time">볼링공 이동 시간:</label>
|
|
<input type="number" id="time" step="0.001" value="2.000" min="0.001"> 초
|
|
</div>
|
|
|
|
<div class="input-group distance-container">
|
|
<div class="distance-half">
|
|
<label for="distance-meters">거리 (미터):</label>
|
|
<input type="number" id="distance-meters" value="18.288" step="0.001" min="0.1" onchange="updateFeet()"> 미터
|
|
</div>
|
|
<div class="distance-half">
|
|
<label for="distance-feet">거리 (피트):</label>
|
|
<input type="number" id="distance-feet" value="60.00" step="0.01" min="1" onchange="updateMeters()"> 피트
|
|
</div>
|
|
</div>
|
|
|
|
<div class="buttons">
|
|
<button onclick="dataLayer.push({event:'button_click', event_category:'볼스피드', event_label:'계산하기'}); calculateSpeed()">계산하기</button> <!-- [GA] 계산하기 버튼 클릭 추적 -->
|
|
<button onclick="dataLayer.push({event:'button_click', event_category:'볼스피드', event_label:'계산초기화'}); resetValues()">초기화</button> <!-- [GA] 계산초기화 버튼 클릭 추적 -->
|
|
</div>
|
|
|
|
<div class="results" id="results">
|
|
<div class="result-item">
|
|
<span>시속 (킬로미터):</span>
|
|
<input type="number" class="result-input" id="kph" value="0" step="0.01" min="0" onchange="calculateTimeFromKPH()">
|
|
<span class="unit">KPH</span>
|
|
</div>
|
|
<div class="result-item">
|
|
<span>시속 (마일):</span>
|
|
<input type="number" class="result-input" id="mph" value="0" step="0.01" min="0" onchange="calculateTimeFromMPH()">
|
|
<span class="unit">MPH</span>
|
|
</div>
|
|
<div class="result-item">
|
|
<span>초속 (피트):</span>
|
|
<input type="number" class="result-input" id="fps" value="0" step="0.01" min="0" onchange="calculateTimeFromFPS()">
|
|
<span class="unit">FPS</span>
|
|
</div>
|
|
<div class="result-item">
|
|
<span>초속 (미터):</span>
|
|
<input type="number" class="result-input" id="mps" value="0" step="0.01" min="0" onchange="calculateTimeFromMPS()">
|
|
<span class="unit">MPS</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer>
|
|
<p> 2025 볼링공 속도 계산기 © <a href="https://lanebow.com" target="_blank">레인보우 - 볼링클럽 매니저</a></p>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
// 소수점 자릿수 반올림 함수
|
|
function perRound(num, precision) {
|
|
precision = precision || 3;
|
|
precision = parseInt(precision);
|
|
var result1 = num * Math.pow(10, precision);
|
|
var result2 = Math.round(result1);
|
|
var result3 = result2 / Math.pow(10, precision);
|
|
return zerosPad(result3, precision);
|
|
}
|
|
|
|
// 소수점 자릿수 0 채우기 함수
|
|
function zerosPad(rndVal, decPlaces) {
|
|
var valStrg = rndVal.toString();
|
|
var decLoc = valStrg.indexOf(".");
|
|
|
|
if (decLoc == -1) {
|
|
decPartLen = 0;
|
|
valStrg += decPlaces > 0 ? "." : "";
|
|
} else {
|
|
decPartLen = valStrg.length - decLoc - 1;
|
|
}
|
|
|
|
var totalPad = decPlaces - decPartLen;
|
|
if (totalPad > 0) {
|
|
for (var i = 1; i <= totalPad; i++) {
|
|
valStrg += "0";
|
|
}
|
|
}
|
|
return valStrg;
|
|
}
|
|
|
|
// 미터를 피트로 변환
|
|
function updateFeet() {
|
|
var meters = document.getElementById("distance-meters").value;
|
|
var feet = perRound(meters * 3.2808399, 2);
|
|
document.getElementById("distance-feet").value = feet;
|
|
}
|
|
|
|
// 피트를 미터로 변환
|
|
function updateMeters() {
|
|
var feet = document.getElementById("distance-feet").value;
|
|
var meters = perRound(feet * 0.3048, 3);
|
|
document.getElementById("distance-meters").value = meters;
|
|
}
|
|
|
|
// 속도 계산 함수
|
|
function calculateSpeed() {
|
|
var time = parseFloat(document.getElementById("time").value);
|
|
var distance = parseFloat(document.getElementById("distance-feet").value);
|
|
|
|
if (time <= 0) {
|
|
alert("시간은 0보다 커야 합니다.");
|
|
return;
|
|
}
|
|
|
|
if (distance <= 0) {
|
|
alert("거리는 0보다 커야 합니다.");
|
|
return;
|
|
}
|
|
|
|
// 속도 계산 (원본 코드의 공식 사용)
|
|
var mph = perRound((distance / time) * (3600 / 5280), 2);
|
|
var kph = perRound((distance / time) * (3600 * 0.0003048), 2);
|
|
var fps = perRound(distance / time, 2);
|
|
var mps = perRound((distance * 0.3048) / time, 2);
|
|
|
|
// 결과 표시 (입력 필드에 값 설정)
|
|
document.getElementById("mph").value = mph;
|
|
document.getElementById("kph").value = kph;
|
|
document.getElementById("fps").value = fps;
|
|
document.getElementById("mps").value = mps;
|
|
}
|
|
|
|
// 시속(KPH)에서 시간 계산
|
|
function calculateTimeFromKPH() {
|
|
var kph = parseFloat(document.getElementById("kph").value);
|
|
var distance = parseFloat(document.getElementById("distance-meters").value);
|
|
|
|
if (kph <= 0) {
|
|
alert("속도는 0보다 커야 합니다.");
|
|
return;
|
|
}
|
|
|
|
// KPH에서 시간(초) 계산: 거리(m) / 속도(m/s) = 시간(s)
|
|
// KPH를 m/s로 변환: kph * 1000 / 3600
|
|
var speedInMps = kph * 1000 / 3600;
|
|
var time = distance / speedInMps;
|
|
|
|
// 시간 설정 및 다른 속도 값 업데이트
|
|
document.getElementById("time").value = perRound(time, 3);
|
|
|
|
// 다른 속도 단위 계산
|
|
var distanceFeet = parseFloat(document.getElementById("distance-feet").value);
|
|
var mph = perRound((distanceFeet / time) * (3600 / 5280), 2);
|
|
var fps = perRound(distanceFeet / time, 2);
|
|
var mps = perRound(speedInMps, 2);
|
|
|
|
// 다른 속도 필드 업데이트 (무한 루프 방지를 위해 이벤트 발생 없이)
|
|
document.getElementById("mph").value = mph;
|
|
document.getElementById("fps").value = fps;
|
|
document.getElementById("mps").value = mps;
|
|
}
|
|
|
|
// 시속(MPH)에서 시간 계산
|
|
function calculateTimeFromMPH() {
|
|
var mph = parseFloat(document.getElementById("mph").value);
|
|
var distance = parseFloat(document.getElementById("distance-feet").value);
|
|
|
|
if (mph <= 0) {
|
|
alert("속도는 0보다 커야 합니다.");
|
|
return;
|
|
}
|
|
|
|
// MPH에서 시간(초) 계산: 거리(feet) / 속도(feet/s) = 시간(s)
|
|
// MPH를 feet/s로 변환: mph * 5280 / 3600
|
|
var speedInFps = mph * 5280 / 3600;
|
|
var time = distance / speedInFps;
|
|
|
|
// 시간 설정 및 다른 속도 값 업데이트
|
|
document.getElementById("time").value = perRound(time, 3);
|
|
|
|
// 다른 속도 단위 계산
|
|
var kph = perRound(mph * 1.60934, 2);
|
|
var fps = perRound(speedInFps, 2);
|
|
var mps = perRound(speedInFps * 0.3048, 2);
|
|
|
|
// 다른 속도 필드 업데이트
|
|
document.getElementById("kph").value = kph;
|
|
document.getElementById("fps").value = fps;
|
|
document.getElementById("mps").value = mps;
|
|
}
|
|
|
|
// 초속(FPS)에서 시간 계산
|
|
function calculateTimeFromFPS() {
|
|
var fps = parseFloat(document.getElementById("fps").value);
|
|
var distance = parseFloat(document.getElementById("distance-feet").value);
|
|
|
|
if (fps <= 0) {
|
|
alert("속도는 0보다 커야 합니다.");
|
|
return;
|
|
}
|
|
|
|
// FPS에서 시간(초) 계산: 거리(feet) / 속도(feet/s) = 시간(s)
|
|
var time = distance / fps;
|
|
|
|
// 시간 설정 및 다른 속도 값 업데이트
|
|
document.getElementById("time").value = perRound(time, 3);
|
|
|
|
// 다른 속도 단위 계산
|
|
var mph = perRound(fps * 3600 / 5280, 2);
|
|
var kph = perRound(fps * 0.3048 * 3.6, 2);
|
|
var mps = perRound(fps * 0.3048, 2);
|
|
|
|
// 다른 속도 필드 업데이트
|
|
document.getElementById("mph").value = mph;
|
|
document.getElementById("kph").value = kph;
|
|
document.getElementById("mps").value = mps;
|
|
}
|
|
|
|
// 초속(MPS)에서 시간 계산
|
|
function calculateTimeFromMPS() {
|
|
var mps = parseFloat(document.getElementById("mps").value);
|
|
var distance = parseFloat(document.getElementById("distance-meters").value);
|
|
|
|
if (mps <= 0) {
|
|
alert("속도는 0보다 커야 합니다.");
|
|
return;
|
|
}
|
|
|
|
// MPS에서 시간(초) 계산: 거리(m) / 속도(m/s) = 시간(s)
|
|
var time = distance / mps;
|
|
|
|
// 시간 설정 및 다른 속도 값 업데이트
|
|
document.getElementById("time").value = perRound(time, 3);
|
|
|
|
// 다른 속도 단위 계산
|
|
var distanceFeet = parseFloat(document.getElementById("distance-feet").value);
|
|
var fps = perRound(mps / 0.3048, 2);
|
|
var mph = perRound(mps * 2.23694, 2);
|
|
var kph = perRound(mps * 3.6, 2);
|
|
|
|
// 다른 속도 필드 업데이트
|
|
document.getElementById("mph").value = mph;
|
|
document.getElementById("kph").value = kph;
|
|
document.getElementById("fps").value = fps;
|
|
}
|
|
|
|
// 초기화 함수
|
|
function resetValues() {
|
|
document.getElementById("time").value = "2.000";
|
|
document.getElementById("distance-feet").value = "60.00";
|
|
document.getElementById("distance-meters").value = "18.288";
|
|
document.getElementById("mph").value = "0";
|
|
document.getElementById("kph").value = "0";
|
|
document.getElementById("fps").value = "0";
|
|
document.getElementById("mps").value = "0";
|
|
}
|
|
|
|
// 스톱워치 관련 변수
|
|
let stopwatchInterval;
|
|
let wStartTime;
|
|
let wElapsedTime = 0;
|
|
let wIsRunning = false;
|
|
|
|
// 스톱워치 시작/정지 토글 함수
|
|
function toggleStopwatch() {
|
|
const toggleBtn = document.getElementById('toggle-btn');
|
|
|
|
if (!wIsRunning) {
|
|
// 시작
|
|
wStartTime = Date.now() - wElapsedTime;
|
|
stopwatchInterval = setInterval(updateStopwatch, 10);
|
|
wIsRunning = true;
|
|
|
|
// 버튼 상태 변경
|
|
toggleBtn.textContent = '정지';
|
|
toggleBtn.classList.add('running');
|
|
} else {
|
|
// 정지
|
|
clearInterval(stopwatchInterval);
|
|
wElapsedTime = Date.now() - wStartTime;
|
|
wIsRunning = false;
|
|
|
|
// 버튼 상태 변경
|
|
toggleBtn.textContent = '시작';
|
|
toggleBtn.classList.remove('running');
|
|
|
|
// 스톱워치 정지 시 정확한 시간 계산
|
|
updateStopwatchDisplay(); // 스톱워치 표시 갱신
|
|
|
|
// 스톱워치와 동일한 값을 입력 필드에 설정
|
|
const exactSeconds = Math.floor((wElapsedTime % 60000) / 1000) + ((wElapsedTime % 1000) / 1000);
|
|
document.getElementById('time').value = exactSeconds.toFixed(3);
|
|
|
|
calculateSpeed();
|
|
}
|
|
}
|
|
|
|
// 스톱워치 초기화 함수
|
|
function resetStopwatch() {
|
|
clearInterval(stopwatchInterval);
|
|
wElapsedTime = 0;
|
|
wIsRunning = false;
|
|
updateStopwatchDisplay();
|
|
|
|
// 버튼 상태 변경
|
|
const toggleBtn = document.getElementById('toggle-btn');
|
|
toggleBtn.textContent = '시작';
|
|
toggleBtn.classList.remove('running');
|
|
}
|
|
|
|
// 스톱워치 표시 업데이트 함수
|
|
function updateStopwatch() {
|
|
wElapsedTime = Date.now() - wStartTime;
|
|
updateStopwatchDisplay();
|
|
}
|
|
|
|
// 스톱워치 시간 표시 형식 변환 함수
|
|
function updateStopwatchDisplay() {
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 페이지 로드 시 초기 계산 및 이벤트 리스너 설정
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
calculateSpeed();
|
|
|
|
// 스톱워치 버튼에 이벤트 리스너 추가
|
|
const toggleBtn = document.getElementById('toggle-btn');
|
|
const resetBtn = document.getElementById('reset-btn');
|
|
|
|
// mousedown과 touchstart 이벤트를 모두 추가하여 모바일과 데스크톱 모두 지원
|
|
toggleBtn.addEventListener('mousedown', toggleStopwatch);
|
|
toggleBtn.addEventListener('touchstart', function(e) {
|
|
e.preventDefault(); // 화면 스크롤 방지
|
|
toggleStopwatch();
|
|
});
|
|
|
|
resetBtn.addEventListener('mousedown', resetStopwatch);
|
|
resetBtn.addEventListener('touchstart', function(e) {
|
|
e.preventDefault();
|
|
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> |