Skip to content
Damiaan Dufaux edited this page Apr 12, 2016 · 2 revisions

Agar.io: from concept to application

Meteor Agar logo

Code repository: https://github.com/meteor-leuven/agar


Table of contents

[TOC]

About Meteor Leuven

This document is part of a workshop given by Meteor Leuven. Meteor Leuven is a Meetup group founded by three Meteor enthousiasts: Jeroen, Damiaan and Kymer. Our group is officially supported by Meteor: Nick Coe, community manager at Meteor who runs the global Meetup program for Meteor, co-founded the group.

If you liked this workshop and can't get enough of Meteor, make sure to join our Meetup group to meet like-minded people in and around Leuven.

For any questions or feedback related to this workshop you can contact Damiaan and Kymer on Twitter: @Dev1an or @Kymer_G. Alternatively use the contact form on the Meteor Leuven Meetup page and we'll get back to you.

Step 1: Make a Meteor app

Install meteor

Follow the steps at: meteor.com/install

On Windows you might be prompted to create a Meteor account: this is not mandatory, you can skip that step.

If you already had Meteor installed, make sure your installation is up to date:

meteor update

Create a Meteor app

Using Terminal or CMD command prompt, type:

meteor create agar # creates a meteor application in the directory ./agar/
cd agar # changes directory to agar
meteor #starts the meteor application residing in the current (agar) directory

If Windows Firewall blocked something (e.g. Evented I/O for V8 JavaScript) you can safely click allow access for both public and private networks.

Try it out

Surf to http://localhost:3000 and admire your new meteor application.

IDE

To make your life easier you should use an IDE (integrated development environment) or a capable code editor. Although I intensely hate Java applications, WebStorm is the handiest one available right now, with support for Meteor out of the box. Alternatively, you can also use Sublime Text or Atom, but those require some additional set-up. Have a look at the Meteor website for some suggestions and links to the useful Meteor plugins.

Open the agar project directory in your editor and play around a bit. Don't be afraid to break things, we will not use the default sample files, in fact after you are done it's time to clean up the project.

Clean things up

Meteor added some sample files to get you started. Your project should look like this:

├── client
│   ├── main.css
│   ├── main.html
│   └── main.js
├── package.json
└── server
    └── main.js

2 directories, 5 files

If you don't see the client and server folder you probably have an outdated version of Meteor installed. In that case take a look at step 1, update Meteor and recreate the agar project.

Delete all the html, css, js and json files. That leaves you with a nice, fresh, empty project:

├── client
└── server

2 directories, 0 files

Note: on Windows you will see files and folders starting with a dot, those are hidden on Mac and Linux and should not be deleted.

Step 2: Create some graphics

2.1: Create vector graphics

Okay, it's time to start drawing circles in your browser!

Add an empty canvas.html file inside the client directory. You can use the command line to quickly do this. The file should automatically appear in your editor.

Mac / Linux:

touch client/canvas.html #creates an empty file inside the client directory

Windows:

type nul >> client/canvas.html

Add a <head> and a <body> element to your HTML file. Inside the <body> you will add an SVG (Scalable Vector Graphics) element. SVG elements have their own viewports and coordinate systems.

We want to render the circle of the player in the center of the screen. The coordinate system looks like this, however:

Grid

Note how the top-left corner has the coordinate (0,0).

We want the circle you control to appear in the middle of the screen. It would make sense to have (0,0) as our center coordinate. We all know you are the center of the universe, aren't you? Therefore, when creating the <svg> element you should make sure the viewBox top-left corner is set at (-200, -200) and has a height and width of 400. (these are so-called user units, in case you're wondering).

Here's a little code snippet to get you on your way:

<svg viewBox="-200 -200 400 400">

<!-- your balls here! -->

</svg>

For more information read the excellent blogpost "Understanding SVG Coordinate Systems and Transformations".

Now add some circles:

  1. A circle in the middle (which you will control later on)
  2. A few smaller "food" circles scattered around the field
  3. A few bigger circles representing other players

