chaos/scripts/fractal.js

247 lines
6.3 KiB
JavaScript

'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
*/
class Fractal extends React.Component {
constructor(props) {
super(props);
this.state = {
zoom: 30,
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);
}
/**
* Get the container size
*
* @return {Array} Width and height of the container
*/
getSize() {
return [this.container.clientWidth, this.container.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 <div id="content" ref={div => this.container = div}>
<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>
</div>;
}
}
Fractal.defaultProps = {
iterations: 200000,
system: [[], []]
};
export { Fractal };