Skip to content

Instantly share code, notes, and snippets.

@fucksophie
Last active December 3, 2022 19:50
Show Gist options
  • Save fucksophie/d7a25adf46a2a6b2024ec942e9d8dd1d to your computer and use it in GitHub Desktop.
Save fucksophie/d7a25adf46a2a6b2024ec942e9d8dd1d to your computer and use it in GitHub Desktop.
how 2 make fluid sim yf style

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

Boilerplate / Setup

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

UI elements

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:

  1. sand - just falls down and doesn't do anything
  2. eraser - deletes stuff
  3. tnt - explodes elements around where it's placed
  4. 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

Type Selection

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;
    })

Rendering the board

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 :)

Handling mouse clicks

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

Physics

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

Sand physics

the sand physics would work something like this:

  1. check if there's a free space on the bottom, left or right
  2. if there is, move to it and remove previous block
  3. if there isn't, stay in the same exact area
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment