Wextensible

Rotaciones y traslaciones: Cómo se hace un reloj con Canvas HTML5

Un reloj con Canvas de HTML-5

Elemento canvas no soportado. Elemento canvas no soportado. Elemento canvas no soportado.
Tamaño: px

Nada mejor para entender las rotaciones con el elemento canvas de HTML-5 que hacer una aplicación práctica: un reloj. En el tema introducción a Canvas expuse como rotar una imagen con el método rotate(angulo) aparte de otras transformaciones y traslaciones. Para hacer el primer reloj de la izquierda se cuenta con tres elementos canvas dispuestos en tres capas. La más profunda porta la aguja de las horas así como una imagen de fondo insertada con {background-image} que presenta el fondo del reloj. Encima de esta capa se ubica otro canvas con la aguja de los minutos y otro más con la de los segundos. Las imágenes de las agujas tienen el fondo transparente para dejar ver el fondo del reloj.

El reloj se impulsa con el propio del sistema mediante el evento setTimeout (girarSegClock, 1000) que ejecuta la función que hace girar la aguja de segundos cada 1000 milisegundos. El círculo se divide en 60 partes por lo que cada ángulo de salto vale 2π/60 radianes. En cada evento, es decir, en cada segundo, se obtiene la hora del sistema hh:mm:ss. A partir de ahí el ángulo de la aguja de segundos será ss*2π/60 (desde la posición 0 que se corresponde con las 12 horas), el ángulo de los minutos será mm*2π/60 y el de las horas será ((hh*60/12) + Math.floor(5*mm/60))*2π/60. Entonces hacemos rotate(-anguloAnterior) con cada aguja para devolverla a la posición cero y luego rotamos desde cero el nuevo ángulo.

Elemento canvas no soportado. Elemento canvas no soportado.

El reloj anterior hace uso de imágenes. Aún podemos intentar hacerlo sólo con recursos de canvas. El de la izquierda se construye completamente en dos canvas, uno en una capa inferior para dibujar la base del reloj y otro en una capa superior para dibujar las agujas. Volvemos a utilizar el window.setTimeout() lanzado cada 1000 milisegundos. Pero para dibujar las agujas en su posición, es decir, con su ángulo correcto, obtenemos los segundos, minutos y horas del sistema con getSeconds(), getMinutes() y getHours() para luego convertirlas en ángulos de forma similar a como hicimos en el ejemplo anterior. En cada segundo dibujamos siempre las tres agujas pero ahora no se utiliza el método rotate() sino que simplemente se trazan las líneas de cada aguja. Se utiliza el método moveTo() para situar el inicio de un camino en el canvas en el centro del reloj y luego se dibuja una línea con lineTo(x,y). El punto(x,y) final de esa línea viene determinado por el ángulo de la aguja, obtenido como se ha dicho a partir de la hora, minuto y segundo del sistema. La posición horizontal la obtenemos a partir del coseno del ángulo y la vertical a partir del seno del ángulo. Estas y otras funciones matemáticas las expliqué en el tema de gráficas matemáticas con Canvas y Javascript, detallando algunas cosas sobre el objeto Math de Javascript.

El primer reloj con imágenes también contiene un configurador que permite al usuario cambiar la imágen de fondo y el tamaño. En la segunda no hay tal configuración. Aunque en ambos casos los relojes deben ser lanzados desde un window.onload o técnica similar que lo ejecute tras la carga total de la página. Para el primero usaremos el método lanzarReloj(configura, conLado). El primer argumento es un valor booleano para incluir el contenedor de configuración. El segundo argumento es opcional y si se pasa se modificará el tamaño del reloj. Para el segundo reloj el método es lanzarClock(conLado) siendo ese argumento también para cambiar el tamaño, aunque ahora no hay posibilidad de configuración de usuario.

El código completo lo puede consultar en los enlaces del código fuente de está pagina, canvas-reloj.js y canvas-reloj.css para el primer reloj y canvas-clock.js y canvas-clock.css para el segundo. A continuación sólo explico algunos detalles acerca de las transformaciones en el elemento canvas.

Probado en Chrome 16.0.912.75, Safari 5.1.2, Firefox 9.0.1, Opera 11.60.

Traslaciones y rotaciones con canvas

Una de las transformaciones del elemento canvas es la rotación. Hemos de entender que lo que se rota es todo el contexto, es decir, el plano o superficie de dibujo cuya coordenada (0,0) se ubica en el vértice superior izquierda del objeto canvas. Así que ese elemento viene a ser como una "ventana" a través de la cual podemos observar un rectángulo de ese plano. En este primer ejemplo tomaré la imagen de la aguja de las horas, un archivo de imagen PNG con fondo transparente a la que le he agregado un borde rojo para observar mejor el ejemplo. Hay entonces dos canvas, uno en una capa más profunda donde trazaré el ejemplo y otro en una capa más alta con dos líneas perpendiculares para mostrar el centro del rectángulo.

Ejemplo:

Elemento canvas no soportado. Elemento canvas no soportado.

