Math behind drawing one game object relative to another?

Let's say I have a square (a) and I want to draw another square (b) that stays on one corner of the square (a).

Square (a) can rotate and move around the game world, what math would be used to keep square (b) on the same relative point on square (a) ?

My current thought process would be:

You give Square b an offset from square a, which would be a distance and an angle.

Then using trigonometry you could update the location of square b to stay in the correct location.

The way this is usually handled in games is something called a transformation hierarchy.

Here objects are arranged in a parent-child relationship, where each object's position, rotation, etc is specified in a local coordinate system relative to its parent's transformation. So to arrange square B so that it stays attached to the corner of square A, you'd set B as a child of A - then B will follow it wherever it goes, by inheriting the motion of A.

To keep the math consistent, these transformations are usually converted to matrices. Then computing the resulting "local to world" transformation (ie from B's local coordinates to A's coordinates to your stage / scene / canvas / world) is just a matter of multiplying each matrix up the parent chain.

For a typical 2D case, an object with local scale factors of $(S_x, S_y)$, a rotation of $\theta$ radians, and a translation of $(T_x, T_y)$ units relative to its parent could be represented by a local transform matrix like this:

$$M_{toParent} = \begin{bmatrix} S_x \cdot cos(\theta) & -S_y \cdot sin(\theta) & T_x \\ S_x \cdot sin(\theta) & S_y \cdot cos(\theta) & T_y \\ 0 & 0 & 1 \end{bmatrix}$$

Then you can transform any point $(P_x, P_y)$ from the original object into its parent's coordinate system by multiplying it by this matrix (with an extra 1 added as a "z" coordinate to enable translations)

$$P_{parentSpace} = M_{toParent} \cdot \begin{pmatrix}P_x \\ P_y \\ 1\end{pmatrix}$$

You can compute a single matrix representing the entire transformation up the parent chain by multiplying those matrices together:

this.M_toWorld = this.M_toParent
currentTransform = this.parent
while(currentTransform != null)
{
M_toWorld = currentTransform.M_toParent * this.M_toWorld;
currentTransform = currentTransform.parent;
}

Or, you can do it recursively:

if(this.parent == null)
this.M_toWorld = this.M_toParent
else
this.M_toWorld = this.parent.M_toWorld * this.M_toParent

Now instead of walking up the whole chain anytime you want to transform a point, you can use this M_toWorld matrix as a shortcut:

P_worldSpace = M_toWorld * P

Any time an object moves, you need to update its transform and cascade these changes down its tree/DAG of child objects, ensuring that they all move together.