Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Math for slow image zoom

I have an .bmp image with a comic book layout. Currently my code works like this. If I right click and hold down mouse button i can draw a marquee type box around one of the frames on the comic book page. When I release the button it will zoom into that frame. But its instant. And I would like for it to have an animation effect.

Thus instead of going and setting the values of PicRect to the "END VALUE"

PicRect.Left
PicRect.right
PicRect.top
PicRect.bottom

as seen in code below, I need a way to slowly get there, Some kind of while loop that sets those values a little at a time till untill it gets to the "end value" But I am not 100% sure on how this math is working. Nor do any of my while loop tries do anything but zoom in way too far. This is the procedure.

procedure TZImage.MouseUp(Button: TMouseButton; Shift: TShiftState;
                      X, Y: Integer);
    var coef:Double;
    t:integer;
begin
   if FMouse=mNone then Exit;
   if x>ShowRect.Right then x:=ShowRect.Right;
   if y>ShowRect.Bottom then y:=ShowRect.Bottom;
   if FMouse=mZoom then begin  //calculate new PicRect
     t:=startx;
     startx:=Min(startx,x);
     x:=Max(t,x);
     t:=starty;
     starty:=Min(starty,y);
     y:=Max(t,y);
     FMouse:=mNone;
     MouseCapture:=False;
//enable the following if you want to zoom-out by dragging in the opposite direction}
    {     if Startx>x then begin
            DblClick;
            Exit;
         end;}
         if Abs(x-startx)<5 then Exit;
         if (x - startx < y - starty) then
         begin
           while (x - startx < y - starty) do
           begin
              x := x + 100;
              startx := startx - 100;
           end;
         end
         else if (x - startx > y - starty) then
         begin
            while (x - startx > y - starty) do
            begin
                y := y + 100;
                starty := starty - 100;
            end;
         end;


    //This is were it sets the zoom info. This is were
    //I have to change to slowly get the PICRECT.Left/right/top/bottom
         if (PicRect.Right=PicRect.Left)
         then
            coef := 100000
         else
            coef:=ShowRect.Right/(PicRect.Right-PicRect.Left);
         PicRect.Left:=Round(PicRect.Left+startx/coef);
         PicRect.Right:=PicRect.Left+Round((x-startx)/coef);
         if (PicRect.Bottom=PicRect.Top)
         then
            coef := 100000
         else
            coef:=ShowRect.Bottom/(PicRect.Bottom-PicRect.Top);
         PicRect.Top:=Round(PicRect.Top+starty/coef);
         PicRect.Bottom:=PicRect.Top+Round((y-starty)/coef);
       end;
       if FMouse=mDrag then begin
         FMouse:=mNone;
         Canvas.Pen.Mode:=pmCopy;
         Screen.Cursor:=crDefault;
       end;
       Invalidate;
    end;

I believe this can be done in the code above. but also wanted to add this incase it helps.

type
    TZImage = class(TGraphicControl)
  private
    FBitmap        : TBitmap;
    PicRect        : TRect;
    ShowRect       : TRect;
    FShowBorder    : boolean;
    FBorderWidth   : integer;
    FForceRepaint  : boolean;
    FMouse         : (mNone, mDrag, mZoom);
    FProportional  : boolean;
    FDblClkEnable  : boolean;
    startx, starty,
    oldx, oldy     : integer;

thanks for any help in getting this to work.

like image 305
Glen Morse Avatar asked Jun 09 '12 22:06

Glen Morse


2 Answers

Do not use a while loop to update the zoom level, because of two problems:

  1. Different animation durations, because the speed of the zoom operation depends on the speed of the code, the current CPU usage, the CPU model, etc...
  2. Blocking of te GUI, because even when you use a delay (e.g. with Sleep), the code is running in the main thread and the program turn out being unresponsive.

Like sarnold and Elling already said: use a timing device (e.g. a TTimer) to perform a piece of the total zoom operation on every interval. Now, there are two ways to calculate those pieces:

  1. Divide the total distance to bridge into a fixed number of small distances, set the timer's interval to the total duration divided by that number and handle the sum of all processed distances on every interval. The drawback with this method is twofold:
    • A timer's set interval is an approximation and will not be exact because of various reasons, one of them being dependend on the Windows messaging system,
    • A possible rough or unsmooth animation because of it.
  2. Recalculate the part of the distance to bridge at every interval. That way the animation will appear smooth always, whether the next interval takes two times more or not.

I used the second solution in this answer to your related question, from which the following relevant snippets are taken:

procedure TZImage.Animate(Sender: TObject); 
var 
  Done: Single; 
begin 
  Done := (GetTickCount - FAnimStartTick) / FAnimDuration; 
  if Done >= 1.0 then 
  begin 
    FAnimTimer.Enabled := False; 
    FAnimRect := FCropRect; 
  end 
  else 
    with FPrevCropRect do 
      FAnimRect := Rect( 
        Left + Round(Done * (FCropRect.Left - Left)), 
        Top + Round(Done * (FCropRect.Top - Top)), 
        Right + Round(Done * (FCropRect.Right - Right)), 
        Bottom + Round(Done * (FCropRect.Bottom - Bottom))); 
  Invalidate; 
end; 

procedure TZImage.Zoom(const ACropRect: TRect); 
begin 
  FPrevCropRect := FCropRect; 
  FAnimRect := FPrevCropRect; 
  FCropRect := ACropRect; 
  FAnimStartTick := GetTickCount; 
  FAnimTimer.Enabled := True; 
end; 

Explanation:

  • FCropRect is the new zooming rectangle, and FPrevCropRect is the previous one,
  • FAnimRect is the rectangle in between both, depending on the progress of the animation,
  • FAnimStartTick is the time on which the zoom operation is started with a call to Zoom,
  • at every timer interval (set to 15 msec, ~67Hz refresh rate), Animate is called,
  • Done is the percentage of animation progress,
  • Invalidate triggers a repaint which draws the graphic into FAnimRect.
like image 45
NGLN Avatar answered Sep 21 '22 19:09

NGLN


I've got a few suggestions; I'm not sure that they'll be sufficient to solve your problem, but I hope it helps you get there.

First, your while loops are doing a fair amount of funny fiddling:

if (x - startx < y - starty) then
     begin
       while (x - startx < y - starty) do
       begin
          x := x + 100;
          startx := startx - 100;
       end;
     end
else if (x - startx > y - starty) then
     /* similar code */

Note that x - start == y - starty case is being completely overlooked. I don't know if this matters.

Second, this could probably be re-written without a loop. I'm guessing here, it'd take a little testing to see if this is correct, but this feels like the right path:

foo := (x - startx) - (y - starty)
if (foo > 200 || foo < -200)
    bar = foo / 200  # I assume integer truncation
    x += bar * 100
    startx += bar * 100

I'm not entirely sure why you're trying to get (x-startx) - (y-starty) to within 200 of each other; there may be something better still.

This section of code is a little confusing:

if (PicRect.Right=PicRect.Left)
     then
        coef := 100000
     else
        coef:=ShowRect.Right/(PicRect.Right-PicRect.Left);
     PicRect.Left:=Round(PicRect.Left+startx/coef);
     PicRect.Right:=PicRect.Left+Round((x-startx)/coef);
     if (PicRect.Bottom=PicRect.Top)
     then
        coef := 100000
     else
        coef:=ShowRect.Bottom/(PicRect.Bottom-PicRect.Top);
     PicRect.Top:=Round(PicRect.Top+starty/coef);
     PicRect.Bottom:=PicRect.Top+Round((y-starty)/coef);
   end;

Is coef supposed to be overwritten from the earlier? Or, should you instead calculate a coefx and coefy and then pick the (larger? smaller? closer to 100000?) value to serve for both the .Left, .Right, .Top, and .Bottom calculations? I have to think that this code, as it stands, is more likely to lead to awkward stretching of your content in a way that will likely annoy users and authors both.

Now, to address the real reason why you're here, animating the zoom -- you'll probably need to drastically change something. I feel like your while loops were probably intended to do the zooming, but they come after the coef calculations, so I assumed they were meant for something else. But, once you do figure out where exactly to place the loop to calculate different coef values over a range from "no zoom" to "final zoom", you'll also need to add calls to repaint the display -- or, depending upon your environment, maybe need to add some callback code fired by a timer every 50 ms or something to repaint with updated coef values.

like image 129
sarnold Avatar answered Sep 23 '22 19:09

sarnold