From ba16106c05449462b290f08683f29780367233de Mon Sep 17 00:00:00 2001 From: jahugg <jan@huggenberg.ch> Date: Tue, 28 Feb 2023 18:14:56 +0100 Subject: [PATCH] first working motion prototype --- src/main.js | 167 ++++++++++++++++++++++++++++++++++++----- src/modules/Vector.js | 60 +++++++++++++++ src/modules/helpers.js | 5 ++ src/styles/config.css | 12 ++- src/styles/main.css | 16 +++- src/styles/main.less | 17 ++++- 6 files changed, 252 insertions(+), 25 deletions(-) create mode 100644 src/modules/Vector.js diff --git a/src/main.js b/src/main.js index 917fdae..efb886b 100644 --- a/src/main.js +++ b/src/main.js @@ -1,10 +1,12 @@ import * as helpers from "./modules/helpers.js"; +import { Point, Vector } from "./modules/Vector.js"; const config = { controls: { mouse: true, button: true, - voice: true, + voice: false, + motion: true, }, }; @@ -100,6 +102,9 @@ const navigation = { }, }; +// Create WebSocket connection. +const socket = new WebSocket(`wss://192.168.227.52:443`); + // init application function init() { // build navigation @@ -129,6 +134,19 @@ function init() { if (config.controls.mouse) addMouseControls(); if (config.controls.button) addButtonControls(); if (config.controls.voice) addVoiceControls(); + if (config.controls.motion && "RelativeOrientationSensor" in window) + addMotionControls(); + + // listen for WebSocket messages + socket.addEventListener("message", (event) => { + let message = JSON.parse(event.data); + console.log(message); + + // perform action without broadcasting message again + if (message.type === "select") navigateToPath(message.value, true, false); + else if (message.type === "focus") + focusNavigationItem(message.value, false); + }); } // update navigation @@ -236,7 +254,7 @@ function buildPageContents(object) { // =================== // navigate to path -function navigateToPath(path, pushState = true) { +function navigateToPath(path, pushState = true, broadcast = true) { // find object with path let navObj = helpers.findNestedObject(navigation, "path", path); if (!navObj) navObj = navigation.home; // fallback to home @@ -251,6 +269,10 @@ function navigateToPath(path, pushState = true) { // Push state to browser history if (pushState) history.pushState(stateObj, stateObj.pageTitle, stateObj.pageUrl); + + // broadcast selection event to other clients + if (broadcast && socket.readyState === WebSocket.OPEN) + socket.send(JSON.stringify({ type: "select", value: path })); } // navigate to currently selected item @@ -265,7 +287,7 @@ function navigateToSelected() { } // focus specific navigation item -function focusNavigationItem(index) { +function focusNavigationItem(index, broadcast = true) { // unselect all items const navigationList = document.querySelectorAll("#navigation li"); for (let item of navigationList) delete item.dataset.selected; @@ -275,6 +297,10 @@ function focusNavigationItem(index) { `#navigation > ul > li:nth-child(${index})` ); navigationItem.dataset.selected = ""; + + // broadcast focus event to other clients + if (broadcast && socket.readyState === WebSocket.OPEN) + socket.send(JSON.stringify({ type: "focus", value: index })); } // focus next navigation item @@ -308,6 +334,30 @@ function focusPreviousNavigationItem() { } } +// focus closest item to point +function focusClosestItem( + focusPoint = new Point(window.innerWidth / 2, window.innerHeight / 2) +) { + let lastDistance = 10000; + let index = 1; + let closestIndex = 0; + const itemList = document.querySelectorAll("#navigation ul li"); + for (let item of itemList) { + let rect = item.getBoundingClientRect(); + let itemCenterPoint = new Point( + rect.left + rect.width / 2, + rect.top + rect.height / 2 + ); + let vector = new Vector(focusPoint, itemCenterPoint); + if (vector.magnitude <= lastDistance) { + closestIndex = index; + lastDistance = vector.magnitude; + } + index++; + } + focusNavigationItem(closestIndex); +} + // moving up navigation tree function moveUpNavigationLevel() { const path = window.location.pathname; @@ -369,10 +419,6 @@ function addButtonControls() { // add two button controls for menu document.addEventListener("keydown", onKeyDown); - window.addEventListener("volumechange", function (event) { - printLogMsg("Volume up button was pressed."); - }); - // on keydown event function onKeyDown(event) { // prevent defaults @@ -398,14 +444,6 @@ function addButtonControls() { else if (event.code === "VolumeUp") printLogMsg("yes"); } - console.log(navigator.mediaSession); - - // listening for hardware buttons - navigator.mediaSession.setActionHandler("previoustrack", () => { - const logEl = document.getElementById("log"); - logEl.innerHTML += "volumeup"; - }); - // const toggleButtonControlsEl = document.getElementById( // "toggle-button-controls" // ); @@ -491,7 +529,7 @@ function addVoiceControls() { setTimeout(() => { recognition.start(); printLogMsg("Voice > listening"); - }, "500"); + }, "1000"); }; recognition.onnomatch = function (event) { @@ -513,7 +551,10 @@ function addVoiceControls() { // add custom commands availableCommands.push({ command: "exit", path: "/" }); - availableCommands.push({ command: "emergency call", path: "/menu/call/emergency" }); + availableCommands.push({ + command: "emergency call", + path: "/menu/call/emergency", + }); // form commands array for grammarlist commands = availableCommands.map((x) => x.command); @@ -521,9 +562,101 @@ function addVoiceControls() { } } +// add motion controls +// =================== +function addMotionControls() { + let pointOfReference; + let vectorToReference; + let screenCenter; + + // create canvas element for motion visualization + const canvasEl = document.createElement("canvas"); + canvasEl.id = "canvas"; + canvasEl.width = window.innerWidth; + canvasEl.height = window.innerHeight; + document.getElementById("app").appendChild(canvasEl); + + //available headset sensors: accelerometer, gyroscope, relativeOrientation, linearAcceleration, GravitySensor + //unavailable headset sensors: magnetometer (this results in horizontal drifting) + const options = { frequency: 20, referenceFrame: "device" }; + const sensor = new RelativeOrientationSensor(options); + sensor.addEventListener("reading", onChangeOrientation); + + // get sensor permission via Permission API + Promise.all([ + navigator.permissions.query({ name: "accelerometer" }), + navigator.permissions.query({ name: "gyroscope" }), + ]).then((results) => { + if (results.every((result) => result.state === "granted")) sensor.start(); + else printLogMsg("No permissions to use RelativeOrientationSensor."); + }); + + // handle motion sensor event + function onChangeOrientation(event) { + let quaternion = event.target.quaternion; + if (pointOfReference === undefined) setPointOfReference(quaternion); + + let mapped = { + x: quaternion[0], + y: quaternion[1], + z: quaternion[2], + w: quaternion[3], + }; + + let targetPoint = new Point(mapped.x, mapped.y); + let motionVector = new Vector(pointOfReference, targetPoint); + motionVector = motionVector.subtract(vectorToReference); + motionVector = motionVector.scale(200); + motionVector = motionVector.add( + new Vector( + new Point(), + new Point(canvasEl.width / 2, canvasEl.height / 2) + ) + ); + + // focus closest item + // ADD THRESHOLD BEFORE TRIGGERING! + focusClosestItem(motionVector); + + drawMotion(motionVector); + // printLogMsg(`${motionVector.magnitude.toFixed(2)}`); + } + + // set new point of reference + function setPointOfReference(quaternion) { + pointOfReference = new Point(quaternion[0], quaternion[1]); + vectorToReference = new Vector(new Point(), pointOfReference); + } + + function drawMotion(vector) { + if (canvasEl.getContext) { + const ctx = canvasEl.getContext("2d"); + const pointSize = 10; + + ctx.clearRect(0, 0, canvasEl.width, canvasEl.height); + + ctx.strokeStyle = "#FFFFFF"; + ctx.fillStyle = "#FFFFFF"; + ctx.lineWidth = 2; + + ctx.beginPath(); + ctx.moveTo(canvasEl.width / 2, canvasEl.height / 2); + ctx.lineTo(vector.x, vector.y); + ctx.stroke(); + ctx.fillRect( + vector.x - pointSize / 2, + vector.y - pointSize / 2, + pointSize, + pointSize + ); + } + } +} + // printing messages on screen for debugging function printLogMsg(message) { const logEl = document.getElementById("log"); + if (logEl.scrollHeight >= 10000) logEl.innerHTML = ""; logEl.innerHTML += `${message}<br>`; logEl.scrollTop = logEl.scrollHeight; console.log(message); diff --git a/src/modules/Vector.js b/src/modules/Vector.js new file mode 100644 index 0000000..9ba9b00 --- /dev/null +++ b/src/modules/Vector.js @@ -0,0 +1,60 @@ +// point class +export class Point { + constructor(x = 0, y = 0) { + this.x = x; + this.y = y; + } + + // set new position + setPosition(x, y) { + this.x = x; + this.y = y; + } +} + +// vector class +export class Vector { + constructor(pointA = new Point(), pointB = new Point()) { + this.x = pointB.x - pointA.x; + this.y = pointB.y - pointA.y; + } + + // get magnitude + get magnitude() { + return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2)); + } + + // get direction + get direction() { + return Math.atan2(this.y, this.x); + } + + // get unit direction + get unit() { + const magnitude = this.magnitude; + if (magnitude === 0) + throw new Error("cannot get unit vector of zero vector"); + return new Vector( + new Point(), + new Point(this.x / magnitude, this.y / magnitude) + ); + } + + subtract(otherVector) { + return new Vector( + new Point(), + new Point(this.x - otherVector.x, this.y - otherVector.y) + ); + } + + add(otherVector) { + return new Vector( + new Point(), + new Point(this.x + otherVector.x, this.y + otherVector.y) + ); + } + + scale(factor) { + return new Vector(new Point(), new Point(this.x * factor, this.y * factor)); + } +} diff --git a/src/modules/helpers.js b/src/modules/helpers.js index eed7762..52b23ae 100644 --- a/src/modules/helpers.js +++ b/src/modules/helpers.js @@ -25,4 +25,9 @@ export function findNestedObject(obj, key, value) { } } } +} + +// clamp number +export function clamp(num, min, max) { + return Math.min(Math.max(num, min), max); } \ No newline at end of file diff --git a/src/styles/config.css b/src/styles/config.css index 49f3f8f..ae105d3 100644 --- a/src/styles/config.css +++ b/src/styles/config.css @@ -2,16 +2,20 @@ html { box-sizing: border-box; } +html, +body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; +} + *, *:after, *:before { box-sizing: inherit; } -body { - margin: 0; -} - ul { margin: 0; padding: 0; diff --git a/src/styles/main.css b/src/styles/main.css index 28043e6..3bfa939 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -1,8 +1,10 @@ @import "config.css"; #app { + position: relative; color: var(--color-white); background: black; height: 100vh; + width: 100vw; font: 300 5vw/1.2em Roboto, sans-serif; display: grid; grid-template-columns: 1fr; @@ -10,6 +12,7 @@ grid-template-areas: "main"; justify-items: center; align-items: center; + padding: 100px; } @keyframes slideIn { from { @@ -80,6 +83,15 @@ opacity: 1; background-position: center center; } +#canvas { + display: block; + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + pointer-events: none; +} aside { display: flex; flex-direction: column; @@ -88,9 +100,9 @@ aside { right: 0; } #log { - color: hsl(0, 100%, 30%); + color: hsl(0, 100%, 80%); position: absolute; - font: normal 0.6em/1.2em sans-serif; + font: normal 1em/1.2em sans-serif; top: 0; left: 0; max-height: 20vh; diff --git a/src/styles/main.less b/src/styles/main.less index 9e2e101..c0e7cdf 100644 --- a/src/styles/main.less +++ b/src/styles/main.less @@ -1,9 +1,11 @@ @import "config.css"; #app { + position: relative; color: var(--color-white); background: black; height: 100vh; + width: 100vw; font: 300 5vw/1.2em Roboto, sans-serif; display: grid; grid-template-columns: 1fr; @@ -11,6 +13,7 @@ grid-template-areas: "main"; justify-items: center; align-items: center; + padding: 100px; } @keyframes slideIn { @@ -94,6 +97,16 @@ } } +#canvas { + display: block; + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + pointer-events: none; +} + aside { display: flex; flex-direction: column; @@ -103,9 +116,9 @@ aside { } #log { - color: hsl(0 100% 30%); + color: hsl(0 100% 80%); position: absolute; - font: normal 0.6em/1.2em sans-serif; + font: normal 1em/1.2em sans-serif; top: 0; left: 0; max-height: 20vh; -- GitLab