ThinkC [Study Group 1] Drawing on the Macintosh

Relating to ThinkC Development

Kabootie Computey!

New Tinkerer
Sep 13, 2022
15
19
3
www.kabootie.com
I cobbled together a first QuickDraw program this afternoon - a recreation of the virus Jeff Goldblum wrote on his Powerbook 5300 in Independence Day. So if aliens ever invade, don't worry folks, I got us covered.

A couple interesting things I learned:
  1. It was really easy to put all the text into a STR# resource and iterate over calls to GetIndString() to retrieve and display it.
  2. I needed to add a call to WaitNextEvent() into the main loop so that I could take a screenshot... otherwise the program never yielded to the OS, and the screenshot wouldn't grab an image until the program had exited.

toxecret.png
 

pretzelFTW

New Tinkerer
Sep 5, 2022
26
8
3
Aha, well that fixed the bounding box issues, still seems to redraw the same pattern. My fault for copy/pasting and editing the repetitive parts. Can you blame me?

My second correction wasn't quite accurate.

Where you have the second instance of this line:
C:
gLines[i].bottom += gDeltaBottom;

…it should be:
C:
gLines[i].right += gDeltaRight;
 

Crutch

Tinkerer
Jul 10, 2022
292
226
43
Chicago
I needed to add a call to WaitNextEvent() into the main loop so that I could take a screenshot... otherwise the program never yielded to the OS, and the screenshot wouldn't grab an image until the program had exited.
Yes - or just call SystemTask(), which yields time to the system if you don’t care about getting an event (or, in an older app/pre-MultiFinder system, where you call _GetNextEvent instead _WaitNextEvent).
 
  • Like
Reactions: Kabootie Computey!

jenna32bit

New Tinkerer
Aug 19, 2022
13
7
3
United States
Ok! I think I have it right now. This is why I treasure PRs in modern dev settings.

C:
#define kNumLines            50 // Can be higher for more linearity!
#define kMoveToFront        (WindowPtr)-1L
#define kRandomUpperLimit    32768
#define kEmptyString        "\p"
#define kEmptyTitle            kEmptyString
#define kVisible            true
#define kNoGoAway            false
#define kNilRefCon            (long)nil

/*** Globals ***/

Rect    gLines[kNumLines];
short    gDeltaTop=3;
short    gDeltaBottom=3;
short    gDeltaLeft=2;
short    gDeltaRight=6;
short    gOldMBarHeight;

/*** Functions ***/

void    ToolBoxInit(void);
void    WindowInit(void);
void    LinesInit(void);
void    MainLoop(void);
void    RandomRect(Rect *rectPtr);
short    Randomize(short range);
void    RecalcLine(short i);
void    DrawLine(short i);

/*** Main ***/

void    main(void)
{
    ToolBoxInit();
    WindowInit();
    LinesInit();
    MainLoop();
}

/*** ToolBoxInit ***/

void    ToolBoxInit(void)
{
    InitGraf(&thePort);
    InitFonts();
    InitWindows();
    InitMenus();
    TEInit();
    InitDialogs(nil);
    InitCursor();
}

/*** WindowInit ***/

void    WindowInit(void)
{
    Rect        totalRect;
    Rect        mBarRect;
    RgnHandle    mBarRgn;
    WindowPtr    window;
    
    gOldMBarHeight = MBarHeight;
    MBarHeight = 0;
    
    window = NewWindow(nil, &(screenBits.bounds),
                       kEmptyTitle, kVisible, plainDBox, kMoveToFront,
                       kNoGoAway, kNilRefCon);
    
    SetRect(&mBarRect, screenBits.bounds.left,
            screenBits.bounds.top, screenBits.bounds.right,
            screenBits.bounds.top+gOldMBarHeight);
    
    mBarRgn = NewRgn();
    RectRgn(mBarRgn, &mBarRect);
    UnionRgn(window->visRgn, mBarRgn, window->visRgn);
    DisposeRgn(mBarRgn);
    SetPort(window);
    FillRect(&(window->portRect), black);
    PenMode(patXor);
}

/*** LinesInit ***/

void    LinesInit(void)
{
    short i;
    
    HideCursor();
    GetDateTime((unsigned long *)(&randSeed));
    RandomRect(&(gLines[0]));
    DrawLine(0);
    
    for (i=1; i<kNumLines; i++)
    {
        gLines[i] = gLines[i-1];
        RecalcLine(i);
        DrawLine(i);
    }
}

/*** MainLoop ***/

void    MainLoop(void)
{
    short    i;
    
    while (!Button())
    {
        DrawLine(kNumLines - 1);
        for (i=kNumLines-1; i>0; i--)
            gLines[i] = gLines[i-1];
        RecalcLine(0);
        DrawLine(0);
    }
    MBarHeight = gOldMBarHeight;
}

/*** RandomRect ***/

void    RandomRect(Rect *rectPtr)
{
    WindowPtr    window;
    
    window = FrontWindow();
    rectPtr->left    = Randomize(window->portRect.right - window->portRect.left);
    rectPtr->right    = Randomize(window->portRect.right - window->portRect.left);
    rectPtr->top    = Randomize(window->portRect.bottom - window->portRect.top);
    rectPtr->bottom    = Randomize(window->portRect.bottom - window->portRect.top);
}

/*** Randomize ***/

short    Randomize(short range)
{
    long randomNumber;
    randomNumber = Random();

    if (randomNumber<0)
        randomNumber *= -1;
    
    return((randomNumber * range)/kRandomUpperLimit);
}

/*** RecalcLine ***/

void    RecalcLine(short i)
{
    WindowPtr    window;
    window = FrontWindow();
    
    gLines[i].top += gDeltaTop;
    if ((gLines[i].top < window->portRect.top) ||
        (gLines[i].top > window->portRect.bottom))
    {
        gDeltaTop *= -1;
        gLines[i].top += 2*gDeltaTop;
    }
    
    gLines[i].bottom += gDeltaBottom;
    if ((gLines[i].bottom < window->portRect.top) ||
        (gLines[i].bottom > window->portRect.bottom))
    {
        gDeltaBottom *= -1;
        gLines[i].bottom += 2*gDeltaBottom;
    }

    gLines[i].left += gDeltaLeft;
    if ((gLines[i].left < window->portRect.left) ||
        (gLines[i].left > window->portRect.right))
    {
        gDeltaLeft *= -1;
        gLines[i].left += 2*gDeltaLeft;
    }

    gLines[i].right += gDeltaRight;
    if ((gLines[i].right < window->portRect.left) ||
        (gLines[i].right > window->portRect.right))
    {
        gDeltaRight *= -1;
        gLines[i].right += 2*gDeltaRight;
    }
}

/*** DrawLine ***/

void    DrawLine(short i)
{
    MoveTo(gLines[i].left, gLines[i].top);
    LineTo(gLines[i].right, gLines[i].bottom);
}
 

Kabootie Computey!

New Tinkerer
Sep 13, 2022
15
19
3
www.kabootie.com
Yes - or just call SystemTask(), which yields time to the system if you don’t care about getting an event (or, in an older app/pre-MultiFinder system, where you call _GetNextEvent instead _WaitNextEvent).
Interestingly, SystemTask() doesn't allow me to take screenshots. It looks like it yields the CPU to desk accessories, but not to whatever routine does screen captures, possibly? With WaitNextEvent() I can specify that I'm yielding for 1 tick per cycle and give it a nil pointer, and everything seems to work. But am I doing something really non-idiomatic here?

C:
while ( !Button() ) {

     WaitNextEvent( everyEvent, nil, 1, nil );

}

ExitToShell();
 

Crutch

Tinkerer
Jul 10, 2022
292
226
43
Chicago
Huh, I probably stand corrected, I thought SystemTask checked for an FKEY press (for a screenshot) but looks like you need GetNextEvent or WaitNextEvent for that. Thanks for making me check myself.

Your snip is totally fine except I think you should really pass a pointer to an EventRecord in your second argument there, even if you don’t care what gets stuck in it. WaitNextEvent isn’t meant to get a NULL EventRecord pointer.
 
  • Like
Reactions: Kabootie Computey!

Kabootie Computey!

New Tinkerer
Sep 13, 2022
15
19
3
www.kabootie.com
Huh, I probably stand corrected, I thought SystemTask checked for an FKEY press (for a screenshot) but looks like you need GetNextEvent or WaitNextEvent for that. Thanks for making me check myself.

Your snip is totally fine except I think you should really pass a pointer to an EventRecord in your second argument there, even if you don’t care what gets stuck in it. WaitNextEvent isn’t meant to get a NULL EventRecord pointer.

Thanks for the tip. In fact, I was seeing some erratic behavior in my Toxecret program that went away when I put an EventRecord pointer back in, so yay! While I was in there, I also incorporated your tip about calculating the line height for the text from post #26, instead of hard-coding it.
 

KennyPowers

Active Tinkerer
Jun 27, 2022
246
289
63
I'm caught up through chapter 3 and did all of the example programs. Then I threw together a simple animated bouncing ball thing. It uses TickCount() for frame-rate independent animation (ball moves the same speed regardless of how fast or slow your mac is...will just be less smooth if the system can't keep up). I also figured out how to draw to an off-screen buffer and then copy it to the window for flicker-free animation. A (horribly compressed) gif of it running on my IIci:

IdenticalGreatAngora-size_restricted.gif
 
Last edited:

pfuentes69

Active Tinkerer
Oct 27, 2021
380
290
63
Switzerland
I'm caught up through chapter 3 and did all of the example programs. Then I threw together a simple animated bouncing ball thing. It uses TickCount() for frame-rate independent animation (ball moves the same speed regardless of how fast or slow your mac is...will just be less smooth if the system can't keep up). I also figured out how to draw to an off-screen buffer and then copy it to the window for flicker-free animation. A (horribly compressed) gif:

View attachment 9149
This is very cool.
Would you share the code?
I’m specially interested on the off-screen drawing.
 

KennyPowers

