Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion packages/documentation/docs/user-guide/features/prompter.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ The prompter can be controlled by different types of controllers. The control mo
| `?mode=shuttlewebhid` | Controlled by a Contour Design ShuttleXpress, using the browser's WebHID API [See configuration details](prompter.md#control-using-contour-shuttlexpress-via-webhid) |
| `?mode=pedal` | Controlled by any MIDI device outputting note values between 0 - 127 of CC notes on channel 8. Analogue Expression pedals work well with TRS-USB midi-converters. [See configuration details](prompter.md#control-using-midi-input-modepedal) |
| `?mode=joycon` | Controlled by Nintendo Switch Joycon, using the HTML5 GamePad API. [See configuration details](prompter.md#control-using-nintendo-joycon-gamepad) |
| `?mode=xbox` | Controlled by Xbox controller, using the HTML5 GamePad API. [See configuration details](prompter.md#control-using-xbox-controller-modexbox) |

#### Control using mouse \(scroll wheel\)

Expand Down Expand Up @@ -161,13 +162,14 @@ The Joycons can operate in 3 modes, the L-stick, the R-stick or both L+R sticks

| Query parameter | Type | Description | Default |
| :----------------------- | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- |
| `joycon_speedMap` | Array of numbes | Speeds to scroll by \(px. pr. frame - approx 60fps\) when scrolling forwards. The beginning of the forwards-range maps to the first number in this array, and thee end of the forwards-range map to the end of this array. All values in between are being interpolated in a spline curve. | `[1, 2, 3, 4, 5, 8, 12, 30]` |
| `joycon_speedMap` | Array of numbers | Speeds to scroll by \(px. pr. frame - approx 60fps\) when scrolling forwards. The beginning of the forwards-range maps to the first number in this array, and the end of the forwards-range map to the end of this array. All values in between are being interpolated in a spline curve. | `[1, 2, 3, 4, 5, 8, 12, 30]` |
| `joycon_reverseSpeedMap` | Array of numbers | Same as `joycon_speedMap` but for the backwards range. | `[1, 2, 3, 4, 5, 8, 12, 30]` |
| `joycon_rangeRevMin` | number | The end of the backwards-range, full speed backwards. | `-1` |
| `joycon_rangeNeutralMin` | number | The beginning of the backwards-range. | `-0.25` |
| `joycon_rangeNeutralMax` | number | The minimum input to run forward, the start of the forward-range \(min speed\). This is also the end of any "deadband" you want filter out before starting moving forwards. | `0.25` |
| `joycon_rangeFwdMax` | number | The maximum input, the end of the forward-range \(max speed\) | `1` |
| `joycon_rightHandOffset` | number | A ratio to increase or decrease the R Joycon joystick sensitivity relative to the L Joycon. | `1.4` |
| `joycon_invertJoystick` | 0 / 1 | Invert the joystick direction. When enabled, pushing the joystick forward scrolls up instead of down. | `1` |

- `joycon_rangeNeutralMin` has to be greater than `joycon_rangeRevMin`
- `joycon_rangeNeutralMax` has to be greater than `joycon_rangeNeutralMin`
Expand Down Expand Up @@ -197,3 +199,47 @@ You can turn on `?debug=1` to see how your input maps to an output.
| _"I can't reach max speed backwards"_ | Increase `joycon_rangeRevMin` |
| _"I can't reach max speed forwards"_ | Decrease `joycon_rangeFwdMax` |
| _"As I find a good speed, it varies a bit in speed up/down even if I hold my finger still"_ | Use `?debug=1` to see what speed is calculated in the position the presenter wants to rest their finger in. Add more of that number in a sequence in the `joycon_speedMap` to flatten out the speed curve, i.e. `[1, 2, 3, 4, 4, 4, 4, 5, ...]` |

#### Control using Xbox controller \(_?mode=xbox_\)

This mode uses the browser's Gamepad API to control the prompter with an Xbox controller. It supports Xbox One, Xbox Series X|S, and compatible third-party controllers.

The controller can be connected via Bluetooth or USB. **Note:** On macOS, Xbox controllers may not be recognized over USB due to driver limitations; Bluetooth is recommended.

**Scroll control:**

- **Right Trigger (RT):** Scroll forward - speed is proportional to trigger pressure
- **Left Trigger (LT):** Scroll backward - speed is proportional to trigger pressure

**Button map:**

| **Button** | **Action** |
| :---------------- | :------------------------ |
| A | Take (go to next part) |
| B | Go to the "On-air" story |
| X | Go to the previous story |
| Y | Go to the following story |
| LB (Left Bumper) | Go to the top |
| RB (Right Bumper) | Go to the "Next" story |
| D-Pad Up | Scroll up (fine control) |
| D-Pad Down | Scroll down (fine control)|

**Configuration parameters:**

| Query parameter | Type | Description | Default |
| :--------------------- | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- |
| `xbox_speedMap` | Array of numbers | Speeds to scroll by (px per frame, ~60fps) when scrolling forwards. Values are interpolated using a spline curve based on trigger pressure. | `[2, 3, 5, 6, 8, 12, 18, 45]` |
| `xbox_reverseSpeedMap` | Array of numbers | Same as `xbox_speedMap` but for the backwards range (left trigger). | `[2, 3, 5, 6, 8, 12, 18, 45]` |
| `xbox_triggerDeadZone` | number | Dead zone for the triggers, to prevent accidental scrolling. Value between 0 and 1. | `0.1` |

You can turn on `?debug=1` to see how your trigger input maps to scroll speed.

**Calibration guide:**

| **Symptom** | **Adjustment** |
| :----------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------- |
| _"It starts scrolling when I'm not touching the trigger"_ | Increase `xbox_triggerDeadZone` (e.g., `0.15` or `0.2`) |
| _"I have to press too hard before it starts moving"_ | Decrease `xbox_triggerDeadZone` (e.g., `0.05`) |
| _"It scrolls too fast"_ | Use smaller values in `xbox_speedMap`, e.g., `[1, 2, 3, 4, 5, 8, 12, 30]` |
| _"It scrolls too slow"_ | Use larger values in `xbox_speedMap`, e.g., `[3, 6, 10, 15, 25, 40, 60, 100]` |
| _"Speed jumps too quickly from slow to fast"_ | Add more intermediate values to `xbox_speedMap` to create a smoother curve, e.g., `[1, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30]` |
16 changes: 16 additions & 0 deletions packages/webui/src/client/ui/Prompter/PrompterView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ interface PrompterConfig {
pedal_rangeNeutralMax?: number
pedal_rangeFwdMax?: number
shuttle_speedMap?: number[]
xbox_speedMap?: number[]
xbox_reverseSpeedMap?: number[]
xbox_triggerDeadZone?: number
marker?: 'center' | 'top' | 'bottom' | 'hide'
showMarker: boolean
showScroll: boolean
Expand All @@ -77,6 +80,7 @@ export enum PrompterConfigMode {
JOYCON = 'joycon',
PEDAL = 'pedal',
SHUTTLEWEBHID = 'shuttlewebhid',
XBOX = 'xbox',
}

export interface IPrompterControllerState {
Expand Down Expand Up @@ -175,6 +179,18 @@ export class PrompterViewContent extends React.Component<Translated<IProps & ITr
pedal_rangeNeutralMin: parseInt(firstIfArray(queryParams['pedal_rangeNeutralMin']) as string, 10) || undefined,
pedal_rangeNeutralMax: parseInt(firstIfArray(queryParams['pedal_rangeNeutralMax']) as string, 10) || undefined,
pedal_rangeFwdMax: parseInt(firstIfArray(queryParams['pedal_rangeFwdMax']) as string, 10) || undefined,
xbox_speedMap:
queryParams['xbox_speedMap'] === undefined
? undefined
: asArray(queryParams['xbox_speedMap']).map((value) => Number.parseInt(value, 10)),
xbox_reverseSpeedMap:
queryParams['xbox_reverseSpeedMap'] === undefined
? undefined
: asArray(queryParams['xbox_reverseSpeedMap']).map((value) => Number.parseInt(value, 10)),
xbox_triggerDeadZone: (() => {
const val = Number.parseFloat(firstIfArray(queryParams['xbox_triggerDeadZone']) as string)
return Number.isNaN(val) ? undefined : val
})(),
marker: (firstIfArray(queryParams['marker']) as any) || undefined,
showMarker: queryParams['showmarker'] === undefined ? true : queryParams['showmarker'] === '1',
showScroll: queryParams['showscroll'] === undefined ? true : queryParams['showscroll'] === '1',
Expand Down
59 changes: 36 additions & 23 deletions packages/webui/src/client/ui/Prompter/controller/joycon-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,43 @@ type JoyconMode = 'L' | 'R' | 'LR' | null
* This class handles control of the prompter using
*/
export class JoyConController extends ControllerAbstract {
private prompterView: PrompterViewContent
private readonly prompterView: PrompterViewContent

private invertJoystick = false // change scrolling direction for joystick
private rangeRevMin = -1 // pedal "all back" position, the max-reverse-position
private rangeNeutralMin = -0.25 // pedal "back" position where reverse-range transitions to the neutral x
private rangeNeutralMax = 0.25 // pedal "front" position where scrolling starts, the 0 speed origin
private rangeFwdMax = 1 // pedal "all front" position where scrolling is maxed out
private rightHandOffset = 1.4 // factor increased by 1.4 to account for the R joystick being less sensitive than L
private speedMap = [1, 2, 3, 4, 5, 8, 12, 30]
private reverseSpeedMap = [1, 2, 3, 4, 5, 8, 12, 30]
private deadBand = 0.25
private readonly invertJoystick: boolean // change scrolling direction for joystick
private readonly rangeRevMin: number // pedal "all back" position, the max-reverse-position
private readonly rangeNeutralMin: number // pedal "back" position where reverse-range transitions to the neutral x
private readonly rangeNeutralMax: number // pedal "front" position where scrolling starts, the 0 speed origin
private readonly rangeFwdMax: number // pedal "all front" position where scrolling is maxed out
private readonly rightHandOffset: number // factor increased by 1.4 to account for the R joystick being less sensitive than L
private readonly speedMap: number[]
private readonly reverseSpeedMap: number[]
private readonly deadBand: number

private speedSpline: Spline | undefined
private reverseSpeedSpline: Spline | undefined
private readonly speedSpline: Spline | undefined
private readonly reverseSpeedSpline: Spline | undefined

private updateSpeedHandle: number | null = null
private timestampOfLastUsedJoyconInput = 0
private currentPosition = 0
private lastInputValue = ''
private lastButtonInputs: { [index: number]: { mode: JoyconMode; buttons: number[] } } = {}

// Bound event handler for cleanup
private readonly updateScrollPositionBound: (() => void) | undefined

constructor(view: PrompterViewContent) {
super()
this.prompterView = view

// assigns params from URL or falls back to the default
this.invertJoystick = view.configOptions.joycon_invertJoystick || this.invertJoystick
this.rangeRevMin = view.configOptions.joycon_rangeRevMin || this.rangeRevMin
this.rangeNeutralMin = view.configOptions.joycon_rangeNeutralMin || this.rangeNeutralMin
this.rangeNeutralMax = view.configOptions.joycon_rangeNeutralMax || this.rangeNeutralMax
this.rangeFwdMax = view.configOptions.joycon_rangeFwdMax || this.rangeFwdMax
this.rightHandOffset = view.configOptions.joycon_rightHandOffset || this.rightHandOffset
this.speedMap = view.configOptions.joycon_speedMap || this.speedMap
this.reverseSpeedMap = view.configOptions.joycon_reverseSpeedMap || this.reverseSpeedMap
this.invertJoystick = view.configOptions.joycon_invertJoystick || false
this.rangeRevMin = view.configOptions.joycon_rangeRevMin || -1
this.rangeNeutralMin = view.configOptions.joycon_rangeNeutralMin || -0.25
this.rangeNeutralMax = view.configOptions.joycon_rangeNeutralMax || 0.25
this.rangeFwdMax = view.configOptions.joycon_rangeFwdMax || 1
this.rightHandOffset = view.configOptions.joycon_rightHandOffset || 1.4
this.speedMap = view.configOptions.joycon_speedMap || [1, 2, 3, 4, 5, 8, 12, 30]
this.reverseSpeedMap = view.configOptions.joycon_reverseSpeedMap || [1, 2, 3, 4, 5, 8, 12, 30]
this.deadBand = Math.min(Math.abs(this.rangeNeutralMin), Math.abs(this.rangeNeutralMax))

// validate range settings, they need to be in sequence, or the logic will break
Expand Down Expand Up @@ -84,12 +87,22 @@ export class JoyConController extends ControllerAbstract {
this.reverseSpeedMap
)

window.addEventListener('gamepadconnected', this.updateScrollPosition.bind(this))
window.addEventListener('gamepaddisconnected', this.updateScrollPosition.bind(this))
this.updateScrollPositionBound = this.updateScrollPosition.bind(this)

window.addEventListener('gamepadconnected', this.updateScrollPositionBound)
window.addEventListener('gamepaddisconnected', this.updateScrollPositionBound)
}

public destroy(): void {
// Nothing
if (this.updateScrollPositionBound) {
window.removeEventListener('gamepadconnected', this.updateScrollPositionBound)
window.removeEventListener('gamepaddisconnected', this.updateScrollPositionBound)
}

if (this.updateSpeedHandle !== null) {
window.cancelAnimationFrame(this.updateSpeedHandle)
this.updateSpeedHandle = null
}
}
public onKeyDown(_e: KeyboardEvent): void {
// Nothing
Expand Down
16 changes: 10 additions & 6 deletions packages/webui/src/client/ui/Prompter/controller/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { JoyConController } from './joycon-device.js'
import { KeyboardController } from './keyboard-device.js'
import { ShuttleKeyboardController } from './shuttle-keyboard-device.js'
import { ShuttleWebHidController } from './shuttle-webhid-device.js'
import { XboxController } from './xbox-controller-device.js'

export class PrompterControlManager {
private _view: PrompterViewContent
Expand All @@ -21,24 +22,27 @@ export class PrompterControlManager {
window.addEventListener('mouseup', this._onMouseKeyUp)

if (Array.isArray(this._view.configOptions.mode)) {
if (this._view.configOptions.mode.indexOf(PrompterConfigMode.MOUSE) > -1) {
if (this._view.configOptions.mode.includes(PrompterConfigMode.MOUSE)) {
this._controllers.push(new MouseIshController(this._view))
}
if (this._view.configOptions.mode.indexOf(PrompterConfigMode.KEYBOARD) > -1) {
if (this._view.configOptions.mode.includes(PrompterConfigMode.KEYBOARD)) {
this._controllers.push(new KeyboardController(this._view))
}
if (this._view.configOptions.mode.indexOf(PrompterConfigMode.SHUTTLEKEYBOARD) > -1) {
if (this._view.configOptions.mode.includes(PrompterConfigMode.SHUTTLEKEYBOARD)) {
this._controllers.push(new ShuttleKeyboardController(this._view))
}
if (this._view.configOptions.mode.indexOf(PrompterConfigMode.PEDAL) > -1) {
if (this._view.configOptions.mode.includes(PrompterConfigMode.PEDAL)) {
this._controllers.push(new MidiPedalController(this._view))
}
if (this._view.configOptions.mode.indexOf(PrompterConfigMode.JOYCON) > -1) {
if (this._view.configOptions.mode.includes(PrompterConfigMode.JOYCON)) {
this._controllers.push(new JoyConController(this._view))
}
if (this._view.configOptions.mode.indexOf(PrompterConfigMode.SHUTTLEWEBHID) > -1) {
if (this._view.configOptions.mode.includes(PrompterConfigMode.SHUTTLEWEBHID)) {
this._controllers.push(new ShuttleWebHidController(this._view))
}
if (this._view.configOptions.mode.includes(PrompterConfigMode.XBOX)) {
this._controllers.push(new XboxController(this._view))
}
}

if (this._controllers.length === 0) {
Expand Down
Loading
Loading