Tuesday, June 7, 2011

Just scale it...

During my work on the SteelSeries library i faced a lot of different problems when i tried to convert the graphical design from the drawing program into the code. One of the problems was related to inner shadows. In the Java2D api there's no method to create an inner shadow for an object but in design inner shadows are really useful to create some kind of realism.
If you take a closer look to an inner shadow you will figure out easily that the shadow itself is in principle a scaled version of the object that has a color gradient which fades from shades of black to translucent (as you could see in the next images).




To keep it simple i used a linear gradient from black to translucent in the examples above which does not correspond to the reality.


Well if you have shapes like circles or rectangles it's not a big deal to create an inner shadow by shrinking the shape arounds it's center and vary the color from shades of black to translucent (for radial shapes you could also use a java.awt.RadialGradientPaint() to achieve the same effect).


This means if you would like to create an effect like an inner shadow it's all about scaling. For standard shapes like ellipses and rectangles it's not a big deal but the real problem will become visible when it comes to polygons because you can't shrink every polygon around it's center (in this case i mean the center of the boundary box) as i'll show you later. 


Center of gravity
You might know the center of gravity (aka Centroid) from 3-dimensional objects but in fact you could also calculate the center of gravity for an area. But why do we need to calculate this point ?
The answer is easy, because you could scale a polygon around it's centroid which means you could create an inner shadow with the same approach that works for circles and rectangles too. To calculate the area of a polygon and it's centroid you could find lot's of information on the web. After i did some math to understand how it works i searched the web for existing stuff to not reinvent the wheel again i found this page which explains the principle and will give you some sourcecode too.
Now that i had something to start with i created a class that takes a object of the type java.awt.Shape, iterates along the path, calculates the area of the polygon and centroid of the shape. By calculating the area of the polygon you have to keep in mind that negative areas (like "holes" in the polygon) have to be taken into account with a negative sign.
So the next step was the creation of a method that scales the given shape by a given factor. To achieve this i calculate the distance of each point in the polygon and multiply it with the scaling factor. With the usage of some trigonometry i  calculated the new scaled position of each point and created a new shape of the type java.awt.geom.GeneralPath from all the calculated points. 
After some tests i tried to optimize the methods and figured out that i could use the inbuild java.awt.geom.AffineTransform to achieve the scaling of the shape too.
That means i really only have to calculate the area of the polygon and it's centroid and use the centroid do translate the shape that i scaled with AffineTransform.createTransformedShape(java.awt.Shape shape).
So now i have a little helper class that will take a shape and a scaling factor as it's argument and that'll return a scaled version of the given shape.




ATTENTION:
The placement of the scaling does only work for convex polyons !!!




Convex polygon:
A convex polygon is a polygon where every internal angle is less than or equal to 180°. In addition to this every line segment between two vertices remains inside or on the boundary of the polygon.


Concave polygon:
In contrast a concave polygon will always have an interior angle that is greater than 180° (e.g. a L-shaped polygon).


To show you the difference between those two types of polygons i prepared two images. On both images the upper polygon was scaled around it's centroid (visualized by a dot) and the lower polygon was scaled around the center of it's boundary.


Scaling of a convex polygon:




Scaling of a concave polygon:




The positioning of the scaled shape is not correct for the concave L-shaped polygon as you can see in the images above. This means if you have to handle this kind of polygons you still have to do some math to calculate and translate it scaled polygon to the right position.


For those of you who would like to read the code...here you go...


public enum Scaler
{
    INSTANCE;
    
    /**
     * Returns a double that represents the area of the given point array of a polygon
     * @param POLYGON
     * @param N
     * @return a double that represents the area of the given point array of a polygon
     */
    private double calcSignedPolygonArea(final Point2D[] POLYGON)
    {        
        final int N = POLYGON.length;
        int i;
        int j;
        double area = 0;

        for (i = 0; i < N; i++)
        {
            j = (i + 1) % N;
            area += POLYGON[i].getX() * POLYGON[j].getY();
            area -= POLYGON[i].getY() * POLYGON[j].getX();
        }
        area /= 2.0;

        return (area);
        //return(area < 0 ? -area : area); for unsigned
    }
    
    /**
     * Returns a Point2D object that represents the center of mass of the given point array which represents a
     * polygon.
     * @param POLYGON
     * @return a Point2D object that represents the center of mass of the given point array
     */
    public Point2D calcCenterOfMass(final Point2D[] POLYGON)
    {
        final int N = POLYGON.length; 
        double cx = 0;
        double cy = 0;        
        double area = calcSignedPolygonArea(POLYGON);
        final Point2D CENTROID = new Point2D.Double();
        int i; 
        int j;

        double factor = 0;
        for (i = 0; i < N; i++)
        {
            j = (i + 1) % N;
            factor = (POLYGON[i].getX() * POLYGON[j].getY() - POLYGON[j].getX() * POLYGON[i].getY());
            cx += (POLYGON[i].getX() + POLYGON[j].getX()) * factor;
            cy += (POLYGON[i].getY() + POLYGON[j].getY()) * factor;
        }
        area *= 6.0f;
        factor = 1 / area;
        cx *= factor;
        cy *= factor;

        CENTROID.setLocation(cx, cy);
        return CENTROID;
    }

