@genart-api/core
Version:
Platform-independent extensible API for browser-based generative art
542 lines (541 loc) • 21.3 kB
TypeScript
import type { MathOps } from "./api/math.js";
import type { APIMessage, MessageType, MessageTypeMap, NotifyType } from "./api/messages.js";
import type { ParamFactories, ParamImpl, ParamSpecs, ParamValue } from "./api/params.js";
import type { PlatformAdapter, RunMode } from "./api/platform.js";
import type { PRNG, PRNGBuiltins, RandomFn } from "./api/random.js";
import type { ScreenConfig } from "./api/screen.js";
import type { APIState } from "./api/state.js";
import type { TimeProvider, TimeProviders } from "./api/time.js";
import type { Traits } from "./api/traits.js";
import type { Utils } from "./api/utils.js";
export * from "./api/math.js";
export * from "./api/messages.js";
export * from "./api/params.js";
export * from "./api/platform.js";
export * from "./api/random.js";
export * from "./api/screen.js";
export * from "./api/state.js";
export * from "./api/time.js";
export * from "./api/traits.js";
export * from "./api/utils.js";
export type NumOrString = number | string;
export type Maybe<T> = T | undefined;
/**
* Animation update/draw function. See {@link GenArtAPI.setUpdate}. If the
* function returns false, the animation loop will be stopped (via
* {@link GenArtAPI.stop}), otherwise the loop continues until stopped
* explicitly.
*
* @remarks
* Arguments are information sourced from {@link TimeProvider.now}.
*
* @param time
* @param frame
*/
export type UpdateFn = (time: number, frame: number) => boolean;
/**
* Configuration options for {@link GenArtAPI.configure}.
*/
export interface GenArtAPIOpts {
/**
* See {@link GenArtAPI.id} for details.
*
* @defaultValue random string, generated at startup
*/
id: string;
/**
* If true, the API will accept {@link ConfigureMessage}s allowing the API
* behavior to be reconfigured by external tooling.
*
* @remarks
* For security reasons, this option is disabled and should only be enabled
* during development. Also see {@link GenArtAPI.configure} and {@link ConfigureMessage}.
*
* @example
* ```ts
* // enable re-configuration via IPC messages (e.g. sent from editors/tooling)
* $genart.configure({ allowExternalConfig: true });
*
* // using Vite.js, this will enable the option only for dev mode:
* $genart.configure({ allowExternalConfig: import.meta.env.DEV });
* ```
*
* @defaultValue false
*/
allowExternalConfig: boolean;
/**
* If true, the API will emit a {@link AnimFrameMessage} for each single
* frame update.
*
* @remarks
* This option is disabled by default, but can be enabled (e.g. by tooling
* interested in this information) by sending a {@link ConfigureMessage} to
* the GenArtAPI instance.
*
* @defaultValue false
*/
notifyFrameUpdate: boolean;
}
export interface GenArtAPI {
/**
* Unique ID for this GenArtAPI instance, intended for use cases where
* multiple `<iframe>` elements with genart pieces exist within a parent
* document and to allow sending messages to specific instances only.
*
* The value can be customized via {@link GenArtAPI.configure}.
*
* @remarks
* The ID will be part of any {@link APIMessage} sent and will also be
* checked by any `genart:...` message received. A message will only be
* processed if its {@link APIMessage.apiID} matches this value or if equal
* to the string `"*""`, i.e. the wildcard catch-all ID, which will be
* matched by any active `GenArtAPI` instance.
*
* Use cases for the wildcard ID (`"*"`) are related to managing multiple
* artworks running in multiple iframes (e.g. in an online gallery
* scenario), for example:
*
* - Detection/registration of all currently running `GenArtAPI` instances
* by broadcasting a {@link GetInfoMessage}, to which each instance then
* responds with a {@link InfoMessage} (which then also includes each
* instance's actual configured `id`)
* - Starting/stopping all currently running `GenArtAPI` instances via
* single message, e.g. `postMessage({ type: "genart:start", apiID: "*"
* }, "*")`.
*
* The initial ID value is auto-generated, but it's considered best practice
* to set it at startup of the artwork (via {@link GenArtAPI.configure}).
*/
readonly id: string;
/**
* API version in semantic versioning format (e.g. 0.12.3).
*
* @remarks
* This information can be used to implement API compatibility checks in
* artworks or platform adapters.
*/
readonly version: string;
/**
* Current deploy/run mode, proxy accessor for {@link PlatformAdapter.mode}.
*/
readonly mode: RunMode;
/**
* Proxy accessor for {@link PlatformAdapter.collector}.
*/
readonly collector?: string;
/**
* Proxy accessor for {@link PlatformAdapter.iteration}.
*/
readonly iteration?: number;
/**
* Returns the platform's configured screen/canvas dimensions & pixel
* density.
*
* @remarks
* Also see {@link ResizeMessage}.
*/
readonly screen: ScreenConfig;
/**
* Returns the platform's configured pseudo-random number generator.
*
* @remarks
* Please see related issue: https://github.com/thi-ng/genart-api/issues/1
*/
readonly random: PRNG;
/**
* The currently configured stringified random seed value, as
* defined/determined by the platform provider and used by the
* {@link GenArtAPI.random} PRNG. For information purposes only.
*/
readonly seed: string;
/**
* The API's current state.
*
* @remarks
* Also see {@link StateChangeMessage}.
*/
readonly state: APIState;
/**
* Readonly access to all parameter definitions declared by the artwork
* (incl. any additionally declared params defined by the platform adapter).
*
* @remarks
* Also see {@link PlatformAdapter.augmentParams}.
*/
readonly paramSpecs: Maybe<Readonly<ParamSpecs>>;
/** Readonly access to the registered {@link PlatformAdapter} */
readonly adapter: Maybe<PlatformAdapter>;
/** Readonly access to the registered {@link TimeProvider} */
readonly timeProvider: Maybe<TimeProvider>;
/** Built-in math utilities */
readonly math: MathOps;
/** Built-in parameter declaration functions */
readonly params: ParamFactories;
/** Built-in PRNG implementations */
readonly prng: PRNGBuiltins;
/** Built-in utilities */
readonly utils: Utils;
/** Built-in time provider factory functions */
readonly time: TimeProviders;
/**
* Registers a new parameter type and its implementation. Supports
* overriding default types, but prints a warning in the console when doing
* so...
*
* @remarks
* When a platform registers any own types, it SHOULD consider namespacing
* the the `type` name, e.g. `platformname:customtype`.
*
* See {@link ParamImpl} for implementation details. See
* {@link ParamFactories} for built-in parameter types.
*
* @param type
* @param impl
*/
registerParamType(type: string, impl: ParamImpl): void;
/**
* Returns the registered {@link ParamImpl} for given parameter type.
*
* @remarks
* See {@link GenArtAPI.registerParamType}.
*
* @param type
*/
paramType(type: string): Maybe<ParamImpl>;
/**
* Called during initialization of the artwork to declare all of its
* available parameters, their configurations and (optional) default values.
*
* @remarks
* This function assumes a {@link PlatformAdapter} has already been
* configured (via {@link GenArtAPI.setAdapter}). To ensure that's the case
* from the artwork's POV, the artwork should always begin with waiting for
* {@link GenArtAPI.waitForAdapter} before calling `setParams()`.
*
* This function first calls {@link PlatformAdapter.augmentParams} (if
* available) to allow for any additional platform-specific params to be
* injected, then validates all params, validates default values or defines
* random default values for those params with missing defaults (note:
* defaults are only used & checked for param types which have no
* {@link ParamImpl.read} method).
*
* If available, the function then calls and waits for
* {@link PlatformAdapter.initParams} to pre-initialize any
* platform-specific param handling and then calls
* {@link GenArtAPI.updateParams} to apply any param
* customizations/overrides sourced via the adapter. Finally, once done, it
* sends a {@link ParamsMessage} message to the current & parent window for
* other software components to be notified (e.g. param editors).
*
* The function is async and returns a promise of a typesafe getter function
* (based on the declared param specs) to obtain param values (wraps
* {@link GenArtAPI.getParamValue}). See that function's documentation for
* optional uses like obtaining randomized or time-based values.
*
* **Parameter IDs declared by the artwork MUST not start with `__`, which
* is a reserved prefix!**
*
* @example
* ```ts
* const param = await $genart.setParams({
* color: $genart.params.color({
* name: "brush color",
* desc: "base color for brush strokes",
* default: "#ffff00",
* }),
* size: $genart.params.range({
* name: "brush size",
* desc: "base brush size (normalized)",
* doc: "percentage of shortest canvas size",
* default: 10,
* min: 5,
* max: 50,
* }),
* density: $genart.params.ramp({
* name: "brush density",
* desc: "density distribution curve",
* stops: [[0, 0.5], [0.5, 0.5], [1, 1]],
* }),
* });
*
* // get possibly customized param values (typesafe)
* const color = param("color"); // inferred as string
* const size = param("size"); // inferred as number
*
* // get a randomized value
* // (within constraints & rules defined by the param)
* const randomSize = param("size", $genart.random.rnd);
*
* // some param types (e.g. ramp) can produce time-based values
* // (here `t` is in [0,1] range). the time arg defaults to 0 and
* // is ignored by other param types...
* const density = param("density", 0.5);
* ```
*
* @param params
*/
setParams<P extends ParamSpecs>(params: P): Promise<(<K extends keyof P>(id: K, timeOrRnd?: number | RandomFn) => ParamValue<P[K]>)>;
/**
* Sets the {@link PlatformAdapter} instance to use.
*
* @param adapter
*/
setAdapter(adapter: PlatformAdapter): void;
/**
* Artwork should call this function **prior to any other interaction** with
* the global `$genart` instance to wait for the {@link PlatformAdapter} to
* be ready.
*
* @example
* ```js
* await $genart.waitForAdapter();
* ```
*/
waitForAdapter(): Promise<PlatformAdapter>;
/**
* Sets the {@link TimeProvider} instance to use.
*
* @param time
*/
setTimeProvider(time: TimeProvider): void;
/**
* Artwork should call this function at start up to wait for the
* {@link TimeProvider} to be ready.
*
* @remarks
* The reference implementation of the {@link GenArtAPI} provides a number
* of default {@link TimeProviders}, so this call is not required here (but
* defined as option for other implementations)...
*/
waitForTimeProvider(): Promise<TimeProvider>;
/**
* Iterates over all registered parameters and calls
* {@link PlatformAdapter.updateParam} and {@link GenArtAPI.setParamValue}
* to apply any param customizations/overrides sourced via the adapter. If
* `notify` is given, sends a {@link ParamChangeMessage} for each changed
* param/value.
*
* @remarks
* By default, this function is only called via {@link GenArtAPI.setParams}
* and will NOT emit any param change messages.
*
* @param notify
*/
updateParams(notify?: NotifyType): Promise<void>;
/**
* Updates the given param's value, or if `key` is specified one its nested
* params' value, then emits a {@link ParamChangeMessage} (depending on
* `notify`, default: "all")
*
* @param id
* @param value
* @param key
* @param notify
*/
setParamValue(id: string, value: any, key?: string, notify?: NotifyType): void;
/**
* Triggers randomization of the given param's value, or if `key` is
* specified one its nested params. Only params which support randomization
* will be handled, otherwise silently ignored. If randomization succeeded,
* calls {@link GenArtAPI.setParamValue} to apply the new value and emit a
* {@link ParamChangeMessage} (depending on `notify`, default: "all").
*
* @remarks
* The optional `rnd` function is passed to {@link ParamImpl.randomize} to
* produce a new random value. The default is `Math.random`.
*
* In the reference implementation of {@link GenArtAPI}, this function can
* also be triggered via a {@link RandomizeParamMessage}.
*
* @param id
* @param key
* @param rnd
* @param notify
*/
randomizeParamValue(id: string, key?: string, rnd?: RandomFn, notify?: NotifyType): void;
/**
* Returns the value for previously registered parameter `id`, possibly
* time-based (if the param type supports such) or randomized (if a
* {@link RandomFn} is given and iff the param type supports randomization).
* A type-safe wrapper of this function (based on declared params) is
* returned by {@link GenArtAPI.setParams}.
*
* @remarks
* Also see {@link GenArtAPI.paramValueGetter} for an alternative.
*
* The following logic is used to produce a param's value:
*
* For non-randomized uses of `getParamValue()`, if a param type defines a
* {@link ParamImpl.read} function, it will take precedent and is called
* with given `time` (default: 0). Otherwise, the param's currently defined
* {@link Param.value} or {@link Param.default} will be returned.
*
* The the `timeOrRnd` arg defaults to zero and is only used if the param
* type supports time-based values, otherwise ignored. Of the built-in param
* types only {@link RampParam} uses time-based values.
*
* If the `timeOrRnd` arg is a {@link RandomFn}, it's only used if the param
* type also supports randomization. In that case `getParamValue()` will
* produce a randomized value using {@link ParamImpl.randomize}, but this
* value is ephemeral and will NOT modify the param's {@link Param.value}
* nor trigger a {@link RandomizeParamMessage} message being broadcast.
*
* **Important: It's the artist's responsibility to ensure deterministic
* behavior of an artwork/variation and if the `rnd` arg is used, most
* likely the currently configured {@link PRNG} function (aka
* `$genart.random.rnd`) SHOULD be used!**
*
* @param id
* @param timeOrRnd
*/
getParamValue<T extends ParamSpecs, K extends keyof T>(id: K, timeOrRnd?: number | RandomFn): ParamValue<T[K]>;
/**
* Higher order version of {@link GenArtAPI.getParamValue}. Returns a
* pre-defined function to access the param for given `id` and is faster
* than `getParamValue()`, since internal validations will only be performed
* once.
*
* @example
* ```ts
* // (assuming a prior call to $genart.setParams() already happened...)
*
* // create a pre-validated getter for param "A"
* const paramA = $genart.paramValueGetter("A");
*
* // get A's current value
* const valueA = paramA();
*
* // get A's value at time = 1000 (only for time-based params)
* const valueA = paramA(1000);
*
* // get a randomized value of A (without changing it's stored value)
* const valueA = paramA($genart.random.rnd);
* ```
*
* @param id
*/
paramValueGetter<T extends ParamSpecs, K extends keyof T>(id: K): (opt?: number | RandomFn) => ParamValue<T[K]>;
/**
* Emits a {@link ParamErrorMessage} message (called from
* {@link GenArtAPI.setParamValue}, if needed, but can be triggered by
* others too...)
*
* @param id
*/
paramError(id: string): void;
/**
* (Optionally) Called by the artwork to declare an object of "traits" (aka
* generated metadata) which should be exposed to the platform or
* viewers/collectors, e.g. to compute the rarity of a variation. The keys
* in this object are trait/feature names, their values can be arbitrary
* strings, numbers or booleans.
*
* @remarks
* Usually these traits are derived from the random seed and currently
* configured parameters. The API will forward this object to
* {@link PlatformAdapter.setTraits} for platform-specific processing, but
* also emits a {@link TraitsMessage} message to the current & parent
* windows.
*
* @example
* ```ts
* $genart.setTraits({
* mood: "bright",
* shapes: "triangles",
* density: 100,
* slowmotion: true
* });
* ```
*
* @param traits
*/
setTraits(traits: Traits): void;
/**
* Configures optional behavior of the API. See {@link GenArtAPIOpts} and
* {@link ConfigureMessage} for details.
*
* @param opts
*/
configure(opts: Partial<GenArtAPIOpts>): void;
/**
* Registers a handler/listener for given message `type`.
*
* @remarks
* This is just facade for `window.addListener("message",...)` which also
* handles pre-filtering of received messages and therefore reduces
* boilerplate in individual listeners. Messages not conforming to the
* overall {@link APIMessage} format, or which aren't addressed to the
* configured {@link GenArtAPI.id} or not matching the message `type` will
* be ignored.
*
* @param type
* @param listener
*/
on<T extends MessageType>(type: T, listener: (e: MessageTypeMap[T]) => void): void;
/**
* Sends an {@link APIMessage} using (optionally) specified `notify` type
* (default: "all").
*
* @remarks
* Auto-injects {@link GenArtAPI.id} as {@link APIMessage.apiID} to support
* differentiation of multiple `GenArtAPI` instances running concurrently
* (in different windows/iframes).
*
* If `notify` is `none`, no message will be emitted (default: "all"). See
* {@link NotifyType} for possible values.
*
* @param e
* @param notify
*/
emit<T extends APIMessage>(e: Omit<T, "apiID">, notify?: NotifyType): void;
/**
* Called from artwork to register the frame loop/update function.
*
* @remarks
* If both platform adapter and time provider are already known, this will
* trigger the GenArtAPI to go into the `ready` state and emit a
* {@link StateChangeMessage} message. In most cases, a platform adapter
* should react to this message and call {@link GenArtAPI.start} to trigger
* auto-playback of the artwork when `ready` state is entered.
*
* @param fn
*/
setUpdate(fn: UpdateFn): void;
/**
* Starts (or resumes) playback of the artwork by triggering an animation
* loop using the configured update function (also involving the configured
* {@link TimeProvider}). Emits a `genart:start` or `genart:resume` message.
*
* @remarks
* By default `resume` is false, meaning the time provider will be
* (re)initialized (otherwise just continues).
*
* Triggers the API to go into `play` state and emits a
* {@link StateChangeMessage}, as well as `genart:start` or `genart:resume`
* messages. Function is idempotent if API is already in `play` state.
*
* An error will be thrown if API is not in `ready` or `stop` state, i.e.
* the API must have a {@link PlatformAdapter}, a {@link TimeProvider} and a
* {@link UpdateFn} must have been configured.
*
* Whilst the animation loop is active, a {@link AnimFrameMessage} will be
* emitted at the end of each frame update. These messages contain the time
* & frame information of the currently rendered frame and are intended for
* 3rd party tooling (i.e. editors, players, sequencers).
*
* @param resume
*/
start(resume?: boolean): void;
/**
* Stops the artwork animation loop
*/
stop(): void;
/**
* Callback to trigger capturing (aka preview/thumbnail generation) of the
* art piece (optionally given canvas or SVG element). Usually called by the
* art piece when ready to capture...
*
* @param el
*/
capture(el?: HTMLCanvasElement | SVGElement): void;
}