top of page
Search
cedarcantab

Danmaku using Phaser 3 (lesson 30 part 2): DRAW revisited - Heart, Spade, Club and Diamond

In the previous post I utilised Phaser's Curves.Path object to draw a star, which consisted of straight lines connecting 5 vertices. In fact, Phaser's Curves.Path object can handle a lot more - specifically, it can handle...as the name suggests, curves. We will use this feature to draw playing card pictures; namely heart, spade, club and diamond.


The members (?) or methods we will use to draw such more complex shapes are: Ellipses, Quadratic Bezier curves, and Cubic Bezier curves. If you want to understand Bezier curves in detail, there are plenty of excellent tutorials on the internet - for our purposes, all we need to know is that you can create curves that bend in the manner you want by setting control points. You can probably figure out the concepts by looking at the code for creating a Curves.Path object for Heart, below (ignore the rotate(angle) and add(centre) for ease of understanding).

function createHeart(heading, offset){
  const angle = heading - Math.PI/2;
  const nominalSize = 100;
  const height = nominalSize;
  const width = nominalSize;
  const topCurveHeight = nominalSize * 0.3;  
  
  const centre = new Phaser.Math.Vector2(offset.x, offset.y).add(new Phaser.Math.Vector2(0,-nominalSize/2).rotate(angle));
  const start = new Phaser.Math.Vector2(0,  topCurveHeight).rotate(angle).add(centre);
  
  const heart = new Phaser.Curves.Path(start.x,start.y);
 
  // top left curve
  heart.cubicBezierTo(
    new Phaser.Math.Vector2(0,0).rotate(angle).add(centre),
    new Phaser.Math.Vector2(-width/2,0).rotate(angle).add(centre),
    new Phaser.Math.Vector2(-width / 2, topCurveHeight ).rotate(angle).add(centre)
  );
  // bottom left curve             
  heart.cubicBezierTo(
    new Phaser.Math.Vector2(- width / 2,  (height + topCurveHeight) / 2).rotate(angle).add(centre),
    new Phaser.Math.Vector2(0, (height + topCurveHeight) / 2).rotate(angle).add(centre),
    new Phaser.Math.Vector2( 0, height).rotate(angle).add(centre)
  );
  // bottom right curve    
  heart.cubicBezierTo(
    new Phaser.Math.Vector2(0, (height + topCurveHeight) / 2).rotate(angle).add(centre),
    new Phaser.Math.Vector2(width / 2, (height + topCurveHeight) / 2).rotate(angle).add(centre),
    new Phaser.Math.Vector2(width / 2, topCurveHeight ).rotate(angle).add(centre)
  );
  // top right curve
  heart.cubicBezierTo(
    new Phaser.Math.Vector2(width / 2, 0).rotate(angle).add(centre) ,
    new Phaser.Math.Vector2(0,0).rotate(angle).add(centre),
    new Phaser.Math.Vector2(0, topCurveHeight ).rotate(angle).add(centre)
  );
 
  return heart
} // end of function to create Heart object

In creating the above code, and the code for the other shapes, I have leveraged extensively the code published here which explains how to draw playing card pictures to canvas using Javascript, but using the Phaser equivalent of the canvas API.


For the Club shape, I made changes to the code in terms of the order in which the curves are drawn and draw arcs as opposed to complete circles - the reference code draws overlapping complete circles and fills them in. Also the way the Phaser curves ellipseTo function works appears to be quite a bit different from the standard Javascript canvas arc function.


function createClub(heading, offset){
   const angle = heading - Math.PI/2;
   const nominalSize = 100;
   const height = nominalSize;
   const width = nominalSize;
   const circleRadius = width * 0.25;
   const bottomWidth = width * 0.5;
   const bottomHeight = height * 0.35;
   
   const centre = new Phaser.Math.Vector2(offset.x, offset.y).add(new Phaser.Math.Vector2(0,-nominalSize *0.3).rotate(angle));
   const start = new Phaser.Math.Vector2(0,  circleRadius *2).rotate(angle).add(centre);

   const club = new Phaser.Curves.Path(start.x, start.y)
   // bottom left circle
   club.ellipseTo(circleRadius,circleRadius,0,-90,false,Phaser.Math.RadToDeg(angle));
   // top circle
   club.ellipseTo(circleRadius, circleRadius,180,0, false,Phaser.Math.RadToDeg(angle));
   // bottome right circle
   club.ellipseTo(circleRadius,circleRadius,-90,180,false,Phaser.Math.RadToDeg(angle));
   // bottom part of club
   club.moveTo(new Phaser.Math.Vector2(0,(height * 0.6)).rotate(angle).add(centre));
   club.quadraticBezierTo(
     new Phaser.Math.Vector2(0,  height).rotate(angle).add(centre) , 
     new Phaser.Math.Vector2( - bottomWidth / 2, height).rotate(angle).add(centre)     
   )
   club.lineTo(new Phaser.Math.Vector2(bottomWidth / 2,height).rotate(angle).add(centre));
   club.quadraticBezierTo(
      new Phaser.Math.Vector2(new Phaser.Math.Vector2(0,height).rotate(angle).add(centre)), 
      new Phaser.Math.Vector2(new Phaser.Math.Vector2(0,(height * 0.6)).rotate(angle).add(centre))
   );  
   return club
 }

