gnustep / libs-gui

The GNUstep gui library is a library of graphical user interface classes written completely in the Objective-C language; the classes are based upon Apple's Cocoa framework (which came from the OpenStep specification). *** Larger patches require copyright assignment to FSF. please file bugs here. ***
http://www.gnustep.org
GNU General Public License v3.0
277 stars 102 forks source link

NSBezierPath does not clip gradients #228

Open optimisme opened 9 months ago

optimisme commented 9 months ago

NSBeizerPath does not clip gradients properly, see

                NSColor *startColor = GVThemeColorRGB(35, 135, 255, 1.0);
                NSColor *endColor = GVThemeColorRGB(0, 110, 255, 1.0);
                NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:startColor endingColor:endColor];

                NSBezierPath    *bezelPath2 = [NSBezierPath bezierPathWithRoundedRect:paddedFrame xRadius:15.0 yRadius:15.0];
                [NSGraphicsContext saveGraphicsState];
                [bezelPath2 setClip];
                [gradient drawInRect:frame angle:90.0]; 
                [NSGraphicsContext restoreGraphicsState];

                [[NSColor redColor] setStroke];
                [bezelPath2 setLineWidth:1.0];
                [bezelPath2 stroke];

The exepected behaviour is seeing nothing blue out of red circle

Screenshot 2023-12-23 at 01 15 23
fredkiefer commented 9 months ago

This bug report is rather disturbing. Up to now I had the impression that clipping was more or less working and there is no special magic build in for gradients. It should just respect the normal clipping mechanisms. Which backend are you using? At the moment cairo is the best supported backend, you really should be working with that.

optimisme commented 9 months ago

I installed GNUStep on Ubuntu 22.04 using this repo https://github.com/plaurent/gnustep-build For me it is the easiest way to install and updated version of GNUStep Looking at the install script: https://github.com/plaurent/gnustep-build/blob/master/ubuntu-22.04-clang-14.0-runtime-2.1/GNUstep-buildon-ubuntu2204.sh I think my installation is using Cairo backend.

optimisme commented 9 months ago

I attach two images from the next test source.

Top image is rendered by GNUStep:

- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];

    // Clipped gradient example
    NSColor *startColor = [NSColor greenColor];
    NSColor *endColor = [NSColor blueColor];
    NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:startColor endingColor:endColor];

    NSRect frame = NSMakeRect(50, 50, 100, 50);
    NSBezierPath *bezelPath = [NSBezierPath bezierPathWithRoundedRect:frame xRadius:15.0 yRadius:15.0];
    [NSGraphicsContext saveGraphicsState];
    [bezelPath setClip];
    [gradient drawInRect:frame angle:90.0];
    [NSGraphicsContext restoreGraphicsState];

    // Star
    NSColor *starStartColor = [NSColor redColor];
    NSColor *starEndColor = [NSColor yellowColor];
    NSGradient *starGradient = [[NSGradient alloc] initWithStartingColor:starStartColor endingColor:starEndColor];

    NSBezierPath *starPath = [NSBezierPath bezierPath];
    CGFloat starRadius = 50.0;  // Radi exterior de la estrella
    CGFloat innerRadius = starRadius * sin(M_PI / 10) / sin(7 * M_PI / 10); // Radi interior
    NSPoint center = NSMakePoint(250, 100); // Centre de l'estrella
    for (int i = 0; i < 10; i++) {
        // Calculem l'angle per a cada punt
        CGFloat angle = (CGFloat)(2 * M_PI / 10 * i);

        // Alternem entre radi exterior i interior
        CGFloat radius = i % 2 == 0 ? starRadius : innerRadius;
        CGFloat x = center.x + sin(angle) * radius;
        CGFloat y = center.y + cos(angle) * radius;

        if (i == 0) {
            [starPath moveToPoint:NSMakePoint(x, y)];
        } else {
            [starPath lineToPoint:NSMakePoint(x, y)];
        }
    }
    [starPath closePath];

    [NSGraphicsContext saveGraphicsState];
    [starPath setClip];
    [starGradient drawInBezierPath:starPath angle:90.0];
    [NSGraphicsContext restoreGraphicsState];

    // 45 degrees Oval
    NSRect ovalRect = NSMakeRect(350, 50, 100, 50);
    NSBezierPath *ovalPath = [NSBezierPath bezierPathWithOvalInRect:ovalRect];
    [NSGraphicsContext saveGraphicsState];
    NSAffineTransform *transform = [NSAffineTransform transform];
    NSPoint centerOval = NSMakePoint(NSMidX(ovalRect), NSMidY(ovalRect));
    [transform translateXBy:centerOval.x yBy:centerOval.y];
    [transform rotateByDegrees:45];
    [transform translateXBy:-centerOval.x yBy:-centerOval.y];
    [ovalPath transformUsingAffineTransform:transform];
    [ovalPath setClip];
    [NSGraphicsContext restoreGraphicsState];
    [gradient drawInBezierPath:ovalPath angle:-25.0];   
}
Captura de pantalla 2023-12-25 a les 17 59 18
optimisme commented 9 months ago

This is the project with the example test-bug-gradients.zip

fredkiefer commented 9 months ago

Thank you very much for these examples. The first looks like a bug in GNUstep and in the new year I hope to find time to investigate that. As for the third example I don't understand how this is working on macOS. What is the setClip doing here? I really need to spend more time on that one.

fredkiefer commented 9 months ago

I looked into the first issue and it is even worse. This is a limitation of the way we interact with the cairo library. When saving the graphics state we need to copy the state within cairo and there is a limitation that only rectangular clipping can be copied correctly. For all other cases a replacement gets used. In this case a rectangular one, which looks like no clipping at all. Maybe it would be possible to implement this interaction completely different, but although I wrote most of the original one, I don't see how to do it differently. This may be the reason for quite a few of the drawing artefacts you may see. As this happens every time a non rectangular shape gets used.

I still need to look into the third case.