Building on our program that draws content at a fixed screen position regardless of window position


Today's Little Program uses the technique we saw last week of drawing content at a fixed screen position, regardless of window position, but adds a little physics to it.

Start with our scratch program and make these changes:

#include <math.h> // physics requires math (go figure)

#define Omega 2.0f

class Motion
{
public:
 Motion() : x0(0.0f), v0(0.0f) { RecalcCurve(); }

 void ShiftOrigin(double dx)
 {
  Tick();
  v0 = v;
  x0 = x + dx;
  RecalcCurve();
 }

 double Pos() { return x; }
 bool Moving() { return fabs(x) >= 0.5f || fabs(v) >= 1.0f; }

 void Tick() {
  t = (GetTickCount() - tm0) / 1000.0f;
  double ewt = exp(-Omega * t);
  double abt = A + B * t;
  x = abt * ewt;
  v = (-Omega * abt + B) * ewt;
 }

private:
 void RecalcCurve() {
  A = x0;
  B = v0 + Omega * x0;
  tm0 = GetTickCount();
 }
public:
 DWORD tm0;
 double x0, v0, A, B, t, x, v;
};

The Motion class simulates damped motion. Ask a physicist how it works.

Motion g_mX;  // motion in x-direction
Motion g_mY;  // motion in y-direction

POINT g_ptRest; // desired rest point

POINT CalcRestPoint(HWND hwnd)
{
    RECT rc;
    GetClientRect(hwnd, &rc);
    MapWindowRect(hwnd, HWND_DESKTOP, &rc);
    POINT pt = { rc.left + (rc.right - rc.left) / 2,
                 rc.top + (rc.bottom - rc.top) / 2 };
    return pt;
}

The rest point is the center of the window.

void ScheduleFrame(HWND hwnd)
{
    InvalidateRect(hwnd, 0, TRUE);
    KillTimer(hwnd, 1);
}

VOID CALLBACK InvalidateMe(HWND hwnd, UINT, UINT_PTR, DWORD)
{
    ScheduleFrame(hwnd);
}

To schedule the painting of a new frame, we invalidate our window and cancel any outstanding animation timer (because the timer is no longer needed now that a paint has been scheduled).

void OnWindowPosChanged(HWND hwnd, LPWINDOWPOS lpwpos)
{
    if (IsWindowVisible(hwnd)) {
        POINT ptRest = CalcRestPoint(hwnd);
        if (ptRest.x != g_ptRest.x ||
            ptRest.y != g_ptRest.y) {
          g_mX.ShiftOrigin(g_ptRest.x - ptRest.x);
          g_mY.ShiftOrigin(g_ptRest.y - ptRest.y);
          ScheduleFrame(hwnd);
        }
        g_ptRest = ptRest;
    }
}

    HANDLE_MSG(hwnd, WM_WINDOWPOSCHANGED, OnWindowPosChanged);

If the window changes its rest point while it is vislble, then move the origin of the motion variables and schedule a new frame of animation.

Okay, here's the fun part: Drawing the moving circle.

void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
 RECT rc;

 g_mX.Tick();
 g_mY.Tick();

 POINT ptOrigin = { 0, 0 };
 ClientToScreen(hwnd, &ptOrigin);
 POINT ptOrg;
 SetWindowOrgEx(pps->hdc, ptOrigin.x, ptOrigin.y, &ptOrg);

 int x = g_ptRest.x + static_cast<int>(floor(g_mX.Pos() + 0.5f));
 int y = g_ptRest.y + static_cast<int>(floor(g_mY.Pos() + 0.5f));
 
 Ellipse(pps->hdc, x - 20, y - 20, x + 20, y + 20);
 SetWindowOrgEx(pps->hdc, ptOrg.x, ptOrg.y, nullptr);

 if (g_mX.Moving() || g_mY.Moving()) {
  SetTimer(hwnd, 1, 30, InvalidateMe);
 }
}

We tick the motion variables to get their current locations, then tinker with our window origin because we're going to be drawing based on screen coordinates. We then draw a circle at the current animated position, and if the circle is still moving, we schedule a timer to draw the next frame.

Finally, we initialize our rest point before we show the window, so that the circle starts out at rest.

        g_ptRest = CalcRestPoint(hwnd);
        ShowWindow(hwnd, nShowCmd);

And that's it. Run the program and move it around. The circle will seek the center of the window, wherever it is.

