455 lines
11 KiB
Markdown
455 lines
11 KiB
Markdown
# @workshack/input
|
|
|
|
Workshack input components library with gamepad and keyboard navigation support for Angular applications.
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
npm install @workshack/input
|
|
```
|
|
|
|
## Services
|
|
|
|
### InputService
|
|
|
|
**Description:** Core service for managing input devices, actions, and schemes.
|
|
|
|
**Usage:**
|
|
```typescript
|
|
import { InputService } from '@workshack/input';
|
|
|
|
@Component({
|
|
// ...
|
|
})
|
|
export class MyComponent {
|
|
constructor(private inputService: InputService) {
|
|
// Define custom actions
|
|
const jumpAction = this.inputService.defineAction('jump');
|
|
jumpAction.onDown(() => console.log('Jump pressed!'));
|
|
jumpAction.onUp(() => console.log('Jump released!'));
|
|
|
|
// Create input scheme
|
|
const gameScheme = this.inputService.defineScheme('game');
|
|
|
|
// Listen for device changes
|
|
this.inputService.deviceListChanged.subscribe(devices => {
|
|
console.log('Available devices:', devices);
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**Methods:**
|
|
- `defineAction(name: string): InputAction` - Creates a new input action
|
|
- `defineScheme(name: string): InputScheme` - Creates a new input scheme
|
|
- `getDevices(): InputDevice[]` - Returns list of available input devices
|
|
|
|
**Properties:**
|
|
- `actions: InputAction[]` - Array of defined actions
|
|
- `schemes: InputScheme[]` - Array of defined schemes
|
|
- `devices: InputDevice[]` - Array of connected devices
|
|
- `deviceListChanged: BehaviorSubject<InputDevice[]>` - Observable for device changes
|
|
|
|
---
|
|
|
|
### ControlService (Example Integration)
|
|
|
|
**Description:** Higher-level service that integrates with InputService to provide common navigation actions.
|
|
|
|
**Usage:**
|
|
```typescript
|
|
import { ControlService } from 'your-app/services/control.service';
|
|
|
|
@Component({
|
|
// ...
|
|
})
|
|
export class MyComponent {
|
|
constructor(private controlService: ControlService) {
|
|
// Service automatically sets up common actions:
|
|
// - Left, Right, Up, Down navigation
|
|
// - Accept, Back, Exit actions
|
|
// - Hint/Help action
|
|
|
|
// Get key names for display
|
|
const keyNames = this.controlService.getKeyNames('Enter', 'Escape');
|
|
console.log('Key names:', keyNames); // ['Enter', 'Escape'] or themed names
|
|
}
|
|
}
|
|
```
|
|
|
|
**Static Properties:**
|
|
- `keyLeft: InputAction` - Left navigation action
|
|
- `keyRight: InputAction` - Right navigation action
|
|
- `keyUp: InputAction` - Up navigation action
|
|
- `keyDown: InputAction` - Down navigation action
|
|
- `keyAccept: InputAction` - Accept/confirm action
|
|
- `keyBack: InputAction` - Back/cancel action
|
|
- `keyExit: InputAction` - Exit action
|
|
- `keyHint: InputAction` - Hint/help action
|
|
|
|
**Methods:**
|
|
- `getKeyNames(...names): string[]` - Get themed key names for display
|
|
- `clearKeyBindings()` - Clear all key bindings from schemes
|
|
- `bindActionsToScheme(device: InputDevice)` - Bind actions to device scheme
|
|
|
|
## Directives
|
|
|
|
### ActionBindingDirective
|
|
|
|
**Selector:** `[inputActionBinding]`
|
|
**Description:** Binds an action name to an HTML element.
|
|
|
|
**Usage:**
|
|
```html
|
|
<button inputActionBinding="confirm">Confirm</button>
|
|
<div inputActionBinding="back">Back</div>
|
|
```
|
|
|
|
**Properties:**
|
|
- `inputActionBinding: string` - Action name to bind to the element
|
|
|
|
**Features:**
|
|
- Automatically sets `action` attribute on the element
|
|
- Works with keyboard and gamepad navigation
|
|
|
|
---
|
|
|
|
### KeyItemDirective
|
|
|
|
**Selector:** `[inputKeyItem]`
|
|
**Description:** Makes an element navigable with keyboard/gamepad and handles focus states.
|
|
|
|
**Usage:**
|
|
```html
|
|
<button
|
|
inputKeyItem
|
|
[active]="'focused'"
|
|
[actionBindings]="{
|
|
Accept: () => onClick(),
|
|
Back: () => onCancel()
|
|
}"
|
|
(activated)="onActivated()"
|
|
(focused)="onFocused()"
|
|
(blurred)="onBlurred()">
|
|
Click me
|
|
</button>
|
|
```
|
|
|
|
**Properties:**
|
|
- `active: string` - CSS class name for active/focused state (default: 'active')
|
|
- `actionBindings: ActionBindings` - Object mapping action names to callback functions
|
|
|
|
**Events:**
|
|
- `activated: EventEmitter<void>` - Emitted when item is activated
|
|
- `deactivated: EventEmitter<void>` - Emitted when item is deactivated
|
|
- `focused: EventEmitter<void>` - Emitted when item receives focus
|
|
- `blurred: EventEmitter<void>` - Emitted when item loses focus
|
|
|
|
**Static Methods:**
|
|
- `FireAction(action: string): boolean` - Fires action on currently focused item
|
|
- `ActivateCurrentItem()` - Activates the currently focused item
|
|
- `SelectNexItem(fromAngle: number, toAngle: number)` - Selects next item in direction
|
|
|
|
---
|
|
|
|
### KeyItemGroupDirective
|
|
|
|
**Selector:** `[inputKeyItemGroup]`
|
|
**Description:** Groups KeyItem elements for scoped navigation.
|
|
|
|
**Usage:**
|
|
```html
|
|
<div inputKeyItemGroup>
|
|
<button inputKeyItem>Button 1</button>
|
|
<button inputKeyItem>Button 2</button>
|
|
<button inputKeyItem>Button 3</button>
|
|
</div>
|
|
```
|
|
|
|
**Features:**
|
|
- Navigation is scoped to items within the group
|
|
- Automatically manages focus within the group
|
|
- Supports nested groups
|
|
|
|
## Models
|
|
|
|
### InputAction
|
|
|
|
**Description:** Represents a bindable input action (e.g., "jump", "fire", "menu").
|
|
|
|
**Usage:**
|
|
```typescript
|
|
const jumpAction = new InputAction('jump');
|
|
|
|
// Bind callbacks
|
|
jumpAction.onDown((value, data, angle, timePressed) => {
|
|
console.log('Jump started!', { value, data, angle, timePressed });
|
|
});
|
|
|
|
jumpAction.onUp((value, data, angle, timePressed) => {
|
|
console.log('Jump ended!', { value, data, angle, timePressed });
|
|
});
|
|
|
|
jumpAction.onChange((value, data, angle, timePressed) => {
|
|
console.log('Jump value changed:', value);
|
|
});
|
|
|
|
// Fire the action
|
|
jumpAction.fire(1.0); // Press
|
|
jumpAction.fire(0.0); // Release
|
|
```
|
|
|
|
**Methods:**
|
|
- `onDown(callback: ActionCallback)` - Register callback for action press
|
|
- `onUp(callback: ActionCallback)` - Register callback for action release
|
|
- `onChange(callback: ActionCallback)` - Register callback for value changes
|
|
- `fire(value: number, data?: unknown, angle?: number, timePressed?: number)` - Fire the action
|
|
- `mapKeysToScheme(scheme: InputScheme, keys: (string|number)[])` - Map keys to scheme
|
|
|
|
---
|
|
|
|
### InputDevice
|
|
|
|
**Description:** Base class for input devices (keyboard, gamepad).
|
|
|
|
**Properties:**
|
|
- `name: string` - Device name
|
|
- `type: string` - Device type ('keyboard', 'gamepad')
|
|
|
|
**Methods:**
|
|
- `hasScheme(): boolean` - Check if device has an input scheme
|
|
- `mapKey(action: InputAction, keys: (string|number)[])` - Map keys to action
|
|
|
|
---
|
|
|
|
### InputKeyboard
|
|
|
|
**Description:** Keyboard input device implementation.
|
|
|
|
**Usage:**
|
|
```typescript
|
|
const keyboard = new InputKeyboard('Main Keyboard');
|
|
keyboard.mapKey(jumpAction, ['Space', 'KeyW']);
|
|
```
|
|
|
|
---
|
|
|
|
### InputGamepad
|
|
|
|
**Description:** Gamepad input device implementation with automatic detection.
|
|
|
|
**Static Properties:**
|
|
- `DeviceConnected: Subject<InputGamepad>` - Observable for gamepad connections
|
|
- `DeviceDisconnected: Subject<InputGamepad>` - Observable for gamepad disconnections
|
|
|
|
**Static Methods:**
|
|
- `Init()` - Initialize gamepad detection
|
|
|
|
**Usage:**
|
|
```typescript
|
|
// Listen for gamepad events
|
|
InputGamepad.DeviceConnected.subscribe(gamepad => {
|
|
console.log('Gamepad connected:', gamepad.name);
|
|
gamepad.mapKey(jumpAction, [0]); // Map to button 0
|
|
});
|
|
|
|
InputGamepad.DeviceDisconnected.subscribe(gamepad => {
|
|
console.log('Gamepad disconnected:', gamepad.name);
|
|
});
|
|
|
|
// Initialize detection
|
|
InputGamepad.Init();
|
|
```
|
|
|
|
## Complete Example
|
|
|
|
```typescript
|
|
import { Component, OnInit } from '@angular/core';
|
|
import {
|
|
InputService,
|
|
NavigationModule,
|
|
KeyItemDirective,
|
|
KeyItemGroupDirective,
|
|
ActionBindingDirective
|
|
} from '@workshack/input';
|
|
|
|
@Component({
|
|
selector: 'app-game-menu',
|
|
imports: [
|
|
NavigationModule,
|
|
KeyItemDirective,
|
|
KeyItemGroupDirective,
|
|
ActionBindingDirective
|
|
],
|
|
template: `
|
|
<div class="game-menu" inputKeyItemGroup>
|
|
<h2>Game Menu</h2>
|
|
|
|
<button
|
|
inputKeyItem
|
|
[actionBindings]="{
|
|
Accept: () => startGame(),
|
|
Back: () => goBack()
|
|
}"
|
|
(focused)="playHoverSound()"
|
|
class="menu-item">
|
|
Start Game
|
|
</button>
|
|
|
|
<button
|
|
inputKeyItem
|
|
[actionBindings]="{
|
|
Accept: () => showOptions(),
|
|
Back: () => goBack()
|
|
}"
|
|
(focused)="playHoverSound()"
|
|
class="menu-item">
|
|
Options
|
|
</button>
|
|
|
|
<button
|
|
inputKeyItem
|
|
[actionBindings]="{
|
|
Accept: () => exitGame(),
|
|
Back: () => goBack()
|
|
}"
|
|
(focused)="playHoverSound()"
|
|
class="menu-item">
|
|
Exit
|
|
</button>
|
|
|
|
<div class="controls-hint">
|
|
<span>Use arrow keys or gamepad to navigate</span>
|
|
<span>Press Enter or A to select</span>
|
|
</div>
|
|
</div>
|
|
`,
|
|
styles: [`
|
|
.game-menu {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.menu-item {
|
|
padding: 1rem 2rem;
|
|
background: #333;
|
|
color: white;
|
|
border: 2px solid transparent;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.menu-item.active,
|
|
.menu-item:hover {
|
|
background: #555;
|
|
border-color: #007bff;
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.controls-hint {
|
|
display: flex;
|
|
flex-direction: column;
|
|
text-align: center;
|
|
font-size: 0.8rem;
|
|
color: #666;
|
|
margin-top: 2rem;
|
|
}
|
|
`]
|
|
})
|
|
export class GameMenuComponent implements OnInit {
|
|
|
|
constructor(private inputService: InputService) {}
|
|
|
|
ngOnInit() {
|
|
// Set up custom actions
|
|
const menuAction = this.inputService.defineAction('menu');
|
|
menuAction.onUp(() => this.toggleMenu());
|
|
|
|
// Listen for device changes
|
|
this.inputService.deviceListChanged.subscribe(devices => {
|
|
console.log('Input devices:', devices.map(d => d.name));
|
|
});
|
|
}
|
|
|
|
startGame() {
|
|
console.log('Starting game...');
|
|
}
|
|
|
|
showOptions() {
|
|
console.log('Showing options...');
|
|
}
|
|
|
|
exitGame() {
|
|
console.log('Exiting game...');
|
|
}
|
|
|
|
goBack() {
|
|
console.log('Going back...');
|
|
return false; // Prevent default navigation
|
|
}
|
|
|
|
playHoverSound() {
|
|
// Play hover sound effect
|
|
console.log('Hover sound');
|
|
}
|
|
|
|
toggleMenu() {
|
|
console.log('Toggle menu');
|
|
}
|
|
}
|
|
```
|
|
|
|
## Features
|
|
|
|
- 🎮 **Gamepad Support** - Automatic detection and mapping for standard gamepads
|
|
- ⌨️ **Keyboard Navigation** - Full keyboard navigation with customizable key bindings
|
|
- 🖱️ **Mouse/Touch Support** - Works seamlessly with mouse and touch interactions
|
|
- 🎯 **Action Binding System** - Flexible system for binding actions to inputs
|
|
- 📐 **Directional Navigation** - Smart directional navigation based on element positions
|
|
- 🎨 **Customizable Themes** - Support for different key name themes (PlayStation, Xbox, etc.)
|
|
- 🔄 **Hot-plugging** - Automatic detection of connected/disconnected devices
|
|
- 🎛️ **Multiple Schemes** - Support for multiple input schemes per device
|
|
|
|
## Development
|
|
|
|
### Building
|
|
```bash
|
|
npm run build:input
|
|
```
|
|
|
|
### Watching for changes
|
|
```bash
|
|
npm run watch:input
|
|
```
|
|
|
|
### Testing
|
|
```bash
|
|
npm run test:input
|
|
```
|
|
|
|
### Publishing
|
|
```bash
|
|
npm run publish:input
|
|
```
|
|
|
|
## License
|
|
|
|
This library is licensed under a custom license that allows:
|
|
|
|
- **Free use** for non-commercial applications (no revenue, no ads, no paid features)
|
|
- **Commercial use** with attribution requirement - you must credit the library in your app (e.g., on an "About" or "For Developers" page)
|
|
|
|
For full license terms, see the [LICENSE](LICENSE) file.
|
|
|
|
### Attribution Example for Commercial Use
|
|
|
|
```
|
|
This application uses Workshack Input Library
|
|
(https://www.npmjs.com/package/@workshack/input)
|
|
© 2025 Workshack Team
|
|
```
|