@magnaboy/react-overlay
v0.0.2
Published
React overlay layout editor and renderer whose documents feed the cx-overlay Skia renderer.
Readme
@magnaboy/react-overlay
A React layout editor and renderer for stream overlays. It started as the cx-live LayoutManager
and is being reshaped so that the documents it produces are the documents the cx-overlay Rust
renderer (cx-toolkit crates/overlay) consumes, with no translation layer in between.
Status
Beta. The editor, canvas, panel and undo model are the cx-live ones with every Kick-specific source, the watermark and clock sources, the shader backgrounds and the Kick username removed.
The package speaks two document formats:
- The version 2
Composition(src/composition.ts) is the renderer's own schema, mirrored field for field. This is what a host should store and hand tocx-overlay. - The version 1 layout (
serializeLayoutSnapshot/parseLayoutSnapshot) is the editor's private import/export format. It is what the panel's export button writes. It carries every version 2 feature through, but the renderer's legacy importer only accepts plain text and image sources from it.
Usage
import { CX_OVERLAY_SOURCE_TYPES, LayoutManager } from '@magnaboy/react-overlay';
<LayoutManager renderers={CX_OVERLAY_SOURCE_TYPES} width={1920} height={1080} />;Editing a renderer document, with a live preview beside the editor:
import {
AssetUrlProvider,
CX_OVERLAY_SOURCE_TYPES,
fromComposition,
LayoutCanvas,
OverlayPreview,
SourcesPanel,
toComposition,
useLayoutManager
} from '@magnaboy/react-overlay';
const initial = fromComposition(document);
const manager = useLayoutManager({
width: initial.container.width,
height: initial.container.height,
sources: initial.sources,
composition: initial.container.composition,
onChange: layout => {
const { composition, dropped } = toComposition(layout);
if (dropped.length > 0) console.warn('not representable in cx-overlay', dropped);
save(composition);
}
});
<AssetUrlProvider resolve={id => assetUrls[id]}>
<LayoutCanvas manager={manager} renderers={CX_OVERLAY_SOURCE_TYPES} fit="contain" />
<SourcesPanel manager={manager} renderers={CX_OVERLAY_SOURCE_TYPES} />
<OverlayPreview composition={composition} data={{ title: 'Live', viewers: 1280 }} resolveAsset={id => assetUrls[id]} />
</AssetUrlProvider>;Chrome-free output of an editor layout:
<LayoutRenderer layout={{ sources, container }} renderers={CX_OVERLAY_SOURCE_TYPES} fit="contain" />Styles ship as CSS modules next to the compiled sources. Consumers need a bundler that understands
*.module.css imports (Vite, Astro, Next.js all do).
Source type registries
| Registry | Types | Use |
| --- | --- | --- |
| CX_OVERLAY_SOURCE_TYPES | text, image | Everything the renderer can draw. The text inspector exposes exactly the renderer's TextProps (text, font size, color, weight, outline, outer outline, letter spacing, font, align, wrap) with the renderer's defaults. |
| BUILTIN_SOURCE_TYPES | text, image, color | The original cx-live registry, kept for compatibility. color sources are dropped by toComposition. |
| CX_OVERLAY_PREVIEW_TYPES | text, image, group | What OverlayPreview renders with. Spread it into an editor registry to see groups (read-only) on the canvas. |
ImageSource treats src as an asset id. Wrap the tree in AssetUrlProvider with
resolve(assetId) => string | undefined and the image asks the host for a URL; without a provider it
uses src as the URL. assetIds(composition) lists every asset id a document references, in
document order, deduplicated.
Composition format
Composition mirrors crates/overlay/src/graph.rs exactly: { version: 2, container: { width, height },
nodes: CompositionNode[], animations: Animation[] }. Enums are string-literal unions using serde's
rename_all spellings (camelCase for imageFit, sizing, align, flow, easing, fill,
animatedProperty; lowercase for text align). Every Rust struct that is deny_unknown_fields is
a TypeScript interface with no extra members, and toComposition never emits a key outside that set.
| Editor (Source) | Renderer (CompositionNode) | Notes |
| --- | --- | --- |
| id | id | Unchanged. Must be unique across the whole tree, including group children. |
| transform (SourceTransform) | transform (Transform) | 1:1: x, y, width, height, rotation, scaleX, scaleY, zIndex, opacity, crop{top,right,bottom,left}. Editor limits are inside the renderer's. |
| visible | visible | Emitted only when false; the renderer defaults to true. |
| type: 'text', props.text/fontSize/color/fontWeight/outlineWidth/outerOutlineWidth/letterSpacing/font/align/wrap | content: { type: 'text', props } | Only these ten keys are copied. Missing keys stay missing so the renderer applies its own defaults (TEXT_PROPS_DEFAULTS). |
| type: 'image', props.src, props.fit | content: { type: 'image', asset, fit? } | src is the asset id. |
| type: 'group', props.composition.group | content: { type: 'group', layout?, children } | Opaque; see below. |
| props.composition.layout | layout (Placement) | Opaque. |
| props.composition.binding | content.binding (TextBinding) | Opaque. |
| container.width/height | container.width/height | Rounded to integers. |
| container.composition.animations | animations | Opaque. |
| name, locked, style, container.zoom/panX/panY/background | – | Editor-only; never leave the editor. |
color values are the renderer's wire format: #RGB, #RGBA, #RRGGBB, #RRGGBBAA or one of
red, white, black, green, blue, yellow, transparent (isCompositionColor).
fontWeight is a string: normal, bold or an integer 1–1000 (isFontWeight).
Converters
toComposition(snapshot: { sources; container }): { composition: Composition; dropped: DroppedFeature[] }
fromComposition(composition: Composition): LayoutSnapshot
parseComposition(value: unknown): Composition | nulltoComposition first runs the snapshot through the editor's own layout validation, then converts
each source. Anything the renderer cannot represent is left out of the document and reported in
dropped, in source order, with props sorted by key so the report is deterministic:
| kind | key | reason | Meaning |
| --- | --- | --- | --- |
| source | source type | unsupported | The whole source was dropped: color and any custom type. |
| source | image / group | invalid | An image without a src, or a group source whose carried children are corrupt. |
| style | SourceStyle key | unsupported | A non-default background, border, blend mode or filter. The node is kept without it. |
| prop | prop key | unsupported | An editor-only prop (for example alt). |
| prop | prop key | invalid | A value the renderer would reject (non-finite, out of range, bad color spelling, letterSpacing larger than the font size). |
parseComposition is strict: it returns null for any unknown key, wrong type, bad enum spelling,
duplicate id, or nesting deeper than 16 groups, because the renderer would reject the same document.
It fills the transform defaults (rotation, scaleX, scaleY, zIndex, opacity, crop) and
leaves every other optional field as it was. Range limits (Limits in Rust) are the renderer's job.
What the editor cannot author yet
Groups, placement (layout), text bindings and animations have no editor UI. They round-trip
unchanged: fromComposition stores them under the reserved props.composition key of each source
(SourceExtras) and under container.composition for the document (DocumentExtras), and
toComposition writes them back. Editor edits merge props, so moving, resizing, hiding or restyling
a source never discards them. A group is a group source whose children stay opaque; its own
transform and visibility are editable. Pass container.composition to useLayoutManager as the
composition option so animations survive the editor session.
OverlayPreview draws all of it as closely as CSS allows: row/column flows become flexbox with the
group's gap, padding, align and justify; anchors offset within the padded box; content sizing
becomes auto; bound text resolves against the data prop with the renderer's number formatting
and fallbacks (resolveBinding). Animations are not previewed.
Folder map
| Folder | Contents |
| --- | --- |
| types.ts | Editor data model (Source, SourceTransform, SourceStyle, Container, BackgroundSpec), defaults, source-type registry, action types |
| composition.ts | Renderer schema types, toComposition, fromComposition, parseComposition, assetIds, resolveBinding |
| assets.tsx | AssetUrlContext, AssetUrlProvider, useAssetUrl |
| serialization.ts | Version 1 layout import validation (parseLayoutSnapshot, parseBackground) |
| state/ | Reducer (undo history, revisions) and the useLayoutManager hook |
| canvas/ | Editor canvas: viewport, sources, selection overlay, gesture math, snapping, pan/zoom/fit hooks, and the read-only LayoutRenderer |
| preview/ | OverlayPreview |
| panel/ | Sidebar: source list, style section, canvas settings, export/import |
| menus/ | Right-click context menus |
| ui/ | Self-contained form controls (LM*), color picker, background presets, icons |
| sources/ | Built-in source types, the cx registry, the group preview renderer |
