Rendering
wity-rkf is rendering-target-agnostic. The evaluate() function returns a ComputedFrame with resolved marks, controls (dir="in" ports), envs, gates, outputs (dir="out" ports), and section structure. A renderer consumes this output and draws it to any target.
ComputedFrame shape
js
{
state: { x: 0.5, y: 0.25, mode: 'linear' }, // current variable values
marks: [ { tag: 'rkf-circle', cx: 200, cy: 150, r: 8, fill: '#3b82f6', visible: true }, ... ],
controls: [ { tag: 'rkf-port', dir: 'in', type: 'continuous', var: 'x', min: 0, max: 1, step: 0.01, value: 0.5, visible: true }, ... ],
envs: [ { tag: 'rkf-env', type: 'prose', content: 'Some text', visible: true }, ... ],
gates: { isRunning: true, converged: false },
outputs: [ { tag: 'rkf-port', dir: 'out', var: 'energy', value: 42 }, ... ],
elastics: [ { var: 'pulse', resetTo: 0, delayMs: 240 }, ... ],
sections: [ { tag: 'rkf-section', id: 'intro', children: [...], visible: true }, ... ],
}Renderer examples
SVG renderer
Map marks to SVG elements:
js
function renderSvg(frame, svgElement) {
svgElement.innerHTML = '';
for (const mark of frame.marks) {
if (!mark.visible) continue;
if (mark.tag === 'rkf-circle') {
const el = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
el.setAttribute('cx', mark.cx);
el.setAttribute('cy', mark.cy);
el.setAttribute('r', mark.r);
el.setAttribute('fill', mark.fill || '#000');
svgElement.appendChild(el);
}
if (mark.tag === 'rkf-line') {
const el = document.createElementNS('http://www.w3.org/2000/svg', 'line');
el.setAttribute('x1', mark.x1);
el.setAttribute('y1', mark.y1);
el.setAttribute('x2', mark.x2);
el.setAttribute('y2', mark.y2);
el.setAttribute('stroke', mark.stroke || '#000');
el.setAttribute('stroke-width', mark.strokeWidth || 1);
svgElement.appendChild(el);
}
// ... handle other mark types
}
}Canvas 2D renderer
js
function renderCanvas(frame, ctx) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
for (const mark of frame.marks) {
if (!mark.visible) continue;
if (mark.tag === 'rkf-circle') {
ctx.beginPath();
ctx.arc(mark.cx, mark.cy, mark.r, 0, Math.PI * 2);
ctx.fillStyle = mark.fill || '#000';
ctx.fill();
}
if (mark.tag === 'rkf-rect') {
ctx.fillStyle = mark.fill || '#000';
ctx.fillRect(mark.x, mark.y, mark.width, mark.height);
}
if (mark.tag === 'rkf-text') {
ctx.fillStyle = mark.color || '#000';
ctx.font = `${mark.fontSize || 14}px sans-serif`;
ctx.textAlign = mark.textAnchor || 'start';
ctx.fillText(mark.content, mark.x, mark.y);
}
}
}DOM renderer for environments
js
function renderEnvs(frame, container) {
container.innerHTML = '';
for (const section of frame.sections) {
if (!section.visible) continue;
const sectionEl = document.createElement('section');
sectionEl.id = section.id;
for (const child of section.children) {
if (!child.visible) continue;
if (child.tag === 'rkf-env' && child.type === 'prose') {
const p = document.createElement('div');
p.innerHTML = child.content;
sectionEl.appendChild(p);
}
if (child.tag === 'rkf-env' && child.type === 'math') {
const math = document.createElement('div');
math.className = child.display === 'inline' ? 'math-inline' : 'math-block';
math.textContent = child.content; // hand to KaTeX/MathJax
sectionEl.appendChild(math);
}
}
container.appendChild(sectionEl);
}
}Reactive loop
A typical interactive rendering loop:
js
import { parse, evaluate, initState, applyUpdates } from '@wity-ai/rkf-core';
const doc = parse(xml);
let state = initState(doc);
function render() {
const frame = evaluate(doc, state);
renderSvg(frame, svgEl);
renderEnvs(frame, envEl);
renderControls(frame, controlsEl, onChange);
// Handle elastic variables
for (const elastic of frame.elastics) {
setTimeout(() => {
state = { ...state, [elastic.var]: elastic.resetTo };
render();
}, elastic.delayMs);
}
}
function onChange(varName, value) {
state = { ...state, [varName]: value };
render();
}
function onTick() {
state = applyUpdates(doc.transforms, state, 'tick');
render();
}
render(); // initial