Active Tinkerer
Jun 27, 2022
246
289
63
This is very cool.
Would you share the code?
I’m specially interested on the off-screen drawing.
Sure...code is attached. I've only compiled it with ThinkC 6 on System 7.1. Be sure to link in the MacTraps library. I don't think this code will work on System 6 either (I think System 7 is needed for offscreen graphics worlds). Chapter 6 in this document was helpful, though I don't claim to entirely know what I'm doing (probably don't need to lock/unlock the offscreen buffer every frame for example) :) Also, the ball's collision detection with the edges of the window is somewhat naive. When a collision is detected, it just backs the ball up to its previous position to prevent the ball getting stuck on the window edge. The "correct" way to do it would probably be to back the ball up along its previous movement vector to the precise point where it collided with the window edge. However, the naive way looks good enough for what was really an exercise in offscreen drawing.

C:
#include <QDOffscreen.h>

#define kBaseResID 128
#define kMoveToFront (WindowPtr)-1L
#define kBallRadius 20
#define kInitialBallXPosition 100
#define kInitialBallYPosition 100
#define kBallXVelocity 2
#define kBallYVelocity 2
#define kBallColor redColor

typedef struct Vec2
{
    float x;
    float y;
} Vec2;

typedef struct Ball
{
    Vec2 position;
    Vec2 velocity;
    float radius;
} Ball;

// Function prototypes
void ToolBoxInit();
WindowPtr WindowInit();
GWorldPtr OffscreenGraphicsInit(WindowPtr window);
short round(float number);
void MainLoop(WindowPtr window, GWorldPtr offscreenGraphicsWorld);
void DoBallPhysics(Ball* ball, float deltaTicks, WindowPtr window, Rect* ballRect);
void StepBallPhysics(Ball* ball, float deltaTime, Rect* ballRect);
void DrawBall(Rect* ballRect, Rect* rectToClear, GWorldPtr offscreenGraphicsWorld);

void main()
{
    WindowPtr window;
    GWorldPtr offscreenGraphicsWorld;

    ToolBoxInit();
    window = WindowInit();
    offscreenGraphicsWorld = OffscreenGraphicsInit(window);
    MainLoop(window, offscreenGraphicsWorld);
}

void ToolBoxInit()
{
    InitGraf(&thePort);
    InitFonts();
    InitWindows();
    InitMenus();
    TEInit();
    InitDialogs(nil);
    InitCursor();
}

WindowPtr WindowInit()
{
    WindowPtr window = GetNewWindow(kBaseResID, nil, kMoveToFront);
   
    if(window == nil) // Couldn't load the WIND resource
    {
        SysBeep(10);
        ExitToShell();
    }
   
    ShowWindow(window);
    SetPort(window);
    FillRect(&(window->portRect), black);
   
    return window;
}

GWorldPtr OffscreenGraphicsInit(WindowPtr window)
{
    GWorldPtr offscreenGraphicsWorld;
   
    QDErr error = NewGWorld(&offscreenGraphicsWorld, 0, &(window->portRect), nil, nil, 0);
    if(error != noErr || offscreenGraphicsWorld == nil) // Couldn't create offscreen graphics world
    {
        SysBeep(10);
        ExitToShell();
    }
   
    return offscreenGraphicsWorld;
}

short round(float number)
{
    return number < 0 ? (short)(number - 0.5f) : (short)(number + 0.5f);
}

void MainLoop(WindowPtr window, GWorldPtr offscreenGraphicsWorld)
{
    Ball ball =
    {
        { kInitialBallXPosition, kInitialBallYPosition },
        { kBallXVelocity, kBallYVelocity },
        kBallRadius
    };
   
    Rect ballRect;
    Rect previousBallRect = window->portRect;

    long lastLoopTicks = TickCount();
   
    while(!Button())
    {
        long currentLoopTicks = TickCount();
        float deltaTicks = currentLoopTicks - lastLoopTicks;
        lastLoopTicks = currentLoopTicks;
        DoBallPhysics(&ball, deltaTicks, window, &ballRect);
        DrawBall(&ballRect, &previousBallRect, offscreenGraphicsWorld);
        previousBallRect = ballRect;
    }
}

void DoBallPhysics(Ball* ball, float deltaTicks, WindowPtr window, Rect* ballRect)
{
    Vec2 initialBallPosition = ball->position;
    float deltaTime = deltaTicks * 0.5f;
   
    StepBallPhysics(ball, deltaTime, ballRect);
   
    if(ballRect->left < window->portRect.left || ballRect->right > window->portRect.right)
    {
        ball->velocity.x = -ball->velocity.x;
        ball->position.x = initialBallPosition.x;
        ball->position.y = initialBallPosition.y;
        StepBallPhysics(ball, deltaTime, ballRect);
    }
    if(ballRect->top < window->portRect.top || ballRect->bottom > window->portRect.bottom)
    {
        ball->velocity.y = -ball->velocity.y;
        ball->position.x = initialBallPosition.x;
        ball->position.y = initialBallPosition.y;
        StepBallPhysics(ball, deltaTime, ballRect);
    }
}

void StepBallPhysics(Ball* ball, float deltaTime, Rect* ballRect)
{
    ball->position.x += ball->velocity.x * deltaTime;
    ball->position.y += ball->velocity.y * deltaTime;
   
    ballRect->top = round(ball->position.y - ball->radius);
    ballRect->left = round(ball->position.x - ball->radius);
    ballRect->bottom = round(ball->position.y + ball->radius);
    ballRect->right = round(ball->position.x + ball->radius);
}

void DrawBall(Rect* ballRect, Rect* rectToClear, GWorldPtr offscreenGraphicsWorld)
{
    GWorldPtr onscreenGraphicsWorld;
    GDHandle onscreenDevice;
    PixMapHandle graphicsWorldPixelMap;

    GetGWorld(&onscreenGraphicsWorld, &onscreenDevice);
    SetGWorld(offscreenGraphicsWorld, nil);
    graphicsWorldPixelMap = GetGWorldPixMap(offscreenGraphicsWorld);
    if(!LockPixels(graphicsWorldPixelMap)) // Graphics world buffer has been purged
    {
        SysBeep(10);
        ExitToShell();
    }
   
    ForeColor(blackColor);
    PaintRect(rectToClear); // Just clear the ball's Rect from the previous frame instead of the whole port
    ForeColor(kBallColor);
    PaintOval(ballRect);
   
    SetGWorld(onscreenGraphicsWorld, onscreenDevice);
    CopyBits(&(((GrafPtr)offscreenGraphicsWorld)->portBits), &(((GrafPtr)onscreenGraphicsWorld)->portBits), &(offscreenGraphicsWorld->portRect), &(onscreenGraphicsWorld->portRect), srcCopy, nil);
    if(QDError() != noErr) // Probably means insufficient memory
    {
        SysBeep(10);
        ExitToShell();
    }
    UnlockPixels(graphicsWorldPixelMap);
}
 
Last edited:
  • Like
Reactions: pfuentes69

Crutch

Tinkerer
Jul 10, 2022
292
226
43
Chicago
I am not sure but actually think NewGWorld etc. was added in (maybe certain versions of?) System 6. I’d love to find out.

Edit: I mean of course I could just try it with every system version. But that’s tiresome ….
 
Last edited:
  • Like
Reactions: pfuentes69

pfuentes69

Active Tinkerer
Oct 27, 2021
380
290
63
Switzerland
This is great. Thanks to all.

I think my development already has other dependencies with System 7, so it would be fine if this is also another one. I'll try to work on this in the weekend and see how it goes.

My main interest is to be able to calculate the screen contents and store it, so I don't need to recalculate for each redraw if the content didn't change.
 

JonThysell

New Tinkerer
Oct 4, 2022
2
2
3
Pacific Northwest
jonthysell.com
I am not sure but actually think NewGWorld etc. was added in (maybe certain versions of?) System 6. I’d love to find out.

Edit: I mean of course I could just try it with every system version. But that’s tiresome ….
AFAIK the GWorld stuff was only added in System 7, which is why I did the backing bitmap approach so I could support System 6.

And I got the idea from looking at the Glypha source here: https://github.com/softdorothy/Glypha3. Specficially the CreateOffscreenBitMap() method here: https://raw.githubusercontent.com/softdorothy/Glypha3/master/Source/Utilities.c
 

pfuentes69

Active Tinkerer
Oct 27, 2021
380
290
63
Switzerland
Sure...code is attached. I've only compiled it with ThinkC 6 on System 7.1. Be sure to link in the MacTraps library. I don't think this code will work on System 6 either (I think System 7 is needed for offscreen graphics worlds). Chapter 6 in this document was helpful, though I don't claim to entirely know what I'm doing (probably don't need to lock/unlock the offscreen buffer every frame for example) :) Also, the ball's collision detection with the edges of the window is somewhat naive. When a collision is detected, it just backs the ball up to its previous position to prevent the ball getting stuck on the window edge. The "correct" way to do it would probably be to back the ball up along its previous movement vector to the precise point where it collided with the window edge. However, the naive way looks good enough for what was really an exercise in offscreen drawing.
Hello,
I tried your code... I had an error with FillRect(&(window->portRect), black); , which I changed to PaintRect().

I could build in Symantec C++ 7, as is the environment I'm using in my project. In this case I can build, but the window doesn't show (I use System 7.5.5)

Then I tried in TC 6, and I got a build error claiming that "thePort" (as in InitGraf(&thePort)) is undefined... I'm just starting with the ToolBox, so need to figure out what else I'm missing...
 

pfuentes69

Active Tinkerer
Oct 27, 2021
380
290
63
Switzerland
Hello,
I tried your code... I had an error with FillRect(&(window->portRect), black); , which I changed to PaintRect().

I could build in Symantec C++ 7, as is the environment I'm using in my project. In this case I can build, but the window doesn't show (I use System 7.5.5)

Then I tried in TC 6, and I got a build error claiming that "thePort" (as in InitGraf(&thePort)) is undefined... I'm just starting with the ToolBox, so need to figure out what else I'm missing...
Never mind... I was forgetting to create the resource file. Your code runs on Symantec C++ 7 and System 7.5.5 with no issue.

But in TC6 I still get the build error (undefined: thePort).
 

Crutch

Tinkerer
Jul 10, 2022
292
226
43
Chicago
AFAIK the GWorld stuff was only added in System 7, which is why I did the backing bitmap approach so I could support System 6.

Just checked this in the 2nd edition of Knaster’s Macintosh Programming Secrets and he confirmed my suspicions.

You could use _NewGWorld etc. under System 6 (maybe even earlier), you just needed the 32-bit “Color QuickDraw” INIT in your System Folder.

System 7 just made these routines built-in. But they were available earlier (and indeed originally made available only) with the INIT installed.