Skip to content

Drawing

These commands turn the current beat into instructions a renderer, like Web UI, can draw. Drawing works its way down the layer tree: a beat draws its images, an image draws its root, a root draws its batches, and a batch draws its layers.

Each command receives the Beat it’s drawing. During a transition, the old scene is drawn with the previous beat, and the new scene with the current one.

Returns what each slot on the canvas should show.

draw(): Promise<null | DrawResult>
type DrawResult = DrawUpdate;

draw is the top of the drawing process. It calls drawBeat for the scene slot, and drawMenu for the menu slot, and returns a DrawUpdate. It only describes what changed, and slots that are left out keep what they have.

Example
const { draw } = getCommands();
const { slots } = await draw();

Draws the current beat.

drawBeat(ctx: DrawBeatContext): Promise<null | DrawBeatResult>
interface DrawBeatContext {
beat: null | Beat;
}
interface DrawBeatResult {
content: SlotContent;
}

content is the update for the scene slot: the beat’s frame, its occurrence, and the enter and replace shaders from resolveShaders when the beat plays a transition.

Moving forward through the story plays the transition. Stepping through history and loading a bookmark cut straight to the beat. Redrawing the same beat changes nothing.

Returns null if there’s no beat to draw.

Example
const { drawBeat } = getCommands();
const { content } = await drawBeat({ beat });

Draws the menu on top of the menu stack.

drawMenu(ctx: DrawMenuContext): Promise<DrawMenuResult>
interface DrawMenuContext {
beat: null | Beat;
}
interface DrawMenuResult {
content: SlotContent;
}

content is the update for the menu slot. The artwork comes from resolveImage, drawn with the labels of the menus on the stack. A menu with no artwork of its own is drawn on the artwork of the menu beneath it, so a submenu keeps the background of the menu that opened it.

The slot’s frame is null only when the stack is empty or none of its menus have artwork.

Example
const { drawMenu } = getCommands();
const { content } = await drawMenu({ beat });

Draws the PSD for a character, scene, or menu.

drawImage(ctx: DrawImageContext): Promise<DrawImageResult>
interface DrawImageContext {
beat: null | Beat;
labels: Array<string>;
logicalUrl: string;
resourceType: "character" | "menu" | "scene";
}
interface DrawImageResult {
imageInstructions: Array<FrameInstruction>;
root: InkRoot;
}

Loads the PSD, then draws its root with the active labels.

Example
const { drawImage } = getCommands();
const { imageInstructions } = await drawImage({
beat,
labels,
logicalUrl,
resourceType: "scene",
});

Draws the root of a PSD, with its layer batches inside a layer group.

drawRoot(ctx: DrawRootContext): Promise<DrawRootResult>
interface DrawRootContext {
beat: null | Beat;
labels: Array<string>;
resourceType: "character" | "menu" | "scene";
root: InkRoot;
}
interface DrawRootResult {
frameInstructions: Array<FrameInstruction>;
}
Example
const { drawRoot } = getCommands();
const { frameInstructions } = await drawRoot({
beat,
labels,
resourceType: "scene",
root,
});

Draws a batch of layers that share clipping, so clipped layers are masked correctly.

drawLayerBatch(ctx: DrawLayerBatchContext): Promise<DrawLayerBatchResult>
interface DrawLayerBatchContext {
batch: InkLayerBatch;
beat: null | Beat;
labels: Array<string>;
resourceType: "character" | "menu" | "scene";
}
interface DrawLayerBatchResult {
frameInstructions: Array<FrameInstruction>;
}
Example
const { drawLayerBatch } = getCommands();
const { frameInstructions } = await drawLayerBatch({
batch,
beat,
labels,
resourceType: "scene",
});

Draws a layer group with its blend mode and opacity.

drawLayerGroup(ctx: DrawLayerGroupContext): Promise<DrawLayerGroupResult>
interface DrawLayerGroupContext {
beat: null | Beat;
labels: Array<string>;
layerGroup: InkLayerGroup;
resourceType: "character" | "menu" | "scene";
}
interface DrawLayerGroupResult {
frameInstructions: Array<FrameInstruction>;
}

Uses resolveDrawActions to decide whether to draw the group, a placeholder, or both.

Example
const { drawLayerGroup } = getCommands();
const { frameInstructions } = await drawLayerGroup({
beat,
labels,
layerGroup,
resourceType: "scene",
});

Draws a single layer.

