diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx index eee9563..1b70672 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx @@ -90,6 +90,7 @@ const BasicKeyframedTrack: React.FC<{ layoutP={layoutP} sheetObject={sheetObject} trackId={trackId} + isScalar={propConfig.isScalar === true} key={'keyframe-' + kf.id} extremumSpace={cachedExtremumSpace.current} color={color} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Curve.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Curve.tsx index 13c7e3b..24b28c4 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Curve.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Curve.tsx @@ -25,8 +25,8 @@ const Curve: React.FC = (props) => { const [contextMenu] = useConnectorContextMenu(node, props) - const curValue = typeof cur.value === 'number' ? cur.value : 0 - const nextValue = typeof next.value === 'number' ? next.value : 1 + const curValue = props.isScalar ? (cur.value as number) : 0 + const nextValue = props.isScalar ? (next.value as number) : 1 const leftYInExtremumSpace = props.extremumSpace.fromValueSpace(curValue) const rightYInExtremumSpace = props.extremumSpace.fromValueSpace(nextValue) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/CurveHandle.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/CurveHandle.tsx index cc9d181..e26acef 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/CurveHandle.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/CurveHandle.tsx @@ -68,12 +68,8 @@ const CurveHandle: React.FC = (props) => { const valInDiffSpace = props.which === 'left' ? cur.handles[3] : next.handles[1] - if (!valInDiffSpace) { - // debugger - } - - const curValue = typeof cur.value === 'number' ? cur.value : 0 - const nextValue = typeof next.value === 'number' ? next.value : 1 + const curValue = props.isScalar ? (cur.value as number) : 0 + const nextValue = props.isScalar ? (next.value as number) : 1 const value = curValue + (nextValue - curValue) * valInDiffSpace @@ -168,8 +164,8 @@ function useOurDrags(node: SVGCircleElement | null, props: IProps): void { const dYInValueSpace = propsAtStartOfDrag.extremumSpace.deltaToValueSpace(dYInExtremumSpace) - const curValue = typeof cur.value === 'number' ? cur.value : 0 - const nextValue = typeof next.value === 'number' ? next.value : 1 + const curValue = props.isScalar ? (cur.value as number) : 0 + const nextValue = props.isScalar ? (next.value as number) : 1 const dyInKeyframeDiffSpace = dYInValueSpace / (nextValue - curValue) if (propsAtStartOfDrag.which === 'left') { diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotNonScalar.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotNonScalar.tsx new file mode 100644 index 0000000..b8e736a --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotNonScalar.tsx @@ -0,0 +1,199 @@ +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 useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' +import useDrag from '@theatre/studio/uiComponents/useDrag' +import useRefAndState from '@theatre/studio/utils/useRefAndState' +import type {VoidFn} from '@theatre/shared/utils/types' +import {val} from '@theatre/dataverse' +import React, {useMemo, useRef, useState} from 'react' +import styled from 'styled-components' +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 + +const Circle = styled.circle` + fill: var(--main-color); + stroke-width: 1px; + vector-effect: non-scaling-stroke; + + r: 2px; +` + +const HitZone = styled.circle` + stroke-width: 6px; + vector-effect: non-scaling-stroke; + r: 6px; + fill: transparent; + ${pointerEventsAutoInNormalMode}; + + &:hover + ${Circle} { + r: 6px; + } + + #pointer-root.normal & { + cursor: ew-resize; + } + + #pointer-root.draggingPositionInSequenceEditor & { + pointer-events: auto; + } + + &.beingDragged { + pointer-events: none !important; + } +` + +type IProps = Parameters[0] & {which: 'left' | 'right'} + +const GraphEditorDotNonScalar: React.FC = (props) => { + const [ref, node] = useRefAndState(null) + + const {index, trackData} = props + const cur = trackData.keyframes[index] + const next = trackData.keyframes[index + 1] + + const [contextMenu] = useKeyframeContextMenu(node, props) + + const curValue = props.which === 'left' ? 0 : 1 + + const isDragging = useDragKeyframe(node, props) + + const cyInExtremumSpace = props.extremumSpace.fromValueSpace(curValue) + + return ( + <> + + + {contextMenu} + + ) +} + +export default GraphEditorDotNonScalar + +function useDragKeyframe( + node: SVGCircleElement | null, + _props: IProps, +): boolean { + const [isDragging, setIsDragging] = useState(false) + useLockFrameStampPosition(isDragging, _props.keyframe.position) + const propsRef = useRef(_props) + propsRef.current = _props + + const gestureHandlers = useMemo[1]>(() => { + let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace'] + + let propsAtStartOfDrag: IProps + let tempTransaction: CommitOrDiscard | undefined + let unlockExtremums: VoidFn | undefined + + return { + lockCursorTo: 'ew-resize', + onDragStart(event) { + setIsDragging(true) + + propsAtStartOfDrag = propsRef.current + + toUnitSpace = val(propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace) + + unlockExtremums = propsAtStartOfDrag.extremumSpace.lock() + }, + onDrag(dx, dy) { + const original = + propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index] + + const deltaPos = toUnitSpace(dx) + + const updatedKeyframes: Keyframe[] = [] + + const cur: Keyframe = { + ...original, + position: original.position + deltaPos, + value: original.value, + handles: [...original.handles], + } + + updatedKeyframes.push(cur) + + tempTransaction?.discard() + tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => { + stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes( + { + ...propsAtStartOfDrag.sheetObject.address, + trackId: propsAtStartOfDrag.trackId, + keyframes: updatedKeyframes, + snappingFunction: val( + propsAtStartOfDrag.layoutP.sheet, + ).getSequence().closestGridPosition, + }, + ) + }) + }, + onDragEnd(dragHappened) { + setIsDragging(false) + if (unlockExtremums) { + const unlock = unlockExtremums + unlockExtremums = undefined + unlock() + } + if (dragHappened) { + tempTransaction?.commit() + } else { + tempTransaction?.discard() + } + tempTransaction = undefined + }, + } + }, []) + + useDrag(node, gestureHandlers) + useCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize') + return isDragging +} + +function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) { + return useContextMenu(node, { + items: () => { + return [ + { + label: 'Delete', + callback: () => { + getStudio()!.transaction(({stateEditors}) => { + stateEditors.coreByProject.historic.sheetsById.sequence.deleteKeyframes( + { + ...props.sheetObject.address, + keyframeIds: [props.keyframe.id], + trackId: props.trackId, + }, + ) + }) + }, + }, + ] + }, + }) +} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Dot.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotScalar.tsx similarity index 97% rename from theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Dot.tsx rename to theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotScalar.tsx index e35c4f9..5c629e1 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Dot.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotScalar.tsx @@ -51,7 +51,7 @@ const HitZone = styled.circle` type IProps = Parameters[0] -const Dot: React.FC = (props) => { +const GraphEditorDotScalar: React.FC = (props) => { const [ref, node] = useRefAndState(null) const {index, trackData} = props @@ -59,12 +59,12 @@ const Dot: React.FC = (props) => { const next = trackData.keyframes[index + 1] const [contextMenu] = useKeyframeContextMenu(node, props) - const isDragging = - typeof cur.value === 'number' ? useDragKeyframe(node, props) : false - const cyInExtremumSpace = props.extremumSpace.fromValueSpace( - typeof cur.value === 'number' ? cur.value : 0, - ) + const curValue = cur.value as number + + const isDragging = useDragKeyframe(node, props) + + const cyInExtremumSpace = props.extremumSpace.fromValueSpace(curValue) return ( <> @@ -93,7 +93,7 @@ const Dot: React.FC = (props) => { ) } -export default Dot +export default GraphEditorDotScalar function useDragKeyframe( node: SVGCircleElement | null, @@ -146,6 +146,7 @@ function useDragKeyframe( value: (original.value as number) + dYInValueSpace, handles: [...original.handles], } + updatedKeyframes.push(cur) if (keepSpeeds) { diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx index 4fe8a75..0eb956b 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx @@ -12,7 +12,8 @@ import type {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel import type {ExtremumSpace} from '@theatre/studio/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack' import Curve from './Curve' import CurveHandle from './CurveHandle' -import Dot from './Dot' +import GraphEditorDotScalar from './GraphEditorDotScalar' +import GraphEditorDotNonScalar from './GraphEditorDotNonScalar' const Container = styled.g` /* position: absolute; */ @@ -28,9 +29,10 @@ const KeyframeEditor: React.FC<{ trackId: SequenceTrackId sheetObject: SheetObject extremumSpace: ExtremumSpace + isScalar: boolean color: keyof typeof graphEditorColors }> = (props) => { - const {index, trackData} = props + const {index, trackData, isScalar} = props const cur = trackData.keyframes[index] const next = trackData.keyframes[index + 1] @@ -48,7 +50,19 @@ const KeyframeEditor: React.FC<{ ) : ( noConnector )} - + {isScalar ? ( + + ) : ( + <> + + {shouldShowCurve && ( + + )} + + )} ) }