Various improvements
This commit is contained in:
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 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
|
||||
const isDebug = import.meta.env.VITE_IS_DEBUG === "true";
|
||||
|
||||
let lastPixelTime = parseInt(localStorage.getItem('lastPixelTime') || '0');
|
||||
const currentSoldiersSpan = document.getElementById("current-soldiers");
|
||||
|
||||
const socket = io(`${wsProtocol}//${wsHost}`, {
|
||||
transports: ['websocket'],
|
||||
let coords = [];
|
||||
const canvas = document.getElementById("canvas");
|
||||
const coordsText = document.getElementById("coords");
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("connect");
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
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) => {
|
||||
socket.on("error", (message) => {
|
||||
alert(message);
|
||||
});
|
||||
|
||||
canvas.addEventListener('click', (event) => {
|
||||
const now = Date.now();
|
||||
socket.on("current_soldiers", (currentSoldiers) => {
|
||||
currentSoldiersSpan.textContent = currentSoldiers;
|
||||
});
|
||||
|
||||
if (now - lastPixelTime < pixelCooldown) {
|
||||
alert(
|
||||
`Please wait ${Math.round(
|
||||
(pixelCooldown - (now - lastPixelTime)) / 1000
|
||||
)} more seconds before placing a new pixel.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(updateCountdown);
|
||||
|
||||
window.addEventListener("mousemove", (event) => {
|
||||
// get coordinates of the mouse inside the canvas
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = Math.floor((event.clientX - rect.left) / pixelSize);
|
||||
const y = Math.floor((event.clientY - rect.top) / pixelSize);
|
||||
// random color
|
||||
const color = hexToU32(colorPicker.value);
|
||||
coords = [x, y];
|
||||
|
||||
const update = { x, y, color };
|
||||
socket.emit('place-pixel', update);
|
||||
|
||||
lastPixelTime = now;
|
||||
localStorage.setItem('lastPixelTime', now.toString());
|
||||
coordsText.textContent = `${x}, ${y}`;
|
||||
});
|
||||
|
||||
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 {
|
||||
border: 1px solid black;
|
||||
@tailwind base;
|
||||
@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("")}`;
|
||||
};
|
||||
Reference in New Issue
Block a user