More API docs

This commit is contained in:
Aria Minaei 2021-09-17 14:58:26 +02:00
parent a86e220bdc
commit 7815fb2dc3
7 changed files with 442 additions and 96 deletions

View file

@ -38,6 +38,15 @@ export interface IProject {
* The project's address * The project's address
*/ */
readonly address: ProjectAddress readonly address: ProjectAddress
/**
* Creates a Sheet under the project
* @param sheetId Sheets are identified by their `sheetId`, which must be a string longer than 3 characters
* @param instanceId Optionally provide an `instanceId` if you want to create multiple instances of the same Sheet
* @returns The newly created Sheet
*
* **Docs: https://docs.theatrejs.com/in-depth/#sheets**
*/
sheet(sheetId: string, instanceId?: string): ISheet sheet(sheetId: string, instanceId?: string): ISheet
} }

View file

@ -1,4 +1,5 @@
import type {$IntentionalAny} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'
import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue'
import type { import type {
IShorthandCompoundProps, IShorthandCompoundProps,
IValidCompoundProps, IValidCompoundProps,
@ -7,48 +8,123 @@ import type {
import {sanitizeCompoundProps} from './internals' import {sanitizeCompoundProps} from './internals'
import {propTypeSymbol} from './internals' import {propTypeSymbol} from './internals'
const validateCommonOpts = (
fnCallSignature: string,
opts?: PropTypeConfigOpts,
) => {
if (process.env.NODE_ENV !== 'production') {
if (opts === undefined) return
if (typeof opts !== 'object' || opts === null) {
throw new Error(
`opts in ${fnCallSignature} must either be undefined or an object.`,
)
}
if (Object.prototype.hasOwnProperty.call(opts, 'label')) {
const {label} = opts
if (typeof label !== 'string') {
throw new Error(
`opts.label in ${fnCallSignature} should be a string. ${userReadableTypeOfValue(
label,
)} given.`,
)
}
if (label.trim().length !== label.length) {
throw new Error(
`opts.label in ${fnCallSignature} should not start/end with whitespace. "${label}" given.`,
)
}
if (label.length === 0) {
throw new Error(
`opts.label in ${fnCallSignature} should not be an empty string. If you wish to have no label, remove opts.label from opts.`,
)
}
}
}
}
/** /**
* Creates a compound prop type (basically a JS object). * A compound prop type (basically a JS object).
*
* Usage: * Usage:
* ```ts * ```ts
* // the root prop type of an object is always a compound * // shorthand
* const props = { * const position = {
* // compounds can be nested * x: 0,
* position: t.compound({ * y: 0
* x: t.number(0),
* y: t.number(0)
* })
* } * }
* assert(sheet.object('some object', position).value.x === 0)
* *
* const obj = sheet.obj('key', props) * // nesting
* console.log(obj.value) // {position: {x: 10.3, y: -1}} * const foo = {bar: {baz: {quo: 0}}}
* assert(sheet.object('some object', foo).bar.baz.quo === 0)
*
* // With additional options:
* const position = t.compound(
* {x: 0, y: 0},
* // a custom label for the prop:
* {label: "Position"}
* )
* ``` * ```
* @param props * @param props
* @param extras * @param opts
* @returns * @returns
* *
*/ */
export const compound = <Props extends IShorthandCompoundProps>( export const compound = <Props extends IShorthandCompoundProps>(
props: Props, props: Props,
extras?: PropTypeConfigExtras, opts?: PropTypeConfigOpts,
): PropTypeConfig_Compound< ): PropTypeConfig_Compound<
ShorthandCompoundPropsToLonghandCompoundProps<Props> ShorthandCompoundPropsToLonghandCompoundProps<Props>
> => { > => {
validateCommonOpts('t.compound(props, opts)', opts)
return { return {
type: 'compound', type: 'compound',
props: sanitizeCompoundProps(props), props: sanitizeCompoundProps(props),
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: extras?.label, label: opts?.label,
} }
} }
/** /**
* A number prop type.
* *
* @param defaultValue * Usage
* @param opts * ```ts
* @returns * // shorthand:
* const obj = sheet.object('key', {x: 0})
* *
* // With options (equal to above)
* const obj = sheet.object('key', {
* x: t.number(0)
* })
*
* // With a range (note that opts.range is just a visual guide, not a validation rule)
* const x = t.number(0, {range: [0, 10]}) // limited to 0 and 10
*
* // With custom nudging
* const x = t.number(0, {nudgeMultiplier: 0.1}) // nudging will happen in 0.1 increments
*
* // With custom nudging function
* const x = t.number({
* nudgeFn: (
* // the mouse movement (in pixels)
* deltaX: number,
* // the movement as a fraction of the width of the number editor's input
* deltaFraction: number,
* // A multiplier that's usually 1, but might be another number if user wants to nudge slower/faster
* magnitude: number,
* // the configuration of the number
* config: {nudgeMultiplier?: number; range?: [number, number]},
* ): number => {
* return deltaX * magnitude
* },
* })
* ```
*
* @param defaultValue The default value (Must be a finite number)
* @param opts The options (See usage examples)
* @returns A number prop config
*/ */
export const number = ( export const number = (
defaultValue: number, defaultValue: number,
@ -56,8 +132,60 @@ export const number = (
nudgeFn?: PropTypeConfig_Number['nudgeFn'] nudgeFn?: PropTypeConfig_Number['nudgeFn']
range?: PropTypeConfig_Number['range'] range?: PropTypeConfig_Number['range']
nudgeMultiplier?: number nudgeMultiplier?: number
} & PropTypeConfigExtras, } & PropTypeConfigOpts,
): PropTypeConfig_Number => { ): PropTypeConfig_Number => {
if (process.env.NODE_ENV !== 'production') {
validateCommonOpts('t.number(defaultValue, opts)', opts)
if (typeof defaultValue !== 'number' || !isFinite(defaultValue)) {
throw new Error(
`Argument defaultValue in t.number(defaultValue) must be a number. ${userReadableTypeOfValue(
defaultValue,
)} given.`,
)
}
if (typeof opts === 'object' && opts) {
if (Object.prototype.hasOwnProperty.call(opts, 'range')) {
if (!Array.isArray(opts.range)) {
throw new Error(
`opts.range in t.number(defaultValue, opts) must be a tuple of two numbers. ${userReadableTypeOfValue(
opts.range,
)} given.`,
)
}
if (opts.range.length !== 2) {
throw new Error(
`opts.range in t.number(defaultValue, opts) must have two elements. ${opts.range.length} given.`,
)
}
if (!opts.range.every((n) => typeof n === 'number' && isFinite(n))) {
throw new Error(
`opts.range in t.number(defaultValue, opts) must be a tuple of two finite numbers.`,
)
}
}
}
if (Object.prototype.hasOwnProperty.call(opts, 'nudgeMultiplier')) {
if (
typeof opts!.nudgeMultiplier !== 'number' ||
!isFinite(opts!.nudgeMultiplier)
) {
throw new Error(
`opts.nudgeMultiplier in t.number(defaultValue, opts) must be a finite number. ${userReadableTypeOfValue(
opts!.nudgeMultiplier,
)} given.`,
)
}
}
if (Object.prototype.hasOwnProperty.call(opts, 'nudgeFn')) {
if (typeof opts?.nudgeFn !== 'function') {
throw new Error(
`opts.nudgeFn in t.number(defaultValue, opts) must be a function. ${userReadableTypeOfValue(
opts!.nudgeFn,
)} given.`,
)
}
}
}
return { return {
type: 'number', type: 'number',
valueType: 0, valueType: 0,
@ -72,57 +200,117 @@ export const number = (
} }
/** /**
* A boolean prop type
* *
* @param defaultValue * Usage:
* @param extras * ```ts
* @returns * // shorthand:
* const obj = sheet.object('key', {isOn: true})
* *
* // with a label:
* const obj = sheet.object('key', {
* isOn: t.boolean(true, {
* label: 'Enabled'
* })
* })
* ```
*
* @param defaultValue The default value (must be a boolean)
* @param opts Options (See usage examples)
*/ */
export const boolean = ( export const boolean = (
defaultValue: boolean, defaultValue: boolean,
extras?: PropTypeConfigExtras, opts?: PropTypeConfigOpts,
): PropTypeConfig_Boolean => { ): PropTypeConfig_Boolean => {
if (process.env.NODE_ENV !== 'production') {
validateCommonOpts('t.boolean(defaultValue, opts)', opts)
if (typeof defaultValue !== 'boolean') {
throw new Error(
`defaultValue in t.boolean(defaultValue) must be a boolean. ${userReadableTypeOfValue(
defaultValue,
)} given.`,
)
}
}
return { return {
type: 'boolean', type: 'boolean',
default: defaultValue, default: defaultValue,
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: extras?.label, label: opts?.label,
} }
} }
/** /**
* A string prop type
* *
* @param defaultValue * Usage:
* @param extras * ```ts
* @returns * // shorthand:
* const obj = sheet.object('key', {message: "Animation loading"})
* *
* // with a label:
* const obj = sheet.object('key', {
* message: t.string("Animation Loading", {
* label: 'The Message'
* })
* })
* ```
*
* @param defaultValue The default value (must be a string)
* @param opts The options (See usage examples)
* @returns A string prop type
*/ */
export const string = ( export const string = (
defaultValue: string, defaultValue: string,
extras?: PropTypeConfigExtras, opts?: PropTypeConfigOpts,
): PropTypeConfig_String => { ): PropTypeConfig_String => {
if (process.env.NODE_ENV !== 'production') {
validateCommonOpts('t.string(defaultValue, opts)', opts)
if (typeof defaultValue !== 'string') {
throw new Error(
`defaultValue in t.string(defaultValue) must be a string. ${userReadableTypeOfValue(
defaultValue,
)} given.`,
)
}
}
return { return {
type: 'string', type: 'string',
default: defaultValue, default: defaultValue,
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: extras?.label, label: opts?.label,
} }
} }
/** /**
* A stringLiteral prop type, useful for building menus or radio buttons.
* *
* @param defaultValue * Usage:
* @param options * ```ts
* @param extras * // Basic usage
* @returns * const obj = sheet.object('key', {
* light: t.stringLiteral("r", {r: "Red", "g": "Green"})
* })
*
* // Shown as a radio switch with a custom label
* const obj = sheet.object('key', {
* light: t.stringLiteral("r", {r: "Red", "g": "Green"})
* }, {as: "switch", label: "Street Light"})
* ```
*
* @param defaultValue A string
* @param options An object like `{[value]: Label}`. Example: {r: "Red", "g": "Green"}
* @param opts Extra opts
* @param opts.as Determines if editor is shown as a menu or a switch. Either 'menu' or 'switch'. Default: 'menu'
* @returns A stringLiteral prop type
* *
*/ */
export function stringLiteral<Opts extends {[key in string]: string}>( export function stringLiteral<Opts extends {[key in string]: string}>(
defaultValue: Extract<keyof Opts, string>, defaultValue: Extract<keyof Opts, string>,
options: Opts, options: Opts,
extras?: {as?: 'menu' | 'switch'} & PropTypeConfigExtras, opts?: {as?: 'menu' | 'switch'} & PropTypeConfigOpts,
): PropTypeConfig_StringLiteral<Extract<keyof Opts, string>> { ): PropTypeConfig_StringLiteral<Extract<keyof Opts, string>> {
return { return {
type: 'stringLiteral', type: 'stringLiteral',
@ -130,24 +318,11 @@ export function stringLiteral<Opts extends {[key in string]: string}>(
options: {...options}, options: {...options},
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
as: extras?.as ?? 'menu', as: opts?.as ?? 'menu',
label: extras?.label, label: opts?.label,
} }
} }
// export const rgba = (
// defaultValue: {r: number; b: number; g: number; a: number},
// extras?: PropTypeConfigExtras,
// ): PropTypeConfig_CSSRGBA => {
// return {
// type: 'cssrgba',
// valueType: null as $IntentionalAny,
// [s]: 'TheatrePropType',
// label: extras?.label,
// default: defaultValue,
// }
// }
interface IBasePropType<ValueType> { interface IBasePropType<ValueType> {
valueType: ValueType valueType: ValueType
[propTypeSymbol]: 'TheatrePropType' [propTypeSymbol]: 'TheatrePropType'
@ -190,7 +365,7 @@ export interface PropTypeConfig_Boolean extends IBasePropType<boolean> {
default: boolean default: boolean
} }
export interface PropTypeConfigExtras { export interface PropTypeConfigOpts {
label?: string label?: string
} }
export interface PropTypeConfig_String extends IBasePropType<string> { export interface PropTypeConfig_String extends IBasePropType<string> {
@ -206,12 +381,6 @@ export interface PropTypeConfig_StringLiteral<T extends string>
as: 'menu' | 'switch' as: 'menu' | 'switch'
} }
/**
* @todo Determine if 'compound' is a clear term for what this is.
* I didn't want to use 'object' as it could get confused with
* SheetObject.
*/
/** /**
* *
*/ */
@ -221,12 +390,6 @@ export interface PropTypeConfig_Compound<Props extends IValidCompoundProps>
props: Record<string, PropTypeConfig> props: Record<string, PropTypeConfig>
} }
// export interface PropTypeConfig_CSSRGBA
// extends IBasePropType<{r: number; g: number; b: number; a: number}> {
// type: 'cssrgba'
// default: {r: number; g: number; b: number; a: number}
// }
export interface PropTypeConfig_Enum extends IBasePropType<{}> { export interface PropTypeConfig_Enum extends IBasePropType<{}> {
type: 'enum' type: 'enum'
cases: Record<string, PropTypeConfig> cases: Record<string, PropTypeConfig>
@ -238,7 +401,6 @@ export type PropTypeConfig_AllPrimitives =
| PropTypeConfig_Boolean | PropTypeConfig_Boolean
| PropTypeConfig_String | PropTypeConfig_String
| PropTypeConfig_StringLiteral<$IntentionalAny> | PropTypeConfig_StringLiteral<$IntentionalAny>
// | PropTypeConfig_CSSRGBA
export type PropTypeConfig = export type PropTypeConfig =
| PropTypeConfig_AllPrimitives | PropTypeConfig_AllPrimitives

View file

@ -28,6 +28,32 @@ export interface ISequence {
* Starts playback of a sequence. * Starts playback of a sequence.
* Returns a promise that either resolves to true when the playback completes, * Returns a promise that either resolves to true when the playback completes,
* or resolves to false if playback gets interrupted (for example by calling sequence.pause()) * or resolves to false if playback gets interrupted (for example by calling sequence.pause())
*
* @returns A promise that resolves when the playback is finished, or rejects if interruped
*
* @example
* ```ts
* // plays the sequence from the current position to sequence.length
* sheet.sequence.play()
*
* // plays the sequence at 2.4x speed
* sheet.sequence.play({rate: 2.4})
*
* // plays the sequence from second 1 to 4
* sheet.sequence.play({range: [1, 4]})
*
* // plays the sequence 4 times
* sheet.sequence.play({iterationCount: 4})
*
* // plays the sequence in reverse
* sheet.sequence.play({direction: 'reverse'})
*
* // plays the sequence back and forth forever (until interrupted)
* sheet.sequence.play({iterationCount: Infinity, direction: 'alternateReverse})
*
* // plays the sequence and logs "done" once playback is finished
* sheet.sequence.play().then(() => console.log('done'))
* ```
*/ */
play(conf?: { play(conf?: {
/** /**
@ -57,13 +83,29 @@ export interface ISequence {
/** /**
* The current position of the playhead. * The current position of the playhead.
* In a time-based sequence, this represents the current time. * In a time-based sequence, this represents the current time in seconds.
*/ */
position: number position: number
/** /**
* Attaches an audio source to the sequence. Playing the sequence automatically
* plays the audio source and their times are kept in sync.
* *
* @param args * @returns A promise that resolves once the audio source is loaded and decoded
*
* @example
* ```ts
* // Loads and decodes audio from the URL and then attaches it to the sequence
* await sheet.sequence.attachAudio({source: "https://localhost/audio.ogg"})
* sheet.sequence.play()
*
* // Providing your own AudioAPI Context, destination, etc
* const audioContext: AudioContext = {...} // create an AudioContext using the Audio API
* const audioBuffer: AudioBuffer = {...} // create an AudioBuffer
* const destinationNode = audioContext.destination
*
* await sheet.sequence.attachAudio({source: audioBuffer, audioContext, destinationNode})
* ```
*/ */
attachAudio(args: IAttachAudioArgs): Promise<void> attachAudio(args: IAttachAudioArgs): Promise<void>
} }

View file

@ -18,20 +18,82 @@ import type {
} from '@theatre/core/propTypes/internals' } from '@theatre/core/propTypes/internals'
export interface ISheetObject<Props extends IShorthandCompoundProps = {}> { export interface ISheetObject<Props extends IShorthandCompoundProps = {}> {
/**
* All Objects will have `object.type === 'Theatre_SheetObject_PublicAPI'`
*/
readonly type: 'Theatre_SheetObject_PublicAPI' readonly type: 'Theatre_SheetObject_PublicAPI'
/** /**
* The current values of the props.
* *
* @example
* ```ts
* const obj = sheet.object("obj", {x: 0})
* console.log(obj.value.x) // prints 0 or the current numeric value
* ```
*/ */
readonly value: ShorthandPropToLonghandProp<Props>['valueType'] readonly value: ShorthandPropToLonghandProp<Props>['valueType']
/**
* A Pointer to the props of the object.
*
* More documentation soon.
*/
readonly props: Pointer<this['value']> readonly props: Pointer<this['value']>
/**
* The instance of Sheet the Object belongs to
*/
readonly sheet: ISheet readonly sheet: ISheet
/**
* The Project the project belongs to
*/
readonly project: IProject readonly project: IProject
/**
* An object representing the address of the Object
*/
readonly address: SheetObjectAddress readonly address: SheetObjectAddress
/**
* Calls `fn` every time the value of the props change.
*
* @returns an Unsubscribe function
*
* @example
* ```ts
* const obj = sheet.object("Box", {position: {x: 0, y: 0}})
* const div = document.getElementById("box")
*
* const unsubscribe = obj.onValuesChange((newValues) => {
* div.style.left = newValues.position.x + 'px'
* div.style.top = newValues.position.y + 'px'
* })
*
* // you can call unsubscribe() to stop listening to changes
* ```
*/
onValuesChange(fn: (values: this['value']) => void): VoidFn onValuesChange(fn: (values: this['value']) => void): VoidFn
// prettier-ignore
/**
* Sets the initial value of the object. This value overrides the default
* values defined in the prop types, but would itself be overridden if the user
* overrides it in the UI with a static or animated value.
*
*
* @example
* ```ts
* const obj = sheet.object("obj", {position: {x: 0, y: 0}})
*
* obj.value // {position: {x: 0, y: 0}}
*
* // here, we only override position.x
* obj.initialValue = {position: {x: 2}}
*
* obj.value // {position: {x: 2, y: 0}}
* ```
*/
set initialValue(value: DeepPartialOfSerializableValue<this['value']>) set initialValue(value: DeepPartialOfSerializableValue<this['value']>)
} }

View file

@ -20,15 +20,55 @@ export type SheetObjectConfig<
> = Props > = Props
export interface ISheet { export interface ISheet {
/**
* All sheets have `sheet.type === 'Theatre_Sheet_PublicAPI'`
*/
readonly type: 'Theatre_Sheet_PublicAPI' readonly type: 'Theatre_Sheet_PublicAPI'
/**
* The Project this Sheet belongs to
*/
readonly project: IProject readonly project: IProject
/**
* The address of the Sheet
*/
readonly address: SheetAddress readonly address: SheetAddress
/**
* Creates a child object for the sheet
*
* **Docs: https://docs.theatrejs.com/in-depth/#objects**
*
* @param key Each object is identified by a key, which is a non-empty string
* @param props The props of the object. See examples
*
* @returns An Object
*
* @example
* ```ts
* // Create an object named "a unique key" with no props
* const obj = sheet.object("a unique key", {})
* obj.address.objectKey // "a unique key"
*
*
* // Create an object with {x: 0}
* const obj = sheet.object("obj", {x: 0})
* obj.value.x // returns 0 or the current number that the user has set
*
* // Create an object with nested props
* const obj = sheet.object("obj", {position: {x: 0, y: 0}})
* obj.value.position // {x: 0, y: 0}
* ```
*/
object<Props extends IShorthandCompoundProps>( object<Props extends IShorthandCompoundProps>(
key: string, key: string,
config: Props, props: Props,
): ISheetObject<Props> ): ISheetObject<Props>
/**
* The Sequence of this Sheet
*/
readonly sequence: ISequence readonly sequence: ISequence
} }

View file

@ -3,15 +3,32 @@ import type {
SerializableMap, SerializableMap,
SerializableValue, SerializableValue,
} from '@theatre/shared/utils/types' } from '@theatre/shared/utils/types'
/**
* Represents the address to a project
*/
export interface ProjectAddress { export interface ProjectAddress {
projectId: string projectId: string
} }
/**
* Represents the address to a specific instance of a Sheet
*
* ```ts
* const sheet = project.sheet('a sheet', 'some instance id')
* sheet.address.sheetId === 'a sheet'
* sheet.address.sheetInstanceId === 'sheetInstanceId'
* ```
*/
export interface SheetAddress extends ProjectAddress { export interface SheetAddress extends ProjectAddress {
sheetId: string sheetId: string
sheetInstanceId: string sheetInstanceId: string
} }
/**
* Removes `sheetInstanceId` from an address, making it refer to
* all instances of a certain `sheetId`
*/
export type WithoutSheetInstance<T extends SheetAddress> = Omit< export type WithoutSheetInstance<T extends SheetAddress> = Omit<
T, T,
'sheetInstanceId' 'sheetInstanceId'
@ -20,7 +37,18 @@ export type WithoutSheetInstance<T extends SheetAddress> = Omit<
export type SheetInstanceOptional<T extends SheetAddress> = export type SheetInstanceOptional<T extends SheetAddress> =
WithoutSheetInstance<T> & {sheetInstanceId?: string | undefined} WithoutSheetInstance<T> & {sheetInstanceId?: string | undefined}
/**
* Represents the address to a Sheet's Object
*/
export interface SheetObjectAddress extends SheetAddress { export interface SheetObjectAddress extends SheetAddress {
/**
* The key of the object.
*
* ```ts
* const obj = sheet.object('foo', {})
* obj.address.objectKey === 'foo'
* ```
*/
objectKey: string objectKey: string
} }
@ -34,6 +62,9 @@ export const encodePathToProp = (p: PathToProp): PathToProp_Encoded =>
export const decodePathToProp = (s: PathToProp_Encoded): PathToProp => export const decodePathToProp = (s: PathToProp_Encoded): PathToProp =>
JSON.parse(s) JSON.parse(s)
/**
* Represents the path to a certain prop of an object
*/
export interface PropAddress extends SheetObjectAddress { export interface PropAddress extends SheetObjectAddress {
pathToProp: PathToProp pathToProp: PathToProp
} }

View file

@ -110,38 +110,38 @@ const Connector: React.FC<IProps> = (props) => {
const modifiedS = orig // window.prompt('As cubic-bezier()', orig) const modifiedS = orig // window.prompt('As cubic-bezier()', orig)
if (modifiedS && modifiedS !== orig) { if (modifiedS && modifiedS !== orig) {
return return
const modified = JSON.parse(modifiedS) // const modified = JSON.parse(modifiedS)
getStudio()!.transaction(({stateEditors}) => { // getStudio()!.transaction(({stateEditors}) => {
const {replaceKeyframes} = // const {replaceKeyframes} =
stateEditors.coreByProject.historic.sheetsById.sequence // stateEditors.coreByProject.historic.sheetsById.sequence
replaceKeyframes({ // replaceKeyframes({
...props.leaf.sheetObject.address, // ...props.leaf.sheetObject.address,
snappingFunction: val(props.layoutP.sheet).getSequence() // snappingFunction: val(props.layoutP.sheet).getSequence()
.closestGridPosition, // .closestGridPosition,
trackId: props.leaf.trackId, // trackId: props.leaf.trackId,
keyframes: [ // keyframes: [
{ // {
...cur, // ...cur,
handles: [ // handles: [
cur.handles[0], // cur.handles[0],
cur.handles[1], // cur.handles[1],
modified[0], // modified[0],
modified[1], // modified[1],
], // ],
}, // },
{ // {
...next, // ...next,
handles: [ // handles: [
modified[2], // modified[2],
modified[3], // modified[3],
next.handles[2], // next.handles[2],
next.handles[3], // next.handles[3],
], // ],
}, // },
], // ],
}) // })
}) // })
} }
}} }}
style={{ style={{