Drawing vector field

by tsulej

folds2d.tumblr.com

f03370000E4D3

The goal

During last few days I’ve made several attempts to create and visualize 2D vector fields. In the following article you can read about this concept with several examples and code in Processing.

There are many beautiful art/code based on vector field visualization around the net. Mostly based on noise function. Check them out before you start:

What is the vector field?

Vector field (in our case) is just a \mathbf{R}^{2} to \mathbf{R}^{2} function exatly the same as variation described in my previous post about Folds.

Why vector? Because you can treat a pair of (x',y') as vector \vec{v} = \begin{bmatrix}x'\\ y'\end{bmatrix} for each point from 2d plane. This way you get field of vectors.

Vector fields can be visualised in the following way

How to get the vector field

The main goal, in this article, is to research various ways to create vector fields. There two cases.

We can operate on functions which returns pair of values like variations. In this case such function defines vector field itself. We can use also some vector operations (like substraction, sum, etc…) to get new functions.

The second option is to get single float number n like from noise(), dist() between points or dot product of vectors, etc. And convert this number into vector.

Drawing method

The method is quite simple.

  1. Create points from [-3,3] floating point range
  2. Draw them
  3. For each of the point calculate relating vector from the vector field
  4. Scale it and add to the point coordinates
  5. Repeat 2-4

Step 4 is key here. We move our points along vectors. Trace lines build the resulting image.

The general flow for constructing vector field follows this scheme:

p=(x,y)\mapsto [multiple operations]\mapsto \vec{v}

Note: I assume that on every step I can multiply function variables or returned values by any number.

Here is code stub for my experiments

// dynamic list with our points, PVector holds position
ArrayList<PVector> points = new ArrayList<PVector>();

// colors used for points
color[] pal = {
  color(0, 91, 197),
  color(0, 180, 252),
  color(23, 249, 255),
  color(223, 147, 0),
  color(248, 190, 0)
};

// global configuration
float vector_scale = 0.01; // vector scaling factor, we want small steps
float time = 0; // time passes by

void setup() {
  size(800, 800);
  strokeWeight(0.66);
  background(0, 5, 25);
  noFill();
  smooth(8);

  // noiseSeed(1111); // sometimes we select one noise field

  // create points from [-3,3] range
  for (float x=-3; x<=3; x+=0.07) {
    for (float y=-3; y<=3; y+=0.07) {
      // create point slightly distorted
      PVector v = new PVector(x+randomGaussian()*0.003, y+randomGaussian()*0.003);
      points.add(v);
    }
  }
}

void draw() {
  int point_idx = 0; // point index
  for (PVector p : points) {
    // map floating point coordinates to screen coordinates
    float xx = map(p.x, -6.5, 6.5, 0, width);
    float yy = map(p.y, -6.5, 6.5, 0, height);

    // select color from palette (index based on noise)
    int cn = (int)(100*pal.length*noise(point_idx))%pal.length;
    stroke(pal[cn], 15);
    point(xx, yy); //draw

    // placeholder for vector field calculations

    // v is vector from the field
    PVector v = new PVector(0, 0);

    p.x += vector_scale * v.x;
    p.y += vector_scale * v.y;

    // go to the next point
    point_idx++;
  }
  time += 0.001;
}

f014700009714

When I change my field vector to be constant one (different than 0), my points start move.

PVector v = new PVector(0.1, 0.1);

f056900004085

Perlin noise

Let’s begin with perlin noise in few configurations. I’m going to change only marked lines from the code stub.

Variant 1

noise(x,y) function in processing returns value from 0 to 1 (actually implementation used in Processing has some problems and real value is often in range from 0.2 to 0.8). I will treat resulted value as an angle in polar coordinates and use sin()/cos() functions to convert it to cartesian ones. To properly do this I have to scale it up to range [0,2\pi ] or [-\pi, \pi ] or even more. I show few variants here.

// placeholder for vector field calculations
float n = TWO_PI * noise(p.x,p.y);
PVector v = new PVector(cos(n),sin(n));

f043800005B6A

Variant 2

Let’s check what happens when I multiply noise by 10, 100, 300 or even 1000. Noise is now scaled to range [-1.1] (it is more usable range in further examples).

float n = 10 * map(noise(p.x/5,p.y/5),0,1,-1,1); // 100, 300 or 1000
PVector v = new PVector(cos(n),sin(n));

Variant 3

