💡 Add parameters
This commit is contained in:
parent
e7f7239b56
commit
8f94659eea
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
|
||||||
|
"rules": {
|
||||||
|
"no-shadow": 2,
|
||||||
|
"no-catch-shadow": 2,
|
||||||
|
"no-shadow-restricted-names": 2,
|
||||||
|
"radix": 2,
|
||||||
|
|
||||||
|
"wrap-iife": 2,
|
||||||
|
"yoda": 2,
|
||||||
|
"semi": 2,
|
||||||
|
"indent": 2,
|
||||||
|
"camelcase": 2,
|
||||||
|
"brace-style": 2,
|
||||||
|
"comma-spacing": 2,
|
||||||
|
"comma-style": 2,
|
||||||
|
"quotes": [2, "single", "avoid-escape"],
|
||||||
|
"no-spaced-func": 2,
|
||||||
|
"space-after-keywords": 2,
|
||||||
|
"space-before-blocks": 2,
|
||||||
|
"space-before-function-paren": [2, {"anonymous": "always", "named": "never"}],
|
||||||
|
"space-in-parens": 2,
|
||||||
|
"space-infix-ops": 2,
|
||||||
|
"space-return-throw-case": 2,
|
||||||
|
"space-unary-ops": 2,
|
||||||
|
"no-trailing-spaces": 0,
|
||||||
|
"no-underscore-dangle": 0
|
||||||
|
},
|
||||||
|
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"modules": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"env": {
|
||||||
|
"es6": true,
|
||||||
|
"browser": true
|
||||||
|
}
|
||||||
|
}
|
268
bundle.js
268
bundle.js
|
@ -465,139 +465,201 @@ function _typeof(obj) { return obj && typeof Symbol !== "undefined" && obj.const
|
||||||
}, {}], 6: [function (require, module, exports) {
|
}, {}], 6: [function (require, module, exports) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var _theDom = require('the-dom');
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.applyChaos = exports.scaleVertices = exports.createRegularVertices = undefined;
|
||||||
|
|
||||||
var _utils = require('./utils');
|
var _utils = require('./utils');
|
||||||
|
|
||||||
var _html = (0, _theDom.html)(document);
|
|
||||||
|
|
||||||
var body = _html.body;
|
|
||||||
var create = _html.create;
|
|
||||||
|
|
||||||
var content = body.find('#content');
|
|
||||||
var plotting = body.find('#plotting');
|
|
||||||
var ctx = plotting.node.getContext('2d');
|
|
||||||
|
|
||||||
var padding = 40; // padding between the canvas edges and the points
|
|
||||||
var image = undefined,
|
|
||||||
width = undefined,
|
|
||||||
height = undefined;
|
|
||||||
var lastUpdate = -Infinity;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a fractal of given width, height, based on
|
* Calculate the position of a regular polygon's vertices
|
||||||
* a polygon of given amount of vertices, using the
|
* inside a 2 x 2 squared centered on the origin
|
||||||
* chaos game applied with given fraction
|
|
||||||
*
|
*
|
||||||
* @param {number} width Fractal width
|
* @param {number} count Vertices amount
|
||||||
* @param {number} height Fractal height
|
* @return {Array<Array>} Array of points representing the vertices
|
||||||
* @param {number} fraction Fraction to use
|
|
||||||
* @param {Array} colors Color of each vertex
|
|
||||||
* @return {ImageData} Generated pixel data
|
|
||||||
*/
|
*/
|
||||||
var chaos = function chaos(width, height, fraction, colors) {
|
var createRegularVertices = exports.createRegularVertices = function createRegularVertices(count) {
|
||||||
var cx = Math.floor(width / 2);
|
var step = 2 * Math.PI / count;
|
||||||
var cy = Math.floor(height / 2);
|
var initial = -Math.atan(Math.sin(step) / (Math.cos(step) - 1));
|
||||||
var radius = Math.min(cx, cy);
|
var result = [];
|
||||||
|
|
||||||
var count = colors.length;
|
|
||||||
var vertices = [];
|
|
||||||
var angleStep = 2 * Math.PI / count;
|
|
||||||
var initialAngle = undefined;
|
|
||||||
|
|
||||||
// creating 0-width image data will throw an error
|
|
||||||
if (width <= 0 || height <= 0) {
|
|
||||||
return ctx.createImageData(1, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
var image = ctx.createImageData(width, height);
|
|
||||||
var 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 (var i = 0; i < count; i += 1) {
|
for (var i = 0; i < count; i += 1) {
|
||||||
var current = angleStep * i + initialAngle;
|
var current = step * i + initial;
|
||||||
|
|
||||||
vertices.push([Math.floor(Math.cos(current) * radius + cx), Math.floor(Math.sin(current) * radius + cy)]);
|
result.push([Math.cos(current), Math.sin(current)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale the vertices so that they fit in given bounding rectangle
|
||||||
|
*
|
||||||
|
* @param {number} width Bounding rectangle width
|
||||||
|
* @param {number} height Bounding rectangle height
|
||||||
|
* @param {Array<Array>} vertices Vertices to scale
|
||||||
|
* @return {Array<Array>} Scaled vertices
|
||||||
|
*/
|
||||||
|
var scaleVertices = exports.scaleVertices = function scaleVertices(width, height, vertices) {
|
||||||
|
var centerX = Math.floor(width / 2);
|
||||||
|
var centerY = Math.floor(height / 2);
|
||||||
|
var radius = Math.min(centerX, centerY);
|
||||||
|
|
||||||
|
return vertices.map(function (vertex) {
|
||||||
|
return [vertex[0] * radius + centerX, vertex[1] * radius + centerY];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the chaos game algorithm in a polygon
|
||||||
|
* of given vertices, with given fraction
|
||||||
|
*
|
||||||
|
* @param {ImageData} image Image to write on Data to amend
|
||||||
|
* @param {number} fraction Fraction to use
|
||||||
|
* @param {Array} vertices List of vertices of the bounding polygon
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
var applyChaos = exports.applyChaos = function applyChaos(image, fraction, vertices) {
|
||||||
|
var count = vertices.length,
|
||||||
|
imageWidth = image.width;
|
||||||
|
|
||||||
// now we apply the chaos algorithm:
|
// now we apply the chaos algorithm:
|
||||||
// for any point, the next point is a `fraction` of the
|
// for any point, the next point is a `fraction` of the
|
||||||
// distance between it and a random vertex
|
// distance between it and a random vertex
|
||||||
var point = vertices[0];
|
var point = vertices[0];
|
||||||
var iterations = 200000;
|
var iterations = Math.floor(500 * imageWidth * fraction);
|
||||||
var drop = 1000;
|
var drop = Math.floor(iterations / 200);
|
||||||
|
|
||||||
while (iterations--) {
|
while (iterations--) {
|
||||||
var vertexNumber = (0, _utils.randomNumber)(0, count);
|
var vertexNumber = (0, _utils.getRandomNumber)(0, count);
|
||||||
var vertex = vertices[vertexNumber],
|
var vertex = vertices[vertexNumber],
|
||||||
color = colors[vertexNumber];
|
color = (0, _utils.getColor)(vertexNumber);
|
||||||
|
|
||||||
point = [Math.floor((point[0] - vertex[0]) * fraction + vertex[0]), Math.floor((point[1] - vertex[1]) * fraction + vertex[1])];
|
point = [Math.floor((point[0] - vertex[0]) * fraction + vertex[0]), Math.floor((point[1] - vertex[1]) * fraction + vertex[1])];
|
||||||
|
|
||||||
// skip the first 1000 points
|
// skip the first 1000 points
|
||||||
if (drop === 0) {
|
if (drop === 0) {
|
||||||
var i = (point[1] * width + point[0]) * 4;
|
var i = (point[1] * imageWidth + point[0]) * 4;
|
||||||
|
|
||||||
data[i] = color[0];
|
image.data[i] = color[0];
|
||||||
data[i + 1] = color[1];
|
image.data[i + 1] = color[1];
|
||||||
data[i + 2] = color[2];
|
image.data[i + 2] = color[2];
|
||||||
data[i + 3] = 255;
|
image.data[i + 3] = 255;
|
||||||
} else {
|
} else {
|
||||||
drop--;
|
drop--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return image;
|
|
||||||
};
|
};
|
||||||
|
}, { "./utils": 8 }], 7: [function (require, module, exports) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var _theDom = require('the-dom');
|
||||||
|
|
||||||
|
var _utils = require('./utils');
|
||||||
|
|
||||||
|
var _chaos = require('./chaos');
|
||||||
|
|
||||||
|
var _html = (0, _theDom.html)(document);
|
||||||
|
|
||||||
|
var body = _html.body;
|
||||||
|
|
||||||
|
var content = body.find('#content');
|
||||||
|
var verticesRange = body.find('#vertices');
|
||||||
|
var fractionRange = body.find('#fraction');
|
||||||
|
|
||||||
|
var plotting = body.find('#plotting').node;
|
||||||
|
var ctx = plotting.getContext('2d');
|
||||||
|
|
||||||
|
var padding = 40; // padding between the canvas edges and the points
|
||||||
|
var width = undefined,
|
||||||
|
height = undefined,
|
||||||
|
vertices = undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the scene, recalculating the points
|
* Re-render the scene from scratch
|
||||||
* positions if they need to
|
|
||||||
*
|
*
|
||||||
* @return {null}
|
* @return {null}
|
||||||
*/
|
*/
|
||||||
var render = function render() {
|
var render = function render() {
|
||||||
// only recalculate every 16.67 ms
|
var fraction = 1 / parseFloat(fractionRange.node.value);
|
||||||
if (+new Date() - lastUpdate > 16.67) {
|
var scaledVerts = (0, _chaos.scaleVertices)(width, height, vertices);
|
||||||
image = chaos(width - 2 * padding, height - 2 * padding, 1 / 2, [[255, 0, 0], [0, 255, 0], [0, 0, 255]]);
|
|
||||||
|
|
||||||
lastUpdate = +new Date();
|
plotting.width = width + 2 * padding;
|
||||||
|
plotting.height = height + 2 * padding;
|
||||||
|
|
||||||
|
// do not plot (very) small sizes
|
||||||
|
if (width < 1) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
// draw the polygon
|
||||||
|
ctx.strokeStyle = '#aaa';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
for (var i = 0; i < vertices.length; i += 1) {
|
||||||
|
ctx.lineTo(scaledVerts[i][0] + padding, scaledVerts[i][1] + padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// draw the vertices
|
||||||
|
for (var i = 0; i < vertices.length; i += 1) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.fillStyle = 'rgb(' + (0, _utils.getColor)(i).join(', ') + ')';
|
||||||
|
|
||||||
|
ctx.arc(scaledVerts[i][0] + padding, scaledVerts[i][1] + padding, 4, 0, Math.PI * 2);
|
||||||
|
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the chaos game
|
||||||
|
var image = ctx.getImageData(padding, padding, width, height);
|
||||||
|
|
||||||
|
(0, _chaos.applyChaos)(image, fraction, scaledVerts);
|
||||||
ctx.putImageData(image, padding, padding);
|
ctx.putImageData(image, padding, padding);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resize the canvas to fit the new
|
* Update the scene when the window has been resized
|
||||||
* window size and redraw the scene
|
|
||||||
*
|
*
|
||||||
* @return {null}
|
* @return {null}
|
||||||
*/
|
*/
|
||||||
var resize = function resize() {
|
var resize = function resize() {
|
||||||
width = content.node.clientWidth;
|
width = content.node.clientWidth - 2 * padding;
|
||||||
height = content.node.clientHeight;
|
height = content.node.clientHeight - 2 * padding;
|
||||||
|
|
||||||
plotting.setAttr('width', width);
|
|
||||||
plotting.setAttr('height', height);
|
|
||||||
|
|
||||||
render();
|
render();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new vertices
|
||||||
|
*/
|
||||||
|
verticesRange.on('input', function () {
|
||||||
|
vertices = (0, _chaos.createRegularVertices)(parseInt(verticesRange.node.value, 10));
|
||||||
|
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
window.onresize = resize;
|
window.onresize = resize;
|
||||||
|
fractionRange.on('input', render);
|
||||||
|
|
||||||
|
vertices = (0, _chaos.createRegularVertices)(3);
|
||||||
resize();
|
resize();
|
||||||
}, { "./utils": 7, "the-dom": 1 }], 7: [function (require, module, exports) {
|
}, { "./chaos": 6, "./utils": 8, "the-dom": 1 }], 8: [function (require, module, exports) {
|
||||||
'use strict'
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
var colors = ['#F44336', '#2196F3', '#4CAF50', '#F9A825', '#E91E63', '#00838F'].map(function (color) {
|
||||||
|
return color.match(/[A-F0-9]{2}/g).map(function (component) {
|
||||||
|
return parseInt(component, 16);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a random whole number
|
* Get a random whole number
|
||||||
|
@ -606,49 +668,19 @@ function _typeof(obj) { return obj && typeof Symbol !== "undefined" && obj.const
|
||||||
* @param {number} max Maximal value for the number (excluded)
|
* @param {number} max Maximal value for the number (excluded)
|
||||||
* @return {number} Random number
|
* @return {number} Random number
|
||||||
*/
|
*/
|
||||||
;
|
var getRandomNumber = exports.getRandomNumber = function getRandomNumber(min, max) {
|
||||||
|
|
||||||
Object.defineProperty(exports, "__esModule", {
|
|
||||||
value: true
|
|
||||||
});
|
|
||||||
var randomNumber = exports.randomNumber = function randomNumber(min, max) {
|
|
||||||
return Math.floor(Math.random() * (max - min)) + min;
|
return Math.floor(Math.random() * (max - min)) + min;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a random color
|
* Get a color at given index. For any given
|
||||||
|
* index, the same color will always be returned
|
||||||
*
|
*
|
||||||
|
* @param {number} index Color index
|
||||||
* @return {Array} RGB components
|
* @return {Array} RGB components
|
||||||
*/
|
*/
|
||||||
var randomColor = exports.randomColor = function randomColor() {
|
var getColor = exports.getColor = function getColor(index) {
|
||||||
var color = [];
|
return colors[index % colors.length];
|
||||||
|
|
||||||
for (var i = 0; i < 3; i++) {
|
|
||||||
color.push(Math.round(Math.random().toFixed(2) * 255));
|
|
||||||
}
|
|
||||||
|
|
||||||
return color;
|
|
||||||
};
|
};
|
||||||
|
}, {}] }, {}, [7]);
|
||||||
/**
|
|
||||||
* Convert a decimal number to its hexadecimal representation
|
|
||||||
*
|
|
||||||
* @param {number} input Number to be converted
|
|
||||||
* @return {string} Number representation
|
|
||||||
*/
|
|
||||||
var hex = function hex(input) {
|
|
||||||
var hex = parseInt(input, 10).toString(16);
|
|
||||||
return hex.length === 1 ? '0' + hex : hex;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a RGB color to its hexadecimal representation
|
|
||||||
*
|
|
||||||
* @param {Array} color RGB color
|
|
||||||
* @return {string} Hex representation
|
|
||||||
*/
|
|
||||||
var rgbToHex = exports.rgbToHex = function rgbToHex(color) {
|
|
||||||
return '#' + hex(color[0]) + hex(color[1]) + hex(color[2]);
|
|
||||||
};
|
|
||||||
}, {}] }, {}, [6]);
|
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Chaos game</title>
|
<title>Chaos game</title>
|
||||||
|
|
||||||
<link href="http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700,400italic|Playfair+Display:400,400italic,700" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700,400italic|Playfair+Display:400,400italic,700" rel="stylesheet">
|
||||||
<link href="styles/common.css" rel="stylesheet">
|
<link href="https://matteodelabre.me/styles/common.css" rel="stylesheet">
|
||||||
<link href="styles/subsite.css" rel="stylesheet">
|
<link href="https://matteodelabre.me/styles/demos.css" rel="stylesheet">
|
||||||
<link href="styles/index.css" rel="stylesheet">
|
<link href="styles/index.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body class="split">
|
<body class="split">
|
||||||
|
@ -22,7 +22,8 @@
|
||||||
<h1>The Chaos Game</h1>
|
<h1>The Chaos Game</h1>
|
||||||
<h3>Creating fractals with the chaos game</h3>
|
<h3>Creating fractals with the chaos game</h3>
|
||||||
|
|
||||||
|
Sommets <input id="vertices" type="range" min="3" max="12" step="1" value="3"><br>
|
||||||
|
Fraction 1/<input id="fraction" type="range" min="1" max="6" step="0.01" value="2">
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { getRandomNumber, getColor } from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the position of a regular polygon's vertices
|
||||||
|
* inside a 2 x 2 squared centered on the origin
|
||||||
|
*
|
||||||
|
* @param {number} count Vertices amount
|
||||||
|
* @return {Array<Array>} Array of points representing the vertices
|
||||||
|
*/
|
||||||
|
export const createRegularVertices = count => {
|
||||||
|
const step = 2 * Math.PI / count;
|
||||||
|
const initial = -Math.atan(Math.sin(step) / (Math.cos(step) - 1));
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
let current = step * i + initial;
|
||||||
|
|
||||||
|
result.push([Math.cos(current), Math.sin(current)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale the vertices so that they fit in given bounding rectangle
|
||||||
|
*
|
||||||
|
* @param {number} width Bounding rectangle width
|
||||||
|
* @param {number} height Bounding rectangle height
|
||||||
|
* @param {Array<Array>} vertices Vertices to scale
|
||||||
|
* @return {Array<Array>} Scaled vertices
|
||||||
|
*/
|
||||||
|
export const scaleVertices = (width, height, vertices) => {
|
||||||
|
const centerX = Math.floor(width / 2);
|
||||||
|
const centerY = Math.floor(height / 2);
|
||||||
|
const radius = Math.min(centerX, centerY);
|
||||||
|
|
||||||
|
return vertices.map(vertex => ([
|
||||||
|
vertex[0] * radius + centerX,
|
||||||
|
vertex[1] * radius + centerY
|
||||||
|
]));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the chaos game algorithm in a polygon
|
||||||
|
* of given vertices, with given fraction
|
||||||
|
*
|
||||||
|
* @param {ImageData} image Image to write on Data to amend
|
||||||
|
* @param {number} fraction Fraction to use
|
||||||
|
* @param {Array} vertices List of vertices of the bounding polygon
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
export const applyChaos = (image, fraction, vertices) => {
|
||||||
|
const count = vertices.length, imageWidth = image.width;
|
||||||
|
|
||||||
|
// 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 = Math.floor(500 * imageWidth * fraction);
|
||||||
|
let drop = Math.floor(iterations / 200);
|
||||||
|
|
||||||
|
while (iterations--) {
|
||||||
|
const vertexNumber = getRandomNumber(0, count);
|
||||||
|
const vertex = vertices[vertexNumber], color = getColor(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] * imageWidth + point[0]) * 4;
|
||||||
|
|
||||||
|
image.data[i] = color[0];
|
||||||
|
image.data[i + 1] = color[1];
|
||||||
|
image.data[i + 2] = color[2];
|
||||||
|
image.data[i + 3] = 255;
|
||||||
|
} else {
|
||||||
|
drop--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
172
scripts/index.js
172
scripts/index.js
|
@ -1,136 +1,96 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { html } from 'the-dom';
|
import { html } from 'the-dom';
|
||||||
import { randomNumber, randomColor } from './utils';
|
import { getColor } from './utils';
|
||||||
|
import { createRegularVertices, scaleVertices, applyChaos } from './chaos';
|
||||||
|
|
||||||
|
const { body } = html(document);
|
||||||
|
|
||||||
const { body, create } = html(document);
|
|
||||||
const content = body.find('#content');
|
const content = body.find('#content');
|
||||||
const plotting = body.find('#plotting');
|
const verticesRange = body.find('#vertices');
|
||||||
const ctx = plotting.node.getContext('2d');
|
const fractionRange = body.find('#fraction');
|
||||||
|
|
||||||
|
const plotting = body.find('#plotting').node;
|
||||||
|
const ctx = plotting.getContext('2d');
|
||||||
|
|
||||||
const padding = 40; // padding between the canvas edges and the points
|
const padding = 40; // padding between the canvas edges and the points
|
||||||
let image, width, height;
|
let width, height, vertices;
|
||||||
let lastUpdate = -Infinity;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a fractal of given width, height, based on
|
* Re-render the scene from scratch
|
||||||
* 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}
|
* @return {null}
|
||||||
*/
|
*/
|
||||||
const render = () => {
|
const render = () => {
|
||||||
// only recalculate every 16.67 ms
|
const fraction = 1 / parseFloat(fractionRange.node.value);
|
||||||
if (+new Date() - lastUpdate > 16.67) {
|
const scaledVerts = scaleVertices(width, height, vertices);
|
||||||
image = chaos(
|
|
||||||
width - 2 * padding, height - 2 * padding, 1/2,
|
|
||||||
[[255, 0, 0], [0, 255, 0], [0, 0, 255]]
|
|
||||||
);
|
|
||||||
|
|
||||||
lastUpdate = +new Date();
|
plotting.width = width + 2 * padding;
|
||||||
|
plotting.height = height + 2 * padding;
|
||||||
|
|
||||||
|
// do not plot (very) small sizes
|
||||||
|
if (width < 1) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
// draw the polygon
|
||||||
ctx.putImageData(
|
ctx.strokeStyle = '#aaa';
|
||||||
image, padding, padding
|
ctx.lineWidth = 1;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
for (let i = 0; i < vertices.length; i += 1) {
|
||||||
|
ctx.lineTo(scaledVerts[i][0] + padding, scaledVerts[i][1] + padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// draw the vertices
|
||||||
|
for (let i = 0; i < vertices.length; i += 1) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.fillStyle = 'rgb(' + getColor(i).join(', ') + ')';
|
||||||
|
|
||||||
|
ctx.arc(
|
||||||
|
scaledVerts[i][0] + padding, scaledVerts[i][1] + padding,
|
||||||
|
4, 0, Math.PI * 2
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the chaos game
|
||||||
|
const image = ctx.getImageData(padding, padding, width, height);
|
||||||
|
|
||||||
|
applyChaos(image, fraction, scaledVerts);
|
||||||
|
ctx.putImageData(image, padding, padding);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resize the canvas to fit the new
|
* Update the scene when the window has been resized
|
||||||
* window size and redraw the scene
|
|
||||||
*
|
*
|
||||||
* @return {null}
|
* @return {null}
|
||||||
*/
|
*/
|
||||||
const resize = () => {
|
const resize = () => {
|
||||||
width = content.node.clientWidth;
|
width = content.node.clientWidth - 2 * padding;
|
||||||
height = content.node.clientHeight;
|
height = content.node.clientHeight - 2 * padding;
|
||||||
|
|
||||||
plotting.setAttr('width', width);
|
|
||||||
plotting.setAttr('height', height);
|
|
||||||
|
|
||||||
render();
|
render();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new vertices
|
||||||
|
*/
|
||||||
|
verticesRange.on('input', () => {
|
||||||
|
vertices = createRegularVertices(
|
||||||
|
parseInt(verticesRange.node.value, 10)
|
||||||
|
);
|
||||||
|
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
window.onresize = resize;
|
window.onresize = resize;
|
||||||
|
fractionRange.on('input', render);
|
||||||
|
|
||||||
|
vertices = createRegularVertices(3);
|
||||||
resize();
|
resize();
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
'#F44336',
|
||||||
|
'#2196F3',
|
||||||
|
'#4CAF50',
|
||||||
|
'#F9A825',
|
||||||
|
'#E91E63',
|
||||||
|
'#00838F'
|
||||||
|
].map(color => color.match(/[A-F0-9]{2}/g).map(
|
||||||
|
component => parseInt(component, 16)
|
||||||
|
));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a random whole number
|
* Get a random whole number
|
||||||
*
|
*
|
||||||
|
@ -7,40 +18,14 @@
|
||||||
* @param {number} max Maximal value for the number (excluded)
|
* @param {number} max Maximal value for the number (excluded)
|
||||||
* @return {number} Random number
|
* @return {number} Random number
|
||||||
*/
|
*/
|
||||||
export const randomNumber = (min, max) =>
|
export const getRandomNumber = (min, max) =>
|
||||||
Math.floor(Math.random() * (max - min)) + min;
|
Math.floor(Math.random() * (max - min)) + min;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a random color
|
* Get a color at given index. For any given
|
||||||
|
* index, the same color will always be returned
|
||||||
*
|
*
|
||||||
|
* @param {number} index Color index
|
||||||
* @return {Array} RGB components
|
* @return {Array} RGB components
|
||||||
*/
|
*/
|
||||||
export const randomColor = () => {
|
export const getColor = index => colors[index % colors.length];
|
||||||
const color = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
color.push(Math.round((Math.random().toFixed(2)) * 255));
|
|
||||||
}
|
|
||||||
|
|
||||||
return color;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a decimal number to its hexadecimal representation
|
|
||||||
*
|
|
||||||
* @param {number} input Number to be converted
|
|
||||||
* @return {string} Number representation
|
|
||||||
*/
|
|
||||||
const hex = input => {
|
|
||||||
let hex = parseInt(input, 10).toString(16);
|
|
||||||
return hex.length === 1 ? '0' + hex : hex;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a RGB color to its hexadecimal representation
|
|
||||||
*
|
|
||||||
* @param {Array} color RGB color
|
|
||||||
* @return {string} Hex representation
|
|
||||||
*/
|
|
||||||
export const rgbToHex =
|
|
||||||
color => '#' + hex(color[0]) + hex(color[1]) + hex(color[2]);
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
/**
|
|
||||||
* Common styles
|
|
||||||
*/
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
font: 16px 'Source Sans Pro', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.6em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2em;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
*:before, *:after, * {
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
/**
|
|
||||||
* Subsite styles
|
|
||||||
* (hosted demos, ...)
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Split view
|
|
||||||
* (sidebar + content)
|
|
||||||
*/
|
|
||||||
|
|
||||||
.split {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split aside {
|
|
||||||
width: 350px;
|
|
||||||
flex-shrink: 4;
|
|
||||||
padding: 8px 16px;
|
|
||||||
color: white;
|
|
||||||
background: #F44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split aside header {
|
|
||||||
margin: -8px -16px 0 -16px;
|
|
||||||
background: #D32F2F;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split aside header a {
|
|
||||||
padding: 8px 16px;
|
|
||||||
display: block;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split aside header:hover {
|
|
||||||
background: #C62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split aside header img {
|
|
||||||
width: 2.5em;
|
|
||||||
float: left;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
|
|
||||||
border-radius: 100%;
|
|
||||||
border: 2px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split aside header span {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split aside header:after {
|
|
||||||
content: '';
|
|
||||||
display: table;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split aside h1 + h3 {
|
|
||||||
margin-top: -1.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split #content {
|
|
||||||
width: calc(100% - 350px);
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
Loading…
Reference in New Issue