unityController - User Interactions

This section contains all the methods used for the User interactions in the index.html web page. All the key-press functions and click functions are then available in this section.

onKeyDown

onKeyDown(event: KeyboardEvent)

The onKeyDown method triggers a specific function depending on which key is pressed while the window is selected.

changeOffset
Changes the video delay offset by changing the value.
toggleGenvidOverlay
Displays or hides the Genvid overlay.
client.videoPlayer.isPaused
Returns a boolean indicating if the video is paused.
client.videoPlayer.play
Plays the video.
client.videoPlayer.pause
Pauses the video.
client.videoPlayer.getMuted
Returns a boolean indicating if the video sound is muted.
client.videoPlayer.setMuted
Sets the sound to mute or unmute depending of the argument.
client.videoPlayer.setVolume
Sets the video volume to the sent argument.
client.videoPlayer.getVolume
Gets the current video volume.
onHelpActivation
Displays or hides the help overlay.
        onKeyDown(event: KeyboardEvent) {
            // Seems to be the most cross-platform way to determine keys:
            // Ref: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code

            let code = event.code || this.getKeyCode(event);
            if (code === "Equal" || code === "NumpadAdd") {
                this.changeOffset(+1, event);
            } else if (code === "Minus" || code === "NumpadSubtract") {
                this.changeOffset(-1, event);
            } else if (code === "NumpadMultiply") {
                this.changeOffset(0, event);
            } else if (code === "KeyG") {
                this.toggleGenvidOverlay();
            }
            else if (code === "Space") {
                if (this.client.videoPlayer.isPaused()) {
                    this.client.videoPlayer.play();
                } else {
                    this.client.videoPlayer.pause();
                }
                event.preventDefault();
            }
            else if (code === "KeyM") {
                this.onMute();
            }
            else if (code === "KeyZ") {
                this.promptOverlay = <HTMLDivElement>document.querySelector("#prompt_overlay");
                this.client.videoPlayer.setVolume(this.client.videoPlayer.getVolume() - 20);
                this.promptOverlay.style.visibility = "visible";
                this.timeVisiblePrompt = 0;
                this.volumeChange = 2;
            }
            else if (code === "KeyX") {
                this.promptOverlay = <HTMLDivElement>document.querySelector("#prompt_overlay");
                this.client.videoPlayer.setVolume(this.client.videoPlayer.getVolume() + 20);
                this.promptOverlay.style.visibility = "visible";
                this.timeVisiblePrompt = 0;
                this.volumeChange = 1;
            }
            else if (code === "KeyH") {
                this.onHelpActivation();
            }
        }

getKeyCode

getKeyCode(event: KeyboardEvent)

The getKeyCode method gets a proper keyCode on different browsers.

        // Compatibility code for browsers (Safari) not having KeyboardEvent.code.
        getKeyCode(event: KeyboardEvent) {
            if (event.keyCode) {
                console.log(event.keyCode, event.code);
                if (65 <= event.keyCode && event.keyCode <= 90) {
                    return "Key" + String.fromCharCode(event.keyCode);
                } else {
                    switch (event.keyCode) {
                        case 13: return "Enter";
                        case 106: return "NumpadMultiply";
                        case 107: return "NumpadAdd";
                        case 109: return "NumpadSubtract";
                        case 110: return "NumpadDecimal";
                        case 111: return "NumpadDivide";
                        case 187: return "Equal";
                        case 188: return "Comma";
                        case 189: return "Minus";
                        case 190: return "Period";
                        case 222: return "Backquote";
                    }
                }
            }
        }

onResize

onResize()

The onResize method resizes the various overlays used in the web page for index.html. The overlays affected by this function are:

  • videoOverlay,
  • canvas3d,
  • promptOverlay (used of sound modification), and
  • the animated divs with the name of each object.