Two of above variants were using classic method. Now I get rid of polar->cartesian conversion and I’m going to treat returned value from noise() as cartesian coordinates. Angle of the vector is the same for whole field and only length of vector is changed. This leads to some interesting patterns. Experiment with scaling input coordinates and n

 

float n = 5*map(noise(p.x,p.y),0,1,-1,1);
PVector v = new PVector(n,n);

Parametric curves

Let’s use parametric curves to convert noise() result into vector. The big list of them is defined in Wolfram Alpha or named here. Parametric curve (or plane curve) is a function \mathbf{R}\rightarrow\mathbf{R}^2. Click section name to see plot of the curve.

 

Astroid

// placeholder for vector field calculations
float n = 5*map(noise(p.x,p.y),0,1,-1,1); // and 2

float sinn = sin(n);
float cosn = cos(n);

float xt = sq(sinn)*sinn;
float yt = sq(cosn)*cosn;

PVector v = new PVector(xt,yt);

Results for noise scale 2 and 5

Cissoid of Diocles

// placeholder for vector field calculations
float n = 3*map(noise(p.x,p.y),0,1,-1,1);

float sinn2 = 2*sq(sin(n));

float xt = sinn2;
float yt = sinn2*tan(n);

PVector v = new PVector(xt,yt);

f146100000B07

Kampyle of Eudoxus

// placeholder for vector field calculations
float n = 6*map(noise(p.x,p.y),0,1,-1,1);

float sec = 1/sin(n);

float xt = sec;
float yt = tan(n)*sec; 

PVector v = new PVector(xt,yt);

f08490000B8A6

Rectangular Hyperbola

// placeholder for vector field calculations
float n = 10*map(noise(p.x*2,p.y*2),0,1,-1,1);

float xt = 1/sin(n);
float yt = tan(n); 

PVector v = new PVector(xt,yt);

f123200005CCF

Superformula

// placeholder for vector field calculations
float n = 5*map(noise(p.x,p.y),0,1,-1,1);

float a = 1;
float b = 1;
float m = 6;
float n1 = 1;
float n2 = 7;
float n3 = 8;

float f1 = pow(abs(cos(m*n/4)/a),n2);
float f2 = pow(abs(sin(m*n/4)/b),n3);
float fr = pow(f1+f2,-1/n1);

float xt = cos(n)*fr;
float yt = sin(n)*fr;

PVector v = new PVector(xt,yt);

f034900000FCF

Noise and curves combined

Now I’m going to play with some combinations of above curves and make multiple passes through noise().

Let’s put all curves into functions (paste following code to the end of the script):

PVector circle(float n) { // polar to cartesian coordinates
  return new PVector(cos(n), sin(n));
}

PVector astroid(float n) {
  float sinn = sin(n);
  float cosn = cos(n);

  float xt = sq(sinn)*sinn;
  float yt = sq(cosn)*cosn;

  return new PVector(xt, yt);
}

PVector cissoid(float n) {
  float sinn2 = 2*sq(sin(n));

  float xt = sinn2;
  float yt = sinn2*tan(n);

  return new PVector(xt, yt);
}

PVector kampyle(float n) {
  float sec = 1/sin(n);

  float xt = sec;
  float yt = tan(n)*sec;

  return new PVector(xt, yt);
}

PVector rect_hyperbola(float n) {
  float sec = 1/sin(n);

  float xt = 1/sin(n);
  float yt = tan(n);

  return new PVector(xt, yt);
}

final static float superformula_a = 1;
final static float superformula_b = 1;
final static float superformula_m = 6;
final static float superformula_n1 = 1;
final static float superformula_n2 = 7;
final static float superformula_n3 = 8;
PVector superformula(float n) {
  float f1 = pow(abs(cos(superformula_m*n/4)/superformula_a), superformula_n2);
  float f2 = pow(abs(sin(superformula_m*n/4)/superformula_b), superformula_n3);
  float fr = pow(f1+f2, -1/superformula_n1);

  float xt = cos(n)*fr;
  float yt = sin(n)*fr;

  return new PVector(xt, yt);
}

Variant 1 – p5art

Code based on p5art web app noisePainting

point\rightarrow noise\rightarrow noise\rightarrow noise\rightarrow vector

// placeholder for vector field calculations
float n1 = 10*noise(1+p.x/20, 1+p.y/20); // shift input to avoid symetry
float n2 = 5*noise(n1, n1);
float n3 = 325*map(noise(n2, n2), 0, 1, -1, 1); // 25,325

PVector v = circle(n3);

Variant 2

point\rightarrow noise\rightarrow curves\rightarrow noise\rightarrow noise\rightarrow circle\rightarrow vector

