Tutorial App mezcla colores primarios!
11
mayo
2017

Hola muchachos! Nuevo tutorial, nuevos conocimientos que compartir, esta vez os voy a contar cómo podéis montar una aplicación en Javascript que permita pintar como si usasemos pinceles y pinturas las cuales al mezclarse los colores producen otros nuevos, partiendo de los colores primarios actuales que son:

 

420px-Sintesis_sustractiva_plano.svg

Colores Primarios: Amarillo, Cían y Magenta

 

Lo que a algunos puede confundiros ya que, los colores primarios que nos enseñaron a algunos fueron el Azul, el Rojo y el Amarillo. Bien teniendo esto claro vamos a ver cómo podemos primero pintar en un lienzo, para ello necesitamos el siguiente código HTML donde vamos a pintar:

<div id="container">
  <canvas id="canvas" style="background-color: rgba(0,0,0,.0);">
    <p id="error">Please upgrade your browser.</p>
  </canvas>
  <div id="ventana-paleta" class=" card Paleta Hidden" >
    <div class="row">
      <div class="col Cian Color " onclick="setColor('#00FFFF')">
      </div>
      <div class="col Magenta Color " onclick="setColor('#FF00FF')">
      </div>
      <div class="col Amarillo Color " onclick="setColor('#FFFF00')">
      </div>
      <div class="col Negro Color "onclick="setColor('#000000')">
      </div>
    </div>
</div>

Con esto tenemos al menos un selector de colores un canvas para pintar encima. El selector de colores, es decir la ventana-paleta estará oculta por ahora.

En la cabecera de nuestro documento necesitaremos incluir los siguiente:

<script language="javascript" src="js/jquery-1.5.1.min.js"></script>
<script language="javascript" src="js/jquery-ui-1.8.14.custom.min.js"></script>
<script language="javascript" src="js/draw-touch.js"></script>

Como el propósito de este tutorial no es explicar los detalles del HTML de la aplicación, sino explicar cómo podemos hacer que nuestra aplicación nos permita pintar colores y mezclarlos con Javascript voy a poner los elementos necesarios para que se pueda pintar. Necesitamos al menos un elemento que muestre nuestra paleta de colores oculta, este elemento puede formar parte de un menú:

<a id="paleta" class="tab-item" onclick="seleccionaPaleta()">
  <div id="color-seleccionado" class="col Color col-center center"></div>
</a>

Primero os voy a contar qué variables vais a necesitar y porqué en vuestro documento Javascript:

//Es el array de colores para guardar la selección actual de colores
var colores = [255, 0, 0, 1];
//Es la última posición(x,y) desde la que el usuario hizo una línea (Luego lo explico)
var lastX = 0;
var lastY = 0;
//Es el porcentaje de mezclado que voy a tener entre colores
var mixval = 0.8;
//Es una variable que controla el número de cerdas, es decir pelos, del pincel
var numCerdasPincel = 80;
//Es un objeto que contiene todo lo relativo al pincel, se detallará más adelante
var cerdasPincel;
//Es el radio que va a tener cada cerda del pincel, se especifica 10 pero luego se cambia
var radio = 10;
//Es el canvas donde vamos a pintar
var drawingCanvas;
//El contexto, es decir, las variables de estado del canvas
var context;
//La distancia de separación entre cerdas del pincel
var dist;
//El ángulo de movimiento de cada cerda del pincel
var angulo;
//Una variable iteradora
var i;
//Control de selección de goma o lápiz
var goma = false;
var lapiz = false;
var marginTop=44;

Una vez mostrada la paleta necesitamos los eventos en Javascript que nos permitan pintar sobre el canvas además de muchos otros que nos modifiquen el tamaño del canvas al tamaño de nuestro viewport para que podamos pintar por todas partes que sería el siguiente código (Esta parte no es necesaria si no se tiene todos los elementos de menú de los que dispongo en esta aplicación):

$(document).ready(function (e) {
//Hacemos que la orientación no pueda cambiarla el usuario ¡Advertencia esto es para móviles!
window.addEventListener('orientationchange', lockOrientation, true);
//Cambiamos el ancho del canvas al tamaño de la ventana
$('#canvas').attr('width', $(window).width());
//Estas líneas sirven para calcular el alto de los elementos de menú que tenemos en la aplicación
//Para poder restarle dicho tamaño el canvas y que se ajuste sólo a la parte visible
//No es necesario si no disponemos de estos elementos
var height = parseInt($('#Cabecera').css('padding-top')) + parseInt($('#Cabecera').css('padding-bottom'));
var height2 = parseInt($('#Tabs').css('padding-top')) + parseInt($('#Tabs').css('padding-bottom'));
//El contenedor lo ajustamos al tamaño de la ventana tanto en ancho como en alto
$('#container').attr('height', $(window).height());
$('#container').attr('width', $(window).width());
//Le quitamos el alto al canvas de los elementos que se encuentran en el DOM que sirven como menú
//Y ponemos el margen al canvas con respecto al tamaño de la barra de menú superior
$('#canvas').attr('height', $(window).height() - $('ion-header-bar').height() - height - $('#Tabs').height() - height2);
$('#canvas').css('top', marginTop + 'px');
//Asigno el elemento canvas
drawingCanvas = document.getElementById('canvas');
//Compruebo si esta establecido el contexto del canvas
if(drawingCanvas.getContext)
{
 //Obtengo el contexto 2d de mi canvas
 context = drawingCanvas.getContext('2d');
 //Esta propiedad del canvas nos permite redondear la unión entre lineas
 context.lineJoin = 'round';
 //Termina las líneas con rendondeo
 context.lineCap = 'round';
 //Inicializamos el objeto cerdasPincel
 cerdasPincel = [];
 //Añadimos el esuchador del evento para móviles touchstart equivalente a mousedown en PC con la función onDown
 drawingCanvas.addEventListener("touchstart",onDown,false);
}
});

Este es nuestro punto de partida, ahora vamos a dividir el problema en partes como buen informático que usa divide y vencerás en sus algoritmos de ordenación:

1. Necesitamos guardar la posición (lastX,lastY) cuando pulsa el canvas, es decir, el evento touchstart, también necesitamos declarar las cerdas del pincel y asignarle los valores, ya que a lo mejor la siguiente vez que pulse el canvas los colores han cambiado o queremos que las cerdas del pincel se muevan de otra forma. Además añadimos en este evento los escuchador touchmove y touchend.

2. Cuando el usuario empiece a moverse por el canvas (evento touchmove) tendremos que guardar la posición donde se ha movido, calcular la distancia desde donde se ha movido hasta donde está actualmente, con esto calculamos la velocidad de movimiento, y por cada una de las cerdas del pincel le asignamos el movimiento que tienen que tener en función de la velocidad,la distancia y el ángulo inicial de cada cerda que han sido calculados en la fase 1. Cogemos el pixel del canvas que está más cercano al radio de donde estamos situados y lo mezclamos con el color de cada cerda del pincel, ya que tenemos el color creamos una linea desde la posición final del pincel anterior hasta la nueva posición donde me encuentro y por último asigno a oldX y oldY las posiciones del siguiente pixel desde donde puedo empezar para que puedan ser usadas en la siguiente línea, y guardamos el lastX y el lastY  que es hasta donde he llegado pintando.

3. Cuando termine el movimiento por el canvas (evento touchend) establecemos lastX y lastY a 0 y quitamos los eventos de touchmove y touchstart.

Ahora que ya sabemos que necesitamos vamos por partes como diría Jack el Destripador:

1. Touchstart:

function onDown(e)
{
  //Evitamos que haga el comportamiento por defecto
 e.preventDefault();
  //Guardamos la Y y la X de donde ha tocado el usuario
 lastX = e.touches[0].pageX;
 lastY = e.touches[0].pageY-marginTop;
  //Añadimos los eventos para cuando mueva el pincel y termine de moverlo
 document.addEventListener("touchmove",onMove,false);
 document.addEventListener("touchend",onUp,false);
  //Establecemos el numero de cercas con respecto al radio del pincel por una constante
 numCerdasPincel = radio*5;
 cerdasPincel = [];
  //Guardo los colores en variables auxiliares
 var rt = colores[0];
 var gt = colores[1];
 var bt = colores[2];
  var at = colores[3];

 for (i = 0; i < numCerdasPincel; ++i)
 {
   //Calculamos la distancia entre cerdas aleatoriamente con respecto al radio
  dist = Math.random() * radio;
  //Calculamos el angulo de la cerda con el que se va a usar para pintarla aleatoriamente
  angulo = Math.random() * 2 * Math.PI;
    //Añadimos las cerdas al array
  cerdasPincel.push({
   ang: angulo, //Angulo que va a describir la cerda
   dist: dist, //La distancia que hemos creado antes
   dx: Math.sin(angulo)*dist, //La distancia en X con respecto al ángulo
   dy: Math.cos(angulo)*dist, //La distancia en Y con respecto al ángulo
   oldX: Math.sin(angulo)*dist + e.clientX, //Establecemos el old como la distancia por el angulo que puede describir en X y la posicion actual
   oldY: Math.cos(angulo)*dist + e.clientY,//Establecemos el old como la distancia por el angulo que puede describir en Y y la posicion actual
   colour:[rt,gt,bt,at] //Guardamos el color con el que ha empezado a pintar
 });
 }

}

2. TouchMove

function onMove(e)
{
  //Evitamos que haga el comportamiento por defecto
 e.preventDefault();
  //Guardamos la Y y la X de donde ha tocado el usuario
 var xp = e.touches[0].pageX;
 var yp = e.touches[0].pageY-marginTop;
 //Con esto calculamos la distancia que ha recorrido desde la última posición en X en Y hasta la actual xp,yp
  //Le quitamos la última posición de donde se encontraba y lo elevamos al cuadrado
 var x2 = Math.pow(xp - lastX, 2);
 var y2 = Math.pow(yp - lastY, 2);
  //Calculamos la velocidad de movimiento haciendo la raiz a la X^2 y la Y^2
 var speed = Math.round(  Math.sqrt(x2 + y2 )) ;
 //Una vez calculada la distancia que la cogemos como velocidad de movimiento
  //Por cada una de las Cerdas del pincel
    for (i = 0; i < numCerdasPincel; i++) {
      //Guardamos la distancia que calculamos con la distancia que tienen que van a tener cada cerda
      //menos la distancia calculada anteriormente por un factor de 0.06 y si diese un número negativo ponemos 0
      var distancia = cerdasPincel[i].dist - (speed * 0.06) < 0 ? 0 : cerdasPincel[i].dist - (speed * 0.06);
      //A la distancia actual le sumamos la distancia que debería recorrer con respecto al ángulo que debería aparecer
      //Y obtenemos la localización a pintar
      var xp2 = xp + cerdasPincel[i].dx;
      var yp2 = yp + cerdasPincel[i].dy;
      //Obtenemos el pixel situado en xp2 y yp2 de la imágen que está actualmente pintada
      var imageData = context.getImageData(xp2, yp2, 1, 1);
      var pixel = imageData.data;

      //Creamos una imágen temporal y un pixel
      var tmpData = context.createImageData(1, 1);
      var tmpPixel = tmpData.data;
      //Comprobamos el alpha del pixel si es 0 es que no se ha pintado nada y establezo el valor al pixel con el color
      if (pixel[3] === 0) {
        pixel[0] = cerdasPincel[i].colour[0];
        pixel[1] = cerdasPincel[i].colour[1];
        pixel[2] = cerdasPincel[i].colour[2];
        //pixel[3] = 0.05;
      }
      //Mezclamos el color de la cerda con el color del pixel con un factor mixval
      var r = mix(cerdasPincel[i].colour[0], pixel[0], mixval);
      var g = mix(cerdasPincel[i].colour[1], pixel[1], mixval);
      var b = mix(cerdasPincel[i].colour[2], pixel[2], mixval);
      var a = mix(cerdasPincel[i].colour[3], pixel[3], mixval);

      //El color que se obtiene lo guardamos en la cerda actual y en el pixel temporal
      cerdasPincel[i].colour[0] = r;
      cerdasPincel[i].colour[1] = g;
      cerdasPincel[i].colour[2] = b;
      cerdasPincel[i].colour[3] = a;

      tmpPixel[0] = r;
      tmpPixel[1] = g;
      tmpPixel[2] = b;
      tmpPixel[3] = a;
      //Creamos un path con el color del pixel temporal con tamaño de línea de 1 desde la posición inicial
      context.beginPath();
      context.strokeStyle = 'rgba( ' + tmpPixel[0] + ', ' + tmpPixel[1] + ', ' + tmpPixel[2] + ', ' + a + ')';
      context.lineWidth = 1;
      //Nos movemos en la imágen a la posición inicial de la cerda donde debería pintarse la línea sólo si no es lápiz
      context.moveTo(cerdasPincel[i].oldX, cerdasPincel[i].oldY);
      if (goma) {
        context.clearRect(xp, yp, radio, radio);
      } else if (lapiz) {
        //Cogemos la primera cerda del bucle y nos salimos del bucle para optimizar
        if (i == 0) {
          //Nos movemos hasta el final de la anterior linea que era lápiz
          context.moveTo(lastX, lastY);
          //Con el radio 10 ya que es un lápiz y no tiene cerdas
          context.lineWidth = radio;
          //Y pintamos hasta donde estamos actualmente
          context.lineTo(xp, yp);
          context.stroke();
          //Guardamos la posición del siguiente pixel al que nos vamos a mover cuando siga arrastrando
          cerdasPincel[i].oldX = xp2;
          cerdasPincel[i].oldY = yp2;

          //Guardamos la posición por la que nos habíamos quedado para empezar desde ahí
          lastX = xp;
          lastY = yp;
          return;
        }
      } else {
        //Calculamos la siguiente distancia de la cerda en base al angulo del la cerda actual
        cerdasPincel[i].dx = Math.sin(cerdasPincel[i].ang) * distancia;
        cerdasPincel[i].dy = Math.cos(cerdasPincel[i].ang) * distancia;
        //Creamos la línea hasta el destino
        context.lineTo(xp2, yp2);
        context.stroke();
        //Asignamos el destino como la posición inicial siguiente de la cerda actual
        cerdasPincel[i].oldX = xp2;
        cerdasPincel[i].oldY = yp2;
      }
    }

  //Guardamos la posición por la que nos habíamos quedado para empezar desde ahí
 lastX = xp;
 lastY = yp;
}

