How to calculate angles and X,Y coordinates for drawing a hand of playing cards on a canvas

2.5k Views Asked by At

Scenario: I'm programming a module to draw a deck of cards on a canvas as though they were being held by a person.

Edit:

I've cleaned up the question as best I can to be clearer.

What I'm looking for now:

The X,Y coords of each star, which represent the center of each card, as well as its rotation from that point to form a semi-circle.

To make it simpler than before, you can now also assume:

  1. The total angle the cards cover is 90º.
  2. There are 5 cards.
  3. The middle card will always be at X = 0
  4. Each end card will be on a rotation of 30º (the left side will be -30º)
  5. The canvas is a fixed size. Note: I put 200 as the height, though this is somewhat of an arbitrary number and I don't even know if it helps or not.

I should also mention that drawing is not exactly to scale, I did the best I can with a somewhat primitive tool for drawing geometric shapes


Solution:

The source for my solution can be found here: https://github.com/tcrosen/playing-cards

A working demo can be found here: http://tcrosen.github.com/playing-cards/demo.html

function drawHand(el, originX, originY, numberOfCards, cardWidth, cardHeight, showOrigin) {

    // The angle off the origin that each card will be referenced from
    // The +1 is added because we want the first card to be positioned above the origin
    var angle = 180 / (numberOfCards + 1);

    // How far each card will be from the origin of the hand.
    // This is proportional to the size of the card so that larger cards avoid too much overlap
    var radius = cardWidth * 1.2;

    // Through trial & error I determined a small hand (3-5 cards) looks most realistic 
    // when the end cards are at a rotation of 30 degrees (90 - 5 * 12). However when larger hands are created
    // the end cards must be rotated at a larger angle in order to be "held" properly.  Anything that would
    // calculate to an angle > 30 (6 cards or more) is simply capped at 45 degrees.
    var endRotation = 12 * numberOfCards > 60 ? 45 : 90 - 12 * numberOfCards;

    // Find an equal angle to split the cards across the entire hand
    var rotationIncrement = endRotation * 2 / (numberOfCards + 1);

    // If the user wants to see the origin for debugging/design purposes, show an X there
    if (showOrigin) {
        $(el).append($('<span>X</span>').css('color', 'red').css('position', 'absolute').css('top', originY + 'px').css('left', originX + 'px'));
    }

    // Loop through each card
    // *Note: I start at 1 (instead of 0) in order to avoid multiplying by 0 and ending up with flat angles. 
    //  If you are using an array of cards (eventual scenario) you would need to account for the 0 index 
    for (var i = 1; i <= numberOfCards; i++) {

        //  Set the card rotation - always rotate from the end point so use the card number as a multiplier
        var rotation = endRotation - rotationIncrement * i;

        // The X,Y coordinates of each card.
        // Note that the origin X,Y is added to each coordinate as a hand would almost never be generated from 0,0 
        // on an HTML canvas.
        var x = radius * Math.cos(toRadians(angle * i)) + originX;
        var y = radius * Math.sin(toRadians(-angle * i)) + originY;

        // This next algorithm is used to push the cards "up" by a larger amount the further you get from the middle of the hand.  
        // This is done because a higher number of cards will start to push down and form a sharper circle. 
        // By moving them up it evens out the semi-circle to appear like something that would be more realistically held by a human.    
        // And as usual, this value is affected by existing variables to always position the hand based on its previous metrics.        
        y = y - Math.abs(Math.round(numberOfCards / 2) - i) * rotationIncrement;

        // HTML positions elements relative to the top left corner, but the CSS3 "rotation" property uses the center.  
        // So I cut the values in half and use the center to position the cards.
        // *Note: I realize both this and the previous line could have been included in the first X,Y calculation. They are separated for clarity.
        x = x - cardWidth / 2;
        y = y - cardHeight / 2;     

        // Create the card using my jQuery plugin
        var $card = $('<div></div>').card({
            width: cardWidth,
            text: i,
            rotation: rotation,
            top: y,
            left: x
        });     

        // Draw it in the parent element
        $(el).append($card);
    }
}

// Helper function to convert to radians from degrees since Javascript's Math library defaults to radians.
function toRadians(degrees) {
    return degrees * Math.PI / 180;
}

Here are some screenshots of the results in action with different values:

enter image description here enter image description here enter image description here enter image description here

2

There are 2 best solutions below

2
On BEST ANSWER

Relative to the center of the circle, the cards are at $(-30^{\circ},-15^{\circ},0^{\circ},15^{\circ},30^{\circ})$. If we take the center as (0,0), the coordinates are then $(r \sin -30^{\circ},-r \cos -30^{\circ}), (r \sin -15^{\circ},-r \cos -15^{\circ})$ and so on. Then you just need a calculator or sine and cosine tables to get $$\begin {array} {ccc} \\ angle & sine & cosine \\-30 & -0.5 & 0.866 \\-15 &-0.259 & 0.966 \\ 0 & 0 & 1 \\15 & 0.259 & 0.966\\30 & 0.5 & 0.866\end {array}$$ If your center is not $(0,0)$, just add it to these values. If you have a different number of cards, you can just equally space the angles from $-30$ to $+30$ degrees

2
On

If I have understood your question properly, you should just be able to apply a simple planar rotation, ie: $$\begin{bmatrix} Cos(\theta) &-Sin(\theta)) \\ Sin (\theta)&Cos(\theta)) \end{bmatrix} *\begin{bmatrix} x\\y \end{bmatrix}$$ where * denotes matrix multiplication, and $\theta$ is the angle you want to rotate by. This will rotate the point (x,y) along a circle centered at the origin.