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.
Drawing commands
Section titled “Drawing commands”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
Section titled “drawBeat”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
Section titled “drawMenu”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
Section titled “drawImage”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
Section titled “drawRoot”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
Section titled “drawLayerBatch”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
Section titled “drawLayerGroup”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
Section titled “drawLayer”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
Section titled “drawPlaceholder”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,});Resolving commands
Section titled “Resolving commands”These commands make the decisions that drawing depends on. Several do little on their own, and are there for you to decorate.
resolveCamera
Section titled “resolveCamera”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
Section titled “resolveShaders”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
Section titled “resolveDrawActions”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
Section titled “resolvePlaceholder”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",});