All keyframe operations now snap to the frame grid
This commit is contained in:
parent
2ec6a54060
commit
d46e0bfd2f
8 changed files with 51 additions and 17 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -227,6 +227,7 @@ namespace utils {
|
||||||
translate: delta,
|
translate: delta,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
origin: 0,
|
origin: 0,
|
||||||
|
snappingFunction: sheet.getSequence().closestGridPosition,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
.filter((kf) => {
|
||||||
if (!isFinite(kf.value)) return false
|
if (!isFinite(kf.value)) return false
|
||||||
if (!kf.handles.every((handleValue) => isFinite(handleValue)))
|
if (!kf.handles.every((handleValue) => isFinite(handleValue)))
|
||||||
return false
|
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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue