const config=require('config'); let currentInputs={}; let handlers=[]; let hud; const toAngleMagnitude=(x, y) => { return { angle: ((Math.atan2(x, y)+2*Math.PI)%(2*Math.PI))/Math.PI, magnitude: Math.hypot(x, y) }; }; const handleAngleMagnitude=(x, y, threshold=0, fn=null) => { const {angle, magnitude}=toAngleMagnitude(x, y); if(magnitude>threshold) { let inputs=currentInputs; if(angle>.25 && angle <.75) inputs.right=true; else if(angle>.75 && angle<1.25) inputs.up=true; else if(angle>1.25 && angle<1.75) inputs.left=true; else inputs.down=true; if(fn) fn(angle, magnitude); } }; const removeChild=(parent, child) => { if(child.parentNode==parent) parent.removeChild(child); }; const handleCrosspad=(() => { const ns='http://www.w3.org/2000/svg'; const cross=document.createElementNS(ns, 'svg'); cross.classList.add('crosspadOverlay'); cross.setAttribute('width', 1000); cross.setAttribute('height', 1000); let dr=document.createElementNS(ns, 'line'); dr.setAttribute('x1', 0); dr.setAttribute('y1', 0); dr.setAttribute('x2', 1000); dr.setAttribute('y2', 1000); dr.setAttribute('stroke', 'black'); cross.appendChild(dr); let dl=document.createElementNS(ns, 'line'); dl.setAttribute('x1', 1000); dl.setAttribute('y1', 0); dl.setAttribute('x2', 0); dl.setAttribute('y2', 1000); dl.setAttribute('stroke', 'black'); cross.appendChild(dl); let useOverlay=false; let enabled=false; const displayOverlay=() => { if(useOverlay && enabled) hud.appendChild(cross); else removeChild(hud, cross); }; config.watchB('input.touchscreen.crosspad.overlay', (k, v) => { useOverlay=v; displayOverlay(); }); const fn=e => handleAngleMagnitude( e.touches[0].clientX-window.innerWidth/2, e.touches[0].clientY-window.innerHeight/2, 0, null ); const init=() => { useOverlay=config.getB('input.touchscreen.crosspad.overlay'); enabled=true; displayOverlay(); }; const fini=() => { enabled=false; displayOverlay(); }; return { touchstart: fn, touchmove: fn, init, fini }; })(); const handleKeyboard={ keydown: e => { let inputs=currentInputs; if(e.key=='ArrowUp') inputs.up=true; else if(e.key=='ArrowDown') inputs.down=true; else if(e.key=='ArrowLeft') inputs.left=true; else if(e.key=='ArrowRight') inputs.right=true; } }; const handleJoystick=(() => { let cvs=document.createElement('canvas'); cvs.classList.add('joystickOverlay'); let ctx=cvs.getContext('2d'); let enabled=false; let useOverlay=false; let firstTouch=false; let center={ x: 0, y: 0 }; let deadzone; const displayOverlay=() => { if(!enabled || !useOverlay || !firstTouch) return removeChild(hud, cvs); cvs.width=cvs.height=4*deadzone+120; hud.appendChild(cvs); ctx.clearRect(0, 0, cvs.width, cvs.width); ctx.strokeStyle='black'; ctx.lineWidth=1; ctx.beginPath(); const rad=2*deadzone+50; ctx.moveTo(rad*Math.cos(Math.PI/4)+cvs.width/2, rad*Math.sin(Math.PI/4)+cvs.width/2); ctx.lineTo(rad*Math.cos(Math.PI/4+Math.PI)+cvs.width/2, rad*Math.sin(Math.PI/4+Math.PI)+cvs.width/2); ctx.moveTo(rad*Math.cos(-Math.PI/4)+cvs.width/2, rad*Math.sin(-Math.PI/4)+cvs.width/2); ctx.lineTo(rad*Math.cos(-Math.PI/4+Math.PI)+cvs.width/2, rad*Math.sin(-Math.PI/4+Math.PI)+cvs.width/2); ctx.stroke(); ctx.strokeStyle='gray'; ctx.lineWidth=2; ctx.beginPath(); ctx.ellipse(cvs.width/2, cvs.width/2, deadzone, deadzone, 0, 0, Math.PI*2); ctx.stroke(); ctx.lineWidth=3; ctx.beginPath(); ctx.ellipse(cvs.width/2, cvs.width/2, deadzone*2+50, deadzone*2+50, 0, 0, Math.PI*2); ctx.stroke(); cvs.style.left=center.x+'px'; cvs.style.top=center.y+'px'; }; const init=() => { enabled=true; deadzone=config.getN('input.touchscreen.joystick.deadzone'); useOverlay=config.getB('input.touchscreen.joystick.overlay'); displayOverlay(); }; const fini=() => { enabled=false; displayOverlay(); }; config.watchN('input.touchscreen.joystick.deadzone', (k, v) => { deadzone=v; displayOverlay(); }); config.watchB('input.touchscreen.joystick.overlay', (k, v) => { useOverlay=v; displayOverlay(); }); return { init, fini, touchstart: e => { center.x=e.touches[0].clientX; center.y=e.touches[0].clientY; firstTouch=true; displayOverlay(); }, touchmove: e => handleAngleMagnitude( e.touches[0].clientX-center.x, e.touches[0].clientY-center.y, deadzone, null ) } })(); const handleSwipe=(() => { let center={ x: 0, y: 0 }; let deadzone; let resetCenter=e => { center.x=e.touches[0].clientX; center.y=e.touches[0].clientY; }; const init=() => { deadzone=config.getN('input.touchscreen.swipe.deadzone'); }; config.watchN('input.touchscreen.swipe.deadzone', (k, v) => { deadzone=v; }); return { init, touchstart: resetCenter, touchmove: e => handleAngleMagnitude( e.touches[0].clientX-center.x, e.touches[0].clientY-center.y, deadzone, () => resetCenter(e) ) } })(); const handleGamepads=(() => { let deadzone; const init=() => { deadzone=config.getN('input.touchscreen.swipe.deadzone'); }; config.watchN('input.touchscreen.swipe.deadzone', (k, v) => { deadzone=v; }); return { init, frame: () => { const gp=navigator.getGamepads()[0]; let inputs=currentInputs; if(!gp || !gp.axes) return; handleAngleMagnitude( gp.axes[0], gp.axes[1], deadzone, null ); } }; })(); const handleEvent=(type, evt) => { for(let handler of handlers) { let fn=handler[type]; if(fn) fn(evt); } }; const enableHandler=handler => { if(!handlers.includes(handler)) { handlers.push(handler); if(handler.init) handler.init(); } }; const disableHandler=handler => { let idx=handlers.indexOf(handler); if(idx!=-1) { handlers.splice(idx, 1); if(handler.fini) handler.fini(); } }; const linkHandler=(handler, key) => { if(config.getB(key)) enableHandler(handler); config.watchB(key, (k, v) => { if(v) enableHandler(handler); else disableHandler(handler); }); }; const init=({hud: hudElem}) => { hud=hudElem; linkHandler(handleCrosspad, 'input.touchscreen.crosspad.enabled'); linkHandler(handleJoystick, 'input.touchscreen.joystick.enabled'); linkHandler(handleSwipe, 'input.touchscreen.swipe.enabled'); linkHandler(handleGamepads, 'input.gamepad.enabled'); linkHandler(handleKeyboard, 'input.keyboard.enabled'); }; const clear=() => Object .keys(currentInputs) .forEach(key => delete currentInputs[key]); for(let type of ['keydown', 'touchstart', 'touchmove']) { window.addEventListener(type, handleEvent.bind(null, type)); } return module.exports={ inputs: currentInputs, clear, framefn: handleEvent.bind(null, 'frame'), init };