Created
March 5, 2026 16:51
-
-
Save RichardPotthoff/af08db6aba8d545d7bd9d7b7b3d1d3ce to your computer and use it in GitHub Desktop.
Trackpad Widget
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"/> | |
| <meta content="width=device-width, initial-scale=1.0, user-scalable=yes" name="viewport"/> | |
| <title>Trackpad Test</title> | |
| </head> | |
| <body> | |
| <script type="text/javascript">(function(global){class StateManager{constructor(){this.state={};this.subscribers=new Set();} | |
| getState(){return{...this.state};} | |
| setState(newState){const hasChanged=Object.keys(newState).some(key=>this.state[key] !==newState[key]);if(!hasChanged) return;this.state={...this.state,...newState};this.notifySubscribers();} | |
| subscribe(callback){this.subscribers.add(callback);return()=>this.subscribers.delete(callback);} | |
| notifySubscribers(){this.subscribers.forEach(callback=>callback(this.state));}} | |
| if(!("modules" in global)){global["modules"]={}} | |
| global.modules["stateManager.js"]={StateManager};})(window);(function(global){class Trackpad{constructor({element,onPositionChange,minX=-40,maxX=40,minY=-30,maxY=30}){this.element=element;this.onPositionChange=onPositionChange;this.config={minX,maxX,minY,maxY};this.isDragging=false;this.currentCanvasX=0;this.currentCanvasY=0;this.bindEvents();} | |
| bindEvents(){const start=(e,touch=false)=>{e.preventDefault();this.isDragging=true;this.updatePosition(e,touch);};const move=(e,touch=false)=>{e.preventDefault();if(!this.isDragging) return;this.updatePosition(e,touch);};const end=()=>{this.isDragging=false;};this.element.addEventListener('mousedown',(e)=>start(e));this.element.addEventListener('mousemove',(e)=>move(e));this.element.addEventListener('mouseup',end);this.element.addEventListener('mouseleave',end);this.element.addEventListener('touchstart',(e)=>{if(e.touches.length===1) start(e,true);},{passive:false});this.element.addEventListener('touchmove',(e)=>{if(e.touches.length===1) move(e,true);},{passive:false});this.element.addEventListener('touchend',end);this.element.addEventListener('touchcancel',end);} | |
| updatePosition(e,touch=false){const rect=this.element.getBoundingClientRect();let clientX=touch?e.touches[0].clientX:e.clientX;let clientY=touch?e.touches[0].clientY:e.clientY;const canvasX=clientX-rect.left;const canvasY=clientY-rect.top;this.currentCanvasX=Math.max(0,Math.min(canvasX,this.element.clientWidth));this.currentCanvasY=Math.max(0,Math.min(canvasY,this.element.clientHeight));const[scaledX,scaledY]=this.scaleFromCanvas(this.currentCanvasX,this.currentCanvasY);this.onPositionChange({x:scaledX,y:scaledY});} | |
| scaleFromCanvas(canvasX,canvasY){const{minX,maxX,minY,maxY}=this.config;const width=this.element.clientWidth;const height=this.element.clientHeight;const x=minX+(canvasX/width)*(maxX-minX);const y=minY+((height-canvasY)/height)*(maxY-minY);return[x,y];} | |
| scaleToCanvas(x,y){const{minX,maxX,minY,maxY}=this.config;const width=this.element.clientWidth;const height=this.element.clientHeight;const canvasX=((x-minX)/(maxX-minX))*width;const canvasY=height-((y-minY)/(maxY-minY))*height;return[canvasX,canvasY];}} | |
| if(!("modules" in global)){global["modules"]={}} | |
| global.modules["trackpad.js"]={Trackpad};})(window);(function(global){class Sliders{constructor(stateManager,config){this.stateManager=stateManager;this.config=config;this.initialize();} | |
| initialize(){this.container=document.createElement('div');document.body.appendChild(this.container);const xWrapper=document.createElement('div');xWrapper.style.display='flex';xWrapper.style.alignItems='center';xWrapper.style.margin='5px 0';const xLabel=document.createElement('span');xLabel.textContent='X: ';xLabel.style.width='50px';xWrapper.appendChild(xLabel);this.xSlider=document.createElement('input');this.xSlider.type='range';this.xSlider.min=this.config.minX;this.xSlider.max=this.config.maxX;this.xSlider.step='0.1';this.xSlider.style.width='200px';xWrapper.appendChild(this.xSlider);this.xValue=document.createElement('span');this.xValue.style.marginLeft='10px';this.xValue.style.width='50px';xWrapper.appendChild(this.xValue);this.container.appendChild(xWrapper);const yWrapper=document.createElement('div');yWrapper.style.display='flex';yWrapper.style.alignItems='center';yWrapper.style.margin='5px 0';const yLabel=document.createElement('span');yLabel.textContent='Y: ';yLabel.style.width='50px';yWrapper.appendChild(yLabel);this.ySlider=document.createElement('input');this.ySlider.type='range';this.ySlider.min=this.config.minY;this.ySlider.max=this.config.maxY;this.ySlider.step='0.1';this.ySlider.style.width='200px';yWrapper.appendChild(this.ySlider);this.yValue=document.createElement('span');this.yValue.style.marginLeft='10px';this.yValue.style.width='50px';yWrapper.appendChild(this.yValue);this.container.appendChild(yWrapper);const initialState=this.stateManager.getState();this.xSlider.value=initialState.x;this.ySlider.value=initialState.y;this.xValue.textContent=initialState.x.toFixed(1);this.yValue.textContent=initialState.y.toFixed(1);this.bindEvents();this.stateManager.subscribe((state)=>{this.xSlider.value=state.x;this.ySlider.value=state.y;this.xValue.textContent=state.x.toFixed(1);this.yValue.textContent=state.y.toFixed(1);});} | |
| bindEvents(){this.xSlider.addEventListener('input',()=>{const state=this.stateManager.getState();this.stateManager.setState({...state,x:parseFloat(this.xSlider.value)});});this.ySlider.addEventListener('input',()=>{const state=this.stateManager.getState();this.stateManager.setState({...state,y:parseFloat(this.ySlider.value)});});}} | |
| if(!("modules" in global)){global["modules"]={}} | |
| global.modules["sliders.js"]={Sliders};})(window);(function(global){let{StateManager}=modules["stateManager.js"];let{Trackpad}=modules["trackpad.js"];let{Sliders}=modules["sliders.js"];const isJupyter=typeof element !=='undefined';const config={minX:-40,maxX:40,minY:-30,maxY:30,width:400,height:300};const stateManager=new StateManager();const canvas=document.createElement('canvas');canvas.width=config.width;canvas.height=config.height;canvas.style.border='1px solid black';const container=isJupyter?element:document.body;container.appendChild(canvas);const ctx=canvas.getContext('2d');const initialState={x:0,y:0};stateManager.setState(initialState);const trackpad=new Trackpad({element:canvas,onPositionChange:(position)=>{stateManager.setState({x:position.x,y:position.y});},minX:config.minX,maxX:config.maxX,minY:config.minY,maxY:config.maxY});const render=()=>{const state=stateManager.getState();const[canvasX,canvasY]=trackpad.scaleToCanvas(state.x,state.y);ctx.clearRect(0,0,canvas.width,canvas.height);ctx.beginPath();ctx.arc(canvasX,canvasY,5,0,2*Math.PI);ctx.fillStyle='blue';ctx.fill();};const uuids={x_slider:null,y_slider:null,state_text:null};function initialize(){if(!isJupyter) return;const xSlider=document.querySelector(`.${uuids.x_slider} input`);const ySlider=document.querySelector(`.${uuids.y_slider} input`);const stateText=document.querySelector(`.${uuids.state_text} input`);if(!xSlider||!ySlider||!stateText){console.error('Could not find Jupyter widgets');return;} | |
| function updateStateText(){const state=stateManager.getState();stateText.value=JSON.stringify(state);} | |
| stateManager.setState(JSON.parse(stateText.value));stateManager.subscribe((state)=>{xSlider.value=state.x;ySlider.value=state.y;updateStateText();});xSlider.addEventListener('input',()=>{const state=stateManager.getState();stateManager.setState({...state,x:parseFloat(xSlider.value)});});ySlider.addEventListener('input',()=>{const state=stateManager.getState();stateManager.setState({...state,y:parseFloat(ySlider.value)});});const observer=new MutationObserver(()=>{const newState=JSON.parse(stateText.value);stateManager.setState(newState);});observer.observe(stateText,{attributes:true,attributeFilter:['value']});} | |
| if(!isJupyter){new Sliders(stateManager,config);stateManager.subscribe(render);render();} else{stateManager.subscribe(render);render();} | |
| if(!("modules" in global)){global["modules"]={}} | |
| global.modules["main.js"]={uuids,initialize};})(window);</script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment