summaryrefslogtreecommitdiff
path: root/rushs/eplace/src/rooms
diff options
context:
space:
mode:
authorMartial Simon <msimon_fr@hotmail.com>2025-09-15 01:08:27 +0200
committerMartial Simon <msimon_fr@hotmail.com>2025-09-15 01:08:27 +0200
commitc9b6b9a5ca082fe7c1b6f58d7713f785a9eb6a5c (patch)
tree3e4f42f93c7ae89a364e4d51fff6e5cec4e55fa9 /rushs/eplace/src/rooms
add: graphs et rushs
Diffstat (limited to 'rushs/eplace/src/rooms')
-rw-r--r--rushs/eplace/src/rooms/canvas/index.js96
-rw-r--r--rushs/eplace/src/rooms/canvas/utils.js432
-rw-r--r--rushs/eplace/src/rooms/chat/index.js4
-rw-r--r--rushs/eplace/src/rooms/chat/utils.js6
-rw-r--r--rushs/eplace/src/rooms/index.js50
-rw-r--r--rushs/eplace/src/rooms/utils.js6
6 files changed, 594 insertions, 0 deletions
diff --git a/rushs/eplace/src/rooms/canvas/index.js b/rushs/eplace/src/rooms/canvas/index.js
new file mode 100644
index 0000000..440e0d3
--- /dev/null
+++ b/rushs/eplace/src/rooms/canvas/index.js
@@ -0,0 +1,96 @@
+// FIXME: This file should handle the room canvas API
+// Link buttons to their respective functions
+// Functions may include:
+
+import { getStudent } from "../../students";
+import { authedAPIRequest } from "../../utils/auth";
+import { getPlacementData } from "./utils";
+
+// - getCanvas (get the canvas of a room and deserialize it)
+export async function getCanvas(slug) {
+ const config = {
+ method: "get",
+ };
+
+ return authedAPIRequest(`/rooms/${slug}/canvas`, config)
+ .then(async (res) => {
+ if (!res) {
+ return null;
+ }
+
+ const response = await res.json();
+
+ const pixels = response.pixels
+ .split("")
+ .map((c) => c.charCodeAt(0).toString(2).padStart(8, "0"))
+ .join("")
+ .match(/.{5}/g)
+ .map((b) => parseInt(b, 2));
+
+ return pixels;
+ })
+ .catch((error) => {
+ console.log(error);
+ return null;
+ });
+}
+// - subscribeToRoom (subscribe to the stream of a room)
+// - getPixelInfo (get the pixel info of a room)
+export async function getPixelInfo() {
+ const info = getPlacementData();
+
+ const response = await authedAPIRequest(
+ `/rooms/epi-place/canvas/pixels?posX=${info.posX}&posY=${info.posY}`,
+ { method: "get" },
+ );
+
+ if (!response) {
+ return null;
+ } else {
+ return response.json();
+ }
+}
+
+// - placePixel (place a pixel in a room)
+export async function placePixel() {
+ const info = getPlacementData();
+
+ const res = await authedAPIRequest(`/rooms/epi-place/canvas/pixels`, {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ posX: info.posX,
+ posY: info.posY,
+ color: info.color,
+ }),
+ });
+
+ if (!res) {
+ return;
+ }
+
+ const response = await res.json();
+
+ const placeDate = new Date(response.timestamp);
+ const studentInfo = await getStudent(response.placedByUid);
+
+ if (!studentInfo) {
+ return;
+ }
+
+ document.getElementById("tooltip-time").innerHTML =
+ placeDate.toLocaleTimeString();
+ document.getElementById("tooltip-date").innerHTML =
+ placeDate.toLocaleDateString();
+ document
+ .getElementById("tooltip-info-avatar")
+ .setAttribute("src", studentInfo.avatarURL ?? "/default-avatar.png");
+ document.getElementById("tooltip-info-quote").innerHTML = studentInfo.quote;
+ document.getElementById("tooltip-info-login").innerHTML = studentInfo.login;
+}
+document
+ .getElementById("color-place-button")
+ .addEventListener("click", placePixel);
diff --git a/rushs/eplace/src/rooms/canvas/utils.js b/rushs/eplace/src/rooms/canvas/utils.js
new file mode 100644
index 0000000..5737dbb
--- /dev/null
+++ b/rushs/eplace/src/rooms/canvas/utils.js
@@ -0,0 +1,432 @@
+// 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";
+import { getPixelInfo } from ".";
+import { getStudent } from "../../students";
+
+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
+ * @returns {{color: number, posX: number, posX: number}} the data
+ */
+export const getPlacementData = () => ({
+ color: selectedColorIdx,
+ posX: target.x,
+ posY: target.y,
+});
+
+/**
+ * Toggle the tooltip and display the pixel's information
+ * @param {boolean} state
+ */
+export const toggleTooltip = async (state = false) => {
+ tooltip.style.display = state ? "flex" : "none";
+
+ if (state) {
+ const pixelInfo = await getPixelInfo();
+ const uid = pixelInfo.placedByUid;
+ const placeDate = new Date(pixelInfo.timestamp);
+
+ if (!pixelInfo) {
+ throw new Error(
+ 'An error occured while hitting the "/rooms/epi-place/canvas/pixels" endpoint',
+ );
+ }
+
+ const studentInfo = await getStudent(uid);
+
+ if (!studentInfo) {
+ throw new Error(
+ 'An error occured while hitting the "/students/:id" endpoint',
+ );
+ }
+
+ document.getElementById("tooltip-time").innerHTML =
+ placeDate.toLocaleTimeString();
+ document.getElementById("tooltip-date").innerHTML =
+ placeDate.toLocaleDateString();
+ document
+ .getElementById("tooltip-info-avatar")
+ .setAttribute(
+ "src",
+ studentInfo.avatarURL ?? "/default-avatar.png",
+ );
+ document.getElementById("tooltip-info-quote").innerHTML =
+ studentInfo.quote;
+ document.getElementById("tooltip-info-login").innerHTML =
+ studentInfo.login;
+ // 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
+ * @param {*} event
+ */
+const positionUpdate = (event) => positionDisplay(calculateTarget(event));
+
+/**
+ * Update the position tooltip
+ * @param {{x: number, y: number}} target
+ */
+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
+ * @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
+ */
+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
+ * @param {*} roomConfig
+ * @param {number[]} pixels
+ */
+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);
+};
+
+/**
+ * Update the canvas
+ * @param {string} color
+ * @param {number} x
+ * @param {number} y
+ */
+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 canvas values
+ */
+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})`;
+};
diff --git a/rushs/eplace/src/rooms/chat/index.js b/rushs/eplace/src/rooms/chat/index.js
new file mode 100644
index 0000000..493f142
--- /dev/null
+++ b/rushs/eplace/src/rooms/chat/index.js
@@ -0,0 +1,4 @@
+// FIXME: This file should handle the room's chat subscription
+// Functions may include:
+// - subscribeToRoomChat (subscribe to the chat of a room)
+// - sendChatMessage (send a chat message)
diff --git a/rushs/eplace/src/rooms/chat/utils.js b/rushs/eplace/src/rooms/chat/utils.js
new file mode 100644
index 0000000..2d21ab2
--- /dev/null
+++ b/rushs/eplace/src/rooms/chat/utils.js
@@ -0,0 +1,6 @@
+// FIXME: This file should handle the room's chat DOM manipulation
+// Link buttons to their respective functions
+// Handle the chat input form and its submission
+// Functions may include:
+// - displayChatMessage (display a chat message in the DOM)
+// - displayUserEvents (display a user event in the DOM)
diff --git a/rushs/eplace/src/rooms/index.js b/rushs/eplace/src/rooms/index.js
new file mode 100644
index 0000000..dde88e6
--- /dev/null
+++ b/rushs/eplace/src/rooms/index.js
@@ -0,0 +1,50 @@
+// FIXME: This file should handle the rooms API
+// Functions may include:
+
+import { subscribe } from "../utils/streams";
+import { authedAPIRequest } from "../utils/auth";
+
+// - fetchRoomConfig (get the configuration of a room)
+export async function fetchRoomConfig(slug) {
+ const config = {
+ method: "get",
+ };
+
+ return authedAPIRequest(`/rooms/${slug}/config`, config)
+ .then(async (res) => {
+ if (!res) {
+ return null;
+ }
+
+ const response = await res.json();
+
+ document.getElementById("room-name").innerHTML =
+ response.metadata.name;
+ const description = document.getElementById("room-description");
+
+ if (response.metadata.description) {
+ description.innerHTML = response.metadata.description;
+ description.style.display = "inherit";
+ } else {
+ description.style.display = "none";
+ }
+
+ return response;
+ })
+ .catch((error) => {
+ console.log(error);
+ return null;
+ });
+}
+// - joinRoom (join a room by its slug)
+export function joinRoom(slug) {
+ if (!slug) {
+ slug = "epi-place";
+ }
+
+ subscribe(slug);
+}
+// - listRooms (list all the rooms available)
+// - createRoom (create a room)
+// - updateRoom (update a room's configuration)
+// - deleteRoom (delete a room)
diff --git a/rushs/eplace/src/rooms/utils.js b/rushs/eplace/src/rooms/utils.js
new file mode 100644
index 0000000..5e94739
--- /dev/null
+++ b/rushs/eplace/src/rooms/utils.js
@@ -0,0 +1,6 @@
+// FIXME: This file should handle the rooms DOM manipulation
+// Link buttons to their respective functions
+// Functions may include:
+// - showModal (add a form modal to the DOM)
+// - createRoomObject (create a room in the DOM)
+// - displayRoomsList (display the rooms list in the DOM)