javascriptp5.jsvertex

How do I change the size of a custom shape using vertexes in p5.js, without changing the placement of my shape?


I am making a game project in JavaScript using p5.js. I made a star shape using the beginShape() function, where vertexes are singular point on the screen that are connected using lines. Currently, I am trying to figure out a way to change the size of my star by manipulating the x and y coordinates of the vertex points without changing the location of my star. Could you please help me figure out how to do this? Thank you in advance:))

Here is the code for my star:

stroke(0);
strokeWeight(3);
fill('#FFD700');
beginShape();
vertex(collectable.x_pos, collectable.y_pos + 297);
vertex(collectable.x_pos + 8, collectable.y_pos + 318);
vertex(collectable.x_pos + 31, collectable.y_pos + 318);
vertex(collectable.x_pos + 15, collectable.y_pos + 331);
vertex(collectable.x_pos + 23, collectable.y_pos + 355);
vertex(collectable.x_pos, collectable.y_pos + 339);
vertex(collectable.x_pos - 23, collectable.y_pos + 355);
vertex(collectable.x_pos - 15, collectable.y_pos + 331);
vertex(collectable.x_pos - 31, collectable.y_pos + 318);
vertex(collectable.x_pos - 8, collectable.y_pos + 318);
vertex(collectable.x_pos, collectable.y_pos + 297);
endShape();

noStroke();
fill(0);
ellipse(collectable.x_pos - 5, collectable.y_pos + 326,5,12);
ellipse(collectable.x_pos + 5, collectable.y_pos + 326,5,13);
fill(255);
ellipse(collectable.x_pos - 5, collectable.y_pos + 323,3,5);
ellipse(collectable.x_pos + 5, collectable.y_pos + 323,3,5);

fill(255,255,255,200);
ellipse(collectable.x_pos, collectable.y_pos + 355,20,10);

I created a 'size' variable set to 1.0, but when I change the size variable, the location of my star changes as well.


