💡 REACT ALL THE THINGS

This commit is contained in:
Mattéo Delabre 2015-12-23 22:31:51 +01:00
parent b47f30951d
commit 9fb99b63ff
7 changed files with 6145 additions and 833 deletions

View File

@ -29,9 +29,14 @@
},
"ecmaFeatures": {
"modules": true
"modules": true,
"jsx": true
},
"plugins": [
"react"
],
"env": {
"es6": true,
"browser": true

6554
bundle.js

File diff suppressed because one or more lines are too long

View File

@ -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"
}
}

View File

@ -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;
};

234
scripts/fractal.js Normal file
View File

@ -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>;
}
}

View File

@ -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')
);

View File

@ -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];