Work on world rendering
This commit is contained in:
parent
b9a54859ce
commit
a89d530cd0
BIN
database/auth.db
BIN
database/auth.db
Binary file not shown.
BIN
database/live.db
BIN
database/live.db
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -324,6 +324,10 @@ span.badge {
|
|||
background-color: #444c55;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.green {
|
||||
background-color: #a6e3a1;
|
||||
}
|
||||
}
|
||||
|
||||
.my-1 { margin-bottom: 0.25rem; margin-top: 0.25rem; }
|
||||
|
@ -517,3 +521,14 @@ body::-webkit-scrollbar-thumb {
|
|||
border-color: #3D444C #2F353B #2C3137;
|
||||
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
}
|
||||
|
||||
#canvas-container {
|
||||
& > canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 440px;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,8 +86,6 @@
|
|||
|
||||
& > span.selected {
|
||||
display: none;
|
||||
margin-left: 1rem;
|
||||
color: #a6e3a1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,42 +15,46 @@ function world_controller_get()
|
|||
*/
|
||||
function world_controller_move_post()
|
||||
{
|
||||
auth_only_and_must_have_character(); csrf_ensure();
|
||||
/*
|
||||
This endpoint is used to move the character around the world. The client sends a POST request with the direction
|
||||
they want to move the character. The server will update the character's position in the database and return the
|
||||
new position to the client.
|
||||
|
||||
We should only be using this endpoint as an AJAX request from the world page. Since we don't need all the character's
|
||||
data to move them, we can just get and update their lcoation using the user's currently selected character ID.
|
||||
*/
|
||||
|
||||
ajax_only(); auth_only(); csrf_ensure();
|
||||
|
||||
define('directions', [
|
||||
[0, -1], // Up
|
||||
[0, 1], // Down
|
||||
[-1, 0], // Left
|
||||
[1, 0] // Right
|
||||
]);
|
||||
|
||||
// direction must exist
|
||||
$direction = $_POST['direction'] ?? false;
|
||||
|
||||
// direction must be valid; 0-3 are sent from the client
|
||||
if (!is_numeric($direction) || $direction < 0 || $direction > 3 || $direction === false) router_error(999);
|
||||
$d = (int) $_POST['direction'] ?? -1;
|
||||
|
||||
// Update the character's position
|
||||
// 0 = up, 1 = down, 2 = left, 3 = right
|
||||
$x = location('x');
|
||||
$y = location('y');
|
||||
|
||||
switch ($direction) {
|
||||
case 0: $y--; break;
|
||||
case 1: $y++; break;
|
||||
case 2: $x--; break;
|
||||
case 3: $x++; break;
|
||||
if (isset(directions[$d])) {
|
||||
$x += directions[$d][0];
|
||||
$y += directions[$d][1];
|
||||
} else {
|
||||
router_error(999);
|
||||
}
|
||||
|
||||
// Update the character's position
|
||||
$r = db_query(db_live(), 'UPDATE char_locations SET x = :x, y = :y WHERE char_id = :c', [
|
||||
':x' => $x,
|
||||
':y' => $y,
|
||||
':c' => char('id')
|
||||
':c' => user('char_id')
|
||||
]);
|
||||
|
||||
// If the query failed, throw an error
|
||||
if ($r === false) throw new Exception('Failed to move character. (wcmp)');
|
||||
|
||||
// If this is an HTMX request, return the new world page
|
||||
if (is_htmx()) {
|
||||
echo render('pages/world/base');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Redirect back to the world page
|
||||
redirect('/world');
|
||||
json_response(['x' => $x, 'y' => $y]);
|
||||
}
|
||||
|
|
|
@ -59,12 +59,7 @@ function csrf()
|
|||
*/
|
||||
function csrf_verify($token)
|
||||
{
|
||||
if (hash_equals($_SESSION['csrf'] ?? '', $token)) {
|
||||
$_SESSION['csrf'] = token();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return hash_equals($_SESSION['csrf'] ?? '', $token);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -196,7 +191,7 @@ function location($field = '')
|
|||
$GLOBALS['location'] = db_query(
|
||||
db_live(),
|
||||
"SELECT * FROM char_locations WHERE char_id = :c",
|
||||
[':c' => char('id')]
|
||||
[':c' => user('char_id')]
|
||||
)->fetchArray(SQLITE3_ASSOC);
|
||||
}
|
||||
|
||||
|
@ -254,7 +249,33 @@ function ce($condition, $value, $or = '')
|
|||
/**
|
||||
* Get whether the request is an HTMX request.
|
||||
*/
|
||||
function is_htmx(): bool
|
||||
function is_htmx()
|
||||
{
|
||||
return isset($_SERVER['HTTP_HX_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the request is an AJAX (fetch) request.
|
||||
*/
|
||||
function is_ajax()
|
||||
{
|
||||
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit a request to AJAX only.
|
||||
*/
|
||||
function ajax_only()
|
||||
{
|
||||
if (!is_ajax()) router_error(418);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a JSON response with the given data.
|
||||
*/
|
||||
function json_response($data)
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
define('DBP', SRC . '/../database');
|
||||
|
||||
/**
|
||||
* Open a connection to a database.
|
||||
*/
|
||||
|
@ -22,7 +24,7 @@ function db_open($path)
|
|||
*/
|
||||
function db_auth()
|
||||
{
|
||||
return $GLOBALS['db_auth'] ??= db_open(SRC . '/../database/auth.db');
|
||||
return $GLOBALS['db_auth'] ??= db_open(DBP . '/auth.db');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,7 +32,7 @@ function db_auth()
|
|||
*/
|
||||
function db_live()
|
||||
{
|
||||
return $GLOBALS['db_live'] ??= db_open(SRC . '/../database/live.db');
|
||||
return $GLOBALS['db_live'] ??= db_open(DBP . '/live.db');
|
||||
}
|
||||
|
||||
|
||||
|
@ -39,7 +41,7 @@ function db_live()
|
|||
*/
|
||||
function db_fights()
|
||||
{
|
||||
return $GLOBALS['db_fights'] ??= db_open(SRC . '/../database/fights.db');
|
||||
return $GLOBALS['db_fights'] ??= db_open(DBP . '/fights.db');
|
||||
}
|
||||
|
||||
|
||||
|
@ -48,7 +50,7 @@ function db_fights()
|
|||
*/
|
||||
function db_blueprints()
|
||||
{
|
||||
return $GLOBALS['db_blueprints'] ??= db_open(SRC . '/../database/blueprints.db');
|
||||
return $GLOBALS['db_blueprints'] ??= db_open(DBP . '/blueprints.db');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
<label for="char_<?= $id ?>">
|
||||
<?= $char['name'] ?>
|
||||
<span class="badge"><?= $char['level'] ?></span>
|
||||
<span class="selected">Active</span>
|
||||
<span class="badge green selected">Active</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -1,113 +1,163 @@
|
|||
<div id="target_world_map">
|
||||
<h1>World</h1>
|
||||
<p>Use WASD keys to move the character</p>
|
||||
<p>Current location: <?= location('x') ?>, <?= location('y') ?></p>
|
||||
<p>Current location: <span id="char_x"><?= location('x') ?></span>, <span id="char_y"><?= location('y') ?></span></p>
|
||||
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
<form hx-post="/move" hx-target="target_world_map" hx-swap="outerHTML" id="form_move">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="direction" value="">
|
||||
</form>
|
||||
<div id="canvas-container">
|
||||
<canvas id="canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let char_x = <?= location('x') ?>;
|
||||
let char_y = <?= location('y') ?>;
|
||||
|
||||
// Configuration
|
||||
const TILE_SIZE = 40; // Fixed size for each tile
|
||||
|
||||
// Sample tile map (0 = empty, 1 = filled)
|
||||
const tileMap = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
];
|
||||
|
||||
// Turn the value at the player's coordinates to be 1 in the map array
|
||||
tileMap[char_y][char_x] = 1;
|
||||
|
||||
const tileColors = {
|
||||
0: '#fff', // Empty tile
|
||||
1: '#333' // Wall tile
|
||||
};
|
||||
|
||||
function resizeCanvas() {
|
||||
// Calculate the actual dimensions needed for the map
|
||||
const mapWidth = tileMap[0].length * TILE_SIZE;
|
||||
const mapHeight = tileMap.length * TILE_SIZE;
|
||||
|
||||
// Get the container's width
|
||||
const containerWidth = canvas.parentElement.clientWidth;
|
||||
|
||||
// Calculate the scale factor to fit the map width to the container
|
||||
const scale = Math.min(1, containerWidth / mapWidth);
|
||||
|
||||
// Set canvas size using the scale factor
|
||||
canvas.width = mapWidth * scale;
|
||||
canvas.height = mapHeight * scale;
|
||||
|
||||
// Scale the context to maintain tile dimensions
|
||||
ctx.scale(scale, scale);
|
||||
|
||||
// Render the map after resize
|
||||
renderMap();
|
||||
}
|
||||
|
||||
function renderMap() {
|
||||
// Clear the canvas
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw each tile
|
||||
for (let row = 0; row < tileMap.length; row++) {
|
||||
for (let col = 0; col < tileMap[row].length; col++) {
|
||||
const tileType = tileMap[row][col];
|
||||
const x = col * TILE_SIZE;
|
||||
const y = row * TILE_SIZE;
|
||||
|
||||
ctx.fillStyle = tileColors[tileType];
|
||||
ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||
|
||||
// Draw tile borders
|
||||
ctx.strokeStyle = '#999';
|
||||
ctx.strokeRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||
const game = {
|
||||
canvas: document.getElementById('canvas'),
|
||||
csrf: '<?= csrf() ?>',
|
||||
tiles: {
|
||||
size: 32,
|
||||
colors: {
|
||||
0: '#fff',
|
||||
1: '#f00'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initial setup
|
||||
resizeCanvas();
|
||||
const map = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 2, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
]
|
||||
|
||||
// Handle window resizing
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
let loc_span = {
|
||||
x: document.getElementById('char_x'),
|
||||
y: document.getElementById('char_y')
|
||||
}
|
||||
|
||||
// On WASD key press, send a POST request to move the character using the form
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (!['w', 'a', 's', 'd'].includes(e.key)) return;
|
||||
let player = { x: <?= location('x') ?>, y: <?= location('y') ?> }
|
||||
let camera = { x: 0, y: 0 }
|
||||
let visible = { x: 0, y: 0 }
|
||||
|
||||
el = document.querySelector('input[name="direction"]');
|
||||
function updateCanvasSize() {
|
||||
game.canvas.width = game.canvas.clientWidth
|
||||
game.canvas.height = game.canvas.clientHeight
|
||||
visible.x = Math.ceil(game.canvas.width / game.tiles.size)
|
||||
visible.y = Math.ceil(game.canvas.height / game.tiles.size)
|
||||
updateCamera()
|
||||
render()
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
window.addEventListener('resize', updateCanvasSize)
|
||||
window.addEventListener('keydown', handleKeyPress)
|
||||
}
|
||||
|
||||
// Handle keyboard input
|
||||
function handleKeyPress(e) {
|
||||
let moved = false;
|
||||
const newPos = { ...player }
|
||||
|
||||
// update the direction input to be 0-3 based on the key pressed
|
||||
// 0 = up, 1 = down, 2 = left, 3 = right
|
||||
el.value = {
|
||||
direction = {
|
||||
'w': 0,
|
||||
's': 1,
|
||||
'a': 2,
|
||||
'd': 3
|
||||
'd': 3,
|
||||
'ArrowUp': 0,
|
||||
'ArrowDown': 1,
|
||||
'ArrowLeft': 2,
|
||||
'ArrowRight': 3
|
||||
}[e.key];
|
||||
|
||||
htmx.trigger(document.getElementById('form_move'), 'submit');
|
||||
if (direction !== undefined) {
|
||||
// Execute a POST request to /move. If successful, the server will return a new x,y position
|
||||
fetch('/move', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: `direction=${direction}&csrf=${game.csrf}`
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
map[player.y][player.x] = 0
|
||||
map[data.y][data.x] = 1
|
||||
player = data
|
||||
loc_span.x.textContent = player.x
|
||||
loc_span.y.textContent = player.y
|
||||
updateCamera()
|
||||
render()
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to move character');
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Update camera position
|
||||
function updateCamera() {
|
||||
camera.x = player.x * game.tiles.size - canvas.width / 2;
|
||||
camera.y = player.y * game.tiles.size - canvas.height / 2;
|
||||
|
||||
// Clamp camera to map bounds
|
||||
camera.x = Math.max(0, Math.min(camera.x,
|
||||
map[0].length * game.tiles.size - canvas.width));
|
||||
camera.y = Math.max(0, Math.min(camera.y,
|
||||
map.length * game.tiles.size - canvas.height));
|
||||
}
|
||||
|
||||
// Render the game
|
||||
function render() {
|
||||
const ctx = game.canvas.getContext('2d')
|
||||
|
||||
ctx.clearRect(0, 0, game.canvas.width, game.canvas.height)
|
||||
|
||||
// Calculate visible tile range
|
||||
const startTileX = Math.floor(camera.x / game.tiles.size)
|
||||
const startTileY = Math.floor(camera.y / game.tiles.size)
|
||||
const endTileX = startTileX + visible.x + 1
|
||||
const endTileY = startTileY + visible.y + 1
|
||||
|
||||
// Only render visible tiles
|
||||
for (let y = startTileY; y < endTileY; y++) {
|
||||
if (y >= map.length) continue
|
||||
|
||||
for (let x = startTileX; x < endTileX; x++) {
|
||||
if (x >= map[0].length) continue
|
||||
|
||||
const screenX = x * game.tiles.size - camera.x
|
||||
const screenY = y * game.tiles.size - camera.y
|
||||
|
||||
if (map[y][x] === 1) {
|
||||
// Draw player
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(screenX, screenY, game.tiles.size, game.tiles.size)
|
||||
} else if (map[y][x] === 2) {
|
||||
// Draw wall
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillRect(screenX, screenY, game.tiles.size, game.tiles.size)
|
||||
} else {
|
||||
// Draw empty tile
|
||||
ctx.strokeStyle = 'gray';
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(screenX, screenY, game.tiles.size, game.tiles.size)
|
||||
ctx.strokeRect(screenX, screenY, game.tiles.size, game.tiles.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
updateCanvasSize()
|
||||
setupEventListeners()
|
||||
render()
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user