0

=== Edited to add more information about the effect I'm trying to get ===

In an old screensaver, I'm drawing something like a comet: a bright object that leaves a fading trail of where it's been. So in each frame, I first draw over the whole screen with transparent black (alpha set to .02 or similar), then I draw the object at it's new location. (And of course in reality there are many of these "comets", 50 or so.)

6 or 8 years ago this worked fine on the Mac, but now in Mojave, although the fade is happening, it flickers wildly, as if the view is being erased to black before every drawRect call.

I'm just updating this old screen saver I wrote in 2010 or so, I'm very fond of it and it stopped working in Mojave. This fade technique worked fine back then, but something has apparently changed. I suspect something about the backing store or layers. I'm hoping for a bit of an education, pointers to relevant info, and suggestions on best practice for doing something like this.

(For the record, I was an active Mac programmer way back in the 90s - anyone remember develop magazine? - but rarely program now, so Cocoa and Objective C are still, ahem, "new" to me.)

The code is trivial. In my view's drawRect, before drawing any new stuff that is intended to fade out, I just do:


// Erase to black, but with much transparency, to fade out any previous drawing
[[NSColor colorWithDeviceRed: 0.0 green: 0.0 blue: 0.0 alpha: 0.02] set];
[NSBezierPath fillRect: [self bounds]];

Again, I'm sure from digging around that this has something to do with views and layer backing, but I'm not sure of the best practice for fixing it, and I'd like to understand what I'm doing. Any pointers in the right direction are appreciated.

UPDATE: I found this question and answer that looks promising. Is it really necessary, though, to create my own offscreen image? I was hoping I could just set a boolean somewhere... :-)

dogrocket
  • 1
  • 2
  • Doesn't directly answer your question about drawing with semi-transparent black, but it would probably be easier to simply animate the view's `alphaValue` toward zero, with an all-black view behind it. See [here](https://stackoverflow.com/questions/25456080/cocoa-nsview-animation/25458169#25458169), for example. – Ken Thomases Jul 15 '19 at 21:37
  • Thanks, but that's for fading out the whole view, yes? I should have a said a little more about what I'm doing: I'm drawing a fading trail of dots, their paths are determined each frame by some fractal math. So each frame, I "wash" the screen with black, fading every dot I've drawn before, then draw bright new dots on top. The effect is sort of a comet tail, bright at the head and fading to black along the previous path. – dogrocket Jul 15 '19 at 22:24
  • Ah. Well, it's never been OK to assume your view is drawing over its old drawing. Cocoa uses the "painter" model of drawing a window. Your view is expected to draw its intended content (within the dirty rect) from scratch every time `-drawRect:` is called, because views behind it may have redrawn. – Ken Thomases Jul 16 '19 at 00:42
  • Ah, that makes perfect sense...I guess I was just lucky until Mojave came along. What, then, is the best practice for repeatedly drawing over the same drawing, then getting it to the screen? (In the old days it was offscreen bitmaps, maybe now it's a bitmap graphics context?) – dogrocket Jul 16 '19 at 00:47
  • There are several possible approaches, with trade-offs between complexity and performance. Yes, a bitmap graphics context can work (as you've seen). You could potentially making it faster by making your view layer backed and assigning the image as its content rather than drawing it. Even faster would be something like Metal, where you maintain a texture and drawing with blending each time. – Ken Thomases Jul 16 '19 at 01:16

1 Answers1

0

I reworked the code to draw into an NSBitmapImageRep, based on the answer here, and it seems to be working fine now. The relevant code in my little test app:

// In this test app, frame redraws are triggered by a timer
- (void)DrawNewFrame:(NSTimer *)timer
{
    // just call our offscreen drawing routine, which will draw and then call setNeedsDisplay
    [self drawOffscreen];
}

- (void)drawOffscreen {
    // Set up the NSBitmapImageRep if it isn't already
    NSBitmapImageRep *cmap = cachedDrawingRep;
    if (!cmap) {
        cmap = [self bitmapImageRepForCachingDisplayInRect:self.bounds];
        cachedDrawingRep = cmap;
    }

    // Get ready to draw
    NSGraphicsContext *ctx = [NSGraphicsContext graphicsContextWithBitmapImageRep:cmap];
    [NSGraphicsContext saveGraphicsState];
    [NSGraphicsContext setCurrentContext:ctx];

    // Erase to black, but with much transparency, to fade out any previous drawing
    [[NSColor colorWithDeviceRed: 0.0 green: 0.0 blue: 0.0 alpha: 0.02] set];
    [NSBezierPath fillRect: [self bounds]];

    // Draw a little text if a key is tapped, to see it fade
    if( drawText )
    {
        // Make the font dictionary
        NSDictionary    *fontDict = [NSDictionary dictionaryWithObjectsAndKeys:
                    [NSFont fontWithName:@"Helvetica" size:12], NSFontAttributeName,
                    [NSColor redColor], NSForegroundColorAttributeName,
                    nil];

        NSString *helpStr = @"Fade This String!";
        [helpStr drawAtPoint:NSMakePoint(random() % 200, random() % 200) withAttributes: fontDict];
        drawText = NO;
    }
    [NSGraphicsContext restoreGraphicsState];

    // Make sure drawRect will get called
    [self setNeedsDisplay:YES];
}

- (void)drawRect:(NSRect)rect {
    if (cachedDrawingRep) {
        [cachedDrawingRep drawInRect:self.bounds];
    }
}

It's working exactly as expected now, so I'm happy. Thanks!

dogrocket
  • 1
  • 2