@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-outandsmoothare 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 GLSLmix().blend-modehas onlyenabledandparams.mode; it has no intensity field or intensity track. computeddeclarations can namedependencies; 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.drawreceives the actual drawing context andoutputpresentation canvas. A texture returned byframe.texturehaslifetime:'frame'; await itssnapshot()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 unitswhitespace-token/newline, and counter roundinghalf-away-from-zeroexplicitly 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.
