how to make a HTML fluid sim
hi this will be my first real blog thing i'll try to explainw hat i m doing
we have to do some ep0c boilerplate.. first lets make a epic index.html and slap something like
<!DOCTYPE html>
<html lang="en">
<head>
<title>my epic fluid sim</title>
<link href="style.css" rel="stylesheet">
</head>
<body>
<canvas id="fluid"></canvas>
<script src="script.js"></script>
</body>
</html>
here we just write some basic boilerplate, and reference a stylesheet (style.css) and a script (script.js) that we will do further operations in now we need to actually populate the js so we can see the canvas fill up
const canvas = document.getElementById("fluid");
const ctx = canvas.getContext('2d');
ctx.fillRect(0, 0, canvas.width, canvas.height);
this gets the canvas, the context (which you use to draw stuff in) and then fills a rectange the size of the canvas
for the css we'd just need to make the canvas the whole page, which is actually somewhat hard considering modern browsers liking to add margins to everything for no reason
body {
margin: 0;
overflow: hidden;
}
#fluid {
height: 100vh;
width: 100%;
}
this removes all margins and makes our canvas as big as possible
we need to make a place where we can select types of elements to place in the fluid sim we aren't doing this in the canvas since finding buttons and making them is kinda of a pain in canvas in our index.html now we'll add
<body>
<div id="header"></div> <!-- <<-- -->
<canvas id="fluid"></canvas>
right above the canvas
we need to add some ep0c css now
#header {
height: 30px;
background-color: gray;
}
this just adds some height to the header and makes it gray so i don't think it'd be epic to add all of the types in html since everything has to be dynamic:tm: we're gonna do it all in js
so some types:
- sand - just falls down and doesn't do anything
- eraser - deletes stuff
- tnt - explodes elements around where it's placed
- fire - tnt but smaller and can set off other fire's
implementing all of these inside of JS would look something like this:
const types = [
{
name: "sand",
color: "#C2B280"
},
{
name: "eraser",
color: "#00ff00"
},
{
name: "tnt",
color: "#ff0000"
},
{
name: "fire",
color: "#FFA500"
}
]
this is just a array filled with type objects that have a name and a color
to access these you'd use something like types.find(e => e.name == "sand")
next we need to actually render them we should be able to iterate through every type and then add them to the UI
const header = document.getElementById("header")
types.forEach(type => {
const typeElement = document.createElement("div")
typeElement.title = type.name
typeElement.style.backgroundColor = type.color;
header.append(typeElement);
})
####### TODO: Add comment about code
great! but wait nothing renders? that's because all of the type-divs have 0 height and 0 width. we need to fix this
with some basic CSS
#header > * {
height: 30px;
width: 30px;
display: inline-block;
margin-left: 15px;
}
this grabs every child of the #header element (being our type-divs) and then adds some height and width to them
now we need to select a type and place it down to fire a event on a type placement, we go back to our js and add an event listener, and a global variable called "selected"
let selected;
....
typeElement.addEventListener("click", () => {
selected = type;
})
now we're getting into the fun stuff.. to place stuff we'll first generate a simple board that's the size of our canvas
const size = canvas.getBoundingClientRect();
const board = Array(size.width*size.height).fill("air");
ctx.fillRect(0, 0, size.width, size.height);
now, we should be able to write the rendering part of our stuff
board.forEach((v, idx) => {
if(v == "air") return; // we are not rendering air
const x = Math.floor(idx/size.width)
const y = idx%size.width;
const type = types.find(e => e.name == v);
ctx.fillStyle = type.color;
ctx.fillRect(x, y, 1, 1)
})
this iterates through every single element in our board and renders it
if we were to now edit the board though, it won't autoupdate.. what now?
forunately for us JS supports a very easy to use Animation loop!
putting all of this into a function, and adding window.requestAnimationFrame(update);
at end of the function and
at then end of the script will fix this :)
function setPos(x, y, type) {
board[y+(x*size.width)] = type;
}
canvas.addEventListener("click", (e) => {
if(!selected) return;
setPos((e.clientX - size.left), (e.clientY - size.top), selected.name);
})
get the board's appropariate pixel (tons of weird math) and then just set it to the selected name. our rendering code will automaticaly see this and render it
for physics, we'll have to extend our types
object and add a "render" object-function on every single type
const types = [
{
name: "sand",
color: "#C2B280",
render: (x, y) => {
}
},
{
name: "eraser",
color: "#00ff00",
render: (x, y) => {
}
},
{
name: "tnt",
color: "#ff0000",
render: (x, y) => {
}
},
{
name: "fire",
color: "#FFA500",
render: (x, y) => {
}
}
]
now in our update() function we'll add
const type = types.find(e => e.name == v);
type.render(x, y);
this will "render" (actually act on physics on every block)
we also need a CheckSpaces function, for the later physics
function getPos(x, y) {
return board[y+(x*size.width)];
}
function checkSpaces(x, y) {
return {
top: getPos(x, y+1) == "air",
bottom: getPos(x, y-1) == "air",
left: getPos(x+1, y) == "air",
right: getPos(x-1, y) == "air",
}
}
and inside of it we should check on every single awsome. now we can get onto making the actual physics
the sand physics would work something like this:
- check if there's a free space on the bottom, left or right
- if there is, move to it and remove previous block
- if there isn't, stay in the same exact area