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