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, Novel draws the old scene with the previous beat, and the new scene with the current one.

draw 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. The update only describes what changed, and slots that are left out keep what they have.

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

drawBeat 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.

When the reader moves forward through the story, the transition plays. When they step through history or load a bookmark, the scene cuts straight to the beat. Redrawing the same beat changes nothing.

It returns null if there’s no beat to draw.

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

drawMenu 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. Novel finds the artwork with resolveImage, and draws it with the labels of the menus on the stack. If a menu has no artwork of its own, Novel draws it 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 });

drawImage 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;
}

It loads the PSD, then draws its root with the active labels.

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

drawRoot 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,
});

drawLayerBatch 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",
});

drawLayerGroup 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>;
}

It 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",
});

drawLayer 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>;
}

It 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",
});

drawPlaceholder draws whatever 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>;
}

It 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.

resolveCamera returns the camera framing for a PSD. By default, that’s 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 });

resolveShaders 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 calls it 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",
});

resolveDrawActions 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;
}

It 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",
});

resolvePlaceholder 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;
}

Novel fills character placeholders from beat.characters. It 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",
});