[SDL] Very Simple Bouncy Ball Simulation Tutorial

Whether you're a newbie or an experienced programmer, any questions, help, or just talk of any language will be welcomed here.

Moderator: Coders of Rage

Post Reply
User avatar
xiphirx
Chaos Rift Junior
Chaos Rift Junior
Posts: 324
Joined: Mon Mar 22, 2010 3:15 pm
Current Project: ******** (Unkown for the time being)
Favorite Gaming Platforms: PC
Programming Language of Choice: C++
Contact:

[SDL] Very Simple Bouncy Ball Simulation Tutorial

Post by xiphirx »

Bouncy Ball Physics Simulation Tutorial
Getting the very basic grasps of things

Hello! This is my first tutorial. It sprouted from being inspired by TheAustech's dynamic 2d lighting tutorial here (good read).

The purpose of this tutorial is to take your knowledge of 2d Vectors and apply it to something, well, "meaningful". We will create a simple simulation, that may not be 100% correct in the physics sense, but it gives you some sort of base to start off with that I hope you find helpful :)

NOTE: I am almost sure some of the physics here is completely wrong, however, it gives a good introduction in my opinion.

Required
Knowledge of C++ (at least up to inheritance, or classes)
A C++ IDE/Compiler/Whatever you use of choice
SDL library (this code should be easily transferrable to any API)
SDL_GFX library (OPTIONAL, I only use it for easy circles :3)
Understanding of vectors (not std::vector, math vectors, the ones with direction and magnitude ;P)
Knowledge of Trigonometry

Getting Started
Before you continue on with the tutorial, please go ahead and create a brand new empty project, link the SDL libraries (or whatever you wish to use), and compile a simple "Hello World" just to make sure things are working.

The Coding Plan (I hope I get criticism for this!)
We will have a simulation class, that will handle our events (in this case, closing the window :P), update the balls, and render the output to our screen. It's a sort of application class that I like to do in each of my projects.

The simulation class will contain a vector (std::vector here) of a ball class, which we will create as well.

Each ball class will handle collision on its own.

Enough talk! Programming time

We will start with main.cpp

NOTE: I explain everything through comments

main.cpp

Code: Select all

//main.cpp - entry point

/*
	We need math.h for rand
	ctime for time()
	I believe D:
*/
#include <math.h>
#include <ctime>

//Main simulation class
#include "simulation.h"

int main(int argc, char* args[])
{
	srand(time(NULL)); // Make sure rand() returns truly random numbers
	simulation bouncyBalls(1024, 768, 50); // Create the simulation
	return bouncyBalls.loop(); // Loop the simulation, exit when the loop is done
}
simulation.h

Code: Select all

//Define guards
#ifndef SIMULATION_H
#define SIMULATION_H

#include "SDL.h" // Main SDL
#include "ball.h" // ball class, cannot use forward declaration here (look below)
#include "vec2.h" // 2d vector class, cannot use forward declaration here (look below)

#include <vector> // We store the balls in a vector

using namespace std; // Dont yell at me for this, I use it for cleaner code

class simulation
{
public:
	simulation(unsigned short w, unsigned short h, unsigned short num); //w = width of window, h = height of window, num = number of balls
	~simulation(){}; // Simple destructor

	int  loop(); // Main loop, will call other functions
	void handleEvents(); // Handle keyboard, mouse input
	void update(); // Updates the balls (calls the balls' update function
	void render(); // Render the balls to the screen

private:
	SDL_Surface * screen; // Main screen used for rendering

	SDL_Event events; // Event queue

	bool quit; // Quit flag, program exits when true
	
	float delta; // This along with thisTime and lastTime are all used for delta timing.
	float thisTime; // Holds the current time
	float lastTime; // Holds the previous time

	vector<ball> balls; // Vector containing all of the balls we want

	vec2 gravity; // An abstract force used in the simulation :D
};

#endif
simulation.cpp

Code: Select all

#include "SDL.h"
#include "SDL_gfxPrimitives.h"

#include "simulation.h"
#include "ball.h"


simulation::simulation(unsigned short w, unsigned short h, unsigned short num)
{
	// We only need video and timing
	SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);
	
	// Set the caption!
	SDL_WM_SetCaption("Bouncy Ball Simulation Tutorial", NULL); // No icon here :P

	// Set the screen up
	screen = SDL_SetVideoMode(w, h, 32, SDL_SWSURFACE | SDL_RESIZABLE); //Yay resizing

	// Make sure the program doesn't quit as soon as it starts...
	quit = false;

	// Initialize delta, thisTime, lastTime so we dont get funky errors
	delta = 0.0f;
	thisTime = 0.0f;
	lastTime = 0.0f;

	// Create balls!
	for (unsigned short i = 0; i < num; i++)
	{
		ball myBall;
		balls.push_back(myBall);
	}

	gravity.x = 0.0f;
	gravity.y = 90.8f;

	// Ready to go!
}

// Remember, the destructor is taken care of in the prototype

int simulation::loop()
{
	while (!quit) // While quit is false
	{
		// Figure out the delta time
		thisTime = (float)SDL_GetTicks();
		delta = ( thisTime - lastTime ) / 1000.0f;
		lastTime = thisTime;

		handleEvents(); // Start calling each needed function, each is self explanatory
		update();
		render();

		SDL_Flip(screen); // Display our wonderful creation!
		SDL_Delay(10); // We dont want to use too much CPU power now do we?
		// Gee isn't that all nice and clean :0
	}

	return 0; //Remember, this returns back to int main(..). So returning 0 here, is exiting the program
}

//Here we handle the very few events we cover in this tutorial
void simulation::handleEvents()
{
	while (SDL_PollEvent(&events)) // While we need to take care of events
	{
		if (events.type == SDL_QUIT) // The user closed the window? BLASPHEMY!
		{
			quit = true; // Quit the program :(
		}

		if (events.type == SDL_VIDEORESIZE) // The user wants more space!
		{
			SDL_FreeSurface(screen); // Remove the current surface from memory (prevent memory leak)
			screen = SDL_SetVideoMode( events.resize.w, events.resize.h, 32, SDL_SWSURFACE | SDL_RESIZABLE ); // Resize the screen

			if(screen == NULL) // If your computer exploded...
			{
				quit = true; // Quit the program :(
			}
		}
	}

	// These are all of the events we will handle in this tutorial, but you can easily expand this to, say, dropping a ball with lmb :)
}

void simulation::update() // Here is where the "physics" happens, sorta
{
	// This function goes through the list of balls, and calls their update function, passing the delta value
	for (unsigned short i = 0; i < balls.size(); i++)
	{
		balls[i].addForce(gravity); // Add gravity to the balls acceleration
		balls[i].update(delta); // Let the ball update itself
		balls[i].bounceScreen(); // See if it collides with the screen, if so, handle it

		for (unsigned short j = 0; j < balls.size(); j++) // THIS IS HORRIBLE FOR EFFECIENCY, feel free to slap me if you wish
		{
			if (j != i) //This basically checks if the current ball is colliding with any other ball.
				balls[i].collideOther(&balls[j]);
		}
	}
}

void simulation::render() // Straightfoward :P Just render circles where the balls are
{
	SDL_FillRect(screen, NULL, NULL); //Clear the screen with black

	for (unsigned short i = 0; i < balls.size(); i++) // For every ball we have...
	{
		filledCircleRGBA(screen, (int)balls[i].position.x, (int)balls[i].position.y, balls[i].radius, 255, 255 ,255, 255);// I'm using white here, you can use whatever :P (this is unique to SDL_GFX, this command isnt in SDL alone)
	}
}
vec2.h

Code: Select all

// Define guards
#ifndef VEC2_H
#define VEC2_H

// This is a vector class that I developed for my personal use, I think its fairly standard and self explanatory, so I won't explain it :)

class vec2
{
public:
	vec2();
	vec2(float tx, float ty);

	~vec2();

	void add(const vec2 & v, float dt = 0.0f);
	void subtract(const vec2 & v);
	void scale(float mul);
	void normalize();

	float getLength() const;
	float getX() const;
	float getY() const;
	void setX(float v);
	void setY(float v);
	float x, y;
};

#endif
vec2.cpp

Code: Select all

// Implementation of the vec2 class. I believe this is fairly standard and doesn't need explaining. 
// If you need help, then you dont have a good enough grasp of vectors
#include "vec2.h"
#include <math.h>

vec2::vec2()
{
	x = y = 0.0f;
}

vec2::vec2(float tx, float ty)
{
	x = x;
	y = y;
}

vec2::~vec2()
{}

void vec2::add(const vec2 & v, float dt)
{
	if (dt == 0.0f)
	{
		x += v.getX();
		y += v.getY();
	}
	else
	{
		x += v.getX() * dt;
		y += v.getY() * dt;
	}
}

void vec2::subtract(const vec2 & v)
{
	x -= v.getX();
	y -= v.getY();
}

void vec2::scale( float mul)
{
	x *= mul;
	y *= mul;
}

void vec2::normalize()
{
	float ln = getLength();
	x /= ln;
	y /= ln;
}

float vec2::getLength() const
{
	return sqrt( (x)*(x) + (y)*(y) );
}

float vec2::getX() const
{
	return x;
}

float vec2::getY() const
{
	return y;
}

void vec2::setX(float v)
{
	x = v;
}

void vec2::setY(float v)
{
	y = v;
}
ball.h

Code: Select all

// Define guards, yet again
#ifndef BALL_H
#define BALL_H

//We only need to use the vector class here
#include "vec2.h"

class ball
{
public:
	ball();
	~ball(){}; // Nothing that needs to be destroyed really

	void update(const float dt); // Updates the balls position, velocity, etc
	void bounceScreen(); // Checks whether the ball hit the edges of the screen, pong style :D
	void collideOther(ball * other); // Checks to see if the ball is colliding with another ball, if so, handle it
	void addForce(vec2 f); // Adds any abstract force to the balls "physics"

	float mass; // The ball's mass
	unsigned short radius; // Radius of the circle that will represent it

	vec2 position; // Vectors for position, velocity, and acceleration
	vec2 velocity;
	vec2 acceleration;
};

#endif
ball.cpp

Code: Select all

#include "SDL.h" // SDL Main

#include <math.h> // rand()

#include "ball.h" // ball class
#include "vec2.h" // vec2 class

#define FRICTION 0.1f // Here is the friction constant we will use, I suppose you can play with this.

ball::ball()
{
	//Random positioning
	position.setX((float)(rand()%SDL_GetVideoSurface()->w));
	position.setY((float)(rand()%SDL_GetVideoSurface()->h));

	//Random initial velocity
	velocity.setX((float)(rand()%100) - 100.0f);
	velocity.setY((float)(rand()%100) - 100.0f);

	//No acceleration at start
	acceleration.setX(0.0f);
	acceleration.setY(0.0f);

	mass = (float)(rand()%10+2); // 0 through 10 :)
	radius = (unsigned short)mass; //Just to reflect the mass differences
}

//Destructor defined in prototype

void ball::addForce(vec2 f)
{
	f.scale(1.0f/mass); // f = ma, so f/m = a. f/m could be written as f * 1/m (which is the case here)
	acceleration.add(f);
}

void ball::update(const float delta) // Update velocity, position, and everything "physics" here
{
	velocity.add(acceleration, delta); // Self Explanatory
	acceleration.scale(0.0f); 
	position.x += velocity.x * delta; // Move the ball
	position.y += velocity.y * delta;
}

void ball::bounceScreen()
{
	vec2 fric; // Friction vector to make things easier
	fric.x = FRICTION * mass;
	fric.y = FRICTION * mass;

	bool hitX = false; // Flags that tell the function whether the ball collided
	bool hitY = false;

	// These 4 functions should be self explanatory
	// If you need help, look up Pong's collision
	if (position.getX() < radius)
	{
		position.setX(radius);
		velocity.setX(velocity.getX() * -1.0f);

		hitX = true;
	}

	if (position.getY() < radius)
	{
		position.setY(radius);
		velocity.setY(velocity.getY() * -1.0f);

		hitY = true;
	}

	if (position.getX() > SDL_GetVideoSurface()->w - radius)
	{
		position.setX((float)SDL_GetVideoSurface()->w - radius);
		velocity.setX(velocity.getX() * -1.0f);

		hitX = true;
	}

	if (position.getY() > SDL_GetVideoSurface()->h - radius)
	{
		position.setY((float)SDL_GetVideoSurface()->h - radius);
		velocity.setY(velocity.getY() * -1.0f);
		
		hitY = true;
	}

	if (hitX || hitY) //If there was a collision
	{
		// Apply friction :D
		if (velocity.x < 0.0f)
			velocity.x += fric.x;
		else
			velocity.x -= fric.x;

		if (velocity.y < 0.0f)
			velocity.y += fric.y;
		else
			velocity.y -= fric.y;
	}
}

void ball::collideOther(ball * other) // This is probably the heart of this simulation
{
	vec2 dist; // The vector that holds the distance between the two balls' centers
	dist.x = position.x - other->position.x;
	dist.y = position.y - other->position.y;

	// If the balls' radiuses squared is greater than the length of the distance vector squared, then they are colliding.
	// If you take out the squared part, this is saying that if the distance between the balls is less than the balls'
	// radii summed up, then they are colliding. This is easy to visualize. Look at Figure 1
	
	if ( (dist.x * dist.x + dist.y * dist.y) < ((radius + other->radius) * (radius + other->radius)) ) //Pythagerom Theorem, minus the sqrt part because thats costly on time :)
	{
		dist.normalize(); // Hey! The balls are colliding
		dist.x *= radius + other->radius;
		dist.y *= radius + other->radius;
		/*
			lets use some numbers here.
			This ball's, A, position is (100,100)
			The ball it collides with, B, is at (110, 105)
			A's radius is 10
			B's radius is 6

			The distance between A and B is
			X: -10
			Y: -5

			When normalized, 
			X: -0.9009009...
			Y: -0.450450...

			So now, we multiply by the radii (16)
			X: -14.4
			Y: -7.2

			This gives us the offsets that we need to add to the other's ball position to put this ball in the correct position
			Lets add them
			A's X: 110 - 14.4 = 95.6
			A's Y: 105 - 7.2 = 97.8

			Lets see what the distance between the two circles is now ;)
			A (95.6, 97.8)
			B (110, 105)
			X: 95.6 - 110 = -14.4
			Y: 97.8 - 105 = -7.2

			-14.4^2 + -7.2^2 = 259.2
			sqrt(259.2) = 16.0996...

			What was the sum of the radii? 16, so this makes the circles just BARELY touch ;)

		*/

		position.x = other->position.x + dist.x; // The previous comment explains this
		position.y = other->position.y + dist.y;

		vec2 velocityB; // We need to preserve the current balls velocity!
		velocityB.x = velocity.x;
		velocityB.y = velocity.y;

		// This calculates the new velocity vectors
		velocity.x = (velocity.x * (mass - other->mass) + 2 * (other->mass * other->velocity.x)) / (mass + other->mass);
		velocity.y = (velocity.y * (mass - other->mass) + 2 * (other->mass * other->velocity.y)) / (mass + other->mass);

		other->velocity.x = (other->velocity.x * (other->mass - mass) + 2 * (mass * velocityB.x)) / (mass + other->mass);
		other->velocity.y = (other->velocity.y * (other->mass - mass) + 2 * (mass * velocityB.y)) / (mass + other->mass);

		//Add friction!
		if (other->velocity.x < 0.0f)
			other->velocity.x += FRICTION * other->mass;
		else
			other->velocity.x -= FRICTION * other->mass;

		if (other->velocity.y < 0.0f)
			other->velocity.y += FRICTION * other->mass;
		else
			other->velocity.y -= FRICTION * other->mass;
	}
}
Figures

Figure 1
Image


Final note:
Please, please, please tell me what you think, what I can improve on, what part of my physics is wrong, if I need to explain something more, etc.!

I have a feeling I need to explain the collision handling more :/

Download
This is a tutorial, where you're supposed to learn. I provided source code above, and providing a download makes it possible for you not to get anything out of this and take the easy way out. :-)
Last edited by xiphirx on Wed Feb 09, 2011 12:34 pm, edited 1 time in total.
StarCraft II Zerg Strategy, open to all levels of players!

Looking for paid work :< Contact me if you are interested in creating a website, need a web design, or anything else you think I'm capable of :)
JesseGuarascia
Chaos Rift Cool Newbie
Chaos Rift Cool Newbie
Posts: 70
Joined: Mon Dec 13, 2010 10:55 pm

Re: [SDL] Very Simple Bouncy Ball Simulation Tutorial

Post by JesseGuarascia »

Hmm, I like :D! The code was simple enough to read for anybody. I'm sure this could be great for people to use when learning 2D physics, or at least some sort of bouncing ball physics. Multi-Ball Pong anyone? ;D

