Mouse example
In this tutorial you will build your first positioning model with OpenHPS. You will create a source node that turns your mouse movements into positions, process them with a filter, and visualize the result on a chart. It requires nothing more than a browser and is a great way to learn the core concepts.
New to OpenHPS? Make sure you have installed @openhps/core first, and read the introduction for a quick overview of the key concepts.
The model you will build looks like this:
graph LR
A[MouseSourceNode] -->|positions| B[Convert from space]
B -->|positions| C[SMAFilterNode]
C -->|smoothed positions| D[ChartSinkNode]
1. Creating the source node
A source node generates data frames. Our MouseSourceNode listens for mouse or touch movement inside a HTML element and pushes a data frame containing the position of a DataObject called mouse.
MouseSourceNode
ts// @filename: MouseSourceNode.tsimport {DataFrame ,SourceNode ,DataObject ,Absolute2DPosition } from '@openhps/core';export classMouseSourceNode extendsSourceNode <DataFrame > {privateelementId : string = "";privatetrackingArea :HTMLElement =undefined ;constructor(elementId : string) {super();this.elementId =elementId ;this.once ('build', this._initMouse .bind (this));}_initMouse () {// Get tracking areathis.trackingArea =document .getElementById (this.elementId );this.trackingArea .onmousemove = this.onMouseMove .bind (this);this.trackingArea .ontouchmove = this.onTouchMove .bind (this);}onMouseMove (e :MouseEvent ) {constrect = this.trackingArea .getBoundingClientRect ();constx =e .clientX -rect .left ;consty =e .clientY -rect .top ;this._emitPosition (x ,y );}onTouchMove (e :TouchEvent ) {if (e .touches .length > 0) {constrect = this.trackingArea .getBoundingClientRect ();consttouch =e .touches [0];constx =touch .clientX -rect .left ;consty =touch .clientY -rect .top ;this._emitPosition (x ,y );}}_emitPosition (x : number,y : number) {constmouse = newDataObject ("mouse");mouse .position = newAbsolute2DPosition (x ,y );constframe = newDataFrame (mouse );this.push (frame );}onPull ():Promise <DataFrame > {return newPromise ((resolve ) => {resolve (undefined );});}}
The source node pushes frames that contain the mouse data object with its position set to the current X,Y coordinates.
2. Creating the sink node
A sink node is the final node in the model. Our ChartSinkNode keeps a history of all positions that passed through the node and draws them on a chart using Chart.js.
ChartSinkNode
ts// @filename: ChartSinkNode.tsimport {DataFrame ,SinkNode ,DataObject ,Absolute2DPosition ,NodeData ,NodeDataService } from '@openhps/core';export classChartSinkNode extendsSinkNode <DataFrame > {privateelementId : string = "";privatechartCanvas :HTMLCanvasElement =undefined ;privatechart : any =undefined ;constructor(elementId : string) {super();this.elementId =elementId ;this.once ('build', this._initChart .bind (this));}_initChart (): void {this.chartCanvas =document .getElementById (this.elementId ) asHTMLCanvasElement ;if (!(window as any).Chart ) {// Load Chart.js if not loaded/* @ts-expect-error: Ignore dynamic import error for CDN Chart.js */import('https://cdn.jsdelivr.net/npm/chart.js').then (() => this._initChart ());return;}}onPush (frame ):Promise <void> {return newPromise ((resolve ,reject ) => {// Store and retrieve historical data (supports multiple different object UIDs)constservice :NodeDataService <any> = this.model .findDataService (NodeData );service // Find node specific data pertaining to "mouse".findData (this.uid ,frame .source ).then ((data ) => {data =data ?? [];data .push ({x :frame .source .position .x ,y :frame .source .position .y });// Store node specific data pertaining to "mouse"returnservice .insertData (this.uid ,frame .source ,data );}).then ((data ) => {// Draw chart with all X,Y locations that passed through this nodethis.drawChart (data );resolve ();}).catch (reject );});}/*** Draw the chart** @params data Array of locations*/drawChart (data : ({x : number,y : number})[]): void {constctx = this.chartCanvas .getContext ('2d');if (this.chart ) this.chart .destroy ();this.chart = new (window as any).Chart (ctx , {type : 'line',data : {labels : ["location"],datasets : [{label : 'Mouse Location',data ,borderColor : '#007bff',backgroundColor : 'rgba(0,123,255,0.1)',fill : false,showLine : true,pointRadius : 2,parsing : false,}]},options : {animation : false,scales : {x : {type : 'linear',min : 0,max : 300,title : {display : true,text : 'X Position' }},y : {type : 'linear',min : 0,max : 200,title : {display : true,text : 'Y Position' }}},plugins : {legend : {display : false }}}});}}
The sink node stores previous positions in a NodeDataService so it can draw the full movement trail.
3. Building the model
Now we can combine everything into a positioning model using the ModelBuilder.
Positioning Model
ts// @filename: mouse.tsimport {ModelBuilder ,DataObject ,Absolute2DPosition ,SMAFilterNode ,ReferenceSpace ,Euler ,AngleUnit } from '@openhps/core';import {MouseSourceNode } from './MouseSourceNode';import {ChartSinkNode } from './ChartSinkNode';constmouseReferenceSpace = newReferenceSpace ().translation (0, 200).rotation (newEuler (180, 0, 0, 'ZXY',AngleUnit .DEGREE ));ModelBuilder .create ()// Step 1. Obtain X,Y location from mouse (active source node).from (newMouseSourceNode ("trackArea"))// Step 2. Flip the axis.convertFromSpace (mouseReferenceSpace )// Step 3. Simple moving average of the X,Y position (average of 40 readings).via (newSMAFilterNode ((obj :DataObject ) => ([{key : "x",value : (obj .position asAbsolute2DPosition ).x },{key : "y",value : (obj .position asAbsolute2DPosition ).y }]),(key : string,value : number,obj :DataObject ) => {obj .position [key ] =value },{taps : 40 }))// Step 4. Plot the results.to (newChartSinkNode ("mouseChart")).build ();
Summary
- A source node produces data frames containing positions.
- A reference space transforms positions between coordinate systems.
- A processing node (like
SMAFilterNode) transforms the data. - A sink node consumes the final result.
You now know how to build a positioning model. Ready to take it further? Learn about positions and reference spaces in the core concepts, or jump to the RF fingerprinting tutorial to build a real indoor positioning system.