// placeholder for vector field calculations
float n1 = 15*map(noise(1+p.x/20, 1+p.y/20),0,1,-1,1);

PVector v1 = cissoid(n1);
PVector v2 = astroid(n1);

float n2a = 5*noise(v1.x,v2.y);
float n2b = 5*noise(v2.x,v1.y);

float n3 = 10*map(noise(n2a, n2b/3), 0, 1, -1, 1);

PVector v = circle(n3);

f071000004C6F

Variant 3

point\rightarrow noise\rightarrow curves\rightarrow noises\rightarrow vector

// placeholder for vector field calculations
float n1 = 15*map(noise(1+p.x/10, 1+p.y/10),0,1,-1,1);

PVector v1 = rect_hyperbola(n1);
PVector v2 = astroid(n1);

float n2a = 2*map(noise(v1.x,v1.y),0,1,-1,1);
float n2b = 2*map(noise(v2.x,v2.y),0,1,-1,1);

PVector v = new PVector(n2a,n2b);

f056400001094

Variant 4

point\rightarrow curves\rightarrow noises\rightarrow circle\rightarrow vector

// placeholder for vector field calculations
PVector v1 = kampyle(p.x);
PVector v2 = superformula(p.y);

float n2a = 3*map(noise(v1.x,v1.y),0,1,-1,1);
float n2b = 3*map(noise(v2.x,v2.y),0,1,-1,1);

PVector v = new PVector(cos(n2a),sin(n2b));

f04470000695F

Variant 5

point\rightarrow noises\rightarrow curves\rightarrow noises\rightarrow circle\rightarrow vector

// placeholder for vector field calculations
float n1a = 3*map(noise(p.x,p.y),0,1,-1,1);
float n1b = 3*map(noise(p.y,p.x),0,1,-1,1);

PVector v1 = rect_hyperbola(n1a);
PVector v2 = astroid(n1b);

float n2a = 3*map(noise(v1.x,v1.y),0,1,-1,1);
float n2b = 3*map(noise(v2.x,v2.y),0,1,-1,1);

PVector v = new PVector(cos(n2a),sin(n2b));

f04750000BD8E

Variant 6 – comparison

Here I want to compare different curves using the same method. I change marked lines into pair of curves defined above. I’ve put noiseSeed(1111); in setup() to have the same noise structure. Check image description to see what curves were used.

point\rightarrow curves\rightarrow subtract\rightarrow vector

// placeholder for vector field calculations
float n1a = 15*map(noise(p.x/10,p.y/10),0,1,-1,1);
float n1b = 15*map(noise(p.y/10,p.x/10),0,1,-1,1);

PVector v1 = superformula(n1a);
PVector v2 = circle(n1b);

PVector diff = PVector.sub(v2,v1);
diff.mult(0.3);

PVector v = new PVector(diff.x,diff.y);

Time

Now let’s introduce time. An external variable which is incremented every frame with some small value. It can be used to slightly change noise field during rendrering or can be used as a scale factor.

Let’s compare the same vector field without time and time used as third noise parameter.

 // placeholder for vector field calculations
 float n1a = 3*map(noise(p.x/2,p.y/2),0,1,-1,1);
 float n1b = 3*map(noise(p.y/2,p.x/2),0,1,-1,1);
 float nn = 6*map(noise(n1a,n1b),0,1,-1,1);

 PVector v = circle(nn);

f041800008831

And now I add time as a third parameter to noise function. This way our vector field changes when time passes.

 // placeholder for vector field calculations
float n1a = 3*map(noise(p.x/2,p.y/2,time),0,1,-1,1);
float n1b = 3*map(noise(p.y/2,p.x/2,time),0,1,-1,1);
float nn = 6*map(noise(n1a,n1b,time),0,1,-1,1);

PVector v = circle(nn);

f051900001798

Now time is used as a scaling factor.

// placeholder for vector field calculations
float n1a = 3*map(noise(p.x/2,p.y/2),0,1,-1,1);
float n1b = 3*map(noise(p.y/2,p.x/2),0,1,-1,1);

float nn = time*10*6*map(noise(n1a,n1b),0,1,-1,1);

PVector v = circle(nn);

f089600004548

Variations

Now let’s introduce variations (see previous post about Folds) as a vector field definition.

Copy below code to the end of your sketch. It includes function definiotions used in this and next chapters.

PVector sinusoidal(PVector v, float amount) {
  return new PVector(amount * sin(v.x), amount * sin(v.y));
}