Here is how you create a basic circle element:

<circle cx="0" cy="0" r="20" fill=lightBlue />

Take a look at this list of named colors if you want to play around with different colors.

2.2: Scale the vector graphics

As you may have noticed, the viewport does not fit the browser window. If you make your browser smaller, the view gets clipped. To fix that we'll need to throw some CSS into the mix.

Create a new css file in the client folder. Using CSS you can scale the <svg> element to fit the browser window.

Mac / Linux:

touch client/stylesheet.css

Windows:

type nul >> client/stylesheet.css

Inside the css file you should add some rules which apply to the height and width of the <svg> element:

svg {

/* your rules here */

}

Here's a tip, take a look at relative css lengths.

Step 3: Decouple data and UI

Okay now you have a feel for how to draw visuals in a browser window. You created some circles and fooled around with the coordinates and radii (strange word, but radiusess isn't much better).

Here's the problem: you hardcoded the circles onto the canvas. The data ( x, y, radius) and the UI (the circle element) are tightly coupled. The amount of food circles and player circles is hardcoded as well. We don't want that, those values are supposed to be variable and updating those variables is now way too complicated. It's time to decouple!

What data defines a food entity and player entity? The location on the map is definitely something that shouldn't be hardcoded. Neither should the number of entities on the playing field. The radius of a player is variable as well, as it will depend on the number of food items they have eaten. (don't worry about your own circle, we'll tackle that in our next step)

Removing all that from the <svg> element inside canvas.html gives you something like this:

<!-- for each food entity, draw a new circle -->
<circle cx='x value of food' cy='y value of player' r=5 fill=black />

<!-- for each player entity, draw a new circle -->
<circle cx='x value of player' cy='y value of player' r='radius of player' fill=orange />

That will obviously not compile. Luckily the real solution is not that much different from what you see above. Here is where we start exploiting the true power of Meteor.

Create an empty JavaScript file.

Mac / Linux:

touch client/canvas.js

Windows:

type nul >> client/canvas.js

This JavaScript file will contain helper functions and other handy properties that can be used in the canvas.html file. Here is how you add helpers to the <body> element:

JavaScript inside canvas.js

Template.body.helpers({
	food: [
		// food entities
	],
  
	otherPlayers: [
		// player entities
	]
})

We added 2 empty arrays in the helpers object. Using the example below, you should be able to add the necessary food and player data and render the circles in the browser.

JavaScript

Template.body.helpers({  
  heroes: [
    {name: "Finn", sidekick: "Jake"},
    {name: "Batman", sidekick: "Robin"}
  ]
}) 

HTML

<body>  
  {{#each hero in heroes}}
    <p>{{hero.name}} & {{hero.sidekick}}</p>
  {{/each}}
</body>

Result

Finn & Jake
Batman & Robin

The double curly braces {{ and }} are Spacebars. They are part of Meteor's templating system called Blaze. You can use them to iterate over 'collections' of data (such as an array, in the example above), access variables, include other HTML snippets (called templates), to control the code flow, etc… Don't worry too much about them, just remember that whenever you see those there is some 'logic' involved.

Step 4: Move within the world

Remember how you were the center of the universe in step 2.1? That's exactly where you remain... even if you are moving. "That's impossible!" you might scream, to which we reply: "No, because all motion is relative!".

Einstein

Yeah, that's what Albert said. It might not feel intuitive, but it does simplify the code.

You moving up is exactly the same as the rest of the world moving down. That's how we will do things in our code as well. We will translate all other entities in the opposite direction you are 'moving' in while you remain stationary in the center of the screen. That way we don't have to worry about your circle moving off screen and don't have to implement a virtual 'camera' that tracks your location.

First, add some extra helpers in canvas.js:

Template.body.helpers({
  me: {x: 10, y: 0, radius: 10},
  invert(number) {return -number},
  // ...
})

me is a plain JavaScript object with some data properties, invert is a simple JavaScript function which inverts a given number.


Sidenote: If you are using Webstorm and get the following error on the invert function:

Method definition shorthands are not supported by current JavaScript version

You should go to the preferences (on OS X +,) and in the top-left corner search for "javascript language version". Under Languages & Frameworks > JavaScript set the JavaScript version to ECMAScript 6 and you are good to go!Screenshot of Webstorm preferences


Time to decouple your own radius. Do not make use of the me x and y values, leave them at (0,0).

<circle cx=0 cy=0 r={{me.radius}} fill=lightblue />

Instead, make use of the invert function to move all other entities in the opposite direction of me. To easily apply a translation to all entities they should be grouped. Move the food and player circles inside a <g> element and apply a translate(x y) transformation on the <g> element:

<g transform="translate(x y)" >
  <!-- food and players -->
</g>

Make use of the newly created helpers to fill in the correct x and y values. Here is how you can call a helper function with a parameter using the Spacebars syntax:

{{function parameter}}

If you have done that you are now you are ready to move within the world. Play around with the x and y values of me in canvas.js and save the file. Because of Meteor's hot code push the browser should automatically refresh and it will look like your circle is moving!

Step 5: Synchronize the world

You might ask yourself why you did all that work to decouple the data from the UI, only to hardcode the circles in a different place. Well it's time to get real. So ladies and gents: hold on to your hats, it's time to... synchronize THE WORLD!

That sounded rather daunting, didn't it? Well it shouldn't. We are working with Meteor, after all. Everything is pretty straightforward.

Start by creating a new directory called lib inside the agar project:

Mac / Linux / Windows:

mkdir lib

You should now have 3 folders: a client, server and lib folder. Files in the client are only accessible on the… client, no surprises there. Idem dito for the server. Files in the lib folder are accessible to both client and server.

5.1 Sync the food

First we need something to store the food entities in. Meteor uses MongoDB as its database by default. We need to define a collection which is accessible to both server and client. The server will insert (write) the food entities into the database, the client will read from the database. (remember how you iterated over an array in step 3, well you can iterate over a Mongo collection just as easily)

Create a new JavaScript file called collections.js inside the lib folder.

Mac / Linux:

touch lib/collections.js

Windows:

type nul >> lib/collections.js

And create a food collection like this:

Food = new Mongo.Collection('food')

Food circles should be scattered around the playing field in a randomly fashion. And you need about 200 circles. You don't want to hardcode those all by hand, now do you?

Let's create a function inside the same collections.js file which returns a random coordinate:

randomLocation = function() {
	return {
		x: 'random value',
		y: 'random value'
	}
}

Take a look at MDN to figure out how to return random numbers:

The Math.random() function returns a floating-point, pseudo-random number in the range [0, 1) that is, from 0 (inclusive) up to, but not including, 1 (exclusive), which you can scale to your desired range.

Make sure your randomLocation function will generate random x and y values between -500 and 499, not 0 and 0.999.

Time to put your shiny new method to the test. Let's create a food generator!

Star Trek animated series - malfunctioning food replicator

Create a new foodGenerator.js file inside the server folder:

Mac / Linux:

touch server/foodGenerator.js

Windows:

type nul >> server/foodGenerator.js

And create a for-loop to populate the food collection with 200 random coordinates:

for (let foodCount = 0; foodCount < 200; foodCount++) {
	Food.insert(randomLocation())
}

For those of you who have never seen a construction like that before, let me break it down for you. It creates a new variable called foodCount. As long as foodCount is strictly smaller than 200 it will run the Food.insert() function and increment the counter by 1.

Somewhere in your helpers in the canvas.js file you should have this:

Template.body.helpers({
	food: [
		{x: -100, y:-30},
		{x: 30, y: 130},
		{x: 87, y: 100}
	],
  // ...
})

food is in this case a plain JavaScript array. You use it in your UI to iterate over the contents using the Spacebars {{#each }} syntax. All you need to do is change food from being an array to be a function, which returns something like an array. Remember: the Food collection is defined in the lib folder, i.e. you can access it from the client:

Template.body.helpers({
	food() {return Food.find()},
  // ...
})

Food.find() (without any parameters) will return a cursor pointing to all food entities:

A pointer to the result set of a query. Clients can iterate through a cursor to retrieve results.

That's all we need! Save your file and look at all the glorious food balls appearing in your browser!

But there's a problem, everytime the server restarts (e.g. when you save a file) 200 food circles are added to the field. Try adjusting the for-loop on the server so there are at most 200 circles on-screen at once.

Hint: you can call the count() method on a cursor:

Append the count() method to a find() query to return the number of matching documents.

5.2 Sync the players

Well, that wasn't that hard now was it? Time to do the same for all the player entities.

Create a new collection called Players in the collections.js file in the lib folder.

In the server folder create a new file called playerBinding.js. This does exactly what it sounds like:

// When a person opens the browser game
Meteor.onConnection(function(connection) {
	// 1. create a new player object
	const newPlayer = randomLocation()
	newPlayer.points = 0;
  
	// 2. add player object to database
	const playerId = Players.insert(newPlayer)
    
    // 3. link the unique ID to the current connection 
	connection.playerId = playerId

    // When a person closes the browser game
	connection.onClose(function() {
      // remove the player from the database (using the unique ID)
      Players.remove(playerId)
	})
})

// this method returns the ID of the current connected user
Meteor.methods({
	getMyPlayerId() { return this.connection.playerId }
})

// empties the Players collection 
Players.remove({})

That's all you need to handle players connecting and disconnecting to the game.

We need to add a package to our project in preperation for the next bit. Open your Terminal app or Command prompt, navigate to your project folder and run:

meteor add reactive-var 

A reactive variable is a special kind of variable. If it changes, all the places where you used that variable are also reactively updated. Automagically! (what a horrible word)

In the client folder create a new file called player.js

Mac / Linux:*

touch client/player.js

Windows:

type nul >> client/player.js
// make the myPlayerId variable importable in other files
export const myPlayerId = new ReactiveVar()

// if a player is connected, fetch his/her ID and put it in the reactive var
Tracker.autorun(function() {
	if (Meteor.status().connected)
		Meteor.call(
			'getMyPlayerId',
			(error, result) => myPlayerId.set(result)
		)
})

From the Meteor documentation:

Tracker.autorun allows you to run a function that depends on reactive data sources, in such a way that, if there are changes to the data later, the function will be rerun.

Add this at the top of the canvas.js file in the client folder:

import { myPlayerId } from './player'

Now we can access the playerId of the current player inside throughout the canvas file.

Update your me and otherPlayers helpers so they make use of the Players collection and add a new helper function called radiusFor:

Template.body.helpers({
	me() {
		return Players.findOne(myPlayerId.get())
	},
	
	otherPlayers() {
      	// returns the entire collection except for the current player
		return Players.find({_id: {$not: myPlayerId.get()} })
	},

	radiusFor(player) {
		return 10 + player.points
	},
  
  // ...
  
})

Update your UI in canvas.html to make use of the new helpers:

{{#each player in otherPlayers}}
	<circle cx={{player.x}} cy={{player.y}} r={{ ??? }} fill=orange />
{{/each}}

{{#if me}}
	<circle cx=0 cy=0 r={{ ??? }} fill=lightblue />
{{/if}}

You need to call the radiusFor helper and give it a player object as parameter.

Done!

Ask your neighbor to 'surf' to your (internal) IP address followed by the port number like so:

http://192.168.0.191:3000

He or she should be able to connect to your game! They can't move yet, so let's implement movement next in step 6.

Note: your neighbor must be on the same network as you for this to work

Step 6: Move the player

It's time to take control! We will use the coordinate of the mouse pointer to determine the direction a player wants to move in and we will update the player coordinate by continously calling a method that updates our x and y values according to the direction.

6.1 Add mouse controls

Inside the player.js file add the following code:

// ...

// Movement
export var direction = {x: 0, y: 0, normalize() {
	const length = Math.hypot(this.x, this.y)
	this.x /= length
	this.y /= length
}}
var myVelocity = 3

The direction variable represents a vector of the direction the user is moving to. A vector is defined by its length (or magnitude) and direction. We are only interested in its direction, so each time the x and y properties are changed the vector should be normalized. A normalized vector is nothing more than a vector with a length of 1.

Don't worry about setting the x and y values of the direction, we'll do that somewhere else.

Now we need to make sure the player coordinate in the Players collection gets continously incremented (note the $inc, that's Mongo syntax to increment a value) to make the player move. Add this snippet and fill in the blanks. Make use of the direction and velocity variables.

window.requestAnimationFrame(function updateMyPosition() {
	Players.update(myPlayerId.get(), {$inc: {
		x: ???,
		y: ???
	}})
	window.requestAnimationFrame(updateMyPosition)
})

The window.requestAnimationFrame() is pretty important. It makes our game 'run'. From MDN:

The Window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint.

Inside that function we call the same function again. That's called recursion. We create an infinite loop on purpose. This is the so called game loop:

The goal of every video game is to present the user(s) with a situation, accept their input, interpret those signals into actions, and calculate a new situation resulting from those acts. Games are constantly looping through these stages, over and over, until some end condition occurs (such as winning, losing, or exiting to go to bed). Not surprisingly, this pattern corresponds to how a game engine is programmed.

The x and y values of the direction vector need to be correctly calculated for that loop to work of course. So import the direction object in the canvas.js file and add an event listener to the <body> element like so:

import { myPlayerId, direction } from './player'

Template.body.helpers({
	//...
}}
  
Template.body.events({
	'mousemove svg'(mouse) {
		direction.x = mouse.clientX - mouse.currentTarget.offsetWidth /2
		direction.y = mouse.clientY - mouse.currentTarget.offsetHeight/2
		direction.normalize()
	}
})

Just like you added helpers to the <body> template, you can add event listeners and, well… 'listen' for events. This enables you to do extra stuff every time the event occurs, stuff like updating the direction vector. Neat!

From MDN:

The mousemove event is fired when a pointing device (usually a mouse) is moved while over an element.

So each time the mouse moves over the <svg> element you update the direction vector. The direction starts from the center of the screen and points to where the cursor is located.

The clientX and clientY properties represent the x and y values of the mouse in the local DOM element, in other words: the coordinate inside the <svg> element.

mouse.currentTarget is in our case the <svg> element, offsetWidth and offsetHeight is the current width and height of the element and we need those to calculate the center of the screen.

Save the file and check your browser: you are skating accross the field! Feel free to play around with your velocity. This is starting to look like a real game.

6.2 Limit players within bounds

Right now the player is able to move way too far from the playing field. We need to set some boundaries.

Let's create a grid to better visualize the playing field. Inside canvas.html add to the <svg> element:

<g transform="translate({{invert me.x}} {{invert me.y}})">
  
	<pattern id=grid20 width=20 height=20 patternUnits=userSpaceOnUse>
		<path d="M 20 0 L 0 0 0 20" fill=none stroke=gray stroke-opacity=0.2 stroke-width=2 />
	</pattern>
	<rect height=1001 width=1001 y=-500 x=-500 fill=url(#grid20) />
	
  <!-- food and players -->
</g>

The <path> element has a 'draw' attribute called d which describes that path. From MDN:

The d attribute is actually a string which contains a series of path descriptions. These paths consist of combinations the following instructions:

Moveto Lineto

[…]

Moveto instructions can be thought of as picking up the drawing instrument and setting it down somewhere else. Unlike Moveto instructions, Lineto instructions will draw a straight line. This line moves from the current position to the specified location.

Save the file and look at your browser, you should see something like this:

Screenshot of playing field with grid

Okay let's make sure the player can't move off the grid. Inside the player.js file add a new variable called myPosition:

const myPosition = {x: 0, y: 0}

Now we need to keep this variable up to date each time the position changes. A perfect task for the Tracker. Add this in the same player.js file:

Tracker.autorun(function() {
	const me = Players.findOne(myPlayerId.get())
	if (me != undefined) {
		myPosition.x = me.x
		myPosition.y = me.y
	}
})

Each time the player position changes, Meteor runs that function and, as a result, the myPostion variable gets updated. We need the position to determine whether the player is moving out of bounds. So update the updateMyPosition() function:

window.requestAnimationFrame(function updateMyPosition() {
	Players.update(myPlayerId.get(), {$inc: {
		x: clamp(direction.x, myPosition.x) * myVelocity,
		y: clamp(direction.y, myPosition.y) * myVelocity
	}})
	window.requestAnimationFrame(updateMyPosition)
})

As you can see it now makes use of a function called clamp(direction, position) that takes 2 numbers as parameter. It's your turn to create that function!

There are 3 possible scenarios to consider:

  1. You are inside the bounds: you can continue moving in either direction.
  2. You just crossed the bounds and are moving away from the playing field: your circle should stop moving.
  3. You just crossed the bounds but changed direction and are now moving towards the playing field: your circle should continue moving (towards the playing field).

The clamp function should return a number. E.g. in scenario 2 the function should return 0 because 0 * myVelocity means your circle stops moving. In scenario 1 and 3 you can simply return the direction parameter, because the player may continue in that direction.

Hint: these Math functions may come in handy:

Math.abs(number) :

The Math.abs() function returns the absolute value of a number

Documentation

Math.sign(number) :

The Math.sign() function returns the sign of a number, indicating whether the number is positive, negative or zero.

Documentation

Step 7: Eating behavior

Didn't your mother tell you not to play with your food?!

Scar playing with his food

Well in our case, we definitely should. So follow along!

7.1 Food

Here is what needs to happen:

  1. Find food circles which are close to you
  2. Check if any of those food circles overlap with your circle
  3. If we detect an overlap:
    1. Remove the food circle from the Food collection
    2. Insert a new food circle at a random location in the Food collection
    3. Update your score / points in the Players collection
    4. Lower your velocity (fatter = slower)

Add a new function called eatWhenPossible which takes a player as paramater inside player.js

// ...

// Eating behavior
function eatWhenPossible(player) {
	const radius = player.points+10
    
    // 1.
	Food.find({
		x: {$gte: ??? , $lte: ??? },
		y: {$gte: ??? , $lte: ??? }
    // 2.
	}).forEach(function(pieceOfFood) {
		if ( ??? ) {
          	// 3. 
			Food.remove(pieceOfFood._id)
			Food.insert(randomLocation())
			Players.update(player._id, {$inc: {points: 1}})
			myVelocity *= 0.95
		}
	})
}

It is up to you to fill in the blanks.

The $gte Mongo syntax means greater than or equal, $lte means, not surprisingly, less than or equal. Makes sense, right?

To check if 2 circles overlap, we need to know their distance and both their radii. The radius is readily available. We do need to calculate their distance though. Take a look at this, it should immediately make you think of something:

Overlap hint

The Pythagorean theorem! Yes, you are correct. Take a look at Wikipedia if you need a reminder. Tip: use the Math.hypot() function:

The Math.hypot() function returns the square root of the sum of squares of its arguments

From: MDN

The eatWhenPossible method should be called each time the player moves. So call it inside the autorun method:

Tracker.autorun(function() {
	const me = Players.findOne(myPlayerId.get())
	if (me != undefined) {
		myPosition.x = me.x
		myPosition.y = me.y
		eatWhenPossible(me)
	}
})

7.2 Players

We need a new property on player entities, so in the file playerBinding.js inside the server folder, add a gameOver property and set it to false like so:

Meteor.onConnection(function(connection) {
	// ...
  
  	newPlayer.gameOver = false
    
	// ...
	})

We only want to show the circles of player who's game is not over, so update the otherPlayers helper inside the canvas.js file:

Template.body.helpers({
	otherPlayers() {
		return Players.find({
			_id: {$not: myPlayerId.get()},
			gameOver: false
		})
	},
  
  // ...
})

Lastly, add the logic to detect a collision with other players in the eatWhenPossible(player) method inside the player.js file.

Here is what needs to happen:

  1. Find player circles in the Players collection which are close to you, and

    1. Are not *you*,
    2. Are smaller than you,
    3. Whose game isn't over.
    
  2. Check if any of those other player circles overlap with our circle

  3. If we detect an overlap:

    1. Set gameOver property of other player to true
    2. Update your score / points in the Players collection
    3. Lower our velocity (fatter = slower)
//Eat other players
	// 1.
	Players.find({
		_id: {$ne: player._id},
		points: {$lt: player.points},
		x: {$gte: ??? , $lte: ??? },
		y: {$gte: ??? , $lte: ??? },
		gameOver: {$ne: true}
    // 2.
	}).forEach(function(otherPlayer) {
		if ( ??? ) {
			Players.update(otherPlayer._id, {$set: {gameOver: true}})
			Players.update(player._id, {$inc: {points: otherPlayer.points+1}})
			myVelocity *= 0.95
		}

Again, fill in the blanks. Once you are done, save the file and open your browser!

Congratulations, you have built a basic prototype of a multiplayer browser game. Isn't that neat?

Cheers

Step 8: Where to take it from here?

There are still lots of features that can be added, some easier than others. With the knowlegde you now have you should be able to very easily implement random colors. Up till now you used named colors, but SVG elements can use RGB colors as well of course.

Because we're working with SVG, adding skins should not be that hard either. You can take any vector graphic and export it to SVG. For example, this Meteor logo:

Meteor logo

If exported from e.g. Illustrator to SVG is nothing more than a bunch of <path> elements:

<path d="M0.215,0.215L107.502,113.857C107.502,113.857 111.157,116.435 113.952,113.428C116.747,110.42 114.597,107.413 114.597,107.413L0.215,0.215Z" style="fill:rgb(223,79,79);"/>

<path d="M34.186,10.956L115.887,99.034C115.887,99.034 119.542,101.612 122.337,98.605C125.132,95.597 122.982,92.59 122.982,92.59L34.186,10.956Z" style="fill:rgb(223,79,79);"/>

<path d="M10.32,33.942L92.021,122.021C92.021,122.021 95.676,124.599 98.471,121.591C101.267,118.583 99.117,115.576 99.117,115.576L10.32,33.942Z" style="fill:rgb(223,79,79);"/>

<path d="M62.903,20.194L119.983,81.729C119.983,81.729 122.537,83.53 124.489,81.428C126.442,79.327 124.94,77.226 124.94,77.226L62.903,20.194Z" style="fill:rgb(223,79,79);"/>

<path d="M18.182,60.581L75.262,122.116C75.262,122.116 77.816,123.917 79.769,121.816C81.721,119.714 80.219,117.613 80.219,117.613L18.182,60.581Z" style="fill:rgb(223,79,79);"/>

<path d="M92.236,33.513L118.107,61.487C118.107,61.487 119.37,62.331 120.336,61.347C121.302,60.362 120.559,59.377 120.559,59.377L92.236,33.513Z" style="fill:rgb(223,79,79);"/>

<path d="M32.465,88.938L58.336,116.912C58.336,116.912 59.599,117.756 60.565,116.771C61.531,115.787 60.788,114.802 60.788,114.802L32.465,88.938Z" style="fill:rgb(223,79,79);"/>

There's no black magic involved. Just plain old paths. You would of course have to define that in a seperate file, otherwise your canvas.html will get way too crowded. But you get the gist.