We get the size value of the video stream displayed in the index.html page and we adjust the other overlays according to this size. In this sample, resize is essential because we need the proper overlay size to perform the object selection properly. Also, we need it to display the WebGL circles at their appropriate locations to follow the objects in the video.

        // Allow to adjust the various overlay when resizing the windows - needed to see the PromptOverlay and Name moving div
        onResize() {
            let refElement = this.client.videoElem; // The element to match.
            let refElementSize = refElement ? genvidMath.vec2(refElement.clientWidth, refElement.clientHeight) : genvidMath.vec2(1280, 720);
            let refElementRatio = refElementSize.x / refElementSize.y;
            let videoRatio = this.client.videoAspectRatio;
            let pos: genvidMath.IVec2;
            let size: genvidMath.IVec2;
            if (videoRatio >= refElementRatio) {
                // Top+Bottom bars, fill width fully, shrink height.
                let ey = refElementSize.x / videoRatio;
                let dy = refElementSize.y - ey;
                // Center vertically.
                let y = dy * 0.5;
                pos = genvidMath.vec2(0, Math.round(y));
                size = genvidMath.vec2(refElementSize.x, Math.round(ey));
            } else {
                // Left+Right bars, fill height fully, shrink width.
                let ex = refElementSize.y * videoRatio;
                let dx = refElementSize.x - ex;
                // Center horizontally.
                let x = dx * 0.5;
                pos = genvidMath.vec2(Math.round(x), 0);
                size = genvidMath.vec2(Math.round(ex), refElementSize.y);
            }
            let style = this.videoOverlay.style;
            let cur_pos = genvidMath.vec2(parseInt(style.left), parseInt(style.top));
            let cur_size = genvidMath.vec2(parseInt(style.width), parseInt(style.height));

            if (!genvidMath.equal2D(cur_size, size, 0.9) || !genvidMath.equal2D(cur_pos, pos, 0.9)) {
                this.videoOverlay.style.left = pos.x + "px";
                this.videoOverlay.style.width = size.x + "px";
                this.videoOverlay.style.top = pos.y + "px";
                this.videoOverlay.style.height = size.y + "px";
                this.canvas3d.width = size.x;
                this.canvas3d.height = size.y;
                this.genvidWebGL.setViewport(0, 0, size.x, size.y); // Render in the whole area.
            }

            if (this.lastSelection != null) {
                this.onSelect(this.lastSelection, true);
            }
            else {
                for (let nameSelect of this.cubeDiv) {
                    nameSelect.style.backgroundColor = "#181818";
                }
            }
        }

toggleFullScreen

toggleFullScreen()

The toggleFullScreen method activates or deactivates fullscreen on the video. We use the checkFullScreen function to verify fullscreen status and adjust to the proper condition.

If we’re already in fullscreen, we cancel fullscreen according to the proper web browser function and we also adjust the fullscreen button icon.

If we aren’t in fullscreen, we proceed to get the video_area element. Afterwards, we use the proper web browser function to activate fullscreen and update the fullscreen button icon.

        toggleFullScreen() {
            let doc = <any>document;
            if (this.checkFullScreen()) {
                if (doc.exitFullscreen) {
                    doc.exitFullscreen();
                } else if (doc.mozCancelFullScreen) {
                    doc.mozCancelFullScreen();
                } else if (doc.webkitExitFullscreen) {
                    doc.webkitExitFullscreen();
                } else if (doc.msExitFullscreen) {
                    doc.msExitFullscreen();
                }
                this.fullScreenElement.classList.remove("fa-compress");
                this.fullScreenElement.classList.add("fa-expand");
            } else {
                let element = <any>document.querySelector("#video_area");
                if (element.requestFullscreen) {
                    element.requestFullscreen();
                } else if (element.mozRequestFullScreen) {
                    element.mozRequestFullScreen();
                } else if (element.webkitRequestFullscreen) {
                    element.webkitRequestFullscreen();
                } else if (element.msRequestFullscreen) {
                    element.msRequestFullscreen();
                }
                this.fullScreenElement.classList.remove("fa-expand");
                this.fullScreenElement.classList.add("fa-compress");
            }
        }

onMute

onMute()

The onMute method is called whenever the video player is muted/unmuted.

