Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions TIMER_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ポモドーロタイマー

最小構成のポモドーロタイマー実装(ステップ1)

## 機能

- ⏱️ 25分のポモドーロタイマー
- 🔵 円形プログレスバーによる視覚的な進捗表示
- ▶️ 開始/停止ボタン
- 🔄 リセットボタン
- 📊 状態表示(作業中/停止中)
- 🔔 タイマー完了時の通知音

## 使い方

1. `timer.html`をブラウザで開く
2. 「開始」ボタンをクリックしてタイマーを開始
3. タイマーが動作中は「停止」ボタンでタイマーを一時停止できます
4. 「リセット」ボタンで初期状態(25:00)に戻ります
5. タイマーが0:00になると通知音が鳴ります

## ファイル構成

```
timer.html - タイマーのHTML構造
timer.css - スタイリング(円形プログレスバー、レイアウト)
timer.js - タイマーロジック(カウントダウン、状態管理)
```

## 開発サーバーでの実行

```bash
# Pythonの簡易HTTPサーバーを使用
python3 -m http.server 8000

# ブラウザで http://localhost:8000/timer.html を開く
```

## 技術仕様

- **HTML5**: セマンティックなマークアップ
- **CSS3**: CSS変数、グラデーション、トランジション
- **JavaScript ES6+**: クラスベース設計、Web Audio API
- **SVG**: 円形プログレスバーの描画

## ブラウザ対応

- Chrome/Edge (最新版)
- Firefox (最新版)
- Safari (最新版)

## 今後の拡張予定

- 休憩時間の実装
- カスタム時間設定
- 統計・履歴機能
- ダークモード対応
131 changes: 131 additions & 0 deletions timer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
:root {
--progress-circumference: 817;
}

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}

.container {
text-align: center;
background: rgba(255, 255, 255, 0.95);
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

h1 {
color: #333;
margin-bottom: 20px;
font-size: 2em;
}

.status-display {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 30px;
padding: 10px 20px;
border-radius: 25px;
display: inline-block;
transition: all 0.3s ease;
}

.status-display.stopped {
background-color: #e0e0e0;
color: #666;
}

.status-display.running {
background-color: #4CAF50;
color: white;
}

.timer-circle {
position: relative;
display: inline-block;
margin: 20px 0;
}

.progress-ring {
transform: rotate(-90deg);
}

.progress-ring-bg {
fill: none;
stroke: #e0e0e0;
stroke-width: 12;
}

.progress-ring-circle {
fill: none;
stroke: #667eea;
stroke-width: 12;
stroke-linecap: round;
stroke-dasharray: var(--progress-circumference);
stroke-dashoffset: 0;
transition: stroke-dashoffset 1s linear;
}

.timer-display {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 3.5em;
font-weight: bold;
color: #333;
font-family: 'Courier New', monospace;
}

.button-group {
margin-top: 30px;
display: flex;
gap: 20px;
justify-content: center;
}

.btn {
padding: 15px 40px;
font-size: 1.1em;
font-weight: bold;
border: none;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}

.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}

.btn:active {
transform: translateY(0);
}

.btn-start {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}

.btn-reset {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}

.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
31 changes: 31 additions & 0 deletions timer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ポモドーロタイマー</title>
<link rel="stylesheet" href="timer.css">
</head>
<body>
<div class="container">
<h1>ポモドーロタイマー</h1>

<div class="status-display" id="statusDisplay">停止中</div>

<div class="timer-circle">
<svg class="progress-ring" width="300" height="300">
<circle class="progress-ring-bg" cx="150" cy="150" r="130"></circle>
<circle class="progress-ring-circle" id="progressCircle" cx="150" cy="150" r="130"></circle>
</svg>
<div class="timer-display" id="timerDisplay">25:00</div>
</div>

<div class="button-group">
<button class="btn btn-start" id="startBtn">開始</button>
<button class="btn btn-reset" id="resetBtn">リセット</button>
</div>
</div>

