From 35b627d7380bad75d280cc1e051ec7ed23aa8995 Mon Sep 17 00:00:00 2001 From: Karsten Schmidt Date: Sun, 1 Dec 2024 18:47:47 +0100 Subject: [PATCH] feat: add/update message types & handling BREAKING CHANGE: update/rename message types (and message `type` IDs) - add support to configure API via messages - add `ConfigureMessage` type and handling - add `InfoMessage` and `GetInfoMessage` types & handling - unify naming of all message types (i.e. `Msg` suffix => `Message`) - update message names to be more reflective of usage - remove `set` from message types which are only informative - rename `genart:setparams` => `genart:params` (new type: `ParamsMessage`) - rename `genart:settraits` => `genart:traits` (new type: `TraitsMessage`) - use hyphenation in message `type` IDs - e.g. `genart:paramchange` => `genart:param-change` - add support for `*` wildcard `apiID` in messages - add/update docs --- src/adapters/urlparams.ts | 8 +-- src/api.ts | 62 +++++++++++++++------- src/api/messages.ts | 109 +++++++++++++++++++++++++++++--------- src/api/params.ts | 4 +- src/api/platform.ts | 2 +- src/index.ts | 79 ++++++++++++++++++--------- 6 files changed, 187 insertions(+), 77 deletions(-) diff --git a/src/adapters/urlparams.ts b/src/adapters/urlparams.ts index efb931e..cd5c0b1 100644 --- a/src/adapters/urlparams.ts +++ b/src/adapters/urlparams.ts @@ -7,7 +7,7 @@ import type { PRNG, RampParam, RangeParam, - ResizeMsg, + ResizeMessage, RunMode, ScreenConfig, } from "../api.js"; @@ -38,7 +38,7 @@ class URLParamsAdapter implements PlatformAdapter { this.params = new URLSearchParams(location.search); this._screen = this.screen; this.initPRNG(); - $genart.on("genart:paramchange", async (e) => { + $genart.on("genart:param-change", async (e) => { const value = await this.serializeParam(e.param); this.params.set(e.paramID, value); // (optional) send updated params to parent GUI for param editing @@ -54,7 +54,7 @@ class URLParamsAdapter implements PlatformAdapter { location.search = this.params.toString(); } }); - $genart.on("genart:statechange", ({ state }) => { + $genart.on("genart:state-change", ({ state }) => { if (state === "ready" && this.params.get(AUTO) !== "0") { $genart.start(); } @@ -68,7 +68,7 @@ class URLParamsAdapter implements PlatformAdapter { dpr !== newScreen.dpr ) { this._screen = newScreen; - $genart.emit({ + $genart.emit({ type: "genart:resize", screen: newScreen, }); diff --git a/src/api.ts b/src/api.ts index 9cb58da..6c11fa2 100644 --- a/src/api.ts +++ b/src/api.ts @@ -53,7 +53,17 @@ export interface GenArtAPIOpts { */ id: string; /** - * If true (default), the API will emit a {@link AnimFrameMsg} for each + * If true, the API will accept {@link SetConfigMessage}s allowing the API + * behavior to be reconfigured by external tooling. + * + * @remarks + * For security reasons, this should only be enabled during development. + * + * @defaultValue false + */ + allowExternalConfig: boolean; + /** + * If true (default), the API will emit a {@link AnimFrameMessage} for each * single frame update. * * @defaultValue true @@ -72,10 +82,24 @@ export interface GenArtAPI { * @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. - * - * The initial value is auto-generated, but it's recommended to set it at - * startup of the artwork (via {@link GenArtAPI.configure}). + * 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 handling multiple + * artworks running in a page/app, regardless if they're sharing the same + * document or in multiple iframes, 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; @@ -113,7 +137,7 @@ export interface GenArtAPI { * The API's current state. * * @remarks - * Also see {@link StateChangeMsg}. + * Also see {@link StateChangeMessage}. */ readonly state: APIState; @@ -184,7 +208,7 @@ export interface GenArtAPI { * 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 SetParamsMsg} message to the current & parent window for + * sends a {@link ParamsMessage} message to the current & parent window for * other software components to be notified (e.g. param editors) * * The function returns a promise of a typesafe getter function (based on @@ -285,7 +309,7 @@ export interface GenArtAPI { * 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 ParamChangeMsg} for each changed + * `notify` is given, sends a {@link ParamChangeMessage} for each changed * param/value. * * @remarks @@ -298,7 +322,7 @@ export interface GenArtAPI { /** * Updates the given param's value, or if `key` is specified one its nested - * params' value, then emits a {@link ParamChangeMsg} (depending on + * params' value, then emits a {@link ParamChangeMessage} (depending on * `notify`, default: "all") * * @param id @@ -318,14 +342,14 @@ export interface GenArtAPI { * 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 ParamChangeMsg} (depending on `notify`, default: "all"). + * {@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 RandomizeParamMsg}. + * also be triggered via a {@link RandomizeParamMessage}. * * @param id * @param key @@ -362,8 +386,8 @@ export interface GenArtAPI { * that's the case and `rnd` is given, `getParamValue()` will produce a * randomized value using {@link ParamImpl.randomize}, but this value is * ephemeral and will NOT modify the param spec's `.value` or trigger a - * {@link RandomizeParamMsg} message being broadcast. If `rnd` is given but - * the param type does NOT support randomization, the param's value is + * {@link RandomizeParamMessage} message being broadcast. If `rnd` is given + * but the param type does NOT support randomization, the param's value is * produced normally (see above). * * **Important: It's the artist's responsibility to ensure deterministic @@ -382,7 +406,7 @@ export interface GenArtAPI { ): ParamValue; /** - * Emits a {@link ParamErrorMsg} message (called from + * Emits a {@link ParamErrorMessage} message (called from * {@link GenArtAPI.setParamValue}, if needed, but can be triggered by * others too...) * @@ -401,7 +425,7 @@ export interface GenArtAPI { * 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 SetTraitsMsg} message to the current & parent + * also emits a {@link TraitsMessage} message to the current & parent * windows. * * @example @@ -468,8 +492,8 @@ export interface GenArtAPI { * @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 StateChangeMsg} message. In most cases, a platform adapter should - * react to this message and call {@link GenArtAPI.start} to trigger + * {@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 @@ -486,14 +510,14 @@ export interface GenArtAPI { * (re)initialized (otherwise just continues). * * Triggers the API to go into `play` state and emits a - * {@link StateChangeMsg}, as well as `genart:start` or `genart:resume` + * {@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 AnimFrameMsg} will be + * 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). diff --git a/src/api/messages.ts b/src/api/messages.ts index 9f9b46f..8372fdf 100644 --- a/src/api/messages.ts +++ b/src/api/messages.ts @@ -1,3 +1,4 @@ +import type { GenArtAPIOpts } from "../api.js"; import type { NestedParam, NestedParamSpecs } from "./params.js"; import type { ScreenConfig } from "./screen.js"; import type { APIState } from "./state.js"; @@ -16,7 +17,11 @@ export interface APIMessage { * see {@link GenArtAPI.id}. */ apiID: string; - /** @internal */ + /** + * Flag used to indicate the message was emitted by the same instance. + * + * @internal + */ __self?: boolean; } @@ -24,8 +29,8 @@ export interface APIMessage { * Message type emitted by {@link GenArtAPI.setTraits} to inform external * tooling about artwork defined {@link Traits}. */ -export interface SetTraitsMsg extends APIMessage { - type: "genart:settraits"; +export interface TraitsMessage extends APIMessage { + type: "genart:traits"; traits: Traits; } @@ -33,8 +38,8 @@ export interface SetTraitsMsg extends APIMessage { * Message type emitted at the end of {@link GenArtAPI.setParams} to inform * external tooling about artwork defined {@link ParamSpecs}. */ -export interface SetParamsMsg extends APIMessage { - type: "genart:setparams"; +export interface ParamsMessage extends APIMessage { + type: "genart:params"; params: NestedParamSpecs; } @@ -42,8 +47,8 @@ export interface SetParamsMsg extends APIMessage { * Command message type received by {@link GenArtAPI} to remotely trigger * {@link GenArtAPI.setParamValue}. */ -export interface SetParamValueMsg extends APIMessage { - type: "genart:setparamvalue"; +export interface SetParamValueMessage extends APIMessage { + type: "genart:set-param-value"; /** * ID of parameter to update. */ @@ -62,8 +67,8 @@ export interface SetParamValueMsg extends APIMessage { * Command message type received by {@link GenArtAPI} to remotely trigger * {@link GenArtAPI.randomizeParamValue}. */ -export interface RandomizeParamMsg extends APIMessage { - type: "genart:randomizeparam"; +export interface RandomizeParamMessage extends APIMessage { + type: "genart:randomize-param"; /** * ID of parameter to randomize. */ @@ -79,8 +84,8 @@ export interface RandomizeParamMsg extends APIMessage { * Message type emitted by {@link GenArtAPI.setParamValue} when a parameter has * been changed/updated. */ -export interface ParamChangeMsg extends APIMessage { - type: "genart:paramchange"; +export interface ParamChangeMessage extends APIMessage { + type: "genart:param-change"; param: NestedParam; paramID: string; /** @@ -94,8 +99,8 @@ export interface ParamChangeMsg extends APIMessage { * Message type emitted by {@link GenArtAPI.setParamValue} if the given value is * not valid or the param couldn't be updated for any other reason. */ -export interface ParamErrorMsg extends APIMessage { - type: "genart:paramerror"; +export interface ParamErrorMessage extends APIMessage { + type: "genart:param-error"; paramID: string; error?: string; } @@ -104,8 +109,8 @@ export interface ParamErrorMsg extends APIMessage { * Message type emitted when the {@link GenArtAPI} internally switches into a * new state. See {@link APIState} for details. */ -export interface StateChangeMsg extends APIMessage { - type: "genart:statechange"; +export interface StateChangeMessage extends APIMessage { + type: "genart:state-change"; /** * New API state */ @@ -121,7 +126,7 @@ export interface StateChangeMsg extends APIMessage { * change occurred and the artwork (or 3rd party tooling) should respond/adapt * to these new dimensions provided. */ -export interface ResizeMsg extends APIMessage { +export interface ResizeMessage extends APIMessage { type: "genart:resize"; /** * New screen/canvas configuration @@ -135,7 +140,7 @@ export interface ResizeMsg extends APIMessage { * of the currently rendered frame and is intended for 3rd party tooling (i.e. * editors, players, sequencers). */ -export interface AnimFrameMsg extends APIMessage { +export interface AnimFrameMessage extends APIMessage { type: "genart:frame"; /** * Current animation time (in seconds) @@ -175,6 +180,55 @@ export interface StopMessage extends APIMessage { type: "genart:stop"; } +/** + * Message type sent when {@link GenArtAPI.configure} is called or a + * {@link ConfigureMessage} or {@link GetInfoMessage} is received by the API. + * Includes all current config options, API state, timing info, + * {@link GenArtAPI.version} and more. + */ +export interface InfoMessage extends APIMessage { + type: "genart:info"; + opts: GenArtAPIOpts; + state: APIState; + version: string; + /** + * Current animation time (in milliseconds). See {@link TimeProvider.now}. + */ + time: number; + /** + * Current animation frame number. See {@link TimeProvider.now}. + */ + frame: number; + /** + * Random seed used by this instance's {@link PRNG}. + */ + seed: string; +} + +/** + * Command message type received by {@link GenArtAPI} to trigger an + * {@link InfoMessage} being sent in response. + */ +export interface GetInfoMessage extends APIMessage { + type: "genart:get-info"; +} + +/** + * Command message type received by {@link GenArtAPI}. Only if the + * {@link GenArtAPIOpts.allowExternalConfig} option is enabled, the message + * payload's options are passed to {@link GenArtAPI.configure}, which then + * results in a {@link InfoMessage} being sent in response. + * + * @remarks + * For security reasons, the {@link GenArtAPIOpts.id} and + * {@link GenArtAPIOpts.allowExternalConfig} options cannot be changed + * themselves using this mechanism. + */ +export interface ConfigureMessage extends APIMessage { + type: "genart:configure"; + opts: Partial>; +} + /** * LUT mapping message types (names) to their respective type of API message. * Used for type checking/inference in {@link GenArtAPI.on}. @@ -182,18 +236,21 @@ export interface StopMessage extends APIMessage { export interface MessageTypeMap { "genart:capture": CaptureMessage; // "genart:capturerequest": APIMessage; - "genart:paramchange": ParamChangeMsg; - "genart:paramerror": ParamErrorMsg; - "genart:randomizeparam": RandomizeParamMsg; - "genart:frame": AnimFrameMsg; - "genart:resize": ResizeMsg; + "genart:configure": ConfigureMessage; + "genart:frame": AnimFrameMessage; + "genart:get-info": GetInfoMessage; + "genart:info": InfoMessage; + "genart:param-change": ParamChangeMessage; + "genart:param-error": ParamErrorMessage; + "genart:randomize-param": RandomizeParamMessage; + "genart:resize": ResizeMessage; "genart:resume": ResumeMessage; - "genart:setparams": SetParamsMsg; - "genart:setparamvalue": SetParamValueMsg; - "genart:settraits": SetTraitsMsg; + "genart:params": ParamsMessage; + "genart:set-param-value": SetParamValueMessage; "genart:start": StartMessage; - "genart:statechange": StateChangeMsg; + "genart:state-change": StateChangeMessage; "genart:stop": StopMessage; + "genart:traits": TraitsMessage; } /** diff --git a/src/api/params.ts b/src/api/params.ts index 5d33f3c..423cf98 100644 --- a/src/api/params.ts +++ b/src/api/params.ts @@ -43,7 +43,7 @@ export interface ParamOpts { * * - `reload`: the piece should be reloaded/relaunched with new param value * (platform providers are responsible to honor & implement this) - * - `event`: the API will trigger a {@link ParamChangeMsg} via + * - `event`: the API will trigger a {@link ParamChangeMessage} via * {@link GenArtAPI.emit} * * @defaultValue "event" @@ -289,7 +289,7 @@ export interface ParamImpl { * @remarks * If this validator returns false, the param update will be terminated * immediately and {@link GenArtAPI.setParamValue} will emit a - * {@link ParamErrorMsg} message (by default). + * {@link ParamErrorMessage} message (by default). * * @param spec * @param value diff --git a/src/api/platform.ts b/src/api/platform.ts index 01adf22..d060c18 100644 --- a/src/api/platform.ts +++ b/src/api/platform.ts @@ -89,7 +89,7 @@ export interface PlatformAdapter { * If this function returned a `value` and/or `update`, and if the retured * value(s) passed param type-specific validation (see * {@link ParamImpl.validate}), then by default - * {@link GenArtAPI.setParamValue} emits a {@link ParamChangeMsg} message + * {@link GenArtAPI.setParamValue} emits a {@link ParamChangeMessage} message * with the updated param spec. * * @param id diff --git a/src/index.ts b/src/index.ts index 0d02298..b7779bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import type { - AnimFrameMsg, + AnimFrameMessage, APIMessage, APIState, CaptureMessage, @@ -7,6 +7,7 @@ import type { GenArtAPI, GenArtAPIOpts, ImageParam, + InfoMessage, Maybe, MessageType, MessageTypeMap, @@ -14,9 +15,10 @@ import type { NestedParamSpecs, NotifyType, Param, - ParamChangeMsg, - ParamErrorMsg, + ParamChangeMessage, + ParamErrorMessage, ParamImpl, + ParamsMessage, ParamSpecs, ParamValue, PlatformAdapter, @@ -25,14 +27,13 @@ import type { RandomFn, RangeParam, ResumeMessage, - SetParamsMsg, - SetTraitsMsg, StartMessage, - StateChangeMsg, + StateChangeMessage, StopMessage, TextParam, TimeProvider, Traits, + TraitsMessage, UpdateFn, WeightedChoiceParam, } from "./api.js"; @@ -59,6 +60,7 @@ class API implements GenArtAPI { protected _opts: GenArtAPIOpts = { // auto-generated instance ID id: Math.floor(Math.random() * 1e12).toString(36), + allowExternalConfig: false, notifyFrameUpdate: true, }; @@ -264,20 +266,31 @@ class API implements GenArtAPI { const data = e.data; if (!this.isRecipient(e) || data?.__self) return; switch (data.type) { - case "genart:start": - this.start(); + case "genart:get-info": + this.notifyInfo(); + break; + case "genart:randomize-param": + this.randomizeParamValue(data.paramID, data.key); break; case "genart:resume": this.start(true); break; - case "genart:stop": - this.stop(); + case "genart:configure": { + const opts = data.opts; + delete opts.id; + delete opts.allowExternalConfig; + this.configure(opts); break; - case "genart:setparamvalue": + } + case "genart:set-param-value": this.setParamValue(data.paramID, data.value, data.key); break; - case "genart:randomizeparam": - this.randomizeParamValue(data.paramID, data.key); + case "genart:start": + this.start(); + break; + case "genart:stop": + this.stop(); + break; } }); } @@ -390,7 +403,7 @@ class API implements GenArtAPI { setTraits(traits: Traits): void { this._traits = traits; - this.emit({ type: "genart:settraits", traits }); + this.emit({ type: "genart:traits", traits }); } async setAdapter(adapter: PlatformAdapter) { @@ -461,9 +474,9 @@ class API implements GenArtAPI { : value; if (!key) spec.state = "custom"; } - this.emit( + this.emit( { - type: "genart:paramchange", + type: "genart:param-change", __self: true, param: this.asNestedParam(spec), paramID: id, @@ -519,11 +532,12 @@ class API implements GenArtAPI { } paramError(paramID: string) { - this.emit({ type: "genart:paramerror", paramID }); + this.emit({ type: "genart:param-error", paramID }); } configure(opts: Partial) { Object.assign(this._opts, opts); + this.notifyInfo(); } on( @@ -555,7 +569,7 @@ class API implements GenArtAPI { } this.setState("play"); // re-use same msg object to avoid per-frame allocations - const msg: AnimFrameMsg = { + const msg: AnimFrameMessage = { type: "genart:frame", __self: true, apiID: this.id, @@ -573,7 +587,7 @@ class API implements GenArtAPI { if (this._opts.notifyFrameUpdate) { msg.time = time; msg.frame = frame; - this.emit(msg); + this.emit(msg); } }; if (!resume) this._time!.start(); @@ -600,8 +614,8 @@ class API implements GenArtAPI { protected setState(state: APIState, info?: string) { this._state = state; - this.emit({ - type: "genart:statechange", + this.emit({ + type: "genart:state-change", __self: true, state, info, @@ -645,13 +659,13 @@ class API implements GenArtAPI { } /** - * Emits {@link SetParamsMsg} message (only iff the params specs aren't + * Emits {@link ParamsMessage} message (only iff the params specs aren't * empty). */ protected notifySetParams() { if (this._params && Object.keys(this._params).length) { - this.emit({ - type: "genart:setparams", + this.emit({ + type: "genart:params", __self: true, params: this.asNestedParams({}, this._params), }); @@ -668,6 +682,19 @@ class API implements GenArtAPI { this.setState("ready"); } + protected notifyInfo() { + const [time, frame] = this._time.now(); + this.emit({ + type: "genart:info", + opts: this._opts, + state: this._state, + version: this.version, + seed: this.random.seed, + time, + frame, + }); + } + /** * Returns true if this API instance is the likely recipient for a received * IPC message. @@ -676,7 +703,9 @@ class API implements GenArtAPI { */ protected isRecipient({ data }: MessageEvent): boolean { return ( - data != null && typeof data === "object" && data.apiID === this.id + data != null && + typeof data === "object" && + (data.apiID === this.id || data.apiID === "*") ); }