From 3c9646cf0bc876f3c608adaf39bca1f511cb0920 Mon Sep 17 00:00:00 2001 From: Jing Kohn Date: Mon, 24 Nov 2025 19:22:10 +1000 Subject: [PATCH] enjoy --- .DS_Store | Bin 0 -> 6148 bytes README.md | 102 ++++++++++++ index.html | 265 ++++++++++++++++++++++++++++++ js/admin.js | 186 +++++++++++++++++++++ js/audio.js | 450 +++++++++++++++++++++++++++++++++++++++++++++++++++ js/bls.js | 26 +++ patient.html | 52 ++++++ 7 files changed, 1081 insertions(+) create mode 100644 .DS_Store create mode 100644 README.md create mode 100644 index.html create mode 100644 js/admin.js create mode 100644 js/audio.js create mode 100644 js/bls.js create mode 100644 patient.html diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ba3b77f6a8c3248a9cea646d67980a85d45fcca5 GIT binary patch literal 6148 zcmeH~Jr2S!425lAKw|00n1usyg9yP1xBvnsRH*}E&(V4QVW2Rh3O!5q7rU|AzM-i_ zM0bztR-_Y=CEO?*3nNqHGr7tAdcXANVd$-pHcMIw-pT0tYadAkr~nn90#twsOh + + + + + Bilataral Stimulation Simulator Admin + + + +
+

BILATERAL STIMULATION SIMULATOR

+
+ +
+
+
+
+

Session Controls

+
+ + + + +
+
+ + +
+ + + + + +
+
+
+ + +
+ + + + + +
+
+
+ + + + + +
+
+ + + 28 px +
+
+ + + 50% + +
+
+ + +
+ + + 100% +
+
+ + + 0.5 Hz +
+
+
+ + + + 10 s +
+
+ + + +
+
+ + + 0 ms +
+
+ + + + 0 px +
+ +
+ +
+ + + +
+
+ + + + +
+
+ + + +
+
+
+ + + + +
+
+ + +
+ +
+ + + 0.5 Hz +
+
+ + + 1.2 Hz +
+
+ + + 60% +
+
+
+ + +
+
+
+ + + Not connected + + + x5 +
+ +
+
+

Preview

+
+ +
+
+ + + 250 px/s +
+
+ + + + + +
+ + +
+ + + 10:00 +
+
+ + +
+
+
Status: Idle
+
Elapsed: 00:00
+
+
+ + + + + + + + Sessions: 0 + Last: +
+
+ +
+
+
This preview mirrors the patient's view.
+
+
+ + + + + + diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000..41deb2f --- /dev/null +++ b/js/admin.js @@ -0,0 +1,186 @@ +function initAdmin(){ + var previewCanvas = document.getElementById('previewCanvas'); + var renderer = BLS.Renderer(previewCanvas); + var chan = BLS.createChannel('bls-session'); + var state = BLS.loadState(); + renderer.setState(state); +var shapeControls = document.getElementById('shapeControls'); + var directionControls = document.getElementById('directionControls'); + var easingControls = document.getElementById('easingControls'); + var shapeColor = document.getElementById('shapeColor'); + var bgColor = document.getElementById('bgColor'); + var shapePreset = document.getElementById('shapePreset'); + var bgPreset = document.getElementById('bgPreset'); +var speed = document.getElementById('speed'); + var speedLabel = document.getElementById('speedLabel'); + var size = document.getElementById('size'); + var sizeLabel = document.getElementById('sizeLabel'); + var yPercent = document.getElementById('yPercent'); + var yPercentLabel = document.getElementById('yPercentLabel'); + var yReset = document.getElementById('yReset'); + var glowEnable = document.getElementById('glowEnable'); + var glowIntensity = document.getElementById('glowIntensity'); + var glowIntensityLabel = document.getElementById('glowIntensityLabel'); + var glowRate = document.getElementById('glowRate'); + var glowRateLabel = document.getElementById('glowRateLabel'); + var rampEnable = document.getElementById('rampEnable'); + var rampSeconds = document.getElementById('rampSeconds'); + var rampLabel = document.getElementById('rampLabel'); + var edgePause = document.getElementById('edgePause'); + var edgePauseLabel = document.getElementById('edgePauseLabel'); + var wiggleEnable = document.getElementById('wiggleEnable'); + var wiggleAmplitude = document.getElementById('wiggleAmplitude'); + var wiggleLabel = document.getElementById('wiggleLabel'); + var presetName = document.getElementById('presetName'); + var presetList = document.getElementById('presetList'); + var savePresetBtn = document.getElementById('savePreset'); + var applyPresetBtn = document.getElementById('applyPreset'); + var deletePresetBtn = document.getElementById('deletePreset'); + var exportPresetsBtn = document.getElementById('exportPresets'); + var importPresetsBtn = document.getElementById('importPresets'); +var timerMinutes = document.getElementById('timerMinutes'); +var timerDisplay = document.getElementById('timerDisplay'); +var statusText = document.getElementById('statusText'); +var elapsedText = document.getElementById('elapsedText'); +var startBtn = document.getElementById('startSession'); +var endBtn = document.getElementById('endSession'); + var openPatient = document.getElementById('openPatient'); + var audioSelect = document.getElementById('audioSelect'); + var pickAudioFolder = document.getElementById('pickAudioFolder'); + var audioEnable = document.getElementById('audioEnable'); + var audioMode = document.getElementById('audioMode'); + var programControls = document.getElementById('programmaticControls'); + var fileControls = document.getElementById('fileControls'); + var programPreset = document.getElementById('programPreset'); + var syncPan = document.getElementById('syncPan'); + var panRate = document.getElementById('panRate'); + var panRateLabel = document.getElementById('panRateLabel'); + var cueRate = document.getElementById('cueRate'); + var cueRateLabel = document.getElementById('cueRateLabel'); + var volume = document.getElementById('volume'); + var volumeLabel = document.getElementById('volumeLabel'); + var testSound = document.getElementById('testSound'); + var connectHID = document.getElementById('connectHID'); + var deviceStatus = document.getElementById('deviceStatus'); + var knobSens = document.getElementById('knobSens'); + var knobSensLabel = document.getElementById('knobSensLabel'); + var hidDevice = null; + var audioFiles = []; + var patientName = document.getElementById('patientName'); + var savePatientBtn = document.getElementById('savePatient'); + var sessionCountEl = document.getElementById('sessionCount'); + var lastSessionInfoEl = document.getElementById('lastSessionInfo'); + var clearSessionsBtn = document.getElementById('clearSessions'); + var deletePatientBtn = document.getElementById('deletePatient'); +function setActive(group, dataKey, stateKey){ + var btns = group.querySelectorAll('button'); + for(var i=0;i'+n+'' }).join(''); var sel = document.getElementById('patientList'); if(sel){ sel.innerHTML = html } + } + function loadPatients(){ try{ var m = JSON.parse(localStorage.getItem('bls_patients')||'null'); return m&&typeof m==='object'? m : {} }catch(e){ return {} } } + function savePatients(m){ try{ localStorage.setItem('bls_patients', JSON.stringify(m)) }catch(e){} } + function getPatient(name){ var n = (name||'').trim()||'Unnamed'; var m = loadPatients(); if(!m[n]) m[n] = { name:n, sessions:[], count:0, last:null }; return m[n] } + function setPatient(p){ var m = loadPatients(); m[p.name] = p; savePatients(m) } + function deletePatient(name){ var n = (name||'').trim()||'Unnamed'; var m = loadPatients(); if(m[n]){ delete m[n]; savePatients(m) } } + function refreshPatientUI(){ var n = (patientName.value||'').trim()||'Unnamed'; var p = getPatient(n); sessionCountEl.textContent = String(p.count||0); if(p.last){ var d = new Date(p.last.end); var dur = Math.round((p.last.durationSec||0)); lastSessionInfoEl.textContent = d.toLocaleString()+' • '+dur+'s' } else { lastSessionInfoEl.textContent = '—' } var host = document.getElementById('sessionHistory'); if(host){ if(!p.sessions || !p.sessions.length){ host.innerHTML = 'No sessions yet' } else { var rows = p.sessions.slice().sort(function(a,b){ return a.start - b.start }).map(function(s, idx){ var start = new Date(s.start).toLocaleString(); var end = new Date(s.end).toLocaleString(); var dur = Math.round(s.durationSec||0); return '
'+String(idx+1)+'. '+start+' → '+end+' • '+dur+'s
' }); host.innerHTML = rows.join('') } } } + function recordSession(){ var n = (patientName.value||'').trim()||'Unnamed'; var st = BLS.loadState(); if(!st.startedAt) return; var end = Date.now(); var durationSec = Math.floor((end - st.startedAt)/1000); var p = getPatient(n); p.sessions.push({ start: st.startedAt, end: end, durationSec: durationSec }); p.count = (p.count||0)+1; p.last = { start: st.startedAt, end: end, durationSec: durationSec }; setPatient(p); refreshPatientUI() } + function refreshPresetList(){ var names = BLS.listPresets(); var html=''; for(var i=0;i'+n+'' } presetList.innerHTML = html } + function applyState(next, broadcast){ state = BLS.mergeState(state, next||{}); renderer.setState(state); if(state.running){ renderer.start() } else { renderer.stop() } if(broadcast){ chan.send(state) } } +shapeControls.addEventListener('click', function(e){ var d = e.target.dataset.shape; if(!d) return; applyState({shape:d}, true); setActive(shapeControls,'shape','shape'); }); + directionControls.addEventListener('click', function(e){ var d = e.target.dataset.direction; if(!d) return; applyState({direction:d}, true); setActive(directionControls,'direction','direction'); }); + easingControls.addEventListener('click', function(e){ var d = e.target.dataset.easing; if(!d) return; applyState({easingMode:d}, true); setActive(easingControls,'easing','easingMode'); }); + shapeColor.addEventListener('input', function(){ applyState({shapeColor:shapeColor.value}, true); }); + bgColor.addEventListener('input', function(){ applyState({bgColor:bgColor.value}, true); }); + shapePreset.addEventListener('click', function(e){ var c = e.target.dataset.color; if(!c) return; shapeColor.value = c; applyState({shapeColor:c}, true); }); + bgPreset.addEventListener('click', function(e){ var c = e.target.dataset.color; if(!c) return; bgColor.value = c; applyState({bgColor:c}, true); }); + speed.addEventListener('input', function(){ updateSpeedLabel(); var m = (BLS.loadState().speedMultiplier||1); applyState({speed:parseInt(speed.value,10)*m}, true); }); + size.addEventListener('input', function(){ updateSizeLabel(); applyState({size:parseInt(size.value,10)}, true); }); + yPercent.addEventListener('input', function(){ updateYPercentLabel(); applyState({yPercent:parseInt(yPercent.value,10)}, true); }); + yReset.addEventListener('click', function(){ yPercent.value=50; updateYPercentLabel(); applyState({yPercent:50}, true); }); + glowEnable.addEventListener('change', function(){ applyState({glowEnabled: glowEnable.checked}, true) }); + glowIntensity.addEventListener('input', function(){ updateGlowIntensityLabel(); var f = parseInt(glowIntensity.value,10)/100; applyState({glowIntensity:f}, true) }); + glowRate.addEventListener('input', function(){ updateGlowRateLabel(); applyState({glowRate: parseFloat(glowRate.value)}, true) }); + rampEnable.addEventListener('change', function(){ var st = BLS.loadState(); var next = {rampEnabled: rampEnable.checked}; if(!rampEnable.checked){ next.easingMode = 'linear' } if(st.running && rampEnable.checked){ next.startedAt = Date.now() } applyState(next, true) }); + rampSeconds.addEventListener('input', function(){ updateRampLabel(); var st = BLS.loadState(); var next = {rampSeconds: parseInt(rampSeconds.value,10)}; if(st.running && st.rampEnabled){ next.startedAt = Date.now() } applyState(next, true) }); + edgePause.addEventListener('input', function(){ updateEdgePauseLabel(); applyState({edgePauseMs: parseInt(edgePause.value,10)}, true) }); + wiggleEnable.addEventListener('change', function(){ applyState({wiggleEnabled: wiggleEnable.checked}, true) }); + wiggleAmplitude.addEventListener('input', function(){ updateWiggleLabel(); applyState({wiggleAmplitude: parseInt(wiggleAmplitude.value,10)}, true) }); + timerMinutes.addEventListener('input', function(){ updateTimerLabel(); }); + savePresetBtn.addEventListener('click', function(){ var n = (presetName.value||'').trim(); if(!n) return; BLS.savePreset(n, BLS.loadState()); refreshPresetList() }) + applyPresetBtn.addEventListener('click', function(){ var n = presetList.value; if(!n) return; var preset = BLS.loadPreset(n); if(!preset) return; applyState(preset, true); setActive(shapeControls,'shape','shape'); setActive(directionControls,'direction','direction'); setActive(easingControls,'easing','easingMode') }) + deletePresetBtn.addEventListener('click', function(){ var n = presetList.value; if(!n) return; BLS.deletePreset(n); refreshPresetList() }) + exportPresetsBtn.addEventListener('click', async function(){ var data = BLS.exportPresets(); if(window.showSaveFilePicker){ try{ var handle = await window.showSaveFilePicker({ suggestedName: 'bls-presets.json', types:[{ description:'JSON', accept:{ 'application/json':['.json'] } }] }); var w = await handle.createWritable(); await w.write(data); await w.close() }catch(e){} } else { var a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([data], {type:'application/json'})); a.download = 'bls-presets.json'; a.click(); URL.revokeObjectURL(a.href) } }) + importPresetsBtn.addEventListener('click', async function(){ if(window.showOpenFilePicker){ try{ var files = await window.showOpenFilePicker({ multiple:false, types:[{ description:'JSON', accept:{ 'application/json':['.json'] } }] }); if(files && files.length){ var file = await files[0].getFile(); var text = await file.text(); BLS.importPresets(text); refreshPresetList() } }catch(e){} } else { var input = document.createElement('input'); input.type='file'; input.accept='.json,application/json'; input.addEventListener('change', function(){ var f = input.files && input.files[0]; if(!f) return; var reader = new FileReader(); reader.onload = function(){ BLS.importPresets(reader.result); refreshPresetList() }; reader.readAsText(f) }); input.click() } }) + async function listAudioFromHandle(handle){ audioFiles=[]; if(!handle) return; try{ for await (const [name, h] of handle.entries()){ if(h.kind==='file' && /\.(mp3|wav)$/i.test(name)){ audioFiles.push({name:name, handle:h}) } } }catch(e){} audioSelect.innerHTML = '' + audioFiles.map(function(f){ return '' }).join('') } + async function pickFolder(){ if(window.showDirectoryPicker){ try{ var dir = await window.showDirectoryPicker(); await listAudioFromHandle(dir); }catch(e){} } else { var input = document.createElement('input'); input.type='file'; input.webkitdirectory=true; input.multiple=true; input.addEventListener('change', function(){ audioFiles = Array.from(input.files).filter(function(f){ return /\.(mp3|wav)$/i.test(f.name) }).map(function(f){ return {name:f.name, file:f} }); audioSelect.innerHTML = '' + audioFiles.map(function(f){ return '' }).join(''); }); input.click(); } } + pickAudioFolder.addEventListener('click', pickFolder); + audioSelect.addEventListener('change', function(){ var name = audioSelect.value || null; applyState({audioName:name}, true) }); + audioEnable.addEventListener('change', function(){ applyState({audioEnabled: audioEnable.checked}, true) }); + audioMode.addEventListener('change', function(){ refreshAudioMode(); applyState({audioMode: audioMode.value}, true) }); + programPreset.addEventListener('change', function(){ applyState({programPreset: programPreset.value}, true) }); + syncPan.addEventListener('change', function(){ applyState({syncPan: syncPan.checked}, true) }); + panRate.addEventListener('input', function(){ updatePanRateLabel(); applyState({panRate: parseFloat(panRate.value)}, true) }); + cueRate.addEventListener('input', function(){ updateCueRateLabel(); applyState({cueRate: parseFloat(cueRate.value)}, true) }); + volume.addEventListener('input', function(){ updateVolumeLabel(); applyState({volume: parseInt(volume.value,10)/100}, true) }); + testSound.addEventListener('click', function(){ try{ var st = BLS.loadState(); if(st.audioMode==='programmatic'){ AudioEngine.setVolume(st.volume||0.6); var pan = 0; if(st.programPreset==='click'){ AudioEngine.playClick(pan) } else if(st.programPreset==='ping'){ AudioEngine.playPing(pan) } else if(st.programPreset==='woodblock'){ AudioEngine.playWoodblock(pan) } else if(st.programPreset==='bell'){ AudioEngine.playBell(pan) } else if(st.programPreset==='bass'){ AudioEngine.playBass(pan) } else if(st.programPreset==='kick'){ AudioEngine.playKick(pan) } else if(st.programPreset==='snare'){ AudioEngine.playSnare(pan) } else if(st.programPreset==='hihat'){ AudioEngine.playHiHat(pan) } else if(st.programPreset==='sweep'){ AudioEngine.playSweep(pan) } else if(st.programPreset==='zap'){ AudioEngine.playZap(pan) } else if(st.programPreset==='bubble'){ AudioEngine.playBubble(pan) } else if(st.programPreset==='pink'){ AudioEngine.startPinkNoisePan(st.panRate||0.5); setTimeout(function(){ AudioEngine.stopNoise() }, 800) } else if(st.programPreset==='hybrid'){ AudioEngine.startHybrid(st.cueRate||1.2); setTimeout(function(){ AudioEngine.stopNoise() }, 1000) } } }catch(e){} }); + knobSens.addEventListener('input', function(){ updateKnobSensLabel() }); + if(clearSessionsBtn){ clearSessionsBtn.addEventListener('click', function(){ var n = (patientName.value||'').trim()||'Unnamed'; var p = getPatient(n); p.sessions = []; p.count = 0; p.last = null; setPatient(p); refreshPatientUI() }) } + if(deletePatientBtn){ deletePatientBtn.addEventListener('click', function(){ var n = (patientName.value||'').trim()||'Unnamed'; deletePatient(n); patientName.value=''; refreshPatientList(); refreshPatientUI() }) } + var loadPatientBtn = document.getElementById('loadPatient'); + if(loadPatientBtn){ loadPatientBtn.addEventListener('click', function(){ var sel = document.getElementById('patientList'); if(!sel) return; var n = sel.value; if(!n) return; patientName.value = n; refreshPatientUI() }) } + var speedBoost = document.getElementById('speedBoost'); + if(speedBoost){ speedBoost.addEventListener('click', function(e){ var m = e.target.dataset.mult; if(!m) return; var mult = parseInt(m,10)||1; var btns = speedBoost.querySelectorAll('button'); for(var i=0;i127) b0 = b0-256 }catch(_){ b0 = 0 } } var sens = parseInt(knobSens.value,10)||1; var delta = b0 * sens; if(delta!==0){ var current = parseInt(speed.value,10)||250; var next = Math.max(parseInt(speed.min,10), Math.min(parseInt(speed.max,10), current + delta)); speed.value = String(next); updateSpeedLabel(); applyState({speed: next}, true) } }) }catch(e){ deviceStatus.textContent = 'Error connecting' } } + connectHID.addEventListener('click', connectHIDDevice); +var tickId = null; +function startTimer(){ if(tickId) clearInterval(tickId); var durationSec = Math.max(0, (parseInt(timerMinutes.value||'0',10)||0)*60); var startedAt = Date.now(); applyState({running:true, durationSec:durationSec, startedAt:startedAt}, true); statusText.textContent = 'Running'; tickId = setInterval(function(){ var st = BLS.loadState(); if(!st.running){ clearInterval(tickId); tickId=null; return } var elapsed = Math.floor((Date.now()-st.startedAt)/1000); var rem = Math.max(0, st.durationSec - elapsed); var em = Math.floor(elapsed/60), es = elapsed%60; elapsedText.textContent = String(em).padStart(2,'0')+':'+String(es).padStart(2,'0'); var rm = Math.floor(rem/60); timerDisplay.textContent = String(rm).padStart(2,'0')+':'+String(rem%60).padStart(2,'0'); if(rem<=0){ stopTimer() } }, 250); } +function stopTimer(){ if(tickId){ clearInterval(tickId); tickId=null } recordSession(); applyState({running:false}, true); statusText.textContent = 'Idle'; refreshPatientUI() } +startBtn.addEventListener('click', startTimer); +endBtn.addEventListener('click', stopTimer); + openPatient.addEventListener('click', function(){ window.open('patient.html','bls-patient'); }); + renderer.resize(360,220); + updateSpeedLabel(); updateSizeLabel(); updateYPercentLabel(); updateTimerLabel(); updateGlowIntensityLabel(); updateGlowRateLabel(); updateRampLabel(); updateEdgePauseLabel(); updateWiggleLabel(); updatePanRateLabel(); updateCueRateLabel(); updateVolumeLabel(); statusText.textContent = state.running? 'Running':'Idle'; setActive(shapeControls,'shape','shape'); setActive(directionControls,'direction','direction'); setActive(easingControls,'easing','easingMode'); + audioEnable.checked = !!state.audioEnabled; if(state.audioName){ var opt = document.createElement('option'); opt.value = state.audioName; opt.textContent = state.audioName; audioSelect.appendChild(opt); audioSelect.value = state.audioName } + glowEnable.checked = !!state.glowEnabled; + glowIntensity.value = Math.round((state.glowIntensity||1)*100); + rampEnable.checked = !!state.rampEnabled; + rampSeconds.value = parseInt(state.rampSeconds||10,10); + edgePause.value = parseInt(state.edgePauseMs||0,10); + wiggleEnable.checked = !!state.wiggleEnabled; + wiggleAmplitude.value = parseInt(state.wiggleAmplitude||0,10); + audioMode.value = state.audioMode || 'programmatic'; + programPreset.value = state.programPreset || 'pink'; + syncPan.checked = !!state.syncPan; + panRate.value = String(state.panRate||0.5); + cueRate.value = String(state.cueRate||1.2); + volume.value = String(Math.round((state.volume||0.6)*100)); + glowRate.value = String(state.glowRate||0.5); + refreshAudioMode(); + updateKnobSensLabel(); + yPercent.value = parseInt(state.yPercent||50,10); + updateYPercentLabel(); + refreshPresetList(); refreshPatientList(); + refreshPatientUI(); +} +function waitForBLS(){ if(window.BLS){ initAdmin() } else { setTimeout(waitForBLS, 50) } } +if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', waitForBLS) } else { waitForBLS() } diff --git a/js/audio.js b/js/audio.js new file mode 100644 index 0000000..3a40e58 --- /dev/null +++ b/js/audio.js @@ -0,0 +1,450 @@ +;(function(){ + function hasStereoPanner(ctx){ return !!(ctx && ctx.createStereoPanner) } + function makePanner(ctx){ + if(hasStereoPanner(ctx)) return ctx.createStereoPanner(); + var gainL = ctx.createGain(); var gainR = ctx.createGain(); var merger = ctx.createChannelMerger(2); + gainL.connect(merger, 0, 0); gainR.connect(merger, 0, 1); + var node = { + _ctx: ctx, _l: gainL, _r: gainR, _merger: merger, + connect: function(dest){ merger.connect(dest) }, + pan: { value: 0 }, + setPan: function(p){ var pan = Math.max(-1, Math.min(1, p||0)); var l = Math.cos((pan+1)*Math.PI/4); var r = Math.sin((pan+1)*Math.PI/4); this._l.gain.setValueAtTime(l, ctx.currentTime); this._r.gain.setValueAtTime(r, ctx.currentTime); this.pan.value = pan } + } + return node + } + + var AudioEngine = { + ctx: null, + master: null, + noiseNodes: [], + panInterval: null, + + ensureCtx: function(){ + if(!this.ctx){ + this.ctx = new (window.AudioContext||window.webkitAudioContext)(); + this.master = this.ctx.createGain(); + this.master.gain.value = 0.6; + this.master.connect(this.ctx.destination) + } + if(this.ctx.state==='suspended'){ this.ctx.resume() } + return this.ctx + }, + + setVolume: function(v){ + var ctx = this.ensureCtx(); + var val = Math.max(0, Math.min(1, v||0.6)); + this.master.gain.setValueAtTime(val, ctx.currentTime) + }, + + // Enhanced original sounds + playClick: function(pan){ + var ctx = this.ensureCtx(); + var osc = ctx.createOscillator(); + var gain = ctx.createGain(); + var filter = ctx.createBiquadFilter(); + var p = makePanner(ctx); + if(p.setPan) p.setPan(pan||0); else if(p.pan) p.pan.value = pan||0; + + osc.type='square'; + osc.frequency.setValueAtTime(1200, ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(800, ctx.currentTime+0.02); + + filter.type = 'lowpass'; + filter.frequency.value = 3000; + filter.Q.value = 2; + + gain.gain.setValueAtTime(0.0001, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.5, ctx.currentTime+0.003); + gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime+0.025); + + osc.connect(filter); + filter.connect(gain); + if(p._l){ gain.connect(p._l); gain.connect(p._r) } else { gain.connect(p) } + p.connect(this.master); + osc.start(); + osc.stop(ctx.currentTime+0.05) + }, + + playPing: function(pan){ + var ctx = this.ensureCtx(); + var osc = ctx.createOscillator(); + var osc2 = ctx.createOscillator(); + var gain = ctx.createGain(); + var filter = ctx.createBiquadFilter(); + var p = makePanner(ctx); + if(p.setPan) p.setPan(pan||0); + + osc.type='sine'; + osc.frequency.setValueAtTime(1600, ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(800, ctx.currentTime+0.15); + + osc2.type='sine'; + osc2.frequency.setValueAtTime(2400, ctx.currentTime); + osc2.frequency.exponentialRampToValueAtTime(1200, ctx.currentTime+0.15); + + filter.type = 'lowpass'; + filter.frequency.value = 4000; + filter.Q.value = 1; + + gain.gain.setValueAtTime(0.0001, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.4, ctx.currentTime+0.01); + gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime+0.2); + + osc.connect(filter); + osc2.connect(filter); + filter.connect(gain); + if(p._l){ gain.connect(p._l); gain.connect(p._r) } else { gain.connect(p) } + p.connect(this.master); + osc.start(); + osc2.start(); + osc.stop(ctx.currentTime+0.25); + osc2.stop(ctx.currentTime+0.25); + }, + + playWoodblock: function(pan){ + var ctx = this.ensureCtx(); + var buffer = ctx.createBuffer(1, 44100, 44100); + var data = buffer.getChannelData(0); + for(var i=0;i0){ wigglePhase += 0.04 + state.speed/4000; var s = Math.sin(wigglePhase); if(state.direction==='lr'){ wy = s * state.wiggleAmplitude } else if(state.direction==='ud'){ wx = s * state.wiggleAmplitude } else { wx = -dir.y * s * state.wiggleAmplitude; wy = dir.x * s * state.wiggleAmplitude } } if(doGlow){ ctx.save(); ctx.shadowBlur = r * gi * (0.7 + a); ctx.shadowColor = 'hsla('+hue+',100%,60%,0.9)' } ctx.fillStyle=state.shapeColor; if(state.shape==='circle'){ ctx.beginPath(); ctx.arc(x+wx,y+wy,r,0,Math.PI*2); ctx.closePath(); ctx.fill() } else if(state.shape==='square'){ ctx.fillRect(x-r+wx,y-r+wy,r*2,r*2) } else { var h2=r*1.6; ctx.beginPath(); ctx.moveTo(x+wx,y-h2/2+wy); ctx.lineTo(x-r+wx,y+h2/2+wy); ctx.lineTo(x+r+wx,y+h2/2+wy); ctx.closePath(); ctx.fill() } if(doGlow){ ctx.restore() } } function effectiveSpeed(w,h,r){ var base = state.speed; var ramp = 1; if(state.rampEnabled && state.startedAt && state.rampSeconds>0){ var dt = Math.max(0,(Date.now()-state.startedAt)/1000); var k = Math.min(1, dt/state.rampSeconds); ramp = 0.2 + 0.8*k } var s=1; if(state.easingMode==='ease'){ if(dir.x!==0){ var u = (pos.x - r)/(w - 2*r); s = 0.3 + 0.7*(0.5 - 0.5*Math.cos(2*Math.PI*Math.max(0,Math.min(1,u)))) } else { var v = (pos.y - r)/(h - 2*r); s = 0.3 + 0.7*(0.5 - 0.5*Math.cos(2*Math.PI*Math.max(0,Math.min(1,v)))) } } return base * ramp * s } function step(dt){ var w=canvas.width,h=canvas.height,r=state.size; if(pauseMs>0){ pauseMs = Math.max(0, pauseMs - dt*1000); return } var sp = effectiveSpeed(w,h,r); pos.x += dir.x * sp * dt; pos.y += dir.y * sp * dt; var hit=false; if(pos.x <= r){ pos.x = r; dir.x = Math.abs(dir.x); hit=true } if(pos.x >= w - r){ pos.x = w - r; dir.x = -Math.abs(dir.x); hit=true } if(pos.y <= r){ pos.y = r; dir.y = Math.abs(dir.y); hit=true } if(pos.y >= h - r){ pos.y = h - r; dir.y = -Math.abs(dir.y); hit=true } if(hit && state.edgePauseMs>0){ pauseMs = state.edgePauseMs } if(onUpdate){ onUpdate({x:pos.x,y:pos.y,w:w,h:h}) } } var last=0; function loop(t){ if(!running){ return } if(!last) last=t; var dt=(t-last)/1000; last=t; clear(); draw(); step(dt); requestAnimationFrame(loop) } function start(){ if(!running){ running=true; last=0; requestAnimationFrame(loop) } } function stop(){ running=false } function setState(next){ var hadY = next && Object.prototype.hasOwnProperty.call(next,'yPercent'); state=mergeState(state,next||{}); setDir(); ensurePos(); if(hadY){ var r=state.size; pos.y = yFromPercent(canvas.height, r, state.yPercent||50) } clear(); draw() } function resize(w,h){ if(typeof w==='number') canvas.width=w; if(typeof h==='number') canvas.height=h; ensurePos(); clear(); draw() } function setOnUpdate(fn){ onUpdate=fn } setDir(); ensurePos(); clear(); draw(); return { start:start, stop:stop, setState:setState, resize:resize, getState:function(){return state}, setOnUpdate:setOnUpdate } } + window.bls = { createChannel:function(name){ return new Channel(name) }, defaultState:defaultState, loadState:loadState, saveState:saveState, mergeState:mergeState, Renderer:Renderer, + listPresets:function(){ var m=loadPresets(); return Object.keys(m).sort() }, + savePreset:function(name, st){ if(!name) return; var m=loadPresets(); m[name]=sanitizePreset(st||loadState()); savePresets(m) }, + loadPreset:function(name){ var m=loadPresets(); return m[name]||null }, + deletePreset:function(name){ var m=loadPresets(); if(m[name]){ delete m[name]; savePresets(m) } }, + exportPresets:function(){ return JSON.stringify(loadPresets(), null, 2) }, + importPresets:function(json){ try{ var data = typeof json==='string'? JSON.parse(json): json; if(!data||typeof data!=='object') return; var m=loadPresets(); for(var k in data){ if(Object.prototype.hasOwnProperty.call(data,k)) m[k]=sanitizePreset(data[k]) } savePresets(m) }catch(e){} } + } + window.BLS = window.bls +})(); diff --git a/patient.html b/patient.html new file mode 100644 index 0000000..bd8de36 --- /dev/null +++ b/patient.html @@ -0,0 +1,52 @@ +BLS + + + + + BLS Patient View + + + + + + + + + +