PVector waves2(PVector p, float weight) {
  float x = weight * (p.x + 0.9 * sin(p.y * 4));
  float y = weight * (p.y + 0.5 * sin(p.x * 5.555));
  return new PVector(x, y);
}

PVector polar(PVector p, float weight) {
  float r = p.mag();
  float theta = atan2(p.x, p.y);
  float x = theta / PI;
  float y = r - 2.0;
  return new PVector(weight * x, weight * y);
}

PVector swirl(PVector p, float weight) {
  float r2 = sq(p.x)+sq(p.y);
  float sinr = sin(r2);
  float cosr = cos(r2);
  float newX = 0.8 * (sinr * p.x - cosr * p.y);
  float newY = 0.8 * (cosr * p.y + sinr * p.y);
  return new PVector(weight * newX, weight * newY);
}

PVector hyperbolic(PVector v, float amount) {
  float r = v.mag() + 1.0e-10;
  float theta = atan2(v.x, v.y);
  float x = amount * sin(theta) / r;
  float y = amount * cos(theta) * r;
  return new PVector(x, y);
}

PVector power(PVector p, float weight) {
  float theta = atan2(p.y, p.x);
  float sinr = sin(theta);
  float cosr = cos(theta);
  float pow = weight * pow(p.mag(), sinr);
  return new PVector(pow * cosr, pow * sinr);
}

PVector cosine(PVector p, float weight) {
  float pix = p.x * PI;
  float x = weight * 0.8 * cos(pix) * cosh(p.y);
  float y = -weight * 0.8 * sin(pix) * sinh(p.y);
  return new PVector(x, y);
}

PVector cross(PVector p, float weight) {
  float r = sqrt(1.0 / (sq(sq(p.x)-sq(p.y)))+1.0e-10);
  return new PVector(weight * 0.8 * p.x * r, weight * 0.8 * p.y * r);
}

PVector vexp(PVector p, float weight) {
  float r = weight * exp(p.x);
  return new PVector(r * cos(p.y), r * sin(p.y));
}

// parametrization P={pdj_a,pdj_b,pdj_c,pdj_d}
float pdj_a = 0.1;
float pdj_b = 1.9;
float pdj_c = -0.8;
float pdj_d = -1.2;
PVector pdj(PVector v, float amount) {
  return new PVector( amount * (sin(pdj_a * v.y) - cos(pdj_b * v.x)),
  amount * (sin(pdj_c * v.x) - cos(pdj_d * v.y)));
}

final float cosh(float x) { return 0.5 * (exp(x) + exp(-x));}
final float sinh(float x) { return 0.5 * (exp(x) - exp(-x));}

Let’s see how individual variations look as vector fields. Sometimes I had to adjust scaling factor to keep points on the screen.

// placeholder for vector field calculations
PVector v = sinusoidal(p,1);
v.mult(1); // different values for different functions

All together

Let’s sumarize what functions we have now defined.

  • \mathbf{R}\rightarrow\mathbf{R}
    • multiplying/dividing by number
    • single variable noise()
    • other single variable functions (sin/cos, exp, log, sq, sqrt, etc.)
  • \mathbf{R}\rightarrow\mathbf{R^2}
    • parametric curves
  • \mathbf{R^2}\rightarrow\mathbf{R}
    • noise() with two parameters
    • distance between vectors
    • dot product of vectors
    • length of the vector
    • angle between vectors
  • \mathbf{R^2}\rightarrow\mathbf{R^2}
    • variations and their combinations
    • sum or difference of vectors
    • atan2() of the vector

All above operations can be used to generate vector field (or simply transform 2d point to another 2d point).

I will play with above transformations in following examples. It’s a record of experiments.

Variant 1

point\rightarrow variations\rightarrow circle\rightarrow vector

// placeholder for vector field calculations
float n1 = 5*map(noise(p.x/5,p.y/5),0,1,-1,1);

PVector v1 = rect_hyperbola(n1);
PVector v2 = swirl(v1,1);

PVector v = new PVector(cos(v2.x),sin(v2.y));

f049300008710

Variant 2

point\rightarrow noises\rightarrow variations\rightarrow subtraction\rightarrow variation\rightarrow vector

// placeholder for vector field calculations
float n1 = 5*map(noise(p.x/5,p.y/5),0,1,-1,1);
float n2 = 5*map(noise(p.y/5,p.x/5),0,1,-1,1);

PVector v1 = vexp(new PVector(n1,n2),1);
PVector v2 = swirl(new PVector(n2,n1),1);

