npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@motionharness/sdk

v0.0.11

Published

MH React authoring SDK: component contracts, editable instances and MH time

Readme

MH SDK

单个公开 React 创作包 @motionharness/sdk。提供 defineComponent、field、defineComposition、Instance、Layer、useTime、Image、Video、Canvas、useFrame 和字体/素材接口。

组件拥有参数契约,普通 React 组件保持 props 和 Hooks 用法。持久实例由宿主初始化与管理,已有值不随源码默认值改变。动画完全依赖 MH 时间,异步绘制通过帧准备接口完成。

Image 自动识别 GIF,并按原件帧时长在 MH 时间中求帧,不依赖浏览器自动播放。它与 Video 共用 startFrom、playbackRate、holdAt 和 loop;loop 默认关闭,播放结束后保持最后一帧。原件仍以 GIF 保存,解码帧只是 Scene 内缓存。useAsset 和素材缩略图返回静态海报;需要动画时使用 Image。

<Image asset={gif} startFrom={0} playbackRate={1} loop />
<Image asset={gif} holdAt={0.35} />

标准动效函数 spring、inertia、oscillate 和带种子的 noise 直接接受 MH 秒数。刚度、阻尼等可声明为组件参数;每次按当前输入计算完整轨迹,不积累隐藏模拟状态。完全由计算决定的 Layer 使用 editable={false};需要保留基础变换编辑时,将物理偏移放到 Layer offset,由宿主处理选框与写回。

需要让倾角或拖曳效果跟随已有运动时,使用 sampleVelocity(time, source) 和 dampedFollow(time, source, { from, damping })。source 是 (localSeconds) => number; 速度单位为源参数单位/秒,阻尼为每秒响应速率。start 默认 0,step 默认 1/60 秒, 与作品 FPS 无关。阻尼响应按固定网格重建,每段使用终点的源值,任意定位得到同一结果。 起点前速度为零、响应保持初值;未指定 from 时初值为 source(start)。

需要停止后回弹时,使用 springFollow(time, source, { from, stiffness, damping, mass }), 或返回位置和速度的 springFollowState。弹簧保留速度,欠阻尼时会越过目标后逐渐稳定; dampedFollow 是单调回稳,不产生这种回弹。弹簧每个固定区间使用起点目标和解析解, 末段沿用同一目标,位置、速度在采样边界连续。start、step 和初值遵循上述秒制语义。

在组件 computed 中,通过 context.sampleNumber(name, localSeconds) 读取已列入 dependencies 的数值参数,包含关键帧、循环和上游计算结果。只求值所需的上游依赖, 不允许时间反馈循环;普通参数、预览、导出和显式烘焙继续使用相同的数据契约。 响应配置描述当前输入决定的完整轨迹,与现有 spring 语义一致;随时间变化的驱动力 放在被取样的源轨迹里。宿主缓存取样结果,修改输入后失效,不生成密集关键帧。 完整用法见 CLI 创作说明的 Standard motion functions。这里提供轻量轨迹响应, 不包含碰撞或刚体模拟。

SDK 不包含 Builder、Studio、数据引擎或导出 Renderer。根入口包含公开 JavaScript 和类型;内部协议通过 host/values 子路径连接。旧节点和旧工程格式接口已移除。

pnpm install && pnpm build && pnpm test 验证 SDK。独立 TSX 类型示例见 tests/authoring-consumer.tsx。需要 Node.js 22.13+ 与 pnpm 10.33。

加入内测与联系开发者

交流使用问题、反馈 Bug,也欢迎分享你的作品和建议。

联系开发者/邀请入群微信:LoomAgent · QQ群号:1009882353

| 微信内测群 | QQ 内测群 | | --- | --- | | | |

微信群二维码有效期为 2026 年 9 月 30 日前;二维码失效或无法入群时,请添加微信 LoomAgent,邀请入群。

点击二维码可查看原图。

Studio standard panel

A root component can offer a simplified Studio interface with a control whose placement is "standard". Inspector, toolbar, and overlay controls keep their existing placements. Standard controls use the same scoped, versioned commit and invoke APIs; they do not maintain a second copy of project data.

