Creación de controles realistas y personalizados en vanilla Javascript


 

En HTML5 disponemos de pocos tipos de entrada <input> y son estándar aunque su aspecto depende del navegador usado para visualizar el documento web.

Por ejemplo, tenemos el deslizador (slider), que en HTML lo incrustamos con la etiqueta <input type=range ... />

Con Firefox el aspecto de este control es, más o menos, así:            

Estos controles deslizantes que aquí se usan para seleccionar el volumen de diversos efectos sonoros podrían tener un aspecto más realista y elegante. Es posible encontrar diseños gratuitos en internet, pero sólo como imágenes estáticas que carecen de toda capacidad de interacción.

Por ejemplo podríamos usar algo como esta imagen gratis y libre de derechos descargada del sitio: https://all-free-download.com/

Pero al tratarse de una imagen, deberemos diseñar código HTML y Javascript para arroparla de la funcionalidad de un elemento de entrada de datos que podamos usar en nuestra aplicación o página web.

Más aún, podríamos generar un código de programa que permita usar como imagen del control la foto que capturásemos de algún mando real de algún aparato de música de nuestra sala de estar, por ejemplo.

En este artículo vamos a ir desarrollando un caso real del proceso de convertir una imagen de un mando integrado en algún dispositivo para crear un mando interactivo de volumen para un panel virtual de nuestra aplicación de música.

Un ejemplo de controles estándar (slider de HTML5) para controlar el volumen de efectos de sonido puede encontrarse aquí: Efectos de sonido

 ________________________________________________________________________________

 

Empezamos capturando una foto de un aparato musical, intentando que sea una toma frontal carente de perspectiva:

A continuación recortamos un área de la foto que muestre el mando centrado. El recorte será cuadrado y el centro del círculo del mando estará en el centro de la imagen:


Ahora creamos una imagen que muestre lo que parecerá un led. Esta nueva imagen tendrá las mismas dimensiones que la anterior y actuará como una capa con transparencia capaz de girar respecto al centro. Lo hacemos con el objetivo de simular un led que muestre que el mando puede girar:

 


Incluso triplicamos esta última imagen para mostrar el led con distintos tonos para simular las condiciones de apagado, encendido verde y encendido rojo. Con ello podremos mostrar no sólo el giro del mando sino también sus estados off, on, y máximo.

El aspecto será así:


Por último crearemos una imagen cuadrada con las mismas dimensiones, que usaremos como elemento superpuesto que actúe a modo de sensor susceptible de recibir eventos del ratón sin que tenga dependencias de los giros de la capa que representa el led.

Hemos generado las tres capas de imagen que integrarán nuestro control:

1 - La capa del fondo con la imagen del mando sobre el panel.

2 - La capa intermedia que girará sobre su centro y que contiene un led que podrá mostrarse oscuro, verde o rojo.

3 - La capa superior transparente que detecte las acciones del ratón.

He aquí el esquema:

En una subcarpeta llamada img colocaremos los cinco archivos que contienen las tres capas. Los nombramos como:

mando_fondo.png : contiene la foto recortada en el fondo, 

mando_led_off.png : contiene el led cuando no brilla en el medio,

mando_led_on.png : contiene el led luciendo verde en el medio,

mando_led_max.png : contiene el led luciendo rojo en el medio,

mando_sensor.png : contiene la zona transparente en lo alto.

En nuestro código HTML el mando estará representado por un contenedor <div> que incluya tres imágenes que obligaremos a superponer usando el estilo CSS adecuado.

La parte del código HTML lo represento aquí:

        ...

<div id=mando >

    <img id=mandofondo src="img/mando_fondo.png" />

    <img id=mandoled src="img/mando_led_off.png" />

    <img id=mandosensor src="img/mando_sensor.png" />

</div>

        ... 


Las características de presentación del mando las establecemos en la hoja de estilos CSS.

El contenedor ocupará una posición relativa y su visualización será inline-block, estableceremos el tamaño de manera absoluta el píxeles con la dimensión de las imágenes que contendrá.

Cada imagen tendrá visulización inline-block con posicionamiento absoluto referido a su condenedor <div> y cada capa se sobreexpondrá  sobre la anterior mediante un z-index progresivo.

Tanto las capas como el contenedor carecerán de márgen, borde y padding.

El código CSS quedará de esta forma:

...

<style>

#mando {
    display: inline-block;
    position: relative;
    margin: 0;
    border: 0;
    padding: 0;
    width: 400px;
    height: 400px;
}

#mandofondo {
    display: inline-block;
    position: absolute;
    margin: 0;
    border: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    z-index: 100;
}

#mandoled {
    display: inline-block;
    position: absolute;
    margin: 0;
    border: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    z-index: 101;
}


#mandosensor {
    display: inline-block;
    position: absolute;
    opacity: 0;
    margin: 0;
    border: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    z-index: 102;
}

</style>

...

_______________________

