Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c84731ae authored by Matthew Bouyack's avatar Matthew Bouyack
Browse files

DO NOT MERGE Improved anti-aliasing for circular display mask

Calculate the alpha value for each pixel in the display mask based on
the area of overlap between the circle and that pixel.

This works around a bug in Skia that is fixed in master.

Change-Id: Ieb8df0d6f46f07cff1dbc368d74938ec699e08cf
parent e04c7f15
Loading
Loading
Loading
Loading
+139 −5
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SURFACE_TRACE
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
@@ -85,12 +86,115 @@ class CircularDisplayMask {
        mSurfaceControl = ctrl;
        mDrawNeeded = true;
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        mScreenOffset = screenOffset;
        mMaskThickness = maskThickness;
    }

    static private double distanceFromCenterSquared(double x, double y) {
        return x*x + y*y;
    }

    static private double distanceFromCenter(double x, double y) {
        return Math.sqrt(distanceFromCenterSquared(x, y));
    }

    static private double verticalLineIntersectsCircle(double x, double radius) {
        return Math.sqrt(radius*radius - x*x);
    }

    static private double  horizontalLineIntersectsCircle(double y, double radius) {
        return Math.sqrt(radius*radius - y*y);
    }

    static private double triangleArea(double width, double height) {
        return width * height / 2.0;
    }

    static private double trapezoidArea(double width, double height1, double height2) {
        return width * (height1 + height2) / 2.0;
    }

    static private double areaUnderChord(double radius, double chordLength) {
        double isocelesHeight = Math.sqrt(radius*radius - chordLength * chordLength / 4.0);
        double areaUnderIsoceles = isocelesHeight * chordLength / 2.0;
        double halfAngle = Math.asin(chordLength / (2.0 * radius));
        double areaUnderArc = halfAngle * radius * radius;

        return areaUnderArc - triangleArea(chordLength, isocelesHeight);
    }

    // Returns the fraction of the pixel at (px, py) covered by
    // the circle with center (cx, cy) and radius 'radius'
    static private double calcPixelShading(double cx, double cy, double px,
            double py, double radius) {
        // Translate so the center is at the origin
        px -= cx;
        py -= cy;

        // Reflect across the axis so the point is in the first quadrant
        px = Math.abs(px);
        py = Math.abs(py);

        // One more transformation which simplifies the logic later
        if (py > px) {
            double temp;

            temp = px;
            px = py;
            py = temp;
        }

        double left = px - 0.5;
        double right = px + 0.5;
        double bottom = py - 0.5;
        double top = py + 0.5;

        if (distanceFromCenterSquared(left, bottom) > radius*radius) {
            return 0.0;
        }

        if (distanceFromCenterSquared(right, top) < radius*radius) {
            return 1.0;
        }

        // Check if only the bottom-left corner of the pixel is inside the circle
        if (distanceFromCenterSquared(left, top) > radius*radius) {
            double triangleWidth = horizontalLineIntersectsCircle(bottom, radius) - left;
            double triangleHeight = verticalLineIntersectsCircle(left, radius) - bottom;
            double chordLength = distanceFromCenter(triangleWidth, triangleHeight);

            return triangleArea(triangleWidth, triangleHeight)
                   + areaUnderChord(radius, chordLength);

        }

        // Check if only the top-right corner of the pixel is outside the circle
        if (distanceFromCenterSquared(right, bottom) < radius*radius) {
            double triangleWidth = right - horizontalLineIntersectsCircle(top, radius);
            double triangleHeight = top - verticalLineIntersectsCircle(right, radius);
            double chordLength = distanceFromCenter(triangleWidth, triangleHeight);

            return 1 - triangleArea(triangleWidth, triangleHeight)
                   + areaUnderChord(radius, chordLength);
        }

        // It must be that the top-left and bottom-left corners are inside the circle
        double trapezoidWidth1 = horizontalLineIntersectsCircle(top, radius) - left;
        double trapezoidWidth2 = horizontalLineIntersectsCircle(bottom, radius) - left;
        double chordLength = distanceFromCenter(1, trapezoidWidth2 - trapezoidWidth1);
        double shading = trapezoidArea(1.0, trapezoidWidth1, trapezoidWidth2)
                         + areaUnderChord(radius, chordLength);

        // When top >= 0 and bottom <= 0 it's possible for the circle to intersect the pixel 4 times.
        // If so, remove the area of the section which crosses the right-hand edge.
        if (top >= 0 && bottom <= 0 && radius > right) {
            shading -= areaUnderChord(radius, 2 * verticalLineIntersectsCircle(right, radius));
        }

        return shading;
    }

    private void drawIfNeeded() {
        if (!mDrawNeeded || !mVisible || mDimensionsUnequal) {
            return;
@@ -123,11 +227,41 @@ class CircularDisplayMask {
            break;
        }

        int circleRadius = mScreenSize.x / 2;
        c.drawColor(Color.BLACK);

        // The radius is reduced by mMaskThickness to provide an anti aliasing effect on the display edges.
        c.drawCircle(circleRadius, circleRadius, circleRadius - mMaskThickness, mPaint);
        int maskWidth = mScreenSize.x - 2*mMaskThickness;
        int maskHeight;

        // Don't render the whole mask if it is partly offscreen.
        if (maskWidth > mScreenSize.y) {
            maskHeight = mScreenSize.y;
        } else {
            // To ensure the mask can be properly centered on the canvas the
            // bitmap dimensions must have the same parity as those of the canvas.
            maskHeight = mScreenSize.y - ((mScreenSize.y - maskWidth) & ~1);
        }

        double cx = (maskWidth - 1.0) / 2.0;
        double cy = (maskHeight - 1.0) / 2.0;
        double radius = maskWidth / 2.0;
        int[] pixels = new int[maskWidth * maskHeight];

        for (int py=0; py<maskHeight; py++) {
            for (int px=0; px<maskWidth; px++) {
                double shading = calcPixelShading(cx, cy, px, py, radius);
                pixels[maskWidth*py + px] =
                    Color.argb(255 - (int)Math.round(255.0*shading), 0, 0, 0);
            }
        }

        Bitmap transparency = Bitmap.createBitmap(pixels, maskWidth, maskHeight,
            Bitmap.Config.ARGB_8888);

        c.drawBitmap(transparency,
                     (float)mMaskThickness,
                     (float)((mScreenSize.y - maskHeight) / 2),
                     mPaint);

        mSurface.unlockCanvasAndPost(c);
    }