UNPKG

@genart-api/core

Version:

Platform-independent extensible API for browser-based generative art

542 lines (541 loc) 21.3 kB
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; }