sammycage / lunasvg

lunasvg is a standalone SVG rendering library in C++
MIT License
818 stars 115 forks source link

Local transformation and internal SVG element access - Element translation #166

Open rossanoparis opened 3 months ago

rossanoparis commented 3 months ago

Data

This issue refers to: issue 98 Library version: v2.3.9 (master) Testing SVG file: groups.svg.zip (InkScape)

Document Details InkScape SVG Properties
image unit: px

document
W: 163 H: 288
Top left (0, 0)
Bottom rigth (163, 288)
Center (81.5, 144)

g1 (group 1)
W: 163 H: 152
B1 (0, 0)
Bottom rigth (163, 152)
C1 (81.5, 76)

g2 (group 2)
W: 108 H: 111
B2 (50, 178)
Bottom rigth (158, 288)
C2 (104, 232.5)
image

Scope

I want to translate in X the element g1 applying a translation of -81.5px, which is half of document width.

Testing code

// Load document
auto document = Document::loadFromFile(filesvg);
Box dbox(0, 0, document->width(), document->height());

// Retrieve the SVG element - g1
auto elementg1 = document->getElementById("g1");
auto boxg1 = elementg1.getBBox().transformed(elementg1.getAbsoluteTransform());

// Retrieve element transformation matrix - g1
auto transformg1 = elementg1.getLocalTransform();

// Modify the transformation matrix
transformg1.translate(-81.5, 0);

// Apply the modified transformation to the SVG element.
setTransformAttribute(elementg1, transformg1);

// Document update
document->updateLayout();

// Render bitmap
auto bitmap = document->renderToBitmap();

Results

The element g1 disappears from the document; it happens using both methods auto transformg1 = elementg1.getLocalTransform(); or auto transformg1 = elementg1.getAbsoluteTransform();

Expected Obtained
image image
sammycage commented 2 months ago

@rossanoparis When working with SVG elements and applying transformations such as scaling, rotating, skewing, and translating, it's often useful to understand the mathematical principles behind these transformations. Specifically, when you want to apply a transformation around a specific point (transform origin), you need to use a sequence of transformations.

Mathematical Explanation

To transform an SVG element around a specific point, you typically use the following transformation sequence:

  1. Translate to the Origin: Move the transform origin to the coordinate origin.
  2. Apply the Desired Transformation: Perform the scaling, rotation, skewing, or any combination of these transformations.
  3. Translate Back: Move the transform origin back to its original position.

In terms of matrices, this is represented as:

T(transformOriginX, transformOriginY) * additionalTransform T(-transformOriginX, -transformOriginY) originalTransform

Practical Example with SVG

hello.svg

<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
   <g transform="translate(100, 100)">
      <rect id="rect1" fill="red" x="50" y="50" width="100" height="100"/>
      <rect id="rect2" opacity="0.8" fill="green" transform="translate(50, 50)" x="0" y="0" width="100" height="100"/>
   </g>
</svg>

Original

original

Rotate(45)

int main(int argc, char** argv)
{
    std::string filename("/home/sammycage/Projects/hello.svg");
    auto document = Document::loadFromFile(filename);

    // Retrieve the SVG element to animate by its ID
    auto element = document->getElementById("rect2");
    auto originalTransform = element.getLocalTransform();
    auto boundingBox = element.getBBox().transformed(originalTransform);

    auto transformOriginX = boundingBox.x;
    auto transformOriginY = boundingBox.y;

    // Optionally, adjust the transform origin to the center of the bounding box
    // transformOriginX += boundingBox.w / 2.f;
    // transformOriginY += boundingBox.h / 2.f;

    auto additionalTransform = Matrix::translated(transformOriginX, transformOriginY);
    additionalTransform.rotate(45);
    additionalTransform.translate(-transformOriginX, -transformOriginY);

    // Combine the new transformation with the original transformation
    additionalTransform.premultiply(originalTransform);

    // Convert the transformation matrix to a string representation for the SVG "transform" attribute
    std::string additionalTransformString("matrix(");
    additionalTransformString += std::to_string(additionalTransform.a);
    additionalTransformString += ' ';
    additionalTransformString += std::to_string(additionalTransform.b);
    additionalTransformString += ' ';
    additionalTransformString += std::to_string(additionalTransform.c);
    additionalTransformString += ' ';
    additionalTransformString += std::to_string(additionalTransform.d);
    additionalTransformString += ' ';
    additionalTransformString += std::to_string(additionalTransform.e);
    additionalTransformString += ' ';
    additionalTransformString += std::to_string(additionalTransform.f);
    additionalTransformString += ')';

    // Output the transformation matrix string
    std::cout << additionalTransformString << std::endl;

    // Apply the new transformation to the SVG element
    element.setAttribute("transform", additionalTransformString);

    // Update the layout of the document to apply the transformation
    document->updateLayout();

    // Render the updated SVG document to a bitmap
    auto bitmap = document->renderToBitmap();
    if(!bitmap.valid()) return 1;
    bitmap.convertToRGBA();

    // Create a filename for the PNG file based on the transformation type.
    std::string basename("rotate"  ".png");
    stbi_write_png(basename.c_str(), bitmap.width(), bitmap.height(), 4, bitmap.data(), 0);
    std::cout << "Generated PNG file : " << basename << std::endl;

    return 0;
}

