Scale 9 Images


#1

Would there be an existing solution for creating a bitmap image for which you can define a grid of nine slices? The point being: when the image is stretched, the corners would remain non-distorted. This would be handy for creating buttons of various sizes or for component backgrounds.

Thanks!

image


#2

Well, for buttons or Component backgrounds, I’d think you’d generally just be drawing everything as vectors in paint() rather than messing around with any sort of bitmaps. However, that doesn’t detract from the fact that this is an interesting problem for which I can’t think of an easy solution.

My first somewhat hacky thought is to do exactly what you said - take your bitmap and load it into 9 arranged bitmaps, each containing their slice of the original image. Then you can stretch and pull each slice to distort it as you please.

A handy place to look for distorting JUCE components/bitmaps is the transforms page of the JUCE demo.


#3

Thanks. I was really looking for a way to provide one bitmap and a rectangular region to define its “border” (inner top x,y,width,height). Other GUI/layout frameworks have this, and it’s handy for when a more branded design calls for something more than what vectors provide.


#4

Funny…I was thinking about implementing one of those a while ago. I don’t think Jules is a big fan of bitmaps…


#5

Custom geometric information related to an Image could be managed in the NamedValueSet returned by Image::getProperties().

The “only” missing building block would be to have those properties serialized and loaded back from the PNG file, perhaps as a custom “chunk” in the metadata. That would mean that they “travel” with the image file and could be authored by the GFX designer.

This other thread seems to suggest a small step in that direction: Get access to libjpeg & libpng

It would be nice if @jules or someone else in the ROLI team could consider this for implementation


#6

For a good reason. They don’t scale at all. Unfortunately as @iamgowan pointed out designers throw curveballs that can’t be vectorized…

I’ve been trying to squeeze out a few hours to code this up for fun but I just really don’t have the time right now. :frowning: My approach would be:

  1. Load your image as a single Image
  2. Use Image::getClippedImage to produce several sub-images which represent the 9 pieces of the original image.
  3. Arrange and render the sub-images as tiles in paint(), stretching and squishing the sub-images as needed to put the edges or pieces or whatever where you need them to be.

It’s quite easy - the only sort of hard bit is managing all the tiles and doing all the geometry to stretch/squish the bounds of each tile correctly.


#7

Actually, they scale very well if properly designed and sliced.


#8

If it was my problem, I’d probably write an ImageEffectFilter, that does the 3D effect border… that can be applied to any Image then…

Doing an alpha gradient towards white on top and towards black at the bottom… could look quite nice.


#9

Give this a go…

class Slice9 : public Component
{
public:
    Slice9(){}
    ~Slice9(){}
  
    // the width and height of the slice rectangle are actually the right and bottom boundries, not width & height
    void setImage(Image* theImage, Rectangle<int>theSlice)
    {
        jassert(theSlice.getWidth() < theImage->getWidth());
        jassert(theSlice.getHeight() < theImage->getHeight());
        
        image = theImage;
        slice = theSlice;
    }
    
    void paint(Graphics& g) override
    {
        if(image != nullptr)
        {
            const int dw = getWidth();
            const int dh = getHeight();
            const int iw = image->getWidth();
            const int ih = image->getHeight();
            
            const int sliceLeft = slice.getX();
            const int sliceTop = slice.getY();
            const int sliceRight = slice.getWidth();
            const int sliceBottom = slice.getHeight();
            
            const int sCenterWidth = iw - sliceLeft - (iw - sliceRight);
            const int sCenterHeight = ih - sliceTop - (ih - sliceBottom);
            const int dRightX = dw - (iw - sliceRight);
            const int dBottomY = dh - (ih - sliceBottom);
            const int dCenterHeight = dh - sliceTop - (ih - sliceBottom);
            const int dCenterWidth = dw - sliceLeft - (iw - sliceRight);
            const int bottomHeight = ih - sliceBottom;
            const int rightWidth = iw - sliceRight;
            
            // left column
            g.drawImage(*image, 0, 0, sliceLeft, sliceTop, 0, 0, sliceLeft, sliceTop);
            g.drawImage(*image, 0, sliceTop, sliceLeft, dCenterHeight, 0, sliceTop, sliceLeft, sCenterHeight);
            g.drawImage(*image, 0, dBottomY, sliceLeft, bottomHeight, 0, sliceBottom, sliceLeft, bottomHeight);
            // center column
            g.drawImage(*image, sliceLeft, 0, dCenterWidth, sliceTop, sliceLeft, 0, sCenterWidth, sliceTop);
            g.drawImage(*image, sliceLeft, sliceTop, dCenterWidth, dCenterHeight, sliceLeft, sliceTop, sCenterWidth, sCenterHeight);
            g.drawImage(*image, sliceLeft, dBottomY, dCenterWidth, bottomHeight, sliceLeft, sliceBottom, sCenterWidth, bottomHeight);
            // right column
            g.drawImage(*image, dRightX, 0, rightWidth, sliceTop, sliceRight, 0, rightWidth, sliceTop);
            g.drawImage(*image, dRightX, sliceTop, rightWidth, dCenterHeight, sliceRight, sliceTop, rightWidth, sCenterHeight);
            g.drawImage(*image, dRightX, dBottomY, rightWidth, bottomHeight, sliceRight, sliceBottom, rightWidth, bottomHeight);
        }
    }
    
private:
    Image* image = nullptr;
    Rectangle<int>slice;
};

Here’s the setup:

{
    buttonImage = ImageCache::getFromMemory(BinaryData::button_png, BinaryData::button_pngSize);
    Rectangle<int>buttonSlice(12, 12, 46, 46);
    
    original.setImage(buttonImage);
    original.setBounds(10, 10, 58, 58);
    addAndMakeVisible(original);
    
    tall.setImage(&buttonImage, buttonSlice);
    tall.setBounds(500, 100, 80, 350);
    addAndMakeVisible(tall);
    
    wide.setImage(&buttonImage, buttonSlice);
    wide.setBounds(100, 500, 350, 80);
    addAndMakeVisible(wide);
    
    big.setImage(&buttonImage, buttonSlice);
    big.setBounds(100, 100, 350, 350);
    addAndMakeVisible(big);
}

And here’s a test…the top left image is the original image as an ImageComponent, the other 3 stretched images are the Slice9s.


#10

Hell yeah!!!


#11

Yes! That’s what I’m talkin’ about!