【錯覚時間】イリュージョンタイマー作ってみた【Python】

## 背景・目的
ランニングマシンでランニングをしているとき、ふと「5分や10分を区切りにして頑張ろう」と思うことがあります。実際に、その区切りが過ぎるまでは頑張ろうという意識が働き、走り続けることができるからです。
これなら、表示時間を短くし、実際の経過時間を長く設定することで、体感時間を短く感じさせ、実際にはより長く頑張ることができるのではないかと考えました。
そこで、「実際の経過時間と表示される時間が異なる」というイリュージョンタイマーを作成しようと思い立ちました。

## 完成イメージ
![起動画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3692990/dc282428-9db8-0504-99e2-fbb7ea136a8e.png)

- 機能概要:
  - 時間の設定（見かけ上の時間と実際の時間）
  - 開始/停止/再開機能
  - リセット機能
  - タイマー終了時の通知機能

## タイマーの仕様

### 設定時間と見かけ上の時間

このタイマーは、設定された「見かけ上の時間（表示される時間）」と、実際に経過している「実際の経過時間（実時間）」が異なる仕様です。

#### 見かけ上の時間と実際の経過時間の算出方法
1. **見かけ上の時間**:
   - ユーザーが設定した時間（例えば、`20分`）が見かけ上のタイマーに表示される時間として使用されます。
   
2. **実際の経過時間**:
   - 実際には、ユーザーが設定した時間よりも長め（例えば、`30分`）の時間が実際に経過します。
   - この差を`time_ratio`（時間比率）で計算します。
   - `time_ratio`は以下の式で計算されます：
     ```python
     time_ratio = 実際の経過時間 / 見かけ上の時間
     ```
     例えば、`見かけ上の時間`が`20分`、`実際の経過時間`が`30分`の場合、`time_ratio`は`30 / 20 = 1.5`となります。

3. **動作の仕組み**:
   - タイマーの内部では、実際の経過時間が`time_ratio`に基づいて更新されます。これにより、表示時間は短く（例えば、`20分`）表示される一方で、実際の経過時間はその時間に対して`1.5倍`（`30分`）進行することになります。


##  ソースコード全体
```python
import tkinter as tk
from tkinter import ttk, messagebox


class IllusionTimerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("イリュージョンタイマー")
        self.root.geometry("400x300")
        self.illusion_time = tk.IntVar(value=20)
        self.actually_time = tk.IntVar(value=30)
        self.is_running = False
        self.time_ratio = 1
        self.remaining_seconds = 0

        self.main_frame = ttk.Frame(self.root)
        self.main_frame.place(relx=0.5, rely=0.5, anchor="center")

        self.setup_gui()
        self.reset_timer()

    def setup_gui(self):
        ttk.Label(self.main_frame, text="見かけ上の時間 (分):").grid(row=0, column=0, padx=5, pady=5, sticky="e")
        ttk.Entry(self.main_frame, textvariable=self.illusion_time, width=5).grid(row=0, column=1, padx=5, pady=5)

        ttk.Label(self.main_frame, text="実際の経過時間 (分):").grid(row=1, column=0, padx=5, pady=5, sticky="e")
        ttk.Entry(self.main_frame, textvariable=self.actually_time, width=5).grid(row=1, column=1, padx=5, pady=5)

        self.timer_label = ttk.Label(self.main_frame, text="00:00", font=("Arial", 40))
        self.timer_label.grid(row=2, column=0, columnspan=2, pady=20)

        self.start_stop_button = ttk.Button(self.main_frame, text="開始", command=self.toggle_timer)
        self.start_stop_button.grid(row=3, column=0, columnspan=2, pady=10)

        self.reset_button = ttk.Button(self.main_frame, text="リセット", command=self.reset_timer)
        self.reset_button.grid(row=4, column=0, columnspan=2, pady=10)

    def toggle_timer(self):
        if self.is_running:
            self.is_running = False
            self.start_stop_button.config(text="再開")
        else:
            self.remaining_seconds = self.illusion_time.get() * 60
            self.start_timer()

    def start_timer(self):
        self.is_running = True
        self.start_stop_button.config(text="停止")
        self.run_timer()

    def initialize_timer(self):
        self.time_ratio = self.actually_time.get() / self.illusion_time.get()

    def run_timer(self):
        if self.remaining_seconds > 0 and self.is_running:
            minutes, seconds = divmod(self.remaining_seconds, 60)
            self.timer_label.config(text=f"{str(minutes).zfill(2)}:{str(seconds).zfill(2)}")
            self.remaining_seconds -= 1
            self.root.after(int(self.time_ratio * 1000), self.run_timer)
        elif self.remaining_seconds <= 0:
            self.timer_label.config(text="00:00", foreground="red")
            messagebox.showinfo("タイマー終了", "タイマーが終了しました！")
            self.start_stop_button.config(text="開始", state="disable")
            self.is_running = False

    def reset_timer(self):
        self.is_running = False
        self.remaining_seconds = self.illusion_time.get() * 60
        self.timer_label.config(text=self.format_time(self.remaining_seconds), foreground="black")
        self.start_stop_button.config(text="開始", state="normal")

    @staticmethod
    def format_time(seconds):
        minutes, seconds = divmod(seconds, 60)
        return f"{str(minutes).zfill(2)}:{str(seconds).zfill(2)}"


if __name__ == "__main__":
    root = tk.Tk()
    app = IllusionTimerApp(root)
    root.mainloop()

```

##  最後に
tkinterを使って錯覚を起こすタイマーを作成しました。設定時間によっては明らかにタイマーの進みが遅いですが意外と使えます。簡単に試すことができるのでぜひ使ってみてください。