Various improvements
This commit is contained in:
@@ -2,15 +2,43 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/png" href="/pixel_war_icon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Painter</title>
|
<title>Pixel War</title>
|
||||||
<link rel="stylesheet" href="/src/style.css" />
|
<link rel="stylesheet" href="/src/style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="flex flex-col items-center justify-center min-h-screen bg-slate-100">
|
||||||
<input type="color" id="color-picker" value="#000000" />
|
<div class="flex flex-wrap gap-2 p-2">
|
||||||
|
<button id="red" class="w-8 h-8 bg-red-500 border border-black shadow"></button>
|
||||||
|
<button id="green" class="w-8 h-8 bg-green-500 border border-black shadow"></button>
|
||||||
|
<button id="blue" class="w-8 h-8 bg-blue-500 border border-black shadow"></button>
|
||||||
|
<button id="cyan" class="w-8 h-8 border border-black shadow bg-cyan-500"></button>
|
||||||
|
<button id="yellow" class="w-8 h-8 bg-yellow-500 border border-black shadow"></button>
|
||||||
|
<button id="black" class="w-8 h-8 bg-black border border-black shadow"></button>
|
||||||
|
<button id="white" class="w-8 h-8 bg-white border border-black shadow"></button>
|
||||||
|
<button id="pink" class="w-8 h-8 bg-pink-500 border border-black shadow"></button>
|
||||||
|
<button id="purple" class="w-8 h-8 bg-purple-500 border border-black shadow"></button>
|
||||||
|
<button id="orange" class="w-8 h-8 bg-orange-500 border border-black shadow"></button>
|
||||||
|
<button id="brown" class="w-8 h-8 bg-orange-800 border border-black shadow"></button>
|
||||||
|
<input class="w-8 h-8 border border-black shadow" type="color" id="color-picker" value="#000000" />
|
||||||
|
</div>
|
||||||
|
<p>Your selected color <span id="current-color-span" class="text-white">COLOR</span></p>
|
||||||
<div id="countdown">You can place a pixel now</div>
|
<div id="countdown">You can place a pixel now</div>
|
||||||
<canvas id="canvas" width="500" height="500"></canvas>
|
<div class="flex items-center gap-2 mx-1 my-4">
|
||||||
|
<label for="toggle-grid">Toggle Grid</label>
|
||||||
|
<input id="toggle-grid" type="checkbox" />
|
||||||
|
<button id="place-pixel" class="px-2 py-1 rounded-md shadow-lg bg-cyan-400 hover:bg-cyan-500 active:bg-cyan-600">Place Pixel</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<p id="coords"></p>
|
||||||
|
</div>
|
||||||
|
<div class="relative p-1 rainbow-border">
|
||||||
|
<canvas class="bg-white rounded-md" id="canvas" width="500" height="500"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 p-2">
|
||||||
|
<p>Current soldiers: <span id="current-soldiers" class="font-bold">0</span></p>
|
||||||
|
<button id="save-canvas" class="px-2 py-1 bg-green-400 rounded-md shadow-lg hover:bg-green-500 active:bg-green-600">Save Canvas</button>
|
||||||
|
</div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.2.0"
|
"vite": "^5.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
1458
painter-js/pnpm-lock.yaml
generated
Normal file
1458
painter-js/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
painter-js/postcss.config.js
Normal file
6
painter-js/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
painter-js/public/pixel_war_icon.png
Normal file
BIN
painter-js/public/pixel_war_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
245
painter-js/src/canvas.js
Normal file
245
painter-js/src/canvas.js
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import socket from "./socket.js";
|
||||||
|
import {
|
||||||
|
hexToU32,
|
||||||
|
u32ToHex,
|
||||||
|
getColorFromElementCSS,
|
||||||
|
rgbToHex,
|
||||||
|
} from "./utils.js";
|
||||||
|
import { pixelSize, pixelCooldown } from "./constants.js";
|
||||||
|
|
||||||
|
const canvas = document.getElementById("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
const countdownDiv = document.getElementById("countdown");
|
||||||
|
let lastPixelTime = parseInt(localStorage.getItem("lastPixelTime") || "0");
|
||||||
|
|
||||||
|
const colorPicker = document.getElementById("color-picker");
|
||||||
|
const redButton = document.getElementById("red");
|
||||||
|
const greenButton = document.getElementById("green");
|
||||||
|
const blueButton = document.getElementById("blue");
|
||||||
|
const yellowButton = document.getElementById("yellow");
|
||||||
|
const purpleButton = document.getElementById("purple");
|
||||||
|
const pinkButton = document.getElementById("pink");
|
||||||
|
const cyanButton = document.getElementById("cyan");
|
||||||
|
const whiteButton = document.getElementById("white");
|
||||||
|
const blackButton = document.getElementById("black");
|
||||||
|
const orangeButton = document.getElementById("orange");
|
||||||
|
const brownButton = document.getElementById("brown");
|
||||||
|
|
||||||
|
const currentColorSpan = document.getElementById("current-color-span");
|
||||||
|
const toggleGridToggle = document.getElementById("toggle-grid");
|
||||||
|
const placePixelButton = document.getElementById("place-pixel");
|
||||||
|
const saveCanvasButton = document.getElementById("save-canvas");
|
||||||
|
|
||||||
|
colorPicker.value = localStorage.getItem("currentColor") || "#000000";
|
||||||
|
|
||||||
|
let currentColor = colorPicker.value;
|
||||||
|
let showGrid = toggleGridToggle.checked;
|
||||||
|
currentColorSpan.style.backgroundColor = currentColor;
|
||||||
|
|
||||||
|
let canvasState = [];
|
||||||
|
let confirmPlacePixel = false;
|
||||||
|
let previewPixel = null;
|
||||||
|
|
||||||
|
const setCurrentColor = (color, isColorPicker = false) => {
|
||||||
|
const hexColor = isColorPicker ? color : rgbToHex(color);
|
||||||
|
currentColor = hexColor;
|
||||||
|
currentColorSpan.style.backgroundColor = hexColor;
|
||||||
|
localStorage.setItem("currentColor", hexColor);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColorPicker = () => {
|
||||||
|
colorPicker.addEventListener("input", (event) => {
|
||||||
|
setCurrentColor(event.target.value, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
redButton.addEventListener("click", () => {
|
||||||
|
setCurrentColor(getColorFromElementCSS(redButton));
|
||||||
|
});
|
||||||
|
|
||||||
|
greenButton.addEventListener("click", () => {
|
||||||
|
setCurrentColor(getColorFromElementCSS(greenButton));
|
||||||
|
});
|
||||||
|
|
||||||
|
blueButton.addEventListener("click", () => {
|
||||||
|
setCurrentColor(getColorFromElementCSS(blueButton));
|
||||||
|
});
|
||||||
|
|
||||||
|
yellowButton.addEventListener("click", () => {
|
||||||
|
setCurrentColor(getColorFromElementCSS(yellowButton));
|
||||||
|
});
|
||||||
|
|
||||||
|
purpleButton.addEventListener("click", () => {
|
||||||
|
setCurrentColor(getColorFromElementCSS(purpleButton));
|
||||||
|
});
|
||||||
|
|
||||||
|
pinkButton.addEventListener("click", () => {
|
||||||
|
setCurrentColor(getColorFromElementCSS(pinkButton));
|
||||||
|
});
|
||||||
|
|
||||||
|
cyanButton.addEventListener("click", () => {
|
||||||
|
setCurrentColor(getColorFromElementCSS(cyanButton));
|
||||||
|
});
|
||||||
|
|
||||||
|
whiteButton.addEventListener("click", () => {
|
||||||
|
setCurrentColor(getColorFromElementCSS(whiteButton));
|
||||||
|
});
|
||||||
|
|
||||||
|
blackButton.addEventListener("click", () => {
|
||||||
|
setCurrentColor(getColorFromElementCSS(blackButton));
|
||||||
|
});
|
||||||
|
|
||||||
|
orangeButton.addEventListener("click", () => {
|
||||||
|
setCurrentColor(getColorFromElementCSS(orangeButton));
|
||||||
|
});
|
||||||
|
|
||||||
|
brownButton.addEventListener("click", () => {
|
||||||
|
setCurrentColor(getColorFromElementCSS(brownButton));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleColorPicker();
|
||||||
|
|
||||||
|
const drawCanvasState = (canvasState) => {
|
||||||
|
for (let y = 0; y < canvasState.length; y++) {
|
||||||
|
for (let x = 0; x < canvasState[y].length; x++) {
|
||||||
|
const color = u32ToHex(canvasState[y][x]);
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("init-canvas", (data) => {
|
||||||
|
const canvasData = JSON.parse(data);
|
||||||
|
drawCanvasState(canvasData);
|
||||||
|
canvasState = canvasData;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("pixel-updated", (update) => {
|
||||||
|
const color = u32ToHex(update.color);
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(
|
||||||
|
update.x * pixelSize,
|
||||||
|
update.y * pixelSize,
|
||||||
|
pixelSize,
|
||||||
|
pixelSize
|
||||||
|
);
|
||||||
|
|
||||||
|
canvasState[update.y][update.x] = update.color;
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkIfCanPlacePixel = () => {
|
||||||
|
const now = Date.now();
|
||||||
|
return now - lastPixelTime >= pixelCooldown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLastPixelTime = () => {
|
||||||
|
lastPixelTime = Date.now();
|
||||||
|
localStorage.setItem("lastPixelTime", lastPixelTime.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlacePixel = (pixelData) => {
|
||||||
|
if (!checkIfCanPlacePixel()) {
|
||||||
|
alert("You can't place a pixel yet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit("place-pixel", pixelData);
|
||||||
|
canvasState[pixelData.y][pixelData.x] = pixelData.color;
|
||||||
|
setLastPixelTime();
|
||||||
|
pixelData = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
canvas.addEventListener("click", (event) => {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = Math.floor((event.clientX - rect.left) / pixelSize);
|
||||||
|
const y = Math.floor((event.clientY - rect.top) / pixelSize);
|
||||||
|
|
||||||
|
const color = hexToU32(currentColor);
|
||||||
|
const oldPreviewPixel = previewPixel;
|
||||||
|
|
||||||
|
const update = { x, y, color };
|
||||||
|
|
||||||
|
if (confirmPlacePixel) {
|
||||||
|
handlePlacePixel(update);
|
||||||
|
} else {
|
||||||
|
previewPixel = update;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previewPixel) {
|
||||||
|
if (oldPreviewPixel) {
|
||||||
|
ctx.clearRect(
|
||||||
|
oldPreviewPixel.x * pixelSize,
|
||||||
|
oldPreviewPixel.y * pixelSize,
|
||||||
|
pixelSize,
|
||||||
|
pixelSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = `rgba(${(color >> 16) & 0xff}, ${(color >> 8) & 0xff}, ${
|
||||||
|
color & 0xff
|
||||||
|
}, 0.5)`;
|
||||||
|
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const removePreviewPixel = () => {
|
||||||
|
previewPixel = null;
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
drawCanvasState(canvasState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawGrid = () => {
|
||||||
|
ctx.strokeStyle = "#000";
|
||||||
|
for (let x = 0; x < canvas.width; x += pixelSize) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, 0);
|
||||||
|
ctx.lineTo(x, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = 0; y < canvas.height; y += pixelSize) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y);
|
||||||
|
ctx.lineTo(canvas.width, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleGrid = () => {
|
||||||
|
toggleGridToggle.addEventListener("change", (event) => {
|
||||||
|
showGrid = event.target.checked;
|
||||||
|
localStorage.setItem("showGrid", showGrid);
|
||||||
|
if (showGrid) {
|
||||||
|
drawGrid();
|
||||||
|
} else {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
drawCanvasState(canvasState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleToggleGrid();
|
||||||
|
|
||||||
|
window.onkeydown = (event) => {
|
||||||
|
// on enter (keycode 13 is enter)
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
if (previewPixel) {
|
||||||
|
handlePlacePixel(previewPixel);
|
||||||
|
removePreviewPixel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
placePixelButton.addEventListener("click", () => {
|
||||||
|
if (previewPixel) {
|
||||||
|
handlePlacePixel(previewPixel);
|
||||||
|
removePreviewPixel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
saveCanvasButton.addEventListener("click", () => {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = canvas.toDataURL();
|
||||||
|
a.download = "canvas.png";
|
||||||
|
a.click();
|
||||||
|
});
|
||||||
2
painter-js/src/constants.js
Normal file
2
painter-js/src/constants.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const pixelSize = 10;
|
||||||
|
export const pixelCooldown = 60 * 1000; // 1 minute
|
||||||
20
painter-js/src/counter.js
Normal file
20
painter-js/src/counter.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { pixelCooldown } from "./constants.js";
|
||||||
|
|
||||||
|
const countdownDiv = document.getElementById("countdown");
|
||||||
|
|
||||||
|
export const updateCountdown = () => {
|
||||||
|
setInterval(() => {
|
||||||
|
const lastPixelTime = parseInt(
|
||||||
|
localStorage.getItem("lastPixelTime") || "0"
|
||||||
|
);
|
||||||
|
const now = Date.now();
|
||||||
|
const timeLeft = Math.max(0, pixelCooldown - (now - lastPixelTime));
|
||||||
|
if (timeLeft > 0) {
|
||||||
|
countdownDiv.textContent = `You can place a pixel in ${Math.ceil(
|
||||||
|
timeLeft / 1000
|
||||||
|
)} seconds`;
|
||||||
|
} else {
|
||||||
|
countdownDiv.textContent = "You can place a pixel now";
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
@@ -1,93 +1,37 @@
|
|||||||
import io from 'socket.io-client';
|
import socket from "./socket.js";
|
||||||
|
import "./canvas.js";
|
||||||
|
import "./counter.js";
|
||||||
|
import { updateCountdown } from "./counter.js";
|
||||||
|
import { pixelSize } from "./constants.js";
|
||||||
|
|
||||||
// Determine the WebSocket protocol based on the page protocol
|
const isDebug = import.meta.env.VITE_IS_DEBUG === "true";
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
||||||
// Use the same host as the page
|
|
||||||
const wsHost = window.location.host;
|
|
||||||
const canvas = document.getElementById('canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const colorPicker = document.getElementById('color-picker');
|
|
||||||
const countdownDiv = document.getElementById('countdown');
|
|
||||||
const pixelSize = 10;
|
|
||||||
const pixelCooldown = 60 * 1000; // 1 minute
|
|
||||||
|
|
||||||
let lastPixelTime = parseInt(localStorage.getItem('lastPixelTime') || '0');
|
const currentSoldiersSpan = document.getElementById("current-soldiers");
|
||||||
|
|
||||||
const socket = io(`${wsProtocol}//${wsHost}`, {
|
let coords = [];
|
||||||
transports: ['websocket'],
|
const canvas = document.getElementById("canvas");
|
||||||
|
const coordsText = document.getElementById("coords");
|
||||||
|
|
||||||
|
socket.on("connect", () => {
|
||||||
|
console.log("connect");
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on("error", (message) => {
|
||||||
console.log('connect');
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('init-canvas', (data) => {
|
|
||||||
const canvasData = JSON.parse(data);
|
|
||||||
for (let y = 0; y < canvasData.length; y++) {
|
|
||||||
for (let x = 0; x < canvasData[y].length; x++) {
|
|
||||||
const color = u32ToHex(canvasData[y][x]);
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('pixel-updated', (update) => {
|
|
||||||
const color = u32ToHex(update.color);
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.fillRect(
|
|
||||||
update.x * pixelSize,
|
|
||||||
update.y * pixelSize,
|
|
||||||
pixelSize,
|
|
||||||
pixelSize
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('error', (message) => {
|
|
||||||
alert(message);
|
alert(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('click', (event) => {
|
socket.on("current_soldiers", (currentSoldiers) => {
|
||||||
const now = Date.now();
|
currentSoldiersSpan.textContent = currentSoldiers;
|
||||||
|
});
|
||||||
|
|
||||||
if (now - lastPixelTime < pixelCooldown) {
|
requestAnimationFrame(updateCountdown);
|
||||||
alert(
|
|
||||||
`Please wait ${Math.round(
|
|
||||||
(pixelCooldown - (now - lastPixelTime)) / 1000
|
|
||||||
)} more seconds before placing a new pixel.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", (event) => {
|
||||||
|
// get coordinates of the mouse inside the canvas
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const x = Math.floor((event.clientX - rect.left) / pixelSize);
|
const x = Math.floor((event.clientX - rect.left) / pixelSize);
|
||||||
const y = Math.floor((event.clientY - rect.top) / pixelSize);
|
const y = Math.floor((event.clientY - rect.top) / pixelSize);
|
||||||
// random color
|
coords = [x, y];
|
||||||
const color = hexToU32(colorPicker.value);
|
|
||||||
|
|
||||||
const update = { x, y, color };
|
coordsText.textContent = `${x}, ${y}`;
|
||||||
socket.emit('place-pixel', update);
|
|
||||||
|
|
||||||
lastPixelTime = now;
|
|
||||||
localStorage.setItem('lastPixelTime', now.toString());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const u32ToHex = (color) => {
|
|
||||||
return `#${color.toString(16).padStart(6, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hexToU32 = (color) => {
|
|
||||||
return parseInt(color.slice(1), 16);
|
|
||||||
};
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
const now = Date.now();
|
|
||||||
const timeLeft = Math.max(0, pixelCooldown - (now - lastPixelTime));
|
|
||||||
if (timeLeft > 0) {
|
|
||||||
countdownDiv.textContent = `You can place a pixel in ${Math.ceil(
|
|
||||||
timeLeft / 1000
|
|
||||||
)} seconds`;
|
|
||||||
} else {
|
|
||||||
countdownDiv.textContent = 'You can place a pixel now';
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|||||||
17
painter-js/src/socket.js
Normal file
17
painter-js/src/socket.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import io from "socket.io-client";
|
||||||
|
|
||||||
|
const isDebug = import.meta.env.VITE_IS_DEBUG === "true";
|
||||||
|
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
const wsHost = window.location.host;
|
||||||
|
|
||||||
|
let socket;
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
socket = io("ws://localhost:3000");
|
||||||
|
} else {
|
||||||
|
socket = io(`${wsProtocol}//${wsHost}`, {
|
||||||
|
transports: ["websocket"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default socket;
|
||||||
@@ -1,3 +1,25 @@
|
|||||||
#canvas {
|
@tailwind base;
|
||||||
border: 1px solid black;
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
.rainbow-border {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 0.5rem; /* Rounded corners */
|
||||||
|
padding: 5px; /* Space for the border */
|
||||||
|
background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet);
|
||||||
|
}
|
||||||
|
.rainbow-border::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: -5px; /* Negative margin to overlap the gradient */
|
||||||
|
border-radius: 0.5rem; /* Match the rounded corners */
|
||||||
|
background: white;
|
||||||
|
z-index: -1; /* Place behind the content */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
18
painter-js/src/utils.js
Normal file
18
painter-js/src/utils.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export const u32ToHex = (color) => {
|
||||||
|
return `#${color.toString(16).padStart(6, "0")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hexToU32 = (color) => {
|
||||||
|
return parseInt(color.slice(1), 16);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getColorFromElementCSS = (element) => {
|
||||||
|
return window.getComputedStyle(element).backgroundColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rgbToHex = (rgbProperty) => {
|
||||||
|
const rgb = rgbProperty.match(/\d+/g);
|
||||||
|
return `#${rgb
|
||||||
|
.map((x) => parseInt(x).toString(16).padStart(2, "0"))
|
||||||
|
.join("")}`;
|
||||||
|
};
|
||||||
8
painter-js/tailwind.config.js
Normal file
8
painter-js/tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
34
src/main.rs
34
src/main.rs
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, HashSet},
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ struct PixelUpdate {
|
|||||||
struct AppState {
|
struct AppState {
|
||||||
canvas: Mutex<Vec<Vec<u32>>>,
|
canvas: Mutex<Vec<Vec<u32>>>,
|
||||||
last_update: Mutex<HashMap<String, DateTime<Utc>>>,
|
last_update: Mutex<HashMap<String, DateTime<Utc>>>,
|
||||||
|
soldiers: Mutex<HashSet<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_connect(socket: SocketRef, state: Arc<AppState>) {
|
async fn on_connect(socket: SocketRef, state: Arc<AppState>) {
|
||||||
@@ -36,15 +37,26 @@ async fn on_connect(socket: SocketRef, state: Arc<AppState>) {
|
|||||||
let canvas_state = state.canvas.lock().unwrap();
|
let canvas_state = state.canvas.lock().unwrap();
|
||||||
let serialized_canvas = serde_json::to_string(&*canvas_state).unwrap();
|
let serialized_canvas = serde_json::to_string(&*canvas_state).unwrap();
|
||||||
socket.emit("init-canvas", &serialized_canvas).ok();
|
socket.emit("init-canvas", &serialized_canvas).ok();
|
||||||
|
let socket_id = socket.id.to_string();
|
||||||
|
|
||||||
|
let mut soldiers = state.soldiers.lock().unwrap();
|
||||||
|
soldiers.insert(socket_id.clone());
|
||||||
|
let soldiers_num = soldiers.len();
|
||||||
|
socket.emit("current_soldiers", &soldiers_num).ok();
|
||||||
|
socket
|
||||||
|
.broadcast()
|
||||||
|
.emit("current_soldiers", &soldiers_num)
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let app_state = state.clone();
|
||||||
|
let app_state_for_disconnect = state.clone();
|
||||||
|
|
||||||
let canvas_state = state.clone();
|
|
||||||
socket.on(
|
socket.on(
|
||||||
"place-pixel",
|
"place-pixel",
|
||||||
move |socket: SocketRef, Data::<PixelUpdate>(update), Bin(_bin)| {
|
move |socket: SocketRef, Data::<PixelUpdate>(update), Bin(_bin)| {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let mut last_update = canvas_state.last_update.lock().unwrap();
|
let mut last_update = app_state.last_update.lock().unwrap();
|
||||||
|
|
||||||
let socket_id = socket.id.to_string();
|
|
||||||
if let Some(&last_time) = last_update.get(&socket_id) {
|
if let Some(&last_time) = last_update.get(&socket_id) {
|
||||||
if now < last_time + Duration::minutes(1) {
|
if now < last_time + Duration::minutes(1) {
|
||||||
let _ = socket.emit(
|
let _ = socket.emit(
|
||||||
@@ -58,7 +70,7 @@ async fn on_connect(socket: SocketRef, state: Arc<AppState>) {
|
|||||||
last_update.insert(socket_id.clone(), now);
|
last_update.insert(socket_id.clone(), now);
|
||||||
|
|
||||||
info!("Received pixel update: {:?}", update);
|
info!("Received pixel update: {:?}", update);
|
||||||
let mut canvas = canvas_state.canvas.lock().unwrap();
|
let mut canvas = app_state.canvas.lock().unwrap();
|
||||||
canvas[update.y as usize][update.x as usize] = update.color;
|
canvas[update.y as usize][update.x as usize] = update.color;
|
||||||
|
|
||||||
info!("Emitting pixel update");
|
info!("Emitting pixel update");
|
||||||
@@ -66,6 +78,17 @@ async fn on_connect(socket: SocketRef, state: Arc<AppState>) {
|
|||||||
socket.broadcast().emit("pixel-updated", &update).ok(); // Send to all other users
|
socket.broadcast().emit("pixel-updated", &update).ok(); // Send to all other users
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
socket.on_disconnect(move |socket: SocketRef| {
|
||||||
|
info!("Socket disconnected: {:?}", socket.id);
|
||||||
|
let mut soldiers = app_state_for_disconnect.soldiers.lock().unwrap();
|
||||||
|
soldiers.remove(&socket.id.to_string());
|
||||||
|
let soldiers_num = soldiers.len();
|
||||||
|
socket
|
||||||
|
.broadcast()
|
||||||
|
.emit("current_soldiers", &soldiers_num)
|
||||||
|
.ok();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -77,6 +100,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let app_state = Arc::new(AppState {
|
let app_state = Arc::new(AppState {
|
||||||
canvas: Mutex::new(vec![vec![0xFFFFFFFF; 500]; 500]),
|
canvas: Mutex::new(vec![vec![0xFFFFFFFF; 500]; 500]),
|
||||||
last_update: Mutex::new(HashMap::new()),
|
last_update: Mutex::new(HashMap::new()),
|
||||||
|
soldiers: Mutex::new(HashSet::new()),
|
||||||
});
|
});
|
||||||
|
|
||||||
let app_state_for_ns = app_state.clone();
|
let app_state_for_ns = app_state.clone();
|
||||||
|
|||||||
Reference in New Issue
Block a user