El código hermoso es una alegría para escribir, pero es difícil compartir esa alegría con otros programadores, por no mencionar los que no son programadores. En mi tiempo libre entre mi trabajo diario y mi tiempo familiar, he estado jugando con la idea de un poema de programación usando el elemento canvas para dibujar en el navegador. Hay una multitud de términos para describir experimentos visuales en la computadora como desarrollo de arte, bosquejo de código, demostración y arte interactivo, pero al final me decidí a programar poemas para describir este proceso. La idea detrás de un poema es una pieza de prosa pulida que se puede compartir fácilmente, conciso y estético. No es una idea a medio terminar en un cuaderno de bocetos, sino una pieza cohesiva presentada al espectador para su disfrute. Un poema no es una herramienta, sino que existe para evocar una emoción.
Para mi propio disfrute, he estado leyendo libros sobre matemáticas, computación, física y biología. Aprendí muy rápido que cuando uso una idea, aburre a la gente bastante rápido. Visualmente puedo tomar algunas de estas ideas que me parecen fascinantes, y rápidamente le doy a alguien una sensación de asombro, incluso si no entienden la teoría detrás del código y los conceptos que lo impulsan. No necesita manejar una filosofía o matemática difícil para escribir un poema de programación, solo el deseo de ver algo en vivo y respirar en la pantalla.
El código y los ejemplos que he reunido a continuación iniciarán una comprensión de cómo llevar a cabo este proceso rápido y altamente satisfactorio. Si desea seguir el código, puede descarga los archivos fuente aquí.
El truco principal cuando se crea un poema es mantenerlo ligero y simple. No pases tres meses construyendo una demo realmente genial. En cambio, crea 10 poemas que desarrollen una idea. Escriba código experimental que sea emocionante y no tenga miedo de fallar.
Para una visión general rápida, el lienzo es esencialmente un elemento de imagen de mapa de bits 2d que vive en DOM que se puede dibujar. El dibujo se puede hacer usando un contexto 2d o un contexto WebGL. El contexto es el objeto JavaScript que usa para acceder a las herramientas de dibujo. Los eventos de JavaScript que están disponibles para el lienzo son muy simples, a diferencia de los disponibles para SVG. Cualquier evento que se desencadena es para el elemento como un todo, no como algo dibujado en el lienzo, como un elemento de imagen normal. Aquí hay un ejemplo básico de lienzo:
var canvas = document.getElementById('example-canvas');var context = canvas.getContext('2d');//Draw a blue rectanglecontext.fillStyle = '#91C0FF';context.fillRect(100, // x100, // y400, // width200 // height);//Draw some textcontext.fillStyle = '#333';context.font = "18px Helvetica, Arial";context.textAlign = 'center';context.fillText("The wonderful world of canvas", // text300, // x200 // y);
Es bastante sencillo comenzar. Lo único que podría ser un poco confuso es que el contexto debe configurarse con las configuraciones como fillStyle, lineWidth, font y strokeStyle antes de que se use la llamada real. Es fácil olvidarse de actualizar o restablecer esas configuraciones y obtener algunos resultados no deseados.
El primer ejemplo solo se ejecutó una vez y dibujó una imagen estática en el lienzo. Está bien, pero cuando realmente se divierte es cuando se actualiza a 60 cuadros por segundo. Los navegadores modernos tienen incorporada la función requestAnimationFrame que sincroniza el código de dibujo personalizado con los ciclos de dibujo del navegador. Esto ayuda en términos de eficiencia y suavidad. El objetivo de una visualización debe ser un código que vibre a 60 cuadros por segundo.
(Una nota sobre soporte: hay algunos polyfills simples disponibles si necesita admitir navegadores más antiguos).
var canvas = document.getElementById('example-canvas');var context = canvas.getContext('2d');var counter = 0;var rectWidth = 40;var rectHeight = 40;var xMovement;//Place rectangle in the middle of the screenvar y = ( canvas.height / 2 ) - ( rectHeight / 2 );context.fillStyle = '#91C0FF';function draw() {//There are smarter ways to increment time, but this is for demonstration purposescounter++;//Cool math below. More explanation in the text following the code.xMovement = Math.sin(counter / 25) * canvas.width * 0.4 + canvas.width / 2 - rectWidth / 2;//Clear the previous drawing resultscontext.clearRect(0, 0, canvas.width, canvas.height);//Actually draw on the canvascontext.fillRect(xMovement,y,rectWidth,rectHeight);//Request once a new animation frame is available to call this function againrequestAnimationFrame( draw );}draw();
Ahora reescribiré mi fórmula del ejemplo del código anterior como una versión más desglosada que es más fácil de leer.
var a = 1 / 25, //Make the oscillation happen a lot slowerx = counter, //Move along the graph a little bit each time draw() is calledb = 0, //No need to adjust the graph up or downc = width * 0.4, //Make the oscillation as wide as a little less than half the canvasd = canvas.width / 2 - rectWidth / 2; //Tweak the position of the rectangle to be centeredxMovement = Math.sin( a * x + b ) * c + d;
Si quiere jugar con el código hasta el momento, le sugiero agregar algún movimiento en la dirección y. Intenta cambiar los valores en la función sin, o cambia a algún otro tipo de función para jugar y ver qué pasa.
Además de manejar el movimiento con las matemáticas, tómese un momento para imaginar lo que puede hacer con diferentes dispositivos de entrada de usuario para mover un cuadrado alrededor de una página. Hay todo tipo de opciones disponibles en el navegador, incluido el micrófono, la cámara web, el mouse, el teclado y el gamepad. Se encuentran disponibles opciones adicionales basadas en el complemento con algo parecido a Leap Motion o Kinect. Al usar WebSockets y un servidor, puede conectar una visualización al hardware de fabricación casera. Conecta un micrófono a Web Audio API y conduce tus píxeles con sonido. Incluso puedes construir un sensor de movimiento desde una cámara web y asustar a una escuela de peces virtuales (bueno, hice la última en Flash hace unos cinco años).
Entonces, ahora que tienes tu gran idea, volvamos a algunos ejemplos más. Un cuadrado es aburrido, suba la apuesta. Primero, creemos una función cuadrada que puede hacer mucho. Lo llamaremos un Punto. Una cosa que ayuda cuando se trabaja con objetos en movimiento es utilizar vectores en lugar de separar las variables xey. En estos ejemplos de código, seleccioné la clase Vector.res.js. Es fácil de usar de inmediato con vector.x y vector.y, pero también tiene un montón de métodos prácticos para trabajar con ellos. Echa un vistazo a los documentos para una inmersión más profunda.
El código de este ejemplo se vuelve un poco más complejo porque interactúa con objetos, pero valdrá la pena. Echa un vistazo al código de ejemplo para ver un nuevo objeto de escena que maneja los aspectos básicos del dibujo en el lienzo. Nuestra nueva clase Dot obtendrá un control de esta escena para acceder a cualquier variable como el contexto canvas que necesitará.
function Dot( x, y, scene ) {var speed = 0.5;this.color = '#000000';this.size = 10;this.position = new THREE.Vector2(x,y);this.direction = new THREE.Vector2(speed * Math.random() - speed / 2,speed * Math.random() - speed / 2);this.scene = scene;}
Para comenzar, el constructor de Dot configura la configuración de su comportamiento y establece algunas variables para usar. De nuevo, esto está usando la clase de vectores three.js. Al renderizar a 60 fps, es importante inicializar previamente los objetos y no crear nuevos mientras se anima. Esto se come en su memoria disponible y puede hacer que su visualización entrecortada. Además, observe cómo se pasa al punto una copia de la escena por referencia. Esto mantiene las cosas limpias.
Dot.prototype = {update : function() {...},draw : function() {...}}
Todo el resto del código se establecerá en el objeto prototipo del punto para que cada nuevo punto que se cree tenga acceso a estos métodos. Iré a funcionar por función en la explicación.
update : function( dt ) {this.updatePosition( dt );this.draw( dt );},
Estoy separando mi código de extracción de mi código de actualización. Esto hace que sea mucho más fácil mantener y modificar su objeto, al igual que el patrón MVC separa el control y la lógica de visualización. La variable dt es el cambio en el tiempo en milisegundos desde la última llamada de actualización. El nombre es bueno y corto y proviene de (no tengas miedo) derivados de cálculo. Lo que esto hace es separar su movimiento de la velocidad de la velocidad de cuadros. De esta forma, no se ralentiza el estilo de NES cuando las cosas se vuelven demasiado complicadas. Tu movimiento soltará los cuadros si está trabajando duro, pero se mantendrá a la misma velocidad.
updatePosition : function() {//This is a little trick to create a variable outside of the render loop//It's expensive to allocate memory inside of the loop.//The variable is only accessible to the function below.var moveDistance = new THREE.Vector2();//This is the actual functionreturn function( dt ) {moveDistance.copy( this.direction );moveDistance.multiplyScalar( dt );this.position.add( moveDistance );//Keep the dot on the screenthis.position.x = (this.position.x + this.scene.canvas.width) % this.scene.canvas.width;this.position.y = (this.position.y + this.scene.canvas.height) % this.scene.canvas.height;}}(), //Note that this function is immediately executed and returns a different function
Esta función es un poco extraña en su estructura, pero útil para las visualizaciones. Es realmente costoso asignar memoria en una función. La variable moveDistance se establece una vez y se vuelve a usar cada vez que se llama a la función.
Este vector solo se usa para ayudar a calcular la nueva posición, pero no se usa fuera de la función. Este es el primer vector matemático que se está usando. En este momento, el vector de dirección se multiplica por el cambio en el tiempo y luego se agrega a la posición. Al final hay una pequeña acción de módulo para mantener el punto en la pantalla.
draw : function(dt) {//Get a short variable name for conveniencevar ctx = this.scene.context;ctx.beginPath();ctx.fillStyle = this.color;ctx.fillRect(this.position.x, this.position.y, this.size, this.size);}
Finalmente, lo fácil. Obtenga una copia del contexto del objeto de escena y luego dibuje un rectángulo (o lo que quiera). Los rectángulos son probablemente lo más rápido que puede dibujar en la pantalla.
En este punto agrego un nuevo punto llamando a this.dot = new Dot (x, y, this) en el constructor principal de la escena, y luego en el método de actualización de escena agrego un this.dot.update (dt) y hay un punto que se acerca a la pantalla. (Consulte el código fuente del código completo en contexto).
Ahora en la escena, en lugar de crear y actualizar un punto , creamos y actualizamos el DotManager . Crearemos 5000 puntos para comenzar.
function Scene() {...this.dotManager = new DotManager(5000, this);...};Scene.prototype = {...update : function( dt ) {this.dotManager.update( dt );}...};
Es un poco confuso en una línea, así que aquí está descompuesto como la función pecado de antes.
var a = 1 / 500, //Make the oscillation happen a lot slowerx = this.scene.currTime, //Move along the graph a little bit each time draw() is calledb = this.position.x / this.scene.canvas.width * 4, //No need to adjust the graph up or downc = 20, //Make the oscillation as wide as a little less than half the canvasd = 0; //Tweak the position of the rectangle to be centeredxMovement = Math.sin( a * x + b ) * c + d;
Conseguir groovy ...
Una pequeña modificación más. El monocromo es un poco gris, así que vamos a agregar un poco de color.
var hue = this.position.x / this.scene.canvas.width * 360;this.color = Utils.hslToFillStyle(hue, 50, 50, 0.5);
Este objeto simple encapsula la lógica de las actualizaciones del mouse del resto de la escena. Solo actualiza el vector de posición en un movimiento de mouse. El resto de los objetos pueden tomar muestras del vector de posición del mouse si se les pasa una referencia al objeto. Una advertencia que ignoro aquí es si el ancho del lienzo no es uno a uno con las dimensiones en píxeles del DOM, es decir, un lienzo de tamaño reajustable o un lienzo de mayor densidad de píxeles (retina) o si el lienzo no está ubicado en el arriba a la izquierda. Las coordenadas del mouse deberán ajustarse en consecuencia.
var Scene = function() {...this.mouse = new Mouse( this );...};
Lo único que le quedaba al mouse era crear el objeto del mouse dentro de la escena. Ahora que tenemos un mouse, atraigamos los puntos.
function Dot( x, y, scene ) {...this.attractSpeed = 1000 * Math.random() + 500;this.attractDistance = (150 * Math.random()) + 180;...}
Agregué algunos valores escalares al punto para que cada uno se comporte un poco diferente en la simulación para darle un poco de realismo. Juega con estos valores para obtener una sensación diferente. Ahora vamos al método del mouse de atracción. Es un poco largo con los comentarios.
attractMouse : function() {//Again, create some private variables for this methodvar vectorToMouse = new THREE.Vector2(),vectorToMove = new THREE.Vector2();//This is the actual public methodreturn function(dt) {var distanceToMouse, distanceToMove;//Get a vector that represents the x and y distance from the dot to the mouse//Check out the three.js documentation for more information on how these vectors workvectorToMouse.copy( this.scene.mouse.position ).sub( this.position );//Get the distance to the mouse from the vectordistanceToMouse = vectorToMouse.length();//Use the individual scalar values for the dot to adjust the distance movedmoveLength = dt * (this.attractDistance - distanceToMouse) / this.attractSpeed;//Only move the dot if it's being attractedif( moveLength > 0 ) {//Resize the vector to the mouse to the desired move lengthvectorToMove.copy( vectorToMouse ).divideScalar( distanceToMouse ).multiplyScalar( moveLength );//Go ahead and add it to the current position now, rather than in the draw callthis.position.add(vectorToMove);}};}()
Este método podría ser un poco confuso si no está actualizado en su matemática vectorial. Los vectores pueden ser muy visuales y pueden ayudar si dibuja algunos garabatos en un trozo de papel manchado de café. En lenguaje simple, esta función está obteniendo la distancia entre el mouse y el punto. Luego, acerca el punto al punto en función de lo cerca que esté del punto y la cantidad de tiempo que ha transcurrido. Hace esto calculando la distancia para moverse (un número escalar normal), y luego multiplicando eso por el vector normalizado (un vector con longitud 1) del punto que apunta al mouse. Ok, esa última frase no era necesariamente simple, pero es un comienzo.