💡 REACT ALL THE THINGS
This commit is contained in:
parent
b47f30951d
commit
9fb99b63ff
|
@ -29,9 +29,14 @@
|
|||
},
|
||||
|
||||
"ecmaFeatures": {
|
||||
"modules": true
|
||||
"modules": true,
|
||||
"jsx": true
|
||||
},
|
||||
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true
|
||||
|
|
16
package.json
16
package.json
|
@ -4,10 +4,10 @@
|
|||
"description": "Plotting fractals with the chaos game",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/MattouFP/chaos.git"
|
||||
"url": "git+https://github.com/matteodelabre/chaos.git"
|
||||
},
|
||||
"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": [
|
||||
"chaos",
|
||||
|
@ -17,15 +17,17 @@
|
|||
"author": "Mattéo Delabre",
|
||||
"license": "CC0-1.0",
|
||||
"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": {
|
||||
"babel-preset-es2015": "^6.3.13",
|
||||
"babelify": "^7.2.0",
|
||||
"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",
|
||||
|
||||
"react": "^0.14.3",
|
||||
"react-dom": "^0.14.3",
|
||||
"the-dom": "^0.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,18 +20,17 @@ const chooseIndex = weights => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Starting from the last point in `points`, add
|
||||
* `iterations` number of point generated by applying
|
||||
* transformations chosen at random among `transforms`
|
||||
* Starting from `point`, generate `iterations` points
|
||||
* by applying randomly-chosen transformations
|
||||
*
|
||||
* @param {Array} points Initial set of points
|
||||
* @param {Array} point Starting point
|
||||
* @param {number} iterations Number of points to plot
|
||||
* @param {Array} transforms List of available transforms
|
||||
* @param {Array} weights Probability weights for each transform
|
||||
* @return {null}
|
||||
* @return {Array} Generated points
|
||||
*/
|
||||
export const applyChaos = (points, iterations, transforms, weights) => {
|
||||
let point = points[points.length - 1];
|
||||
export const applyChaos = (point, iterations, transforms, weights) => {
|
||||
const points = [];
|
||||
|
||||
if (weights === undefined) {
|
||||
weights = Array.apply(null, Array(transforms.length)).map(
|
||||
|
@ -44,4 +43,6 @@ export const applyChaos = (points, iterations, transforms, weights) => {
|
|||
point = transforms[index](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';
|
||||
|
||||
import { html } from 'the-dom';
|
||||
import { applyChaos } from './chaos';
|
||||
import { Fractal } from './fractal';
|
||||
import * as React from 'react'; // eslint-disable-line no-unused-vars
|
||||
import { render } from 'react-dom';
|
||||
import { barnsley } from './ifs';
|
||||
|
||||
const { body } = html(document);
|
||||
|
||||
const content = body.find('#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();
|
||||
render(
|
||||
<Fractal system={barnsley} />,
|
||||
document.querySelector('#content')
|
||||
);
|
||||
|
|
|
@ -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