Make it possible to move the playhead to an exact position (#92)
This commit is contained in:
parent
fb7467862b
commit
3f2a9032f1
2 changed files with 138 additions and 24 deletions
|
@ -16,6 +16,9 @@ import {
|
||||||
useLockFrameStampPosition,
|
useLockFrameStampPosition,
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||||
|
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
||||||
|
import PlayheadPositionPopover from './PlayheadPositionPopover'
|
||||||
|
|
||||||
const Container = styled.div<{isVisible: boolean}>`
|
const Container = styled.div<{isVisible: boolean}>`
|
||||||
--thumbColor: #00e0ff;
|
--thumbColor: #00e0ff;
|
||||||
|
@ -150,6 +153,20 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [thumbRef, thumbNode] = useRefAndState<HTMLElement | null>(null)
|
const [thumbRef, thumbNode] = useRefAndState<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover(
|
||||||
|
{},
|
||||||
|
() => {
|
||||||
|
return (
|
||||||
|
<BasicPopover>
|
||||||
|
<PlayheadPositionPopover
|
||||||
|
layoutP={layoutP}
|
||||||
|
onRequestClose={closePopover}
|
||||||
|
/>
|
||||||
|
</BasicPopover>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
|
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
|
||||||
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
||||||
|
|
||||||
|
@ -215,32 +232,38 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
posInClippedSpace <= val(layoutP.clippedSpace.width)
|
posInClippedSpace <= val(layoutP.clippedSpace.width)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<>
|
||||||
isVisible={isVisible}
|
{popoverNode}
|
||||||
style={{transform: `translate3d(${posInClippedSpace}px, 0, 0)`}}
|
<Container
|
||||||
className={isSeeking ? 'seeking' : ''}
|
isVisible={isVisible}
|
||||||
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
style={{transform: `translate3d(${posInClippedSpace}px, 0, 0)`}}
|
||||||
>
|
|
||||||
<Thumb
|
|
||||||
ref={thumbRef as $IntentionalAny}
|
|
||||||
data-pos={posInUnitSpace.toFixed(3)}
|
|
||||||
>
|
|
||||||
<RoomToClick room={8} />
|
|
||||||
<Squinch />
|
|
||||||
<Tooltip>
|
|
||||||
{sequence.positionFormatter.formatForPlayhead(
|
|
||||||
sequence.closestGridPosition(posInUnitSpace),
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
</Thumb>
|
|
||||||
|
|
||||||
<Rod
|
|
||||||
data-pos={posInUnitSpace.toFixed(3)}
|
|
||||||
className={isSeeking ? 'seeking' : ''}
|
className={isSeeking ? 'seeking' : ''}
|
||||||
/>
|
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||||
</Container>
|
>
|
||||||
|
<Thumb
|
||||||
|
ref={thumbRef as $IntentionalAny}
|
||||||
|
data-pos={posInUnitSpace.toFixed(3)}
|
||||||
|
onClick={(e) => {
|
||||||
|
openPopover(e, thumbNode!)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RoomToClick room={8} />
|
||||||
|
<Squinch />
|
||||||
|
<Tooltip>
|
||||||
|
{sequence.positionFormatter.formatForPlayhead(
|
||||||
|
sequence.closestGridPosition(posInUnitSpace),
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</Thumb>
|
||||||
|
|
||||||
|
<Rod
|
||||||
|
data-pos={posInUnitSpace.toFixed(3)}
|
||||||
|
className={isSeeking ? 'seeking' : ''}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}, [layoutP])
|
}, [layoutP, thumbRef, popoverNode])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Playhead
|
export default Playhead
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
|
import {usePrism} from '@theatre/react'
|
||||||
|
import type {BasicNumberInputNudgeFn} from '@theatre/studio/uiComponents/form/BasicNumberInput'
|
||||||
|
import BasicNumberInput from '@theatre/studio/uiComponents/form/BasicNumberInput'
|
||||||
|
import {propNameText} from '@theatre/studio/panels/DetailPanel/propEditors/utils/SingleRowPropEditor'
|
||||||
|
import {useLayoutEffect, useMemo, useRef} from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import {val} from '@theatre/dataverse'
|
||||||
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
|
import clamp from 'lodash-es/clamp'
|
||||||
|
|
||||||
|
const greaterThanZero = (v: number) => isFinite(v) && v > 0
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
height: 28px;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Label = styled.div`
|
||||||
|
${propNameText};
|
||||||
|
white-space: nowrap;
|
||||||
|
`
|
||||||
|
|
||||||
|
const nudge: BasicNumberInputNudgeFn = ({deltaX}) => deltaX * 0.25
|
||||||
|
|
||||||
|
const PlayheadPositionPopover: React.FC<{
|
||||||
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
|
/**
|
||||||
|
* Called when user hits enter/escape
|
||||||
|
*/
|
||||||
|
onRequestClose: () => void
|
||||||
|
}> = ({layoutP, onRequestClose}) => {
|
||||||
|
const sheet = val(layoutP.sheet)
|
||||||
|
const sequence = sheet.getSequence()
|
||||||
|
|
||||||
|
const fns = useMemo(() => {
|
||||||
|
let tempPosition: number | undefined
|
||||||
|
const originalPosition = sequence.position
|
||||||
|
|
||||||
|
return {
|
||||||
|
temporarilySetValue(newPosition: number): void {
|
||||||
|
if (tempPosition) {
|
||||||
|
tempPosition = undefined
|
||||||
|
}
|
||||||
|
tempPosition = clamp(newPosition, 0, sequence.length)
|
||||||
|
sequence.position = tempPosition
|
||||||
|
},
|
||||||
|
discardTemporaryValue(): void {
|
||||||
|
if (tempPosition) {
|
||||||
|
tempPosition = undefined
|
||||||
|
sequence.position = originalPosition
|
||||||
|
}
|
||||||
|
},
|
||||||
|
permenantlySetValue(newPosition: number): void {
|
||||||
|
if (tempPosition) {
|
||||||
|
tempPosition = undefined
|
||||||
|
}
|
||||||
|
sequence.position = clamp(newPosition, 0, sequence.length)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [layoutP, sequence])
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
inputRef.current!.focus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return usePrism(() => {
|
||||||
|
const sequence = sheet.getSequence()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Label>Playhead position</Label>
|
||||||
|
<BasicNumberInput
|
||||||
|
value={sequence.position}
|
||||||
|
{...fns}
|
||||||
|
isValid={greaterThanZero}
|
||||||
|
inputRef={inputRef}
|
||||||
|
onBlur={onRequestClose}
|
||||||
|
nudge={nudge}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}, [sheet, fns, inputRef])
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlayheadPositionPopover
|
Loading…
Reference in a new issue