<script src="timer.js"></script>
</body>
</html>
156 changes: 156 additions & 0 deletions timer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// タイマーの状態管理
class PomodoroTimer {
constructor() {
// 初期設定: 25分 = 1500秒
this.initialTime = 25 * 60;
this.timeRemaining = this.initialTime;
this.isRunning = false;
this.timerInterval = null;

// DOM要素の取得
this.timerDisplay = document.getElementById('timerDisplay');
this.statusDisplay = document.getElementById('statusDisplay');
this.startBtn = document.getElementById('startBtn');
this.resetBtn = document.getElementById('resetBtn');
this.progressCircle = document.getElementById('progressCircle');

// 円の周の長さを計算 (2πr, r=130)
this.circumference = 2 * Math.PI * 130;

// AudioContextを1回だけ作成
this.audioContext = null;
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
} catch (e) {
console.warn('Web Audio API not supported:', e);
}

// イベントリスナーの設定
this.startBtn.addEventListener('click', () => this.toggleTimer());
this.resetBtn.addEventListener('click', () => this.resetTimer());

// 初期表示の更新
this.updateDisplay();
this.updateStatus();
}

// タイマーの開始/停止を切り替え
toggleTimer() {
if (this.isRunning) {
this.stopTimer();
} else {
this.startTimer();
}
}

// タイマーを開始
startTimer() {
this.isRunning = true;
this.startBtn.textContent = '停止';
this.updateStatus();

// 1秒ごとにカウントダウン
this.timerInterval = setInterval(() => {
if (this.timeRemaining > 0) {
this.timeRemaining--;
this.updateDisplay();
this.updateProgress();
} else {
// タイマー終了
this.stopTimer();
this.playSound();
this.showNotification('ポモドーロ完了!お疲れ様でした!');
}
}, 1000);
}

// タイマーを停止
stopTimer() {
this.isRunning = false;
this.startBtn.textContent = '開始';
this.updateStatus();

if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
}
}

// タイマーをリセット
resetTimer() {
this.stopTimer();
this.timeRemaining = this.initialTime;
this.updateDisplay();
this.updateProgress();
}

// 時間表示を更新
updateDisplay() {
const minutes = Math.floor(this.timeRemaining / 60);
const seconds = this.timeRemaining % 60;

// 2桁表示にフォーマット
const formattedMinutes = String(minutes).padStart(2, '0');
const formattedSeconds = String(seconds).padStart(2, '0');

this.timerDisplay.textContent = `${formattedMinutes}:${formattedSeconds}`;
}

// 状態表示を更新
updateStatus() {
if (this.isRunning) {
this.statusDisplay.textContent = '作業中';
this.statusDisplay.className = 'status-display running';
} else {
this.statusDisplay.textContent = '停止中';
this.statusDisplay.className = 'status-display stopped';
}
}

// 円形プログレスバーを更新
updateProgress() {
const progress = this.timeRemaining / this.initialTime;
const offset = this.circumference * (1 - progress);
this.progressCircle.style.strokeDashoffset = offset;
}

// 通知を表示
showNotification(message) {
// ブラウザの通知APIを試す
if ('Notification' in window && Notification.permission === 'granted') {
new Notification('ポモドーロタイマー', { body: message });
} else {
// フォールバックとしてコンソールログ
console.log('Timer Complete:', message);
}
}

// 音を鳴らす
playSound() {
if (!this.audioContext) return;

try {
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();

oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);

oscillator.frequency.value = 800;
oscillator.type = 'sine';

gainNode.gain.setValueAtTime(0.3, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.5);

oscillator.start(this.audioContext.currentTime);
oscillator.stop(this.audioContext.currentTime + 0.5);
} catch (e) {
console.warn('Failed to play sound:', e);
}
}
}

// ページ読み込み時にタイマーを初期化
document.addEventListener('DOMContentLoaded', () => {
new PomodoroTimer();
});