Merge pull request #1 from type-two/code-cleanup-and-optimization-12114006608232278591
Refactor and optimize EMDR simulator for performance
This commit is contained in:
commit
e3396e6084
@ -131,8 +131,9 @@ function recordSession(){ var n = (patientName.value||'').trim()||'Unnamed'; var
|
||||
function refreshPresetList(){ var names = BLS.listPresets(); var html=''; for(var i=0;i<names.length;i++){ var n=names[i]; html += '<option value="'+n+'">'+n+'</option>' } presetList.innerHTML = html }
|
||||
function applyState(next, broadcast){
|
||||
var prevRunning = state && !!state.running;
|
||||
state = BLS.mergeState(state, next||{});
|
||||
renderer.setState(state);
|
||||
// Explicitly persist=true for Admin actions
|
||||
state = BLS.mergeState(state, next||{}, true);
|
||||
renderer.setState(state, false); // No need to double-save in renderer
|
||||
if(prevRunning !== !!state.running){ if(state.running){ renderer.start() } else { renderer.stop() } }
|
||||
if(broadcast){ chan.send(state) }
|
||||
try{ if(window.Recorder && Recorder.running){ Recorder.recordState(BLS.loadState()) } }catch(_){ }
|
||||
@ -147,8 +148,8 @@ function recordSession(){ var n = (patientName.value||'').trim()||'Unnamed'; var
|
||||
function labelForEasing(e){ return e==='ease'? 'Ease In-Out' : 'Linear' }
|
||||
function labelForProgram(p){ var map={pink:'Pink Noise', hybrid:'Hybrid', tone:'Tone', click:'Click', ping:'Ping', woodblock:'Woodblock', bell:'Bell', bass:'Bass', kick:'Kick', snare:'Snare', hihat:'Hi-Hat', sweep:'Sweep', zap:'Zap', bubble:'Bubble'}; return map[p]||p }
|
||||
function applyStateLocal(next){
|
||||
state = BLS.mergeState(state, next||{});
|
||||
renderer.setState(state);
|
||||
state = BLS.mergeState(state, next||{}, true);
|
||||
renderer.setState(state, false);
|
||||
if(speed){ speed.value = String(parseInt(state.speed||parseInt(speed.value,10)||250, 10)) }
|
||||
if(size){ size.value = String(state.size||parseFloat(size.value)||80) }
|
||||
if(glowEnable){ glowEnable.checked = !!state.glowEnabled }
|
||||
|
||||
354
js/bls.js
354
js/bls.js
@ -1,26 +1,340 @@
|
||||
;(function(){
|
||||
// --- Constants & Defaults ---
|
||||
var defaultState = {
|
||||
shape:'circle', shapeColor:'#00ff88', bgColor:'#000000', direction:'lr',
|
||||
speed:250, speedMultiplier:1, size:28,
|
||||
yPercent:50, yOscillateEnabled:false, yOscillateRate:0.5,
|
||||
glowEnabled:false, glowIntensity:1, glowRate:0.5,
|
||||
rampEnabled:false, rampSeconds:10, easingMode:'linear',
|
||||
edgePauseMs:0,
|
||||
wiggleEnabled:false, wiggleAmplitude:0,
|
||||
audioMode:'programmatic', programPreset:'pink',
|
||||
syncPan:true, panRate:0.5, cueRate:1.2, volume:0.6,
|
||||
audioName:null, audioEnabled:false,
|
||||
running:false, paused:false, pausedMs:0, pausedAt:null, durationSec:600, startedAt:null,
|
||||
patientWidth:'full',
|
||||
toneFreq:440, toneDurationMs:70, adsrAttackMs:8, adsrDecayMs:80, adsrSustainLevel:0.3, adsrReleaseMs:10
|
||||
};
|
||||
|
||||
// --- Channel (Cross-Tab Communication) ---
|
||||
function Channel(name){ this.name = name; this.handlers = []; }
|
||||
Channel.prototype.on = function(handler){ this.handlers.push(handler) };
|
||||
Channel.prototype.send = function(payload){ try{ localStorage.setItem('bls_session', JSON.stringify({name:this.name, payload:payload, t:Date.now()})) }catch(e){} };
|
||||
window.addEventListener('storage', function(e){ if(e.key!=='bls_session') return; try{ var data = JSON.parse(e.newValue||'{}'); if(!data) return; if(Channel._handlers){ var hs = Channel._handlers[data.name]||[]; for(var i=0;i<hs.length;i++){ hs[i](data.payload) } } }catch(e){} });
|
||||
Channel.prototype.send = function(payload){
|
||||
try{
|
||||
localStorage.setItem('bls_session', JSON.stringify({name:this.name, payload:payload, t:Date.now()}));
|
||||
} catch(e){}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', function(e){
|
||||
if(e.key !== 'bls_session') return;
|
||||
try {
|
||||
var data = JSON.parse(e.newValue || '{}');
|
||||
if(!data) return;
|
||||
if(Channel._handlers){
|
||||
var hs = Channel._handlers[data.name] || [];
|
||||
for(var i=0; i<hs.length; i++){ hs[i](data.payload); }
|
||||
}
|
||||
} catch(e){}
|
||||
});
|
||||
|
||||
Channel._handlers = {};
|
||||
Channel.prototype.on = function(handler){ if(!Channel._handlers[this.name]) Channel._handlers[this.name] = []; Channel._handlers[this.name].push(handler) };
|
||||
var defaultState = { shape:'circle', shapeColor:'#00ff88', bgColor:'#000000', direction:'lr', speed:250, speedMultiplier:1, size:28, yPercent:50, yOscillateEnabled:false, yOscillateRate:0.5, glowEnabled:false, glowIntensity:1, glowRate:0.5, rampEnabled:false, rampSeconds:10, easingMode:'linear', edgePauseMs:0, wiggleEnabled:false, wiggleAmplitude:0, audioMode:'programmatic', programPreset:'pink', syncPan:true, panRate:0.5, cueRate:1.2, volume:0.6, audioName:null, audioEnabled:false, running:false, paused:false, pausedMs:0, pausedAt:null, durationSec:600, startedAt:null, patientWidth:'full', toneFreq:440, toneDurationMs:70, adsrAttackMs:8, adsrDecayMs:80, adsrSustainLevel:0.3, adsrReleaseMs:10 };
|
||||
function loadState(){ try{ var s = JSON.parse(localStorage.getItem('bls_state')||'null'); if(!s) return Object.assign({}, defaultState); return Object.assign({}, defaultState, s) }catch(e){ return Object.assign({}, defaultState) } }
|
||||
function saveState(s){ try{ localStorage.setItem('bls_state', JSON.stringify(s)) }catch(e){} }
|
||||
function mergeState(prev, next){ var s = Object.assign({}, prev, next); saveState(s); return s }
|
||||
function presetKeys(){ return ['shape','shapeColor','bgColor','direction','speed','speedMultiplier','size','yPercent','glowEnabled','glowIntensity','glowRate','rampEnabled','rampSeconds','easingMode','edgePauseMs','wiggleEnabled','wiggleAmplitude','audioMode','programPreset','syncPan','panRate','cueRate','volume','audioName','audioEnabled','patientWidth','toneFreq','toneDurationMs','adsrAttackMs','adsrDecayMs','adsrSustainLevel','adsrReleaseMs'] }
|
||||
function sanitizePreset(s){ var out={}; var keys=presetKeys(); for(var i=0;i<keys.length;i++){ var k=keys[i]; if(Object.prototype.hasOwnProperty.call(s,k)) out[k]=s[k] } return out }
|
||||
function loadPresets(){ try{ var m = JSON.parse(localStorage.getItem('bls_presets')||'null'); return m&&typeof m==='object'? m : {} }catch(e){ return {} } }
|
||||
function savePresets(m){ try{ localStorage.setItem('bls_presets', JSON.stringify(m)) }catch(e){} }
|
||||
function Renderer(canvas){ var ctx = canvas.getContext('2d'); var state = loadState(); var pos=null; var dir={x:1,y:0}; var running=false; var onUpdate=null; var pauseMs=0; var wigglePhase=0; function setDir(){ if(state.direction==='lr'){dir.x=1;dir.y=0}else if(state.direction==='ud'){dir.x=0;dir.y=1}else if(state.direction==='diag_down'){dir.x=1;dir.y=1}else{dir.x=1;dir.y=-1} var len=Math.sqrt(dir.x*dir.x+dir.y*dir.y); dir.x/=len; dir.y/=len } function yFromPercent(h,r,p){ return Math.max(r, Math.min(h-r, r + (h-2*r) * (p/100))) } function ensurePos(){ var w=canvas.width,h=canvas.height,r=state.size; var defPct = (state.direction==='lr'?50:state.direction==='ud'?5:state.direction==='diag_down'?5:95); var pct = (state.yPercent===undefined || state.yPercent===null ? defPct : state.yPercent); if(!pos){ if(state.direction==='lr'){pos={x:r+2,y:yFromPercent(h,r,pct)}} else if(state.direction==='ud'){pos={x:w/2,y:yFromPercent(h,r,pct)}} else {pos={x:r+2,y:yFromPercent(h,r,pct)}} } else { pos.y = yFromPercent(h, r, pct) } pos.x=Math.max(r,Math.min(w-r,pos.x)); pos.y=Math.max(r,Math.min(h-r,pos.y)) } function clear(){ ctx.fillStyle=state.bgColor; ctx.fillRect(0,0,canvas.width,canvas.height) } function draw(){ var r=state.size; var x=pos.x,y=pos.y; var doGlow = !!state.glowEnabled; var gi = state.glowIntensity || 1; var t = Date.now()/1000; var freq = Math.max(0.05, state.glowRate||0.5); var a = 0.5 + 0.5*Math.sin(t*2*Math.PI*freq); var hue = (t*60*freq)%360; var wx=0, wy=0; if(state.wiggleEnabled && state.wiggleAmplitude>0){ 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 n = next||{}; var prevDir = state.direction; var prevSize = state.size; var prevY = state.yPercent; var hadDir = Object.prototype.hasOwnProperty.call(n,'direction'); var hadSize = Object.prototype.hasOwnProperty.call(n,'size'); var hadY = Object.prototype.hasOwnProperty.call(n,'yPercent'); state=mergeState(state,n); if(hadDir && state.direction!==prevDir){ setDir() } if(hadSize && state.size!==prevSize){ ensurePos() } if(hadY){ var r=state.size; var defPct=(state.direction==='lr'?50:state.direction==='ud'?5:state.direction==='diag_down'?5:95); var pct=(state.yPercent===undefined||state.yPercent===null?defPct:state.yPercent); pos.y = yFromPercent(canvas.height, r, pct) } 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){} }
|
||||
Channel.prototype.on = function(handler){
|
||||
if(!Channel._handlers[this.name]) Channel._handlers[this.name] = [];
|
||||
Channel._handlers[this.name].push(handler);
|
||||
};
|
||||
|
||||
// --- State Persistence ---
|
||||
function loadState(){
|
||||
try{
|
||||
var s = JSON.parse(localStorage.getItem('bls_state') || 'null');
|
||||
if(!s) return Object.assign({}, defaultState);
|
||||
return Object.assign({}, defaultState, s);
|
||||
} catch(e){
|
||||
return Object.assign({}, defaultState);
|
||||
}
|
||||
}
|
||||
window.BLS = window.bls
|
||||
|
||||
function saveState(s){
|
||||
try{ localStorage.setItem('bls_state', JSON.stringify(s)); } catch(e){}
|
||||
}
|
||||
|
||||
function mergeState(prev, next, persist){
|
||||
var s = Object.assign({}, prev, next);
|
||||
if(persist !== false) saveState(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
// --- Presets ---
|
||||
function presetKeys(){
|
||||
return ['shape','shapeColor','bgColor','direction','speed','speedMultiplier','size','yPercent','glowEnabled','glowIntensity','glowRate','rampEnabled','rampSeconds','easingMode','edgePauseMs','wiggleEnabled','wiggleAmplitude','audioMode','programPreset','syncPan','panRate','cueRate','volume','audioName','audioEnabled','patientWidth','toneFreq','toneDurationMs','adsrAttackMs','adsrDecayMs','adsrSustainLevel','adsrReleaseMs'];
|
||||
}
|
||||
|
||||
function sanitizePreset(s){
|
||||
var out={}; var keys=presetKeys();
|
||||
for(var i=0; i<keys.length; i++){
|
||||
var k=keys[i];
|
||||
if(Object.prototype.hasOwnProperty.call(s,k)) out[k]=s[k];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function loadPresets(){
|
||||
try{
|
||||
var m = JSON.parse(localStorage.getItem('bls_presets') || 'null');
|
||||
return m && typeof m==='object' ? m : {};
|
||||
} catch(e){ return {}; }
|
||||
}
|
||||
|
||||
function savePresets(m){
|
||||
try{ localStorage.setItem('bls_presets', JSON.stringify(m)); } catch(e){}
|
||||
}
|
||||
|
||||
// --- Renderer ---
|
||||
function Renderer(canvas){
|
||||
var ctx = canvas.getContext('2d');
|
||||
var state = loadState(); // Initial state loaded from storage
|
||||
var pos = null;
|
||||
var dir = {x:1, y:0};
|
||||
var running = false;
|
||||
var onUpdate = null;
|
||||
var pauseMs = 0;
|
||||
var wigglePhase = 0;
|
||||
|
||||
// Optimized: Reusable objects/vars to avoid GC
|
||||
var _drawVars = {
|
||||
x: 0, y: 0, r: 0, wx: 0, wy: 0,
|
||||
doGlow: false, gi: 0, freq: 0, a: 0, hue: 0, t: 0, s: 0, h2: 0
|
||||
};
|
||||
|
||||
function setDir(){
|
||||
if(state.direction === 'lr'){ dir.x=1; dir.y=0; }
|
||||
else if(state.direction === 'ud'){ dir.x=0; dir.y=1; }
|
||||
else if(state.direction === 'diag_down'){ dir.x=1; dir.y=1; }
|
||||
else { dir.x=1; dir.y=-1; }
|
||||
|
||||
var len = Math.sqrt(dir.x*dir.x + dir.y*dir.y);
|
||||
dir.x /= len;
|
||||
dir.y /= len;
|
||||
}
|
||||
|
||||
function yFromPercent(h, r, p){
|
||||
return Math.max(r, Math.min(h-r, r + (h-2*r) * (p/100)));
|
||||
}
|
||||
|
||||
function ensurePos(){
|
||||
var w=canvas.width, h=canvas.height, r=state.size;
|
||||
var defPct = (state.direction==='lr'?50 : state.direction==='ud'?5 : state.direction==='diag_down'?5 : 95);
|
||||
var pct = (state.yPercent===undefined || state.yPercent===null ? defPct : state.yPercent);
|
||||
|
||||
if(!pos){
|
||||
if(state.direction === 'lr'){ pos = {x: r+2, y: yFromPercent(h,r,pct)}; }
|
||||
else if(state.direction === 'ud'){ pos = {x: w/2, y: yFromPercent(h,r,pct)}; }
|
||||
else { pos = {x: r+2, y: yFromPercent(h,r,pct)}; }
|
||||
} else {
|
||||
pos.y = yFromPercent(h, r, pct);
|
||||
}
|
||||
pos.x = Math.max(r, Math.min(w-r, pos.x));
|
||||
pos.y = Math.max(r, Math.min(h-r, pos.y));
|
||||
}
|
||||
|
||||
function clear(){
|
||||
ctx.fillStyle = state.bgColor;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
function draw(){
|
||||
var v = _drawVars;
|
||||
v.r = state.size;
|
||||
v.x = pos.x;
|
||||
v.y = pos.y;
|
||||
v.doGlow = !!state.glowEnabled;
|
||||
v.gi = state.glowIntensity || 1;
|
||||
v.t = Date.now()/1000;
|
||||
v.wx = 0; v.wy = 0;
|
||||
|
||||
if(state.wiggleEnabled && state.wiggleAmplitude > 0){
|
||||
wigglePhase += 0.04 + state.speed/4000;
|
||||
v.s = Math.sin(wigglePhase);
|
||||
if(state.direction === 'lr'){ v.wy = v.s * state.wiggleAmplitude; }
|
||||
else if(state.direction === 'ud'){ v.wx = v.s * state.wiggleAmplitude; }
|
||||
else { v.wx = -dir.y * v.s * state.wiggleAmplitude; v.wy = dir.x * v.s * state.wiggleAmplitude; }
|
||||
}
|
||||
|
||||
if(v.doGlow){
|
||||
v.freq = Math.max(0.05, state.glowRate || 0.5);
|
||||
v.a = 0.5 + 0.5 * Math.sin(v.t * 2 * Math.PI * v.freq);
|
||||
v.hue = (v.t * 60 * v.freq) % 360;
|
||||
|
||||
ctx.save();
|
||||
ctx.shadowBlur = v.r * v.gi * (0.7 + v.a);
|
||||
ctx.shadowColor = 'hsla(' + v.hue + ',100%,60%,0.9)';
|
||||
}
|
||||
|
||||
ctx.fillStyle = state.shapeColor;
|
||||
|
||||
if(state.shape === 'circle'){
|
||||
ctx.beginPath();
|
||||
ctx.arc(v.x + v.wx, v.y + v.wy, v.r, 0, Math.PI*2);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
} else if(state.shape === 'square'){
|
||||
ctx.fillRect(v.x - v.r + v.wx, v.y - v.r + v.wy, v.r*2, v.r*2);
|
||||
} else {
|
||||
v.h2 = v.r * 1.6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(v.x + v.wx, v.y - v.h2/2 + v.wy);
|
||||
ctx.lineTo(v.x - v.r + v.wx, v.y + v.h2/2 + v.wy);
|
||||
ctx.lineTo(v.x + v.r + v.wx, v.y + v.h2/2 + v.wy);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
if(v.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, persist){
|
||||
var n = next || {};
|
||||
var prevDir = state.direction;
|
||||
var prevSize = state.size;
|
||||
var hadDir = Object.prototype.hasOwnProperty.call(n,'direction');
|
||||
var hadSize = Object.prototype.hasOwnProperty.call(n,'size');
|
||||
var hadY = Object.prototype.hasOwnProperty.call(n,'yPercent');
|
||||
|
||||
state = mergeState(state, n, persist); // Default persist=false for Renderer calls generally
|
||||
|
||||
if(hadDir && state.direction !== prevDir){ setDir(); }
|
||||
if(hadSize && state.size !== prevSize){ ensurePos(); }
|
||||
|
||||
if(hadY){
|
||||
var r = state.size;
|
||||
var defPct = (state.direction==='lr'?50 : state.direction==='ud'?5 : state.direction==='diag_down'?5 : 95);
|
||||
var pct = (state.yPercent===undefined || state.yPercent===null ? defPct : state.yPercent);
|
||||
pos.y = yFromPercent(canvas.height, r, pct);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// --- Export ---
|
||||
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;
|
||||
})();
|
||||
|
||||
450
js/patient.js
Normal file
450
js/patient.js
Normal file
@ -0,0 +1,450 @@
|
||||
;(function(){
|
||||
// --- Constants & Globals ---
|
||||
var canvas, renderer, chan, ws;
|
||||
var widthMode = 'full';
|
||||
var running = false;
|
||||
var audioFolder = null;
|
||||
var audioBuffer = null;
|
||||
var audioUnlocked = false;
|
||||
var pendingProgramState = null;
|
||||
|
||||
// Audio Context & Nodes
|
||||
var audioCtx = null, panner = null, gain = null, source = null;
|
||||
|
||||
// --- Initialization ---
|
||||
function initPatient(){
|
||||
canvas = document.getElementById('canvas');
|
||||
|
||||
// Load initial width preference
|
||||
var initialState = BLS.loadState();
|
||||
widthMode = initialState.patientWidth || 'full';
|
||||
|
||||
// Setup Resize Handler
|
||||
window.addEventListener('resize', onResize);
|
||||
size();
|
||||
sendViewport(); // Initial send
|
||||
|
||||
// Initialize Renderer
|
||||
// Note: We will modify BLS.Renderer to NOT save state by default,
|
||||
// or we will just use it as is and fix the saving issue in bls.js later.
|
||||
renderer = BLS.Renderer(canvas);
|
||||
|
||||
// Setup Local Channel (for same-browser tab communication)
|
||||
chan = BLS.createChannel('bls-session');
|
||||
chan.on(handleStateUpdate);
|
||||
|
||||
// Setup Network
|
||||
setupNetwork();
|
||||
|
||||
// Initial State Load
|
||||
renderer.setState(initialState);
|
||||
var st = renderer.getState();
|
||||
if(st.running){
|
||||
renderer.start();
|
||||
}
|
||||
|
||||
// Audio Unlock Binding
|
||||
bindUnlock();
|
||||
|
||||
// Sync Panner with Renderer
|
||||
renderer.setOnUpdate(onRenderUpdate);
|
||||
}
|
||||
|
||||
// --- Viewport Management ---
|
||||
function size(){
|
||||
var w = window.innerWidth;
|
||||
var h = window.innerHeight;
|
||||
var fac = (widthMode === 'reduced' ? 0.7 : 1);
|
||||
canvas.width = Math.floor(w * fac);
|
||||
canvas.height = h;
|
||||
}
|
||||
|
||||
var sendViewportTimeout = null;
|
||||
function sendViewport(){
|
||||
if(sendViewportTimeout) clearTimeout(sendViewportTimeout);
|
||||
sendViewportTimeout = setTimeout(doSendViewport, 200);
|
||||
}
|
||||
|
||||
function doSendViewport(){
|
||||
try {
|
||||
var pv = {
|
||||
winW: window.innerWidth,
|
||||
winH: window.innerHeight,
|
||||
canvasW: canvas.width,
|
||||
canvasH: canvas.height
|
||||
};
|
||||
|
||||
if(ws && ws.readyState === 1){
|
||||
ws.send(JSON.stringify({type:'patientViewport', payload: pv}));
|
||||
}
|
||||
|
||||
if(window.NetRTC && NetRTC.send){
|
||||
NetRTC.send({type:'patientViewport', payload: pv});
|
||||
}
|
||||
|
||||
try {
|
||||
if(window.opener){
|
||||
window.opener.postMessage({type:'patientViewport', payload: pv}, '*');
|
||||
}
|
||||
} catch(_){}
|
||||
|
||||
try {
|
||||
if(window.BLS){
|
||||
BLS.createChannel('bls-session').send({type:'patientViewport', payload: pv});
|
||||
}
|
||||
} catch(_){}
|
||||
} catch(e){}
|
||||
}
|
||||
|
||||
function onResize(){
|
||||
size();
|
||||
sendViewport();
|
||||
}
|
||||
|
||||
// --- State Management ---
|
||||
function handleStateUpdate(msg){
|
||||
if(!msg || typeof msg !== 'object') return;
|
||||
|
||||
// Deep copy to avoid reference issues
|
||||
var apply = Object.assign({}, msg);
|
||||
|
||||
// Fixups for specific flags
|
||||
if(apply.rampEnabled === false) apply.easingMode = 'linear';
|
||||
if(apply.glowEnabled === false) apply.glowIntensity = 0;
|
||||
|
||||
// Update Renderer State
|
||||
// Important: The patient view should not persist this state to localStorage
|
||||
// to avoid performance hits. We disable persistence here.
|
||||
renderer.setState(apply, false);
|
||||
|
||||
// Check for Width Mode change
|
||||
var nextMode = apply.patientWidth || widthMode;
|
||||
if(nextMode !== widthMode){
|
||||
widthMode = nextMode;
|
||||
size();
|
||||
}
|
||||
|
||||
var st = renderer.getState();
|
||||
|
||||
// Handle Running State
|
||||
if(!st.running || st.paused){
|
||||
stopAudio();
|
||||
stopProgrammatic();
|
||||
renderer.stop();
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Audio
|
||||
if(st.audioEnabled){
|
||||
if(st.audioMode === 'file'){
|
||||
handleFileAudio(st);
|
||||
} else {
|
||||
stopAudio();
|
||||
if(!audioUnlocked) bindUnlock();
|
||||
startProgrammatic(st);
|
||||
}
|
||||
} else {
|
||||
stopAudio();
|
||||
stopProgrammatic();
|
||||
}
|
||||
|
||||
if(!running){
|
||||
renderer.start();
|
||||
running = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Network ---
|
||||
function setupNetwork(){
|
||||
var qs = new URLSearchParams(location.search);
|
||||
var hashSid = (location.hash || '').replace('#', '');
|
||||
var wsSid = qs.get('sid') || hashSid || 'default';
|
||||
var wsUrl = qs.get('url') || ('ws://' + (location.hostname || 'localhost') + ':8787');
|
||||
var rtcMode = !!qs.get('p2p');
|
||||
|
||||
if(!rtcMode && wsUrl){
|
||||
connectWebSocket(wsUrl, wsSid);
|
||||
} else if(rtcMode){
|
||||
setupWebRTC();
|
||||
}
|
||||
}
|
||||
|
||||
function connectWebSocket(url, sid){
|
||||
try {
|
||||
ws = new WebSocket(url);
|
||||
} catch(e){ return; }
|
||||
|
||||
ws.onopen = function(){
|
||||
try {
|
||||
ws.send(JSON.stringify({type:'join', sessionId: sid, role:'patient'}));
|
||||
doSendViewport();
|
||||
} catch(e){}
|
||||
};
|
||||
|
||||
ws.onmessage = function(ev){
|
||||
try {
|
||||
var msg = JSON.parse(ev.data);
|
||||
if(msg && msg.type === 'state'){
|
||||
handleStateUpdate(msg.payload || {});
|
||||
}
|
||||
} catch(e){}
|
||||
};
|
||||
|
||||
ws.onclose = function(){ ws = null; };
|
||||
}
|
||||
|
||||
function setupWebRTC(){
|
||||
var offerStr = prompt('Paste Offer from Admin');
|
||||
if(offerStr){
|
||||
NetRTC.startAnswerer(offerStr).then(function(answer){
|
||||
alert('Show this Answer to Admin to complete connect');
|
||||
console.log(answer);
|
||||
try {
|
||||
if(navigator.clipboard) navigator.clipboard.writeText(answer);
|
||||
} catch(_){}
|
||||
});
|
||||
|
||||
NetRTC.onMessage(function(msg){
|
||||
if(msg && msg.type === 'state'){
|
||||
handleStateUpdate(msg.payload || {});
|
||||
}
|
||||
});
|
||||
|
||||
try { doSendViewport(); } catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Audio Logic ---
|
||||
async function handleFileAudio(st){
|
||||
if(!audioFolder){
|
||||
await ensureFolder();
|
||||
}
|
||||
if(st.audioName){
|
||||
await loadAudioByName(st.audioName);
|
||||
playBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
function bindUnlock(){
|
||||
var ov = document.getElementById('overlay');
|
||||
if(ov) ov.style.display = 'flex';
|
||||
|
||||
var events = ['pointerdown','keydown','touchstart','mousemove'];
|
||||
|
||||
function unlockOnce(){
|
||||
try {
|
||||
AudioEngine.ensureCtx();
|
||||
audioUnlocked = true;
|
||||
AudioEngine.playPing(0);
|
||||
} catch(e){}
|
||||
|
||||
if(ov) ov.style.display = 'none';
|
||||
|
||||
if(pendingProgramState){
|
||||
var st = pendingProgramState;
|
||||
pendingProgramState = null;
|
||||
startProgrammatic(st);
|
||||
}
|
||||
|
||||
events.forEach(function(ev){
|
||||
try{ window.removeEventListener(ev, unlockOnce); } catch(_){}
|
||||
});
|
||||
}
|
||||
|
||||
events.forEach(function(ev){
|
||||
window.addEventListener(ev, unlockOnce, { once:true });
|
||||
});
|
||||
}
|
||||
|
||||
var progRunning = false;
|
||||
|
||||
function stopProgrammatic(){
|
||||
AudioEngine.stopNoise();
|
||||
progRunning = false;
|
||||
}
|
||||
|
||||
function startProgrammatic(st){
|
||||
if(!audioUnlocked){
|
||||
pendingProgramState = st;
|
||||
bindUnlock();
|
||||
return;
|
||||
}
|
||||
|
||||
AudioEngine.setVolume(st.volume || 0.6);
|
||||
|
||||
// Clear previous update handlers related to audio
|
||||
renderer.setOnUpdate(onRenderUpdate);
|
||||
|
||||
if(st.programPreset === 'pink'){
|
||||
if(st.syncPan){
|
||||
// We handle panning in onRenderUpdate
|
||||
AudioEngine.startPinkNoisePan(Math.max(0.1, st.panRate || 0.5));
|
||||
} else {
|
||||
AudioEngine.startPinkNoisePan(Math.max(0.1, st.panRate || 0.5));
|
||||
}
|
||||
progRunning = true;
|
||||
} else if(st.programPreset === 'hybrid'){
|
||||
AudioEngine.startHybrid(Math.max(0.1, st.cueRate || 1.2));
|
||||
progRunning = true;
|
||||
} else {
|
||||
// Discrete sounds on edges
|
||||
renderer.setOnUpdate(function(info){
|
||||
onRenderUpdate(info); // Call the base update for visual/panner sync
|
||||
|
||||
var r = renderer.getState().size;
|
||||
var hitL = info.x <= r + 0.1;
|
||||
var hitR = info.x >= info.w - r - 0.1;
|
||||
var hitT = info.y <= r + 0.1;
|
||||
var hitB = info.y >= info.h - r - 0.1;
|
||||
|
||||
if(hitL || hitR || hitT || hitB){
|
||||
var pan = hitL ? -1 : (hitR ? 1 : 0);
|
||||
var pre = st.programPreset;
|
||||
|
||||
if(pre === 'tone'){
|
||||
AudioEngine.playTone({
|
||||
freq: st.toneFreq || 440,
|
||||
durationMs: st.toneDurationMs || 70,
|
||||
pan: pan,
|
||||
adsr: {
|
||||
attackMs: st.adsrAttackMs || 8,
|
||||
decayMs: st.adsrDecayMs || 80,
|
||||
sustain: st.adsrSustainLevel || 0.3,
|
||||
releaseMs: st.adsrReleaseMs || 10
|
||||
}
|
||||
});
|
||||
} else if(pre === 'click'){ AudioEngine.playClick(pan); }
|
||||
else if(pre === 'ping'){ AudioEngine.playPing(pan); }
|
||||
else if(pre === 'woodblock'){ AudioEngine.playWoodblock(pan); }
|
||||
else if(pre === 'bell'){ AudioEngine.playBell(pan); }
|
||||
else if(pre === 'bass'){ AudioEngine.playBass(pan); }
|
||||
else if(pre === 'kick'){ AudioEngine.playKick(pan); }
|
||||
else if(pre === 'snare'){ AudioEngine.playSnare(pan); }
|
||||
else if(pre === 'hihat'){ AudioEngine.playHiHat(pan); }
|
||||
else if(pre === 'sweep'){ AudioEngine.playSweep(pan); }
|
||||
else if(pre === 'zap'){ AudioEngine.playZap(pan); }
|
||||
else if(pre === 'bubble'){ AudioEngine.playBubble(pan); }
|
||||
else { AudioEngine.playWoodblock(pan); }
|
||||
}
|
||||
});
|
||||
progRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureAudio(){
|
||||
if(!audioCtx){
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
panner = audioCtx.createStereoPanner ? audioCtx.createStereoPanner() : null;
|
||||
gain = audioCtx.createGain();
|
||||
if(panner){ panner.connect(gain); }
|
||||
gain.connect(audioCtx.destination);
|
||||
}
|
||||
if(audioCtx.state === 'suspended'){ audioCtx.resume(); }
|
||||
}
|
||||
|
||||
async function chooseAudioFolder(){
|
||||
ensureAudio();
|
||||
if(window.showDirectoryPicker){
|
||||
try {
|
||||
audioFolder = await window.showDirectoryPicker();
|
||||
var st0 = BLS.loadState();
|
||||
if(st0.audioEnabled && st0.audioName){
|
||||
await loadAudioByName(st0.audioName);
|
||||
if(st0.running) playBuffer();
|
||||
}
|
||||
} catch(e){ return; }
|
||||
} else {
|
||||
// Fallback for non-Chrome browsers
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.webkitdirectory = true;
|
||||
input.multiple = true;
|
||||
input.addEventListener('change', function(){
|
||||
var st0 = BLS.loadState();
|
||||
var files = Array.from(input.files);
|
||||
var match = files.find(function(f){ return f.name === st0.audioName });
|
||||
if(match){
|
||||
var fr = new FileReader();
|
||||
fr.onload = function(){
|
||||
audioCtx.decodeAudioData(fr.result).then(function(buf){
|
||||
audioBuffer = buf;
|
||||
if(st0.running && st0.audioEnabled) playBuffer();
|
||||
});
|
||||
};
|
||||
fr.readAsArrayBuffer(match);
|
||||
}
|
||||
});
|
||||
input.click();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAudioByName(name){
|
||||
if(!name) return;
|
||||
ensureAudio();
|
||||
if(!audioFolder) return;
|
||||
try {
|
||||
let handle = null;
|
||||
for await (const [n,h] of audioFolder.entries()){
|
||||
if(h.kind === 'file' && n === name){ handle = h; break; }
|
||||
}
|
||||
if(!handle) return;
|
||||
var file = await handle.getFile();
|
||||
var buf = await file.arrayBuffer();
|
||||
audioBuffer = await audioCtx.decodeAudioData(buf);
|
||||
} catch(e){}
|
||||
}
|
||||
|
||||
function playBuffer(){
|
||||
if(!audioBuffer || !audioCtx) return;
|
||||
if(source){
|
||||
try{ source.stop(); } catch(e){}
|
||||
source.disconnect();
|
||||
}
|
||||
source = audioCtx.createBufferSource();
|
||||
source.buffer = audioBuffer;
|
||||
source.loop = true;
|
||||
var nodeChainStart = source;
|
||||
if(panner){ nodeChainStart.connect(panner); } else { nodeChainStart.connect(gain); }
|
||||
source.start();
|
||||
}
|
||||
|
||||
function stopAudio(){
|
||||
if(source){
|
||||
try{ source.stop(); } catch(e){}
|
||||
source.disconnect();
|
||||
source = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureFolder(){
|
||||
if(!audioFolder){
|
||||
try{ await chooseAudioFolder(); } catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
function onRenderUpdate(info){
|
||||
if(panner){
|
||||
var pan = (info.x / info.w) * 2 - 1;
|
||||
if(pan < -1) pan = -1;
|
||||
if(pan > 1) pan = 1;
|
||||
try{ panner.pan.value = pan; } catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Bootstrap ---
|
||||
function wait(){
|
||||
if(window.BLS){
|
||||
initPatient();
|
||||
} else {
|
||||
setTimeout(wait, 50);
|
||||
}
|
||||
}
|
||||
|
||||
if(document.readyState === 'loading'){
|
||||
document.addEventListener('DOMContentLoaded', wait);
|
||||
} else {
|
||||
wait();
|
||||
}
|
||||
|
||||
})();
|
||||
75
patient.html
75
patient.html
@ -1,4 +1,4 @@
|
||||
BLS<!doctype html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@ -23,77 +23,6 @@ BLS<!doctype html>
|
||||
<script src="js/bls.js"></script>
|
||||
<script src="js/audio.js"></script>
|
||||
<script src="js/webrtc.js"></script>
|
||||
<script>
|
||||
function initPatient(){
|
||||
var canvas = document.getElementById('canvas')
|
||||
var widthMode = (BLS.loadState().patientWidth||'full');
|
||||
function size(){ var w = window.innerWidth, h = window.innerHeight; var fac = (widthMode==='reduced'? 0.7 : 1); canvas.width = Math.floor(w*fac); canvas.height = h }
|
||||
function sendViewport(){ try{ var pv = { winW: window.innerWidth, winH: window.innerHeight, canvasW: canvas.width, canvasH: canvas.height }; if(ws && ws.readyState===1){ ws.send(JSON.stringify({type:'patientViewport', payload: pv})) } if(window.NetRTC && NetRTC.send){ NetRTC.send({type:'patientViewport', payload: pv}) } try{ if(window.opener){ window.opener.postMessage({type:'patientViewport', payload: pv}, '*') } }catch(_){ } try{ if(window.BLS){ BLS.createChannel('bls-session').send({type:'patientViewport', payload: pv}) } }catch(_){ } }catch(e){} }
|
||||
size(); sendViewport(); window.addEventListener('resize', function(){ size(); sendViewport() })
|
||||
var renderer = BLS.Renderer(canvas)
|
||||
var chan = BLS.createChannel('bls-session')
|
||||
var qs = new URLSearchParams(location.search)
|
||||
var hashSid = (location.hash||'').replace('#','')
|
||||
var wsSid = qs.get('sid') || hashSid || 'default'
|
||||
var wsUrl = qs.get('url') || ('ws://' + (location.hostname||'localhost') + ':8787')
|
||||
var ws = null
|
||||
var rtcMode = !!qs.get('p2p')
|
||||
chan.on(function(msg){ if(!msg || typeof msg !== 'object') return; var apply = Object.assign({}, msg); if(apply && apply.rampEnabled === false){ apply.easingMode = 'linear' } if(apply && apply.glowEnabled === false){ apply.glowIntensity = 0 } renderer.setState(apply); var st = renderer.getState(); if(st.running){ renderer.start() } else { renderer.stop() } })
|
||||
var running = false;
|
||||
function wsConnect(){ if(!wsUrl) return; try{ ws = new WebSocket(wsUrl) }catch(e){ return } ws.onopen = function(){ try{ ws.send(JSON.stringify({type:'join', sessionId: wsSid, role:'patient'})) }catch(e){} };
|
||||
ws.onmessage = function(ev){
|
||||
try{
|
||||
var msg = JSON.parse(ev.data);
|
||||
if(msg && msg.type==='state'){
|
||||
var apply = Object.assign({}, msg.payload||{});
|
||||
if(apply && apply.rampEnabled === false){ apply.easingMode = 'linear' }
|
||||
if(apply && apply.glowEnabled === false){ apply.glowIntensity = 0 }
|
||||
renderer.setState(apply);
|
||||
var nextMode = apply.patientWidth || widthMode;
|
||||
if(nextMode !== widthMode){ widthMode = nextMode; size() }
|
||||
var st = renderer.getState();
|
||||
if(!st.running || st.paused){ stopAudio(); stopProgrammatic(); renderer.stop(); running=false; return }
|
||||
if(st.audioEnabled){
|
||||
if(st.audioMode==='file'){
|
||||
(async function(){ if(!audioFolder){ await ensureFolder() } if(st.audioName){ await loadAudioByName(st.audioName); playBuffer() } })()
|
||||
} else {
|
||||
stopAudio(); if(!audioUnlocked) bindUnlock(); startProgrammatic(st)
|
||||
}
|
||||
} else { stopAudio(); stopProgrammatic() }
|
||||
if(!running){ renderer.start(); running=true }
|
||||
}
|
||||
}catch(e){}
|
||||
};
|
||||
ws.onclose = function(){ ws = null }
|
||||
ws.onopen = function(){ try{ sendViewport() }catch(e){} }
|
||||
}
|
||||
if(!rtcMode && wsUrl){ wsConnect() }
|
||||
if(rtcMode){
|
||||
var offerStr = prompt('Paste Offer from Admin');
|
||||
if(offerStr){ NetRTC.startAnswerer(offerStr).then(function(answer){ alert('Show this Answer to Admin to complete connect'); console.log(answer); try{ navigator.clipboard && navigator.clipboard.writeText(answer) }catch(_){ } }); NetRTC.onMessage(function(msg){ if(msg && msg.type==='state'){ var apply = Object.assign({}, msg.payload||{}); if(apply && apply.rampEnabled === false){ apply.easingMode = 'linear' } if(apply && apply.glowEnabled === false){ apply.glowIntensity = 0 } renderer.setState(apply); var st = renderer.getState(); if(!st.running || st.paused){ stopAudio(); stopProgrammatic(); renderer.stop(); return } if(st.audioEnabled){ if(st.audioMode==='file'){ (async function(){ if(!audioFolder){ await ensureFolder() } if(st.audioName){ await loadAudioByName(st.audioName); playBuffer() } })() } else { stopAudio(); if(!audioUnlocked) bindUnlock(); startProgrammatic(st) } } else { stopAudio(); stopProgrammatic() } renderer.start() } }); try{ sendViewport() }catch(e){} }
|
||||
}
|
||||
renderer.setState(BLS.loadState())
|
||||
var st = renderer.getState(); if(st.running){ renderer.start() }
|
||||
var audioCtx = null, panner = null, gain = null, source = null, audioFolder = null, audioBuffer = null
|
||||
var audioUnlocked = false
|
||||
var pendingProgramState = null
|
||||
function bindUnlock(){ var ov = document.getElementById('overlay'); if(ov){ ov.style.display='flex' } ['pointerdown','keydown','touchstart','mousemove'].forEach(function(ev){ window.addEventListener(ev, function once(){ try{ AudioEngine.ensureCtx(); audioUnlocked = true; AudioEngine.playPing(0) }catch(e){} if(ov){ ov.style.display='none' } if(pendingProgramState){ var st = pendingProgramState; pendingProgramState = null; startProgrammatic(st) } try{ window.removeEventListener(ev, once) }catch(_){ } }, { once:true }) }) }
|
||||
bindUnlock()
|
||||
var progRunning = false
|
||||
function stopProgrammatic(){ AudioEngine.stopNoise(); progRunning=false }
|
||||
function startProgrammatic(st){ if(!audioUnlocked){ pendingProgramState = st; bindUnlock(); return } AudioEngine.setVolume(st.volume||0.6); if(st.programPreset==='pink'){ if(st.syncPan){ renderer.setOnUpdate(function(info){ var pan=(info.x/info.w)*2-1; }); AudioEngine.startPinkNoisePan(Math.max(0.1, st.panRate||0.5)) } else { AudioEngine.startPinkNoisePan(Math.max(0.1, st.panRate||0.5)) } progRunning=true } else if(st.programPreset==='hybrid'){ AudioEngine.startHybrid(Math.max(0.1, st.cueRate||1.2)); progRunning=true } else { renderer.setOnUpdate(function(info){ var r = renderer.getState().size; var hitL = info.x<=r+0.1; var hitR = info.x>=info.w-r-0.1; var hitT = info.y<=r+0.1; var hitB = info.y>=info.h-r-0.1; if(hitL||hitR||hitT||hitB){ var pan = hitL? -1 : hitR? 1 : 0; var pre = st.programPreset; if(pre==='tone'){ AudioEngine.playTone({ freq: st.toneFreq||440, durationMs: st.toneDurationMs||70, pan: pan, adsr:{ attackMs: st.adsrAttackMs||8, decayMs: st.adsrDecayMs||80, sustain: st.adsrSustainLevel||0.3, releaseMs: st.adsrReleaseMs||10 } }) } else if(pre==='click'){ AudioEngine.playClick(pan) } else if(pre==='ping'){ AudioEngine.playPing(pan) } else if(pre==='woodblock'){ AudioEngine.playWoodblock(pan) } else if(pre==='bell'){ AudioEngine.playBell(pan) } else if(pre==='bass'){ AudioEngine.playBass(pan) } else if(pre==='kick'){ AudioEngine.playKick(pan) } else if(pre==='snare'){ AudioEngine.playSnare(pan) } else if(pre==='hihat'){ AudioEngine.playHiHat(pan) } else if(pre==='sweep'){ AudioEngine.playSweep(pan) } else if(pre==='zap'){ AudioEngine.playZap(pan) } else if(pre==='bubble'){ AudioEngine.playBubble(pan) } else { AudioEngine.playWoodblock(pan) } } }); progRunning=true } }
|
||||
function ensureAudio(){ if(!audioCtx){ audioCtx = new (window.AudioContext||window.webkitAudioContext)(); panner = audioCtx.createStereoPanner ? audioCtx.createStereoPanner() : null; gain = audioCtx.createGain(); if(panner){ panner.connect(gain) } gain.connect(audioCtx.destination) } if(audioCtx.state==='suspended'){ audioCtx.resume() } }
|
||||
async function chooseAudioFolder(){ ensureAudio(); if(window.showDirectoryPicker){ try{ audioFolder = await window.showDirectoryPicker(); var st0 = BLS.loadState(); if(st0.audioEnabled && st0.audioName){ await loadAudioByName(st0.audioName); if(st0.running) playBuffer() } }catch(e){ return } } else { var input = document.createElement('input'); input.type='file'; input.webkitdirectory=true; input.multiple=true; input.addEventListener('change', function(){ var st0 = BLS.loadState(); var files = Array.from(input.files); var match = files.find(function(f){ return f.name === st0.audioName }); if(match){ var fr = new FileReader(); fr.onload = function(){ audioCtx.decodeAudioData(fr.result).then(function(buf){ audioBuffer = buf; if(st0.running && st0.audioEnabled) playBuffer() }) }; fr.readAsArrayBuffer(match) } }); input.click(); }
|
||||
}
|
||||
async function loadAudioByName(name){ if(!name) return; ensureAudio(); if(!audioFolder) return; try{ let handle = null; for await (const [n,h] of audioFolder.entries()){ if(h.kind==='file' && n===name){ handle=h; break } } if(!handle) return; var file = await handle.getFile(); var buf = await file.arrayBuffer(); audioBuffer = await audioCtx.decodeAudioData(buf); }catch(e){} }
|
||||
function playBuffer(){ if(!audioBuffer || !audioCtx) return; if(source){ try{ source.stop() }catch(e){} source.disconnect() } source = audioCtx.createBufferSource(); source.buffer = audioBuffer; source.loop = true; var nodeChainStart = source; if(panner){ nodeChainStart.connect(panner) } else { nodeChainStart.connect(gain) } source.start() }
|
||||
function stopAudio(){ if(source){ try{ source.stop() }catch(e){} source.disconnect(); source=null } }
|
||||
async function ensureFolder(){ if(!audioFolder){ try{ await chooseAudioFolder() }catch(e){} } }
|
||||
renderer.setOnUpdate(function(info){ if(panner){ var pan = (info.x / info.w) * 2 - 1; if(pan < -1) pan=-1; if(pan>1) pan=1; try{ panner.pan.value = pan }catch(e){} } })
|
||||
chan.on(async function(msg){ if(!msg || typeof msg!=='object') return; if(!msg.running || msg.paused){ stopAudio(); stopProgrammatic(); renderer.stop(); return } if(msg.audioEnabled){ if(msg.audioMode==='file'){ if(!audioFolder){ await ensureFolder() } if(msg.audioName){ await loadAudioByName(msg.audioName); playBuffer() } } else { stopAudio(); if(!audioUnlocked) bindUnlock(); startProgrammatic(msg) } } else { stopAudio(); stopProgrammatic() } })
|
||||
}
|
||||
function wait(){ if(window.BLS){ initPatient() } else { setTimeout(wait, 50) } }
|
||||
if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', wait) } else { wait() }
|
||||
</script>
|
||||
<script src="js/patient.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user