Ahora toca diseñar el código Javascript que infunda alma a nuestro mando de volumen. Todo el código va a quedar muy sencillo si primero repasamos algunos conceptos importantes.

Será muchísimo más sencillo que el desarrollo que os voy a adelantar, en el cual estoy experimentando con algunas librerías propias que no tienen que ver con el tema aquí tratado.

Pero aunque todo quedará muy sencillo en este desarrollo que nos atañe aquí, la funcionalidad será igual de reactiva  que el ejemplo que os adelanto en el siguiente enlace para que interactuéis con él:

https://explicarlos.info/controlmusica/

_________________________________

Vamos a incluir otra capa más para representar la imagen del led en su estado previo, cuando todavía no hemos seleccionado un nuevo valor.

De esta manera tenemos un contenedor que incluye cuatro imágenes: el fondo, el led en estado previo, el led en estado actual y la capa sensora que recoge los eventos.

El código HTML queda ahora así:

    ...

<div id=mando >

    <img id=mandofondo src="img/mando_fondo.png" />

    <img id=mandoledpre src="img/mando_led_off.png" />

    <img id=mandoled src="img/mando_led_off.png" />

    <img id=mandosensor src="img/mando_sensor.png" />

</div>

    ... 

__________________________

La imagen mandosersor podrá detectar tres eventos correspondientes a tres acciones de ratón:

        - Evento click al pulsar sobre el mando. Esta acción ejecutará la función actualizarVolumen en la que el volumen del sonido queda establecido al nuevo valor, y la capa del led previo gira hasta el ángulo actual.

        - Evento mousemove al desplazar el puntero del ratón sobre el mando. Esta acción ejecutará la función seguirControl en la que el led gira de acuerdo al desplazamiento del ratón. El estado del led previo no se altera y queda representado mediante una transparencia parcial. Quien cambia es el led actual, representado con opacidad máxima, que lo hace dependiendo del ratón.

        - Evento mouseout al salir el puntero fuera de la imagen del mando ejecutará la función dejarControl. Ambas capas, led y led previo, giran a la posición actual y el mando ya no cambia.

_____________

Con estos añadidos, nuestro código HTML correspondiente al contenedor de las imágenes del mando queda así:

            ...

<div id=mando >

    <img id=mandofondo src="img/mando_fondo.png" />

    <img id=mandoledpre src="img/mando_led_off.png" />

    <img id=mandoled src="img/mando_led_off.png" />

    <img id=mandosensor src="img/mando_sensor.png"

        onmousemove="seguirControl(event);"

        onclick="actualizarVolumen();"

        onmouseout="dejarControl();"

    />

</div>

        ...

___________________

Vamos a poner un ejemplo de código que, por mor de sencillez, constará de un solo documento que albergue los tres aspectos: HTML, CSS y Javascript.

Habrá repeticiones y será algo chapucero. Tampoco usaremos nuevos objetos ni funciones flecha, pero ahora lo importante es comprender los conceptos y cálculos que vamos a emplear.

He aquí el documento web:

