JoyCon Presenter Tool

*A fully functioning presenter tool with working IMU*


Bundling

*This step is already done in the notebook*

Firstly install dependencies locally

.sh npm install joy-con-webhid --prefix . Now Javascript helper

.esm import { connectJoyCon, connectedJoyCons } from 'joy-con-webhid'; // Create and style the connection button const connectButton = document.createElement('button'); connectButton.className = 'relative cursor-pointer rounded-md h-6 pl-3 pr-2 text-left text-gray-500 focus:outline-none ring-1 sm:text-xs sm:leading-6 bg-gray-100'; connectButton.innerText = "Connect"; let connectionState = "Connect"; let isJoyConConnected = false; let lastUpdateTime = performance.now(); const buttonStates = { a: false, b: false, home: false, plus: false, r: false, sl: false, sr: false, x: false, y: false, zr: false }; const joystickPosition = [0.0, 0.0]; let restingJoystick = [0.0, 0.0]; let isCalibrated = false; let imuEnabled = false; let isAllowedToConnect = false; // Enable IMU mode if allowed core.JoyConIMU = async (args, env) => { imuEnabled = await interpretate(args[0], env); }; // Function to handle Joy-Con input function handleJoyConInput(detail) { if (!isCalibrated) { restingJoystick = [Number(detail.analogStickRight.horizontal), Number(detail.analogStickRight.vertical)]; isCalibrated = true; return; } const currentTime = performance.now(); if (currentTime - lastUpdateTime > 50) { // Update every 50ms lastUpdateTime = currentTime; let buttonPressed = false; let joystickMoved = false; for (const key of Object.keys(buttonStates)) { if (!buttonStates[key] && detail.buttonStatus[key]) buttonPressed = true; buttonStates[key] = detail.buttonStatus[key]; } const verticalOffset = Number(detail.analogStickRight.vertical) - restingJoystick[1]; const horizontalOffset = Number(detail.analogStickRight.horizontal) - restingJoystick[0]; if (Math.abs(verticalOffset) > 0.1 || Math.abs(horizontalOffset) > 0.1) { joystickMoved = true; } joystickPosition[0] = horizontalOffset; joystickPosition[1] = -verticalOffset; if (imuEnabled) { server.kernel.io.fire('JoyCon', { 'Accelerometer': Object.values(detail.actualAccelerometer), 'Gyroscope': Object.values(detail.actualGyroscope.dps) }, 'IMU'); } if (buttonPressed) { for (const key of Object.keys(buttonStates)) { if (buttonStates[key]) { server.kernel.io.fire('JoyCon', true, key); break; } } } if (joystickMoved) { server.kernel.io.fire('JoyCon', joystickPosition, 'Stick'); } } } // Periodically check for connected Joy-Cons const connectionCheckInterval = setInterval(async () => { if (!isAllowedToConnect) return; const connectedDevices = connectedJoyCons.values(); isJoyConConnected = false; for (const joyCon of connectedDevices) { isJoyConConnected = true; if (joyCon.eventListenerAttached) continue; await joyCon.open(); await joyCon.enableStandardFullMode(); await joyCon.enableIMUMode(); await joyCon.enableVibration(); await joyCon.rumble(600, 600, 0.5); joyCon.addEventListener('hidinput', ({ detail }) => handleJoyConInput(detail)); joyCon.eventListenerAttached = true; } updateConnectionState(); }, 2000); // Update button UI based on connection state function updateConnectionState() { if (isJoyConConnected && connectionState !== "Connected") { connectionState = "Connected"; connectButton.innerText = connectionState; connectButton.style.background = '#d8ffd8'; } else if (!isJoyConConnected && connectionState !== "Connect") { connectionState = "Connect"; connectButton.innerText = connectionState; connectButton.style.background = ''; } } // Handle button click event connectButton.addEventListener('click', async () => { isAllowedToConnect = true; if (!isJoyConConnected) { await connectJoyCon(); } }); const container = document.createElement('div'); container.innerHTML = `<small>Presenter controller</small>`; container.appendChild(connectButton); container.className = 'flex flex-col gap-y-2 bg-white rounded-md shadow-md'; this.return(container); this.ondestroy(() => { cancelInterval(connectionCheckInterval); }); *Click above to connect*

Examples

The easiest one is a slide flipper

EventHandler["JoyCon", { "zr" -> (FrontSubmit[FrontSlidesSelected["navigateNext", 1]]&), "y" -> (FrontSubmit[FrontSlidesSelected["navigatePrev", 1]]&) }]; .slide # First --- # Second The next one uses analog stick to control widgets

pos = {0.,0.}; EventHandler["JoyCon", {"Stick" -> ((pos += 0.1 #)&)}]; FaradayWidget := ManipulatePlot[ Abs[(E^(I w (-1 + Sqrt[1 + (f/((-I g - w) w + (d - w0)^2))])) + E^(I w (-1 + Sqrt[1 + (f/((-I g - w) w + (d + w0)^2))]))) /. {g -> 0.694, w0 -> 50.0}] , {w, 20, 80}, {{f,10},0,100,10}, {{d,0},0,10,1} , FrameLabel->{"wavenumber", "transmission"} , Frame->True , "TrackedExpression" -> Offload[5 pos] (* <-- *) ]; FaradayWidget And the very last one uses IMU to control the rotation of 3D plot

FrontSubmit[JoyConIMU[True]]; timestamp = AbsoluteTime[]; angle = 0.; rotation = RotationMatrix[angle, {0,0,1.0}]; EventHandler["JoyCon", { "IMU" -> Function[val, With[{angularSpeed = val["Gyroscope"][[1]], time = AbsoluteTime[], oldAngle = angle}, angle += (time - timestamp) angularSpeed; timestamp = time; ]; rotation = RotationMatrix[angle, {0,0,1.0}]; ] }]; Horse = Graphics3D[ GeometricTransformation[ExampleData[{"Geometry3D","Horse"}] // First, rotation // Offload] , ViewPoint->3.5{1.0,0.5,0.5} , ImageSize->{550,600} ]; Horse