Direct2D Transformations

Transformations in Direct2D allow you to manipulate geometric shapes and images by applying operations such as translation, scaling, rotation, and skewing. These operations are fundamental for creating dynamic and visually rich graphics.

Matrix-Based Transformations

Direct2D uses 3x3 transformation matrices to represent all geometric transformations. A 2D point (x, y) is represented in homogeneous coordinates as (x, y, 1). A transformation matrix M is applied to this point to produce a new point (x', y', w'):


[ x' ]   [ m11 m12 m13 ] [ x ]
[ y' ] = [ m21 m22 m23 ] [ y ]
[ w' ]   [ m31 m32 m33 ] [ 1 ]
            

The transformed 2D point is then (x'/w', y'/w'). In Direct2D, the bottom row of the matrix (m31, m32, m33) is typically (0, 0, 1) for affine transformations, simplifying the calculation to:


x' = m11*x + m12*y + m13
y' = m21*x + m22*y + m23
            

Common Transformation Types

Translation

Translation shifts an object by a specified distance along the x and y axes. The translation matrix is:


[ 1  0  tx ]
[ 0  1  ty ]
[ 0  0  1  ]
            

Where tx is the translation distance along the x-axis and ty is the translation distance along the y-axis.

Scaling

Scaling resizes an object. It can be uniform (same scale factor for both axes) or non-uniform. The scaling matrix is:


[ sx 0  0 ]
[ 0  sy 0 ]
[ 0  0  1 ]
            

Where sx is the scaling factor along the x-axis and sy is the scaling factor along the y-axis.

Rotation

Rotation turns an object around a specified point (usually the origin). The rotation matrix for an angle theta (in radians) is:


[ cos(theta)  -sin(theta)  0 ]
[ sin(theta)   cos(theta)  0 ]
[ 0            0           1 ]
            

Skewing (Shearing)

Skewing distorts an object by shifting points in one direction based on their position in another direction. The skew matrix is:


[ 1  shx 0 ]
[ shy 1  0 ]
[ 0  0   1 ]
            

Where shx determines the horizontal shear and shy determines the vertical shear.

Applying Transformations in Direct2D

Direct2D's ID2D1RenderTarget provides methods to set and manipulate transformations. The most common methods are:

Important: Transformations are cumulative. If you apply a translation and then a rotation, the rotation will be applied relative to the translated origin. Use SetTransform to reset the transformation or carefully chain transformations using MultiplyTransform.

Transformation Order Matters

The order in which transformations are applied significantly affects the final result. For example, rotating an object and then translating it will produce a different outcome than translating it and then rotating it.

Example: Rotating a Rectangle around its Center

To rotate a rectangle around its center, you typically perform these steps:

  1. Translate the origin to the center of the rectangle.
  2. Apply the rotation.
  3. Translate the origin back to its original position (or the opposite of the first translation).

Code Snippet (Conceptual C++)


// Assume:
// - pRT is your ID2D1RenderTarget
// - rect is your D2D1_RECT_F
// - angleDegrees is the rotation angle in degrees

D2D1_RECT_F center = D2D1_RECT_F{(rect.left + rect.right) / 2.0f, (rect.top + rect.bottom) / 2.0f, (rect.left + rect.right) / 2.0f, (rect.top + rect.bottom) / 2.0f};
float angleRadians = DipsToPixels(angleDegrees) * D2D1_PI / 180.0f; // Assuming DipsToPixels is defined

D2D1_MATRIX_3X2_F transformMatrix;

// Create a matrix that translates to the center, rotates, and translates back
transformMatrix = D2D1::Matrix3x2F::Rotation(angleDegrees, center.left, center.top);

// Save the current transform and set the new one
D2D1_MATRIX_3X2_F originalTransform;
pRT->GetTransform(&originalTransform);
pRT->SetTransform(transformMatrix);

// Draw the rectangle (it will be drawn transformed)
pRT->DrawRectangle(rect, pBlackBrush);

// Restore the original transform
pRT->SetTransform(originalTransform);
                

Interactive Transformation Demo

The following is a conceptual visualization. In a real application, you would use Direct2D APIs to render this dynamically.

Visual Representation

Imagine a square being rotated around its center. You can control the rotation angle, scale factor, and translation using sliders or input fields.

1.0

Implementing the Demo (Conceptual JS)

This JavaScript code illustrates how you might handle updates for the interactive demo. In a real Direct2D application, this logic would be in C++ using the Direct2D API.


const canvas = document.getElementById('transformationCanvas');
const ctx = canvas.getContext('2d');
const rotationSlider = document.getElementById('rotationSlider');
const rotationValue = document.getElementById('rotationValue');
const scaleSlider = document.getElementById('scaleSlider');
const scaleValue = document.getElementById('scaleValue');
const translateXInput = document.getElementById('translateX');
const translateYInput = document.getElementById('translateY');

const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const centerX = canvasWidth / 2;
const centerY = canvasHeight / 2;
const squareSize = 50;

function drawSquare(rotation, scaleX, scaleY, translateX, translateY) {
    ctx.clearRect(0, 0, canvasWidth, canvasHeight); // Clear canvas

    // Save the default context state
    ctx.save();

    // Apply transformations
    // 1. Translate to center
    ctx.translate(centerX, centerY);
    // 2. Apply rotation
    ctx.rotate(rotation * Math.PI / 180);
    // 3. Apply scaling
    ctx.scale(scaleX, scaleY);
    // 4. Apply translation
    ctx.translate(translateX, translateY);

    // Set styles for the square
    ctx.fillStyle = 'rgba(209, 52, 60, 0.7)'; // Accent color for square
    ctx.strokeStyle = '#0078d4'; // Primary color for border
    ctx.lineWidth = 2;

    // Calculate square position relative to its new origin (center after translations)
    const halfSize = squareSize / 2;
    ctx.fillRect(-halfSize, -halfSize, squareSize, squareSize);
    ctx.strokeRect(-halfSize, -halfSize, squareSize, squareSize);

    // Restore the context to its state before transformations
    ctx.restore();
}

rotationSlider.addEventListener('input', (e) => {
    const rotation = e.target.value;
    rotationValue.textContent = `${rotation}°`;
    const scale = parseFloat(scaleSlider.value);
    const tx = parseFloat(translateXInput.value);
    const ty = parseFloat(translateYInput.value);
    drawSquare(rotation, scale, scale, tx, ty);
});

scaleSlider.addEventListener('input', (e) => {
    const scale = parseFloat(e.target.value);
    scaleValue.textContent = scale.toFixed(1);
    const rotation = parseFloat(rotationSlider.value);
    const tx = parseFloat(translateXInput.value);
    const ty = parseFloat(translateYInput.value);
    drawSquare(rotation, scale, scale, tx, ty);
});

translateXInput.addEventListener('input', (e) => {
    const tx = parseFloat(e.target.value);
    const rotation = parseFloat(rotationSlider.value);
    const scale = parseFloat(scaleSlider.value);
    const ty = parseFloat(translateYInput.value);
    drawSquare(rotation, scale, scale, tx, ty);
});

translateYInput.addEventListener('input', (e) => {
    const ty = parseFloat(e.target.value);
    const rotation = parseFloat(rotationSlider.value);
    const scale = parseFloat(scaleSlider.value);
    const tx = parseFloat(translateXInput.value);
    drawSquare(rotation, scale, scale, tx, ty);
});

// Initial draw
drawSquare(0, 1, 1, 0, 0);
            

Further Reading