'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