Crear interfaces gráficas con JavaScript y Lil-GUI

En este artíclulo veremos cómo crear interfaces gráficas con JavaScript y Lil-GUI.

https://lil-gui.georgealways.com/ “Makes a floating panel for controllers on the web. Works as a drop-in replacement for dat.gui.”

See the Pen Crear interfaces gráficas con JavaScript y Lil-GUI by Danivalldo (@Danivalldo) on CodePen.

¿Quieres aprender a hacer este ejercicio? Salta directament al apartado

Durante la fase de desarrollo de cualquier proyecto, es habitual dedicar horas a ajustar cientos de parámetros, hasta dar con el acabado deseado. 

Si nuestro objetivo es ofrecer una experiencia interactiva que sorprenda al usuario, es necesario cuidar hasta el último detalle. Y como es bien sabido, una aplicación puede llegar a manejar una cantidad enorme de parámetros.

Por poner un par de ejemplos, se podrían definir valores para controlar el tiempo de transición entre apartados de una webapp. O para establecer el número de puntos en un sistema de partículas para un videojuego.

Sin embargo, puede llegar a ser extremadamente tedioso, estar yendo y viniendo del editor de código al navegador. Editando valores y viendo como queda el resultado cada vez.

Por no mencionar la incomodidad le supone a una persona sin el conocimiento técnico, tener que decidir sobre dichos parámetros, sin entender el código fuente.

Interfaz gráfica de usuario al rescate

Una forma de resolver esta problemática, es a través de la creación de una interfaz gráfica de usuario (GUI). Que permita a cualquier persona modificar los valores de forma visual y cómoda.

Sin embargo, crear una interfaz que parametrice múltiples valores de la aplicación, puede llevar mucho tiempo de desarrollo.

Por suerte, crear interfaces gráficas con JavaScript y Lil-GUI es pan comido. 

Lil-GUI permite a los desarrolladores crear una interfaz gráfica en forma de panel de control flotante, y vincular directamente todo tipo de variables.

De forma autónoma, la librería reconoce diversos tipos de la variables. Y genera un “input” adecuado para cada uno, por ejemplo, un “slider”, controlaria una valor numérico.

Tal y como cuentan en su página oficial, Lil-GUI, nace como alternativa actualizada a dat.GUI, otra librería creada con el mismo propósito.

De un modo similar al caso que ya vimos con DayJs y MomentJs, Lil-GUI y dat.GUI comparten un diseño de API prácticamente idéntico, de modo que migrar el código de una librería a la otra, es un proceso prácticamente directo.

Lil-GUI es una iniciativa de George Michael Brower. A pesar de tener solo 187 estrellas en su repositorio de Github, en los últimos meses, el módulo npm ha crecido exponencialmente en descargas semanales.

Resumen de descargas semanales en npmjs.com
Resumen de descargas semanales en npmjs.com

Dicho paquete se puede instalar mediante el comando “npm install lil-gui –save-dev”. El cual nos va a descargar un directorio con la versión minificada de tan solo 29KB.

A partir de entonces, ya se puede importar y empezar a utilizar la clase GUI. 

¿Cómo es la API de Lil-GUI?

Al instanciar esa clase, pasándole un argumento de configuración, nos devolverá el objeto “gui”, con el que iremos definiendo la interfaz de usuario.

La forma más cómoda de crear la interfaz gráfica, es a través de un objeto Javascript con todas las propiedades que van a ser editables. A ese objeto lo vamos a llamar, por ejemplo, “debuggerObject”.

Utilizando el método “add” del objeto “gui”, pasamos como primer argumento “debuggerObject”. Como segundo argumento, una cadena de texto, con el nombre de la propiedad parametrizable.

Lil-GUI, reconocerá cada tipo de variable, y generará un input adecuado.

Adicionalmente, la API nos ofrece la capacidad de personalizar un poco cada input. Por ejemplo, definiendo el la etiqueta del campo, o los valores máximo, mínimo y de salto, en un controlador numérico. Todo a través de métodos descriptivos como, “name()”, “max()”, “min()” o “step()”.

Finalmente, cabe mencionar, que la librería también permite organizar las propiedades editables en “carpetas” independientes, mediante el método “addFolder()”.

Este método, devolverá un objeto nuevo similar a “gui”. Con la peculiaridad que va a generar un subnivel en la interfaz gráfica, para agrupar propiedades dentro de un desplegable.

El fantástico portfolio de Bruno Simon en su versión debug, es uno de los mejores ejemplos para ver este tipo de librerías en acción

¿Cómo crear interfaces gráficas con JavaScript y Lil-GUI?