const Poster = defineComponent({
  id: 'poster',
  parameters: { gap: field.number({ label: 'Spacing', default: 24 }) },
  controls: {
    layout: {
      label: 'Layout',
      placement: 'standard',
      parameters: ['gap'],
      component: ({ values, commit }) => (
        <button onClick={() => void commit([
          { type: 'edit', parameter: 'gap', value: 48 },
        ])}>
          Increase spacing (current: {values.gap})
        </button>
      ),
    },
  },
  component: ({ gap }) => <div style={{ display: 'flex', gap }}>...</div>,
});

Studio offers Standard mode when the composition root declares a standard control. Its panel stays bound to the root when selection changes in PRO mode. Removing the declaration returns an open Standard workspace to PRO. These controls belong to Studio chrome and are not included in rendered output.

Standard controls can call the renderField(reference, name) prop for Studio-owned text, number, color, or asset editors. Bind the owning component parameter in parameters before rendering any child fields. The host enforces that scope and provides import, replacement, reset, and undo through its existing project operations.

Standard is for quick edits to a finished template. Choose fields from the changes a user is likely to make: text, images/video, chart data, visibility of main content, and a small set of overall appearance choices. Do not generate the panel by enumerating every parameter or layer. For many similar items, let the user choose an item and edit only that item's content. Keep shader/material internals, individual lights, transform coordinates, animation progress and per-layer fine tuning in PRO; an "all parameters" accordion still makes Standard an advanced panel. Preserve the full parameter contracts and PRO editability. This is an authoring preference, not a field-count limit or a validation gate.

Controls also receive an optional locale (zh-CN or en in Studio) for their section headings and help text. Keep authored values unchanged and declare field label translations with labelTranslations so native editors follow the UI language.

Use the public ControlPanel and ControlGroup React components for grouped controls. Studio owns their card border, header, collapse indicator, spacing and theme; author code supplies labels, descriptions and children. No Studio CSS class names are required. ControlGroup uses native details/summary, starts expanded, supports defaultOpen={false} (or controlled open/onToggle), and preserves its expansion while parameters change. These are editor controls; render them in the control component, outside the composition artwork.

import { ControlPanel, ControlGroup } from '@motionharness/sdk';

function StandardPanel(props) {
  return <ControlPanel>
    <ControlGroup label="Wallpaper">
      {props.renderField(props.values.screen, 'image')}
    </ControlGroup>
    <ControlGroup label="Screen content" description="Edit both screens together.">
      {props.renderField(props.values.outer, 'text', {
        label: 'Clock text',
        linked: [{ reference: props.values.inner, parameter: 'text' }],
      })}
    </ControlGroup>
  </ControlPanel>;
}

renderField accepts an optional presentation label and additional linked parameter bindings. Text, number, color, checkbox and select editors can commit the same value to several fields in one atomic operation; reset restores all of those fields together, and undo restores the whole edit. Every target must belong to the control's declared parameter scope. Use the leading field's value and schema as the editor presentation. Linked numeric edits commit together when the interaction completes; individual fields retain their live preview behavior. Asset editors keep the host's import and replacement flow. Avoid reimplementing native parameter widgets just to link fields or change a label.

Standard groups can link to the full inspector without exposing its fields in Standard. Pass proTargets and onOpenPro={props.openPro} to ControlGroup:

<ControlGroup label="Card" onOpenPro={props.openPro}
  proTargets={[{ label: 'Card text', reference: props.values.card, parameter: 'title' }]}>
  {props.renderField(props.values.card, 'title')}
</ControlGroup>

One target renders a PRO ↗ button; several targets render a chooser. A target contains an instance reference and optional parameter name. openPro switches Studio to PRO, selects that instance, reveals its timeline row and focuses its property when visible. Navigation changes only editor view state, not authored data, keyframes or undo history. Focused input is committed first; invalid input, obsolete source callbacks and missing/out-of-scope targets keep the current view. For a selected-item panel, compute its PRO target from the same selected item.

See examples/product-intro/src/StandardPanel.tsx for grouped controls and the Duo 3D and native AI workflow examples for linked parameters.

Component lists may declare named creation presets. Preset values are validated at discovery, and Studio lists them alongside the default creation entry:

cards: field.componentList({
  label: 'Cards',
  component: Card,
  presets: {
    featured: { label: 'Featured card', values: { fontSize: 48 } },
  },
})

