Implement Zoom And Pan on TSVG2Image

This example shows how Zoom and Pan can be implemented on the TSVG2Image control.

In SVG zooming and panning is controlled by the viewPort, viewBox and preserveAspectRatio attributes on the outer SVG element.

The outer SVG element is the first SVG element in the xml file. In most cases, there is only one SVG element within an SVG xml file.

View port

The viewPort is the window in which the SVG image is displayed. The viewPort is defined by the dimensions of the outer SVG element, so the width and height attribute or, if width and height are expressed as percentages, the client rectangle of the SVG’s container.

For our purposes we will override the width and height attributes of the outer SVG element and set them to 100%, so width = 100% and height=100%. With these settings the viewPort will be equal to the ClientRect of the TSVG2Image control.

View box

The viewBox attribute selects the part of the SVG image that is displayed. This is an optional attribute, not all SVG images have a viewBox defined.

For zooming and panning we need a viewBox, so if the SVG image has no viewBox defined, we will create one and initially set it to the same size as the viewPort.

Preserve aspect ratio

The preserveAspectRatio defines how the viewBox is aligned within the viewPort. We want to preserve the aspect ratio of the SVG image and we want the center it. For this we need the setting xMidYMid.

Basics of zooming and panning in SVG

For zooming and panning an SVG image we only need to modify the viewBox attribute.

For panning we simply change the top left point of the viewBox rectangle.

For zooming we change with equal proportion the width and height of the viewBox rectangle. The smaller the viewBox, the more we are zooming in.

Structure of the example application

  • A form with on it one TSVG2Image set to align “alClient” to the form
  • Property “DoubleBuffered” on the form is set to “True”, to ensure smooth zooming and panning
  • Property “AutoViewbox” on the TSVG2Image is set to “True”, so the width and height of the outer SVG will be automatically set to 100%
  • Property “AspectRatioAlign” will be set to “arXMidYMid” to preserve the aspect ratio of the SVG image and center it within the viewPort.
  • Property “AspectRatioMeetOrSlice” can either be “Meet” or “Slice”
  • Event “OnAfterParse” on TSVGImage will be used to read the “viewBox” attribute from the outer SVG element and store it in form variable “FViewBox”. If it does not exist, we will initialize FViewBox with the size of the viewPort.
  • Event “OnDblClick” on TSVGImage will allow loading of a new SVG image
  • Events “OnMouseDown”, “OnMouseMove” and “OnMouseUp” on TSVGImage will be used to pan the SVG image. Panning will be proportional to the zoom level.
  • Event “OnMouseWheel” on the form will be used to increase or decrease the zoom on the SVG image. The focus of the zoom will be the position of the mouse pointer on the SVG image.

So after creating the application project and setting the properties on the form and the TSVG2Image we need to implement the event handlers.

OnAfterParse

After parsing the SVG xml file an in-memory node tree data structure will be available for us to read and modify SVG elements attributes.

We can access the element nodes through the TSVG2Image “SVGRoot” property.

The “SVG” property of SVGRoot will point to the outer SVG element of the SVG image.

In the code below, we assign the viewBox attribute value to the form variable FViewBox. If no viewBox is defined on the outer SVG we initialize FViewBox with the size of the viewPort.

procedure TForm1.SVG2Image1AfterParse(Sender: TObject);
var
  SVG: ISVG;
  CR: TRect;
begin
  if not assigned(SVG2Image1.SVGRoot) then
    Exit;

  SVG := SVG2Image1.SVGRoot.SVG;

  if not assigned(SVG) then
    Exit;

  if SVG.ViewBox.IsUndefined then
  begin
    // No viewBox so we will create one with the same size as the viewPort.
    // The viewPort is defined by the dimensions of the outer SVG element.

    CR := SVG2Image1.ClientRect;

    FViewBox := SVG2Image1.SVGRoot.CalcIntrinsicSize(SVGRect(0, 0, CR.Width, CR.Height));
  end else
    FViewBox := SVG.ViewBox;
end;

Utility functions

The basis for all calculations in the application is the conversion from Mouse point to ViewBox point. The formula for this depends on the preserveAspectRatio settings. If you choose an other setting than xMidYMid the calculations will be different.

CalcZoomFactor

First we create a function to calculate the current zoom factor.

Depending on the preserveAspectRatio setting “Meet” or “Slice” and the aspect ratios of the viewBox or the viewPort, the zoom factor is calculated from the width or height ratio between the viewBox and the viewPort.

function TForm1.CalcZoomFactor: TSVGFloat;
var
  ViewPort: TRect;
  ViewPortRatioXY,
  ViewBoxRatioXY: TSVGFloat;
begin
  // This calculates the zoom factor between between the viewPort and the viewBox

  ViewPort := SVG2Image1.ClientRect;

  // We have to take into consideration the aspect ratio settings

  ViewPortRatioXY := ViewPort.Width / ViewPort.Height;
  ViewBoxRatioXY := FViewBox.Width / FViewBox.Height;

  if SVG2Image1.AspectRatioMeetOrSlice = arSlice then
  begin

    if ViewPortRatioXY > ViewBoxRatioXY then
      Result := FViewBox.Width / ViewPort.Width
    else
      Result := FViewBox.Height / ViewPort.Height;

  end else begin

    if ViewPortRatioXY > ViewBoxRatioXY then
      Result := FViewBox.Height / ViewPort.Height
    else
      Result := FViewBox.Width / ViewPort.Width;

  end;
end;

CalcViewBoxPt

Next we can create a function to calculate a viewBox point from a mouse point. This calculation is based on the xMidYMid setting.

So basically we calculate the distance of the mousepoint to the middle of the viewPort. Multiply this by the zoom factor to convert it to viewBox coordinates and then subtract it from the middle of the viewBox and finally add the top left point of the viewBox.

function TForm1.CalcViewBoxPt(const aMousePt: TPoint): TSVGPoint;
var
  ViewPort: TRect;
  Zoom: TSVGFloat;
begin
  ViewPort := SVG2Image1.ClientRect;

  Zoom := CalcZoomFactor;

  // Convert mousepoint to viewboxpoint

  Result.X := FViewBox.Left + FViewBox.Width/2 - (ViewPort.Width/2 - aMousePt.X) * Zoom;
  Result.Y := FViewBox.Top + FViewBox.Height/2 - (ViewPort.Height/2 - aMousePt.Y) * Zoom;
end;

SVGRepaint

With the SVGRepaint procedure we set the modified form variable FViewBox on the outer SVG element and force a repaint of the TSVG2Image.

procedure TForm1.SVGRepaint;
var
  SVG: ISVG;
begin
  // Get the outer SVG element

  if not assigned(SVG2Image1.SVGRoot) then
    Exit;

  SVG := SVG2Image1.SVGRoot.SVG;

  if not assigned(SVG) then
    Exit;

  // Set the viewBox attribute on the outer SVG element

  SVG.ViewBox := FViewBox;
  SVG2Image1.Repaint;
end;

Now we are ready to implement the event handlers.

OnMouseDown

On the OnMouseDown event we start panning if the left mouse button is pressed and it is not a double click.

procedure TForm1.SVG2Image1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if (mbLeft = Button) and not(ssDouble in Shift) then
  begin
    // Save the mousedown point on mousedown

    FMouseDown := True;
    FMousePt := Point(X, Y);
  end;
end;

OnMouseMove

If we move the mouse we want to display the viewBox coordinates corresponding with the mouse pointer on the form caption.

If we are panning (FMouseDown = True) we calculate the new viewBox position from the delta mouse position.

procedure TForm1.SVG2Image1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  Delta: TPoint;
  ViewPort: TRect;
  Zoom: TSVGFLoat;
  ViewBoxPt: TSVGPoint;
begin
  // Convert the mouse coords to viewBox coords and display in caption
  // Move the viewBox if the mouse btn is pressed

  ViewBoxPt := CalcViewBoxPt(Point(X, Y));
  Caption := Format('ViewBox Pt: %3.1f %3.1f', [ViewBoxPt.X, ViewBoxPt.Y]);

  if FMouseDown then
  begin
    // Compensate the mousemovent with the zoom factor so the image moves at
    // the same rate as the mouse

    ViewPort := SVG2Image1.ClientRect;

    Zoom := CalcZoomFactor;

    Delta := Point(FMousePt.X - X, FMousePt.Y - Y);

    // Calculate the new viewport position.

    FViewBox.Left := FViewBox.Left + Delta.X * Zoom;
    FViewBox.Right := FViewBox.Right + Delta.X * Zoom;
    FViewBox.Top := FViewBox.Top + Delta.Y * Zoom;
    FViewBox.Bottom := FViewBox.Bottom + Delta.Y * Zoom;

    // Save the new mousedown position

    FMousePt := Point(X, Y);

    // Set the new viewBox and repaint the SVG image.

    SVGRepaint;
  end;
