WIP: Editing curves using the css cubic bezier function

This commit is contained in:
Aria Minaei 2021-11-13 20:06:17 +01:00
parent 4a65c6e91c
commit 786f645d0c
3 changed files with 205 additions and 48 deletions

View file

@ -15,6 +15,9 @@ import type {
import {dotSize} from './Dot'
import type KeyframeEditor from './KeyframeEditor'
import type Sequence from '@theatre/core/sequences/Sequence'
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
import CurveEditorPopover from './CurveEditorPopover/CurveEditorPopover'
const connectorHeight = dotSize / 2 + 1
const connectorWidthUnscaled = 1000
@ -47,6 +50,16 @@ const Container = styled.div<{isSelected: boolean}>`
z-index: 0;
cursor: ew-resize;
&:after {
display: block;
position: absolute;
content: ' ';
top: -4px;
bottom: -4px;
left: 0;
right: 0;
}
&:hover {
background: ${(props) =>
props.isSelected
@ -65,6 +78,17 @@ const Connector: React.FC<IProps> = (props) => {
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover(
{},
() => {
return (
<BasicPopover>
<CurveEditorPopover {...props} onRequestClose={closePopover} />
</BasicPopover>
)
},
)
const [contextMenu] = useContextMenu(node, {
items: () => {
return [
@ -86,6 +110,12 @@ const Connector: React.FC<IProps> = (props) => {
}
},
},
{
label: 'Edit Curve',
callback: (e) => {
openPopover(e, node!)
},
},
]
},
})
@ -96,60 +126,13 @@ const Connector: React.FC<IProps> = (props) => {
<Container
isSelected={!!props.selection}
ref={nodeRef}
onClick={(event) => {
if (event.button !== 0) return
// @todo Put this in the context menu
const orig = JSON.stringify([
cur.handles[2],
cur.handles[3],
next.handles[0],
next.handles[1],
])
const modifiedS = orig // window.prompt('As cubic-bezier()', orig)
if (modifiedS && modifiedS !== orig) {
return
// const modified = JSON.parse(modifiedS)
// getStudio()!.transaction(({stateEditors}) => {
// const {replaceKeyframes} =
// stateEditors.coreByProject.historic.sheetsById.sequence
// replaceKeyframes({
// ...props.leaf.sheetObject.address,
// snappingFunction: val(props.layoutP.sheet).getSequence()
// .closestGridPosition,
// trackId: props.leaf.trackId,
// keyframes: [
// {
// ...cur,
// handles: [
// cur.handles[0],
// cur.handles[1],
// modified[0],
// modified[1],
// ],
// },
// {
// ...next,
// handles: [
// modified[2],
// modified[3],
// next.handles[2],
// next.handles[3],
// ],
// },
// ],
// })
// })
}
}}
style={{
transform: `scale3d(calc(var(--unitSpaceToScaledSpaceMultiplier) * ${
connectorLengthInUnitSpace / connectorWidthUnscaled
}), 1, 1)`,
}}
>
{popoverNode}
{contextMenu}
</Container>
)

View file

@ -0,0 +1,169 @@
import type {Pointer} from '@theatre/dataverse'
import {val} from '@theatre/dataverse'
import React, {useLayoutEffect, useMemo, useRef} from 'react'
import styled from 'styled-components'
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 {propNameText} from '@theatre/studio/panels/DetailPanel/propEditors/utils/SingleRowPropEditor'
import BasicStringInput from '@theatre/studio/uiComponents/form/BasicStringInput'
import type KeyframeEditor from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor'
import {round} from 'lodash-es'
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
const greaterThanZero = (v: number) => isFinite(v) && v > 0
const Container = styled.div`
display: flex;
gap: 8px;
padding: 4px 8px;
height: 28px;
align-items: center;
`
const Label = styled.div`
${propNameText};
white-space: nowrap;
`
const CurveEditorPopover: React.FC<
{
layoutP: Pointer<SequenceEditorPanelLayout>
/**
* Called when user hits enter/escape
*/
onRequestClose: () => void
} & Parameters<typeof KeyframeEditor>[0]
> = (props) => {
const fns = useMemo(() => {
let tempTransaction: CommitOrDiscard | undefined
return {
temporarilySetValue(newCurve: string): void {
if (tempTransaction) {
tempTransaction.discard()
tempTransaction = undefined
}
const args = cssCubicBezierArgsToHandles(newCurve)!
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
const {replaceKeyframes} =
stateEditors.coreByProject.historic.sheetsById.sequence
replaceKeyframes({
...props.leaf.sheetObject.address,
snappingFunction: val(props.layoutP.sheet).getSequence()
.closestGridPosition,
trackId: props.leaf.trackId,
keyframes: [
{
...cur,
handles: [cur.handles[0], cur.handles[1], args[0], args[1]],
},
{
...next,
handles: [args[2], args[3], next.handles[2], next.handles[3]],
},
],
})
})
},
discardTemporaryValue(): void {
if (tempTransaction) {
tempTransaction.discard()
tempTransaction = undefined
}
},
permenantlySetValue(newCurve: string): void {
if (tempTransaction) {
tempTransaction.discard()
tempTransaction = undefined
}
const args = cssCubicBezierArgsToHandles(newCurve)!
getStudio()!.transaction(({stateEditors}) => {
const {replaceKeyframes} =
stateEditors.coreByProject.historic.sheetsById.sequence
replaceKeyframes({
...props.leaf.sheetObject.address,
snappingFunction: val(props.layoutP.sheet).getSequence()
.closestGridPosition,
trackId: props.leaf.trackId,
keyframes: [
{
...cur,
handles: [cur.handles[0], cur.handles[1], args[0], args[1]],
},
{
...next,
handles: [args[2], args[3], next.handles[2], next.handles[3]],
},
],
})
})
},
}
}, [props.layoutP, props.index])
const inputRef = useRef<HTMLInputElement>(null)
useLayoutEffect(() => {
inputRef.current!.focus()
}, [])
const {index, trackData} = props
const cur = trackData.keyframes[index]
const next = trackData.keyframes[index + 1]
const cssCubicBezierString = keyframesToCssCubicBezierArgs(cur, next)
return (
<Container>
<Label>cubic-bezier{'('}</Label>
<BasicStringInput
value={cssCubicBezierString}
{...fns}
isValid={isValid}
inputRef={inputRef}
onBlur={props.onRequestClose}
/>
<Label>{')'}</Label>
</Container>
)
}
export default CurveEditorPopover
function keyframesToCssCubicBezierArgs(left: Keyframe, right: Keyframe) {
return [left.handles[2], left.handles[3], right.handles[0], right.handles[1]]
.map((n) => round(n, 3).toString())
.join(', ')
}
const isValid = (str: string): boolean => !!cssCubicBezierArgsToHandles(str)
function cssCubicBezierArgsToHandles(
str: string,
):
| undefined
| [
leftHandle2: number,
leftHandle3: number,
rightHandle0: number,
rightHandle1: number,
] {
if (str.length > 128) {
// string too long
return undefined
}
const args = str.split(',')
if (args.length !== 4) return undefined
const nums = args.map((arg) => {
return Number(arg.trim())
})
if (!nums.every((v) => isFinite(v))) return undefined
if (nums[0] < 0 || nums[0] > 1 || nums[2] < 0 || nums[2] > 1) return undefined
return nums as $IntentionalAny
}

View file

@ -29,6 +29,10 @@ const Input = styled.input.attrs({type: 'text'})`
background-color: #10101042;
border-color: #00000059;
}
&.invalid {
border-color: red;
}
`
type IState_NoFocus = {
@ -165,6 +169,7 @@ const BasicStringInput: React.FC<{
<Input
key="input"
type="text"
className={!isValid(value) ? 'invalid' : ''}
onChange={callbacks.inputChange}
value={value}
onBlur={callbacks.onBlur}