0
\$\begingroup\$

I'm creating a simple program in C# and now i'm trying to slice an image into multiple tiles similar to a grid pattern so it does not get stretched when drawing to different resolutions.

What i'm trying to achieve:

  • I want the image to be sliced into multiple tiles.
  • I need the image to be scaled to the dimension of a specified rectangle.
  • I need the image to be drawn without losing the aspect it had before being scaled.

Below is the code i currently have, at the moment it does not scales the image correctly and it seem that the image is losing its aspect. Also, note that i'm using an Matrix to scale the graphics object to a certain resolution.

public static List<RectangleF> TileRectangle (this RectangleF rectangle, int rows, int columns)
{
    var rectangles = new List<RectangleF>();

    float width  = rectangle.Width / rows;
    float height = rectangle.Width / columns;

    for (int x = 0; x < columns; x++)
    {
        for (int y = 0; y < rows; y++)
        {
            rectangles.Add(new RectangleF(rectangle.X + (width * x), rectangle.Y + (height * y), width, height));
        }
    }

    return rectangles;
}

public void Draw (Graphics graphics)
{
    graphics.MultiplyTransform(ResolutionMatrix());
    graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
    graphics.PixelOffsetMode = PixelOffsetMode.Half;

    var destination_slices = new RectangleF(0, 0, 512, 512).TileRectangle(3, 3);
    var source_slices = new RectangleF(0, 0, this.image.Width, this.image.Height).TileRectangle(3, 3);

    for (int i = 0; i < source_slices.Count; i++)
    {
        graphics.DrawImage(this.image, destination_slices[i], source_slices[i], GraphicsUnit.Pixel);
        Debug.WriteLine(source_slices[i]);
    }
}

At the moment this code is causing the following problems:

  • The image scales correctly only in the horizontal direction.
  • The image does not maintains its aspect when scaled.
  • When changing the columns and rows to different values, the drawing becomes sliced.

The drawing code produces the following output for the "source slices" when sliced in 3 by 3 tiles:

X=0,Y=0,Width=63,333332,Height=16,333334
X=0,Y=16,333334,Width=63,333332,Height=16,333334
X=0,Y=32,666668,Width=63,333332,Height=16,333334
    
X=63,333332, Y=0, Width=63,333332, Height=16,333334
X=63,333332, Y=16,333334, Width=63,333332, Height=16,333334
X=63,333332, Y=32,666668, Width=63,333332, Height=16,333334
    
X=126,666664, Y=0, Width=63,333332, Height=16,333334
X=126,666664, Y=16,333334, Width=63,333332, Height=16,333334
X=126,666664, Y=32,666668, Width=63,333332, Height=16,333334

Here is a image to describe the results i'm expecting:

Image

I believe there might be a simple solution for this, so could someone post a direct answer with the code to accomplish these tasks?

\$\endgroup\$
6
  • 2
    \$\begingroup\$ Can you show us the unwanted symptoms this code produces, and the inputs that cause it? \$\endgroup\$
    – DMGregory
    Sep 15, 2021 at 17:11
  • 2
    \$\begingroup\$ Are you always working with a destination size that is a multiple of the tile size? If not, what is the scaling behaviour you expect? \$\endgroup\$
    – user35344
    Sep 15, 2021 at 17:17
  • \$\begingroup\$ Thanks for the comments, i updated the code with the output of the drawing method and wrote some problems its causing. \$\endgroup\$
    – RickS.
    Sep 15, 2021 at 17:23
  • \$\begingroup\$ I expect the image to not lose its aspect by slicing it in multiple tiles. \$\endgroup\$
    – RickS.
    Sep 15, 2021 at 17:25
  • 1
    \$\begingroup\$ Does that mean that you expect repetition and partial pieces then for the slices? For an example for 10px sized pieces into a 33px sized destination, do you expect all pieces to be rendered at 11px size? Or perhaps corner pieces at 10px, middle one scaled to 13px? Or corners at 10px, middle piece at 10px and a sliced middle piece at 3px to fill the gap? An image of your current and expected results would be helpful. \$\endgroup\$
    – user35344
    Sep 15, 2021 at 17:42

1 Answer 1

1
\$\begingroup\$

I did a brief implementation of a 9 slicer in Pygame, but the same ideas should be easily applicable in any capable rendering framework since it's the ideas here that are the key.

This implementation always preserves the size of the corner pieces, and the height of the top and middle slices, and the width of the left and right slices. Holes are filled by repeating the pieces instead of scaling them, which means that your slices should be drawn in a way where they can be cut at any point. If scaling is desired instead, it'd be of course possible to just render the non-corner pieces with a scaling destination rectangle.

I didn't do much to think about more clever ways to handle the cropping of the pieces, so I ended up writing separate logic for the horizontal, vertical and middle parts.

