Download Now
Forums

    Catapult

    For the past few weeks I've been working on writing and testing an object oriented encapsulation of the free open source SDL and NanoVG libraries. Below is a short video clip of one of my demo programs testing both the graphics and physics portion of my library.
    This demo is a Free Pascal pascal program allowing the user to draw hollow and solid physics objects then run them in a physics simulation. In the demo, shapes are stylized as crayon drawings.
    This program has been tested and runs well on Windows, Linux, Mac, and Raspberry Pi

    Video

    Here is a screen recording of the program in a scene simulating a catapult launching a ball.
    Video: Catapult


    Source Code

    The following is a listing of the entire unit defining this scene. Comments are provided to explain how it is written and how it works.
    unit Play.DrawPhysics;
    
    {$mode delphi}
    
    interface
    
    uses
      Tiny.System,
      Tiny.Application,
      Tiny.Graphics,
      Tiny.Widgets,
      Tiny.Widgets.Custom,
      Tiny.Physics,
      Tiny.Physics.Scene;
    
    { TDrawPhysics is a scene where users can draw hollow or solid physics objects
      using a simulated crayon style }
    
    type
      TDrawing = TArrayList<TPointD>;
    
      TDrawPhysics = class(TPhysicsScene)
      private
        FOutlinePen: IPen;
        FSolidPen: IPen;
        FBlueCrayon: IBitmapBrush;
        FRedCrayon: IBitmapBrush;
        FBackground: IBitmap;
        FGlyph: IFont;
        FDraw: TGlyphButton;
        FFill: TGlyphButton;
        FGrab: TGlyphButton;
        FErase: TGlyphButton;
        FSync: TGlyphButton;
        FFullscreen: TGlyphButton;
        FGraph: TGlyphButton;
        FStats: TPerformanceGraph;
        FDrawing: TDrawing;
        FIsDrawing: Boolean;
        FIsFilling: Boolean;
        FIsErasing: Boolean;
        procedure GenerateWidgets;
        procedure SyncClick(Sender: TObject);
        procedure FullscreenClick(Sender: TObject);
        procedure GraphClick(Sender: TObject);
        procedure ExitClick(Sender: TObject);
        procedure GrabClick(Sender: TObject);
      protected
        procedure DoMouseDown(var Args: TMouseArgs); override;
        procedure DoMouseMove(var Args: TMouseArgs); override;
        procedure DoMouseUp(var Args: TMouseArgs); override;
        function DrawCustomBody(Body: TBody): Boolean; override;
        procedure Load; override;
        procedure Render(Width, Height: Integer; const Time: Double); override;
      end;
    
    implementation
    
    const
      Outline = Pointer(1);
      Fill = Pointer(2);
      Thick = 10;
      Thin = 6;
    
    procedure TDrawPhysics.GrabClick(Sender: TObject);
    begin
      { Allow the user to move physics objects if the grab button is toggled }
      GrabBodies := FGrab.Down;
    end;
    
    procedure TDrawPhysics.DoMouseDown(var Args: TMouseArgs);
    var
      P: TPointF;
    begin
      if (Widget.FindWidget(Args.X, Args.Y) = nil) and (Args.Button = buttonLeft) then
        if FDraw.Down or FFill.Down then
        begin
          { If hollow or solid drawing }
          if FDraw.Down then
            FIsDrawing := True
          else
            FIsFilling := True;
          { Start capturing drawing points }
          FDrawing.Clear;
          P := PointToStudio(Args.X, Args.Y);
          FDrawing.Push(P);
          Mouse.Visible := False;
          Args.Handled := True;
        end
        else if FErase.Down then
        begin
          { If erasing }
          FIsErasing := True;
          Args.Handled := True;
        end;
      if not Args.Handled then
        inherited DoMouseDown(Args);
    end;
    
    procedure TDrawPhysics.DoMouseMove(var Args: TMouseArgs);
    var
      A, B: TPointF;
      S: TShape;
    begin
      if FIsDrawing or FIsFilling then
      begin
        { If hollow or solid drawing }
        A := PointToStudio(Args.X, Args.Y);
        B := FDrawing.Last;
        if A.Distance(B) > 10 then
          FDrawing.Push(A);
        Mouse.Visible := False;
        Args.Handled := True;
      end
      else if FIsErasing then
      begin
        { If erasing }
        A := PointToStudio(Args.X, Args.Y);
        S := ShapeNearPoint(A.X, A.Y, 10);
        if (S <> nil) and (S.Body.Kind = bodyDynamic) then
          S.Body.Free;
        Args.Handled := True;
      end
      else
        { Turn off the mouse cursor if there is no widget underneath the mouse }
        Mouse.Visible := Widget.FindWidget(Args.X, Args.Y) <> nil;
      if not Args.Handled then
        inherited DoMouseMove(Args);
    end;
    
    procedure TDrawPhysics.DoMouseUp(var Args: TMouseArgs);
    var
      B: TBody;
      S: TShape;
      I: Integer;
    begin
      if (Args.Button = buttonLeft) and (FIsDrawing or FIsFilling) then
      begin
        if FDrawing.Length > 1 then
        begin
          { If we were drawing then }
          B := Space.NewBody;
          if FIsDrawing then
          begin
            { Add a hollow physics object }
            for I := 1 to FDrawing.Length - 1 do
            begin
              S := B.NewSegment(FDrawing[I - 1], FDrawing[I], 5);
              S.Density := 2;
              S.Friction := 0.6;
              S.Elasticity := 0.4;
              S.Categories := 1;
            end;
            B.UserData := Outline;
          end
          else
          begin
            { Add a solid physics object }
            S := B.NewPolygon(@FDrawing.Items[0], FDrawing.Length);
            S.Density := 4;
            S.Friction := 0.6;
            S.Elasticity := 0.4;
            S.Categories := 1;
            B.UserData := Fill;
          end;
          FIsDrawing := False;
          FIsFilling := False;
        end;
        FDrawing.Clear;
        Args.Handled := True;
      end
      else if (Args.Button = buttonLeft) and FIsErasing then
      begin
        { Else stop erasing }
        FIsErasing := False;
        Args.Handled := True;
      end;
      if not Args.Handled then
        inherited DoMouseUp(Args);
    end;
    
    procedure TDrawPhysics.SyncClick(Sender: TObject);
    begin
      Application.VSync := FSync.Down;
    end;
    
    procedure TDrawPhysics.FullscreenClick(Sender: TObject);
    begin
      Application.Fullscreen := FFullscreen.Down;
    end;
    
    procedure TDrawPhysics.GraphClick(Sender: TObject);
    begin
      FStats.Visible := FGraph.Down;
    end;
    
    procedure TDrawPhysics.ExitClick(Sender: TObject);
    begin
      Application.Terminate;
    end;
    
    function TDrawPhysics.DrawCustomBody(Body: TBody): Boolean;
    var
      S: TShape;
      G: TPolygon;
      P: TPointF;
      I: Integer;
    begin
      { Return true if we want to draw the physics ourselves }
      Result := True;
      { Don't draw ground plane }
      if Body = Space.Ground then
        Exit;
      if Body.UserData = Outline then
      begin
        { Draw the hollow body }
        for S in Body.Shapes do
        begin
          P := Body.BodyToWorld(S.AsSegment.A);
          Canvas.MoveTo(P.X, P.Y);
          P := Body.BodyToWorld(S.AsSegment.B);
          Canvas.LineTo(P.X, P.Y);
        end;
        P := Body.Position;
        FBlueCrayon.Offset := P;
        FBlueCrayon.Angle := Body.Angle;
        FOutlinePen.Width := Thin;
        Canvas.Stroke(FOutlinePen, True);
        FOutlinePen.Width := Thick;
        Canvas.Stroke(FOutlinePen);
      end
      else if Body.UserData = Fill then
      begin
        { Draw the solid body }
        G := Body.Shape.AsPolygon;
        for I := 0 to G.VertCount - 1 do
        begin
          P := Body.BodyToWorld(G.Vert[I]);
          if I = 0 then
            Canvas.MoveTo(P.X, P.Y)
          else
            Canvas.LineTo(P.X, P.Y);
        end;
        Canvas.ClosePath;
        P := Body.Position;
        FRedCrayon.Offset := P;
        FRedCrayon.Angle := Body.Angle;
        Canvas.Fill(FRedCrayon, True);
        FSolidPen.Width := Thin;
        Canvas.Stroke(FSolidPen);
        FSolidPen.Width := Thick;
        Canvas.Stroke(FSolidPen);
      end
      else
        Result := False;
    end;
    
    procedure TDrawPhysics.GenerateWidgets;
    begin
      { Generate the toolbar at the top of our window }
      with Widget.Add<THBox> do
      begin
        Sector := 2;
        Margin := -5;
        Fade := 0.15;
        with This.Add<TVBox> do
        begin
          Margin := 0;
          with This.Add<THBox> do
          begin
            Align := alignCenter;
            Margin := 0;
            with This.Add<TGlyphButton>(FDraw) do
            begin
              CanToggle := True;
              Text := '󰽉';
              Hint := 'Draw shapes outlines';
              Down := true;
              Group := 1;
            end;
            with This.Add<TGlyphButton>(FFill) do
            begin
              CanToggle := True;
              Text := '󱠓';
              Hint := 'Draw solid shapes';
              Group := 1;
            end;
            with This.Add<TGlyphButton>(FGrab) do
            begin
              CanToggle := True;
              Text := '󰆽';
              Hint := 'Grab shapes';
              OnClick := GrabClick;
              Group := 1;
            end;
            with This.Add<TGlyphButton>(FErase) do
            begin
              CanToggle := True;
              Text := '󰙂';
              Hint := 'Erase shapes';
              Group := 1;
            end;
            This.Add<TSpacer>;
            with This.Add<TGlyphButton>(FSync) do
            begin
              CanToggle := True;
              Text := '󰷛';
              Hint := 'Unlock verticle sync';
              OnClick := SyncClick;
            end;
            with This.Add<TGlyphButton>(FFullscreen) do
            begin
              Down := Application.Fullscreen;
              CanToggle := True;
              Text := '󰊓';
              Hint := 'Switch to fullscreen mode';
              OnClick := FullscreenClick;
            end;
            with This.Add<TGlyphButton>(FGraph) do
            begin
              CanToggle := True;
              Text := '󰄧';
              Hint := 'Show performance information';
              OnClick := GraphClick;
            end;
            This.Add<TSpacer>;
            with This.Add<TGlyphButton> do
            begin
              Text := '󰅚';
              OnClick := ExitClick;
              Hint := 'Exit this program ESC';
            end;
          end;
          with This.Add<TPerformanceGraph>(FStats) do
          begin
            Align := alignCenter;
            Width := 500;
            Height := 50;
            Margin := 5;
            Visible := False;
          end;
        end;
      end;
      Mouse.Visible := False;
    end;
    
    procedure TDrawPhysics.Load;
    begin
      inherited Load;
      { Create our pens and brushes }
      FOutlinePen := NewPen;
      FOutlinePen.LineCap := capRound;
      FOutlinePen.LineJoin := joinRound;
      FOutlinePen.Width := Thick;
      FBlueCrayon := NewBrush(Canvas.LoadBitmap('blue-crayon', '../assets/blue-crayon.png'));
      FBlueCrayon.Opacity := 0.5;
      FOutlinePen.Brush := FBlueCrayon;
      FSolidPen := NewPen;
      FSolidPen.LineCap := capRound;
      FSolidPen.LineJoin := joinRound;
      FSolidPen.Width := Thick;
      FRedCrayon := NewBrush(Canvas.LoadBitmap('red-crayon', '../assets/red-crayon.png'));
      FRedCrayon.Opacity := 0.5;
      FSolidPen.Brush := FRedCrayon;
      FBackground := Canvas.LoadBitmap('meadow', '../assets/meadow.png');
      { Generate our user interface controls }
      GenerateWidgets;
      { Generate our cursor }
      FGlyph := Canvas.LoadFont('glyph');
      FGlyph.Color := colorBlack;
      FGlyph.Size := 32;
      FGlyph.Align := fontCenter;
      FGlyph.Layout := fontMiddle;
      { Setup our physics }
      GenerateStudioWalls;
      Space.Gravity := Vect(0, 1000);
    end;
    
    procedure TDrawPhysics.Render(Width, Height: Integer; const Time: Double);
    var
      P: TPointF;
      S: string;
      I: Integer;
    begin
      inherited Render(Width, Height, Time);
      { Make the physics rendering fill the entire window }
      ScaleToStudio;
      { Draw the background }
      Canvas.DrawImage(FBackground, 0, 0);
      { Draw the physics objects }
      DrawPhysics;
      { If we are currently drawing something then render it manually }
      if FDrawing.Length > 1 then
      begin
        { Create the path on the canvas }
        for I := 0 to FDrawing.Length - 1 do
          with FDrawing[I] do
            if I = 0 then
              Canvas.MoveTo(X, Y)
            else
              Canvas.LineTo(X, Y);
        if FIsDrawing then
        begin
          { Draw a blue outline for hollow shapes }
          FBlueCrayon.Offset := NewPointF(0, 0);
          FBlueCrayon.Angle := 0;
          FOutlinePen.Width := Thin;
          Canvas.Stroke(FOutlinePen, True);
          FOutlinePen.Width := Thick;
          Canvas.Stroke(FOutlinePen)
        end
        else
        begin
          { Draw a red outline for solid shapes }
          FRedCrayon.Offset := NewPointF(0, 0);
          FRedCrayon.Angle := 0;
          FSolidPen.Width := Thin;
          Canvas.Stroke(FSolidPen, True);
          FSolidPen.Width := Thick;
          Canvas.Stroke(FSolidPen)
        end;
      end;
      P := NewPointF(Mouse.X, Mouse.Y);
      if Widget.FindWidget(P.X, P.Y) = nil then
      begin
        { Draw a custom cursor based on a glyph }
        if FDraw.Down then
          S := '󰃣'
        else if  FFill.Down then
          S := '󰃣'
        else if FGrab.Down then
          S := FGrab.Text
        else
          S := FErase.Text;
        P := PointToStudio(P);
        { Make a soft black drop shadow }
        FGlyph.Color := colorBlack;
        FGlyph.Blur := 2;
        Canvas.DrawText(FGlyph, S, P.X, P.Y);
        Canvas.DrawText(FGlyph, S, P.X, P.Y);
        Canvas.DrawText(FGlyph, S, P.X, P.Y);
        { Then draw the cursor in white }
        FGlyph.Color := colorWhite;
        FGlyph.Blur := 0;
        Canvas.DrawText(FGlyph, S, P.X, P.Y);
      end;
      { Record frame information to the performance graph }
      FStats.Update;
      { Reset the physics view matrix and draw the user interface controls }
      Canvas.Matrix.Identity;
      Widget.Render(Width, Height, Time);
    end;
    
    end.