Simulate book pages turning reacting to mouse (bezier)

I’m trying to create a simulation of book pages that turn left or right depending on where the mouse is. Kind of as if the mouse is the wind and the pages go in its direction one by one.

This is how it should look like:

I have been working on this for a couple of days already, but am stuck and don’t know how to proceed from this point on.

This is what I have so far: p5.js Web Editor

I tried different approaches, and so far using Bezier curves and angles seems to be the best way. But I can’t think of a good way to make the pages move independently and accordingly to their position so that some are more to one side or the other or higher or lower.

I tried using the indexes as a proportion of the x and y positions. The challenge is that it only works left to right or right to left. I can’t think of way for it to be symmetrical. I tried for example reversing the array depending on which side the mouse is, but couldn’t get it to work.

Any ideas that could point me in the right direction? Or if I should consider a complete different approach altogether?

1 Like

in my opinion you need to move “angle” in the class and calculate it individually for each page (depending on its x-position). Because each page needs to have its individual angle.

I tried but you need to improve the calculation of the angle please

That’s this line in the class:

   this.angle = constrain(map(mouseX, 0, width, -this.xpos, this.xpos), -this.xpos, this.xpos);

  • we need to pretend this.xpos is the middle of the mouse area or the center of the screen or something

Code

let lines = [];
let halfWidth;
let spacing = 20;
let lineAmount = 15;

function setup() {
  createCanvas(800, 400);
  angleMode(DEGREES);
  noFill();

  halfWidth = width * 0.5;

  for (let i = 0; i < lineAmount; i++) {
    lines.push(new Line(i));
  }
}

function draw() {
  background(color("#1d1419"));

  stroke(200, 0, 0);
  line(halfWidth, 0, halfWidth, height);

  for (const line of lines) {
    line.show();
  }

  // console.log(
  //   constrain(
  //     floor(map(abs(halfWidth - mouseX), 0, halfWidth, 0, lineAmount - 1)),
  //     0,
  //     lineAmount - 1
  //   )
  // );
}

class Line {
  constructor(i) {
    this.i = i;
    this.angle = 0;
    this.xpos = 0;
  }

  show() {
    this.xpos = this.i * spacing + (width - (lineAmount * spacing - spacing)) / 2;
    this.angle = constrain(map(mouseX, 0, width, -this.xpos, this.xpos), -this.xpos, this.xpos);

    push();
    translate(
      this.i * spacing + (width - (lineAmount * spacing - spacing)) / 2,
      height
    );

    this.x1 = 0;
    this.y1 = 0;
    this.x1control = this.x1;
    this.y1control = this.y1;

    this.x2 = 300 * cos(this.angle);
    this.y2 = 300 * sin(this.angle);
    this.x2control = this.x2 - 200 * abs(cos(this.angle)) * cos(-this.angle);
    this.y2control = this.y2 - 150 * abs(cos(this.angle)) * sin(-this.angle);

    stroke(color("#f9933e"));
    bezier(
      this.x1,
      this.y1,
      this.x1control,
      this.y1control,
      this.x2control,
      this.y2control,
      this.x2,
      this.y2
    );

    stroke(100);
    line(this.x2, this.y2, this.x2control, this.y2control);

    textSize(12);
    textAlign(CENTER, CENTER);
    fill(200);
    text(this.i, 0, -10);
    pop();
  }
}

2 Likes

Hey, thanks for the suggestion! And I did consider each line to have it’s own angle.

The thing is that the angle needs to be connected to 180 to 360 somehow, which is the part of the circle that the pages should rotate on. So my idea was to use the index of each page to modify the “main angle”.

For example like this:

    this.x2 = 200 * cos(angle) + this.index * 10;
    this.y2 = 200 * sin(angle) - this.index * 10;

You can see the effect in the editor: p5.js Web Editor

I would have to solve the symmetry problem still. Somehow reversing the indexes depending on which side the mouse is.

And to achieve the wind effect I think I might have to use vectors. So I may have to do the angles individually anyway somehow.

I appreciate your reply

Still think individual angle

When the mouse is on the left side, the pages on the legt fall left, the others fall right

1 Like

The thing is they shouldn’t fall below the bottom of the canvas. So no less 180 degrees and no more than 360 degrees. I will see if I find a way to have individual angles while keeping them between the 180-360 degrees.

1 Like

this looks nice

let lines = [];
let halfWidth;
let spacing = 20;
let lineAmount = 15;

function setup() {
  createCanvas(800, 400);
  angleMode(DEGREES);
  noFill();

  halfWidth = width * 0.5;

  for (let i = 0; i < lineAmount; i++) {
    lines.push(new Line(i));
  }
}

function draw() {
  background(color("#1d1419"));

  stroke(200, 0, 0);
  line(halfWidth, 0, halfWidth, height);

  for (const line of lines) {
    line.show();
  }

  // console.log(
  //   constrain(
  //     floor(map(abs(halfWidth - mouseX), 0, halfWidth, 0, lineAmount - 1)),
  //     0,
  //     lineAmount - 1
  //   )
  // );
}

class Line {
  constructor(i) {
    this.i = i;
    this.angle = 0;
    this.xpos = 0;
  }

