Publicado por

Theremin Glitch: proceso creativo y desarrollo

Publicado por

Theremin Glitch: proceso creativo y desarrollo

Theremin Glitch es una aplicación audiovisual interactiva que transforma la inclinación del dispositivo móvil en sonido y visuales generativos. El proyecto explora…
Theremin Glitch es una aplicación audiovisual interactiva que transforma la inclinación del dispositivo móvil en sonido y visuales generativos.…

Theremin Glitch es una aplicación audiovisual interactiva que transforma la inclinación del dispositivo móvil en sonido y visuales generativos. El proyecto explora la relación entre gesto físico, síntesis sonora y representación visual en tiempo real, inspirándose en el funcionamiento del theremin tradicional, un instrumento que se toca sin contacto.

Repositorio de GitHub: https://github.com/dimecris/theremin-app

La app se puede probar en modo simulación moviendo el ratón.

Proceso de ideación

La idea del proyecto surgió a partir de la voluntad de trabajar con el movimiento del dispositivo como interfaz principal, evitando controles tradicionales como botones o sliders. El theremin apareció como una referencia clara, ya que plantea una relación directa entre gesto y sonido sin mediación física.

Desde el inicio se planteó la aplicación no como un instrumento musical preciso, sino como una experiencia experimental y sensorial. El objetivo era priorizar la exploración libre, donde cualquier gesto produjera una respuesta sonora y visual coherente, incluso imprevisible.

Proceso de diseño y prototipado

Antes de llegar a la versión final de la aplicación, se realizó una fase de exploración visual y funcional mediante bocetos y wireframes. El objetivo de esta etapa fue definir la relación entre la interfaz, el gesto físico del usuario y la respuesta audiovisual de la aplicación.

En una primera fase se trabajó con bocetos de baja fidelidad, centrados en la disposición de los elementos principales: botón de inicio, selección de tipo de onda y representación gráfica del sonido. Estos bocetos permitieron validar rápidamente la jerarquía visual y la claridad de la interacción sin entrar aún en decisiones estéticas.

Boceto inicial con exploración de ondas continuas y partículas como feedback visual.

 

Variación del boceto inicial, probando cambios en la densidad y posición de las partículas.

Posteriormente se desarrollaron wireframes más estructurados, en los que se definieron con mayor precisión los controles disponibles y su comportamiento. En esta fase se exploraron distintas representaciones de las ondas según el tipo de oscilador (senoidal, cuadrada, sierra y triangular), evaluando su legibilidad y coherencia visual.

Wireframe con representación de onda senoidal y controles básicos de interacción.

 

Wireframe con onda cuadrada, utilizado para evaluar diferencias visuales entre tipos de onda.

Este proceso de prototipado permitió tomar decisiones clave antes de la implementación final, como simplificar la interfaz para no interferir con la experiencia sonora; mantener el inglés, ya que la nomenclatura del tipo de onda era más precisa, corta y se adaptaba mejor a las dimensiones de la pantalla; mantener un número reducido de controles visibles y priorizar una estética minimalista y atmosférica que reforzara la relación entre gesto, sonido y visualización.

Desarrollo técnico y estructura

El desarrollo se basó en una arquitectura modular que separa claramente las responsabilidades del proyecto, facilitando la iteración y el mantenimiento del código. Cada módulo se encarga de una parte específica de la aplicación:

  • motion.js: lectura de los sensores de orientación mediante Capacitor
  • audio.js: síntesis sonora y cuantización musical
  • sketch.js: visualización generativa con p5.js
  • storage.js: persistencia de configuración con LocalStorage
  • main.js: módulo orquestador que coordina el flujo general

El proyecto se desarrolló con Vite como entorno de desarrollo y Capacitor como puente para acceder a funcionalidades nativas del dispositivo, como los sensores de movimiento y la vibración.

Stack tecnológico y plugins utilizados

La aplicación se desarrolló utilizando un stack basado en tecnologías web, priorizando herramientas ya trabajadas durante la asignatura y compatibles con el uso de sensores y audio en dispositivos móviles.

  • Vite: entorno de desarrollo y herramienta de build, utilizado por su rapidez y simplicidad en proyectos JavaScript modernos.
  • Capacitor: framework empleado para empaquetar la aplicación web como app nativa y acceder a funcionalidades del dispositivo.
  • @capacitor/motion: plugin oficial para la lectura de sensores de orientación e inclinación.
  • @capacitor/haptics: plugin utilizado para proporcionar feedback táctil mediante vibración.
  • p5.js: librería para la visualización generativa y el trabajo con canvas.
  • p5.sound: librería de síntesis sonora integrada en p5.js.

Este conjunto de tecnologías permitió desarrollar una aplicación experimental manteniendo una base web y extendiéndola a un contexto móvil sin necesidad de aprender un lenguaje nativo específico.

Proceso de desarrollo y workflow

El desarrollo del proyecto siguió un flujo de trabajo iterativo, alternando pruebas en navegador y pruebas en dispositivo móvil. Durante la fase de desarrollo se utilizó el servidor de desarrollo de Vite para iterar rápidamente sobre el código.

Una vez alcanzada una versión funcional, la aplicación se compiló y sincronizó con Capacitor para su ejecución en Android, siguiendo el flujo recomendado:

npm run build

npx cap sync

npx cap open android

Este proceso permitió comprobar el comportamiento real de los sensores, el audio y el rendimiento de la aplicación en un entorno nativo (Medium Phone API 36.1, Android 16 “Backlava” arm64), detectando diferencias respecto al comportamiento en escritorio.

