A client-side switch scanning simulator built with TypeScript and Vite.
This project is named after and inspired by Gary Derwent, an Occupational Therapist who created a seminal Flash-based switch scanning demo in the late 1990s. His original work helped demonstrate the mechanics and clinical applications of switch scanning for assistive technology. This project aims to replicate and extend those concepts using modern web technologies.
- Multiple Scanning Strategies:
- Row-Column
- Column-Row
- Linear & Snake
- Quadrant (Group Scanning)
- Elimination (Binary/Quaternary)
- Continuous (Mouse emulation)
- Predictive Scanning: Integrates PPM (Prediction by Partial Matching) to reorder targets based on probability.
- Configurable Settings: Adjustable scan rate, acceptance time, post-selection delay, and more.
- Audio Feedback: Web Audio API generated feedback for scanning steps and selections.
- Responsive Design: Grid layout that adapts to container size.
The simulator mimics a multi-switch setup using keyboard keys:
| Switch Function | Default Key(s) | Description |
|---|---|---|
| Switch 1 (Primary) | Space, Enter |
Select or Hold. Activates the currently highlighted item or group. |
| Switch 2 (Secondary) | 2 |
Step or Back-up. Manually moves to the next item or goes back a level. |
| Switch 3 (Utility) | 3 |
Home/Reset. Restarts the scan sequence or clears input. |
| Switch 4 (Cancel) | 4 |
Cancel. Stops the current action. |
| Settings Toggle | S |
Shows/Hides the configuration overlay. |
The application can be configured via URL parameters, making it easy to embed in iframes with specific presets.
Example: ?rate=800&strategy=row-column&ui=hidden
| Parameter | Values | Description |
|---|---|---|
ui |
hidden |
Hides the settings button and overlay (useful for embedding). |
rate |
number (ms) |
Sets the scan speed in milliseconds (e.g., 1000). |
strategy |
row-column, linear, snake, quadrant, group-row-column, elimination, continuous, probability |
Sets the initial scanning strategy. |
content |
numbers, keyboard |
Sets the grid content type. |
lang |
en, etc. |
Sets the language for prediction and keyboard layout. |
layout |
alphabetical, frequency |
Sets the keyboard layout mode. |
view |
standard, cost-numbers, cost-heatmap |
Visual debug modes to analyze scanning cost. |
heatmax |
number |
Sets the maximum value for the heatmap visualization. |
The simulator includes visualization modes to help analyze the efficiency of different scanning strategies and layouts.
- Open the Settings menu (click the gear icon or press
S). - Scroll down to the Visualization section.
- Change View Mode to:
- Cost (Numbers): Displays the "cost" (number of steps) to reach each item.
- Cost (Heatmap): Colors items from Green (low cost) to Red (high cost).
- Adjust Heatmap Max Cost to change the color scale sensitivity. Default is 20.
- Tip: If the grid is large, increase this value to see a smoother gradient.
This repo publishes two packages that can be used independently in other apps:
scan-engine— headless scanning engine (strategies, timing, selection).scan-engine-dom— DOM helpers for continuous scanning overlays + hit testing.react-scan-engine— tiny React wrapper (<Scanner>+<Scannable>) forscan-engine.
npm install scan-engine scan-engine-domFor React compatibility-style usage:
npm install react-scan-engine scan-enginereact-scan-engine intentionally mirrors the familiar react-scannable component shape:
import React from 'react';
import { Scannable, Scanner } from 'react-scan-engine';
class Example extends React.Component {
state = {
isActive: true,
};
render() {
return (
<Scanner
active={this.state.isActive}
config={{ scanPattern: 'row-column', scanTechnique: 'block', scanRate: 900 }}
>
<Scannable>
<button>CLICK</button>
</Scannable>
</Scanner>
);
}
}Compatibility details:
activemaps to engine start/stop.Scannablechildren are scanned in DOM order.- Defaults:
Enter/Spaceselect,ArrowRight/ArrowDownstep,Backspacereset,Escapecancel. - You can pass advanced engine config via the
configprop.
import { LinearScanner } from 'scan-engine';
const surface = {
getItemsCount: () => items.length,
getColumns: () => 8,
setFocus: (indices: number[]) => highlight(indices),
setSelected: (index: number) => flash(index),
};
const configProvider = {
get: () => ({
scanRate: 800,
scanInputMode: 'auto',
scanDirection: 'circular',
scanPattern: 'linear',
scanTechnique: 'point',
scanMode: null,
continuousTechnique: 'crosshair',
compassMode: 'continuous',
eliminationSwitchCount: 4,
allowEmptyItems: false,
initialItemPause: 0,
scanLoops: 0,
criticalOverscan: { enabled: false, fastRate: 100, slowRate: 1000 },
colorCode: { errorRate: 0.1, selectThreshold: 0.95 },
})
};
const scanner = new LinearScanner(surface, configProvider, {
onSelect: (index: number) => console.log('Selected', index),
});
scanner.start();import { ContinuousScanner } from 'scan-engine';
import { ContinuousOverlay, resolveIndexAtPoint } from 'scan-engine-dom';
const container = document.querySelector('.grid') as HTMLElement;
const overlay = new ContinuousOverlay(container);
const surface = {
getItemsCount: () => items.length,
getColumns: () => cols,
setFocus: (indices: number[]) => highlight(indices),
setSelected: (index: number) => flash(index),
resolveIndexAtPoint: (x: number, y: number) => resolveIndexAtPoint(container, x, y),
};
const scanner = new ContinuousScanner(surface, configProvider, {
onContinuousUpdate: (state) => overlay.update(state),
onSelect: (index: number) => console.log('Selected', index),
});
scanner.start();
// Cleanup when switching modes:
overlay.destroy();- Node.js (latest LTS recommended)
- npm
Clone the repository and install dependencies:
git clone <repository-url>
cd scan-engine-lab
npm installStart the development server:
npm run devThe app will be available at http://localhost:5173 (or the port shown in the terminal).
Build the project for production:
npm run buildThis generates the static assets in the dist/ directory.
This repo publishes:
scan-enginescan-engine-domreact-scan-engine
Recommended release flow:
- Prepare versions and build artifacts:
npm run release:prepare -- patchUse patch, minor, major, or an explicit version (for example 0.2.0).
- Commit the version changes and create a git tag.
- Publish all packages in dependency order:
npm run release:publishIf you want one command for everything (prepare + publish):
npm run release:full -- patch- Language: TypeScript
- Bundler: Vite
- Audio: Web Audio API (No external assets required)
- Prediction:
@willwade/ppmpredictor
This project is licensed under the MIT License - see the LICENSE file for details.