Moisés Gabriel Cachay Tello
Creator, destructor.
@xpktro - LimaJS
Puede verse esta presentación online visitando:
El código fuente mostrado aquí puede encontrarse en la siguiente dirección:
Un fractal es:
La palabra, etimológicamente, proviene del latín fractus significa fragmentado/roto.
El término fue acuñado por Benoit Mandelbrot, quien puso en valor la existencia de estos a través de una serie de publicaciones e investigación en los años 70.
Fractales/conjuntos notables:
<canvas id="canvas">Tu navegador es del 2010 :'(</canvas>
El canvas es un nuevo elemento de HTML5 que brinda una superficie para realizar dibujos con un API sencillo y capacidades de aceleración de hardware.
Para dibujar en un canvas, debemos utilizar una serie de funciones que son parte del contexto de renderizado, que es una palabra bonita para referirse a la interfaz de dibujo del canvas.
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
Las dimensiones del canvas se asumen como 150x300 pixeles, sin embargo pueden cambiarse usando CSS, especificando el tamaño en el mismo elemento o estableciéndolo mediante javascript:
canvas.width = 300;
canvas.height = 300;
A partir de aquí podemos, por ejemplo, dibujar un rectángulo que ocupe toda la pantalla usando un color aleatorio:
context.rect(0, 0, canvas.width, canvas.height);
context.fillStyle = randColor();
context.fill();
function randColor() {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
}
También podemos dibujar líneas:
context.lineWidth = 5;
context.strokeStyle = randColor();
context.beginPath();
context.moveTo(50, 20);
context.lineTo(120, 150);
context.stroke();
El contexto 2d posee una función llamada createImageData que provee de un array con números que podemos manipular para dar un color y una opacidad a cada pixel.
const imageData = context.createImageData(canvas.width, canvas.height);
for(let i = 0; i < imageData.data.length; i += 1) {
imageData.data[i] = Math.random() * 255;
}
context.putImageData(imageData, 0, 0);
El array de imageData es un grupo de números que representan a cada pixel, estos números han de agrupar de 4 en 4, donde los 3 primeros corresponden a los valores RGB del pixel y el cuarto a su opacidad.
const imageData = context.createImageData(canvas.width, canvas.height);
for(let i = 0; i < imageData.data.length; i += 4) {
imageData.data[i] = 255;
imageData.data[i + 1] = 0;
imageData.data[i + 2] = 0;
imageData.data[i + 3] = 255;
}
context.putImageData(imageData, 0, 0);
De hecho, esta será la forma en la que dibujaremos cosas en el canvas. Crearemos una serie de abstracciones para manejar coordenadas en vez de posiciones en el array de imageData.
function indexToCoord (index) {
index /= 4;
const coordinates = {
x: index % canvas.width,
y: Math.floor(index / canvas.width)
}
return coordinates;
}
Si bien esto es bastante útil, también necesitaremos desacoplar los pixeles del canvas del sistema de coordenadas sobre el que se dibujará.
var indexToCoord = function(index) {
index /= 4;
const range = 4;
const coord = {
x: index % 10,
y: (canvas.height - 1) - Math.floor(index / canvas.width)
}
coord.x = ((coord.x * range / canvas.height) - range / 2);
coord.y = ((coord.y * range / canvas.width) - range / 2) * -1;
return coord;
}
La idea es recorrer cada 4-tupla de elementos pertenecientes a imageData, obtener su valor en coordenadas mediante nuestra función, y pintar el pixel respectivo de acuerdo a una función adicional que responderá sí/no.
function render(predicate) {
for(let i = 0; i < imageData.data.length; i += 4) {
const set = predicate(indexToCoord(i)) ? 255 : 0;
this.imageData.data[i] = 0;
this.imageData.data[i + 1] = 0;
this.imageData.data[i + 2] = 0;
this.imageData.data[i + 3] = set;
}
context.putImageData(this.imageData, 0, 0);
}
Para tener esto mucho mejor ordenado y bonito, vamos a hacer una clase que contenga toda la funcionalidad que hemos escrito hasta ahora.
class Graph {
constructor(canvasId, range=4, center={x: 0, y: 0}, canvasWidth=400, canvasHeight=400) {
this.canvas = document.getElementById(canvasId);
this.canvas.width = canvasWidth;
this.canvas.height = canvasHeight;
this.context = this.canvas.getContext('2d');
this.imageData = this.context.createImageData(canvasWidth, canvasHeight);
this.range = range;
this.center = center;
}
indexToCoord(index) {
index /= 4;
const coordinates = {
x: index % this.canvas.width,
y: (this.canvas.height - 1) - Math.floor(index / this.canvas.width)
};
coordinates.x = (coordinates.x/this.canvas.width * this.range) - this.range/2 + this.center.x;
coordinates.y = (coordinates.y/this.canvas.height * this.range) - this.range/2 + this.center.y;
return coordinates;
}
render(predicate) {
for(let i = 0; i < this.imageData.data.length; i += 4) {
const set = predicate(this.indexToCoord(i)) ? 255 : 0;
this.imageData.data[i] = 0;
this.imageData.data[i + 1] = 0;
this.imageData.data[i + 2] = 0;
this.imageData.data[i + 3] = set;
}
this.context.putImageData(this.imageData, 0, 0);
}
}
Hagamos una prueba simple con nuestra nueva clase:
const graph = new Graph('canvas');
graph.render(function(coord) {
return coord.x == coord.y
|| coord.x * 2 == coord.y
|| coord.x * 3 == coord.y
|| coord.x * 4 == coord.y
|| coord.x * 5 == coord.y
});
El conjunto de Mandelbrot se define como:
El conjunto de todos los números complejos que al iterarse sobre la función $$ f(z) = z^{2} + c $$esta se mantiene acotada.
Números complejos
Las operaciones que necesitaremos hacer con estos números son las siguientes:
$$ z^{2} = (a, bi)^{2} = a^{2} - b^{2}, (2 \times a \times b)i $$
$$ z_{1} + z_{2} = (a_{1}, b_{1}i) + (a_{2}, b_{2}i) = (a_{1} + a_{2}), (b_{1} + b_{2})i $$
Iterar sobre una función significa repetirla usando el último resultado obtenido de la misma:
$$ z_{1} = f(z) $$
$$ z_{2} = f(z_{1}) $$
$$ z_{3} = f(z_{2}) $$
$$ z_{4} = ... $$
Siendo que
$$ f(z) = (a, bi) $$
La función del conjunto de Mandelbrot se mantiene acotada cuando tras cierto número de iteraciones:
$$ a^{2} + b^{2} <= 4 $$
En resumen:
Tenemos que repetir $$ z^{2} + c $$ siendo z un punto en la pantalla y c un valor arbitrario hasta que $$ a^{2} + b^{2} < 4 $$ o agotemos cierta cantidad de intentos
En forma de código:
graph.render((coordinates) => {
let c_real = coordinates.x
, c_imaginary = coordinates.y
, z_real = c_real
, z_imaginary = c_imaginary;
for(let i = 0; i < 400; i++) {
if(z_real ** 2 + z_imaginary ** 2 > 4) {
return false;
}
// z ** 2 + c
let newz_real = (z_real * z_real) - (z_imaginary * z_imaginary) + c_real
, newz_imaginary = ((z_real * z_imaginary) * 2) + c_imaginary;
z_real = newz_real;
z_imaginary = newz_imaginary;
}
return true;
});
Graph('canvas', 0.005, {x: -0.7463, y: 0.1102});
Graph('canvas', 0.00065, {x: -0.7453, y: 0.1127});
Graph('canvas', 0.046, {x: -0.16, y: 1.0405});
Los colores se producen al tomar en cuenta la cantidad de iteraciones realizadas para determinar una respuesta:
graph.render((coordinates) => {
let c_real = coordinates.x
, c_imaginary = coordinates.y
, z_real = c_real
, z_imaginary = c_imaginary
, iterations = 400
, i = 0;
for(; i < iterations; i++) {
if(z_real ** 2 + z_imaginary ** 2 > 4) {
return [false, i];
}
// z ** 2 + c
let newz_real = (z_real * z_real) - (z_imaginary * z_imaginary) + c_real
, newz_imaginary = ((z_real * z_imaginary) * 2) + c_imaginary;
z_real = newz_real;
z_imaginary = newz_imaginary;
}
return [true, i];
});
Finalmente, la función de renderizado debe ajustarse a este cambio:
render(predicate) {
for(let i = 0; i < this.imageData.data.length; i += 4) {
const result = predicate(this.indexToCoord(i))
, onSet = result[0]
, iterations = result[1]
, color = onSet ? 0 : iterations/400;
this.imageData.data[i] = color * 2000;
this.imageData.data[i + 1] = color * 255;
this.imageData.data[i + 2] = color * 5000;
this.imageData.data[i + 3] = 255;
}
this.context.putImageData(this.imageData, 0, 0);
}
Bonus: Versión WebGL
Bonus: Variaciones de la fórmula
?
By Moisés Gabriel Cachay Tello