A list operation with operation: "add", preset: "featured" creates a new instance using those initial values. Existing instances retain their values when the source preset changes. Presets cannot replace component ownership references; use component composition for nested structure. Default animation remains the component's animation contract; changing a preset value does not rewrite its keys.

Basic graphics

Graphic provides text, rectangle, and ellipse variants as an ordinary React component. basicGraphicPresets supplies the same named creation defaults used by Studio's component panel:

graphics: field.componentList({
  label: 'Graphics',
  component: Graphic,
  presets: basicGraphicPresets,
})

Render the list with graphics.map(ref => <Instance key={ref.$instance} value={ref} />). Its independent position, size, scale, rotation and anchor fields support canvas manipulation; color, opacity, font size, and corner radius can be keyframed. Text is rendered as React text, preserves line breaks, and supports weight and alignment. Place it in a positioned parent for absolute placement. Media and container components are separate capabilities.

Image and video components

Bind an imported original to a reusable editable media definition:

import photo from './photo.png';
import { defineMediaComponent, field } from '@motionharness/sdk';
const Photo = defineMediaComponent({ id: 'photo', asset: photo });
// In the containing component's parameters:
photos: field.componentList({ label: 'Photos', component: Photo })

The definition exposes asset replacement, editable geometry, contain/cover/fill, opacity, source offset, playback rate, looping and hold controls. Image definitions accept image originals (including deterministic GIF); video definitions accept video originals. Both use the existing MH resource and frame preparation ports. The source must provide a real default original; the factory never fabricates a placeholder asset hash. Use distinct stable IDs for different media definitions.

Containers and repeated layout

const GraphicGroup = defineContainerComponent({
  id: 'graphic-group',
  component: Graphic,
  presets: basicGraphicPresets,
  initialItems: [{ kind: 'rectangle' }, { kind: 'ellipse' }],
});

The container exposes its own geometry, clipping and opacity plus a typed child list. Free layout leaves child coordinates untouched; cells-row, cells-column and cells-grid modes place each child in a cell using cell width/height and gap. Cell layout does not rescale child content. Child geometry remains relative to its positioned cell; percent geometry continues to use composition dimensions, when explicitly converted with composition-percent geometry. Duplicate list items to repeat content and use editable child start times (or the existing stagger command) for time offsets. List items retain independent identities, parameters and animations; this is not a second object tree.

Basic graphic text also exposes line height and top/center/bottom alignment; rectangle and ellipse variants expose stroke color/width. Media definitions expose horizontal/vertical crop offsets (percentage points around the center), content scale and corner radius. Content scale affects the inner image/video, while the outer geometry remains the editable frame and clips overflow. These scalar style parameters use the same MH keyframe evaluation as other declared fields.

Basic text can display a literal string or a formatted numeric counter value (with decimals, grouping, prefix and suffix). Animate counter with regular keyframes. Enabling revealEnabled reveals that result using the keyframable reveal progress and character/word/line units. Character units preserve Unicode grapheme clusters. Optional cursor blinking reads local MH time, never a timer. No extra keyframes are generated by enabling these controls; the caller controls the animation explicitly through the existing parameter contract.

For portable font-backed text, use defineTextComponent({ id: 'brand-title', font }) with an imported font original. It shares Graphic's text, geometry, typewriter and counter controls, adds an editable font asset field, and resolves it through useFont. Studio's existing project/local-font picker edits that field; preview and export use the same prepared original. The component keeps its own geometry ownership, so adding the font binding does not disable canvas editing.

All basic graphic, font-backed text, media and container definitions expose an effects field. Studio's effect catalog and stack editor use that declaration. Effects render inside the editable geometry boundary. EffectStack additionally accepts "100%" for width/height to fill an explicit parent box, including a parent-sized Layer; other CSS size strings remain unsupported.

Fields may declare visibleWhen: { mode: ['text'], enabled: [true] } to keep Studio's single-instance inspector focused. Conditions are combined with AND; values within each condition are alternatives. Dependencies must be declared, static boolean/select fields. Visibility is presentation only: hidden values and keyframes remain saved, render normally and remain addressable by CLI commands.

Parameter label translations

