UX Improvements
* Length of the sequence has a clear indicator now * The frame stamp snaps to the time of keyframes when they're hovered * The frame stamp hides when hovering over elements over which it wouldn't be helpful
This commit is contained in:
parent
c6c37992ac
commit
0a3c699180
13 changed files with 266 additions and 59 deletions
|
@ -12,6 +12,11 @@ import {lighten} from 'polished'
|
||||||
import React, {useMemo, useRef} from 'react'
|
import React, {useMemo, useRef} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import type KeyframeEditor from './KeyframeEditor'
|
import type KeyframeEditor from './KeyframeEditor'
|
||||||
|
import type {FrameStampPositionLock} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
import {
|
||||||
|
attributeNameThatLocksFramestamp,
|
||||||
|
useFrameStampPosition,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
|
||||||
export const dotSize = 6
|
export const dotSize = 6
|
||||||
const hitZoneSize = 12
|
const hitZoneSize = 12
|
||||||
|
@ -64,8 +69,12 @@ const Dot: React.FC<IProps> = (props) => {
|
||||||
<>
|
<>
|
||||||
<HitZone
|
<HitZone
|
||||||
ref={ref}
|
ref={ref}
|
||||||
title={props.keyframe.position.toString()}
|
title={props.keyframe.position.toFixed(8)}
|
||||||
data-pos={props.keyframe.position.toFixed(3)}
|
data-pos={props.keyframe.position.toFixed(3)}
|
||||||
|
{...{
|
||||||
|
[attributeNameThatLocksFramestamp]:
|
||||||
|
props.keyframe.position.toFixed(3),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Square isSelected={!!props.selection} />
|
<Square isSelected={!!props.selection} />
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
|
@ -103,6 +112,8 @@ function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
||||||
|
const {getLock} = useFrameStampPosition()
|
||||||
|
|
||||||
const propsRef = useRef(props)
|
const propsRef = useRef(props)
|
||||||
propsRef.current = props
|
propsRef.current = props
|
||||||
|
|
||||||
|
@ -110,6 +121,8 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
||||||
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
|
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
|
||||||
let tempTransaction: CommitOrDiscard | undefined
|
let tempTransaction: CommitOrDiscard | undefined
|
||||||
let propsAtStartOfDrag: IProps
|
let propsAtStartOfDrag: IProps
|
||||||
|
let frameStampPositionLock: FrameStampPositionLock
|
||||||
|
|
||||||
let selectionDragHandlers:
|
let selectionDragHandlers:
|
||||||
| ReturnType<DopeSheetSelection['getDragHandlers']>
|
| ReturnType<DopeSheetSelection['getDragHandlers']>
|
||||||
| undefined
|
| undefined
|
||||||
|
@ -131,8 +144,10 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
propsAtStartOfDrag = propsRef.current
|
propsAtStartOfDrag = propsRef.current
|
||||||
|
|
||||||
toUnitSpace = val(propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace)
|
toUnitSpace = val(propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace)
|
||||||
|
|
||||||
|
frameStampPositionLock = getLock()
|
||||||
|
frameStampPositionLock.set(propsAtStartOfDrag.keyframe.position)
|
||||||
},
|
},
|
||||||
onDrag(dx, dy, event) {
|
onDrag(dx, dy, event) {
|
||||||
if (selectionDragHandlers) {
|
if (selectionDragHandlers) {
|
||||||
|
@ -140,6 +155,10 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const delta = toUnitSpace(dx)
|
const delta = toUnitSpace(dx)
|
||||||
|
const original =
|
||||||
|
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
|
||||||
|
frameStampPositionLock.set(original.position + delta)
|
||||||
|
|
||||||
if (tempTransaction) {
|
if (tempTransaction) {
|
||||||
tempTransaction.discard()
|
tempTransaction.discard()
|
||||||
tempTransaction = undefined
|
tempTransaction = undefined
|
||||||
|
@ -158,6 +177,8 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onDragEnd(dragHappened) {
|
onDragEnd(dragHappened) {
|
||||||
|
frameStampPositionLock.unlock()
|
||||||
|
|
||||||
if (selectionDragHandlers) {
|
if (selectionDragHandlers) {
|
||||||
selectionDragHandlers.onDragEnd?.(dragHappened)
|
selectionDragHandlers.onDragEnd?.(dragHappened)
|
||||||
|
|
||||||
|
|
|
@ -12,46 +12,58 @@ import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||||
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import type Sheet from '@theatre/core/sheets/Sheet'
|
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||||
|
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||||
|
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
|
||||||
const coverWidth = 1000
|
const coverWidth = 1000
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
stripNormal: `#0000006c`,
|
||||||
|
stripActive: `#000000`,
|
||||||
|
}
|
||||||
|
|
||||||
const Strip = styled.div`
|
const Strip = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 4px;
|
width: 4px;
|
||||||
z-index: ${() => zIndexes.lengthIndicatorStrip};
|
z-index: ${() => zIndexes.lengthIndicatorStrip};
|
||||||
pointer-events: auto;
|
pointer-events: none;
|
||||||
cursor: ew-resize;
|
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
display: block;
|
display: block;
|
||||||
content: ' ';
|
content: ' ';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: ${topStripHeight}px;
|
/* top: ${topStripHeight}px; */
|
||||||
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: -1px;
|
left: -1px;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background-color: #000000a6;
|
background-color: ${colors.stripNormal};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:after,
|
&:hover:after,
|
||||||
&.dragging:after {
|
&.dragging:after {
|
||||||
background-color: #000000;
|
background-color: ${colors.stripActive};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Info = styled.div`
|
const Bulge = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: ${topStripHeight + 4}px;
|
top: ${topStripHeight + 4}px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
left: 4px;
|
left: 0px;
|
||||||
color: #eee;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: none;
|
padding: 2px 8px 2px 8px;
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: ew-resize;
|
||||||
|
color: #555;
|
||||||
|
background-color: ${colors.stripNormal};
|
||||||
|
|
||||||
${Strip}:hover &, ${Strip}.dragging & {
|
${Strip}:hover &, ${Strip}.dragging & {
|
||||||
display: block;
|
color: white;
|
||||||
|
background-color: ${colors.stripActive};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -64,7 +76,7 @@ const Cover = styled.div`
|
||||||
z-index: ${() => zIndexes.lengthIndicatorCover};
|
z-index: ${() => zIndexes.lengthIndicatorCover};
|
||||||
transform-origin: left top;
|
transform-origin: left top;
|
||||||
|
|
||||||
${Strip}:hover ~ &, ${Strip}.dragging ~ & {
|
${Strip}.dragging ~ & {
|
||||||
background-color: rgb(23 23 23 / 60%);
|
background-color: rgb(23 23 23 / 60%);
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -74,8 +86,11 @@ type IProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
|
const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
|
||||||
const [stripRef, stripNode] = useRefAndState<HTMLDivElement | null>(null)
|
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||||
const [isDraggingD] = useDragStrip(stripNode, {layoutP})
|
const [isDraggingD] = useDragBulge(node, {layoutP})
|
||||||
|
const [popoverNode, openPopover, _, isPopoverOpen] = usePopover(() => {
|
||||||
|
return <div>poppio</div>
|
||||||
|
})
|
||||||
|
|
||||||
return usePrism(() => {
|
return usePrism(() => {
|
||||||
const sheet = val(layoutP.sheet)
|
const sheet = val(layoutP.sheet)
|
||||||
|
@ -101,19 +116,25 @@ const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{popoverNode}
|
||||||
<Strip
|
<Strip
|
||||||
title="Change Sequence Length"
|
title="Change Sequence Length"
|
||||||
ref={stripRef}
|
|
||||||
style={{
|
style={{
|
||||||
height: height + 'px',
|
height: height + 'px',
|
||||||
transform: `translateX(${translateX === 0 ? -1000 : translateX}px)`,
|
transform: `translateX(${translateX === 0 ? -1000 : translateX}px)`,
|
||||||
}}
|
}}
|
||||||
className={val(isDraggingD) ? 'dragging' : ''}
|
className={val(isDraggingD) ? 'dragging' : ''}
|
||||||
>
|
>
|
||||||
<Info>
|
<Bulge
|
||||||
sequence.length:{' '}
|
ref={nodeRef}
|
||||||
|
title="Length of the sequence (sequence.length)"
|
||||||
|
onClick={(e) => {
|
||||||
|
openPopover(e, node!)
|
||||||
|
}}
|
||||||
|
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||||
|
>
|
||||||
{sequence.positionFormatter.formatForPlayhead(sequenceLength)}
|
{sequence.positionFormatter.formatForPlayhead(sequenceLength)}
|
||||||
</Info>
|
</Bulge>
|
||||||
</Strip>
|
</Strip>
|
||||||
<Cover
|
<Cover
|
||||||
title="Length"
|
title="Length"
|
||||||
|
@ -132,10 +153,10 @@ const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, [layoutP, stripRef, isDraggingD])
|
}, [layoutP, nodeRef, isDraggingD, popoverNode])
|
||||||
}
|
}
|
||||||
|
|
||||||
function useDragStrip(node: HTMLDivElement | null, props: IProps) {
|
function useDragBulge(node: HTMLDivElement | null, props: IProps) {
|
||||||
const propsRef = useRef(props)
|
const propsRef = useRef(props)
|
||||||
propsRef.current = props
|
propsRef.current = props
|
||||||
const isDragging = useMemo(() => new Box(false), [])
|
const isDragging = useMemo(() => new Box(false), [])
|
||||||
|
|
|
@ -95,6 +95,16 @@ const FrameStampPositionProvider: React.FC<{
|
||||||
|
|
||||||
export const useFrameStampPosition = () => useContext(context)
|
export const useFrameStampPosition = () => useContext(context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This attribute is used so that when the cursor hovers over a keyframe,
|
||||||
|
* the framestamp snaps to the position of that keyframe.
|
||||||
|
*
|
||||||
|
* Elements that need this behavior must set a data attribute like so:
|
||||||
|
* <div data-theatre-lock-framestamp-to="120.55" />
|
||||||
|
* Setting this attribute to "hide" hides the stamp.
|
||||||
|
*/
|
||||||
|
export const attributeNameThatLocksFramestamp =
|
||||||
|
'data-theatre-lock-framestamp-to'
|
||||||
const pointerPositionInUnitSpace = (
|
const pointerPositionInUnitSpace = (
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||||
): IDerivation<number> => {
|
): IDerivation<number> => {
|
||||||
|
@ -103,7 +113,25 @@ const pointerPositionInUnitSpace = (
|
||||||
const clippedSpaceToUnitSpace = val(layoutP.clippedSpace.toUnitSpace)
|
const clippedSpaceToUnitSpace = val(layoutP.clippedSpace.toUnitSpace)
|
||||||
const leftPadding = val(layoutP.scaledSpace.leftPadding)
|
const leftPadding = val(layoutP.scaledSpace.leftPadding)
|
||||||
|
|
||||||
const {clientX, clientY} = val(mousePositionD)
|
const mousePos = val(mousePositionD)
|
||||||
|
if (!mousePos) return -1
|
||||||
|
|
||||||
|
for (const el of mousePos.composedPath()) {
|
||||||
|
if (!(el instanceof HTMLElement || el instanceof SVGElement)) break
|
||||||
|
|
||||||
|
if (el.hasAttribute(attributeNameThatLocksFramestamp)) {
|
||||||
|
const val = el.getAttribute(attributeNameThatLocksFramestamp)
|
||||||
|
if (typeof val !== 'string') continue
|
||||||
|
if (val === 'hide') return -1
|
||||||
|
const double = parseFloat(val)
|
||||||
|
|
||||||
|
if (isFinite(double) && double >= 0) return double
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (mousePos.composedPath())
|
||||||
|
|
||||||
|
const {clientX, clientY} = mousePos
|
||||||
|
|
||||||
const {screenX: x, screenY: y, width: rightWidth, height} = rightDims
|
const {screenX: x, screenY: y, width: rightWidth, height} = rightDims
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import styled from 'styled-components'
|
||||||
import type KeyframeEditor from './KeyframeEditor'
|
import type KeyframeEditor from './KeyframeEditor'
|
||||||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
import type {FrameStampPositionLock} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import type {FrameStampPositionLock} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {useFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {useFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
|
||||||
export const dotSize = 6
|
export const dotSize = 6
|
||||||
|
@ -60,6 +61,9 @@ const Dot: React.FC<IProps> = (props) => {
|
||||||
cx: `calc(var(--unitSpaceToScaledSpaceMultiplier) * ${cur.position} * 1px)`,
|
cx: `calc(var(--unitSpaceToScaledSpaceMultiplier) * ${cur.position} * 1px)`,
|
||||||
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
||||||
}}
|
}}
|
||||||
|
{...{
|
||||||
|
[attributeNameThatLocksFramestamp]: cur.position.toFixed(3),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Circle
|
<Circle
|
||||||
style={{
|
style={{
|
||||||
|
|
|
@ -6,6 +6,7 @@ import React, {useCallback} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
import {VscTriangleUp} from 'react-icons/all'
|
import {VscTriangleUp} from 'react-icons/all'
|
||||||
|
import {attributeNameThatLocksFramestamp} from './FrameStampPositionProvider'
|
||||||
|
|
||||||
const Container = styled.button`
|
const Container = styled.button`
|
||||||
outline: none;
|
outline: none;
|
||||||
|
@ -64,6 +65,7 @@ const GraphEditorToggle: React.FC<{
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
title={'Toggle graph editor'}
|
title={'Toggle graph editor'}
|
||||||
className={isOpen ? 'open' : ''}
|
className={isOpen ? 'open' : ''}
|
||||||
|
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||||
>
|
>
|
||||||
<VscTriangleUp />
|
<VscTriangleUp />
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
import mousePositionD from '@theatre/studio/utils/mousePositionD'
|
|
||||||
import {usePrism, useVal} from '@theatre/dataverse-react'
|
import {usePrism, useVal} from '@theatre/dataverse-react'
|
||||||
import type {Pointer} from '@theatre/dataverse'
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
|
@ -8,7 +7,6 @@ import styled from 'styled-components'
|
||||||
import {stampsGridTheme} from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid'
|
import {stampsGridTheme} from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid'
|
||||||
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
||||||
import {topStripTheme} from './TopStrip'
|
import {topStripTheme} from './TopStrip'
|
||||||
import {inRange} from 'lodash-es'
|
|
||||||
import {useFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {useFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
|
||||||
const Label = styled.div`
|
const Label = styled.div`
|
||||||
|
@ -77,33 +75,3 @@ const FrameStamp: React.FC<{
|
||||||
})
|
})
|
||||||
|
|
||||||
export default FrameStamp
|
export default FrameStamp
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @returns -1 if outside, otherwise, a positive number
|
|
||||||
*/
|
|
||||||
const usePointerPositionInUnitSpace = (
|
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
|
||||||
): number => {
|
|
||||||
return usePrism(() => {
|
|
||||||
const rightDims = val(layoutP.rightDims)
|
|
||||||
const clippedSpaceToUnitSpace = val(layoutP.clippedSpace.toUnitSpace)
|
|
||||||
const leftPadding = val(layoutP.scaledSpace.leftPadding)
|
|
||||||
|
|
||||||
const {clientX, clientY} = val(mousePositionD)
|
|
||||||
|
|
||||||
const {screenX: x, screenY: y, width: rightWidth, height} = rightDims
|
|
||||||
|
|
||||||
if (
|
|
||||||
inRange(clientX, x, x + rightWidth) &&
|
|
||||||
inRange(clientY, y, y + height)
|
|
||||||
) {
|
|
||||||
const posInRightDims = clientX - x
|
|
||||||
const posInUnitSpace = clippedSpaceToUnitSpace(posInRightDims)
|
|
||||||
|
|
||||||
return posInUnitSpace
|
|
||||||
} else {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
}, [layoutP])
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {position} from 'polished'
|
||||||
import React, {useCallback, useMemo, useState} from 'react'
|
import React, {useCallback, useMemo, useState} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
||||||
|
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
--threadHeight: 6px;
|
--threadHeight: 6px;
|
||||||
|
@ -237,7 +238,10 @@ const HorizontalScrollbar: React.FC<{
|
||||||
}, [layoutP, relevantValuesD])
|
}, [layoutP, relevantValuesD])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container style={{bottom: bottom + 8 + 'px'}}>
|
<Container
|
||||||
|
style={{bottom: bottom + 8 + 'px'}}
|
||||||
|
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||||
|
>
|
||||||
<TimeThread>
|
<TimeThread>
|
||||||
<DraggableArea
|
<DraggableArea
|
||||||
onDragStart={handles.onDragStart}
|
onDragStart={handles.onDragStart}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import clamp from 'lodash-es/clamp'
|
||||||
import React, {useMemo} from 'react'
|
import React, {useMemo} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
||||||
|
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
|
||||||
const Container = styled.div<{isVisible: boolean}>`
|
const Container = styled.div<{isVisible: boolean}>`
|
||||||
--thumbColor: #00e0ff;
|
--thumbColor: #00e0ff;
|
||||||
|
@ -177,6 +178,7 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
style={{transform: `translate3d(${posInClippedSpace}px, 0, 0)`}}
|
style={{transform: `translate3d(${posInClippedSpace}px, 0, 0)`}}
|
||||||
className={isSeeking ? 'seeking' : ''}
|
className={isSeeking ? 'seeking' : ''}
|
||||||
|
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||||
>
|
>
|
||||||
<Thumb ref={thumbRef as $IntentionalAny}>
|
<Thumb ref={thumbRef as $IntentionalAny}>
|
||||||
<RoomToClick room={8} />
|
<RoomToClick room={8} />
|
||||||
|
|
|
@ -5,6 +5,7 @@ import styled from 'styled-components'
|
||||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
import StampsGrid from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid'
|
import StampsGrid from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid'
|
||||||
import PanelDragZone from '@theatre/studio/panels/BasePanel/PanelDragZone'
|
import PanelDragZone from '@theatre/studio/panels/BasePanel/PanelDragZone'
|
||||||
|
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
|
||||||
export const topStripHeight = 20
|
export const topStripHeight = 20
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ const TopStrip: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const width = useVal(layoutP.rightDims.width)
|
const width = useVal(layoutP.rightDims.width)
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container {...{[attributeNameThatLocksFramestamp]: 'hide'}}>
|
||||||
<StampsGrid layoutP={layoutP} width={width} height={topStripHeight} />
|
<StampsGrid layoutP={layoutP} width={width} height={topStripHeight} />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
105
theatre/studio/src/uiComponents/Popover/Popover.tsx
Normal file
105
theatre/studio/src/uiComponents/Popover/Popover.tsx
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
|
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
|
||||||
|
import transparentize from 'polished/lib/color/transparentize'
|
||||||
|
import React, {useLayoutEffect, useState} from 'react'
|
||||||
|
import {createPortal} from 'react-dom'
|
||||||
|
import useWindowSize from 'react-use/esm/useWindowSize'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const minWidth = 190
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far from the menu should the pointer travel to auto close the menu
|
||||||
|
*/
|
||||||
|
const defaultPointerDistanceThreshold = 200
|
||||||
|
|
||||||
|
const Container = styled.ul`
|
||||||
|
position: absolute;
|
||||||
|
min-width: ${minWidth}px;
|
||||||
|
z-index: 10000;
|
||||||
|
background: ${transparentize(0.2, '#111')};
|
||||||
|
color: white;
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 2px 0;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 1px;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: all;
|
||||||
|
border-radius: 3px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Popover: React.FC<{
|
||||||
|
clickPoint: {clientX: number; clientY: number}
|
||||||
|
target: HTMLElement
|
||||||
|
onRequestClose: () => void
|
||||||
|
children: () => React.ReactNode
|
||||||
|
pointerDistanceThreshold?: number
|
||||||
|
}> = (props) => {
|
||||||
|
const pointerDistanceThreshold =
|
||||||
|
props.pointerDistanceThreshold ?? defaultPointerDistanceThreshold
|
||||||
|
|
||||||
|
const [container, setContainer] = useState<HTMLElement | null>(null)
|
||||||
|
const rect = useBoundingClientRect(container)
|
||||||
|
const windowSize = useWindowSize()
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!rect || !container) return
|
||||||
|
|
||||||
|
const preferredAnchorPoint = {
|
||||||
|
left: rect.width / 2,
|
||||||
|
top: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = {
|
||||||
|
left: props.clickPoint.clientX - preferredAnchorPoint.left,
|
||||||
|
top: props.clickPoint.clientY - preferredAnchorPoint.top,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos.left < 0) {
|
||||||
|
pos.left = 0
|
||||||
|
} else if (pos.left + rect.width > windowSize.width) {
|
||||||
|
pos.left = windowSize.width - rect.width
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos.top < 0) {
|
||||||
|
pos.top = 0
|
||||||
|
} else if (pos.top + rect.height > windowSize.height) {
|
||||||
|
pos.top = windowSize.height - rect.height
|
||||||
|
}
|
||||||
|
|
||||||
|
container.style.left = pos.left + 'px'
|
||||||
|
container.style.top = pos.top + 'px'
|
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
e.clientX < pos.left - pointerDistanceThreshold ||
|
||||||
|
e.clientX > pos.left + rect.width + pointerDistanceThreshold ||
|
||||||
|
e.clientY < pos.top - pointerDistanceThreshold ||
|
||||||
|
e.clientY > pos.top + rect.height + pointerDistanceThreshold
|
||||||
|
) {
|
||||||
|
props.onRequestClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
|
if (!e.composedPath().includes(container)) {
|
||||||
|
props.onRequestClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', onMouseMove)
|
||||||
|
window.addEventListener('mousedown', onMouseDown, {capture: true})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousemove', onMouseMove)
|
||||||
|
window.removeEventListener('mousedown', onMouseDown, {capture: true})
|
||||||
|
}
|
||||||
|
}, [rect, container, props.clickPoint, windowSize, props.onRequestClose])
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<Container ref={setContainer}>{props.children()}</Container>,
|
||||||
|
getStudio()!.ui.containerShadow,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Popover
|
48
theatre/studio/src/uiComponents/Popover/usePopover.tsx
Normal file
48
theatre/studio/src/uiComponents/Popover/usePopover.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import React, {useCallback, useState} from 'react'
|
||||||
|
import Popover from './Popover'
|
||||||
|
|
||||||
|
type OpenFn = (e: React.MouseEvent, target: HTMLElement) => void
|
||||||
|
type CloseFn = () => void
|
||||||
|
type State =
|
||||||
|
| {isOpen: false}
|
||||||
|
| {
|
||||||
|
isOpen: true
|
||||||
|
clickPoint: {
|
||||||
|
clientX: number
|
||||||
|
clientY: number
|
||||||
|
}
|
||||||
|
target: HTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function usePopover(
|
||||||
|
render: () => React.ReactNode,
|
||||||
|
): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] {
|
||||||
|
const [state, setState] = useState<State>({
|
||||||
|
isOpen: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const open = useCallback<OpenFn>((e, target) => {
|
||||||
|
setState({
|
||||||
|
isOpen: true,
|
||||||
|
clickPoint: {clientX: e.clientX, clientY: e.clientY},
|
||||||
|
target,
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const close = useCallback<CloseFn>(() => {
|
||||||
|
setState({isOpen: false})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const node = state.isOpen ? (
|
||||||
|
<Popover
|
||||||
|
children={render}
|
||||||
|
clickPoint={state.clickPoint}
|
||||||
|
target={state.target}
|
||||||
|
onRequestClose={close}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)
|
||||||
|
|
||||||
|
return [node, open, close, state.isOpen]
|
||||||
|
}
|
|
@ -5,7 +5,10 @@ import React from 'react'
|
||||||
import {createPortal} from 'react-dom'
|
import {createPortal} from 'react-dom'
|
||||||
|
|
||||||
const ShowMousePosition: React.FC<{}> = (props) => {
|
const ShowMousePosition: React.FC<{}> = (props) => {
|
||||||
const pos = usePrism(() => val(mousePositionD), [])
|
const pos = usePrism(
|
||||||
|
() => val(mousePositionD) ?? {clientX: 0, clientY: 0},
|
||||||
|
[],
|
||||||
|
)
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import {prism} from '@theatre/dataverse'
|
import {prism} from '@theatre/dataverse'
|
||||||
|
|
||||||
const mousePositionD = prism(() => {
|
const mousePositionD = prism(() => {
|
||||||
const [pos, setPos] = prism.state('pos', {clientX: 0, clientY: 0})
|
const [pos, setPos] = prism.state<MouseEvent | null>('pos', null)
|
||||||
prism.effect(
|
prism.effect(
|
||||||
'setupListeners',
|
'setupListeners',
|
||||||
() => {
|
() => {
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
setPos({clientX: e.clientX, clientY: e.clientY})
|
setPos(e)
|
||||||
}
|
}
|
||||||
document.addEventListener('mousemove', handleMouseMove)
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue