Viscosidad

facebook-svg gplus-svg twitter-svg

A continuación queremos crear la ilusión de viscosidad. Este es un ejemplo práctico de como manipular una imagen.

En pocas palabras:

1. Creamos unos cuantos circulos ( pelotas ) que tienen como relleno un gradiente radial: desde completamente opacos en el centro, a totalmente transparentes.

2. El siguiente paso es acceder uno por uno los pixeles de esta imagen, y si la transparencia se encuentra debajo de un cierto umbral ( 160 en este caso ) cambiamos el valor del componente alpha a 0.  Donde las pelotas se sobreponen el valor del componente alfa de cada pixel puede pasar del umbral establecido, creando de esta manera la impresión que las dos pelotas se funden en una sola como dos gotas de liquido.

En más detalle:

Prácticamente trabajamos con dos elementos canvas.
- Un primer canvas donde dibujamos y animamos las pelotas, y
- un segundo canvas donde ponemos, con putImageData(), la imagen generada en el primer canvas ya manipulada.

Normalmente el primer canvas no aparece en pantalla, sino que se queda en memoria. Pero en el siguiente ejemplo, para que las cosas sean más claras, los dos elementos canvas aparecen en el documento.

El código

Primero iniciamos los dos elementos canvas, que en este caso tienen  300 por 300 pixeles:


var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
var _c = document.getElementById("_canvas");
var _ctx = _c.getContext("2d");
var cw = c.width = _c.width = 300,
    cx = cw / 2;
var ch = c.height = _c.height = 300,
    cy = ch / 2;

A continuación establecemos algunas variables necesarias;


var rad = Math.PI / 180; // el valor de un grado sexagesimal en radianes
var umbral = 160; // el umbral de transparencia debajo del cual los pixeles son totalmente transparentes
var pelotas = []; // el array de las pelotas

Para popular el array de las pelotas construimos la función Pelota()


function Pelota() {
    this.r = randomIntFromInterval(50, 100);// el radio de la pelota
    this.x = randomIntFromInterval(this.r, cw - this.r);
    this.y = randomIntFromInterval(this.r, ch - this.r);
    this.vx = Math.random() * 7 - 3.5;// la velocidad en x
    this.vy = Math.random() * 7 - 3.5;// la velocidad en y
}

Para poder dibujar fácilmente las pelotas extendemos la funcionalidad de la Pelota() con el método dibujar().


Pelota.prototype.dibujar = function() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
    ctx.fillStyle = Grd(this.x, this.y, this.r);
    ctx.fill();
  }

El método dibujar() utiliza la función Grd() que devuelve un gradiente radial para llenar de color cada pelota. Los colores que utilizamos son colores hsla. Observe por favor que el gradiente va desde completamente opaco en el centro 'hsla(0,10%,65%,1)', a totalmente transparente 'hsla(0,10%,25%,0)'. Esto es muy importante.


function Grd(x, y, r) {
    grd = ctx.createRadialGradient(x, y, 0, x, y, r);
    grd.addColorStop(0, 'hsla(0,10%,65%,1)');
    grd.addColorStop(1, 'hsla(0,10%,25%,0)');
    return grd;
}

Necesitamos también una función para actualizar la pelota. Durante la animación la pelota se mueve a velocidad constante:

this.x += this.vx;
this.y += this.vy;

Y si toca los bordes del canvas, la pelota cambia de sentido.

this.vx *= -1; 
........
this.vy *= -1;

Pelota.prototype.actualizar = function() {
    if (this.x + this.r >= cw ||
      this.x - this.r <= 0) {
      this.vx *= -1;
    }
    if (this.y + this.r >= ch ||
      this.y - this.r <= 0) {
      this.vy *= -1;
    }
    this.x += this.vx;
    this.y += this.vy;
  
    this.dibujar();
  }

Para popular el array de las pelotas utilizamos un bucle for de esta manera:


for (var i = 0; i < 10; i++) {
    var pelota = new Pelota();
    pelotas.push(pelota);
  }

La función Animacion()

Se trata de una función recursiva, o sea una función que se llama a si misma, y esto es lo primero que hace la función Animacion().

elId = window.requestAnimationFrame(Animacion);

A continuación limpia el canvas:

ctx.clearRect(0, 0, cw, ch);

y actualiza el array de las pelotas

for (var i = 0; i < pelotas.length; i++) {
      pelotas[i].actualizar()
    }

Aquí viene la parte importante:

La función Animacion() recupera los datos de todos los píxeles de la imagen generada ( o sea las pelotas que rebotan en las paredes del canvas ), y lo hace utilizando el método getImageData():

var imgData = ctx.getImageData(0, 0, cw, ch);
var pixels = imgData.data;

Sabemos que los píxeles de una imagen tienen una "anchura" de 4 bytes, donde el cuarto byte representa el componente alpha. También sabemos que el componente alpha puede tomar valores entre 0 ( totalmente transparente ) y 255 ( totalmente opaco ). El siguiente paso es acceder uno por uno los pixeles de esta imagen, y modificar el cuarto componente ( la transparencia ). Si la transparencia se encuentra debajo de un cierto umbral ( 160 en este caso ) cambiamos el valor del componente alpha a 0. 

for (var i = 3; i < pixels.length; i += 4) {
      if (pixels[i] < umbral)
        pixels[i] = 0;
  }

Al final ponemos la imagen manipulada en el otro _canvas:

_ctx.putImageData(imgData, 0, 0);

function Animacion() {
    elId = window.requestAnimationFrame(Animacion);
    ctx.clearRect(0, 0, cw, ch);
    for (var i = 0; i < pelotas.length; i++) {
      pelotas[i].actualizar()
    }

  var imgData = ctx.getImageData(0, 0, cw, ch);   var pixels = imgData.data;

  for (var i = 3; i < pixels.length; i += 4) {     if (pixels[i] < umbral)       pixels[i] = 0;   }   _ctx.putImageData(imgData, 0, 0);   }

Lo ponemos todo junto

Es recomendable abrirlo y manosearlo en codepen.io

See the Pen viscosidad by Gabi (@enxaneta) on CodePen.

El primer canvas se queda en memoria

En el siguiente ejemplo el primer canvas no aparece en el DOM, sino que se queda en memoria ( buffer canvas ). Para esto sencillamente creamos un nuevo elemento canvas, sin anexarlo al documento.

var c = document.createElement("canvas");
var ctx = c.getContext("2d");

El resto del código queda como antes.

See the Pen viscosidad - buffer canvas* by Gabi (@enxaneta) on CodePen.