Fields and select options can declare labelTranslations: { en: 'Media start time' } alongside their default label. Studio selects an explicitly declared label for its current locale and otherwise uses label. This metadata changes presentation only: parameter identifiers, option values, defaults and saved instance values remain unchanged. Author labels without this metadata are displayed verbatim, even if their text matches a built-in label. Discovery validates that locale keys and translated labels are non-empty strings and copies the metadata into the serializable contract.

Global parameters

Select the composition root in Studio's layer tree to edit its declared parameters in the Props inspector. The Global panel edits project settings. Use ordinary root parameters for background, lighting, spacing, or other composition-wide controls; the same values drive React preview and export. The CLI starter declares an editable background color and uses it in the root element's style. Existing authored projects are not rewritten: expose a root parameter explicitly if a hard-coded style should become editable.

Independent base animation

Use basicTransformParameters({ animations: { y: ..., opacity: ... } }) for a slide-and-fade. Each property has separate times, values and easing. Render with Layer and basicTransformValue(values), with bindings="parameters" for editable ownership. basicSizeParameters(width,height) adds explicit width/height and an aspect lock. React auto layout and intrinsic size can be retained. Anchor X/Y values are local pixels from the layer's top-left corner in saved data, keyframes, Studio and CLI edits. They may be negative or extend beyond the layer. The default is (0, 0); set explicit pixel defaults for another pivot, for example defaults: { anchorX: 125, anchorY: 40 } for the center of a 250 × 80 box. Resizing the box does not rescale its saved anchor. React auto layout remains intact. The canvas anchor handle and nine-point presets compensate position at the current frame. Direct numeric anchor edits are available for animation.

There is one Layer and one factory for each component kind. No compatibility aliases are provided. Layer without bindings renders computed/presentational values. bindings="parameters" binds independent fields by name; a mapping can bind specific fields. A bound computed field remains read-only until explicitly baked. Numeric equality never decides ownership.

Use an additive offset for motion that accompanies an editable base transform:

<Layer bindings="parameters" value={basicTransformValue(values)}
  offset={{ rotation: values.tilt }}>
  <Card />
</Layer>

offset.x/y are parent-space pixels added to base position; offset.rotation is degrees added about the same anchor. Scale, skew, dimensions and opacity retain their base values. applyTransformOffset(base, offset) returns this final pose for connection endpoints or other dependent drawing. Keep the physical transform on this Layer so its selection outline follows the displayed card. Numeric parameter edits address the base. Canvas gestures use the displayed pose and subtract the offset captured at gesture start before writing changed base channels. With base rotation 10° and offset 3°, dragging the displayed angle to 20° saves 17°; entering 20° in the base parameter displays 23°. Offsets are derived presentation values, never implicit keys. Animate only intentional base channels; constant defaults and generated physical responses need no duplicate tracks. Offsets are evaluated again from the edited inputs. Anchor compensation uses the displayed offset, but changing keyed X/Y can also change a motion-derived offset: that response is recomputed, so a dynamic offset does not promise frozen pixels after an anchor edit. The host does not invert an arbitrary dependency formula.

Layer time ranges

Component instances use { start, duration?, offset? } in the containing group's local seconds. start is the visible start, duration is the visible length, and offset is animation-local time at the visible start (default 0). Local animation time is groupTime - start + offset; visibility uses groupTime - start against [0, duration). Moving a group changes its start once, carrying its members without rewriting their local tracks. Left trimming increases start and offset by the same amount and shortens duration; right trimming only changes duration. Keys outside the visible window remain intact. Group membership follows declared component references; it is not a separate transform-following link.

Instance timing is editable by default. Set editableTiming: false on a component or component-list field to prevent editing its timing in Studio/CLI. Instance receives a reference through value; it has no custom time prop. The project root and output camera retain project time.

Bitmap inputs for Canvas and GPU effects

Canvas accepts a React 19 ref. For a raw bitmap input, explicitly set textureOptions={{ source: { source: 'bitmap' } }} for its named source input on GpuCanvas:

const bitmap = useRef<HTMLCanvasElement>(null);
const bitmapOptions = { source: 'bitmap' } as const;
// Inside the Scene:
<Canvas ref={bitmap} width={560} height={300} draw={drawParticles} />
<GpuCanvas inputs={{ source: bitmap }} width={560} height={300}
  textureOptions={{ source: bitmapOptions }} graph={graph} />

