💡 REACT ALL THE THINGS
This commit is contained in:
parent
b47f30951d
commit
9fb99b63ff
|
@ -29,9 +29,14 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"ecmaFeatures": {
|
"ecmaFeatures": {
|
||||||
"modules": true
|
"modules": true,
|
||||||
|
"jsx": true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"plugins": [
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
|
||||||
"env": {
|
"env": {
|
||||||
"es6": true,
|
"es6": true,
|
||||||
"browser": true
|
"browser": true
|
||||||
|
|
16
package.json
16
package.json
|
@ -4,10 +4,10 @@
|
||||||
"description": "Plotting fractals with the chaos game",
|
"description": "Plotting fractals with the chaos game",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/MattouFP/chaos.git"
|
"url": "git+https://github.com/matteodelabre/chaos.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "browserify -t [ babelify --presets [ es2015 ] ] scripts/index.js | babel --presets es2015 > bundle.js"
|
"build": "browserify -t [ babelify --presets [ es2015 react ] ] scripts/index.js | babel --presets es2015 > bundle.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"chaos",
|
"chaos",
|
||||||
|
@ -17,15 +17,17 @@
|
||||||
"author": "Mattéo Delabre",
|
"author": "Mattéo Delabre",
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/MattouFP/chaos/issues"
|
"url": "https://github.com/matteodelabre/chaos/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/MattouFP/chaos#readme",
|
"homepage": "https://github.com/matteodelabre/chaos#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-preset-es2015": "^6.3.13",
|
|
||||||
"babelify": "^7.2.0",
|
|
||||||
"babel-cli": "^6.3.17",
|
"babel-cli": "^6.3.17",
|
||||||
|
"babel-preset-es2015": "^6.3.13",
|
||||||
|
"babel-preset-react": "^6.3.13",
|
||||||
|
"babelify": "^7.2.0",
|
||||||
"browserify": "^12.0.1",
|
"browserify": "^12.0.1",
|
||||||
|
"react": "^0.14.3",
|
||||||
|
"react-dom": "^0.14.3",
|
||||||
"the-dom": "^0.1.0"
|
"the-dom": "^0.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,18 +20,17 @@ const chooseIndex = weights => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starting from the last point in `points`, add
|
* Starting from `point`, generate `iterations` points
|
||||||
* `iterations` number of point generated by applying
|
* by applying randomly-chosen transformations
|
||||||
* transformations chosen at random among `transforms`
|
|
||||||
*
|
*
|
||||||
* @param {Array} points Initial set of points
|
* @param {Array} point Starting point
|
||||||
* @param {number} iterations Number of points to plot
|
* @param {number} iterations Number of points to plot
|
||||||
* @param {Array} transforms List of available transforms
|
* @param {Array} transforms List of available transforms
|
||||||
* @param {Array} weights Probability weights for each transform
|
* @param {Array} weights Probability weights for each transform
|
||||||
* @return {null}
|
* @return {Array} Generated points
|
||||||
*/
|
*/
|
||||||
export const applyChaos = (points, iterations, transforms, weights) => {
|
export const applyChaos = (point, iterations, transforms, weights) => {
|
||||||
let point = points[points.length - 1];
|
const points = [];
|
||||||
|
|
||||||
if (weights === undefined) {
|
if (weights === undefined) {
|
||||||
weights = Array.apply(null, Array(transforms.length)).map(
|
weights = Array.apply(null, Array(transforms.length)).map(
|
||||||
|
@ -44,4 +43,6 @@ export const applyChaos = (points, iterations, transforms, weights) => {
|
||||||
point = transforms[index](point);
|
point = transforms[index](point);
|
||||||
points.push(point);
|
points.push(point);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,234 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { applyChaos } from './chaos';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a canvas element that draws a fractal out of a
|
||||||
|
* given iterated function system
|
||||||
|
*/
|
||||||
|
export class Fractal extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
zoom: 200,
|
||||||
|
dragging: false,
|
||||||
|
center: null,
|
||||||
|
points: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust zooming level and center so that wheeling the mouse
|
||||||
|
* on given point zooms around it
|
||||||
|
*
|
||||||
|
* @param {WheelEvent} event Mouse event
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
wheel(event) {
|
||||||
|
const zoom = this.state.zoom;
|
||||||
|
const center = this.state.center;
|
||||||
|
const height = this.ctx.canvas.height;
|
||||||
|
|
||||||
|
const delta = event.deltaMode === 0 ? event.deltaY / 53 : event.deltaY;
|
||||||
|
const newZoom = zoom * Math.max(0, 1 - delta * .035);
|
||||||
|
|
||||||
|
// which (unprojected) point does the mouse point on?
|
||||||
|
const mouse = [
|
||||||
|
(event.nativeEvent.offsetX - center[0]) / zoom,
|
||||||
|
(height - event.nativeEvent.offsetY - center[1]) / zoom
|
||||||
|
];
|
||||||
|
|
||||||
|
// we need to set the center so that `mouse` stays at
|
||||||
|
// the same position on screen, i.e (vectorially):
|
||||||
|
// mouse * newZoom + newCenter = mouse * zoom + center
|
||||||
|
// => newCenter = mouse * zoom - mouse * newZoom + center
|
||||||
|
this.setState({
|
||||||
|
zoom: newZoom,
|
||||||
|
center: [
|
||||||
|
mouse[0] * zoom - mouse[0] * newZoom + center[0],
|
||||||
|
mouse[1] * zoom - mouse[1] * newZoom + center[1]
|
||||||
|
]
|
||||||
|
}, this.draw);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the mouse coordinates when we click down,
|
||||||
|
* for use by `mouseMove()`
|
||||||
|
*
|
||||||
|
* @param {MouseEvent} event Mouse event
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
mouseDown(event) {
|
||||||
|
this.setState({
|
||||||
|
dragging: [
|
||||||
|
event.nativeEvent.offsetX,
|
||||||
|
event.nativeEvent.offsetY
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the fact that the mouse was released
|
||||||
|
*
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
mouseUp() {
|
||||||
|
this.setState({
|
||||||
|
dragging: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When moving the mouse while clicking, pan the figure
|
||||||
|
*
|
||||||
|
* @param {MouseEvent} event Mouse event
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
mouseMove(event) {
|
||||||
|
const dragging = this.state.dragging;
|
||||||
|
const center = this.state.center;
|
||||||
|
|
||||||
|
if (dragging !== false) {
|
||||||
|
const newMouse = [
|
||||||
|
event.nativeEvent.offsetX,
|
||||||
|
event.nativeEvent.offsetY
|
||||||
|
];
|
||||||
|
|
||||||
|
const movement = [
|
||||||
|
newMouse[0] - dragging[0],
|
||||||
|
newMouse[1] - dragging[1]
|
||||||
|
];
|
||||||
|
|
||||||
|
// move the center by given offset and redraw
|
||||||
|
this.setState({
|
||||||
|
dragging: newMouse,
|
||||||
|
center: [
|
||||||
|
center[0] + movement[0],
|
||||||
|
center[1] - movement[1]
|
||||||
|
]
|
||||||
|
}, this.draw);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redraw `points` on the canvas, scaled with given
|
||||||
|
* `zoom` and based on current `center`
|
||||||
|
*
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
draw() {
|
||||||
|
const width = this.ctx.canvas.width;
|
||||||
|
const height = this.ctx.canvas.height;
|
||||||
|
const zoom = this.state.zoom;
|
||||||
|
const center = this.state.center;
|
||||||
|
const points = this.state.points;
|
||||||
|
|
||||||
|
// do not plot (very) small sizes
|
||||||
|
if (width < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// fill each point in `points` skipping the first 50 ones
|
||||||
|
const image = this.ctx.getImageData(0, 0, width, height);
|
||||||
|
const length = points.length;
|
||||||
|
const color = [0, 0, 0];
|
||||||
|
|
||||||
|
for (let i = 50; i < length; i += 1) {
|
||||||
|
const x = Math.floor(points[i][0] * zoom + center[0]);
|
||||||
|
const y = height - Math.floor(points[i][1] * zoom + center[1]);
|
||||||
|
|
||||||
|
if (x >= 0 && x < width && y >= 0 && y < height) {
|
||||||
|
const index = (y * width + x) * 4;
|
||||||
|
|
||||||
|
image.data[index] = color[0];
|
||||||
|
image.data[index + 1] = color[1];
|
||||||
|
image.data[index + 2] = color[2];
|
||||||
|
image.data[index + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx.putImageData(image, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is dirty. TODO: make it less coupled
|
||||||
|
getSize() {
|
||||||
|
const wrapping = document.querySelector('#content');
|
||||||
|
return [wrapping.clientWidth, wrapping.clientHeight];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the canvas size to its parent size
|
||||||
|
* and redraw
|
||||||
|
*
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
resize() {
|
||||||
|
[this.ctx.canvas.width, this.ctx.canvas.height] = this.getSize();
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate points with current system
|
||||||
|
*
|
||||||
|
* @return {Array} Points to be drawn
|
||||||
|
*/
|
||||||
|
calculate() {
|
||||||
|
return applyChaos(
|
||||||
|
[0, 0],
|
||||||
|
this.props.iterations,
|
||||||
|
...this.props.system
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup resize listener and make initial drawing
|
||||||
|
* when the component has been mounted
|
||||||
|
*
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener('resize', this.resize.bind(this));
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
center: this.getSize().map(x => Math.floor(x / 2)),
|
||||||
|
points: this.calculate()
|
||||||
|
}, this.resize.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the resize listener before unmounting the component
|
||||||
|
*
|
||||||
|
* @return {null}
|
||||||
|
*/
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('resize', this.resize.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Never create a new canvas
|
||||||
|
*/
|
||||||
|
shouldComponentUpdate() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a canvas with correct listeners
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
return <canvas
|
||||||
|
ref={canvas => this.ctx = canvas.getContext('2d')}
|
||||||
|
onWheel={this.wheel.bind(this)}
|
||||||
|
onMouseDown={this.mouseDown.bind(this)}
|
||||||
|
onMouseUp={this.mouseUp.bind(this)}
|
||||||
|
onMouseMove={this.mouseMove.bind(this)}
|
||||||
|
></canvas>;
|
||||||
|
}
|
||||||
|
}
|
121
scripts/index.js
121
scripts/index.js
|
@ -1,118 +1,11 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { html } from 'the-dom';
|
import { Fractal } from './fractal';
|
||||||
import { applyChaos } from './chaos';
|
import * as React from 'react'; // eslint-disable-line no-unused-vars
|
||||||
|
import { render } from 'react-dom';
|
||||||
import { barnsley } from './ifs';
|
import { barnsley } from './ifs';
|
||||||
|
|
||||||
const { body } = html(document);
|
render(
|
||||||
|
<Fractal system={barnsley} />,
|
||||||
const content = body.find('#content');
|
document.querySelector('#content')
|
||||||
const plotting = body.find('#plotting').node;
|
);
|
||||||
const ctx = plotting.getContext('2d');
|
|
||||||
|
|
||||||
let dragging = false;
|
|
||||||
let center, zoom = 200;
|
|
||||||
let width, height;
|
|
||||||
|
|
||||||
let points = [[0, 0]];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-render the scene from scratch
|
|
||||||
*
|
|
||||||
* @return {null}
|
|
||||||
*/
|
|
||||||
const render = () => {
|
|
||||||
plotting.width = width;
|
|
||||||
plotting.height = height;
|
|
||||||
|
|
||||||
// do not plot (very) small sizes
|
|
||||||
if (width < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// do the chaos game
|
|
||||||
const image = ctx.getImageData(0, 0, width, height);
|
|
||||||
const length = points.length;
|
|
||||||
const color = [0, 0, 0];
|
|
||||||
|
|
||||||
for (let i = 50; i < length; i += 1) {
|
|
||||||
const x = Math.floor(points[i][0] * zoom + center[0]);
|
|
||||||
const y = height - Math.floor(points[i][1] * zoom + center[1]);
|
|
||||||
|
|
||||||
if (x >= 0 && x < width && y >= 0 && y < height) {
|
|
||||||
const index = (y * width + x) * 4;
|
|
||||||
|
|
||||||
image.data[index] = color[0];
|
|
||||||
image.data[index + 1] = color[1];
|
|
||||||
image.data[index + 2] = color[2];
|
|
||||||
image.data[index + 3] = 255;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.putImageData(image, 0, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the scene when the window has been resized
|
|
||||||
*
|
|
||||||
* @return {null}
|
|
||||||
*/
|
|
||||||
const resize = () => {
|
|
||||||
width = content.node.clientWidth;
|
|
||||||
height = content.node.clientHeight;
|
|
||||||
center = [Math.floor(width / 2), Math.floor(height / 2)];
|
|
||||||
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zoom on the cursor position when using mouse wheel
|
|
||||||
*/
|
|
||||||
content.on('wheel', event => {
|
|
||||||
const delta = event.deltaMode === 0 ? event.deltaY / 53 : event.deltaY;
|
|
||||||
const newZoom = zoom * Math.max(0, 1 - delta * .035);
|
|
||||||
|
|
||||||
// which (unprojected) point does the mouse point on?
|
|
||||||
const mouse = [
|
|
||||||
(event.offsetX - center[0]) / zoom,
|
|
||||||
(height - event.offsetY - center[1]) / zoom
|
|
||||||
];
|
|
||||||
|
|
||||||
// we need to set the center so that `mouse` stays at
|
|
||||||
// the same position on screen, i.e (vectorially):
|
|
||||||
// mouse * newZoom + newCenter = mouse * zoom + center
|
|
||||||
// => newCenter = mouse * zoom - mouse * newZoom + center
|
|
||||||
center = [
|
|
||||||
mouse[0] * zoom - mouse[0] * newZoom + center[0],
|
|
||||||
mouse[1] * zoom - mouse[1] * newZoom + center[1]
|
|
||||||
];
|
|
||||||
|
|
||||||
zoom = newZoom;
|
|
||||||
render();
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pan the content with click-drag action
|
|
||||||
*/
|
|
||||||
content.on('mousedown', event => dragging = [event.offsetX, event.offsetY]);
|
|
||||||
content.on('mouseup', () => dragging = false);
|
|
||||||
content.on('mousemove', event => {
|
|
||||||
if (dragging !== false) {
|
|
||||||
const newMouse = [event.offsetX, event.offsetY];
|
|
||||||
const movement = [newMouse[0] - dragging[0], newMouse[1] - dragging[1]];
|
|
||||||
|
|
||||||
center[0] += movement[0];
|
|
||||||
center[1] -= movement[1];
|
|
||||||
|
|
||||||
render();
|
|
||||||
dragging = newMouse;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
applyChaos(points, 200000, ...barnsley);
|
|
||||||
window.onresize = resize;
|
|
||||||
resize();
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
'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
|
|
||||||
*
|
|
||||||
* @param {number} min Minimal value for the number
|
|
||||||
* @param {number} max Maximal value for the number (excluded)
|
|
||||||
* @return {number} Random number
|
|
||||||
*/
|
|
||||||
export const getRandomNumber = (min, max) =>
|
|
||||||
Math.floor(Math.random() * (max - min)) + min;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 getColor = index => colors[index % colors.length];
|
|
Loading…
Reference in New Issue