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.