Let's first define a new number. No worries, it's an easy one.
Or, to put it simply: f = √3 × i, with i being the imaginary unit. With this, a rotation by 60 degrees clockwise is the same as multiplication by 1/2 × (1 - f), and rotation by 60 degrees counter-clockwise the same as multiplication by 1/2 × (1 + f). If this sounds strange, remember that multiplication by a complex number is the same as rotation in the 2D plane. We just "squash" the complex numbers in the imaginary direction a bit (by √3) to not have to deal with square roots ... or non-integers, for that matter.
We can also write the point (a,b) as a + b × f.
This lets us rotate any point in the plane; for example, the point (2,0) = 2 + 0 × f rotates to (1,-1), then to (-1,-1), (-2,0), (-1,1), (1,1) and finally back to (2,0), simply by multiplying it.
Of course, we need a way to translate those points from our coordinates to those we do the rotations in, and then back again. For this, another bit of information is needed: If the point we do the rotation around is to the "left" or to the "right" of the vertical line. For simplicity, we declare that is has a "wobble" value w of 0 if it's to the left of it (like the center of the rotation [0,0] in your bottom two pictures), and of 1 if it's to the right of it. This extends our original points to be three-dimensional; (x, y, w), with "w" being either 0 or 1 after normalisation. The normalisation function is:
NORM: (x, y, w) -> (x + floor(w / 2), y, w mod 2), with the "mod" operation defined such that it only returns positive values or zero.
Our algorithm now looks as follows:
Transform our points (a, b, c) to their positions relative to the rotational centre (x, y, w) by calculating (a - x, b - y, c - w), then normalising the result. This puts the rotational centre at (0,0,0) obviously.
Transform our points from their "native" coordinates to the rotational complex ones: (a, b, c) -> (2 × a + c, b) = 2 × a + c + b × f
Rotate our points by multiplying them with one of the rotational numbers above, as needed.
Ra-transform the points back from the rotational coordinates to their "native" ones: (r, s) -> (floor(r / 2), s, r mod 2), with "mod" defined as above.
Re-transform the points back to their original position by adding them to the rotational centre (x, y, z) and normalising.
A simple version of our "triplex" numbers based f in C++ would look like this:
class hex {
public:
int x;
int y;
int w; /* "wobble"; for any given map, y+w is either odd or
even for ALL hexes of that map */
hex(int x, int y, int w) : x(x), y(y), w(w) {}
/* rest of the implementation */
};
class triplex {
public:
int r; /* real part */
int s; /* f-imaginary part */
triplex(int new_r, int new_s) : r(new_r), s(new_s) {}
triplex(const hex &hexfield)
{
r = hexfield.x * 2 + hexfield.w;
s = hexfield.y;
}
triplex(const triplex &other)
{
this->r = other.r; this->s = other.s;
}
private:
/* C++ has crazy integer division and mod semantics. */
int _div(int a, unsigned int b)
{
int res = a / b;
if( a < 0 && a % b != 0 ) { res -= 1; }
return res;
}
int _mod(int a, unsigned int b)
{
int res = a % b;
if( res < 0 ) { res += a; }
return res;
}
public:
/*
* Self-assignment operator; simple enough
*/
triplex & operator=(const triplex &rhs)
{
this->r = rhs.r; this->s = rhs.s;
return *this;
}
/*
* Multiplication operators - our main workhorse
* Watch out for overflows
*/
triplex & operator*=(const triplex &rhs)
{
/*
* (this->r + this->s * f) * (rhs.r + rhs.s * f)
* = this->r * rhs.r + (this->r * rhs.s + this->s * rhs.r ) * f
* + this->s * rhs.s * f * f
*
* ... remembering that f * f = -3 ...
*
* = (this->r * rhs.r - 3 * this->s * rhs.s)
* + (this->r * rhs.s + this->s * rhs.r) * f
*/
int new_r = this->r * rhs.r - 3 * this->s * rhs.s;
int new_s = this->r * rhs.s + this->s * rhs.r;
this->r = new_r; this->s = new_s;
return *this;
}
const triplex operator*(const triplex &other)
{
return triplex(*this) *= other;
}
/*
* Now for the rotations ...
*/
triplex rotate60CW() /* rotate this by 60 degrees clockwise */
{
/*
* The rotation is the same as multiplikation with (1,-1)
* followed by halving all values (multiplication by (1/2, 0).
* If the values come from transformation from a hex field,
* they will always land back on the hex field; else
* we might lose some information due to the last step.
*/
(*this) *= triplex(1, -1);
this->r /= 2;
this->s /= 2;
}
triplex rotate60CCW() /* Same, counter-clockwise */
{
(*this) *= triplex(1, 1);
this->r /= 2;
this->s /= 2;
}
/*
* Finally, we'd like to get a hex back (actually, I'd
* typically create this as a constructor of the hex class)
*/
operator hex()
{
return hex(_div(this->r, 2), this->s, _mod(this->r, 2));
}
};