Skip to content

Renderers

Los renderers de Oroya Animate son traductores que convierten el scene graph agnóstico en salida visual. El core no conoce a los renderers — cada uno lee el scene graph y produce su propia representación.


Visión general

graph TD
    SG["Scene Graph\n(@joroya/core)"]
    R3["ThreeRenderer\n(@joroya/renderer-three)"]
    RS["renderToSVG\n(@joroya/renderer-svg)"]
    WEBGL["WebGL Canvas (3D)"]
    SVG["SVG String (2D)"]

    SG -->|"mount + render"| R3
    SG -->|"function call"| RS
    R3 --> WEBGL
    RS --> SVG
AspectoThreeRendererrenderToSVG
ParadigmaInstancia con estado (class)Función pura (stateless)
OutputDibuja en un <canvas>Retorna un string SVG
Requiere DOM✅ Sí (HTMLCanvasElement)❌ No (funciona en Node.js)
3D✅ Perspectiva, luces, sombras❌ Solo 2D
Vectorial❌ Rasterizado✅ Infinitamente escalable

@joroya/renderer-three — Three.js (WebGL)

El renderer principal para visualización 3D interactiva.

Setup

import { ThreeRenderer } from '@joroya/renderer-three';

const renderer = new ThreeRenderer({
  canvas: document.getElementById('canvas') as HTMLCanvasElement,
  width: window.innerWidth,
  height: window.innerHeight,
  dpr: window.devicePixelRatio,  // opcional
});

Opciones del constructor

OpciónTipoDefaultDescripción
canvasHTMLCanvasElement(requerido)Elemento canvas destino
widthnumber(requerido)Ancho del viewport
heightnumber(requerido)Alto del viewport
dprnumberwindow.devicePixelRatioDevice pixel ratio (HiDPI)

Métodos

MétodoDescripción
mount(scene)Conecta una escena. Reconstruye la escena Three.js, detecta la cámara activa, agrega luces
render()Sincroniza transforms, propaga matrices y dibuja un frame
dispose()Libera recursos WebGL

Ciclo de vida

sequenceDiagram
    participant U as User Code
    participant TR as ThreeRenderer
    participant TS as THREE.Scene

    Note over U,TS: Montaje
    U->>TR: mount(scene)
    TR->>TS: clear + add lights
    TR->>TR: traverse → create Mesh/Group/Camera per node
    TR->>TR: Set first Camera as activeCamera

    Note over U,TS: Render loop
    loop requestAnimationFrame
        U->>TR: render()
        TR->>TR: updateWorldMatrices()
        TR->>TS: sync worldMatrix → Three.js objects
        TR->>TR: webglRenderer.render()
    end

Traducción de componentes

Nodo OroyaObjeto Three.js
Node sin Geometry ni CameraTHREE.Group
Node + Geometry(Box)THREE.Mesh(BoxGeometry)
Node + Geometry(Sphere)THREE.Mesh(SphereGeometry)
Node + Geometry(Path2D)❌ Ignorado
Node + Camera(Perspective)THREE.PerspectiveCamera
Material con colorMeshStandardMaterial({ color })
Material con opacity < 1MeshStandardMaterial({ transparent: true })
Sin MaterialMeshStandardMaterial({ color: 0xcccccc })

Iluminación automática

TipoConfig
AmbientLightBlanco, intensidad 0.5
DirectionalLightBlanco, intensidad 1.5, posición (2, 5, 3)

Resolución de cámaras

flowchart TD
    START["mount(scene)"] --> TRAVERSE["Traverse scene graph"]
    TRAVERSE --> FOUND{"¿Encontró Camera?"}
    FOUND -->|"Sí"| USE["Usar primera como activa"]
    FOUND -->|"No"| FALLBACK["Fallback: PerspectiveCamera, FOV 75, z=5"]

Ejemplo completo

import { Scene, Node, createBox, Material, Camera, CameraType } from '@joroya/core';
import { ThreeRenderer } from '@joroya/renderer-three';

const scene = new Scene();

const cam = new Node('cam');
cam.addComponent(new Camera({
  type: CameraType.Perspective, fov: 75,
  aspect: window.innerWidth / window.innerHeight, near: 0.1, far: 1000,
}));
cam.transform.position.z = 5;
scene.add(cam);

const box = new Node('box');
box.addComponent(createBox(1, 1, 1));
box.addComponent(new Material({ color: { r: 0.2, g: 0.6, b: 1.0 } }));
scene.add(box);

const renderer = new ThreeRenderer({
  canvas: document.getElementById('canvas') as HTMLCanvasElement,
  width: window.innerWidth, height: window.innerHeight,
});
renderer.mount(scene);

