The best way to Use Gyroscope in Shows, or Why Take a JoyCon to DPG2025

Picture by creator

explores how browser-based computational notebooks — notably the WLJS Pocket book — can remodel static slides into dynamic, real-time experiences. This strategy isn’t restricted to displays; you’ll be able to put together interactive lecture notes for college students or colleagues and publish it on net. For knowledge scientists, physicists, it highlights new methods to speak fashions, simulations, and visualizations, making complicated concepts extra intuitive and fascinating.

Is a PDF Sufficient?

Animations, bells and whistles, particularly the type that have been widespread in PowerPoint 15–20 years in the past, have largely taken a backseat. Add to this the compatibility points between LibreOffice and MS Workplace (even between variations for Home windows and Mac), the presence or absence of obligatory fonts — and the will to do one thing uncommon on the “stage” fades away rapidly.

Take a look at trendy technical displays: very often, it’s only a PDF doc consisting of pages with vector and raster graphics, and generally GIF animations that eat up megabytes (like this submit), with no mercy.

Unused Potential

It’s value separating ornamental bells and whistles from those who carry further data in some media format. For instance, check out ECMA-363 [1] specification.

Convert MATLAB Determine to 3D PDF / Picture by Ioannis F. Filippidis (fig2u3d guide), BSD-2-Clause

A 3D mannequin inside a PDF doc merely enhances the consumer/viewer expertise. You observe the item from totally different angles/cross-sections.

It’s disappointing that such a function is sort of nowhere supported apart from Adobe Acrobat and certain won’t be. It seems like we made a leap up to now, however now now we have returned to static slides.

Giant Scientific Convention DPG

DPG-Frühjahrstagung  is a big European physics convention organized by the German Bodily Society (DPG) [2]. Yearly, they collect greater than 10^4 scientists and happen in German cities, masking an enormous array of physics fields.

Deutsche Physikalische Gesellschaft / Picture by Wikimedia, PD-textlogo
DPG2025 (Spring Assembly) happened within the great metropolis of Regensburg / Picture by Tobi &Chris, Pexels License

There are such a lot of displays, and it lasts virtually every week, so by the tip, it turns into too overwhelming. Nonetheless, this doesn’t diminish its worth as a platform for networking, practising displays, and a dependable strategy to be taught what’s at the moment in the marketplace, which trains have gone already, and that are simply departing.

The members in plenary classes are principally Grasp’s college students and PhD college students, with postdocs being rarer.

Such a big and accessible platform is a superb motivation to attempt one thing new 💡 even when one thing might go flawed.

What’s a JoyCon?

Certainly, the reader has seen units like this:

A median PPT clicker gadget / AI Generated “PPT Clicker” picture utilizing Dalle 3 by OpenAI

This gadget acts as a slide switcher and generally as a laser pointer, connecting through Bluetooth or by way of a dongle. In any case, it’s a kind of controller with buttons. Controllers may be extra attention-grabbing — just like the one from the 2017 Nintendo Change handheld console

JoyCon (R) / Picture tailored Wikimedia, PD

It’s not a lot greater, nevertheless it has some further cool options:

  • Analog stick 🕹️
  • 11 buttons ☎️
  • IR digicam 📸 (tough to make use of, no good API documentation)
  • Full IMU 🌐 (Inertial Measurement Unit) aka gyroscope with an accelerometer
  • Bluetooth connectivity; acknowledged as an everyday HID

The buttons can certainly be mapped to PowerPoint, or the stick can be utilized to manage slides, emulating mouse or keyboard clicks, because it was applied in these initiatives:

I believed it might be cool to by some means use the IMU and analog stick. However for that, one would wish to transcend PowerPoint and PDF 🧙🏼‍♂️

Transferring Slides to Browser Surroundings

The thought behind this isn’t new, nevertheless it’s essential to keep in mind that this strategy might not work for everybody. Nonetheless, by shifting the presentation show and creation to the browser (notably Javascript and HTML lands), we routinely acquire entry to all the probabilities of contemporary net expertise: peripheral gadget help, JavaScript, CSS animation magic, and far more, together with video. It’s essential to notice that every one of that is cross-platform by default and can work virtually in all places.

For instance, slides may be created in Markdown (or/and HTML) with the assistance of a easy framework (somewhat, a small library)  RevealJS [5]

There’s additionally MDX-based presentation engines, and issues like Manim [6], Movement Canvas [7], however these guys require much more expertise to grasp.

The RevealJS API is sort of easy, so controlling the slides through JavaScript instructions is simple to implement:

setTimeout(() =>{
  Reveal.navigateNext(1);
}, 1000)

Nonetheless, this direct strategy has vital drawbacks. It requires an web connection, and in case you’d want to keep away from it, you’ll want to make use of bundlers (reminiscent of Rollup) and embed all JavaScript libraries right into a single HTML file, for example. Alternatively, you may run an area net server.

Choice with Jupyter Pocket book

Should you like Python and IPYNB, then use nbconvert — it would convert your pocket book immediately right into a RevealJS presentation, and also you gained’t even discover it! Or use the extension for Jupyter—RISE [8]

Create Presentation from Jupyter Pocket book / Picture by creator

In any case, the concept stays easy — we have to by some means enter the online browser setting to reap the benefits of all the probabilities of JoyCon.

Attempt it on Binder!

Choice with WLJS Pocket book

My opinion on WLJS [9] could be considerably biased as I’m considered one of its builders (and energetic customers). This open-source IDE with a pocket book interface is extra tightly built-in with the online setting, as slides are usually not exported there however are as a substitute executed and are simply one other kind of output cell, alongside the acquainted Markdown.

WLJS Pocket book / Picture by creator

Beneath the hood, it additionally makes use of RevealJS however with a couple of additional options:

  • It really works offline
  • It permits embedding interactive components and parts, much like LaTeX Beamer
  • It’s built-in with Wolfram Language (freeware distribution)

See extra about it in this story [10]. An final information on methods to make presentation there we revealed in our official weblog: Dynamic Presentation, or The best way to Code a Slide with Markdown and WL [11].

Let’s Dive into JoyCon

So, the simplest choice is to make use of the already ready-made library joy-con-webhid [12]. Why spend time reinventing the wheel when folks have already performed a fantastic job for us?

npm set up joy-con-webhid --prefix .

All subsequent examples might be taken from the WLJS Pocket book. Nonetheless, you are able to do just about the identical factor utilizing Python + FAST API to interface with JavaScript or one thing related, and even simply use JS alone. The web model of the pocket book is on the market right here [13].

First, let’s take heed to what’s coming from the controller port.

Code
.esm
import { connectJoyCon, connectedJoyCons } from 'joy-con-webhid';

// Create join button
const connectButton = doc.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 = "Join";
let connectionState = "Join";
let isJoyConConnected = false;
let lastUpdateTime = efficiency.now();
let isAllowedToConnect = false;
// major handler perform (warning! referred to as at 60FPS)
perform handleJoyConInput(element) {
  const currentTime = efficiency.now();
  if (currentTime - lastUpdateTime > 50) { // decelerate
    lastUpdateTime = currentTime;
    console.log(element);
  }
}
// JoyCon periodically goes to sleep, we have to wake it up
const connectionCheckInterval = setInterval(async () => {
  if (!isAllowedToConnect) return;
  const connectedDevices = connectedJoyCons.values();
  isJoyConConnected = false;
  for (const joyCon of connectedDevices) {
    isJoyConConnected = true;
    if (joyCon.eventListenerAttached) proceed;
    await joyCon.open();
    await joyCon.enableStandardFullMode();
    await joyCon.enableIMUMode();
    await joyCon.enableVibration();
    await joyCon.rumble(600, 600, 0.5);
    joyCon.addEventListener('hidinput', ({ element }) => handleJoyConInput(element));
    joyCon.eventListenerAttached = true;
  }
  updateConnectionState();
}, 2000);
// Replace button state
perform updateConnectionState() {
  if (isJoyConConnected && connectionState !== "Linked") {
    connectionState = "Linked";
    connectButton.innerText = connectionState;
    connectButton.type.background = '#d8ffd8';
  } else if (!isJoyConConnected && connectionState !== "Join") {
    connectionState = "Join";
    connectButton.innerText = connectionState;
    connectButton.type.background = '';
  }
}
// Deal with click on occasion
connectButton.addEventListener('click on', async () => {
  isAllowedToConnect = true;
  if (!isJoyConConnected) {
    await connectJoyCon();
  }
});
// Simply decorations
const container = doc.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';
// Return DOM component to the web page
this.return(container);
// When a cell obtained eliminated
this.ondestroy(() => {
  cancelInterval(connectionCheckInterval);
});