Código de este ejemplo

<div class="ejemplo-linea">
    <div style="position: relative; height: 200px;">
        <canvas id="canvas-0" width="400" height="200"
        style="border: blue solid 1px; background-color: aqua; 
        position: absolute; z-index: 0;">
            <span style="color: red">Elemento 
            <b>canvas</b> no soportado.</span>
        </canvas>
        <canvas id="canvas-rejilla-0" width="400" height="200"
        style="border: blue solid 1px; background-color: transparent; 
        position: absolute; z-index: 1;">
            <span style="color: red">Elemento 
            <b>canvas</b> no soportado.</span>
        </canvas>
    </div>
    <div>
        <input type="button" value="trazar" 
        onclick="trazarCnv0()" />
        <input type="button" value="borrar" 
        onclick="borrarCnv0()" />
        <input type="button" value="girar derecha"
        onclick="girarDerecha0()" />
        <input type="button" value="girar izquierda"
        onclick="girarIzquierda0()" />
    </div>
</div>    
<script>
    var angulo0 = 2*Math.PI/60;
    var canvasR0 = document.getElementById("canvas-rejilla-0");
    var cnvR0 = null;
    if (canvasR0.getContext) cnvR0 = canvasR0.getContext("2d");
    cnvR0.beginPath();
    cnvR0.strokeStyle = "blue";
    cnvR0.moveTo(0, canvasR0.height/2);
    cnvR0.lineTo(canvasR0.width, canvasR0.height/2);
    cnvR0.stroke();
    cnvR0.closePath();
    cnvR0.beginPath();
    cnvR0.moveTo(canvasR0.width/2, 0);
    cnvR0.lineTo(canvasR0.width/2, canvasR0.height);
    cnvR0.stroke();
    cnvR0.closePath();
    var canvas0 = document.getElementById("canvas-0");
    var cnv0 = null;
    if (canvas0.getContext) cnv0 = canvas0.getContext("2d");
    var img0 = new Image();
    img0.src = "/como-se-hace/html5-canvas-reloj/ejemplos/prueba-rota.png";
    cnv0.fillStyle = "yellow";
    img0.onload = function(){
        trazarCnv0();
    }
    function trazarCnv0(){
        cnv0.fillRect(0, 0, canvas0.width,canvas0.height); 
        cnv0.drawImage(img0, (canvas0.width/2)-(img0.width/2), 
                (canvas0.height/2)-(img0.height/2), img0.width, img0.height);
    }
    function borrarCnv0(){
        cnv0.clearRect(0, 0, canvas0.width, canvas0.height);
    }
    function girarDerecha0(){
        borrarCnv0();
        cnv0.rotate(angulo0);
        trazarCnv0();
    }
    function girarIzquierda0(){
        borrarCnv0();
        cnv0.rotate(-angulo0);
        trazarCnv0();
    }
</script>       
         

Con la carga de la página se rellena un rectángulo de amarillo con fillRect() que coincide con el ancho de la ventana, es decir, del canvas. Luego se dibuja la imagen con drawImage() ubicándola exactamente en el centro. Cuando pulsamos alguno de los botones para girar la imagen usando el método rotate(), lo que estamos haciendo realmente es girar todo el plano en torno al punto (0,0), el vértice superior izquierda del canvas. Entonces ¿cómo hacemos para que la imagen gire sobre sí misma?. O mejor dicho, ¿cómo giramos el plano en el punto donde está el centro de la imagen?. La respuesta está en otra de las transformaciones que nos permite el canvas. Se trata de una traslación:

Ejemplo:

Elemento canvas no soportado. Elemento canvas no soportado.

Código de este ejemplo

<div class="ejemplo-linea">
    <div style="position: relative; height: 200px;">
        <canvas id="canvas-1" width="400" height="200"
        style="border: blue solid 1px; background-color: aqua; 
        position: absolute; z-index: 0;">
            <span style="color: red">Elemento 
            <b>canvas</b> no soportado.</span>
        </canvas>
        <canvas id="canvas-rejilla-1" width="400" height="200"
        style="border: blue solid 1px; background-color: transparent; 
        position: absolute; z-index: 1;">
            <span style="color: red">Elemento 
            <b>canvas</b> no soportado.</span>
        </canvas>
    </div>
    <div>
        <input type="button" value="trazar" 
        onclick="trazarCnv1()" />
        <input type="button" value="borrar" 
        onclick="borrarCnv1()" />
        <input type="button" value="girar derecha"
        onclick="girarDerecha1()" />
        <input type="button" value="girar izquierda"
        onclick="girarIzquierda1()" />
    </div>
