Single tween editor for aggregate rows (#178)
Co-authored-by: Cole Lawrence <cole@colelawrence.com> Co-authored-by: Aria Minaei <aria.minaei@gmail.com>
This commit is contained in:
parent
9b4aa4b0e0
commit
564e54c314
9 changed files with 535 additions and 244 deletions
|
@ -179,6 +179,31 @@ export default abstract class AbstractDerivation<V> implements IDerivation<V> {
|
|||
* Gets the current value of the derivation. If the value is stale, it causes the derivation to freshen.
|
||||
*/
|
||||
getValue(): V {
|
||||
/**
|
||||
* TODO We should prevent (or warn about) a common mistake users make, which is reading the value of
|
||||
* a derivation in the body of a react component (e.g. `der.getValue()` (often via `val()`) instead of `useVal()`
|
||||
* or `uesPrism()`).
|
||||
*
|
||||
* Although that's the most common example of this mistake, you can also find it outside of react components.
|
||||
* Basically the user runs `der.getValue()` assuming the read is detected by a wrapping prism when it's not.
|
||||
*
|
||||
* Sometiems the derivation isn't even hot when the user assumes it is.
|
||||
*
|
||||
* We can fix this type of mistake by:
|
||||
* 1. Warning the user when they call `getValue()` on a cold derivation.
|
||||
* 2. Warning the user about calling `getValue()` on a hot-but-stale derivation
|
||||
* if `getValue()` isn't called by a known mechanism like a `DerivationEmitter`.
|
||||
*
|
||||
* Design constraints:
|
||||
* - This fix should not have a perf-penalty in production. Perhaps use a global flag + `process.env.NODE_ENV !== 'production'`
|
||||
* to enable it.
|
||||
* - In the case of `DerivationValuelessEmitter`, we don't control when the user calls
|
||||
* `getValue()` (as opposed to `DerivationEmitter` which calls `getValue()` directly).
|
||||
* Perhaps we can disable the check in that case.
|
||||
* - Probably the best place to add this check is right here in this method plus some changes to `reportResulutionStart()`,
|
||||
* which would have to be changed to let the caller know if there is an actual collector (a prism)
|
||||
* present in its stack.
|
||||
*/
|
||||
reportResolutionStart(this)
|
||||
|
||||
if (!this._isFresh) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
SequenceEditorTree_SheetObject,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||
import type {Pointer} from '@theatre/dataverse'
|
||||
import {prism} from '@theatre/dataverse'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
@ -20,11 +21,28 @@ import {AggregateKeyframePositionIsSelected} from './AggregatedKeyframeTrack'
|
|||
import type {KeyframeWithTrack} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
|
||||
import {DopeSnapHitZoneUI} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnapHitZoneUI'
|
||||
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
||||
import {usePointerCapturing} from '@theatre/studio/UIRoot/PointerCapturing'
|
||||
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||
import CurveEditorPopover, {
|
||||
isConnectionEditingInCurvePopover,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/CurveEditorPopover/CurveEditorPopover'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import {usePrism} from '@theatre/react'
|
||||
import {selectedKeyframeConnections} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||
import type {SheetObjectAddress} from '@theatre/shared/utils/addresses'
|
||||
|
||||
const POPOVER_MARGIN_PX = 5
|
||||
|
||||
const AggregateKeyframeEditorContainer = styled.div`
|
||||
position: absolute;
|
||||
`
|
||||
|
||||
const EasingPopoverWrapper = styled(BasicPopover)`
|
||||
--popover-outer-stroke: transparent;
|
||||
--popover-inner-stroke: rgba(26, 28, 30, 0.97);
|
||||
`
|
||||
|
||||
const noConnector = <></>
|
||||
|
||||
export type IAggregateKeyframesAtPosition = {
|
||||
|
@ -41,6 +59,12 @@ export type IAggregateKeyframesAtPosition = {
|
|||
}[]
|
||||
}
|
||||
|
||||
type AggregatedKeyframeConnection = SheetObjectAddress & {
|
||||
trackId: SequenceTrackId
|
||||
left: Keyframe
|
||||
right: Keyframe
|
||||
}
|
||||
|
||||
export type IAggregateKeyframeEditorProps = {
|
||||
index: number
|
||||
aggregateKeyframes: IAggregateKeyframesAtPosition[]
|
||||
|
@ -51,24 +75,50 @@ export type IAggregateKeyframeEditorProps = {
|
|||
selection: undefined | DopeSheetSelection
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO we're spending a lot of cycles on each render of each aggreagte keyframes.
|
||||
*
|
||||
* Each keyframe node is doing O(N) operations, N being the number of underlying
|
||||
* keyframes it represetns.
|
||||
*
|
||||
* The biggest example is the `isConnectionEditingInCurvePopover()` call which is run
|
||||
* for every underlying keyframe, every time this component is rendered.
|
||||
*
|
||||
* We can optimize this away by doing all of this work _once_ when a curve editor popover
|
||||
* is open. This would require having some kind of stable identity for each aggregate row.
|
||||
* Let's defer that work until other interactive keyframe editing PRs are merged in.
|
||||
*/
|
||||
const AggregateKeyframeEditor: React.VFC<IAggregateKeyframeEditorProps> = (
|
||||
props,
|
||||
) => {
|
||||
const {index, aggregateKeyframes} = props
|
||||
const cur = aggregateKeyframes[index]
|
||||
const next = aggregateKeyframes[index + 1]
|
||||
const connected =
|
||||
next && cur.keyframes.length === next.keyframes.length
|
||||
? // all keyframes are same in the next position
|
||||
cur.keyframes.every(
|
||||
({track}, ind) => next.keyframes[ind].track === track,
|
||||
) && {
|
||||
length: next.position - cur.position,
|
||||
selected:
|
||||
cur.selected === AggregateKeyframePositionIsSelected.AllSelected &&
|
||||
next.selected === AggregateKeyframePositionIsSelected.AllSelected,
|
||||
const {cur, connected, isAggregateEditingInCurvePopover} =
|
||||
useAggregateKeyframeEditorUtils(props)
|
||||
|
||||
const {isPointerBeingCaptured} = usePointerCapturing(
|
||||
'AggregateKeyframeEditor Connector',
|
||||
)
|
||||
|
||||
const [popoverNode, openPopover, closePopover] = usePopover(
|
||||
() => {
|
||||
const rightDims = val(props.layoutP.rightDims)
|
||||
|
||||
return {
|
||||
debugName: 'Connector',
|
||||
closeWhenPointerIsDistant: !isPointerBeingCaptured(),
|
||||
constraints: {
|
||||
minX: rightDims.screenX + POPOVER_MARGIN_PX,
|
||||
maxX: rightDims.screenX + rightDims.width - POPOVER_MARGIN_PX,
|
||||
},
|
||||
}
|
||||
: null
|
||||
},
|
||||
() => {
|
||||
return (
|
||||
<AggregateCurveEditorPopover {...props} closePopover={closePopover} />
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||
|
||||
return (
|
||||
<AggregateKeyframeEditorContainer
|
||||
|
@ -91,20 +141,103 @@ const AggregateKeyframeEditor: React.VFC<IAggregateKeyframeEditorProps> = (
|
|||
/>
|
||||
{connected ? (
|
||||
<ConnectorLine
|
||||
/* TEMP: Disabled until interactivity */
|
||||
mvpIsInteractiveDisabled={true}
|
||||
ref={nodeRef}
|
||||
connectorLengthInUnitSpace={connected.length}
|
||||
isPopoverOpen={false}
|
||||
isPopoverOpen={isAggregateEditingInCurvePopover}
|
||||
// if all keyframe aggregates are selected
|
||||
isSelected={connected.selected}
|
||||
openPopover={(e) => {
|
||||
if (node) openPopover(e, node)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
noConnector
|
||||
)}
|
||||
{popoverNode}
|
||||
</AggregateKeyframeEditorContainer>
|
||||
)
|
||||
}
|
||||
|
||||
function useAggregateKeyframeEditorUtils(
|
||||
props: Pick<
|
||||
IAggregateKeyframeEditorProps,
|
||||
'index' | 'aggregateKeyframes' | 'selection' | 'viewModel'
|
||||
>,
|
||||
) {
|
||||
return usePrism(() => {
|
||||
const {index, aggregateKeyframes} = props
|
||||
|
||||
const cur = aggregateKeyframes[index]
|
||||
const next = aggregateKeyframes[index + 1]
|
||||
|
||||
const curAndNextAggregateKeyframesMatch =
|
||||
next &&
|
||||
cur.keyframes.length === next.keyframes.length &&
|
||||
cur.keyframes.every(({track}, ind) => next.keyframes[ind].track === track)
|
||||
|
||||
const connected = curAndNextAggregateKeyframesMatch
|
||||
? {
|
||||
length: next.position - cur.position,
|
||||
selected:
|
||||
cur.selected === AggregateKeyframePositionIsSelected.AllSelected &&
|
||||
next.selected === AggregateKeyframePositionIsSelected.AllSelected,
|
||||
}
|
||||
: null
|
||||
|
||||
const aggregatedConnections: AggregatedKeyframeConnection[] = !connected
|
||||
? []
|
||||
: cur.keyframes.map(({kf, track}, i) => ({
|
||||
...props.viewModel.sheetObject.address,
|
||||
trackId: track.id,
|
||||
left: kf,
|
||||
right: next.keyframes[i].kf,
|
||||
}))
|
||||
|
||||
const {projectId, sheetId} = props.viewModel.sheetObject.address
|
||||
|
||||
const selectedConnections = prism
|
||||
.memo(
|
||||
'selectedConnections',
|
||||
() =>
|
||||
selectedKeyframeConnections(
|
||||
props.viewModel.sheetObject.address.projectId,
|
||||
props.viewModel.sheetObject.address.sheetId,
|
||||
props.selection,
|
||||
),
|
||||
[projectId, sheetId, props.selection],
|
||||
)
|
||||
.getValue()
|
||||
|
||||
const allConnections = [...aggregatedConnections, ...selectedConnections]
|
||||
|
||||
const isAggregateEditingInCurvePopover = aggregatedConnections.every(
|
||||
(con) => isConnectionEditingInCurvePopover(con),
|
||||
)
|
||||
|
||||
return {cur, connected, isAggregateEditingInCurvePopover, allConnections}
|
||||
}, [])
|
||||
}
|
||||
|
||||
const AggregateCurveEditorPopover: React.FC<
|
||||
IAggregateKeyframeEditorProps & {closePopover: (reason: string) => void}
|
||||
> = React.forwardRef((props, ref) => {
|
||||
const {allConnections} = useAggregateKeyframeEditorUtils(props)
|
||||
|
||||
return (
|
||||
<EasingPopoverWrapper
|
||||
showPopoverEdgeTriangle={false}
|
||||
// @ts-ignore @todo
|
||||
ref={ref}
|
||||
>
|
||||
<CurveEditorPopover
|
||||
curveConnection={allConnections[0]}
|
||||
additionalConnections={allConnections}
|
||||
onRequestClose={props.closePopover}
|
||||
/>
|
||||
</EasingPopoverWrapper>
|
||||
)
|
||||
})
|
||||
|
||||
const DOT_SIZE_PX = 16
|
||||
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 5
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import {useMemo, useRef} from 'react'
|
|||
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
||||
import CurveEditorPopover, {
|
||||
isCurveEditorOpenD,
|
||||
isConnectionEditingInCurvePopover,
|
||||
} from './CurveEditorPopover/CurveEditorPopover'
|
||||
import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
|
||||
import type {OpenFn} from '@theatre/studio/src/uiComponents/Popover/usePopover'
|
||||
|
@ -19,13 +19,11 @@ import type {ISingleKeyframeEditorProps} from './SingleKeyframeEditor'
|
|||
import type {IConnectorThemeValues} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/keyframeRowUI/ConnectorLine'
|
||||
import {ConnectorLine} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/keyframeRowUI/ConnectorLine'
|
||||
import {COLOR_POPOVER_BACK} from './CurveEditorPopover/colors'
|
||||
import {useVal} from '@theatre/react'
|
||||
import {isKeyframeConnectionInSelection} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||
import {usePrism} from '@theatre/react'
|
||||
import type {KeyframeConnectionWithAddress} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||
import {selectedKeyframeConnections} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||
|
||||
const CONNECTOR_HEIGHT = DOT_SIZE_PX / 2 + 1
|
||||
const CONNECTOR_WIDTH_UNSCALED = 1000
|
||||
import styled from 'styled-components'
|
||||
import {DOT_SIZE_PX} from './SingleKeyframeDot'
|
||||
|
||||
const POPOVER_MARGIN = 5
|
||||
|
||||
|
@ -49,23 +47,19 @@ const BasicKeyframeConnector: React.VFC<IBasicKeyframeConnectorProps> = (
|
|||
'KeyframeEditor Connector',
|
||||
)
|
||||
|
||||
const rightDims = val(props.layoutP.rightDims)
|
||||
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover(
|
||||
{
|
||||
() => {
|
||||
const rightDims = val(props.layoutP.rightDims)
|
||||
return {
|
||||
debugName: 'Connector',
|
||||
closeWhenPointerIsDistant: !isPointerBeingCaptured(),
|
||||
constraints: {
|
||||
minX: rightDims.screenX + POPOVER_MARGIN,
|
||||
maxX: rightDims.screenX + rightDims.width - POPOVER_MARGIN,
|
||||
},
|
||||
}
|
||||
},
|
||||
() => {
|
||||
return (
|
||||
<EasingPopover showPopoverEdgeTriangle={false}>
|
||||
<CurveEditorPopover {...props} onRequestClose={closePopover} />
|
||||
</EasingPopover>
|
||||
)
|
||||
},
|
||||
() => <SingleCurveEditorPopover {...props} closePopover={closePopover} />,
|
||||
)
|
||||
|
||||
const [contextMenu] = useConnectorContextMenu(
|
||||
|
@ -79,26 +73,27 @@ const BasicKeyframeConnector: React.VFC<IBasicKeyframeConnectorProps> = (
|
|||
|
||||
const connectorLengthInUnitSpace = next.position - cur.position
|
||||
|
||||
// The following two flags determine whether this connector
|
||||
// is being edited as part of a selection using the curve
|
||||
// editor popover
|
||||
const isCurveEditorPopoverOpen = useVal(isCurveEditorOpenD)
|
||||
const isInCurveEditorPopoverSelection =
|
||||
isCurveEditorPopoverOpen &&
|
||||
props.selection !== undefined &&
|
||||
isKeyframeConnectionInSelection([cur, next], props.selection)
|
||||
const isInCurveEditorPopoverSelection = usePrism(
|
||||
() =>
|
||||
isConnectionEditingInCurvePopover({
|
||||
...props.leaf.sheetObject.address,
|
||||
trackId: props.leaf.trackId,
|
||||
left: cur,
|
||||
right: next,
|
||||
}),
|
||||
[props.leaf.sheetObject.address, props.leaf.trackId, cur, next],
|
||||
)
|
||||
|
||||
const themeValues: IConnectorThemeValues = {
|
||||
isPopoverOpen: isPopoverOpen || isInCurveEditorPopoverSelection || false,
|
||||
isSelected: !!props.selection,
|
||||
isPopoverOpen: isInCurveEditorPopoverSelection,
|
||||
isSelected: props.selection !== undefined,
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectorLine
|
||||
ref={nodeRef}
|
||||
connectorLengthInUnitSpace={connectorLengthInUnitSpace}
|
||||
isPopoverOpen={isPopoverOpen}
|
||||
isSelected={!!props.selection}
|
||||
{...themeValues}
|
||||
openPopover={(e) => {
|
||||
if (node) openPopover(e, node)
|
||||
}}
|
||||
|
@ -110,6 +105,48 @@ const BasicKeyframeConnector: React.VFC<IBasicKeyframeConnectorProps> = (
|
|||
}
|
||||
export default BasicKeyframeConnector
|
||||
|
||||
const SingleCurveEditorPopover: React.FC<
|
||||
IBasicKeyframeConnectorProps & {closePopover: (reason: string) => void}
|
||||
> = React.forwardRef((props, ref) => {
|
||||
const {index, trackData, selection} = props
|
||||
const cur = trackData.keyframes[index]
|
||||
const next = trackData.keyframes[index + 1]
|
||||
|
||||
const trackId = props.leaf.trackId
|
||||
const address = props.leaf.sheetObject.address
|
||||
|
||||
const selectedConnections = usePrism(
|
||||
() =>
|
||||
selectedKeyframeConnections(
|
||||
address.projectId,
|
||||
address.sheetId,
|
||||
selection,
|
||||
).getValue(),
|
||||
[address, selection],
|
||||
)
|
||||
|
||||
const curveConnection: KeyframeConnectionWithAddress = {
|
||||
left: cur,
|
||||
right: next,
|
||||
trackId,
|
||||
...address,
|
||||
}
|
||||
|
||||
return (
|
||||
<EasingPopover
|
||||
showPopoverEdgeTriangle={false}
|
||||
// @ts-ignore @todo
|
||||
ref={ref}
|
||||
>
|
||||
<CurveEditorPopover
|
||||
curveConnection={curveConnection}
|
||||
additionalConnections={selectedConnections}
|
||||
onRequestClose={props.closePopover}
|
||||
/>
|
||||
</EasingPopover>
|
||||
)
|
||||
})
|
||||
|
||||
function useDragKeyframe(
|
||||
node: HTMLDivElement | null,
|
||||
props: IBasicKeyframeConnectorProps,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type {Pointer} from '@theatre/dataverse'
|
||||
import {Box, prism} from '@theatre/dataverse'
|
||||
import type {KeyboardEvent} from 'react'
|
||||
import React, {
|
||||
|
@ -10,10 +9,8 @@ import React, {
|
|||
} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import fuzzy from 'fuzzy'
|
||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||
import type {ISingleKeyframeEditorProps} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/SingleKeyframeEditor'
|
||||
import CurveSegmentEditor from './CurveSegmentEditor'
|
||||
import EasingOption from './EasingOption'
|
||||
import type {CSSCubicBezierArgsString, CubicBezierHandles} from './shared'
|
||||
|
@ -27,11 +24,7 @@ import {COLOR_BASE, COLOR_POPOVER_BACK} from './colors'
|
|||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||
import {useUIOptionGrid, Outcome} from './useUIOptionGrid'
|
||||
import {useVal} from '@theatre/react'
|
||||
import {
|
||||
flatSelectionTrackIds,
|
||||
selectedKeyframeConnections,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||
import type {KeyframeConnectionWithAddress} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||
|
||||
const PRESET_COLUMNS = 3
|
||||
const PRESET_SIZE = 53
|
||||
|
@ -126,16 +119,22 @@ enum TextInputMode {
|
|||
multipleValues,
|
||||
}
|
||||
|
||||
type IProps = {
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
|
||||
type ICurveEditorPopoverProps = {
|
||||
/**
|
||||
* Called when user hits enter/escape
|
||||
*/
|
||||
onRequestClose: (reason: string) => void
|
||||
} & ISingleKeyframeEditorProps
|
||||
|
||||
const CurveEditorPopover: React.FC<IProps> = (props) => {
|
||||
curveConnection: KeyframeConnectionWithAddress
|
||||
additionalConnections: Array<KeyframeConnectionWithAddress>
|
||||
}
|
||||
|
||||
const CurveEditorPopover: React.VFC<ICurveEditorPopoverProps> = (props) => {
|
||||
const allConnections = useMemo(
|
||||
() => [props.curveConnection, ...props.additionalConnections],
|
||||
[props.curveConnection, ...props.additionalConnections],
|
||||
)
|
||||
|
||||
////// `tempTransaction` //////
|
||||
/*
|
||||
* `tempTransaction` is used for all edits in this popover. The transaction
|
||||
|
@ -144,7 +143,7 @@ const CurveEditorPopover: React.FC<IProps> = (props) => {
|
|||
*/
|
||||
const tempTransaction = useRef<CommitOrDiscard | null>(null)
|
||||
useEffect(() => {
|
||||
const unlock = getLock()
|
||||
const unlock = getLock(allConnections)
|
||||
// Clean-up function, called when this React component unmounts.
|
||||
// When it unmounts, we want to commit edits that are outstanding
|
||||
return () => {
|
||||
|
@ -154,14 +153,11 @@ const CurveEditorPopover: React.FC<IProps> = (props) => {
|
|||
}, [tempTransaction])
|
||||
|
||||
////// Keyframe and trackdata //////
|
||||
const {index, trackData} = props
|
||||
const cur = trackData.keyframes[index]
|
||||
const next = trackData.keyframes[index + 1]
|
||||
const easing: CubicBezierHandles = [
|
||||
trackData.keyframes[index].handles[2],
|
||||
trackData.keyframes[index].handles[3],
|
||||
trackData.keyframes[index + 1].handles[0],
|
||||
trackData.keyframes[index + 1].handles[1],
|
||||
props.curveConnection.left.handles[2],
|
||||
props.curveConnection.left.handles[3],
|
||||
props.curveConnection.right.handles[0],
|
||||
props.curveConnection.right.handles[1],
|
||||
]
|
||||
|
||||
////// Text input data and reactivity //////
|
||||
|
@ -209,7 +205,7 @@ const CurveEditorPopover: React.FC<IProps> = (props) => {
|
|||
} else if (textInputMode === TextInputMode.multipleValues) {
|
||||
if (inputValue !== '') setInputValue('')
|
||||
}
|
||||
}, [trackData])
|
||||
}, allConnections)
|
||||
|
||||
// `edit` keeps track of the current edited state of the curve.
|
||||
const [edit, setEdit] = useState<CSSCubicBezierArgsString | null>(
|
||||
|
@ -220,21 +216,19 @@ const CurveEditorPopover: React.FC<IProps> = (props) => {
|
|||
|
||||
// When `preview` or `edit` change, use the `tempTransaction` to change the
|
||||
// curve in Theate's data.
|
||||
useMemo(() => {
|
||||
if (textInputMode !== TextInputMode.init)
|
||||
setTempValue(tempTransaction, props, cur, next, preview ?? edit ?? '')
|
||||
}, [preview, edit])
|
||||
////// selection stuff //////
|
||||
let selectedConnections: Array<[Keyframe, Keyframe]> = useVal(
|
||||
selectedKeyframeConnections(
|
||||
props.leaf.sheetObject.address.projectId,
|
||||
props.leaf.sheetObject.address.sheetId,
|
||||
props.selection,
|
||||
),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedConnections.some(areConnectedKeyframesTheSameAs([cur, next])) &&
|
||||
textInputMode !== TextInputMode.init &&
|
||||
textInputMode !== TextInputMode.multipleValues
|
||||
)
|
||||
setTempValue(tempTransaction, allConnections, preview ?? edit ?? '')
|
||||
}, [preview, edit, textInputMode])
|
||||
|
||||
////// selection stuff //////
|
||||
if (
|
||||
allConnections.some(
|
||||
areConnectedKeyframesTheSameAs(props.curveConnection),
|
||||
) &&
|
||||
textInputMode === TextInputMode.init
|
||||
) {
|
||||
setTextInputMode(TextInputMode.multipleValues)
|
||||
|
@ -289,7 +283,7 @@ const CurveEditorPopover: React.FC<IProps> = (props) => {
|
|||
setPreview(item.value)
|
||||
const onEasingOptionMouseOut = () => setPreview(null)
|
||||
const onSelectEasingOption = (item: {label: string; value: string}) => {
|
||||
setTempValue(tempTransaction, props, cur, next, item.value)
|
||||
setTempValue(tempTransaction, allConnections, item.value)
|
||||
props.onRequestClose('selected easing option')
|
||||
|
||||
return Outcome.Handled
|
||||
|
@ -396,7 +390,8 @@ const CurveEditorPopover: React.FC<IProps> = (props) => {
|
|||
</OptionsContainer>
|
||||
<CurveEditorContainer onClick={() => inputRef.current?.focus()}>
|
||||
<CurveSegmentEditor
|
||||
{...props}
|
||||
curveConnection={props.curveConnection}
|
||||
backgroundConnections={props.additionalConnections}
|
||||
onCurveChange={onCurveChange}
|
||||
onCancelCurveChange={onCancelCurveChange}
|
||||
/>
|
||||
|
@ -409,18 +404,19 @@ export default CurveEditorPopover
|
|||
|
||||
function setTempValue(
|
||||
tempTransaction: React.MutableRefObject<CommitOrDiscard | null>,
|
||||
props: IProps,
|
||||
cur: Keyframe,
|
||||
next: Keyframe,
|
||||
newCurve: string,
|
||||
keyframeConnections: Array<KeyframeConnectionWithAddress>,
|
||||
newCurveCssCubicBezier: string,
|
||||
): void {
|
||||
tempTransaction.current?.discard()
|
||||
tempTransaction.current = null
|
||||
|
||||
const handles = handlesFromCssCubicBezierArgs(newCurve)
|
||||
const handles = handlesFromCssCubicBezierArgs(newCurveCssCubicBezier)
|
||||
if (handles === null) return
|
||||
|
||||
tempTransaction.current = transactionSetCubicBezier(props, cur, next, handles)
|
||||
tempTransaction.current = transactionSetCubicBezier(
|
||||
keyframeConnections,
|
||||
handles,
|
||||
)
|
||||
}
|
||||
|
||||
function discardTempValue(
|
||||
|
@ -431,37 +427,37 @@ function discardTempValue(
|
|||
}
|
||||
|
||||
function transactionSetCubicBezier(
|
||||
props: IProps,
|
||||
cur: Keyframe,
|
||||
next: Keyframe,
|
||||
keyframeConnections: Array<KeyframeConnectionWithAddress>,
|
||||
handles: CubicBezierHandles,
|
||||
): CommitOrDiscard {
|
||||
return getStudio().tempTransaction(({stateEditors}) => {
|
||||
const {setTweenBetweenKeyframes} =
|
||||
const {setHandlesForKeyframe} =
|
||||
stateEditors.coreByProject.historic.sheetsById.sequence
|
||||
|
||||
// set easing for current connector
|
||||
setTweenBetweenKeyframes({
|
||||
...props.leaf.sheetObject.address,
|
||||
trackId: props.leaf.trackId,
|
||||
keyframeIds: [cur.id, next.id],
|
||||
handles,
|
||||
})
|
||||
|
||||
// set easings for selection
|
||||
if (props.selection) {
|
||||
for (const {objectKey, trackId, keyframeIds} of flatSelectionTrackIds(
|
||||
props.selection,
|
||||
)) {
|
||||
setTweenBetweenKeyframes({
|
||||
projectId: props.leaf.sheetObject.address.projectId,
|
||||
sheetId: props.leaf.sheetObject.address.sheetId,
|
||||
for (const {
|
||||
projectId,
|
||||
sheetId,
|
||||
objectKey,
|
||||
trackId,
|
||||
keyframeIds,
|
||||
handles,
|
||||
left,
|
||||
right,
|
||||
} of keyframeConnections) {
|
||||
setHandlesForKeyframe({
|
||||
projectId,
|
||||
sheetId,
|
||||
objectKey,
|
||||
trackId,
|
||||
keyframeId: left.id,
|
||||
start: [handles[0], handles[1]],
|
||||
})
|
||||
setHandlesForKeyframe({
|
||||
projectId,
|
||||
sheetId,
|
||||
objectKey,
|
||||
trackId,
|
||||
keyframeId: right.id,
|
||||
end: [handles[2], handles[3]],
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -479,36 +475,48 @@ function setTimeoutFunction(f: Function, timeout?: number) {
|
|||
return () => setTimeout(f, timeout)
|
||||
}
|
||||
|
||||
function areConnectedKeyframesTheSameAs([kfcur1, kfnext1]: [
|
||||
Keyframe,
|
||||
Keyframe,
|
||||
]) {
|
||||
return ([kfcur2, kfnext2]: [Keyframe, Keyframe]) =>
|
||||
kfcur1.handles[2] !== kfcur2.handles[2] ||
|
||||
kfcur1.handles[3] !== kfcur2.handles[3] ||
|
||||
kfnext1.handles[0] !== kfnext2.handles[0] ||
|
||||
kfnext1.handles[1] !== kfnext2.handles[1]
|
||||
function areConnectedKeyframesTheSameAs({
|
||||
left: left1,
|
||||
right: right1,
|
||||
}: {
|
||||
left: Keyframe
|
||||
right: Keyframe
|
||||
}) {
|
||||
return ({left: left2, right: right2}: {left: Keyframe; right: Keyframe}) =>
|
||||
left1.handles[2] !== left2.handles[2] ||
|
||||
left1.handles[3] !== left2.handles[3] ||
|
||||
right1.handles[0] !== right2.handles[0] ||
|
||||
right1.handles[1] !== right2.handles[1]
|
||||
}
|
||||
|
||||
const {isCurveEditorOpenD, getLock} = (() => {
|
||||
let lastId = 0
|
||||
const idsOfOpenCurveEditors = new Box<number[]>([])
|
||||
const {isCurveEditorOpenD, isConnectionEditingInCurvePopover, getLock} =
|
||||
(() => {
|
||||
const connectionsInCurvePopoverEdit = new Box<
|
||||
Array<KeyframeConnectionWithAddress>
|
||||
>([])
|
||||
|
||||
return {
|
||||
getLock() {
|
||||
const id = lastId++
|
||||
idsOfOpenCurveEditors.set([...idsOfOpenCurveEditors.get(), id])
|
||||
getLock(connections: Array<KeyframeConnectionWithAddress>) {
|
||||
connectionsInCurvePopoverEdit.set(connections)
|
||||
|
||||
return function unlock() {
|
||||
idsOfOpenCurveEditors.set(
|
||||
idsOfOpenCurveEditors.get().filter((cid) => cid !== id),
|
||||
)
|
||||
connectionsInCurvePopoverEdit.set([])
|
||||
}
|
||||
},
|
||||
isCurveEditorOpenD: prism(() => {
|
||||
return idsOfOpenCurveEditors.derivation.getValue().length > 0
|
||||
return connectionsInCurvePopoverEdit.derivation.getValue().length > 0
|
||||
}),
|
||||
// must be run in a prism
|
||||
isConnectionEditingInCurvePopover(con: KeyframeConnectionWithAddress) {
|
||||
prism.ensurePrism()
|
||||
return connectionsInCurvePopoverEdit.derivation
|
||||
.getValue()
|
||||
.some(
|
||||
({left, right}) =>
|
||||
con.left.id === left.id && con.right.id === right.id,
|
||||
)
|
||||
},
|
||||
}
|
||||
})()
|
||||
})()
|
||||
|
||||
export {isCurveEditorOpenD}
|
||||
export {isCurveEditorOpenD, isConnectionEditingInCurvePopover}
|
||||
|
|
|
@ -2,12 +2,12 @@ import React from 'react'
|
|||
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import clamp from 'lodash-es/clamp'
|
||||
import type CurveEditorPopover from './CurveEditorPopover'
|
||||
import styled from 'styled-components'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import type {CubicBezierHandles} from './shared'
|
||||
import {useFreezableMemo} from './useFreezableMemo'
|
||||
import {COLOR_BASE} from './colors'
|
||||
import type {KeyframeConnectionWithAddress} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||
|
||||
// Defines the dimensions of the SVG viewbox space
|
||||
const VIEWBOX_PADDING = 0.12
|
||||
|
@ -30,6 +30,13 @@ const CONTROL_COLOR = '#B3B3B3'
|
|||
const HANDLE_COLOR = '#3eaaa4'
|
||||
const HANDLE_HOVER_COLOR = '#67dfd8'
|
||||
|
||||
const BACKGROUND_CURVE_COLORS = [
|
||||
'goldenrod',
|
||||
'cornflowerblue',
|
||||
'dodgerblue',
|
||||
'lawngreen',
|
||||
]
|
||||
|
||||
const Circle = styled.circle`
|
||||
stroke-width: 0.1px;
|
||||
vector-effect: non-scaling-stroke;
|
||||
|
@ -53,23 +60,26 @@ const HitZone = styled.circle`
|
|||
}
|
||||
`
|
||||
|
||||
type IProps = {
|
||||
type ICurveSegmentEditorProps = {
|
||||
onCurveChange: (newHandles: CubicBezierHandles) => void
|
||||
onCancelCurveChange: () => void
|
||||
} & Parameters<typeof CurveEditorPopover>[0]
|
||||
|
||||
const CurveSegmentEditor: React.FC<IProps> = (props) => {
|
||||
const {index, trackData} = props
|
||||
const cur = trackData.keyframes[index]
|
||||
const next = trackData.keyframes[index + 1]
|
||||
curveConnection: KeyframeConnectionWithAddress
|
||||
backgroundConnections: Array<KeyframeConnectionWithAddress>
|
||||
}
|
||||
|
||||
const CurveSegmentEditor: React.VFC<ICurveSegmentEditorProps> = (props) => {
|
||||
const {
|
||||
curveConnection,
|
||||
curveConnection: {left, right},
|
||||
backgroundConnections,
|
||||
} = props
|
||||
// Calculations towards keeping the handles in the viewbox. The extremum space
|
||||
// of this editor vertically scales to keep the handles in the viewbox of the
|
||||
// SVG. This produces a nice "stretching space" effect while you are dragging
|
||||
// the handles.
|
||||
// Demo: https://user-images.githubusercontent.com/11082236/164542544-f1f66de2-f62e-44dd-b4cb-05b5f6e73a52.mp4
|
||||
const minY = Math.min(0, 1 - next.handles[1], 1 - cur.handles[3])
|
||||
const maxY = Math.max(1, 1 - next.handles[1], 1 - cur.handles[3])
|
||||
const minY = Math.min(0, 1 - right.handles[1], 1 - left.handles[3])
|
||||
const maxY = Math.max(1, 1 - right.handles[1], 1 - left.handles[3])
|
||||
const h = Math.max(1, maxY - minY)
|
||||
|
||||
const toExtremumSpace = (y: number) => (y - minY) / h
|
||||
|
@ -81,26 +91,26 @@ const CurveSegmentEditor: React.FC<IProps> = (props) => {
|
|||
|
||||
const [refLeft, nodeLeft] = useRefAndState<SVGCircleElement | null>(null)
|
||||
useKeyframeDrag(nodeSVG, nodeLeft, props, (dx, dy) => {
|
||||
const handleX = clamp(cur.handles[2] + dx * viewboxToElWidthRatio, 0, 1)
|
||||
const handleY = cur.handles[3] - dy * viewboxToElHeightRatio
|
||||
|
||||
return [handleX, handleY, next.handles[0], next.handles[1]]
|
||||
// TODO - document this
|
||||
const handleX = clamp(left.handles[2] + dx * viewboxToElWidthRatio, 0, 1)
|
||||
const handleY = left.handles[3] - dy * viewboxToElHeightRatio
|
||||
return [handleX, handleY, right.handles[0], right.handles[1]]
|
||||
})
|
||||
|
||||
const [refRight, nodeRight] = useRefAndState<SVGCircleElement | null>(null)
|
||||
useKeyframeDrag(nodeSVG, nodeRight, props, (dx, dy) => {
|
||||
const handleX = clamp(next.handles[0] + dx * viewboxToElWidthRatio, 0, 1)
|
||||
const handleY = next.handles[1] - dy * viewboxToElHeightRatio
|
||||
|
||||
return [cur.handles[2], cur.handles[3], handleX, handleY]
|
||||
// TODO - document this
|
||||
const handleX = clamp(right.handles[0] + dx * viewboxToElWidthRatio, 0, 1)
|
||||
const handleY = right.handles[1] - dy * viewboxToElHeightRatio
|
||||
return [left.handles[2], left.handles[3], handleX, handleY]
|
||||
})
|
||||
|
||||
const curvePathDAttrValue = `M0 ${toExtremumSpace(1)} C${
|
||||
cur.handles[2]
|
||||
} ${toExtremumSpace(1 - cur.handles[3])}
|
||||
${next.handles[0]} ${toExtremumSpace(1 - next.handles[1])} 1 ${toExtremumSpace(
|
||||
0,
|
||||
)}`
|
||||
const curvePathDAttrValue = (connection: KeyframeConnectionWithAddress) =>
|
||||
`M0 ${toExtremumSpace(1)} C${connection.left.handles[2]} ${toExtremumSpace(
|
||||
1 - connection.left.handles[3],
|
||||
)} ${connection.right.handles[0]} ${toExtremumSpace(
|
||||
1 - connection.right.handles[1],
|
||||
)} 1 ${toExtremumSpace(0)}`
|
||||
|
||||
return (
|
||||
<svg
|
||||
|
@ -173,8 +183,8 @@ ${next.handles[0]} ${toExtremumSpace(1 - next.handles[1])} 1 ${toExtremumSpace(
|
|||
<line
|
||||
x1={0}
|
||||
y1={toExtremumSpace(1)}
|
||||
x2={cur.handles[2]}
|
||||
y2={toExtremumSpace(1 - cur.handles[3])}
|
||||
x2={left.handles[2]}
|
||||
y2={toExtremumSpace(1 - left.handles[3])}
|
||||
stroke={CONTROL_COLOR}
|
||||
strokeWidth="0.01"
|
||||
/>
|
||||
|
@ -182,26 +192,35 @@ ${next.handles[0]} ${toExtremumSpace(1 - next.handles[1])} 1 ${toExtremumSpace(
|
|||
<line
|
||||
x1={1}
|
||||
y1={toExtremumSpace(0)}
|
||||
x2={next.handles[0]}
|
||||
y2={toExtremumSpace(1 - next.handles[1])}
|
||||
x2={right.handles[0]}
|
||||
y2={toExtremumSpace(1 - right.handles[1])}
|
||||
stroke={CONTROL_COLOR}
|
||||
strokeWidth="0.01"
|
||||
/>
|
||||
|
||||
{/* Curve "shadow": the low-opacity filled area between the curve and the diagonal */}
|
||||
<path
|
||||
d={curvePathDAttrValue}
|
||||
d={curvePathDAttrValue(props.curveConnection)}
|
||||
stroke="none"
|
||||
fill="url('#myGradient')"
|
||||
opacity="0.1"
|
||||
/>
|
||||
{/* The background curves (e.g. multiple different values) */}
|
||||
{backgroundConnections.map((connection, i) => (
|
||||
<path
|
||||
key={connection.objectKey + '/' + connection.left.id}
|
||||
d={curvePathDAttrValue(connection)}
|
||||
stroke={BACKGROUND_CURVE_COLORS[i % BACKGROUND_CURVE_COLORS.length]}
|
||||
opacity={0.6}
|
||||
strokeWidth="0.01"
|
||||
/>
|
||||
))}
|
||||
{/* The curve */}
|
||||
<path
|
||||
d={curvePathDAttrValue}
|
||||
d={curvePathDAttrValue(props.curveConnection)}
|
||||
stroke="url('#myGradient')"
|
||||
strokeWidth="0.02"
|
||||
/>
|
||||
|
||||
{/* Right end of curve */}
|
||||
<circle
|
||||
cx={0}
|
||||
|
@ -224,21 +243,24 @@ ${next.handles[0]} ${toExtremumSpace(1 - next.handles[1])} 1 ${toExtremumSpace(
|
|||
{/* Right handle and hit zone */}
|
||||
<HitZone
|
||||
ref={refLeft}
|
||||
cx={cur.handles[2]}
|
||||
cy={toExtremumSpace(1 - cur.handles[3])}
|
||||
cx={left.handles[2]}
|
||||
cy={toExtremumSpace(1 - left.handles[3])}
|
||||
fill={CURVE_START_COLOR}
|
||||
opacity={0.2}
|
||||
/>
|
||||
<Circle cx={cur.handles[2]} cy={toExtremumSpace(1 - cur.handles[3])} />
|
||||
<Circle cx={left.handles[2]} cy={toExtremumSpace(1 - left.handles[3])} />
|
||||
{/* Left handle and hit zone */}
|
||||
<HitZone
|
||||
ref={refRight}
|
||||
cx={next.handles[0]}
|
||||
cy={toExtremumSpace(1 - next.handles[1])}
|
||||
cx={right.handles[0]}
|
||||
cy={toExtremumSpace(1 - right.handles[1])}
|
||||
fill={CURVE_END_COLOR}
|
||||
opacity={0.2}
|
||||
/>
|
||||
<Circle cx={next.handles[0]} cy={toExtremumSpace(1 - next.handles[1])} />
|
||||
<Circle
|
||||
cx={right.handles[0]}
|
||||
cy={toExtremumSpace(1 - right.handles[1])}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
@ -247,7 +269,7 @@ export default CurveSegmentEditor
|
|||
function useKeyframeDrag(
|
||||
svgNode: SVGSVGElement | null,
|
||||
node: SVGCircleElement | null,
|
||||
props: IProps,
|
||||
props: ICurveSegmentEditorProps,
|
||||
setHandles: (dx: number, dy: number) => CubicBezierHandles,
|
||||
): void {
|
||||
const handlers = useFreezableMemo<Parameters<typeof useDrag>[1]>(
|
||||
|
@ -269,7 +291,7 @@ function useKeyframeDrag(
|
|||
}
|
||||
},
|
||||
}),
|
||||
[svgNode, props.trackData],
|
||||
[svgNode, props.onCurveChange, props.onCancelCurveChange],
|
||||
)
|
||||
|
||||
useDrag(node, handlers)
|
||||
|
|
|
@ -57,8 +57,6 @@ const Container = styled.div<IConnectorThemeValues>`
|
|||
|
||||
type IConnectorLineProps = React.PropsWithChildren<{
|
||||
isPopoverOpen: boolean
|
||||
/** TEMP: Remove once interactivity is added for aggregate? */
|
||||
mvpIsInteractiveDisabled?: boolean
|
||||
openPopover?: (event: React.MouseEvent) => void
|
||||
isSelected: boolean
|
||||
connectorLengthInUnitSpace: number
|
||||
|
@ -82,7 +80,6 @@ export const ConnectorLine = React.forwardRef<
|
|||
transform: `scaleX(calc(var(--unitSpaceToScaledSpaceMultiplier) * ${
|
||||
props.connectorLengthInUnitSpace / CONNECTOR_WIDTH_UNSCALED
|
||||
}))`,
|
||||
pointerEvents: props.mvpIsInteractiveDisabled ? 'none' : undefined,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
props.openPopover?.(e)
|
||||
|
|
|
@ -16,29 +16,42 @@ import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Histo
|
|||
* keyframe in the connection is selected
|
||||
*/
|
||||
export function isKeyframeConnectionInSelection(
|
||||
keyframeConnection: [Keyframe, Keyframe],
|
||||
keyframeConnection: {left: Keyframe; right: Keyframe},
|
||||
selection: DopeSheetSelection,
|
||||
): boolean {
|
||||
for (const {keyframeId} of flatSelectionKeyframeIds(selection)) {
|
||||
if (keyframeConnection[0].id === keyframeId) return true
|
||||
if (keyframeConnection.left.id === keyframeId) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export type KeyframeConnectionWithAddress = {
|
||||
projectId: ProjectId
|
||||
sheetId: SheetId
|
||||
objectKey: ObjectAddressKey
|
||||
trackId: SequenceTrackId
|
||||
left: Keyframe
|
||||
right: Keyframe
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all the selected keyframes
|
||||
* that are connected to one another. Useful for changing
|
||||
* the tweening in between keyframes.
|
||||
*
|
||||
* TODO - rename to selectedKeyframeConnectionsD(), or better yet,
|
||||
* make it a `prism.ensurePrism()` function, rather than returning
|
||||
* a prism.
|
||||
*/
|
||||
export function selectedKeyframeConnections(
|
||||
projectId: ProjectId,
|
||||
sheetId: SheetId,
|
||||
selection: DopeSheetSelection | undefined,
|
||||
): IDerivation<Array<[left: Keyframe, right: Keyframe]>> {
|
||||
): IDerivation<Array<KeyframeConnectionWithAddress>> {
|
||||
return prism(() => {
|
||||
if (selection === undefined) return []
|
||||
|
||||
let ckfs: Array<[Keyframe, Keyframe]> = []
|
||||
let ckfs: Array<KeyframeConnectionWithAddress> = []
|
||||
|
||||
for (const {objectKey, trackId} of flatSelectionTrackIds(selection)) {
|
||||
const track = val(
|
||||
|
@ -48,9 +61,16 @@ export function selectedKeyframeConnections(
|
|||
|
||||
if (track) {
|
||||
ckfs = ckfs.concat(
|
||||
keyframeConnections(track.keyframes).filter((kfc) =>
|
||||
isKeyframeConnectionInSelection(kfc, selection),
|
||||
),
|
||||
keyframeConnections(track.keyframes)
|
||||
.filter((kfc) => isKeyframeConnectionInSelection(kfc, selection))
|
||||
.map(({left, right}) => ({
|
||||
left,
|
||||
right,
|
||||
trackId,
|
||||
objectKey,
|
||||
sheetId,
|
||||
projectId,
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -60,10 +80,10 @@ export function selectedKeyframeConnections(
|
|||
|
||||
export function keyframeConnections(
|
||||
keyframes: Array<Keyframe>,
|
||||
): Array<[Keyframe, Keyframe]> {
|
||||
): Array<{left: Keyframe; right: Keyframe}> {
|
||||
return keyframes
|
||||
.map((kf, i) => [kf, keyframes[i + 1]] as [Keyframe, Keyframe])
|
||||
.slice(0, -1) // remmove the last entry because it is [kf, undefined]
|
||||
.map((kf, i) => ({left: kf, right: keyframes[i + 1]}))
|
||||
.slice(0, -1) // remmove the last entry because it is { left: kf, right: undefined }
|
||||
}
|
||||
|
||||
export function flatSelectionKeyframeIds(selection: DopeSheetSelection): Array<{
|
||||
|
|
|
@ -775,7 +775,17 @@ namespace stateEditors {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the easing between two keyframes
|
||||
* Sets the easing between keyframes
|
||||
*
|
||||
* X = in keyframeIds
|
||||
* * = not in keyframeIds
|
||||
* + = modified handle
|
||||
* ```
|
||||
* X- --- -*- --- -X
|
||||
* X+ --- +*- --- -X+
|
||||
* ```
|
||||
*
|
||||
* TODO - explain further
|
||||
*/
|
||||
export function setTweenBetweenKeyframes(
|
||||
p: WithoutSheetInstance<SheetObjectAddress> & {
|
||||
|
@ -828,6 +838,34 @@ namespace stateEditors {
|
|||
})
|
||||
}
|
||||
|
||||
export function setHandlesForKeyframe(
|
||||
p: WithoutSheetInstance<SheetObjectAddress> & {
|
||||
trackId: SequenceTrackId
|
||||
keyframeId: KeyframeId
|
||||
start?: [number, number]
|
||||
end?: [number, number]
|
||||
},
|
||||
) {
|
||||
const track = _getTrack(p)
|
||||
if (!track) return
|
||||
track.keyframes = track.keyframes.map((kf) => {
|
||||
if (kf.id === p.keyframeId) {
|
||||
// Use given value or fallback to original value,
|
||||
// allowing the caller to customize exactly which side
|
||||
// of the curve they are editing.
|
||||
return {
|
||||
...kf,
|
||||
handles: [
|
||||
p.end?.[0] ?? kf.handles[0],
|
||||
p.end?.[1] ?? kf.handles[1],
|
||||
p.start?.[0] ?? kf.handles[2],
|
||||
p.start?.[1] ?? kf.handles[3],
|
||||
],
|
||||
}
|
||||
} else return kf
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteKeyframes(
|
||||
p: WithoutSheetInstance<SheetObjectAddress> & {
|
||||
trackId: SequenceTrackId
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import React, {useCallback, useContext, useEffect, useRef} from 'react'
|
||||
import {createPortal} from 'react-dom'
|
||||
import {PortalContext} from 'reakit'
|
||||
import type {AbsolutePlacementBoxConstraints} from './TooltipWrapper'
|
||||
|
@ -25,6 +19,12 @@ type State =
|
|||
clientY: number
|
||||
}
|
||||
target: HTMLElement
|
||||
opts: Opts
|
||||
onPointerOutside?: {
|
||||
threshold: number
|
||||
callback: (e: MouseEvent) => void
|
||||
}
|
||||
onClickOutside: () => void
|
||||
}
|
||||
|
||||
const PopoverAutoCloseLock = React.createContext({
|
||||
|
@ -37,33 +37,61 @@ const PopoverAutoCloseLock = React.createContext({
|
|||
},
|
||||
})
|
||||
|
||||
export default function usePopover(
|
||||
opts: {
|
||||
type Opts = {
|
||||
debugName: string
|
||||
closeWhenPointerIsDistant?: boolean
|
||||
pointerDistanceThreshold?: number
|
||||
closeOnClickOutside?: boolean
|
||||
constraints?: AbsolutePlacementBoxConstraints
|
||||
},
|
||||
}
|
||||
|
||||
export default function usePopover(
|
||||
opts: Opts | (() => Opts),
|
||||
render: () => React.ReactElement,
|
||||
): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] {
|
||||
const _debug = (...args: any) => {} // console.debug.bind(console, opts.debugName)
|
||||
const _debug = (...args: any) => {}
|
||||
|
||||
const [state, setState] = useState<State>({
|
||||
const [stateRef, state] = useRefAndState<State>({
|
||||
isOpen: false,
|
||||
})
|
||||
|
||||
const optsRef = useRef(opts)
|
||||
|
||||
const close = useCallback<CloseFn>((reason: string): void => {
|
||||
_debug(`closing due to "${reason}"`)
|
||||
stateRef.current = {isOpen: false}
|
||||
}, [])
|
||||
|
||||
const open = useCallback<OpenFn>((e, target) => {
|
||||
setState({
|
||||
const opts =
|
||||
typeof optsRef.current === 'function'
|
||||
? optsRef.current()
|
||||
: optsRef.current
|
||||
|
||||
function onClickOutside(): void {
|
||||
if (lock.childHasFocusRef.current) return
|
||||
if (opts.closeOnClickOutside !== false) {
|
||||
close('clicked outside popover')
|
||||
}
|
||||
}
|
||||
|
||||
stateRef.current = {
|
||||
isOpen: true,
|
||||
clickPoint: {clientX: e.clientX, clientY: e.clientY},
|
||||
target,
|
||||
})
|
||||
}, [])
|
||||
|
||||
const close = useCallback<CloseFn>((reason) => {
|
||||
_debug(`closing due to "${reason}"`)
|
||||
setState({isOpen: false})
|
||||
opts,
|
||||
onClickOutside: onClickOutside,
|
||||
onPointerOutside:
|
||||
opts.closeWhenPointerIsDistant === false
|
||||
? undefined
|
||||
: {
|
||||
threshold: opts.pointerDistanceThreshold ?? 100,
|
||||
callback: () => {
|
||||
if (lock.childHasFocusRef.current) return
|
||||
close('pointer outside')
|
||||
},
|
||||
},
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
|
@ -76,24 +104,7 @@ export default function usePopover(
|
|||
state,
|
||||
})
|
||||
|
||||
const onClickOutside = useCallback(() => {
|
||||
if (lock.childHasFocusRef.current) return
|
||||
if (opts.closeOnClickOutside !== false) {
|
||||
close('clicked outside popover')
|
||||
}
|
||||
}, [opts.closeOnClickOutside])
|
||||
|
||||
const portalLayer = useContext(PortalContext)
|
||||
const onPointerOutside = useMemo(() => {
|
||||
if (opts.closeWhenPointerIsDistant === false) return undefined
|
||||
return {
|
||||
threshold: opts.pointerDistanceThreshold ?? 100,
|
||||
callback: () => {
|
||||
if (lock.childHasFocusRef.current) return
|
||||
close('pointer outside')
|
||||
},
|
||||
}
|
||||
}, [opts.closeWhenPointerIsDistant])
|
||||
|
||||
const node = state.isOpen ? (
|
||||
createPortal(
|
||||
|
@ -101,9 +112,9 @@ export default function usePopover(
|
|||
<TooltipWrapper
|
||||
children={render}
|
||||
target={state.target}
|
||||
onClickOutside={onClickOutside}
|
||||
onPointerOutside={onPointerOutside}
|
||||
constraints={opts.constraints}
|
||||
onClickOutside={state.onClickOutside}
|
||||
onPointerOutside={state.onPointerOutside}
|
||||
constraints={state.opts.constraints}
|
||||
/>
|
||||
</PopoverAutoCloseLock.Provider>,
|
||||
portalLayer!,
|
||||
|
|
Loading…
Reference in a new issue