(For extra credit, you can also add

UINT OnNCHitTest(HWND hwnd, int x, int y)
{
    UINT ht = FORWARD_WM_NCHITTEST(hwnd, x, y, DefWindowProc);
    if (ht == HTCLIENT) ht = HTCAPTION;
    return ht;
}
    HANDLE_MSG(hwnd, WM_NCHITTEST, OnNCHitTest);

so that the window can be dragged by its client area.)

Comments (11)
  1. Bobo says:

    Fixmes:

    1. TimerProc ahould be InvalidateMe

    2. Circle shakes around while dragging (drag window in s wide circle)

    [#1 is fixed, thanks. The circle also flickers a lot. Fixing these glitches I leave as an exercise. My goal was to get the general effect because it sounded fun. -Raymond]
  2. Exercise: rewrite the program so it uses Windows Animation Manager.

  3. James says:

    @Azarien: ew! That is cheating.

    It can be done without going there. If you got the knowledge that is.

    Hint: Games do it.

  4. I wonder why this was written in C++ when it doesn't use any C++ features? (using a class a simple struct is not "using C++ features")

    [If "struct with methods" is not a C++ feature, and it's not a C feature, then what is it? (I was not aware of the rule that says "You are not allowed to use any C++ features unless you use all of them." I can't find it in my copy of the C++11 standard.) -Raymond]
  5. err, Needs edit… (using a class as a simple struct is not "using C++ features")

  6. JDP says:

    Roysna: Probably because it's working off the scratch program, which was written in C++? I doubt Raymond would start with a whole new program for every sample he does.

  7. Simon Buchan says:

    Nice impl. on the motion-curve, but I generally prefer the simpler and easier to understand spring simulation method for that effect:

    DWORD lastTick;

    double target, position, velocity;

    bool InMotion() { return !velocity && position == target; }

    void Tick() {

      DWORD tick = GetTickCount();

      double seconds = (tick – lastTick);

      lastTick = tick;

      velocity += (target – position);

      velocity /= DAMPENING; // ~= 2.0 is good

      position += (velocity * seconds);

      if (abs(velocity) < 1.0 && abs(position – target) < 1.0) { // close enough: stop simulating

         velocity = 0;

         position = target;

      }

    }

    [The downside of this version is that it requires ticks to arrive at relatively high and steady frequency, since it is doing numerical integration. If, for example, the application takes a page fault and gets stuck on I/O for for 2 seconds, the ball will overshoot the target by a wide margin. -Raymond]
  8. Simon Buchan says:

    Some bugfixes: seconds should of course be (tick – lastTick) / 1000.0;, and InMotion() should be inverted: (velocity || position != target)

    Try it with DAMPENING less than 2.0 for a nice overshooting springy effect, or less than 1 to break the universe :) (really it should be velocity /= 1 + DAMPENING;, but that's no fun)

    Note also that the positions this gives you are absolute, you can straight up update the targets at any time to any value and the spring response: I assume the motion curve is doing the int x = g_ptRest.x + …; because it needed to be in relative to not wig out when moved.

  9. Neil says:

    If C had been invented in the UK, would we #include "maths.h" instead?

  10. Ian Boyd says:

    When i got my Nexus 4 (i.e. Android) last year, it drove me nuts trying to figure out why "momentum scrolling" didn't feel as nice as it did on the iPhone. i would flick a little, and it would scroll a little. i would flick a lot, and it would scroll a lot. i couldn't find anything obviously "wrong", yet it didn't "feel" right. Going back to the iPhone; flicking and scrolling, and timing, and counting, i realized what Apple does that is non-obvious, ignores physics, but "feels" better:

    Scrolling under it's own momentum lasts 3 seconds. Always. Whether you just flicked it a little, or you did a massive throw, the momentum scrolling will always take 3 seconds to come to a stop.

    You would think that a massive throw should cause the scrolling to last longer, and a little flick should only last a moment. And as a technically minded, computer programmer, who understands physics, you would be right; momentum and friction should lead to exponential speed decay.

    Except usability doesn't care about physics, it cares about what "feels" right.

    i've forgotten the math, but it turned out the math became much simpler when you decided the exponential decay parameters based on initial velocity and a known 3 second duration. It amounted to a simple multiplication factor each tick.

  11. Bart says:

    Resizing will not make the circle jump around but window movement will. I can't figure out why. Does anyone know why?

    Is it how the window position is updated?

Comments are closed.