Whenever this function is invoked, we can retrieve the mute state by calling getMuted(), and adjust the visibility of the audio, volume up icons, and the overlay message appropriately.

        // Depending on the status, mute or unmute the video player audio
        onMute() {
            const muteIcon = document.querySelector("#mute-button i");
            this.promptOverlay = <HTMLDivElement>document.querySelector("#prompt_overlay");
            if (this.client.videoPlayer.getMuted()) {
                muteIcon.classList.remove("fa-volume-off");
                muteIcon.classList.add("fa-volume-up");
                this.client.videoPlayer.setMuted(false);
                this.promptOverlay.style.visibility = "visible";
                this.promptOverlay.textContent = "Volume is unmuted";
                this.timeVisiblePrompt = 0;
            } else {
                muteIcon.classList.remove("fa-volume-up");
                muteIcon.classList.add("fa-volume-off");
                this.client.videoPlayer.setMuted(true);
                this.promptOverlay.style.visibility = "visible";
                this.promptOverlay.textContent = "Volume is muted";
                this.timeVisiblePrompt = 0;
            }
        }

showOverlay

showOverlay()

The showOverlay method displays the Genvid overlay.

        // Change the style to display the Genvid overlay
        showOverlay() {
            this.genvidOverlay.style.display = "block";
        }

hideOverlay

hideOverlay()

The hideOverlay method hides the Genvid overlay.

        // Change the style to hide the Genvid overlay
        hideOverlay() {
            this.genvidOverlay.style.display = "none";
        }

clickCube

clickCube(event: MouseEvent)

The clickCube function returns true if the pickCube method determines that a cube was selected by clicking the WebGL overlay. If not, we handle the event propagation in this function.

        // Method used when clicking on the WebGL overlay
        clickCube(event: MouseEvent) {
            let best = this.pickCube(event);
            if (best) {
                return true;
            } else {
                // Continue propagation.
                return false;
            }
        }

pickCube

pickCube(event: MouseEvent)

The pickCube method determines if a cube is selected by clicking on the WebGL overlay.

  1. We verify that the game data is valid, otherwise we are not doing any verification.

  2. We get the position of the click in the window.

  3. Then we convert this coordinate into the projected space.

                if (this.lastGameData == null) {
                    return false;
                }
    
                // [0, 1] coordinates in the window.
                let rect = this.canvas3d.getBoundingClientRect();
                let x = event.pageX - rect.left; // More robust to recompute from pageX/pageY.
                let y = event.pageY - rect.top;
                let p01 = genvidMath.vec2(x / rect.width, y / rect.height);
                // [-1, 1] coordinates in projection space.
                let p = genvidMath.mad2D(p01, genvidMath.vec2(2, -2), genvidMath.vec2(-1, 1));
                let mat = this.convertMatrix(this.lastGameData.MatProjView);
                let best = null;
                let bestDist = Infinity;
    
  4. We start a loop to verify the coordinates of each object.

  5. In this loop, we get the object coordinates and apply a radius to have a zone to search.

  6. Finally, we search to discover if the coordinates are within the object zone and keep it as the best result based on the distance. (In the event that two objects are close, the one with nearer to the zone is selected).

                for (let cube of this.lastGameData.cubes) {
                    let pos3D = genvidMath.vec3(cube.mat.e03, cube.mat.e13, cube.mat.e23);
                    let radius = 1.0;
                    let pos2D_rad2D = this.projectWithRadius(mat, pos3D, radius);
                    let pos2D = pos2D_rad2D[0];
                    let rad2D = pos2D_rad2D[1];
    
                    let pos2D_to_p = genvidMath.sub2D(p, pos2D);
                    let d = genvidMath.length2D(pos2D_to_p);
                    if (d < rad2D && d < bestDist) {
                        best = cube.name;
                        bestDist = d;
                    }
                }
    
                if (event.ctrlKey || event.metaKey) {
                    this.toggleSelection(best);
                    this.onSelect(best, false);
                } else if (best != null) {
                    this.setSelection(best);
                    this.onSelect(best, false);
                }
                return best;
    

toggleSelection

toggleSelection(name)

The toggleSelection method adds the name of the object sent as a selection.

        // Select a cube via the circle
        toggleSelection(name) {
            if (!this.removeFromArray(this.selection, name)) {
                this.addToSelection(name);
            }
        }

setSelection

setSelection(name)

The setSelection method resets the selection list to only the selected object.

        // Set the selection to a specific cube
        setSelection(name) {
            this.selection = [];
            if (name) {
                this.selection.push(name);
                this.client.sendEventObject({ select: name });
            }
        }

