All keyframe operations now snap to the frame grid

This commit is contained in:
Aria Minaei 2021-08-09 11:31:37 +02:00
parent 2ec6a54060
commit d46e0bfd2f
8 changed files with 51 additions and 17 deletions

View file

@ -97,11 +97,13 @@ export default class Sequence {
return this.closestGridPosition(this.position) return this.closestGridPosition(this.position)
} }
closestGridPosition(posInUnitSpace: number): number { closestGridPosition = (posInUnitSpace: number): number => {
const subUnitsPerUnit = this.subUnitsPerUnit const subUnitsPerUnit = this.subUnitsPerUnit
const gridLength = 1 / subUnitsPerUnit const gridLength = 1 / subUnitsPerUnit
return Math.round(posInUnitSpace / gridLength) * gridLength return parseFloat(
(Math.round(posInUnitSpace / gridLength) * gridLength).toFixed(3),
)
} }
set position(requestedPosition: number) { set position(requestedPosition: number) {

View file

@ -144,12 +144,14 @@ export default class StudioStore {
) as $FixMe as SequenceTrackId | undefined ) as $FixMe as SequenceTrackId | undefined
if (typeof trackId === 'string') { if (typeof trackId === 'string') {
const seq = root.sheet.getSequence()
stateEditors.coreByProject.historic.sheetsById.sequence.setKeyframeAtPosition( stateEditors.coreByProject.historic.sheetsById.sequence.setKeyframeAtPosition(
{ {
...propAddress, ...propAddress,
trackId, trackId,
position: root.sheet.getSequence().position, position: seq.position,
value: v as $FixMe, value: v as $FixMe,
snappingFunction: seq.closestGridPosition,
}, },
) )
} else { } else {
@ -196,7 +198,8 @@ export default class StudioStore {
{ {
...propAddress, ...propAddress,
trackId, trackId,
position: root.sheet.getSequence().position, position:
root.sheet.getSequence().positionSnappedToGrid,
}, },
) )
} else { } else {

View file

@ -14,6 +14,7 @@ import type {
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' } from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import {dotSize} from './Dot' import {dotSize} from './Dot'
import type KeyframeEditor from './KeyframeEditor' import type KeyframeEditor from './KeyframeEditor'
import type Sequence from '@theatre/core/sequences/Sequence'
const connectorHeight = dotSize / 2 + 1 const connectorHeight = dotSize / 2 + 1
const connectorWidthUnscaled = 1000 const connectorWidthUnscaled = 1000
@ -108,6 +109,8 @@ const Connector: React.FC<IProps> = (props) => {
replaceKeyframes({ replaceKeyframes({
...props.leaf.sheetObject.address, ...props.leaf.sheetObject.address,
snappingFunction: val(props.layoutP.sheet).getSequence()
.closestGridPosition,
trackId: props.leaf.trackId, trackId: props.leaf.trackId,
keyframes: [ keyframes: [
{ {
@ -157,6 +160,7 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
let selectionDragHandlers: let selectionDragHandlers:
| ReturnType<DopeSheetSelection['getDragHandlers']> | ReturnType<DopeSheetSelection['getDragHandlers']>
| undefined | undefined
let sequence: Sequence
return { return {
lockCursorTo: 'ew-resize', lockCursorTo: 'ew-resize',
onDragStart(event) { onDragStart(event) {
@ -174,6 +178,7 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
} }
propsAtStartOfDrag = propsRef.current propsAtStartOfDrag = propsRef.current
sequence = val(propsAtStartOfDrag.layoutP.sheet).getSequence()
toUnitSpace = val(propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace) toUnitSpace = val(propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace)
}, },
@ -201,6 +206,7 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
translate: delta, translate: delta,
scale: 1, scale: 1,
origin: 0, origin: 0,
snappingFunction: sequence.closestGridPosition,
}, },
) )
}) })

View file

@ -209,6 +209,9 @@ function useDragKeyframe(
...propsAtStartOfDrag.leaf.sheetObject.address, ...propsAtStartOfDrag.leaf.sheetObject.address,
trackId: propsAtStartOfDrag.leaf.trackId, trackId: propsAtStartOfDrag.leaf.trackId,
keyframes: [{...original, position: newPosition}], keyframes: [{...original, position: newPosition}],
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
}, },
) )
}) })

View file

@ -227,6 +227,7 @@ namespace utils {
translate: delta, translate: delta,
scale: 1, scale: 1,
origin: 0, origin: 0,
snappingFunction: sheet.getSequence().closestGridPosition,
}) })
} }
} }

View file