A very powerful perform right here is:

perform handleJoyConInput(element) {
  const currentTime = efficiency.now();
  if (currentTime - lastUpdateTime > 50) { // decelerate
    lastUpdateTime = currentTime;
    console.log(element); //output to the console
  }
}

It appears like there are various steps to do. In actuality, most of this code offers with connecting the controller and drawing a big “Join” button. Don’t pay an excessive amount of consideration to the particular strategies — they will simply get replaced with these out there in your particular setting:

  • this.return(dom) passes a DOMElement for embedding on the web page
  • this.ondestroy(perform) calls perform when the cell is deleted, to wash up timers, and so on.
  • The primary line .esm is a strategy to specify the JavaScript cell subtype in WLJS Pocket book, which requires pre-bundling.

After we run this code cell, we are going to see the next:

DOM Output Component / Picture by creator

Then observe these steps:

  • Disconnect the controller from the Nintendo Change (System → Controllers → Disconnect).
  • Pair the JoyCon (R) with the PC by holding the small button on the facet.
  • Press “Join” on our presenter controller.

Opening the browser console, we reveal the next messages:

{
    "buttonStatus": {
        "y": false,
        "x": false,
        "b": false,
        "a": false,
        "r": false,
        "zr": false,
        "sr": false,
        "sl": false,
        "plus": false,
        "rightStick": false,
        "dwelling": false,
    },
    "analogStickRight": {
        "horizontal": "0.1",
        "vertical": "0.3"
    },
    "actualAccelerometer": {
        "x": 0,
        "y": 0,
        "z": 0
    },
    "actualGyroscope": {
        "dps": {
            "x": 0,
            "y": 0,
            "z": 0
        },
        "rps": {
            "x": 0,
            "y": 0,
            "z": 0
        }
    }
}

Numerous knowledge! Let’s attempt utilizing this for the advantage of our presentation 💡

Buttons ☎️

To start, we are able to use two buttons to change slides

Picture by creator

Within the WLJS pocket book, slides can be managed programmatically by way of a Wolfram wrapper perform that calls the RevealJS API.

FrontSlidesSelected["navigateNext", 1] // FrontSubmit

All that’s left is to set off this perform on the proper second when the button (or change) is clicked. To do that, occasions have to be despatched from the Javascript world to the Wolfram machine, the place we are able to then do no matter we would like with them. This ends in the next diagram:

Picture by creator

You don’t have to consider this, since it’s seamlessly applied through APIs

Let’s return to the code cell and modify the handler.

Code
//....
//.......
const buttonStates = { //all buttons states on JoyCon (R)
  a: false, b: false, dwelling: 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;

perform handleJoyConInput(element) {
  if (!isCalibrated) { //calibration
    restingJoystick = [Number(detail.analogStickRight.horizontal), Number(detail.analogStickRight.vertical)];
    isCalibrated = true;
    return;
  }
  const currentTime = efficiency.now();
  if (currentTime - lastUpdateTime > 50) {
    lastUpdateTime = currentTime;
    let buttonPressed = false;
    let joystickMoved = false;
    for (const key of Object.keys(buttonStates)) {
      if (!buttonStates[key] && element.buttonStatus[key]) buttonPressed = true;
      buttonStates[key] = element.buttonStatus[key];
    }
    const verticalOffset = Quantity(element.analogStickRight.vertical) - restingJoystick[1];
    const horizontalOffset = Quantity(element.analogStickRight.horizontal) - restingJoystick[0];
    if (Math.abs(verticalOffset) > 0.1 || Math.abs(horizontalOffset) > 0.1) {
      joystickMoved = true;
    }
    joystickPosition[0] = horizontalOffset;
    joystickPosition[1] = -verticalOffset;
    if (buttonPressed) {
      for (const key of Object.keys(buttonStates)) {
        if (buttonStates[key]) {
          server.kernel.io.fireplace('JoyCon', true, key);
          break;
        }
      }
    }
    if (joystickMoved) {
      server.kernel.io.fireplace('JoyCon', joystickPosition, 'Stick');
    }
  }
}
//.......
//..

As you’ll be able to see, now we have added a number of objects right here:

  • Joystick calibration — analog sticks drift, so their digital place isn’t good 0.,0..
  • State of all buttons — why hammer the door each time in case you solely want to softly knock when the state adjustments? This reduces system stress.
  • Sending states to the occasion pool — that is particular to WLJS, the place we ship knowledge to the Wolfram machine (or Python in case you’re in Jupyter).

The final level appears like this (change with the equal in your setting):

server.kernel.io.fireplace(String title, Object state, String sample);

Then, on the Wolfram facet, we are able to simply subscribe to those occasions like this

EventHandler["name", {
  "pattern" -> Function[state,
    Print[state];
  ]
}]

That is very handy, as Javascript sends the names of the pressed buttons because the sample. On this case, you’ll be able to instantly subscribe to slip switching, for instance, like this:

  • ZR — subsequent slide
  • Y — again

Thus, programmatically controlling slides turns into intuitive:

EventHandler["JoyCon", {
  "zr" -> (FrontSubmit[FrontSlidesSelected["navigateNext", 1]]&),
  "y" -> (FrontSubmit[FrontSlidesSelected["navigatePrev", 1]]&)
}];

Let’s Check in Apply

Let’s create a easy presentation. Begin typing with

.slide

# Slide 1

__Hey Medium!__

---

![](https://add.wikimedia.org/wikipedia/en/7/7d/Lenna_percent28test_imagepercent29.png)

Now, let’s join the JoyCon to the PC and hyperlink it to our Javascript script by urgent the Join button. Then, subscribe to the occasions as soon as within the energetic session.

Now, simply run the cell with the slides:

The primary large step in mastering JoyCon has been made! / Picture by creator

Analog Stick 🕹️

The stick theoretically permits controlling two sliders concurrently. For the DPG Spring Conferences, I had the concept of a reside demonstration of a really peculiar impact m𝒶𝑔𝒾c𝑎𝓁 𝓌𝑜𝓇𝒹𝓈 𝒻𝓇𝑜𝓂 𝓅𝒽𝓎𝓈𝒾𝒸𝓈. I imagine that some ideas are far more impactful and understandable when demonstrated reside on stage.

Here’s a condensed code snippet for the interactive widget:

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,2},0,100,1}, {{d,0},0,10,1}
, FrameLabel->{"wavenumber", "transmission"}
, Body->True
];
FaradayWidget

The interactive on-line model of this widget is on the market right here [14]

Picture by creator

To embed it in a slide, insert its image as a tag (much like JSX):

.slide

# Faraday Widget
Right here it's in motion
<FaradayWidget/>

Now, let’s hyperlink it to our stick

Picture by creator

To start with, let’s carry out a easy check and bind its place to a disk on the display screen:

pos = {0.,0.};
EventHandler["JoyCon", {"Stick" -> ((pos = #)&)}];

Graphics[{
  Circle[{0,0}, 2.],
  Disk[pos // Offload, 0.1]
}]
Picture by creator

Clearly, the actions are too abrupt. Furthermore, making small changes is kinda painful utilizing JoyCon. The answer? Integration!

EventHandler["JoyCon", {"Stick" -> ((pos += 0.1 #)&)}];
Picture by creator

Now, let’s hyperlink the pos variable to the sliders of our widget:

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,2},0,100,1}, {{d,0},0,10,1}
, FrameLabel->{"wavenumber", "transmission"}
, Body->True
, "TrackedExpression" -> Offload[5 pos] (* <-- *)
];

Right here’s the way it appears reside on a slide:

Picture by creator

And within the precise DPG presentation:

Picture by creator

A Second of Relaxation

Final yr, DPG happened in Berlin, and this yr — in Regensburg, which has about 23 occasions fewer inhabitants and is 10 occasions smaller in space. Nonetheless, the comfortable lands of Bavaria have at all times been nearer to my ❤️

Picture by creator

And that is the college. A stable 60s-style constructing. Wha 💪🏻

Picture by creator

A brand new invention — a cup “Drink and Eat Me”

Picture by creator

As a bonus, each drink will get a touch of waffle taste! However, be careful — don’t chunk into it whereas it’s crammed with sizzling tea.

I couldn’t take extra photographs since I obtained sick on the primary day and went again dwelling to Augsburg. On the whole, spending six days at a convention is sort of difficult.

Picture by creator

Again to enterprise 🐏

IMU or Gyroscope-Accelerometer Mixture 🌐

To make use of them, we have to learn the corresponding fields from the particulars object, specifically:

  • actualAccelerometer: x, y, z
  • actualGyroscope: rps (radians per second)
Code
//..
//....
const buttonStates = {
  a: false, b: false, dwelling: 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;
// Allow IMU mode if allowed
core.JoyConIMU = async (args, env) => {
  imuEnabled = await interpretate(args[0], env);
};
// Perform to deal with Pleasure-Con enter
perform handleJoyConInput(element) {
  if (!isCalibrated) {
    restingJoystick = [Number(detail.analogStickRight.horizontal), Number(detail.analogStickRight.vertical)];
    isCalibrated = true;
    return;
  }
  const currentTime = efficiency.now();
  if (currentTime - lastUpdateTime > 50) { // Replace each 50ms
    lastUpdateTime = currentTime;
    let buttonPressed = false;
    let joystickMoved = false;
    for (const key of Object.keys(buttonStates)) {
      if (!buttonStates[key] && element.buttonStatus[key]) buttonPressed = true;
      buttonStates[key] = element.buttonStatus[key];
    }
    const verticalOffset = Quantity(element.analogStickRight.vertical) - restingJoystick[1];
    const horizontalOffset = Quantity(element.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.fireplace('JoyCon', {
        'Accelerometer': Object.values(element.actualAccelerometer),
        'Gyroscope': Object.values(element.actualGyroscope.dps)
      }, 'IMU');
    }
    if (buttonPressed) {
      for (const key of Object.keys(buttonStates)) {
        if (buttonStates[key]) {
          server.kernel.io.fireplace('JoyCon', true, key);
          break;
        }
      }
    }
    if (joystickMoved) {
      server.kernel.io.fireplace('JoyCon', joystickPosition, 'Stick');
    }
  }
}
//....
//..

Since IMU isn’t at all times wanted, the script features a boolean variable and a management perform JoyConIMU[True | False], permitting IMU measurements to be enabled or disabled.

The JoyCon, like most different units with IMU (some smartphones, watches, however positively not VR headsets or quadcopters), consists of:

  • 3-axis gyroscope — returns angular velocity in rad/sec round all three axes
  • 3-axis accelerometer — returns a single acceleration vector

Query: Why can’t we use solely a gyroscope or an accelerometer?
Let’s attempt outputting each. First, allow IMU utilization:

JoyConIMU[True] // FrontSubmit;

Now, outline auxiliary features and variables:

prevTime = AbsoluteTime[];
angles = {0,0,0};
acceleration = {0,0,-1};

course of[imu_] := With[{time = AbsoluteTime[]},
  With[{dt = time - prevTime},
    angles = (angles + {-1,1,1} imu["Gyroscope"][[{3,1,2}]] dt);
    acceleration = imu["Accelerometer"];
    prevTime = time;
  ]
]

What occurs right here:

  • The accelerometer vector is solely saved in acceleration.
  • Gyroscope knowledge is processed by:
    • Reordering angular velocity values (JoyCon {hardware} orientation) and adjusting instructions.
    • Integrating over time to acquire orientation angles

Consequently, we get hold of:

  • Three angles defining JoyCon orientation angles.
  • One acceleration vector (at relaxation — the gravity course) acceleration.

These three angles are conveniently expressed as a matrix (tensor):

RollPitchYawMatrix[{[Alpha], [Beta], [Gamma]}] // MatrixForm

Making use of this matrix to any 3D object permits it to be oriented in response to these angles. Bodily, on the JoyCon, it appears like this:

Picture by creator

You will need to word that since we measure solely the primary spinoff (utilizing Gyro), then the preliminary IMU orientation stays unknown. Due to this fact, we manually set the preliminary state, i.e.

angles = {0., 0., 0}

EventHandler["JoyCon", {
  "IMU" -> Function[val, 
    process[val];
  ]
}];

angles = {0,0,0}; (* calibration *)
Refresh[acceleration, 0.25] (* dynamically replace *)
Refresh[angles, 0.25] (* dynamically replace *)

Actual-time Knowledge Output:

Picture by creator

Properly… Not fairly apparent what these values imply. Let’s attempt to attract then as vectors in 3D area:

axis = Desk[{{0.,0.,0.}, Table[1.0 KroneckerDelta[i, j], {i,3}]}, {j,3}];

EventHandler["JoyCon", {
  "IMU" -> Function[val, 
    process[val];
    axis[[1]] = {{0.,0.,0.}, RollPitchYawMatrix[angles].{0,1.0,0.0}};
    axis[[2]] = {{0.,0.,0.}, RollPitchYawMatrix[angles].{-1.0,0.0,0}};
    axis[[3]] = {{0.,0.,0.}, -Normalize[acceleration][[{2,1,3}]]};
    axis = axis;
  ]
}];

After which render them as coloured cones, the place:

  • Blue and purple — defines angles derived from the gyroscope knowledge
  • Inexperienced — accelerometer knowledge (inverted and normalized)
{
  {Opacity[0.2], Sphere[]}, 
  Crimson, Tube[axis[[1]]//Offload, {0.2, 0.01}],
  Blue, Tube[axis[[2]]//Offload, {0.2, 0.01}],
  Inexperienced, Tube[axis[[3]]//Offload, {0.2, 0.01}]
} // Graphics3D

EventHandler[InputButton["Reset"], Perform[Null, angles *= .0]]
Picture by creator

The inexperienced vector is at all times aligned “accurately,” whereas the blue and purple vectors, representing gyroscope angles, accumulate errors over time, particularly with fast actions, inflicting drift.

There are various methods to unravel this situation. The final thought is to regulate the angles utilizing accelerometer knowledge (inexperienced vector), because the accelerometer exactly determines the downward course ( till an exterior drive disturbs it).

For a extra detailed clarification, try a fantastic video by James Lambert [15], which explores these issues and their options, together with a detailed instance with Oculus DK1.

Why the heck do we want this within the presentation?

I requested myself this query once I found how deep the rabbit gap goes. In my discuss on the magnetism session, there was precisely one slide the place the concept of an IMU made any sense:

Picture by creator

Do you see the crystalline construction? Discovering a “good” digicam angle for it’s certainly tough, so why not rotate it immediately, full of life? We don’t want all three angles and acceleration-just one will suffice.

Thus, we discard the accelerometer and hold solely the gyroscope:

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}];
  ]
}];

Now, we simply want to use the rotation transformation tensor to our 3D construction. Since this crystal accommodates many ions, that are additionally coloured, I compressed them into base64

base64 code
CrystalRawStructure = "1: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" // Uncompress ;
CrystalStructure = Graphics3D[
  GeometricTransformation[CrystalRawStructure, rotation // Offload]
, ViewPoint->3.5{1.0,0.5,0.5}
, ImageSize->{550,600}
]

Let’s embed it into our slide:

.slide

# Slide

Right here is my crystal construction!

<CrystalStructure/>
Picture by creator

To make it much more handy, it might be helpful to subscribe to IMU solely when the slide is energetic and unsubscribe when leaving it. That is straightforward to do as a result of RevealJS indicators the core on slide state change occasions. Let’s implement this as a element utilizing .wlx cell kind:

.wlx

InteractiveCrystalStructure := Module[{rotation = RotationMatrix[1Degree, {0,0,1.0}], id = CreateUUID[], timestamp = AbsoluteTime[], angle = 0., CrystalStructure},
  
  CrystalStructure = Graphics3D[
    GeometricTransformation[CrystalRawStructure, rotation // Offload]
    , ViewPoint->3.5{1.0,0.5,0.5}
    , ImageSize->{550,600}
  ];
  EventHandler[id, {
    "Slide" -> Function[Null,
      FrontSubmit[JoyConIMU[True]];
      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}];
        ]
      }];
    ],
    ("Destroy" | "Left")   -> Perform[Null,
      FrontSubmit[JoyConIMU[False]];
    ]
  }];
<div>
    <CrystalStructure/>
    <SlideEventListener Id={id}/>
</div>
]

By putting this code on any slide, we obtain the specified end result with out polluting the worldwide area or interfering with different occasion handlers. Thus, IMU subscription administration is localized to the particular slide, and when switching slides, we accurately allow and disable knowledge dealing with with out affecting different slides and their processing

.slide

# Earlier than

---

# Slide

Right here is my crystal construction!

<InteractiveCrystalStructure/>

---
# After

These are precise slides from DPG2025:

Picture by creator
Brief video with my DPG2025 slides

Closing Code and Pocket book

The compiled presenter controller cell code is offered underneath the spoiler. If inserted into an empty cell, it would produce a practical widget for connecting a JoyCon.

Compressed cells
jsfc4uri1percent3AeJztfWtz28aWoGtmdqdq99NU7Q9oa3ZiUiJpAqRkmYqccWTnxlNx4vXjfvGqHJBoSkhAgAJAiaSjpercent2FRf7afpercent2FsnnO6Gpercent2BhuNEjKj3vvVN1UTAHd531Ovxpercent2B4P05fTpercent2Fpercent2Fh3r17percent2BTpercent2FBz09RXkzpercent2FEdpercent2Fpercent2BOpercent2Fw8zfN0EgVFlCYVyOtFzNpercent2Fgw7OgCN78v3percent2B5d6percent2FH89npercent2F7ntpercent2BNJunWcEpercent2BskmaJHxSpercent2FEe6OkuTjnrloUjI2S2bZumMPfgtXXUhs3vDx5dRpercent2BOAEieCpercent2Fhwpercent2FZWcaDgrMgCVlerGLOikuuCIE4bLwoijRBYEjMC5X1PSWzUxamk8WMJ0VvQoSexxzfWg8E3oP2iUStkHqTOMjzn4MZBpercent2FQHGY9B72vgucjyNOvO0ygpeMaydJGEPOzOQnbZPWLzuDtg86zrs4Ivi27Mp4V4usiCVfew32dTECQfpYsijhLeTdKEsyxKLroey2cjAl3mpercent2BBjzIMT0Izapercent2BENhevpercent2Fpercent2FAIWYEL9lbQAQx985E1l5pupgXmpneFGhEGw5holw440z5BqCmQZzzEgKsUbybh0DgbUQ2mfNsmmazIJnwXpLetNolUpercent2BECYVpimQP4R8xhLBgJuh02Lp8u0xkvXpercent2BYx2Ee9ZOVTHlePZaoguSxzVuXTWsEgyK0lGcRZXkST31percent2BleUTRc8re93v9DoOf81LhjANQcvEfEtgNFOVnQRyNs8Blsmi2eJ4E49iVlTpercent2BN4percent2FSGh29TaXIDREa9wGYvXr5jszTkLJqyQKAJZTLeE25DiFMW5KtkwlpBdpF3GEpercent2Bu2percent2Bz0ibK7IUtwE0QgA0bwPOPoIUJ63z8XeCem1UCSHxaJKGhFyi6hFIJUwLkLrIHMfFEg2FTBCAApGea2QmASxe1SmClr3ddNVpercent2BYwl9lpercent2FXszGPJNEekESxOnFG8x8HV1cFr3LNIvWaVIEcbvDtsBe8wxeAFK4EBlaLiyyBTpercent2BphCkWWSJfb5U9GJOVzCLLoBbZWB6Esjpk1y5KT9hhHwzAwMwilXEQcwWps1wJUit9GsWTSi4A5KrkvQJL5lboKQhVAl6m1xaEggJVQGxS83epercent2BYumUpercent2FTLpercent2BDcK0B295Sypercent2Fbbc150rV69ntAOGfffMOkS6q8hchr1percent2BQ1XMBYndppI7ES7VZXRuihfPpercent2FLdJpzLGpercent2B7xgq4zIrK917FSRCvgnBX8lrYOhjIOqYMUjDry6C47AXjvGUq0oYA6vc89scfrISwpZEwhqfsGDCsbpjPri9BOIC3eZw0gnsI3jWlrmlXVVCGlDnPABGiLkt43IvS3jTKeOuBqFwedHRQxh48nUx4zKEzwaFqezBSMXsdxAuel56YFIsgNkDbHYPMn1ZZmkpercent2FSOd9MogTrhfO8XVG47bAHUCMpercent2FaLvNidoaIW8opercent2FKkFT6erlRULZrtBMQw6yLp9YiKOoef0u5F2q6ncqKkRZndyrR1HYFUqPrZd5Zpercent2BSM9Sjr3gWpSFGW7xik0sO7Qjatex5qsYrrpercent2FUYgc8Zwrpercent2FAxhGcDpEL0areWrKJNVpW0ZjZ7XlbbzyMVkMJ8YxfRxPqG9k9YhVtZROysXcmYLS4percent2BY2AMXRsXpr9XTSNKkA6Dpercent2FJ70B4lBY4FOPQ1nxZFACYFXwJ1qLMWZsMhpercent2BhYSEcpG0qrcZeRxKu0QqkkYZOEPizhpercent2BCX2czdBQqrYDpercent2FZkac3BkE1gGFXPMW0d96M3RT7932DaUkIBBGD7XVW89gHEJ9XcgPFsfZSvEbikcGns97ROLrNOcpvm1YrSgZvpercent2FM7MRL1aCm8fv9fiV81YcQNQF794KNA2xW4VkbLpercent2BVIxOi0NbDRo7weMtCs26OLpercent2B6fVpercent2BIKHe1rENY9DAO7EgnKMbiz8BgwaHvbGweT3Cxqc4ejtX8Pj6TQ8fqCMyziUHVVw76jTbhp9XX0e6FGi13spercent2Fiq659P0kxg40RVt93FiPbQKHwHbVca7hihaublNqlhLlz5gIKEun0MIePmLlEsAYOdswegpercent2Bj62roLqCFhX98percent2Bpercent2FInwPv123wGTcATbGc5jdcRLktjaPipercent2FfSjyfrXwgznUWeHZZRSHLcNiNiNjfmAa8yXDnpercent2B4kjdlFMOpercent2Buuj4O3W8uIwgMbZogvwzC9AaeqvmN4jLKe6K1aJX022Z2Cvg5iL5qGY6Z4HAjLpunhkZMVhbtE8e0zdvVnLpercent2F5Lpercent2FBAdZYF8M84rxPl8zhYvflv8DyByncWZVma3WlW6Hpercent2F833v3IDoTwP4AA1nIyxpercent2Bakz0PQwjDh7MgvLiBIOz9lht100uZ3sqD2RxaAalWh6VzzKpercent2BaNfkOHlFP0Cvpercent2BeHuit7percent2BCxg8ZvwIwjwpercent2FYQ2aSldA0koIavCLWo1eg2O8NNaAoiaATHImxiwIN0xeQHGF6IOYYTkWJYdpercent2BJppuN9AKEdK76KFCHXXnwF1qlK1percent2Fpercent2BHeBfDTDjk2jpercent2BxlCjVAITJGhpPmXWpzpercent2Bpercent2BfiNaB2hHWxfLDrtYwb81FHl4DuA5WGtltmT1MwxrjeFjDlLlIGnuw7percent2BBkXX1LC1QBpercent2Fjjiz8D8WdogF37V0ADfj369ekXIKpercent2BHlD6k9CGlH9PzMT5f9TH3ysOUK8K6GlxVpercent2FIk32qp3yPZZqwuG3GcXSxhdgSnhaYVPA3xatw0kv0ICJxDOgcRZlzgrE2dg4xB1Tpercent2BAcSJyliTO0cQjSK7F9C4eq1VawpODpY8sUrLTntXg2xwGlx4AXou6Dc4ELIO7jzwGi7ZOj9percent2FdZF6TRuvQIfurwOSP0hpx1Uw76GIRApa76Zrqn0j0z3Vfpvpkpercent2BUOkDPX1I9Ic1percent2BkOiP6zRHxL9YY3percent2BMcEf1percent2BCPCf7YhscohHTyoMEYAxMzPJvSldDsqqYahi9mDGzdcuRACu4L7ANhHulNlYMMu8Kg6F2dgCcIkCzIpSvsKAkIo0kiIq8vAqQrsA6EWSSP6k3KIggHa51jaVxJVwrd14T269JIjr7g4RscfYOjXpercent2BM4KN0s9dSMUWrpS64iz7cNZZSXHIWFnwO03z7percent2BHKBepercent2Bpercent2FhzgPzgaeAqOYjoLgVIqCHHb8wZNOWIKq57KlomlNXK9PVMz8oc6Jmpercent2BlTnUM6tYLEf24MiDUynAvt0GVdWcp6D8TVCpercent2BghpsghooqOEGKMOBslCqapUCSYaRqpSvHA68anDfVYPzrhpcdpercent2BV03K3VHkpercent2ByNM8percent2FzDPoCU2KVtX2dtgYnsfwPF4bMpercent2BLYM2QfcaGDqvExNkpUjSPsaiSfsaGjCn8MlNYjpercent2BYzlS2At2W2DQBx6ZNnT5AL6ZT9k6ezFbPE6CA3BZvA8gpercent2BfZ2hzpercent2BQH9qHhWTS5zgE7OPMJ73CZVe86sMNKy1PVUTJ2gUsznWV82GgQYfxpercent2BkOPLpercent2BGJ2E7RLW3lH9X8upercent2FapoHDAaChSY9UJbxfh59kCnqS5i3EtiHyuYLIo6RF9qmBZDqIi8gKbTpDywHDLhgei2VmAS0JCP1cCiTYga0JFpercent2FjsozwHgsBEvNpz12Kt0fLh6rID9NsntTCU4B3hpercent2BY4wYFNoFenpercent2FWkCHM0vgxW9pgVaLo8lKt6sG2VPy7VcTNZolV7old0abzJu4iXh285o38dqAZMaLjkSGczPKmhi5cErP3IyEh0G5fbTnQelpercent2Fpercent2BIGAhgok1percent2FO7KiBkpercent2FmqkJVT4E5Wpercent2F1unniJpercent2Fr4E0xgAOkXWsTjuOtO9VHlukiHA9ZURfUyrzeZkRXvRtQgP4c4Jpercent2BleFuKt5V4W4m3tXhzdJpercent2BpUyhpOVuSU0Xb2Z6cKl7OVuVU8a7nGsNPewpx4yiwYQioW7fDQh4XAS4zvuETo1Gyx6A6IA6Sm9prMVdUCm2uHm6IlHqP5GsOUCpercent2BB8eXKSMKpercent2BLMpDf1fy71qMYkX6WPxi2pCeh2OR7x7rQrp8868sIcVAt08D3b4cpercent2Bg7Koa9HKR6liGEwETQGw2jmmRg5XqdRCENG8MlsZSes7QRtsDnTBpszNdjU3XWniQWjz60tqpercent2BtdzL8P4Ppercent2BWBvCGJNTLmC1Vx2K2Un2KmVOSWaMks0ZJZhunEjAy5WQCSWJnr4zslZ29NrLXZrZXEfdcxLpercent2FqNAZVA7p0NpqGWMNunozoi8mIvi2f5FbnhFVMmWHwaJzX8AQpr0bKE6S8GqnGmRChpEPBpimSy6Xqpercent2B5INuioKaKxpercent2FoJwuR34CTk4siBkCAeprCWs1TyCgUVT1PDBZY6zJmJTsBDVNkLUafsrwIl5dCScEkRJcabM7mgyrOl9sYcqOIY6sLpHuJRbKSwSpercent2FNBbtsSWijReloGpGRgqhSjCJrQs6UAOOymJpercent2BabEqU0guRbam5saqQKHMZs66yjFmcpercent2FpSWMxryYkgOWvTJ9sFy3blLQHRLpercent2BUj0GDVJoTxWhWWFtkMpercent2FmK13VVORXEFLYJsSU7Ip00percent2Bx8xWVpercent2BLKeBJEvbZOlUoAIZr0percent2BnLmAZPRlzJzWROLVDsgVXUCUloxv4dCExmjERUzfGIWbbO5percent2BpvNNVTVXguZlTNw8lEYDJubygaDzzRsZYmKZvpercent2BT7dqq3NQV8VV306db2VdFaKuZBzuY2dpercent2FFzBB4w3FZY7gN9BlW93RPfr7Vpercent2B5rVpercent2FS9l9YGIbWpercent2Br0f1NRtdN2VyGP6dm6GtUpercent2Fcpercent2BuGbwvY72percent2FT2mzv09ppercent2F0WmtMuZJ7F7aOQYEXZoUApercent2BjQV5UczIt1percent2FT2zYjhqHcJfzyai8KR7XqEEpQzSpercent2FJJ3percent2FOydVfBPMhyLrcUXAcZw1MZakcBGDjhN9UGA09OMSNchtt0dwIMQjpercent2FkOHpercent2FqHffZQ9Epercent2Fepercent2FWC8svpF9yF9WYxawVZhgPiCCwB1uClIeSRg0WsNtJqGpercent2FpoOyDu5yPkpercent2Bsyp2AuOk0SSaoswzHGogrpfjvetYSfyZqflQzUt8F3JYaRyD1SSXajMwNAUujWsMeNB0jK1kShop5dlbodJVXDPi6bUbc22Ok7NuMJOMU8uCpwxJzDQkCWLOAb1percent2BqAYpfUEyIkhkUT7rtmFDxXMiP0cpercent2FOwQcBLEk0UMwN8HBWCtfuLXPFaaaQEQY7rkntpercent2FQnLKAetpercent2FXdpercent2FJOQBB2PKqcR3i4percent2FWwKCu1ptaC5aZfwhi68GQpercent2BjxWwLpupercent2FCjNObLWieC22SRbQbfAtu34XLZpercent2FNi1YwY8mkAUefkehlkF1Fyseesxirr36qifVbuF8N9UkDiI6jD9n7Cg11yDpercent2FFeBwzD9mhTv5Y2gLRXWapR2JOVF9IdR0FOc0mPDsu0Ncpercent2FS71U6pFU5ORiKq4HfqxcQb75ZuRTp82rKvIWHRf5MO4hxIk7uUepercent2BwQNpercent2Fx3mFyuexFaJWSJL0BVs8gWI1jLCIzxBqiot8rohnPC2jBoHi0ELPrzMUiglutRtUpercent2BJheNU2Rpercent2BYgpDjaC2lqgrgbPppercent2BwybQTN55U5eU3K7LkEQzy9x3YF65soN1JurAR1UFqX5percent2BLDQzpercent2FmQuPeteVKNhtxPRt43qVOOTnwpiGNvE6UaK4lqBpDR8ZBsVVX5Gu2LYDZrYCuydL6rnfgiVNfF2O4YkNVGVbBRu3LsPT7GgOl6ThvvYzvarhpSSBgOpercent2Buzf2ONpercent2Bu1ekP0RLHraO2hBNDbgO1MGRgduhbuHIIYLsLpoSmKhktJ11EibeRNFSRENogL91NDFGFaD1tK6s4n2DpVusUsGvUdiWS8qioUhvaWStVpRFE0C9lZG1XlMWDYF6akLH9n9L9ZH29YV2GrwphjihrMjcgJWXOOpercent2Bpercent2Fwmn1Nc6y39y0nQ6sCMvFa1wxNQjTCoAUHYg4fblRvpUkc1AaBpercent2BXrKgEPnAI2OEnzDJY5qpVjV2WM1Uk9woztmVU3treQC3pAsrcUtT7uYKCpercent2Fa8EE0percent2BnvSv5d11aQDZJWh90q33rfpercent2BPO4q66iQdHF3DalPMEljnqYfSnZbY74jey34zbkP8OrgOuNF9Op2mJdSkDjhBpercent2FiNChaLX845N0j8DXSQGleJIV31OqL81GGo93CPeMXGef5K5694SBNpercent2BKUFhB7CkdcffIaErpercent2Fl1Gi9otpercent2FJXk9I7egx2fPTJUhItcXbpRTJNoZTeoAgdBvEWWGKOVpercent2BKguYTp5TFgtbzDDvMOoXh6ntmTmUbZ7CbIpercent2BMvgtzT7M89y4Pc6QEWJkEQHQb22OoxZQ42SDagwhvVrqAX0IuugMM4dmNLNgsnTMMTjgXXoIcjUbwIH2PeKpUGkBpercent2BPJ58HkstVK6FyqfopABpercent2B3NFpercent2FmlhAHvvCnwegJwZLln69Zkncpercent2BjszROsxfJu9yhmgdm8HwTpRxqfmQfwFujmsV9qF4percent2FXPKlO0eZX1ppercent2BBGRm6MRRk087bIaeGjU67rZDjhlZ3f330lvgxPOOZqKRbq7fUmhv9kZ7IJhhilHNMnRs9hQ3699aVVpercent2FTKJnCn85lvOZ4l8aLZxtLQKRDauY1SwSFtLJvaKXbohkkmyTETRpercent2FZRsmwi581SkQlxSERpdsSEakmSYzh9cbKQgNslIuKpUMumU7DxVHD2N4Grpercent2BmhS9CkTnUab2vtNzFAP00lW0STZqPNtTP4m22uAW6KBaeAnktAnWKzDXF6tfjCYg467Mglpkxfjdj3aRrjlJTHvlG47wfnbZrfVHmpercent2BnTeu8oZ2XlDlHdt5WZV3VENca7meX8MN05vELe4hZipercent2FmbnkpkzptbpkpHzuUbrFFdoPYlLmOm8Sm7FzX6shUCjcyOfMEpkZ44DdjDmrqQjOCl9O4bDXEfHF3jctaw9JadITdbbKhMpkFc2zDiAtznBpSpercent2FiSYQpercent2FngDWoKEDkT9qcsmjfZGQApercent2FpcQ9rW662FjQtBsxNpWzYUM5G9aE0wjuIBvO4TXIRxsAypercent2Bstqj7lpercent2B6Nz9gdTSO8fnYOxvMM2percent2Bpercent2FZbdixFMdBa2kUcOAP2percent2BDGu3dHqo191fcuOKW0ylLdkILbG58kTNgTWKuX4HHkOkVBXrYjpmOUVIThV6Pu4eNTIteYMmtxscshRhz12OUSmVpercent2Fp2KoGa3YSsdnAVza3e1VePDV95percent2Fbpercent2BQs5CR5S3Ppercent2B5ruEhPPTf56bHSnw3rGHT1G3Jpc9kMUF1t6glMCaRTXWdjrJV1QaRJDXACRbhbkWgI190SgTpercent2BS5percent2ByIixxZKUWwM6G314YZ60IMKz3MaRpercent2BbUXNZcB75ZjCfpbBYk4ZbxRK4BNkuGw9FDp2Qix5ZMp7pdRBh7xCtkt6OkJXyzwG5hNwlaEm12rTY5vrl7acyj03CdDnZtcLtbXpUD9Eb2ZJiTSBuHuatGTjhP8shtGZmzAycBSpzWjZweAZCzDSlzduAkQNu4I6DDNljQB5F8p15lznZuEnSzBX0QyXfqVebswEmAbragD3X3wDmGL3N24CRAt1twACINnHqVOdu5SdDNFhyASEOnXmXODpwE6GYLDmE4OXTW7WXOdk4SlCx4btepercent2BeiFvqjXKpercent2B8M21xjl2hzVFu8bShWo7TttVpercent2BaE83zkmpl2EsLhioB3zhO7cUQkOUMW53LcMw8qZzfxJPidxFM4G8SDKtJ31rNlzo7iCfC7iSdxIIo6rMG9A5zpercent2FcNqvzNlNQAlpercent2BJwEVTrP9BqDCwGmpercent2FMmdH8QT43cSTOBvEgpercent2Fp74GwEypwdxRPgdxNP4mxy7xDHs077lTm7CSjB7ySgwmm23xBUGDrtVpercent2BbsKJ4Av5t4EmeDeNC4DJ0tVJmzo3gCpercent2FG7iSRxwr9UIVPX2ph1hTpercent2Bu3UZp7LXKrOeBxMM95KOpercent2BBPeRdukbDwDC3salpercent2BZ40PdZHBYPqKndiYZ1KbBfNW64IWjC5wIxoOTTUpjEU8at7vRNDbRnB9R4Lpercent2BZoL1kVLNMNv9VTbdrcrLupercent2Fipgm720Zpercent2BM9p7U1dBqzuiwjSDedhA02LlIEndG6percent2Bapercent2B3mBKapercent2BVX3FBh6UCXSq8c6d65uKXCTvfPnVsnSKbXUXJxVnXEnN0lXKpercent2Bc0KXeDY3BccNyk8rIiyyIkpG54K1oiCVv7MwicH1Fu7a2J6SpFMJpercent2Fupercent2BxalosvUmNtpercent2B7KZwcprv0NaJjcuhxNb9vg0SvgbTsPYNCueInh5npercent2Fcp3mNZjXQ7jCpercent2FndHmfAO7Qelu6KJ7jpW8veZ4HF3TPoUzu7cnbLyV7n17Qcqpercent2BgLEU5VzemZjxP42u80p3percent2FRrej6qvP1SIhkMTdLnjn6lvxat53Jpercent2FZ3EqdexmfpNTcvMdyjBcuMZNpercent2FriKtftQVU85QECtJCWUm5lkNT7aISaH0Opercent2BaB6l8tuFgO0J124aAuN23kooyeEAy1xnyB0q8wLeq2j0cYlu9KfYnID5X4XJcXxU9yNLGmHVYjqqmpbyfFS4I7YSX2uXS4MyFkE4xDTpercent2B64bhpEFEsG50XOxmdzYM9KoiPupercent2B4Cpercent2FmzknMg0zFTBlKxqGiKqbKaDzUrpS5LZpercent2BUSLU7MneWR9x3qejkZeHDdUzLcpercent2Bpercent2F76r6apercent2Bvpercent2B9Xq8qmpercent2BeNWzCwEiqFwAty36b95aBPlzJsKPxGyRpercent2Bx99gPPD63qwAcQ3vDERvgONdkTPftQj308uwdVo6percent2B792ZJc4RulkORszzjzuC93A7c8percent2F3vLvzx7HDgO4X8prd8GXpercent2F94eDZiM7rAxtzPNlgToePr6rfsePG1kd4wrAEbD0af9LvfIbsT36NAk2pklaQC2ySEKq8F3i8esXyQpercent2F4percent2FYMCWqVpdHF4dldZHpercent2FsddgTuwOESPRlGw8bZx3E85B3Cwpercent2BFQypercent2FXoHpercent2F7QUB8fjnBpercent2Bq4QZNvjCG2IAHhHxQ0W40WSPpercent2Fbp38iLI0AA8S4L4VRpjSB4percent2BvbPuQkYRhn6zAH1dAFHJ1Auh6gpsBKPi4oTU420TQN3jTmi3gQxQ41MjW3tGkDaxe0SToMAdWyFVqTndnNFh43pnaCJbzqBH79ligutEgeyMswM2lopercent2Blk6HyLpBY30wZd1hggapdM5pKKJ247Rj40oXB4FhoSsKcUcPyNsjAmIaIUiTVmytb1XwxhwaoamHoVmABRcGGD2ZmtaWcpercent2BsLlMYsRHXvqqK3ZfbWVulpercent2Fuhu7bVzmJTpy4zV27Qh5vN9HkoOverRt9RAzYQJoetw6Ftja8BPtB3rMuikhvHMHYHDOqZlIXHuz8mlpercent2BBMQptP2ntKB1UgeYeuuq8qb3ShIMz3percent2F4MiOyc7d6mWpercent2Fjl1ki9C626K3afUoqcVAqJ7ry4d34kg4LSb21caW9np0vgRYAHptbJm5percent2BECDnuq9K49HC4tQ0ERl0GiNStFTq53GrPJHA9MnaQ9tbumOmxpnXOTOpercent2FXemo02KxtmjcObdohZpercent2BwJpercent2FKRoO6ui7bjpercent2FyeF29gXD7Xtz76QWcMauyjuFnMSkLZXkRp2JMpercent2Bx0Xo2BZwI1hd64gdcOwbej3Hpercent2BlAJSf2IhwBpercent2BSPL56JT2d8ZpWHOxwHX67apercent2B2LmcClepercent2B7bI5percent2BsOA6Tpercent2FFLqXX0r5TJWPaID2N64ydAupercent2FtM6fXtXpercent2BRd2sfevmM5Vpercent2B5G91NAxN0DRHQpercent2Fvtrpercent2BHsL6v5Fnfpercent2FbWguXK7mwJupercent2BrqLyNe4bqL178z20DOqzc1LCmkUXpercent2BRiMoeszwVMyNJTFGf6erlPem2JXvCWOGGXVhCeeffGP2percent2Bypercent2BuHihtBaOI4DDlnGDPomGswH2NJp33tYb77sT8O9CoI4percent2Bpercent2BFzpercent2BQ53ArcNj8rtVcXqDt8UseDJZddhldHGpveJlMlGxCHnNhzD8pCP24uwebWbHQ01L8hIdeoUU8ZFBSNWg2gLsZFshO3IXIUvNxxUhmkqmbwlWVpercent2F7QLRhTMVqezS1dhpercent2F3e8aPD4percent2BNDKHzpercent2BUc8percent2FPvIG2pQ8bQWucC3LHHu9R4feI9w45hpercent2F6vcNHvn90VCETolCfvtaDepercent2BblYeA4vfBbkL9P31DEpercent2B5YeHpercent2BEKo3Zj7FRdUuDEjXXco6FlJpercent2BEWIXPpPlHR2Lo9nc2NUkOo9q2sBGYYVfvgFWJ8i7J4j5xIrUpqBN3HaxtQ14Ev1X7IWrgvmsDm6Q1C4UoYauZtYugPnPw2s8Mf36b6KVLjdRBd5gpercent2BP7AKmIk6TRriQSLfJb4eWwpercent2BZBFhV4IF6gpercent2FRvzDafI7CeWW7rd2PBfJYBkLv4percent2BeaLZUSQdnOpBs4GFAPpercent2FjFPRpercent2BdHRc50QFjr7GCdH8DfMPD82y6Iss4skw4ppercent2BANMcC0iq2g3OSGOGU3E2gw3PUQECZTGldij6hROEKf76FQsWigwNDLaKCZ3gjpercent2BvQrKUFvdQ2percent2FTsOb8percent2BKn58percent2FEFpercent2FGSWsX68y8fXr97percent2Bf1Pz5u7h3bvwuyADLEx2LHz4Gh4oA9SirBx1aiukqGNmDTkIX0bEKPIw1MLiUsu3SAGmpMXfn1tC7dvTtnpercent2FaQlpercent2B7Apercent2FBeAguTpzj5rtxx1nv33fmbk2w1sxBUn2OUOaspVosLgXD0vCRYhyXykXHqSMn0KACPBWf8zMqgpercent2FvGJgj8rpercent2Blpercent2Bbdki1percent2BbLW3ZIKc54UsxRStQSs90nuORLufWeoGhHCZ7gpi4GPjgPlAul58Hkdy7OsRiHjkeuU9DCPpJdW5uoUZdmKQUMs9CNUkeDkTkppTGGgiPeOsZBt5HjoKspQUcpercent2FIjOqH4OzocX5lZFxaqZRJpercent2FzPpercent2FvjuraXVYDCyUobHOpercent2BlJx6lHpercent2BjluW1Z9dmzkOGZtw5unhkfOk8w1HnVrO8percent2FtbrD6T3TU1HnIbwPWa3GA1X3gzMZTB3xG1jGjjZ7DEqqNgBxbPppcE1Sh5Aghpercent2FRjNyHGwpxmpercent2BPM1SQ6sO29jY1ez8qHYpx0b1bzfaYnhcs0XDKRnXMRuT8YmLjrF33t50vwNpercent2BNqfvM2percent2Ffpercent2FGfsqNP30l33gEa77SYffgHyYTN5sGKdpercent2FKatpoJyQJTfB71lD7JxoXOlHtb4cN7A7qq8sYeZn2BpgQnoRIUKpF5139HJbkVBl1K71Y7I1rQaCV7W1kJcRA9REHTqrcrpercent2BJYugQaWZpJF1j561ikqITWo4yFX6W4T127nKR6BQvXRo3percent2BAZYhr7Hncvao0Nxq3edRD27VWlpercent2B7se3nliFEqx1prxCYpercent2Bu9WJfw3Ws6Wo89Gbkux4tmDSzMRoYB4Gm5WOFT70FWqqR2HYnrK6OpofVTQyjfB5Ar0LQwpercent2B7Q2SIv0pl4N9cfGxZfG9gbahr63U0EaxWqcUHOEMPeoSCPfZu7FOTmBSnNDhsUnBOOzW6p0OWKnXK21ur3qlW7DUCrXYDGuwAFuwDhVQpercent2B7wGW7AK13gsJbH3ZiWV4zcXKXIFKftjcCSFbHW0JHHUHpercent2FG4ydxXwXkpercent2BEdKLvAxeqpercent2Bgl1csAsg3SiyEpercent2Bed4mgnKHk7yK76percent2FoUi6Upercent2B4oB7EZpercent2BUNXHpercent2FFePoqSuKpercent2FXba3RUnIlpercent2FX9percent2FjwUpercent2BmOH8eEpercent2Bpercent2Bpercent2FcPH169epercent2F38wwe2percent2F1DeTD5vVVeSC9W12percent2BAw8YKrvU84X994YCAp95PlPRLml6llSrrqlj0xJ57lboTENLciRLfKmVQkgoKQWpercent2FDUNHIVGUEYPlP73LacdYhwPk1T0percent2BKIQGnMcaK49euPYIXStiP2Pz9G4S382j07Y8qiDvBzMOO3v2oMDFpercent2FRVsEo7JgrdEI6JVvbUlfsjpercent2FlaGkOIfXWlRYUCeuuqJcF1dIHD5R5EumMnjaQCxUlqpercent2FLGc9dK1LoPB3EDa3olHpTywaWCgm9percent2FFI0wnixmetaiTfpercent2FbLS6ypercent2BMC0NQh5WurhO5VDU49dlRGyYspcezcutkgqjvMixMTLUvKTbVuVupNuqupBWKbeqNootJq7ktRWgfZhmLyCOvGHpercent2FqDoEX2Qre478vZDgvEHdTNpercent2BZCY5RbLR9U2Km067ompercent2Bc6t5gARsU0Uchxw7s1pY8FhtJFbipercent2BBcO8w8TyTB4PcjZhRwDcVXpzxpercent2FE1ZW1sMpxM2VmF0Xf5b4ULVXpercent2FXfa1qWy3BNRB9bpercent2B3nrBZ0g9percent2BTF9qz1ur1n2N8hiZw9M0SxV7TJnQK3Sa1aj8DdoAtHC0xzc7GRs2m7QzN8fQtZM2y5percent2Fchs3wSMVhPKjsC3eQE9iCdW6IjpTmyBVT0zgZF8wZpercent2FHfCbGegSwJ5gYSD3qLgmXsb2Mx0EBpercent2FRz8UEeeZt15GiV45pdWN3nYnYXssnvE5nF3wOZZ12cF9LO62OETTxdZsOoe9vtsCoLko3RRxFHCu0macJqc6Hosn40IdJnjYywpercent2Btww0xxcC2percent2Bv39xxiRvCSvQVEFFNODOpercent2FZRSgSfVDuhIlyEWhnquUBqGkAoV5C4KyNpercent2BP6MPH8Lpercent2FcEpnlVIJvpnDehTDGWHV2yUlpercent2F4dCYod8To2X8UFfHqKuPpPT8nMV7x10Hi38pfm68p8XStos775TQbyqzSP5HHO99V6opercent2BhNAEByoSKpercent2BDhDlZ0FMM9kOM0azxXOKbUdWpercent2FjSO0xsevk2lGwyQSZrxnvASlIuqKgyyixzq0uRarw0NNqJkUbDOoQjhehoi0TlixNNKUnk4percent2BBJKacwlOpercent2ByVt0SP3LhNpercent2Fb6uqtFxrZnoZ7rpercent2BWBLp2esFverOtHaHbYFVd6q1qwVeypercent2BTaN5ut5uxWb3rlB3c2xrPxpRpercent2BC7NpF4QmeeTQpercent2FTmEUFA25FApbK1FMXuGdx1Y0KAgVjCpercent2Bhpercent2F1SH0M6g4unR6ugpvOUtvRCap07Jc3o2Hj49x8percent2FnSovrA1bKa9dkNSzMWJ3aaSOxesdC6KDcpercent2Bst0mtN89a5hAB6xAu59bdtlFVpercent2B7ktci0sFA23percent2BA5hRfOBjnLVOJNu4n6Xl4BWoJYUsiYewegOF3w9ql2eyqSmz8sumfNILTtpWuKbGhVVWJGNLlPAMkiLIs4XEvSnvTKOOtPVFX4OC9AmVsz1hG2BupGKXNcHlppercent2FfqKQ7tjkCnXHDaTKMFoEaeicNthe1Br7jXMqBvhbSj7qYVMp6uVi9ri3DZjous7yNpe1rGXI9yHwQ0tjbC6k0vt2AFrUlGp21N03percent2BtdjzNxsBsce003dNIJapercent2FHWcoyNZOtiN4htszaf6Bd1czkuycVpercent2BDHMALWOlrNM3dncMr8sONDje5qP38x30jEIrzU9dW32Ipercent2B7QoYOCJ3gDqUMNoKM298Lv1q3fqWTuBtA3hbjC5ffYIzwnTT7percent2BnbVWTQPVRvTbRVx51klMGjd2OGlmnGU2zy5AU31I5M3vB6gsoHearOyjKvk8Dgh6bdYdD82l3tPEqh70SYs8cEzd0yQHuxIJydPItpercent2FAaMvFjBuHscTH6percent2FoDEKsvnX8Hg6DYpercent2FLz7VV49n7d9ZpN42percent2Brj57J3a9Ypercent2BI4psVirLicE0nOHrgWUG4zNR0bEGDmd37MGSKI7ISm5ZuGpmF0XY1LBbSw249vXpercent2F4EeL9percent2Bm8percent2BCOH6CLRenweikHNtpercent2Fpercent2B1Dkpercent2FWrhB3OoS8KzyygOW4atbEbG4Hca8yXDnpercent2B4kjdlFMOpercent2BuYIQL49KbywjcrY2B88sgTGpercent2FgSbjmoipercent2FPKGK93Sqptpercent2FXMFHBzEHtlXk0zwb54XDYTDY2JMfU2percent2FYd79percent2B7lpercent2FwQpercent2FrxdQJpercent2BID7sp781percent2Fh4RfarGpBpercent2FDM8PIvyeRys3vwjPPN89v8BSeiR3Qpercent3Dpercent3D

No analysis is required (don’t run it ;)). Merely conceal the enter cell, leaving solely the output seen by way of the properties (click on on the top-right nook of the group).

Here’s a pocket book with all examples [13] (a few of which work in a browser as nicely with out a kernel).

What if utilizing my very own laptop computer isn’t an choice?

The WLJS Pocket book may be exported to HTML, preserving even some dynamic habits [16]. That is achieved by way of a fairly refined algorithm that tracks all occasion chains occurring on the JS and WL sides, making an attempt to approximate them utilizing a easy state machine. The result’s a regular HTML file containing all cells, slides, and the information of those state machines.
Nonetheless, as a result of number of values obtained from the IMU, the exporter can’t routinely seize this in a JS state machine. Consequently, rotations and the joystick enter won’t be preserved. Nonetheless, all the things else will perform as anticipated. 🙂

… like a fish wants a bicycle

Please check with the ultimate sentence of that stunning comics by
Zach Weinersmith.

If not for the pressing have to showcase a rotating crystalline construction, this submit wouldn’t exist.

Thanks on your consideration, and to those that learn this far 🧙🏼‍♂️ 🤍

References

[1] — ECMA-363 Specification, Wikipedia: https://en.wikipedia.org/wiki/Universal_3D
[2] — DPG2025, Homepage: https://www.dpg-physik.de/
[3] — Leo. Proper Pleasure-con Controller as a Distant, Hackster: https://www.hackster.io/leo49/right-joy-con-controller-as-a-presentation-remote-5810e4 (2024)
[4] — Jen Tong, Nintendo Change Pleasure-Con Presentation Distant, Medium: https://medium.com/@mimming/nintendo-switch-joy-con-presentation-remote-5a7e08e7ad11 (2018)
[5] — RevealJS, Homepage: https://revealjs.com/
[6] — Manim, Homepage: https://www.manim.group/
[7] — Movement Canvas, Homepage: https://motioncanvas.io/
[8] — RISE, Github Web page: https://github.com/damianavila/RISE
[9] — WLJS Pocket book, Homepage: https://wljs.io/
[10] — Vasin Ok. Reinventing dynamic and transportable notebooks with Javascript and Wolfram Language, Medium: https://medium.com/@krikus.ms/reinventing-dynamic-and-portable-notebooks-with-javascript-and-wolfram-language-22701d38d651 (2024)
[11] — Vasin Ok. Dynamic Presentation, or The best way to Code a Slide with Markdown and WL, Weblog submit: https://wljs.io/weblog/2025/03/02/ultimate-ppt (2025)
[12] — Pleasure-Con WebHID, Github Web page: https://github.com/tomayac/joy-con-webhid
[13] — JoyCon Presenter Device, On-line pocket book: https://jerryi.github.io/wljs-demo/PresenterJoyCon.html
[14] — Faraday Impact, On-line pocket book: https://jerryi.github.io/wljs-demo/THzFaraday.html
[15] — James Lambert VR powered by N64, Youtube video: https://www.youtube.com/watch?v=ha3fDU-1wHk
[16] — Dynamic HTML, WLJS Documentation web page: https://wljs.io/frontend/Exporting/Dynamicpercent20HTML/

All hyperlinks offered have been visited on March 2025.