Bitmap mode accepts image, video and canvas elements at their intrinsic pixel size, following drawImage and texImage2D. Use the same options with await frame.texture(ref.current, { source: 'bitmap' }) inside Canvas.draw. The frame scope waits for tracked GIF/video/Canvas producers, checks Scene ownership and rejects feedback cycles. CPU consumers receive an owned snapshot; GPU consumers can directly retain a managed GPU output from the same frame. Snapshots never clear the authored source. Texture lifetime and device recovery remain host responsibilities.

CSS backgrounds, borders, transforms, crop and descendants belong to the default DOM capture mode. Bitmap options reject DOM crop/resize/exclusion fields at type checking and at runtime. Apply crop, transforms and compositing with ordinary Canvas calls or explicit shader passes when working with raw pixels.

Multi-input GPU graphs with standard GLSL

GpuCanvas accepts complete GLSL ES 3.00 fragment shaders. Write #version 300 es, precision, uniform declarations, an output and main() normally. The host supplies a fullscreen vertex stage with in vec2 vUV: normalized UVs use WebGL's bottom-left origin. Both shader sources are compiled unchanged. An optional vertexShader can supply other varyings: it must generate a fullscreen triangle from gl_VertexID (three vertices, no vertex buffers). Use one fragment output at location 0. Use Canvas mode="webgl2" for general mesh rendering. Numeric values (including integer, boolean, vector, matrix and numeric-array uniforms) and named sampler2D inputs are bound explicitly; no time or resolution uniforms are injected. Use useTime() and ordinary component props for their values.

const shader = `#version 300 es
precision highp float;
in vec2 vUV;
uniform sampler2D background;
uniform sampler2D mask;
uniform float strength;
out vec4 color;
void main() {
  vec4 c = texture(background, vUV);
  color = vec4(c.rgb, c.a * mix(1.0, texture(mask, vUV).r, strength));
}`;
// Both refs point to explicit source content, outside the output's subtree.
<GpuCanvas width={1280} height={960}
  inputs={{ background: backgroundRef, mask: maskRef }}
  graph={{ passes: [{
    id: 'glass', fragmentShader: shader,
    uniforms: { strength },
    textures: { background: { input: 'background' }, mask: { input: 'mask' } },
  }], output: { pass: 'glass' } }} />

Full shaders use straight RGBA inputs and outputs by default. The host converts at the boundaries to/from its premultiplied internal targets. Set alphaMode: 'premultiplied' when the entire pass uses premultiplied samples/output, for example for alpha-correct blur. This avoids those conversions. Colors use the existing sRGB-encoded channel values; no automatic linear-light conversion occurs. RGBA8 targets may introduce rounding. Standard GLSL textureSize reports physical texture dimensions, including export density. Active uniforms without bindings fail with their names instead of inheriting another instance's values.

Each pass names its texture dependencies ({ input: name } for source refs, { pass: id } for intermediate results). size: { width, height } specifies logical output pixels; export density applies to every intermediate. Passes may be listed in any order. The host detects cycles and missing references, schedules reachable work, releases intermediates after the last consumer and presents only the final result. Full shaders do not have implicit primary-input, mix or bypass behavior; express blending in GLSL and select the desired graph output in JavaScript.

There are at most 32 passes and 64 declared inputs. Physical sizes and sampler counts must fit the device. Texture sampler arrays and uniform blocks are not supported by the binding interface. CPU/DOM inputs are captured after their producers finish; same-frame managed GPU outputs can reuse GPU textures. Failed, cancelled or stale frames cannot replace a completed output; context loss rebuilds the device and retries with prepared inputs.

Explicit background content

Give a background element a React ref and use it as an ordinary graph input. Its internal layout can use flex, grid, positioning and z-index normally. Place the consumer outside that input subtree, so the source never includes its own result. If needed, textureOptions={{ background: { region: { x, y, width, height } } }} crops in the source element's local coordinates (top-left, logical pixels).