Para ilustrar el funcionamiento básico de la librería, vamos a suponer que un cliente ha pedido el diseño de una mascota para su sitio web.

Tras definir los aspectos básicos de su aspecto, solo queda afinar algunas partes conjuntamente con el equipo de diseño.

Para ello, primero generamos el “layout” en HTML con las partes del personaje:

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Library title</title>
</head>
  <body>
    <div class="monster">
      <div class="eyes-container">
        <div class="eye eye-left">
          <div class="eye-pupille"></div>
        </div>
        <div class="eye eye-right">
          <div class="eye-pupille"></div>
        </div>
      </div>
      <div class="mouth-container">
        <div class="mouth">
          <div class="tongue"></div>
        </div>
      </div>
    </div>
  </body>
</html>

Mediante CSS, definimos los estilos generales de cada uno de los elementos que lo conforman:

body {
  margin: 0;
  padding: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.monster {
  width: 300px;
  height: 300px;
  background-color: orange;
  border-top-left-radius: 50%;
  border-top-right-radius: 50%;
  border-bottom-left-radius: 30px;
  border-bottom-right-radius: 30px;
  box-shadow: -11px 7px 14px 0px rgb(0 0 0 / 20%);
  position: relative;
  .eyes-container {
    height: 50%;
    // position: absolute;
    width: 100%;
    top: 50px;
    display: flex;
    justify-content: center;
    align-items: center;
    .eye {
      width: 60px;
      height: 60px;
      border-radius: 50%;
      margin: 3px 3px 3px 3px;
      background-color: #fff;
      position: relative;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      .eye-pupille {
        display: block;
        position: relative;
        width: 50px;
        height: 50px;
        border-radius: 50%;
        max-width: 90%;
        max-height: 90%;
        background-color: #000;
        &:after {
          content: "";
          position: absolute;
          top: 0;
          left: 0;
          width: 50%;
          height: 50%;
          border-radius: 50%;
          background-color: #fff;
        }
      }
    }
  }
  .mouth-container {
    // border: solid 1px red;
    height: 50%;
    display: flex;
    justify-content: center;
    .mouth {
      position: relative;
      bottom: 0;
      width: 120px;
      left: 0;
      height: 100px;
      background-color: rgba(100, 20, 20, 1);
      overflow: hidden;
      border-radius: 20px 20px 60px 60px;
      box-shadow: 0 10px 2px -2px rgba(0, 0, 0, 0.2),
        0 5px 60px rgba(0, 0, 0, 1) inset, 0 -5px 1px 7px rgba(0, 0, 0, 1);
      transition: all 0.2s ease;
      border-bottom: solid 10px rgb(240, 240, 240);
      border-top: solid 10px rgb(240, 240, 240);
      &:before,
      &:after {
        content: "";
        position: absolute;
        top: -100px;
        left: -100px;
        width: 400px;
        height: 100px;
        z-index: 100;
      }
      .tongue {
        position: absolute;
        bottom: 0;
        width: 100%;
        height: 20%;
        background-color: #b60606;
        border-radius: 20px 0px 400px 400px;
        &:before,
        &:after {
          content: "";
          position: absolute;
          background-color: #b60606;
          width: 55%;
          height: 40px;
          border-radius: 50% 100% 0px 0px;
          // box-shadow: 0 10px 15px rgba(0, 0, 0, 0.3) inset,
          //   2px 3px 5px rgba(0, 0, 0, 0.2) inset;
          bottom: 15px;
          bottom: 100%;
        }
        &:before {
          border-radius: 100% 50% 0px 0px;
          right: 0;
        }
      }
    }
  }
}

Finalmente, vamos a conectar algunas propiedades del estilo del personaje a una instancia de Lil-GUI, para generar una interfaz donde manipular los acabados.

En este caso, nuestro objeto «debuggerObject», va a permitir controlar las siguientes partes del personaje.

El tamaño de los ojos, el tamaño de las pupilas, el tamaño de la boca y el tamaño y color del cuerpo, quedando de la siguiente manera.

const debuggerObject = {
  eyes: {
    eyeLeft: {
      size: 60,
      pupilleSize: 50,
    },
    eyeRight: {
      size: 60,
      pupilleSize: 50,
    },
  },
  mouth: {
    size: 1,
  },
  body: {
    sizeX: 1,
    sizeY: 1,
    color: "#ffa500",
  },
};

Tras instanciar la clase GUI, creamos varios directorios mediante el método «addFolder», con el objetivo de organizar nuestra interfaz.

const eyesFolder = lilGui.addFolder("Ojos");
const leftEyeFolder = eyesFolder.addFolder("Ojo izquierdo");
const rightEyeFolder = eyesFolder.addFolder("Ojo derecho");
const mouthFolder = lilGui.addFolder("Boca");
const bodyFolder = lilGui.addFolder("Cuerpo");

A continuación, declaramos un conjunto de funciones de “callback”, para aplicar los cambios sobre los estilos del elemento que “pinta” el personaje en el DOM.

const onChangeEye = (eye, size) => {
  const eyeDom = monster.querySelector(eye);
  eyeDom.setAttribute("style", `width: ${size}px; height: ${size}px`);
};
const onChangePupille = (pupille, size) => {
  const pupilleDom = monster.querySelector(pupille);
  pupilleDom.setAttribute("style", `width: ${size}px; height: ${size}px`);
};
const onChangeMouth = (size) => {
  const mouthDom = monster.querySelector(".mouth");
  mouthDom.setAttribute("style", `transform: scale(${size})`);
};
const onChangeColor = (color) => {
  monster.style.backgroundColor = color;
};
const onChangeBodySize = (dimention, value) => {
  switch (dimention) {
    case "width":
      monster.style.transform = `scaleX(${value}) scaleY(${debuggerObject.body.sizeY})`;
      return;
    case "height":
      monster.style.transform = `scaleX(${debuggerObject.body.sizeX}) scaleY(${value})`;
  }
};

Estas funciones van a ser llamadas cada vez que el usuario interactúe con uno de los controladores de la interfaz.

Por consiguiente, sólo queda crear cada uno de los controladores. Para ello, lanzamos el método «add» con nuestro objeto “debugger” en el directorio correspondiente. Encadenamos una serie métodos para ajustar las propiedades y nombres de cada input. Y conectamos la función de callback en cada caso a través del método onChange.

leftEyeFolder
  .add(debuggerObject.eyes.eyeLeft, "size")
  .name("tamaño ojo")
  .min(1)
  .max(100)
  .onChange((size) => {
    onChangeEye(".eye-left", size);
  });
leftEyeFolder
  .add(debuggerObject.eyes.eyeLeft, "pupilleSize")
  .name("tamaño pupila")
  .min(1)
  .max(100)
  .onChange((size) => {
    onChangePupille(".eye-left > .eye-pupille", size);
  });
rightEyeFolder
  .add(debuggerObject.eyes.eyeRight, "size")
  .name("tamaño ojo")
  .min(1)
  .max(100)
  .onChange((size) => {
    onChangeEye(".eye-right", size);
  });
rightEyeFolder
  .add(debuggerObject.eyes.eyeRight, "pupilleSize")
  .name("tamaño pupila")
  .min(1)
  .max(100)
  .onChange((size) => {
    onChangePupille(".eye-right > .eye-pupille", size);
  });
mouthFolder
  .add(debuggerObject.mouth, "size")
  .name("tamaño")
  .min(0)
  .max(1.5)
  .step(0.001)
  .onChange((size) => {
    onChangeMouth(size);
  });
bodyFolder
  .addColor(debuggerObject.body, "color")
  .name("color")
  .onChange((color) => {
    onChangeColor(color);
  });
bodyFolder
  .add(debuggerObject.body, "sizeX")
  .name("ancho")
  .min(0.2)
  .max(1.5)
  .onChange((width) => {
    onChangeBodySize("width", width);
  });
bodyFolder
  .add(debuggerObject.body, "sizeY")
  .name("alto")
  .min(0.2)
  .max(1.5)
  .onChange((height) => {
    onChangeBodySize("height", height);
  });

Experiencia de desarrollador

Hace relativamente poco, escuché por primera vez a alguien acuñar el término “experiencia de desarrollador” o “developer experience (DX)”.

Me pareció muy interesante la idea de poner atención en la fase de desarrollo. Tratar de hacer el proceso más amigable para el programador y su equipo. Del mismo modo que se busca hacer mas amigable la interacción usuario-maquina bajo el concepto de “experiencia de usuario (UX)”

No creo que Lil-GUI esté pensado para ser ofrecido al usuario final. Sin embargo, me parece una excelente herramienta, para incorporar durante la toma de decisiones en la producción de una aplicación web. O como “widget” adicional para una demo.

O incluso, como panel de control para un artísta digital. Como ya vimos crear arte con código es posible gracias a P5.js.

Este ha sido un pequeño vistazo a la librería Lil-GUI. A continuación, os dejo enlaces a más recursos, así como el ejercicio subido al repositorio de Github de “Librerías Js”.

Nos vemos pronto, un abrazo desarrolladores!

Deja un comentario