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.