├── README.md ├── LICENSE └── index.js /README.md: -------------------------------------------------------------------------------- 1 | # flow-fields 2 | 3 | A library for generating flow fields. 4 | 5 | Check out the visualizer: 6 | - [flow-fields.romellogoodman.com](https://flow-fields.romellogoodman.com/) 7 | - [Flow Fields Visualizer Github Repo](https://github.com/romellogoodman/flow-fields-visualizer) 8 | 9 | Resources for learning more about flow fields and noise: 10 | 11 | - [Flow Field Are.na Board](https://www.are.na/romello-goodman/flow-fields-ddrqag_mr44) 12 | - [Flow Fields by Tyler Hobbs](https://tylerxhobbs.com/essays/2020/flow-fields) 13 | - [Noise by Varun Vachhar](https://varun.ca/noise/) 14 | 15 | ## Use 16 | 17 | ```js 18 | const field = generateField({ count: 100, height: 1000, width: 1000 }); 19 | ``` 20 | 21 | ## Contributing 22 | 23 | All contributors and all contributions both big and small are welcome in this project. 24 | 25 | ## Author 26 | 27 | [Romello Goodman](https://www.romellogoodman.com/) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Romello Goodman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { MersenneTwister19937, Random } from "random-js"; 2 | import { createNoise2D } from "simplex-noise"; 3 | 4 | const getRandom = (randomSeed) => { 5 | const engine = randomSeed 6 | ? MersenneTwister19937.seed(randomSeed) 7 | : MersenneTwister19937.autoSeed(); 8 | 9 | return new Random(engine); 10 | }; 11 | 12 | const noise2D = createNoise2D(); 13 | 14 | const getNoiseValue = (x, y, frequency, amplitude) => { 15 | return noise2D(x * frequency, y * frequency) * amplitude; 16 | }; 17 | 18 | const getBounds = (width, height, margin) => { 19 | const marginWidth = width * margin; 20 | const marginHeight = height * margin; 21 | 22 | return { 23 | minWidth: marginWidth, 24 | maxWidth: width - marginWidth, 25 | minHeight: marginHeight, 26 | maxHeight: height - marginHeight, 27 | }; 28 | }; 29 | 30 | const isInBound = (xCood, yCoord, width, height, margin) => { 31 | const { minWidth, maxWidth, minHeight, maxHeight } = getBounds( 32 | width, 33 | height, 34 | margin 35 | ); 36 | 37 | return ( 38 | xCood > minWidth && 39 | xCood < maxWidth && 40 | yCoord > minHeight && 41 | yCoord < maxHeight 42 | ); 43 | }; 44 | 45 | /** 46 | * Creates the particles for a flow field. 47 | * 48 | * @param {Object} config Configuration object 49 | * @param {Number} config.count Number of particles in the field 50 | * @param {Number} config.height Height of space 51 | * @param {Number} config.margin Percent of height/width to create a padding 52 | * @param {String} config.seed Random (random-js) seed 53 | * @param {Number} config.width Width of space 54 | * @return {Array} List of particle objects containing the starting x and y coordinates 55 | */ 56 | export const generateParticles = ({ 57 | count, 58 | height, 59 | margin = 0.1, 60 | seed, 61 | width, 62 | }) => { 63 | const random = getRandom(seed); 64 | const bounds = getBounds(width, height, margin); 65 | const { minWidth, maxWidth, minHeight, maxHeight } = bounds; 66 | let particles = []; 67 | 68 | // Generate some particles with a random position 69 | for (let i = 0; i < count; i++) { 70 | particles.push({ 71 | x: random.real(minWidth, maxWidth), 72 | y: random.real(minHeight, maxHeight), 73 | vx: 0, 74 | vy: 0, 75 | line: [], 76 | }); 77 | } 78 | 79 | return particles; 80 | }; 81 | 82 | /** 83 | * Computes the new position for a particle and adds it to the particle.line array. 84 | * 85 | * @param {Object} config Configuration object 86 | * @param {Number} config.amplitude Controls the range of values we get from the noise function 87 | * @param {Number} config.damping Slows down the particle (think friction) 88 | * @param {Number} config.frequency Controls how quickly/slowly the noise function is "evolving over time" 89 | * @param {Number} config.lengthOfStep Amount to move the coordinate 90 | * @param {Object} config.particle Particle object containing the current position and velocity 91 | * @return {void} Operates on the particle and returns nothing 92 | */ 93 | export const moveParticle = ({ 94 | amplitude, 95 | damping, 96 | frequency, 97 | lengthOfStep, 98 | particle, 99 | }) => { 100 | // Calculate direction from noise 101 | const angle = getNoiseValue(particle.x, particle.y, frequency, amplitude); 102 | 103 | // Update the velocity of the particle based on the direction 104 | particle.vx += Math.cos(angle) * lengthOfStep; 105 | particle.vy += Math.sin(angle) * lengthOfStep; 106 | 107 | // Move the particle 108 | particle.x += particle.vx; 109 | particle.y += particle.vy; 110 | 111 | // Use damping to slow down the particle (think friction) 112 | particle.vx *= damping; 113 | particle.vy *= damping; 114 | 115 | particle.line.push([particle.x, particle.y]); 116 | }; 117 | 118 | /** 119 | * Creates a flow field with particles and lines. 120 | * 121 | * @param {Object} config Configuration object 122 | * @param {Number} [config.amplitude=5] Controls the range of values we get from the noise function 123 | * @param {Number} [config.count=1000] Number of particles in the field 124 | * @param {Number} [config.damping=0.1] Percentage that slows down the particle (think friction) 125 | * @param {Number} config.height Height of space 126 | * @param {Number} [config.margin=0.1] Percent of height/width to create a padding 127 | * @param {Object} [config.particles] List of particles to use instead of generating them 128 | * @param {String} [config.scale=1] Used to compute frequency, number of steps and step length 129 | * @param {String} config.seed Random (random-js) seed 130 | * @param {Number} config.width Width of space 131 | * @return {Array} List of particle objects containing the line coordinates 132 | */ 133 | export const generateField = ({ 134 | amplitude = 5, 135 | count = 1000, 136 | damping = 0.1, 137 | height, 138 | margin = 0.1, 139 | particles: suppliedParticles, 140 | scale = 1, 141 | seed, 142 | width, 143 | } = {}) => { 144 | const maxParticleSteps = 30 * scale; 145 | const lengthOfStep = 5 * scale; 146 | const frequency = 0.001 / scale; 147 | const particles = 148 | suppliedParticles || 149 | generateParticles({ count, height, margin, seed, width }) || 150 | []; 151 | 152 | particles?.forEach((particle) => { 153 | while (particle.line.length < maxParticleSteps) { 154 | moveParticle({ 155 | amplitude, 156 | damping, 157 | frequency, 158 | lengthOfStep, 159 | particle, 160 | }); 161 | } 162 | }); 163 | 164 | particles?.forEach((particle) => { 165 | particle.line = particle.line.filter((particle) => { 166 | return isInBound(particle[0], particle[1], width, height, margin); 167 | }); 168 | }); 169 | 170 | return particles; 171 | }; 172 | --------------------------------------------------------------------------------