Similarly for the spade shape, I had to change the code quite a bit because the original code assumes that the shape is filled in.



function createSpade(heading, offset){
  const angle = heading - Math.PI/2;
  const nominalSize = 100;
  const height = nominalSize;
  const width = nominalSize;
  
  var bottomWidth = width * 0.7;
  var topHeight = height * 0.7;
  var bottomHeight = height * 0.3;
  
  const centre = new Phaser.Math.Vector2(offset.x, offset.y).add(new Phaser.Math.Vector2(0,-nominalSize/2).rotate(angle));

  const spade = new Phaser.Curves.Path().moveTo(centre);

  // top left of spade          
  spade.cubicBezierTo(
    new Phaser.Math.Vector2(0, topHeight / 2).rotate(angle).add(centre),// control point 1
    new Phaser.Math.Vector2(- width / 2, topHeight / 2).rotate(angle).add(centre), // control point 2
    new Phaser.Math.Vector2(- width / 2, topHeight).rotate(angle).add(centre)   // end point
  );  
  // bottom left of spade
   spade.cubicBezierTo(
     new Phaser.Math.Vector2( - width / 2, topHeight * 1.3).rotate(angle).add(centre), // control point 1
     new Phaser.Math.Vector2(0, topHeight * 1.3).rotate(angle).add(centre), // control point 2
     new Phaser.Math.Vector2(0,  topHeight).rotate(angle).add(centre)          // end point
   );
                     
  // bottom right of spade  
  spade.cubicBezierTo(
     new Phaser.Math.Vector2(0, topHeight * 1.3).rotate(angle).add(centre), // control point 1
     new Phaser.Math.Vector2(width / 2, topHeight * 1.3).rotate(angle).add(centre), // control point 2
     new Phaser.Math.Vector2( width / 2, topHeight).rotate(angle).add(centre)            // end point      
  );    
  // top right of spade    
  spade.cubicBezierTo(
    new Phaser.Math.Vector2(width / 2, topHeight / 2).rotate(angle).add(centre), // control point 1
    new Phaser.Math.Vector2(0, topHeight / 2).rotate(angle).add(centre), // control point 2
    new Phaser.Math.Vector2(0, 0).rotate(angle).add(centre)             // end point         
  );
                 
  spade.moveTo(new Phaser.Math.Vector2(0, topHeight).rotate(angle).add(centre))
  
  spade.quadraticBezierTo(
    new Phaser.Math.Vector2(0, topHeight + bottomHeight).rotate(angle).add(centre), // control point
    new Phaser.Math.Vector2( - bottomWidth / 2, topHeight + bottomHeight).rotate(angle).add(centre)             // end point   
  );
  
  spade.lineTo(new Phaser.Math.Vector2(bottomWidth / 2, topHeight + bottomHeight).rotate(angle).add(centre));
  spade.quadraticBezierTo(
     new Phaser.Math.Vector2(0, topHeight + bottomHeight).rotate(angle).add(centre), // control point
     new Phaser.Math.Vector2(0, topHeight).rotate(angle).add(centre)            // end point           
  );
 
  return spade
} // end of function to create spade object



Diamond is the simplest of them all. I have put in a width to height ratio of 1.4, since without, it just looks like a square. For all the other shapes, the height and width is set to 1:1.

function createDiamond(heading, offset){
  const angle = heading - Math.PI/2;
  const nominalSize = 100;
  const height = nominalSize;
  const width = nominalSize / 1.4
  
  const centre = new Phaser.Math.Vector2(offset.x, offset.y).add(new Phaser.Math.Vector2(0,-nominalSize/2).rotate(angle));
  const diamond = new Phaser.Curves.Path().moveTo(centre);
  // top left edge
  diamond.lineTo(new Phaser.Math.Vector2(-width/2, height/2).rotate(angle).add(centre));             
  // bottom left edge
  diamond.lineTo(new Phaser.Math.Vector2(0, height).rotate(angle).add(centre));
  // bottom right edge
  diamond.lineTo(new Phaser.Math.Vector2(width/2, height/2).rotate(angle).add(centre));
  // closing the path automatically creates
  // the top right edge
  diamond.closePath();
                
  return diamond     
} // end of function to create diamond object

And that's it for this post. I will not publish the CodePen, because I have one other addition to make to the DRAW danmaku code.



Comments


記事: Blog2_Post
bottom of page