@ -175,6 +175,9 @@ function useOurDrags(node: SVGCircleElement | null, props: IProps): void {
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes( stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{ {
...propsAtStartOfDrag.sheetObject.address, ...propsAtStartOfDrag.sheetObject.address,
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
trackId: propsAtStartOfDrag.trackId, trackId: propsAtStartOfDrag.trackId,
keyframes: [ keyframes: [
{ {
@ -194,6 +197,9 @@ function useOurDrags(node: SVGCircleElement | null, props: IProps): void {
{ {
...propsAtStartOfDrag.sheetObject.address, ...propsAtStartOfDrag.sheetObject.address,
trackId: propsAtStartOfDrag.trackId, trackId: propsAtStartOfDrag.trackId,
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
keyframes: [ keyframes: [
{ {
...next, ...next,

View file

@ -198,6 +198,9 @@ function useDragKeyframe(
...propsAtStartOfDrag.sheetObject.address, ...propsAtStartOfDrag.sheetObject.address,
trackId: propsAtStartOfDrag.trackId, trackId: propsAtStartOfDrag.trackId,
keyframes: updatedKeyframes, keyframes: updatedKeyframes,
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
}, },
) )
}) })

View file

@ -458,11 +458,13 @@ namespace stateEditors {
trackId: string trackId: string
position: number position: number
value: number value: number
snappingFunction: SnappingFunction
}, },
) { ) {
const position = p.snappingFunction(p.position)
const {keyframes} = _getTrack(p) const {keyframes} = _getTrack(p)
const existingKeyframeIndex = keyframes.findIndex( const existingKeyframeIndex = keyframes.findIndex(
(kf) => kf.position === p.position, (kf) => kf.position === position,
) )
if (existingKeyframeIndex !== -1) { if (existingKeyframeIndex !== -1) {
const kf = keyframes[existingKeyframeIndex] const kf = keyframes[existingKeyframeIndex]
@ -471,12 +473,12 @@ namespace stateEditors {
} }
const indexOfLeftKeyframe = findLastIndex( const indexOfLeftKeyframe = findLastIndex(
keyframes, keyframes,
(kf) => kf.position < p.position, (kf) => kf.position < position,
) )
if (indexOfLeftKeyframe === -1) { if (indexOfLeftKeyframe === -1) {
keyframes.unshift({ keyframes.unshift({
id: generateKeyframeId(), id: generateKeyframeId(),
position: p.position, position,
connectedRight: true, connectedRight: true,
handles: [0.5, 1, 0.5, 0], handles: [0.5, 1, 0.5, 0],
value: p.value, value: p.value,
@ -486,7 +488,7 @@ namespace stateEditors {
const leftKeyframe = keyframes[indexOfLeftKeyframe] const leftKeyframe = keyframes[indexOfLeftKeyframe]
keyframes.splice(indexOfLeftKeyframe + 1, 0, { keyframes.splice(indexOfLeftKeyframe + 1, 0, {
id: generateKeyframeId(), id: generateKeyframeId(),
position: p.position, position,
connectedRight: leftKeyframe.connectedRight, connectedRight: leftKeyframe.connectedRight,
handles: [0.5, 1, 0.5, 0], handles: [0.5, 1, 0.5, 0],
value: p.value, value: p.value,
@ -508,6 +510,8 @@ namespace stateEditors {
keyframes.splice(index, 1) keyframes.splice(index, 1)
} }
type SnappingFunction = (p: number) => number
export function transformKeyframes( export function transformKeyframes(
p: WithoutSheetInstance<SheetObjectAddress> & { p: WithoutSheetInstance<SheetObjectAddress> & {
trackId: string trackId: string
@ -515,6 +519,7 @@ namespace stateEditors {
translate: number translate: number
scale: number scale: number
origin: number origin: number
snappingFunction: SnappingFunction
}, },
) { ) {
const track = _getTrack(p) const track = _getTrack(p)
@ -527,7 +532,9 @@ namespace stateEditors {
const transformed = selectedKeyframes.map((untransformedKf) => { const transformed = selectedKeyframes.map((untransformedKf) => {
const oldPosition = untransformedKf.position const oldPosition = untransformedKf.position
const newPosition = transformNumber(oldPosition, p) const newPosition = p.snappingFunction(
transformNumber(oldPosition, p),
)
return {...untransformedKf, position: newPosition} return {...untransformedKf, position: newPosition}
}) })
@ -551,17 +558,20 @@ namespace stateEditors {
p: WithoutSheetInstance<SheetObjectAddress> & { p: WithoutSheetInstance<SheetObjectAddress> & {
trackId: string trackId: string
keyframes: Array<Keyframe> keyframes: Array<Keyframe>
snappingFunction: SnappingFunction
}, },
) { ) {
const track = _getTrack(p) const track = _getTrack(p)
const initialKeyframes = current(track.keyframes) const initialKeyframes = current(track.keyframes)
const sanitizedKeyframes = p.keyframes.filter((kf) => { const sanitizedKeyframes = p.keyframes
if (!isFinite(kf.value)) return false .filter((kf) => {
if (!kf.handles.every((handleValue) => isFinite(handleValue))) if (!isFinite(kf.value)) return false
return false if (!kf.handles.every((handleValue) => isFinite(handleValue)))
return false
return true return true
}) })
.map((kf) => ({...kf, position: p.snappingFunction(kf.position)}))
const newKeyframesById = keyBy(sanitizedKeyframes, 'id') const newKeyframesById = keyBy(sanitizedKeyframes, 'id')
@ -569,13 +579,13 @@ namespace stateEditors {
(kf) => !newKeyframesById[kf.id], (kf) => !newKeyframesById[kf.id],
) )
const unselectedByPositino = keyBy(unselected, 'position') const unselectedByPosition = keyBy(unselected, 'position')
// If the new transformed keyframes overlap with any existing keyframes, // If the new transformed keyframes overlap with any existing keyframes,
// we remove the overlapped keyframes // we remove the overlapped keyframes
sanitizedKeyframes.forEach(({position}) => { sanitizedKeyframes.forEach(({position}) => {
const existingKeyframeAtThisPosition = const existingKeyframeAtThisPosition =
unselectedByPositino[position] unselectedByPosition[position]
if (existingKeyframeAtThisPosition) { if (existingKeyframeAtThisPosition) {
pullFromArray(unselected, existingKeyframeAtThisPosition) pullFromArray(unselected, existingKeyframeAtThisPosition)
} }