Oh, a quick note. I'm sure it's not relevant, because I did it and it still worked, but using "SDL_FreeSurface(screen);" is supposed to be bad, and return "undefined results". I don't know though; it's never bothered me :P.
-- Jesse Guarascia

I like C/++, SDL, SFML, OpenGL and Lua. If you don't like those, then gtfo my sig pl0x (jk trollololololol)
User avatar
eatcomics
ES Beta Backer
ES Beta Backer
Posts: 2528
Joined: Sat Mar 08, 2008 7:52 pm
Location: Illinois

Re: [SDL] Very Simple Bouncy Ball Simulation Tutorial

Post by eatcomics »

I saw bouncy ball physics and was like OOH I WANT. Then I saw SDL I thought it was OpenGL xD Still though great tutorial. Very n00b friendly.
Image
User avatar
dandymcgee
ES Beta Backer
ES Beta Backer
Posts: 4709
Joined: Tue Apr 29, 2008 3:24 pm
Current Project: https://github.com/dbechrd/RicoTech
Favorite Gaming Platforms: NES, Sega Genesis, PS2, PC
Programming Language of Choice: C
Location: San Francisco
Contact:

Re: [SDL] Very Simple Bouncy Ball Simulation Tutorial

Post by dandymcgee »

JesseGuarascia wrote: Oh, a quick note. I'm sure it's not relevant, because I did it and it still worked, but using "SDL_FreeSurface(screen);" is supposed to be bad, and return "undefined results". I don't know though; it's never bothered me :P.
It's unnecessary, SDL_Quit takes care of the screen surface for you.
Falco Girgis wrote:It is imperative that I can broadcast my narcissistic commit strings to the Twitter! Tweet Tweet, bitches! :twisted:
User avatar
xiphirx
Chaos Rift Junior
Chaos Rift Junior
Posts: 324
Joined: Mon Mar 22, 2010 3:15 pm
Current Project: ******** (Unkown for the time being)
Favorite Gaming Platforms: PC
Programming Language of Choice: C++
Contact:

Re: [SDL] Very Simple Bouncy Ball Simulation Tutorial

Post by xiphirx »

dandymcgee wrote:
JesseGuarascia wrote: Oh, a quick note. I'm sure it's not relevant, because I did it and it still worked, but using "SDL_FreeSurface(screen);" is supposed to be bad, and return "undefined results". I don't know though; it's never bothered me :P.
It's unnecessary, SDL_Quit takes care of the screen surface for you.
Thanks for the info, fixed in the tutorial.
StarCraft II Zerg Strategy, open to all levels of players!

Looking for paid work :< Contact me if you are interested in creating a website, need a web design, or anything else you think I'm capable of :)
User avatar
dandymcgee
ES Beta Backer
ES Beta Backer
Posts: 4709
Joined: Tue Apr 29, 2008 3:24 pm
Current Project: https://github.com/dbechrd/RicoTech
Favorite Gaming Platforms: NES, Sega Genesis, PS2, PC
Programming Language of Choice: C
Location: San Francisco
Contact:

Re: [SDL] Very Simple Bouncy Ball Simulation Tutorial

Post by dandymcgee »

xiphirx wrote:
dandymcgee wrote:
JesseGuarascia wrote: Oh, a quick note. I'm sure it's not relevant, because I did it and it still worked, but using "SDL_FreeSurface(screen);" is supposed to be bad, and return "undefined results". I don't know though; it's never bothered me :P.
It's unnecessary, SDL_Quit takes care of the screen surface for you.
Thanks for the info, fixed in the tutorial.
Sure. By the way, I neglected to say how well organized and helpful this looks. I am especially fond of the visual aids.
Falco Girgis wrote:It is imperative that I can broadcast my narcissistic commit strings to the Twitter! Tweet Tweet, bitches! :twisted:
User avatar
xiphirx
Chaos Rift Junior
Chaos Rift Junior
Posts: 324
Joined: Mon Mar 22, 2010 3:15 pm
Current Project: ******** (Unkown for the time being)
Favorite Gaming Platforms: PC
Programming Language of Choice: C++
Contact:

Re: [SDL] Very Simple Bouncy Ball Simulation Tutorial

Post by xiphirx »

Thank you, even though there's one visual aid XD
StarCraft II Zerg Strategy, open to all levels of players!