isSelected

isSelected(name)

The isSelected method tells us if an object is selected or not.

        // Verify if the cube is selected
        isSelected(name) {
            return this.selection.indexOf(name) !== -1;
        }

addToSelection

addToSelection(name)

The addToSelection method adds an object as a selection if the object was not previously selected.

        // Add the cube selected to the selection list
        addToSelection(name) {
            if (name && !this.isSelected(name)) {
                this.selection.push(name);
            }
        }

changeOffset

changeOffset(direction, event)

The changeOffset method adds, reduces, or resets the delay offset of the video. If the argument direction is equal to 0, the delay offset of the video is reset. Otherwise, it is changed according to the value sent.

        // Function that changed the delay offset depending of the key pressed
        private changeOffset(direction, event) {
            if (direction !== 0) {
                let delayDelta = 100 * direction;
                if (event.altKey) delayDelta *= 10;
                if (event.shiftKey) delayDelta /= 2;
                let newDelayOffset = this.client.delayOffset + delayDelta;
                this.client.delayOffset = newDelayOffset;
            } else {
                this.client.delayOffset = 0;
            }
        }

toggleGenvidOverlay

toggleGenvidOverlay()

The toggleGenvidOverlay method displays or hides the Genvid overlay. The Genvid overlay displays various stats about the stream.

Raw Video
Time when the raw video is received.
Est. Video
Estimated time when the video is received.
Last Compose
Last compose time.
Est. Compose
Estimated compose time.
Stream
Time since the streaming has started.
Latency
Latency occurring when watching the stream.
DelayOffset
Current delay offset for the video.
        // Display or remove the Genvid Overlay
        private toggleGenvidOverlay() {
            if (this.genvidOverlay.getAttribute("data-isHidden")) {
                this.genvidOverlay.setAttribute("data-isHidden", "");
                this.genvidOverlay.style.visibility = "visible";
                this.genvidOverlayButton.classList.remove("disabled");
            } else {
                this.genvidOverlay.setAttribute("data-isHidden", "true");
                this.genvidOverlay.style.visibility = "hidden";
                this.genvidOverlayButton.classList.add("disabled");
            }
        }

onHelpActivation

onHelpActivation()

The onHelpActivation method displays or hides the help overlay.

        // Display or remove the help overlay
        private onHelpActivation() {
            if (this.help_overlay.style.visibility === "visible") {
                this.help_overlay.style.visibility = "hidden";
            }
            else {
                this.help_overlay.style.visibility = "visible";
            }
        }

onCheer

onCheer(cubeName: string)

The onCheer method sends and event to the client when a button is clicked to cheer a player. The event contains the name of the object.

        // Upon cheering a player
        private onCheer(cubeName: string) {
            this.client.sendEventObject({ cheer: cubeName });
        }

onReset

onReset(cubeName: string)

The onReset method sends an event when a button is clicked to reset a player. The event contains the name of the object.

        // Reset the position of the cube
        private onReset(cubeName: string) {
            this.client.sendEventObject({ reset: cubeName });
        }

onColorChange

onColorChange(cube: string, color: string)

The onColorChange method sends an event when a button is clicked on to change the color of a player. The event contains the name of the object and the color.

        // Method used when clicking on a color to change the color of a cube
        private onColorChange(cube: string, color: string) {
            let evt = {
                key: ["changeColor", cube],
                value: color,
            };
            this.client.sendEvent([evt]);
        }

onSelect

onSelect(cubeName: string, selectionInterface: boolean)

The onSelect method changes the UI when the user clicks the player table located under the video stream. We reset the color for each table and apply the color to the selected table only, then we select the WebGL circle.

        // Select the cube from the interface
        private onSelect(cubeName: string, selectionInterface: boolean) {

            for (let nameSelect of this.cubeDiv) {
                nameSelect.style.backgroundColor = "#181818";
            }

            let cubeDiv = <HTMLDivElement>document.querySelector(".cube" + cubeName);
            this.lastSelection = cubeName;
            cubeDiv.style.backgroundColor = "#32324e";

            if (selectionInterface) {
                this.setSelection(cubeName);
            }
        }