How do I get this array of circles to appear within the boundary of a parallelogram?

Viewed 72

I understand how to get an array of circles to appear within a given square space, but how do I get them to appear within irregular shaped spaces. I have a parallelogram outlined that defines the boundaries of where I am trying to get the circles to appear, but no idea how to achieve this. Thanks!

Code from p5.js:

let circles = [];

function setup() {
  createCanvas(1200, 1200);
  colorMode(HSB,360,100,100,1);
  
  
  for (let i = 0; i < 500; i++) {
    x = random(400, 800);
    y = random(400, 800);
    d = 10;
    circles[i] = new circleClass(x, y, d);
  }
}


function draw() {
  
  background(35,13,90,1);
  
  noLoop();

  for (let i = 0; i < circles.length; i++) {
    circles[i].show();
  }
  
  
  noFill();
  stroke(0);
  quad(400,600,800,200,800,600,400,1000);
  
}



class circleClass {
  constructor(x, y, d) {
    this.x = x;
    this.y = y;
    this.d = d;
  }
  show() {
    noStroke();
    fill(27, 71, 73, 1);
    ellipse(this.x, this.y, this.d);
  }
}
2 Answers

Seems you want to generate small circles in random positions inside parallelogram.

Take two vectors of parallelogram sides, and generate random points using linear combination of these vectors:

ax = v[1].x - v[0].x  (800-400 for your quad)
ay = v[1].y - v[0].y  (200-600 for your quad)
bx = v[2].x - v[0].x   (and so on)
by = v[2].y - v[0].y

t = random(0..1)
u = random(0..1)
x = v[0].x + ax * t + bx * u
y = v[0].y + ay * t + by * u

where v[] is array of parallelogram vertices

let v = [400,600,800,200,800,600,400,1000]
let ax = v[2] - v[0]
let ay = v[3] - v[1]
let bx = v[4] - v[0]
let by = v[5] - v[0]

