Getting started
Write your first environment, from its definition to a drawn surface.
You will build canvas.greeting: a line of text that grows at a step of the scene, with a toolbar button that counts how often the reader waves back.
The SDK is not published yet (see Installation), so for now the code runs inside the Internote repository.
#Define the environment
An environment starts with defineEnvironment. The name is what authors write after canvas..
The greeting’s text is its content, so it goes in the body, between the braces. body: { inline: 'all' } lets the body hold any inline content: plain text, bold, links or maths.
Settings go in attributes. size declares animatable: 'number', so a document can animate it with !cue.to.
import { defineEnvironment } from '@internote/canvas-sdk';
export const greetingEnvironment = defineEnvironment({
name: 'greeting',
body: { inline: 'all' },
attributes: {
size: { type: 'float', default: 1, animatable: 'number' },
},
Surface: GreetingSurface,
});#Draw the surface
Surface is a React component. It receives the environment’s context, whose animation.attrValues() are the attributes at this moment, mid-animation included. The host re-renders it whenever they change. ctx.node.body is the compiled body, and InlineContent draws it with its formatting.
useEnvironmentSession reads the environment’s session state and re-renders when it changes. You will write to it in the next step.
import { numberAttr, type SurfaceProps } from '@internote/canvas-sdk';
import { InlineContent, useEnvironmentSession } from '@internote/canvas-sdk/react';
export interface GreetingState {
waves: number;
}
const initialState = (): GreetingState => ({ waves: 0 });
function GreetingSurface({ ctx }: SurfaceProps) {
const attrs = ctx.animation.attrValues();
const [state] = useEnvironmentSession(ctx, initialState);
return (
<p style={{ fontSize: `${numberAttr(attrs, 'size', 1)}em` }}>
<InlineContent items={ctx.node.body} /> 👋 × {state.waves}
</p>
);
}#Add a toolbar button
A toolbar is a list of items, not components: the host draws them, so every environment’s toolbar looks and behaves the same. The button updates the session, and the surface re-renders with the new count.
storage declares the session tier and its starting state. Session state lasts while the document is open, including when the reader scrolls away and back.
export const greetingEnvironment = defineEnvironment({
// …name, body, attributes and Surface as before
toolbar: (ctx) => [
{
type: 'button',
id: 'wave',
text: 'Wave',
label: 'Wave back',
onPress: () => ctx.storage.session.update((previous) => {
const state = (previous as GreetingState | undefined) ?? initialState();
return { waves: state.waves + 1 };
}),
},
],
storage: { tiers: 'session', initState: initialState },
});#Register it
Nothing registers itself. Register the environment before compiling a document that uses it; compiling with no environments registered throws.
import { EnvironmentRegistry } from '@internote/canvas-sdk';
import { greetingEnvironment } from './greeting';
EnvironmentRegistry.register(greetingEnvironment);#Write a document
The scene has two steps, separated by ===. At step 1, size animates from 1 to 2 over 300 ms.
*(title: Greetings)
scene{
A greeting.
===
Then it grows.
}[
canvas.greeting{Hello, **canvas**}(
size: 1 !cue.to{2}(at: 1; over: 300ms)
)
]#Compile it
@internote/canvas-sdk/compile runs on the server only. The compiler checks the document against your attributes, so a misspelt attribute or a value of the wrong type is an error here.
import { dotchalkService } from '@internote/canvas-sdk/compile';
const result = dotchalkService.compile(source);
if (!result.ok) throw new Error(result.error);#Draw it
Hand the compiled document to CanvasDocumentHost, and import the stylesheet once. The host draws the scene: your surface, the toolbar with the Wave button, and the step position.
import { CanvasDocumentHost } from '@internote/canvas-sdk/host';
import '@internote/canvas-sdk/styles';
<CanvasDocumentHost document={result.data} environments={[greetingEnvironment]} />#Test it
The headless host mounts the same document without a browser, on a clock you control. Step the scene, read an attribute mid-animation, and press the toolbar button.
import { createHeadlessCanvasHost } from '@internote/canvas-sdk/testing';
let now = 0;
const host = createHeadlessCanvasHost({
environments: [greetingEnvironment],
document: result.data,
clock: () => now,
source,
});
const scene = host.sceneAt(0);
const greeting = scene.environments[0];
greeting.runtime.attrValue('size'); // 1
now = 1000;
scene.steps.goTo(1);
now = 1400;
greeting.runtime.attrValue('size'); // 2
const wave = greeting.toolbarItems().find((item) => item.type === 'button' && item.id === 'wave');
if (wave?.type === 'button') wave.onPress();
greeting.ctx.storage.session.get(); // { waves: 1 }
host.dispose();#Next steps
- How environments work explains why the API is shaped this way.
- Environments lists everything a definition can hold.
- Animation covers steps, cues and custom verbs.
- Toolbar and footer covers toggles, separators and the footer.