<!DOCTYPE html>
<html lang=es >
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta name=keywords content="html, html5, javascript, interfaz, control" />
        <meta name=author content="Carlos Grasa Lambea" />
        <meta name=date content="01/10/2020" />
        <title>Componentes de interfaz personalizados</title>
        <style>
            body {
                font-family: Arial, Helvetica, sans-serif; background-color: bisque;
            }
            #mando {
                display: inline-block; position: relative; margin: 0; border: 0;
                padding: 0; width: 200px; height: 200px;
            }
            #mandofondo {
                display: inline-block; position: absolute; margin: 0; border: 0;
                padding: 0; width: 100%; height: 100%; z-index: 100;
            }
            #mandoledpre {
                display: inline-block; position: absolute; opacity: 0.4; margin: 0;
                border: 0; padding: 0; width: 100%; height: 100%; z-index: 101;
            }
            #mandoled {
                display: inline-block; position: absolute; margin: 0; border: 0;
                padding: 0; width: 100%; height: 100%; z-index: 102;
            }
            #mandosensor {
                display: inline-block; position: absolute; opacity: 0; margin: 0;
                border: 0; padding: 0; width: 100%; height: 100%; z-index: 103;
            }
        </style>
    </head>
    <body>
        <audio id=sonido src="audio/sonidoambiente.mp3" loop ></audio>
        <header>
            <h1>Componentes de interfaz personalizados</h1>
            <hr/>
        </header>
        <section id=controles >
            <div id=mando >
                <img id=mandofondo src="img/mando_fondo.png" />
                <img id=mandoledpre src="img/mando_led_off.png" />
                <img id=mandoled src="img/mando_led_off.png" />
                <img id=mandosensor src="img/mando_sensor.png"
                    onmousemove="seguirControl(event);"
                    onclick="actualizarVolumen();"
                    onmouseout="dejarControl();"
                />
            </div>
            <hr/>
        </section>
        <section id=salida ></section>
    </body>
        <script>
            let entra=0;
            let entrando=0;
            let ambiente=document.querySelector("#sonido");
            let led=document.querySelector("#mandoled");
            let ledPrevio=document.querySelector("#mandoledpre");
            let comentario=document.querySelector("#salida");
            let minimo=0.6;
            let maximo=0.9;
            let sentidoHorario=true;
            ambiente.volume=0;
            ambiente.pause();

            const seguirControl=function(evento) {
                min=(minimo+1)%1;
                max=(maximo+1)%1;
                let elemento=evento.target;
                let hayCruce=(sentidoHorario && min<max) || (!sentidoHorario && min>max);
                let x=(1-(Math.atan2(evento.offsetY-elemento.height/2, evento.offsetX-elemento.width/2)/2/Math.PI))%1;
                let desplazamiento=1.5-(max+min)/2+0.5*hayCruce;
                let y=(x+desplazamiento)%1;
                min=(min+desplazamiento)%1;
                max=(max+desplazamiento)%1;
                y=Math.max(0, Math.min(1, (y-(min+max)/2)/(max-min)+0.5));
                entrando=y;
                if (entrando==0)
                    led.src="img/mando_led_off.png";
                else if (entrando<1)
                    led.src="img/mando_led_on.png";
                else
                    led.src="img/mando_led_max.png";
                led.style.transform="rotate("+(entrando*(min-max))+"turn)";
                comentario.innerHTML=
                    "valor devuelto: "+Math.floor(entra*100)+"%<br/>"+
                    "valor candidato: "+Math.floor(entrando*100)+"%";
                return;
            }

            const actualizarVolumen=function(evento) {
                entra=entrando;
                if (entra==0)
                    ledPrevio.src="img/mando_led_off.png";
                else if (entra<1)
                    ledPrevio.src="img/mando_led_on.png";
                else
                    ledPrevio.src="img/mando_led_max.png";
                ledPrevio.style.transform="rotate("+(entra*(min-max))+"turn)";
                comentario.innerHTML=
                    "valor devuelto: "+Math.floor(entra*100)+"%<br/>"+
                    "valor candidato: "+Math.floor(entrando*100)+"%";
                ambiente.volume=entra;
                if (entra)
                    ambiente.play();
                return;
            }

            const dejarControl=function() {
                if (entra==0)
                    led.src="img/mando_led_off.png";
                else if (entra<1)
                    led.src="img/mando_led_on.png";
                else
                    led.src="img/mando_led_max.png";
                led.style.transform="rotate("+(entra*(min-max))+"turn)";
                comentario.innerHTML="valor devuelto: "+Math.floor(entra*100)+"%";
                return;
            }
        </script>
    </body>
</html>

______________________

 

La ejecución del programa puede comprobarse en: https://explicarlos.info/blogger/controlmusica/

 

Si lo prefiere, también se puede descargar el archivo ZIP en la dirección: descargar proyecto

Y ahora procederemos a explicar detalladamente cada detalle del programa.



 

A grandes rasgos éstas son las tareas que tenemos que implementar en nuestro código:

En primer lugar nos fijamos en los tres eventos esperados que hemos definido en la imagen de la capa mandosensor:

        ...

<div id=mando >
    <img id=mandofondo src="img/mando_fondo.png" />
    <img id=mandoledpre src="img/mando_led_off.png" />
    <img id=mandoled src="img/mando_led_off.png" />
    <img id=mandosensor src="img/mando_sensor.png"
        onmousemove="seguirControl(event);"
        onclick="actualizarVolumen();"
        onmouseout="dejarControl();"
     />

 </div>

             
...

Cuando el puntero de ratón se desplace sobre el mando, la función seguircontrol(event) realizará un seguimiento de adónde estamos señalando. Necesitamos pasarle los parámetros del evento producido para calcular las coordenadas relativas al elemento que capta los movimientos del ratón. A partir de las coordenadas cartesianas calcularemos cuál es el ángulo girado del mando rotatorio. Éste ángulo corresponderá a un valor que determine el volumen candidato (hasta que se decida con un clic) de la música y también el giro que hay que aplicar a la capa led para que éste luzca en la posición correcta y con el color adecuado.

Luego profundizaremos en la explicación.

______________

Cuando pinchemos (o hagamos clic) sobre el elemento, el evento click disparará la ejecución de la función actualizarVolumen(), la cual deberá tomar el último valor de la posición del mando y establecerla como el valor actual del volumen de la música. Ahora la capas que representan el valor actual (devuelto) quedará fija en su ángulo correspondiente, mientras que el valor previo (candidato) del led estará cambiando según el movimiento del ratón.

______________

Por último, cuando saquemos el ratón fuera de la zona de detección, la capa que registra el valor candidato (señalado pero no seleccionado) deberá coincidir con el valor actual (real) del volumen de la música. Ambas capas, mandoled (valor real) y mandoledpre (valor candidato) estarán giradas el mismo ángulo y presentarán el mismo color de led, superpuestas y coincidentes.

 



No hay comentarios:

Publicar un comentario