for loop {
    let t = Math.random()
    let u = Math.random()
    let x = v[0] + ax * t + bx * u
    let y = v[1] + ay * t + by * u
    set circle center in x, y

Here's a p5.js version:

function setup() {
  createCanvas(600, 600);
  background(255);
  
  let v = [200, 300,   // top left     array indices: [x=0, y=1]
           400, 100,   // top right    array indices: [x=2, y=3]
           400, 300,   // bottom right array indices: [x=4, y=5]
           200, 500];  // bottom left  array indices: [x=6, y=7]
  
  // top right - top left
  let ax = v[2] - v[0];
  let ay = v[3] - v[1];
  // bottom left - top left
  let bx = v[6] - v[0];
  let by = v[7] - v[1];

  for(let i = 0; i < 500; i++) {
      let t = random(); // interpolation amount on 1 axis
      let u = random(); // interpolation amount on the other
      // offset by top left point (v[0], v[1])
      // interpolate along top left to top right (imagine X axis aligned with parallelogram sides)
      // and top left to bottom left (imagine Y axis aligned with parallelogram sides)
      let x = v[0] + (ax * t + bx * u);
      let y = v[1] + (ay * t + by * u);
      // render point
      circle(x, y, 10);
  }
  
  noFill();
  // pass the v array as arguments using the spread operator
  quad(...v);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

This is an addition to MBo's answer above (+1).

The gist of is this section:

let x = v[0] + ax * t + bx * u
let y = v[1] + ay * t + by * u

This might look more familiar as the equation of a line.

In p5.js there's already a very useful lerp() function which takes two numbers and third parameter t between 0.0 and 1.0 and returns a number between the first two (e.g. if t is 0.5 the result will be 1/2 between the numbers, 0.25 would the 1/4 there and .75 would be 3/4, etc.). Here's a basic example:

let v1, v2, v3, v4;

function setup() {
  createCanvas(600, 600);
  
  v1 = createVector(200, 300);
  v2 = createVector(400, 100);
  v3 = createVector(400, 300);
  v4 = createVector(200, 500);
}

function draw(){
  background(255);
  
  let t = map(constrain(mouseX, 0, width), 0, width, 0.0, 1.0);
  // interpolate each component, x,y between v1 and v2 using horizontal mouse movement
  let x = lerp(v1.x, v2.x, t);
  let y = lerp(v1.y, v2.y, t);
  // preview interpolated position
  circle(x, y, 10);
  
  noFill();
  stroke(0);
  quad(v1.x, v1.y, v2.x, v2.y, v3.x, v3.y, v4.x, v4.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

We could use this directly, but it would get tedious to do 8 times (2 coordinates (x,y) for 4 points (quad)).

There's also very useful p5.Vector class in p5 with a p5.Vector.lerp() which can help easily interpolate between two points:

let v1, v2, v3, v4;

function setup() {
  createCanvas(600, 600);
  
  v1 = createVector(200, 300);
  v2 = createVector(400, 100);
  v3 = createVector(400, 300);
  v4 = createVector(200, 500);
}

function draw(){
  background(255);
  
  let t = map(constrain(mouseX, 0, width), 0, width, 0.0, 1.0);
  // interpolate each component, x,y between v1 and v2 using horizontal mouse movement
  let l = p5.Vector.lerp(v1, v2, t);
  // preview interpolated position
  circle(l.x, l.y, 10);
  
  noFill();
  stroke(0);
  quad(v1.x, v1.y, v2.x, v2.y, v3.x, v3.y, v4.x, v4.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

If we group interpolation between pairs of points using two different interpolation amounts into a reusable function we essentially implement a form of bilinear interpolation:

/**
* Bilinear interpolation: interpolates a point on a between two lines (defined by 4 points)
* @param xt: traversal on first axis (0.0 -> 1.0)
* @param yt: traversal on second axis (0.0 -> 1.0)
**/
function quadLerp(v1, v2, v3, v4, xt, yt){
    let v1to2 = p5.Vector.lerp(v1, v2, yt);
    let v3to4 = p5.Vector.lerp(v3, v4, yt);
    return p5.Vector.lerp(v1to2, v3to4, xt);
}

Demo:

let v1, v2, v3, v4;

function setup() {
  createCanvas(600, 600);
  
  v1 = createVector(200, 300);
  v2 = createVector(400, 100);
  v3 = createVector(400, 300);
  v4 = createVector(200, 500);
}

/**
* Bilinear interpolation: interpolates a point on a between two lines (defined by 4 points)
* @param xt: traversal on first axis (0.0 -> 1.0)
* @param yt: traversal on second axis (0.0 -> 1.0)
**/
function quadLerp(v1, v2, v3, v4, xt, yt){
    let v1to2 = p5.Vector.lerp(v1, v2, yt);
    let v3to4 = p5.Vector.lerp(v3, v4, yt);
    // text() is simply for debugging/visualisation purposed: not actually required
    text("v1to2", v1to2.x, v1to2.y);
    text("v3to4", v3to4.x, v3to4.y);
    return p5.Vector.lerp(v1to2, v3to4, xt);
}

function draw(){
  background(192, 255, 192);
  
  let u = map(constrain(mouseX, 0, width), 0, width, 0.0, 1.0);
  let v = map(constrain(mouseY, 0, height), 0, height, 0.0, 1.0);
  // vertex order (winding) is important
  let l = quadLerp(v1, v2, v4, v3, u, v);
  // preview interpolated position
  circle(l.x, l.y, 10);
  
  noFill();
  stroke(0);
  quad(v1.x, v1.y, v2.x, v2.y, v3.x, v3.y, v4.x, v4.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

Notice as the mouse moves across the whole sketch canvas the circle always stays within the bounds of the quad.

That's pretty much the solution: simply swap the mapped mouse coordinates for random numbers between 0.0 and 1.0.

function setup() {
  createCanvas(1200, 1200);
  background(192, 255, 192);
  
  let v1 = createVector(400, 600);
  let v2 = createVector(800, 200);
  let v3 = createVector(800, 600);
  let v4 = createVector(400, 1000);
  
  for (let i = 0 ; i < 500; i++) {
      let u = random();
      let v = random();
      // vertex order (winding) is important
      let l = quadLerp(v1, v2, v4, v3, u, v);
      circle(l.x, l.y, 10);
  }
  
  noFill();
  stroke(0);
  quad(v1.x, v1.y, v2.x, v2.y, v3.x, v3.y, v4.x, v4.y);
}

/**
* Bilinear interpolation: interpolates a point on a between two lines (defined by 4 points)
* @param xt: traversal on first axis (0.0 -> 1.0)
* @param yt: traversal on second axis (0.0 -> 1.0)
**/
function quadLerp(v1, v2, v3, v4, xt, yt){
    let v1to2 = p5.Vector.lerp(v1, v2, yt);
    let v3to4 = p5.Vector.lerp(v3, v4, yt);
    return p5.Vector.lerp(v1to2, v3to4, xt);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

Here's a variant closer to your code:

let circles = [];

function setup() {
  createCanvas(1200, 1200);
  colorMode(HSB,360,100,100,1);
  background(35,13,90,1);
  
  let v1 = createVector(400, 600);
  let v2 = createVector(800, 200);
  let v3 = createVector(800, 600);
  let v4 = createVector(400, 1000);
  
  for (let i = 0 ; i < 500; i++) {
      let u = random();
      let v = random();
      // vertex order (winding) is important
      let l = quadLerp(v1, v2, v4, v3, u, v);
      circles[i] = new Circle(l.x, l.y, 10);
      circles[i].show();
  }
  
  noFill();
  stroke(0);
  quad(v1.x, v1.y, v2.x, v2.y, v3.x, v3.y, v4.x, v4.y);
}

/**
* Bilinear interpolation: interpolates a point on a between two lines (defined by 4 points)
* @param xt: traversal on first axis (0.0 -> 1.0)
* @param yt: traversal on second axis (0.0 -> 1.0)
**/
function quadLerp(v1, v2, v3, v4, xt, yt){
    let v1to2 = p5.Vector.lerp(v1, v2, yt);
    let v3to4 = p5.Vector.lerp(v3, v4, yt);
    return p5.Vector.lerp(v1to2, v3to4, xt);
}

class Circle {
  constructor(x, y, d) {
    this.x = x;
    this.y = y;
    this.d = d;
  }
  show() {
    noStroke();
    fill(27, 71, 73, 1);
    ellipse(this.x, this.y, this.d);
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

I've made a couple of tweaks:

  • renamed circleClass to simply Circle keeping the naming convention of a title case class name. I've removed the Class part to avoid potential confusion when instantiating circles (e.g. new circleClass())
  • I've removed draw(), hence removing the need for noLoop();
  • the circles array is sort of redundant too in this basic example, but could useful in a more detailed, animated version of this sketch.

Take you time to read/tweak/break/fix the code above. Understanding the basics will save you so much time on the long run. It's may appear counter intuitive, but sometimes slowing down helps you speed up. Have fun!

Related