3. Touchend

//Terminamos de pintar y quitamos los listener y las posiciones por donde continuar
function onUp(e)
{
 lastX = 0;
 lastY = 0;

 document.removeEventListener("touchmove",onMove,false);
 document.removeEventListener("touchend",onUp,false);
}

Y nos faltan algunos métodos para establecer colores, mezclarlos y poner la paleta visible:

//Mezcla los colores dependiendo del tipo de pincel
function mix(colour1, colour2, mv)
{
  var val=0;
 if(goma)
  val = 255;
 else if(lapiz)
  val = colour1;
 else
    val = (colour1+colour2)/2;

 return val;
}
//Muestra la paleta para seleccionar el color
function seleccionaPaleta()
{
 if($('#ventana-paleta').hasClass( "Hidden" ))
  $('#ventana-paleta').removeClass('Hidden');
 else
  $('#ventana-paleta').addClass('Hidden');
}
//Establece el color en la variable colores
function setColor(color){
 colores[0] = HexToR(color);
 colores[1] = HexToG(color);
 colores[2] = HexToB(color);
 colores[3] = 1;
 $('#ventana-paleta').addClass('Hidden');
}
//Cambia los string de color a rgb
function HexToR(h) { return parseInt((cutHex(h)).substring(0,2),16) };
function HexToG(h) { return parseInt((cutHex(h)).substring(2,4),16) };
function HexToB(h) { return parseInt((cutHex(h)).substring(4,6),16) };
function cutHex(h) { return (h.charAt(0)=="#") ? h.substring(1,7) : h}

Y esto es todo, ya sabeís como crear una aplicación para mezclar colores en Javascript, si queréis saber más publicaremos el código de la aplicación cuando la terminemos sólo que la nuestra va a ser muy distinta a lo que os he enseñado. Lo más parecido es el código de la página web de donde he sacado la referencia.

Un saludo!!!

Referencias:

Cólores primarioshttp://comose.net/colores-primarios-y-secundarios/

Código de referenciahttp://www.purplesquirrels.com.au/2011/06/javascript-and-canvas-wet-paint-mixing/

Imágen de cabecera diseñada porDiseñado por Freepik

Comenta!

Loading Facebook Comments ...
Leave a Comment