<div style={{ position: 'relative' }}>
  <div ref={backgroundRef} style={{ display: 'flex' }}>
    <Wallpaper /><Clock />
  </div>
  <div style={{ position: 'absolute', inset: 0 }}>
    <GpuCanvas inputs={{ background: backgroundRef }}
      width={1280} height={960} graph={glassGraph} />
  </div>
</div>

This captures the referenced content, not an inferred DOM paint-order backdrop. For multiple glass controls, share an explicit background ref or graph result. Self-feedback, cross-Scene refs and cyclic producer dependencies produce errors.

Replaceable image/video fields

field.asset({ label: 'Wallpaper', accept: ['image', 'video'], default: image }) allows either kind in one field. Studio's picker and upload replacement, CLI validation and saved documents use the declared accepted kinds. Render with Image or Video according to the reference's kind; an asset reference always retains its actual kind. Changing the accepted set still requires normal schema migration; simply reordering the same accepted set does not.

Native inputs inside custom controls

Custom controls can compose the host's existing inputs with the renderField render prop: renderField(childReference, 'wallpaper'). The same asset picker, color editor and numeric editor are used as in PRO. The field must be within the control's declared parameter/child scope. Changes retain the control's build, revision and time checks; computed fields are read-only. Use ordinary JSX and commit([...]) for explicitly linked edits to multiple child fields.

Explicit authoring contracts

  • CSS easing names use CSS curves and outgoing (left-key) interval easing. quad-in, quad-out and smooth are explicitly mathematical alternatives.
  • Sequence spacing: 'fixed' preserves item duration/stagger; spacing: 'fit' uses a total duration and rejects a nonpositive item window. No silent cap.
  • useSequenceProgress({mode:'scale'|'override', value}) makes composition explicit; the presence of a keyframe never changes that operation.
  • Colors accept portable hex and rgb()/rgba() forms, plus transparent/black/white. Other syntax is rejected on declaration/edit. All accepted forms use the same premultiplied-sRGB interpolation; var() and currentColor are not portable values.
  • field.asset({accept:'image', ...}) retains the image kind in inferred component props. AssetReference<'image'> names it explicitly when needed. Texture/frame effects accept image references; video/font references fail at compile time.
  • CSS/SVG effects expose effect-specific intensity. Pixel result mixing belongs in a GPU shader using explicit textures and GLSL mix(). blend-mode has only enabled and params.mode; it has no intensity field or intensity track.
  • computed declarations can name dependencies; derived values are evaluated in dependency order, with cycle errors. Their parameters need not be animatable unless they will later be baked to editable tracks.
  • Layer scales follow CSS: negative values mirror, zero collapses, and animation may cross zero. This applies to structured and independent editable transforms. Numeric edits/reset remain available; only inverse-dependent canvas gestures are disabled when the corresponding layer or ancestor matrix is singular. Proportional linked edits cannot recover a ratio from a zero source axis: unlink the axes for independent numeric edits, or reset their scale.
  • DOM refs point to presentation elements. Canvas.draw receives the actual drawing context and output presentation canvas. A texture returned by frame.texture has lifetime:'frame'; await its snapshot() to retain an owned ImageBitmap and close that bitmap when finished. Drawing state is frame-local.
  • DOM wrappers forward refs, events, aria-* and data-*; declared geometry owns transform/pivot/opacity/size, while ordinary styling remains ordinary CSS.
  • oscillate.phaseCycles, text units whitespace-token/newline, and counter rounding half-away-from-zero explicitly name their units/semantics. Counter decimal/group separators are configurable.
  • A reusable component can declare fonts: [fontDefinition]; discovery collects them for preparation, so its consumer need not repeat the registration.
  • Host integration protocols live at @motionharness/sdk/host, separate from the root authoring interface. GPU uniform values also accept Float32Array, Int32Array and Uint32Array.

Explicit structured layer bindings

<Layer value={geometry} binding="box" /> uses the declared geometry field for canvas editing, with the same top-left and pixel-pivot semantics as CSS. <Layer value={transform} binding="placement" /> binds a structured transform. For independent numeric fields, use bindings="parameters" or an explicit axis mapping. Omitting a binding renders normally without guessing ownership from equal values. Computed fields remain read-only until baked. These forms share one Layer entry point; no separate geometry or legacy layer implementation is required.

