ID:2781401
 
(See the best response by Kaiochao.)


Problem description:

I'm trying to update the hand so that the rotation and spread of them is even and looks similar to the picture I'm going to link below this text, the code below is how I'm currently doing what's shown and I've tried out a bajillion different ways to try and get this to work but, my brain is currently lacking in this field of programming and math and would great appreciate any kind of help in solving the issue at hand :D



Code:
mob/player/proc/UpdateHand()
var start = 0 - (1*hand.len) - 22.5,p
for(var/obj/card/c in hand)
c.transform = turn(matrix(),start).Translate(-32,-48)
start += (p * 32) + (hand.len/2) + 16
screen.vis_contents |= c
c.pixel_x = (32 * 10 / 2 - 32) + sin(start) * 64
c.pixel_y = (8) + cos(start) * 32


Best response
Let's split the problem into parts. 1st is the angle of a card. 2nd is the card's position.

1. Say the the first card has the minimum angle (e.g. -10 degrees), the last card has the maximum angle (e.g. 10 degrees), and any card in between should have an angle proportionally in between the minimum and maximum angle. There are many functions to achieve this, but a simple straightforward one would be what's commonly known as "map" or "re-map", where you take a value from one range of values and move it to another range at the same proportion. It takes advantage of a couple other commonly used functions, interpolate (AKA lerp, linear interpolation) and its inverse.
proc/remap(v, a1, b1, a2, b2)
return interpolate(a2, b2,
inverse_interpolate(a1, b1, v))

proc/interpolate(a, b, t)
return a * (1 - t) + b * t

proc/inverse_interpolate(a, b, v)
return (v - a) / (b - a)

So the angle, from minimum_angle to maximum_angle, of the nth card, where the cards are numbered from 1 to hand.len, would be remap(n, 1, hand.len, minimum_angle, maximum_angle).
Warning: when using remap() where a1 = b1, or inverse_interpolate() where a = b, you'll get a division by zero. It's up to you what should happen in this case: when you have only one card in your hand, where should it go?

2. The position of a card appears to resemble the result of rotating by its angle about a common pivot point that is far enough below them. To pivot an object around a point, you can do that with matrix operations in this order: (this also applies the card's rotation)
var/pivot = -100 // experiment with this value
card.transform = matrix(
).Translate(0, -pivot
).Turn(card_angle
).Translate(0, pivot)

There's definitely more tweaking to do, but this might be a start.
In response to Kaiochao
You're awesome thank you very much.



// Credit to Kaiochao

proc/interpolate(a,b,t)
return a * (1 - t) + b * t
proc/inverse_interpolate(a,b,v)
return (v-a) == 0 ? 0 : (v-a) / (b-a)
proc/remap(v,a1,b1,a2,b2)
return interpolate(a2,b2,inverse_interpolate(a1,b1,v))

mob/player/proc/UpdateHand(obj/card/vc)
var pos = 0
var pivot = -500
var angle = (hand.len * 2) + hand.len - 2
if(angle > 13) angle = 13
for(var/obj/card/c in hand)
c.pixel_x = 112
c.pixel_y = vc == c ? 64 : 8
c.layer = vc == c ? 101 : 100
c.transform = matrix(
).Translate(0, -pivot
).Turn(remap(++pos, 1, hand.len, -angle, angle)
).Translate(0, pivot)
screen.vis_contents |= hand

obj/card/Click()
if(!(src in usr:hand)){return}
usr:UpdateHand(src)

The : operator is kinda being abused here though. I'd switch to defining hand and UpdateHand() under the general mob instead of /mob/player so you can use the . operator instead.
Popped animate() into it and it looks so much better animated.

// Credit to Kaiochao

proc/interpolate(a,b,t)
return a * (1 - t) + b * t
proc/inverse_interpolate(a,b,v)
return (v-a) == 0 ? 0 : (v-a) / (b-a)
proc/remap(v,a1,b1,a2,b2)
return interpolate(a2,b2,inverse_interpolate(a1,b1,v))

mob/player/proc/UpdateHand()
var pos = 0
var pivot = -2000 + (hand.len * 100)
for(var/obj/card/c in hand)
animate(c,transform = matrix(
).Translate(0, -pivot
).Turn(remap(++pos, 1, hand.len, -hand.len, hand.len)
).Translate(0, pivot),
pixel_x = 208, pixel_y = 8, layer = 100, time=3, flags=ANIMATION_LINEAR_TRANSFORM)
screen.vis_contents |= hand

obj/card/MouseDrop(mob/m)
var mob/player/p = usr
if(!m || !istype(m,/mob) || !(src in p.hand)){return}
mouse_opacity = 0
p.hand -= src
p.UpdateHand()
animate(src,transform=matrix()*4,alpha=0,time=10)
spawn(10){p.screen.vis_contents -= src}

obj/card/MouseEntered()
var mob/player/p = usr
if(!(src in p.hand)){return}
var f = p.hand.Find(src)+1
if(p.hand.len>=f)
animate(p.hand[f],layer=101,time=1,flags=ANIMATION_LINEAR_TRANSFORM)
animate(src,pixel_y=32,layer=102,time=3,flags=ANIMATION_LINEAR_TRANSFORM)

obj/card/MouseExited()
var mob/player/p = usr
if(!(src in p.hand)){return}
var f = p.hand.Find(src)+1
if(p.hand.len>=f)
animate(p.hand[f],layer=100,time=1,flags=ANIMATION_LINEAR_TRANSFORM)
animate(src,pixel_y=8,layer=100,time=1,flags=ANIMATION_LINEAR_TRANSFORM)
In response to Kaiochao
Kaiochao wrote:
card.transform = matrix().Translate(0, -pivot).Turn(card_angle).Translate(0, pivot)


This is the one thing I can't mentally understand, why exactly does translating it back and forth after turning it work or needed? Is there some kind of visualization I can be shown?
In response to Kozuma3

(edit: axis wasn't actually centered)

When doing matrix operations, you can imagine a global coordinate axis centered on the icon with null transform. This coordinate axis doesn't change between operations.

Translating moves the icon relative to the axis.

Turning rotates the icon about the origin. Rotating the icon when it's not at the origin (for example, it was just translated) means it revolves around the origin in addition to rotating. The order of matrix operations matters, and this is a common example of that.

Moving it back just makes it so that an unrotated icon is also unmoved. It's not totally necessary, since there are many offsets you can apply to icons aside from transform, but it's convenient when the matrix is taken out of context.
In response to Kaiochao
Kaiochao wrote:

That is the best picture ever. Thank you.