La separación entre entorno de desarrollo (navegador) y entorno de ejecución (dispositivo móvil) fue clave para entender las limitaciones y particularidades del desarrollo web aplicado a apps móviles.

Decisiones técnicas relevantes

Durante el desarrollo surgió la duda de si gestionar el audio directamente con la Web Audio API o utilizar la librería  de p5.sound. Aunque la Web Audio API ofrece un control muy detallado y es perfectamente válida si se prefiere no añadir dependencias externas, finalmente se optó por p5.sound para mantener coherencia con el stack propuesto en el tutorial base y facilitar la integración con p5.js.

También se probó la librería Tone.js propuesta por el profesor, que es una opción moderna y muy potente para síntesis y manipulación de audio en la web. Tone.js destaca por su flexibilidad, su API orientada a músicos y su integración sencilla con proyectos JavaScript modernos. Sin embargo, para este proyecto concreto, se usó p5.sound que ya había trabajado en otras asignaturas.

Cabe destacar que la importación de p5.sound como módulo puede generar conflictos con sistemas de bundling como Vite. Por ello, se siguió la estrategia recomendada en el tutorial: cargar p5.js y p5.sound desde la carpeta public/ y acceder a ellas como variables globales. Esta solución evitó problemas de compilación y aseguró un comportamiento estable tanto en navegador como en la app empaquetada con Capacitor.

Consultas externas y documentación

A lo largo del proyecto se realizaron consultas a documentación externa y ejemplos de código para validar el uso de sensores, la gestión del audio en dispositivos móviles y el comportamiento de la aplicación en distintos entornos.

La documentación oficial de Capacitor fue especialmente relevante para comprender el funcionamiento del plugin Motion y las diferencias de comportamiento entre plataformas, especialmente en lo relativo a permisos en iOS frente a Android.

Pruebas, problemas y mejoras

Uno de los principales problemas detectados en las primeras versiones fue el carácter poco armónico del sonido al mapear directamente valores continuos de inclinación a frecuencia. Para solucionarlo, se implementó un sistema de cuantización a una escala pentatónica mayor, que permitió obtener un resultado más musical sin perder expresividad.

Otro reto importante fue poder desarrollar y depurar la aplicación sin depender constantemente de un dispositivo móvil. Para ello se incorporó un modo de depuración en escritorio que simula la inclinación del dispositivo mediante el ratón.

La parte visual también se fue refinando progresivamente. El sistema inicial de partículas resultaba demasiado errático, por lo que se ajustaron parámetros como la fricción, el uso de ruido Perlin y la sensibilidad al movimiento, logrando un comportamiento más orgánico y controlado.

La app se ha probado en IOS (con Xcode y con un dispositivo real) pero sin éxito. Incluso en el navegador Safari se probó, pero no funciona correctamente. Se detectó un error: se pasaba un NaN a la frecuencia en audio.js. Queda pendiente para su implementación y prueba en IOS en siguientes fases.

Funcionamiento e interacción

La interacción principal de la aplicación se basa en la inclinación del dispositivo móvil, que actúa como interfaz gestual. El usuario no manipula controles tradicionales, sino que modifica el sonido y la visualización a través del movimiento físico del teléfono.

En dispositivo móvil, el sistema interpreta la orientación del terminal en dos ejes principales:

  • Inclinación horizontal (eje X): controla la frecuencia del tono generado por el oscilador.
  • Inclinación vertical (eje Y): controla el brillo del sonido mediante un filtro pasa-bajos.

El inicio de la experiencia se realiza mediante un botón de activación que pone en marcha tanto los sensores como el audio. Este paso es necesario para cumplir con las políticas de los navegadores y sistemas operativos móviles, que requieren una interacción explícita del usuario para activar el AudioContext.

La aplicación permite detener el sonido en cualquier momento, pausando la síntesis sin perder el estado visual ni la configuración actual.

La configuración del sistema, como el tipo de onda o la sensibilidad del movimiento, se almacena localmente mediante LocalStorage. De este modo, la aplicación conserva las preferencias del usuario entre sesiones sin necesidad de configuración adicional.

Mapeo sensor–audio

El mapeo entre los sensores y los parámetros sonoros se diseñó de forma intencionadamente simple y legible, facilitando una relación directa entre gesto y resultado auditivo.

// Eje X (izquierda / derecha) → Frecuencia (200–1000 Hz)
tiltX → frequency (cuantizada a escala pentatónica mayor)

// Eje Y (adelante / atrás) → Filtro
tiltY → filterFrequency (400–1400 Hz)

La cuantización a escala pentatónica mayor permite evitar disonancias extremas y facilita que cualquier movimiento produzca un resultado sonoro coherente, manteniendo el carácter experimental de la aplicación.

Aprendizajes y conclusiones

El proyecto ha permitido comprender mejor las particularidades del desarrollo web aplicado a dispositivos móviles, especialmente en lo referente a sensores, permisos y restricciones de audio. También ha reforzado la importancia de una buena organización del código y de tomar decisiones técnicas coherentes con el contexto del proyecto.

Como conclusión, Theremin Glitch demuestra que es posible crear experiencias audiovisuales ricas utilizando tecnologías web estándar. La combinación de sensores, sonido y visualización generativa abre un amplio campo de experimentación artística e interactiva, con múltiples posibilidades de evolución futura.


Anexo técnico: instalación y despliegue

Este anexo recoge los pasos técnicos necesarios para crear el proyecto desde cero y desplegarlo en Android. Se incluye como referencia futura y como apoyo para comprender el entorno de desarrollo utilizado durante el proyecto.

Instalación desde cero

1. Verificación del entorno

Antes de iniciar el proyecto, es necesario comprobar que Node.js y npm están correctamente instalados:

node -v
npm -v

2. Creación del proyecto con Vite

El proyecto se crea utilizando Vite con el template vanilla, tal como se propone en el enunciado de la práctica:

npm create vite@latest theremin -- --template vanilla

cd theremin
npm install

3. Instalación e inicialización de Capacitor

Una vez creado el proyecto base, se instala Capacitor y se inicializa indicando el directorio de salida del build:

npm install @capacitor/core @capacitor/cli --save

npx cap init "Theremin" "com.theremin.app" --web-dir dist

Build y despliegue en Android

4. Añadir la plataforma Android

npm install @capacitor/android
npx cap add android

5. Instalación de plugins de Capacitor

Para acceder a los sensores de movimiento y al feedback háptico se instalan los siguientes plugins oficiales:

npm install @capacitor/motion @capacitor/haptics

6. Compilación y prueba en Android Studio

Una vez completado el desarrollo web, el proyecto se compila y se sincroniza con Capacitor para su ejecución en Android Studio:

npm run build

npx cap sync

npx cap open android

Este flujo permite ejecutar la aplicación en un emulador o dispositivo físico y comprobar el comportamiento real de los sensores, el audio y la visualización en un entorno nativo.


Nota metodológica sobre el uso de herramientas de inteligencia artificial

Durante el desarrollo del proyecto se utilizaron herramientas de asistencia basadas en inteligencia artificial como apoyo al proceso de aprendizaje y resolución de problemas técnicos.

En concreto, se empleó GitHub Copilot como herramienta de ayuda para la revisión de código y la generación de sugerencias durante la fase de desarrollo, y ChatGPT como soporte para la resolución de dudas conceptuales, técnicas y de documentación relacionadas con el stack tecnológico utilizado.

Bibliografía

  • Capacitor. (s. f.-a). Introducción a Capacitor. Disponible en: https://capacitorjs.com/docs (Consultado el: 23 de diciembre de 2025).
  • Capacitor. (s. f.-b). Instalando Capacitor. Disponible en: https://capacitorjs.com/docs/getting-started (Consultado el: 23 de diciembre de 2025).
  • Capacitor. (s. f.-c). Configuración del entorno. Disponible en: https://capacitorjs.com/docs/getting-started/environment-setup (Consultado el: 23 de diciembre de 2025).
  • Capacitor. (s. f.-d). Guía de desarrollo para Android. Disponible en: https://capacitorjs.com/docs/android (Consultado el: 23 de diciembre de 2025).
  • Capacitor. (s. f.-e). Vibración (Haptics) — Capacitor Plugin API. Disponible en: https://capacitorjs.com/docs/apis/haptics (Consultado el: 23 de diciembre de 2025).
  • Capacitor. (s. f.-f). Acelerómetro y giroscopio (Motion) — Capacitor Plugin API. Disponible en: https://capacitorjs.com/docs/apis/motion (Consultado el: 23 de diciembre de 2025).
  • Capacitor. (s. f.-g). Cámara (Camera) — Capacitor Plugin API. Disponible en: https://capacitorjs.com/docs/apis/camera (Consultado el: 23 de diciembre de 2025).
  • Capacitor. (s. f.-h). Almacenamiento local (Preferences) — Capacitor Plugin API. Disponible en: https://capacitorjs.com/docs/apis/preferences (Consultado el: 23 de diciembre de 2025).
  • Capacitor. (s. f.-i). Conceptos web/nativos (Development Workflow). Disponible en: https://capacitorjs.com/docs/basics/workflow (Consultado el: 23 de diciembre de 2025).
  • GitHub. (s. f.). GitHub Copilot. https://github.com/features/copilot
  • OpenAI. (s. f.). ChatGPT. https://chat.openai.com/
  • Tone.js. (s. f.). https://tonejs.github.io/

Debate0en Theremin Glitch: proceso creativo y desarrollo

No hay comentarios.

Publicado por

Add on Firefox. Reloj creativo

Publicado por

Add on Firefox. Reloj creativo

Creación de un add-on de reloj animado para Firefox He desarrollado un add-on para Firefox que muestra un reloj animado basado en…
Creación de un add-on de reloj animado para Firefox He desarrollado un add-on para Firefox que muestra un reloj…

Creación de un add-on de reloj animado para Firefox

He desarrollado un add-on para Firefox que muestra un reloj animado basado en visualizaciones de datos temporales. El proyecto utiliza p5.js v2 y está construido siguiendo los requisitos de Manifest V3. Esta versión es una adaptación refinada de la propuesta presentada en la primera entrega de la asignatura, originalmente diseñada para pantalla completa.

En la versión actual he mejorado el contador de tiempo, incorporando la visualización de milésimas y permitiendo su modificación mediante un slider que altera la velocidad temporal. También he añadido una ventana modal que ofrece información contextual sobre el proyecto, ampliando así la claridad y la accesibilidad de la interfaz.

Concepto inicial