drawLayer(ctx: DrawLayerContext): Promise<DrawLayerResult>
interface DrawLayerContext {
beat: null | Beat;
labels: Array<string>;
layer: InkLayer;
resourceType: "character" | "menu" | "scene";
}
interface DrawLayerResult {
frameInstructions: Array<FrameInstruction>;
}

Uses resolveDrawActions to decide whether to draw the layer, a placeholder, or both.

Example
const { drawLayer } = getCommands();
const { frameInstructions } = await drawLayer({
beat,
labels,
layer,
resourceType: "character",
});

Draws what fills a placeholder slot, scaled to fit the bounds of its parent layer.

drawPlaceholder(ctx: DrawPlaceholderContext): Promise<DrawPlaceholderResult>
interface DrawPlaceholderContext {
beat: null | Beat;
parentLayer: AnyLayer;
placeholder: Placeholder;
}
interface DrawPlaceholderResult {
frameInstructions: Array<FrameInstruction>;
}

Applies any shaders from resolveShaders.

Example
const { drawPlaceholder } = getCommands();
const { frameInstructions } = await drawPlaceholder({
beat,
parentLayer,
placeholder,
});

These commands make the decisions that drawing depends on. Several do little on their own, and are there for you to decorate.

Returns the camera framing for a PSD. Defaults to the bounds of the artwork.

resolveCamera(ctx: ResolveCameraContext): Promise<ResolveCameraResult>
interface ResolveCameraContext {
beat: null | Beat;
root: InkRoot;
}
interface ResolveCameraResult {
camera: CameraInstruction;
}

Decorate it to add pans, zooms, or shots that frame part of the scene.

Example
const { resolveCamera } = getCommands();
const { camera } = await resolveCamera({ beat, root });

Returns the shaders for a layer, or for a frame arriving in or leaving a slot.

resolveShaders(ctx: ResolveShadersContext): Promise<ResolveShadersResult>
type ResolveShadersContext =
| ResolveLayerShadersContext
| ResolveTransitionShadersContext;
interface ResolveLayerShadersContext {
beat: null | Beat;
labels: Array<string>;
layer: AnyLayer;
resourceType: "character" | "menu" | "scene";
}
interface ResolveTransitionShadersContext {
beat: null | Beat;
labels: Array<string>;
name: null | string;
resourceType: "transition";
role: "new" | "old";
}
interface ResolveShadersResult {
shaders: Array<ShaderInstruction>;
}

For a layer, the context includes the layer and the kind of artwork it belongs to. For a transition, it includes the transition’s name as written in the screenplay, and a role: new for the arriving frame, or old for the frame it replaces. Novel asks once for each role.

Novel returns no shaders. Decorate this command to add visual effects. Web UI decorates it to add character shaders and transitions.

Example
const { resolveShaders } = getCommands();
const { shaders } = await resolveShaders({
beat,
labels,
layer,
resourceType: "character",
});

Decides what to do with a layer: draw it, draw a placeholder in its place, or both.

resolveDrawActions(
ctx: ResolveDrawActionsContext,
): Promise<ResolveDrawActionsResult>
interface ResolveDrawActionsContext {
beat: null | Beat;
labels: Array<string>;
layer: AnyLayer;
resourceType: "character" | "menu" | "scene";
}
interface ResolveDrawActionsResult {
actions: Array<"draw" | "draw-placeholder">;
match: undefined | string;
placeholder?: null | Placeholder;
}

Compares the beat’s active labels with the layer’s labels to decide whether the layer is shown, and finds any placeholder the layer should hold.

Example
const { resolveDrawActions } = getCommands();
const { actions, placeholder } = await resolveDrawActions({
beat,
labels,
layer,
resourceType: "scene",
});

Returns what should fill a placeholder slot, like the character PSD in a scene’s character slot.

resolvePlaceholder(
ctx: ResolvePlaceholderContext,
): Promise<ResolvePlaceholderResult>
interface ResolvePlaceholderContext {
beat: null | Beat;
layer: AnyLayer;
resourceType: null | "character" | "menu" | "scene";
}
interface ResolvePlaceholderResult {
placeholder: null | Placeholder;
}

Character placeholders are filled from beat.characters. Returns null if the layer has no placeholder to fill, or if beat is null.

Example
const { resolvePlaceholder } = getCommands();
const { placeholder } = await resolvePlaceholder({
beat,
layer,
resourceType: "character",
});