Skip to content

Instantly share code, notes, and snippets.

@RichardPotthoff
Created March 5, 2026 16:51
Show Gist options
  • Select an option

  • Save RichardPotthoff/af08db6aba8d545d7bd9d7b7b3d1d3ce to your computer and use it in GitHub Desktop.

Select an option

Save RichardPotthoff/af08db6aba8d545d7bd9d7b7b3d1d3ce to your computer and use it in GitHub Desktop.
Trackpad Widget
<!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