Custom Render Targets
One of Rasen's core strengths is the ability to render to any target. This guide shows how to create custom render target adapters.
The MountFunction Pattern
Every render target implements the same pattern:
typescript
type MountFunction<Host> = (host: Host) => (() => void) | undefined- Input: A host (the render target)
- Output: An unmount function for cleanup
Example: Three.js Adapter
Let's create a simple Three.js adapter:
typescript
import * as THREE from 'three'
import { watchProp, unref } from '@rasenjs/dom'
import type { PropValue, MountFunction } from '@rasenjs/core'
// Host type
type ThreeHost = THREE.Object3D
// Primitive: mesh
export const mesh = (props: {
geometry: THREE.BufferGeometry
material: THREE.Material
position?: PropValue<{ x: number; y: number; z: number }>
rotation?: PropValue<{ x: number; y: number; z: number }>
}): MountFunction<ThreeHost> => (parent) => {
const mesh = new THREE.Mesh(props.geometry, props.material)
const stops: (() => void)[] = []
if (props.position) {
stops.push(watchProp(
() => unref(props.position),
(pos) => {
if (pos) mesh.position.set(pos.x, pos.y, pos.z)
}
))
}
if (props.rotation) {
stops.push(watchProp(
() => unref(props.rotation),
(rot) => {
if (rot) mesh.rotation.set(rot.x, rot.y, rot.z)
}
))
}
parent.add(mesh)
return () => {
stops.forEach(stop => stop())
parent.remove(mesh)
mesh.geometry.dispose()
if (mesh.material instanceof THREE.Material) {
mesh.material.dispose()
}
}
}
// Primitive: group
export const group = (props: {
position?: PropValue<{ x: number; y: number; z: number }>
children?: MountFunction<ThreeHost>[]
}): MountFunction<ThreeHost> => (parent) => {
const grp = new THREE.Group()
const stops: (() => void)[] = []
const childUnmounts: ((() => void) | undefined)[] = []
if (props.position) {
stops.push(watchProp(
() => unref(props.position),
(pos) => {
if (pos) grp.position.set(pos.x, pos.y, pos.z)
}
))
}
if (props.children) {
for (const child of props.children) {
childUnmounts.push(child(grp))
}
}
parent.add(grp)
return () => {
stops.forEach(stop => stop())
childUnmounts.forEach(unmount => unmount?.())
parent.remove(grp)
}
}
// Mount helper
export function mount(
component: MountFunction<ThreeHost>,
scene: THREE.Scene
) {
return component(scene)
}Usage
typescript
import { setReactiveRuntime } from '@rasenjs/core'
import { createVueRuntime } from '@rasenjs/reactive-vue'
import { ref } from 'vue'
import * as THREE from 'three'
import { mesh, group, mount } from './three-adapter'
setReactiveRuntime(createVueRuntime())
const scene = new THREE.Scene()
const rotation = ref({ x: 0, y: 0, z: 0 })
const SpinningCube = () => mesh({
geometry: new THREE.BoxGeometry(1, 1, 1),
material: new THREE.MeshBasicMaterial({ color: 0x00ff00 }),
rotation
})
mount(SpinningCube(), scene)
// Animate
function animate() {
rotation.value = {
x: rotation.value.x + 0.01,
y: rotation.value.y + 0.01,
z: 0
}
requestAnimationFrame(animate)
}
animate()Key Principles
1. Accept PropValue for Reactive Props
typescript
interface Props {
staticProp: string // Always static
reactiveProp: PropValue<number> // Can be static or reactive
}2. Use watchProp for Updates
typescript
watchProp(
() => unref(props.value),
(newValue) => {
// Apply update to host
}
)3. Return Cleanup Function
typescript
return () => {
// Stop all watchers
stops.forEach(stop => stop())
// Unmount children
childUnmounts.forEach(unmount => unmount?.())
// Remove from host
host.remove(element)
// Dispose resources
element.dispose?.()
}4. Support Children
typescript
if (props.children) {
for (const child of props.children) {
childUnmounts.push(child(element))
}
}More Examples
Terminal UI
typescript
type TerminalHost = { write: (text: string) => void }
const text = (content: PropValue<string>): MountFunction<TerminalHost> =>
(terminal) => {
watchProp(
() => unref(content),
(text) => terminal.write(text)
)
return () => terminal.write('\x1b[2K') // Clear line
}PDF Generation
typescript
type PDFHost = { addPage: () => void; text: (s: string, x: number, y: number) => void }
const paragraph = (props: { text: string; x: number; y: number }): MountFunction<PDFHost> =>
(pdf) => {
pdf.text(props.text, props.x, props.y)
return undefined // PDFs are write-once
}The pattern is infinitely flexible — anywhere you can mount and unmount, you can use Rasen.