EnvironmentDefinition
One canvas environment, on both sides of the system: what authors may write inside it, and how it draws and behaves.
@internote/canvas-sdkimport { EnvironmentDefinition } from '@internote/canvas-sdk';The compile side is the name, attributes, vocabulary and body policy the compiler and the semantic pass validate. The render side is the surface, the animation handlers, the toolbar, the footer, the storage declaration and the interpolators. Both halves are one object, so an environment cannot describe itself one way to the compiler and another to the host.
JSX enters in one place, Surface, and in the custom rows of the footer for what no other environment could share. The toolbar and the footer are otherwise data the host merges and draws, animation handlers react to the engine, and state goes through the declared storage tiers.
Build one with defineEnvironment, which checks the shape, and hand it to EnvironmentRegistry to make it available.
#Fields
animationEnvironmentAnimationattributescontinue:.Record<string, CanvasAttrSpec>bodynone without a vocabulary.PolicySpeccompileattributes, vocabulary and body below.EnvironmentSpecKeep the spec in a React-free module when a server compiles documents without loading the surface: register the spec there for compiling, and pass the same object here for mounting. Registration with the same object on both sides is idempotent.
displayNamestringfreeParameters(ctx: EnvironmentContext) => string[]The host cannot work these out, because which identifiers in a body are free letters is a vocabulary fact: Q1 on an axis ruled in quarters is a quarter and not Q × 1, and the y in a surface's expression: is the axis it is evaluated over. Called during the host's render, so it may not use hooks. An environment whose attributes hold no expressions leaves it out.
interpolationsanimatable.Record<string, Interpolator>The built-in kinds cover numbers, lists, intervals, points, colours and edges. An environment with a shape of its own, such as a map of a plane, declares a kind of its own name and says here how two such values blend. Registering a built-in kind's name replaces it for every environment in the host, so an environment names its own kinds.
name<name>{…}.stringstorageCanvasStorageDeclarationSurfaceComponentType<SurfaceProps>tasksctx.tasks. Called once, when the host mounts the scene. An environment that asks nothing leaves it out.(node: CompiledNode) => string[]toolbar{ type: 'view', channel } for the scene's one view group. Data only, no JSX. Called on every render of the host, so it may not use hooks.(ctx: EnvironmentContext) => ToolbarContribution[]useFooter(ctx: EnvironmentContext) => FooterContributionA hook, because what it reports is live — a hover, a caret, a run's output — and those readings are hooks. The host calls it on every render of the scene's footer, in environment order, so it must call the same hooks every time. The host merges every environment's answer: one colour key, one row per distinct scale.
vocabularyRecord<string, CanvasNodeSpec>#Example
export const greetingEnvironment = defineEnvironment({
name: 'greeting',
attributes: { size: { type: 'float', default: 1, animatable: 'number' } },
vocabulary: { wave: { body: 'none', attributes: { colour: { type: 'paint' } } } },
Surface: GreetingSurface,
toolbar: (ctx) => [{ type: 'button', id: 'reset', label: 'Reset', onPress: () => reset(ctx) }],
useFooter: (ctx) => ({ bar: [{ type: 'readout', id: 'waves', text: `${waveCount(ctx)} waves` }] }),
storage: { tiers: 'session', initState: () => ({ waves: 0 }) },
});Declared in packages/canvas-sdk/src/environment/types.ts:220.