La idea central del proyecto era aplicar las Leyes de la Simplicidad de John Maeda. En particular, resultaban fundamentales dos principios: “restar lo obvio y añadir lo significativo” y “organizar, agrupar y jerarquizar” para reducir la sensación de complejidad (1). Aunque estos criterios ya se habían trabajado en la primera entrega, la reducción del lienzo a 300 × 150 píxeles obligaba a reconsiderar cuidadosamente qué elementos mantener, cuáles eliminar y cómo reorganizar la interfaz sin perder legibilidad ni intención comunicativa.

Para ello realicé una serie de bocetos exploratorios que me permitieron limpiar la composición, definir la jerarquía visual y asegurar que cada componente aportaba significado dentro del espacio extremadamente limitado.

Estructura técnica

El proyecto cuenta con un manifest.json en V3 y un sketch.js que son documentos imprescindibles para la creación de un addon. Además, utiliza la librería P5 y una fuente externa y un index.html con elementos DOM aparte del canvas.

reloj/
├── manifest.json          # Configuración del add-on
├── index.html             # Popup de la extensión
├── sketch.js              # Lógica del reloj con p5.js
├── style.css              # Estilos y variables CSS
├── assets/
│   ├── icon.png
│   └── Barlow/            # Fuentes custom
└── libraries/
    └── p5.min.js          # Librería p5.js

Destacables técnicos

Cargas asíncronas en p5.js v2

La versión 2 de p5.js cambió cómo se cargan recursos. En vez de usar  preload(), utilicé async/await en el setup():

async function setup() {
  try {
    f = await loadFont("assets/Barlow/Barlow-Regular.ttf");
    f_bold = await loadFont("assets/Barlow/Barlow-Bold.ttf");
  } catch (err) {
    console.error("Error cargando fuentes:", err);
  }
}

Manifest V3 y CSP estricta

Firefox con Manifest V3 requiere una Content Security Policy muy restrictiva. Todo el código JavaScript debe estar en archivos externos, sin inline scripts:

"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"

}

Duda relacionada al manifest:
¿no es redundante conceder permisos explícitos en el manifest al utilizar la función nativa de P5?
"permissions": [
"storage"
],

Funcionalidades añadidas

Modo oscuro persistente

Implementé un botón con forma de luna en la esquina inferior derecha. El estado se guarda usando las funciones nativas de p5.js:

storeItem("modoOscuro", modoOscuro);
modoOscuro = getItem("modoOscuro") ?? false;

Esto usa localStorage internamente, así que la preferencia se mantiene al cerrar y reabrir el popup.

Control manual de la hora

Añadí un botón de reloj que despliega dos sliders: uno para las horas y otro para los minutos. Esto permite «viajar en el tiempo» y ver cómo se vería el reloj en cualquier momento del día. El botón de reset (↻) vuelve a la hora actual del sistema.

Modal informativo

Un botón «?» abre un modal con contexto histórico sobre la Declaración Balfour. Quería que el reloj no fuera solo decorativo, sino que invitara a reflexionar sobre la usurpación de la identidad Palestina.

Detalles visuales

Paleta de colores: Definida en variables CSS (:root) y leída desde JavaScript con getComputedStyle().

Instalación en modo desarrollo

  1. Abrir Firefox y navegar a about:debugging
  2. Clic en «This Firefox»
  3. Clic en «Load Temporary Add-on…»
  4. Seleccionar el archivo manifest.json

El add-on aparece en la barra de herramientas. Al hacer clic, se abre el popup con el reloj animado.

Propuesta de mejoras

Geolocalización del dispositivo

Integrar la geolocalización del dispositivo para poder aplicar, mediante una API como SunCalc.js, o la API del navegador, la hora exacta de la puesta de sol en la ubicación real del usuario. De esta manera, la altura de la zona verde del reloj se ajustaría automáticamente según la luz solar disponible en cada momento. Además, podría añadir un botón adicional para cambiar la visualización a la hora local de Palestina y recalcular la puesta de sol utilizando las coordenadas obtenidas a través de la misma API, adaptando así el comportamiento del reloj a la hora real de Palestina.

Modo oscuro o claro automático

Cambio automático entre modo claro y modo oscuro según la hora del día, aunque se siga manteniendo la funcionalidad manual.

Addon fijo

Actualmente, se cierra el addon cuando clicas fuera de él. Sería interesante poder configurarlo para que se mantenga fijo. Según investigación, se podría hacer una ventana flotante fuera del addon porque por defecto no dejaría que el addon permaneciera abierto.

Conclusiones

Este proyecto me permitió explorar:

  • Las restricciones de seguridad de Manifest V3
  • Persistencia de datos con p5.storage
  • Integración de p5.js con elementos del DOM (sliders, modales)

El resultado es un reloj funcional pero también conceptual: cada vez que lo abro, me recuerda que el pueblo palestino lleva más de 39.000 días sin el reconocimiento de la identidad robada.

Repositorio: https://github.com/dimecris/addon-reloj

Licencia: CC BY 4.0

Screenshot

(1) Maeda J. The Laws of Simplicity. Cambridge: MIT Press; 2006.

Bibliografía

Debate0en Add on Firefox. Reloj creativo

No hay comentarios.

Publicado por

Detectar rostros y expresiones en p5.js

Publicado por

Detectar rostros y expresiones en p5.js

Ver en p5.js https://editor.p5js.org/dimecris/full/1cvO0N9kT Repositorio Github https://github.com/dimecris/halloween [Ver proyecto en vivo](https://dimecris.github.io/halloween/) Estudio comparativo de librerías de detección de rostro: del cálculo manual…
Ver en p5.js https://editor.p5js.org/dimecris/full/1cvO0N9kT Repositorio Github https://github.com/dimecris/halloween [Ver proyecto en vivo](https://dimecris.github.io/halloween/) Estudio comparativo de librerías de detección de rostro:…

Ver en p5.js

https://editor.p5js.org/dimecris/full/1cvO0N9kT

Repositorio Github

Estudio comparativo de librerías de detección de rostro: del cálculo manual al poder de los Blendshapes

¿Qué conviene usar para detectar gestos faciales, los landmarks (puntos brutos del rostro) o los blendshapes (valores de expresión pre-calculados)?

Esa distinción es clave cuando lo que se busca no es solo identificar un rostro, sino entender su expresión. Por ejemplo, medir cuándo se abre la boca para activar un sistema de partículas o animar un efecto visual.

En este post explico cómo abordé esa decisión, qué librerías comparé y por qué finalmente elegí MediaPipe Face Landmarker, que combina precisión, rendimiento y una API perfecta para proyectos creativos con p5.js.


Explorando opciones: cuatro librerías de detección facial

En el ecosistema web hay muchas herramientas para detección de rostros, pero cuatro destacan por su solidez y versatilidad:

1. MediaPipe (Face Landmarker)

Desarrollada por Google, es una solución completa para visión por ordenador. Ofrece 478 landmarks 3D y, lo más importante, una serie de blendshapes que representan expresiones faciales en valores normalizados (de 0 a 1). Entre ellos están jawOpen (apertura de mandíbula), mouthFunnel (forma de túnel o soplo) o mouthSmile (sonrisa), que simplifican muchísimo la lógica de interacción.

2. ml5.js (FaceMesh)

Basada en el modelo FaceMesh, es una librería pensada para el entorno educativo y artístico. Funciona muy bien con p5.js, pero solo devuelve los landmarks brutos. Eso significa que, si quiero saber si una persona tiene la boca abierta, debo calcularlo manualmente midiendo la distancia entre el labio superior e inferior y normalizarla según la escala de la cara.

3. face-api.js

Una de las veteranas de JavaScript para detección facial, construida sobre TensorFlow.js. Detecta caras, estima edad y género y reconoce expresiones básicas (“happy”, “sad”, “surprised”…). Sin embargo, trabaja con solo 68 puntos 2D y sus expresiones son clasificaciones discretas, no valores continuos: sabe si alguien está “sorprendido”, pero no cuán abierta está su mandíbula.

4. Jeeliz (jeelizFaceFilter.js)

Una opción sorprendentemente rápida y ligera. Usa WebGL para ofrecer un rendimiento altísimo, ideal para proyectos de realidad aumentada tipo filtros de Snapchat. Aunque no tan conocida, incluye también coeficientes de expresión parecidos a los blendshapes, entre ellos el de apertura de boca.


Comparativa general

  • Tecnología base:
    MediaPipe: Propia de Google ·
    ml5.js: Wrapper de TensorFlow.js ·
    face-api.js: TensorFlow.js ·
    Jeeliz: WebGL + IA propia
  • Nº de puntos:
    MediaPipe: ~478 (3D) ·
    ml5.js: ~478 (3D) ·
    face-api.js: 68 (2D) ·
    Jeeliz: ~11 + pose 3D
  • Rendimiento:
    MediaPipe: Muy alto ·
    ml5.js: Alto ·
    face-api.js: Medio-alto ·
    Jeeliz: Extremadamente alto
  • Facilidad con p5.js:
    MediaPipe: Media ·
    ml5.js: Muy fácil ·
    face-api.js: Fácil ·
    Jeeliz: Media
  • Detección de boca:
    MediaPipe: Blendshapes ·
    ml5.js: Landmarks (distancia manual) ·
    face-api.js: Clasificación (sorpresa) ·
    Jeeliz: Coeficiente de expresión


Del método “difícil” al método “inteligente”

Escenario 1: el enfoque clásico (ml5.js)

Con ml5.js, la detección de la boca abierta requiere varios pasos:

  1. Obtener el array de landmarks.
  2. Localizar los puntos del labio superior e inferior.
  3. Calcular la distancia euclidiana entre ellos.
  4. Normalizar esa distancia con respecto al tamaño de la cara.
  5. Definir un umbral (un “número mágico”) para decidir cuándo la boca está abierta.

Funciona, pero implica muchos cálculos y ajustes manuales. Además, la distancia cambia si la persona se acerca o se aleja de la cámara.

Escenario 2: el enfoque inteligente (MediaPipe)

Con MediaPipe Face Landmarker, todo ese trabajo desaparece. El modelo ya analiza internamente los landmarks y devuelve coeficientes de expresión listos para usar. Basta con obtener el valor de jawOpen o mouthFunnel:

let jawOpenScore = results.faceBlendshapes[0].categories
  .find(c => c.categoryName === 'jawOpen').score;

let mouthFunnelScore = results.faceBlendshapes[0].categories
  .find(c => c.categoryName === 'mouthFunnel').score;

Ese jawOpenScore ya viene normalizado (de 0.0 a 1.0) y es independiente de la distancia a la cámara. En otras palabras: un indicador robusto, limpio y directo de la apertura de la mandíbula.


Aplicación en p5.js: de la expresión a la interacción

Una vez se tiene ese valor, la creatividad entra en juego. En mi caso, lo uso para generar partículas que salen de la boca según el grado de apertura:

let particleCount = map(jawOpenScore, 0, 1, 0, 500);
let particleSpeed = map(jawOpenScore, 0, 1, 1, 10);

Incluso podría combinar otros coeficientes:

  • mouthFunnel para simular un soplido que empuja las partículas.
  • mouthSmile para cambiar su color o comportamiento.

De este modo, el rostro se convierte en un controlador expresivo, no solo en un disparador binario de eventos.


Conclusión

Después de probar varias opciones, MediaPipe Face Landmarker se impone claramente por su equilibrio entre precisión, rendimiento y simplicidad. Mientras que ml5.js sigue siendo ideal para empezar o para proyectos educativos, MediaPipe ofrece una capa de abstracción mucho más potente gracias a los blendshapes, que permiten traducir la expresión facial directamente en comportamiento interactivo, sin cálculos intermedios.


Diseño de la aplicación

Moodboard Inspiracional

Wireframe de alta definición


Workflow de la App

La aplicación sigue un flujo de trabajo continuo centrado en la detección facial en tiempo real. Al iniciarse, setup() configura el canvas WEBGL en 1080p (aunque se podría adaptar a otros tamaños), inicializa MediaPipe Face Landmarker para análisis facial, y carga los recursos multimedia (fuentes Halloween, imágenes decorativas y audio).

El bucle principal draw() ejecuta tres procesos simultáneos: primero captura video de la cámara del usuario aplicando efecto espejo para interacción natural, luego procesa cada frame mediante detectFaceAndJaw() que utiliza MediaPipe para analizar 468 landmarks faciales y extraer el valor de apertura de mandíbula (jawOpen), y finalmente coordina la respuesta visual generando partículas Halloween desde las coordenadas exactas de la boca detectada con los landmarks del labio superior e inferior y comisuras.

El sistema de partículas implementa física realista con gravedad, colisiones y apilamiento, mientras que elementos visuales como el texto 3D "ABRE LA BOCA", el background dinámico y el audio ambiental responden en tiempo real al estado de la boca, creando una experiencia interactiva completa donde la expresión facial del usuario controla directamente los efectos visuales y sonoros del entorno Halloween.

 


Proceso de desarrollo de código — Proyecto Halloween p5.js

Fase 1: Configuración Inicial y Setup Básico

Primeros Pasos

El proyecto se estructuró con los archivos principales index.html, sketch.js, particulas.js y style.css. Se definió un canvas WEBGL para habilitar gráficos 3D y se preparó la integración con MediaPipe y p5.sound.

<!-- index.html -->
<meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'
  https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://storage.googleapis.com;
  style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:;
  connect-src 'self' https://cdn.jsdelivr.net https://storage.googleapis.com;">
<script src="https://cdn.jsdelivr.net/npm/p5@1.9.0/lib/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5@1.9.0/lib/addons/p5.sound.min.js"></script>
<script type="module" src="sketch.js"></script>
<script src="particulas.js"></script>

Durante esta fase aparecieron errores menores con el Content-Security-Policy y la carga del modelo de MediaPipe, que se solucionaron ajustando permisos y rutas.


Fase 2: Integración de Video y Cámara

Desafío: Captura de Video

La cámara se configuró con createCapture(), solicitando una resolución de 1280×720 (para conseguir un 1080p) y orientación frontal. Se aplicó un efecto espejo en la visualización para que la interacción fuese natural. No funcionaba con flipped y se añadió scale().

// sketch.js
capture = createCapture({
  video: { width: 1280, height: 720, facingMode: 'user' },
  audio: false,
  flipped: true
});
capture.hide();

// En draw():
push();
scale(-1, 1); // espejo horizontal
imageMode(CENTER);
image(capture, 0, 0, newVideoWidth, newVideoHeight);
pop();

El canvas se adaptó dinámicamente al tamaño de ventana con resizeCanvas(windowWidth, windowHeight), logrando un diseño responsivo incluso en modo WEBGL, aunque el uso está pensado para un monitor 1080p.


Fase 3: Implementación de MediaPipe

Mayor Desafío Técnico

Integrar MediaPipe Face Landmarker (vía @mediapipe/tasks-vision) fue el reto más complejo. El modelo se carga asíncronamente desde el CDN de Google y se ejecuta en modo VIDEO para análisis frame a frame.

Si se carga la librería https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3 desde el index da error.

// sketch.js — inicialización
const vision = await import('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3');
const resolver = await vision.FilesetResolver.forVisionTasks(
  "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
);
faceLandmarker = await vision.FaceLandmarker.createFromOptions(resolver, {
  baseOptions: {
    modelAssetPath:
      "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task"
  },
  outputFaceBlendshapes: true,
  runningMode: "VIDEO",
  numFaces: 1
});

Fase 4: Detección de Gestos Faciales

La detección de apertura de boca se basó en el valor jawOpen dentro de los blendshapes de MediaPipe. Este valor (0–1) permite medir el grado de apertura mandibular sin tener que calcular distancias entre landmarks.

// sketch.js — detección facial
const results = faceLandmarker.detectForVideo(video, nowInMs);
if (results.faceBlendshapes?.length > 0) {
  const blendshapes = results.faceBlendshapes[0].categories;
  for (let c of blendshapes) {
    if (c.categoryName === 'jawOpen') {
      jawOpenValue = c.score;
      jawOpen = jawOpenValue > jawOpenThreshold;
      break;
    }
  }
}

