|
@@ -7,8 +7,8 @@ import ToggleButton from '@mui/material/ToggleButton'
|
|
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
|
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
|
|
|
import {PB} from 'smartknobjs-proto'
|
|
import {PB} from 'smartknobjs-proto'
|
|
|
import {VideoInfo} from './types'
|
|
import {VideoInfo} from './types'
|
|
|
-import {Button, Card, CardActions, CardContent} from '@mui/material'
|
|
|
|
|
-import {exhaustiveCheck} from './util'
|
|
|
|
|
|
|
+import {Card, CardActions, CardContent} from '@mui/material'
|
|
|
|
|
+import {exhaustiveCheck, lerp, NoUndefinedField} from './util'
|
|
|
|
|
|
|
|
const socket = io()
|
|
const socket = io()
|
|
|
|
|
|
|
@@ -18,6 +18,12 @@ enum Mode {
|
|
|
Speed = 'Speed',
|
|
Speed = 'Speed',
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+type State = {
|
|
|
|
|
+ mode: Mode
|
|
|
|
|
+ playbackSpeed: number
|
|
|
|
|
+ currentFrame: number
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export type AppProps = {
|
|
export type AppProps = {
|
|
|
info: VideoInfo
|
|
info: VideoInfo
|
|
|
}
|
|
}
|
|
@@ -25,14 +31,15 @@ export const App: React.FC<AppProps> = ({info}) => {
|
|
|
const [isConnected, setIsConnected] = useState(socket.connected)
|
|
const [isConnected, setIsConnected] = useState(socket.connected)
|
|
|
const [state, setState] = useState<string>('')
|
|
const [state, setState] = useState<string>('')
|
|
|
|
|
|
|
|
- const [mode, setRawMode] = useState<Mode>(Mode.Scroll)
|
|
|
|
|
|
|
+ const [derivedState, setDerivedState] = useState<State>({
|
|
|
|
|
+ mode: Mode.Scroll,
|
|
|
|
|
+ playbackSpeed: 0,
|
|
|
|
|
+ currentFrame: 0,
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- const [playbackSpeed, setPlaybackSpeed] = useState<number>(0)
|
|
|
|
|
- const [currentFrame, setCurrentFrame] = useState<number>(0)
|
|
|
|
|
-
|
|
|
|
|
- const pushCurrentConfig = () => {
|
|
|
|
|
|
|
+ const pushConfig = (state: State) => {
|
|
|
let config: PB.SmartKnobConfig
|
|
let config: PB.SmartKnobConfig
|
|
|
- if (mode === Mode.Scroll) {
|
|
|
|
|
|
|
+ if (state.mode === Mode.Scroll) {
|
|
|
// TODO
|
|
// TODO
|
|
|
config = PB.SmartKnobConfig.create({
|
|
config = PB.SmartKnobConfig.create({
|
|
|
numPositions: 32,
|
|
numPositions: 32,
|
|
@@ -46,10 +53,10 @@ export const App: React.FC<AppProps> = ({info}) => {
|
|
|
snapPointBias: 0,
|
|
snapPointBias: 0,
|
|
|
snapPointBiasCenterPosition: 0,
|
|
snapPointBiasCenterPosition: 0,
|
|
|
})
|
|
})
|
|
|
- } else if (mode === Mode.Frames) {
|
|
|
|
|
|
|
+ } else if (state.mode === Mode.Frames) {
|
|
|
config = PB.SmartKnobConfig.create({
|
|
config = PB.SmartKnobConfig.create({
|
|
|
numPositions: info.totalFrames,
|
|
numPositions: info.totalFrames,
|
|
|
- position: currentFrame,
|
|
|
|
|
|
|
+ position: state.currentFrame,
|
|
|
positionWidthRadians: (1 * Math.PI) / 180,
|
|
positionWidthRadians: (1 * Math.PI) / 180,
|
|
|
detentStrengthUnit: 1,
|
|
detentStrengthUnit: 1,
|
|
|
endstopStrengthUnit: 1,
|
|
endstopStrengthUnit: 1,
|
|
@@ -59,10 +66,10 @@ export const App: React.FC<AppProps> = ({info}) => {
|
|
|
snapPointBias: 0,
|
|
snapPointBias: 0,
|
|
|
snapPointBiasCenterPosition: 0,
|
|
snapPointBiasCenterPosition: 0,
|
|
|
})
|
|
})
|
|
|
- } else if (mode === Mode.Speed) {
|
|
|
|
|
|
|
+ } else if (state.mode === Mode.Speed) {
|
|
|
config = PB.SmartKnobConfig.create({
|
|
config = PB.SmartKnobConfig.create({
|
|
|
numPositions: 13,
|
|
numPositions: 13,
|
|
|
- position: 6 + Math.trunc(playbackSpeed),
|
|
|
|
|
|
|
+ position: 6 + Math.trunc(state.playbackSpeed),
|
|
|
positionWidthRadians: (60 * Math.PI) / 180,
|
|
positionWidthRadians: (60 * Math.PI) / 180,
|
|
|
detentStrengthUnit: 1,
|
|
detentStrengthUnit: 1,
|
|
|
endstopStrengthUnit: 1,
|
|
endstopStrengthUnit: 1,
|
|
@@ -73,21 +80,34 @@ export const App: React.FC<AppProps> = ({info}) => {
|
|
|
snapPointBiasCenterPosition: 6,
|
|
snapPointBiasCenterPosition: 6,
|
|
|
})
|
|
})
|
|
|
} else {
|
|
} else {
|
|
|
- throw exhaustiveCheck(mode)
|
|
|
|
|
|
|
+ throw exhaustiveCheck(state.mode)
|
|
|
}
|
|
}
|
|
|
socket.emit('set_config', config)
|
|
socket.emit('set_config', config)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const setCurrentFrame = (fn: (oldFrame: number) => number) => {
|
|
|
|
|
+ setDerivedState((cur) => {
|
|
|
|
|
+ const newState = {...cur}
|
|
|
|
|
+ if (cur.mode === Mode.Speed) {
|
|
|
|
|
+ newState.currentFrame = fn(cur.currentFrame)
|
|
|
|
|
+ }
|
|
|
|
|
+ return newState
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- const fps = info.frameRate * playbackSpeed
|
|
|
|
|
|
|
+ const fps = info.frameRate * derivedState.playbackSpeed
|
|
|
const msPerFrame = 1000 / fps
|
|
const msPerFrame = 1000 / fps
|
|
|
- if (fps !== 0) {
|
|
|
|
|
|
|
+ if (derivedState.mode === Mode.Speed && fps !== 0) {
|
|
|
const timer = setInterval(() => {
|
|
const timer = setInterval(() => {
|
|
|
setCurrentFrame((oldFrame) => {
|
|
setCurrentFrame((oldFrame) => {
|
|
|
- const newFrame = oldFrame + Math.sign(playbackSpeed)
|
|
|
|
|
|
|
+ const newFrame = oldFrame + Math.sign(derivedState.playbackSpeed)
|
|
|
if (newFrame < 0 || newFrame >= info.totalFrames) {
|
|
if (newFrame < 0 || newFrame >= info.totalFrames) {
|
|
|
- setPlaybackSpeed(0)
|
|
|
|
|
- pushCurrentConfig()
|
|
|
|
|
|
|
+ pushConfig({
|
|
|
|
|
+ mode: derivedState.mode,
|
|
|
|
|
+ playbackSpeed: 0,
|
|
|
|
|
+ currentFrame: -1, // unused
|
|
|
|
|
+ })
|
|
|
return oldFrame
|
|
return oldFrame
|
|
|
} else {
|
|
} else {
|
|
|
return newFrame
|
|
return newFrame
|
|
@@ -96,15 +116,7 @@ export const App: React.FC<AppProps> = ({info}) => {
|
|
|
}, msPerFrame)
|
|
}, msPerFrame)
|
|
|
return () => clearInterval(timer)
|
|
return () => clearInterval(timer)
|
|
|
}
|
|
}
|
|
|
- }, [playbackSpeed, info.totalFrames, info.frameRate])
|
|
|
|
|
-
|
|
|
|
|
- const setMode = (m: Mode) => {
|
|
|
|
|
- setRawMode(m)
|
|
|
|
|
- if (m !== Mode.Speed) {
|
|
|
|
|
- setPlaybackSpeed(0)
|
|
|
|
|
- }
|
|
|
|
|
- pushCurrentConfig()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ }, [derivedState.mode, derivedState.playbackSpeed, info.totalFrames, info.frameRate])
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
socket.on('connect', () => {
|
|
socket.on('connect', () => {
|
|
@@ -117,10 +129,40 @@ export const App: React.FC<AppProps> = ({info}) => {
|
|
|
|
|
|
|
|
socket.on('state', (input: {pb: PB.SmartKnobState}) => {
|
|
socket.on('state', (input: {pb: PB.SmartKnobState}) => {
|
|
|
const {pb: state} = input
|
|
const {pb: state} = input
|
|
|
- const stateObj = PB.SmartKnobState.toObject(state, {defaults: true})
|
|
|
|
|
|
|
+ const stateObj = PB.SmartKnobState.toObject(state, {
|
|
|
|
|
+ defaults: true,
|
|
|
|
|
+ }) as NoUndefinedField<PB.ISmartKnobState>
|
|
|
setState(JSON.stringify(stateObj, undefined, 4))
|
|
setState(JSON.stringify(stateObj, undefined, 4))
|
|
|
|
|
+ setDerivedState((cur) => {
|
|
|
|
|
+ const modeText = stateObj.config.text
|
|
|
|
|
+ if (modeText === Mode.Scroll) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ mode: Mode.Scroll,
|
|
|
|
|
+ playbackSpeed: 0,
|
|
|
|
|
+ currentFrame: cur.currentFrame, // TODO - map from state
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (modeText === Mode.Frames) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ mode: Mode.Frames,
|
|
|
|
|
+ playbackSpeed: 0,
|
|
|
|
|
+ currentFrame: stateObj.currentPosition ?? 0,
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (modeText === Mode.Speed) {
|
|
|
|
|
+ const normalizedWholeValue = stateObj.currentPosition - stateObj.config.snapPointBiasCenterPosition
|
|
|
|
|
+ const normalizedFractional =
|
|
|
|
|
+ Math.sign(stateObj.subPositionUnit) *
|
|
|
|
|
+ lerp(stateObj.subPositionUnit * Math.sign(stateObj.subPositionUnit), 0.1, 0.9, 0, 1)
|
|
|
|
|
+ const normalized = normalizedWholeValue + normalizedFractional
|
|
|
|
|
+ const speed = Math.sign(normalized) * Math.pow(2, Math.abs(normalized) - 1)
|
|
|
|
|
+ return {
|
|
|
|
|
+ mode: Mode.Speed,
|
|
|
|
|
+ playbackSpeed: speed,
|
|
|
|
|
+ currentFrame: cur.currentFrame,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return cur
|
|
|
|
|
+ })
|
|
|
})
|
|
})
|
|
|
-
|
|
|
|
|
return () => {
|
|
return () => {
|
|
|
socket.off('connect')
|
|
socket.off('connect')
|
|
|
socket.off('disconnect')
|
|
socket.off('disconnect')
|
|
@@ -136,13 +178,17 @@ export const App: React.FC<AppProps> = ({info}) => {
|
|
|
</Typography>
|
|
</Typography>
|
|
|
<ToggleButtonGroup
|
|
<ToggleButtonGroup
|
|
|
color="primary"
|
|
color="primary"
|
|
|
- value={mode}
|
|
|
|
|
|
|
+ value={derivedState.mode}
|
|
|
exclusive
|
|
exclusive
|
|
|
onChange={(e, value: Mode | null) => {
|
|
onChange={(e, value: Mode | null) => {
|
|
|
if (value === null) {
|
|
if (value === null) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- setMode(value)
|
|
|
|
|
|
|
+ pushConfig({
|
|
|
|
|
+ mode: value,
|
|
|
|
|
+ currentFrame: derivedState.currentFrame,
|
|
|
|
|
+ playbackSpeed: derivedState.playbackSpeed,
|
|
|
|
|
+ })
|
|
|
}}
|
|
}}
|
|
|
aria-label="Mode"
|
|
aria-label="Mode"
|
|
|
>
|
|
>
|
|
@@ -153,15 +199,17 @@ export const App: React.FC<AppProps> = ({info}) => {
|
|
|
))}
|
|
))}
|
|
|
</ToggleButtonGroup>
|
|
</ToggleButtonGroup>
|
|
|
<Typography component="h1" variant="h5">
|
|
<Typography component="h1" variant="h5">
|
|
|
- Frame {currentFrame} / {info.totalFrames - 1}
|
|
|
|
|
|
|
+ Frame {derivedState.currentFrame} / {info.totalFrames - 1}
|
|
|
|
|
+ <br />
|
|
|
|
|
+ Speed {derivedState.playbackSpeed}
|
|
|
</Typography>
|
|
</Typography>
|
|
|
</CardContent>
|
|
</CardContent>
|
|
|
<CardActions>
|
|
<CardActions>
|
|
|
- {mode === Mode.Speed && (
|
|
|
|
|
|
|
+ {/* {derivedState.mode === Mode.Speed && (
|
|
|
<Button
|
|
<Button
|
|
|
size="small"
|
|
size="small"
|
|
|
onClick={() => {
|
|
onClick={() => {
|
|
|
- if (playbackSpeed !== 0) {
|
|
|
|
|
|
|
+ if (derivedState.playbackSpeed !== 0) {
|
|
|
setPlaybackSpeed(0)
|
|
setPlaybackSpeed(0)
|
|
|
} else {
|
|
} else {
|
|
|
setPlaybackSpeed(1)
|
|
setPlaybackSpeed(1)
|
|
@@ -171,7 +219,7 @@ export const App: React.FC<AppProps> = ({info}) => {
|
|
|
>
|
|
>
|
|
|
Play
|
|
Play
|
|
|
</Button>
|
|
</Button>
|
|
|
- )}
|
|
|
|
|
|
|
+ )} */}
|
|
|
</CardActions>
|
|
</CardActions>
|
|
|
</Card>
|
|
</Card>
|
|
|
<Card>
|
|
<Card>
|