Spaces:
Sleeping
Sleeping
| import random | |
| import time | |
| import gradio as gr | |
| from wcwidth import wcswidth | |
| def quick_draw( | |
| n, # 要抽出的數量 | |
| do_shuffle, # 是否在抽獎前先打亂原始名單 | |
| no_self, # 是否禁止抽到自己 | |
| names: str, # 名單字串(以換行分隔) | |
| frame_delay_scale, # 延遲時間縮放倍率,用於加速或減速動畫 | |
| emoji, # 用於顯示箭頭的表情符號 | |
| char_delay=0.01, # 每個字元出現的延遲(製造打字動畫效果) | |
| item_delay=0.1, # 每項抽完後的延遲(項目間動畫) | |
| ): | |
| # 先將 names 字串拆成列表(以換行為分隔)並移除空白行 | |
| src_names = [name for name in names.strip().split("\n") if name] | |
| # 若名單人數不足 n,補上「編號 i」 | |
| if len(src_names) < n: | |
| for i in range(n - len(src_names)): | |
| src_names.append(f"編號 {i}") | |
| # 目標名單(會被打亂後作為抽中結果) | |
| dst_names = src_names.copy() | |
| random.shuffle(dst_names) | |
| # 若 do_shuffle 為 True,也把原始名單打亂 | |
| random.shuffle(src_names) if do_shuffle else None | |
| # 根據 frame_delay_scale 調整延遲(用來快轉/慢動作用) | |
| char_delay *= frame_delay_scale | |
| item_delay *= frame_delay_scale | |
| # 若不允許抽到自己,需重新洗牌 dst_names,直到沒有任何同名配對 | |
| if no_self: | |
| while True: | |
| for src_name, dst_name in zip(src_names, dst_names): | |
| if src_name == dst_name: # 有抽到自己的情況 | |
| break | |
| # 若有抽到自己,重新洗牌後再檢查 | |
| if src_name == dst_name: | |
| random.shuffle(dst_names) | |
| continue | |
| break | |
| msg = list() # 用來累積每一筆抽獎輸出 | |
| # 逐一比對 src -> dst | |
| for src_name, dst_name in zip(src_names, dst_names): | |
| # 若抽到自己 | |
| if src_name == dst_name: | |
| msg.append(f"{src_name} 送給自己!") | |
| else: | |
| msg.append(f"{src_name} {emoji}→ {dst_name}") | |
| # 若設定了 char_delay,則逐字輸出動畫 | |
| if char_delay: | |
| for i in range(len(msg[-1])): | |
| # 產生目前的輸出畫面(前面完整行 + 最後一行逐字顯示) | |
| yield "\n".join(msg[:-1] + [msg[-1][: i + 1]]) | |
| time.sleep(char_delay) | |
| # 每個項目完成後的延遲動畫 | |
| if item_delay: | |
| yield "\n".join(msg) | |
| time.sleep(item_delay) | |
| # 最終完整結果 | |
| yield "\n".join(msg) | |
| def simple_start(n, names: str): | |
| # 將輸入的 names 字串按行拆解,並移除空白行 | |
| name_list = [name for name in names.strip().split("\n") if name] | |
| # 若名單數量不足 n,則以「編號 i」補足 | |
| if len(name_list) < n: | |
| for i in range(n - len(name_list)): | |
| name_list.append(f"編號 {i}") | |
| # 將名單再組合成以換行分隔的字串 | |
| name_list = "\n".join(name_list) | |
| return names, name_list, name_list, "" | |
| def simple_draw( | |
| src_name_text: str, # 左側來源名單(尚未抽中的人) | |
| dst_name_text: str, # 右側目標名單(可被抽到的人) | |
| results: str, # 已抽結果字串 | |
| frame_delay_scale, # 動畫速度縮放倍率 | |
| do_shuffle, # 是否隨機選取來源 index | |
| no_self, # 是否禁止抽到自己 | |
| enable_sfx, # 是否啟用音效 | |
| emoji, # 顯示箭頭符號 | |
| turn_time=0.25, # 每輪滾動時間 | |
| n_turns=3, # 滾輪來回次數 | |
| n_blink=3, # 中獎後閃爍次數 | |
| blink_delay=0.1, # 閃爍間隔 | |
| ): | |
| # 若來源名單已空 → 全部抽完 | |
| if not src_name_text.strip(): | |
| gr.Info(title="訊息", message="抽完了!") | |
| yield src_name_text, dst_name_text, results, None | |
| return | |
| # 拆解來源與目標名單為列表 | |
| src_names = [name for name in src_name_text.strip().split("\n")] | |
| dst_names = [name for name in dst_name_text.strip().split("\n")] | |
| # 套用動畫時間倍率 | |
| turn_time *= frame_delay_scale | |
| blink_delay *= frame_delay_scale | |
| # 如果啟用音效,載入音檔 | |
| rlt_sfx = get_sfx("roulette.mp3") if enable_sfx else None | |
| win_sfx = get_sfx("win.mp3") if enable_sfx else None | |
| # 建立格式化名稱+箭頭,並處理全形/半形字寬 | |
| def get_arrow(name, width): | |
| pad_len = width - wcswidth(name) + 2 # 計算補齊空白寬度 | |
| return name + " " * pad_len + emoji # 在名字後補空白並加箭頭 | |
| # 選取來源 index(可選擇洗牌或固定從 0 開始) | |
| src_idx = random.choice(range(len(src_names))) if do_shuffle else 0 | |
| # 隨機選取目標 index | |
| dst_idx = random.choice(range(len(dst_names))) | |
| # 若不允許抽到自己 → 調整 dst_idx | |
| if no_self: | |
| # 特例:只有兩人時,抽到自己會導致死循環,需要特殊邏輯 | |
| if len(src_names) == 2: | |
| src_left_idx = (src_idx + 1) & 1 # 另一個 index | |
| if src_names[src_left_idx] in dst_names: | |
| dst_idx = dst_names.index(src_names[src_left_idx]) | |
| else: | |
| # 一般情況下,若抽到自己就重新選擇 | |
| while src_names[src_idx] == dst_names[dst_idx]: | |
| dst_idx = random.choice(range(len(dst_names))) | |
| dst_name = dst_names[dst_idx] # 最終目標(被抽中的人) | |
| # 計算字寬,使箭頭對齊 | |
| max_dst_name_width = max(wcswidth(s) for s in dst_names) | |
| max_src_name_width = max(wcswidth(s) for s in dst_names) | |
| # 在來源名單中標示出「目前正在抽的人」 | |
| curr_src_name = get_arrow(src_names[src_idx], max_src_name_width) | |
| curr_src_names = src_names[:src_idx] + [curr_src_name] + src_names[src_idx + 1 :] | |
| src_name_text = "\n".join(curr_src_names) | |
| # 顯示來源列表的更新(右側尚未進入滾動) | |
| yield src_name_text, dst_name_text, results, None | |
| # 將實際要抽出的人從來源名單移除 | |
| src_name = src_names.pop(src_idx) | |
| # 產生當前滾動階段的目的名單(某一行加上箭頭) | |
| def get_curr_names(i): | |
| if i is None: | |
| return "\n".join(dst_names) # 無高亮,顯示完整列表 | |
| selected_item = get_arrow(dst_names[i], max_dst_name_width) | |
| curr_names = dst_names[:i] + [selected_item] + dst_names[i + 1 :] | |
| return "\n".join(curr_names) | |
| # 設定滾輪動畫的延遲(越滾越慢) | |
| delay = turn_time / len(dst_names) | |
| # 主滾動動畫:前後來回 n_turns 次 | |
| for _ in range(n_turns): | |
| # 從頭滾到尾 | |
| for i in range(0, len(dst_names)): | |
| yield src_name_text, get_curr_names(i), results, f"{rlt_sfx}{i}" | |
| time.sleep(delay) | |
| # 從尾倒回到頭 | |
| for i in range(len(dst_names) - 2, 0, -1): | |
| yield src_name_text, get_curr_names(i), results, f"{rlt_sfx}{i}" | |
| time.sleep(delay) | |
| delay *= 2 # 每次回合後增加等待時間(營造減速效果) | |
| # 最終慢慢滾到中獎者的位置 | |
| for i in range(0, len(dst_names)): | |
| yield src_name_text, get_curr_names(i), results, f"{rlt_sfx}{i}" | |
| if dst_names[i] == dst_name: | |
| break | |
| time.sleep(delay) | |
| time.sleep(delay) | |
| # 昇格畫面 → 目標列表停止滾動,高亮中獎者 | |
| yield src_name_text, get_curr_names(None), results, win_sfx | |
| # 中獎閃爍效果 | |
| for _ in range(n_blink): | |
| yield src_name_text, get_curr_names(None), results, None | |
| time.sleep(blink_delay) | |
| yield src_name_text, get_curr_names(i), results, None | |
| time.sleep(blink_delay) | |
| # 將已被抽中的目標從名單移除 | |
| dst_names.pop(i) | |
| # 記錄抽獎結果 | |
| if src_name == dst_name: | |
| results = f"{results}{src_name} 抽到自己!\n" | |
| else: | |
| results = f"{results}{src_name} {emoji}→ {dst_name}\n" | |
| # 更新名單(顯示在前端) | |
| yield "\n".join(src_names), "\n".join(dst_names), results, None | |
| # 若只剩 1 對 1,則自動進行最後一次配對 | |
| if len(dst_names) == 1: | |
| src_name = src_names.pop(0) | |
| dst_name = dst_names.pop(0) | |
| results = f"{results}{src_name} {emoji}→ {dst_name}" | |
| yield "\n".join(src_names), "\n".join(dst_names), results, None | |
| def gr_textarea(label, interactive=False, lines=10, max_lines=10): | |
| """ | |
| 建立一個 Gradio TextArea(多行文字輸入框) | |
| 參數: | |
| label — 顯示在元件上的標籤文字 | |
| interactive — 是否允許使用者編輯(False 時僅能顯示) | |
| lines — 預設顯示的行數高度 | |
| max_lines — 最大可擴張到的行數高度 | |
| 功能: | |
| - 用於顯示抽籤名單或抽籤結果 | |
| """ | |
| return gr.TextArea( | |
| label=label, | |
| lines=lines, | |
| max_lines=max_lines, | |
| interactive=interactive, | |
| ) | |
| def gr_slider(label, value=10, minimum=2, maximum=300): | |
| """ | |
| 建立一般範圍調整用 Slider(滑桿) | |
| 參數: | |
| label — 標籤文字 | |
| value — 預設值 | |
| minimum — 最小值 | |
| maximum — 最大值 | |
| 功能: | |
| - 常用於「參與人數」、「輪盤大小」等需要數值輸入的場景 | |
| """ | |
| return gr.Slider(label=label, value=value, minimum=minimum, maximum=maximum) | |
| def gr_slider_delay(label, value, minimum=0, maximum=1, step=0.01): | |
| """ | |
| 建立可用於動畫速度控制的 Slider | |
| 參數: | |
| label — 標籤文字 | |
| value — 預設值 | |
| minimum — 最小值 | |
| maximum — 最大值 | |
| step — 調整步進(例如 0.01) | |
| 功能: | |
| - 用於 frame_delay_scale(動畫速率倍率) | |
| - 使用小步進可使動畫速度更細緻 | |
| """ | |
| return gr.Slider( | |
| label=label, | |
| value=value, | |
| minimum=minimum, | |
| maximum=maximum, | |
| step=step, # 微調精度 | |
| ) | |
| def draw_roulette(arr: list[str], index: int) -> str: | |
| # 陣列長度 | |
| n = len(arr) | |
| if n == 0: | |
| return | |
| # 將陣列平均分配到四邊(上、右、下、左) | |
| side_length = n // 4 # 每側的基本數量 | |
| remainder = n % 4 # 平均分配後剩餘的數量 | |
| # 依照剩餘數量,從上→右→下依序多分配 1 個 | |
| length_top = side_length + (1 if remainder > 0 else 0) | |
| length_right = side_length + (1 if remainder > 1 else 0) | |
| length_bottom = side_length + (1 if remainder > 2 else 0) | |
| length_left = side_length # 左側不額外補 | |
| # 用索引切分四邊 | |
| idx_top_end = length_top | |
| idx_right_end = idx_top_end + length_right | |
| idx_bottom_end = idx_right_end + length_bottom | |
| list_top = arr[0:idx_top_end] | |
| list_right = arr[idx_top_end:idx_right_end] | |
| list_bottom = arr[idx_right_end:idx_bottom_end] | |
| list_left = arr[idx_bottom_end:] | |
| # 計算所有字串最大寬度(支援全形字) | |
| max_text_width = max((wcswidth(s) for s in arr), default=0) | |
| # 每格寬度 = 最大字寬 + padding | |
| cell_width = max_text_width + 2 | |
| # 使 cell_width 為奇數,以便在中央置中 | |
| if cell_width % 2 == 0: | |
| cell_width += 1 | |
| # 上方格子一排的總寬度 | |
| box_inner_width = length_top * cell_width | |
| # 左右兩側格子的總高度(每項佔 2 行,中間留空) | |
| max_side_items = max(length_right, length_left) | |
| box_inner_height = (max_side_items * 2) + 1 | |
| # 建立空白網格,後續會在上面放置「@」 | |
| grid = [[" " for _ in range(box_inner_width)] for _ in range(box_inner_height)] | |
| # 目標位置(@)的座標 | |
| target_pos = None | |
| # 判斷 index 屬於哪一側,並計算其在圓形 ASCII 中的實際位置 | |
| if 0 <= index < idx_top_end: | |
| # 屬於上側 | |
| local_idx = index | |
| center_x = (local_idx * cell_width) + (cell_width // 2) | |
| target_pos = (0, center_x) # row=0 → 第一行 | |
| elif idx_top_end <= index < idx_right_end: | |
| # 屬於右側 | |
| local_idx = index - idx_top_end | |
| target_row = 1 + (local_idx * 2) # 每項佔兩行 | |
| target_pos = (target_row, box_inner_width - 2) | |
| elif idx_right_end <= index < idx_bottom_end: | |
| # 屬於下側(需反向映射) | |
| local_idx = index - idx_right_end | |
| offset_from_right = local_idx | |
| visual_slot_from_left = (length_top - 1) - offset_from_right | |
| center_x = (visual_slot_from_left * cell_width) + (cell_width // 2) | |
| target_pos = (box_inner_height - 1, center_x) | |
| else: | |
| # 屬於左側 | |
| local_idx = index - idx_bottom_end | |
| target_row = (box_inner_height - 2) - (local_idx * 2) | |
| target_pos = (target_row, 1) # 左側靠內一格 | |
| # 在 grid 上標記 @ 位置 | |
| if target_pos: | |
| row, col = target_pos | |
| if 0 <= row < box_inner_height and 0 <= col < box_inner_width: | |
| grid[row][col] = "@" | |
| # 上方標籤需要向右縮排(給左右側留空間) | |
| left_margin_width = cell_width | |
| # 上邊文字(置中) | |
| top_labels = "".join([get_centered_cell(s, cell_width) for s in list_top]) | |
| # 開始組建輸出內容 | |
| outputs = list() | |
| # 上方第一行:空白 + 上側文字 | |
| outputs.append(" " * left_margin_width + " " + top_labels) | |
| # 上方第二行:空白 + 上框線 | |
| outputs.append(" " * left_margin_width + " " + "#" * box_inner_width + " ") | |
| # 中間內容(含左側與右側) | |
| for row in range(box_inner_height): | |
| left_char = " " * cell_width # 預設左側為空 | |
| right_char = " " * cell_width # 預設右側為空 | |
| # 左右項目位於 (row - 1) % 2 == 0 的行上 | |
| if (row - 1) % 2 == 0: | |
| # 計算左側 index(從下往上排) | |
| calc_left_idx = (box_inner_height - 2 - row) // 2 | |
| is_in_left = 0 <= calc_left_idx < len(list_left) | |
| if is_in_left and (box_inner_height - 2 - row) % 2 == 0: | |
| left_char = get_right_align_cell(list_left[calc_left_idx], cell_width) | |
| # 計算右側 index(從上往下排) | |
| calc_right_idx = (row - 1) // 2 | |
| is_in_right = 0 <= calc_right_idx < len(list_right) | |
| if is_in_right and (row - 1) >= 0: | |
| right_char = get_left_align_cell(list_right[calc_right_idx], cell_width) | |
| # grid[row] 中可能有 @ 或空白 | |
| row_content = "".join(grid[row]) | |
| # 左 / 框線 / 右 | |
| outputs.append(f"{left_char}#{row_content}#{right_char}") | |
| # 下方框線 | |
| outputs.append(" " * left_margin_width + " " + "#" * box_inner_width + " ") | |
| # 下方文字排在框線下方(注意 bottom list 是反向排列) | |
| bottom_padding_cells = length_top - length_bottom | |
| bottom_border = "".join([get_centered_cell(s, cell_width) for s in list_bottom[::-1]]) | |
| bottom_labels = (" " * cell_width * bottom_padding_cells) + bottom_border | |
| outputs.append(" " * left_margin_width + " " + bottom_labels) | |
| return "\n".join(outputs) | |
| def get_centered_cell(text, width): | |
| # 計算字串的實際寬度(支援全形字) | |
| text_width = wcswidth(text) | |
| # 如果字串本身超過格子寬度,直接回傳(不裁切) | |
| if text_width >= width: | |
| return text | |
| # 需要補的空白總量 | |
| total_space = width - text_width | |
| # 左右平均分配空白 | |
| left_space = total_space // 2 | |
| right_space = total_space - left_space | |
| # 回傳:左空白 + 文字 + 右空白 | |
| return " " * left_space + text + " " * right_space | |
| def get_right_align_cell(text, width): | |
| # 計算字串寬度 | |
| text_width = wcswidth(text) | |
| # 若字串比格子寬,不處理 | |
| if text_width >= width: | |
| return text | |
| # 左側需要補多少空白,使文字靠右 | |
| # 最右邊保留一格空白,讓版面不緊貼 | |
| left_space = width - text_width - 1 | |
| # 回傳:左空白 + 文字 + 最右一格空白 | |
| return " " * left_space + text + " " | |
| def get_left_align_cell(text, width): | |
| # 計算字串寬度 | |
| text_width = wcswidth(text) | |
| # 若字串比格子寬,直接回傳 | |
| if text_width >= width: | |
| return text | |
| # 讓文字靠左,右側補空白(右側 -1 是為了留一格邊界空位) | |
| right_space = width - text_width - 1 | |
| # 回傳:左邊固定一格空白 + 文字 + 右邊空白 | |
| return " " + text + " " * right_space | |
| def make_pre_html(content): | |
| # 將 ASCII 輸出包裝成 HTML,利用 <pre> 保留字元排版 | |
| # 外層 div 用於樣式設定(例如固定寬度、捲動條等) | |
| return f'<div class="ascii-container"><pre class="ascii">{content}</pre></div>' | |
| def get_sfx(path): | |
| # 回傳 HTML <audio> 標籤,用於播放音效 | |
| # autoplay 會使音效自動播放 | |
| # /gradio_api/file=path 為 Gradio 的檔案 URL | |
| return f'<audio src="/gradio_api/file={path}" autoplay></audio>' | |
| def draw_roulette_html(arr, index): | |
| # 呼叫 draw_roulette 生成 ASCII 輪盤 | |
| # 再用 make_pre_html 包成 HTML 並回傳 | |
| return make_pre_html(draw_roulette(arr, index)) | |
| def galaxy_start(n, names: str, shuffle): | |
| # 將輸入 names 以換行切割成列表,並移除空白行 | |
| name_list = [name for name in names.strip().split("\n") if name] | |
| # 若名單數量不足 n,則以「編號 i」補足 | |
| if len(name_list) < n: | |
| for i in range(n - len(name_list)): | |
| name_list.append(f"編號 {i}") | |
| # src_names 為來源名單的副本(後續可依需求洗牌) | |
| src_names = name_list[::] | |
| # 若 shuffle = True,將來源名單打亂 | |
| if shuffle: | |
| random.shuffle(src_names) | |
| return draw_roulette_html(name_list, -1), src_names, name_list[::], "" | |
| def galaxy_draw( | |
| src_names: list, # 左側:尚未抽出的人(來源名單) | |
| dst_names: list, # 右側:可被抽到的人(目標名單) | |
| html, # 目前顯示的 ASCII 輪盤 HTML | |
| results, # 已抽紀錄字串 | |
| frame_delay_scale, # 動畫速度倍率 | |
| no_self, # 是否禁止抽到自己 | |
| enable_sfx, # 是否啟用音效 | |
| emoji, # 箭頭符號 | |
| turn_time=0.1, # 每輪滾動時間參數 | |
| n_turns=5, # 完整繞圈次數(未指定 index) | |
| acc_factor=2, # 每輪速度加倍倍率(越滾越快) | |
| n_blink=3, # 結果出現後閃爍次數 | |
| blink_delay=0.1, # 閃爍延遲 | |
| ): | |
| # 若來源名單已空 → 全部抽完 | |
| if not src_names: | |
| gr.Info(title="訊息", message="抽完了!") | |
| yield html, results, None | |
| return | |
| # 套用速度倍率 | |
| turn_time *= frame_delay_scale | |
| blink_delay *= frame_delay_scale | |
| # 載入音效 | |
| rlt_sfx = get_sfx("roulette.mp3") if enable_sfx else None | |
| win_sfx = get_sfx("win.mp3") if enable_sfx else None | |
| # 隨機挑一個目標 index | |
| idx = random.choice(range(len(dst_names))) | |
| # 禁止抽到自己的處理 | |
| if no_self: | |
| # 特例:只剩 2 人時避免死循環 | |
| if len(src_names) == 2: | |
| if src_names[-1] in dst_names: | |
| idx = dst_names.index(src_names[-1]) | |
| else: | |
| # 一般情況 → 若抽到自己就重抽 index | |
| while src_names[0] == dst_names[idx]: | |
| idx = random.choice(range(len(dst_names))) | |
| # 取出來源名單的第一人(固定第一項,而非隨機) | |
| src_name = src_names.pop(0) | |
| # 在結果欄先加上「來源 →」 | |
| results = f"{results}{src_name} {emoji}→ " | |
| # 先輸出一次更新 | |
| yield html, results, None | |
| # 初始滾動區間(愈滾愈快) | |
| delay = turn_time / len(dst_names) | |
| # 主轉盤滾動迴圈(未鎖定目標) | |
| for _ in range(n_turns): | |
| for i in range(len(dst_names)): | |
| yield draw_roulette_html(dst_names, i), results, f"{rlt_sfx}{i}" | |
| time.sleep(delay) | |
| # 每一圈後加速 | |
| delay *= acc_factor | |
| # 最終跑到指定 index 的滾動 | |
| for i in range(len(dst_names)): | |
| yield draw_roulette_html(dst_names, i), results, f"{rlt_sfx}{i}" | |
| time.sleep(delay) | |
| if i == idx: | |
| break | |
| # 中獎特效:顯示無選取狀態的輪盤,播放勝利音效 | |
| yield draw_roulette_html(dst_names, -1), results, win_sfx | |
| # 閃爍效果(選取框消失 / 出現交替) | |
| for _ in range(n_blink): | |
| yield draw_roulette_html(dst_names, -1), results, None | |
| time.sleep(blink_delay) | |
| yield draw_roulette_html(dst_names, i), results, None | |
| time.sleep(blink_delay) | |
| # 更新 HTML 顯示最後位置 | |
| html = draw_roulette_html(dst_names, i) | |
| # 取出目標名單中的中獎者 | |
| dst_name = dst_names.pop(idx) | |
| # 在結果欄加上目標名字 | |
| results = f"{results}{dst_name}\n" | |
| # 若目標名單剩 1 人 → 自動配對最後一組 | |
| if len(dst_names) == 1: | |
| src_name = src_names.pop(0) | |
| dst_name = dst_names.pop(0) | |
| results = f"{results}{src_name} {emoji}→ {dst_name}" | |
| # 回傳最終畫面 | |
| yield html, results, None | |
| def change_title(text): | |
| # 回傳 Markdown 標題(第一層標題) | |
| # 加上 "# " 使文字以大標題方式顯示 | |
| return f"# {text}" | |
| def change_sub_title(text): | |
| # 回傳 Markdown 子標題(第三層標題) | |
| # 加上 "### " 使文字成為較小的子標題 | |
| return f"### {text}" | |
| with gr.Blocks(title="交換禮物抽籤") as app: | |
| # 顯示主標題(Markdown) | |
| title_md = gr.Markdown("# 🎄 交換禮物抽籤") | |
| # 顯示副標題(Markdown) | |
| sub_title_md = gr.Markdown("### 一起來快樂的交換禮物吧!") | |
| # 左側 sidebar 區域(設定面板) | |
| with gr.Sidebar(width=350): | |
| # 參加者名稱輸入區(多行文字) | |
| names = gr_textarea("📝 人名", True) | |
| # 基礎設定區(Accordion 預設展開) | |
| with gr.Accordion("🛠️ 基本設定", open=True): | |
| # 參與者人數(從名稱過濾後補足用) | |
| n_people = gr_slider("🙋♂️ 參與者人數") | |
| # 動畫延遲倍率控制(速度加減速) | |
| frame_delay_scale = gr_slider_delay("⏱️ 影格延遲比率", 1.0, 0, 2, 0.1) | |
| # 是否打亂來源名單 | |
| do_shuffle = gr.Checkbox(label="🔀 隨機順序") | |
| # 是否禁止抽到自己 | |
| no_self = gr.Checkbox(label="🙅 不會抽到自己") | |
| # 是否啟用音效 | |
| enable_sfx = gr.Checkbox(label="🔔 啟用音效", value=True) | |
| # 進階設定區(Accordion 預設收合) | |
| with gr.Accordion("⚙️ 詳細設定", open=False): | |
| # UI 顯示的主標題 | |
| title = gr.Textbox("🎄 交換禮物抽籤", label="🎉 主標題") | |
| # UI 顯示的副標題 | |
| sub_title = gr.Textbox("一起來快樂的交換禮物吧!", label="🎈 副標題") | |
| # 禮物 emoji(例如:🎁) | |
| gift_emoji = gr.Textbox("🎁", label="🔢 禮物符號") | |
| # 背景音樂設定,可 loop | |
| bgm = gr.Audio( | |
| "bgm.mp3", | |
| type="filepath", | |
| autoplay=True, | |
| loop=True, | |
| label="🎶 背景音樂", | |
| ) | |
| # 供 SFX 注入 HTML 用(隱藏) | |
| sfx_html = gr.HTML(elem_classes="hidden") | |
| # 第一個頁籤:快速抽籤模式 | |
| with gr.Tab("🚀 快速抽籤"): | |
| quick_results = gr_textarea("🎯 抽籤結果") | |
| quick_btn = gr.Button("▶️ 抽籤!") | |
| # 第二個頁籤:簡易輪盤模式 | |
| with gr.Tab("✨ 簡易輪盤"): | |
| with gr.Group(): | |
| with gr.Row(): | |
| # 左側:送禮物者名單 | |
| simple_src = gr_textarea("📤 送禮物的人") | |
| # 中間:收禮物者名單 | |
| simple_dst = gr_textarea("📬 收禮物的人") | |
| # 右側:抽籤結果 | |
| simple_results = gr_textarea("🎯 抽籤結果") | |
| # 控制按鈕:開始 或 下一個 | |
| with gr.Row(): | |
| simple_start_btn = gr.Button("▶️ 開始") | |
| simple_next_btn = gr.Button("⏭️ 下一個") | |
| # 第三個頁籤:華麗輪盤模式(ASCII 螢幕動畫) | |
| with gr.Tab("🎡 華麗輪盤"): | |
| with gr.Group(): | |
| with gr.Row(): | |
| # 左側大區域:顯示 ASCII 輪盤(HTML) | |
| with gr.Column(scale=2, elem_classes="ascii-container"): | |
| galaxy_roulette = gr.HTML(elem_classes="ascii-container") | |
| # 右側:抽籤結果 | |
| with gr.Column(scale=1): | |
| galaxy_results = gr_textarea("🎯 抽籤結果") | |
| # 使用 State 儲存來源名單與目標名單 | |
| galaxy_src = gr.State(None) | |
| galaxy_dst = gr.State(None) | |
| # 操作按鈕:開始 or 下一輪 | |
| with gr.Row(): | |
| galaxy_start_btn = gr.Button("▶️ 開始") | |
| galaxy_next_btn = gr.Button("⏭️ 下一個") | |
| # 當使用者修改主標題時,更新左上角的 Markdown 顯示 | |
| title.change( | |
| change_title, # 觸發的函式 | |
| title, # 函式輸入(使用者輸入的文字) | |
| title_md, # 輸出到 Markdown 元件 | |
| show_progress="hidden", | |
| ) | |
| # 修改副標題時同理更新 UI | |
| sub_title.change(change_sub_title, sub_title, sub_title_md, show_progress="hidden") | |
| # ------------------------- | |
| # 🚀 快速抽籤模式按鈕 | |
| # ------------------------- | |
| quick_btn.click( | |
| quick_draw, # 使用快速抽籤函式 | |
| [ | |
| n_people, # 抽幾人 | |
| do_shuffle, # 是否洗牌 | |
| no_self, # 是否禁止抽到自己 | |
| names, # 名字來源 | |
| frame_delay_scale, | |
| gift_emoji, | |
| ], | |
| quick_results, # 輸出到快速抽籤結果框 | |
| show_progress="hidden", | |
| concurrency_limit=100, # 限制此事件在多位使用者同時執行的最大數量 | |
| ) | |
| # ------------------------- | |
| # ✨ 簡易輪盤:開始按鈕 | |
| # ------------------------- | |
| simple_start_btn.click( | |
| simple_start, # 初始化名單 | |
| [n_people, names], # 輸入參數 | |
| [names, simple_src, simple_dst, simple_results], # 多輸出 | |
| show_progress="hidden", | |
| ) | |
| # ------------------------- | |
| # ✨ 簡易輪盤:下一個按鈕 | |
| # ------------------------- | |
| simple_next_btn.click( | |
| simple_draw, | |
| [ | |
| simple_src, # 來源名單 | |
| simple_dst, # 目標名單 | |
| simple_results, # 抽籤結果 | |
| frame_delay_scale, | |
| do_shuffle, | |
| no_self, | |
| enable_sfx, | |
| gift_emoji, | |
| ], | |
| [simple_src, simple_dst, simple_results, sfx_html], # 更新名單 + 音效輸出 | |
| show_progress="hidden", | |
| scroll_to_output=True, | |
| concurrency_limit=100, | |
| ).then( | |
| lambda: None, None, sfx_html # 動畫結束後清空 sfx_html | |
| ) | |
| # ------------------------- | |
| # 🎡 華麗輪盤:開始按鈕 | |
| # ------------------------- | |
| galaxy_start_btn.click( | |
| galaxy_start, | |
| [n_people, names, do_shuffle], | |
| [ | |
| galaxy_roulette, # ASCII 輪盤顯示 | |
| galaxy_src, # 儲存來源名單狀態 | |
| galaxy_dst, # 儲存目標名單狀態 | |
| galaxy_results, # 結果文字 | |
| ], | |
| show_progress="hidden", | |
| ) | |
| # ------------------------- | |
| # 🎡 華麗輪盤:下一個按鈕(動畫版輪盤) | |
| # ------------------------- | |
| galaxy_next_btn.click( | |
| galaxy_draw, | |
| [ | |
| galaxy_src, # 狀態來源名單 | |
| galaxy_dst, # 狀態目標名單 | |
| galaxy_roulette, # ASCII 畫面 | |
| galaxy_results, # 抽籤結果 | |
| frame_delay_scale, | |
| no_self, | |
| enable_sfx, | |
| gift_emoji, | |
| ], | |
| [galaxy_roulette, galaxy_results, sfx_html], # 新畫面 + 結果 + 音效 | |
| show_progress="hidden", | |
| concurrency_limit=100, | |
| ) | |
| # ============================================================ | |
| # 本地儲存功能(Local Storage) | |
| # ============================================================ | |
| # 要儲存的 UI 元件(會同步保存至瀏覽器 Local Storage) | |
| stored_components: list[gr.Textbox] = [ | |
| names, | |
| n_people, | |
| frame_delay_scale, | |
| do_shuffle, | |
| no_self, | |
| enable_sfx, | |
| title, | |
| sub_title, | |
| gift_emoji, | |
| ] | |
| # 觸發儲存事件的條件:當上述元件的 value 改變 | |
| triggers = [c.change for c in stored_components] | |
| # 建立可以儲存整組資料的 BrowserState(本地瀏覽器儲存) | |
| local_storage = gr.BrowserState( | |
| [c.value for c in stored_components], # 預設值 | |
| storage_key="storage-key", # 儲存 key | |
| secret="secret", # 加密用(防篡改) | |
| ) | |
| # App 載入時 → 從 LocalStorage 還原 UI 設定 | |
| def load_from_local_storage(data): | |
| return data | |
| # 當使用者修改任一設定 → 自動更新 LocalStorage | |
| def save_to_local_storage(*data): | |
| return data | |
| # ============================================================ | |
| # 啟動應用程式 | |
| # ============================================================ | |
| app.launch( | |
| css_paths="style.css", # 自訂 CSS | |
| theme=gr.themes.Ocean( | |
| text_size=gr.themes.sizes.text_lg, | |
| radius_size=gr.themes.sizes.radius_lg, | |
| spacing_size=gr.themes.sizes.spacing_lg, | |
| primary_hue=gr.themes.colors.lime, # 主題顏色 | |
| ), | |
| allowed_paths=[ # 允許本地檔案存取 | |
| "SarasaFixedTC-Regular.ttf", | |
| "win.mp3", | |
| "roulette.mp3", | |
| ], | |
| share=False, # 是否生成可分享連結 | |
| footer_links=[None], # 隱藏 footer 連結 | |
| pwa=True, # 啟用 PWA(可安裝成 APP) | |
| favicon_path="favicon.png", | |
| ) | |