TOWARDSDATASCIENCE.COM
How to Use Gyroscope in Presentations, or Why Take a JoyCon to DPG2025
Image by author This article explores how browser-based computational notebooks — particularly the WLJS Notebook — can transform static slides into dynamic, real-time experiences. This approach isn’t limited to presentations; you can prepare interactive lecture notes for students or colleagues and publish it on web. For data scientists, physicists, it highlights new ways to communicate models, simulations, and visualizations, making complex ideas more intuitive and engaging. Is a PDF Enough? Animations, bells and whistles, especially the kind that were popular in PowerPoint 15–20 years ago, have largely taken a backseat. Add to this the compatibility issues between LibreOffice and MS Office (even between versions for Windows and Mac), the presence or absence of necessary fonts — and the desire to do something unusual on the “stage” fades away quickly. Have a look at modern technical presentations: quite often, it’s just a PDF document consisting of pages with vector and raster graphics, and sometimes GIF animations that eat up megabytes (like this post), with no mercy. Unused Potential It’s worth separating decorative bells and whistles from those that carry additional information in some media format. For example, take a look at ECMA-363 [1] specification. Convert MATLAB Figure to 3D PDF / Image by Ioannis F. Filippidis (fig2u3d manual), BSD-2-Clause A 3D model inside a PDF document simply enhances the user/viewer experience. You observe the object from different angles/cross-sections. It’s disappointing that such a feature is almost nowhere supported except for Adobe Acrobat and likely will not be. It feels like we made a leap in the past, but now we have returned to static slides. Large Scientific Conference DPG DPG-Frühjahrstagung  is a large European physics conference organized by the German Physical Society (DPG) [2]. Every year, they gather more than 10^4 scientists and take place in German cities, covering a vast array of physics fields. Deutsche Physikalische Gesellschaft / Image by Wikimedia, PD-textlogo DPG2025 (Spring Meeting) took place in the wonderful city of Regensburg / Photo by Tobi &Chris, Pexels License There are so many presentations, and it lasts almost a week, so by the end, it becomes too overwhelming. Nevertheless, this does not diminish its value as a platform for networking, practicing presentations, and a reliable way to learn what is currently on the market, which trains have gone already, and which are just departing. The participants in plenary sessions are mostly Master’s students and PhD students, with postdocs being rarer. Such a large and accessible platform is an excellent motivation to try something new even if something may go wrong. What is a JoyCon? Surely, the reader has seen devices like this: An average PPT clicker device / AI Generated “PPT Clicker” image using Dalle 3 by OpenAI This device acts as a slide switcher and sometimes as a laser pointer, connecting via Bluetooth or through a dongle. In any case, it is a type of controller with buttons. Controllers can be more interesting — like the one from the 2017 Nintendo Switch handheld console JoyCon (R) / Image adapted Wikimedia, PD It is not much bigger, but it has some additional cool features: Analog stick 11 buttons IR camera (difficult to use, no good API documentation) Full IMU (Inertial Measurement Unit) aka gyroscope with an accelerometer Bluetooth connectivity; recognized as a regular HID The buttons can indeed be mapped to PowerPoint, or the stick can be used to control slides, emulating mouse or keyboard clicks, as it was implemented in these projects: Hackster: Right Joy-con Controller as a Remote (Python) [3] Medium: Nintendo Switch Joy-Con Presentation Remote (USB Override MacOS) [4] I thought it would be cool to somehow use the IMU and analog stick. But for that, one would need to go beyond PowerPoint and PDF Moving Slides to Browser Environment The idea behind this is not new, but it’s important to remember that this approach may not work for everyone. However, by moving the presentation display and creation to the browser (particularly Javascript and HTML lands), we automatically gain access to all the possibilities of modern web technology: peripheral device support, JavaScript, CSS animation magic, and much more, including video. It’s important to note that all of this is cross-platform by default and will work almost everywhere. For example, slides can be created in Markdown (or/and HTML) with the help of a simple framework (rather, a small library)  RevealJS [5] There is also MDX-based presentation engines, and things like Manim [6], Motion Canvas [7], but these guys require even more skills to master. The RevealJS API is quite simple, so controlling the slides via JavaScript commands is easy to implement: setTimeout(() =>{ Reveal.navigateNext(1); }, 1000) However, this direct approach has significant drawbacks. It requires an internet connection, and if you’d prefer to avoid it, you’ll need to use bundlers (such as Rollup) and embed all JavaScript libraries into a single HTML file, for instance. Alternatively, you could run a local web server. Option with Jupyter Notebook If you like Python and IPYNB, then use nbconvert — it will convert your notebook directly into a RevealJS presentation, and you won’t even notice it! Or use the extension for Jupyter—RISE [8] Create Presentation from Jupyter Notebook / Image by author In any case, the idea remains simple — we need to somehow enter the web browser environment to take advantage of all the possibilities of JoyCon. Try it on Binder! Option with WLJS Notebook My opinion on WLJS [9] might be somewhat biased as I am one of its developers (and active users). This open-source IDE with a notebook interface is more tightly integrated with the web environment, as slides are not exported there but are instead executed and are just another type of output cell, alongside the familiar Markdown. WLJS Notebook / Image by author Under the hood, it also uses RevealJS but with a few extra features: It works offline It allows embedding interactive elements and components, similar to LaTeX Beamer It is integrated with Wolfram Language (freeware distribution) See more about it in this story [10]. An ultimate guide on how to make presentation there we published in our official blog: Dynamic Presentation, or How to Code a Slide with Markdown and WL [11]. Let’s Dive into JoyCon So, the easiest option is to use the already ready-made library joy-con-webhid [12]. Why spend time reinventing the wheel when people have already done a great job for us? npm install joy-con-webhid --prefix . All subsequent examples will be taken from the WLJS Notebook. However, you can do pretty much the same thing using Python + FAST API to interface with JavaScript or something similar, or even just use JS alone. The online version of the notebook is available here [13]. First, let’s listen to what’s coming from the controller port. Code .esm import { connectJoyCon, connectedJoyCons } from 'joy-con-webhid'; // Create connect 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(); let isAllowedToConnect = false; // main handler function (warning! called at 60FPS) function handleJoyConInput(detail) { const currentTime = performance.now(); if (currentTime - lastUpdateTime > 50) { // slow down lastUpdateTime = currentTime; console.log(detail); } } // JoyCon periodically goes to sleep, we need 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) 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 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 click event connectButton.addEventListener('click', async () => { isAllowedToConnect = true; if (!isJoyConConnected) { await connectJoyCon(); } }); // Just decorations 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'; // Return DOM element to the page this.return(container); // When a cell got removed this.ondestroy(() => { cancelInterval(connectionCheckInterval); }); The most important function here is: function handleJoyConInput(detail) { const currentTime = performance.now(); if (currentTime - lastUpdateTime > 50) { // slow down lastUpdateTime = currentTime; console.log(detail); //output to the console } } It looks like there are many steps to do. In reality, most of this code deals with connecting the controller and drawing a large “Connect” button. Don’t pay too much attention to the special methods — they can easily be replaced with those available in your specific environment: this.return(dom) passes a DOMElement for embedding on the page this.ondestroy(function) calls function when the cell is deleted, to clean up timers, etc. The first line .esm is a way to specify the JavaScript cell subtype in WLJS Notebook, which requires pre-bundling. When we run this code cell, we will see the following: DOM Output Element / Image by author Then follow these steps: Disconnect the controller from the Nintendo Switch (System → Controllers → Disconnect). Pair the JoyCon (R) with the PC by holding the small button on the side. Press “Connect” on our presenter controller. Opening the browser console, we reveal the following messages: { "buttonStatus": { "y": false, "x": false, "b": false, "a": false, "r": false, "zr": false, "sr": false, "sl": false, "plus": false, "rightStick": false, "home": 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 } } } Quite a lot of data! Let’s try using this for the benefit of our presentation Buttons To begin, we can use two buttons to switch slides Image by author In the WLJS notebook, slides can also be controlled programmatically through a Wolfram wrapper function that calls the RevealJS API. FrontSlidesSelected["navigateNext", 1] // FrontSubmit All that’s left is to trigger this function at the right moment when the button (or switch) is clicked. To do this, events need to be sent from the Javascript world to the Wolfram machine, where we can then do whatever we want with them. This results in the following diagram: Image by author You don’t have to think about this, since it is seamlessly implemented via APIs Let’s go back to the code cell and modify the handler. Code //.... //....... const buttonStates = { //all buttons states on JoyCon (R) 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; function handleJoyConInput(detail) { if (!isCalibrated) { //calibration restingJoystick = [Number(detail.analogStickRight.horizontal), Number(detail.analogStickRight.vertical)]; isCalibrated = true; return; } const currentTime = performance.now(); if (currentTime - lastUpdateTime > 50) { 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 (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'); } } } //....... //.. As you can see, we have added several items here: Joystick calibration — analog sticks drift, so their digital position is never perfect 0.,0.. State of all buttons — why hammer the door every time if you only need to gently knock when the state changes? This reduces system stress. Sending states to the event pool — this is specific to WLJS, where we send data to the Wolfram machine (or Python if you’re in Jupyter). The last point looks like this (replace with the equivalent in your environment): server.kernel.io.fire(String name, Object state, String pattern); Then, on the Wolfram side, we can easily subscribe to these events like this EventHandler["name", { "pattern" -> Function[state, Print[state]; ] }] This is very convenient, as Javascript sends the names of the pressed buttons as the pattern. In this case, you can immediately subscribe to slide switching, for example, like this: ZR — next slide Y — back Thus, programmatically controlling slides becomes intuitive: EventHandler["JoyCon", { "zr" -> (FrontSubmit[FrontSlidesSelected["navigateNext", 1]]&), "y" -> (FrontSubmit[FrontSlidesSelected["navigatePrev", 1]]&) }]; Let’s Test in Practice Let’s create a simple presentation. Start typing with .slide # Slide 1 __Hey Medium!__ --- ![](https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png) Now, let’s connect the JoyCon to the PC and link it to our Javascript script by pressing the Connect button. Then, subscribe to the events once in the active session. Now, just run the cell with the slides: The first big step in mastering JoyCon has been made! / Image by author Analog Stick  The stick theoretically allows controlling two sliders simultaneously. For the DPG Spring Meetings, I had the idea of a live demonstration of a very peculiar effect m𝒶𝑔𝒾c𝑎𝓁 𝓌𝑜𝓇𝒹𝓈 𝒻𝓇𝑜𝓂 𝓅𝒽𝓎𝓈𝒾𝒸𝓈. I believe that some concepts are much more impactful and comprehensible when demonstrated live on stage. Here is 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"} , Frame->True ]; FaradayWidget The interactive online version of this widget is available here [14] Image by author To embed it in a slide, insert its symbol as a tag (similar to JSX): .slide # Faraday Widget Here it is in action <FaradayWidget/> Now, let’s link it to our stick Image by author To begin with, let’s perform a simple test and bind its position to a disk on the screen: pos = {0.,0.}; EventHandler["JoyCon", {"Stick" -> ((pos = #)&)}]; Graphics[{ Circle[{0,0}, 2.], Disk[pos // Offload, 0.1] }] Image by author Obviously, the movements are too abrupt. Moreover, making small adjustments is kinda painful using JoyCon. The solution? Integration! EventHandler["JoyCon", {"Stick" -> ((pos += 0.1 #)&)}]; Image by author Now, let’s link 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"} , Frame->True , "TrackedExpression" -> Offload[5 pos] (* <-- *) ]; Here’s how it looks live on a slide: Image by author And in the actual DPG presentation: Image by author A Moment of Rest Last year, DPG took place in Berlin, and this year — in Regensburg, which has about 23 times fewer population and is 10 times smaller in area. However, the cozy lands of Bavaria have always been closer to my Image by author And this is the university. A solid 60s-style building. Wha Image by author A new invention — a cup “Drink and Eat Me” Image by author As a bonus, every drink gets a hint of waffle flavor! But, watch out — don’t bite into it while it’s filled with hot tea. I couldn’t take more photos since I got sick on the first day and went back home to Augsburg. In general, spending six days at a conference is quite challenging. Image by author Back to business IMU or Gyroscope-Accelerometer Combination To use them, we need to read the corresponding fields from the details object, namely: actualAccelerometer: x, y, z actualGyroscope: rps (radians per second) Code //.. //.... 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; // 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'); } } } //.... //.. Since IMU is not always needed, the script includes a boolean variable and a control function JoyConIMU[True | False], allowing IMU measurements to be enabled or disabled. The JoyCon, like most other devices with IMU (some smartphones, watches, but definitely not VR headsets or quadcopters), includes: 3-axis gyroscope — returns angular velocity in rad/sec around all three axes 3-axis accelerometer — returns a single acceleration vector Question: Why can’t we use only a gyroscope or an accelerometer?Let’s try outputting both. First, enable IMU usage: JoyConIMU[True] // FrontSubmit; Now, define auxiliary functions and variables: prevTime = AbsoluteTime[]; angles = {0,0,0}; acceleration = {0,0,-1}; process[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 happens here: The accelerometer vector is simply stored in acceleration. Gyroscope data is processed by: Reordering angular velocity values (JoyCon hardware orientation) and adjusting directions. Integrating over time to obtain orientation angles As a result, we obtain: Three angles defining JoyCon orientation angles. One acceleration vector (at rest — the gravity direction) acceleration. These three angles are conveniently expressed as a matrix (tensor): RollPitchYawMatrix[{\[Alpha], \[Beta], \[Gamma]}] // MatrixForm Applying this matrix to any 3D object allows it to be oriented according to these angles. Physically, on the JoyCon, it looks like this: Image by author It is important to note that since we measure only the first derivative (using Gyro), then the initial IMU orientation remains unknown. Therefore, we manually set the initial state, i.e. angles = {0., 0., 0} EventHandler["JoyCon", { "IMU" -> Function[val, process[val]; ] }]; angles = {0,0,0}; (* calibration *) Refresh[acceleration, 0.25] (* dynamically update *) Refresh[angles, 0.25] (* dynamically update *) Real-time Data Output: Image by author Well… Not quite obvious what these values mean. Let’s try to draw then as vectors in 3D space: axis = Table[{{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; ] }]; And then render them as colored cones, where: Blue and red — defines angles derived from the gyroscope data Green — accelerometer data (inverted and normalized) { {Opacity[0.2], Sphere[]}, Red, Tube[axis[[1]]//Offload, {0.2, 0.01}], Blue, Tube[axis[[2]]//Offload, {0.2, 0.01}], Green, Tube[axis[[3]]//Offload, {0.2, 0.01}] } // Graphics3D EventHandler[InputButton["Reset"], Function[Null, angles *= .0]] Image by author The green vector is always aligned “correctly,” while the blue and red vectors, representing gyroscope angles, accumulate errors over time, especially with rapid movements, causing drift. There are many ways to solve this issue. The general idea is to adjust the angles using accelerometer data (green vector), as the accelerometer precisely determines the downward direction ( until an external force disturbs it). For a more detailed explanation, check out a great video by James Lambert [15], which explores these problems and their solutions, including a detailed example with Oculus DK1. Why the heck do we need this in the presentation? I asked myself this question when I discovered how deep the rabbit hole goes. In my talk at the magnetism session, there was exactly one slide where the idea of an IMU made any sense: Image by author Do you see the crystalline structure? Finding a “good” camera angle for it is indeed difficult, so why not rotate it directly, lively? We don’t need all three angles and acceleration-just one will suffice. Thus, we discard the accelerometer and keep only 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 just need to apply the rotation transformation tensor to our 3D structure. Since this crystal contains many ions, which are also colored, 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 Here is my crystal structure! <CrystalStructure/> Image by author To make it even more convenient, it would be useful to subscribe to IMU only when the slide is active and unsubscribe when leaving it. This is easy to do because RevealJS signals the core on slide state change events. Let’s implement this as a component using .wlx cell type: .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") -> Function[Null, FrontSubmit[JoyConIMU[False]]; ] }]; <div> <CrystalStructure/> <SlideEventListener Id={id}/> </div> ] By placing this code on any slide, we achieve the desired result without polluting the global space or interfering with other event handlers. Thus, IMU subscription management is localized to the specific slide, and when switching slides, we correctly enable and disable data handling without affecting other slides and their processing .slide # Before --- # Slide Here is my crystal structure! <InteractiveCrystalStructure/> --- # After These are actual slides from DPG2025: Image by author Short video with my DPG2025 slides Final Code and Notebook The compiled presenter controller cell code is provided under the spoiler. If inserted into an empty cell, it will produce a functional widget for connecting a JoyCon. Compressed cells jsfc4uri1%3AeJztfWtz28aWoGtmdqdq99NU7Q9oa3ZiUiJpAqRkmYqccWTnxlNx4vXjfvGqHJBoSkhAgAJAiaSj%2FRf7af%2FsnnO6G%2BhuNEjKj3vvVN1UTAHd531Ovx%2B4P05fT%2F%2Fh3r17%2BT%2FBz09RXkz%2FEd%2F%2BO%2Fw8zfN0EgVFlCYVyOtFzN%2Fgw7OgCN78v3%2B5d6%2FH89n%2F7nt%2BNJunWcE%2BskmaJHxS%2FEe6OkuTjnrloUjI2S2bZumMPfgtXXUhs3vDx5dR%2BOAEieC%2Fhw%2FZWcaDgrMgCVlerGLOikuuCIE4bLwoijRBYEjMC5X1PSWzUxamk8WMJ0VvQoSexxzfWg8E3oP2iUStkHqTOMjzn4MZB%2FQHGY9B72vgucjyNOvO0ygpeMaydJGEPOzOQnbZPWLzuDtg86zrs4Ivi27Mp4V4usiCVfew32dTECQfpYsijhLeTdKEsyxKLroey2cjAl3m%2BBjzIMT0Iza%2BENhev%2F%2FAIWYEL9lbQAQx985E1l5pupgXmpneFGhEGw5holw440z5BqCmQZzzEgKsUbybh0DgbUQ2mfNsmmazIJnwXpLetNolU%2BECYVpimQP4R8xhLBgJuh02Lp8u0xkvX%2BYx2Ee9ZOVTHlePZaoguSxzVuXTWsEgyK0lGcRZXkST31%2BleUTRc8re93v9DoOf81LhjANQcvEfEtgNFOVnQRyNs8Blsmi2eJ4E49iVlT%2BN4%2FSGh29TaXIDREa9wGYvXr5jszTkLJqyQKAJZTLeE25DiFMW5KtkwlpBdpF3GE%2Bu2%2Bz0ibK7IUtwE0QgA0bwPOPoIUJ63z8XeCem1UCSHxaJKGhFyi6hFIJUwLkLrIHMfFEg2FTBCAApGea2QmASxe1SmClr3ddNV%2BYwl9l%2FXszGPJNEekESxOnFG8x8HV1cFr3LNIvWaVIEcbvDtsBe8wxeAFK4EBlaLiyyBT%2BphCkWWSJfb5U9GJOVzCLLoBbZWB6Esjpk1y5KT9hhHwzAwMwilXEQcwWps1wJUit9GsWTSi4A5KrkvQJL5lboKQhVAl6m1xaEggJVQGxS83e%2BYumU%2FTL%2BDcK0B295Sy%2Fbbc150rV69ntAOGfffMOkS6q8hchr1%2BQ1XMBYndppI7ES7VZXRuihfP%2FLdJpzLG%2B7xgq4zIrK917FSRCvgnBX8lrYOhjIOqYMUjDry6C47AXjvGUq0oYA6vc89scfrISwpZEwhqfsGDCsbpjPri9BOIC3eZw0gnsI3jWlrmlXVVCGlDnPABGiLkt43IvS3jTKeOuBqFwedHRQxh48nUx4zKEzwaFqezBSMXsdxAuel56YFIsgNkDbHYPMn1ZZmk%2FSOd9MogTrhfO8XVG47bAHUCM%2FaLvNidoaIW8o%2FKkFT6erlRULZrtBMQw6yLp9YiKOoef0u5F2q6ncqKkRZndyrR1HYFUqPrZd5Z%2BSM9Sjr3gWpSFGW7xik0sO7Qjatex5qsYrr%2FUYgc8Zwr%2FAxhGcDpEL0areWrKJNVpW0ZjZ7XlbbzyMVkMJ8YxfRxPqG9k9YhVtZROysXcmYLS4%2BY2AMXRsXpr9XTSNKkA6D%2FJ70B4lBY4FOPQ1nxZFACYFXwJ1qLMWZsMh%2BhYSEcpG0qrcZeRxKu0QqkkYZOEPizh%2BCX2czdBQqrYD%2FZkac3BkE1gGFXPMW0d96M3RT7932DaUkIBBGD7XVW89gHEJ9XcgPFsfZSvEbikcGns97ROLrNOcpvm1YrSgZv%2FM7MRL1aCm8fv9fiV81YcQNQF794KNA2xW4VkbL%2BVIxOi0NbDRo7weMtCs26OL%2B6fV%2BIKHe1rENY9DAO7EgnKMbiz8BgwaHvbGweT3Cxqc4ejtX8Pj6TQ8fqCMyziUHVVw76jTbhp9XX0e6FGi13s%2Fiq659P0kxg40RVt93FiPbQKHwHbVca7hihaublNqlhLlz5gIKEun0MIePmLlEsAYOdsweg%2Bj62roLqCFhX98%2B%2FInwPv123wGTcATbGc5jdcRLktjaPi%2FfSjyfrXwgznUWeHZZRSHLcNiNiNjfmAa8yXDn%2B4kjdlFMO%2Buuj4O3W8uIwgMbZogvwzC9AaeqvmN4jLKe6K1aJX022Z2Cvg5iL5qGY6Z4HAjLpunhkZMVhbtE8e0zdvVnL%2F5L%2FBAdZYF8M84rxPl8zhYvflv8DyByncWZVma3WlW6H%2F833v3IDoTwP4AA1nIyx%2Bakz0PQwjDh7MgvLiBIOz9lht100uZ3sqD2RxaAalWh6VzzK%2BaNfkOHlFP0Cv%2BeHuit7%2BCxg8ZvwIwjw%2FYQ2aSldA0koIavCLWo1eg2O8NNaAoiaATHImxiwIN0xeQHGF6IOYYTkWJYd%2BJppuN9AKEdK76KFCHXXnwF1qlK1%2F%2BHeBfDTDjk2j%2BxlCjVAITJGhpPmXWpz%2B%2BfiNaB2hHWxfLDrtYwb81FHl4DuA5WGtltmT1MwxrjeFjDlLlIGnuw7%2BBkXX1LC1QB%2Fjjiz8D8WdogF37V0ADfj369ekXIK%2BHlD6k9CGlH9PzMT5f9TH3ysOUK8K6GlxV%2FIk32qp3yPZZqwuG3GcXSxhdgSnhaYVPA3xatw0kv0ICJxDOgcRZlzgrE2dg4xB1T%2BAcSJyliTO0cQjSK7F9C4eq1VawpODpY8sUrLTntXg2xwGlx4AXou6Dc4ELIO7jzwGi7ZOj9%2FdZF6TRuvQIfurwOSP0hpx1Uw76GIRApa76Zrqn0j0z3Vfpvpk%2BUOkDPX1I9Ic1%2BkOiP6zRHxL9YY3%2BMcEf1%2BCPCf7YhscohHTyoMEYAxMzPJvSldDsqqYahi9mDGzdcuRACu4L7ANhHulNlYMMu8Kg6F2dgCcIkCzIpSvsKAkIo0kiIq8vAqQrsA6EWSSP6k3KIggHa51jaVxJVwrd14T269JIjr7g4RscfYOjX%2BM4KN0s9dSMUWrpS64iz7cNZZSXHIWFnwO03z7%2BHKBe%2B%2FhzgPzgaeAqOYjoLgVIqCHHb8wZNOWIKq57KlomlNXK9PVMz8oc6Jm%2BlTnUM6tYLEf24MiDUynAvt0GVdWcp6D8TVC%2BghpsghooqOEGKMOBslCqapUCSYaRqpSvHA68anDfVYPzrhpcd%2BV03K3VHk%2ByNM8%2FzDPoCU2KVtX2dtgYnsfwPF4bM%2BLYM2QfcaGDqvExNkpUjSPsaiSfsaGjCn8MlNYj%2BYzlS2At2W2DQBx6ZNnT5AL6ZT9k6ezFbPE6CA3BZvA8g%2BfZ2hz%2BQH9qHhWTS5zgE7OPMJ73CZVe86sMNKy1PVUTJ2gUsznWV82GgQYfx%2BkOPL%2BGJ2E7RLW3lH9X8u%2FapoHDAaChSY9UJbxfh59kCnqS5i3EtiHyuYLIo6RF9qmBZDqIi8gKbTpDywHDLhgei2VmAS0JCP1cCiTYga0JF%2FjsozwHgsBEvNpz12Kt0fLh6rID9NsntTCU4B3h%2BY4wYFNoFen%2FWkCHM0vgxW9pgVaLo8lKt6sG2VPy7VcTNZolV7old0abzJu4iXh285o38dqAZMaLjkSGczPKmhi5cErP3IyEh0G5fbTnQel%2F%2BIGAhgok1%2FO7KiBk%2FmqkJVT4E5W%2F1unniJ%2Fr4E0xgAOkXWsTjuOtO9VHlukiHA9ZURfUyrzeZkRXvRtQgP4c4J%2BleFuKt5V4W4m3tXhzdJ%2BpUyhpOVuSU0Xb2Z6cKl7OVuVU8a7nGsNPewpx4yiwYQioW7fDQh4XAS4zvuETo1Gyx6A6IA6Sm9prMVdUCm2uHm6IlHqP5GsOUC%2BB8eXKSMK%2BLMpDf1fy71qMYkX6WPxi2pCeh2OR7x7rQrp8868sIcVAt08D3b4c%2Bg7Koa9HKR6liGEwETQGw2jmmRg5XqdRCENG8MlsZSes7QRtsDnTBpszNdjU3XWniQWjz60tq%2BtdzL8P4P%2BWBvCGJNTLmC1Vx2K2Un2KmVOSWaMks0ZJZhunEjAy5WQCSWJnr4zslZ29NrLXZrZXEfdcxL%2FqNAZVA7p0NpqGWMNunozoi8mIvi2f5FbnhFVMmWHwaJzX8AQpr0bKE6S8GqnGmRChpEPBpimSy6Xq%2B5INuioKaKx%2FoJwuR34CTk4siBkCAeprCWs1TyCgUVT1PDBZY6zJmJTsBDVNkLUafsrwIl5dCScEkRJcabM7mgyrOl9sYcqOIY6sLpHuJRbKSwS%2FNBbtsSWijReloGpGRgqhSjCJrQs6UAOOymJ%2BabEqU0guRbam5saqQKHMZs66yjFmc%2FpSWMxryYkgOWvTJ9sFy3blLQHRL%2BUj0GDVJoTxWhWWFtkM%2FmK13VVORXEFLYJsSU7Ip00%2Bx8xWV%2BLKeBJEvbZOlUoAIZr0%2BnLmAZPRlzJzWROLVDsgVXUCUloxv4dCExmjERUzfGIWbbO5%2BpvNNVTVXguZlTNw8lEYDJubygaDzzRsZYmKZv%2BT7dqq3NQV8VV306db2VdFaKuZBzuY2d%2FFzBB4w3FZY7gN9BlW93RPfr7V%2B5rV%2FS9l9YGIbW%2Br0f1NRtdN2VyGP6dm6GtU%2Fc%2BuGbwvY72%2FT2mzv09p%2F0WmtMuZJ7F7aOQYEXZoUA%2BjQV5UczIt1%2FT2zYjhqHcJfzyai8KR7XqEEpQzS%2FJJ3%2FOydVfBPMhyLrcUXAcZw1MZakcBGDjhN9UGA09OMSNchtt0dwIMQj%2FkOH%2FqHffZQ9E%2Fe%2FWC8svpF9yF9WYxawVZhgPiCCwB1uClIeSRg0WsNtJqG%2FpoOyDu5yPk%2Bsyp2AuOk0SSaoswzHGogrpfjvetYSfyZqflQzUt8F3JYaRyD1SSXajMwNAUujWsMeNB0jK1kShop5dlbodJVXDPi6bUbc22Ok7NuMJOMU8uCpwxJzDQkCWLOAb1%2BqAYpfUEyIkhkUT7rtmFDxXMiP0c%2FOwQcBLEk0UMwN8HBWCtfuLXPFaaaQEQY7rknt%2FQnLKAet%2FXd%2FJOQBB2PKqcR3i4%2FWwKCu1ptaC5aZfwhi68GQ%2BjxWwLpu%2FCjNObLWieC22SRbQbfAtu34XLZ%2FNi1YwY8mkAUefkehlkF1Fyseesxirr36qifVbuF8N9UkDiI6jD9n7Cg11yD%2FFeBwzD9mhTv5Y2gLRXWapR2JOVF9IdR0FOc0mPDsu0Nc%2FS71U6pFU5ORiKq4HfqxcQb75ZuRTp82rKvIWHRf5MO4hxIk7uUe%2BwQN%2Fx3mFyuexFaJWSJL0BVs8gWI1jLCIzxBqiot8rohnPC2jBoHi0ELPrzMUiglutRtU%2BJheNU2R%2BYgpDjaC2lqgrgbPp%2BwybQTN55U5eU3K7LkEQzy9x3YF65soN1JurAR1UFqX5%2BLDQz%2FmQuPeteVKNhtxPRt43qVOOTnwpiGNvE6UaK4lqBpDR8ZBsVVX5Gu2LYDZrYCuydL6rnfgiVNfF2O4YkNVGVbBRu3LsPT7GgOl6ThvvYzvarhpSSBgO%2Buzf2ON%2Bu1ekP0RLHraO2hBNDbgO1MGRgduhbuHIIYLsLpoSmKhktJ11EibeRNFSRENogL91NDFGFaD1tK6s4n2DpVusUsGvUdiWS8qioUhvaWStVpRFE0C9lZG1XlMWDYF6akLH9n9L9ZH29YV2GrwphjihrMjcgJWXOO%2B%2Fwmn1Nc6y39y0nQ6sCMvFa1wxNQjTCoAUHYg4fblRvpUkc1AaB%2BXrKgEPnAI2OEnzDJY5qpVjV2WM1Uk9woztmVU3treQC3pAsrcUtT7uYKC%2Fa8EE0%2BnvSv5d11aQDZJWh90q33rf%2BPO4q66iQdHF3DalPMEljnqYfSnZbY74jey34zbkP8OrgOuNF9Op2mJdSkDjhB%2FiNChaLX845N0j8DXSQGleJIV31OqL81GGo93CPeMXGef5K5694SBN%2BKUFhB7CkdcffIaEr%2Fl1Gi9ot%2FJXk9I7egx2fPTJUhItcXbpRTJNoZTeoAgdBvEWWGKOV%2BKguYTp5TFgtbzDDvMOoXh6ntmTmUbZ7CbI%2BMvgtzT7M89y4Pc6QEWJkEQHQb22OoxZQ42SDagwhvVrqAX0IuugMM4dmNLNgsnTMMTjgXXoIcjUbwIH2PeKpUGkB%2BPJ58HkstVK6FyqfopAB%2B3NF%2FmlhAHvvCnwegJwZLln69Zknc%2BjszROsxfJu9yhmgdm8HwTpRxqfmQfwFujmsV9qF4%2FXPKlO0eZX1p%2BBGRm6MRRk087bIaeGjU67rZDjhlZ3f330lvgxPOOZqKRbq7fUmhv9kZ7IJhhilHNMnRs9hQ3699aVV%2FTKJnCn85lvOZ4l8aLZxtLQKRDauY1SwSFtLJvaKXbohkkmyTETR%2FZRsmwi581SkQlxSERpdsSEakmSYzh9cbKQgNslIuKpUMumU7DxVHD2N4Gr%2BmhS9CkTnUab2vtNzFAP00lW0STZqPNtTP4m22uAW6KBaeAnktAnWKzDXF6tfjCYg467Mglpkxfjdj3aRrjlJTHvlG47wfnbZrfVHm%2BnTeu8oZ2XlDlHdt5WZV3VENca7meX8MN05vELe4hZi%2FmbnkpkzptbpkpHzuUbrFFdoPYlLmOm8Sm7FzX6shUCjcyOfMEpkZ44DdjDmrqQjOCl9O4bDXEfHF3jctaw9JadITdbbKhMpkFc2zDiAtznBpS%2FiSYQ%2FngDWoKEDkT9qcsmjfZGQA%2FpcQ9rW662FjQtBsxNpWzYUM5G9aE0wjuIBvO4TXIRxsAy%2Bstqj7l%2B6Nz9gdTSO8fnYOxvMM2%2B%2FZbdixFMdBa2kUcOAP2%2BDGu3dHqo191fcuOKW0ylLdkILbG58kTNgTWKuX4HHkOkVBXrYjpmOUVIThV6Pu4eNTIteYMmtxscshRhz12OUSmV%2Fp2KoGa3YSsdnAVza3e1VePDV95%2Fb%2BQs5CR5S3P%2B5ruEhPPTf56bHSnw3rGHT1G3Jpc9kMUF1t6glMCaRTXWdjrJV1QaRJDXACRbhbkWgI190SgT%2BS5%2ByIixxZKUWwM6G314YZ60IMKz3MaR%2BbUXNZcB75ZjCfpbBYk4ZbxRK4BNkuGw9FDp2Qix5ZMp7pdRBh7xCtkt6OkJXyzwG5hNwlaEm12rTY5vrl7acyj03CdDnZtcLtbXpUD9Eb2ZJiTSBuHuatGTjhP8shtGZmzAycBSpzWjZweAZCzDSlzduAkQNu4I6DDNljQB5F8p15lznZuEnSzBX0QyXfqVebswEmAbragD3X3wDmGL3N24CRAt1twACINnHqVOdu5SdDNFhyASEOnXmXODpwE6GYLDmE4OXTW7WXOdk4SlCx4bte%2BeiFvqjXK%2B8M21xjl2hzVFu8bShWo7TttV%2BaE83zkmpl2EsLhioB3zhO7cUQkOUMW53LcMw8qZzfxJPidxFM4G8SDKtJ31rNlzo7iCfC7iSdxIIo6rMG9A5z%2FcNqvzNlNQAl%2BJwEVTrP9BqDCwGm%2FMmdH8QT43cSTOBvEg%2Fp74GwEypwdxRPgdxNP4mxy7xDHs077lTm7CSjB7ySgwmm23xBUGDrtV%2BbsKJ4Av5t4EmeDeNC4DJ0tVJmzo3gC%2FG7iSRxwr9UIVPX2ph1hT%2Bu3UZp7LXKrOeBxMM95KO%2BBPeRdukbDwDC3sal%2BZ40PdZHBYPqKndiYZ1KbBfNW64IWjC5wIxoOTTUpjEU8at7vRNDbRnB9R4L%2BZoL1kVLNMNv9VTbdrcrLu%2Fipgm720Z%2BM9p7U1dBqzuiwjSDedhA02LlIEndG6%2Ba%2B3mBKa%2BVX3FBh6UCXSq8c6d65uKXCTvfPnVsnSKbXUXJxVnXEnN0lXK%2Bc0KXeDY3BccNyk8rIiyyIkpG54K1oiCVv7MwicH1Fu7a2J6SpFMJ%2Fu%2BxalosvUmNt%2B7KZwcprv0NaJjcuhxNb9vg0SvgbTsPYNCueInh5n%2Fcp3mNZjXQ7jC%2FndHmfAO7Qelu6KJ7jpW8veZ4HF3TPoUzu7cnbLyV7n17Qcq%2BgLEU5VzemZjxP42u80p3%2FRrej6qvP1SIhkMTdLnjn6lvxat53J%2FZ3EqdexmfpNTcvMdyjBcuMZN%2FriKtftQVU85QECtJCWUm5lkNT7aISaH0O%2BaB6l8tuFgO0J124aAuN23kooyeEAy1xnyB0q8wLeq2j0cYlu9KfYnID5X4XJcXxU9yNLGmHVYjqqmpbyfFS4I7YSX2uXS4MyFkE4xDT%2B64bhpEFEsG50XOxmdzYM9KoiPu%2B4C%2FmzknMg0zFTBlKxqGiKqbKaDzUrpS5LZ%2BUSLU7MneWR9x3qejkZeHDdUzLc%2B%2F76r6a%2Bv%2B9Xq8qm%2BeNWzCwEiqFwAty36b95aBPlzJsKPxGyR%2Bx99gPPD63qwAcQ3vDERvgONdkTPftQj308uwdVo6%2B792ZJc4RulkORszzjzuC93A7c8%2F3vLvzx7HDgO4X8prd8GX%2F94eDZiM7rAxtzPNlgToePr6rfsePG1kd4wrAEbD0af9LvfIbsT36NAk2pklaQC2ySEKq8F3i8esXyQ%2F4%2FYMCWqVpdHF4dldZH%2FsddgTuwOESPRlGw8bZx3E85B3Cw%2BFQy%2FXoH%2F7QUB8fjnB%2Bq4QZNvjCG2IAHhHxQ0W40WSP%2Fbp38iLI0AA8S4L4VRpjSB4%2BvbPuQkYRhn6zAH1dAFHJ1Auh6gpsBKPi4oTU420TQN3jTmi3gQxQ41MjW3tGkDaxe0SToMAdWyFVqTndnNFh43pnaCJbzqBH79ligutEgeyMswM2lo%2Blk6HyLpBY30wZd1hggapdM5pKKJ247Rj40oXB4FhoSsKcUcPyNsjAmIaIUiTVmytb1XwxhwaoamHoVmABRcGGD2ZmtaWc%2BsLlMYsRHXvqqK3ZfbWVul%2Fuhu7bVzmJTpy4zV27Qh5vN9HkoOverRt9RAzYQJoetw6Ftja8BPtB3rMuikhvHMHYHDOqZlIXHuz8ml%2BBMQptP2ntKB1UgeYeuuq8qb3ShIMz3%2F4MiOyc7d6mW%2Fjl1ki9C626K3afUoqcVAqJ7ry4d34kg4LSb21caW9np0vgRYAHptbJm5%2BECDnuq9K49HC4tQ0ERl0GiNStFTq53GrPJHA9MnaQ9tbumOmxpnXOTO%2FXemo02KxtmjcObdohZ%2BwJ%2FKRoO6ui7bj%2FyeF29gXD7Xtz76QWcMauyjuFnMSkLZXkRp2JM%2Bx0Xo2BZwI1hd64gdcOwbej3H%2BlAJSf2IhwB%2BSPL56JT2d8ZpWHOxwHX67a%2B2LmcCle%2B7bI5%2BsOA6T%2FFLqXX0r5TJWPaID2N64ydAu%2FtM6fXtX%2BRd2sfevmM5V%2B5G91NAxN0DRHQ%2Fvtr%2BHsL6v5Fnf%2FbWguXK7mwJu%2BrqLyNe4bqL178z20DOqzc1LCmkUX%2BRiMoeszwVMyNJTFGf6erlPem2JXvCWOGGXVhCeeffGP2%2By%2BuHihtBaOI4DDlnGDPomGswH2NJp33tYb77sT8O9CoI4%2B%2BFz%2BQ53ArcNj8rtVcXqDt8UseDJZddhldHGpveJlMlGxCHnNhzD8pCP24uwebWbHQ01L8hIdeoUU8ZFBSNWg2gLsZFshO3IXIUvNxxUhmkqmbwlWV%2F7QLRhTMVqezS1dh%2F3e8aPD4%2BNDKHz%2BUc8%2FPvIG2pQ8bQWucC3LHHu9R4feI9w45h%2F6vcNHvn90VCETolCfvtaDe%2BblYeA4vfBbkL9P31DE%2B5YeH%2BEKo3Zj7FRdUuDEjXXco6FlJ%2BEWIXPpPlHR2Lo9nc2NUkOo9q2sBGYYVfvgFWJ8i7J4j5xIrUpqBN3HaxtQ14Ev1X7IWrgvmsDm6Q1C4UoYauZtYugPnPw2s8Mf36b6KVLjdRBd5g%2BP7AKmIk6TRriQSLfJb4eWw%2BZBFhV4IF6g%2FRvzDafI7CeWW7rd2PBfJYBkLv4%2BeaLZUSQdnOpBs4GFAP%2FjFPR%2BdHRc50QFjr7GCdH8DfMPD82y6Iss4skw4p%2BANMcC0iq2g3OSGOGU3E2gw3PUQECZTGldij6hROEKf76FQsWigwNDLaKCZ3gj%2BvQrKUFvdQ2%2FTsOb8%2BKn58%2FEF%2FGSWsX68y8fXr97%2Bf1Pz5u7h3bvwuyADLEx2LHz4Gh4oA9SirBx1aiukqGNmDTkIX0bEKPIw1MLiUsu3SAGmpMXfn1tC7dvTtn%2FaQl%2B7A%2FBeAguTpzj5rtxx1nv33fmbk2w1sxBUn2OUOaspVosLgXD0vCRYhyXykXHqSMn0KACPBWf8zMqg%2FvGJgj8r%2Bl%2Bbdki1%2BbLW3ZIKc54UsxRStQSs90nuORLufWeoGhHCZ7gpi4GPjgPlAul58Hkdy7OsRiHjkeuU9DCPpJdW5uoUZdmKQUMs9CNUkeDkTkppTGGgiPeOsZBt5HjoKspQUc%2FIjOqH4OzocX5lZFxaqZRJ%2FzP%2FvjuraXVYDCyUobHO%2BlJx6lH%2BjluW1Z9dmzkOGZtw5unhkfOk8w1HnVrO8%2FtbrD6T3TU1HnIbwPWa3GA1X3gzMZTB3xG1jGjjZ7DEqqNgBxbPppcE1Sh5Agh%2FRjNyHGwpxm%2BPM1SQ6sO29jY1ez8qHYpx0b1bzfaYnhcs0XDKRnXMRuT8YmLjrF33t50vwN%2BNqfvM2%2Ff%2FGfsqNP30l33gEa77SYffgHyYTN5sGKd%2FKatpoJyQJTfB71lD7JxoXOlHtb4cN7A7qq8sYeZn2BpgQnoRIUKpF5139HJbkVBl1K71Y7I1rQaCV7W1kJcRA9REHTqrcr%2BJYugQaWZpJF1j561ikqITWo4yFX6W4T127nKR6BQvXRo3%2BAZYhr7Hncvao0Nxq3edRD27VWl%2B7se3nliFEqx1prxCY%2Bu9WJfw3Ws6Wo89Gbkux4tmDSzMRoYB4Gm5WOFT70FWqqR2HYnrK6OpofVTQyjfB5Ar0LQw%2B7Q2SIv0pl4N9cfGxZfG9gbahr63U0EaxWqcUHOEMPeoSCPfZu7FOTmBSnNDhsUnBOOzW6p0OWKnXK21ur3qlW7DUCrXYDGuwAFuwDhVQ%2B7wGW7AK13gsJbH3ZiWV4zcXKXIFKftjcCSFbHW0JHHUH%2FG4ydxXwXk%2BEdKLvAxeq%2Bgl1csAsg3SiyE%2Bed4mgnKHk7yK76%2FoUi6U%2B4oB7EZ%2BUNXH%2FFePoqSuK%2FXba3RUnIl%2FX9%2FjwU%2BmOH8eE%2B%2B%2FcPH169e%2F38wwe2%2F1DeTD5vVVeSC9W12%2BAw8YKrvU84X994YCAp95PlPRLml6llSrrqlj0xJ57lboTENLciRLfKmVQkgoKQW%2FDUNHIVGUEYPlP73LacdYhwPk1T0%2BKIQGnMcaK49euPYIXStiP2Pz9G4S382j07Y8qiDvBzMOO3v2oMDF%2FRVsEo7JgrdEI6JVvbUlfsj%2FlaGkOIfXWlRYUCeuuqJcF1dIHD5R5EumMnjaQCxUlq%2FLGc9dK1LoPB3EDa3olHpTywaWCgm9%2FFI0wnixmetaiTf%2FbLS6y%2BMC0NQh5WurhO5VDU49dlRGyYspcezcutkgqjvMixMTLUvKTbVuVupNuqupBWKbeqNootJq7ktRWgfZhmLyCOvGH%2FqDoEX2Qre478vZDgvEHdTN%2BZCY5RbLR9U2Km067om%2Bc6t5gARsU0Uchxw7s1pY8FhtJFbi%2BBcO8w8TyTB4PcjZhRwDcVXpzx%2FE1ZW1sMpxM2VmF0Xf5b4ULVX%2FXfa1qWy3BNRB9b%2B3nrBZ0g9%2BTF9qz1ur1n2N8hiZw9M0SxV7TJnQK3Sa1aj8DdoAtHC0xzc7GRs2m7QzN8fQtZM2y5%2Fchs3wSMVhPKjsC3eQE9iCdW6IjpTmyBVT0zgZF8wZ%2FHfCbGegSwJ5gYSD3qLgmXsb2Mx0EB%2FRz8UEeeZt15GiV45pdWN3nYnYXssnvE5nF3wOZZ12cF9LO62OETTxdZsOoe9vtsCoLko3RRxFHCu0macJqc6Hosn40IdJnjYyw%2Btww0xxcC2%2Bv39xxiRvCSvQVEFFNODO%2FZRSgSfVDuhIlyEWhnquUBqGkAoV5C4KyN%2BP6MPH8L%2FcEpnlVIJvpnDehTDGWHV2yUl%2F4dCYod8To2X8UFfHqKuPpPT8nMV7x10Hi38pfm68p8XStos775TQbyqzSP5HHO99V6o%2BhNAEByoSK%2BDhDlZ0FMM9kOM0azxXOKbUdW%2FjSO0xsevk2lGwyQSZrxnvASlIuqKgyyixzq0uRarw0NNqJkUbDOoQjhehoi0TlixNNKUnk4%2BBJKacwlO%2ByVt0SP3LhN%2Fb6uqtFxrZnoZ7r%2BWBLp2esFverOtHaHbYFVd6q1qwVey%2BTaN5ut5uxWb3rlB3c2xrPxpR%2BC7NpF4QmeeTQ%2FTmEUFA25FApbK1FMXuGdx1Y0KAgVjC%2Bh%2F1SH0M6g4unR6ugpvOUtvRCap07Jc3o2Hj49x8%2FnSovrA1bKa9dkNSzMWJ3aaSOxesdC6KDc%2Bst0mtN89a5hAB6xAu59bdtlFV%2B7ktci0sFA23%2BA5hRfOBjnLVOJNu4n6Xl4BWoJYUsiYewegOF3w9ql2eyqSmz8sumfNILTtpWuKbGhVVWJGNLlPAMkiLIs4XEvSnvTKOOtPVFX4OC9AmVsz1hG2BupGKXNcHlp%2FfqKQ7tjkCnXHDaTKMFoEaeicNthe1Br7jXMqBvhbSj7qYVMp6uVi9ri3DZjous7yNpe1rGXI9yHwQ0tjbC6k0vt2AFrUlGp21N03%2BtdjzNxsBsce003dNIJa%2FHWcoyNZOtiN4htszaf6Bd1czkuycV%2BDHMALWOlrNM3dncMr8sONDje5qP38x30jEIrzU9dW32I%2B7QoYOCJ3gDqUMNoKM298Lv1q3fqWTuBtA3hbjC5ffYIzwnTT7%2BnbVWTQPVRvTbRVx51klMGjd2OGlmnGU2zy5AU31I5M3vB6gsoHearOyjKvk8Dgh6bdYdD82l3tPEqh70SYs8cEzd0yQHuxIJydPIt%2FAaMvFjBuHscTH6%2FoDEKsvnX8Hg6DY%2FLz7VV49n7d9ZpN42%2Brj57J3a9Y%2BI4psVirLicE0nOHrgWUG4zNR0bEGDmd37MGSKI7ISm5ZuGpmF0XY1LBbSw249vX%2F4EeL9%2Bm8%2BCOH6CLRenweikHNt%2F%2B1Dk%2FWrhB3OoS8KzyygOW4atbEbG4Hca8yXDn%2B4kjdlFMO%2BuYIQL49KbywjcrY2B88sgTG%2FgSbjmoi%2FPKGK93Sqpt%2FXMFHBzEHtlXk0zwb54XDYTDY2JMfU2%2FYd79%2B7l%2FwQ%2FrxdQJ%2BID7sp781%2Fh4RfarGpB%2FDM8PIvyeRys3vwjPPN89v8BSeiR3Q%3D%3D No evaluation is required (don’t run it ;)). Simply hide the input cell, leaving only the output visible through the properties (click on the top-right corner of the group). Here is a notebook with all examples [13] (some of which work in a browser as well without a kernel). What if using my own laptop is not an option? The WLJS Notebook can be exported to HTML, preserving even some dynamic behavior [16]. This is achieved through a quite sophisticated algorithm that tracks all event chains occurring on the JS and WL sides, attempting to approximate them using a simple state machine. The result is a standard HTML file containing all cells, slides, and the data of these state machines.However, due to the variety of values received from the IMU, the exporter cannot automatically capture this in a JS state machine. Consequently, rotations and the joystick input will not be preserved. Nonetheless, everything else will function as expected. … like a fish needs a bicycle Please refer to the final sentence of that beautiful comics by Zach Weinersmith. If not for the urgent need to showcase a rotating crystalline structure, this post would not exist. Thank you for your attention, and to those who read 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. Right Joy-con Controller as a Remote, Hackster: https://www.hackster.io/leo49/right-joy-con-controller-as-a-presentation-remote-5810e4 (2024)[4] — Jen Tong, Nintendo Switch Joy-Con Presentation Remote, 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.community/[7] — Motion Canvas, Homepage: https://motioncanvas.io/[8] — RISE, Github Page: https://github.com/damianavila/RISE[9] — WLJS Notebook, Homepage: https://wljs.io/[10] — Vasin K. Reinventing dynamic and portable 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 K. Dynamic Presentation, or How to Code a Slide with Markdown and WL, Blog post: https://wljs.io/blog/2025/03/02/ultimate-ppt (2025)[12] — Joy-Con WebHID, Github Page: https://github.com/tomayac/joy-con-webhid[13] — JoyCon Presenter Tool, Online notebook: https://jerryi.github.io/wljs-demo/PresenterJoyCon.html[14] — Faraday Effect, Online notebook: 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 page: https://wljs.io/frontend/Exporting/Dynamic%20HTML/ All links provided were visited on March 2025. The post How to Use Gyroscope in Presentations, or Why Take a JoyCon to DPG2025 appeared first on Towards Data Science.
0 Комментарии 0 Поделились 71 Просмотры