K3 ENGINE v0.3.0
Versión: 0.3.0
Estado: Blueprint de Desarrollo
Última actualización: Abril 2026
Autor: Santiago Bustelo
K3 Engine is licensed under the Apache License, Version 2.0. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0
Resumen
K3 Engine es un motor de renderizado 3D experimental, exclusivamente CSS, para el navegador. Explora los límites de lo que se puede lograr usando HTML y CSS puros (sin WebGL, sin canvas) aprovechando las transformaciones 3D de CSS para construir objetos y escenas tridimensionales interactivas.
El proyecto nació de una pregunta simple: dados los asombrosos demos CSS producidos por la comunidad de programación creativa, ¿sería posible construir un motor 3D entero solo con CSS?
Inspiraciones y Agradecimientos
K3 se apoya sobre los hombros de la comunidad de arte y demos CSS. Los siguientes autores y sus trabajos publicados fueron inspiraciones directas para este proyecto, y sus técnicas informaron muchas de las decisiones arquitectónicas tomadas a lo largo del motor.
- Ricardo Oliva Alonso
- Ben Evans
- Josetxu
- Tamara Littlewood
- Jhey Tompkins
- Alvaro Montoro
- Ana Tudor
- S. Shahriar
- Sarah Fossheim
- Temani Afif
- Adam Kuhn
- Julia Miocene
- Burank Can
Su trabajo colectivo (que abarca trucos de perspectiva, magia con transform-origin, CSS matemático y animación con CSS puro) demostró que la capa de estilos del navegador puede ser una superficie de renderizado mucho más expresiva de lo que se le suele reconocer.
ÍNDICE DE CONTENIDOS
- Visión y Filosofía
- Análisis del Estado Actual
- Panorama de la Arquitectura
- Especificaciones de Componentes
- Contrato de API v1.0
- Suite de Pruebas y Criterios de Aceptación
- Limitaciones Conocidas
1. VISIÓN Y FILOSOFÍA
1.1 Qué es K3
K3 es un sistema 3D declarativo para objetos que viven DENTRO del documento, no separados de él.
Principios Fundamentales:
- DOM-primero: los objetos 3D son elementos HTML, no texturas de canvas
- Declarativo: el estado se describe, no se construye imperativamente
- Honesto: las limitaciones se documentan, no se ocultan
- Educativo: el código enseña, no solo funciona
1.2 Qué NO es K3
- ❌ Un renderizador fotorrealista (sin PBR, sin raytracing)
- ❌ Un motor de juegos para títulos AAA (sin sombras, sin más de 10.000 objetos)
- ❌ Un wrapper de WebGL (CSS 3D puro)
- ❌ Un hack rápido (esto es publicado, versionado, mantenido)
1.3 Público Objetivo
Primario:
- Desarrolladores frontend que necesitan 3D en documentos
- Educadores creando contenido STEM interactivo
- Redactores técnicos construyendo manuales interactivos
Secundario:
- Diseñadores prototipando interfaces 3D
- Agencias construyendo configuradores de producto (estilizados)
- Desarrolladores indies (flat-shading, estilo Monument Valley)
1.4 Propuesta de Valor
Three.js: basado en canvas, fotorrealista, desconectado del DOM
K3: basado en DOM, estilizado, integrado con HTML
Cuándo usar K3:
- Documentación técnica con diagramas 3D
- Educación científica (moléculas, física, anatomía)
- Diagramas de arquitectura (diseño de sistemas)
- Personalización de productos (simple, estilizada)
- Juegos indies (estética flat-shading)
Cuándo usar Three.js:
- Renderizado fotorrealista
- Escenas grandes (1000+ objetos)
- Iluminación/sombras avanzadas
- Aplicaciones críticas de rendimiento
2. ANÁLISIS DEL ESTADO ACTUAL
2.1 Qué EXISTE Hoy (según dump.txt)
✅ Clases Núcleo:
K3Element(clase base con TRS)K3Scene(contenedor con perspectiva)K3Group(jerarquía de transformaciones)K3Definition(registro de plantillas)K3Use(instanciación con Shadow DOM)
✅ Primitivas:
K3Plane(cara única)K3Box(6 caras con esquinas redondeadas)K3Circle(círculo billboard)K3Sphere(impostor 2.5D con mapeo de textura)K3Stack(extrusión con morphing)K3Extrude(proyección lineal para SVG/texto)
✅ Sistemas:
- Sistema de transformaciones TRS (orden Translate-Rotate-Scale)
- Bus de señales (rigging con k3-signal/k3-axis/k3-range)
- Sistema de glare (sombreado procedural)
- Interacción básica (rotación de cámara)
✅ Editor (HARDCODEADO):
- Lógica de selección (modo root vs leaf)
- Sincronización de gizmo (bounds visuales)
- UI del inspector (sliders de propiedades)
- ❌ PROBLEMA: manipulación directa del DOM, sin uso de API
2.2 Qué está ROTO/FALTANTE
❌ Sin API Pública:
// Estos no existen:
K3.create()
K3.update()
K3.getState()
K3.serialize()
❌ Sin MutationObserver:
- Los cambios de atributos no disparan re-render
- El DOM es "fuente de verdad" solo al cargar
❌ Sin Manejo de Errores:
- Los atributos inválidos fallan silenciosamente
- Sin validación contra restricciones del esquema
- Sin eventos
k3:error
❌ Sin Propiedades Generativas:
<!-- Esto no funciona: -->
<k3-use type="gear" teeth="20"></k3-use>
<!-- Cambiar teeth="30" no reconstruye la geometría -->
❌ Sin Snapshots de Estado:
- No se puede serializar el estado actual
- Sin base para undo/redo
❌ El Editor es un Anti-patrón:
- Muestra lo que NO hay que hacer
- No puede usarse como código de ejemplo
3. PANORAMA DE LA ARQUITECTURA
3.1 Capas del Sistema
┌─────────────────────────────────────────────┐
│ CÓDIGO DE USUARIO (HTML/JS) │
│ <k3-scene>, K3.create(), K3.update() │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ CAPA DE API PÚBLICA (v1.0 NUEVA) │
│ - Creación (create, instantiate, clone) │
│ - Gestión de Estado (get/set/update) │
│ - Consultas (pick, bounds, hierarchy) │
│ - Serialización (export/import) │
│ - Validación (constraints) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ MOTOR NÚCLEO (existe, necesita refactor) │
│ - K3Element (sistema TRS) │
│ - K3Definition (registro de plantillas) │
│ - K3Use (instanciación Shadow DOM) │
│ - Bus de Señales (rigging) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ PRIMITIVAS (existen, necesitan generativas) │
│ - K3Plane, K3Box, K3Sphere │
│ - K3Stack, K3Extrude │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ NAVEGADOR (Transformaciones CSS 3D) │
│ - preserve-3d │
│ - matrix3d() │
│ - backface-visibility │
└─────────────────────────────────────────────┘
3.2 Flujo de Datos
ACCIÓN DEL USUARIO → API PÚBLICA → VALIDACIÓN → PIPELINE DE ACTUALIZACIÓN → DOM → CSS → RENDER
Ejemplo: K3.update('box', { x: 100 })
↓
1. Validar: ¿x es un número? ¿Está dentro de las restricciones?
2. Aplicar: element.setAttribute('x', 100)
3. Transformar: element.updateTransform()
4. Evento: K3.emit('k3:change', { id, changes })
5. Render: el navegador aplica matrix3d()
3.3 Gestión de Estado
// EL ESTADO VIVE EN 3 LUGARES:
// 1. DOM (fuente de verdad)
<k3-box x="100" y="50" z="0"></k3-box>
// 2. Caché interna (rendimiento)
K3._stateManager._cache = {
'box-1': {
position: {x: 100, y: 50, z: 0},
rotation: {rx: 0, ry: 0, rz: 0},
bounds: {...},
_dirty: false
}
}
// 3. Computado (bajo demanda)
const state = K3.getState('box-1');
// Devuelve un snapshot congelado
CRÍTICO: el DOM es la fuente de verdad, la caché es optimización.
Las mutaciones directas del DOM se consideran fuera de contrato.
Ver: Addendum 01 — Política de Mutación del DOM
4. ESPECIFICACIONES DE COMPONENTES
4.1 K3.API (NUEVA - v1.0)
Ubicación: js/engine/k3-api.js
/**
* API Pública de K3
* La ÚNICA forma en que los usuarios deberían interactuar con K3.
*/
window.K3 = {
// === CREACIÓN ===
/**
* Crea un elemento primitivo
* @param {string} type - 'k3-plane'|'k3-box'|'k3-sphere'|'k3-stack'|'k3-extrude'
* @param {object} props - Propiedades iniciales
* @param {Element} parent - Padre opcional al que anexar
* @returns {Element} Elemento creado con ID autogenerado
*/
create(type, props = {}, parent = null) {
const element = document.createElement(type);
element.id = props.id || `k3-${type}-${Date.now()}`;
// Aplicar props iniciales
for (const [key, value] of Object.entries(props)) {
if (key !== 'id') {
element.setAttribute(key, value);
}
}
// Inicializar
if (element.render) {
element.render();
}
if (parent) {
parent.appendChild(element);
}
K3.emit('k3:create', { id: element.id, type });
return element;
},
/**
* Instancia una plantilla registrada
* @param {string} templateName - Nombre de <k3-object name="...">
* @param {object} props - Overrides de propiedades
* @param {Element} parent - Padre opcional
* @returns {Promise<Element>} Resuelve cuando la plantilla se cargó
*/
async instantiate(templateName, props = {}, parent = null) {
// Esperar la plantilla si no está cargada
if (!K3.registry.has(templateName)) {
await new Promise(resolve => {
const handler = (e) => {
if (e.detail.name === templateName) {
document.removeEventListener('k3-def-registered', handler);
resolve();
}
};
document.addEventListener('k3-def-registered', handler);
});
}
const use = document.createElement('k3-use');
use.setAttribute('type', templateName);
use.id = props.id || `k3-${templateName}-${Date.now()}`;
// Aplicar props (variables CSS + atributos)
for (const [key, value] of Object.entries(props)) {
if (key.startsWith('--')) {
use.style.setProperty(key, value);
} else if (key !== 'id') {
use.setAttribute(key, value);
}
}
if (parent) {
parent.appendChild(use);
}
// Disparar inicialización
if (use.initInstance) {
use.initInstance();
}
K3.emit('k3:create', { id: use.id, type: templateName });
return use;
},
/**
* Clona un elemento existente
* @param {string} sourceId - ID del elemento a clonar
* @param {object} props - Overrides de propiedades
* @returns {Element} Elemento nuevo
*/
clone(sourceId, props = {}) {
const source = document.getElementById(sourceId);
if (!source) throw new Error(`Elemento ${sourceId} no encontrado`);
const clone = source.cloneNode(true);
clone.id = props.id || `${sourceId}-clone-${Date.now()}`;
// Aplicar overrides
for (const [key, value] of Object.entries(props)) {
if (key !== 'id') {
clone.setAttribute(key, value);
}
}
return clone;
},
// === GESTIÓN DE ESTADO ===
/**
* Obtener snapshot inmutable del estado del elemento
* @param {string} id - ID del elemento
* @returns {object} Objeto de estado congelado
*/
getState(id) {
return K3._stateManager.getState(id);
},
/**
* Restaurar el elemento a un estado previo
* @param {string} id - ID del elemento
* @param {object} state - Estado de getState()
*/
setState(id, state) {
K3._stateManager.setState(id, state);
},
/**
* Actualizar propiedades del elemento (EL MÉTODO NÚCLEO)
* @param {string} id - ID del elemento
* @param {object} changes - Propiedades a cambiar
* @param {object} options - { immediate: boolean, force: boolean }
* @returns {object} { success: boolean, errors: array, applied: object }
*/
update(id, changes, options = {}) {
return K3._updatePipeline.update(id, changes, options);
},
/**
* Actualizar múltiples elementos en lote atómicamente
* @param {array} updates - [{id, changes}, ...]
* @returns {array} Resultados de cada actualización
*/
batch(updates) {
return K3._updatePipeline.batch(updates);
},
/**
* Forzar re-sincronización desde el DOM (usar con moderación)
* @param {string} id - ID del elemento
*/
sync(id) {
K3._stateManager.markDirty(id);
K3._stateManager.getState(id); // Fuerza recomputar
},
// === HELPERS DE TRANSFORMACIÓN ===
setPosition(id, {x, y, z}) {
return K3.update(id, {x, y, z});
},
setRotation(id, {rx, ry, rz}) {
return K3.update(id, {rx, ry, rz});
},
setScale(id, scale) {
if (typeof scale === 'number') {
return K3.update(id, {scale});
}
return K3.update(id, scale); // {sx, sy, sz}
},
translate(id, delta) {
const state = K3.getState(id);
return K3.update(id, {
x: state.position.x + (delta.x || 0),
y: state.position.y + (delta.y || 0),
z: state.position.z + (delta.z || 0)
});
},
rotate(id, delta) {
const state = K3.getState(id);
return K3.update(id, {
rx: state.rotation.rx + (delta.rx || 0),
ry: state.rotation.ry + (delta.ry || 0),
rz: state.rotation.rz + (delta.rz || 0)
});
},
// === CONSULTAS ===
/**
* Obtener bounding box (local + world)
* @param {string} id - ID del elemento
* @returns {object} { local: {...}, world: {...} }
*/
getBounds(id) {
return K3.getState(id).bounds;
},
/**
* Obtener restricciones del esquema
* @param {string} id - ID del elemento
* @returns {object} { position: {...}, rotation: {...}, forbidden: [...] }
*/
getConstraints(id) {
return K3._constraintSystem.getConstraints(id);
},
/**
* Raycast para encontrar el elemento en coordenadas de pantalla
* @param {number} x - clientX
* @param {number} y - clientY
* @param {object} options - { mode: 'root'|'leaf', filter: fn }
* @returns {Element|null}
*/
pick(x, y, options = {}) {
return K3._spatialQuery.pick(x, y, options);
},
/**
* Consultar elementos con filtros
* @param {string} selector - Selector CSS
* @param {object} filters - { type: string, bounds: {...} }
* @returns {Element[]}
*/
query(selector, filters = {}) {
return K3._spatialQuery.query(selector, filters);
},
/**
* Encontrar todas las instancias de un tipo
* @param {string} type - Nombre de plantilla
* @returns {Element[]}
*/
findByType(type) {
return Array.from(document.querySelectorAll(`k3-use[type="${type}"]`));
},
/**
* Obtener hijos K3 de un elemento
* @param {string} id - ID del elemento padre
* @param {boolean} recursive - Incluir descendientes
* @returns {string[]} Array de IDs de hijos
*/
findChildren(id, recursive = false) {
return K3._hierarchyManager.getChildren(id, recursive);
},
// === SERIALIZACIÓN ===
/**
* Exportar elemento a string HTML
* @param {string} id - ID del elemento
* @returns {string} Fragmento HTML
*/
serialize(id) {
return K3._serializer.toHTML(id);
},
/**
* Crear elemento a partir de string HTML
* @param {string} html - Fragmento HTML
* @returns {Element|Element[]}
*/
deserialize(html) {
return K3._serializer.fromHTML(html);
},
/**
* Exportar en distintos formatos
* @param {string} id - ID del elemento
* @param {string} format - 'html'|'json'
* @returns {string|object}
*/
export(id, format = 'html') {
if (format === 'html') return K3.serialize(id);
if (format === 'json') return K3._serializer.toJSON(id);
throw new Error(`Formato desconocido: ${format}`);
},
/**
* Importar desde distintos formatos
* @param {string|object} data - Datos a importar
* @param {string} format - 'html'|'json'
* @returns {Element|Element[]}
*/
import(data, format = 'html') {
if (format === 'html') return K3.deserialize(data);
if (format === 'json') return K3._serializer.fromJSON(data);
throw new Error(`Formato desconocido: ${format}`);
},
// === VALIDACIÓN ===
/**
* Validar un valor antes de aplicarlo
* @param {string} id - ID del elemento
* @param {string} attr - Nombre del atributo
* @param {any} value - Valor a validar
* @returns {object} { valid: boolean, corrected: any, error: string }
*/
validate(id, attr, value) {
const element = document.getElementById(id);
return K3._validator.validate(element, attr, value);
},
/**
* Verificar si el elemento viola restricciones
* @param {string} id - ID del elemento
* @returns {object} { valid: boolean, violations: array }
*/
checkConstraints(id) {
return K3._constraintSystem.check(id);
},
// === SEÑALES (RIGGING) ===
signals: {
emit(target, name, value) {
const event = new CustomEvent('k3-signal', {
detail: { name, value: Math.max(0, Math.min(1, value)) },
bubbles: true
});
if (typeof target === 'string') {
target = document.getElementById(target);
}
target.dispatchEvent(event);
},
register(target, name, callback) {
if (typeof target === 'string') {
target = document.getElementById(target);
}
const handler = (e) => {
if (e.detail.name === name) {
callback(e.detail.value);
}
};
target.addEventListener('k3-signal', handler);
return () => target.removeEventListener('k3-signal', handler);
},
unregister(target, name) {
// El handler devuelto por register() ya es la limpieza
}
},
// === EVENTOS ===
/**
* Escuchar eventos de K3
* @param {string} event - 'k3:ready'|'k3:change'|'k3:create'|'k3:error'
* @param {function} callback - Handler
*/
on(event, callback) {
document.addEventListener(event, callback);
},
off(event, callback) {
document.removeEventListener(event, callback);
},
once(event, callback) {
const handler = (e) => {
callback(e);
document.removeEventListener(event, handler);
};
document.addEventListener(event, handler);
},
/**
* Emitir evento K3
* @param {string} event - Nombre del evento
* @param {object} detail - Datos del evento
*/
emit(event, detail) {
document.dispatchEvent(new CustomEvent(event, { detail }));
},
// === REGISTRO (SOLO LECTURA) ===
registry: new Proxy({}, {
get(target, prop) {
if (prop === 'has') {
return (name) => window.K3._internalRegistry.has(name);
}
if (prop === 'get') {
return (name) => window.K3._internalRegistry.get(name);
}
if (prop === 'list') {
return () => Array.from(window.K3._internalRegistry.keys());
}
return undefined;
},
set() {
throw new Error('K3.registry es de solo lectura');
}
}),
// Registro interno (no expuesto)
_internalRegistry: new Map(),
// === CONFIGURACIÓN ===
config: {
coordSystem: 'native', // 'native' o 'cartesian'
maxSlices: 100,
debug: false,
snapToGrid: false,
gridSize: 10
},
// === UTILS ===
utils: {
normalizeAngle(degrees) {
let angle = degrees % 360;
if (angle < 0) angle += 360;
return angle;
},
lerpColor(c1, c2, t) {
// Implementación desde ColorUtils
return ColorUtils.lerpColor(
ColorUtils.parse(c1),
ColorUtils.parse(c2),
t
);
},
degToRad(deg) {
return deg * Math.PI / 180;
},
radToDeg(rad) {
return rad * 180 / Math.PI;
}
}
};
// Inicializar sistemas internos
K3._stateManager = new K3StateManager();
K3._updatePipeline = new K3UpdatePipeline();
K3._validator = new K3Validator();
K3._constraintSystem = new K3ConstraintSystem();
K3._spatialQuery = new K3SpatialQuery();
K3._hierarchyManager = new K3HierarchyManager();
K3._serializer = new K3Serializer();
// Congelar superficie pública
Object.freeze(K3.create);
Object.freeze(K3.update);
Object.freeze(K3.getState);
// ... congelar todos los métodos públicos
4.2 K3StateManager (NUEVA - v1.0)
Ubicación: js/engine/core/k3-state-manager.js
/**
* Gestor de Estado de K3
* Administra snapshots de estado y seguimiento de "sucios".
*/
export class K3StateManager {
constructor() {
this._cache = new Map(); // id -> state
this._dirty = new Set(); // ids que necesitan recomputarse
}
/**
* Obtener snapshot inmutable del estado
* @param {string} id - ID del elemento
* @returns {object} Estado congelado
*/
getState(id) {
const element = document.getElementById(id);
if (!element) {
throw new Error(`Elemento ${id} no encontrado`);
}
// Devolver caché si no está sucio
if (!this._dirty.has(id) && this._cache.has(id)) {
return this._cache.get(id);
}
// Computar estado fresco
const state = this._computeState(element);
this._cache.set(id, state);
this._dirty.delete(id);
return state;
}
/**
* Restaurar el elemento al estado
* @param {string} id - ID del elemento
* @param {object} state - Estado previo
*/
setState(id, state) {
const element = document.getElementById(id);
if (!element) {
throw new Error(`Elemento ${id} no encontrado`);
}
// Aplicar posición
if (state.position) {
element.setAttribute('x', state.position.x);
element.setAttribute('y', state.position.y);
element.setAttribute('z', state.position.z);
}
// Aplicar rotación
if (state.rotation) {
element.setAttribute('rx', state.rotation.rx);
element.setAttribute('ry', state.rotation.ry);
element.setAttribute('rz', state.rotation.rz);
}
// Aplicar escala
if (state.scale) {
if (state.scale.uniform !== undefined) {
element.setAttribute('scale', state.scale.uniform);
} else {
element.setAttribute('sx', state.scale.sx);
element.setAttribute('sy', state.scale.sy);
element.setAttribute('sz', state.scale.sz);
}
}
// Aplicar geometría (si cambió y es generativa)
if (state.geometry) {
for (const [key, value] of Object.entries(state.geometry)) {
element.setAttribute(key, value);
}
}
// Aplicar apariencia
if (state.appearance) {
for (const [key, value] of Object.entries(state.appearance)) {
element.setAttribute(key, value);
}
}
// Actualizar transformación
if (element.updateTransform) {
element.updateTransform();
}
this.markDirty(id);
}
/**
* Marcar el elemento como que necesita recomputarse
* @param {string} id - ID del elemento
*/
markDirty(id) {
this._dirty.add(id);
// Marcar hijos sucios (las transformaciones se propagan)
const children = this._getK3Children(id);
children.forEach(childId => this._dirty.add(childId));
}
/**
* Computar estado fresco desde el DOM
* @private
*/
_computeState(element) {
const id = element.id;
const tagName = element.tagName.toLowerCase();
// Estado base
const state = {
id,
tagName,
type: element.getAttribute('type') || null,
position: {
x: parseFloat(element.getAttribute('x') || 0),
y: parseFloat(element.getAttribute('y') || 0),
z: parseFloat(element.getAttribute('z') || 0)
},
rotation: {
rx: parseFloat(element.getAttribute('rx') || 0),
ry: parseFloat(element.getAttribute('ry') || 0),
rz: parseFloat(element.getAttribute('rz') || 0)
},
scale: {
sx: parseFloat(element.getAttribute('sx') || element.getAttribute('scale') || 1),
sy: parseFloat(element.getAttribute('sy') || element.getAttribute('scale') || 1),
sz: parseFloat(element.getAttribute('sz') || element.getAttribute('scale') || 1),
uniform: parseFloat(element.getAttribute('scale') || 1)
}
};
// Geometría (específica por tipo)
state.geometry = this._getGeometry(element);
// Apariencia
state.appearance = this._getAppearance(element);
// Bounds
state.bounds = this._computeBounds(element);
// Esquema (para k3-use)
if (tagName === 'k3-use' && state.type) {
const def = window.K3._internalRegistry.get(state.type);
state.schema = def?.schema || null;
}
// Restricciones
state.constraints = this