├── LICENSE ├── README.md ├── css ├── controls.css └── main.css ├── imgs ├── black_white.png ├── multi_color.png └── red.png ├── index.html └── js ├── controls.js ├── particle.js └── sketch.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michael Freeman 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flowFields 2 | This project uses a particle simulation to visualize a field of directional vectors. It uses Perlin Noise to construct a field of random (but related) forces in horizontal and vertical directions (that change over time). The project uses the P5js library, and is heavily based on this tutorial series and the corresponding code. 3 | 4 | Here are a few examples of what you can make using the tool. 5 | 6 | ![Multi color drawing](imgs/multi_color.png) 7 | 8 | ![Black and white drawing](imgs/black_white.png) 9 | 10 | ![Red drawing](imgs/red.png) -------------------------------------------------------------------------------- /css/controls.css: -------------------------------------------------------------------------------- 1 | /* Control styles */ 2 | .slider, .color-picker{ 3 | color:#898787; 4 | } 5 | 6 | input { 7 | cursor: pointer; 8 | } 9 | .button-wrapper { 10 | display: inline-block; 11 | } 12 | 13 | 14 | div.color-picker { 15 | margin-bottom:4px; 16 | } 17 | 18 | #control-wrapper { 19 | float: left; 20 | max-width:200px; 21 | max-height:calc(100vh - 150px); 22 | overflow-y:scroll; 23 | } 24 | 25 | button.btn { 26 | width: 95px; 27 | padding: 5px; 28 | margin: 2px; 29 | background-color:rgb(232, 138, 159); 30 | cursor:pointer; 31 | } 32 | 33 | #control-wrapper a { 34 | color:inherit; 35 | } -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | @import url("https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"); 2 | 3 | body { 4 | font-family: sans-serif; 5 | box-sizing: border-box; 6 | } 7 | 8 | header h1, header p { 9 | background-color:rgb(216, 60, 95); 10 | color:white; 11 | padding-left:20px; 12 | } 13 | 14 | header h1 { 15 | font-weight: 100; 16 | font-size: 2.5em; 17 | margin-bottom: 0; 18 | } 19 | 20 | h2 { 21 | font-weight:100; 22 | } 23 | 24 | .p5_canvas { 25 | border: 1px solid #d3d3d3; 26 | margin: 20px; 27 | } 28 | 29 | .container { 30 | max-width: inherit; 31 | } 32 | 33 | 34 | /* Footer */ 35 | footer { 36 | width: 100%; 37 | background-color: white; 38 | position: fixed; 39 | bottom: 0px; 40 | z-index: 9999; 41 | } 42 | 43 | .footer-copyright { 44 | border-top: 1px solid rgb(216, 60, 95); 45 | margin-top: 10px; 46 | padding: 5px; 47 | opacity: .5; 48 | } 49 | 50 | .right { 51 | float: right; 52 | } -------------------------------------------------------------------------------- /imgs/black_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkfreeman/flowFields/b37d56eafec3f3c5c123ed217a07a8f186c4062d/imgs/black_white.png -------------------------------------------------------------------------------- /imgs/multi_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkfreeman/flowFields/b37d56eafec3f3c5c123ed217a07a8f186c4062d/imgs/multi_color.png -------------------------------------------------------------------------------- /imgs/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkfreeman/flowFields/b37d56eafec3f3c5c123ed217a07a8f186c4062d/imgs/red.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | FlowFields 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 44 | 45 | 46 | 47 | 48 |
49 | 53 |
54 |
55 | 56 | 84 |
85 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /js/controls.js: -------------------------------------------------------------------------------- 1 | 2 | // Write a function to create a slider 3 | function makeSlider(label, minVal = 0, maxVal = 50, value = 10, step = 1, parent = createDiv(), update = () => {}) { 4 | let wrapper = createDiv(label); 5 | wrapper.parent(parent); 6 | wrapper.class("slider"); 7 | let slider = createSlider(minVal, maxVal, value, step); 8 | slider.input(update); // function to do on update 9 | slider.class("form-control-range") 10 | slider.parent(wrapper); 11 | return (slider); 12 | } 13 | 14 | // Function to make a button 15 | function makeButton(text, parent, callback, type = "not_modal") { 16 | let buttonWrapper = createDiv(); 17 | buttonWrapper.class("button-wrapper"); 18 | let button = createButton(text); 19 | button.class("btn") 20 | if(type === "modal") { 21 | button.attribute("data-toggle", "modal") 22 | button.attribute("data-target", "#exampleModal") 23 | } 24 | button.parent(buttonWrapper) 25 | buttonWrapper.parent(parent); 26 | button.mousePressed(callback); 27 | } 28 | 29 | // Function to make a color picker 30 | function makeColorPicker(label = "Pick a color", startColor = "red", parent = createDiv(), update = () => {}) { 31 | let wrapper = createDiv(label); 32 | wrapper.class("color-picker"); 33 | wrapper.parent(parent) 34 | let picker = createColorPicker(startColor) 35 | picker.input(() => update(picker.value())); 36 | picker.parent(wrapper); 37 | picker.class("form-control-range") 38 | return (picker); 39 | } -------------------------------------------------------------------------------- /js/particle.js: -------------------------------------------------------------------------------- 1 | // Daniel Shiffman 2 | // http://codingtra.in 3 | // http://patreon.com/codingtrain 4 | // Code for: https://youtu.be/BjoM9oKOAKY 5 | 6 | function Particle(cellWidth = 400, cellHeight = 400) { 7 | this.pos = createVector(random(width), random(height)); 8 | this.vel = createVector(0, 0); 9 | this.acc = createVector(0, 0); 10 | this.maxspeed = 2; 11 | 12 | this.prevPos = this.pos.copy(); 13 | 14 | this.update = function () { 15 | this.vel.add(this.acc); 16 | this.vel.limit(this.maxspeed); 17 | this.pos.add(this.vel); 18 | this.acc.mult(0); 19 | }; 20 | 21 | this.follow = function (vectors) { 22 | var x = floor(this.pos.x / cellWidth); 23 | var y = floor(this.pos.y / cellHeight); 24 | var index = x + y * ncol; 25 | var force = vectors[index]; 26 | this.applyForce(force); 27 | }; 28 | 29 | this.applyForce = function (force) { 30 | this.acc.add(force); 31 | }; 32 | 33 | this.getColor = function() { 34 | let color = strokeColorPicker.color()._array.slice(0, 3) 35 | .concat(opacitySlider.value()) 36 | .map((d) => d * 100); 37 | return(color); 38 | } 39 | this.show = function () { 40 | stroke(this.getColor()); 41 | 42 | strokeWeight(.3); 43 | line(this.pos.x, this.pos.y, this.prevPos.x, this.prevPos.y); 44 | this.updatePrev(); 45 | }; 46 | 47 | this.updatePrev = function () { 48 | this.prevPos.x = this.pos.x; 49 | this.prevPos.y = this.pos.y; 50 | }; 51 | 52 | this.edges = function () { 53 | if (this.pos.x > width | this.pos.x < 0 | this.pos.y > height | this.pos.y < 0) { 54 | this.pos = createVector(random(width), random(height)); 55 | this.updatePrev(); 56 | } 57 | }; 58 | } -------------------------------------------------------------------------------- /js/sketch.js: -------------------------------------------------------------------------------- 1 | // Main script to construct the noise field 2 | 3 | // "Global" variables 4 | let X_START = 0; 5 | const Y_START = 0; 6 | let xoff = 0; 7 | let yoff = 0; 8 | let zoff = 0; 9 | let particles = []; 10 | let flowfield = []; 11 | let canvas; 12 | let nrow, ncol, rectWidth, rectHeight; 13 | let xIncrementSlider, yIncrementSlider, zIncrementSlider, particleSlider, opacitySlider, strokeColorPicker, backgroundColorPicker; 14 | 15 | function makeControls() { 16 | // Controls 17 | let controlWrapper = createDiv().id("control-wrapper"); 18 | let controlHeader = createDiv("

Controls

"); 19 | controlHeader.parent(controlWrapper); 20 | nrowSlider = makeSlider("Vertical Anchors", minVal = 2, maxVal = 50, value = 30, step = 1, parent = controlWrapper, clearContent); 21 | ncolSlider = makeSlider("Horizontal Anchors", minVal = 2, maxVal = 50, value = 30, step = 1, parent = controlWrapper, clearContent); 22 | xIncrementSlider = makeSlider("Horizontal Smoothness", minVal = .0001, maxVal = .3, value = .05, step = .0001, parent = controlWrapper, clearContent); 23 | yIncrementSlider = makeSlider("Vertical Smoothness", minVal = .0001, maxVal = .3, value = .05, step = .0001, parent = controlWrapper, clearContent); 24 | zIncrementSlider = makeSlider("Fluctuations in Forces", minVal = 0, maxVal = .3, value = .01, step = .0001, parent = controlWrapper, clearContent); 25 | particleSlider = makeSlider("Number of Particles", minVal = 10, maxVal = 10000, value = 500, step = 10, parent = controlWrapper, clearContent); 26 | opacitySlider = makeSlider("Line Opacity", minVal = 0, maxVal = 1, value = .1, step = .01, parent = controlWrapper); 27 | strokeColorPicker = makeColorPicker("Line Color", startColor = "rgb(216, 60, 95)", parent = controlWrapper); 28 | backgroundColorPicker = makeColorPicker("Background Color", startColor = "white", parent = controlWrapper, (d) => setBackgroundColor(d)); 29 | 30 | // Buttons 31 | makeButton("Pause", controlWrapper, noLoop); 32 | makeButton("Resume", controlWrapper, loop); 33 | makeButton("Clear  ", controlWrapper, clearContent); 34 | makeButton("Download", controlWrapper, download); 35 | makeButton("About", controlWrapper, () => {}, "modal"); 36 | makeButton("GitHub", controlWrapper, () => { 37 | window.open("https://github.com/mkfreeman/flowFields", "_blank"); 38 | }); 39 | return controlWrapper; 40 | } 41 | 42 | // Function to set background color 43 | function setBackgroundColor() { 44 | // Avoids clearing the content 45 | canvas.style("background-color", backgroundColorPicker.value()) 46 | } 47 | // Create particles 48 | function createEmptyParticles() { 49 | particles = []; 50 | for (let i = 0; i < particleSlider.value(); i++) { 51 | particles[i] = new Particle(rectWidth, rectHeight); 52 | } 53 | } 54 | 55 | // Clear content 56 | function clearContent() { 57 | clear(); 58 | createEmptyParticles(); 59 | flowfield = []; 60 | xoff = X_START = random(100); 61 | yoff = random(100); 62 | zoff = random(100); 63 | } 64 | 65 | // Download canvas 66 | function download() { 67 | noLoop(); // pause 68 | let link = document.createElement('a'); 69 | link.download = 'noise_field.png'; 70 | link.href = document.querySelector('canvas').toDataURL() 71 | link.click(); 72 | } 73 | 74 | // Set up (elements only drawn once) 75 | function setup() { 76 | // Get window size 77 | let windowWidth = window.innerWidth - 270; 78 | let windowHeight = window.innerHeight - 180; 79 | 80 | // Container for everything 81 | let container = createDiv().class("container"); 82 | 83 | // Create controls and canvas 84 | let controls = makeControls(); 85 | controls.parent(container); 86 | let canvasContainer = createDiv(); 87 | canvas = createCanvas(windowWidth, windowHeight).class("p5_canvas"); 88 | canvasContainer.parent(container); 89 | canvas.parent(canvasContainer); 90 | 91 | // Set color mode to RGB percentages 92 | colorMode(RGB, 100); 93 | 94 | // Create set of particles 95 | getSize(); 96 | createEmptyParticles(); 97 | } 98 | 99 | function getSize() { 100 | // Construct a grid of rectangles (rows/columns) 101 | nrow = nrowSlider.value(); 102 | ncol = ncolSlider.value(); 103 | rectWidth = width / ncol; 104 | rectHeight = height / nrow; 105 | } 106 | 107 | function draw() { 108 | getSize(); 109 | // Iterate through grid and set vector forces 110 | for (let row = 0; row < nrow; row++) { 111 | for (let col = 0; col < ncol; col++) { 112 | let angle = noise(xoff, yoff, zoff) * 4 * PI; 113 | var v = p5.Vector.fromAngle(angle); 114 | v.setMag(1); 115 | flowfield.push([v.x, v.y]); 116 | xoff += xIncrementSlider.value(); 117 | } 118 | xoff = X_START; 119 | yoff += yIncrementSlider.value(); 120 | } 121 | 122 | // Position particles given field of vector forces 123 | for (var i = 0; i < particles.length; i++) { 124 | particles[i].follow(flowfield); 125 | particles[i].update(); 126 | particles[i].edges(); 127 | particles[i].show(); 128 | } 129 | zoff += zIncrementSlider.value(); // think of this as time! 130 | } --------------------------------------------------------------------------------