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
*/
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
}

View file

@ -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 extends IShorthandCompoundProps>(
props: Props,
extras?: PropTypeConfigExtras,
opts?: PropTypeConfigOpts,
): PropTypeConfig_Compound<
ShorthandCompoundPropsToLonghandCompoundProps<Props>
> => {
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<Opts extends {[key in string]: string}>(
defaultValue: Extract<keyof Opts, string>,
options: Opts,
extras?: {as?: 'menu' | 'switch'} & PropTypeConfigExtras,
opts?: {as?: 'menu' | 'switch'} & PropTypeConfigOpts,
): PropTypeConfig_StringLiteral<Extract<keyof Opts, string>> {
return {
type: 'stringLiteral',
@ -130,24 +318,11 @@ export function stringLiteral<Opts extends {[key in string]: string}>(
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: ValueType
[propTypeSymbol]: 'TheatrePropType'
@ -190,7 +365,7 @@ export interface PropTypeConfig_Boolean extends IBasePropType<boolean> {
default: boolean
}
export interface PropTypeConfigExtras {
export interface PropTypeConfigOpts {
label?: string
}
export interface PropTypeConfig_String extends IBasePropType<string> {
@ -206,12 +381,6 @@ export interface PropTypeConfig_StringLiteral<T extends string>
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>
}
// 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<string, PropTypeConfig>
@ -238,7 +401,6 @@ export type PropTypeConfig_AllPrimitives =
| PropTypeConfig_Boolean
| PropTypeConfig_String
| PropTypeConfig_StringLiteral<$IntentionalAny>
// | PropTypeConfig_CSSRGBA
export type PropTypeConfig =
| PropTypeConfig_AllPrimitives

View file

@ -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<void>
}

View file

@ -18,20 +18,82 @@ import type {
} from '@theatre/core/propTypes/internals'
export interface ISheetObject<Props extends IShorthandCompoundProps = {}> {
/**
* 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<Props>['valueType']
/**
* A Pointer to the props of the object.
*
* More documentation soon.
*/
readonly props: Pointer<this['value']>
/**
* 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<this['value']>)
}

View file

@ -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<Props extends IShorthandCompoundProps>(
key: string,
config: Props,
props: Props,
): ISheetObject<Props>
/**
* The Sequence of this Sheet
*/
readonly sequence: ISequence
}

View file

@ -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<T extends SheetAddress> = Omit<
T,
'sheetInstanceId'
@ -20,7 +37,18 @@ export type WithoutSheetInstance<T extends SheetAddress> = Omit<
export type SheetInstanceOptional<T extends SheetAddress> =
WithoutSheetInstance<T> & {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
}

View file

@ -110,38 +110,38 @@ const Connector: React.FC<IProps> = (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={{