<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>経過時間お知らせタイマー</title>
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background-color: #f0f2f5;
padding: 20px;
box-sizing: border-box;
}
.container {
text-align: center;
background: white;
padding: 2.5rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
width: 100%;
max-width: 360px;
box-sizing: border-box;
}
.label {
font-size: 1.2rem;
color: #666;
margin-bottom: 0.5rem;
}
#timer {
font-size: 3.5rem;
font-weight: bold;
margin-bottom: 1.5rem;
color: #333;
font-variant-numeric: tabular-nums;
}
.settings {
margin-bottom: 1.5rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
text-align: left;
}
.settings label {
display: block;
font-size: 0.9rem;
color: #555;
margin-bottom: 0.5rem;
font-weight: bold;
}
.speed-control {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 1.2rem;
}
input[type="range"] {
flex: 1;
}
#speedVal {
font-weight: bold;
min-width: 2rem;
}
.toggle-group {
display: flex;
gap: 6px;
margin-bottom: 1.2rem;
flex-wrap: wrap;
}
.toggle-group button {
flex: 1;
min-width: 60px;
font-size: 0.85rem;
padding: 0.5rem 0.25rem;
background-color: #e9ecef;
color: #495057;
border: 1px solid #ced4da;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.toggle-group button.active {
background-color: #007bff;
color: white;
border-color: #007bff;
font-weight: bold;
}
.btn-group {
display: flex;
gap: 10px;
justify-content: center;
margin-bottom: 10px;
}
.main-btn {
font-size: 1.1rem;
padding: 0.75rem 1.5rem;
cursor: pointer;
color: white;
border: none;
border-radius: 6px;
transition: background-color 0.2s;
}
#startBtn { background-color: #28a745; flex: 1; }
#startBtn:hover { background-color: #218838; }
#stopBtn { background-color: #dc3545; flex: 1; }
#stopBtn:hover { background-color: #c82333; }
#sampleBtn {
background-color: #6c757d;
width: 100%;
font-size: 1rem;
padding: 0.5rem;
border: none;
border-radius: 6px;
color: white;
cursor: pointer;
}
#sampleBtn:hover { background-color: #5a6268; }
button:disabled {
background-color: #ccc !important;
border-color: #ccc !important;
color: white !important;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="container">
<div class="label">経過時間</div>
<div id="timer">00:00:00</div>
<div class="settings">
<!-- アナウンス間隔の設定 -->
<label>お知らせの間隔</label>
<div class="toggle-group">
<button id="interval5" class="active" onclick="setIntervalMinutes(5)">5分毎</button>
<button id="interval10" onclick="setIntervalMinutes(10)">10分毎</button>
<button id="interval15" onclick="setIntervalMinutes(15)">15分毎</button>
<button id="interval30" onclick="setIntervalMinutes(30)">30分毎</button>
</div>
<!-- 声の種類の設定 -->
<label>声の種類</label>
<div class="toggle-group">
<button id="femaleBtn" class="active" onclick="setGender('female')">?? 女性</button>
<button id="maleBtn" onclick="setGender('male')">?? 男性</button>
</div>
<!-- 読み上げ速度の設定 -->
<label for="speedRange">読み上げ速度(標準: 1.0)</label>
<div class="speed-control">
<input type="range" id="speedRange" min="0.5" max="1.5" step="0.1" value="1.0" oninput="changeSpeed(this.value)">
<span id="speedVal">1.0</span>
</div>
</div>
<div class="btn-group">
<button id="startBtn" class="main-btn" onclick="startTimer()">スタート</button>
<button id="stopBtn" class="main-btn" onclick="stopTimer()" disabled>ストップ</button>
</div>
<button id="sampleBtn" onclick="playSample()">?? サンプル音声を聴く</button>
</div>
<script>
let timeElapsed = 0;
let timerId = null;
let currentSpeed = 1.0;
let currentGender = 'female';
let announceIntervalSeconds = 300; // デフォルトは5分(300秒)
const MAX_TIME = 12 * 60 * 60;
let voicesList = [];
let selectedVoice = null;
function loadVoices() {
voicesList = window.speechSynthesis.getVoices();
updateSelectedVoice();
}
function updateSelectedVoice() {
const jaVoices = voicesList.filter(v => v.lang === 'ja-JP');
if (currentGender === 'male') {
selectedVoice = jaVoices.find(v => v.name.includes('Ichiro')) ||
jaVoices.find(v => v.name.includes('Otoya')) ||
jaVoices.find(v => v.name.toLowerCase().includes('male')) ||
jaVoices;
} else {
selectedVoice = jaVoices.find(v => v.name.includes('Google')) ||
jaVoices.find(v => v.name.includes('Kyoko')) ||
jaVoices.find(v => v.name.includes('Ayumi')) ||
jaVoices.find(v => v.name.toLowerCase().includes('female')) ||
jaVoices;
}
}
window.speechSynthesis.onvoiceschanged = loadVoices;
loadVoices();
// 間隔を切り替える関数
function setIntervalMinutes(minutes) {
announceIntervalSeconds = minutes * 60;
// ボタンのアクティブ状態を切り替え
[5, 10, 15, 30].forEach(m => {
document.getElementById(`interval${m}`).classList.toggle('active', m === minutes);
});
// 切り替えたことを音声でプレビュー
window.speechSynthesis.cancel();
window.speechSynthesis.speak(createUtterance(`${minutes}分ごとにお知らせします。`));
}
function setGender(gender) {
currentGender = gender;
document.getElementById('femaleBtn').classList.toggle('active', gender === 'female');
document.getElementById('maleBtn').classList.toggle('active', gender === 'male');
updateSelectedVoice();
window.speechSynthesis.cancel();
const msg = gender === 'female' ? "女性の声です。" : "男性の声です。";
window.speechSynthesis.speak(createUtterance(msg));
}
function changeSpeed(val) {
currentSpeed = parseFloat(val);
document.getElementById('speedVal').textContent = currentSpeed.toFixed(1);
}
function updateDisplay() {
const hours = Math.floor(timeElapsed / 3600);
const minutes = Math.floor((timeElapsed % 3600) / 60);
const seconds = timeElapsed % 60;
document.getElementById('timer').textContent =
`${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
function startTimer() {
window.speechSynthesis.speak(new SpeechSynthesisUtterance(''));
document.getElementById('startBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;
timerId = setInterval(() => {
timeElapsed++;
updateDisplay();
// 設定された間隔ごとに音声でお知らせ
if (timeElapsed % announceIntervalSeconds === 0 && timeElapsed > 0) {
speak(timeElapsed);
}
if (timeElapsed >= MAX_TIME) {
stopTimer();
speakFinal();
}
}, 1000);
}
function stopTimer() {
clearInterval(timerId);
document.getElementById('startBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
}
function createUtterance(text) {
const uttr = new SpeechSynthesisUtterance(text);
uttr.lang = "ja-JP";
uttr.rate = currentSpeed;
if (selectedVoice) {
uttr.voice = selectedVoice;
}
return uttr;
}
function speak(secondsTotal) {
const hours = Math.floor(secondsTotal / 3600);
const minutes = Math.floor((secondsTotal % 3600) / 60);
let text = "";
if (hours > 0) text += `${hours}時間`;
if (minutes > 0) text += `${minutes}分`;
text += "経過しました。";
window.speechSynthesis.speak(createUtterance(text));
}
function speakFinal() {
window.speechSynthesis.speak(createUtterance("制限時間の12時間が経過しました。"));
}
function playSample() {
window.speechSynthesis.cancel();
const intervalText = announceIntervalSeconds / 60;
window.speechSynthesis.speak(createUtterance(`現在は、${intervalText}分ごとに、この声でお知らせします。`));
}
</script>
</body>
</html>