247 lines
6.3 KiB
JavaScript
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 };
|