How do I oscillate the height of individual shapes whilst keeping the total height of their sum constant?

Viewed 48

I am trying to create an effect where the total height of a group of shapes is constant (say 300), whilst each shape within that group has a dynamic, oscillating, height. In one instance, maybe the middle shape is 'taller' whilst the outer shapes are shorter.

This desired effect is similar to if you held a slinky, with each end in one hand fixed at 30cm apart, and then shook it around: the total height remains the same (30cm) but the 'sections' inside the slinky are having their individual heights bounce up and down.

My attempts so far use the sin function to get an oscillating number as an angle value increases. This works for the sections, but I can't figure out how to maintain the constant overall height. See the code snippet below; red (and the tip of the bottom black triangle) should always be touching the bottom of the container.

// Prepare variables for angles, separated by 1
let a1 = 0;
let a2 = 1;
let a3 = 2;
let a4 = 3;
let a5 = 4;

// Prepare shape width
let shapeW = 150;

function setup() {
  createCanvas(300, 300);
  rect(10, 10, 10, 10);
}

function draw() {
  background(240);
  noStroke();

  // Use the sin ratio to 'oscillate' a height value between 0 and 60
  let x1 = map(sin(a1), -1, 1, 0, height / 5);
  let x2 = map(sin(a2), -1, 1, 0, height / 5);
  let x3 = map(sin(a3), -1, 1, 0, height / 5);
  let x4 = map(sin(a4), -1, 1, 0, height / 5);
  let x5 = map(sin(a5), -1, 1, 0, height / 5);

  // Store these in an array so I can loop through
  let listOfValues = [x1, x2, x3, x4, x5];

  // Loop through and draw shapes
  push();
  translate((width / 2) - shapeW / 2, 0)
  for (let i = 0; i < listOfValues.length; i++) {
    fill(255, 0, 0);
    rect(0, 0, shapeW, listOfValues[i]);
    fill(0)
    triangle(0, 0, shapeW / 2, listOfValues[i], shapeW, 0)
    translate(0, listOfValues[i]);
  }
  pop();

  // Increment each angle by the same amount
  let incAmount = 0.1;
  a1 += incAmount;
  a2 += incAmount;
  a3 += incAmount;
  a4 += incAmount;
  a5 += incAmount;
}
html,
body {
  margin: 0;
  padding: 0;
}

canvas {
  display: block;
}
<script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.js"></script>

2 Answers

With help, I've found the solution is to use binomial coefficients. That is achieved here via a binomial() function. The only caveat is that the number of 'sections' (represented as n) must be an even number.

let angle = 0;
let N;

let containerW = 300;
let shapeW = 150;

let n = 6;
let speed = 0.0075;

function setup() {
  createCanvas(containerW, containerW);
  N = n * binomial(n, n / 2);
}

function draw() {
  background(240);
  noStroke();

  let listOfVals = [];
  for (let i = 0; i < n; i++) {
    listOfVals.push(x(i + 1));
  }

  push();
  translate(width / 2 - shapeW / 2, 0);
  for (let i = 0; i < listOfVals.length; i++) {
    fill(255, 0, 0);
    rect(0, 0, shapeW, listOfVals[i] * height);
    fill(0);
    triangle(0, 0, shapeW / 2, listOfVals[i] * height, shapeW, 0);
    translate(0, listOfVals[i] * height);
  }
  pop();

  // Increment angle
  angle += speed;
}

function x(k) {
  return (2 ** n * sin(angle + (k * PI) / n) ** n) / N;
}

function binomial(n, k) {
  if (typeof n !== "number" || typeof k !== "number") return false;
  var coeff = 1;
  for (var x = n - k + 1; x <= n; x++) coeff *= x;
  for (x = 1; x <= k; x++) coeff /= x;
  return coeff;
}
html,
body {
  margin: 0;
  padding: 0;
}

canvas {
  display: block;
}
<script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.js"></script>

Nice self answer (+1).

This is more of an idea for a slightly different approach, hopefully with a few simplifications:

// Prepare shape width
let shapeW = 150;
// Prepare shape height
let shapeH;
// total number of shapes
let numShapes = 5;
// Increment each angle offset by the same amount
let incAmount = 0.05;
  
function setup() {
  createCanvas(300, 300);
  rect(10, 10, 10, 10);
  // assign shape height after sketch height has been set
  shapeH = height / 5;
}

function draw() {
  background(240);
  noStroke();

  // Loop through and draw shapes
  push();
  
  // horizontally center shapes
  translate((width - shapeW) / 2, 0);
  // for each shape
  for (let i = 0; i < numShapes; i++) {
    // map the current height to the increment asdasdakrk
    let currentH = map(sin(i + (frameCount * incAmount)), -1, 1, 0, shapeH);
    fill(255, 0, 0);
    rect(0, 0, shapeW, currentH);
    fill(0)
    triangle(0, 0, shapeW / 2, currentH, shapeW, 0)
    translate(0, currentH);
  }
  pop();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

The above is using the same logic, mostly removing the need for the a1, a2, a3, a4, a5 values as they coincide with the i counter for each shape.

A visual way I think about it having to connect the tip of one triangle with the base of the next triangle (or the current triangle's base being the same as the the previous triangle tip's y position):

// Prepare shape width
let shapeW = 150;
// Prepare shape height
let shapeH;
// total number of shapes
let numShapes = 5;
// Increment each angle offset by the same amount
let incAmount = 0.05;
// sine driven scales
let minYScale = 0.5;
let maxYScale = 2.0;

function setup() {
  createCanvas(300, 300);
  rect(10, 10, 10, 10);
  // assign shape height after sketch height has been set
  shapeH = height / 5;
}

function draw() {
  background(240);
  noStroke();

  // Loop through and draw shapes
  push();
  
  // horizontally center shapes
  translate((width - shapeW) / 2, 0);
  // draw red background
  fill(255, 0, 0);
  rect(0, 0, shapeW, height);
  
  // remember where the previous array base was
  let lastY = 0;
  // for each shape
  for (let i = 0; i < numShapes; i++) {
    // map the current y scale to the increment
    let currentYScale = map(sin(i + (frameCount * incAmount)), -1, 1, minYScale, maxYScale);
    // compute the current scale based on the sine scalar
    let currentH = currentYScale * shapeH;
    
    fill(0);
    triangle(0, lastY, 
             shapeW / 2, lastY + currentH, 
             shapeW, lastY);
    // update absolute y position of the arrow base
    lastY += currentH;
    // optional: for debugging only, visualise lastY
    if(mouseIsPressed) rect(-shapeW, lastY, width + shapeW, 3);
  }
  pop();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

Related