PVector v3 = PVector.sub(v2,v1);

PVector v4 = waves2(v1,1);
v4.mult(0.8);

PVector v = new PVector(v4.x,v4.y);

f04190000D54D

Variant 3

point\rightarrow variation\rightarrow noises\rightarrow variation\rightarrow vector

// placeholder for vector field calculations
PVector v1 = vexp(p,1);

float n1 = map(noise(v1.x,v1.y,time),0,1,-1,1);
float n2 = map(noise(v1.y,v1.x,-time),0,1,-1,1);

PVector v2 = vexp(new PVector(n1,n2),1);

PVector v = new PVector(v2.x,v2.y);

f028600002221

Variant 4

point\rightarrow variation\rightarrow noise\rightarrow curve\rightarrow normalization\rightarrow vector

// placeholder for vector field calculations
PVector v1 = polar(cross(p,1),1);

float n1 = 15*map(noise(v1.x,v1.y,time),0,1,-1,1);
PVector v2 = cissoid(n1);
v2.normalize();

PVector v = new PVector(v2.x,v2.y);

f08680000EA5D

Variant 5

point\rightarrow variation\rightarrow noises\rightarrow variation\rightarrow angle\rightarrow variation\rightarrow circle\rightarrow vector

// placeholder for vector field calculations
PVector v1 = power(p,1);

float n1 = 5*map(noise(v1.x,v1.y,time),0,1,-1,1);
float n2 = 5*map(noise(p.x/3,p.y/3,-time),0,1,-1,1);

PVector v2 = cosine(new PVector(n1,n2),1);

float a1 = PVector.angleBetween(v1,v2);

PVector v3 = superformula(a1);
v3.mult(3);

PVector v = new PVector(cos(v3.x),sin(v3.y));

f04770000F923

Variant 6

point\rightarrow variation\rightarrow subtraction\rightarrow angles\rightarrow combination \rightarrow circle\rightarrow vector

// placeholder for vector field calculations
PVector v1 = waves2(p,1);
PVector v2 = PVector.sub(v1,p);

float n1 = 8*noise(time)*atan2(v1.y,v1.x);
float n2 = 8*noise(time+0.5)*atan2(v2.y,v2.x);

PVector v = new PVector(cos(v2.x*n1),sin(v1.y+n2));

f035200007633

Variant 7

point\rightarrow variation\rightarrow subtraction\rightarrow noises\rightarrow angles\rightarrow curves\rightarrow variations\rightarrow subtraction\rightarrow variation\rightarrow angle\rightarrow combination\rightarrow circle\rightarrow vector

// placeholder for vector field calculations
PVector v1 = swirl(p,1);
v1.mult(0.5);
PVector v2 = PVector.sub(v1,p);

float nv1 = noise(v1.x,v1.y,time);
float nv2 = noise(v2.x,v2.y,-time);

float n1 = (atan2(v1.y,v1.x)+nv2);
float n2 = (atan2(v2.y,v2.x)+nv1);

PVector v3 = superformula(n1);
PVector v4 = rect_hyperbola(n2);

PVector tv3 = waves2(v3,1);
PVector tv4 = sinusoidal(v4,1);

PVector v5 = PVector.sub(tv4,tv3);

PVector v6 = pdj(v5,1);

float an = PVector.angleBetween(v6,new PVector(n1,n2));

PVector v = new PVector(cos(v2.x+sin(an)),sin(v6.y-cos(an)));
v.mult(1+time*40);

f040300002B89

Image

The last part is about using channel information from an image as a vector field. I wrote two sketches some time ago which are based on vector field concept.

Drawing generative

res_75948_natalie

Threadraw

res_4BC7E81C_natalie

How to use image data

First load your image at the beginning of the sketch

img = loadImage("natalie.jpg"); // PImage img; must be declared globally

Then use image data following way

// placeholder for vector field calculations
PVector v1 = sinusoidal(p,1);

int img_x = (int)map(p.x,-3,3,0,img.width);
int img_y = (int)map(p.y,-3,3,0,img.height);

float b = brightness( img.get(img_x,img_y) )/255.0;
PVector br = circle(b);

float a = 5*PVector.angleBetween(br,v1);

PVector v = astroid(a);

f015500000067

Appendix

This concept is taken from zach lieberman and Keith Peters who explored painting by non-intersecting segments. This idea can be used on vector fields too. Here are two examples.

See the code HERE.

The end

Questions? generateme.blog@gmail.com or