Solution

  • Currently all the coordinates used for the start and ellipses are absolute from the top left corner. (Imagine the pivot of your shape being TL corner). When you scale this scale if will scale from the TL corner.

    Ideally you want to draw your custom shape with a pivot at the centre of the shape and with no offset (as if it was centred to the top left corner of the sketch).

    Once you have this shape you can easily offset it's position and it would still be centred. Similarly scaling will be applied from the centre of the shape.

    To illustrate the point here are your drawing commands encapsulated into a function. The sketh then scales drawing and it can also be moved by dragging. Notice that the shape doesn't snap to the cursor as the pivot of the custom shape is not 0,0.

    let collectable = {
        x: 200,
        y: 200
      };
    
    function setup() {
      createCanvas(900, 900);
    }
    
    function drawStarCollectable(x, y, scaleFactor = 1.0){
      // star
      stroke(0);
      strokeWeight(3);
      fill('#FFD700');
      beginShape();
      vertex((x     ) * scaleFactor, (y + 297) * scaleFactor);
      vertex((x +  8) * scaleFactor, (y + 318) * scaleFactor);
      vertex((x + 31) * scaleFactor, (y + 318) * scaleFactor);
      vertex((x + 15) * scaleFactor, (y + 331) * scaleFactor);
      vertex((x + 23) * scaleFactor, (y + 355) * scaleFactor);
      vertex((x     ) * scaleFactor, (y + 339) * scaleFactor);
      vertex((x - 23) * scaleFactor, (y + 355) * scaleFactor);
      vertex((x - 15) * scaleFactor, (y + 331) * scaleFactor);
      vertex((x - 31) * scaleFactor, (y + 318) * scaleFactor);
      vertex((x -  8) * scaleFactor, (y + 318) * scaleFactor);
      vertex((x     ) * scaleFactor, (y + 297) * scaleFactor);
      endShape();
      // eyes
      noStroke();
      fill(0);
      ellipse((x - 5) * scaleFactor, (y + 326) * scaleFactor, 5 * scaleFactor, 12 * scaleFactor);
      ellipse((x + 5) * scaleFactor, (y + 326) * scaleFactor, 5 * scaleFactor, 13 * scaleFactor);
      fill(255);
      ellipse((x - 5) * scaleFactor, (y + 323) * scaleFactor, 3 * scaleFactor, 5 * scaleFactor);
      ellipse((x + 5) * scaleFactor, (y + 323) * scaleFactor, 3 * scaleFactor, 5 * scaleFactor);
      // shadow
      fill(255,255,255,200);
      ellipse(x * scaleFactor, (y + 355) * scaleFactor, 20 * scaleFactor, 10 * scaleFactor);
    }
    
    
    function draw() {
      let startScale = map((frameCount % 100), 0, 100, 0.85, 1.15);
      
      background(234, 10);
      drawStarCollectable(collectable.x, collectable.y, startScale);
      
    }
    
    function mouseDragged(){
      collectable.x = mouseX;
      collectable.y = mouseY;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.3/p5.min.js"></script>

    One important detail here is also using encapsulation. In this case, wrapping the drawing calls in re-usable function that can be customised with parameters.

    p5.js conveniently has a start drawing example. (If you're curious about the trigonometry behind it you can find a lot more details here)

    Let's use p5's (reusable) example start function and encapsulate it into a custom shape drawing function, this time with the pivot at the centre:

    let collectable = {
        x: 200,
        y: 200
      };
    
    function setup() {
      createCanvas(900, 900);
    }
    
    function drawStarCollectable(x, y, scaleFactor = 1.0){
      // star
      stroke(0);
      strokeWeight(3);
      fill('#FFD700');
      beginShape();
      vertex((x     ) * scaleFactor, (y + 297) * scaleFactor);
      vertex((x +  8) * scaleFactor, (y + 318) * scaleFactor);
      vertex((x + 31) * scaleFactor, (y + 318) * scaleFactor);
      vertex((x + 15) * scaleFactor, (y + 331) * scaleFactor);
      vertex((x + 23) * scaleFactor, (y + 355) * scaleFactor);
      vertex((x     ) * scaleFactor, (y + 339) * scaleFactor);
      vertex((x - 23) * scaleFactor, (y + 355) * scaleFactor);
      vertex((x - 15) * scaleFactor, (y + 331) * scaleFactor);
      vertex((x - 31) * scaleFactor, (y + 318) * scaleFactor);
      vertex((x -  8) * scaleFactor, (y + 318) * scaleFactor);
      vertex((x     ) * scaleFactor, (y + 297) * scaleFactor);
      endShape();
      // eyes
      noStroke();
      fill(0);
      ellipse((x - 5) * scaleFactor, (y + 326) * scaleFactor, 5 * scaleFactor, 12 * scaleFactor);
      ellipse((x + 5) * scaleFactor, (y + 326) * scaleFactor, 5 * scaleFactor, 13 * scaleFactor);
      fill(255);
      ellipse((x - 5) * scaleFactor, (y + 323) * scaleFactor, 3 * scaleFactor, 5 * scaleFactor);
      ellipse((x + 5) * scaleFactor, (y + 323) * scaleFactor, 3 * scaleFactor, 5 * scaleFactor);
      // shadow
      fill(255,255,255,200);
      ellipse(x * scaleFactor, (y + 355) * scaleFactor, 20 * scaleFactor, 10 * scaleFactor);
    }
    
    
    function draw() {
      let startScale = map((frameCount % 100), 0, 100, 0.85, 1.15);
      
      background(234, 10);
      drawStarCollectable(collectable.x, collectable.y, startScale);
      
      drawStarCollectableFromCenter(collectable.x, collectable.y, startScale * 45, 5);
      
    }
    
    function mouseDragged(){
      collectable.x = mouseX;
      collectable.y = mouseY;
    }
    
    function drawStarCollectableFromCenter(x, y, radius){
      // star
      stroke(0);
      strokeWeight(3);
      fill('#FFD700');
      star(x, y, radius * 0.45, radius, 5);
      // eyes
      const eyeOffsetX     = radius / 9;
      const eyeBlackWidth  = radius / 9;
      const eyeBlackHeight = radius / 3.75;
      noStroke();
      fill(0);
      ellipse(x - eyeOffsetX, y, eyeBlackWidth, eyeBlackHeight);
      ellipse(x + eyeOffsetX, y, eyeBlackWidth, eyeBlackHeight);
      
      const eyeWhiteWidth  = radius / 15;
      const eyeWhiteHeight = radius / 9;
      const eyeOffsetY     = radius / 15;
      fill(255);
      ellipse(x - eyeOffsetX, y - eyeOffsetY, eyeWhiteWidth, eyeWhiteHeight);
      ellipse(x + eyeOffsetX, y - eyeOffsetY, eyeWhiteWidth, eyeWhiteHeight);
      
      // shadow
      fill(255,255,255,200);
      const shadowWidth   = radius / 2.25;
      const shadowHeight  = radius / 4.5;
      const shadowOffsetY = radius / 1.15;
      ellipse(x, y + shadowOffsetY, shadowWidth, shadowHeight);
    }
    
    function star(x, y, radius1, radius2, npoints) {
      let angle = TWO_PI / npoints;
      let halfAngle = angle / 2.0;
      beginShape();
      for (let a = 0; a < TWO_PI; a += angle) {
        // offset rotation so tip points up
        let angle = a - HALF_PI;
        let sx = x + cos(angle) * radius2;
        let sy = y + sin(angle) * radius2;
        vertex(sx, sy);
        sx = x + cos(angle + halfAngle) * radius1;
        sy = y + sin(angle + halfAngle) * radius1;
        vertex(sx, sy);
      }
      endShape(CLOSE);
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.3/p5.min.js"></script>

    Notice how the start if drawn from 0,0 and the other ellipse are drawn relative to 0,0 and also as a proportion of the star radius. Side by side you can notice how this shape follows the cursor and scales appropriately.

    Additionally there's another p5.js functionality that can be very useful in such situations: using push()/pop() to isolate coordinate system.

    There is a great Processing 2D Transformations tutorial you can check out. Although the syntax is Processing it's almost identical to p5.js.

    In theory you could use two nested coordinate spaces:

    1. the first one to offset the drawing so the pivot at 0,0 centred to the shape
    2. the second one would scale, translate (and as you'll read in the tutorial the order of transformations is imporant).

    The main thing to bare in mind with this approach is this applies purely to drawing/rendering shapes. You won't be able to easily get the transformed positions to do collision detection for example. With the approach above, since you know the x,y and radius of the shape you easily check if based on the distance another circle enclosed shape is intersecting or not.