diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..56af0ac --- /dev/null +++ b/.eslintrc @@ -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 + } +} diff --git a/bundle.js b/bundle.js index 59767d7..a28003b 100644 --- a/bundle.js +++ b/bundle.js @@ -465,139 +465,201 @@ function _typeof(obj) { return obj && typeof Symbol !== "undefined" && obj.const }, {}], 6: [function (require, module, exports) { '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 _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 - * a polygon of given amount of vertices, using the - * chaos game applied with given fraction + * Calculate the position of a regular polygon's vertices + * inside a 2 x 2 squared centered on the origin * - * @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 + * @param {number} count Vertices amount + * @return {Array} Array of points representing the vertices */ - var chaos = function chaos(width, height, fraction, colors) { - var cx = Math.floor(width / 2); - var cy = Math.floor(height / 2); - var radius = Math.min(cx, cy); - - 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; - } + var createRegularVertices = exports.createRegularVertices = function createRegularVertices(count) { + var step = 2 * Math.PI / count; + var initial = -Math.atan(Math.sin(step) / (Math.cos(step) - 1)); + var result = []; 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} vertices Vertices to scale + * @return {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: // for any point, the next point is a `fraction` of the // distance between it and a random vertex var point = vertices[0]; - var iterations = 200000; - var drop = 1000; + var iterations = Math.floor(500 * imageWidth * fraction); + var drop = Math.floor(iterations / 200); while (iterations--) { - var vertexNumber = (0, _utils.randomNumber)(0, count); + var vertexNumber = (0, _utils.getRandomNumber)(0, count); 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])]; // skip the first 1000 points if (drop === 0) { - var i = (point[1] * width + point[0]) * 4; + var i = (point[1] * imageWidth + point[0]) * 4; - data[i] = color[0]; - data[i + 1] = color[1]; - data[i + 2] = color[2]; - data[i + 3] = 255; + image.data[i] = color[0]; + image.data[i + 1] = color[1]; + image.data[i + 2] = color[2]; + image.data[i + 3] = 255; } else { 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 - * positions if they need to + * Re-render the scene from scratch * * @return {null} */ var render = function 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]]); + var fraction = 1 / parseFloat(fractionRange.node.value); + var scaledVerts = (0, _chaos.scaleVertices)(width, height, vertices); - 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); }; /** - * Resize the canvas to fit the new - * window size and redraw the scene + * Update the scene when the window has been resized * * @return {null} */ var resize = function resize() { - width = content.node.clientWidth; - height = content.node.clientHeight; - - plotting.setAttr('width', width); - plotting.setAttr('height', height); + width = content.node.clientWidth - 2 * padding; + height = content.node.clientHeight - 2 * padding; render(); }; + /** + * Create new vertices + */ + verticesRange.on('input', function () { + vertices = (0, _chaos.createRegularVertices)(parseInt(verticesRange.node.value, 10)); + + render(); + }); + window.onresize = resize; + fractionRange.on('input', render); + + vertices = (0, _chaos.createRegularVertices)(3); resize(); - }, { "./utils": 7, "the-dom": 1 }], 7: [function (require, module, exports) { - 'use strict' + }, { "./chaos": 6, "./utils": 8, "the-dom": 1 }], 8: [function (require, module, exports) { + '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 @@ -606,49 +668,19 @@ function _typeof(obj) { return obj && typeof Symbol !== "undefined" && obj.const * @param {number} max Maximal value for the number (excluded) * @return {number} Random number */ - ; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - var randomNumber = exports.randomNumber = function randomNumber(min, max) { + var getRandomNumber = exports.getRandomNumber = function getRandomNumber(min, max) { 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 */ - var randomColor = exports.randomColor = function randomColor() { - var color = []; - - for (var i = 0; i < 3; i++) { - color.push(Math.round(Math.random().toFixed(2) * 255)); - } - - return color; + var getColor = exports.getColor = function getColor(index) { + return colors[index % colors.length]; }; - - /** - * 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]); + }, {}] }, {}, [7]); diff --git a/index.html b/index.html index 24c5a95..2150f64 100644 --- a/index.html +++ b/index.html @@ -4,9 +4,9 @@ Chaos game - - - + + + @@ -22,7 +22,8 @@

The Chaos Game

Creating fractals with the chaos game

- + Sommets
+ Fraction 1/
diff --git a/scripts/chaos.js b/scripts/chaos.js new file mode 100644 index 0000000..f5f6b81 --- /dev/null +++ b/scripts/chaos.js @@ -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 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} vertices Vertices to scale + * @return {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--; + } + } +}; diff --git a/scripts/index.js b/scripts/index.js index dc4e190..c3069d9 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,136 +1,96 @@ 'use strict'; 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 plotting = body.find('#plotting'); -const ctx = plotting.node.getContext('2d'); +const verticesRange = body.find('#vertices'); +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 -let image, width, height; -let lastUpdate = -Infinity; +let width, height, vertices; /** - * 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 + * Re-render the scene from scratch * * @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]] - ); + const fraction = 1 / parseFloat(fractionRange.node.value); + const scaledVerts = scaleVertices(width, height, vertices); - 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); - ctx.putImageData( - image, padding, padding - ); + // draw the polygon + ctx.strokeStyle = '#aaa'; + 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 - * window size and redraw the scene + * Update the scene when the window has been resized * * @return {null} */ const resize = () => { - width = content.node.clientWidth; - height = content.node.clientHeight; - - plotting.setAttr('width', width); - plotting.setAttr('height', height); + width = content.node.clientWidth - 2 * padding; + height = content.node.clientHeight - 2 * padding; render(); }; +/** + * Create new vertices + */ +verticesRange.on('input', () => { + vertices = createRegularVertices( + parseInt(verticesRange.node.value, 10) + ); + + render(); +}); + window.onresize = resize; +fractionRange.on('input', render); + +vertices = createRegularVertices(3); resize(); diff --git a/scripts/utils.js b/scripts/utils.js index 59aa572..476e6a9 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,5 +1,16 @@ '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 * @@ -7,40 +18,14 @@ * @param {number} max Maximal value for the number (excluded) * @return {number} Random number */ -export const randomNumber = (min, max) => +export const getRandomNumber = (min, max) => 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 */ -export const randomColor = () => { - 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]); +export const getColor = index => colors[index % colors.length]; diff --git a/styles/common.css b/styles/common.css deleted file mode 100644 index 1c744d2..0000000 --- a/styles/common.css +++ /dev/null @@ -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; -} diff --git a/styles/subsite.css b/styles/subsite.css deleted file mode 100644 index 1c66622..0000000 --- a/styles/subsite.css +++ /dev/null @@ -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; -}