Keyframe snapping
* Implemented useCursorLock() to lock the cursor icon during a gesture * Keyframes now snap to other keyframes * Keyframes snap to the playhead * The playhead also snaps to keyframes
This commit is contained in:
parent
e6bfd999a2
commit
197d59e4c4
27 changed files with 340 additions and 81 deletions
|
@ -9,7 +9,7 @@ export default class UI {
|
|||
_showing = false
|
||||
private _renderTimeout: NodeJS.Timer | undefined = undefined
|
||||
private _documentBodyUIIsRenderedIn: HTMLElement | undefined = undefined
|
||||
readonly containerShadow: HTMLElement
|
||||
readonly containerShadow: ShadowRoot & HTMLElement
|
||||
|
||||
constructor(readonly studio: Studio) {
|
||||
// @todo we can't bootstrap theatre (as in, to design theatre using theatre), if we rely on IDed elements
|
||||
|
@ -29,7 +29,7 @@ export default class UI {
|
|||
// To see why I had to cast this value to HTMLElement, take a look at its
|
||||
// references. There are a few functions that actually work with a ShadowRoot
|
||||
// but are typed to accept HTMLElement
|
||||
}) as $IntentionalAny as HTMLElement
|
||||
}) as $IntentionalAny as ShadowRoot & HTMLElement
|
||||
}
|
||||
|
||||
show() {
|
||||
|
|
|
@ -11,6 +11,7 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
|||
import {PortalContext} from 'reakit'
|
||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
import useKeyboardShortcuts from './useKeyboardShortcuts'
|
||||
import PointerEventsHandler from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
:host {
|
||||
|
@ -31,14 +32,13 @@ const GlobalStyle = createGlobalStyle`
|
|||
}
|
||||
`
|
||||
|
||||
const Container = styled.div`
|
||||
const Container = styled(PointerEventsHandler)`
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
pointer-events: none;
|
||||
`
|
||||
|
||||
const PortalLayer = styled.div`
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import {lighten} from 'polished'
|
||||
import {css} from 'styled-components'
|
||||
|
||||
export const pointerEventsAutoInNormalMode = css`
|
||||
pointer-events: none;
|
||||
#pointer-root.normal & {
|
||||
pointer-events: auto;
|
||||
}
|
||||
`
|
||||
|
||||
export const theme = {
|
||||
panel: {
|
||||
|
|
|
@ -77,8 +77,28 @@ const ClosePanelButton = styled.button`
|
|||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* The &:after part blocks pointer events from reaching the content of the
|
||||
* pane when a drag gesture is active in theatre's UI. It's a hack and its downside
|
||||
* is that pane content cannot interact with the rest of theatre's UI while a drag
|
||||
* gesture is active.
|
||||
* @todo find a less hacky way?
|
||||
*/
|
||||
const F2 = styled(F2Impl)`
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: block;
|
||||
content: ' ';
|
||||
pointer-events: none;
|
||||
|
||||
#pointer-root:not(.normal) & {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const ErrorContainer = styled.div`
|
||||
|
|
|
@ -7,11 +7,12 @@ import {lighten} from 'polished'
|
|||
import React, {useMemo, useRef, useState} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {panelDimsToPanelPosition, usePanel} from './BasePanel'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
||||
const Base = styled.div`
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {usePanel} from './BasePanel'
|
||||
|
@ -7,7 +8,7 @@ const Container = styled.div`
|
|||
position: absolute;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
/* box-shadow: 1px 2px 10px -5px black; */
|
||||
|
||||
z-index: 1000;
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
TitleBar_Piece,
|
||||
TitleBar_Punctuation,
|
||||
} from '@theatre/studio/panels/BasePanel/common'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
||||
const Container = styled.div`
|
||||
background-color: transparent;
|
||||
|
@ -30,7 +31,7 @@ const Container = styled.div`
|
|||
bottom: 0;
|
||||
right: 0;
|
||||
width: 20px;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
}
|
||||
`
|
||||
|
||||
|
@ -54,7 +55,7 @@ const Title = styled.div`
|
|||
font-weight: 500;
|
||||
font-size: 10px;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -84,7 +85,7 @@ const Header = styled.div`
|
|||
`
|
||||
|
||||
const Body = styled.div`
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
position: absolute;
|
||||
top: ${headerHeight};
|
||||
left: 0;
|
||||
|
|
|
@ -15,11 +15,12 @@ import {
|
|||
rowBg,
|
||||
} from './utils/SingleRowPropEditor'
|
||||
import DefaultOrStaticValueIndicator from './utils/DefaultValueIndicator'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
||||
const Container = styled.div`
|
||||
--step: 8px;
|
||||
--left-pad: 0px;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
`
|
||||
|
||||
const Header = styled.div`
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import {transparentize} from 'polished'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
@ -75,7 +76,8 @@ const PrevOrNextButton = styled(Button)<{available: boolean}>`
|
|||
props.available
|
||||
? nextPrevCursorsTheme.onColor
|
||||
: nextPrevCursorsTheme.offColor};
|
||||
pointer-events: ${(props) => (props.available ? 'auto' : 'none')};
|
||||
|
||||
${(props) => (props.available ? pointerEventsAutoInNormalMode : '')};
|
||||
`
|
||||
|
||||
const Prev = styled(PrevOrNextButton)<{available: boolean}>`
|
||||
|
|
|
@ -9,6 +9,7 @@ import type {useEditingToolsForPrimitiveProp} from '@theatre/studio/panels/Objec
|
|||
import {shadeToColor} from '@theatre/studio/panels/ObjectEditorPanel/propEditors/utils/useEditingToolsForPrimitiveProp'
|
||||
import styled, {css} from 'styled-components'
|
||||
import {transparentize} from 'polished'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
||||
export const indentationFormula = `calc(var(--left-pad) + var(--depth) * var(--step))`
|
||||
|
||||
|
@ -39,7 +40,7 @@ const Row = styled.div`
|
|||
align-items: stretch;
|
||||
--right-width: 60%;
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
||||
${rowBg};
|
||||
`
|
||||
|
|
|
@ -5,6 +5,7 @@ import styled, {css} from 'styled-components'
|
|||
import noop from '@theatre/shared/utils/noop'
|
||||
import {transparentize, darken, opacify, lighten} from 'polished'
|
||||
import {rowBgColor} from '@theatre/studio/panels/ObjectEditorPanel/propEditors/utils/SingleRowPropEditor'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
||||
export const Container = styled.li<{depth: number}>`
|
||||
--depth: ${(props) => props.depth};
|
||||
|
@ -70,7 +71,7 @@ const Head_Label = styled.span`
|
|||
${outlineItemFont};
|
||||
|
||||
padding: 2px 8px;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 13px;
|
||||
|
@ -96,7 +97,7 @@ const Head_Label = styled.span`
|
|||
display: block;
|
||||
content: ' ';
|
||||
z-index: 0;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react'
|
|||
import styled from 'styled-components'
|
||||
import {panelZIndexes} from '@theatre/studio/panels/BasePanel/common'
|
||||
import ProjectsList from './ProjectsList/ProjectsList'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
||||
const Container = styled.div`
|
||||
background-color: transparent;
|
||||
|
@ -21,7 +22,7 @@ const Container = styled.div`
|
|||
bottom: 0;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
}
|
||||
|
||||
&:hover:before {
|
||||
|
@ -58,7 +59,7 @@ const Header = styled.div`
|
|||
top: 0;
|
||||
left: 0;
|
||||
width: 180px;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
|
@ -78,14 +79,14 @@ const Title = styled.div`
|
|||
font-weight: 500;
|
||||
font-size: 10px;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const Body = styled.div`
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
position: absolute;
|
||||
top: ${headerHeight};
|
||||
left: 0;
|
||||
|
|
|
@ -14,6 +14,7 @@ import styled from 'styled-components'
|
|||
import type KeyframeEditor from './KeyframeEditor'
|
||||
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {useCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
|
||||
export const dotSize = 6
|
||||
const hitZoneSize = 12
|
||||
|
@ -46,10 +47,20 @@ const Square = styled.div<{isSelected: boolean}>`
|
|||
const HitZone = styled.div`
|
||||
position: absolute;
|
||||
${dims(hitZoneSize)};
|
||||
|
||||
z-index: 1;
|
||||
|
||||
cursor: ew-resize;
|
||||
|
||||
&:hover + ${Square} {
|
||||
#pointer-root.draggingpositioninsequenceeditor & {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&.beingDragged {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
&:hover + ${Square}, &.beingDragged + ${Square} {
|
||||
${dims(dotSize + 5)}
|
||||
}
|
||||
`
|
||||
|
@ -60,7 +71,7 @@ const Dot: React.FC<IProps> = (props) => {
|
|||
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||
|
||||
const [contextMenu] = useKeyframeContextMenu(node, props)
|
||||
useDragKeyframe(node, props)
|
||||
const [isDragging] = useDragKeyframe(node, props)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -71,6 +82,7 @@ const Dot: React.FC<IProps> = (props) => {
|
|||
[attributeNameThatLocksFramestamp]:
|
||||
props.keyframe.position.toFixed(3),
|
||||
}}
|
||||
className={isDragging ? 'beingDragged' : ''}
|
||||
/>
|
||||
<Square isSelected={!!props.selection} />
|
||||
{contextMenu}
|
||||
|
@ -107,7 +119,10 @@ function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
|
|||
})
|
||||
}
|
||||
|
||||
function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
||||
function useDragKeyframe(
|
||||
node: HTMLDivElement | null,
|
||||
props: IProps,
|
||||
): [isDragging: boolean] {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
useLockFrameStampPosition(isDragging, props.keyframe.position)
|
||||
|
||||
|
@ -125,7 +140,6 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
|||
| undefined
|
||||
|
||||
return {
|
||||
lockCursorTo: 'ew-resize',
|
||||
onDragStart(event) {
|
||||
setIsDragging(true)
|
||||
if (propsRef.current.selection) {
|
||||
|
@ -149,11 +163,30 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
|||
selectionDragHandlers.onDrag(dx, dy, event)
|
||||
return
|
||||
}
|
||||
|
||||
const original =
|
||||
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
|
||||
const deltaPos = toUnitSpace(dx)
|
||||
const newPosBeforeSnapping = Math.max(original.position + deltaPos, 0)
|
||||
|
||||
let newPosition = newPosBeforeSnapping
|
||||
|
||||
const snapTarget = event
|
||||
.composedPath()
|
||||
.find(
|
||||
(el): el is Element =>
|
||||
el instanceof Element &&
|
||||
el !== node &&
|
||||
el.hasAttribute('data-pos'),
|
||||
)
|
||||
|
||||
if (snapTarget) {
|
||||
const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!)
|
||||
if (isFinite(snapPos)) {
|
||||
newPosition = snapPos
|
||||
}
|
||||
}
|
||||
|
||||
if (tempTransaction) {
|
||||
tempTransaction.discard()
|
||||
tempTransaction = undefined
|
||||
|
@ -163,7 +196,7 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
|||
{
|
||||
...propsAtStartOfDrag.leaf.sheetObject.address,
|
||||
trackId: propsAtStartOfDrag.leaf.trackId,
|
||||
keyframes: [{...original, position: newPosBeforeSnapping}],
|
||||
keyframes: [{...original, position: newPosition}],
|
||||
},
|
||||
)
|
||||
})
|
||||
|
@ -191,4 +224,8 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
|||
}, [])
|
||||
|
||||
useDrag(node, gestureHandlers)
|
||||
|
||||
useCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
||||
|
||||
return [isDragging]
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ import {clamp, mapValues} from 'lodash-es'
|
|||
import React, {useLayoutEffect, useMemo} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {useReceiveVerticalWheelEvent} from '@theatre/studio/panels/SequenceEditorPanel/VerticalScrollContainer'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import {useCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
|
||||
const Container = styled.div`
|
||||
position: absolute;
|
||||
|
@ -16,7 +18,7 @@ const Container = styled.div`
|
|||
right: 0;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
pointer-events: all;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
||||
// hide the scrollbar on Gecko
|
||||
scrollbar-width: none;
|
||||
|
@ -87,16 +89,42 @@ function useDragHandlers(
|
|||
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
||||
|
||||
return {
|
||||
onDrag(dx: number) {
|
||||
onDrag(dx: number, _, event) {
|
||||
const deltaPos = scaledSpaceToUnitSpace(dx)
|
||||
const newPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
|
||||
sequence.position = newPos
|
||||
const unsnappedPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
|
||||
|
||||
let newPosition = unsnappedPos
|
||||
|
||||
const snapTarget = event.composedPath().find(
|
||||
(el): el is Element =>
|
||||
el instanceof Element &&
|
||||
// el !== thumbNode &&
|
||||
el.hasAttribute('data-pos'),
|
||||
)
|
||||
|
||||
if (snapTarget) {
|
||||
const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!)
|
||||
|
||||
if (isFinite(snapPos)) {
|
||||
newPosition = snapPos
|
||||
}
|
||||
}
|
||||
|
||||
sequence.position = newPosition
|
||||
},
|
||||
onDragStart(event) {
|
||||
if (event.target instanceof HTMLInputElement) return false
|
||||
if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
event
|
||||
.composedPath()
|
||||
.some((el) => el instanceof HTMLElement && el.draggable === true)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const initialPositionInClippedSpace =
|
||||
event.clientX - containerEl!.getBoundingClientRect().left
|
||||
|
||||
|
@ -114,11 +142,12 @@ function useDragHandlers(
|
|||
onDragEnd() {
|
||||
setIsSeeking(false)
|
||||
},
|
||||
lockCursorTo: 'ew-resize',
|
||||
}
|
||||
}, [layoutP, containerEl])
|
||||
|
||||
useDrag(containerEl, handlers)
|
||||
const [isDragigng] = useDrag(containerEl, handlers)
|
||||
|
||||
useCursorLock(isDragigng, 'draggingPositionInSequenceEditor', 'ew-resize')
|
||||
}
|
||||
|
||||
function useHandlePanAndZoom(
|
||||
|
@ -201,13 +230,17 @@ function useUpdateScrollFromClippedSpaceRange(
|
|||
return rangeStartInScaledSpace
|
||||
})
|
||||
|
||||
const untap = d.changesWithoutValues().tap(() => {
|
||||
const update = () => {
|
||||
const rangeStartInScaledSpace = d.getValue()
|
||||
|
||||
node.scrollLeft = rangeStartInScaledSpace
|
||||
})
|
||||
}
|
||||
const untap = d.changesWithoutValues().tap(update)
|
||||
|
||||
update()
|
||||
const timeout = setTimeout(update, 100)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
untap()
|
||||
}
|
||||
}, [layoutP, node])
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {GoChevronLeft, GoChevronRight} from 'react-icons/all'
|
||||
import LengthEditorPopover from './LengthEditorPopover'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
||||
const coverWidth = 1000
|
||||
|
||||
|
@ -71,7 +72,7 @@ const Tooltip = styled.div`
|
|||
white-space: nowrap;
|
||||
padding: 1px 8px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
cursor: ew-resize;
|
||||
color: #464646;
|
||||
background-color: #0000004d;
|
||||
|
@ -89,7 +90,7 @@ const Tumb = styled.div`
|
|||
white-space: nowrap;
|
||||
padding: 1px 2px;
|
||||
border-radius: 2px;
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: ew-resize;
|
||||
|
|
|
@ -11,6 +11,7 @@ import React, {useMemo, useRef} from 'react'
|
|||
import styled from 'styled-components'
|
||||
import {transformBox} from './Curve'
|
||||
import type KeyframeEditor from './KeyframeEditor'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
||||
export const dotSize = 6
|
||||
|
||||
|
@ -28,7 +29,7 @@ const HitZone = styled.circle`
|
|||
r: 6px;
|
||||
fill: transparent;
|
||||
cursor: move;
|
||||
pointer-events: all;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
&:hover {
|
||||
}
|
||||
&:hover + ${Circle} {
|
||||
|
|
|
@ -12,6 +12,8 @@ import type KeyframeEditor from './KeyframeEditor'
|
|||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import {useCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
|
||||
export const dotSize = 6
|
||||
|
||||
|
@ -28,13 +30,23 @@ const HitZone = styled.circle`
|
|||
vector-effect: non-scaling-stroke;
|
||||
r: 6px;
|
||||
fill: transparent;
|
||||
cursor: move;
|
||||
pointer-events: all;
|
||||
&:hover {
|
||||
}
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
||||
&:hover + ${Circle} {
|
||||
r: 6px;
|
||||
}
|
||||
|
||||
#pointer-root.normal & {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
#pointer-root.draggingpositioninsequenceeditor & {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&.beingDragged {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
`
|
||||
|
||||
type IProps = Parameters<typeof KeyframeEditor>[0]
|
||||
|
@ -47,7 +59,7 @@ const Dot: React.FC<IProps> = (props) => {
|
|||
const next = trackData.keyframes[index + 1]
|
||||
|
||||
const [contextMenu] = useKeyframeContextMenu(node, props)
|
||||
useDragKeyframe(node, props)
|
||||
const isDragging = useDragKeyframe(node, props)
|
||||
|
||||
const cyInExtremumSpace = props.extremumSpace.fromValueSpace(cur.value)
|
||||
|
||||
|
@ -63,6 +75,8 @@ const Dot: React.FC<IProps> = (props) => {
|
|||
{...{
|
||||
[attributeNameThatLocksFramestamp]: cur.position.toFixed(3),
|
||||
}}
|
||||
data-pos={cur.position.toFixed(3)}
|
||||
className={isDragging ? 'beingDragged' : ''}
|
||||
/>
|
||||
<Circle
|
||||
style={{
|
||||
|
@ -78,7 +92,10 @@ const Dot: React.FC<IProps> = (props) => {
|
|||
|
||||
export default Dot
|
||||
|
||||
function useDragKeyframe(node: SVGCircleElement | null, _props: IProps): void {
|
||||
function useDragKeyframe(
|
||||
node: SVGCircleElement | null,
|
||||
_props: IProps,
|
||||
): boolean {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
useLockFrameStampPosition(isDragging, _props.keyframe.position)
|
||||
const propsRef = useRef(_props)
|
||||
|
@ -203,6 +220,8 @@ function useDragKeyframe(node: SVGCircleElement | null, _props: IProps): void {
|
|||
}, [])
|
||||
|
||||
useDrag(node, gestureHandlers)
|
||||
useCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
||||
return isDragging
|
||||
}
|
||||
|
||||
function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import React, {useCallback, useMemo, useState} from 'react'
|
|||
import styled from 'styled-components'
|
||||
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
||||
const Container = styled.div`
|
||||
--threadHeight: 6px;
|
||||
|
@ -20,7 +21,7 @@ const Container = styled.div`
|
|||
width: 100%;
|
||||
left: 12px;
|
||||
/* bottom: 8px; */
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
z-index: ${() => zIndexes.horizontalScrollbar};
|
||||
`
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
attributeNameThatLocksFramestamp,
|
||||
useLockFrameStampPosition,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
||||
const Container = styled.div<{isVisible: boolean}>`
|
||||
--thumbColor: #00e0ff;
|
||||
|
@ -36,6 +37,17 @@ const Rod = styled.div`
|
|||
height: calc(100% - 8px);
|
||||
border-left: 1px solid #27e0fd;
|
||||
z-index: 10;
|
||||
|
||||
#pointer-root.draggingpositioninsequenceeditor &:not(.seeking) {
|
||||
pointer-events: auto;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
display: block;
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Thumb = styled.div`
|
||||
|
@ -47,7 +59,11 @@ const Thumb = styled.div`
|
|||
left: -2px;
|
||||
z-index: 11;
|
||||
cursor: ew-resize;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
||||
#pointer-root.draggingpositioninsequenceeditor &:not(.seeking) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
|
@ -148,10 +164,29 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
||||
setIsSeeking(true)
|
||||
},
|
||||
onDrag(dx) {
|
||||
onDrag(dx, _, event) {
|
||||
const deltaPos = scaledSpaceToUnitSpace(dx)
|
||||
const newPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
|
||||
sequence.position = newPos
|
||||
const unsnappedPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
|
||||
|
||||
let newPosition = unsnappedPos
|
||||
|
||||
const snapTarget = event
|
||||
.composedPath()
|
||||
.find(
|
||||
(el): el is Element =>
|
||||
el instanceof Element &&
|
||||
el !== thumbNode &&
|
||||
el.hasAttribute('data-pos'),
|
||||
)
|
||||
|
||||
if (snapTarget) {
|
||||
const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!)
|
||||
if (isFinite(snapPos)) {
|
||||
newPosition = snapPos
|
||||
}
|
||||
}
|
||||
|
||||
sequence.position = newPosition
|
||||
},
|
||||
onDragEnd() {
|
||||
setIsSeeking(false)
|
||||
|
@ -186,7 +221,10 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
className={isSeeking ? 'seeking' : ''}
|
||||
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||
>
|
||||
<Thumb ref={thumbRef as $IntentionalAny}>
|
||||
<Thumb
|
||||
ref={thumbRef as $IntentionalAny}
|
||||
data-pos={posInUnitSpace.toFixed(3)}
|
||||
>
|
||||
<RoomToClick room={8} />
|
||||
<Squinch />
|
||||
<Tooltip>
|
||||
|
@ -196,7 +234,10 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
</Tooltip>
|
||||
</Thumb>
|
||||
|
||||
<Rod />
|
||||
<Rod
|
||||
data-pos={posInUnitSpace.toFixed(3)}
|
||||
className={isSeeking ? 'seeking' : ''}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}, [layoutP])
|
||||
|
|
|
@ -6,6 +6,7 @@ import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEdi
|
|||
import StampsGrid from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid'
|
||||
import PanelDragZone from '@theatre/studio/panels/BasePanel/PanelDragZone'
|
||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
||||
export const topStripHeight = 20
|
||||
|
||||
|
@ -23,7 +24,7 @@ const Container = styled(PanelDragZone)`
|
|||
box-sizing: border-box;
|
||||
background: ${topStripTheme.backgroundColor};
|
||||
border-bottom: 1px solid ${topStripTheme.borderColor};
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
`
|
||||
|
||||
const TopStrip: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
// export const
|
||||
export {}
|
81
theatre/studio/src/uiComponents/PointerEventsHandler.tsx
Normal file
81
theatre/studio/src/uiComponents/PointerEventsHandler.tsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
// using an ID to make CSS selectors faster
|
||||
const elementId = 'pointer-root'
|
||||
|
||||
const Container = styled.div`
|
||||
pointer-events: auto;
|
||||
&.normal {
|
||||
pointer-events: none;
|
||||
}
|
||||
`
|
||||
|
||||
const CursorOverride = styled.div<{cursor: null | string}>`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
|
||||
#pointer-root:not(.normal) > & {
|
||||
cursor: ${(props) => props.cursor ?? 'default'};
|
||||
pointer-events: auto;
|
||||
}
|
||||
`
|
||||
|
||||
type Context = {
|
||||
getLock: (className: string, cursor: string) => () => void
|
||||
}
|
||||
|
||||
type Lock = {className: string; cursor: string}
|
||||
|
||||
const context = createContext<Context>({} as $IntentionalAny)
|
||||
|
||||
const PointerEventsHandler: React.FC<{
|
||||
className?: string
|
||||
}> = (props) => {
|
||||
const [locks, setLocks] = useState<Lock[]>([])
|
||||
const contextValue = useMemo<Context>(() => {
|
||||
const getLock = (className: string, cursor: string) => {
|
||||
const lock = {className, cursor}
|
||||
setLocks((s) => [...s, lock])
|
||||
const unlock = () => {
|
||||
setLocks((s) => s.filter((l) => l !== lock))
|
||||
}
|
||||
return unlock
|
||||
}
|
||||
return {
|
||||
getLock,
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<context.Provider value={contextValue}>
|
||||
<Container id={elementId} className={locks[0]?.className ?? 'normal'}>
|
||||
<CursorOverride cursor={locks[0]?.cursor}>
|
||||
{props.children}
|
||||
</CursorOverride>
|
||||
</Container>
|
||||
</context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useCursorLock = (
|
||||
enabled: boolean,
|
||||
className: string,
|
||||
cursor: string,
|
||||
) => {
|
||||
const ctx = useContext(context)
|
||||
useLayoutEffect(() => {
|
||||
if (!enabled) return
|
||||
return ctx.getLock(className, cursor)
|
||||
}, [enabled, className, cursor])
|
||||
}
|
||||
|
||||
export default PointerEventsHandler
|
|
@ -1,3 +1,4 @@
|
|||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
|
||||
import transparentize from 'polished/lib/color/transparentize'
|
||||
|
@ -23,7 +24,7 @@ const Container = styled.ul`
|
|||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
pointer-events: all;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
border-radius: 3px;
|
||||
`
|
||||
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
export function createCursorLock(cursor: string) {
|
||||
const el = document.createElement('div')
|
||||
el.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 9999999;`
|
||||
// import getStudio from '@theatre/studio/getStudio'
|
||||
|
||||
el.style.cursor = cursor
|
||||
document.body.appendChild(el)
|
||||
const relinquish = () => {
|
||||
document.body.removeChild(el)
|
||||
}
|
||||
// export function createCursorLock(cursor: string) {
|
||||
// const el = getStudio()!.ui.containerShadow.getElementById(
|
||||
// 'pointer-events-root',
|
||||
// )! as HTMLDivElement
|
||||
|
||||
return relinquish
|
||||
}
|
||||
// el.style.cursor = cursor
|
||||
// el.classList.remove('pointer-events-mode-normal')
|
||||
// el.classList.add('pointer-events-mode-locked-for-drag')
|
||||
// const relinquish = () => {
|
||||
// el.style.cursor = ''
|
||||
// el.classList.add('pointer-events-mode-normal')
|
||||
// el.classList.remove('pointer-events-mode-locked-for-drag')
|
||||
// }
|
||||
|
||||
// return relinquish
|
||||
// }
|
||||
export {}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
|
||||
import transparentize from 'polished/lib/color/transparentize'
|
||||
|
@ -26,7 +27,7 @@ const Container = styled.ul`
|
|||
margin: 0;
|
||||
border-radius: 1px;
|
||||
cursor: default;
|
||||
pointer-events: all;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
border-radius: 3px;
|
||||
`
|
||||
|
||||
|
|
|
@ -4,11 +4,12 @@ import styled from 'styled-components'
|
|||
import type {ButtonProps} from 'reakit'
|
||||
import {outlinePanelTheme} from '@theatre/studio/panels/OutlinePanel/BaseItem'
|
||||
import {darken, opacify} from 'polished'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
||||
const {baseBg, baseBorderColor, baseFontColor} = outlinePanelTheme
|
||||
|
||||
export const TheButton = styled.button`
|
||||
pointer-events: auto;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {voidFn} from '@theatre/shared/utils'
|
||||
import type {$FixMe} from '@theatre/shared/utils/types'
|
||||
import {useLayoutEffect, useRef} from 'react'
|
||||
import {createCursorLock} from './createCursorLock'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import {useCursorLock} from './PointerEventsHandler'
|
||||
|
||||
export type UseDragOpts = {
|
||||
disabled?: boolean
|
||||
|
@ -15,12 +15,19 @@ export type UseDragOpts = {
|
|||
export default function useDrag(
|
||||
target: HTMLElement | SVGElement | undefined | null,
|
||||
opts: UseDragOpts,
|
||||
) {
|
||||
): [isDragging: boolean] {
|
||||
const optsRef = useRef<typeof opts>(opts)
|
||||
optsRef.current = opts
|
||||
|
||||
const modeRef =
|
||||
useRef<'dragStartCalled' | 'dragging' | 'notDragging'>('notDragging')
|
||||
const [modeRef, mode] = useRefAndState<
|
||||
'dragStartCalled' | 'dragging' | 'notDragging'
|
||||
>('notDragging')
|
||||
|
||||
useCursorLock(
|
||||
mode === 'dragging' && typeof opts.lockCursorTo === 'string',
|
||||
'dragging',
|
||||
opts.lockCursorTo!,
|
||||
)
|
||||
|
||||
const stateRef = useRef<{
|
||||
dragHappened: boolean
|
||||
|
@ -38,12 +45,7 @@ export default function useDrag(
|
|||
return [event.screenX - startPos.x, event.screenY - startPos.y]
|
||||
}
|
||||
|
||||
let relinquishCursorLock = voidFn
|
||||
|
||||
const dragHandler = (event: MouseEvent) => {
|
||||
if (!stateRef.current.dragHappened && optsRef.current.lockCursorTo) {
|
||||
relinquishCursorLock = createCursorLock(optsRef.current.lockCursorTo)
|
||||
}
|
||||
if (!stateRef.current.dragHappened) stateRef.current.dragHappened = true
|
||||
modeRef.current = 'dragging'
|
||||
|
||||
|
@ -57,8 +59,6 @@ export default function useDrag(
|
|||
|
||||
optsRef.current.onDragEnd &&
|
||||
optsRef.current.onDragEnd(stateRef.current.dragHappened)
|
||||
relinquishCursorLock()
|
||||
relinquishCursorLock = voidFn
|
||||
}
|
||||
|
||||
const addDragListeners = () => {
|
||||
|
@ -119,7 +119,6 @@ export default function useDrag(
|
|||
removeDragListeners()
|
||||
target.removeEventListener('mousedown', onMouseDown as $FixMe)
|
||||
target.removeEventListener('click', preventUnwantedClick as $FixMe)
|
||||
relinquishCursorLock()
|
||||
|
||||
if (modeRef.current !== 'notDragging') {
|
||||
optsRef.current.onDragEnd &&
|
||||
|
@ -128,4 +127,6 @@ export default function useDrag(
|
|||
modeRef.current = 'notDragging'
|
||||
}
|
||||
}, [target])
|
||||
|
||||
return [mode === 'dragging']
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue