Post Content

THAT Screensaver
Have you ever wondered how you can make something like the classic DVD bouncing logo screensaver?
Me neither, but today I’m gonna show you some of the basics of Object-Oriented Programming that you would use for something like Video Game Development, to create the effect that you’re currently watching on screen (if you don’t have JavaScript turned on then you’re missing out).
And you may say: “How would this help me out?” Trust me, if you’re interested in video game development this might be interesting for you!
This will be a very small project, less than 170 lines, so we are going to use only one file.
We’ll be using TypeScript by the way! JavaScript is ok, but with any big-scale project using typed languages are a must.
Let’s start with the basic logic for this:
We know that if we want to move a circle across the screen we need two things:
Direction and Velocity or force that we need to apply to the object to move it around, so let’s start by creating our first class:
class Character {
x: number;
y: number;
x_power: number;
y_power: number;
}
Notice that everytime the ball bounces, the force applied to both axis change individually, so with x and y we are tracking the current coordinates of the Character, and with x_power and y_power we are tracking the force applied to them every frame.
Now that we’ve introduced the Character class and its core properties—x, y, x_power, and y_power—let’s take the next step and make this into something more dynamic.
Right now, x_power and y_power are just numbers. That works fine, but in game development, we often need more flexibility. For example, what if we want to increase the power applied to theese properties? We could do that manually incrementing the property, or we can create a Power class, that will make easer to create re-usable logic.
Let’s create a Power class
We can encapsulate the behavior of x_power and y_power in a class. This is one of the key ideas behind Object-Oriented Programming: grouping related data and behavior into one unit.
class Power {
val = 0;
constructor() {
this.update_power();
}
update_power() {
this.val = Math.random() * (7 - 3) + 3; // random between 3 and 7
}
}
Now we go back and update our Character class to use this:
class Character {
x: number;
y: number;
x_power: Power;
y_power: Power;
constructor() {
this.x = Math.random() * window.innerWidth;
this.y = Math.random() * window.innerHeight;
this.x_power = new Power();
this.y_power = new Power();
}
}
We’re not just storing a number anymore—we’re using a mini-engine for each axis’s power. This opens the door to more interesting behavior.
Notice that for the constructor of the Character class we are applying a random value to x and y, and we are using the same Math.random() method for both. Let’s change that:
We have two alternatives: we can create a class called Utils that stores a static method inside of it, something like this:
class Utils {
static random_between(min:number, max: number): number {
return Math.random() * (max - min) + min;
}
}
//Let's use it
Utils.random_between(1,10);
That’s one way to do things, we are encapsulating a method that could be utilized in a lot of different places under a class which is self-explanatory.
The static keyword can be applied to any function or variable that we want to be available at the class level, so we don’t need to construct it before using it.
That’s cool and all, but I’m gonna stick with just writing a global function since I’m not really adding any more utility functions to this project:
function random_between(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
Adding Direction
Next, we need to decide which way the ball/character moves. We’ll use enums for clarity:
enum XDirection {
LEFT,
RIGHT,
}
enum YDirection {
UP,
DOWN,
}
With this, we can now build logic that says: “if I’m going left, subtract from x; if I’m going right, add to x”—same for y.
Handling Movement
Now that we have the force and direction, we need a method that moves the ball based on that.
private handle_x() {
if (this.x_dir === XDirection.LEFT) {
this.x += this.x_power.val;
} else {
this.x -= this.x_power.val;
}
}
private handle_y() {
if (this.y_dir === YDirection.DOWN) {
this.y += this.y_power.val;
} else {
this.y -= this.y_power.val;
}
}
This is a bit cumbersome, we are using exactly the same function two times, with different values; however, I’m going to leave it like this, we are moving fast!
We also want to change direction when the character hits the canvas bounds, and while we’re at it, maybe change color too! (Just like that old DVD logo bouncing.)
Generating Color
Let’s generate a random color every time the character bounces:
private get_new_color() {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
this.c_c = `rgb(${r},${g},${b})`;
}
We’ll call this in our bounce logic to give it that satisfying color pop:
private update_direction() {
this.x_power.update_power();
this.y_power.update_power();
this.get_new_color();
}
You’re probably noticing the private keyword before almost every function or property inside the Character class. This is to maintain clarity and control; we only want to expose certain functions or values that must be accessed outside of the class.
Looping and Drawing
Now we add a loop method that gets called every frame. This is where the magic happens:
loop(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
if (this.y > canvas.height) {
this.y_dir = YDirection.UP;
this.update_direction();
}
if (this.y < 0) {
this.y_dir = YDirection.DOWN;
this.update_direction();
}
if (this.x > canvas.width) {
this.x_dir = XDirection.RIGHT;
this.update_direction();
}
if (this.x < 0) {
this.x_dir = XDirection.LEFT;
this.update_direction();
}
this.handle_x();
this.handle_y();
this.draw(ctx);
}
And drawing is as simple as:
private draw(ctx: CanvasRenderingContext2D) {
ctx.fillStyle = this.c_c;
ctx.beginPath();
ctx.arc(this.x, this.y, 30, 0, Math.PI * 2);
ctx.fill();
}
Putting It All Together
Finally, in your main function:
export default function () {
const canvas = document.querySelector("#canvas") as HTMLCanvasElement;
if (!canvas) return;
function resize_canvas() {
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
}
resize_canvas();
window.addEventListener("resize", resize_canvas);
const character = new Character(window);
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
function loop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
character.loop(canvas, ctx);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
As you can see, i added a function to get the updated data of the window’s size in case someone resizes it, now it’s (probably) bulletproof.
Not let’s see the whole script:
//Constants
const MIN_COLOR_VAL = 0;
const MAX_COLOR_VAL = 255;
//Utility
function random_between(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
//Types
enum XDirection {
LEFT,
RIGHT,
}
enum YDirection {
UP,
DOWN,
}
//Classes
class Character {
private x: number;
private y: number;
private c_c: string = "";
private x_power: Power;
private y_power: Power;
private x_dir = XDirection.LEFT;
private y_dir = YDirection.DOWN;
constructor(window: Window) {
this.x = random_between(0, window.innerWidth);
this.y = random_between(0, window.innerHeight);
this.get_new_color();
this.x_power = new Power();
this.y_power = new Power();
}
private add(x: number, y: number) {
return x + y;
}
private subtract(x: number, y: number) {
return x - y;
}
private handle_x() {
if (this.x_dir == XDirection.LEFT) {
this.x = this.add(this.x, this.x_power.val);
} else {
this.x = this.subtract(this.x, this.x_power.val);
}
}
private handle_y() {
if (this.y_dir == YDirection.DOWN) {
this.y = this.add(this.y, this.y_power.val);
} else {
this.y = this.subtract(this.y, this.y_power.val);
}
}
private get_new_color() {
const r = random_between(MIN_COLOR_VAL, MAX_COLOR_VAL);
const g = random_between(MIN_COLOR_VAL, MAX_COLOR_VAL);
const b = random_between(MIN_COLOR_VAL, MAX_COLOR_VAL);
this.c_c = `rgb(${r},${g},${b})`;
}
private update_direction() {
this.x_power.update_power();
this.y_power.update_power();
this.get_new_color();
}
loop(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
if (this.y > canvas.height) {
this.y_dir = YDirection.UP;
this.update_direction();
}
if (this.y < 0) {
this.y_dir = YDirection.DOWN;
this.update_direction();
}
if (this.x > canvas.width) {
this.x_dir = XDirection.RIGHT;
this.update_direction();
}
if (this.x < 0) {
this.x_dir = XDirection.LEFT;
this.update_direction();
}
this.handle_x();
this.handle_y();
this.draw(ctx);
}
private draw(ctx: CanvasRenderingContext2D) {
ctx.fillStyle = this.c_c;
ctx.beginPath();
ctx.arc(this.x, this.y, 30, 0, Math.PI * 2);
ctx.fill();
}
}
class Power {
val = 0;
constructor() {
this.update_power();
}
update_power() {
this.val = random_between(3, 7);
}
}
export default function () {
//HTMLElements
const canvas = document.querySelector("#canvas") as HTMLCanvasElement;
if (!canvas) return null;
//Resize
function resize_canvas() {
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
}
resize_canvas();
window.addEventListener("resize", resize_canvas);
//Characters
const c = new Character(window);
//Modify State & Draw
let ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
if (!ctx) return null;
function delete_last_state() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function state() {
c.loop(canvas, ctx);
}
function loop() {
delete_last_state();
state();
window.requestAnimationFrame(loop);
}
window.requestAnimationFrame(loop);
}
And that’s it!
Bottom line is that this is just a simple animation; however, with the way we made this, it’s really easy to add a few event listeners and update the movement logic, and we could have an actual character on screen.
Conclusion
With under 170 lines of code, we created those DVD screen saver things that I used to love back in the early 2000s, here’s a summary of what we learned:
How to represent game objects with classes
How to use enums for direction handling
How to encapsulate behavior inside reusable classes (Power)
How to animate in the browser with requestAnimationFrame
This kind of thinking forms the backbone of game development and interactive graphics programming.