New feature: Keyframe "hold" as a new easing type (#360)
This commit is contained in:
parent
00baa3ae22
commit
103c35737c
14 changed files with 367 additions and 174 deletions
|
@ -71,6 +71,8 @@ export type HistoricPositionalSequence = {
|
||||||
*/
|
*/
|
||||||
export type TrackData = BasicKeyframedTrack
|
export type TrackData = BasicKeyframedTrack
|
||||||
|
|
||||||
|
export type KeyframeType = 'bezier' | 'hold'
|
||||||
|
|
||||||
export type Keyframe = {
|
export type Keyframe = {
|
||||||
id: KeyframeId
|
id: KeyframeId
|
||||||
/** The `value` is the raw value type such as `Rgba` or `number`. See {@link SerializableValue} */
|
/** The `value` is the raw value type such as `Rgba` or `number`. See {@link SerializableValue} */
|
||||||
|
@ -80,6 +82,8 @@ export type Keyframe = {
|
||||||
position: number
|
position: number
|
||||||
handles: [leftX: number, leftY: number, rightX: number, rightY: number]
|
handles: [leftX: number, leftY: number, rightX: number, rightY: number]
|
||||||
connectedRight: boolean
|
connectedRight: boolean
|
||||||
|
// defaults to 'bezier' to support project states made with theatre0.5.1 or earlier
|
||||||
|
type?: KeyframeType
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrackDataCommon<TypeName extends string> = {
|
type TrackDataCommon<TypeName extends string> = {
|
||||||
|
|
|
@ -184,12 +184,6 @@ const states = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const solver = new UnitBezier(
|
|
||||||
left.handles[2],
|
|
||||||
left.handles[3],
|
|
||||||
right.handles[0],
|
|
||||||
right.handles[1],
|
|
||||||
)
|
|
||||||
const globalProgressionToLocalProgression = (
|
const globalProgressionToLocalProgression = (
|
||||||
globalProgression: number,
|
globalProgression: number,
|
||||||
): number => {
|
): number => {
|
||||||
|
@ -197,12 +191,21 @@ const states = {
|
||||||
(globalProgression - left.position) / (right.position - left.position)
|
(globalProgression - left.position) / (right.position - left.position)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const der = prism(() => {
|
|
||||||
|
if (!left.type || left.type === 'bezier') {
|
||||||
|
const solver = new UnitBezier(
|
||||||
|
left.handles[2],
|
||||||
|
left.handles[3],
|
||||||
|
right.handles[0],
|
||||||
|
right.handles[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
const bezierDer = prism(() => {
|
||||||
const progression = globalProgressionToLocalProgression(
|
const progression = globalProgressionToLocalProgression(
|
||||||
progressionD.getValue(),
|
progressionD.getValue(),
|
||||||
)
|
)
|
||||||
|
|
||||||
const valueProgression = solver.solveSimple(progression)
|
const valueProgression = solver.solveSimple(progression)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
left: left.value,
|
left: left.value,
|
||||||
right: right.value,
|
right: right.value,
|
||||||
|
@ -214,7 +217,27 @@ const states = {
|
||||||
started: true,
|
started: true,
|
||||||
validFrom: left.position,
|
validFrom: left.position,
|
||||||
validTo: right.position,
|
validTo: right.position,
|
||||||
der,
|
der: bezierDer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const holdDer = prism(() => {
|
||||||
|
const progression = globalProgressionToLocalProgression(
|
||||||
|
progressionD.getValue(),
|
||||||
|
)
|
||||||
|
const valueProgression = Math.floor(progression)
|
||||||
|
return {
|
||||||
|
left: left.value,
|
||||||
|
right: right.value,
|
||||||
|
progression: valueProgression,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
started: true,
|
||||||
|
validFrom: left.position,
|
||||||
|
validTo: right.position,
|
||||||
|
der: holdDer,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
|
|
|
@ -274,6 +274,7 @@ describe(`SheetObject`, () => {
|
||||||
position: 10,
|
position: 10,
|
||||||
connectedRight: true,
|
connectedRight: true,
|
||||||
handles: [0.5, 0.5, 0.5, 0.5],
|
handles: [0.5, 0.5, 0.5, 0.5],
|
||||||
|
type: 'bezier',
|
||||||
value: 3,
|
value: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -281,6 +282,7 @@ describe(`SheetObject`, () => {
|
||||||
position: 20,
|
position: 20,
|
||||||
connectedRight: false,
|
connectedRight: false,
|
||||||
handles: [0.5, 0.5, 0.5, 0.5],
|
handles: [0.5, 0.5, 0.5, 0.5],
|
||||||
|
type: 'bezier',
|
||||||
value: 6,
|
value: 6,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -144,6 +144,7 @@ export default function createTransactionPrivateApi(
|
||||||
position: seq.position,
|
position: seq.position,
|
||||||
value: value as $FixMe,
|
value: value as $FixMe,
|
||||||
snappingFunction: seq.closestGridPosition,
|
snappingFunction: seq.closestGridPosition,
|
||||||
|
type: 'bezier',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -514,6 +514,7 @@ function pasteKeyframesToMultipleTracks(
|
||||||
handles: keyframe.handles,
|
handles: keyframe.handles,
|
||||||
value: keyframe.value,
|
value: keyframe.value,
|
||||||
snappingFunction: sequence.closestGridPosition,
|
snappingFunction: sequence.closestGridPosition,
|
||||||
|
type: keyframe.type,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -548,6 +549,7 @@ function pasteKeyframesToSpecificTracks(
|
||||||
handles: keyframe.handles,
|
handles: keyframe.handles,
|
||||||
value: keyframe.value,
|
value: keyframe.value,
|
||||||
snappingFunction: sequence.closestGridPosition,
|
snappingFunction: sequence.closestGridPosition,
|
||||||
|
type: keyframe.type,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,6 +187,7 @@ function pasteKeyframesContextMenuItem(
|
||||||
handles: keyframe.handles,
|
handles: keyframe.handles,
|
||||||
value: keyframe.value,
|
value: keyframe.value,
|
||||||
snappingFunction: sequence.closestGridPosition,
|
snappingFunction: sequence.closestGridPosition,
|
||||||
|
type: keyframe.type,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -416,12 +416,14 @@ function setTempValue(
|
||||||
tempTransaction.current = null
|
tempTransaction.current = null
|
||||||
|
|
||||||
const handles = handlesFromCssCubicBezierArgs(newCurveCssCubicBezier)
|
const handles = handlesFromCssCubicBezierArgs(newCurveCssCubicBezier)
|
||||||
if (handles === null) return
|
if (handles === null) {
|
||||||
|
tempTransaction.current = transactionSetHold(keyframeConnections)
|
||||||
|
} else {
|
||||||
tempTransaction.current = transactionSetCubicBezier(
|
tempTransaction.current = transactionSetCubicBezier(
|
||||||
keyframeConnections,
|
keyframeConnections,
|
||||||
handles,
|
handles,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function discardTempValue(
|
function discardTempValue(
|
||||||
|
@ -436,7 +438,7 @@ function transactionSetCubicBezier(
|
||||||
handles: CubicBezierHandles,
|
handles: CubicBezierHandles,
|
||||||
): CommitOrDiscard {
|
): CommitOrDiscard {
|
||||||
return getStudio().tempTransaction(({stateEditors}) => {
|
return getStudio().tempTransaction(({stateEditors}) => {
|
||||||
const {setHandlesForKeyframe} =
|
const {setHandlesForKeyframe, setKeyframeType: setKeyframeType} =
|
||||||
stateEditors.coreByProject.historic.sheetsById.sequence
|
stateEditors.coreByProject.historic.sheetsById.sequence
|
||||||
|
|
||||||
for (const {
|
for (const {
|
||||||
|
@ -463,6 +465,40 @@ function transactionSetCubicBezier(
|
||||||
keyframeId: right.id,
|
keyframeId: right.id,
|
||||||
end: [handles[2], handles[3]],
|
end: [handles[2], handles[3]],
|
||||||
})
|
})
|
||||||
|
setKeyframeType({
|
||||||
|
projectId,
|
||||||
|
sheetId,
|
||||||
|
objectKey,
|
||||||
|
trackId,
|
||||||
|
keyframeId: left.id,
|
||||||
|
keyframeType: 'bezier',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function transactionSetHold(
|
||||||
|
keyframeConnections: Array<KeyframeConnectionWithAddress>,
|
||||||
|
): CommitOrDiscard {
|
||||||
|
return getStudio().tempTransaction(({stateEditors}) => {
|
||||||
|
const {setKeyframeType: setKeyframeType} =
|
||||||
|
stateEditors.coreByProject.historic.sheetsById.sequence
|
||||||
|
|
||||||
|
for (const {
|
||||||
|
projectId,
|
||||||
|
sheetId,
|
||||||
|
objectKey,
|
||||||
|
trackId,
|
||||||
|
left,
|
||||||
|
} of keyframeConnections) {
|
||||||
|
setKeyframeType({
|
||||||
|
projectId,
|
||||||
|
sheetId,
|
||||||
|
objectKey,
|
||||||
|
trackId,
|
||||||
|
keyframeId: left.id,
|
||||||
|
keyframeType: 'hold',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,6 +111,8 @@ const CurveSegmentEditor: React.VFC<ICurveSegmentEditorProps> = (props) => {
|
||||||
1 - connection.right.handles[1],
|
1 - connection.right.handles[1],
|
||||||
)} 1 ${toExtremumSpace(0)}`
|
)} 1 ${toExtremumSpace(0)}`
|
||||||
|
|
||||||
|
const holdPointsAttrValue = `0,100 100,100 100,0`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
height="100%"
|
height="100%"
|
||||||
|
@ -178,6 +180,8 @@ const CurveSegmentEditor: React.VFC<ICurveSegmentEditorProps> = (props) => {
|
||||||
fill="url(#dot-background-pattern-2)"
|
fill="url(#dot-background-pattern-2)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{!left.type || left.type === 'bezier' ? (
|
||||||
|
<>
|
||||||
{/* Line from right end of curve to right handle */}
|
{/* Line from right end of curve to right handle */}
|
||||||
<line
|
<line
|
||||||
x1={0}
|
x1={0}
|
||||||
|
@ -196,7 +200,6 @@ const CurveSegmentEditor: React.VFC<ICurveSegmentEditorProps> = (props) => {
|
||||||
stroke={CONTROL_COLOR}
|
stroke={CONTROL_COLOR}
|
||||||
strokeWidth="0.01"
|
strokeWidth="0.01"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Curve "shadow": the low-opacity filled area between the curve and the diagonal */}
|
{/* Curve "shadow": the low-opacity filled area between the curve and the diagonal */}
|
||||||
<path
|
<path
|
||||||
d={curvePathDAttrValue(props.curveConnection)}
|
d={curvePathDAttrValue(props.curveConnection)}
|
||||||
|
@ -209,7 +212,9 @@ const CurveSegmentEditor: React.VFC<ICurveSegmentEditorProps> = (props) => {
|
||||||
<path
|
<path
|
||||||
key={connection.objectKey + '/' + connection.left.id}
|
key={connection.objectKey + '/' + connection.left.id}
|
||||||
d={curvePathDAttrValue(connection)}
|
d={curvePathDAttrValue(connection)}
|
||||||
stroke={BACKGROUND_CURVE_COLORS[i % BACKGROUND_CURVE_COLORS.length]}
|
stroke={
|
||||||
|
BACKGROUND_CURVE_COLORS[i % BACKGROUND_CURVE_COLORS.length]
|
||||||
|
}
|
||||||
opacity={0.6}
|
opacity={0.6}
|
||||||
strokeWidth="0.01"
|
strokeWidth="0.01"
|
||||||
/>
|
/>
|
||||||
|
@ -247,7 +252,10 @@ const CurveSegmentEditor: React.VFC<ICurveSegmentEditorProps> = (props) => {
|
||||||
fill={CURVE_START_COLOR}
|
fill={CURVE_START_COLOR}
|
||||||
opacity={0.2}
|
opacity={0.2}
|
||||||
/>
|
/>
|
||||||
<Circle cx={left.handles[2]} cy={toExtremumSpace(1 - left.handles[3])} />
|
<Circle
|
||||||
|
cx={left.handles[2]}
|
||||||
|
cy={toExtremumSpace(1 - left.handles[3])}
|
||||||
|
/>
|
||||||
{/* Left handle and hit zone */}
|
{/* Left handle and hit zone */}
|
||||||
<HitZone
|
<HitZone
|
||||||
ref={refRight}
|
ref={refRight}
|
||||||
|
@ -260,6 +268,43 @@ const CurveSegmentEditor: React.VFC<ICurveSegmentEditorProps> = (props) => {
|
||||||
cx={right.handles[0]}
|
cx={right.handles[0]}
|
||||||
cy={toExtremumSpace(1 - right.handles[1])}
|
cy={toExtremumSpace(1 - right.handles[1])}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<line
|
||||||
|
x1={0}
|
||||||
|
y1={toExtremumSpace(1)}
|
||||||
|
x2={1}
|
||||||
|
y2={toExtremumSpace(1)}
|
||||||
|
stroke={CONTROL_COLOR}
|
||||||
|
strokeWidth="0.01"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1={1}
|
||||||
|
y1={toExtremumSpace(1)}
|
||||||
|
x2={1}
|
||||||
|
y2={0}
|
||||||
|
stroke={CONTROL_COLOR}
|
||||||
|
strokeWidth="0.01"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx={0}
|
||||||
|
cy={1}
|
||||||
|
r="0.025"
|
||||||
|
stroke={CURVE_END_COLOR}
|
||||||
|
strokeWidth="0.02"
|
||||||
|
fill={COLOR_BASE}
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx={1}
|
||||||
|
cy={0}
|
||||||
|
r="0.025"
|
||||||
|
stroke={CURVE_END_COLOR}
|
||||||
|
strokeWidth="0.02"
|
||||||
|
fill={COLOR_BASE}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,20 +21,17 @@ type IProps = {
|
||||||
|
|
||||||
const SVGCurveSegment: React.FC<IProps> = (props) => {
|
const SVGCurveSegment: React.FC<IProps> = (props) => {
|
||||||
const {easing, isSelected} = props
|
const {easing, isSelected} = props
|
||||||
|
|
||||||
if (!easing) return <></>
|
|
||||||
|
|
||||||
const curveColor = isSelected ? SELECTED_CURVE_COLOR : CURVE_COLOR
|
const curveColor = isSelected ? SELECTED_CURVE_COLOR : CURVE_COLOR
|
||||||
|
|
||||||
const leftControlPoint = [easing[0], toVerticalSVGSpace(easing[1])]
|
|
||||||
const rightControlPoint = [easing[2], toVerticalSVGSpace(easing[3])]
|
|
||||||
|
|
||||||
// With a padding of 0, this results in a "unit viewbox" i.e. `0 0 1 1`.
|
// With a padding of 0, this results in a "unit viewbox" i.e. `0 0 1 1`.
|
||||||
// With padding e.g. VIEWBOX_PADDING=0.1, this results in a viewbox of `-0.1 -0,1 1.2 1.2`,
|
// With padding e.g. VIEWBOX_PADDING=0.1, this results in a viewbox of `-0.1 -0,1 1.2 1.2`,
|
||||||
// i.e. a viewbox with a top left coordinate of -0.1,-0.1 and a width and height of 1.2,
|
// i.e. a viewbox with a top left coordinate of -0.1,-0.1 and a width and height of 1.2,
|
||||||
// resulting in bottom right coordinate of 1.1,1.1
|
// resulting in bottom right coordinate of 1.1,1.1
|
||||||
const SVG_VIEWBOX_ATTR = `${-VIEWBOX_PADDING} ${-VIEWBOX_PADDING} ${VIEWBOX_SIZE} ${VIEWBOX_SIZE}`
|
const SVG_VIEWBOX_ATTR = `${-VIEWBOX_PADDING} ${-VIEWBOX_PADDING} ${VIEWBOX_SIZE} ${VIEWBOX_SIZE}`
|
||||||
|
|
||||||
|
// Bezier SVG
|
||||||
|
if (easing) {
|
||||||
|
const leftControlPoint = [easing[0], toVerticalSVGSpace(easing[1])]
|
||||||
|
const rightControlPoint = [easing[2], toVerticalSVGSpace(easing[3])]
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
height="100%"
|
height="100%"
|
||||||
|
@ -100,5 +97,36 @@ const SVGCurveSegment: React.FC<IProps> = (props) => {
|
||||||
<circle cx={1} cy={0} r={SVG_CIRCLE_RADIUS} fill={curveColor} />
|
<circle cx={1} cy={0} r={SVG_CIRCLE_RADIUS} fill={curveColor} />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Hold" SVG
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
viewBox={SVG_VIEWBOX_ATTR}
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1="0"
|
||||||
|
y1="1"
|
||||||
|
x2={1}
|
||||||
|
y2={1}
|
||||||
|
stroke={curveColor}
|
||||||
|
strokeWidth="0.08"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="1"
|
||||||
|
y1="0"
|
||||||
|
x2={1}
|
||||||
|
y2={1}
|
||||||
|
stroke={curveColor}
|
||||||
|
strokeWidth="0.08"
|
||||||
|
/>
|
||||||
|
<circle cx={0} cy={1} r={SVG_CIRCLE_RADIUS} fill={curveColor} />
|
||||||
|
<circle cx={1} cy={0} r={SVG_CIRCLE_RADIUS} fill={curveColor} />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
export default SVGCurveSegment
|
export default SVGCurveSegment
|
||||||
|
|
|
@ -85,6 +85,7 @@ export const EASING_PRESETS = [
|
||||||
|
|
||||||
{label: 'linear', value: '0.5, 0.5, 0.5, 0.5'},
|
{label: 'linear', value: '0.5, 0.5, 0.5, 0.5'},
|
||||||
{label: 'In Out', value: '0.42,0,0.58,1'},
|
{label: 'In Out', value: '0.42,0,0.58,1'},
|
||||||
|
{label: 'Hold', value: '0, 0, Infinity, Infinity'},
|
||||||
|
|
||||||
/* These easings are not being included initially in order to
|
/* These easings are not being included initially in order to
|
||||||
simplify the choices */
|
simplify the choices */
|
||||||
|
|
|
@ -74,6 +74,19 @@ const Diamond = styled.div<IDiamond>`
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const Square = styled.div<IDiamond>`
|
||||||
|
position: absolute;
|
||||||
|
${absoluteDims(DOT_SIZE_PX * 1.5)}
|
||||||
|
|
||||||
|
background: ${(props) => selectBacgroundForDiamond(props)};
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.flag === PresenceFlag.Primary ? 'outline: 2px solid white;' : ''};
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
`
|
||||||
|
|
||||||
const HitZone = styled.div<{isInlineEditorPopoverOpen: boolean}>`
|
const HitZone = styled.div<{isInlineEditorPopoverOpen: boolean}>`
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
|
@ -121,6 +134,8 @@ const SingleKeyframeDot: React.VFC<ISingleKeyframeDotProps> = (props) => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const showDiamond = !props.keyframe.type || props.keyframe.type === 'bezier'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HitZone
|
<HitZone
|
||||||
|
@ -128,11 +143,19 @@ const SingleKeyframeDot: React.VFC<ISingleKeyframeDotProps> = (props) => {
|
||||||
isInlineEditorPopoverOpen={isInlineEditorPopoverOpen}
|
isInlineEditorPopoverOpen={isInlineEditorPopoverOpen}
|
||||||
{...presence.attrs}
|
{...presence.attrs}
|
||||||
/>
|
/>
|
||||||
|
{showDiamond ? (
|
||||||
<Diamond
|
<Diamond
|
||||||
isSelected={!!props.selection}
|
isSelected={!!props.selection}
|
||||||
isInlineEditorPopoverOpen={isInlineEditorPopoverOpen}
|
isInlineEditorPopoverOpen={isInlineEditorPopoverOpen}
|
||||||
flag={presence.flag}
|
flag={presence.flag}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Square
|
||||||
|
isSelected={!!props.selection}
|
||||||
|
isInlineEditorPopoverOpen={isInlineEditorPopoverOpen}
|
||||||
|
flag={presence.flag}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{inlineEditorPopover}
|
{inlineEditorPopover}
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -15,6 +15,9 @@ const SVGPath = styled.path`
|
||||||
|
|
||||||
type IProps = Parameters<typeof KeyframeEditor>[0]
|
type IProps = Parameters<typeof KeyframeEditor>[0]
|
||||||
|
|
||||||
|
// for keyframe.type === 'hold'
|
||||||
|
const pathForHoldType = `M 0 0 L 1 0 L 1 1`
|
||||||
|
|
||||||
const Curve: React.VFC<IProps> = (props) => {
|
const Curve: React.VFC<IProps> = (props) => {
|
||||||
const {index, trackData} = props
|
const {index, trackData} = props
|
||||||
const cur = trackData.keyframes[index]
|
const cur = trackData.keyframes[index]
|
||||||
|
@ -56,7 +59,7 @@ const Curve: React.VFC<IProps> = (props) => {
|
||||||
<>
|
<>
|
||||||
<SVGPath
|
<SVGPath
|
||||||
ref={nodeRef}
|
ref={nodeRef}
|
||||||
d={pathD}
|
d={!cur.type || cur.type === 'bezier' ? pathD : pathForHoldType}
|
||||||
style={{
|
style={{
|
||||||
transform,
|
transform,
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -55,9 +55,14 @@ const KeyframeEditor: React.VFC<IKeyframeEditorProps> = (props) => {
|
||||||
{shouldShowCurve ? (
|
{shouldShowCurve ? (
|
||||||
<>
|
<>
|
||||||
<Curve {...props} />
|
<Curve {...props} />
|
||||||
|
{!cur.type ||
|
||||||
|
(cur.type === 'bezier' && (
|
||||||
|
<>
|
||||||
<CurveHandle {...props} which="left" />
|
<CurveHandle {...props} which="left" />
|
||||||
<CurveHandle {...props} which="right" />
|
<CurveHandle {...props} which="right" />
|
||||||
</>
|
</>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
noConnector
|
noConnector
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type {
|
||||||
BasicKeyframedTrack,
|
BasicKeyframedTrack,
|
||||||
HistoricPositionalSequence,
|
HistoricPositionalSequence,
|
||||||
Keyframe,
|
Keyframe,
|
||||||
|
KeyframeType,
|
||||||
SheetState_Historic,
|
SheetState_Historic,
|
||||||
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
import type {Drafts} from '@theatre/studio/StudioStore/StudioStore'
|
import type {Drafts} from '@theatre/studio/StudioStore/StudioStore'
|
||||||
|
@ -696,6 +697,17 @@ namespace stateEditors {
|
||||||
return _ensureTracksOfObject(p).trackData[p.trackId]
|
return _ensureTracksOfObject(p).trackData[p.trackId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _getKeyframeById(
|
||||||
|
p: WithoutSheetInstance<SheetObjectAddress> & {
|
||||||
|
trackId: SequenceTrackId
|
||||||
|
keyframeId: KeyframeId
|
||||||
|
},
|
||||||
|
): Keyframe | undefined {
|
||||||
|
const track = _getTrack(p)
|
||||||
|
if (!track) return
|
||||||
|
return track.keyframes.find((kf) => kf.id === p.keyframeId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a keyframe at the exact specified position.
|
* Sets a keyframe at the exact specified position.
|
||||||
* Any position snapping should be done by the caller.
|
* Any position snapping should be done by the caller.
|
||||||
|
@ -707,6 +719,7 @@ namespace stateEditors {
|
||||||
handles?: [number, number, number, number]
|
handles?: [number, number, number, number]
|
||||||
value: T
|
value: T
|
||||||
snappingFunction: SnappingFunction
|
snappingFunction: SnappingFunction
|
||||||
|
type?: KeyframeType
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const position = p.snappingFunction(p.position)
|
const position = p.snappingFunction(p.position)
|
||||||
|
@ -734,6 +747,7 @@ namespace stateEditors {
|
||||||
position,
|
position,
|
||||||
connectedRight: true,
|
connectedRight: true,
|
||||||
handles: p.handles || [0.5, 1, 0.5, 0],
|
handles: p.handles || [0.5, 1, 0.5, 0],
|
||||||
|
type: p.type || 'bezier',
|
||||||
value: p.value,
|
value: p.value,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -744,6 +758,7 @@ namespace stateEditors {
|
||||||
position,
|
position,
|
||||||
connectedRight: leftKeyframe.connectedRight,
|
connectedRight: leftKeyframe.connectedRight,
|
||||||
handles: p.handles || [0.5, 1, 0.5, 0],
|
handles: p.handles || [0.5, 1, 0.5, 0],
|
||||||
|
type: p.type || 'bezier',
|
||||||
value: p.value,
|
value: p.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -868,24 +883,15 @@ namespace stateEditors {
|
||||||
end?: [number, number]
|
end?: [number, number]
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const track = _getTrack(p)
|
const keyframe = _getKeyframeById(p)
|
||||||
if (!track) return
|
if (keyframe) {
|
||||||
track.keyframes = track.keyframes.map((kf) => {
|
keyframe.handles = [
|
||||||
if (kf.id === p.keyframeId) {
|
p.end?.[0] ?? keyframe.handles[0],
|
||||||
// Use given value or fallback to original value,
|
p.end?.[1] ?? keyframe.handles[1],
|
||||||
// allowing the caller to customize exactly which side
|
p.start?.[0] ?? keyframe.handles[2],
|
||||||
// of the curve they are editing.
|
p.start?.[1] ?? keyframe.handles[3],
|
||||||
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(
|
export function deleteKeyframes(
|
||||||
|
@ -902,6 +908,19 @@ namespace stateEditors {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setKeyframeType(
|
||||||
|
p: WithoutSheetInstance<SheetObjectAddress> & {
|
||||||
|
trackId: SequenceTrackId
|
||||||
|
keyframeId: KeyframeId
|
||||||
|
keyframeType: KeyframeType
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const kf = _getKeyframeById(p)
|
||||||
|
if (kf) {
|
||||||
|
kf.type = p.keyframeType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Future: consider whether a list of "partial" keyframes requiring `id` is possible to accept
|
// Future: consider whether a list of "partial" keyframes requiring `id` is possible to accept
|
||||||
// * Consider how common this pattern is, as this sort of concept would best be encountered
|
// * Consider how common this pattern is, as this sort of concept would best be encountered
|
||||||
// a few times to start to see an opportunity for improved ergonomics / crdt.
|
// a few times to start to see an opportunity for improved ergonomics / crdt.
|
||||||
|
|
Loading…
Reference in a new issue