From 7815fb2dc3c80f5adba38c0ff61f22d25db3b45a Mon Sep 17 00:00:00 2001 From: Aria Minaei Date: Fri, 17 Sep 2021 14:58:26 +0200 Subject: [PATCH] More API docs --- theatre/core/src/projects/TheatreProject.ts | 9 + theatre/core/src/propTypes/index.ts | 284 ++++++++++++++---- theatre/core/src/sequences/TheatreSequence.ts | 46 ++- .../src/sheetObjects/TheatreSheetObject.ts | 64 +++- theatre/core/src/sheets/TheatreSheet.ts | 42 ++- theatre/shared/src/utils/addresses.ts | 31 ++ .../KeyframeEditor/Connector.tsx | 62 ++-- 7 files changed, 442 insertions(+), 96 deletions(-) diff --git a/theatre/core/src/projects/TheatreProject.ts b/theatre/core/src/projects/TheatreProject.ts index c7639e1..1828ba2 100644 --- a/theatre/core/src/projects/TheatreProject.ts +++ b/theatre/core/src/projects/TheatreProject.ts @@ -38,6 +38,15 @@ export interface IProject { * The project's address */ 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 } diff --git a/theatre/core/src/propTypes/index.ts b/theatre/core/src/propTypes/index.ts index f7e31ab..45b62ac 100644 --- a/theatre/core/src/propTypes/index.ts +++ b/theatre/core/src/propTypes/index.ts @@ -1,4 +1,5 @@ import type {$IntentionalAny} from '@theatre/shared/utils/types' +import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue' import type { IShorthandCompoundProps, IValidCompoundProps, @@ -7,48 +8,123 @@ import type { import {sanitizeCompoundProps} 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: * ```ts - * // the root prop type of an object is always a compound - * const props = { - * // compounds can be nested - * position: t.compound({ - * x: t.number(0), - * y: t.number(0) - * }) + * // shorthand + * const position = { + * x: 0, + * y: 0 * } + * assert(sheet.object('some object', position).value.x === 0) * - * const obj = sheet.obj('key', props) - * console.log(obj.value) // {position: {x: 10.3, y: -1}} + * // nesting + * 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 extras + * @param opts * @returns * */ export const compound = ( props: Props, - extras?: PropTypeConfigExtras, + opts?: PropTypeConfigOpts, ): PropTypeConfig_Compound< ShorthandCompoundPropsToLonghandCompoundProps > => { + validateCommonOpts('t.compound(props, opts)', opts) return { type: 'compound', props: sanitizeCompoundProps(props), valueType: null as $IntentionalAny, [propTypeSymbol]: 'TheatrePropType', - label: extras?.label, + label: opts?.label, } } /** + * A number prop type. * - * @param defaultValue - * @param opts - * @returns + * Usage + * ```ts + * // 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 = ( defaultValue: number, @@ -56,8 +132,60 @@ export const number = ( nudgeFn?: PropTypeConfig_Number['nudgeFn'] range?: PropTypeConfig_Number['range'] nudgeMultiplier?: number - } & PropTypeConfigExtras, + } & PropTypeConfigOpts, ): 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 { type: 'number', valueType: 0, @@ -72,57 +200,117 @@ export const number = ( } /** + * A boolean prop type * - * @param defaultValue - * @param extras - * @returns + * Usage: + * ```ts + * // 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 = ( defaultValue: boolean, - extras?: PropTypeConfigExtras, + opts?: PropTypeConfigOpts, ): 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 { type: 'boolean', default: defaultValue, valueType: null as $IntentionalAny, [propTypeSymbol]: 'TheatrePropType', - label: extras?.label, + label: opts?.label, } } /** + * A string prop type * - * @param defaultValue - * @param extras - * @returns + * Usage: + * ```ts + * // 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 = ( defaultValue: string, - extras?: PropTypeConfigExtras, + opts?: PropTypeConfigOpts, ): 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 { type: 'string', default: defaultValue, valueType: null as $IntentionalAny, [propTypeSymbol]: 'TheatrePropType', - label: extras?.label, + label: opts?.label, } } /** + * A stringLiteral prop type, useful for building menus or radio buttons. * - * @param defaultValue - * @param options - * @param extras - * @returns + * Usage: + * ```ts + * // Basic usage + * 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( defaultValue: Extract, options: Opts, - extras?: {as?: 'menu' | 'switch'} & PropTypeConfigExtras, + opts?: {as?: 'menu' | 'switch'} & PropTypeConfigOpts, ): PropTypeConfig_StringLiteral> { return { type: 'stringLiteral', @@ -130,24 +318,11 @@ export function stringLiteral( options: {...options}, [propTypeSymbol]: 'TheatrePropType', valueType: null as $IntentionalAny, - as: extras?.as ?? 'menu', - label: extras?.label, + as: opts?.as ?? 'menu', + 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: ValueType [propTypeSymbol]: 'TheatrePropType' @@ -190,7 +365,7 @@ export interface PropTypeConfig_Boolean extends IBasePropType { default: boolean } -export interface PropTypeConfigExtras { +export interface PropTypeConfigOpts { label?: string } export interface PropTypeConfig_String extends IBasePropType { @@ -206,12 +381,6 @@ export interface PropTypeConfig_StringLiteral 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: Record } -// 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<{}> { type: 'enum' cases: Record @@ -238,7 +401,6 @@ export type PropTypeConfig_AllPrimitives = | PropTypeConfig_Boolean | PropTypeConfig_String | PropTypeConfig_StringLiteral<$IntentionalAny> -// | PropTypeConfig_CSSRGBA export type PropTypeConfig = | PropTypeConfig_AllPrimitives diff --git a/theatre/core/src/sequences/TheatreSequence.ts b/theatre/core/src/sequences/TheatreSequence.ts index d161902..c752bc6 100644 --- a/theatre/core/src/sequences/TheatreSequence.ts +++ b/theatre/core/src/sequences/TheatreSequence.ts @@ -28,6 +28,32 @@ export interface ISequence { * Starts playback of a sequence. * 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()) + * + * @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?: { /** @@ -57,13 +83,29 @@ export interface ISequence { /** * 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 /** + * 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 } diff --git a/theatre/core/src/sheetObjects/TheatreSheetObject.ts b/theatre/core/src/sheetObjects/TheatreSheetObject.ts index 6c0e777..bae1983 100644 --- a/theatre/core/src/sheetObjects/TheatreSheetObject.ts +++ b/theatre/core/src/sheetObjects/TheatreSheetObject.ts @@ -18,20 +18,82 @@ import type { } from '@theatre/core/propTypes/internals' export interface ISheetObject { + /** + * All Objects will have `object.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['valueType'] + + /** + * A Pointer to the props of the object. + * + * More documentation soon. + */ readonly props: Pointer + /** + * The instance of Sheet the Object belongs to + */ readonly sheet: ISheet + + /** + * The Project the project belongs to + */ readonly project: IProject + + /** + * An object representing the address of the Object + */ 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 - // 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) } diff --git a/theatre/core/src/sheets/TheatreSheet.ts b/theatre/core/src/sheets/TheatreSheet.ts index 8a0bd54..76469fd 100644 --- a/theatre/core/src/sheets/TheatreSheet.ts +++ b/theatre/core/src/sheets/TheatreSheet.ts @@ -20,15 +20,55 @@ export type SheetObjectConfig< > = Props export interface ISheet { + /** + * All sheets have `sheet.type === 'Theatre_Sheet_PublicAPI'` + */ readonly type: 'Theatre_Sheet_PublicAPI' + + /** + * The Project this Sheet belongs to + */ readonly project: IProject + + /** + * The address of the Sheet + */ 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( key: string, - config: Props, + props: Props, ): ISheetObject + /** + * The Sequence of this Sheet + */ readonly sequence: ISequence } diff --git a/theatre/shared/src/utils/addresses.ts b/theatre/shared/src/utils/addresses.ts index b9d4977..78952fc 100644 --- a/theatre/shared/src/utils/addresses.ts +++ b/theatre/shared/src/utils/addresses.ts @@ -3,15 +3,32 @@ import type { SerializableMap, SerializableValue, } from '@theatre/shared/utils/types' + +/** + * Represents the address to a project + */ export interface ProjectAddress { 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 { sheetId: string sheetInstanceId: string } +/** + * Removes `sheetInstanceId` from an address, making it refer to + * all instances of a certain `sheetId` + */ export type WithoutSheetInstance = Omit< T, 'sheetInstanceId' @@ -20,7 +37,18 @@ export type WithoutSheetInstance = Omit< export type SheetInstanceOptional = WithoutSheetInstance & {sheetInstanceId?: string | undefined} +/** + * Represents the address to a Sheet's Object + */ export interface SheetObjectAddress extends SheetAddress { + /** + * The key of the object. + * + * ```ts + * const obj = sheet.object('foo', {}) + * obj.address.objectKey === 'foo' + * ``` + */ objectKey: string } @@ -34,6 +62,9 @@ export const encodePathToProp = (p: PathToProp): PathToProp_Encoded => export const decodePathToProp = (s: PathToProp_Encoded): PathToProp => JSON.parse(s) +/** + * Represents the path to a certain prop of an object + */ export interface PropAddress extends SheetObjectAddress { pathToProp: PathToProp } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Connector.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Connector.tsx index 9e47ec3..0fa8193 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Connector.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Connector.tsx @@ -110,38 +110,38 @@ const Connector: React.FC = (props) => { const modifiedS = orig // window.prompt('As cubic-bezier()', orig) if (modifiedS && modifiedS !== orig) { return - const modified = JSON.parse(modifiedS) - getStudio()!.transaction(({stateEditors}) => { - const {replaceKeyframes} = - stateEditors.coreByProject.historic.sheetsById.sequence + // const modified = JSON.parse(modifiedS) + // getStudio()!.transaction(({stateEditors}) => { + // const {replaceKeyframes} = + // stateEditors.coreByProject.historic.sheetsById.sequence - replaceKeyframes({ - ...props.leaf.sheetObject.address, - snappingFunction: val(props.layoutP.sheet).getSequence() - .closestGridPosition, - trackId: props.leaf.trackId, - keyframes: [ - { - ...cur, - handles: [ - cur.handles[0], - cur.handles[1], - modified[0], - modified[1], - ], - }, - { - ...next, - handles: [ - modified[2], - modified[3], - next.handles[2], - next.handles[3], - ], - }, - ], - }) - }) + // replaceKeyframes({ + // ...props.leaf.sheetObject.address, + // snappingFunction: val(props.layoutP.sheet).getSequence() + // .closestGridPosition, + // trackId: props.leaf.trackId, + // keyframes: [ + // { + // ...cur, + // handles: [ + // cur.handles[0], + // cur.handles[1], + // modified[0], + // modified[1], + // ], + // }, + // { + // ...next, + // handles: [ + // modified[2], + // modified[3], + // next.handles[2], + // next.handles[3], + // ], + // }, + // ], + // }) + // }) } }} style={{