Skip to content
Open
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
71 changes: 71 additions & 0 deletions src/components/profile/section.html
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,69 @@ <h2>{{getSectionTitle()}}</h2>
</div>
</div>

<!-- GYRO MOMENTUM SETTINGS -->
<div *ngIf='sectionIsGyro() && getSectionAsGyro() as gyro' class='gyro-momentum'>
<div class='setting'>
<div class='text'>
<div class='name'>Enable Momentum</div>
<div class='material' (click)='showDialogHelp("gyroMomentum")'>help</div>
</div>
<app-input-toggle
[value]='gyro.momentumEnabled'
(update)='gyro.momentumEnabled=$event; save()'
/>
</div>

<div class='block' *ngIf='gyro.momentumEnabled'>
<div class='subtitle'>Vertical Dampening <span class='material help-icon' (click)='showDialogHelp("gyroMomentumDamping")'>help</span></div>
<div class='island'>
<app-input-number
[value]='gyro.momentumDampingV'
(update)='gyro.momentumDampingV=$any($event); save()'
[unit]="''"
[min]='0.1'
[max]='50'
[step]='0.1'
[factor]='1'
[decimals]='1'
/>
</div>
</div>

<div class='block' *ngIf='gyro.momentumEnabled'>
<div class='subtitle'>Horizontal Dampening <span class='material help-icon' (click)='showDialogHelp("gyroMomentumDamping")'>help</span></div>
<div class='island'>
<app-input-number
[value]='gyro.momentumDampingH'
(update)='gyro.momentumDampingH=$any($event); save()'
[unit]="''"
[min]='0.1'
[max]='50'
[step]='0.1'
[factor]='1'
[decimals]='1'
/>
</div>
</div>

<div class='block' *ngIf='gyro.momentumEnabled'>
<div class='subtitle'>Cutoff Velocity <span class='material help-icon' (click)='showDialogHelp("gyroMomentumCutoff")'>help</span></div>
<div class='island'>
<app-input-number
[value]='gyro.momentumThreshold'
(update)='gyro.momentumThreshold=$any($event); save()'
[unit]="''"
[min]='100'
[max]='10000'
[step]='50'
[factor]='1'
[decimals]='1'
/>
</div>
</div>
</div>


<!-- GYRO AXIS -->
<div *ngIf='sectionIsGyroAxis() && getSectionAsGyroAxis() as gyroAxis'>
<h2>{{getSectionTitle()}}</h2>
Expand Down Expand Up @@ -630,3 +693,11 @@ <h2>{{getSectionTitle()}}</h2>
<button (click)='hideDialogKeypicker()'>Close</button>
</div>
</dialog>

<dialog id='dialog-help'>
<h2>{{helpTitle}}</h2>
<p [innerHTML]='helpText'></p>
<div class='close'>
<button (click)='hideDialogHelp()'>Close</button>
</div>
</dialog>
52 changes: 52 additions & 0 deletions src/components/profile/section.sass
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
.subtitle
margin-bottom: 5px
font-size: 14px
.help-icon
font-size: 14px
vertical-align: middle
color: hsl(0deg, 0%, 40%)
cursor: pointer

p
font-size: 14px
Expand Down Expand Up @@ -262,3 +267,50 @@
button
text-align: center

// Gyro momentum settings
.gyro-momentum
margin-top: 10px

.setting
display: flex
align-items: center
margin-bottom: 15px
.text
display: flex
flex: 1
min-width: 0
height: 40px
margin-right: 10px
align-items: center
background-color: hsl(0deg, 0%, 20%)
border-radius: 5px
.name
font-size: 14px
font-weight: normal
white-space: nowrap
.material
font-size: 18px
color: hsl(0deg, 0%, 40%)
padding-left: 6px
padding-right: 4px
cursor: pointer
line-height: 40px

::ng-deep app-input-number
width: 100px
height: 40px !important
& *
height: 40px !important
line-height: 40px !important
.number
border-radius: 5px
background-color: black
.value, .unit
font-size: 16px !important
font-weight: normal !important
color: white !important
.button
color: hsl(0deg, 0%, 40%) !important

::ng-deep app-input-toggle
height: 40px !important
45 changes: 45 additions & 0 deletions src/components/profile/section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { ActionSelectorComponent } from './action_selector'
import { InputNumberComponent } from 'components/input_number/input_number'
import { InputToggleComponent } from 'components/input_toggle/input_toggle'
import { WebusbService } from 'services/webusb'
import { Profile } from 'lib/profile'
import { CtrlSection, CtrlSectionMeta, CtrlButton, CtrlRotary } from 'lib/ctrl'
Expand All @@ -24,6 +25,7 @@ import { delay } from 'lib/delay'
CommonModule,
FormsModule,
InputNumberComponent,
InputToggleComponent,
ActionSelectorComponent,
],
templateUrl: './section.html',
Expand All @@ -41,6 +43,10 @@ export class SectionComponent {
pickerTune = 0
profileOverwriteIndex = 0
profiles = this.webusb.getProfiles()!
// Help dialog
dialogHelp: any
helpTitle = ''
helpText = ''
// Template aliases.
HID = HID
SectionIndex = SectionIndex
Expand Down Expand Up @@ -303,6 +309,45 @@ export class SectionComponent {
save = async () => {
await this.webusb.trySetSection(this.profileIndex, this.section)
}

showDialogHelp(key: string) {
const titles: {[key: string]: string} = {
gyroMomentum: "Enable Momentum",
gyroMomentumDamping: "Momentum Dampening",
gyroMomentumCutoff: "Momentum Cutoff Velocity",
}
const texts: {[key: string]: string} = {
gyroMomentum:
`When enabled, the mouse cursor will have "momementum" and continue moving after
disabling the gyro for a short time. The cursor will have the initial velocity at
the moment of disabling, and then it will slow down at an exponential rate.
This creates a more fluid, natural feel similar to trackpad or trackball momentum.<br><br>

Works in both TOUCH_ON mode (momentum continues after releasing the button) and TOUCH_OFF
mode (momentum continues after pressing the button to stop gyro input).`,
gyroMomentumDamping:
`Controls how quickly the momentum decays. Consider these values "friction" values.
with Higher values = faster decay.<br><br>
Separate values for vertical and horizontal allow you to customize the feel for each axis.
For example, you might want faster horizontal swipes but more controlled vertical movement.<br><br>
Recommended range: 1.0-20.0. Default: H=4.0, V=4.0`,
gyroMomentumCutoff:
`The minimum velocity (in pixels per second) below which momentum stops completely. At 250Hz,
The minimum visible velocity is about 250 pixels/second, but the cutoff can be set even lower.
This prevents the cursor from drifting indefinitely at very slow speeds.<br><br>
Lower values = momentum lasts longer but may feel floaty<br>
Higher values = momentum stops sooner but may feel abrupt<br><br>
Recommended range: 100-10000.0. Default: 500.0`,
}
this.helpTitle = titles[key]
this.helpText = texts[key]
this.dialogHelp = document.getElementById('dialog-help')
if (this.dialogHelp) this.dialogHelp.showModal()
}

hideDialogHelp() {
if (this.dialogHelp) this.dialogHelp.close()
}
}

interface SectionTitles {
Expand Down
49 changes: 39 additions & 10 deletions src/lib/ctrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,27 +608,56 @@ export class CtrlGyro extends CtrlSection {
public override sectionIndex: SectionIndex,
public mode: GyroMode,
public engage: number,
// Momentum settings
public momentumEnabled: boolean = false,
public momentumDampingH: number = 4.0,
public momentumDampingV: number = 4.0,
public momentumThreshold: number = 200.0,
) {
super(1, DeviceId.ALPAKKA, MessageType.SECTION_SHARE)
}

static override decode(buffer: Uint8Array) {
const data = Array.from(buffer)
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
// Buffer layout: 4-byte Ctrl header + 2-byte indices + 58-byte CtrlGyro struct
// CtrlGyro struct starts at buffer offset 6:
// byte 0 (buf 6): mode
// byte 1 (buf 7): engage
// byte 2 (buf 8): momentum_enabled
// byte 3 (buf 9): _reserved1 (padding)
// bytes 4-7 (buf 10-13): momentum_damping_h (float)
// bytes 8-11 (buf 14-17): momentum_damping_v (float)
// bytes 12-15 (buf 18-21): momentum_threshold (float)
return new CtrlGyro(
data[4], // ProfileIndex.
data[5], // SectionIndex.
data[6], // Gyro mode.
data[7], // Engage button.
data[4], // profileIndex
data[5], // sectionIndex
data[6], // mode
data[7], // engage
data[8] !== 0, // momentumEnabled
view.getFloat32(10, true), // momentumDampingH
view.getFloat32(14, true), // momentumDampingV
view.getFloat32(18, true), // momentumThreshold
)
}

override payload() {
return [
this.profileIndex,
this.sectionIndex,
Number(this.mode),
Number(this.engage),
]
// Payload: profileIndex (1) + sectionIndex (1) + CtrlGyro struct (58) = 60 bytes
const buffer = new ArrayBuffer(60)
const view = new DataView(buffer)
const arr = new Uint8Array(buffer)
arr[0] = this.profileIndex
arr[1] = this.sectionIndex
// CtrlGyro struct:
arr[2] = Number(this.mode) // byte 0: mode
arr[3] = Number(this.engage) // byte 1: engage
arr[4] = this.momentumEnabled ? 1 : 0 // byte 2: momentum_enabled
arr[5] = 0 // byte 3: _reserved1
view.setFloat32(6, this.momentumDampingH, true) // bytes 4-7
view.setFloat32(10, this.momentumDampingV, true) // bytes 8-11
view.setFloat32(14, this.momentumThreshold, true) // bytes 12-15
// bytes 16-57: padding (zeroed by ArrayBuffer)
return Array.from(arr)
}
}

Expand Down