@benev/tact 0.1.0-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +299 -0
  3. package/package.json +60 -0
  4. package/s/demo/main.bundle.ts +14 -0
  5. package/s/demo/main.css +57 -0
  6. package/s/index.html.ts +42 -0
  7. package/s/index.ts +16 -0
  8. package/s/nubs/lookpad/styles.ts +21 -0
  9. package/s/nubs/lookpad/utils/listeners.ts +53 -0
  10. package/s/nubs/lookpad/view.ts +32 -0
  11. package/s/nubs/stick/device.ts +30 -0
  12. package/s/nubs/stick/styles.ts +22 -0
  13. package/s/nubs/stick/utils/calculate_new_vector_from_pointer_position.ts +27 -0
  14. package/s/nubs/stick/utils/find_closest_point_on_circle.ts +15 -0
  15. package/s/nubs/stick/utils/make_pointer_listeners.ts +50 -0
  16. package/s/nubs/stick/utils/within_radius.ts +6 -0
  17. package/s/nubs/stick/view.ts +50 -0
  18. package/s/nubs/stick-graphic/styles.ts +38 -0
  19. package/s/nubs/stick-graphic/types/basis.ts +5 -0
  20. package/s/nubs/stick-graphic/utils/calculate_basis.ts +19 -0
  21. package/s/nubs/stick-graphic/utils/stick_vector_to_pixels.ts +13 -0
  22. package/s/nubs/stick-graphic/utils/transform.ts +10 -0
  23. package/s/nubs/stick-graphic/view.ts +43 -0
  24. package/s/nubs/virtual-gamepad/device.ts +25 -0
  25. package/s/nubs/virtual-gamepad/styles.css.ts +133 -0
  26. package/s/nubs/virtual-gamepad/utils/gamepad-inputs.ts +42 -0
  27. package/s/nubs/virtual-gamepad/utils/prevent-default-touch-shenanigans.ts +12 -0
  28. package/s/nubs/virtual-gamepad/utils/touch-tracking.ts +75 -0
  29. package/s/nubs/virtual-gamepad/view.ts +139 -0
  30. package/s/station/devices/gamepad.ts +81 -0
  31. package/s/station/devices/infra/device.ts +7 -0
  32. package/s/station/devices/infra/group.ts +17 -0
  33. package/s/station/devices/infra/sampler.ts +22 -0
  34. package/s/station/devices/keyboard.ts +53 -0
  35. package/s/station/devices/pointer.ts +95 -0
  36. package/s/station/parts/action.ts +26 -0
  37. package/s/station/parts/defaults.ts +28 -0
  38. package/s/station/parts/resolver.ts +73 -0
  39. package/s/station/parts/routines/aggregate_samples_into_map.ts +20 -0
  40. package/s/station/parts/routines/build_updatable_actions_structure.ts +29 -0
  41. package/s/station/parts/routines/lensing_algorithm.ts +74 -0
  42. package/s/station/parts/switchboard-bindings.ts +21 -0
  43. package/s/station/station.test.ts +86 -0
  44. package/s/station/station.ts +47 -0
  45. package/s/station/switchboard.ts +107 -0
  46. package/s/station/testing/testing.ts +47 -0
  47. package/s/station/types.ts +72 -0
  48. package/s/station/utils/is-pressed.ts +5 -0
  49. package/s/station/utils/modprefix.ts +16 -0
  50. package/s/station/utils/tmax.ts +7 -0
  51. package/s/station/utils/tmin.ts +7 -0
  52. package/s/tests.test.ts +8 -0
  53. package/s/utils/evergreen.ts +10 -0
  54. package/s/utils/gamepads.ts +41 -0
  55. package/s/utils/split-axis.ts +7 -0
  56. package/x/demo/main.bundle.d.ts +1 -0
  57. package/x/demo/main.bundle.js +11 -0
  58. package/x/demo/main.bundle.js.map +1 -0
  59. package/x/demo/main.bundle.min.js +139 -0
  60. package/x/demo/main.bundle.min.js.map +7 -0
  61. package/x/demo/main.css +57 -0
  62. package/x/index.d.ts +13 -0
  63. package/x/index.html +97 -0
  64. package/x/index.html.d.ts +2 -0
  65. package/x/index.html.js +37 -0
  66. package/x/index.html.js.map +1 -0
  67. package/x/index.js +14 -0
  68. package/x/index.js.map +1 -0
  69. package/x/nubs/lookpad/styles.d.ts +1 -0
  70. package/x/nubs/lookpad/styles.js +21 -0
  71. package/x/nubs/lookpad/styles.js.map +1 -0
  72. package/x/nubs/lookpad/utils/listeners.d.ts +19 -0
  73. package/x/nubs/lookpad/utils/listeners.js +37 -0
  74. package/x/nubs/lookpad/utils/listeners.js.map +1 -0
  75. package/x/nubs/lookpad/view.d.ts +1 -0
  76. package/x/nubs/lookpad/view.js +24 -0
  77. package/x/nubs/lookpad/view.js.map +1 -0
  78. package/x/nubs/stick/device.d.ts +15 -0
  79. package/x/nubs/stick/device.js +27 -0
  80. package/x/nubs/stick/device.js.map +1 -0
  81. package/x/nubs/stick/styles.d.ts +1 -0
  82. package/x/nubs/stick/styles.js +22 -0
  83. package/x/nubs/stick/styles.js.map +1 -0
  84. package/x/nubs/stick/utils/calculate_new_vector_from_pointer_position.d.ts +3 -0
  85. package/x/nubs/stick/utils/calculate_new_vector_from_pointer_position.js +16 -0
  86. package/x/nubs/stick/utils/calculate_new_vector_from_pointer_position.js.map +1 -0
  87. package/x/nubs/stick/utils/find_closest_point_on_circle.d.ts +2 -0
  88. package/x/nubs/stick/utils/find_closest_point_on_circle.js +6 -0
  89. package/x/nubs/stick/utils/find_closest_point_on_circle.js.map +1 -0
  90. package/x/nubs/stick/utils/make_pointer_listeners.d.ts +16 -0
  91. package/x/nubs/stick/utils/make_pointer_listeners.js +34 -0
  92. package/x/nubs/stick/utils/make_pointer_listeners.js.map +1 -0
  93. package/x/nubs/stick/utils/within_radius.d.ts +2 -0
  94. package/x/nubs/stick/utils/within_radius.js +4 -0
  95. package/x/nubs/stick/utils/within_radius.js.map +1 -0
  96. package/x/nubs/stick/view.d.ts +2 -0
  97. package/x/nubs/stick/view.js +38 -0
  98. package/x/nubs/stick/view.js.map +1 -0
  99. package/x/nubs/stick-graphic/styles.d.ts +1 -0
  100. package/x/nubs/stick-graphic/styles.js +38 -0
  101. package/x/nubs/stick-graphic/styles.js.map +1 -0
  102. package/x/nubs/stick-graphic/types/basis.d.ts +4 -0
  103. package/x/nubs/stick-graphic/types/basis.js +2 -0
  104. package/x/nubs/stick-graphic/types/basis.js.map +1 -0
  105. package/x/nubs/stick-graphic/utils/calculate_basis.d.ts +2 -0
  106. package/x/nubs/stick-graphic/utils/calculate_basis.js +10 -0
  107. package/x/nubs/stick-graphic/utils/calculate_basis.js.map +1 -0
  108. package/x/nubs/stick-graphic/utils/stick_vector_to_pixels.d.ts +2 -0
  109. package/x/nubs/stick-graphic/utils/stick_vector_to_pixels.js +7 -0
  110. package/x/nubs/stick-graphic/utils/stick_vector_to_pixels.js.map +1 -0
  111. package/x/nubs/stick-graphic/utils/transform.d.ts +2 -0
  112. package/x/nubs/stick-graphic/utils/transform.js +7 -0
  113. package/x/nubs/stick-graphic/utils/transform.js.map +1 -0
  114. package/x/nubs/stick-graphic/view.d.ts +3 -0
  115. package/x/nubs/stick-graphic/view.js +30 -0
  116. package/x/nubs/stick-graphic/view.js.map +1 -0
  117. package/x/nubs/virtual-gamepad/device.d.ts +7 -0
  118. package/x/nubs/virtual-gamepad/device.js +20 -0
  119. package/x/nubs/virtual-gamepad/device.js.map +1 -0
  120. package/x/nubs/virtual-gamepad/styles.css.d.ts +2 -0
  121. package/x/nubs/virtual-gamepad/styles.css.js +133 -0
  122. package/x/nubs/virtual-gamepad/styles.css.js.map +1 -0
  123. package/x/nubs/virtual-gamepad/utils/gamepad-inputs.d.ts +29 -0
  124. package/x/nubs/virtual-gamepad/utils/gamepad-inputs.js +31 -0
  125. package/x/nubs/virtual-gamepad/utils/gamepad-inputs.js.map +1 -0
  126. package/x/nubs/virtual-gamepad/utils/prevent-default-touch-shenanigans.d.ts +1 -0
  127. package/x/nubs/virtual-gamepad/utils/prevent-default-touch-shenanigans.js +9 -0
  128. package/x/nubs/virtual-gamepad/utils/prevent-default-touch-shenanigans.js.map +1 -0
  129. package/x/nubs/virtual-gamepad/utils/touch-tracking.d.ts +6 -0
  130. package/x/nubs/virtual-gamepad/utils/touch-tracking.js +55 -0
  131. package/x/nubs/virtual-gamepad/utils/touch-tracking.js.map +1 -0
  132. package/x/nubs/virtual-gamepad/view.d.ts +2 -0
  133. package/x/nubs/virtual-gamepad/view.js +120 -0
  134. package/x/nubs/virtual-gamepad/view.js.map +1 -0
  135. package/x/station/devices/gamepad.d.ts +10 -0
  136. package/x/station/devices/gamepad.js +70 -0
  137. package/x/station/devices/gamepad.js.map +1 -0
  138. package/x/station/devices/infra/device.d.ts +4 -0
  139. package/x/station/devices/infra/device.js +3 -0
  140. package/x/station/devices/infra/device.js.map +1 -0
  141. package/x/station/devices/infra/group.d.ts +7 -0
  142. package/x/station/devices/infra/group.js +13 -0
  143. package/x/station/devices/infra/group.js.map +1 -0
  144. package/x/station/devices/infra/sampler.d.ts +8 -0
  145. package/x/station/devices/infra/sampler.js +17 -0
  146. package/x/station/devices/infra/sampler.js.map +1 -0
  147. package/x/station/devices/keyboard.d.ts +9 -0
  148. package/x/station/devices/keyboard.js +42 -0
  149. package/x/station/devices/keyboard.js.map +1 -0
  150. package/x/station/devices/pointer.d.ts +11 -0
  151. package/x/station/devices/pointer.js +79 -0
  152. package/x/station/devices/pointer.js.map +1 -0
  153. package/x/station/parts/action.d.ts +12 -0
  154. package/x/station/parts/action.js +23 -0
  155. package/x/station/parts/action.js.map +1 -0
  156. package/x/station/parts/defaults.d.ts +5 -0
  157. package/x/station/parts/defaults.js +22 -0
  158. package/x/station/parts/defaults.js.map +1 -0
  159. package/x/station/parts/resolver.d.ts +10 -0
  160. package/x/station/parts/resolver.js +63 -0
  161. package/x/station/parts/resolver.js.map +1 -0
  162. package/x/station/parts/routines/aggregate_samples_into_map.d.ts +3 -0
  163. package/x/station/parts/routines/aggregate_samples_into_map.js +11 -0
  164. package/x/station/parts/routines/aggregate_samples_into_map.js.map +1 -0
  165. package/x/station/parts/routines/build_updatable_actions_structure.d.ts +5 -0
  166. package/x/station/parts/routines/build_updatable_actions_structure.js +18 -0
  167. package/x/station/parts/routines/build_updatable_actions_structure.js.map +1 -0
  168. package/x/station/parts/routines/lensing_algorithm.d.ts +2 -0
  169. package/x/station/parts/routines/lensing_algorithm.js +42 -0
  170. package/x/station/parts/routines/lensing_algorithm.js.map +1 -0
  171. package/x/station/parts/switchboard-bindings.d.ts +2 -0
  172. package/x/station/parts/switchboard-bindings.js +19 -0
  173. package/x/station/parts/switchboard-bindings.js.map +1 -0
  174. package/x/station/station.d.ts +15 -0
  175. package/x/station/station.js +35 -0
  176. package/x/station/station.js.map +1 -0
  177. package/x/station/station.test.d.ts +11 -0
  178. package/x/station/station.test.js +80 -0
  179. package/x/station/station.test.js.map +1 -0
  180. package/x/station/switchboard.d.ts +30 -0
  181. package/x/station/switchboard.js +90 -0
  182. package/x/station/switchboard.js.map +1 -0
  183. package/x/station/testing/testing.d.ts +58 -0
  184. package/x/station/testing/testing.js +39 -0
  185. package/x/station/testing/testing.js.map +1 -0
  186. package/x/station/types.d.ts +56 -0
  187. package/x/station/types.js +5 -0
  188. package/x/station/types.js.map +1 -0
  189. package/x/station/utils/is-pressed.d.ts +1 -0
  190. package/x/station/utils/is-pressed.js +4 -0
  191. package/x/station/utils/is-pressed.js.map +1 -0
  192. package/x/station/utils/modprefix.d.ts +1 -0
  193. package/x/station/utils/modprefix.js +16 -0
  194. package/x/station/utils/modprefix.js.map +1 -0
  195. package/x/station/utils/tmax.d.ts +1 -0
  196. package/x/station/utils/tmax.js +6 -0
  197. package/x/station/utils/tmax.js.map +1 -0
  198. package/x/station/utils/tmin.d.ts +1 -0
  199. package/x/station/utils/tmin.js +6 -0
  200. package/x/station/utils/tmin.js.map +1 -0
  201. package/x/tests.test.d.ts +1 -0
  202. package/x/tests.test.js +6 -0
  203. package/x/tests.test.js.map +1 -0
  204. package/x/utils/evergreen.d.ts +1 -0
  205. package/x/utils/evergreen.js +10 -0
  206. package/x/utils/evergreen.js.map +1 -0
  207. package/x/utils/gamepads.d.ts +14 -0
  208. package/x/utils/gamepads.js +40 -0
  209. package/x/utils/gamepads.js.map +1 -0
  210. package/x/utils/split-axis.d.ts +1 -0
  211. package/x/utils/split-axis.js +6 -0
  212. package/x/utils/split-axis.js.map +1 -0