rotate

SkewX(45)

...
    auto additionalTransform = Matrix::translated(transformOriginX, transformOriginY);
    additionalTransform.shear(45, 0);
    additionalTransform.translate(-transformOriginX, -transformOriginY);

    // Combine the new transformation with the original transformation
    additionalTransform.premultiply(originalTransform);
...

shear

Translate(50, 50)

...
    auto additionalTransform = Matrix::translated(transformOriginX, transformOriginY);
    additionalTransform.translate(50, 50);
    additionalTransform.translate(-transformOriginX, -transformOriginY);

    // Combine the new transformation with the original transformation
    additionalTransform.premultiply(originalTransform);
...

translate

Translate(50, 50) Rotate(45)

...
    auto additionalTransform = Matrix::translated(transformOriginX, transformOriginY);
    additionalTransform.translate(50, 50);
    additionalTransform.rotate(45);
    additionalTransform.translate(-transformOriginX, -transformOriginY);

    // Combine the new transformation with the original transformation
    additionalTransform.premultiply(originalTransform);
...

translate-rotate

Demo

int main(int argc, char** argv)
{
    std::string filename("/home/sammycage/Projects/hello.svg");
    auto document = Document::loadFromFile(filename);

    // Retrieve the SVG element to animate by its ID
    auto element = document->getElementById("rect2");
    auto originalTransform = element.getLocalTransform();
    auto boundingBox = element.getBBox().transformed(originalTransform);

    auto transformOriginX = boundingBox.x;
    auto transformOriginY = boundingBox.y;

    // Optionally, adjust the transform origin to the center of the bounding box
    // transformOriginX += boundingBox.w / 2.f;
    // transformOriginY += boundingBox.h / 2.f;

    // Loop through angles from 0 to 360 degrees, incrementing by 30 degrees
    for(auto angle = 0; angle <= 360; angle += 30) {
        auto additionalTransform = Matrix::translated(transformOriginX, transformOriginY);
        additionalTransform.rotate(angle);
        additionalTransform.translate(-transformOriginX, -transformOriginY);

        // Combine the new transformation with the original transformation
        additionalTransform.premultiply(originalTransform);

        // Convert the transformation matrix to a string representation for the SVG "transform" attribute
        std::string additionalTransformString("matrix(");
        additionalTransformString += std::to_string(additionalTransform.a);
        additionalTransformString += ' ';
        additionalTransformString += std::to_string(additionalTransform.b);
        additionalTransformString += ' ';
        additionalTransformString += std::to_string(additionalTransform.c);
        additionalTransformString += ' ';
        additionalTransformString += std::to_string(additionalTransform.d);
        additionalTransformString += ' ';
        additionalTransformString += std::to_string(additionalTransform.e);
        additionalTransformString += ' ';
        additionalTransformString += std::to_string(additionalTransform.f);
        additionalTransformString += ')';

        // Output the transformation matrix string
        std::cout << additionalTransformString << std::endl;

        // Apply the new transformation to the SVG element
        element.setAttribute("transform", additionalTransformString);

        // Update the layout of the document to apply the transformation
        document->updateLayout();

        // Render the updated SVG document to a bitmap
        auto bitmap = document->renderToBitmap();
        if(!bitmap.valid()) return 1;
        bitmap.convertToRGBA();

        // Create a filename for the PNG file based on the current angle
        std::string basename("hello-" + std::to_string(angle) + ".png");
        stbi_write_png(basename.c_str(), bitmap.width(), bitmap.height(), 4, bitmap.data(), 0);
        std::cout << "Generated PNG file : " << basename << std::endl;
    }

    return 0;
}

Output

animation

Uncomment the following lines to adjust the transform origin to the center of the bounding box: // transformOriginX += boundingBox.w / 2.f; // transformOriginY += boundingBox.h / 2.f;

Output

animation

Demo 2 (with your SVG file groups.svg)

int main(int argc, char** argv)
{
    std::string filename("/home/sammycage/Projects/groups.svg");
    auto document = Document::loadFromFile(filename);

    // Retrieve the SVG element to animate by its ID
    auto element = document->getElementById("g1");
    auto originalTransform = element.getLocalTransform();
    auto boundingBox = element.getBBox().transformed(originalTransform);

    auto transformOriginX = boundingBox.x;
    auto transformOriginY = boundingBox.y;

    // Adjust the transform origin to the center of the bounding box for better visibility.
    transformOriginX += boundingBox.w / 2.f;
    transformOriginY += boundingBox.h / 2.f;

    // Rest of the code Here

    return 0;
}

Output

animation

rossanoparis commented 2 months ago

Thank you @sammycage for these precious explanation. As soon as I can, I'll apply them to my debugging code ...