/* 词页共用计时器+浏览器内合成环境声(2026-10-23 关键词引流批次)。 为什么合成而不是放音频文件:/pomodoro-timer-with-sounds 这页要承诺"无下载、无文件、 锁屏笔记本也能用",用 mp3 就要么加几百 KB 要么写假话。这里全部用 WebAudio 现算: white=白噪,pink=粉噪(Paul Kellet 算法),rain=白噪过带通+慢调制,ocean=棕噪过低通+LFO。 首页 index.html 的五声(forest/rain/whitenoise/pinknoise/ocean)是另一套实现,这页不冒充它, 也不发新埋点事件名(埋点新事件要过 track.js+RPC+表 CHECK 三道白名单门,动它得抬全站 ?v=)。 用法(页内必须有这块骨架,id 逐字对齐):
*/ (function () { var root = document.getElementById('wt'); if (!root) return; var modes = (root.dataset.modes || '25').split(',').map(function (x) { return parseInt(x, 10) || 25; }); var labels = (root.dataset.labels || modes.join(',')).split(','); var def = parseInt(root.dataset.default, 10) || modes[0]; var doneMsg = root.dataset.done || 'Done!'; var soundDefs = (root.dataset.sounds || '').split(',').filter(Boolean); root.innerHTML = '
' + (root.dataset.bird || '🐦') + '
' + '
' + fmt(def * 60) + '
' + '
' + '
' + '' + '' + '
' + (soundDefs.length ? '
' : '') + '

' + (root.dataset.hint || 'Press play. Nothing is uploaded — the countdown runs in this tab.') + '

'; var total = def * 60, rem = total, int = null, run = false; var tEl = document.getElementById('wtT'), go = document.getElementById('wtGo'); var baseTitle = document.title; function fmt(s) { var m = Math.floor(s / 60), ss = s % 60; return String(m).padStart(2, '0') + ':' + String(ss).padStart(2, '0'); } function tickTitle() { document.title = run ? fmt(rem) + ' · ' + baseTitle : baseTitle; } function pick(m, btn) { stop(); total = m * 60; rem = total; tEl.textContent = fmt(rem); Array.prototype.forEach.call(document.querySelectorAll('#wtM .mode-btn'), function (x) { x.classList.remove('active'); }); if (btn) btn.classList.add('active'); } var mBox = document.getElementById('wtM'); modes.forEach(function (m, i) { var b = document.createElement('button'); b.className = 'mode-btn' + (m === def ? ' active' : ''); b.textContent = (labels[i] || (m + ' min')).trim(); b.onclick = function () { pick(m, b); }; mBox.appendChild(b); }); function stop() { if (int) clearInterval(int); int = null; run = false; go.textContent = '▶'; } function start() { run = true; go.textContent = '⏸'; int = setInterval(function () { rem = Math.max(0, rem - 1); tEl.textContent = fmt(rem); tickTitle(); if (rem <= 0) { stop(); rem = total; tEl.textContent = fmt(rem); document.title = baseTitle; try { new Function('')(); } catch (e) {} flash(); } }, 1000); } function flash() { var prev = tEl.textContent; tEl.textContent = '✓'; setTimeout(function () { tEl.textContent = fmt(rem); }, 900); if (window.console) console.log(doneMsg); try { Notification && Notification.permission === 'granted' && new Notification('Timerbird', { body: doneMsg }); } catch (e) {} } go.onclick = function () { run ? stop() : start(); tickTitle(); }; document.getElementById('wtRe').onclick = function () { stop(); rem = total; tEl.textContent = fmt(rem); document.title = baseTitle; }; /* ---- 合成环境声 ---- */ var actx = null, src = null, gain = null, lfo = null, cur = null; var sBox = document.getElementById('wtS'); function noiseBuffer(kind) { var len = actx.sampleRate * 2, buf = actx.createBuffer(1, len, actx.sampleRate), d = buf.getChannelData(0); var i, last = 0, b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0; for (i = 0; i < len; i++) { var w = Math.random() * 2 - 1; if (kind === 'pink') { b0 = 0.99886 * b0 + w * 0.0555179; b1 = 0.99332 * b1 + w * 0.0750759; b2 = 0.96900 * b2 + w * 0.1538520; b3 = 0.86650 * b3 + w * 0.3104856; b4 = 0.55000 * b4 + w * 0.5329522; b5 = -0.7616 * b5 - w * 0.0168980; d[i] = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + w * 0.5362) * 0.11; b6 = w * 0.115926; } else if (kind === 'brown') { last = (last + 0.02 * w) / 1.02; d[i] = last * 3.5; } else { d[i] = w * 0.6; } } return buf; } function setSound(id, btn) { if (!actx) { var AC = window.AudioContext || window.webkitAudioContext; if (!AC) return; actx = new AC(); } if (cur === id) { off(); btn.classList.remove('on'); cur = null; return; } off(); var need = (id === 'ocean') ? 'brown' : 'white', kind = (id === 'pink') ? 'pink' : need; src = actx.createBufferSource(); src.buffer = noiseBuffer(kind); src.loop = true; var tail = actx.createGain(); tail.gain.value = 0.55; if (id === 'rain') { var bp = actx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 1100; bp.Q.value = 0.7; var mod = actx.createOscillator(); mod.frequency.value = 0.22; var modG = actx.createGain(); modG.gain.value = 0.18; mod.connect(modG); modG.connect(tail.gain); mod.start(); lfo = mod; src.connect(bp); bp.connect(tail); } else if (id === 'ocean') { var lp = actx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 420; var sw = actx.createOscillator(); sw.frequency.value = 0.11; var swG = actx.createGain(); swG.gain.value = 0.25; sw.connect(swG); swG.connect(tail.gain); sw.start(); lfo = sw; src.connect(lp); lp.connect(tail); } else { src.connect(tail); } gain = actx.createGain(); gain.gain.value = 0; tail.connect(gain); gain.connect(actx.destination); src.start(); gain.gain.linearRampToValueAtTime(0.5, actx.currentTime + 0.4); cur = id; Array.prototype.forEach.call(document.querySelectorAll('#wtS .snd'), function (x) { x.classList.remove('on'); }); btn.classList.add('on'); } function off() { try { if (src) { src.stop(); src.disconnect(); } } catch (e) {} try { if (lfo) { lfo.stop(); lfo.disconnect(); } } catch (e) {} src = null; lfo = null; } var NAMES = { white: 'White noise', pink: 'Pink noise', rain: 'Rain', ocean: 'Ocean', forest: 'Forest' }; if (sBox) { soundDefs.forEach(function (id) { var b = document.createElement('button'); b.className = 'snd'; b.textContent = NAMES[id] || id; b.dataset.id = id; b.onclick = function () { setSound(id, b); }; sBox.appendChild(b); }); } document.addEventListener('visibilitychange', function () { if (document.hidden) off(), cur = null; }); })();