Object Following A Moving Bezier Graph

Hi everybody,

I am trying to make an object follow a curved path (representing a breathing ratio). The object is centred on the x-axis, however, should dynamically adjust its y-position to match/stay on the path, which is moving infinitely from right to left. When moving up/down on the path (inhalation/exhalation phase) the object changes its color and rotation to indicate the current phase. (see images)

What I’ve already tried so far is to use an oscillating sine wave to fake the rolling movement of the graph.
This approach had the advantage that I could easily get the exact y-values of the graph and set the objects y-position to match. Problem here is that since I also want to include a moving background image behind the graph it is hard to make the wave oscillate at the exact same speed as the moving background, so that it looks like they’re both just moving together from right to left. Additionally, I wasn’t able to figure out how to create more complex wave shapes where e.g. the descending part of the curve is twice the length of the ascending. As I want the wave to represent multiple different breathing ratios at the end, this is a must have.

Therefor, I switched to my current approach (see code below). Now, I am creating the graph statically (inside a PImage) using BezierVertex. This has the advantage that both the background and the image of the graph can be moved together. To achieve the y-movement of the object I iterate through the points the graph is being drawn through using bezierPoint.

The issue I’m having here is that there sometimes (you might need to let the sketch run for a little while to see it) is a glitch happening whenever a phase switch at one of the peaks of the graph takes place. I think it has to do with a rounding issue of processing but I’m not entirely sure. Another issue is that whenever I change the moving speed of background and the image of the graph I need to tweak the iteration speed of bezierPoint as well to match (as close as possible) the new speed again.
So, to sum up, I think the solution is far from ideal and probably overly complicated :see_no_evil:

How would you best approach this? I don’t mind starting from 0 again if somebody points me in the direction of a better solution… However, any tips on why the glitch is happening and how I could fix it are also very welcome :slight_smile:

I’ve separated my code into three tabs, you need all of the below for it to work. I tried to simplify it as much as possible. If anything is unclear or missing please let me know.

Main Sketch:

// Declare breathing-ratio images
PImage breathingRatio_1_1;
PImage breathingRatio_1_2;
int imgx1 = 0, imgx2 = 1920, imgx3 = 1920*2; // images positions
int img2x1 = 960, img2x2 = 2880+960, img2x3 = 2880*2+960; // images positions

// Player
float playerPosX, playerPosY;
PVector speed, pos;
float rotationAngle;

color lineColor, playerColor;
color currentPhaseColor;
color guidingPathColor = #BF8D54;

int guidingPathPosX;
float angle = 0;

float bgVelocity = 5; // Bg scroll speed

void setup(){
  fullScreen();
  //size(1920,1080);
  
  // Create breathing-ratio images
  breathingRatio_1_1 = createImageBR_1_1();
  breathingRatio_1_2 = createImageBR_1_2();
  
  // Create array for 'followPath'
  brList = new ArrayList();
}
 
void draw(){
  background(#F2E1CE);
    
  drawBr1_1();
  //drawBr1_2();
  drawPositionIndiators();
  
  createBr1_1();
  //createBr1_2();
  //showBezier();
  displayPlayer1_1();
  //displayPlayer1_2();
}

void drawPositionIndiators() {
  // Draw Line
  noStroke();
  fill(#000000, 40);
  rect(0,0, width/2,height);
  noFill();
  stroke(lineColor);
  strokeWeight(4);
  line(width/2,height,width/2,-height);
}

Code for creation of breathing ratio images:

PImage createImageBR_1_1() {
  float amplitude = 150;
  float xspacing = width/8;
  float bezierAmt = 150;
  
  color pathColor = #BF8D54;
  
  final PGraphics pg = createGraphics(width, height, JAVA2D);
 
  pg.beginDraw();
  pg.smooth();
  pg.background(0, 0);
  
  pg.noFill();
  pg.stroke(pathColor);
  pg.strokeWeight(100);
  pg.pushMatrix();
    pg.translate(0,height/2);
    pg.beginShape();
    pg.vertex(0,amplitude); // first point
    pg.bezierVertex(0+bezierAmt,amplitude, xspacing*2-bezierAmt,-amplitude, xspacing*2,-amplitude);  
    pg.bezierVertex(xspacing*2+bezierAmt,-amplitude, xspacing*4-bezierAmt,amplitude, xspacing*4,amplitude);
    pg.bezierVertex(xspacing*4+bezierAmt,amplitude, xspacing*6-bezierAmt,-amplitude, xspacing*6,-amplitude);
    pg.endShape();
  pg.popMatrix();
  
  pg.pushMatrix();
    pg.translate(width/2,height/2);
    pg.beginShape();
    pg.vertex(0,amplitude); // first point
    pg.bezierVertex(0+bezierAmt,amplitude, xspacing*2-bezierAmt,-amplitude, xspacing*2,-amplitude);  
    pg.bezierVertex(xspacing*2+bezierAmt,-amplitude, xspacing*4-bezierAmt,amplitude, xspacing*4,amplitude);
    pg.bezierVertex(xspacing*4+bezierAmt,amplitude, xspacing*6-bezierAmt,-amplitude, xspacing*6,-amplitude);
    pg.endShape();
  pg.popMatrix();
 
  // Anchor Points
  /*pg.fill(255);
  pg.noStroke();
  pg.ellipse(-width/8, height/2+150, 16, 16);
  pg.ellipse(width/8, height/2-150, 16, 16);
  pg.ellipse(width/8*3, height/2+150, 16, 16);
  pg.ellipse(width/8*5,height/2-150, 16, 16);
  pg.pushMatrix();
    pg.translate(width/2,0);
    pg.ellipse(width/8,height/2-150, 16, 16);
    pg.ellipse(width/8*3,height/2+150, 16, 16);
    pg.ellipse(width/8*5,height/2-150, 16, 16);
    pg.ellipse(width/8*5,height/2+150, 16, 16);
  pg.popMatrix();*/
  
  pg.endDraw();
  return pg.get();
}

PImage createImageBR_1_2() {
  float amplitude = 150;
  float xspacing = width/8;
  float bezierAmt = 150;
  
  color pathColor = #BF8D54;
  
  final PGraphics pg = createGraphics(width+width/2, height, JAVA2D);
 
  pg.beginDraw();
  pg.smooth();
  pg.background(0, 0);
  
  pg.noFill();
  pg.stroke(pathColor);
  pg.strokeWeight(100);
  pg.pushMatrix();
    pg.translate(0,height/2);
    pg.beginShape();
    pg.vertex(0,amplitude); // first point
    pg.bezierVertex(0+bezierAmt,amplitude, xspacing*2-bezierAmt,-amplitude, xspacing*2,-amplitude);  
    pg.bezierVertex(xspacing*2+bezierAmt,-amplitude, xspacing*6-bezierAmt,amplitude, xspacing*6,amplitude);
    pg.bezierVertex(xspacing*6+bezierAmt,amplitude, xspacing*8-bezierAmt,-amplitude, xspacing*8,-amplitude);
    pg.endShape();
  pg.popMatrix();
  
  pg.pushMatrix();
    pg.translate(width/8*6,height/2);
    pg.beginShape();
    pg.vertex(0,amplitude); // first point
    pg.bezierVertex(0+bezierAmt,amplitude, xspacing*2-bezierAmt,-amplitude, xspacing*2,-amplitude);  
    pg.bezierVertex(xspacing*2+bezierAmt,-amplitude, xspacing*6-bezierAmt,amplitude, xspacing*6,amplitude);
    pg.bezierVertex(xspacing*6+bezierAmt,amplitude, xspacing*8-bezierAmt,-amplitude, xspacing*8,-amplitude);
    pg.endShape();
  pg.popMatrix();
  
  pg.endDraw();
  return pg.get();
}

void drawBr1_1(){
  // Draw breathing-ratio images
  image(breathingRatio_1_1, imgx1, 0);
  image(breathingRatio_1_1, imgx2, 0);
  image(breathingRatio_1_1, imgx3, 0);

  // Move breathing-ratio images position 
  imgx1 -= bgVelocity; imgx2 -= bgVelocity; imgx3 -= bgVelocity;
  
  // Reset image position for infinite scroll
  if(imgx1 <= -width){imgx1 = width*2;}
  if(imgx2 <= -width){imgx2 = width*2;}
  if(imgx3 <= -width){imgx3 = width*2;}
}

void drawBr1_2(){
  // Draw breathing-ratio images
  image(breathingRatio_1_2, img2x1, 0);
  image(breathingRatio_1_2, img2x2, 0);
  image(breathingRatio_1_2, img2x3, 0);
  
  // Move breathing-ratio images position 
  img2x1 -= bgVelocity; img2x2 -= bgVelocity; img2x3 -= bgVelocity;
  
  if(img2x1 <= -width-width/2){img2x1 = (width+width/2)*2;}
  if(img2x2 <= -width-width/2){img2x2 = (width+width/2)*2;}
  if(img2x3 <= -width-width/2){img2x3 = (width+width/2)*2;}
}

Code for object to follow y-postions of path:

ArrayList<brPoints> brList = new ArrayList();
float invert;
float y, y2;
float t;

void createBr1_1() {
  float amplitude = 150;
  float xspacing = width/8;
  float bezierAmt = 150;
  
  // Point 1
  brPoints point1 = new brPoints(0,amplitude);
  brList.add(point1);
  
  // Point 2
  brPoints point2_cp1 = new brPoints(0+bezierAmt,amplitude);
  brPoints point2_cp2 = new brPoints(xspacing*2-bezierAmt,-amplitude);
  brPoints point2 = new brPoints(xspacing*2,-amplitude); 
  brList.add(point2_cp1);
  brList.add(point2_cp2);
  brList.add(point2);
  
  // Point 3
  brPoints point3_cp1 = new brPoints(xspacing*2+bezierAmt,-amplitude);
  brPoints point3_cp2 = new brPoints(xspacing*4-bezierAmt,amplitude);
  brPoints point3 = new brPoints(xspacing*4,amplitude); 
  brList.add(point3_cp1);
  brList.add(point3_cp2);
  brList.add(point3);
  
  // Point 4
  brPoints point4_cp1 = new brPoints(xspacing*4+bezierAmt,amplitude);
  brPoints point4_cp2 = new brPoints(xspacing*6-bezierAmt,-amplitude);
  brPoints point4 = new brPoints(xspacing*6,-amplitude); 
  brList.add(point4_cp1);
  brList.add(point4_cp2);
  brList.add(point4);
}

void createBr1_2() {
  float amplitude = 150;
  float xspacing = width/8;
  float bezierAmt = 150;
  
  // Point 1
  brPoints point1 = new brPoints(0,amplitude);
  brList.add(point1);
  
  // Point 2
  brPoints point2_cp1 = new brPoints(0+bezierAmt,amplitude);
  brPoints point2_cp2 = new brPoints(xspacing*2-bezierAmt,-amplitude);
  brPoints point2 = new brPoints(xspacing*2,-amplitude); 
  brList.add(point2_cp1);
  brList.add(point2_cp2);
  brList.add(point2);
  
  // Point 3
  brPoints point3_cp1 = new brPoints(xspacing*2+bezierAmt,-amplitude);
  brPoints point3_cp2 = new brPoints(xspacing*6-bezierAmt,amplitude);
  brPoints point3 = new brPoints(xspacing*6,amplitude); 
  brList.add(point3_cp1);
  brList.add(point3_cp2);
  brList.add(point3);
  
  // Point 4
  brPoints point4_cp1 = new brPoints(xspacing*6+bezierAmt,amplitude);
  brPoints point4_cp2 = new brPoints(xspacing*8-bezierAmt,-amplitude);
  brPoints point4 = new brPoints(xspacing*8,-amplitude); 
  brList.add(point4_cp1);
  brList.add(point4_cp2);
  brList.add(point4);
}

void displayPlayer1_1() {
  //Get points of graph
  int i=0;
  brPoints point1=(brPoints)brList.get(i);
  brPoints point2_cp1=(brPoints)brList.get(i+1);
  brPoints point2_cp2=(brPoints)brList.get(i+2);
  brPoints point2=(brPoints)brList.get(i+3);
  brPoints point3_cp1=(brPoints)brList.get(i+4);
  brPoints point3_cp2=(brPoints)brList.get(i+5);
  brPoints point3=(brPoints)brList.get(i+6);
  
  //Iterate through y-values of points on the graph
  y = bezierPoint(point1.y, point2_cp1.y, point2_cp2.y, point2.y, t/9.5);
  y2 = bezierPoint(point2.y, point3_cp1.y, point3_cp2.y, point3.y, t/9.5);
  
  //Reverse iteration once highest/lowest y-value is reached
  if(t >= 9.5){
    invert = -1;
    y = y2;
    rotationAngle = PI/2;
    playerColor = #ff0000;
    lineColor = #ff0000;
  }
  if (t <= 0) {
    invert = 1;
    y = bezierPoint(point1.y, point2_cp1.y, point2_cp2.y, point2.y, t/9.5);//Set y back to y
    rotationAngle = -PI/2;
    playerColor = #00ff00;
    lineColor = #00ff00;
  }
  
  //Iteration speed
  t += 0.1 * invert;
 
  //Draw player
  pushMatrix();
    translate(width/2, height/2 + y);
    rotate(rotationAngle);
    fill(#000000, 40);
    noStroke();
    triangle(+28,0, -24,-26, -24,+26);
    fill(playerColor);
    triangle(+20,0, -20,-20, -20,+20); 
  popMatrix();
}

void displayPlayer1_2() {
  
  int i=0;
  
  brPoints point1=(brPoints)brList.get(i);
  
  brPoints point2_cp1=(brPoints)brList.get(i+1);
  brPoints point2_cp2=(brPoints)brList.get(i+2);
  brPoints point2=(brPoints)brList.get(i+3);
  
  brPoints point3_cp1=(brPoints)brList.get(i+4);
  brPoints point3_cp2=(brPoints)brList.get(i+5);
  brPoints point3=(brPoints)brList.get(i+6);
  
  y = bezierPoint(point1.y, point2_cp1.y, point2_cp2.y, point2.y, t/9.5);
  y2 = bezierPoint(point2.y, point3_cp1.y, point3_cp2.y, point3.y, t/9.5);
  
  if(t >= 9.5){
    invert = -0.5;
    y = bezierPoint(point2.y, point3_cp1.y, point3_cp2.y, point3.y, t/9.5);
    rotationAngle = PI/2;
    playerColor = #ff0000;
    lineColor = #ff0000;
  }
  
  if (t <= 0) {
    invert = 1;
    y = bezierPoint(point1.y, point2_cp1.y, point2_cp2.y, point2.y, t/9.5);
    rotationAngle = -PI/2;
    playerColor = #00ff00;
    lineColor = #00ff00;
  }
}
  
void showBezier() {
  int i=0; 
  brPoints point1=(brPoints)brList.get(i);
  
  brPoints point2_cp1=(brPoints)brList.get(i+1);
  brPoints point2_cp2=(brPoints)brList.get(i+2);
  brPoints point2=(brPoints)brList.get(i+3);
  
  brPoints point3_cp1=(brPoints)brList.get(i+4);
  brPoints point3_cp2=(brPoints)brList.get(i+5);
  brPoints point3=(brPoints)brList.get(i+6);
  
  brPoints point4_cp1=(brPoints)brList.get(i+7);
  brPoints point4_cp2=(brPoints)brList.get(i+8);
  brPoints point4=(brPoints)brList.get(i+9);

  stroke(255);
  noFill(); 
  pushMatrix();
    beginShape();
      translate(width/2,height/2);
      vertex(point1.x, point1.y);
      bezierVertex(point2_cp1.x, point2_cp1.y, point2_cp2.x, point2_cp2.y, point2.x, point2.y);
      bezierVertex(point3_cp1.x, point3_cp1.y, point3_cp2.x, point3_cp2.y, point3.x, point3.y);
      bezierVertex(point4_cp1.x, point4_cp1.y, point4_cp2.x, point4_cp2.y, point4.x, point4.y);
    endShape();
  popMatrix();
}

class brPoints {

  float x; 
  float y; 

  brPoints(float _x, float _y ) {
    x = _x; 
    y = _y;
  }

  void display() {
    pushMatrix();
      translate(0,height/2);
      stroke(255); 
      strokeWeight(2);
      point(x, y); 
      point(x+1, y+1);
    popMatrix();
  }
}

Am I right in assuming the data is time based i.e. a stream of data from a monitor or something similar?

If so I am confused what is the difference between the data on the left and the data on the right of the screen and what represents the instantaneous / immediate value?

Please describe the real world scenario you are trying to capture?

No the data is not time based, there actually is no incoming data I use at all. The differentiation between the left and the right side of the screen is only made to highlight the past and upcoming part of the graph.

The real world scenario of this is to give breathing instructions for people to look at and align their own breathing to. So this visualisation is supposed to allow you to breathe along the given breathing ratio. The triangle marks the current position in the breathing cycle. Whenever it is green and moves up, you should breathe in and when it is red and moves down you should breathe out.

Thanks I will have another look at it.

I have created a sketch that does what you require it could easily be modified to your specific needs or you can use ideas from it in your own sketch.

float maxAmp;
int midW, midH;

int pastCol = 0xFF8888FF;    // left side colour
int futureCol = 0xFFFFFF88;  // right side colour
int traceCol = 0xFF000088;   // trace line colour

float trace[];
int traceStep; // horizontal spacing in pixels
int maxSteps;  // maximum size of trace list
int midSteps;  // mid element in trace list

void setup() {
  fullScreen();
  // calculate maximum size of trace array and centre element position
  traceStep = 5; 
  maxSteps = 1 + width / traceStep;
  trace = new float[maxSteps];
  midSteps = maxSteps / 2;
  // Calculate pixel positions/values for graphics
  midW = midSteps * traceStep;
  midH = height / 2;
  maxAmp = height / 4;
}

void draw() {
  background(255);
  drawScene();
  // Replace the next line with updateTrace1 for alternative trace
  updateTrace0();
  drawTrace();
  drawMarker();
}

float t = 0;

// Test trace
void updateTrace0() {
  // Move trace to left
  arrayCopy(trace, 1, trace, 0, trace.length - 1);
  // Calculate next value to add to trace
  trace[trace.length - 1] = sin(t);
  // Increment parametric value
  t += 0.3 / TWO_PI;
}

// Alternative test trace
void updateTrace1() {
  arrayCopy(trace, 1, trace, 0, trace.length - 1);
  trace[trace.length - 1] = sin(t) + 0.2 * sin(0.5 * t + 0.25);
  t += 0.3 / TWO_PI;
}

void drawScene() {
  noStroke();
  fill(pastCol);
  rect(0, 0, midW, height);
  fill(futureCol);
  rect(midW, 0, midW, height);
}

void drawTrace() {
  stroke(traceCol);
  strokeWeight(16);
  float prevX, prevY, currX, currY;
  int pos = 0;
  prevX = pos * traceStep;
  prevY = midH + trace[pos] * maxAmp;
  for (pos = 1; pos < trace.length; pos++) {
    currX = pos * traceStep;
    currY = midH + trace[pos] * maxAmp;
    line(prevX, prevY, currX, currY);
    prevX = currX;
    prevY = currY;
  }
}

void drawMarker() {
  float dir = trace[midSteps + 1] - trace[midSteps] >= 0 ? 1 : -1;
  int markerCol = dir == -1 ? 0xFF00CC00 : 0xFFbb0000;
  stroke(markerCol);
  strokeWeight(6);
  line(midW, 0, midW, height);
  stroke(0);
  strokeWeight(3);
  fill(markerCol);
  float currY =  midH + trace[midSteps] * maxAmp;
  pushMatrix();
  translate(midW, currY);
  scale(dir);
  beginShape();
  vertex(-20, -30);
  vertex(20, -30);
  vertex(0, 30);
  endShape(CLOSE);
  popMatrix();
}

Awesome, thanks a lot for the effort!!

I will take an in-depth look at your sketch tomorrow!

One thing I noticed is that you seem to be able to adjust the trace in whichever way you like, correct? Because thats exactly what I struggled with when I tried a similar approach to yours with an oscillating sine wave. I’ve tried playing around with the values in the line below (60), however, can’t really adjust it to my needs. Could you maybe explain how the trace is build up / which value influences what in that formula?

What I need is for the wave to keep the same amplitude, all ascending parts should be equal and all descending parts should be equal. So a breathing ratio of 1:2 (e.g. 1sec inhalation / 2sec exhalation) would look like the following. Would that be possible to create using your method?

I assume you want a smooth curve and the answer is yes but I will need to give it a little more thought…

Yes it needs to be a smooth curve. Great, sounds promising! Sure thing, take your time :slight_smile: I did some research on sine waves and additive waves before, but wasn’t able to figure it out.

This is the best I can do at the moment.

float maxAmp;
int midW, midH;

int pastCol = 0xFF8888FF;    // left side colour
int futureCol = 0xFFFFFF88;  // right side colour
int traceCol = 0xFF000088;   // trace line colour

float trace[];
int traceStep; // horizontal spacing in pixels
int maxSteps;  // maximum size of trace list
int midSteps;  // mid element in trace list

// Proportion of breathing cycle used to inhale
float inhaleF = 0.33;
float peakF = 0.05;

void setup() {
  fullScreen();
  // calculate maximum size of trace array and centre element position
  traceStep = 5; 
  maxSteps = 1 + width / traceStep;
  trace = new float[maxSteps];
  midSteps = maxSteps / 2;
  // Calculate pixel positions/values for graphics
  midW = midSteps * traceStep;
  midH = height / 2;
  maxAmp = height / 4;
}

void draw() {
  background(255);
  drawScene();
  // Replace the next line with updateTrace1 for alternative trace
  updateTrace();
  drawTrace();
  drawMarker();
}

float t = 0, deltaT = 0.005;

void updateTrace() {
  // Move trace to left
  arrayCopy(trace, 1, trace, 0, trace.length - 1);
  float d, y0, y1, y2, y3, tt;
  if (t <= inhaleF) {
    tt = t / inhaleF;
    d = peakF;
    y0 = 1; 
    y1 = 1 - d; 
    y2 = d - 1; 
    y3 = -1;
  } else {
    tt = (t - inhaleF)/(1 - inhaleF);
    d = peakF;
    y0 = -1; 
    y1 = d - 1; 
    y2 = 1 - d; 
    y3 = 1;
  }
  trace[trace.length - 1] = bezierPoint(y0, y1, y2, y3, tt);
  t += deltaT;
  t = t % 1;
}

void drawScene() {
  noStroke();
  fill(pastCol);
  rect(0, 0, midW, height);
  fill(futureCol);
  rect(midW, 0, midW, height);
}

void drawTrace() {
  stroke(traceCol);
  strokeWeight(16);
  float prevX, prevY, currX, currY;
  int pos = 0;
  prevX = pos * traceStep;
  prevY = midH + trace[pos] * maxAmp;
  for (pos = 1; pos < trace.length; pos++) {
    currX = pos * traceStep;
    currY = midH + trace[pos] * maxAmp;
    line(prevX, prevY, currX, currY);
    prevX = currX;
    prevY = currY;
  }
}

void drawMarker() {
  float dir = trace[midSteps + 1] - trace[midSteps] >= 0 ? 1 : -1;
  int markerCol = dir == -1 ? 0xFF00CC00 : 0xFFbb0000;
  stroke(markerCol);
  strokeWeight(6);
  line(midW, 0, midW, height);
  stroke(0);
  strokeWeight(3);
  fill(markerCol);
  float currY =  midH + trace[midSteps] * maxAmp;
  pushMatrix();
  translate(midW, currY);
  scale(dir);
  beginShape();
  vertex(-20, -30);
  vertex(20, -30);
  vertex(0, 30);
  endShape(CLOSE);
  popMatrix();
}
1 Like

That’s perfect, exactly what I needed! Thank you!

Do you know why increasing the strokeWeight of the trace slows its movement down? Is there any way to adjust the “horizontal movement” speed without altering the shape of the trace as well?

Increasing the traceStep speeds things up but also sets the points wider apart, hence altering the shape of the trace

Iterating through the for loop with larger steps e.g: pos+=5 also speeds things up but leads to a glitchy drawing of the trace…

I need a quite large strokeWeight as I want the trace to resemble a path through the woods.

I was so blinded with how to get a smooth curve I forgot that time is important if the breather has to follow a particular pattern. So just before falling asleep last night the solution came to me and I hoped I would remember it in the morning.

So in this sketch you can independently set the inhale time and the exhale time and it will draw a smooth curve for the breather to follow. I have tried it myself (1.5s-4.33s) and it worked.

float maxAmp;
int midW, midH;

int pastCol = 0xFF8888FF;    // left side colour
int futureCol = 0xFFFFFF88;  // right side colour
int traceCol = 0xFF000088;   // trace line colour

float trace[];
int traceStep; // horizontal spacing in pixels
int maxSteps;  // maximum size of trace list
int midSteps;  // mid element in trace list

// ########################################################
// inTime = inhale time in seconds
// exTime = exhale time in seconds
// by modifying these values you can get any regular 
// breathing pattern you like.
// ########################################################
float inTime = 1.5;
float exTime =  4.33;

void setup() {
  fullScreen();
  // calculate maximum size of trace array and centre element position
  traceStep = 2; 
  maxSteps = 1 + width / traceStep;
  trace = new float[maxSteps];
  midSteps = maxSteps / 2;
  // Calculate pixel positions/values for graphics
  midW = midSteps * traceStep;
  midH = height / 2;
  maxAmp = height / 4;
}

void draw() {
  background(255);
  drawScene();
  // Replace the next line with updateTrace1 for alternative trace
  updateTrace();
  drawTrace();
  drawMarker();
}

void updateTrace() {
  // Move trace to left
  arrayCopy(trace, 1, trace, 0, trace.length - 1);
  float time = (0.001 * millis()) % (inTime + exTime);
  float tr = (time <= inTime) 
    ? map(time, 0, inTime, 0, PI) 
    : map(time - inTime, 0, exTime, PI, TWO_PI);
  trace[trace.length - 1] = cos(tr);
}

void drawScene() {
  noStroke();
  fill(pastCol);
  rect(0, 0, midW, height);
  fill(futureCol);
  rect(midW, 0, midW, height);
}

void drawTrace() {
  stroke(traceCol);
  strokeWeight(16);
  float prevX, prevY, currX, currY;
  int pos = 0;
  prevX = pos * traceStep;
  prevY = midH + trace[pos] * maxAmp;
  for (pos = 1; pos < trace.length; pos++) {
    currX = pos * traceStep;
    currY = midH + trace[pos] * maxAmp;
    line(prevX, prevY, currX, currY);
    prevX = currX;
    prevY = currY;
  }
}

void drawMarker() {
  float dir = trace[midSteps + 1] - trace[midSteps] >= 0 ? 1 : -1;
  int markerCol = dir == -1 ? 0xFF00CC00 : 0xFFbb0000;
  stroke(markerCol);
  strokeWeight(6);
  line(midW, 0, midW, height);
  stroke(0);
  strokeWeight(3);
  fill(markerCol);
  float currY =  midH + trace[midSteps] * maxAmp;
  pushMatrix();
  translate(midW, currY);
  scale(dir);
  beginShape();
  vertex(-20, -30);
  vertex(20, -30);
  vertex(0, 30);
  endShape(CLOSE);
  popMatrix();
}

Since my last solution uses the internal clock you can be more precise over breathing pattern so is independent of traceStep. Using of cosine function removes the transition glitches.

Works like a charm :slight_smile: Really cool, now I can dynamically adjust the breathing pattern even during the session and always got the correct timing right away.

The bigger stroke weight weirdly still seems to slow things down, do you know why that is the case?

Also I’d love to let everything start from the center, so that there is no straight line on the left side of the screen at the beginning (see image).
Additionally, I’d like to be able to decide whether the trace starts with an inhale (-1) or exhale (1).

I might be able to figure that out on my own but can’t really get my head around it till now. Any ideas on that would be very welcome :slight_smile:

If I set pos = width/2 / traceStep; I’m somewhat getting there, however, I would somehow need to reset pos = 0; once the updated trace points reach the center, so that they continue to move to the left edge of the screen and I do not already loose the history of the trace in the center…

Try adding the statement
hint(ENABLE_OPTIMIZED_STROKE);
immediately after the fullScreen(); stament in setup()

It might not help but worth a tray.

Nope, that unfortunately didn’t change anything - disabling neither (as I just read somewhere that it should be enabled by default).

Starting from the center will be very difficult and I can’t think of a practical way to do it because the length of the trace on the right is now time dependent. (I will have to leave that with you to solve)

Fortunately the other options are easier to implement - see below

float maxAmp;
int midW, midH;

int pastCol = 0xFF8888FF;    // left side colour
int futureCol = 0xFFFFFF88;  // right side colour
int traceCol = 0xFF000088;   // trace line colour

float trace[];
int traceStep; // horizontal spacing in pixels
int maxSteps;  // maximum size of trace list
int midSteps;  // mid element in trace list

// ########################################################
// inTime = inhale time in seconds
// exTime = exhale time in seconds
// by modifying these values you can get any regular 
// breathing pattern you like.
// ########################################################
float inTime = 1.5;
float exTime =  4.33;
float START_WITH_INHALE = inTime / 2;
float START_WITH_EXHALE = inTime + exTime / 2;
int startMillis;

void setup() {
  fullScreen();
  hint(ENABLE_OPTIMIZED_STROKE);
  // calculate maximum size of trace array and centre element position
  traceStep = 2; 
  maxSteps = 1 + width / traceStep;
  trace = new float[maxSteps];
  midSteps = maxSteps / 2;
  // Calculate pixel positions/values for graphics
  midW = midSteps * traceStep;
  midH = height / 2;
  maxAmp = height / 4;
  // This next line must be the last statement
  startMillis = millis() - int(1000 * START_WITH_EXHALE);
}

void draw() {
  background(255);
  drawScene();
  // Replace the next line with updateTrace1 for alternative trace
  updateTrace();
  drawTrace();
  drawMarker();
}

void updateTrace() {
  // Move trace to left
  arrayCopy(trace, 1, trace, 0, trace.length - 1);
  float time = (0.001 * (millis() - startMillis)) % (inTime + exTime);
  float tr = (time <= inTime) 
    ? map(time, 0, inTime, 0, PI) 
    : map(time - inTime, 0, exTime, PI, TWO_PI);
  trace[trace.length - 1] = cos(tr);
}

void drawScene() {
  noStroke();
  fill(pastCol);
  rect(0, 0, midW, height);
  fill(futureCol);
  rect(midW, 0, midW, height);
}

void drawTrace() {
  stroke(traceCol);
  strokeWeight(16);
  float prevX, prevY, currX, currY;
  int pos = 0;
  prevX = pos * traceStep;
  prevY = midH + trace[pos] * maxAmp;
  for (pos = 1; pos < trace.length; pos++) {
    currX = pos * traceStep;
    currY = trace[pos] * maxAmp;
    if (prevY != 0 || currY != 0)
      line(prevX, midH + prevY, currX, midH + currY);
    prevX = currX;
    prevY = currY;
  }
}

void drawMarker() {
  float dir = trace[midSteps + 1] - trace[midSteps] >= 0 ? 1 : -1;
  int markerCol = dir == -1 ? 0xFF00CC00 : 0xFFbb0000;
  stroke(markerCol);
  strokeWeight(6);
  line(midW, 0, midW, height);
  stroke(0);
  strokeWeight(3);
  fill(markerCol);
  float currY =  midH + trace[midSteps] * maxAmp;
  pushMatrix();
  translate(midW, currY);
  scale(dir);
  beginShape();
  vertex(-20, -30);
  vertex(20, -30);
  vertex(0, 30);
  endShape(CLOSE);
  popMatrix();
}

Alright I see, do you think it is easier to let the initial y-position match the y-position at the start of the trace (-maxAmp if we start with exhalation, maxAmp if we start with inhalation)?
That way one could get rid of that bump at the start and would have a straight line leading up to the first ascending/descending part of the trace. I might even prefer that to starting in the center, as it naturally provides for a couple of seconds before the start.

If you run my latest code you will see that I have removed the straight line and bump -
Screenshot 2021-06-11 at 08.42.41

Yeah I’ve seen that :+1: Weird, looks good on your picture, if I run it it is still somehow there at the very left of the screen vertically.

I removed the division by 2 in these statements to start at the very bottom/top of the inhale/exhale phase, therefor I get that bump again -.-

This should fix all the problems except starting mid width :grinning:

float maxAmp;
int midW, midH;

int pastCol = 0xFF8888FF;    // left side colour
int futureCol = 0xFFFFFF88;  // right side colour
int traceCol = 0xFF000088;   // trace line colour

float trace[];
int traceStep; // horizontal spacing in pixels
int maxSteps;  // maximum size of trace list
int midSteps;  // mid element in trace list

// ########################################################
// inTime = inhale time in seconds
// exTime = exhale time in seconds
// by modifying these values you can get any regular 
// breathing pattern you like.
// ########################################################
float inTime = 1.5;
float exTime =  4.33;
float START_WITH_INHALE = inTime;
float START_WITH_EXHALE = inTime + exTime;
int startMillis;
int startIdx;

void setup() {
  fullScreen();
  hint(ENABLE_OPTIMIZED_STROKE);
  // calculate maximum size of trace array and centre element position
  traceStep = 2; 
  maxSteps = 1 + width / traceStep;
  trace = new float[maxSteps];
  startIdx = trace.length;
  midSteps = maxSteps / 2;
  // Calculate pixel positions/values for graphics
  midW = midSteps * traceStep;
  midH = height / 2;
  maxAmp = height / 4;
  // This next line must be the last statement
  startMillis = millis() - int(1000 * START_WITH_EXHALE);
}

void draw() {
  background(255);
  drawScene();
  // Replace the next line with updateTrace1 for alternative trace
  updateTrace();
  drawTrace();
  drawMarker();
}

void updateTrace() {
  // Move trace to left
  arrayCopy(trace, 1, trace, 0, trace.length - 1);
  startIdx--;
  float time = (0.001 * (millis() - startMillis)) % (inTime + exTime);
  float tr = (time <= inTime) 
    ? map(time, 0, inTime, 0, PI) 
    : map(time - inTime, 0, exTime, PI, TWO_PI);
  trace[trace.length - 1] = cos(tr);
}

void drawScene() {
  noStroke();
  fill(pastCol);
  rect(0, 0, midW, height);
  fill(futureCol);
  rect(midW, 0, midW, height);
}

void drawTrace() {
  stroke(traceCol);
  strokeWeight(16);
  float prevX, prevY, currX, currY;
  int pos = max(1, startIdx);
  prevX = pos * traceStep;
  prevY = trace[pos] * maxAmp;
  for (; pos < trace.length; pos++) {
    currX = pos * traceStep;
    currY = trace[pos] * maxAmp;
    if (prevY != 0 || currY != 0)
      line(prevX, midH + prevY, currX, midH + currY);
    prevX = currX;
    prevY = currY;
  }
}

void drawMarker() {
  float dir = trace[midSteps + 1] - trace[midSteps] >= 0 ? 1 : -1;
  int markerCol = dir == -1 ? 0xFF00CC00 : 0xFFbb0000;
  stroke(markerCol);
  strokeWeight(6);
  line(midW, 0, midW, height);
  stroke(0);
  strokeWeight(3);
  fill(markerCol);
  float currY =  midH + trace[midSteps] * maxAmp;
  pushMatrix();
  translate(midW, currY);
  scale(dir);
  beginShape();
  vertex(-20, -30);
  vertex(20, -30);
  vertex(0, 30);
  endShape(CLOSE);
  popMatrix();
}