First, you want to precalculate the source rectangles for the individual slices:

rectangles = (
    pygame.Rect(0, 0, padding, padding),
    pygame.Rect(padding, 0, width - 2 * padding, padding),
    pygame.Rect(width - padding, 0, padding, padding),

    pygame.Rect(0, padding, padding, height - 2 * padding),
    pygame.Rect(padding, padding, width - 2 * padding, height - 2 * padding),
    pygame.Rect(width - padding, padding, padding, height - 2 * padding),

    pygame.Rect(0, height - padding, padding, padding),
    pygame.Rect(padding, height - padding, width - 2 * padding, padding),
    pygame.Rect(width - padding, height - padding, padding, padding),
)

There's nothing special going on here, just hardcoded positions for the slices based on a single padding value, which indicates the size of the corner slices. If your corner slices aren't squares, it's of course easy to adjust this to use separate vertical / horizontal paddings. width and height are the size of the source image.

With the source rectangles, we can now implement a method that takes in a destination rectangle destination. I started by checking that there's at least room to draw the corners; You might want to handle this more gracefully:

dest_middle_width = destination.width - 2 * self.padding
dest_middle_height = destination.height - 2 * self.padding

assert(dest_middle_width >= 0 and dest_middle_height >= 0)

The corners are now trivial: just draw then into their correct places (if you're unfamiliar with Python, blit takes in the source image, destination position and source rectangle called area):

target.blit(self.surface, (destination.left, destination.top), area=self.rectangles[0])
target.blit(self.surface, (destination.right - self.padding, destination.top), area=self.rectangles[2])
target.blit(self.surface, (destination.left, destination.bottom - self.padding), area=self.rectangles[6])
target.blit(self.surface, (destination.right - self.padding, destination.bottom - self.padding), area=self.rectangles[8])

It's also worth noting that here the destination size of the rendered pieces is always the same as the source pieces.

Then we do a pass over the width of the middle piece, and render the top and bottom slices:

middle = self.rectangles[4]

# Top and bottom
for x in range(0, dest_middle_width, middle.width):
    width = min(middle.width, dest_middle_width - x)

    top_area = pygame.Rect(self.rectangles[1])
    top_area.width = width
    target.blit(self.surface, (destination.left + self.padding + x, destination.top), area=top_area)

    bottom_area = pygame.Rect(self.rectangles[7])
    bottom_area.width = width
    target.blit(self.surface, (destination.left + self.padding + x, destination.bottom - self.padding), area=bottom_area)

The key here is the min(middle.width, dest_middle_width - x) part, which makes sure that the last slice we draw to fill the desired middle piece width is clipped properly.

The slices for left and right are handled just as top and bottom, but instead the iteration is over the vertical axis, and of course coordinate calculations adjusted accordingly.

Now all that remains is the middle piece, which is just a combination of iterating over both the vertical and horizontal axes:

# Middle
for x in range(0, dest_middle_width, middle.width):
    width = min(middle.width, dest_middle_width - x)
    for y in range(0, dest_middle_height, middle.height):
        height = min(middle.height, dest_middle_height - y)
        area = pygame.Rect(middle)
        area.width = width
        area.height = height
        target.blit(self.surface, (destination.left + self.padding + x, destination.top + self.padding + y), area=area)

And here's the result using a destination size that is not a multiple of the piece sizes:

Demo image of the 9 slicer in action

\$\endgroup\$
7
  • \$\begingroup\$ Hey sorry for the delay, you code works as i wanted but there is no overload to the Graphics.DrawImage that accepts a single PointF and a source RectangleF, so i had to use a rectangle in place of the point which messed the drawing, what can i do? I'll update my question with your code. Thanks a lot for the answer. \$\endgroup\$
    – RickS.
    Sep 15, 2021 at 19:57
  • 1
    \$\begingroup\$ Like I tried to point out in the answer, the default destination size in Pygame is the size of the source (area) rectangle, so you can use that as your destination size in case you have to explicitly define it. \$\endgroup\$
    – user35344
    Sep 15, 2021 at 20:10
  • 1
    \$\begingroup\$ You don't seem to be passing the source size as the destination size, and instead have hardcoded constants as the size, so what exactly are you trying? \$\endgroup\$
    – user35344
    Sep 15, 2021 at 20:33
  • 1
    \$\begingroup\$ Assert goes after calculating the sizes in the assertion, if that's the behaviour you want for invalid rectangles. The source rectangles are the ones passed as the area param to blit, as they determine what area of the source image gets drawn. \$\endgroup\$
    – user35344
    Sep 15, 2021 at 20:51
  • 1
    \$\begingroup\$ Let us continue this discussion in chat. \$\endgroup\$
    – user35344
    Sep 15, 2021 at 20:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .