eplace/src/rooms/canvas/utils.js

468 lines
13 KiB
JavaScript
Raw Normal View History

2026-05-15 11:08:23 +02:00
// This file handles the room canvas DOM manipulation
// Functions includes:
// - initCanvas (initialize the canvas)
// - renderCanvasUpdate (render a canvas update)
// - getPlacementData (get the necessary data to place a pixel)
// - toggleTooltip (toggle the tooltip and display the pixel's information)
import $ from "jquery";
const canvasContainer = $("#canvas-container")?.[0];
const canvas = $("#canvas")?.[0];
const canvasCtx = canvas.getContext("2d");
const selector = $("#selector")?.[0];
const positionTooltip = $("#position-tooltip")?.[0];
const tooltip = $("#tooltip")?.[0];
const colorPicker = $("#color-picker")?.[0];
const colorWheelContainer = $("#color-wheel-container")?.[0];
const colorWheel = $("#color-wheel")?.[0];
/**
* Global variables
*/
let board, palette, selectedColorIdx;
let animation;
const zoomSpeed = 1 / 25;
let zoom = 2.5;
let x, y;
let cx = 0;
let cy = 0;
let target = { x: 0, y: 0 };
let isDrag = false;
/**
* Returns the necessary data to place a pixel.
*
* Get the placement data, i.e. the color the user has selected and the
* coordinates of the pixel he is focusing on.
*
2026-05-16 17:18:47 +02:00
* @returns {{color: number, posX: number, posY: number}} the data
2026-05-15 11:08:23 +02:00
*/
export const getPlacementData = () => ({
color: selectedColorIdx,
posX: target.x,
posY: target.y,
});
/**
* Get the currently focused pixel's information and display it in the tooltip.
*
* @param {boolean} [state=state]
*
* @returns {Promise<void>}
*/
export const toggleTooltip = async (state = false) => {
tooltip.style.display = state ? "flex" : "none";
if (state) {
// FIXME: You should implement or call a function to get the pixel's information
// and display it. Make use of target.x and target.y to get the pixel's position.
}
};
/**
* Calculate the target position according to the top left corner of the canvas.
*
* @param {*} event
*
* @returns {x: number, y: number} the target position
*/
const calculateTarget = (event) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const canvasLeft = rect.left + window.pageXOffset;
const canvasTop = rect.top + window.pageYOffset;
return {
x: Math.floor(
((event?.pageX ?? window.innerWidth / 2) - canvasLeft) * scaleX,
),
y: Math.floor(
((event?.pageY ?? window.innerHeight / 2) - canvasTop) * scaleY,
),
};
};
/**
* Update the position tooltip according to the event.
*
* @param {*} event
*
* @returns {void}
*/
const positionUpdate = (event) => positionDisplay(calculateTarget(event));
/**
* Update the tooltip's position.
*
* @param {{x: number, y: number}} target
*
* @returns {void}
*/
const positionDisplay = ({ x, y }) => {
positionTooltip.innerText = `X=${x} Y=${y}`;
canvas.style.transform = `translate(${cx}px, ${cy}px) scale(${zoom})`;
// We add the canvas.width * zoom to make cx and cy positive
let selectorX = cx + canvas.width * zoom;
let selectorY = cy + canvas.height * zoom;
// Make odd canvas align
if (canvas.width % 2 !== 0) {
selectorX += zoom / 2;
selectorY += zoom / 2;
}
// Find the translate
selectorX %= zoom;
selectorY %= zoom;
// Center selector on the pixel
selectorX -= zoom / 2;
selectorY -= zoom / 2;
selector.style.transform = `translate(${selectorX}px, ${selectorY}px) scale(${zoom})`;
};
// Toggle the color wheel on click on the color picker
colorPicker.addEventListener("click", () => {
const state = colorWheelContainer.style.display;
colorWheelContainer.style.display =
!state || state === "none" ? "block" : "none";
});
/**
* Transform #RRGGBB to 0xBBGGRRAA. Hexadecimal color to 32 bits integer.
*
* @param {string} hex
*
* @returns {number} the 32 bits color
*/
const transformHexTo32Bits = (hex) => {
const reverse = hex.substring(1).match(/.{2}/g).reverse().join("");
return parseInt(`0xFF${reverse}`, 16);
};
/**
* Render the canvas.
*
* @param {number[]} pixels
* @param {string[]} colors
*
* @returns {void}
*/
const renderCanvas = (pixels, colors) => {
const img = new ImageData(canvas.width, canvas.height);
const data = new Uint32Array(img.data.buffer);
board = pixels;
palette = colors;
for (let i = 0; i < pixels.length; i++) {
data[i] = transformHexTo32Bits(colors[pixels[i]]);
}
canvasCtx.putImageData(img, 0, 0);
canvasCtx.imageSmoothingEnabled = false;
canvas.style.imageRendering = "pixelated";
// Remove all the colors from the color wheel
while (colorWheel.firstChild) {
colorWheel.removeChild(colorWheel.firstChild);
}
// Add the colors to the color wheel
for (let i = 0; i < colors.length; i++) {
const btn = document.createElement("button");
colorWheel.appendChild(btn);
btn.addEventListener("click", () => {
selectedColorIdx = i;
colorPicker.style.color = colors[i];
colorPicker.style.border = `${colors[i]} 0.1rem solid`;
});
btn.style.backgroundColor = colors[i];
}
};
/**
* Initialize the canvas with the given room configuration and pixels.
*
* @param {*} roomConfig
* @param {number[]} pixels
*
* @returns {void}
*/
export const initCanvas = (roomConfig, pixels) => {
const canvasDimensions = roomConfig.metadata.canvasDimensions;
canvas.width = canvasDimensions;
canvas.height = canvasDimensions;
positionDisplay({ x: canvasDimensions / 2, y: canvasDimensions / 2 });
selectedColorIdx = 0;
const roomColors = roomConfig.settings.roomColors.split(",");
colorPicker.style.color = roomColors[0];
colorPicker.style.border = `${roomColors[0]} 0.1rem solid`;
renderCanvas(pixels, roomColors);
};
/**
* Render the canvas update, i.e. update the pixel at the given coordinates.
*
* @param {string} color
* @param {number} x
* @param {number} y
*
* @returns {void}
*/
export const renderCanvasUpdate = (color, x, y) => {
const img = new ImageData(canvas.width, canvas.height);
const data = new Uint32Array(img.data.buffer);
board[y * canvas.width + x] = color;
for (let i = 0; i < board.length; i++) {
data[i] = transformHexTo32Bits(palette[board[i]]);
}
canvasCtx.putImageData(img, 0, 0);
};
/**
* Reset the values of the canvas, i.e. the zoom, the coordinates and the
* display position.
*
* @returns {void}
*/
export const resetValues = () => {
zoom = 2.5;
x = 0;
y = 0;
cx = 0;
cy = 0;
isDrag = false;
positionDisplay({ x, y });
colorWheelContainer.style.display = "none";
toggleTooltip(false);
};
// Handle scroll on canvas
document.addEventListener("wheel", (e) => {
// Make sure we're scrolling on the canvas or the body and not the UI
if (e.target !== canvas && e.target !== canvasContainer) {
return;
}
clearInterval(animation);
toggleTooltip(false);
const delta = Math.sign(e.deltaY) * zoomSpeed;
const zoomFactor = 1 + delta;
const oldZoom = zoom;
const newZoom = Math.max(2.5, Math.min(40, oldZoom * zoomFactor));
// Get the position of the mouse relative to the canvas
const mouseX = e.clientX - window.innerWidth / 2;
const mouseY = e.clientY - window.innerHeight / 2;
// Calculate the new center point based on the mouse position
const newCx = mouseX - (mouseX - cx) * (newZoom / oldZoom);
const newCy = mouseY - (mouseY - cy) * (newZoom / oldZoom);
if (newZoom !== oldZoom) {
zoom = newZoom;
cx = newCx;
cy = newCy;
positionUpdate();
}
});
// Handle click and drag on canvas
document.addEventListener("mousedown", (e) => {
// Make sure we're clicking on the canvas or the body and not the UI
if (e.target !== canvas && e.target !== canvasContainer) {
return;
}
e.preventDefault();
// Ignore if right click
if (e.button === 2) {
return;
}
clearInterval(animation);
isDrag = false;
x = e.clientX;
y = e.clientY;
document.addEventListener("mousemove", mouseMove);
});
// Smooth animation
function easeOutQuart(t, b, c, d) {
t /= d;
t--;
return -c * (t * t * t * t - 1) + b;
}
// Handle when the user releases the mouse
document.addEventListener("mouseup", (e) => {
document.removeEventListener("mousemove", mouseMove);
// Make sure we're clicking on the canvas or the body and not the UI
if (e.target !== canvas && e.target !== canvasContainer) {
return;
}
e.preventDefault();
// Get the tile position
target = calculateTarget(e);
// Make sure we're clicking on the canvas
if (
target.x >= 0 &&
target.x < canvas.width &&
target.y >= 0 &&
target.y < canvas.height
) {
// We want to differentiate between a click and a drag
// If it is a click, we want to move the camera to the clicked tile
// We wait to see if the position changed
// If it did not, we consider it a click
if (!isDrag) {
const duration = 1000;
const startZoom = zoom;
const endZoom = Math.max(15, Math.min(40, zoom));
// Get the position of the click in relation to the center of the screen
const clickX = e.clientX - window.innerWidth / 2;
const clickY = e.clientY - window.innerHeight / 2;
const canvaswidthzoom = canvas.width * startZoom;
const canvasheightzoom = canvas.height * startZoom;
const startx = (cx + canvaswidthzoom / 2) / startZoom;
const starty = (cy + canvasheightzoom / 2) / startZoom;
const endx = startx - clickX / startZoom;
const endy = starty - clickY / startZoom;
const endCx = endx * endZoom - (canvas.width / 2) * endZoom;
const endCy = endy * endZoom - (canvas.height / 2) * endZoom;
const startCx = cx;
const startCy = cy;
const startTime = Date.now();
// If the distance is small enough, we just warp to it
if (
Math.abs(endCx - startCx) < 10 &&
Math.abs(endCy - startCy) < 10
) {
cx = endCx;
cy = endCy;
zoom = endZoom;
canvas.style.transform = `translate(${cx}px, ${cy}px) scale(${zoom})`;
} else {
clearInterval(animation);
animation = setInterval(() => {
const elapsed = Date.now() - startTime;
if (elapsed >= duration) {
clearInterval(animation);
return;
}
const t = elapsed / duration;
zoom = easeOutQuart(t, startZoom, endZoom - startZoom, 1);
cx = easeOutQuart(t, startCx, endCx - startCx, 1);
cy = easeOutQuart(t, startCy, endCy - startCy, 1);
positionUpdate();
}, 10);
}
}
// Toggle the tooltip if it is a click
toggleTooltip(!isDrag);
// Update the position of the tooltip
positionDisplay(target);
}
});
// Handle mouse move
const mouseMove = (e) => {
e.preventDefault();
toggleTooltip(false);
positionUpdate();
const dx = e.clientX - x;
const dy = e.clientY - y;
// For a big enough delta, we consider it a drag
if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
isDrag = true;
}
x = e.clientX;
y = e.clientY;
cx += dx;
cy += dy;
canvas.style.transform = `translate(${cx}px, ${cy}px) scale(${zoom})`;
};
export const displayLeaderboard = (guilds) => {
console.log(guilds);
const list = document.getElementById("leaderboard-list");
for (const { name, points } of guilds) {
console.log(`${name} has ${points}`);
const existingItem = [
...list.querySelectorAll(".LeaderboardItem"),
].find((el) => el.dataset.guild === name);
if (existingItem) {
existingItem.querySelector(".GuildPoints").textContent = points;
existingItem.dataset.points = points;
} else {
const li = document.createElement("li");
li.className = "LeaderboardItem";
li.dataset.guild = name;
li.dataset.points = points;
li.innerHTML = `
<span class="Rank"></span>
<span class="GuildName">${name}</span>
<span class="GuildPoints">${points}</span>
`;
list.appendChild(li);
}
}
const items = [...list.querySelectorAll(".LeaderboardItem")].sort(
(a, b) => b.dataset.points - a.dataset.points,
);
list.innerHTML = "";
items.slice(0, 5).forEach((item, index) => {
item.className = `LeaderboardItem rank-${index + 1}`;
item.querySelector(".Rank").textContent = index + 1;
list.appendChild(item);
});
};