  show() {
    this.xpos =
      this.i * spacing + (width - (lineAmount * spacing - spacing)) / 2;

    this.myDist = this.xpos - mouseX;

    this.angle = map(this.myDist, -140, 140, 180, 360);
    this.max1 = 185;
    this.max2 = 357;
    if (this.angle < this.max1) this.angle = this.max1;
    if (this.angle > this.max2) this.angle = this.max2;

    push();
    translate(this.xpos, height);

    this.x1 = 0;
    this.y1 = 0;
    this.x1control = this.x1;
    this.y1control = this.y1;

    this.x2 = 300 * cos(this.angle);
    this.y2 = 300 * sin(this.angle);
    this.x2control = this.x2 - 200 * abs(cos(this.angle)) * cos(-this.angle);
    this.y2control = this.y2 - 150 * abs(cos(this.angle)) * sin(-this.angle);

    stroke(color("#f9933e"));
    bezier(
      this.x1,
      this.y1,
      this.x1control,
      this.y1control,
      this.x2control,
      this.y2control,
      this.x2,
      this.y2
    );

    stroke(100);
    line(this.x2, this.y2, this.x2control, this.y2control);

    textSize(12);
    textAlign(CENTER, CENTER);
    fill(200);
    text(this.i, 0, -10);
    text(this.myDist, -2, -24);

    pop();
  }
}

2 Likes

@Chrisir that did look much nicer! Although the pages should move in the direction of the mouse :wink:

I came up with another idea for giving unique angles to each page. Check it out: p5.js Web Editor

It’s pretty much working now how I wanted it: p5.js Web Editor

Now I’m just thinking of ways to optimize it for performance and improve it in general. Any tips/ideas?

1 Like

My experience is that it’s very hard to optimize code to a result that is visible

Make a copy first

  • First thing would be to get rid of push pop and translate.
    You can implement the numbers from translate into the formula you use

  • It might be faster to store mouseX in a variable and use this

  • You can maybe calculate some numbers that are constant in the constructor (as you did in setup())

2 Likes

Thanks for these tips! :slight_smile:

I got rid of push/pop/translate.
Do they affect performance a lot in general? And probably especially the combination of them together?

Why would storing mouseX in a variable be faster?
Would it be something like let mousePositionX = mouseX ?
Wouldn’t that be the same thing?

1 Like

I think so, there is stuff going on behind the screnes

I once had this and it was faster (imho). Maybe because we need to access the OS only once and working with the stored value is faster than accessing the OS?

Yes.

See above.

2 Likes

Hmm, but I don’t get it. The position of the mouse has to continuously be tracked. So if we save it to a variable it still has to keep accessing the OS to get the value of mouseX to then store it in the variable. Sounds like more work to me really. Am I missing something?

Not my strong area of knowledge

It’s just instead of accessing it twice we only access it once

keep in mind that in this theory mouseX is a different kind of variable than mousePositionX

Never mind

When there’s a speed difference it’s probably very small

2 Likes

These are very powerful methods that if used correctly will have minimal affect on performance.

You use them in your first sketch and examining the code you have made excellent use of them and any loss of performance (if any) will be of the order of nanoseconds.

I strongly recommend using the push/translate/rotate/scale/pop methods in this type of situation.

3 Likes

Okay, but when you calculate the positions anyway
you can as well integrate your translation code into the calculation?

1 Like

It’s true that very often such optimizations have minimal effect. I guess you recommend using these methods to make the code more legible? Or is there any other reason besides making the code easier to read for using them?

I can choose to use between this:

  show() {
    push();

    translate(this.origin, height);

    this.x = 400 * cos(this.angle);
    this.y = 350 * sin(this.angle);
    this.x2 = this.origin;
    this.y2 = this.y - (100 + sin(this.angle) * 100);

    bezier(0, 0, 0, 0, this.x2, this.y2, this.x, this.y);

    pop();
  }

Or this:

  show() {
    this.x = this.origin + 400 * cos(this.angle);
    this.y = height + 400 * sin(this.angle);
    this.x2 = this.origin;
    this.y2 = this.y - (100 + sin(this.angle) * 100);

    bezier(
      this.origin,
      height,
      this.origin,
      height,
      this.x2,
      this.y2,
      this.x,
      this.y
    );
  }

I think they are both about as easy to read. Would you still use push/pop/translate? Or here it would make sense to just leave them out?

Hello @slacle,

How can you balance performance and readability in your software code?

In the end you have to maintain your code and may want to share with others so the decision is yours.

:)

I am not sure that I would call the push/translate/rotate/scale/pop methods optimizations as they are an integral part of 2D/3D graphics programming, in fact anywhere you have distinct, hierarchical graphic elements. I use them extensively in many of my libraries e.g. G4P, Shapes3D, canvasGUI. These libraries would have been much harder to develop and maintain otherwise.

2 Likes

Notice that push() is pushMatrix() + pushStyle() in Processing:

p5js only has push(): reference | p5.js
Which stacks lots of information, which might be a tad slow.

4 Likes

I meant NOT using the push/translate/rotate/scale/pop methods to be an optimization :wink:

But anyway, I’m not an “optimization freak”. I won’t sacrifice code legibility for an unnoticeable gain in performance.

In this case, in my opinion, the legibility is about equal whether using or not using those methods. And the performance is probably going to be just about the same in either case. So I can’t see any extra benefit to either using them or not using them, and don’t see any reason to choose one or the other. So I guess in this case it’s just a matter of personal preference then.

1 Like