function loop() {
  box.transform.rotation.y = performance.now() * 0.001;
  box.transform.updateLocalMatrix();
  renderer.render();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

@joroya/renderer-svg — SVG (2D)

Renderer ligero que genera markup SVG. Ideal para arte generativo, exportación vectorial y server-side rendering.

renderToSVG — String puro (server-safe)

Función pura y stateless que retorna un string SVG. Funciona en Node.js sin DOM.

import { renderToSVG } from '@joroya/renderer-svg';

const svg: string = renderToSVG(scene, { width: 400, height: 300 });

Opciones (SvgRenderOptions)

OpciónTipoDefaultDescripción
widthnumber(requerido)Ancho del SVG
heightnumber(requerido)Alto del SVG
viewBoxstring"0 0 {width} {height}"viewBox personalizado

renderToSVGElement — DOM interactivo

Crea un SVGSVGElement real con event delegation. Los nodos con componente Interactive reciben listeners de pointer/click/wheel.

import { renderToSVGElement } from '@joroya/renderer-svg';

const { svg, dispose } = renderToSVGElement(scene, {
  width: 800,
  height: 600,
  container: document.getElementById('app')!,
});

// Cuando ya no se necesite:
dispose(); // Limpia listeners y remueve el SVG del DOM

Opciones (SvgElementRenderOptions)

Extiende SvgRenderOptions con:

OpciónTipoDescripción
containerHTMLElement(opcional) Elemento padre donde se adjunta el SVG automáticamente

Retorno

CampoTipoDescripción
svgSVGSVGElementEl elemento SVG creado
dispose() => voidLimpia event listeners y remueve el SVG del DOM

Eventos interactivos soportados

Evento DOMInteractionEventType
clickClick
pointerdownPointerDown
pointerupPointerUp
pointermovePointerMove
pointerenterPointerEnter
pointerleavePointerLeave
wheelWheel

Pipeline

flowchart TD
    START["renderToSVG / renderToSVGElement"] --> UPDATE["scene.updateWorldMatrices()"]
    UPDATE --> WALK["Recorrer árbol recursivamente"]
    WALK --> GEO{"¿Geometry?"}
    GEO -->|"Path2D"| PATH["→path"]
    GEO -->|"Box"| RECT["→rect"]
    GEO -->|"Sphere"| CIRCLE["→circle"]
    GEO -->|"Text"| TEXT["→text"]
    GEO -->|"Ninguno"| GROUP["Solo g si tiene hijos"]
    PATH & RECT & CIRCLE & TEXT --> MAT{"¿Material?"}
    MAT -->|"fill/stroke"| STYLE["fill + stroke + opacity"]
    MAT -->|"fillGradient"| GRAD["url(#gradient-id) + defs"]
    MAT -->|"filter/clip/mask"| FILT["url(#filter-id) + defs"]
    MAT -->|"Ninguno"| NONE["fill='none'"]
    STYLE & GRAD & FILT & NONE --> ANIM{"¿Animation?"}
    ANIM -->|"Sí"| ANIMC["animate / animateTransform hijos"]
    ANIM -->|"No"| NOANIM["Sin animación"]
    ANIMC & NOANIM --> TRANSFORM{"¿Transform ≠ identity?"}
    TRANSFORM -->|"Sí"| MATRIX["g transform='matrix(a,b,c,d,e,f)'"]
    TRANSFORM -->|"No"| DIRECT["Elemento directo"]
    MATRIX & DIRECT --> CHILDREN{"¿Hijos?"}
    CHILDREN -->|"Sí"| NEST["Anidar en g"]
    CHILDREN -->|"No"| LEAF["Nodo hoja"]

Soporte de geometrías

GeometríaElemento SVG generado
Path2D<path d="...">
Box<rect> (width × height, depth ignorado)
Sphere<circle> (radio)
Text<text> con font-size, font-family, font-weight, text-anchor, dominant-baseline

Propiedades del material para SVG

CampoTipoEfecto SVGSi ausente
fillColorRGBfill="rgb(R,G,B)"fill="none"
strokeColorRGBstroke="rgb(R,G,B)"Sin stroke
strokeWidthnumberstroke-width="N"1
opacitynumberopacity="N"Sin atributo (opaco)
fillGradientGradientDeffill="url(#id)" + <defs>Usa fill normal
strokeGradientGradientDefstroke="url(#id)" + <defs>Usa stroke normal
filterSvgFilterDeffilter="url(#id)" + <filter> en <defs>Sin filtro
clipPathSvgClipPathDefclip-path="url(#id)" + <clipPath> en <defs>Sin recorte
maskSvgMaskDefmask="url(#id)" + <mask> en <defs>Sin máscara

Transforms y jerarquía

El renderer SVG aplica el localMatrix de cada nodo como atributo transform="matrix(a,b,c,d,e,f)" y genera <g> para representar la jerarquía padre-hijo del scene graph.

const parent = new Node('group');
parent.transform.position = { x: 100, y: 50, z: 0 };

const child = new Node('square');
child.addComponent(createBox(30, 30, 0));
child.addComponent(new Material({ fill: { r: 1, g: 0, b: 0 } }));

parent.add(child);
scene.add(parent);

Genera:

<g transform="matrix(1,0,0,1,100,50)">
  <rect x="-15" y="-15" width="30" height="30" fill="rgb(255, 0, 0)" />
</g>

Gradientes

const circle = new Node('sun');
circle.addComponent(createSphere(80));
circle.addComponent(new Material({
  fillGradient: {
    type: 'radial',
    cx: 0.5, cy: 0.5, r: 0.5,
    stops: [
      { offset: 0, color: { r: 1, g: 1, b: 0 } },
      { offset: 1, color: { r: 1, g: 0.3, b: 0 }, opacity: 0.8 },
    ],
  },
}));

Tipos de gradiente:

TipoDefiniciónElemento SVG
linearLinearGradientDef (x1, y1, x2, y2)<linearGradient>
radialRadialGradientDef (cx, cy, r, fx, fy)<radialGradient>

Texto

const label = new Node('title');
label.addComponent(createText('Oroya Animate', {
  fontSize: 24,
  fontFamily: 'Inter',
  fontWeight: 'bold',
  textAnchor: 'middle',
}));
label.addComponent(new Material({ fill: { r: 0, g: 0, b: 0 } }));
label.transform.position = { x: 200, y: 30, z: 0 };
scene.add(label);

CSS Classes y IDs semánticos

Cada nodo puede tener un cssClass y/o cssId que se emiten como atributos class e id en los elementos SVG generados.

const node = new Node('highlight-box');
node.addComponent(createBox(100, 60, 0));
node.addComponent(new Material({ fill: { r: 1, g: 0.9, b: 0 } }));
node.cssClass = 'highlight animated';
node.cssId = 'main-callout';
scene.add(node);

Genera:

<rect id="main-callout" class="highlight animated" x="-50" y="-30" width="100" height="60" fill="rgb(255, 230, 0)" />

Cuando el nodo tiene hijos o transform, el atributo se aplica al <g> contenedor:

<g id="main-callout" class="highlight animated" transform="matrix(1,0,0,1,50,25)">
  <rect x="-50" y="-30" width="100" height="60" fill="rgb(255, 230, 0)" />
</g>

Serialización: cssClass y cssId se preservan en serialize() / deserialize().

Cámara ortográfica y viewBox

Si la escena contiene un nodo con OrthographicCameraDef, el renderer SVG calcula automáticamente el viewBox a partir del frustum de la cámara. Un viewBox explícito en las opciones tiene prioridad.

const cam = new Node('ortho-cam');
cam.addComponent(new Camera({
  type: CameraType.Orthographic,
  left: -400, right: 400,
  top: -300, bottom: 300,
  near: 0.1, far: 1000,
}));
scene.add(cam);

// viewBox se calcula como "-400 -300 800 600"
const svg = renderToSVG(scene, { width: 800, height: 600 });

La posición de la cámara se aplica como offset al viewBox:

cam.transform.position = { x: 50, y: 25, z: 0 };
// viewBox se calcula como "-350 -275 800 600"

SVG Filters, Clip Paths y Masks

El renderer soporta filtros SVG nativos, clip paths y máscaras a través de campos en MaterialDef.

Blur

const blurred = new Node('soft');
blurred.addComponent(createSphere(40));
blurred.addComponent(new Material({
  fill: { r: 0.5, g: 0.8, b: 1 },
  filter: { effects: [{ type: 'blur', stdDeviation: 3 }] },
}));

Genera:

<defs>
  <filter id="oroya-filter-0">
    <feGaussianBlur stdDeviation="3" />
  </filter>
</defs>
<circle cx="0" cy="0" r="40" fill="rgb(128, 204, 255)" filter="url(#oroya-filter-0)" />

Drop Shadow

new Material({
  fill: { r: 1, g: 0, b: 0 },
  filter: {
    effects: [{
      type: 'dropShadow', dx: 4, dy: 4,
      stdDeviation: 2, floodColor: '#333', floodOpacity: 0.6,
    }],
  },
});

Clip Path

new Material({
  fill: { r: 0, g: 1, b: 0 },
  clipPath: {
    path: [
      { command: 'M', args: [0, 0] },
      { command: 'L', args: [100, 0] },
      { command: 'L', args: [50, 100] },
      { command: 'Z', args: [] },
    ],
  },
});

Mask

new Material({
  fill: { r: 0, g: 0, b: 1 },
  mask: {
    path: [
      { command: 'M', args: [0, 0] },
      { command: 'L', args: [80, 0] },
      { command: 'L', args: [80, 80] },
      { command: 'Z', args: [] },
    ],
    fill: 'white',
    opacity: 0.8,
  },
});

SVG Animaciones nativas

El componente Animation permite agregar animaciones SVG declarativas (<animate> y <animateTransform>) que se ejecutan en el navegador sin JavaScript.

import { Animation } from '@joroya/core';

const circle = new Node('pulse');
circle.addComponent(createSphere(30));
circle.addComponent(new Material({ fill: { r: 1, g: 0, b: 0 } }));
circle.addComponent(new Animation([
  {
    type: 'animate',
    attributeName: 'opacity',
    values: '1;0.3;1',
    dur: '2s',
    repeatCount: 'indefinite',
  },
]));

Genera:

<circle cx="0" cy="0" r="30" fill="rgb(255, 0, 0)">
  <animate attributeName="opacity" values="1;0.3;1" dur="2s" repeatCount="indefinite" />
</circle>

Animaciones de transformación:

new Animation([
  {
    type: 'animateTransform',
    transformType: 'rotate',
    from: '0 50 50',
    to: '360 50 50',
    dur: '4s',
    repeatCount: 'indefinite',
  },
]);

Genera:

<animateTransform attributeName="transform" type="rotate"
  from="0 50 50" to="360 50 50" dur="4s" repeatCount="indefinite" />

fill=“freeze” mantiene el valor final después de que la animación termina, en lugar de revertir.

Nota: Las animaciones nativas solo aplican al renderer SVG. El renderer Three.js las ignora.

Ejemplo completo

const triangle = new Node('triangle');
triangle.addComponent(createPath2D([
  { command: 'M', args: [200, 50] },
  { command: 'L', args: [350, 250] },
  { command: 'L', args: [50, 250] },
  { command: 'Z', args: [] },
]));
triangle.addComponent(new Material({
  fill: { r: 0.2, g: 0.8, b: 0.4 },
  stroke: { r: 0, g: 0, b: 0 },
  strokeWidth: 2,
  opacity: 0.9,
}));
scene.add(triangle);

const svg = renderToSVG(scene, { width: 400, height: 300 });

Casos de uso

CasoVentaja
Exportar a .svgAbrir en Figma, Illustrator, Inkscape
Server-side renderingNode.js sin DOM
Arte generativoPatrones procedurales como vectores
ImpresiónEscalable sin pérdida
Interactividad SVGrenderToSVGElement con event delegation

Comparación entre renderers

Soporte de geometrías

GeometríaThree.jsSVG
Box<rect>
Sphere<circle>
Path2D<path>
Text<text>

Soporte de material

PropiedadThree.jsSVG
color
opacity
fill
stroke
strokeWidth
fillGradient
strokeGradient
filter
clipPath
mask

Soporte de transforms

FeatureThree.jsSVG
Position (translate)matrix()
Rotationmatrix()
Scalematrix()
Jerarquía (<g>)✅ Groups<g>

Soporte de componentes especiales

FeatureThree.jsSVG
Camera (Perspective)
Camera (Orthographic)✅ viewBox
Interactive (eventos)✅ Raycaster✅ Event delegation
Animation (SVG nativo)<animate> / <animateTransform>
cssClass / cssId✅ atributos class / id

Crear un renderer personalizado

El contrato es simple — implementar mount, render y dispose:

import { Scene, ComponentType, Geometry, Material, GeometryPrimitive } from '@joroya/core';

export class Canvas2DRenderer {
  private ctx: CanvasRenderingContext2D;
  private scene: Scene | null = null;

  constructor(canvas: HTMLCanvasElement) {
    this.ctx = canvas.getContext('2d')!;
  }

  mount(scene: Scene): void { this.scene = scene; }

  render(): void {
    if (!this.scene) return;
    this.scene.updateWorldMatrices();
    this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);

    this.scene.traverse(node => {
      const geo = node.getComponent<Geometry>(ComponentType.Geometry);
      if (!geo) return;
      const mat = node.getComponent<Material>(ComponentType.Material);
      const wm = node.transform.worldMatrix;

      this.ctx.save();
      this.ctx.translate(wm[12], wm[13]);

      if (geo.definition.type === GeometryPrimitive.Box) {
        const { width, height } = geo.definition;
        if (mat?.definition.color) {
          const c = mat.definition.color;
          this.ctx.fillStyle = `rgb(${c.r*255},${c.g*255},${c.b*255})`;
        }
        this.ctx.fillRect(-width/2, -height/2, width, height);
      }
      this.ctx.restore();
    });
  }

  dispose(): void { this.scene = null; }
}

Checklist

PasoDescripción
1Crear paquete en packages/renderer-xxx/
2Agregar @joroya/core como dependencia
3Implementar mount() — recorrer árbol y crear objetos
4Implementar render() — sincronizar transforms y dibujar
5Implementar dispose() — liberar recursos
6Documentar geometrías y materiales soportados