JoyCon Presenter Tool
Bundling
.sh
npm install joy-con-webhid --prefix .
.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);
});
Examples
EventHandler["JoyCon", {
"zr" -> (FrontSubmit[FrontSlidesSelected["navigateNext", 1]]&),
"y" -> (FrontSubmit[FrontSlidesSelected["navigatePrev", 1]]&)
}];
.slide
# First
---
# Second
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
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