Looking for paid work :< Contact me if you are interested in creating a website, need a web design, or anything else you think I'm capable of :)
User avatar
dandymcgee
ES Beta Backer
ES Beta Backer
Posts: 4709
Joined: Tue Apr 29, 2008 3:24 pm
Current Project: https://github.com/dbechrd/RicoTech
Favorite Gaming Platforms: NES, Sega Genesis, PS2, PC
Programming Language of Choice: C
Location: San Francisco
Contact:

Re: [SDL] Very Simple Bouncy Ball Simulation Tutorial

Post by dandymcgee »

xiphirx wrote:Thank you, even though there's one visual aid XD
Actually it's one image with two diagrams. You've just forgotten to label the second one "Figure 2". :P (a.k.a. my bad, I made that plural)
Falco Girgis wrote:It is imperative that I can broadcast my narcissistic commit strings to the Twitter! Tweet Tweet, bitches! :twisted:
User avatar
GroundUpEngine
Chaos Rift Devotee
Chaos Rift Devotee
Posts: 835
Joined: Sun Nov 08, 2009 2:01 pm
Current Project: mixture
Favorite Gaming Platforms: PC
Programming Language of Choice: C++
Location: UK

Re: [SDL] Very Simple Bouncy Ball Simulation Tutorial

Post by GroundUpEngine »

Wow just found this, nice tutorial! :)

How would this be done differently?

Code: Select all

for (unsigned short j = 0; j < balls.size(); j++) // THIS IS HORRIBLE FOR EFFECIENCY, feel free to slap me if you wish
      {
         if (j != i) //This basically checks if the current ball is colliding with any other ball.
            balls[i].collideOther(&balls[j]);
      }
User avatar
Ginto8
ES Beta Backer
ES Beta Backer
Posts: 1064
Joined: Tue Jan 06, 2009 4:12 pm
Programming Language of Choice: C/C++, Java

Re: [SDL] Very Simple Bouncy Ball Simulation Tutorial

Post by Ginto8 »

GroundUpEngine wrote:Wow just found this, nice tutorial! :)

How would this be done differently?

Code: Select all

for (unsigned short j = 0; j < balls.size(); j++) // THIS IS HORRIBLE FOR EFFECIENCY, feel free to slap me if you wish
      {
         if (j != i) //This basically checks if the current ball is colliding with any other ball.
            balls[i].collideOther(&balls[j]);
      }
With something like a quadtree or otherwise, basically he's saying that checking EVERY possible collision is inefficient.
Quit procrastinating and make something awesome.
Ducky wrote:Give a man some wood, he'll be warm for the night. Put him on fire and he'll be warm for the rest of his life.
User avatar
GroundUpEngine
Chaos Rift Devotee
Chaos Rift Devotee
Posts: 835
Joined: Sun Nov 08, 2009 2:01 pm
Current Project: mixture
Favorite Gaming Platforms: PC
Programming Language of Choice: C++
Location: UK

Re: [SDL] Very Simple Bouncy Ball Simulation Tutorial

Post by GroundUpEngine »

Ginto8 wrote:
GroundUpEngine wrote:Wow just found this, nice tutorial! :)

How would this be done differently?

Code: Select all

for (unsigned short j = 0; j < balls.size(); j++) // THIS IS HORRIBLE FOR EFFECIENCY, feel free to slap me if you wish
      {
         if (j != i) //This basically checks if the current ball is colliding with any other ball.
            balls[i].collideOther(&balls[j]);
      }
With something like a quadtree or otherwise, basically he's saying that checking EVERY possible collision is inefficient.
Righto, good point. ;)
User avatar
xiphirx
Chaos Rift Junior
Chaos Rift Junior
Posts: 324
Joined: Mon Mar 22, 2010 3:15 pm
Current Project: ******** (Unkown for the time being)
Favorite Gaming Platforms: PC
Programming Language of Choice: C++
Contact:

Re: [SDL] Very Simple Bouncy Ball Simulation Tutorial

Post by xiphirx »

Yep, I haven't messed with quad trees or looked into new methods for collision detection among multiple objects :P
StarCraft II Zerg Strategy, open to all levels of players!

Looking for paid work :< Contact me if you are interested in creating a website, need a web design, or anything else you think I'm capable of :)
Post Reply