El cálculo de posición del centro de la boca se realizó con los landmarks 13 y 14, mapeando coordenadas normalizadas al tamaño del vídeo dentro del canvas WEBGL:

// sketch.js
const upperLip = landmarks[13];
const lowerLip = landmarks[14];
const normalizedMouthX = (upperLip.x + lowerLip.x) / 2;
const normalizedMouthY = (upperLip.y + lowerLip.y) / 2;

mouthCenterX = map(1 - normalizedMouthX, 0, 1, -newVideoWidth/2, newVideoWidth/2);
mouthCenterY = map(normalizedMouthY, 0, 1, -newVideoHeight/2, newVideoHeight/2);

Fase 5: Sistema de Partículas

Evolución del Sistema

El sistema comenzó como un conjunto de partículas simples sin física. Se evolucionó hacia una clase con gravedad, fricción, rebote y tipos visuales (spark y brush).

// particulas.js — clase Particle
class Particle {
  constructor(x, y, size, col, life, type = 'spark') {
    this.pos = createVector(x, y);
    this.vel = p5.Vector.random2D().mult(random(0.3, 5.5));
    this.gravity = 0.1;
    this.bounce = 0.3;
    this.friction = 0.95;
    this.size = size;
    this.color = color(col);
    this.life = life;
    this.type = type;
  }

  update() {
    this.vel.y += this.gravity;
    this.pos.add(this.vel);
    // rebote con el suelo
    let ground = height / 2;
    if (this.pos.y >= ground - this.size/2) {
      this.pos.y = ground - this.size/2;
      this.vel.y *= -this.bounce;
      this.vel.x *= this.friction;
    }
    this.life -= 1;
  }

  draw() {
    push();
    fill(this.color);
    noStroke();
    if (this.type === 'spark') ellipse(this.pos.x, this.pos.y, this.size);
    else rect(this.pos.x, this.pos.y, this.size*2.2, this.size*0.7, 6);
    pop();
  }
}

Cada vez que jawOpen supera el umbral, se generan partículas en la posición de la boca:

// sketch.js
if (jawOpen && frameCount % 3 === 0) {
  createHalloweenParticles();
}

function createHalloweenParticles() {
  let x = mouthCenterX;
  let y = mouthCenterY;
  let color = random(Object.values(COLORS));
  let particleSize = map(jawOpenValue, 0, 1, 1, 25);
  let type = random(['spark', 'brush']);
  particles.push(new Particle(x, y, particleSize, color, 1000, type));
}

Fase 6: Coordinación Facial–Partículas

El reto principal fue sincronizar los valores de jawOpen con el sistema de partículas sin provocar caídas de rendimiento. Además, se añadió desvanecimiento progresivo al cerrar la boca:

// sketch.js — updateParticles()
if (!jawOpen) {
  particles[i].alpha = lerp(particles[i].alpha, 0, 0.03);
  particles[i].life -= 1;
} else {
  particles[i].alpha = lerp(particles[i].alpha, 255, 0.08);
}

La función lerp() (interpolación lineal) permite una transición visual suave al pasar de estado activo (boca abierta) a inactivo (boca cerrada).


Fase 7: Elementos Visuales y Audio

Se implementó un texto 3D “ABRE LA BOCA” que rota con rotateX() y rotateY(), y un sistema de sonido reactivo que se activa cuando la boca se abre. Al inicio del proceso de programación funcionaba con el ratón:

// sketch.js — audio reactivo
if (jawOpen && !sonido.isPlaying()) {
  sonido.play();
  sonido.loop();
} else if (!jawOpen && sonido.isPlaying()) {
  sonido.stop();
}

Empaquetar un proyecto p5.js en modo kiosko con Electron

El empaquetado de la experiencia en modo kiosko (pantalla completa, sin marco) expuso varias particularidades en torno al audio local. En navegador, la carga mediante loadSound("music/halloween-ghost.mp3") funcionaba correctamente; sin embargo, en Electron la resolución de rutas file:// con espacios generaba codificaciones dobles (%2520) y el audio no se encontraba.

