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:
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 primarios: http://comose.net/colores-primarios-y-secundarios/
Código de referencia: http://www.purplesquirrels.com.au/2011/06/javascript-and-canvas-wet-paint-mixing/
Imágen de cabecera diseñada por: Diseñado por Freepik
Comenta!