-
-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat (ui): implement speed controls #1206
base: master
Are you sure you want to change the base?
Changes from all commits
3109f3a
df57bdd
3226688
dce25f3
425394e
6fe25eb
4d42d5c
94a7a11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
.panelContainer { | ||
display: flex; | ||
flex-direction: column; | ||
gap: 0.5rem; | ||
margin-top: 2rem; | ||
} | ||
|
||
// TODO: unify panel label | ||
.label { | ||
display: block; | ||
font-size: $inner-section-text-size; | ||
color: $label-gray; | ||
} | ||
|
||
.highlight { | ||
color: $orange-600; | ||
} | ||
|
||
.speedContainer { | ||
background-color: $gray-1100; | ||
width: 100%; | ||
height: 4px; | ||
border-radius: 1px; | ||
position: relative; | ||
overflow: hidden; | ||
} | ||
|
||
.speedRegular { | ||
position: absolute; | ||
left: 0; | ||
height: 100%; | ||
width: 33.33%; | ||
background-color: $ui-white; | ||
} | ||
|
||
.speedOverride { | ||
position: absolute; | ||
background-color: $orange-700; | ||
left: 0; | ||
height: 100%; | ||
width: calc(var(--override, 0) * 1%); | ||
} | ||
|
||
.labels { | ||
margin-top: 0.25rem; | ||
font-size: calc(1rem - 3px); | ||
color: $label-gray; | ||
position: relative; | ||
|
||
> span { | ||
position: absolute; | ||
transform: translateX(-50%); | ||
|
||
&:first-child { | ||
transform: translateX(0); | ||
} | ||
|
||
&:last-child { | ||
transform: translateX(-100%); | ||
} | ||
} | ||
|
||
.override { | ||
color: $gray-1100; | ||
background-color: $orange-600; | ||
padding: 0 0.25rem; | ||
border-radius: 2px; | ||
top: calc(-4px - 0.5rem); // speed + gap * 2 | ||
transform: translate(-50%, -100%); | ||
// TODO: account for case where we translate all the way left | ||
font-weight: 600; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { useState } from 'react'; | ||
import { Button } from '@chakra-ui/react'; | ||
|
||
import { setTimerSpeed, useTimerSpeed } from '../../../../common/hooks/useSocket'; | ||
|
||
import style from './TimerSpeed.module.scss'; | ||
|
||
// TODO: extract and test | ||
function mapRange(value: number, fromA_start: number, fromA_end: number, toB_start: number, toB_end: number): number { | ||
return ((value - fromA_start) * (toB_end - toB_start)) / (fromA_end - fromA_start) + toB_start; | ||
} | ||
|
||
export default function TimerSpeed() { | ||
// TODO: this is the speed currently in place | ||
const { speed: _currentSpeed } = useTimerSpeed(); | ||
|
||
// TODO: new speed comes from reply from server | ||
const [newSpeed, _setNewSpeed] = useState(1.23); | ||
const newSpeedIndicator = mapRange(newSpeed, 0.5, 2.0, 0, 100); | ||
const { calculateSpeed, setSpeed, resetSpeed } = setTimerSpeed; | ||
|
||
console.log('newSpeedIndicator', newSpeedIndicator); | ||
|
||
const handleApply = () => { | ||
console.log('timerSpeedControl.apply'); | ||
// TODO: add dynamic value | ||
setSpeed(1.23); | ||
}; | ||
|
||
const handleReset = () => { | ||
console.log('timerSpeedControl.reset'); | ||
resetSpeed(); | ||
}; | ||
|
||
const handleMeetSchedule = () => { | ||
console.log('timerSpeedControl.calculate'); | ||
calculateSpeed(); | ||
}; | ||
|
||
return ( | ||
<div className={style.panelContainer}> | ||
<div className={style.label}>Timer speed</div> | ||
<div style={{ display: 'flex', gap: '1rem' }}> | ||
<Button size='sm' variant='ontime-subtle-white' onClick={handleApply}> | ||
Apply | ||
</Button> | ||
<Button size='sm' variant='ontime-subtle-white' onClick={handleReset}> | ||
Reset | ||
</Button> | ||
<Button size='sm' variant='ontime-subtle-white' onClick={handleMeetSchedule}> | ||
Meet schedule | ||
</Button> | ||
</div> | ||
<div> | ||
<span>1.0x</span> | ||
<span>{'->'}</span> | ||
<span className={style.highlight}>{`${newSpeed}x`}</span> | ||
</div> | ||
<div> | ||
<div className={style.speedContainer}> | ||
<div className={style.speedOverride} style={{ '--override': newSpeedIndicator }} /> | ||
<div className={style.speedRegular} /> | ||
</div> | ||
<div className={style.labels}> | ||
<span>0.5x</span> | ||
<span className={style.override} style={{ left: `${newSpeedIndicator}%` }}>{`${newSpeed}x`}</span> | ||
<span style={{ left: '33.33%' }}>1.0x</span> | ||
<span style={{ left: '66.66%' }}>1.5x</span> | ||
<span style={{ left: '100%' }}>2.0x</span> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,6 +47,7 @@ const initialTimer: TimerState = { | |
playback: Playback.Stop, // change initiated by user | ||
secondaryTimer: null, // change on every update | ||
startedAt: null, // change can only be initiated by user | ||
speed: 1.0, //change initiated by user | ||
} as const; | ||
|
||
export type RuntimeState = { | ||
|
@@ -360,6 +361,33 @@ export function updateAll(rundown: OntimeRundown) { | |
loadBlock(rundown); | ||
} | ||
|
||
export function setSpeed(speed: number) { | ||
runtimeState.timer.speed = speed; | ||
} | ||
|
||
export function resetSpeed() { | ||
runtimeState.timer.speed = 1.0; | ||
} | ||
|
||
export function getSpeed() { | ||
return runtimeState.timer.speed; | ||
} | ||
|
||
export function calculateSpeed() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please dont see these as answers, but as suggestions Right now, we are attaching the functionality of time manipulation to a target time. I believe that we should not make the feature in a way that would stop us from doing some time manipulation on its own (ie, set timer to 1.2x) With this assumption, this is a utility function that gives me the speed that I would need to finish on time. without making mutations to state.
Good question.
I dont think we should do rounding. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
// TODO: what should this return if no timer is running? | ||
if (runtimeState.eventNow !== null) { | ||
const timeToDesiredFinish = runtimeState.eventNow.timeEnd - runtimeState.clock; | ||
const timeToActualFinish = runtimeState.timer.expectedFinish - runtimeState.clock; | ||
const factor = 1 / (timeToDesiredFinish / timeToActualFinish); | ||
|
||
// TODO: this can produce negative speeds (i.e. the desired finish time is in the past) | ||
// TODO: should there be some clamping or rounding on this number? | ||
return factor; | ||
} | ||
|
||
return 1.0; | ||
} | ||
|
||
export function start(state: RuntimeState = runtimeState): boolean { | ||
if (state.eventNow === null) { | ||
return false; | ||
|
@@ -476,6 +504,7 @@ export type UpdateResult = { | |
}; | ||
|
||
export function update(): UpdateResult { | ||
const timeSinceLastUpdate = clock.timeNow() - runtimeState.clock; | ||
// 0. there are some things we always do | ||
runtimeState.clock = clock.timeNow(); // we update the clock on every update call | ||
|
||
|
@@ -500,6 +529,12 @@ export function update(): UpdateResult { | |
} | ||
} | ||
|
||
const catchUpMultiplier = 1 - runtimeState.timer.speed; | ||
|
||
if (runtimeState.timer.playback === Playback.Play) { | ||
runtimeState.timer.addedTime += timeSinceLastUpdate * catchUpMultiplier; | ||
} | ||
|
||
// update timer state | ||
runtimeState.timer.current = getCurrent(runtimeState); | ||
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Address TODO: Extract and test
mapRange
function.The
mapRange
function should be moved to a separate utility file and unit tested.Would you like me to create a new utility file for this function and generate unit tests? I can also open a GitHub issue to track this task if you prefer.