136 lines
3.5 KiB
JavaScript
136 lines
3.5 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
import { body, create } from './dom';
|
||
|
import { randomNumber, randomColor } from './utils';
|
||
|
|
||
|
const content = body.get('#content');
|
||
|
const plotting = body.get('#plotting');
|
||
|
const ctx = plotting.node.getContext('2d');
|
||
|
|
||
|
const padding = 40; // padding between the canvas edges and the points
|
||
|
let image, width, height;
|
||
|
let lastUpdate = -Infinity;
|
||
|
|
||
|
/**
|
||
|
* Create a fractal of given width, height, based on
|
||
|
* a polygon of given amount of vertices, using the
|
||
|
* chaos game applied with given fraction
|
||
|
*
|
||
|
* @param {number} width Fractal width
|
||
|
* @param {number} height Fractal height
|
||
|
* @param {number} fraction Fraction to use
|
||
|
* @param {Array} colors Color of each vertex
|
||
|
* @return {ImageData} Generated pixel data
|
||
|
*/
|
||
|
const chaos = (width, height, fraction, colors) => {
|
||
|
const cx = Math.floor(width / 2);
|
||
|
const cy = Math.floor(height / 2);
|
||
|
const radius = Math.min(cx, cy);
|
||
|
|
||
|
const count = colors.length;
|
||
|
const vertices = [];
|
||
|
const angleStep = 2 * Math.PI / count;
|
||
|
let initialAngle;
|
||
|
|
||
|
// creating 0-width image data will throw an error
|
||
|
if (width <= 0 || height <= 0) {
|
||
|
return ctx.createImageData(1, 1);
|
||
|
}
|
||
|
|
||
|
const image = ctx.createImageData(width, height);
|
||
|
const data = image.data;
|
||
|
|
||
|
// we will rotate around an inscribed circle to calculate
|
||
|
// the vertices' positions. We adapt the initial angle so
|
||
|
// that usual polygons look better
|
||
|
if (count === 3) {
|
||
|
initialAngle = -Math.PI / 2;
|
||
|
} else if (count === 4) {
|
||
|
initialAngle = Math.PI / 4;
|
||
|
} else {
|
||
|
initialAngle = 0;
|
||
|
}
|
||
|
|
||
|
for (let i = 0; i < count; i += 1) {
|
||
|
let current = angleStep * i + initialAngle;
|
||
|
|
||
|
vertices.push([
|
||
|
Math.floor(Math.cos(current) * radius + cx),
|
||
|
Math.floor(Math.sin(current) * radius + cy)
|
||
|
]);
|
||
|
}
|
||
|
|
||
|
// now we apply the chaos algorithm:
|
||
|
// for any point, the next point is a `fraction` of the
|
||
|
// distance between it and a random vertex
|
||
|
let point = vertices[0];
|
||
|
let iterations = 200000;
|
||
|
let drop = 1000;
|
||
|
|
||
|
while (iterations--) {
|
||
|
const vertexNumber = randomNumber(0, count);
|
||
|
const vertex = vertices[vertexNumber], color = colors[vertexNumber];
|
||
|
|
||
|
point = [
|
||
|
Math.floor((point[0] - vertex[0]) * fraction + vertex[0]),
|
||
|
Math.floor((point[1] - vertex[1]) * fraction + vertex[1])
|
||
|
];
|
||
|
|
||
|
// skip the first 1000 points
|
||
|
if (drop === 0) {
|
||
|
const i = (point[1] * width + point[0]) * 4;
|
||
|
|
||
|
data[i] = color[0];
|
||
|
data[i + 1] = color[1];
|
||
|
data[i + 2] = color[2];
|
||
|
data[i + 3] = 255;
|
||
|
} else {
|
||
|
drop--;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return image;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Render the scene, recalculating the points
|
||
|
* positions if they need to
|
||
|
*
|
||
|
* @return {null}
|
||
|
*/
|
||
|
const render = () => {
|
||
|
// only recalculate every 16.67 ms
|
||
|
if (+new Date() - lastUpdate > 16.67) {
|
||
|
image = chaos(
|
||
|
width - 2 * padding, height - 2 * padding, 1/2,
|
||
|
[[255, 0, 0], [0, 255, 0], [0, 0, 255]]
|
||
|
);
|
||
|
|
||
|
lastUpdate = +new Date();
|
||
|
}
|
||
|
|
||
|
ctx.clearRect(0, 0, width, height);
|
||
|
ctx.putImageData(
|
||
|
image, padding, padding
|
||
|
);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Resize the canvas to fit the new
|
||
|
* window size and redraw the scene
|
||
|
*
|
||
|
* @return {null}
|
||
|
*/
|
||
|
const resize = () => {
|
||
|
width = content.node.clientWidth;
|
||
|
height = content.node.clientHeight;
|
||
|
|
||
|
plotting.setAttr('width', width);
|
||
|
plotting.setAttr('height', height);
|
||
|
|
||
|
render();
|
||
|
};
|
||
|
|
||
|
window.onresize = resize;
|
||
|
resize();
|