Generic scene/job/render contracts live in the type-only @motionharness/sdk/host/render entry. It has no React or DOM type dependencies, so Node renderers can use it with ES/Node libraries only. Browser frame ports and providers remain in @motionharness/sdk/host.

Three.js / React Three Fiber

The optional @motionharness/sdk/three entry exports ThreeCanvas. Install the tested peer versions [email protected], @react-three/[email protected] and, for TypeScript, @types/[email protected]. Use React/React DOM 19 and a matching current MH host. Plain 2D projects do not need these peers. Close and reopen the project after adding them.

For editable colors, use materialColor from the same entry: <meshStandardMaterial {...materialColor(color)} />. It converts every supported MH CSS color (including eight-digit keyframe colors) into Three-compatible RGB, opacity and transparent. Three.Color has no alpha channel and does not parse eight-digit hex. For emissive/shader colors use materialColor(color).color and apply its opacity explicitly where your effect needs it. The helper never patches Three.js or changes color interpolation.

import { useTime } from '@motionharness/sdk';
import { ThreeCanvas } from '@motionharness/sdk/three';

function Ring({ speed = 0.5 }) {
  const time = useTime();
  return <mesh rotation={[0, time * speed, 0]}>
    <torusGeometry args={[1, 0.15, 24, 96]} />
    <meshStandardMaterial color="#35e986" metalness={0.6} roughness={0.3} />
  </mesh>;
}
// Within an ordinary composition component:
<ThreeCanvas width={640} height={360} camera={{ position: [0, 0, 5] }}>
  <ambientLight intensity={0.5} />
  <directionalLight position={[3, 4, 5]} intensity={2} />
  <Ring />
</ThreeCanvas>

ThreeCanvas accepts camera, orthographic, shadows, linear, flat and ordinary canvas DOM attributes/ref. Width/height default to composition dimensions and use logical pixels. The host owns DPR, the renderer/context and frame loop; gl, frameloop, dpr, size, events, performance and onCreated overrides are rejected. Configure scene objects using normal R3F components and useThree. The first release does not connect R3F pointer/raycast events or WebXR.

useTime, useAsset, parameter sampling and ordinary React context work inside the 3D tree. defineComponent/Instance can render R3F content with independent saved parameters and local start/offset. Use a Three.js group for its transforms; DOM Layer belongs outside ThreeCanvas. 3D units and radians keep their standard meaning. Studio edits declared fields; it does not provide a 3D gizmo. 2D layer order does not override Three.js depth ordering.

Simple animations use useTime() in JSX. Heavy per-frame work may use R3F's useFrame from @react-three/fiber. Its clock is the containing ThreeCanvas's local time; nested MH instances use MH useTime() for their own local time. The R3F clock does not run on wall time. Delta is zero on the first/repeated frame and signed elapsed seconds between subsequent draws, including negative seeks. Calculate final state from absolute time; incremental physics, independently created clocks and temporal-feedback effects are not made seekable automatically. Global R3F frame effects are not advanced across unrelated scenes.

For a managed image, use standard useLoader(TextureLoader, useAsset(image)). ThreeCanvas waits for its default Suspense boundary, descendant effect setup and registered MH async frame work before advancing R3F and presenting a completed frame. An explicitly nested author Suspense boundary retains normal React fallback semantics: that fallback is authored content. Unregistered asynchronous work or async R3F useFrame callbacks are not a completion signal; use MH useFrame for awaitable preparation. Resource failures reach the frame barrier; incomplete frames do not replace the last completed preview. Preparation has a 20-second timeout.

A ref points to the prepared presentation canvas. It can be sampled by Canvas or GpuCanvas with source: 'bitmap'; consumers wait for the same-frame 3D producer. The host rebuilds the R3F root on WebGL context loss, so create GPU-dependent resources in effects and release them in cleanup. JSX resources follow R3F ownership; external primitives, loader caches, cloned textures and custom postprocessing targets retain their normal explicit ownership. Standard positive-priority R3F frame callbacks can render a composer; the same frame barrier covers that draw.

The repository's examples/green-lantern combines a procedural energy ring, shader shield, deterministic particles, bloom, a managed texture and DOM typography. GLTF/GLB original management, animated video/GIF textures, physics and WebXR are not included in this first integration.