generated at
Lightning Timer

Scrapboxでライトニングトークする時に便利なScrapbox User Scriptつくった
指定したURLプリフィクスを持つページでリロードするとタイマーを表示します
2021年8月頃にプライベートな時間でつくって、株式会社Helpfeelで2022年11月現在も運用中です
「スタート」をクリックすると、カウントダウンが始まります
3分後にDOUBLE_BELL_URLに記載の音が鳴ります
2分30秒後(残り30秒)のタイミングでSINGLE_BELL_URLの音が鳴ります
「ストップ」をクリックすると、0秒にリセットします
同じプロジェクトであればページを遷移してもタイマーが継続します
タイマー稼働中に「スタート」をクリックすると、3分を計り直します
早めに話終わった人がいた時に、次の人の発表を開始するのに便利です
残り30秒で黄色い表示になり、残り0秒を超過すると赤い表示でカウントダウンし続けます
残り時間が無くなっても話し続けている人が、どれぐらい話し続けているかを意識してもらうためです
右下の「🛎」をクリックすると、ベルを1回鳴らせます
サウンドテストに便利です
スクリーン共有後、音声がリモート先の人に届いてるか確認してください
大幅に時間が超過しても喋り続ける人がいたら連打してください



PIXTAの有料効果音素材を使うのがオススメ
SINGLE_BELL_URL
DOUBLE_BELL_URL
もしくは、ZAPSPLATの無料効果音を使うのがオススメ
Sound effects obtained from https://www.zapsplat.com
SINGLE_BELL_URL
DOUBLE_BELL_URL
※必ず配布元の利用規約をよく読み、配布元のサイトより効果音ファイルをダウンロードしてください



script.js
/* Lightning Timer © akiroom */ // 本タイマーをセットするURL(先頭一致で判定します) const LOADABLE_URL_PREFIX = 'https://scrapbox.io/akiroom/lightning_timer' // この例だと、/akiroomプロジェクトの「Lightning Timer」「Lightning Timer 3」「Lightning Timer MTG 第12回」のようなタイトルのページでリロードした時にタイマーを表示する // 何分間を計測するか(開始何分後に2回鳴らすか) const GOAL_MINUTES = 3 // 3分 // 終了何分前に1回鳴らすか const PRE_GOAL_MINUTES = .5 // 30秒 const SINGLE_BELL_URL = 'https://scrapbox.io/files/6287a062dad5c50022aadae8.mp3' const DOUBLE_BELL_URL = 'https://scrapbox.io/files/6287a069f289c7001d4c0e5d.mp3' // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart#fixed_width_string_number_conversion function leftFillNum(num, targetLength) { return num.toString().padStart(targetLength, 0); } function getTimerLabelString(targetTime) { let prefix = '' let minutes, secs, milliSecs if (targetTime >= 0) { minutes = Math.floor(targetTime / 1000 / 60) secs = Math.floor((targetTime - minutes * 60 * 1000) / 1000) milliSecs = targetTime % 1000 } else { prefix = '-' minutes = Math.abs(Math.floor(targetTime / 1000 / 60) + 1) secs = Math.floor((minutes * 60 * 1000 - targetTime) / 1000) milliSecs = Math.abs(targetTime % 1000) } return `${prefix}${leftFillNum(minutes, 2)}:${leftFillNum(secs, 2)}.${leftFillNum(milliSecs, 3)}` } const TIMER_STAGE_ID = 'scrapbox-timer-stage' const style = ` #${TIMER_STAGE_ID} { position: fixed; right: 0; bottom: 40px; padding: 16px; border-radius: 8px 0 0 8px; z-index: 999; background: rgba(0,0,0,.9); color: #fff; } #${TIMER_STAGE_ID} button { background: rgba(255,255,255,.2); border: 2px solid rgba(255,255,255,.8); } #${TIMER_STAGE_ID} .timer-status-label { text-align: right; font-size: 32px; } #${TIMER_STAGE_ID} .timer-status-label.warn { color: #fcbe40; } #${TIMER_STAGE_ID} .timer-status-label.minus { color: red; } ` const isTargetPage = location.href.toLowerCase().startsWith(LOADABLE_URL_PREFIX.toLowerCase()) let timerStage = document.getElementById(TIMER_STAGE_ID) if (!timerStage && isTargetPage) { document.head.insertAdjacentHTML('beforeend', `<style type="text/css">${style}</style>`) document.body.insertAdjacentHTML('beforeend', `<div id="${TIMER_STAGE_ID}"><div class="timer-status-label">00:00</div><div><button class="start-button">スタート</button><button class="stop-button">ストップ</button><button class="sound-test-button">🛎</button></div></div>`) const startButton = document.querySelector(`#${TIMER_STAGE_ID} .start-button`) const stopButton = document.querySelector(`#${TIMER_STAGE_ID} .stop-button`) const soundTestButton = document.querySelector(`#${TIMER_STAGE_ID} .sound-test-button`) const timerStatusLabel = document.querySelector(`#${TIMER_STAGE_ID} .timer-status-label`) timerStatusLabel.innerText = getTimerLabelString(0) const singleBellAudio = new Audio(SINGLE_BELL_URL) const doubleBellAudio = new Audio(DOUBLE_BELL_URL) const goalTime = 1000 * 60 * GOAL_MINUTES const preGoalTime = 1000 * 60 * PRE_GOAL_MINUTES let startDate = null let currentTimer = null let singleBellPlayed = false let doubleBellPlayed = false function resetTimer () { if (currentTimer) { clearInterval(currentTimer) } timerStatusLabel.innerText = getTimerLabelString(0) singleBellAudio.pause() singleBellAudio.currentTime = 0 doubleBellAudio.pause() doubleBellAudio.currentTime = 0 timerStatusLabel.classList.remove('warn') timerStatusLabel.classList.remove('minus') } // スタートボタンを押した時の処理 startButton.addEventListener('click', (e) => { e.stopPropagation() resetTimer() startDate = new Date() singleBellPlayed = false doubleBellPlayed = false currentTimer = setInterval(() => { const elapsedTime = new Date() - startDate const statusTime = goalTime - elapsedTime timerStatusLabel.innerText = getTimerLabelString(statusTime) if (statusTime < preGoalTime) { if (!singleBellPlayed) { singleBellPlayed = true singleBellAudio.pause() singleBellAudio.currentTime = 0 singleBellAudio.play() timerStatusLabel.classList.add('warn') } } else { timerStatusLabel.classList.remove('warn') } if (statusTime >= 0) { timerStatusLabel.classList.remove('minus') } else { if (!doubleBellPlayed) { doubleBellPlayed = true doubleBellAudio.pause() doubleBellAudio.currentTime = 0 doubleBellAudio.play() } timerStatusLabel.classList.add('minus') } }, 10) }) // ストップボタンを押した時の処理 stopButton.addEventListener('click', (e) => { e.stopPropagation() resetTimer() }) // サウンドテストボタンを押した時の処理 soundTestButton.addEventListener('click', (e) => { e.stopPropagation() singleBellAudio.pause() singleBellAudio.currentTime = 0 singleBellAudio.play() }) }