💡 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