</div>    
<script>
    var angulo1 = 2*Math.PI/60;
    var canvasR1 = document.getElementById("canvas-rejilla-1");
    var cnvR1 = null;
    if (canvasR1.getContext) cnvR1 = canvasR1.getContext("2d");
    cnvR1.beginPath();
    cnvR1.strokeStyle = "blue";
    cnvR1.moveTo(0, canvasR1.height/2);
    cnvR1.lineTo(canvasR1.width, canvasR1.height/2);
    cnvR1.stroke();
    cnvR1.closePath();
    cnvR1.beginPath();
    cnvR1.moveTo(canvasR1.width/2, 0);
    cnvR1.lineTo(canvasR1.width/2, canvasR1.height);
    cnvR1.stroke();
    cnvR1.closePath();
    var canvas1 = document.getElementById("canvas-1");
    var cnv1 = null;
    if (canvas1.getContext) cnv1 = canvas1.getContext("2d");
    var img1 = new Image();
    img1.src = "/como-se-hace/html5-canvas-reloj/ejemplos/prueba-rota.png";
    cnv1.fillStyle = "yellow";
    cnv1.translate(canvas1.width/2, canvas1.height/2);
    img1.onload = function(){trazarCnv1()};
    function trazarCnv1(){
        cnv1.fillRect(0, 0, canvas1.width,canvas1.height); 
        cnv1.drawImage(img1, -img1.width/2, -img1.height/2, img1.width, img1.height);
    }
    function borrarCnv1(){
        cnv1.clearRect(-canvas1.width/2, -canvas1.height/2, canvas1.width, canvas1.height);
    }
    function girarDerecha1(){
        borrarCnv1();
        cnv1.rotate(angulo1);
        trazarCnv1();
    }
    function girarIzquierda1(){
        borrarCnv1();
        cnv1.rotate(-angulo1);
        trazarCnv1();
    }
</script>       
         

En el ejemplo trasladamos el plano para que el punto (0,0) se desplace hasta el centro de la ventana, lo que hacemos con cnv1.translate(canvas1.width/2, canvas1.height/2). A partir de aquí cualquier rotación se aplicará en este nuevo origen. Otra aplicación posible sería girar un texto sobre su centro:

Ejemplo:

Elemento canvas no soportado. Elemento canvas no soportado.

Código de este ejemplo

<div class="ejemplo-linea">
    <div style="position: relative; height: 200px;">
        <canvas id="canvas-2" width="400" height="200"
        style="border: blue solid 1px; background-color: aqua; 
        position: absolute; z-index: 0;">
            <span style="color: red">Elemento 
            <b>canvas</b> no soportado.</span>
        </canvas>
        <canvas id="canvas-rejilla-2" width="400" height="200"
        style="border: blue solid 1px; background-color: transparent; 
        position: absolute; z-index: 1;">
            <span style="color: red">Elemento 
            <b>canvas</b> no soportado.</span>
        </canvas>
    </div>
    <div>
        <input type="button" value="trazar" 
        onclick="trazarCnv2()" />
        <input type="button" value="borrar" 
        onclick="borrarCnv2()" />
        <input type="button" value="girar derecha"
        onclick="girarDerecha2()" />
        <input type="button" value="girar izquierda"
        onclick="girarIzquierda2()" />
    </div>
</div>    
<script>
    var angulo2 = 2*Math.PI/60;
    var canvasR2 = document.getElementById("canvas-rejilla-2");
    var cnvR2 = null;
    if (canvasR2.getContext) cnvR2 = canvasR2.getContext("2d");
    cnvR2.beginPath();
    cnvR2.strokeStyle = "blue";
    cnvR2.moveTo(0, canvasR2.height/2);
    cnvR2.lineTo(canvasR2.width, canvasR2.height/2);
    cnvR2.stroke();
    cnvR2.closePath();
    cnvR2.beginPath();
    cnvR2.moveTo(canvasR2.width/2, 0);
    cnvR2.lineTo(canvasR2.width/2, canvasR2.height);
    cnvR2.stroke();
    cnvR2.closePath();
    var canvas2 = document.getElementById("canvas-2");
    var cnv2 = null;
    if (canvas2.getContext) cnv2 = canvas2.getContext("2d");
    var texto = "CANVAS";
    var altoTexto = 48;
    cnv2.lineWidth = 2;               
    cnv2.font = "italic bold " + altoTexto + "px Courier New";
    var objTexto = cnv2.measureText(texto);
    var anchoTexto = objTexto.width;
    cnv2.translate(canvas2.width/2, canvas2.height/2);
    trazarCnv2();
    function trazarCnv2(){
        cnv2.fillStyle = "yellow";
        cnv2.fillRect(0, 0, canvas2.width,canvas2.height); 
        cnv2.strokeStyle = "navy";
        cnv2.strokeText(texto,-anchoTexto/2,altoTexto/4);
        cnv2.fillStyle = "lime";
        cnv2.fillText(texto,-anchoTexto/2,altoTexto/4);
    }
    function borrarCnv2(){
        cnv2.clearRect(-canvas2.width/2, -canvas2.height/2, canvas2.width, canvas2.height);
    }
    function girarDerecha2(){
        borrarCnv2();
        cnv2.rotate(angulo2);
        trazarCnv2();
    }
    function girarIzquierda2(){
        borrarCnv2();
        cnv2.rotate(-angulo2);
        trazarCnv2();
    }
</script>