@@ -0,0 +1,139 @@
1
+
2
+ import {html} from "lit"
3
+ import {view} from "@e280/sly"
4
+ import {ev, MapG} from "@e280/stz"
5
+
6
+ import stylesCss from "./styles.css.js"
7
+
8
+ import {NubStick} from "../stick/view.js"
9
+ import {VirtualGamepadDevice} from "./device.js"
10
+ import {GamepadInputs} from "./utils/gamepad-inputs.js"
11
+ import {touchTracking} from "./utils/touch-tracking.js"
12
+ import {preventDefaultTouchShenanigans} from "./utils/prevent-default-touch-shenanigans.js"
13
+
14
+ export const VirtualGamepad = view(use => (device: VirtualGamepadDevice) => {
15
+ use.name("virtual-gamepad")
16
+ use.css(stylesCss)
17
+
18
+ const buttons = use.once(() => new Set<HTMLButtonElement>())
19
+ const codes = use.once(() => new MapG<HTMLButtonElement, keyof GamepadInputs>())
20
+
21
+ use.rendered.then(() => {
22
+ const elements = Array.from(
23
+ use.shadow.querySelectorAll<HTMLButtonElement>("button[x-code]")
24
+ )
25
+ for (const button of elements) {
26
+ const code = button.getAttribute("x-code")
27
+ if (code) {
28
+ buttons.add(button)
29
+ codes.set(button, code as keyof GamepadInputs)
30
+ }
31
+ }
32
+ })
33
+
34
+ use.mount(() => preventDefaultTouchShenanigans())
35
+
36
+ use.mount(() => touchTracking({
37
+ target: use.shadow,
38
+ buttons,
39
+ touchdown: button => {
40
+ const code = codes.require(button)
41
+ device.setSample(code, 1)
42
+ },
43
+ touchup: button => {
44
+ const code = codes.require(button)
45
+ device.setSample(code, 0)
46
+ },
47
+ }))
48
+
49
+ use.mount(() => ev(use.shadow, {
50
+ contextmenu: (e: Event) => e.preventDefault(),
51
+ }))
52
+
53
+ function button(code: string, label: string) {
54
+ return html`
55
+ <button x-code="${code}">${label}</button>
56
+ `
57
+ }
58
+
59
+ function renderDPad() {
60
+ return html`
61
+ <div class=pad>
62
+ <div>
63
+ ${button("gamepad.left", "w")}
64
+ </div>
65
+ <div>
66
+ ${button("gamepad.up", "n")}
67
+ ${button("gamepad.down", "s")}
68
+ </div>
69
+ <div>
70
+ ${button("gamepad.right", "e")}
71
+ </div>
72
+ </div>
73
+ `
74
+ }
75
+
76
+ function renderButtonPad() {
77
+ return html`
78
+ <div class=pad>
79
+ <div>
80
+ ${button("gamepad.x", "x")}
81
+ </div>
82
+ <div>
83
+ ${button("gamepad.y", "y")}
84
+ ${button("gamepad.a", "a")}
85
+ </div>
86
+ <div>
87
+ ${button("gamepad.b", "b")}
88
+ </div>
89
+ </div>
90
+ `
91
+ }
92
+
93
+ function renderLeftShoulder() {
94
+ return html`
95
+ <div class=shoulder>
96
+ ${button("gamepad.trigger.left", "lt")}
97
+ ${button("gamepad.bumper.left", "lb")}
98
+ ${button("gamepad.stick.left.click", "lc")}
99
+ </div>
100
+ `
101
+ }
102
+
103
+ function renderRightShoulder() {
104
+ return html`
105
+ <div class=shoulder>
106
+ ${button("gamepad.trigger.right", "rt")}
107
+ ${button("gamepad.bumper.right", "rb")}
108
+ ${button("gamepad.stick.right.click", "rc")}
109
+ </div>
110
+ `
111
+ }
112
+
113
+ return html`
114
+ <div class=upper>
115
+ ${button("gamepad.alpha", "alpha")}
116
+ ${button("gamepad.beta", "beta")}
117
+ ${button("gamepad.gamma", "gamma")}
118
+ </div>
119
+
120
+ <div class=lower>
121
+ <div class="left side">
122
+ ${renderLeftShoulder()}
123
+ ${renderDPad()}
124
+ ${NubStick
125
+ .attr("class", "stick")
126
+ .props(device.stickLeft)}
127
+ </div>
128
+
129
+ <div class="right side">
130
+ ${renderRightShoulder()}
131
+ ${renderButtonPad()}
132
+ ${NubStick
133
+ .attr("class", "stick")
134
+ .props(device.stickRight)}
135
+ </div>
136
+ </div>
137
+ `
138
+ })
139
+
@@ -0,0 +1,81 @@
1
+
2
+ import {tmax} from "../utils/tmax.js"
3
+ import {SamplerDevice} from "./infra/sampler.js"
4
+ import {splitAxis} from "../../utils/split-axis.js"
5
+ import {gamepads, Pad} from "../../utils/gamepads.js"
6
+
7
+ const gamepadButtonCodes = [
8
+ "gamepad.a",
9
+ "gamepad.b",
10
+ "gamepad.x",
11
+ "gamepad.y",
12
+ "gamepad.bumper.left",
13
+ "gamepad.bumper.right",
14
+ "gamepad.trigger.left",
15
+ "gamepad.trigger.right",
16
+ "gamepad.alpha",
17
+ "gamepad.beta",
18
+ "gamepad.stick.left.click",
19
+ "gamepad.stick.right.click",
20
+ "gamepad.up",
21
+ "gamepad.down",
22
+ "gamepad.left",
23
+ "gamepad.right",
24
+ "gamepad.gamma",
25
+ ]
26
+
27
+ export class GamepadDevice extends SamplerDevice {
28
+ static on(fn: (device: GamepadDevice) => () => void) {
29
+ return gamepads(pad => fn(new this(pad)))
30
+ }
31
+
32
+ constructor(public pad: Pad) {
33
+ super()
34
+ }
35
+
36
+ get gamepad() {
37
+ return this.pad.gamepad
38
+ }
39
+
40
+ takeSamples() {
41
+ const {gamepad} = this.pad
42
+ this.#pollButtons(gamepad)
43
+ this.#pollSticks(gamepad)
44
+ return super.takeSamples()
45
+ }
46
+
47
+ #pollButtons(gamepad: Gamepad) {
48
+ let anyButtonValue = 0
49
+
50
+ const recordAny = (value: number) => {
51
+ anyButtonValue = tmax([anyButtonValue, value])
52
+ }
53
+
54
+ for (const [index, code] of gamepadButtonCodes.entries()) {
55
+ const value = gamepad.buttons.at(index)?.value ?? 0
56
+ recordAny(value)
57
+ this.setSample(code, value)
58
+ }
59
+
60
+ this.setSample("gamepad.any", anyButtonValue)
61
+ }
62
+
63
+ #pollSticks(gamepad: Gamepad) {
64
+ const [leftY, leftX, rightY, rightX] = gamepad.axes
65
+
66
+ const [leftUp, leftDown] = splitAxis(leftX)
67
+ const [leftLeft, leftRight] = splitAxis(leftY)
68
+ this.setSample("gamepad.stick.left.up", leftUp)
69
+ this.setSample("gamepad.stick.left.down", leftDown)
70
+ this.setSample("gamepad.stick.left.left", leftLeft)
71
+ this.setSample("gamepad.stick.left.right", leftRight)
72
+
73
+ const [rightUp, rightDown] = splitAxis(rightX)
74
+ const [rightLeft, rightRight] = splitAxis(rightY)
75
+ this.setSample("gamepad.stick.right.up", rightUp)
76
+ this.setSample("gamepad.stick.right.down", rightDown)
77
+ this.setSample("gamepad.stick.right.left", rightLeft)
78
+ this.setSample("gamepad.stick.right.right", rightRight)
79
+ }
80
+ }
81
+
@@ -0,0 +1,7 @@
1
+
2
+ import {Sample} from "../../types.js"
3
+
4
+ export abstract class Device {
5
+ abstract takeSamples(): Sample[]
6
+ }
7
+
@@ -0,0 +1,17 @@
1
+
2
+ import {SetG} from "@e280/stz"
3
+ import {Device} from "./device.js"
4
+
5
+ export class GroupDevice extends Device {
6
+ devices = new SetG<Device>()
7
+
8
+ constructor(...devices: Device[]) {
9
+ super()
10
+ this.devices.adds(...devices)
11
+ }
12
+
13
+ takeSamples() {
14
+ return [...this.devices].flatMap(device => device.takeSamples())
15
+ }
16
+ }
17
+
@@ -0,0 +1,22 @@
1
+
2
+ import {sub} from "@e280/stz"
3
+ import {Device} from "./device.js"
4
+ import {Sample, SampleMap} from "../../types.js"
5
+
6
+ export class SamplerDevice extends Device {
7
+ on = sub<Sample>()
8
+ #map: SampleMap = new Map()
9
+
10
+ setSample(code: string, value: number) {
11
+ this.#map.set(code, value)
12
+ this.on.pub(code, value)
13
+ return this
14
+ }
15
+
16
+ takeSamples(): Sample[] {
17
+ const samples = [...this.#map]
18
+ this.#map.clear()
19
+ return samples
20
+ }
21
+ }
22
+
@@ -0,0 +1,53 @@
1
+
2
+ import {coalesce, ev, sub} from "@e280/stz"
3
+ import {Sample} from "../types.js"
4
+ import {Device} from "./infra/device.js"
5
+ import {modprefix} from "../utils/modprefix.js"
6
+
7
+ export class KeyboardDevice extends Device {
8
+ on = sub<Sample>()
9
+ dispose: () => void
10
+ #held = new Set<string>()
11
+
12
+ constructor(target: EventTarget) {
13
+ super()
14
+
15
+ const down = (code: string) => {
16
+ this.#held.add(code)
17
+ this.on.pub(code, 1)
18
+ }
19
+
20
+ const up = (code: string) => {
21
+ this.#held.delete(code)
22
+ this.on.pub(code, 0)
23
+ }
24
+
25
+ this.dispose = coalesce(
26
+ ev(target, {
27
+ keydown: (event: KeyboardEvent) => {
28
+ if (event.repeat) return
29
+ down(event.code)
30
+ down(modprefix(event, event.code))
31
+ },
32
+ keyup: (event: KeyboardEvent) => {
33
+ up(event.code)
34
+ up(modprefix(event, event.code))
35
+ },
36
+ }),
37
+
38
+ ev(window, {
39
+ blur: () => {
40
+ for (const code of this.#held)
41
+ this.on.pub(code, 0)
42
+ this.#held.clear()
43
+ },
44
+ })
45
+ )
46
+ }
47
+
48
+ takeSamples() {
49
+ return [...this.#held]
50
+ .map(code => [code, 1] as Sample)
51
+ }
52
+ }
53
+
@@ -0,0 +1,95 @@
1
+
2
+ import {ev} from "@e280/stz"
3
+ import {Vec2} from "@benev/math"
4
+ import {modprefix} from "../utils/modprefix.js"
5
+ import {SamplerDevice} from "./infra/sampler.js"
6
+ import {splitAxis} from "../../utils/split-axis.js"
7
+
8
+ export class PointerDevice extends SamplerDevice {
9
+ client = new Vec2(0, 0)
10
+ movement = new Vec2(0, 0)
11
+ dispose: () => void
12
+
13
+ static buttonCode(event: PointerEvent) {
14
+ switch (event.button) {
15
+ case 0: return "pointer.button.left"
16
+ case 1: return "pointer.button.middle"
17
+ case 2: return "pointer.button.right"
18
+ default: return `pointer.button.${event.button + 1}`
19
+ }
20
+ }
21
+
22
+ static wheelCodes(event: WheelEvent) {
23
+ const movements: [string, number][] = []
24
+
25
+ if (event.deltaX)
26
+ movements.push([
27
+ event.deltaX > 0 ? "pointer.wheel.right" : "pointer.wheel.left",
28
+ event.deltaX,
29
+ ])
30
+
31
+ if (event.deltaY)
32
+ movements.push([
33
+ event.deltaY > 0 ? "pointer.wheel.down" : "pointer.wheel.up",
34
+ event.deltaY,
35
+ ])
36
+
37
+ return movements
38
+ }
39
+
40
+ constructor(target: EventTarget) {
41
+ super()
42
+
43
+ const dispatch = (event: PointerEvent | WheelEvent, code: string, value: number) => {
44
+ this.setSample(code, value)
45
+ this.setSample(modprefix(event, code), value)
46
+ }
47
+
48
+ this.dispose = ev(target, {
49
+ pointerdown: (event: PointerEvent) => {
50
+ const code = PointerDevice.buttonCode(event)
51
+ dispatch(event, code, 1)
52
+ },
53
+
54
+ pointerup: (event: PointerEvent) => {
55
+ const code = PointerDevice.buttonCode(event)
56
+ dispatch(event, code, 0)
57
+ },
58
+
59
+ pointermove: (event: PointerEvent) => {
60
+ this.client.x = event.clientX
61
+ this.client.y = event.clientY
62
+ this.movement.x += event.movementX
63
+ this.movement.y += event.movementY
64
+ },
65
+
66
+ wheel: (event: WheelEvent) => {
67
+ for (const [code, value] of PointerDevice.wheelCodes(event))
68
+ dispatch(event, code, value)
69
+ },
70
+ })
71
+ }
72
+
73
+ takeSamples() {
74
+ const [x, y] = this.movement
75
+ const [left, right] = splitAxis(x)
76
+ const [down, up] = splitAxis(y)
77
+
78
+ if (x) {
79
+ if (x >= 0)
80
+ this.setSample(`pointer.move.right`, Math.abs(right))
81
+ else
82
+ this.setSample(`pointer.move.left`, Math.abs(left))
83
+ }
84
+ if (y) {
85
+ if (y >= 0)
86
+ this.setSample(`pointer.move.up`, Math.abs(up))
87
+ else
88
+ this.setSample(`pointer.move.down`, Math.abs(down))
89
+ }
90
+
91
+ this.movement.set_(0, 0)
92
+ return super.takeSamples()
93
+ }
94
+ }
95
+
@@ -0,0 +1,26 @@
1
+
2
+ import {sub} from "@e280/stz"
3
+ import {isPressed} from "../utils/is-pressed.js"
4
+
5
+ export class Action {
6
+ #value = 0
7
+ #previous = 0
8
+
9
+ static updateValue(action: Action, newValue: number) {
10
+ action.#previous = action.#value
11
+ action.#value = newValue
12
+ if (action.changed) action.on.pub(action)
13
+ if (action.down) action.onDown.pub(action)
14
+ }
15
+
16
+ readonly on = sub<[Action]>()
17
+ readonly onDown = sub<[Action]>()
18
+
19
+ get value() { return this.#value }
20
+ get previous() { return this.#previous }
21
+ get changed() { return this.#value !== this.#previous }
22
+ get pressed() { return isPressed(this.#value) }
23
+ get down() { return !isPressed(this.#previous) && isPressed(this.#value) }
24
+ get up() { return isPressed(this.#previous) && !isPressed(this.#value) }
25
+ }
26
+
@@ -0,0 +1,28 @@
1
+
2
+ import {LensSettings, LensState} from "../types.js"
3
+
4
+ export const defaultHoldTime = 250
5
+
6
+ export function defaultLensState(): LensState {
7
+ return {
8
+ lastValue: 0,
9
+ holdStart: 0,
10
+ }
11
+ }
12
+
13
+ export function defaultLensSettings(): LensSettings {
14
+ return {
15
+ scale: 1,
16
+ invert: false,
17
+ deadzone: 0.2,
18
+ timing: {style: "direct"},
19
+ }
20
+ }
21
+
22
+ export function defaultifyLensSettings(partial: Partial<LensSettings> = {}): LensSettings {
23
+ return {
24
+ ...defaultLensSettings(),
25
+ ...partial,
26
+ }
27
+ }
28
+
@@ -0,0 +1,73 @@
1
+
2
+ import {WeakMapG} from "@e280/stz"
3
+ import {tmax} from "../utils/tmax.js"
4
+ import {lensing_algorithm} from "./routines/lensing_algorithm.js"
5
+ import {Actions, Bindings, Lens, LensState, SampleMap, Spoon} from "../types.js"
6
+ import {defaultifyLensSettings, defaultLensState} from "./defaults.js"
7
+ import {build_updatable_actions_structure} from "./routines/build_updatable_actions_structure.js"
8
+
9
+ export class Resolver<B extends Bindings> {
10
+ actions: Actions<B>
11
+
12
+ #now = 0
13
+ #lenses = new WeakMapG<Lens, LensState>()
14
+ #updateValues: () => void
15
+
16
+ constructor(
17
+ public bindings: B,
18
+ private modes: Set<keyof B>,
19
+ private samples: SampleMap,
20
+ ) {
21
+ this.modes = modes as Set<keyof Bindings>
22
+ const structure = build_updatable_actions_structure(
23
+ bindings,
24
+ this.#resolveActionValue,
25
+ )
26
+ this.actions = structure.actions
27
+ this.#updateValues = structure.updateValues
28
+ }
29
+
30
+ poll(now: number) {
31
+ this.#now = now
32
+ this.#updateValues()
33
+ }
34
+
35
+ #resolveActionValue = (mode: keyof B, actionKey: keyof B[keyof B]) => {
36
+ const fork = this.bindings[mode][actionKey]
37
+ const isModeActive = this.modes.has(mode)
38
+ if (!isModeActive) return 0
39
+ return tmax(fork.map(this.#resolveSpoon))
40
+ }
41
+
42
+ #resolveSpoon = ({lenses, required = [], forbidden = []}: Spoon) => {
43
+ const satisfiedRequirements = () => {
44
+ if (required.length === 0) return true
45
+ const requiredValues = required.map(this.#resolveLens)
46
+ return !requiredValues.some(value => value <= 0)
47
+ }
48
+
49
+ const satisfiedForbiddens = () => {
50
+ if (forbidden.length === 0) return true
51
+ const forbiddenValues = forbidden.map(this.#resolveLens)
52
+ return !forbiddenValues.some(value => value > 0)
53
+ }
54
+
55
+ const combineValues = () => {
56
+ const values = lenses.map(this.#resolveLens)
57
+ return tmax(values)
58
+ }
59
+
60
+ return (satisfiedRequirements() && satisfiedForbiddens())
61
+ ? combineValues()
62
+ : 0
63
+ }
64
+
65
+ #resolveLens = (lens: Lens) => {
66
+ const {code} = lens
67
+ const value = this.samples.get(code) ?? 0
68
+ const state = this.#lenses.guarantee(lens, defaultLensState)
69
+ const settings = defaultifyLensSettings(lens.settings)
70
+ return lensing_algorithm(value, settings, state, this.#now)
71
+ }
72
+ }
73
+
@@ -0,0 +1,20 @@
1
+
2
+ import {SampleMap} from "../../types.js"
3
+ import {Device} from "../../devices/infra/device.js"
4
+
5
+ export function aggregate_samples_into_map(
6
+ devices: Set<Device>,
7
+ map: SampleMap,
8
+ ) {
9
+
10
+ for (const device of devices) {
11
+ for (const [code, value] of device.takeSamples()) {
12
+ const previous = map.get(code) ?? 0
13
+ if (value > previous)
14
+ map.set(code, value)
15
+ }
16
+ }
17
+
18
+ return map
19
+ }
20
+
@@ -0,0 +1,29 @@
1
+
2
+ import {obMap, xub} from "@e280/stz"
3
+ import {Action} from "../action.js"
4
+ import {Bindings, Actions} from "../../types.js"
5
+
6
+ export function build_updatable_actions_structure<B extends Bindings>(
7
+ bindings: B,
8
+ resolveActionValue: (mode: keyof B, actionKey: keyof B[keyof B]) => number,
9
+ ) {
10
+
11
+ const updateValues = xub()
12
+
13
+ const actions = obMap(bindings, (bracket, mode) =>
14
+ obMap(bracket, (_, actionKey) => {
15
+ const action = new Action()
16
+ updateValues.on(() => {
17
+ const value = resolveActionValue(mode, actionKey)
18
+ Action.updateValue(action, value)
19
+ })
20
+ return action
21
+ })
22
+ ) as Actions<B>
23
+
24
+ return {
25
+ actions,
26
+ updateValues: updateValues.publish,
27
+ }
28
+ }
29
+
@@ -0,0 +1,74 @@
1
+
2
+ import {pipe} from "@e280/stz"
3
+ import {Scalar} from "@benev/math"
4
+ import {defaultHoldTime} from "../defaults.js"
5
+ import {isPressed} from "../../utils/is-pressed.js"
6
+ import {LensSettings, LensState} from "../../types.js"
7
+
8
+ export const lensing_algorithm = (
9
+ v: number,
10
+ settings: LensSettings,
11
+ state: LensState,
12
+ now: number,
13
+ ) => pipe(v).line(
14
+
15
+ function deadzone(value) {
16
+ if (value < settings.deadzone)
17
+ return 0
18
+
19
+ if (value > 1)
20
+ return value
21
+
22
+ return Scalar.remap(
23
+ value,
24
+ settings.deadzone, 1,
25
+ 0, 1,
26
+ )
27
+ },
28
+
29
+ function inversion(value) {
30
+ return settings.invert
31
+ ? value * -1
32
+ : value
33
+ },
34
+
35
+ function scaling(value) {
36
+ return settings.scale * value
37
+ },
38
+
39
+ function timing(value) {
40
+ const holdTime = (
41
+ settings.timing.style === "direct"
42
+ ? undefined
43
+ : settings.timing.holdTime
44
+ ) ?? defaultHoldTime
45
+
46
+ const isFreshlyPressed = !isPressed(state.lastValue) && isPressed(value)
47
+ const isFreshlyReleased = isPressed(state.lastValue) && !isPressed(value)
48
+ const isHolding = (now - state.holdStart) >= holdTime
49
+
50
+ if (isFreshlyPressed)
51
+ state.holdStart = now
52
+
53
+ state.lastValue = value
54
+
55
+ switch (settings.timing.style) {
56
+ case "direct":
57
+ return value
58
+
59
+ case "tap":
60
+ return (isFreshlyReleased && !isHolding)
61
+ ? 1
62
+ : 0
63
+
64
+ case "hold":
65
+ return (isPressed(value) && isHolding)
66
+ ? value
67
+ : 0
68
+
69
+ default:
70
+ throw new Error(`unknown bindings timing`)
71
+ }
72
+ },
73
+ )
74
+
@@ -0,0 +1,21 @@
1
+
2
+ import {Switchboard} from "../switchboard.js"
3
+ import {AsSwitchboardBindings, Bindings} from "../types.js"
4
+
5
+ export function switchboardBindings<B extends Bindings>(b: B): AsSwitchboardBindings<B> {
6
+ return {
7
+ ...b,
8
+ [Switchboard.mode]: {
9
+ shimmyNext: [
10
+ {lenses: [{code: "BracketRight"}]},
11
+ {lenses: [{code: "gamepad.right"}], required: [{code: "gamepad.gamma"}]},
12
+ {lenses: [{code: "gamepad.bumper.right"}], required: [{code: "gamepad.gamma"}]},
13
+ ],
14
+ shimmyPrevious: [
15
+ {lenses: [{code: "BracketLeft"}]},
16
+ {lenses: [{code: "gamepad.left"}], required: [{code: "gamepad.gamma"}]},
17
+ {lenses: [{code: "gamepad.bumper.left"}], required: [{code: "gamepad.gamma"}]},
18
+ ],
19
+ },
20
+ }
21
+ }