end;

OnMouseUp

On the mouseup event we reset the form variables that control panning.

procedure TForm1.SVG2Image1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  FMousePt := Point(0, 0);
  FMouseDown := False;
end

OnMouseWheel

On every change of the mousewheel we increase or decrease the zoom level with a constant factor F.

  // Zoom with arbitrary factor

  Sign := WheelDelta / abs(WheelDelta);
  F := -Sign * 0.05;

So for zooming in and out we simply increase the width and height of the viewBox with this factor.

FViewBox.Width = FViewBox.Width * (1 + F)
FViewBox.Height = FViewBox.Height * (1 + F)

The challenge now is to focus the zoom onto the current mousepointer. This means that we want the viewBox point under the mousepointer to remain the same after changing the zoom level. For this we need to move the viewBox a bit after zooming. This is what we now have to calculate.

The viewBox points can be calculated from the mousepoint, in case of xMidYMid we have the following formula’s:


X = ViewBox.Left + ViewBox.Width/2 + (MouseX - ViewPort.Width/2) * Zoom
Y = ViewBox.Top + ViewBox.Height/2 + (MouseY - ViewPort.Height/2) * Zoom

After changing the zoom with a factor F, the formula’s can be written as follows:

X = NewViewBox.Left + ViewBox.Width/2 * (1 + F) + (MouseX - ViewPort.Width/2) * Zoom * (1 + F)
Y = NewViewBox.Top + ViewBox.Height/2 * (1 + F) + (MouseY - ViewPort.Height/2) * Zoom * (1 + F)

Because X and Y must remain the same before and after changing the zoom level, we can now calculate the values of NewViewBox.Left and NewViewBox.Top.

NewViewBox.Left = ViewBox.Left - ViewBox.Width/2 * F - (MouseX - ViewPort.Width/2) * Zoom * F
NewViewBox.Top = ViewBox.Top - ViewBox.Height/2 * F - (MouseY - ViewPort.Height/2) * Zoom * F

So the complete MouseWheel event handler looks like this:

procedure TForm1.FormMouseWheel(Sender: TObject; Shift: TShiftState;
  WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
var
  P: TPoint;
  F: TSVGFloat;
  Sign: TSVGFloat;
  ViewPort: TRect;
  ViewBoxOriginal: TSVGRect;
  Zoom: TSVGFloat;
begin
  // Zoom the SVG in or out on every change of the mousewheel

  if WheelDelta = 0 then
    Exit;

  // Zoom with arbitrary factor

  Sign := WheelDelta / abs(WheelDelta);
  F := -Sign * 0.05;

  ViewBoxOriginal := FViewBox;

  ViewPort := SVG2Image1.ClientRect;

  Zoom := CalcZoomFactor;

  P := SVG2Image1.ScreenToClient(MousePos);

  FViewBox.Left := ViewBoxOriginal.Left - ViewBoxOriginal.Width/2 * F 
    - (P.X - ViewPort.Width/2) * Zoom * F;

  FViewBox.Top := ViewBoxOriginal.Top - ViewBoxOriginal.Height/2 * F 
    - (P.Y - ViewPort.Height/2) * Zoom * F;

  FViewBox.Right := FViewBox.Left + ViewBoxOriginal.Width * (1 + F);
  FViewBox.Bottom := FViewBox.Top + ViewBoxOriginal.Height * (1 + F);

  SVGRepaint;
end;


OnDblClick

Finally we create an even handler for loading an SVG file into the TSVG2Image control.

procedure TForm1.SVG2Image1DblClick(Sender: TObject);
begin
  // Load a new SVG image by dubbel clicking the TSVG2Image

  if OpenDialog1.Execute then
  begin
    // Clear any SVG present in the SVG stringlist property, because it has
    // priority over the SVG filename property

    SVG2Image1.SVG.Clear;

    FViewBox := TSVGRect.CreateUndefined;

    SVG2Image1.Filename := OpenDialog1.FileName;
  end;
end;

The complete source code of this example project can be found here.