Se implementó una interceptación de rutas en main.js con session.webRequest.onBeforeRequest para redirigir cualquier solicitud /music/*.mp3 a la ruta real del sistema tanto en desarrollo (__dirname/music) como en producción (process.resourcesPath/music).

En el sketch.js se mantuvo la instrucción original loadSound("music/halloween-ghost.mp3"), dejando que Electron resolviera la ruta, y se incluyó music/** en  package.json para asegurar la disponibilidad de los recursos durante el empaquetado.

Con la interceptación, el archivo de audio se carga correctamente y no provoca errores de ruta; no obstante, el sonido no llega a reproducirse en la aplicación empaquetada. Se revisaron los permisos de cámara y micrófono, el desbloqueo del AudioContext tras la interacción y la reproducción en bucle, pero no se logró una reproducción audible en el binario final. El problema se mantiene como no resuelto.

Para ejecutar el proyecto en modo desarrollo (kiosko en Electron) se utiliza el siguiente comando:

npm run dev

 

Dificultades principales superadas

  1. Coordinación de sistemas asíncronos: MediaPipe, partículas, audio y render 3D corren en paralelo.
  2. Optimización de rendimiento: límite de partículas simultáneas y eliminación cuando salen del canvas para finalmente realizar rebote y acumulación.
  3. Responsive design: adaptación del canvas WEBGL al tamaño de ventana, mantener ratio de video y efecto espejo.

Resultado Final

El proyecto evolucionó de un sketch básico a una aplicación interactiva que integra:

  • Utilización del entorno de p5.js v2 y cargas asíncronas.
  • Detección facial en tiempo real con MediaPipe
  • Sistema de partículas con física y apilamiento
  • Elementos visuales y texto 3D p5.js v2
  • Audio ambiental reactivo
  • Canvas responsivo en WEBGL p5.js v2
  • Paleta cromática temática de Halloween

Créditos:

Halloween Ghost Sound 3 (Vol 003) by AlesiaDavina — https://freesound.org/s/688599/ — License: Attribution 4.0
Gráficos de Freepik
Foto de P.A.U.L.A en Unsplash

Debate0en Detectar rostros y expresiones en p5.js

No hay comentarios.

Publicado por

Reloj Anagnórisis — proceso de trabajo

Publicado por

Reloj Anagnórisis — proceso de trabajo

Proyecto: Visualización del tiempo con p5.js para una fachada digital. Obra: https://editor.p5js.org/dimecris/full/xaN7oUCzb Planteamiento El reto consistía en representar el paso del tiempo…
Proyecto: Visualización del tiempo con p5.js para una fachada digital. Obra: https://editor.p5js.org/dimecris/full/xaN7oUCzb Planteamiento El reto consistía en representar el…

Proyecto: Visualización del tiempo con p5.js para una fachada digital.

Obra: https://editor.p5js.org/dimecris/full/xaN7oUCzb

Planteamiento

El reto consistía en representar el paso del tiempo sin usar un reloj convencional, transformando la medición en una experiencia visual y simbólica capaz de generar atención y reflexión en el espacio público.

Ideas preliminares

Antes de llegar a la solución final, exploré varias metáforas y direcciones visuales:

  • Jardín del tiempo: cada segundo hacía crecer una planta que florecía al completar el minuto.
  • Respiración del reloj: un círculo que se expandía y contraía como la respiración.
  • Arena digital: granos que caían y formaban dunas que el viento deshacía.
  • Mapa en descomposición: mapas históricos de Palestina que se reducían a lo largo del día.
  • Reloj de ecos: puntos de luz que se activaban por minuto como voces o memorias.
  • Ciclos de luz: amanecer/mediodía/noche en transiciones cromáticas mínimas.

Todas estas pruebas convergieron en una solución más sintética: un reloj que mostrara el tiempo como identidad y como pérdida, reduciendo el lenguaje a líneas, color y ritmo.

Concepto final

El proyecto se titula Anagnórisis, término griego que alude al reconocimiento de la identidad por otros. No se trata de una identidad individual sino colectiva e histórica: la de un pueblo cuya memoria resiste el borrado.

El reloj se convierte así en un espacio de reconocimiento y conciencia.

1917: el inicio del tiempo robado

La pieza destaca la fecha 2 de noviembre de 1917, día de la Declaración Balfour, cuando el gobierno británico prometió la creación de un “hogar nacional para el pueblo judío” en Palestina sin el consentimiento de su población.
Esta fecha marca el inicio del proceso de usurpación de la identidad palestina, por eso fue elegida frente a otras como 1948 (Nakba) o 2023. Estas últimas remiten a momentos de destrucción. 1917 señala el punto de partida, el comienzo de la fractura histórica que la obra quiere evidenciar.

La Bauhaus y la memoria

La estética de la obra dialoga con la Bauhaus (1919–1933), escuela que fusionó funcionalidad y belleza mediante geometrías esenciales, tipografía limpia y paletas contenidas. La Bauhaus fue clausurada por el régimen nazi; muchos de sus miembros
fueron perseguidos o exiliados. Ese cruce entre arte y represión refuerza el sentido del proyecto: una forma mínima que carga una memoria histórica. La paleta —negro, rojo, verde y blanco— remite a la bandera palestina y a la economía cromática del diseño moderno.

 

Diseño y estructura visual

  • Segundos (columna izquierda): ascienden de 00 a 60, marcando el pulso vital.
  • Minutos (bloque central): se apilan como líneas irregulares, simulando un trazo manual que acumula memoria.
  • Horas (columna derecha): se disponen de 06 a 05 para simular el recorrido del sol.
  • Sol rojo: orbita verticalmente según la hora; al cruzar la franja verde del “suelo” altera su apariencia mediante fusión de color.
  • Suelo verde: banda que ocupa aproximadamente nueve horas de altura, símbolo de tierra, arraigo y esperanza.

Del boceto al código

El proceso partió de bocetos digitales para definir proporciones, jerarquías y recorridos visuales.
La implementación se realizó con p5.js, conectando el tiempo real a la gráfica mediante second(), minute() y hour(). Se añadieron:

  • Trazos irregulares precalculados para las 60 líneas, simulando un gesto humano estable.
  • Controles manuales para ajustar hora y minutos con rueda o teclado (HH:MM), y reinicio a la hora del sistema.
  • Modos de fusión para la interacción entre sol y suelo, enfatizando amanecer y puesta.
  • Paleta simbólica coherente con el significado político y la referencia.

Resultado

Anagnórisis no pretende medir el tiempo, sino recordarlo.
Cada segundo que asciende, cada línea que se deposita y cada desplazamiento del sol activan una memoria compartida. La pieza transforma el reloj en un acto de reconocimiento: una invitación a mirar el tiempo y, al mismo tiempo, a reconocer la identidad que persiste.

Debate0en Reloj Anagnórisis — proceso de trabajo

No hay comentarios.