Deadzone:
Override global deadzone
{{thumbstick.deadzone_override ? 'check_box' : 'check_box_outline_blank'}}
@@ -227,7 +211,7 @@
{{getSectionTitle()}}
{{getSectionTitle()}}
/>
-
+
+
+
+
+
+
Acceleration curve:
+
+
+
+
Sensitivity vertical ratio:
+
+
+
+
Sensitivity as mouse:
+
+
+
+
Sensitivity as scrollwheel:
+
+
+
+
Extras:
+
+
+ Push auto-toggle
+
+ {{thumbstick.push_auto_toggle ? 'check_box' : 'check_box_outline_blank'}}
+
+
+
+ Axis self-align
+
+ {{thumbstick.distance_mode ? 'check_box' : 'check_box_outline_blank'}}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/profile/section.sass b/src/components/profile/section.sass
index ec5313f..2a98f42 100644
--- a/src/components/profile/section.sass
+++ b/src/components/profile/section.sass
@@ -17,6 +17,27 @@
p
font-size: 14px
+ .tabs
+ width: 100%
+ display: flex
+ background-color: black
+ border-radius: 6px 6px 0 0
+ border-bottom: 3px solid $green
+ font-size: 14px
+ font-weight: bold
+ height: 32px
+ line-height: 32px
+ cursor: pointer
+ user-select: none
+ .tab
+ width: 50%
+ text-align: center
+ color: hsl(0deg, 0%, 50%)
+ border-radius: 6px 6px 0 0
+ &.active
+ color: black
+ background-color: $green
+
.island
border-radius: 6px
overflow: hidden
@@ -121,6 +142,16 @@
background-color: $yellow !important
color: black !important
+ .plots
+ display: flex
+ padding-top: 15px
+ .plot
+ background: black
+ border-radius: 10px
+ transform: scaleY(-1)
+ &:first-child
+ margin-right: 10px
+
#dialog-keypicker
min-width: 935px
min-height: 600px
diff --git a/src/components/profile/section.ts b/src/components/profile/section.ts
index 5172f50..65285f4 100644
--- a/src/components/profile/section.ts
+++ b/src/components/profile/section.ts
@@ -1,17 +1,17 @@
// SPDX-License-Identifier: GPL-2.0-only
// Copyright (C) 2023, Input Labs Oy.
-import { Component, Input } from '@angular/core'
+import { Component, Input, ViewChild, ElementRef} from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { ActionSelectorComponent } from './action_selector'
import { InputNumberComponent } from 'components/input_number/input_number'
import { WebusbService } from 'services/webusb'
import { Profile } from 'lib/profile'
-import { CtrlSection, CtrlSectionMeta, CtrlButton, CtrlRotary } from 'lib/ctrl'
+import { CtrlSection, CtrlSectionMeta, CtrlButton, CtrlRotary, ConfigIndex } from 'lib/ctrl'
import { CtrlThumbstick, CtrlGyro, CtrlGyroAxis, CtrlHome } from 'lib/ctrl'
import { SectionIndex, sectionIsAnalog } from 'lib/ctrl'
-import { ThumbstickMode, ThumbstickDistanceMode, GyroMode } from 'lib/ctrl'
+import { ThumbstickMode, GyroMode } from 'lib/ctrl'
import { ActionGroup } from 'lib/actions'
import { HID, isAxis } from 'lib/hid'
import { PinV0, PinV1 } from 'lib/pin'
@@ -41,16 +41,27 @@ export class SectionComponent {
pickerTune = 0
profileOverwriteIndex = 0
profiles = this.webusb.getProfiles()!
+ globalDeadzone = 0
+ tab = 0
+ canvasCircle!: ElementRef
+ canvasRamp!: ElementRef
+ green = 'hsl(160deg, 100%, 50%)'
+ purple = 'hsl(266deg, 100%, 50%)'
// Template aliases.
HID = HID
SectionIndex = SectionIndex
GyroMode = GyroMode
ThumbstickMode = ThumbstickMode
- ThumbstickDistanceMode = ThumbstickDistanceMode
constructor(
public webusb: WebusbService,
- ) {}
+ ) {
+ this.afterConstructor()
+ }
+
+ async afterConstructor() {
+ this.globalDeadzone = await this.fetchGlobalDeadzone()
+ }
sectionIsMeta = () => this.section instanceof CtrlSectionMeta
sectionIsButton = () => this.section instanceof CtrlButton && !(this.section instanceof CtrlHome)
@@ -67,6 +78,18 @@ export class SectionComponent {
getSectionAsGyro = () => this.section as CtrlGyro
getSectionAsGyroAxis = () => this.section as CtrlGyroAxis
+ @ViewChild('_canvasCircle') set _canvasCircle(canvas: ElementRef) {
+ if (!canvas) return
+ this.canvasCircle = canvas
+ this.plot()
+ }
+
+ @ViewChild('_canvasRamp') set _canvasRamp(canvas: ElementRef) {
+ if (!canvas) return
+ this.canvasRamp = canvas
+ this.plot()
+ }
+
getSectionTitle() {
return sectionTitles[this.section.sectionIndex]
}
@@ -86,6 +109,11 @@ export class SectionComponent {
else return PinV1
}
+ async fetchGlobalDeadzone() {
+ const preset = await this.webusb.tryGetConfig(ConfigIndex.DEADZONE)
+ return preset.values[preset.presetIndex]
+ }
+
isButtonBlockVisible(group: number) {
const button = this.getSectionAsButton()
if (group == 0) return true
@@ -158,6 +186,129 @@ export class SectionComponent {
a.remove()
}
+ plot() {
+ this.plotCircle()
+ this.plotRamp()
+ }
+
+ plotCircle = () => {
+ if (!this.canvasCircle) return
+ const ctx = this.canvasCircle.nativeElement.getContext('2d')!
+ const thumbstick = this.getSectionAsThumbstick()
+ const deadzone = thumbstick.deadzone_override ? thumbstick.deadzone : this.globalDeadzone
+ let overlap = thumbstick.overlap
+ if (overlap == 0) overlap = -2.5 // Force a visual gap when value is zero.
+ const size = this.canvasCircle.nativeElement.width
+ const mid = size / 2
+ const max = size * 0.4
+ let overlapDeg = (50-overlap) / 100 * 90
+ let overlapDegNeg = -overlap / 100 * 90
+ // Helper functions.
+ const deg = (angle: number) => {
+ return angle * (Math.PI / 180)
+ }
+ const drawArc = (stroke: number, angle: number, size: number) => {
+ let half = size / 2
+ ctx.beginPath();
+ ctx.arc(mid, mid, max, angle-half, angle+half)
+ ctx.strokeStyle = this.green
+ ctx.lineWidth = stroke
+ ctx.stroke()
+ }
+ const drawCircle = (radius: number, lineWidth: number) => {
+ ctx.beginPath()
+ ctx.arc(mid, mid, radius, 0, deg(360))
+ ctx.strokeStyle = this.purple
+ ctx.lineWidth = lineWidth
+ ctx.stroke()
+ }
+ // Draw.
+ ctx.clearRect(0, 0, size, size);
+ if (overlap > 0) {
+ drawArc(3, deg(0), deg(45+overlapDeg))
+ drawArc(3, deg(90), deg(45+overlapDeg))
+ drawArc(3, deg(180), deg(45+overlapDeg))
+ drawArc(3, deg(270), deg(45+overlapDeg))
+ }
+ if (overlap > 0) {
+ drawArc(1, deg(45+0), deg(45-overlapDeg))
+ drawArc(1, deg(45+90), deg(45-overlapDeg))
+ drawArc(1, deg(45+180), deg(45-overlapDeg))
+ drawArc(1, deg(45+270), deg(45-overlapDeg))
+ }
+ if (overlap < 0) {
+ drawArc(3, deg(0), deg(90-overlapDegNeg))
+ drawArc(3, deg(90), deg(90-overlapDegNeg))
+ drawArc(3, deg(180), deg(90-overlapDegNeg))
+ drawArc(3, deg(270), deg(90-overlapDegNeg))
+ }
+ drawCircle(deadzone*max/100, 3)
+ drawCircle(thumbstick.outer_threshold*max/100, 3)
+ }
+
+ plotRamp = () => {
+ if (!this.canvasRamp) return
+ const ctx = this.canvasRamp.nativeElement.getContext('2d')!
+ const thumbstick = this.getSectionAsThumbstick()
+ const deadzone = thumbstick.deadzone_override ? thumbstick.deadzone : this.globalDeadzone
+ const size = this.canvasRamp.nativeElement.width
+ const min = 2
+ const max = size - 2
+ type Point = {x: number, y:number}
+ const pointA = {x: min, y: min}
+ const pointB = {x: min + deadzone/100*max, y: min}
+ const pointC = {x: pointB.x, y: min + thumbstick.antideadzone/100*max}
+ const pointD = {x: thumbstick.saturation/100*max, y: max}
+ const pointE = {x: max, y: max}
+ // Accel curve.
+ const curvePoints = 20
+ const startX = min + (deadzone / 100) * max
+ const startY = min + (thumbstick.antideadzone / 100) * max
+ const endX = (thumbstick.saturation / 100) * max
+ const scaleX = endX - startX
+ const scaleY = max - startY
+ // Helper functions.
+ const drawVert = (x: number) => {
+ ctx.beginPath();
+ ctx.moveTo(x, min);
+ ctx.lineTo(x, max);
+ ctx.strokeStyle = this.purple;
+ ctx.lineWidth = 3;
+ ctx.stroke();
+ }
+ const drawLines = (points: Point[]) => {
+ ctx.beginPath()
+ ctx.moveTo(points[0].x, points[0].y)
+ for (const point of points.slice(1)) {
+ ctx.lineTo(point.x, point.y)
+ }
+ ctx.strokeStyle = this.green
+ ctx.lineWidth = 3
+ ctx.stroke()
+ }
+ const felixCurve = (x: number, k: number) => {
+ return (x*k+x) / (2*x*k-k+1)
+ }
+ let pointsCtoD: Point[] = []
+ for(let i=0; i<=curvePoints; i++) {
+ const k = thumbstick.accel_curve / 100
+ let x = i / curvePoints
+ let y = felixCurve(x, k)
+ x *= scaleX
+ y *= scaleY
+ x += startX
+ y += startY
+ pointsCtoD.push({x, y})
+ }
+ // Draw.
+ ctx.clearRect(0, 0, size, size);
+ drawVert(pointB.x)
+ drawVert(thumbstick.outer_threshold / 100 * max)
+ drawLines([pointA, pointB, pointC])
+ drawLines(pointsCtoD) // Curve.
+ drawLines([pointD, pointE])
+ }
+
showDialogKeypicker = (pickerGroup: number) => {
this.pickerGroup = pickerGroup
const section = this.section as (CtrlButton | CtrlRotary | CtrlGyroAxis)
diff --git a/src/lib/ctrl.ts b/src/lib/ctrl.ts
index 0c3809d..1b4e1e3 100644
--- a/src/lib/ctrl.ts
+++ b/src/lib/ctrl.ts
@@ -133,11 +133,6 @@ export enum ThumbstickMode {
DIR8,
}
-export enum ThumbstickDistanceMode {
- AXIAL,
- RADIAL,
-}
-
export enum GyroMode {
OFF,
ALWAYS_ON,
@@ -560,12 +555,18 @@ export class CtrlThumbstick extends CtrlSection {
public override profileIndex: number,
public override sectionIndex: SectionIndex,
public mode: ThumbstickMode,
- public distance_mode: ThumbstickDistanceMode,
+ public distance_mode: boolean,
public deadzone: number,
public overlap : number,
public deadzone_override: boolean,
public antideadzone: number,
public saturation: number,
+ public outer_threshold: number,
+ public push_auto_toggle: boolean,
+ public sens_mouse: number,
+ public sens_scroll: number,
+ public sens_xy_ratio: number,
+ public accel_curve: number,
) {
super(1, DeviceId.ALPAKKA, MessageType.SECTION_SHARE)
}
@@ -578,12 +579,18 @@ export class CtrlThumbstick extends CtrlSection {
data[4], // ProfileIndex.
data[5], // SectionIndex.
data[6], // Mode.
- data[7], // Distance mode.
+ Boolean(data[7]), // Distance mode / Axis self align.
data[8], // Deadzone.
data[9] <= 128 ? data[9] : data[9]-256, // Axis overlap (unsigned to signed).
Boolean(data[10]), // Deadzone override.
data[11], // Antideadzone.
data[12] > 0 ? data[12] : 100, // Saturation.
+ data[13] > 0 ? data[13] : 80, // Outer threshold.
+ Boolean(data[14]), // Push auto-toggle.
+ data[15] > 0 ? data[15] : 10, // Sens mouse.
+ data[16] > 0 ? data[16] : 10, // Sens scroll.
+ data[17] > 0 ? data[17] : 100, // Sens Y ratio.
+ data[18] <= 128 ? data[18] : data[18]-256, // Accel (unsigned to signed).
)
}
@@ -598,6 +605,12 @@ export class CtrlThumbstick extends CtrlSection {
Number(this.deadzone_override),
this.antideadzone,
this.saturation,
+ this.outer_threshold,
+ Number(this.push_auto_toggle),
+ this.sens_mouse,
+ this.sens_scroll,
+ this.sens_xy_ratio,
+ this.accel_curve,
]
}
}
diff --git a/src/lib/profile.ts b/src/lib/profile.ts
index e92b652..4e0c2fd 100644
--- a/src/lib/profile.ts
+++ b/src/lib/profile.ts
@@ -6,6 +6,10 @@ import { SectionIndex, CtrlGyro, CtrlGyroAxis, CtrlHome } from 'lib/ctrl'
import { ActionGroup } from 'lib/actions'
import { HID } from 'lib/hid'
+const getDefaultThumbstick = () => {
+ return new CtrlThumbstick(0, 0, 0, !!0, 0, 0, false, 0, 0, 80, false, 100, 10, 1, 0)
+}
+
export class Profile {
home: CtrlHome
@@ -30,7 +34,7 @@ export class Profile {
public buttonR2: CtrlButton = new CtrlButton(0, 0, 0),
public buttonR4: CtrlButton = new CtrlButton(0, 0, 0),
// Left stick.
- public settingsLStick: CtrlThumbstick = new CtrlThumbstick(0, 0, 0, 0, 0, 0, false, 0, 0),
+ public settingsLStick: CtrlThumbstick = getDefaultThumbstick(),
public buttonLStickLeft: CtrlButton = new CtrlButton(0, 0, 0),
public buttonLStickRight: CtrlButton = new CtrlButton(0, 0, 0),
public buttonLStickUp: CtrlButton = new CtrlButton(0, 0, 0),
@@ -43,7 +47,7 @@ export class Profile {
public buttonLStickInner: CtrlButton = new CtrlButton(0, 0, 0),
public buttonLStickOuter: CtrlButton = new CtrlButton(0, 0, 0),
// Right stick (stick or dhat).
- public settingsRStick: CtrlThumbstick = new CtrlThumbstick(0, 0, 0, 0, 0, 0, false, 0, 0),
+ public settingsRStick: CtrlThumbstick = getDefaultThumbstick(),
public buttonRStickLeft: CtrlButton = new CtrlButton(0, 0, 0),
public buttonRStickRight: CtrlButton = new CtrlButton(0, 0, 0),
public buttonRStickUp: CtrlButton = new CtrlButton(0, 0, 0),
diff --git a/src/lib/profiles.ts b/src/lib/profiles.ts
index bcbf839..1441541 100644
--- a/src/lib/profiles.ts
+++ b/src/lib/profiles.ts
@@ -14,7 +14,6 @@ import {
CtrlThumbstick,
CtrlSection,
ThumbstickMode,
- ThumbstickDistanceMode
} from 'lib/ctrl'
const NUMBER_OF_PROFILES = 13 // Home + 12 builtin.
@@ -180,12 +179,18 @@ export class Profiles {
sections[0].profileIndex,
SectionIndex.RSTICK_SETTINGS,
ThumbstickMode.DIR8,
- ThumbstickDistanceMode.AXIAL,
+ false, // Distance mode / Ignore misalignment.
60, // Deadzone.
50, // Axis overlap (unsigned to signed).
true, // Deadzone override.
0, // Antideadzone.
70, // Saturation.
+ 80, // Outer threshold.
+ false, // Push auto-toggle.
+ 100, // Sens mouse.
+ 10, // Sens scroll.
+ 100, // Sens Y ratio.
+ 0, // Accel.
)
sections.push(rStickSection)
}