    /**
     * Returns a Point2D object that represents the center of mass of the given shape.
     * @param SHAPE
     * @return a Point2D object that represents the center of mass of the given shape
     */
    public Point2D getCentroid(final Shape SHAPE)
    {        
        ArrayList pointList = new ArrayList(32);
        final PathIterator PATH_ITERATOR = SHAPE.getPathIterator(null);
        int lastMoveToIndex = -1;
        while(!PATH_ITERATOR.isDone())
        {
            final double[] COORDINATES = new double[6];            
            switch (PATH_ITERATOR.currentSegment(COORDINATES))
            {               
                case PathIterator.SEG_MOVETO:
                    pointList.add(new Point2D.Double(COORDINATES[0], COORDINATES[1]));
                    lastMoveToIndex++;
                    break;
                case PathIterator.SEG_LINETO:
                    pointList.add(new Point2D.Double(COORDINATES[0], COORDINATES[1]));
                    break;
                case PathIterator.SEG_QUADTO:
                    pointList.add(new Point2D.Double(COORDINATES[0], COORDINATES[1]));
                    pointList.add(new Point2D.Double(COORDINATES[2], COORDINATES[3]));
                    break;    
                case PathIterator.SEG_CUBICTO:
                    pointList.add(new Point2D.Double(COORDINATES[0], COORDINATES[1]));
                    pointList.add(new Point2D.Double(COORDINATES[2], COORDINATES[3]));
                    pointList.add(new Point2D.Double(COORDINATES[4], COORDINATES[5]));
                    break;
                case PathIterator.SEG_CLOSE:
                    if (lastMoveToIndex >= 0)
                    {
                        pointList.add(pointList.get(lastMoveToIndex));
                    }
                    break;
            }                                   
            PATH_ITERATOR.next();
        }
        final Point2D[] POINT_ARRAY = new Point2D[pointList.size()];
        pointList.toArray(POINT_ARRAY);        
        return (calcCenterOfMass(POINT_ARRAY));
    }
                    
    /**
     * Returns a scaled version of the given shape, calculated by the given scale factor.
     * The scaling will be calculated around the centroid of the shape.
     * @param SHAPE
     * @param SCALE_FACTOR
     * @return a scaled version of the given shape, calculated around the centroid by the given scale factor.
     */
    public Shape scale(final Shape SHAPE, final double SCALE_FACTOR)
    {
        final Point2D CENTROID = getCentroid(SHAPE);
        final AffineTransform TRANSFORM = AffineTransform.getTranslateInstance((1.0 - SCALE_FACTOR) * CENTROID.getX(), (1.0 - SCALE_FACTOR) * CENTROID.getY());
        TRANSFORM.scale(SCALE_FACTOR, SCALE_FACTOR);
        return TRANSFORM.createTransformedShape(SHAPE);
    }
    
    /**
     * Returns a scaled version of the given shape, calculated by the given scale factor.
     * The scaling will be calculated around the given point.
     * @param SHAPE
     * @param SCALE_FACTOR
     * @param SCALE_CENTER
     * @return a scaled version of the given shape, calculated around the given point with the given scale factor.
     */
    public Shape scale(final Shape SHAPE, final double SCALE_FACTOR, final Point2D SCALE_CENTER)
    {        
        final AffineTransform TRANSFORM = AffineTransform.getTranslateInstance((1.0 - SCALE_FACTOR) * SCALE_CENTER.getX(), (1.0 - SCALE_FACTOR) * SCALE_CENTER.getY());
        TRANSFORM.scale(SCALE_FACTOR, SCALE_FACTOR);
        return TRANSFORM.createTransformedShape(SHAPE);
    }
}


And for the others that would prefer download and use it...that's for you...


    Scaler.java


Fortunately i do not have to handle concave polygons in the SteelSeries library and so the scaler class will be part of the SteelSeries library too and in the upcoming 3.9.3 release i'll use it to create some effects for the frames.


That's it for today...so as always...keep coding...





1 comment:

  1. Brilliant, just what I've been looking for. I never noticed that when I scaled my polygons they were also moving position, quite a hard bug to find when you aren't rendering them. This looks like just the thing I need to solve it, thanks!

    ReplyDelete